人人都在说 Agent,拆开只是一个 while 循环:它为什么会跑飞
去年还叫聊天机器人的产品,今年全改名 Agent 了。它到底多了什么?把 Claude Code 修 bug 的过程拆开看,骨架只有三行伪代码:一个 while 循环、一箱工具、一个停止条件。和聊天机器人、固定流程的分界线只有一句话,"下一步由谁决定"。顺着这三行往下推,就能看懂 agent 为什么会同一个坏工具连调几百次、为什么修 bug 修成了重写、为什么"最大轮数"是保险丝而不是刹车,以及人该站在循环的哪个位置。
去年这时候,大家管这类产品叫聊天机器人。今年打开任何一个 AI 产品的官网,全是 Agent:编程 Agent、客服 Agent、数据分析 Agent。名字换了,底下的模型好像还是那几家的。到底多了什么?
我自己的感受来自一个具体场景。让 Claude Code 修一个测试失败,它读了报错,翻了三个文件,改了一处,跑测试,又失败,再改,再跑,一共来回十一轮,中间我一个字没说。同样的模型放在网页聊天框里,我得自己把报错贴进去,它给我一段代码,我贴回编辑器,跑,再把新的报错贴进去。两边模型一样,体验差了一个量级。
差的那部分,拆开看小得让人意外。
三行伪代码
昨天那篇讲了一次工具调用的往返:模型写出”我要调 get_weather”,程序替它调,把结果塞回对话,模型接着写。那是一个来回。
Agent 就是把这个来回套进一个循环:
messages = [用户的目标]
while True:
reply = model(messages, tools) # 模型看全部对话,决定下一步
if reply 里没有工具调用: # 它觉得做完了,就退出
break
result = run(reply.tool_call) # 程序替它执行
messages += [reply, result] # 结果塞回去,再来一轮
去掉注释,真正干活的就三行:调模型、判断要不要停、执行工具。剩下的是把结果追加到对话里。Claude Code 修 bug 那十一轮,就是这个循环转了十一圈,每一圈模型都重新看一遍到目前为止发生的全部事情,然后决定下一步是读文件、改文件、跑测试,还是”我做完了”。

关键在第三行那个判断。循环要不要再转一圈,是模型决定的。它输出里带工具调用,程序就再来一轮;它输出的是一段人话,程序就把这段话给你看,结束。程序自己不知道任务做完没有,它只认这一个信号。
分界线只有一句话
理解了这三行,“Agent 到底比聊天机器人多了什么”就有了一个可以操作的判断标准:下一步由谁决定。
聊天机器人:一问一答,一轮结束。就算带工具,也是查一次天气就回答,循环最多转一圈。下一步是”把结果给用户看”,写死的。
工作流:步骤是人事先编排好的。比如”先把用户的邮件分类,再按类别抽取字段,再生成回复草稿”,三步固定,每一步调一次模型填空。模型决定每一步的内容,但不决定有几步、下一步是什么。转多少圈、每圈干什么,写代码的人早就定了。
Agent:步骤不预先规定。给它一个目标和一箱工具,每一圈它自己看情况决定下一步用哪个工具,或者宣布做完。转几圈事先没人知道。

