资讯详情

Hermes Agent Loop:拆解AI Agent执行循环,告别死循环与失控

📅 2026/10/31 9:06:49 | 华诺云谱 👁 阅读
Hermes Agent Loop:拆解AI Agent执行循环,告别死循环与失控
跑AI Agent 最直观的感受就是绕。你给模型一个任务它会在“思考—调用工具—观察结果—再思考”这个循环里反复打转顺利时三轮两轮就把活干完不顺利时它能转上几十轮最后不是超时就是上下文窗口塞满直接报错。我在实际项目里踩过不少这样的坑后来把这个循环彻底拆开才发现大多数问题根本不在模型本身而是出在循环设计上。这次要聊的就是我从项目里反复打磨出来的一套执行循环拆解——Hermes Agent Loop它解决的核心问题只有一个让 Agent 知道自己在循环的哪一步也知道自己什么时候该停下来。Hermes Agent Loop 这个名字有两层意思。Hermes 在开源大模型圈子里本身就是个比较有名的系列而它更贴近 Agent 的含义是“信使”——模型在一个循环里不断把用户意图传递给工具世界再把工具世界的结果带回来本质上干的就是信使的活。这篇文章会从循环的整体结构、七个执行阶段、三个最容易翻车的机制、一个最小可运行实现到常见问题排查完整过一遍。适合正在搭 Agent、想理解 Agent 执行细节、或者被“Agent 死循环”折磨过的开发者参考。1. Agent Loop 到底在循环什么1.1 循环不是 bug是 Agent 的基本工作方式很多人第一次调试 Agent 时看到模型反复调用工具会觉得它“卡住了”。其实这不是异常Agent 从设计上就是一个循环结构模型接收输入决定要不要调用工具如果调用就把结果塞回去让模型再看一眼模型根据结果决定是继续调用还是输出最终答案。整个过程本质上就是一个 while 循环模型是循环体里的决策者工具调用是它的“手”工具返回结果是它的“眼睛”。这个循环结构之所以存在是因为大模型本身没有实时获取信息的能力。它训练完那一刻知识就固定了你问它“今天天气怎么样”它没有感知你让它“查一下订单 20240001 的物流”它不知道外部系统里发生了什么。Agent Loop 就是给模型装上了一套“眼睛和手”通过工具调用来感知外部世界再把感知结果送回给模型做下一步判断。把所有 Agent 框架抽象到最底层核心都是同一个骨架输入 - 模型推理 - 工具调用可选 - 结果反馈 - 模型再推理 - ... - 终止。不同的框架之间的差异主要就在于这个骨架每一环的处理方式。Hermes Agent Loop 做的就是把这每一环都规范化什么信息该进上下文、什么信息不该进、每轮循环要额外注入什么、什么时候必须停下来。把这些规则定清楚Agent 的行为就会稳定一大截。1.2 为什么叫 Hermes信使模式的三个原则在设计这套循环时我对自己提了三个问题模型在这个循环里到底扮演什么角色上下文里的信息流该怎么管理循环什么时候结束才是“真结束”回答完这三个问题就形成了 Hermes Agent Loop 的三个核心原则也是它和其它 Agent 实现最大的区别。第一个原则叫“模型是信使不是决策中心之外的第二大脑”。意思是不要让模型自己瞎猜工具的返回格式也不要在每轮循环里把海量历史信息堆给它。信使的职责是传递和转译把用户的需求翻译成工具调用参数再把工具结果翻译回用户能理解的语言仅此而已。第二个原则是“每一轮循环都必须有可观测的进度”。很多 Agent 失控是因为模型自己都不知道自己走到了第几步。Hermes Agent Loop 会在每轮循环后向上下文里注入一条进度消息告诉模型当前是第几轮、任务目标是什么、已经完成哪些步。这一个看似简单的操作实测对减少复读机式死循环帮助非常大。第三个原则是“终止条件必须显式声明”。模型天然倾向于“再想想”如果你不在每次推理前提醒它“用户问题已经完全回答时结束循环并输出答案”它就会在工具调用里绕圈子。终止不是模型的隐含能力而是循环设计的一部分这个后面在复现环节会给出具体写法。2. Hermes Agent Loop 的七段执行流程拆解2.1 感知输入与意图路由第一道关卡最容易偷懒循环的第一步是接收用户输入。听起来没什么技术含量但这里有个高频坑直接把原始输入塞给模型不做任何预处理。用户抛过来一句“帮我看看那个订单到哪了”如果你什么都不做就进入循环模型就要自己去判断“那个订单”是什么极容易产生幻觉参数。Hermes Agent Loop 在感知阶段会做三件事。第一去噪过滤无意义的前缀后缀比如问候语、语气词保留核心指令。第二实体抽取用规则或轻量模型把关键实体先抓出来比如订单号、日期、用户名如果缺失就明确询问用户而不是让主模型猜。第三意图分级判断这个请求是“直接回答型”不需要工具还是“工具调用型”需要外部数据如果是前者直接走单轮回答不进入循环。这三步看着小实际能省下大量无效工具调用。我曾经试过一批订单查询请求不做实体抽取直接进循环模型大概有 20% 的概率把订单号里的数字读错产生无效查询。后来在前端加了一个正则预提取逻辑先把“2024 开头的数字串”抽出来作为参数候选模型的错误率立刻降到了 2% 以下。这一步的核心思路是能用规则解决的事情绝不要指望模型的推理能力。2.2 任务规划把模糊目标变成可执行步骤当请求确实需要进入循环后下一个阶段是任务规划。很多轻量 Agent 会跳过这一步让模型直接边调工具边想下一步这在简单任务里没事但在多步骤任务里容易出大问题。比如“帮我比较这三家店的商品价格并算出最优方案”模型如果边做边规划很容易忘记前面查过什么到最后凑不出来一个完整答案。Hermes Agent Loop 的任务规划阶段会要求模型先输出一个简短的任务步骤列表例如提取三件商品的链接或名称分别查询价格对比并计算最优购买方案汇总输出这个规划不一定要写得多严谨但它的存在有两个作用。一个是给循环一个“路线图”让模型后续每一步都知道自己在执行哪个环节另一个是给开发者一个观测点模型输出的计划全部写入日志方便后面排查它是不是理解错了需求。一个常见的细节是规划阶段不要要求模型输出完整的 JSON 或严格的格式。我在实践中发现越严格的格式要求越容易让模型把精力花在凑格式上任务步骤反而写得变形。允许它用自然语言写步骤再用后面的工具调用结果来逐步校验比一开始就逼它格式化要可靠得多。2.3 工具决策与行动执行从选对工具到把参数传对任务规划完成之后模型就要面对整个循环里最核心的一环工具决策。它需要从注册表里选一个工具生成调用参数然后执行。工具决策有两个关键点。第一是选择准确性模型面对多个相似工具时要能选对。这高度依赖工具本身的名称和描述写得好不好而不是依赖模型有多聪明。比如一个叫“get_order_status”、描述为“查询订单的当前状态。当用户询问订单物流、配送进度或签收情况时使用”的工具模型几乎不会选错。反过来如果你图省事只写“订单查询”模型就可能把它和“订单列表查询”混在一起。第二是参数生成准确率模型从用户输入里提取参数时最容易出的问题是把信息格式搞错。比如说日期格式工具期望的是“2024-06-01”模型可能生成“2024年6月1日”或者“06/01/2024”。这就是为什么工具 schema 的参数 description 里必须写明格式示例最好直接给出正例和反例。我在项目里吃过这个亏后来在 description 里加了“格式必须为 YYYY-MM-DD例如 2024-06-01”参数生成准确率肉眼可见地提升了。行动执行阶段也不只是把工具跑一遍那么简单。Hermes Agent Loop 要求每个工具调用都包一层异常捕获和超时控制。工具返回结果再统一转成 JSON 字符串标记好 tool_call_id严格按照协议格式塞回消息队列。这一步在代码里只是几行但如果不做工具一旦抛出异常模型看到的是一段堆栈信息很容易被带偏在错误的方向上反复尝试。2.4 结果解析、记忆更新与终止判断循环的出口在这工具执行完之后结果会作为一条新消息回到模型面前模型需要解读这个结果更新自己的认知然后决定是继续循环还是终止。结果解析阶段最容易犯的错是原样把工具返回的大量原始数据塞回上下文。我处理过一个真实案例一个工具返回了 80KB 的 JSON里面有大量无关字段。模型需要的信息其实只有其中的两个字段但整段 JSON 被原样塞进了消息直接占掉一大块上下文。更麻烦的是模型在处理这种冗长数据时容易“看花眼”把无关字段当成关键信息导致它在错误的结论上继续推理。Hermes Agent Loop 的做法是在工具返回后加一个“结果精简”步骤先用规则或者模型把返回信息压缩成一段结构化的摘要再放回上下文。记忆更新也是循环里容易被忽略的一环。一个 Agent 的会话过程本质上是一个不断积累信息的短期工作记忆区。每轮工具调用的结果如果全量保留上下文迟早爆掉。Hermes 的做法是把记忆分为两层短期工作记忆只保留当前任务必需的最近几轮信息长期记忆则把任务中产生的结论性信息比如“用户地址是北京市朝阳区 XX 街道”写入持久化存储供以后的会话复用。短期记忆用滑动窗口长期记忆用显式写入两件事分开做。最后是终止判断。循环必须有一个明确的退出条件我在实际项目里一般同时采用三层策略模型主动输出最终答案时终止轮数达到上限时强制终止上下文长度接近模型上限时提前强制终止。三层缺一不可具体设计在第四部分会给出代码级的实现。3. 三个最容易翻车的核心机制3.1 上下文窗口Agent 的工作台不是垃圾场上下文窗口是整个 Agent 系统里最稀缺的资源也是新手最不在意的东西。很多人把 Agent 当成普通的 chat API 来用消息列表越堆越长每轮都往里面塞新内容结果模型跑着跑着就开始“失忆”——它忘了最开始的任务目标是什么。我把 Agent 的上下文比喻成一个人的工作台。台面就那么大你想要的工具、当前的任务说明、最近的几个关键结果必须摆在台面上而那些已经用过的旧消息、不再需要的中间产物就应该及时收进柜子或者丢掉。Hermes Agent Loop 在上下文管理上遵循三条规矩系统提示词始终固定在开头包含任务目标和行为规则永不被滚动掉。消息队列使用滑动窗口只保留最近的 N 轮对话和工具调用记录。超过窗口阈值的旧消息通过摘要压缩成一条“历史小结”放回上下文。给一个可参考的数值如果你用的是 128K 上下文的模型把滑动窗口设在最近 20 到 30 轮是一个相对稳健的区间。每轮的消息如果平均 1500 token30 轮就是 45000 token加上系统提示和当前的工具结果摘要大概能控制在 60% 占用率以内。留出上下文余量非常重要因为模型需要考虑和推理的空间磁盘满了会崩上下文满了会让模型变傻。3.2 工具调用协议描述写得好参数才传得准工具调用出问题的概率远高于大多数人预期。原因是模型对工具的描述非常敏感你写“describe the weather”和写“查询指定城市的当前天气、温湿度及未来三小时降雨概率。当用户关心天气、出行是否下雨、是否需要带伞时使用”模型选对工具的概率完全不一样。工具描述不是给程序员看的注释它是给模型看的“使用说明书”。工具 schema 里更容易被忽视的是参数 description。我整理过一个参数描述的四要素模板字段含义、取值类型、格式示例、常见错误示例。举个例子order_id: { type: string, description: 订单号纯数字字符串通常以2024开头格式示例20240601001。注意不要填入带#号或-号的完整字符串。 }这样的参数描述会直接降低模型填错参数的概率。另外还有一个重要的细节工具的返回结果也要在设计阶段就考虑“模型友好性”。工具返回的字段命名清晰、层级扁平、必要的值放在最显眼的位置模型解读结果的准确度会明显提升。如果你的工具没法改输出格式那就在 result 精简阶段做一次字段映射把它转成模型更容易读的结构。3.3 记忆读写短期记过程长期记结论记忆机制是 Agent 从“能用”到“好用”的分水岭。很多团队做的 Agent 单个任务没问题但一旦跨多轮对话或者用户隔几天回来接着聊它就变成全新的人工智障什么都不记得。原因就是没有设计记忆读写。Hermes Agent Loop 的记忆读写本身不复杂难在分清什么该写、什么不该写。我的经验是短期工作记忆记录“过程”比如已经查过哪些数据、当前做到第几步长期记忆记录“结论”比如用户所在城市、用户的偏好、已经完成的任务结果。过程的记忆不要写长期存储否则垃圾越积越多结论的记忆一定要写长期存储否则下次对话又要重新问一遍。这里有一个真实踩过的坑长期记忆写成“用户之前问过订单状态”。这种描述毫无价值对后续任务没有任何帮助。正确的做法是写成“用户当前有一个未完成的订单订单号 20240601001最后状态是运输中”。它包含实体、属性和状态下次对话如果涉及这个订单模型可以直接利用。4. 手把手复现一个最小可运行的 Hermes Agent Loop4.1 整体代码骨架下面这段代码是我在项目里用的 Loop 骨架的简化版本只保留了最核心的结构没有依赖任何重框架只有 Python 标准库和 LLM 客户端的 chat 接口。它演示的就是 Hermes Agent Loop 的核心循环、工具调度、结果反馈、终止判断。import json class HermesAgentLoop: def __init__(self, llm, tools: dict, system_prompt: str, max_steps8): self.llm llm self.tools tools # {name: {schema: {...}, fn: callable}} self.system_prompt system_prompt self.max_steps max_steps self.messages [{role: system, content: system_prompt}] def run(self, user_input: str) - str: self.messages.append({role: user, content: user_input}) for step in range(1, self.max_steps 1): resp self.llm.chat( self.messages, tools[t[schema] for t in self.tools.values()], tool_choiceauto, ) # 模型没有请求调用工具说明它认为任务已完成 if not resp.tool_calls: return resp.content self.messages.append({ role: assistant, content: None, tool_calls: [ { id: c.id, type: function, function: { name: c.function.name, arguments: c.function.arguments, }, } for c in resp.tool_calls ], }) for call in resp.tool_calls: tool_name call.function.name args json.loads(call.function.arguments) try: result self.tools[tool_name][fn](**args) content json.dumps(result, ensure_asciiFalse) except Exception as e: # 工具异常也要格式化成模型能理解的结构 content json.dumps({error: str(e)}, ensure_asciiFalse) self.messages.append({ role: tool, tool_call_id: call.id, content: content, }) # 注入进度提示防止模型在循环里“忘记自己走到哪了” self.messages.append({ role: system, content: f[进度] 当前已完成第 {step}/{self.max_steps} 轮循环。 f请根据已有的工具结果判断用户问题是否已经回答完整 f完整则输出最终答案不完整则继续调用工具。, }) return 超出最大循环次数强制终止请向用户说明当前已获得的信息。这段代码的核心逻辑很直接每次调用模型后检查它是否请求了工具调用。有工具调用就执行并把结果追加进消息队列没有工具调用就把它的输出作为最终答案返回。工具执行抛出的异常也被格式化成 JSON 返回给模型而不是让模型面对一堆堆栈信息。4.2 System Prompt 与工具注册的关键细节代码骨架只是壳真正决定 Agent 行为的是 System Prompt 和工具注册表。我常用的 prompt 模板长这样你是一个任务执行代理核心工作方式是一个循环 理解需求 - 调用可用工具 - 观察工具结果 - 判断任务是否完成。 规则 1. 只有确实需要外部信息时才调用工具能用已有结果回答的问题不要再调用工具。 2. 每次调用工具时优先选择最合适的那一个参数严格按工具说明填写。 3. 工具返回异常时先分析错误信息中的原因不要盲目重复相同的调用。 4. 当且仅当用户的问题已经被完整回答时结束循环输出最终答案。四个规则里面前两条控制工具调用的频率和准确性第三条防止“错误重试死循环”第四条是终止条件的显式声明。从我的实测看这个 prompt 里最有效的是第四条因为模型在没有明确终止指令时倾向于继续调用工具求一个“更完美”的答案。工具注册方面我建议用标准的 function calling schema。这里给一个查询订单状态的示例重点看 description 的写法{ type: function, function: { name: query_order_status, description: 查询订单的当前状态。当用户询问订单物流、配送进度或签收情况时使用。, parameters: { type: object, properties: { order_id: { type: string, description: 订单号纯数字字符串通常以2024开头。格式示例20240601001。不要填写带#或-号的字符串。 } }, required: [order_id] } } }工具注册表直接放在循环外面通过 name 与工具函数绑定。这种设计的优势是方便后续扩展加一个工具只需要注册一条 dict循环内部完全不用改。4.3 循环终止条件怎么设计才不失控终止条件这一节我要单独拎出来说因为“Agent 不终止”是这个领域最高频的问题。从设计角度终止条件必须有至少三层。第一层是模型自主终止模型判断任务已经完成输出最终答案不再请求工具调用。这一层是理想态但不可依赖它单独工作因为模型有时会“自认为完成”实际上漏掉了步骤。第二层是轮数硬上限比如上面代码里的 max_steps8。超了之后就强制返回并向用户说明“当前已获取的信息”。轮数的数值不固定简单任务 5 轮以内复杂任务可以放宽到 15 轮左右。太小的值容易在任务完成前被截断太大的值会让系统在最坏情况下耗时过长。第三层是上下文长度保护在每轮模型请求前检查当前消息队列的总 token 数接近某个阈值比如模型最大上下文长度的 70%时提前终止循环不再请求模型。这一层平时很少触发但一旦触发就能避免一次“上下文超限”的崩溃。三层配合下来Agent 的行为曲线会稳定很多绝大多数正常任务在第一层就结束了少部分任务因为模型犹豫在第二层被兜底极端场景由第三层保护住系统不崩。5. 常见问题与排查技巧实录5.1 六个典型的 Loop 翻车现场下面是我在实际运行调试里遇到频率最高的六个问题整理成速查表现象与排查思路都在里面问题现象可能根因排查思路解决建议同一个工具被无限重复调用模型没有从上次的错误结果里吸取教训错误信息格式不友好检查工具返回的异常结构是否为模型可读的 JSON在异常返回里给出可能原因和建议动作在 prompt 规则 3 中明确禁止盲目重试循环轮数不多但上下文爆掉工具返回大 JSON 被原样塞回消息队列查看日志中每轮消息的 token 统计加结果精简步骤对工具返回做字段映射和摘要模型选了功能相近但错误的工具工具描述写得太泛或者互相重叠逐个检查工具 description 是否区分清晰重写描述明确各自使用场景和“不使用”场景传错参数日期格式、订单号带多余符号参数 description 缺少格式示例和反例观察模型生成 arguments 的原始值在参数 description 中补充格式正例和错误指引任务没完成但模型提前终止缺少对“完成”的校验模型急于收尾检查最终答案与用户问题的对应关系在每轮进度提示中重申任务目标必要时增加“最终答案自检”步骤第二次会话完全不知道第一次聊过什么长期记忆未写入或未加载检查长期记忆存储和注入逻辑每次会话开始时拉取相关长期记忆注入系统提示词六个问题里最值得展开的是第一个。工具返回的异常如果直接抛给模型模型看到的是 Python 的 traceback里面全是“File xxx.py line xx in xxx”它根本不知道该怎么办。把异常捕获住统一格式化成“工具执行失败原因xxx建议检查参数后重试”模型才知道下一步怎么走。这就是我在代码骨架里对工具调用做 try-except 的原因。5.2 一条万能的排查路线如果 Agent 行为异常但你看不出哪里有问题我建议一条固定的排查路线先看日志里模型的工具调用序列再看每轮消息的 token 消耗最后看终止原因。工具调用序列是最直观的排查入口。把每轮循环里的 tool_calls 按时间排出来如果看到相同调用反复出现问题大概率出在“模型没有从失败结果中学习”或“上下文里缺少进度信息”如果看到工具调用跳跃不定问题出在工具描述不清或者上下文里塞了太多无关信息。token 消耗统计能快速定位上下文管理的问题。给每轮消息记录打印 token 数如果每轮涨幅非常大说明工具返回没有被精简结果摘要机制没有生效如果整体平稳但突然在某一步后猛涨说明这一步塞入了超大返回。终止原因是最后一个诊断出口。在代码里把终止路径记录到日志是模型主动输出、轮数超限还是上下文保护触发。如果大量任务都走轮数超限终止说明模型无法在预算内完成要么 max_steps 设小了要么工具调用效率太低如果大量任务走上下文保护终止说明消息队列的滑动窗口需要调小或者结果精简没做好。这三板斧下来绝大部分 Loop 异常都能定位到具体环节。我自己调试一个不稳定 Agent 时通常不超过半小时就能找到根因靠的就是这条固定路线。写在最后我把这套执行循环从设计到落地反复打磨了几个月最大的体会是Agent 的稳定性不靠模型而是靠循环结构把不确定性兜住。模型在该发挥创造力的地方发挥创造力循环结构则在该收口的地方收口——终止条件、进度提示、结果精简、记忆分层每一个看起来都是小设计连在一起才让 Agent 真正可控制、可观测、可维护。最后再分享一个小技巧给每轮循环都打上一个结构化的日志包含轮数、调用的工具、参数摘要、token 增量后面排查问题时你会发现日志比模型本身还能解释问题。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