Chapter 2: 模型管理器 (ModelManager)

歡迎來到 open-notebook 教學系列的第二章!在上一章 物件模型 (ObjectModel) 中,我們學習了如何為應用程式中的核心資料(例如筆記本、筆記)建立一個統一的藍圖。這讓我們能夠以標準化的方式儲存、讀取和管理這些資料物件。

但是,open-notebook 不僅僅是儲存筆記。它的核心功能依賴於各種人工智慧 (AI) 模型來處理和增強您的內容。例如,您可能想:

這些不同的功能需要不同的 AI 模型。我們如何有效地管理這些模型,並在需要時輕鬆取用它們呢?這就是「模型管理器 (ModelManager)」登場的時候了!

為什麼需要模型管理器?

想像一下,您正在準備一個大型專案,需要用到各種不同的工具:鐵鎚、扳手、螺絲起子、電鑽等等。如果這些工具散落在各處,每次需要時都要花時間尋找,那將會非常沒有效率。更糟的是,您可能不知道哪把扳手是適合特定螺帽的,或者電鑽是否已經裝好鑽頭並充好電。

open-notebook 中,不同的 AI 模型就像是這些工具。我們有:

如果沒有一個統一的管理機制,我們就需要在程式碼的各個地方手動設定和初始化這些模型。這樣做會導致:

「模型管理器 (ModelManager)」就是為了解決這些問題而設計的。

什麼是模型管理器 (ModelManager)?

模型管理器 (ModelManager) 是 open-notebook 中負責管理和提供應用程式所需各種 AI 模型的中央元件。您可以把它想像成一個萬能工具箱的管理員

透過 ModelManager,應用程式的其他部分(例如 使用者介面內容處理流程)只需要向 ModelManager 索取所需類型的模型,而不需要關心模型的具體來源、如何初始化等細節。

ModelManager 的主要程式碼位於 open_notebook/domain/models.py 檔案中。

如何使用模型管理器

使用 ModelManager 非常簡單。因為它被設計成一個「單例 (Singleton)」(表示整個應用程式中只有一個 ModelManager 實例),我們首先需要取得這個唯一的實例。

# 從 open_notebook.domain.models 模組匯入 model_manager 實例
from open_notebook.domain.models import model_manager

# 現在我們可以使用 model_manager 來取得模型了
print("模型管理器已準備就緒!")

程式碼解釋:

現在,讓我們看看如何使用 model_manager 來獲取不同類型的模型:

1. 獲取預設聊天模型

假設我們想進行聊天,需要一個語言模型。我們可以向 model_manager 索取「預設」的聊天模型。預設模型是在應用程式的設定中指定的。

# 獲取預設的聊天模型
# 這裡我們指定類型為 'chat'
default_chat_model = model_manager.get_default_model("chat")

if default_chat_model:
    print(f"成功獲取預設聊天模型:")
    print(f" - 模型名稱: {default_chat_model.model_name}")
    # 這個 default_chat_model 物件現在可以用來進行聊天互動
else:
    print("錯誤:找不到預設的聊天模型。請檢查設定。")

程式碼解釋:

2. 獲取預設嵌入模型

同樣地,如果我們需要為文本產生嵌入向量,可以獲取預設的嵌入模型:

# 獲取預設的嵌入模型
# 這裡我們指定類型為 'embedding'
default_embedding_model = model_manager.get_default_model("embedding")

if default_embedding_model:
    print(f"成功獲取預設嵌入模型:")
    print(f" - 模型名稱: {default_embedding_model.model_name}")
    # 這個 default_embedding_model 物件可以用來產生嵌入向量
    # 例如:embedding = default_embedding_model.embed("一些文字")
else:
    print("錯誤:找不到預設的嵌入模型。請檢查設定。")

程式碼解釋:

3. 根據 ID 獲取特定模型

有時候,我們可能不想使用預設模型,而是想明確指定使用某個特定的模型(假設我們知道它的 ID)。例如,在設定頁面(pages/7_🤖_Models.py)中,我們可能會列出所有已設定的模型,讓使用者選擇。

模型的 ID 通常是由資料庫(例如 SurrealDB)產生的,格式可能是 table_name:unique_part,例如 model:openai_gpt_4o

# 假設我們知道一個模型的 ID
# 這個 ID 是在設定模型時儲存到資料庫中的 Model 物件的 ID
known_model_id = "model:openai_gpt_4o" # 範例 ID

try:
    # 使用 get_model 方法並傳入 ID 來獲取特定模型
    specific_model = model_manager.get_model(known_model_id)
    
    print(f"成功根據 ID [{known_model_id}] 獲取模型:")
    print(f" - 模型名稱: {specific_model.model_name}")
    print(f" - 模型類型: {type(specific_model)}") # 顯示實際的類別
    
except ValueError as e:
    print(f"獲取模型失敗:{e}")
except Exception as e:
    print(f"發生預期外的錯誤:{e}")

程式碼解釋:

深入探索:ModelManager 的內部機制

