☰
拆掉Agent的while循环:事件驱动阶段化架构让AI任务可控
2026/10/7 6:30:05 网站建设 项目流程

去年冬天凌晨3点,我被一条线上告警叫醒。一个调研型Agent从晚上7点开始就没停过,它在一个while循环里反复调用搜索工具,一会觉得“信息还不够”,一会又觉得“再验证一下这个数据”,直到上下文窗口被撑爆、token预算烧穿,整个任务以失败告终。我盯着日志里那几十条高度重复的搜索记录,脑子里蹦出一个问题:为什么Agent系统里一定要有一个while循环?如果把它拆掉,任务还能跑吗?这篇东西就是我后来把循环拆掉、换了一种Agent架构之后想好好说清楚的事。它不是什么论文级别的理论,就是一次真实的架构调整,但那次调整之后,我对“Agent到底该怎么被组织”的理解彻底变了。

1. 复盘那次深夜跑飞的Agent:while循环到底把人坑在哪

1.1 Agent框架里的while循环,长得几乎一模一样

只要是做过Agent开发的人,大概率写过或者见过这样一段代码:

async def run_agent(task: str, tools: list[BaseTool]) -> str: state = { "task": task, "messages": [], "tool_results": [], } done = False while not done: resp = await llm.complete( messages=build_prompt(state), tools=[t.schema for t in tools], ) if resp.tool_calls: for call in resp.tool_calls: result = await execute_tool(call) state["messages"].append( tool_result_message(call, result) ) elif resp.final_answer: done = True return resp.final_answer else: # 防空转 await asyncio.sleep(0.5)

不管是ReAct、Function-Calling还是市面上大多数Agent框架,核心骨架基本都是这个形态:外部循环里调用大模型,模型决定是调用工具还是给出最终回答,直到遇到“final_answer”才跳出循环。这个设计太自然了,自然到没有人会去质疑它。Agent嘛,本来就要“反复思考、不断行动”,while循环简直就是为它量身定做的。

但恰恰是这个“天经地义”,埋下了我那天夜里所有问题的根源。

1.2 一次真实的失控时间线

那天跑飞的任务本身不复杂,要让Agent产出一份“RAG在金融场景中的落地调研报告”。我给了它搜索工具、网页解析工具,也写了提示词,要求它“收集足够的信息后输出报告”。看起来没什么问题,对吧?可实际执行过程是这样的:

时间动作上下文变化
19:02任务启动,LLM第一次调用搜索工具上下文约3k token
19:05搜索“RAG 金融 落地案例”约12k token
19:10又搜索“RAG 风控 合规”约25k token
19:21搜索“RAG 金融 落地案例”(重复)约41k token
20:18开始写报告初稿约60k token
20:35模型觉得案例太少,又重新搜索约75k token
23:47继续补采,搜索关键词与之前高度重叠约110k token
03:12上下文溢出,任务直接失败token耗尽

凌晨3点的时候我看到的是:40多次工具调用,其中至少三分之一是重复搜索,累计烧掉了近百万token,最后任务没有完成,也没有任何可用的中间结果落地。最讽刺的是,它搜索过的页面,我后来用脚本去重之后发现,真正有价值的信息只来自不到10个页面。

我不怪LLM。模型只是在不断做它认为“正确”的事——信息不够就继续找。真正的问题在架构层面:我把“这个任务应该什么时候停下来”的判断权,交给了概率模型。

1.3 问题的本质:让LLM给while循环踩刹车,等于把方向盘交给概率

你可能觉得,在提示词里写清楚“收集足够信息后必须结束”不就行了吗?我那天也是这么想的,而且写得更严格,甚至加了“如果你觉得信息不足,最多再补充两次搜索”这样的限制。结果呢?模型在长上下文里渐渐丢失了约束,它每轮看到的都只有“我还可以再做好一点”,于是不断继续。

