AI Agent执行循环设计指南:上下文、工具调用与状态管理
发布时间:2026/10/5 9:21:34 作者:尧图编辑部 阅读量:1,286

做了几年 LLM 应用之后我有个越来越强烈的感受一个 AI Agent 项目的上限由模型决定下限却由执行循环决定。模型再强如果 Agent Loop 设计得糙——上下文乱塞、状态丢三落四、循环停不下来——跑出来的效果依然没法看。反过来循环够扎实哪怕用中等规模的模型也能稳定地完成不少多步任务。这篇文章要拆的就是 Hermes Agent Loop我在多个实际项目里反复打磨过的一套 AIAgent 执行流程。核心思路并不新鲜就是 ReAct 那套思考→行动→观察的闭环但真正值得展开的是闭环里每个工程细节上下文怎么组装、工具调用怎么解析、反馈怎么回灌、状态怎么保存、循环什么时候该停。适合两类人看一类正在自己搭 Agent 框架想知道每一步该怎么设计和取舍另一类在用 LangChain 之类的现成框架但遇到问题只能瞎试想搞清楚框架内部的执行逻辑。接下来我会按执行流程一条线拆下去关键节点都会给具体做法和参数参考踩过的坑也一并说清楚。1. 为什么 Agent 必须是一个循环而不是一次调用1.1 一次调用解决不了信息获取和动作执行先回答那个最基础的问题为什么要循环因为绝大多数真实任务靠单次 LLM 调用根本完不成。模型的知识在训练完成那一刻就冻结了它不知道今天的气温查不了你数据库里的订单也没能力帮你点按钮、发请求。单次调用能做的只有一件事基于已有知识生成一段文本。所以一旦任务涉及获取实时信息或者对外部系统执行动作单次调用必然失败。举个实际例子你要 Agent 整理一个竞品主页上的价格信息。模型可能知道这个竞品是谁但不知道它现在的定价就算知道也没法打开网页确认。没有工具调用没有执行循环模型只能在它的训练记忆里检索一个大概然后很有信心地编给你。这在工程上完全不可接受。所以 Agent 的第一步是把知道升级成能查、能取、能改。要做到这一点模型就必须通过工具去触碰外部世界而工具返回的结果又必须回到模型的输入里这样一个交互过程天然就需要循环。1.2 从问-答到行动-观察闭环是 Agent 的分界线聊天机器人是问-答模型Agent 是行动-观察模型。这个区分我觉得特别重要。问-答模型下模型拿到用户消息生成回复完事。整个过程没有外部反馈没有中间状态错了也没机会纠正。而 Agent 的工作方式完全不同它先根据当前目标决定下一步动作执行动作拿到结果再把结果作为新的上下文继续推理直到目标完成或确认无法完成。这个执行-反馈-再决策的闭环本质上和工程里的反馈控制系统是一回事——没有反馈的循环是开环开环系统在环境变化时必然失控。闭环带来的第一个好处是容错。第一次搜索没找到结果模型看到空结果后会调整关键词再搜一次第一次调数据库报错模型看到报错信息后会检查参数重试。这些都是人最基本的做事方式但如果没有循环机器永远做不到。第二个好处是任务可以拆解。复杂任务被拆成多个子步骤每步执行完确认一下再走下一步不会出现一次性幻觉出一整篇错误答案的情况。1.3 Hermes Agent Loop 的总体流程骨架Hermes Agent Loop 整体上是个标准的 while 循环核心骨架用伪代码可以写成这样def run_agent(task: str, tools: list[Tool], max_iterations: int 12) - str: state AgentState(tasktask) for step in range(max_iterations): # 1. 组装上下文系统提示词 工具定义 历史消息 最近观察 context build_context(state, tools) # 2. 思考让 LLM 输出下一步动作思考过程 ReAct 格式动作 response llm_reason(context, temperature0.2) # 3. 解析动作从模型输出里解析出工具名和参数或最终答案 action parse_action(response) # 4. 如果是最终答案循环结束 if action.type final_answer: return action.content # 5. 执行工具捕获结果或异常 observation execute_tool(action, state) # 6. 把观察结果写回消息历史循环继续 state.messages.append(observation) state.step 1 raise MaxIterationsExceeded(f达到最大迭代次数 {max_iterations})别看它短每一行展开都是文章。build_context 决定模型看得到什么parse_action 决定模型命令能不能被正确执行execute_tool 决定动作是否会落在真实系统上而 observation 回写则决定模型能不能从反馈里学到下一步该怎么走。后面几个部分就是沿着这个骨架把每个节点拆开讲。2. 思考-行动-观察Hermes Agent Loop 的三段式内核2.1 思考阶段上下文不是全塞进去而是排优先级我见过很多初版实现build_context 就是把所有历史消息、所有工具定义、一整份系统提示词全部拼起来丢给模型。窗口够大的模型一次两次看不出问题但只要任务复杂一点、历史长一点效果立刻崩塌——模型开始忽略工具定义、重复之前的错误步骤、甚至把旧观察当成新指令。Hermes Agent Loop 的原则是上下文里只保留当前决策真正需要的信息并按优先级分配预算。我的默认分配方案大概是这样内容块预算占比说明系统提示词10%-15%角色、输出格式、安全边界写死不参与裁剪工具定义20%-30%按调用热度排序一次对话里最多呈现 8-12 个工具任务描述与执行计划10%初始任务原文 已经完成的步骤摘要最近的观察与消息剩余按时间倒序保留越近的越完整为什么要控制工具数量因为模型对工具定义的注意力会随数量增加而急剧衰减。我曾经在一份系统里挂了 30 个工具结果模型频繁调用错工具把订单查询调成订单创建事故很严重。后来按场景把工具拆成组一次循环只暴露相关的 8 个左右误调率立刻降了一个量级。思考阶段的另一个关键参数是温度。工具调用和推理决策需要稳定输出我统一用 0.2而不是默认的 0.7。低温能明显减少 JSON 格式漂移和工具名幻觉代价是回答的创造性降低但 Agent 循环里本来就不需要创造性需要的是稳定。2.2 行动阶段工具调用从生成到执行的完整链路模型输出动作之后紧接的问题是怎么把文本变成真正的函数调用。目前主流方案有两种。第一种是原生函数调用格式OpenAI 和 Anthropic 的接口都支持。模型直接返回结构化的 tool_calls不用自己解析可靠性最高。第二种是 ReAct 文本格式让模型在回答里输出 Action: tool_name\nAction Input: {...} 这样的固定格式再用正则或 JSON 解析器提取。Hermes Agent Loop 默认先走原生函数调用退化为文本格式作为兜底。不管哪种方式解析环节都必须做三层防护第一层如果模型输出的是 JSON先做容错解析——很多模型会输出带注释的 JSON、带多余逗号的 JSON标准 json.loads 会挂需要清理后重试第二层用 JSON Schema 校验参数缺字段或者类型不对时把校验错误原样返回给模型让它自己修正第三层如果解析失败超过两次就给模型返回一条格式错误的观察而不是默默跳过这个动作。我见过不少实现在这里选择静默失败整个循环就会在同一个地方反复打转白白消耗 token。工具执行的细节同样容易被忽略。每个工具调用都要有超时时间Hermes Agent Loop 的默认值是 30 秒超过直接按失败处理把 TimeoutError 当作观察返回给模型。为什么不是更长因为一个 30 秒都没返回的工具大概率是卡死而不是慢继续等只会拖垮整个循环。另外工具执行要尽可能做得幂等——同一个工具参数执行两次不应该产生两笔订单、两条记录。现实中很多工具做不到完全幂等所以我会在工具描述里明确提示此操作不可重复请确认无副作用后再执行让模型在调用前多一层自我检查。2.3 观察阶段工具结果如何变成下一步决策的输入工具执行完毕结果要回灌到上下文里成为观察。这一步的工程细节是很多人不重视的地方但它直接影响循环质量。首先是截断。工具返回内容经常很长一个网页抓下来几十万字符直接塞进上下文既烧钱又冲淡注意力。Hermes Agent Loop 对观察结果默认截断到 2000 字符并且要求工具在返回时自带摘要接口大结果尽量让工具侧先摘要模型侧只看到精炼版。截断要注意保留头部——错误信息、状态码、核心数据一般都在前面尾部通常是模板代码和广告脚本丢了不可惜。其次是归一化。不同工具返回的格式五花八门JSON、Markdown 表格、纯文本、二进制错误码。我要求所有工具统一返回字符串并附一个类型标记json/text/error。这样模型看到的观察格式稳定减少它自行猜测的负担。最后是错误信息的处理。我特别强调一点错误也是观察而且是高价值观察。很多初版 Agent 在工具抛异常后就终止循环其实完全没必要。正确的做法是把异常堆栈或错误信息格式化后返回给模型让它判断下一步。模型看到 FileNotFoundError: no such file 之后通常能自己意识到路径参数写错了然后重试。错误信息本身就是在帮助模型纠正行为把它丢掉等于掐断了循环的自我修正能力。3. 记忆与状态循环中间最容易失忆的环节3.1 工作记忆所有决策都发生在同一个窗口里Agent 的记忆在一个循环内其实就是消息历史本身。模型没有隐式记忆所有决策依据都必须出现在输入里。所以工作记忆的核心是让当前这一步决策需要的所有信息都还在窗口里而无关信息尽量少。这个听起来简单实际很容易翻车。我见过一次事故Agent 在执行一个长批量任务到了第 20 轮它已经完全忘了初始任务是什么开始自顾自地做一些完全无关的操作。原因就是历史消息太长初始任务被挤出窗口了。修复方式很朴素但有效——把初始任务原文和已完成步骤摘要固定在上下文的高优先级区位置放在观察历史之前。模型即使后面看得稀里糊涂至少不会忘记自己在干什么。另一个常见问题是观察过载。每轮工具结果都追加进历史到第 10 轮之后一条关键信息可能已经被淹没在几千条旧消息里。我的做法是给每条消息打标签系统提示词、任务描述、历史消息、工具观察分别存放组装上下文时按优先级取用而不是简单地按先后顺序拼接。这样既能保证信息完整又能让模型在每次决策时看到的是最新观察列表而不是全部聊天记录。3.2 上下文裁剪窗口不够用时的三级降级策略窗口是物理瓶颈。即使用 200K 上下文的模型过度填充也会带来两个问题费用爆炸和注意力稀释。Hermes Agent Loop 用了三级降级策略按顺序触发。第一级滑窗裁剪。保留最近 N 轮消息默认 30 轮更早的按已完成步骤摘要的形式压缩成几句话。适合大部分任务成本低语义损失小。第二级LLM 摘要。当滑窗方案丢掉的关键信息影响了任务推进时用一次额外的 LLM 调用把中间过程的核心事实提取成结构化摘要再塞回上下文。要注意这一步的输入是全部旧消息输出要控制长度我一般限制在 500 字以内。第三级外部存储。把工具执行的历史记录向量化存入持久化存储模型需要时通过记忆检索工具主动查询。这一级是真正的长期记忆也是后面要讲的跨循环状态的基础。三级策略的触发由 token 计数自动控制。我会在每次组装上下文前计算当前消息历史的 token 数超过窗口预算的 70% 就触发裁剪而不是等到 100% 才被动截断。70% 这个阈值是实测出来的因为模型输出也需要 token留出余量可以避免中途被截。3.3 跨循环的状态持久化崩溃了还能接着跑一个真实的 Agent 任务尤其是一次性处理大量文件或数据的批任务往往要跑很长时间。这种场景下循环里的状态就不能只放在内存里必须持久化到磁盘或数据库。Hermes Agent Loop 采用的方案是 checkpoint 机制每一轮循环结束时把当前的消息历史、任务原文、已完成的步骤、待办队列序列化保存下来。存储格式我用 JSON Lines路径按任务 ID 分目录每轮一个文件。这样如果进程崩溃或 API 限流导致任务中断恢复时只需加载最后一个 checkpoint从断点继续循环而不必从头开始。持久化带来的另一个收益是排查问题。当某个任务跑出离谱结果时把 checkpoint 文件按轮次回放一遍模型每一轮看到了什么、执行了什么、观察到了什么一目了然。这比事后看一份日志有用得多。后面讲可观测性的时候我还会再提到 checkpoint 这个复用思路。4. 循环怎么停下来终止条件与失败恢复4.1 四层终止条件设计循环最怕两件事该停不停和不该停乱停。Hermes Agent Loop 的终止条件设计了四层层层递进任何一层触发都会终止当前循环。第一层自然完成。模型在输出里明确返回最终答案或者调用专门的完成工具如 finish 工具循环正常结束。这一层是最常见的终止方式前提是模型有明确的结束信号——我在系统提示词里会反复强调当任务目标已经达成时必须输出最终答案而不是继续调用工具。第二层最大迭代次数。默认 12 次属于兜底防止模型陷入重复调用的死循环。第三层token 预算。整个循环消耗达到预设上限比如 50 万 token时强制终止。这层主要防的是成本失控适合在长任务里做全局刹车。第四层人工干预。循环每轮之间检查是否收到用户中断指令支持人在回路。做银行或者电商场景时关键环节必须等人工确认这时候人工干预不是兜底而是合规要求。四层顺序我建议不要反过来。自然完成最优先判断因为它是正常路径迭代上限和 token 预算是在自然完成没触发时防止失控人工干预则应该放在每轮循环之间异步检查而不是阻塞等待除非业务上强制要求同步确认。4.2 迭代上限怎么定12 这个数字是怎么来的12 不是拍脑袋定的。我把任务复杂度按工具调用次数做了个粗略分级单工具单步任务3 轮以内查资料-总结型任务搜索、抓取、整理5-8 轮需要条件判断和多工具协作的任务8-12 轮超过 12 轮还没完成的任务继续跑下去大概率不是任务难而是循环进入了错误路径模型在同一个问题上反复打转。这里有个重要判断高迭代轮数并不是越给越多越好。轮数给多了模型不会因此变得更聪明反而更容易产生长路径漂移——前面某一步的错误会在后面的循环里被当成既定事实放大。所以我的策略是默认 12如果任务确实复杂就在任务开始时显式声明这是一个需要多阶段探索的任务最多可以跑 30 轮让模型知道预算充足同时在大跨度探路场景比如让 Agent 自己研究一个陌生 API里把上限放到 30-50。迭代上限触发后的处理也很关键。直接抛异常结束任务是最差的做法。我会把已达到迭代上限当前进度如下请基于已知信息给出你目前能给出的最好结论作为提示再让模型做最后一次总结性输出。这样做即使任务没完成用户至少拿到一份有中间结果的报告而不是什么都没有。4.3 失败重试与策略回退让循环有自救能力失败在循环里不可避免关键是 Agent 怎么应对。Hermes Agent Loop 里有一层失败策略的调度逻辑三层递进。第一步单轮内重试。工具调用失败后把错误信息作为观察返回给模型让它修正参数或换一种调用方式再试。模型看到错误信息后大概率会自己调整这是最常见的自愈路径。第二步策略切换。同一工具连续失败 3 次继续让模型空转没有意义。这时我会主动干预向模型提示该工具连续失败建议换一种策略并注入替代方案提示比如如果搜索 API 不可用可以回退到数据库查询。第三步路径回退。如果整体策略都不对就回退到最近一个可靠 checkpoint把失败相关信息作为新的观察追加进去让模型重新规划。这一步等价于人类做事回到原点重新想。我特别想提一个误区不要为了显示智能而无限重试。重试是手段不是目的。每多一次重试就多一笔 token 成本、多一份模型把错误信息误解成正常结果的风险。所以我对重试的预设策略是单工具最多允许 3 次失败重试超过就换策略整个循环的失败率超过 30% 时自动缩短后续工具的调用粒度宁可多分几步也不要让一步里塞太多假设。5. 完整跑一遍一个多步骤任务在循环里的真实轨迹前面讲了不少设计这一节用一个具体案例把整个循环走一遍看看每轮到底发生了什么。5.1 任务设定与初始状态假设任务是查询某家公司在近一个月内的融资信息并输出一句话结论注明消息来源。配置给 Agent 的工具只有三个web_search(keywords) 做网页搜索、web_fetch(url) 抓取网页内容、finish(answer) 结束任务。初始状态里消息历史只有一条用户消息任务原文被单独存到高优先级字段最大迭代次数 10温度 0.2。这里的工具配置是刻意裁剪过的搜索、抓取、结束三个工具覆盖查-看-交差三个环节没有多余的选项。工具多的好处是上限高但在这个任务里三个足够少了选择歧义。5.2 逐轮拆解每一轮循环发生了什么第 1 轮模型收到任务和工具列表思考后选择调用 web_search关键词是公司名 融资 近一个月。工具返回 8 条搜索结果其中第 2 条看起来是科技媒体的报道。观察被截断到 2000 字符标题和摘要都在。模型决定跟进。第 2 轮模型调用 web_fetch抓取第 2 条结果的页面。工具成功返回了正文但正文很长超过了截断阈值观察里只保留了开头部分。前几段包含了融资金额、领投方和日期够用了。模型判断信息齐全但仍然调用了一次 web_search 做交叉验证——它搜了公司名 融资 官方来确认消息来源。第 3 轮搜索结果里出现公司官网或官方公众号的信息模型对比后发现两个来源一致于是调用 finish输出结论该公司在近期完成了新一轮融资金额约 X由 Y 领投来源为某科技媒体报道及公司官方渠道。整个任务只用了 3 轮token 消耗大概在 8000 左右。这不是巧合是工具裁剪和上下文优先级设计的结果。很多人搭的 Agent 跑类似任务动辄十几轮不是因为模型笨而是因为上下文里塞了大量无关信息模型每轮都要做很多无效思考。5.3 出错与恢复搜索无结果时 Agent 的实际表现这个案例里有一轮值得单独讲就是初始搜索如果返回空结果会怎么样。假设第 1 轮 web_search 返回空列表。这时观察是未找到结果四个字。模型看到空结果后并没有结束任务也没有傻傻地用同样的关键词重试它做了一次调整把关键词从公司名 融资 近一个月改成公司名 融资 最近新闻同时扩大时间范围不限定近一个月这个自然语言表达而是改成具体日期区间。第 2 轮搜索成功返回结果。这个调整过程完全发生在循环内部没有人工介入。关键原因有两个一是空结果被如实返回了模型没有机会在以为有结果的状态下继续二是搜索结果工具的描述里写了提示——如果返回结果为空请尝试使用不同的关键词、同义词或更宽泛的时间范围。工具描述本身就是给模型的提示这比在系统提示词里写一大段抽象规则有效得多。再假设一种情况web_fetch 抓取失败返回 HTTP 403。模型看到 403 后会基于搜索摘要里的链接换一个来源再抓。如果连续 3 次抓取都失败策略切换逻辑会提示它抓取连续失败请返回搜索结果只基于摘要中的信息形成结论并在结论中说明信息来源的局限性。于是任务以部分信息不可验证的状态收尾而不是无限重试。6. 落地期躲不开的那些坑最后这部分是运营和维护层面的问题也是真正决定 Agent 能不能在生产环境跑稳的部分。6.1 工具调用格式漂移同一个模型也会不稳定最让我头疼的坑是格式漂移。同一个模型、同一个温度昨天输出还规规矩矩的 JSON今天可能就在 function name 前面多打几个反引号或者在参数里混进一句解释文字。这不是偶发是常态。应对思路是分层容错。第一层能走原生函数调用就走原生不自己解析文本。第二层兜底解析器要写得足够宽容——剥离 Markdown 代码块标记、去除注释、修复未闭合的 JSON 引号。第三层解析失败时把解析器的原始报错返回给模型明确告诉它你的输出无法被解析错误是 XX请重新输出纯 JSON。我压箱底的做法是准备一份格式错误示范清单放在工具定义附近模型一旦格式崩了就会看到这个清单从最小几个错误类型里找答案实测恢复速度比给抽象提示快得多。另外一个细节每次模型升级都要重新做一遍工具调用链路测试。很多模型在某个版本里 JSON 能力很强换个版本就变了。我用一组覆盖所有工具类型的回归用例跑一遍如果格式空格或引号风格有变就调整兜底解析器。别小看这件事模型升级导致的 Agent 集体失灵我见过不止一次。6.2 工具执行的安全边界与超时给 Agent 接工具一定要想清楚这个工具执行时能碰什么、不能碰什么。这不是技术洁癖是事故教训。把数据库写操作、发送消息、创建订单这类有副作用的工具暴露给循环等于允许模型在无人确认的情况下执行外部动作。我的做法是分三类处理只读工具搜索、查询直接放行有副作用的工具创建、修改、删除默认要求二次确认只有在任务描述里明确授权时才自动执行对外发送类工具一律强制人工确认。这套分类直接写进系统提示词和工具描述让模型在调用前就知道边界。超时方面除了前面说的单工具 30 秒超时还要把整个循环的最大运行时长纳入考虑。批处理任务里一个 Agent 实例跑 30 分钟并不可怕可怕的是 10 个实例同时卡住资源被占满。我在执行器层加了信号量限制并发 Agent 实例数同时给每个循环实例设置统一的超时信号超时后强制序列化当前 checkpoint 并终止循环。这样既不会丢进度也不会拖垮服务。6.3 日志、追踪与成本监控运营一个循环的必要基础最后讲可观测性。Agent 循环的执行日志和普通 API 日志完全是两回事——同样一次运行里面嵌套了多轮模型调用和工具调用只看一层是看不懂的。我每轮循环都打这些指标轮次号、模型输入 token 数、输出 token 数、调用的工具、工具耗时、是否成功、观察截断后的长度。同时用 trace 把模型思考→工具动作→观察回灌串成一个 span这样排查问题时能按任务 ID 拉出完整链路。成本监控也在这里做按任务维度累计 token 花费超过预算时告警。我还开发了一个最笨但最有效的方式checkpoint 回放。就像前面讲的每个 checkpoint 保存了那一轮的完整上下文和模型输出排查问题时按轮次一页页翻可以看到模型是在哪一轮开始偏离的。很多框架都提供 debug 模式但自己实现过这个回放功能后我对循环的理解比用任何框架都深。如果让我只保留一个调试工具我肯定选 checkpoint 回放。这里也有个平衡日志要全面到能复现问题又不能全面到拖垮性能。我的原则是文本日志只记元信息和摘要完整请求响应体存到对象存储里默认保留 30 天。排查问题时再去取完整体日常运行只扫元信息就够了。这样既保留了完整证据链又不会让日志系统先于 Agent 崩溃。