歡迎來到 open-notebook
教學系列的第二章!在上一章 物件模型 (ObjectModel) 中,我們學習了如何為應用程式中的核心資料(例如筆記本、筆記)建立一個統一的藍圖。這讓我們能夠以標準化的方式儲存、讀取和管理這些資料物件。
但是,open-notebook
不僅僅是儲存筆記。它的核心功能依賴於各種人工智慧 (AI) 模型來處理和增強您的內容。例如,您可能想:
這些不同的功能需要不同的 AI 模型。我們如何有效地管理這些模型,並在需要時輕鬆取用它們呢?這就是「模型管理器 (ModelManager)」登場的時候了!
想像一下,您正在準備一個大型專案,需要用到各種不同的工具:鐵鎚、扳手、螺絲起子、電鑽等等。如果這些工具散落在各處,每次需要時都要花時間尋找,那將會非常沒有效率。更糟的是,您可能不知道哪把扳手是適合特定螺帽的,或者電鑽是否已經裝好鑽頭並充好電。
在 open-notebook
中,不同的 AI 模型就像是這些工具。我們有:
如果沒有一個統一的管理機制,我們就需要在程式碼的各個地方手動設定和初始化這些模型。這樣做會導致:
「模型管理器 (ModelManager)」就是為了解決這些問題而設計的。
模型管理器 (ModelManager) 是 open-notebook
中負責管理和提供應用程式所需各種 AI 模型的中央元件。您可以把它想像成一個萬能工具箱的管理員:
name
)、供應商 (provider
,如 OpenAI, Google, Ollama)和類型 (type
,如 language, embedding)。這些資訊通常儲存在資料庫中(透過 Model
這個物件,它本身也繼承自 物件模型 (ObjectModel))。透過 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("模型管理器已準備就緒!")
程式碼解釋:
open_notebook.domain.models
匯入已經建立好的 model_manager
實例。不需要自己去建立它。現在,讓我們看看如何使用 model_manager
來獲取不同類型的模型:
假設我們想進行聊天,需要一個語言模型。我們可以向 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("錯誤:找不到預設的聊天模型。請檢查設定。")
程式碼解釋:
model_manager.get_default_model()
並傳入字串 "chat"
,表示我們想要預設的聊天(語言)模型。ModelManager
會查詢它的設定(DefaultModels
物件),找到 default_chat_model
對應的模型 ID。get_model(model_id)
方法來獲取並初始化這個模型。OpenAILanguageModel
),並建立該類別的實例。OpenAILanguageModel
的實例)。這個物件可以直接用來執行 AI 任務。如果沒有設定預設模型,則返回 None
。同樣地,如果我們需要為文本產生嵌入向量,可以獲取預設的嵌入模型:
# 獲取預設的嵌入模型
# 這裡我們指定類型為 '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("錯誤:找不到預設的嵌入模型。請檢查設定。")
程式碼解釋:
"embedding"
。ModelManager
會查找 default_embedding_model
的設定,並返回一個初始化好的嵌入模型物件(例如 OllamaEmbeddingModel
的實例)。有時候,我們可能不想使用預設模型,而是想明確指定使用某個特定的模型(假設我們知道它的 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}")
程式碼解釋:
model_manager.get_model()
並傳入我們想要載入的模型的 ID 字串。Model.get(known_model_id)
從資料庫讀取 Model
物件的資訊 (這利用了我們在 物件模型 (ObjectModel) 中學到的 get
方法)。type
和 provider
,從 MODEL_CLASS_MAP
找到對應的 Python 類別。name
初始化該類別。ValueError
。現在我們了解如何使用 ModelManager
,讓我們稍微窺探一下它的內部運作方式。
get_model()
方法的執行流程(概覽)當您呼叫 model_manager.get_model("some_model_id")
時,大致會發生以下情況:
ModelManager
首先檢查內部快取 (_model_cache
) 中是否已經有這個 model_id
對應的模型實例。如果有,直接返回快取的實例。ModelManager
會使用 Model.get("some_model_id")
從資料庫讀取 Model
物件。這個 Model
物件包含了模型的 name
, provider
, 和 type
等資訊。ModelManager
使用 model.type
(例如 "language"
)和 model.provider
(例如 "openai"
)作為索引,在一個稱為 MODEL_CLASS_MAP
的字典中查找對應的 Python 類別(例如 OpenAILanguageModel
)。ModelManager
會使用從資料庫讀取的 model.name
(例如 "gpt-4o"
) 和任何額外傳入的參數 (**kwargs
) 來建立這個類別的實例。例如:OpenAILanguageModel(model_name="gpt-4o", **kwargs)
。_model_cache
中,以 model_id
(或包含 kwargs
的組合鍵)作為鍵。以下是一個簡化的序列圖,展示了 get_model()
的過程:
ModelManager
的核心程式碼片段讓我們看看 open_notebook/domain/models.py
中 ModelManager
類別的一些關鍵部分(已簡化):
# 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
類別: 這是一個繼承自 ObjectModel 的類別,用於在資料庫 (model
表) 中儲存每個可用 AI 模型的元數據(名稱、提供者、類型)。ModelManager
使用 Model.get(id)
來讀取這些資訊。DefaultModels
類別: 這也是一個特殊的 ObjectModel
(繼承自 RecordModel
),它在資料庫中有一個固定的 ID (open_notebook:default_models
)。它儲存了各種預設模型的 ID(例如 default_chat_model
欄位儲存的是預設聊天模型的 Model
物件的 ID)。ModelManager
類別:__new__
和 _instance
實現了單例模式,確保全域只有一個管理器。_model_cache
是一個字典,用於快取已經載入和初始化的模型實例,避免重複建立。_default_models
儲存從資料庫載入的 DefaultModels
物件的實例。refresh_defaults()
方法用於從資料庫讀取或更新 DefaultModels
設定。get_model(model_id, **kwargs)
是核心方法,執行我們之前描述的查找、實例化和快取邏輯。它依賴 Model.get()
來獲取模型資訊,並使用 MODEL_CLASS_MAP
來找到正確的 Python 類別。get_default_model(model_type, **kwargs)
是一個方便的方法,它先從 defaults
屬性(即 DefaultModels
物件)中找到對應類型的預設模型 ID,然後呼叫 get_model()
來獲取該模型。@property
方法(如 embedding_model
)提供了更簡潔的方式來直接獲取預設模型,例如 model_manager.embedding_model
就相當於 model_manager.get_default_model("embedding")
。model_manager = ModelManager()
: 在模組的末尾,直接建立 ModelManager
的單例實例,以便其他模組可以直接 import model_manager
來使用。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,
},
# ... 其他模型類型 ...
}
程式碼解釋:
MODEL_CLASS_MAP
是一個嵌套字典。"language"
, "embedding"
等)。"openai"
, "ollama"
等)。OpenAILanguageModel
)。ModelManager.get_model()
就是利用這個映射表,根據從資料庫讀取的 type
和 provider
,動態地找到並實例化正確的模型類別。open-notebook
為不同類型的 AI 模型定義了抽象基礎類別(Abstract Base Classes, ABCs),例如 LanguageModel
, EmbeddingModel
, SpeechToTextModel
等,它們位於 open_notebook/models/
目錄下的不同檔案中(如 llms.py
, embedding_models.py
)。
這些基礎類別定義了該類型模型應該具有的共同方法。例如:
EmbeddingModel
(在 embedding_models.py
) 要求所有繼承它的具體嵌入模型(如 OpenAIEmbeddingModel
)都必須實現一個 embed(self, text: str) -> List[float]
方法,用於將文本轉換為嵌入向量。LanguageModel
(在 llms.py
) 要求所有繼承它的具體語言模型(如 OpenAILanguageModel
)都必須實現一個 to_langchain(self) -> BaseChatModel
方法,用於將其轉換為 LangChain 函式庫可以使用的格式。這確保了無論 ModelManager
返回哪個具體的模型實例,只要它們屬於同一類型,應用程式的其他部分就可以用相同的方式與它們互動。
在本章中,我們學習了 open-notebook
如何使用「模型管理器 (ModelManager)」來應對管理多種 AI 模型的挑戰。
ModelManager
就像一個 AI 工具箱的管理員,負責集中管理、設定和提供應用程式所需的各種模型(語言、嵌入、語音等)。Model
物件來了解有哪些可用的模型,並透過 DefaultModels
物件來知道各種任務的預設模型。model_manager.get_default_model()
來獲取預設模型,以及如何使用 model_manager.get_model()
來根據 ID 獲取特定模型。MODEL_CLASS_MAP
查找對應的 Python 類別,以及如何實例化模型。ModelManager
的存在使得應用程式的其他部分可以輕鬆地獲取所需的 AI 功能,而無需關心底層模型的具體實現和初始化細節。有了管理資料的 物件模型 (ObjectModel) 和管理 AI 工具的 ModelManager
,我們已經準備好開始建構應用程式的「門面」了。使用者如何與 open-notebook
互動?他們如何觸發這些 AI 功能?
在下一章,我們將探索 使用者介面 (Streamlit UI),看看如何使用 Streamlit 這個 Python 函式庫來建立一個互動式網頁介面,讓使用者可以輕鬆地使用 open-notebook
的各種功能,並看看 UI 如何與我們今天學習的 ModelManager
進行互動。