这件事让我想明白一个关键点:循环式Agent的退出条件,本质上是一个语义判断。什么叫“信息足够了”?这个问题没有数值解。相比之下,普通程序里的while循环几乎都有确定性的终止条件。比如初学者最常见的题目:用while循环计算常数e的级数,e = 1 + 1/1! + 1/2! + 1/3! ...,直到最后一项小于10^-4为止。程序什么时候停?当某一项的值真正小于10^-4,这是一个可以计算的量,是确定性的。

Agent的循环却没有这样的“数值判据”。“任务完成”是一个模糊的、依赖模型状态和上下文理解的判断。你让一个概率模型去决定无限循环的出口,出问题的概率天然就不低。那个凌晨的一切,在架构设计出来的那一刻就已经注定了。

2. 藏在while循环背后的五个工程代价

2.1 行为空间不可枚举,测试调试像盲人摸象

写普通代码时,我们能对一个函数的行为做穷举测试,因为输入空间和输出空间是可枚举的。但循环式Agent不是。每一轮循环,模型可能选择调用工具A、工具B、或者直接输出答案;工具返回的结果可能成功、失败、部分失败;上下文的长度还在不断变化。这些因素组合起来,Agent的行为空间是一个接近无限深的树。

这意味着什么?意味着你没法通过写测试用例来保证Agent的行为边界。你只能靠“观察几次跑得还行”来获得信心。我那个Agent在测试环境跑了十几轮都正常,一到线上面对真实搜索结果就失控了。因为测试集永远覆盖不了真实世界的多样性。一个没有可枚举行为空间的系统,本质上是一个不可测试的系统。

2.2 状态全部堆在上下文里,Agent记忆变成历史包袱

循环式Agent最省事的做法,是把所有中间状态都追加到messages里:用户的任务、模型的历史思考、每次工具调用的原始返回、中间结果、甚至失败的报错,全部堆在一起。最初这样确实简单,但问题很快出现。

首先是成本。每多一轮循环,下一轮请求的input token就越大,成本非线性上涨。其次是效果。当上下文长度超过某个阈值,模型对早期信息的“注意力”会迅速衰减。我做过一个实验,把前面10轮的思考过程丢弃,只保留工具结果,模型对任务的理解反而更清晰。这说明上下文里的很多东西不是记忆,而是噪声。

循环式架构把“记忆”和“过程日志”混为一谈。真正优秀的Agent记忆,应该是只保留有价值的结论和状态,而不是把所有历史过程都背在身上。

2.3 并发改造极难,扛并发时要推倒重来

做Agent最常被问的问题就是“ai agent怎么扛并发”。老实说,循环式Agent在这个问题上是天生被动的。因为整个循环是一个串行的自我演化过程,状态都藏在run_agent函数的局部变量里。你没法把“任务执行到一半”的状态保存下来,分发给另一个worker去继续。每个任务只能在一个进程里从头跑到尾。

如果你打算用“一个任务起一个进程”的方式来扛并发,那其实就是把并发问题往后推,并没有真正解决。一旦某个Agent在循环里偶发卡住,那个worker就会被占住直到超时,资源利用率非常难看。阶段化架构最大的好处之一,就是中间状态可以序列化、可以分发,分布式执行的可能性被打开了。

2.4 终止条件脆弱,线上事故的根源

循环式Agent的退出条件,通常由模型产出的final_answer标志来决定。但这个标志本身并不可靠。模型可能在一轮认为任务完成了,给出答案;如果在下一轮重新询问,它又可能推翻自己,觉得“还需要补充”。这种判断的不稳定性,加上长上下文的干扰,让终止条件变成了整个系统里最脆弱的部分。

工程系统的核心原则之一,是每一层都要有确定性的兜底。循环式Agent把“是否终止”这个最关键的兜底逻辑交给了概率模型,这是架构性的脆弱。

2.5 成本方差巨大,预算控制形同虚设

循环式Agent的token消耗方差极大。同一个任务,状态好的时候10万token就结束了,状态差的时候能跑出100万token。这种方差让成本控制变成一句空话:你按80万token做预算,它跑飞的时候照样超;你按20万做预算,正常情况又浪费。在toB生产环境里,这种不可预测性是非常致命的,因为客户要的是稳定的成本报价。

