Agent 主迴圈
職責
run_conversation 是 Hermes 的心臟:接收一條使用者訊息,反覆呼叫模型 + 工具 (tool),直到迭代預算 (iteration budget) 耗盡或任務完成。它同時協調上下文 (context) 建構、工具分發 (dispatch)、停止條件判定,以及失敗時的回退 (fallback)。
檔案總長約 5800 行,邏輯密度很高——這一頁只看主線,細節拆到子頁。
關鍵檔案
run_conversation 入口:588-720— 函式簽名、初始化與迴圈前準備while 主迴圈:721-760— 條件api_call_count < agent.max_iterations and agent.iteration_budget.remaining > 0,含 grace callAPI 呼叫重試子迴圈:1227-1280—while retry_count < max_retriesbuild_turn_context— 每輪上下文裝配prompt_builder— 系統提示 (prompt) 組裝moa_loop— Mixture-of-Agents 聚合路徑context_compressor— 命中閾值時壓縮 (compression) 歷史IterationBudget— 預算計數核心
資料流
run_agent.py AIAgent是瘦轉發器,實際依賴注入在init_agent完成。- 呼叫方傳入
user_message,run_conversation先走build_turn_context裝配本輪上下文(api_messages)。 - 進入
while主迴圈:檢查中斷 → 遞減預算(iteration_budget.consume())→ 建構/消毒api_messages→ 預壓縮 → 排空/steer文字 → MoA 聚合 → 進入 API 呼叫重試子迴圈。 - 模型回傳後做回應校驗與
finish_reason處理;失敗時走agent._try_activate_fallback()。 - 命中壓縮閾值時走
context_compressor回收空間。 - 迴圈退出後(預算耗盡/任務完成/中斷)回傳最終訊息,由網關層 (gateway layer) 投遞(見網關層)。
主迴圈的真實樣貌見 agent/conversation_loop.py:721-740:
while (api_call_count < agent.max_iterations and agent.iteration_budget.remaining > 0) or agent._budget_grace_call:
agent._checkpoint_mgr.new_turn()
if agent._interrupt_requested:
interrupted = True
_turn_exit_reason = "interrupted_by_user"
break
api_call_count += 1
if agent._budget_grace_call:
agent._budget_grace_call = False
elif not agent.iteration_budget.consume():
_turn_exit_reason = "budget_exhausted"
break迴圈條件外掛的 or agent._budget_grace_call 是「預算歸零後再給一次機會」的支點:進入這輪開頭立即清掉 flag,所以「一次機會」真的是一次。
API 呼叫重試不是遞迴,而是內層 while retry_count < max_retries。下面是 agent/conversation_loop.py:1227-1245 開頭:
while retry_count < max_retries:
if agent.provider == "nous":
try:
from agent.nous_rate_guard import nous_rate_limit_remaining
if nous_rate_limit_remaining() is not None and nous_rate_limit_remaining() > 0:
if agent._try_activate_fallback():
active_system_prompt = _sync_failover_system_message(
agent, api_messages, active_system_prompt)
retry_count = 0
continue
return {"final_response": "⏳ rate-limited, no fallback.",
"messages": messages, "completed": False, "failed": True}
except Exception:
pass # Never let rate guard break the agent loop切完 provider 後 retry_count = 0 從乾淨狀態重試。最外層 except Exception: pass 是有意的:守衛是優化,絕不能反過來把主迴圈打斷。
設計動機
為什麼重試做成子迴圈而不是遞迴?遞迴在 Python 裡吃呼叫堆疊(深度受限),一次失敗的狀態散到多個堆疊幀難以統一清理。改成 while retry_count < max_retries 後,所有重試狀態(retry_count、compression_attempts)都是同一堆疊幀裡的區域變數,fallback 切換時一行 retry_count = 0 整體復位。
為什麼有 grace call?預算是硬上限,但模型有時候差最後一兩個工具呼叫就能完成,直接卡死會丟已經做的工作。_budget_grace_call 由內層判斷「差一點就成」時主動置位,外層 or 讓這輪跑完,但 flag 進入迴圈立即被清——「一次機會」真的是一次。
為什麼用 iteration_budget 物件而不是簡單計數?預算會被跨執行緒存取(子 agent、壓縮 refund),IterationBudget 用鎖包住 consume/refund,既保證執行緒安全,又讓壓縮器在摘除舊訊息後能呼叫 refund() 把迭代額度還回來。簡單整數做不到這兩件事。
邊界與失敗
- 預算耗盡中途:退出前
_persist_session(messages, conversation_history)把當前會話落盤;_turn_exit_reason標記為"budget_exhausted",供上層判斷是正常結束還是被截斷。 - 工具呼叫例外:工具側拋錯由內層重試捕獲,重試到
max_retries後走agent._try_activate_fallback();fallback 鏈空時回傳failed: True的結果字典,而不是拋例外出去——上層網關只看到結構化失敗。 - 限流守衛自身出錯:
nous_rate_guard的 import 失敗或呼叫例外被except Exception兜住,守衛降級為「不生效」,主迴圈繼續按原路徑走,註解寫得直白:"Never let rate guard break the agent loop"。 - OAuth 池無限刷新:
_auth_pool_refresh_counts在 turn 開頭重置,防範單條目 OAuth 池在持續 401 時被try_refresh_current()無限「成功」(#26080的兜底)。
小結
主迴圈只負責編排:輪次驅動、預算管控、重試與回退。真正的能力來自能力層的工具與技能,多平台投遞交給網關層,自我進化由學習閉環驅動。
想對照官方中文介紹,看 README.zh-CN 與 website/i18n/zh-Hans/。