Skip to content

AIAgent / init_agent

源码版本v2026.7.20

職責

init_agent 是 agent 的「裝配工廠」:把 provider、toolsets、壓縮 (compression) 閾值、自訂 provider extra_body、記憶與學習元件等設定注入到 agent 實例,使其具備執行一輪對話所需的全部依賴。AIAgent 類別本身只是它產物的薄殼。

設計動機

設定和執行時分開,是為了讓「跑一輪對話」的程式碼路徑盡可能無狀態。init_agent 一次性把所有啟動期決策(provider 選擇、壓縮策略、toolsets 開關、extra_body 合併)落定,之後 run_conversation 進入「餵訊息→跑迴圈」的節奏,不必回頭改設定。這樣做的直接好處是:run_conversation 可以放心並發,同一 agent 實例同時被多條入站訊息驅動時,沒有「設定正在被改」的競態。

壓縮閾值放在啟動期解析、而不是在壓縮觸發時再算,另一個動機:Codex gpt-5.x 家族的 272K 視窗需要把閾值「自動抬上去」用滿視窗,這個抬升要通知使用者一次。執行時每次算要麼反覆彈通知,要麼漏彈。啟動期算一次、寫 marker 檔案,後續直接讀 marker 判定。

關鍵檔案

資料流

  1. AIAgent.__init__(run_agent.py:423)把建構參數原樣收集。
  2. 呼叫 init_agent:276,依次:
  3. 裝配完成的 agent 暴露 run_conversation 方法(run_agent.py:6350),轉發到模組級 agent/conversation_loop.py:588
  4. 網關 (gateway) GatewayRunner(gateway/run.py:3029)持有裝配好的 agent,把入站訊息交給它。

provider 名在 init_agent 裡被規整為小寫,空白被剝離;provider 既用作路由提示也用作 api_mode 推斷的依據:

python
agent.base_url = base_url or ""
provider_name = provider.strip().lower() if isinstance(provider, str) and provider.strip() else None
agent.provider = provider_name or ""
...
if api_mode in {"chat_completions", "codex_responses", "anthropic_messages", "bedrock_converse", "codex_app_server"}:
    agent.api_mode = api_mode
elif agent.provider == "openai-codex":
    agent.api_mode = "codex_responses"
elif agent.provider in {"xai", "xai-oauth"}:
    agent.api_mode = "codex_responses"

壓縮閾值的解析在 _resolve_compression_threshold 裡。Codex 的 autoraise 是單向的——只能抬高,不能壓低使用者已經設的更高閾值:

python
if model_cthresh is None:
    return global_threshold, None
if is_codex_autoraise:
    if model_cthresh <= global_threshold + 1e-9:
        # Autoraise never lowers; keep the user's higher/equal threshold.
        return global_threshold, None
    return model_cthresh, {
        "model": model,
        "from": global_threshold,
        "to": model_cthresh,
    }
return model_cthresh, None

自訂 provider 的 extra_body 合併,核心是按 base_url + model 匹配設定項,找到後把 extra_body 併進 request_overrides,已有鍵優先保留使用者自己設的值:

python
merged_extra_body = dict(extra_body)
existing_extra_body = overrides.get("extra_body")
if isinstance(existing_extra_body, dict):
    merged_extra_body.update(existing_extra_body)
overrides["extra_body"] = merged_extra_body
agent.request_overrides = overrides

update 的方向是「設定裡的 extra_body 當底,使用者已有的 extra_body 覆蓋上面」,所以執行時塞進 request_overrides["extra_body"] 的欄位不會被啟動設定覆蓋。

邊界與失敗

  • provider 名無法解析到 api_mode:使用者給了一個未知的 provider="foobar",api_mode 不會被推斷,會落到預設的 chat_completions 路徑。如果 foobar 實際只支援 codex_responses,執行時第一條訊息就會 400。init_agent 不校驗 provider 是否在白名單裡,只規整大小寫。
  • custom extra_body 合併衝突:同一個 base_url 下配了多個 entry,且都匹配同一個 model,_custom_provider_extra_body_for_agent 走「第一個 model 顯式匹配的贏」,後面被忽略。entry 裡沒有 model 欄位時作為 fallback,只有當沒有顯式 model 匹配項時才用。使用者改了設定但沒重啟 agent,以為新 extra_body 生效了,實際還是舊 fallback。
  • 壓縮閾值非法值:global_threshold 如果是 None 或負數,_resolve_compression_threshold 不做範圍校驗,直接透傳。負數會讓壓縮永遠不觸發,None 在比較時會拋 TypeError。設定層(hermes config)有範圍檢查,但 init_agent 不重複校驗,直接信任 config 產物。
  • autoraise notice 寫 marker 失敗:$HERMES_HOME 唯讀或磁碟滿時,_record_codex_gpt55_autoraise_notice 靜默跳過,代價是下次 init 再彈一次通知。這是有意的 trade-off:寫 marker 失敗不應該讓 agent 起不來。
  • _ra() 的惰性 import:init_agent 裡輔助函式透過 _ra()run_agent 模組,而不是頂部 import,目的是讓測試可以 monkeypatch run_agent.OpenAI 等屬性後再調 init_agent。代價是首次呼叫多一次 import,patch 時機晚了的話已經綁定的屬性不會被新 patch 覆蓋。

小結

init_agent 是設定→執行時的翻譯層,所有啟動期決策(provider 選擇、壓縮策略、toolsets 開關)都在這裡一次性落定。之後 agent 進入無狀態的「餵訊息→跑迴圈」節奏,不再回頭改設定。

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