五个代价叠加在一起,已经足够说明:循环不是实现细节,而是影响整个系统可靠性的架构决策。

3. 拆掉循环之后:事件驱动的阶段化Agent架构核心设计

3.1 一个根本性的换位:确定性编排器接管“要不要继续”

我拆掉while循环之后想明白的第一个道理,不是“让Agent别循环了”,而是“把要不要继续的决策,从LLM手里拿回来”。新架构叫它“阶段化流水线”也好、“事件驱动Agent”也好,本质都是一回事:先把任务拆成确定的阶段,每个阶段有明确的输入、输出和出口条件,LLM只在阶段内部工作,阶段之间的流转由确定性的编排器控制。

可以这样理解:以前的Agent是一个实习生,老板只给了个大目标,实习生每做完一点就跑去问“我接下来该干嘛?我是不是可以停了?”——每一个“该干嘛”都让LLM回答。新的架构里,实习生拿到的是一张任务卡,上面写着“第一件事做什么,做完了交给谁”。他自己不需要不断决定“下一步是什么”,只需要专注于把当前这一步做到位。

这种换位的核心意义在于:把系统的不可控空间从无限缩小到了有限。循环式Agent的行为空间是无限深的树,阶段化Agent的行为空间是一个固定深度的DAG,每个节点的执行规则是确定的。这让测试、调试、并发、成本估算都重新变得可能。

3.2 阶段化骨架到底长什么样

我当时搭的骨架非常简单,一个阶段枚举、一张流转表、一个执行器:

from enum import Enum class Stage(str, Enum): GATHER = "gather" # 信息收集 SYNTHESIZE = "synthesize" # 综合归纳 VERIFY = "verify" # 完整性校验 REPORT = "report" # 输出报告 # 阶段流转表:每个阶段结束后确定性地进入下一阶段 STAGE_FLOW = { Stage.GATHER: Stage.SYNTHESIZE, Stage.SYNTHESIZE: Stage.VERIFY, Stage.VERIFY: Stage.REPORT, Stage.REPORT: None, }

注意这里我依然写了Stage.REPORT映射到None,也就是没有下一个阶段,任务自然结束。整个流程不是靠LLM说“我完成了”来结束,而是编排器确定性地走完所有阶段。LLM在GATHER阶段负责决定搜索哪些关键词,在SYNTHESIZE阶段负责归纳总结,在VERIFY阶段负责检查遗漏,但它在任何阶段都不能决定“任务到此为止”。这个决定权,被我从模型手里拿走了。

可能会有朋友说:这不还是一个while循环吗?外层确实是循环,但它的步数等于阶段数量,是一个常量。我们拆掉的不是“结构上的循环”,而是“语义上的无限循环”。这个差别至关重要:外层循环的每一步都对应一个预先定义好的阶段处理器,没有哪一步会“意外地多执行一次”。

3.3 LLM从无限循环的司机变成阶段内的执行者

架构调整后,LLM的角色发生了微妙但重要的变化。在循环式架构里,LLM是“司机”,方向盘、刹车、油门全在它手里,它既能决定去哪个工具,也能决定何时停车,但车可能失控。在阶段化架构里,LLM更像“高铁列车员”,列车在哪一站停、什么时候停,由轨道系统决定;列车员负责在指定站点把服务做到位。

实际上这并不会让Agent变笨,反而让它在自己的职责范围内更加专注。它不再需要花心智去纠结“我是不是该结束了”,而是可以把全部能力用在“怎么在这个阶段把任务做好”。我后来观察到一个有意思的现象:同样的模型,在阶段里输出的思考质量明显比在无限循环里更高,因为不需要分心做元决策。

架构层面的“降权”反而带来了模型输出质量的“升维”,这是当时没有预料到的额外收获。

4. 一次完整重构:调研类Agent从循环式改成阶段流水线

4.1 先把任务边界和验收标准定死