所以有一个很实用的反问:如果一个产品自称 Agent,问一句”它的第二步是谁定的”。如果答案是”产品经理画在流程图上的”,那是工作流套了个新名字。这没什么不好,工作流便宜、稳定、可预测,能用工作流的地方就该用工作流。但它和 Agent 是两种东西。
反过来也成立。什么任务值得用 Agent?事先说不清要几步的任务。修一个 bug,不知道要看几个文件;排查一个线上问题,不知道要查几条日志;给一个陈旧的仓库升级依赖,不知道会连锁炸出几处编译错误。这类任务写成固定流程,写的人自己都不知道该画几个框。让模型自己决定下一步,正是为了应付这种不确定。
它为什么会跑飞
三行伪代码里没有任何一处写着”人”。循环转不转,全看模型这一轮的输出。这就是 Agent 强的原因,也是它出事的原因。
今年有不少人在讨论 Agent 在生产环境里失控的案例。有一个被反复引用的:一个 Agent 对着同一个坏掉的工具连续调了四百多次,五分钟,直到撞上平台的限流才停下来。另一个常见的说法是,一个失控的 Agent 在有人注意到之前能烧掉几十到几百美元的 API 费用。
回到那三行代码,这些事故都能推出来。
第一种,原地打转。工具报错了,模型看到错误,最自然的续写是什么?“再试一次”。人也会这样,但人试三次会停下来想想是不是路错了,模型不一定。每一圈它看到的都是”上一次失败了”,每一圈最顺手的下一步都是重试。于是四百次。有时候它会换一个几乎相同的参数再试,看起来像在调整,其实还是在原地。
第二种,目标漂移。让它修一个测试,它发现测试依赖的函数写得不好,顺手重构了那个函数,重构完发现另外三处调用要跟着改,改完又发现一个新的测试挂了。二十圈之后它改了十几个文件,最初那个测试倒是过了,但你拿到的是一次没人要求过的重写。每一圈它都只看眼前”下一步最合理是什么”,没有一圈在问”这还是我最初要做的事吗”。
第三种,走太远忘了起点。循环每转一圈,对话就长一截:模型的输出、工具的返回,全追加进去。二十圈之后对话里塞满了文件内容和命令输出,最开头那句用户目标被挤到很远的地方。之前写过上下文窗口有多大、模型对开头的内容记得多牢,那些限制在这里全都会兑现。
三种跑飞,共同点是循环的每一圈都只有局部视角。模型在每一圈都做了当时看起来合理的决定,串起来就不合理了。
停止条件不止一个
于是”停止条件”成了 Agent 设计里最要紧的一件事。伪代码里只写了一个出口:“模型说做完了”。真正跑起来的 Agent 至少要再加几道。
最大轮数。转到第 N 圈强制退出。这是最简单的一道,也是最后一道。它防的是无限烧钱,不防干错事。到了上限才停的 Agent,多半已经做了一堆没用的事,硬上限是保险丝,不是刹车。
预算上限。按 token 数或者花费计。比轮数更接近你真正关心的东西,但同样是事后止损。
重复检测。同一个工具、同样的参数、连续第三次,程序层直接打断,把”你已经用同样的方式试了三次”写进对话,让模型换路。这一道针对的正是原地打转,而且它介入得早,是真正的刹车。
不可逆动作前停下来问人。删文件、发邮件、提交代码、花钱,这类动作在执行前把循环暂停,等一个人点头。Claude Code 在跑命令前弹出的那个权限确认,就是把人放进循环的这个位置。你会发现它读文件不问你,改文件和跑命令才问,这就是按”可逆不可逆”划的线。
阶段性核对目标。每转若干圈,把最初的目标重新摆到模型眼前,问它一句”目前的进展还在往这个方向走吗”。这一道针对目标漂移。它不是硬性的停止,是把”整体视角”周期性地塞回只有局部视角的循环里。

这五道里,前两道是兜底,后三道才是设计。一个只靠最大轮数保命的 Agent,和一个在关键位置放了人、放了重复检测的 Agent,跑起来完全是两种东西,虽然骨架都是那三行。
Subagent 是循环里的循环
顺便说清一个概念。之前写的 subagent,放到今天的框架里位置很明确:主 Agent 的某一圈,调用的那个”工具”本身是另一个 while 循环。主 Agent 说”去把这个模块的调用方全找出来”,程序起一个子循环,子循环自己转它的圈、用它的工具,转完把一段结论返回,主 Agent 拿到的只是这段结论,中间的几十圈对话它看不到。
好处正是解决”走太远忘了起点”:子循环把自己那堆文件内容和命令输出消化掉,只把结果交上去,主循环的对话就不会被撑爆。代价是子循环也有自己的跑飞风险,刹车要各装一套。
门槛低,正是它厉害的地方
写到这儿,“Agent 到底是什么”的答案已经有点反高潮了:一个 while 循环、一箱工具、一个停止条件。骨架三行代码,任何一个会写程序的人一下午就能搭出来。
我反而觉得这是好事。它说明 Agent 的能力不在骨架上,在模型有多会判断下一步、工具箱里有什么、刹车装在哪。这三样各自都能单独改进,骨架不用动。去年到今年 Agent 突然好用了,主要是模型判断下一步的能力过了某个门槛,那三行代码几年前就有人写过了。
所以以后再看到一个产品说自己是 Agent,我会问两个问题。一个是”第二步谁定的”,判断它是不是真的。另一个是”什么时候它会停下来问你”,判断它靠不靠得住。两个问题都能从那三行伪代码里读出来。
评论