現在我們了解如何使用 ModelManager,讓我們稍微窺探一下它的內部運作方式。

get_model() 方法的執行流程(概覽)

當您呼叫 model_manager.get_model("some_model_id") 時,大致會發生以下情況:

  1. 檢查快取: ModelManager 首先檢查內部快取 (_model_cache) 中是否已經有這個 model_id 對應的模型實例。如果有,直接返回快取的實例。
  2. 讀取模型資訊: 如果快取中沒有,ModelManager 會使用 Model.get("some_model_id") 從資料庫讀取 Model 物件。這個 Model 物件包含了模型的 name, provider, 和 type 等資訊。
  3. 查找模型類別: ModelManager 使用 model.type(例如 "language")和 model.provider(例如 "openai")作為索引,在一個稱為 MODEL_CLASS_MAP 的字典中查找對應的 Python 類別(例如 OpenAILanguageModel)。
  4. 實例化模型: 找到正確的類別後,ModelManager 會使用從資料庫讀取的 model.name(例如 "gpt-4o") 和任何額外傳入的參數 (**kwargs) 來建立這個類別的實例。例如:OpenAILanguageModel(model_name="gpt-4o", **kwargs)
  5. 存入快取: 將新建立的模型實例存儲到 _model_cache 中,以 model_id(或包含 kwargs 的組合鍵)作為鍵。
  6. 返回實例: 返回新建立的模型實例。

以下是一個簡化的序列圖,展示了 get_model() 的過程:

sequenceDiagram participant UserCode as 使用者程式碼 participant ModelManager as 模型管理器 participant ModelCache as 模型快取 participant Database as 資料庫 (Model 物件) participant ModelClassMap as 模型類別對應表 participant SpecificModel as 具體模型類別 (例如 OpenAILanguageModel) UserCode->>ModelManager: get_model(model_id, **kwargs) ModelManager->>ModelCache: 檢查快取(model_id + kwargs) alt 快取命中 ModelCache-->>ModelManager: 返回快取的模型實例 ModelManager-->>UserCode: 返回模型實例 else 快取未命中 ModelManager->>Database: Model.get(model_id) Database-->>ModelManager: 返回模型資訊 (name, provider, type) ModelManager->>ModelClassMap: 查找類別(type, provider) ModelClassMap-->>ModelManager: 返回對應的 Python 類別 ModelManager->>SpecificModel: 建立實例(model_name=name, **kwargs) SpecificModel-->>ModelManager: 返回新模型實例 ModelManager->>ModelCache: 儲存新模型實例 ModelManager-->>UserCode: 返回新模型實例 end

ModelManager 的核心程式碼片段

讓我們看看 open_notebook/domain/models.pyModelManager 類別的一些關鍵部分(已簡化):

# open_notebook/domain/models.py
from typing import ClassVar, Dict, Optional
from open_notebook.domain.base import ObjectModel, RecordModel
# 匯入模型類型定義和 MODEL_CLASS_MAP
from open_notebook.models import (
    MODEL_CLASS_MAP, 
    ModelType, 
    LanguageModel, 
    EmbeddingModel,
    # ... 其他模型類型 ...
)

# --- Model 物件定義 ---
class Model(ObjectModel): # Model 繼承自 ObjectModel
    table_name: ClassVar[str] = "model" # 對應資料庫中的 'model' 表格
    name: str       # 模型名稱 (e.g., "gpt-4o", "text-embedding-3-small")
    provider: str   # 供應商 (e.g., "openai", "ollama", "gemini")
    type: str       # 類型 (e.g., "language", "embedding")
    # ... 其他可能的方法 ...

# --- 預設模型設定定義 ---
class DefaultModels(RecordModel): # RecordModel 也是 ObjectModel 的變體
    record_id: ClassVar[str] = "open_notebook:default_models" # 固定的記錄 ID
    default_chat_model: Optional[str] = None # 預設聊天模型的 ID
    default_embedding_model: Optional[str] = None # 預設嵌入模型的 ID
    # ... 其他預設模型 ID ...

