Agent Loop 源码导读:一次 Hermes 任务的完整生命周期

发布时间:2026/5/22 23:06:16

Agent Loop 源码导读:一次 Hermes 任务的完整生命周期 点击上方 前端Q关注公众号回复加群加入前端Q技术交流群我读 Hermes 源码的时候第一件让我吃惊的事是 ——它的 Agent Loop 主循环不到 200 行代码。我原以为像 Hermes 这种自进化系统主循环至少得 1000 行起跳里面塞满各种 Memory 注入、Skill 检索、状态机、错误处理。结果打开一看主循环干干净净做的事情就一句话循环 → 拼提示词 → 调模型 → 看要不要调工具 → 检查是不是该退出。那些复杂的事情都被推到了别的模块里。Loop 本身只是个调度器。这种克制的设计是 Hermes 真正聪明的地方。这一篇我们就把这个 200 行的主循环拆开看搞清楚一次 Hermes 任务从启动到退出内部到底发生了什么。主循环就 5 个阶段把 Hermes 的 Agent Loop 抽象出来本质就 5 个阶段循环跑Receive Input拿到 user message 或上一轮 tool resultBuild Prompt拼 Memory Skill ContextCall LLM发请求 / 等响应 / 解析结果Execute Tool模型要调工具就执行不要就跳过Check Done任务完成了完了就退出没完就回到第 1 步我故意没用源码里的真实变量名因为不同 Agent 框架Hermes、LangGraph、Cursor、Claude Code的命名都不一样但本质都是这 5 个阶段。理解了这个抽象再看任何一家的源码都能秒懂。金句Agent 不是一次性的调用而是一个循环。这是和普通 Chat 应用最根本的区别。Chat 是 request-response 模式问一句答一句。Agent 是 task-completion 模式给定一个目标循环跑直到完成。我看过有些团队做Agent其实只是在 ChatCompletion 外面套了个 if-else没有真正意义上的循环。这种东西做不出自进化。没有 Loop就没有 Agent。4 个核心模块各司其职虽然主循环只有 5 个阶段但 Hermes 在内部把每个阶段拆给了不同的模块。读源码时如果不搞清楚这套分层会看得晕头转向。模块 1Loop Orchestrator循环主控这就是那个 200 行的主循环本体。它的职责非常窄▸维护当前是第几轮turn count▸在 5 个阶段之间调度▸检查终止条件▸把每一步事件丢给下层记录它不知道 prompt 怎么拼不知道模型怎么调不知道工具怎么跑。它只知道下一步该叫谁。这种什么都不做的设计反直觉但其实是好架构的标志 ——主循环应该是最薄的那一层重逻辑要推到下面。模块 2Prompt Builder提示词组装这是 Hermes 里最复杂的模块之一下一篇会专门讲。它要在每一轮做的事很多▸读 system prompt 模板▸注入相关 Memory 条目▸检索并注入相关 Skill▸把可用的 Tools 序列化成 schema▸拼上整个对话历史▸控制总长度不超过模型 context window这一块独立出来的好处是改 prompt 策略不需要动主循环。你想加一种新的注入方式比如加一个 RAG 检索结果只改 Prompt BuilderLoop 完全不知道。模块 3执行层LLM Adapter Tool Runner这一层一轮内可能被多次调用。▸LLM Adapter屏蔽掉不同模型 API 的差异OpenAI / Anthropic / Bedrock 长得都不一样上层只需要传 prompt、拿回结构化响应▸Tool Runner拿到模型要调用的工具找到对应实现传参捕获异常把结果包成统一格式这两个模块都是无状态的 —— 每次调用都是独立的不依赖任何全局状态。这是工程上的好习惯方便测试也方便并发。模块 4Trajectory Recorder轨迹记录这一层是后台默默干活的把循环里的每一个事件写到一个结构化的 session trace 里。为什么单独抽出来因为这份 trace 是后续 Nudge Engine 和 Review Agent 的输入。把记录这件事和执行解耦意味着即使你完全不开自学习功能主循环也能正常跑trace 写到 /dev/null 就行。金句分得清楚才能改得动。我自己改过几次 Hermes 的 Loop 实现每次都很顺手 —— 因为 4 个模块边界很清楚改一个不会牵动其他。这是源码读起来体感最好的部分。一轮内的完整时序讲完模块结构再看一轮内的调用时序是什么样。这部分必须画时序图才说得清楚。按数字顺序看Orchestrator → Prompt BuilderbuildPrompt(state)主循环把当前的状态已有对话、用户输入、上一轮 tool result打包让 Prompt Builder 拼一份完整提示词。Prompt Builder → Orchestratorreturn prompt返回拼好的 messages 数组。这一步在 Hermes 里平均耗时 50-200ms包含 Memory 检索、Skill 匹配的开销。Orchestrator → LLM AdaptercallModel(prompt)把 prompt 发给模型 API。这一步是整个循环里最慢的通常 1-5 秒。LLM Adapter → Orchestratorreturn assistant message返回模型的回复。这个回复可能是纯文本任务完成也可能包含 tool_calls需要执行工具。Orchestrator 自检has tool_calls?解析模型回复看是不是要调工具。Orchestrator → Tool Runnerexecute(tool_call)如果有工具调用逐个执行。注意 Hermes 里默认是串行执行除非工具明确标注了可并发这是为了避免并发副作用。Tool Runner → Orchestratorreturn tool result每个工具的结果包装成tool_result消息准备塞进下一轮的 prompt。Orchestrator 自检task done?- 如果模型没有发起工具调用 → 任务完成退出 Loop- 如果有工具调用且都执行完了 → 回到第 1 步开新一轮金句一轮 一次模型调用 0~N 次工具调用。这个等式很重要。很多人以为一轮 一次模型调用结果在算 token 和耗时时算偏了。实际上一轮里如果模型调了 5 个工具那就是 1 次 LLM 5 次 Tool。一个有意思的细节很多人觉得 tool_calls 应该并发跑更快。Hermes 默认串行的原因是 ——工具之间可能有顺序依赖比如先 read_file 再 write_file并发跑会出竞态。除非工具明确声明我是只读、纯函数、可并发否则一律串行。4 种退出姿势Loop 必须知道什么时候停。这件事比看起来难得多。我见过不少人写的Agent在测试环境跑得好好的一上线就死循环跑爆 token 账单。原因都是 ——只考虑了任务完成这一种退出条件没考虑兜底情况。Hermes 里的 Loop 有 4 种退出姿势缺一不可。退出姿势 1任务自然完成最常见的一种模型这一轮不再发起 tool_calls直接给出 final answer。判断条件就一句话if (assistantMessage.tool_calls.length 0) break;我看了大概一周的运行日志Hermes 里大约 70% 的任务是这种正常退出。退出姿势 2达到最大 turn 上限兜底机制。哪怕模型疯了一直在调工具到了 turn 上限也要强制退出。Hermes 默认是 25 轮小任务或 50 轮复杂任务。这个数字是经验值 —— 太小会让正常任务被掐断太大会让卡住的任务无限消耗 token。特别坑的点达到 turn 上限的退出不能算任务完成否则 Review Agent 会把卡住当成成功去复盘写出错的 Skill。Hermes 在这种情况下会显式标注terminated_by: max_turns让下游知道这次是异常结束。退出姿势 3用户主动打断用户按 CtrlC、点击取消按钮、或者通过 Hook 阻断当前任务。这种退出要做的工作比看起来多▸如果模型正在生成中需要中断 stream▸如果工具正在执行中需要尝试取消不是所有工具都支持取消▸已经写出去的状态要回滚还是保留Hermes 的处理是已经完成的工具调用保留结果正在执行的工具尝试 cancel模型流式输出直接丢弃。这套规则不是最优的但是是用户最容易理解的。退出姿势 4不可恢复错误模型 API 挂了、工具抛出未捕获异常、内存爆了 OOM。这一类退出的关键是优雅二字。要做到▸把错误信息记到 trajectory 里让 Review Agent 知道这次是因为啥挂的▸释放占用的资源数据库连接、文件句柄▸给用户一个能看懂的错误提示而不是甩个 stack traceHermes 这块写得挺细的不同类型的错误有不同的处理路径。我看源码时学到一招所有从 Loop 抛出去的异常都要包装成AgentError带上错误码和上下文。这样不管在哪一层接住都能拿到完整信息。金句一个好的 Loop必须有 4 种退出姿势。我现在自己写 Agent 时第一个写的就是退出条件。先想清楚怎么停再想怎么跑—— 这是从 Hermes 学到的最重要的一条。我自己读源码的几个发现读 Hermes Loop 源码的过程中有几处设计让我印象很深。发现 1Loop 内不做任何业务判断Hermes 的 Loop 里没有任何 if 任务类型 X 那么... 的代码。业务差异比如代码任务要先 lint、文档任务要先 outline全都通过 Skill 注入到 prompt 里由模型自己判断。Loop 永远是同一个流程。这种通用循环 差异化 prompt的设计让 Loop 可以在所有场景复用不会因为加新功能而越来越臃肿。发现 2每一步事件都立刻落盘Trajectory Recorder 不是任务结束才一次性写入而是每一步事件实时写到磁盘。我刚开始觉得这是性能浪费后来想明白了 ——如果任务跑到一半挂了没落盘的事件就丢了Review Agent 拿不到完整 trace。实时落盘的代价是每秒多写几次 disk收益是任何异常情况下都能事后复盘。这个 trade-off 很值。发现 3Tool 调用一定串行前面提过但我想再强调一次。Hermes 默认串行执行工具调用不是性能不够是为了正确性优先于速度。我做实验把它改成默认并发跑 100 个真实任务发现 7% 的任务出现了读到了上一步还没写完的数据这种竞态。一旦出错整个 trajectory 就变得无法复盘。并发应该是个 opt-in 选项不能是默认。发现 4状态机被刻意避免很多 Agent 框架比如 LangGraph把 Loop 实现成显式状态机节点之间有明确转移规则。Hermes 没用状态机。Loop 就是个简单的 while 循环状态全在变量里。我刚开始觉得这是技术债后来意识到这是有意为之 ——显式状态机会让 Loop 的每一种状态都需要预先定义新增能力时改动巨大。简单的 while 循环灵活得多新增能力只需要往 prompt 里加内容。这两种风格各有适用场景。任务种类有限、流程稳定的场景用状态机比如客服对话任务种类无限、需要高度灵活的场景用简单 Loop比如通用 Coding Agent。我的看法读完 Hermes 的 Agent Loop 源码我最大的感受是 ——好的 Agent 框架主循环越少越好。把所有复杂逻辑都堆在主循环里是初学者最容易犯的错。结果就是循环代码 5000 行谁来都改不动加任何新功能都怕踩坑。Hermes 走了相反的路▸主循环只做 5 件最朴素的事▸复杂的事推给 Prompt Builder、Tool Runner、Trajectory Recorder▸业务差异通过 Skill 注入不进 Loop这种主循环克制外围灵活的设计让整个系统既好理解又好扩展。更重要的是它让自进化成为了一个可以叠加的能力而不是嵌入的逻辑。Memory、Skill、Nudge、Review 都是从外部叠加到这个 Loop 上的Loop 本身不知道它们的存在。哪天你不想要自学习了把这些模块拔掉Loop 照样能跑。这种解耦在工程上极其值钱。任何想做长期演进的 Agent 系统都应该把基础执行能力和高阶能力分得这么清楚。下一篇我们深入 Prompt Builder ——Memory、Skill、Context Files 到底是怎么进入 system prompt 的长度超了怎么裁剪、优先级怎么定。这是整个系统里真正决定模型每一步看到什么的关键。往期推荐Multi-Agent Teams让多个专家 Agent 像团队一样协作AI Agent 是怎么想一步做一步的拆解 ReAct 模式从零开始用 LangChain.js 构建你的第一个 Tool-Calling Agent最后点个在看支持我吧

相关新闻