Skip to content

外掛系統

源码版本v2026.7.20

職責

外掛 (plugin) 是 Hermes 的擴展骨架:provider profile、平台轉接器 (adapter)、context engine、cron provider、記憶後端、瀏覽器、圖像/影片生成……都透過外掛機制接入。plugins/ 提供 PluginContext(註冊入口)+ 工具函式,各外掛子目錄按領域自包含。它和 ToolRegistry/PlatformRegistry 協同:外掛只負責「宣告並註冊」,註冊表 (registry) 負責「查詢與調度」。

設計動機

為什麼是「目錄約定 + 模組匯入時自註冊」,而不是顯式 plugins.yaml?核心是降低新增能力的摩擦:把子目錄扔進 $HERMES_HOME/plugins/model-providers/<name>/,__init__.py 被發現時自動 import,模組級程式碼呼叫 register_provider() 完成註冊——不用改設定檔。代價是排序和覆蓋要靠註冊表策略(命名空間、override 顯式 opt-in)兜底,換來的是熱插拔和第三方外掛零摩擦接入。

關鍵檔案

資料流

  1. 啟動期 init_agent(agent/agent_init.py:276)觸發外掛發現。
  2. 每個外掛子目錄被匯入,模組級程式碼呼叫 PluginContext 的註冊方法:
  3. 註冊表(單例)成為查詢真相,主迴圈與網關 (gateway) 只讀註冊表,不直接依賴具體外掛。
  4. 外掛可熱插拔:卸載只需 unregister,註冊表處理優先級與覆蓋。

關鍵程式碼

目錄掃描發現外掛

_discover_providers 是 model-providers 的發現入口。它掃描兩個目錄(倉庫內建 + $HERMES_HOME 使用者目錄),對每個子目錄呼叫 _import_plugin_dir:

python
def _discover_providers() -> None:
    """1. Bundled at <repo>/plugins/model-providers/<name>/
       2. User at $HERMES_HOME/plugins/model-providers/<name>/
       Later steps win on name collision."""
    global _discovered
    if _discovered: return
    _discovered = True
    if _BUNDLED_PLUGINS_DIR.is_dir():
        for child in sorted(_BUNDLED_PLUGINS_DIR.iterdir()):
            if child.is_dir() and not child.name.startswith(("_", ".")):
                _import_plugin_dir(child, "bundled")
    user_dir = _user_plugins_dir()
    if user_dir is not None:
        for child in sorted(user_dir.iterdir()):
            if child.is_dir() and not child.name.startswith(("_", ".")):
                _import_plugin_dir(child, "user")

「使用者目錄覆蓋內建」就來自這個順序:兩步都呼叫 register_provider,後者覆蓋前者。_/. 前綴的目錄被忽略,留出 __pycache__ 之類的逃生口。

自註冊:模組匯入即註冊

_import_plugin_dirimportlib.util 把目錄當模組載入,模組級程式碼執行時呼叫 register_provider(profile) 把 profile 推進全域註冊表:

python
module_name = (f"plugins.model_providers.{safe_name}" if source == "bundled"
               else f"_hermes_user_provider_{safe_name}")
spec = importlib.util.spec_from_file_location(
    module_name, init_file, submodule_search_locations=[str(plugin_dir)])
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)

bundled 和 user 走不同模組命名空間,避免兩個 HERMES_HOME 下的同名 profile 在 sys.modules 裡互相 alias。

註冊表:防止默默覆蓋內建工具

registry.register 預設拒絕跨 toolset 的同名覆蓋。MCP 之間允許覆蓋(刷新場景合法),但外掛想替內建工具必須顯式 override=True,且要先有 operator 在 config 裡 allow_tool_override: true 才放行——否則直接拋 PermissionError,而不是默默替換。owner 判定基於 handler.__globals__["__name__"],定義時綁定,外掛沒法用巢狀 lambda 把覆蓋洗白成「來自內建模組」。詳見 registry.register:365-436

邊界與失敗

  • 外掛 import 失敗:_import_plugin_dir 把例外吞掉只打 warning,並從 sys.modules 彈出避免半載入狀態。一個壞外掛不會拖死整個 agent,但對應的 provider/platform 就拿不到,需要看日誌才能發現。
  • 覆蓋內建工具沒顯式 opt-in:registry.registerPermissionError 而不是默默替換;operator 必須在 config.yaml 裡寫 plugins.entries.<plugin_id>.allow_tool_override: true 才放行。
  • context engine 不可深拷貝:多 agent 共享的 context engine 在 child agent 啟動時 copy.deepcopy 隔離 budget 狀態;外掛若持有鎖、DB 連線等不可深拷貝物件,深拷貝失敗會 fallback 到內建 compressor。

小結

外掛 = 「子目錄 + 註冊」。所有跨切面能力(provider、平台、工具、記憶、cron、多模態)都走同一套註冊骨架,核心迴圈因此保持穩定。新增能力 = 新建一個外掛子目錄並註冊,不必動 agent 主幹。

非官方社群學習站,內容以 MIT 授權的 NousResearch/hermes-agent 原始碼為依據。