# --- ModelManager 類別 ---
class ModelManager:
    _instance = None # 用於實現單例模式

    def __new__(cls): # 確保只有一個實例
        if cls._instance is None:
            cls._instance = super(ModelManager, cls).__new__(cls)
        return cls._instance

    def __init__(self):
        if not hasattr(self, "_initialized"): # 防止重複初始化
            self._initialized = True
            # 模型快取:儲存已載入的模型實例
            self._model_cache: Dict[str, ModelType] = {} 
            self._default_models = None # 儲存 DefaultModels 的實例
            self.refresh_defaults() # 初始化時載入預設模型設定

    def get_model(self, model_id: str, **kwargs) -> Optional[ModelType]:
        if not model_id: return None

        # 建立快取鍵 (考慮 kwargs 以區分不同設定的同 ID 模型)
        cache_key = f"{model_id}:{str(kwargs)}" 
        
        # 1. 檢查快取
        if cache_key in self._model_cache:
            return self._model_cache[cache_key]

        # 2. 從資料庫讀取模型資訊 (使用 Model.get)
        model: Model = Model.get(model_id) 
        if not model:
            raise ValueError(f"找不到 ID 為 {model_id} 的模型")

        # 檢查模型類型和提供者是否有效
        if not model.type or model.type not in MODEL_CLASS_MAP:
            raise ValueError(f"無效的模型類型:{model.type}")
        
        # 3. 查找模型類別
        provider_map = MODEL_CLASS_MAP[model.type]
        if model.provider not in provider_map:
            raise ValueError(f"{model.provider} 不支援 {model.type} 模型")
        model_class = provider_map[model.provider] # 獲取 Python 類別

        # 4. 實例化模型
        model_instance = model_class(model_name=model.name, **kwargs)

        # 5. 存入快取
        self._model_cache[cache_key] = model_instance
        # 6. 返回實例
        return model_instance

    def refresh_defaults(self):
        """從資料庫重新載入預設模型設定"""
        self._default_models = DefaultModels() # 載入 DefaultModels 記錄

    @property
    def defaults(self) -> DefaultModels:
        """獲取預設模型設定物件"""
        if not self._default_models: self.refresh_defaults()
        # ... 錯誤處理 ...
        return self._default_models

    def get_default_model(self, model_type: str, **kwargs) -> Optional[ModelType]:
        """根據類型獲取預設模型"""
        model_id = None
        defaults = self.defaults # 獲取 DefaultModels 物件
        
        # 根據 model_type 查找對應的預設模型 ID
        if model_type == "chat": model_id = defaults.default_chat_model
        elif model_type == "embedding": model_id = defaults.default_embedding_model
        # ... 其他類型的判斷 ...

        if not model_id: return None # 如果沒設定預設 ID,返回 None

        # 使用 get_model 獲取實際的模型實例
        return self.get_model(model_id, **kwargs)

    # ... 提供方便屬性來直接獲取預設模型 (例如 model_manager.embedding_model) ...
    @property
    def embedding_model(self, **kwargs) -> Optional[EmbeddingModel]:
        return self.get_default_model("embedding", **kwargs)

    # ... 其他屬性如 speech_to_text, text_to_speech ...

# --- 建立單例實例 ---
model_manager = ModelManager() 

程式碼解釋:

MODEL_CLASS_MAP

這個重要的字典定義在 open_notebook/models/__init__.py 中,它建立了模型類型和提供者與實際 Python 類別之間的映射關係。

# open_notebook/models/__init__.py (簡化)
from typing import Dict, Type
# 匯入各種模型的基礎類別和具體實現類別
from .llms import LanguageModel, OpenAILanguageModel, OllamaLanguageModel # ...
from .embedding_models import EmbeddingModel, OpenAIEmbeddingModel # ...
from .speech_to_text_models import SpeechToTextModel, OpenAISpeechToTextModel # ...
# ... 其他模型 ...

# 定義模型類型的聯合類型
ModelType = Union[LanguageModel, EmbeddingModel, SpeechToTextModel, ...] 
# 定義提供者到類別的映射字典類型
ProviderMap = Dict[str, Type[ModelType]]

# 主要的映射表
MODEL_CLASS_MAP: Dict[str, ProviderMap] = {
    "language": { # 語言模型
        "openai": OpenAILanguageModel,
        "ollama": OllamaLanguageModel,
        "gemini": GeminiLanguageModel,
        # ... 其他語言模型提供者 ...
    },
    "embedding": { # 嵌入模型
        "openai": OpenAIEmbeddingModel,
        "ollama": OllamaEmbeddingModel,
        # ... 其他嵌入模型提供者 ...
    },
    "speech_to_text": { # 語音轉文字
        "openai": OpenAISpeechToTextModel,
        "groq": GroqSpeechToTextModel,
    },
    # ... 其他模型類型 ...
}

程式碼解釋:

模型介面

open-notebook 為不同類型的 AI 模型定義了抽象基礎類別(Abstract Base Classes, ABCs),例如 LanguageModel, EmbeddingModel, SpeechToTextModel 等,它們位於 open_notebook/models/ 目錄下的不同檔案中(如 llms.py, embedding_models.py)。

這些基礎類別定義了該類型模型應該具有的共同方法。例如:

這確保了無論 ModelManager 返回哪個具體的模型實例,只要它們屬於同一類型,應用程式的其他部分就可以用相同的方式與它們互動。

總結

在本章中,我們學習了 open-notebook 如何使用「模型管理器 (ModelManager)」來應對管理多種 AI 模型的挑戰。

有了管理資料的 物件模型 (ObjectModel) 和管理 AI 工具的 ModelManager,我們已經準備好開始建構應用程式的「門面」了。使用者如何與 open-notebook 互動?他們如何觸發這些 AI 功能?

在下一章,我們將探索 使用者介面 (Streamlit UI),看看如何使用 Streamlit 這個 Python 函式庫來建立一個互動式網頁介面,讓使用者可以輕鬆地使用 open-notebook 的各種功能,並看看 UI 如何與我們今天學習的 ModelManager 進行互動。