任何重构的第一步都不是写代码,而是把任务和目标定清楚。我选了一个非常典型的场景:技术调研Agent,任务描述是“产出一份《RAG在金融场景中的落地调研报告》”。重构前我定义了四个硬性验收标准:

  • 报告必须包含四部分:场景现状、真实案例、潜在风险、结论建议;
  • token总预算不超过30万;
  • 端到端耗时不超过10分钟;
  • 任何一步失败,必须有明确的日志定位,而不是整个任务黑盒消失。

这四个约束直接倒逼架构设计。token预算意味着不能做无限制的探索式搜索;耗时意味着搜索阶段必须支持并发;可定位意味着每一步都要有独立的可观测节点。

4.2 重构前的循环式实现差在哪

重构前,Agent拿到任务后在while循环里自由发挥,结果就是我在第一章复盘的那种局面:重复搜索、上下文膨胀、无法按预算收敛。更深层的麻烦是,循环式实现里“搜索关键词”是模型每一轮临时生成的,它不记得上一轮搜过什么,也没有一个全局的去重机制。这不是某个参数的问题,而是循环式架构天然缺失“计划”和“执行”的分层。

我当时给Agent的能力是足够的,搜索工具、网页解析工具、评分能力都有,但它把这些能力用成了一把完全随机打出去的霰弹枪。

4.3 重构后的阶段划分与并发改造

新架构把任务拆成四个阶段:

GATHER阶段负责信息收集。进入阶段后,先用确定性规则生成一组初始搜索词,而不是让LLM在循环里逐个临时想词。我让LLM一次性生成6个覆盖不同子主题的搜索关键词,然后用协程并发执行。

SYNTHESIZE阶段负责综合归纳。等所有搜索结果回来,把6组结果一次性交给LLM,要求它输出结构化摘要。因为输入在这一阶段是固定的,token消耗是可控的。

VERIFY阶段负责完整性检查。用规则判断摘要是否覆盖了四个必备部分,缺哪一部分就单独补充搜索,但只补一轮,补完立即合成,不再二次延展。

REPORT阶段负责格式化输出。把校验过的内容渲染成最终报告。

这里特别要注意:补采阶段虽然带了点循环的味道,但它是有限循环,最多执行一次。它不是while,而是if——缺哪补哪,补完就停。

并发化改造的收益很大。GATHER里6个搜索用协程并发执行,从串行的十来分钟压缩到两分钟左右。这正好回应了“ai agent怎么扛并发”的问题:当你把任务拆成独立阶段后,阶段内部的并行改造就变得非常自然,而循环式架构想做到这一点就非常困难。

4.4 一版能直接落地的代码骨架

下面这个版本我做了一定简化,但保留了全部关键结构,可以直接在此基础上扩展:

import asyncio from dataclasses import dataclass, field @dataclass class TaskContext: task: str queries: list[str] = field(default_factory=list) gather_results: dict = field(default_factory=dict) synthesis: str = "" missing_parts: list = field(default_factory=list) report: str = "" async def stage_gather(ctx: TaskContext) -> TaskContext: # 第一阶段:一次性生成搜索计划,而不是在循环里临时想 prompt = f"针对以下任务,给出6个独立的搜索关键词:{ctx.task}" resp = await llm.complete(prompt) ctx.queries = parse_queries(resp) # 并发执行搜索,每个查询互相独立 async def search_one(q: str) -> tuple[str, str]: result = await search_tool.search(q) return q, result results = await asyncio.gather( *[search_one(q) for q in ctx.queries], return_exceptions=True, ) ctx.gather_results = dict(results) return ctx async def stage_synthesize(ctx: TaskContext) -> TaskContext: # 第二阶段:把所有结果一次性交给LLM综合归纳 content = json.dumps(ctx.gather_results, ensure_ascii=False) prompt = f""" 请根据以下调研材料,输出一份结构化摘要。 必须包含:场景现状、真实案例、潜在风险、结论建议。 材料如下: {content} """ ctx.synthesis = await llm.complete(prompt) return ctx async def stage_verify(ctx: TaskContext) -> TaskContext: # 第三阶段:规则校验,缺什么补什么,最多补一轮 ctx.missing_parts = check_required_sections(ctx.synthesis) if ctx.missing_parts: supplement_queries = [f"RAG金融 {p}" for p in ctx.missing_parts] supplements = await asyncio.gather( *[search_tool.search(q) for q in supplement_queries] ) ctx.gather_results.update(supplements) ctx.synthesis = await llm.complete( f"请在上文摘要基础上补充以下缺失部分并重新汇总:{ctx.missing_parts}\n" f"补充材料:{json.dumps(supplements, ensure_ascii=False)}" ) return ctx async def stage_report(ctx: TaskContext) -> TaskContext: # 第四阶段:格式化输出最终报告 ctx.report = render_markdown_report(ctx.synthesis) return ctx

