AIAgent / init_agent
職責
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 判定。
關鍵檔案
class AIAgent:400-497— 類別定義,欄位即設定__init__ 轉發:497-560—from agent.agent_init import init_agent; init_agent(...)def init_agent:276— 裝配主函式入口自訂 provider extra_body 輔助:183-275—_normalized_custom_base_url/_custom_provider_model_matches/_merge_custom_provider_extra_body_resolve_compression_threshold:93-127— 壓縮閾值解析輔助函式區:62-91—_ra、autoraise notice 等
資料流
AIAgent.__init__(run_agent.py:423)把建構參數原樣收集。- 呼叫
init_agent:276,依次:- 解析 provider 名與 base_url(
agent/agent_init.py:183) - 合併自訂 provider 的
extra_body(agent/agent_init.py:257) - 解析壓縮閾值(
agent/agent_init.py:93) - 注入 toolsets、記憶管理器、學習圖等
- 解析 provider 名與 base_url(
- 裝配完成的 agent 暴露
run_conversation方法(run_agent.py:6350),轉發到模組級agent/conversation_loop.py:588。 - 網關 (gateway)
GatewayRunner(gateway/run.py:3029)持有裝配好的 agent,把入站訊息交給它。
provider 名在 init_agent 裡被規整為小寫,空白被剝離;provider 既用作路由提示也用作 api_mode 推斷的依據:
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 是單向的——只能抬高,不能壓低使用者已經設的更高閾值:
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,已有鍵優先保留使用者自己設的值:
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 = overridesupdate 的方向是「設定裡的 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,目的是讓測試可以 monkeypatchrun_agent.OpenAI等屬性後再調init_agent。代價是首次呼叫多一次 import,patch 時機晚了的話已經綁定的屬性不會被新 patch 覆蓋。
小結
init_agent 是設定→執行時的翻譯層,所有啟動期決策(provider 選擇、壓縮策略、toolsets 開關)都在這裡一次性落定。之後 agent 進入無狀態的「餵訊息→跑迴圈」節奏,不再回頭改設定。