Skip to content

エージェント主ループ

源码版本v2026.7.20

職務

run_conversation は Hermes の中心:ユーザーメッセージを受け取り、イテレーション予算 (iteration budget) が尽きるかタスクが完了するまで、モデル + ツール (tool) を繰り返し呼ぶ。同時にコンテキスト (context) 構築、ツールディスパッチ (tool 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) 層が配信する(ゲートウェイ層参照)。

主ループの実際の姿は 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)ので、IterationBudgetconsume/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 ソースに基づく。