然后编排器把这些阶段串联起来:

STAGE_HANDLERS = { Stage.GATHER: stage_gather, Stage.SYNTHESIZE: stage_synthesize, Stage.VERIFY: stage_verify, Stage.REPORT: stage_report, } async def run_pipeline(task: str) -> str: ctx = TaskContext(task=task) stage = Stage.GATHER while stage is not None: handler = STAGE_HANDLERS[stage] ctx = await handler(ctx) stage = STAGE_FLOW[stage] return ctx.report

注意这里依然有一个while,但它最多循环4次,每次执行哪个函数完全是确定的。你甚至可以把这个循环直接展开成顺序调用,效果没有任何区别。这正是“拆掉语义循环”的含义:不是消灭循环语法,而是让执行路径变得完全可预判。

4.5 重构前后的实测数字

重构完成之后,我在同一批任务上跑了十轮,取了典型数据:

维度循环式阶段化
平均耗时23分钟6分钟
平均token消耗56万18万
重复搜索次数约5~12次0次
失败定位难度需要翻全部日志直接定位到阶段
单次成本方差极大可控
并发利用基本串行阶段内并行
任务结果稳定性忽高忽低稳定达验收标准

这套数字不算惊艳,但已经足够让我在线上环境放心替换。更重要的是,“失败定位”从玄学变成了日常操作。哪一步挂了、挂在哪一阶段、是搜索超时还是校验没过,一眼就能找到。

5. 拆完之后踩到的三个坑:隐性循环、工具超时与阶段粒度

5.1 坑一:阶段内的隐性循环,差点把架构改回原样

我在第一版重构里,VALIDATE阶段写得比较随意。原计划是“如果校验没过,就重新生成报告”,结果写着写着变成了这样:

while not passed: ctx.synthesis = await llm.complete(...) passed = check(ctx.synthesis)

这就是典型的隐形循环迁移。你以为拆掉了主循环,结果把循环搬进了阶段内部。只要这个循环不断让LLM重新生成,它照样可以无限转下去。而且因为嵌套更深,反而更难发现。

教训是:阶段内任何重试都必须有明确的次数上限。要么是固定重试一次,要么是补充一轮后无论如何都进入下一阶段,要么是把问题抛给人工审核通道。绝不能留着无界循环。我当时甚至是刻意在每个阶段处理器里加了一个重试计数,超过阈值直接跳出整个pipeline,保证“任何路径都是有限步数”。

5.2 坑二:阶段化之后,工具超时和重试必须先于LLM解决

循环式架构有一个隐藏的“缓冲”作用:模型每轮只调一两次工具,工具超时对整个任务的影响不大,大不了下一轮再调。阶段化之后,GATHER阶段要并发跑6个搜索,任何一个工具迟迟不返回,整个阶段都会被拖住。

所以你在做阶段化改造时,工具层的健壮性必须前置处理。我给每个工具调用挂了独立的超时控制,3秒没有任何返回就标记为失败,把失败原因写进结果集,后续阶段依然可以基于“部分缺失的数据”工作,而不是整体崩溃。这个细节看起来小,但真正撑起了并发阶段的高成功率。

async def safe_search(query: str) -> tuple[str, str | None]: try: result = await asyncio.wait_for( search_tool.search(query), timeout=3.0 ) return query, result except Exception as exc: return query, f"[ERROR] {exc}"

