Skip to main content

第2章:Agent Loop —— 让一个请求持续到任务完成

一、直接问模型为什么不够​

直接调用模型只有一个动作:输入消息,得到文本。Agent 需要面对的是一个不确定的状态机:模型可能要求读文件、执行命令、调用浏览器、生成子任务,然后再根据结果决定下一步。真正的循环是:

用户输入
→ 组装可见上下文
→ 请求模型
→ 读取 assistant 文本与 tool calls
→ 执行工具并追加 tool result
→ 再次请求模型
→ 直到 stop / interrupt / failure / budget

Hermes 还在每一圈加入重试、provider failover、上下文压缩、空响应修复、消息交替修复、用量记账和流式显示。因此“循环”既是控制流,也是所有边界的汇合点。

二、核心对象:AIAgent 与 run_conversation​

run_agent.py:409 的 AIAgent 更像“运行时容器”,而不是一个无状态服务。它持有的状态至少包括:

  • 当前 provider、model、API 模式和请求参数;
  • 当前 session_id、历史 messages、来源 platform 和 task id;
  • ContextCompressor 或其他 ContextEngine;
  • MemoryManager、工具注册表和 toolset 过滤结果;
  • 中断标志、当前工具、工作线程集合、回调和显示模式;
  • 重试、计费、prompt cache、relay 和 session DB 相关状态。

run_agent.py:7153 的 AIAgent.run_conversation 负责设置一次运行的边界,agent/conversation_loop.py:1142 的 run_conversation 负责更细的请求循环。前者处理“这个 Agent 如何运行”,后者处理“这一轮如何推进”。

三、把一个 Turn 拆成六个阶段​

阶段 1:确定运行身份​

运行前要确定 session_source、cwd、profile、task id、当前模型和可用工具。CLI session 可以记录启动 cwd;Gateway 或远程后端则不能把宿主 cwd 当成 Agent 工具的 cwd。run_agent.py 的 _launch_cwd_for_session 明确区分了这一点。

阶段 2:准备系统提示词和历史​

agent/system_prompt.py:152 的 build_system_prompt_parts 将身份、平台提示、工具说明、环境提示、技能清单、上下文文件和订阅/能力提示组合起来。conversation_loop.py 在真正发请求前还会:

  • 去掉只用于持久化或显示的字段;
  • 把外部记忆和插件注入放进当前 user message 的 API sidecar;
  • 修复坏掉的 tool call JSON;
  • 修复 role alternation 和孤立 tool result。

关键取舍是:干净 transcript 与发送到模型的 payload 可以不同。这样记忆或平台信息不会污染可搜索、可恢复的原始会话。

阶段 3:请求模型​

模型调用不直接写死在 loop 里,而是落到 transport/adapter。loop 只消费统一的 assistant message、usage、finish reason 和错误分类。流式 delta 可以先进入显示和 relay,完整 assistant message 仍要进入历史,才能保证下一轮 API 请求有完整的 tool call。

阶段 4:处理 stop reason​

概念上可以把停止信号分成四类:

信号含义后续动作
普通文本结束模型认为任务已完成返回结果
tool calls模型要求外部世界发生动作进入工具执行
interrupted用户或上层取消追加取消结果,安全收尾
error / budget请求、配额或策略失败分类、重试、failover 或结束

Hermes 不把异常都当成“停止”。某些 provider 的 401、超时、空响应、上下文过长或 billing block 需要不同的恢复策略。

阶段 5:执行工具​

agent/tool_executor.py 提供 concurrent、sequential 和 segmented 三种入口。工具结果按模型给出的 tool call 顺序写回消息,即使执行实际使用线程池并发完成。这样既能利用并发,又不破坏 OpenAI/Anthropic 风格的成对消息约束。

阶段 6:决定是否再转一圈​

工具结果写入后,循环再次检查:是否有 steering/redirect、是否需要压缩、是否达到 iteration budget、是否应执行 follow-up、是否收到中断。只有当模型给出可接受的终止状态,或者运行时明确拒绝继续时,Turn 才结束。

四、并发工具为什么不能简单 gather​

Hermes 的并发执行还要处理:

  • 同一批工具的显示顺序与消息顺序;
  • 共享审批/授权 gate;
  • 每个 worker 的中断传播和线程清理;
  • checkpoint 需要在真正执行破坏性命令之前建立;
  • tool output budget 和过长结果截断;
  • 工具 middleware 的前置/后置回调。

execute_tool_calls_concurrent 中的 _begin_in_order 用条件变量约束“开始阶段”的顺序;worker 完成后把 (name, args, result, duration, error, blocked, trace) 放回原位置。这里体现一个很好的原则:并发是执行优化,不应改变对话协议的可观察顺序。

五、异常不是一个 except Exception​

agent/error_classifier.py 把错误分类为可用于决策的原因;conversation_loop.py 还会对 stale credential、provider entitlement、上下文长度、图片尺寸、Copilot credential 等情况给出不同指导。这样的层次是必要的:

原始异常
→ provider/HTTP/SDK 细节
→ FailoverReason / billing / entitlement 分类
→ 用户可理解的提示
→ 是否重试、换 key、换 provider 或结束

如果只把错误字符串放回模型,模型可能会无限重试;如果只返回“失败”,用户又无法知道应该补 key、缩短图片还是降低上下文。

六、Loop 的四个可迁移结论​

  1. 把 Turn 和 Trace 分开:一轮用户输入可以有多个 API call;一次 session 又可以包含很多 turn。
  2. 让终止由结构化信号驱动:不要用“文本里有没有 done”判断结束。
  3. 把副作用放在可拦截的边界:工具、模型请求、持久化和投递都需要 middleware 或回调。
  4. 保持干净状态与线缆状态分离:可显示/可恢复的 transcript 不等于每次实际发出的 payload。

下一章进入 Loop 的“翻译层”:同一套 Agent 如何对接不同 provider、认证方式和请求协议。