ReAct 循环跑偏的三种样子,以及怎么在工程上兜住
几乎所有 Agent 框架的骨架都是同一个循环。模型想一下,决定调哪个工具,拿到结果再想一下,直到它认为可以回答。这个循环看起来极简单,写一个最小实现不超过一百行。
真正上线之后,问题都出在循环跑偏上。它可能一直绕圈,可能调错工具,也可能把工具返回的报错当成正常数据,一本正经地往下推理。这三种跑偏方式的表现完全不同,兜法也不一样。框架给你的是循环的骨架,加固得自己加。
先说清楚循环里有什么
一次完整的迭代有四个产物。思考,模型说它打算干什么。动作,具体到工具名和参数。观察,工具真实返回的东西。状态,这一轮之后任务推进到哪一步。
绝大多数实现只把前三个当回事,状态是靠模型自己在上下文里记的。这是后面所有麻烦的起点,因为模型的记忆是文本,文本会被压缩、会被截断、会被它自己的措辞带偏。到了一定轮数,它对自己完成了哪些步骤的判断就开始模糊。
第一种跑偏,原地绕圈
表现是同一个动作反复出现。查了一遍库存,觉得不够,再查一遍同一个仓库,再查一遍。或者思考内容每轮都在换措辞,但实质是同一句话的变形。
原因是模型缺少进展信号。它不知道自己已经查过,也不知道这次查询和上次是重复的。上下文长了之后,前面那次的记录被淹没了。
工程上的兜法有三层。第一层是动作签名去重,把工具名加关键参数拼成一个字符串,短期内重复出现就拦下,把提示信息塞回去告诉它这一步已经做过。第二层是步数预算,比如超过十二步强制收敛,让它基于现有信息作答或者明确说做不到。第三层是思考停滞检测,连续几轮思考的词相似度都很高,说明没有实质推进,直接终止比继续烧钱划算。
有人会问,为什么不让模型自己判断是否在绕圈。因为它判断的依据是上下文,而上下文正是被绕圈污染的地方。这类判断必须放在外部,用代码做,不依赖模型自觉。
第二种跑偏,选错工具
这个和参数错误不是一回事。参数错误是选对了工具填错了值,选错工具是从一开始就找错了方向。表现是明明有精确查询的接口,它用了模糊搜索;明明有专门的退款接口,它去调了通用的工单创建。
原因通常是工具太多或者描述含糊。工具数量超过十五个之后,模型的选择准确率会明显下降,因为描述挤在一起互相干扰。两个功能相近的工具,名字里都带查询,一个按订单号一个按手机号,模型很容易挑错。
三层兜法。第一层是工具检索,别把所有工具一次性塞进提示词,先按用户意图筛出候选的七八个。这样既省 token 又提高准确率。第二层是描述去歧义,在描述里明确写出使用场景和不该用的场景,比如查单号用这个,查历史用那个。第三层是失败反馈,工具被调用但前置条件不满足时,返回的信息要指向正确的工具,而不只是报一句参数不合法。
有一类决策必须拦住不让模型做,就是有副作用的操作选错对象。删错一条记录、退错一笔款,代价不是多花几分钱。这类操作要么加确认环节,要么在工具层加二次校验,把业务规则写死在代码里。
第三种跑偏,误读观察结果
这一种最隐蔽,因为它看起来一切正常。工具返回了一段文字,模型把它当成事实往下推。可那段文字可能是超时提示,可能是空结果,可能是带警告的部分成功。
举个常见的例子。数据库查询超时了,接口按约定返回了一个空列表加一条警告。模型看到空列表,得出这个用户没有订单的结论,然后热心地建议用户去注册。用户当然有订单,只是这次没查出来。
兜法是让工具返回值结构化,别返回一段自由文本。成功、失败、部分成功、空结果,这四种状态各自有明确字段,错误码和错误原因分开。模型看到的是结构,不是散文,误读的空间立刻小一大截。
第二道兜法是关键字段的断言。涉及金额、数量、状态的结论,要求模型引用具体字段值,程序侧核对这个值是否真的来自工具返回。这一招成本很低,能挡住大部分凭空推断。
第三道兜法是部分成功的显式处理。比如五个子任务里三个成功两个失败,工具必须明确说出哪两个失败了。如果不这么做,模型很容易概括成全部完成。
状态要放在外面
前面三种跑偏都指向同一件事,状态得由程序维护,不能靠模型的文本记忆。
具体做法是维护一份结构化的任务状态,包含目标、已完成的步骤、每步的结论摘要、待办清单、已消耗的预算。每一轮开始前把这份状态渲染进提示词,而不是把全部历史对话塞进去。历史越长越要这么做,这和上下文压缩的思路一致。
状态还有个额外好处,可以断点续跑。任务中途失败,重启时从状态恢复,不用从头再来。异步长任务里这一条尤其重要,用户等了三分钟不想再等三分钟。
终止条件写清楚
循环必须有明确的退出条件,而且要写三份。任务完成的条件,任务无法完成的条件,预算耗尽的条件。
第一份比较好写,验收标准是什么就写什么。第二份经常被忽略,比如需要的权限没有、数据不存在、用户的要求自相矛盾,这些情况下继续尝试没有任何意义,应该立刻停止并如实说明。第三份是兜底,步数、耗时、token 三个维度都要有上限。
三份里最容易被跳过的就是第一份的实际校验。模型说完成了,就当真完成吗。正确做法是用程序核对最终交付物,订单真的取消了,文件真的生成了,字段真的写进去了。模型的自述只是线索,交付物才是证据。
每步都要留痕
想让这些机制可调试,就得记下每一轮的全部产物。思考原文、工具名、参数、返回、耗时、token、判定结果。
有了这份记录,绕圈能一眼看出来,因为同一动作出现了多次。选错工具也能看出来,因为工具名和用户意图明显不匹配。误读更容易定位,把返回和模型的下一条思考对照就能发现。
没有这份记录,排查会退化成猜。而这类系统的行为受提示词、模型版本、上下文长度共同影响,猜中的概率很低。
结合系列
最小 Agent 那篇搭的是循环骨架,这篇补的是骨架之外的三道加固。参数校验篇讲的是动作这一步的校验,这篇讲的是选择和观察两端。执行循环篇的终止条件和重试上限,在这里落成三份退出条件和动作去重。可观测性篇的 trace 结构,正好对应这里要求的每步留痕,一次请求就是一棵树,每个节点是一轮迭代。异步长任务篇的断点续跑,靠的就是这份外部状态。