Skip to content

Agent 主迴圈

源码版本v2026.7.20

職責

run_conversation 是 Hermes 的心臟:接收一條使用者訊息,反覆呼叫模型 + 工具 (tool),直到迭代預算 (iteration budget) 耗盡或任務完成。它同時協調上下文 (context) 建構、工具分發 (dispatch)、停止條件判定,以及失敗時的回退 (fallback)。

檔案總長約 5800 行,邏輯密度很高——這一頁只看主線,細節拆到子頁。

關鍵檔案

資料流

  1. run_agent.py AIAgent 是瘦轉發器,實際依賴注入在 init_agent 完成。
  2. 呼叫方傳入 user_message,run_conversation 先走 build_turn_context 裝配本輪上下文(api_messages)。
  3. 進入 while 主迴圈:檢查中斷 → 遞減預算(iteration_budget.consume())→ 建構/消毒 api_messages → 預壓縮 → 排空 /steer 文字 → MoA 聚合 → 進入 API 呼叫重試子迴圈。
  4. 模型回傳後做回應校驗與 finish_reason 處理;失敗時走 agent._try_activate_fallback()
  5. 命中壓縮閾值時走 context_compressor 回收空間。
  6. 迴圈退出後(預算耗盡/任務完成/中斷)回傳最終訊息,由網關層 (gateway layer) 投遞(見網關層)。

主迴圈的真實樣貌見 agent/conversation_loop.py:721-740:

python
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 開頭:

python
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_countcompression_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-CNwebsite/i18n/zh-Hans/

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