
1. 从能跑到能扛Agent Loop 工程化的分水岭在哪很多人第一次接触 Agent Loop 这个概念是在某个深夜调通了一个 ReAct 循环——模型思考、调用工具、拿到结果、再思考循环几轮之后任务完成了。那一刻确实很爽但接下来发生的事情往往更让人印象深刻换个任务就崩了多跑几轮就死循环了并发一上来 token 成本直接失控日志里全是我不知道该怎么继续。这就是 Demo 和工程之间的分水岭。Agent Loop 本身不复杂它的核心逻辑用几十行代码就能写出来维护一个消息历史让模型决定下一步动作执行动作把结果塞回历史判断是否终止。真正难的是让这个循环在真实业务里稳定、可控、可观测、可回滚。而 Graph Engineer 这个角色本质上就是来解决这个问题的——把 Agent 的行为从一段会自己转的代码升级成一张可设计、可调试、可演进的图。我自己的体会是Agent Loop 的工程化实战核心矛盾集中在三个地方循环的终止条件怎么设计才不失控、状态在循环中怎么管理才不丢不乱、整条链路怎么观测才能在出问题时快速定位。这三个问题不解决Agent 永远只能停在玩具阶段。下面我会围绕这三点结合 Graph Engineer 的思路把我在实际项目里踩过的坑和总结出来的做法完整拆一遍。这篇文章适合两类人一类是已经写过基础 Agent Loop、但被稳定性和成本问题折磨的开发者另一类是正在评估要不要引入图结构来管理 Agent 流程的技术负责人。如果你还在纠结要不要用 Agent那可能先补一下基础概念会更顺。但只要你已经跑通过至少一个循环这里的内容应该能直接对上你的痛点。2. Agent Loop 的三种典型失控方式与根因拆解2.1 无限循环模型在再试一次里出不来最常见的失控就是死循环。表现是 Agent 反复调用同一个工具、反复得到相似结果、反复决定再试一次。我见过最夸张的一次一个查询类 Agent 在数据库返回空结果后连续重试了四十多次每次都是同样的查询语句直到把 token 预算烧穿才停。根因其实不在模型笨而在于循环里缺少显式的状态推进判断。ReAct 式的循环默认每一轮都是新的一轮模型看不到我已经试过这条路了这个事实。它每次拿到的上下文里历史消息虽然都在但模型对重复的敏感度很低尤其是当工具返回的结果格式相似时它很容易把又一次空结果理解成还没试够。工程上的解法不是简单加一个最多循环 N 次的硬上限——那只是兜底不是治理。真正的做法是引入循环指纹每一轮把当前决策 工具名 关键参数哈希成一个指纹存进一个集合里。下一轮如果生成了相同指纹直接判定为重复路径强制触发终止或切换策略。这个机制我在多个项目里验证过能把大部分死循环在第二轮就掐掉。2.2 状态漂移跑到第五轮Agent 忘了自己要干嘛第二种失控更隐蔽Agent 没有死循环每一轮都在做看起来合理的事但整体方向已经偏了。比如一个帮我整理竞品资料的任务跑到后面变成了在反复润色某一段文字完全忘了最初的目标是整理而不是润色。这个问题的根因是长上下文里的目标衰减。随着消息历史变长最初的任务描述被淹没在大量的工具返回和中间推理里模型对原始目标的注意力权重下降。这不是模型能力问题是上下文工程的必然结果。我的处理方式是在循环里维护一个目标锚点每一轮在构造 prompt 时把原始任务描述单独拎出来放在一个固定位置通常是系统消息之后、历史消息之前并且用明确的分隔标记。更激进一点的做法是每一轮都让模型先复述一次当前子目标是什么再决定动作。这个复述动作看起来浪费 token但它能显著降低漂移率实测下来在长任务里非常值。2.3 成本爆炸循环轮数和上下文长度是乘法关系第三种失控是成本。很多人算 Agent 成本时只算单次调用多少钱忽略了循环场景下成本是轮数乘以每轮上下文长度。而上下文长度本身又随轮数线性增长所以实际成本接近轮数的平方级增长。我做过一个粗略测算一个每轮平均 2000 token 上下文的 Agent跑 10 轮大概消耗 2 万 token 左右考虑累积但如果失控跑到 30 轮消耗会飙到 15 万 token 以上。差距不是三倍是七八倍。这就是为什么加个上限虽然粗暴但确实是必要的兜底。更精细的做法是分层预算给整个任务设一个总预算给每一轮设一个单轮预算再给重试单独设一个子预算。任何一层超了都触发降级或终止。这样即使某一轮突然上下文膨胀也不会把整个任务的预算一次性吃掉。失控类型典型表现根因工程解法无限循环重复调用同一工具缺少状态推进判断循环指纹去重状态漂移偏离原始目标长上下文目标衰减目标锚点 每轮复述成本爆炸token 消耗失控轮数与上下文乘法增长分层预算 硬上限3. 用图结构重新表达 AgentGraph Engineer 的核心思路3.1 为什么图比循环更适合表达复杂 Agent循环是一个线性结构它天然假设下一步只有一个方向。但真实任务里Agent 的决策往往是分支的查到了资料走一条路没查到走另一条工具报错走重试重试失败走降级。用循环表达这些分支代码里就会堆满 if-else最后变成一坨没人敢改的意大利面。图结构的价值在于它把状态和转移显式化了。每个节点是一个明确的状态比如解析任务调用搜索校验结果生成回答每条边是一个明确的转移条件。这样做的好处有三个可视化能画出流程图团队沟通成本骤降、可测试每个节点可以单独测、可演进加一个分支就是加一条边不用动核心逻辑。Graph Engineer 这个角色说白了就是设计这张图的人。他要决定有哪些节点、节点之间怎么连、什么条件下走哪条边、状态在节点间怎么传递。这比写循环代码更接近架构设计而不是写逻辑。3.2 节点设计把思考和行动拆开在图结构里我强烈建议把思考和行动拆成两类节点。思考节点负责让模型做决策下一步做什么行动节点负责执行具体操作调工具、查数据。拆开的好处是思考节点可以复用不同任务可能用同一个决策逻辑行动节点可以独立测试不用每次都跑模型。具体来说一个典型的图可能长这样入口节点接收任务进入规划节点生成子任务列表然后进入执行节点逐个处理子任务每个子任务内部可能又有决策-行动-校验的小循环。校验节点判断结果是否达标达标走汇总节点不达标走重试节点重试超过阈值走降级节点。这种拆法的一个关键收益是可观测性。因为每个节点是独立的你可以在每个节点入口和出口打点记录进入时间、耗时、输入输出摘要。出问题时你能直接看到是哪个节点卡住了而不是面对一坨循环日志发呆。3.3 边设计条件转移比无条件转移可靠得多边是图里最容易被忽视的部分。很多人设计图时节点画得很细但边全是无条件的执行完 A 就执行 B。这等于把循环里的隐式假设搬到了图里问题一点没解决。好的边设计应该是条件驱动的。每条边都带一个判断函数返回 true 才走。比如校验节点到汇总节点的边条件是结果完整度大于阈值到重试节点的边条件是结果完整度小于阈值且重试次数小于上限。这样整张图的行为就是确定的、可预测的。我自己的经验是边上的条件函数要尽量简单、纯函数化不要在里面做复杂计算或 IO。条件函数越简单图的行为越容易推理。复杂的判断逻辑应该放在节点内部节点输出一个明确的决策标签边只根据标签做路由。4. 状态管理Agent Loop 里最容易翻车的地方4.1 状态该存什么、不该存什么状态管理是 Agent 工程化里最考验功力的部分。存少了模型没有足够上下文做决策存多了token 成本爆炸且模型注意力被稀释。我的原则是存决策必需的不存过程性的。具体来说必须存的是原始任务描述、当前子目标、已完成的子任务摘要、关键工具返回的精炼结果、当前重试计数。不该存的是完整的工具原始返回除非必要、模型的完整推理过程可以存摘要、已经处理完的中间态。这里有个反直觉的点模型的推理过程thought其实不建议全量保留。很多人觉得 thought 是宝贵信息全存下来。但实际上历史 thought 对后续决策的帮助有限反而会占用大量上下文。我的做法是只保留最近一两轮的 thought更早的压缩成一句话摘要。4.2 状态在节点间怎么传递才不丢图结构里状态传递有两种模式全局状态和边传递。全局状态是一个共享对象所有节点读写同一个边传递是每个节点输出一个结果通过边传给下一个节点。全局状态的好处是方便坏处是容易产生隐式依赖——节点 A 改了状态里的某个字段节点 B 依赖这个字段但这条依赖在图上完全看不出来。边传递的好处是显式每个节点的输入输出都清清楚楚坏处是状态多了之后参数列表会很长。我的折中方案是核心状态用全局对象但每个节点声明自己读哪些字段、写哪些字段。这样既保留了全局状态的便利又通过声明让依赖显式化。更进一步可以在运行时校验如果某个节点写了它没声明的字段直接报错。这个约束在团队协作里特别有用能防止有人偷偷改状态导致别人踩坑。4.3 状态快照与回滚出问题时能回到上一个好状态Agent 跑长任务时中途出错是常态。如果没有状态快照出错就只能从头再来成本极高。我的做法是在每个关键节点执行前打一个状态快照快照存的是状态的深拷贝或者用不可变数据结构天然支持快照。有了快照回滚就很简单出错时找到最近一个健康的快照从那里重新执行。这在重试场景里特别有用——不是从任务开头重试而是从出错节点重试前面已经完成的工作不浪费。这里有个细节要注意快照不能太频繁也不能太稀疏。太频繁存储和拷贝开销大太稀疏回滚粒度粗浪费多。我的经验是按节点打快照因为节点本身就是逻辑边界回滚到节点入口语义清晰。5. 可观测性建设让 Agent 的每一步都能被追问5.1 日志之外Agent 需要的是轨迹而不是日志传统日志是线性的文本流适合排查单点问题。但 Agent 的问题是过程性的——你要看的不是某一行日志而是从任务开始到当前Agent 经历了哪些决策、走了哪些分支、每步耗时多少。这需要的是轨迹trace而不是日志。轨迹的核心是结构化的每个节点是一个 spanspan 有开始时间、结束时间、输入、输出、状态变更。span 之间通过父子关系或前后关系连接形成一棵树或一张图。有了这个结构你就能回答这个任务为什么慢哪个 span 耗时最长、为什么错哪个 span 输出异常、为什么贵哪个 span token 消耗最多。我用的方案是基于 OpenTelemetry 的思路每个节点执行时创建一个 span把关键信息作为 span 的属性。这样轨迹可以直接接入现有的可观测性平台不用自己造轮子。5.2 关键指标轮数、耗时、token、成功率轨迹是定性的指标是定量的。Agent 工程化必须盯的几个指标平均轮数反映任务复杂度、P95 耗时反映长尾问题、单任务 token 消耗反映成本、任务成功率反映整体健康度、重试率反映稳定性。这些指标要按任务类型、按节点、按时间段分别统计。比如你可能会发现某类任务的轮数突然上升那大概率是某个工具返回格式变了导致模型反复重试。这种问题不看指标很难发现因为单次任务看起来成功了只是多跑了几轮。5.3 回放能力把一次失败的任务完整重演最有价值的可观测性能力是回放。给定一次失败任务的轨迹能够完整重演它的每一步当时的状态是什么、模型看到了什么 prompt、输出了什么、工具返回了什么。有了回放排查问题就从猜变成了看。回放的实现依赖前面说的状态快照和轨迹记录。每个节点的输入输出都存下来回放时按顺序重放即可。注意回放不一定要真的重新调用模型和工具可以用记录的结果模拟执行这样回放成本几乎为零。我在项目里把回放做成了一个内部工具任何人发现异常任务一键回放几分钟就能定位到问题节点。这个工具上线后Agent 相关问题的平均排查时间从几小时降到了十几分钟。6. 实战中的取舍什么时候该上 Graph什么时候循环就够了6.1 判断标准分支数量与状态复杂度不是所有 Agent 都需要图结构。我的判断标准很简单如果任务的分支数量超过三个或者状态字段超过五个就该考虑上图了。低于这个复杂度循环加几个 if-else 完全够用上图反而是过度设计。分支数量好理解状态复杂度指的是需要在多轮之间保持一致的字段有多少。如果只有当前任务和历史结果两个字段循环足够如果有子任务列表每个子任务的完成状态重试计数降级标记等一堆字段循环里的状态管理会迅速失控这时候图结构的显式状态管理就是刚需。6.2 迁移路径从循环到图的渐进式改造如果你已经有一个跑着的循环 Agent不建议推倒重来。我的做法是渐进式改造先把循环里的决策和行动拆成两个函数然后把这两个函数包装成两个节点用最简单的图两个节点一条边跑起来。跑通之后再把循环里的分支逻辑逐步抽成条件边。这个过程可以分多次迭代每次只改一小块保证系统始终可用。我见过有人一上来就重写成大图结果调试了两周还没跑通最后又退回循环。渐进式改造虽然慢但风险低得多。6.3 团队协作图是沟通工具不只是执行结构最后说一个容易被忽视的价值图是团队沟通工具。循环代码只有写的人看得懂但一张图产品、测试、运营都能看懂。当产品说这个任务应该先查资料再回答你可以直接指着图说这里加一条边就行沟通成本大幅降低。我在团队里推行图结构之后最明显的变化是需求评审效率提升了。以前讨论 Agent 行为大家各说各的因为脑子里想的流程不一样现在对着同一张图讨论分歧点一目了然。这个收益甚至比技术上的稳定性提升更让我意外。7. 几个我踩过的坑和对应的处理方式第一个坑是过度依赖模型做路由。早期我让模型在每个节点决定下一步去哪个节点结果模型经常选错而且选错之后很难纠正。后来改成模型只输出决策标签路由由代码根据标签决定稳定性立刻上来了。模型擅长做判断不擅长做精确的路由控制这个分工要明确。第二个坑是状态快照存了引用而不是拷贝。有一次回滚之后发现状态还是错的排查半天才发现快照存的是对象引用后续节点修改状态时把快照也改了。这个坑很隐蔽因为大部分时候不触发只有回滚时才暴露。后来统一改成深拷贝问题消失。第三个坑是重试没有区分错误类型。一开始所有错误都重试结果遇到参数错误这种重试一万次也没用的错误白白烧钱。后来把错误分成可重试网络超时、限流和不可重试参数错误、权限不足只对可重试的做重试成本立刻降下来。第四个坑是日志打得太细反而没用。早期每个节点的每个字段变更都打日志结果日志量巨大真出问题时根本找不到关键信息。后来改成只打决策点和状态变更点日志量降了九成可读性反而上去了。这些坑的共同点是它们都不是技术难题而是工程判断问题。Agent Loop 的工程化难点从来不在能不能实现而在怎么设计才不给自己挖坑。Graph Engineer 的价值也正在于把这些判断显式化、结构化让整个系统可推理、可维护、可演进。