资讯详情

为什么我拆掉了Agent框架里的while循环?DAG架构的实践与思考

📅 2026/10/7 13:41:58 | 华诺云谱 👁 阅读
为什么我拆掉了Agent框架里的while循环?DAG架构的实践与思考
如果你写过或者读过几份 Agent 框架的源码大概率会看到同一个影子一个while循环。不管是while(true)还是带max_iterations的for循环内核都是同一套逻辑——让大模型反复“观察、思考、行动”直到它自己觉得任务完成。这个模式已经被写进太多框架里以至于很少有人怀疑它是不是唯一答案。我动这个念头是在一次生产事故之后。一个 Agent 在某个迭代里反复调用同一个查询工具因为它在等一个根本不会出现的前置条件而停止条件又被大模型自己掌握于是它一边烧 token 一边对我的日志输出无意义的中间结果。当时我盯着滚动日志脑子里冒出一个问题为什么让一个概率模型来决定循环何时终止为什么不直接把循环从架构里拿掉这篇文章不是理论探讨。我把我实现的架构、踩过的坑、实测的数据和适用边界都写出来。如果你正在设计自己的 Agent 框架或者想给现有的while循环换一个更可控的骨架这篇文章应该能给你一个不一样的视角。1. 传统主循环的问题那个 while true 为什么总在错误的时间停在错误的地方1.1 循环的本质是让 LLM 兼职做流程控制先看最典型的 Agent 主循环长什么样我用伪代码表示这个形态你在几乎所有开源框架里都能找到state init_state(task) while not done(state): observation observe(state) # 读取环境或工具结果 thought llm.think(state, observation) # 大模型推理 action llm.decide(thought) # 大模型选择动作 state execute(action, observation) # 执行动作更新状态 done llm.should_stop(state) # 让大模型判断是否结束这个结构看起来顺理成章但它有一个隐蔽的前提假设大模型既当运动员又当裁判。它既要负责完成任务推理又要负责判断“我做得够不够好、该不该停下来”。我在实际使用中发现的第一个问题就是should_stop这个判断极不稳定。模型在长上下文中容易陷入“过度自信”或“自我怀疑”前者导致任务没完成就提前退出后者导致反复执行同一类动作永远不收敛。你可以在日志里看到模型自言自语“我已经完成了大部分我认为可以通过了”然后生成一段残缺的结果给你——它把停止条件当成了敷衍出口。1.2 循环把计算路径变成了一条看不见的辫子while循环带来的第二个问题更隐蔽它把 Agent 的行为折叠成一条动态的、无法预测的控制流。每次循环模型都可能选择新的工具、改变路径、插入新的子目标。架构层面完全没有静态视图可用。这意味着什么意味着你没有办法回答一个最基本的运维问题当前这个 Agent 到底处于哪个阶段它是在数据采集、是分析、还是生成了结论你只能通过日志文本去“猜”。更糟的是一旦中途失败你没法从某个明确的位置恢复——你只能从头再来因为循环中每一步的路径都是模型临时“编”出来的不具有可恢复的结构性。我做过一次简单的故障分析某次任务的 Agent 跑了 19 次循环才停下来其中 12 次都在反复修正同一个小问题。在用户视角里它的行为就是“卡住了”。我需要一个包含两轮observe - decide的工具调用结果因为它总想“再打磨一下”硬生生变成了 19 轮。成本和体验双双崩盘。1.3 循环是天然的单行道并发几乎无从谈起还有一个被忽略的致命点while循环天然是串行的。每一次迭代必须等上一次完成状态有前后依赖。但真实的业务任务里很多步骤其实是并行的——比如生成一份市场分析报告你需要同时查销售数据、查竞品动态、查用户评论这三者互不依赖。传统循环架构里这些操作只能排队执行。while循环的问题总结起来就一句话它把本该由编排系统保证的确定性、可恢复性、可并发性全部交给了大模型的自由发挥。大模型的优势在语义理解劣势恰恰是精确的流程控制。让它在循环里控制死循环、退出时机和分支路径属于让短跑运动员去当裁判。2. 把循环变成 DAG一次规划、一次遍历的执行模型2.1 核心思路规划时用大模型执行时用小函数既然循环的核心问题在于“运行时不可预测”我的方案很简单把运行时的大模型决策全部前移变成规划阶段的一次性输出。具体做法是Agent 在一开始的时候调用一次大模型让它把整个任务拆解成一份有向无环图DAG。图中的节点是原子操作边是依赖关系。规划完之后大模型就退场了剩下的执行阶段完全由一套确定性的图执行引擎来完成里面没有任何循环等待模型决策的代码。# 一次规划的产物节点的集合 依赖边 task_graph { nodes: [ {id: fetch_sales, type: tool, tool: query_sales_db}, {id: fetch_competitors, type: tool, tool: query_competitors}, {id: fetch_reviews, type: tool, tool: query_reviews}, {id: analyze, type: llm, prompt: summary_analysis}, {id: report, type: llm, prompt: write_report} ], edges: [ (fetch_sales, analyze), (fetch_competitors, analyze), (fetch_reviews, analyze), (analyze, report) ] }执行引擎只需要做一件事沿着依赖边做一次拓扑遍历。先跑没有任何前置依赖的节点——上面这个例子里就是三个fetch节点——跑完一个就把下游节点的依赖计数减一减到零就触发执行。这个模型天然支持并行、天然没有死循环、天然可以在任意节点处停止和恢复。可能有人会问如果任务复杂到规划阶段没法一次性产生完整 DAG 怎么办我的回答是那就让规划阶段产出多层 DAG而不是在运行时动态加边。每个子任务对应一张子图执行引擎负责递归展开子图但展开逻辑也是确定性的不会有“模型在关键时刻决定要不要再加一轮”的随机性。2.2 为什么大多数“需要循环”的场景其实不需要循环我一开始以为拆掉循环后很多任务会做不了比如那些看起来需要“迭代反思”的任务。结果发现一个规律大部分真实业务场景里的“循环”本质上是对不确定结果的重试而不是对流程的反复。举例来说一个 Agent 要抓取网页信息并提取结构化数据有时网页结构变了导致提取失败。传统循环的处理方式是让 Agent 自己发现失败了然后“思考一下”换个方式再试。但拆掉循环之后我用的是另一种思路提取节点失败时执行引擎自动触发一次带不同参数的提取动作重试两次后如果还不行就把错误信息汇入分析节点的上下文让下游节点感知“这里有缺失数据”而不是无限次循环重试。这两者的区别非常关键。前者是流程问题需要大模型动态判断“我该不该重试”后者是数据质量问题重试策略是确定性的数据缺失的影响由下游语义节点来处理。把不确定的范围缩小到一个纯粹的数据维度整个系统的可预测性就上来了。还有一个更常见的伪循环需求是“反思改进”。传统 Agent 会在生成初稿后自己读一遍然后说“我觉得这里不够好我改一下”改完又说“还有那里”。拆掉循环以后我把这个流程显式地建模成 DAGdraft - review - revise三个节点review只负责输出修改意见revise只负责把意见应用到草稿上两个节点都只执行一次。如果你真的需要多轮反思就在 DAG 上显式地加节点但你清楚地知道每一轮在做什么——而不是把轮次的控制权交给模型自己。2.3 图执行引擎的骨架代码其实非常简单说了这么多直接上一段执行引擎的核心代码。用 Python 写一个最简版本图存储就用上面那种nodes edges结构from collections import deque def run_dag(graph, contexts): # 统计每个节点的入度 indegree {n[id]: 0 for n in graph[nodes]} children {n[id]: [] for n in graph[nodes]} for src, dst in graph[edges]: indegree[dst] 1 children[src].append(dst) ready deque([n[id] for n in graph[nodes] if indegree[n[id]] 0]) results {} while ready: node_id ready.popleft() node next(n for n in graph[nodes] if n[id] node_id) # 节点执行工具节点调用工具LLM 节点调用模型 if node[type] tool: results[node_id] run_tool(node[tool], contexts.get(node_id)) else: results[node_id] run_llm(node[prompt], results, node_id) # 更新下游节点的就绪状态 for child in children[node_id]: indegree[child] - 1 if indegree[child] 0: ready.append(child) return results这段代码没有for i in range(max_iter)没有while not done没有大模型来“判断是否停”。所有流程控制都是数据驱动的一个节点的输入齐了它就执行。这个确定性是这个架构最大的价值所在。3. 事件驱动运行时状态快照、失败续跑与并发分支的落地细节3.1 状态外置可能是拆掉循环后最大的红利传统循环架构里Agent 的“状态”通常被塞在上下文窗口里模型每看一次上下文就重新理解一次“现在进行到哪了”。我拆掉循环后的第一个设计决定就是把状态从上下文窗口里完全剥离外置成一个不可变的状态对象。每个节点执行前后系统都会把当前状态保存为一个不可变快照存成 JSON 或者结构化对象。这个设计带来的直接好处是任何一步失败都可以从上一个成功快照恢复而不是从头开始跑。我遇到过最典型的一次故障某个工具节点在处理数据时抛了一个异常导致后面十个节点全部白跑。在传统循环架构里这时候唯一的办法是清空上下文从头来一次而大模型从头再来很可能会走出不同的路线等于把不确定性放大了。但在 DAG 架构里我给执行引擎加了一个简单的重试逻辑——从失败节点的上一个快照状态开始按原来的边继续往下走。因为执行路径是预先确定的重跑和原本的路径完全一致不会出现“第二次跑出错个节点”的诡异情况。状态快照的另一个好处是调试体验的质变。以前排查 Agent 问题时我只能看模型的“自言自语”日志然后在脑子里把它的想法和实际工具调用对应起来。现在我可以直接导出某一个节点的输入输出清楚地看到数据是从哪条路径来的被哪个节点改成了什么样。这个体验上的差距就像你以前只通过电话描述来排查故障现在可以直接看系统运行轨迹。3.2 并发分支把三个小时的串行任务压到四十分钟拆掉循环之后并发成了一个顺带的结果因为 DAG 本身就是并发的天然表达。我的执行引擎用事件驱动的模式实现节点完成时发出node_completed事件事件处理器负责检查下游节点的依赖是否满足。这里我用一个简单的示例说明def on_node_completed(node_id, result): for child in children[node_id]: dependency_left[child] - 1 if dependency_left[child] 0: # 只有所有前置节点都完成才启动下游节点 executor.submit(run_node, child)这里用executor.submit把不同的就绪节点丢到线程池里。由于每个节点读写的是不同的结果键天然没有数据竞争。我拿一个实际业务场景来量化收益生成一份季度竞品报告需要分别获取六个不同平台的数据然后汇总分析。传统循环架构里这些节点只能按顺序跑假设每个数据源耗时大约 15 分钟光获取数据就是 90 分钟。改成 DAG 之后六个数据节点一次性并行奔跑总耗时被六分之一算力拖平到 15 分钟汇总分析再加 20 分钟整体从 110 分钟压缩到 35 分钟左右。同样的任务并行能力带来的提升远比“优化提示词”来得生猛。3.3 不可变状态带来的记忆策略变化状态外置也直接影响到了 Agent 的记忆设计。在传统架构里“记忆”通常等于“把历史对话拼进上下文”上下文越长模型越容易迷失。拆掉循环后我用了另一种方案记忆是节点级的每个节点只读取它依赖的那部分状态。比如分析节点只读取三个数据源的输出结果报告节点只读取分析节点的输出。它不需要“记得”用户最开始的原话也不需要“记得”工具调用时传了什么参数。定义 Agent 记忆的时候我关心的不是“上下文够不够长”而是“每个节点在什么条件下看到哪些数据”这其实是一种结构化记忆。这样既省 token又比以前任何时候都清楚“模型到底看到了什么”。4. harness 和 agent 的真正边界拆循环之后谁在管什么4.1 大模型是决策器不是流程控制中心之前很多人问过我“harness 和 agent 到底什么区别”我当时的回答比较含糊。但在拆循环的实践里这个边界被我逼着给划清楚了。沿用我的定义Agent只负责输出“怎么做”的决策内容具体来说就是在规划阶段生成 DAG以及在每个 LLM 节点上生成针对性的文本输出。它不控制执行顺序不决定何时停止不管理重试。Harness负责一切非语义的骨架工作。包括维护状态快照、调度节点执行、处理失败重试、注入上下文、限制工具权限、记录运行轨迹。这些工作和大模型的语义能力毫无关系是纯粹的工程问题。在我现在的代码库里Agent类的全部职责浓缩成两个方法plan_task(task) - TaskGraph和generate(agent_ctx, prompt_inputs) - str。除此之外所有东西都被挪进了Runtime也就是我的 harness 实现。这个分层带来的直接收益是我可以单独写单元测试来测 harness 的调度逻辑不需要任何大模型参与。换一个更弱的模型甚至非模型实现只要它输出的 DAG 结构合法执行过程不会有任何变化。这在以前是不可想象的——以前要测试 while 循环的终止条件你必须真的去问一次大模型。4.2 harness 里必须处理的那些脏活我梳理一下拆循环之后 harness 至少要承担这些具体工作节点级别的超时控制任何节点超过预定时长没返回直接判定失败走重试分支。以前循环架构里超时控制很难做因为你不知道“这一轮思考”该给多少时间而大模型自己也不知道什么时候能停下。工具权限的边界控制每个节点声明它需要的工具白名单harness 在执行前把环境配置好。比如fetch_sales节点只能访问数据库不能调搜索 API。传统循环里工具始终是全集可见的模型自由选择出错概率高得多。注入上下文的最小化harness 从状态结果中选择性地组装上下文只把节点真正需要的数据传给当前 LLM 调用。每个 LLM 节点看到的是一个独立的、被裁剪过的上下文不再背负整段历史。4.3 事件日志从“散文”变成结构化轨迹传统循环架构下你要了解 Agent 做了什么只能去读它的思考日志——一段散文。而在新架构下harness 天然生成结构化的事件序列每个节点完成时记录节点 ID、类型、输入输出摘要、耗时、token 消耗。我后来在一次复盘里发现这个结构特别值钱。当用户报告“Agent 给出的结论有问题”时我可以直接定位到是哪个数据节点提供了脏数据而不是去猜模型哪句话让它理解了错误信息。日志从“剧情回顾”变成了“操作审计”这两者对排查问题的效率影响是量级上的差距。5. 连锁改造记忆、工具调用和安全的连带变化5.1 工具调用从“返回值拼接”变成“事件流”传统循环里Agent 调用工具的常规套路是工具返回一个字符串或 JSON拼接到上下文里大模型再继续推理。这个模式在 DAG 架构里显然行不通——因为节点的输出是结构化的而且是给下游节点用的不是给同一个上下文继续“看”的。我把工具调用改造成事件流模式工具执行过程中产生的流式事件比如进度、部分结果、错误提示会被广播到对应的事件频道下游节点按需订阅这些事件。比如fetch_sales返回的不是一段文本而是一个包含status、rows、meta的结构化对象。analyze节点消费这个对象而不是重新阅读一段话。这个改造顺带解决了一个老问题工具调用结果过长时传统循环会把好几页的数据塞进上下文大模型在长文本里提取关键信息的能力下降极快。拆循环之后数据结构化地交给下游节点下游节点的大模型调用只面对处理过的摘要token 消耗和错误率都明显下降。5.2 权限和安全模型变得更加可预期while循环架构里Agent 的安全问题非常吓人因为大模型在循环里自由决定下一步动作工具是全集暴露的你没法在执行中间阶段动态收紧权限。模型突然想调用某个高权限工具你不会提前知道。拆掉循环以后权限系统变成了图级别的静态分析。规划阶段产出 DAG 时harness 会对每个节点做一次权限校验这个节点声明的工具是否在允许范围内数据流向是否合法如果规划阶段生成了一个不该存在的边harness 可以直接拒绝整张图而不是等模型运行到那一步才拦截。有一次我在测试时给 Agent 配置了搜索工具权限但它的 DAG 里碰巧多了一个自动发送邮件的节点harness 直接报权限错误。如果放在传统循环架构里模型可能在第四轮突然决定“给用户发一封邮件”到那时候你只能靠运气。拆掉循环之后这类问题在静态检查阶段就被过滤掉了。5.3 token 成本的意外下降这个改善是最让我意外的。传统循环架构下Agent 的内存占用和 token 消耗无上限地增长因为每轮循环都要把历史轨迹完整读一遍。DAG 架构下每个 LLM 节点只读取它依赖结构的上下文模型不需要一遍遍回顾“我上一次干了什么”。我统计了几个典型任务的数据发现传统循环的每一步节点都会附带前面所有步骤的上下文token 消耗呈平方级增长而 DAG 架构的 token 消耗大致呈线性增长。举一个长任务实例一次 5 节点串行的任务传统循环大概消耗 45k tokenDAG 架构大概 22k token省了将近一半。不是说拆掉循环一定能省 token而是说不再循环重复读取历史之后无谓的上下文重读开销直接消失了。6. 上线实测与适用边界哪些场景收益最大哪些场景最好别拆6.1 一组有参考价值的对比数据我挑三个有代表性的任务类型记录改造前后的对比数据。样本量不大单次任务数据会有波动但趋势非常一致。任务类型传统循环消耗 tokenDAG 架构消耗 token平均延迟多源数据汇总报告~48k~23k从 90 分钟降到 35 分钟带重试的数据提取~31k~19k从 40 分钟降到 16 分钟开放式的知识检索~52k无法一次规划不适用前两行的收益很明显但第三行我特意留了个反例。这引出一个很重要的问题拆掉循环不是万能药。6.2 不适合拆循环的场景我在生产里试过不是所有任务都适合在规划阶段生成一次性的 DAG。开放式研究型任务就是个典型给一个高度模糊的目标比如“调研一下这个行业 2024 年的主要变化”你需要随着检索到的新信息不断调整方向可能这一轮发现的数据会推翻上一轮的假设。这种场景下一次性生成完整 DAG 几乎不可能因为“要查什么”本身是动态变化的。我在这种任务上硬套 DAG结果很惨规划阶段产出的图不完整执行阶段发现缺少关键节点只能手动回退到规划阶段重新规划而重新规划后 DAG 又变了整个执行过程不停地在“规划-执行-重新规划”之间震荡反而比 while 循环还要慢。另外一个不适合拆循环的场景是人机交互型 Agent。如果 Agent 需要和用户进行多轮澄清对话每个下一步都依赖于用户上一个问题这就天然构成一个对话循环你不能预先规划“用户会问什么然后我答什么”。这个场景里 while 循环是合理的因为它本质上处理的是无限流动的外部事件。6.3 我的折中方案两层架构DAG 为体有限循环为用经过一段时间的试错我最后采用的是折中方案默认走 DAG 流程但保留一个特殊的“动态扩展节点”。具体来说执行引擎里的节点类型除了tool和llm我还加了一个agent_loop类型。这类节点被显式标记为“内部循环”它有一个明确的退出条件声明比如“循环直到用户确认”或者“循环直到检索到的文档超过五份”。当执行引擎遇到这种节点时它不启动一个全局的 while 循环而是为一个有限的子任务执行局部循环并且循环的终止条件由 harness 检查不再由大模型决定。这个折中把不确定性的范围锁进了一个个小盒子里全局执行路径是确定的、可恢复、可并发的但局部可以保留必要的循环能力。它比纯循环架构可控得多也比纯 DAG 架构灵活得多。在实际使用中我发现绝大多数普通任务根本不需要走到agent_loop这一层——你只需要在规划阶段把任务拆细一点让每个节点足够原子问题就会简化很多。真正需要局部循环的场景往往是那些必须有外界反馈的任务比如人工审批、实时数据等待这种场景下明确声明一个循环节点比让模型在全局循环里自由发挥要安全得多。6.4 如果你也想拆掉循环我建议你先从这三件事开始最后给想尝试的朋友几个具体的操作建议都是我从实践中得到的经验第一先从日志开始改造。不要急着重写执行引擎先记录现有循环架构下每一步“节点 ID、输入、输出、耗时”这些结构化信息。跑一段时间观察你的任务里哪些步骤是真正动态的哪些其实只是假装动态。我敢打赌你会发现大部分所谓的“动态决策”其实就是固定的几个分支。第二给工具调用加上声明式接口。定义每个工具的输入输出 schema让工具结果天然是结构化的数据而不是字符串。这样拆循环之后下游节点才能直接消费结构化的输入而不需要再靠大模型去解析一段话。第三写一个最简单的 DAG 执行引擎让一个固定图跑起来。不用一开始就做大模型规划先手工构造一个几张节点的固定图跑通执行、重试、恢复全流程。等这个引擎稳定了再让大模型接管规划节点。从固定图到动态图是渐进式的别一上来就追求“大模型自动规划一切”。这套架构我第一次跑通时心里其实有点空——因为太简单了没有 while 循环那种“大模型在思考”的仪式感。但正是这种简单让它的行为变得可以预测。现在我的所有线上 Agent 服务都跑在这套 DAG 架构上配合局部的有限循环节点运维压力比之前小了不止一个量级。如果你也被 while 循环的不可控困扰过真心建议你试着把它拆掉看看。
📝

华诺云谱内容团队

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

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

你可能需要的服务

订阅华诺云谱资讯周报

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

↑