Skip to content

Agent 主循环

源码版本v2026.7.20

职责

run_conversation 是 Hermes 的心脏:接收一条用户消息,反复调用模型 + 工具,直到迭代预算耗尽或任务完成。它同时协调上下文构建、工具分发、停止条件判定,以及失败时的回退。

文件总长约 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. 循环退出后(预算耗尽/任务完成/中断)返回最终消息,由网关层投递(见网关层)。

主循环的真实样貌见 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 源码为依据。