5.3 坑三:阶段粒度过粗或过细,都会让Agent“变笨”

阶段太粗,比如把“综合归纳+校验+补采”合并成一个大阶段,结果就是你又在阶段里偷偷造了一个小循环。阶段太细,比如把“搜索关键词生成”拆成独立阶段、“解析搜索结果”又拆一个阶段,你会发现大多数阶段本身不完整,反而要频繁切换状态,开销大,收益低。

我实际跑下来的经验是:一个阶段内应该只包含一到两个LLM调用和一到两个工具调用,并且要有一个明确的产出物。产出物明确,阶段边界才清晰;动作数量少,阶段内部才不需要复杂的自控制逻辑。如果你发现自己写的一个阶段处理器超过50行,多半是该拆了。

这个“粒度”原则,有点像是厨房切菜。切得太粗,菜不入味;切得太细,浪费时间还容易碎。合适的标准就是:“下一道工序可以直接使用,不需要再加工。”

6. 循环式还是阶段化:Agent主循环的选型边界与决策顺序

6.1 三种模式的适用场景

写完上面的重构,我并不建议所有人无脑抛弃循环式Agent。它们在各自的场景里都成立。我现在的判断方式是画一张表:

维度循环式Agent阶段化Agent混合式Agent
典型任务开放式探索、头脑风暴有明确产出物的任务探索+产出物混合
定义任务边界基本不定义必须提前拆清只定义主边界
成本稳定性差好中
并发优势弱强中
调试难度高低中
适合阶段原型验证生产环境复杂生产任务
失败兜底依赖LLM判断确定性编排器兜底阶段内部有限循环+编排器兜底

我的经验是:只要任务能提前映射出阶段(一个能产出的结果、一组固定动作),阶段化就是更省心的选项。凡是把自己封闭在while循环里的Agent,上线前必须回答三个问题:

  • 如果模型连续三轮判断不一致,系统会怎样?
  • 如果任务做了40轮还没完成,用户是什么体验?
  • 你能承受最坏情况下的token成本吗?

这三个问题但凡有一个让你心里发虚,就说明循环式架构的生产级能力不足。

6.2 我现在的默认选择:阶段化外壳,循环化内核

上面说了这么多,其实我最推荐的既不是纯循环也不是纯阶段化,而是混合式:外部用确定性编排器控制阶段流转,LLM的探索能力被限制在阶段内部,并且任何内部探索都必须是有限次数的。这相当于把while循环从“系统主循环”降级为“阶段附属工具”。

比如在一个“写代码Agent”里,主流程可以分成“需求理解、方案设计、代码生成、测试验证、修复”五个阶段;在“测试验证”阶段内部,让Agent循环执行测试、定位、修复的动作,但设定最多重试5次,超过5次就请求人工介入。这样一来,Agent的探索性还在,但系统永远不会失控。

6.3 下次写Agent时,先画阶段流转图

我现在开发Agent的第一件事,就是在一张纸上画这个任务从输入到输出要经过哪些阶段、每个阶段产出的物化结果是什么、阶段之间如何流转。如果这张图画不出来,说明你对任务本身的理解还不够透,也不该急着写代码。如果能画出来,哪怕阶段画粗一点,也一定要按照阶段化的框架搭骨架。

很多朋友一开始觉得“阶段化”是在额外增加工程量,我承认前期多花一小时拆阶段确实费事。但它的回报会在线上运行的第一天就体现出来:不再有凌晨的告警,不再有烧穿的预算,不再有翻不完的日志。

我最后一次半夜爬起来维护Agent,不是因为它聪明过头跑飞了,而是因为我当初给了它无限的自由。拆掉那个主循环之后,它反而在一套确定的轨道里,把每一件小事都做得比之前更好。如果你也在为Agent跑飞、token失控、难以排错而头疼,我的建议很简单:试着把它的主循环拆掉看看,先从一个明确的任务开始,设计出它的第一阶段,剩下的自然就有了方向。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询