造 AI Agent:循环、停止条件、和能说「不行」的东西
循环转起来只要一个 while。难的是让它停,而且停得对——因为让干活的那个判断干完没有,是让它给自己判卷。
上一篇讲了工作流。这一篇造 agent —— 也就是把「下一步做什么」交给模型。
循环本身很简单
for turn in range(max_turns): msg = model(messages, tools=schemas) messages.append(msg)
if not msg.tool_calls: # ← 它自己决定停 break
for call in msg.tool_calls: # ← 它自己决定用哪个工具 out = tools[call.name](**call.args) messages.append(tool_result(call, out))
else: return "达到轮数上限" # ← 有界,不许无限转加粗的两行是全部的自主性,第三处是全部的安全感。 其余是管道。
框架会在这外面包很多东西 —— 追踪、会话、并发、编排 —— 但内核不变。看懂这十几行,再读任何框架的文档,你都能一眼认出它替你做了哪一段。
停止条件是真正难的地方
「循环」这个词容易让人以为难点在转起来。难的是停。
三类停止条件,可靠性差别很大:
一、固定轮数。 最钝:三轮可能没干完,也可能第一轮就该停。
二、模型自己说完成了。 最危险 —— 它几乎总是说完成了。让干活的那个判断干完没有,是让它给自己判卷。
三、独立判据。 交给一段确定性检查,或者一个没参与干活的模型。
第三类才是正解。这是银行业几十年的双人复核原则:录入大额转账的人和复核的人必须是两个人。搬到 agent 上就是 —— 写东西的和判断写完没有的,不能是同一个。
为什么它会夸自己
这不是智力问题,是结构问题。
它没法跳出自己的视角:它脑子里装的是「我为什么这么做」的一整套理由,所以它看到的不是结果,是那串说服自己的过程。
有个反直觉但很有用的结论:教一个独立的评估器变挑剔,比教生成器自我批评容易得多。 前者你可以直接给它一句「我假设这是坏的,除非你证明给我看」;后者你没法让作者跳出自己。
评估器要动手,不能只读
换个 agent 还不够。只读的评估器判的是「这看起来对吗」,不是「这跑起来对吗」。
有实践是把评估器接上浏览器工具,让它真的打开页面、点按钮、截图、查 DOM,判断依据于是从「这段代码看着没问题」变成「我点了按钮,页面跳转了,截图在这儿」。
判断的依据要从「看起来对」变成「我试过了」。验证器决定上限这条规律在多份材料里反复出现。
循环跑飞的代价要乘以轮数
单次调用跑飞,代价是一次调用。循环跑飞,代价乘以轮数 —— 而且经常是在没人看的时候乘。
所以预算是三个数同时卡着:单次运行上限、每日上限、最大重试次数。少任何一个都留着一条跑不停的路。
两个实现细节我都踩过:
记账要记在动作之前。 计数器写在调用返回之后,超时和被限流的请求就不进账 —— 它们花了时间和算力却不消耗预算。
当心 SDK 自己的重试。 常见客户端库默认静默重试两次 —— 你眼里的一次调用,网络上是三次请求。重试只能有一层负责。
有上限还不够
这不是个别现象,是最常见的那一类。MAST 标注了 7 个多 agent 框架的 1642 条执行轨迹(标注者一致性 κ=0.88),失败模式的分布是:
| 失败模式 | 占比 |
|---|---|
| 原地重复同一步 | 15.7%(单项最高) |
| 不知道什么时候算结束 | 12.4% |
| 不遵守任务规格 | 11.8% |
排第一和排第二的,全都是「停不下来」。
它跑满了上限,上限没救它。 所以还需要一条:连续多少轮无进展就升级给人。
判据可以很土:连续三轮的失败信息完全一样,说明在原地打转,停。
边界与代价
agent 的自主性和可测量性是此消彼长的。 每多给一分自主,你比较两个版本的能力就少一分。先确认你需不需要比较。
「有界」不等于「安全」。 上限防的是无限循环,防不了「它在上限内做了不该做的事」。不可逆动作仍然需要一道显式的门。
评估器本身也会错。 换一个 agent 换来的是结构上的独立,不是能力上的更强。它的价值在于它没被生成过程说服过 —— 仅此而已,但这已经很值钱。