Agent 主循环
职责
run_conversation 是 Hermes 的心脏:接收一条用户消息,反复调用模型 + 工具,直到迭代预算耗尽或任务完成。它同时协调上下文构建、工具分发、停止条件判定,以及失败时的回退。
文件总长约 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— 系统提示组装moa_loop— Mixture-of-Agents 聚合路径context_compressor— 命中阈值时压缩历史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回收空间。 - 循环退出后(预算耗尽/任务完成/中断)返回最终消息,由网关层投递(见网关层)。
主循环的真实样貌见 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/。