0. 前言#
項目已在 Github 上開源,這裡是一些基礎的技術講解。
Github Repo not found
The embedded github repo could not be found…
1. 結構設計:插件化工程#
在項目中,採用插件化架構進行系統構建,利用 Ncatbot 的插件模式,可以很好地將主程序與功能隔離開。
插件架構的優勢在於模塊邊界清晰、依賴隔離度高、系統可拓展性強。標準的插件化極大程度上將代碼之間解耦,無論是開發還是部署都是十分友好的。
項目系統目錄結構如下:
ModuleChat/
├── main.py # 插件主入口,負責指令註冊與調度邏輯
├── chat.py # 模型適配層,封裝本地和雲端模型的調用
├── config.yml # 配置文件,集中控制模型參數與啟用選項
├── requirements.txt # 依賴庫
└── cache/
└── history.json # 聊天歷史記憶文件
chat.py 是系統的核心模塊,主程序 main.py 負責接收、解析指令,並且路由消息到模型模塊處理。在開發中這是十分友好的一種結構,使得專注於單個功能模塊的開發與調試,極大降低了維護複雜度。
而配置項集中於 config.yml,進一步提升了靈活性與環境適配能力。
採用臨時 json 文件的方式,記錄指令的調用與回覆,並且傳入 API 接口,可以使得大模型可以獲得一定程度上的短期記憶功能,但一定程度上來說,這對系統並不友好,我認為更優解是採用數據庫的形式,但使用數據庫很大程度上增大了系統的複雜性,所以使用 json 文件是一個很好的代替選擇。
2. 插件主程序:指令解耦與路由中樞#
main.py 是插件的主入口,通過 register_user_func 方法註冊兩個指令:/chat 和 /clear chat_history,分別對應聊天功能和歷史記錄清除。
此外,主程序支持對圖像消息的自動識別,提取其中的圖像 URL 並傳遞給 chat_model_instance.recognize_image 方法,自動獲取視覺描述。
if image_url and self.chat_model.get('enable_vision', True) and not self.chat_model.get('use_local_model'):
# 使用圖像識別功能
image_description = await chat_model_instance.recognize_image(image_url)
user_input = f"用戶發送了一張圖片,圖片描述是:{image_description}。用戶說:{user_input}"
elif image_url and not self.chat_model.get('enable_vision', True):
# 圖像識別功能未開啟,但檢測是否是本地模型
if self.chat_model.get('use_local_model'):
user_input = f"用戶發送了一張圖片,但用戶使用的是本地模型,無法進行圖像識別。用戶說:{user_input}"
else:
user_input = f"用戶發送了一張圖片,但圖像識別功能未開啟。用戶說:{user_input}"
獲取到視覺描述後,再轉到語言大模型來進行輸出,其實這是一種很好的解決方法,在當前使用場景下,更多的是需要對圖片進行識別後對內容進行分析處理,而不是處理圖像本身,這可以很大程度上減少 API 調用,提高緩存命中率,減少 TOKEN 使用,從而降低 API 調用成本,並且可以使用雲端大模型來進行識別後,再交給本地大模型回答,進一步壓縮成本。
錯誤處理方面,try...except 包裹了整個聊天邏輯,避免了圖像解碼失敗或 API 異常造成主流程崩潰,保持插件健壯性。
整合來看,main.py 是典型的 “輕控制器” 模式,僅協調各組件而不承擔業務邏輯細節,使整個插件具備良好的工程可讀性。
3. 模型適配模塊:多模型封裝與語義一致性#
chat.py 是插件的核心邏輯。它負責處理模型調用、聊天歷史記憶、圖像識別等任務。為了兼容多種模型接口(如 OpenAI API 與 Ollama 本地服務),採用了統一封裝接口的策略,使外部調用者無需關注模型細節,只需通過 useCloudModel() 或 useLocalModel() 兩個方法即可完成對話。
async def useLocalModel(self, msg: BaseMessage, user_input: str):
"""使用本地模型處理消息"""
try:
# 構建消息列表,包含歷史記錄
messages = self._build_messages(user_input, msg.user_id if hasattr(msg, 'user_id') else None)
response: ChatResponse = chat(
model=self.config['model'],
messages=messages
)
reply = response.message.content.strip()
# 保存當前對話到歷史記錄
if hasattr(msg, 'user_id'):
self._update_user_history(msg.user_id, {"role": "user", "content": user_input})
self._update_user_history(msg.user_id, {"role": "assistant", "content": reply})
except Exception as e:
reply = f"請求出錯了:{str(e)}"
return reply
值得注意的是,由於 OpenAI 接口調用和 Ollama 調用存在些許不同,並且某些模型的參數並不完全,所以相比起來,使用 OpenAI 接口,調用雲端模型其實可以獲得更好的體驗,例如我們可以控制模型的 temperature 使它更具想像力亦或者是更注重實時,減少幻覺。
所有用戶歷史被存儲在 cache/history.json 文件中,這是一種持久化保存的方案,並且可以具備一定程度的可追溯性,歷史記錄通過 _update_user_history 方法動態更新,控制在配置文件設定的最大輪數之內。這種方式防止了上下文過大帶來的性能問題,同時確保模型能理解連續上下文,提升回答質量,使得即使對接接口也可以擁有近似記憶的能力。
類中還集成了 OpenAI 的圖像識別模型,通過 _build_vision_messages 構建多模態消息結構。並且設計上我將圖像處理、消息構造、異常處理、調用模型等函數分離,使得開發中可以更快定位問題所在,開源後也更利於其他開發者閱讀。
4. 雲端模型對接(OpenAI):標準化封裝#
雲端模型的調用主要通過 openai 官方庫封裝,利用 chat.completions.create 方法完成上下文構建與回覆生成。在每一次調用中都通過 _build_messages() 方法構造完整的對話上下文,加入系統提示詞和使用 cache/history.json 保存的歷史記錄,實現多輪記憶式對話。
def _build_messages(self, user_input: str, user_id: str = None):
"""構建消息列表"""
messages = []
# 添加系統提示詞
system_prompt = self.config.get('system_prompt', "你是一名聊天陪伴機器人")
messages.append({"role": "system", "content": system_prompt})
if user_id:
history = self._get_user_history(user_id)
messages.extend(history)
# 添加當前用戶輸入
messages.append({"role": "user", "content": user_input})
return messages
調用邏輯中封裝了 temperature 參數,支持通過配置文件靈活控制模型輸出的隨機性。
當遇到 Bug report 時,統一使用 return 將報錯反饋到用戶面,這樣可以減少大量的報錯開發處理,並且將常見的由於配置失誤導致的問題,更清晰的反饋出來,即統一模型 + 規則處理的兩種方法來反饋運行時遇到的問題。
if "401" in str(fallback_error) or "Unauthorized" in str(fallback_error):
raise Exception("模型API認證失敗,請檢查配置文件")
raise Exception(f"圖像識別出錯: {str(e)}, 備用方法也失敗: {str(fallback_error)}")
返回結果後,會將本輪問答同步到用戶歷史緩存中,並保存到本地文件中,確保下一輪能正常取回上下文。這樣可以降低內存依賴、增強緩存命中率,同時為後續調試與行為復現提供依據。
5. 本地模型調用(Ollama):輕量化推理與接口統一#
本地模型的調用通過 ollama.chat() 完成,複用了 _build_messages() 的上下文構建邏輯,以確保調用邏輯與雲端一致,保持接口一致性。
這種本地推理機制的優點非常明顯:在無網絡或私有部署環境下依舊能使用智能對話功能,極大增強了插件的部署靈活性和安全性。即便是在隱私敏感場景中,也可以做到本地化部署與運行。
設計上保持本地與雲端調用接口一致(都封裝為 use*Model()),外部調用方無需判斷模型來源,從而降低複雜度。除此之外,同樣實現了歷史記錄更新、異常捕獲機制,使本地模型具備與雲端一致的功能完整度與穩定性。
6. 圖像識別處理邏輯:多模態輸入的語義增強策略#
圖像識別功能是本插件的一大亮點。插件支持識別圖像消息並通過 OpenAI 視覺模型進行處理。整個流程如下:
-
- 從圖像消息中提取 URL;
for segment in msg.message:
if isinstance(segment, dict) and segment.get("type") == "image":
image_url = segment.get("data", {}).get("url")
break
-
- 通過 HTTP 請求獲取圖像內容並進行 Base64 編碼;
-
- 構造視覺輸入格式(包括
image_url和text prompt);
- 構造視覺輸入格式(包括
response = requests.get(image_url)
response.raise_for_status()
return base64.b64encode(response.content).decode('utf-8')
-
- 調用視覺模型完成圖像描述;
# 獲取並編碼圖片
image_data = self._encode_image_from_url(image_url)
# 構建消息
messages = self._build_vision_messages(image_data, prompt)
# 調用視覺模型
response = self.vision_client.chat.completions.create(
model=self.config.get('vision_model'),
messages=messages,
temperature=self.config.get('model_temperature', 0.6),
stream=False,
max_tokens=2048
)
-
- 將圖像描述拼接到用戶輸入中,提升上下文語義完整性。
這一機制有效解決了圖文混合輸入場景中的信息不對稱問題,同時通過分級調節調用,可以僅利用雲端高算力處理複雜問題,再交給本地處理簡化後的問題,極大程度上減少了 TOKEN 使用率。
異常處理方面,我們設計了兩級降級策略:如主調用失敗則嘗試純文本 fallback prompt;若仍失敗,則提示用戶檢查 API 密鑰或模型狀態。這種容錯設計使插件能在部分失敗時依舊保持服務不中斷。
7. 聊天歷史系統:記憶窗口控制#
聊天歷史存儲在 cache/history.json 文件中,按用戶維度管理。該設計使系統可同時服務多用戶,並為每個用戶維護獨立上下文。通過 _get_user_history 和 _update_user_history 方法,插件能自動在每輪對話中注入歷史信息,實現類 “記憶式” 問答體驗。
我們對歷史記錄長度做了窗口限制(默認 10 輪),以控制上下文規模並避免模型處理壓力過大、過度消耗 TOKEN。緩存更新為同步寫入的操作,確保在系統崩潰、斷電等異常情況下不會造成信息丟失。
async def clear_user_history(self, user_id: str):
"""清除指定用戶的歷史記錄"""
user_id = str(user_id)
if user_id in self.history:
del self.history[user_id]
self._save_history()
reply = "已清空聊天記錄"
else:
reply = "沒有找到用戶的聊天記錄"
return reply
此外,還支持通過指令 /clear chat_history 主動清除用戶歷史,為隱私或重新對話提供了便利。這種機制讓插件既具備持久性,又保留用戶主動控制空間。
8. DEBUG & LOG#
在調試處打斷點、print 標誌 這都是一個很好的測試習慣,我還從微信開發學到了一招 —— print ("FUCK"),在長期運行時偶爾會出現崩潰的情況,這時候事實上可以在 log 中輸出指定的字符,當查看日誌定位問題的時候,可以直接搜索字符串來迅速定位,FUCK 無疑是一種有趣的方式。
此文由 Mix Space 同步更新至 xLog 原始鏈接為 https://fmcf.cc/posts/technology/ncatbot-multimodal-plugin