去年下半年圈子里开始流传一句话Loop Engineering 已经死了。我一开始以为又是标题党等自己在一个多轮工具调用的 Agent 项目里被 while True 折磨了三周才真正理解了这句话指向的问题。越来越多的团队正在把大模型应用的编排方式从“循环”换成另一种更可控的范式——Graph Engineering。这里说的图工程不是社交网络图分析也不是知识图谱而是把“有向图”作为大模型应用的编排骨架节点是操作边是流转规则状态是节点之间传递的数据契约。这篇文章不打算站队说谁取代谁只想把我观察到的、实践过的两种范式的分界点、迁移经验和踩坑记录摊开来讲清楚。适合正在做 Agent 编排、或者已经在纠结要不要迁移现有代码的人。1. 从一个“while True”开始循环工程为何成为默认范式1.1 循环是直觉的产物技术进步挤出来的淤泥早期做 LLM 应用的时候大家的第一个直觉都是调用一次模型不够就再调一次。尤其是当模型需要“先查资料再回答”或者“先调工具再总结”的时候你没有办法在一个 Prompt 里预先穷尽所有步骤于是很自然地写出了类似这样的结构while not done and step MAX_STEP: step 1 response call_llm(messages) if has_tool_call(response): messages.append(run_tool(response.tool_call)) else: done True我把这类工程统称为“循环工程”。它的核心假设是模型自身是一个全局状态机只要把上下文喂回去它就能在下一轮自动决定该做什么。你不需要显式地告诉系统“这一步结束后下一步应该去哪”模型会从对话历史里推断出来。这种模式之所以成为默认思路因为它确实符合直觉。你让一个实习生去写工具调用 Agent他大概率也会写一个 while 循环你让一个后端工程师去接入模型他对 if-else 的信任远高于对状态机的信任。加上早期的 Demo 普遍只有两三个步骤循环的缺陷根本暴露不出来看起来非常顺滑。1.2 循环模式的三副常见面孔站在今天的视角回看循环工程里反复出现的模式其实就那么几种都是“用迭代逼近目标”的思路。第一种是重试型循环。模型输出经常不稳定比如要求返回 JSON 却夹杂了 Markdown或者字段名变了。处理办法就是解析失败后把错误信息拼进 Prompt让模型再改一遍。改到成功或者步数用尽为止。这种循环最常用但也是最容易被滥用的一种。第二种是推理型循环。仿照“思考、行动、观察”的链路让模型每轮先想一步、做一步然后观察结果继续思考。很多推理任务被证实确实需要这种多步展开才能得到正确答案所以很多人把步数当成超参去调大却没想到步数变大之后上下文污染和成本问题会同步放大。第三种是反思型循环。生成器输出初稿评估器给出修改建议再回到生成器重写。视觉、文案、代码生成类任务里都有这种影子。它的好处是能让输出质量上一个台阶坏处是评估标准如果写得不够硬循环会在两种风格之间来回横跳始终不收敛。基于我观察到的很多团队实践可以给这些模式归一个类循环工程把控制权托付给了模型的“临场发挥”一旦模型在某一轮忘了该收敛你就只能靠步数上限兜底。下一节我想重点聊这个兜底为什么靠不住。2. 循环的失控面步数上限、上下文污染和不可复现2.1 步数上限只是止损不是终止保障很多团队在循环工程里最自信的配置是那个 MAX_STEP。我听过一句话“我设了最大步数所以不会死循环。”这句话对了一半——它确实防止了进程卡死但它没有保证结果正确。步数上限更像保险丝熔断的意义不是让电路继续工作而是告诉你哪里出了问题但在 Agent 场景里你往往收不到清晰的熔断信号只会在监控里看到一批“步数耗尽”的异常记录。我在模拟项目 X 里遇到过一个典型案例客服 Agent 需要判断用户意图、查订单、偶尔升级人工。某个用户问了句“你们周六发货吗”模型在“查物流规则”和“直接回答”之间反复摇摆连续五轮都先选了调工具每次工具都返回同一个无差别结果然后模型又觉得信息不够又去调了一次。系统最后靠步数上限把它掐断但用户最终看到的是一个“系统繁忙”的兜底回复。这类问题不是模型智商不够而是循环本身不区分“同一动作重复执行”和“推进新步骤”。在人类看来已经显而易见的振荡模型在自回归生成时未必能察觉。如果你要求它在第 N 轮主动说“我刚才已经查过这个问题了”它大概率会因为上下文堆叠而忘记自己第一轮做过什么。2.2 上下文污染每一轮都在给自己加噪声循环工程最隐蔽的成本是上下文膨胀。每多一轮你就把上一轮的工具输出、中间分析、甚至错误信息全部追加进消息列表。表面上只是多个几百个 token实际上模型需要从越来越长的历史里去分辨“哪些信息已经过时、哪些结论已被推翻”。这就像开会时每个发言人都把前一个人的话完整复述一遍再开始讲自己的观点开到第五轮全场已经没人记得原始议题了。模型的表现通常会随着轮次增加先升后降前两轮还能保持高准确率四轮以上的回答开始掺入早前工具调用的残留表述甚至把错误消息当作事实引用。我在做日志抓取时发现那些长时间运行的循环 Agent其 Prompt 里真正有用的核心指令占比常常跌到 5% 以下。大部分 token 都在给模型交代历史而不是给模型当前任务。你以为是上下文增强其实是上下文污染。2.3 不可复现调试变成对着黑盒猜谜循环工程的另一个大问题是同一个输入不一定产生同一条执行路径。因为模型有随机性可能这一轮先调了检索工具下一轮先调了计算器。你回看日志发现结果不对想定位是哪一步引入的错误却很难复现当时的中间状态。传统后端调试可以断点、可以打日志、可以按固定调用链回放。循环工程里调用链是模型现场生成的你没有办法把一条“思考轨迹”冻结下来重新从中间某一步继续跑。你只能重放整个流程期待模型这次还走原来那条路——但往往不复现。为了解决这个问题很多团队最后会偷偷把 temperature 调到 0指望输出稳定。但这只是治标模型在长上下文下即使 temperature 为 0也可能因为 token 采样细节不同而走偏。真正的问题在于循环把本该由代码控制的“下一步去哪”交给了模型文本生成而文本生成天然不具备路径确定性。3. 状态机思维图工程真正改掉的底层假设3.1 从“模型决定下一步”到“图结构决定下一步”Graph Engineering 和循环工程表面上的区别是“用图替代循环”但底层假设完全变了。循环工程假设“模型是个黑盒状态机对话历史就是状态”图工程假设“模型只是图上一个节点它负责完成局部计算不负责全局路由”。全局路由被显式建模成一张图包含节点、边和状态容器。节点是一个有限的计算单位可以是一次 LLM 调用、一个工具函数、一段规则判断也可以是一个人工审批动作。边是节点之间的合法转换关系可以带条件。状态则是横跨所有节点的一个显式数据快照比如当前累计了多少上下文、已经调用了哪些工具、还有哪些信息缺失。这个数据对象由开发者定义而不是由模型的输出临时拼接。把这三样东西建模好之后Agent 的执行就不再是黑盒的“对话轮次推进”而是一条可以被外部观察和干预的显式路径。你可以在任意两个节点之间插入一个新节点而不用强迫模型理解新流程你也可以在一个节点失败时把状态快照拿出来看看数据到底在哪个环节变了形。3.2 图的表达能力串行、并行、条件、循环全都能表示有工程经验的朋友可能会反问图形结构是不是为了可控性牺牲了表达力我和团队刚接触图工程时也有这个疑虑后来发现恰恰相反。有向图加上判断边几乎可以表达传统程序里的所有控制流串行就是一条没有分叉的链并行就是多个节点从同一父节点分叉出去再汇聚条件分支就是边上挂一个谓词函数循环则可以建模成一条“走回去的边”或者抽象成一个内部循环节点。更重要的是图的表达力是“可编辑的”。你想让流程在某些分支上多绕一圈直接改边的目标节点就行不用去改 Prompt 说“如果没查到请再试一次”。这种可编辑性在生产环境里非常珍贵因为它让流程变更从“调 Prompt 然后祈祷”变成了“改代码然后测试”。我在前面提到的模拟项目 X 里最直观的感受是循环工程时期改动一个路由逻辑需要写一大段指令来约束模型然后观察好几个 case 才能确认生效图工程时期改动只是把一条边的条件函数从“大于 0.5”改成“大于 0.7”立刻就有一个明确的触发范围。3.3 状态设计的核心每次转换提交一个契约图工程的实现细节千差万别但有一个共同要求每个节点的输入和输出必须有明确契约。这个契约通常就是状态对象的字段变化。一个节点运行前需要拿到什么字段运行后必须写入哪些字段都要提前在状态定义里说清楚。回顾我们的实践定义状态这一步最容易犯的错误是“图省事把整个上下文塞进一个字段”。这等于把一个循环工程的核心毛病——上下文膨胀——原封不动搬进了图工程。正确的做法是把状态拆细用户原始输入放一个字段工具结果放一个字段中间推理过程放另一个字段并标记哪些字段在每个节点结束时允许保留和清理。这一步可能会让初看 Graph Engineering 的人觉得繁琐但它是后续所有调试、测试、监控的基础。只有状态结构是显式的你才能在某个节点失败时快速判断是上游没写对字段还是下游读错了字段。下一节我会用一个完整的重构案例把状态设计的每个细节摊开来讲。4. 实战迁移把一套客服编排从循环重构为有向无环图4.1 循环版的客服 Agent 为什么难收场为了不涉及真实项目隐私我这里用模拟项目 X 来说明一个需要做意图分类、订单检索、人工升级的客服 Agent。循环版本大概长这样def run(query): messages [system_prompt, user_message(query)] step 0 while not done and step 6: step 1 response call_llm(messages) if response.tool_call: tool_result call_tool(response.tool_call.name, response.tool_call.args) messages.append(tool_result) continue if response.should_escalate: messages.append(请转人工) notify_agent(query, messages) return 人工坐席即将介入 if response.final_answer: return response.final_answer messages.append(请继续补充信息并给出最终回复)这段代码的问题在真实故障中暴露得很彻底工具调用连续失败时错误文本会被反复送进模型模型既不会主动放弃也不会改走人工通道订单检索成功后模型可能因为上下文太长而忽略检索结果凭空给出一个泛泛的答复。最关键的是当我们需要插入一个“先验证用户身份再查询订单”的中间步骤时得去修改一大段系统提示词而效果不可控。4.2 图版本的状态定义字段、角色、生命周期重构的第一步是定义状态。我们不再用“整个消息列表”作为隐性状态而是设计了几个有明确用途的字段class AgentState: user_query: str # 用户原始输入只读 intent: str # 意图分类结果由意图节点写入 order_detail: str # 订单检索结果由检索节点写入 authentication_ok: bool # 身份验证是否通过由验证节点写入 should_escalate: bool # 是否需要升级人工 final_answer: str # 最终回复每个字段的“写权限”被限定在特定节点避免所有节点都往同一个 context 里塞东西。这个设计模仿了传统后端接口的参数校验让运行时能在早期发现字段缺失或类型意外。4.3 图版本的节点与边把模型降级为普通执行单元有了状态定义节点就变得很朴素。以意图分类节点为例class IntentNode(Node): def run(self, state: AgentState) - AgentState: intent classify(state.user_query) state.intent intent if intent 需要身份验证: state.next verify_user elif intent 查订单: state.next query_order else: state.next final_answer return state边不写在节点内部而是由图的构建代码统一声明g Graph(AgentState) g.add_node(intent, IntentNode()) g.add_node(verify_user, VerifyUserNode()) g.add_node(query_order, QueryOrderNode()) g.add_node(final_answer, FinalAnswerNode()) g.add_node(human_review, HumanReviewNode()) g.add_edge(intent, verify_user, conditionlambda s: s.next verify_user) g.add_edge(intent, query_order, conditionlambda s: s.next query_order) g.add_edge(intent, final_answer, conditionlambda s: s.next final_answer) g.add_edge(verify_user, query_order, conditionlambda s: s.authentication_ok) g.add_edge(query_order, final_answer, conditionlambda s: s.order_detail) g.add_edge(verify_user, human_review, conditionlambda s: not s.authentication_ok)你会发现路由逻辑不再依赖“模型在第几轮想起了什么”而是依赖状态字段的显式判断。模型的作用被限制在每个节点内的局部理解上它不需要反复从历史里判断“我是不是该转人工了”。4.4 重构后的指标变化调用次数、延迟和可追踪性迁移完成后我们记录了前后两周的指标对比如下示意值真实数据因业务而异指标循环版图版平均模型调用次数3.8 次1.9 次P95 响应延迟6.2 秒3.4 秒单次对话峰值 Token42002100工具重试导致的无效调用约 18%约 3%失败后可恢复断点续跑不支持支持数字下降的原因很简单原先模型第 2 轮调工具失败会在循环里反复重试图版则在“工具节点”旁增加了一个轻量级的校验子节点校验失败直接走异常分支回退不再整个循环重跑。模型调用次数几乎砍了一半延迟自然跟着降下来。5. 图工程下的质量基础设施从单测到断点续跑5.1 状态可构造意味着每个节点都能独立测试图工程给我带来的最大红利不是运行速度快而是终于能像测普通函数一样测 Agent 了。因为状态对象是显式的测试时可以手工构造一个中间状态喂给任意一个节点断言它返回的状态字段是否符合预期。比如测试“身份验证不通过”分支不再需要先真的跑一遍对话、让模型把问题引到身份验证上而是直接构造一个 authentication_okFalse 的状态调用节点验证逻辑。这一步让我对在线流程的稳定性有了实打实的信心而不是靠“多测几条 prompt”来祈祷。为了保证测试质量我建议把“状态契约”纳入代码评审范围。未在状态定义中出现的字段不允许被节点写入节点声明要读取的状态必须在运行前被上游明确写入。这听起来像普通接口设计但很多人做 Agent 时却忘了坚持这套方法论。5.2 中间可观测性回放一条执行路径变成现实图版执行时每个节点的开始时间、结束时间、状态快照都可以被天然记录下来。遇到线上问题你可以把整条路径拿出来逐节点回放定位到具体是哪一个节点产生了错误输出而不是对着一个长对话日志猜模型“当时在想什么”。在循环版里我们只能记录 messages 数组的增量很难说清楚“哪一次工具调用是错误路由造成的”在图版里每一条边被触发的原因都写在条件函数中监控系统可以直接展示某条请求走了哪个分支把案例的归因分析时间从半天压缩到十几分钟。5.3 人工审批节点图工程独一无二的优势客服升级、转账确认、内容审核这类场景里流程中必须停留等待人来裁决。循环工程处理这种阶段通常会用一个“阻塞调用”把整个线程挂起等人工操作完成才能继续。这个实现不仅笨重而且中途想加一个“超时自动降级”逻辑时又得去折腾异步任务。图工程天然支持把“人工审批”建模成一个状态特殊的节点执行到该节点时暂停流程图外部系统通过回调或轮询更新状态确认通过后流程继续。更重要的是你可以在该节点的出边上挂多个条件审批通过走 A 分支拒绝走 B 分支超时走 C 分支。这种精确的路由控制是循环很难优雅表达的。基于我在多个项目中的观察凡是涉及人工审核的 Agent 流程最终都会倒向图工程。6. 循环最好的归宿是图里的一条可编辑的边6.1 循环没有死它只是被降级成了局部策略说了这么多图的优势我还是得给循环“平反”。Loop Engineering 真正的死因不是循环这个语法有问题而是“让模型在全局流程层面通过循环自行摸索”的做法不可持续。循环本身作为一种局部策略不仅没有死反而应该被大量使用。最典型的是工具调用重试循环。你把一个“最多重试 3 次、每次指数退避”的小循环封装成一个工具节点放到图里。这样循环的边界被牢牢锁在节点内部外部永远看不见它反复尝试的过程一旦重试耗尽节点返回一个明确错误由图的边决定走降级分支还是人工分支。这个设计既保留了循环的灵活性又避免了全局失控。另一个常见位置是结构化输出修复循环。模型返回的 JSON 不合规时在节点内部做小循环修正直到通过 schema 校验或达到次数上限。这类循环最怕的是把错误信息一路带进全局状态所以在节点出口处只暴露“解析后的结构体”或“失败标识”不暴露中间的错误堆栈。6.2 选型判断什么场景别强行换图那么什么样的项目应该尽早引入图工程我很粗略地给一个判断方法项目特征推荐范式流程固定、分支少、模型单次输出直接可用循环足以应对需要工具调用、有中间状态、可能有分支回退图工程收益明显涉及人工审批、审计要求、断点续跑必须图工程完全开放式的创意探索、路径不可预知循环更合适我参与过一个纯创意写作 Demo它的流程只有“写初稿、自评、改稿”三个节点用循环写三天就能跑通强行换成图版反而增加维护成本。图工程的价值随着流程复杂度的增加而放大但不要为了赶时髦去给简单任务加结构。6.3 我踩过两个坑提前帮你排掉第一个坑是过度抽象。刚接触图工程时我觉得每个判断都需要一个节点于是把“判断意图”和“判断是否转人工”拆成了四个节点。结果图变得很碎真正的路由逻辑散落在十几个条件函数里调试时比循环还累。后来总结的经验是节点粒度应该对齐“业务上需要单独观察或单独重跑的最小单位”而不是对齐认知里的每一步。第二个坑是状态字段命名随意。图工程里状态是所有节点的公共契约命名不一致会导致节点之间互相读错字段。我们后来给状态字段加了前缀约定输入类字段用 in_ 开头输出类字段用 out_ 开头临时数据放入 temp_。这让跨节点排查时一眼就能看出数据流方向强烈建议接手新项目时先统一这套约定。最后再分享一个我的个人体会真正决定范式优劣的不是“循环”还是“图”这两个词而是控制权放在哪里。模型擅长的是在局部给出语义判断工程系统擅长的是在全局保证确定性和可观测性。graph 就是把两者的分工制度化关键的下一步决策由边和状态来定模型只在节点里发挥它该发挥的理解力。如果你现在正被循环里那些反复横跳的 Agent 搞得焦头烂额不妨照着上面的思路先定义状态再把主流程画成图哪怕不做完整重构只把最痛的两三个分支抽出来建图也能立刻体会到那种“终于握住方向盘”的感觉。