跳到主要内容
让另一个 agent 检查,不叫检查,叫联署

让另一个 agent 检查,不叫检查,叫联署

阿舟
阿舟

· 阅读约 8 分钟

我塞进循环里的那个 review 会话,连着三轮说 OK。

第四轮我自己点开 diff 看了一眼。它把时间聚合从 UTC 换成了本地时区,旁边还写了行注释解释为什么——需求里确实有一句"按用户所在时区的自然日聚合"。字面上,它没错。

但我们的批处理窗口会横跨夏令时切换那几个小时。按本地时间聚合,那一批数据会碎成两段,落错天。这个边界情况不在任何文档里,是去年一次对不上账之后口头定下来的,只在一个很丑的 if 里留着痕迹——丑到我每次重构都想删掉它。

那个 review 会话没看出来。它读的是同一份需求、同一份 diff、同一个模型的同一套理解。它不是没认真检查,它是认真检查了一遍自己的理解,然后盖章同意。

问题不在"要不要加一道检查"。"检查"这个词被偷换了。一个共享上下文的模型对另一个模型盖章,那叫联署,不叫审核。

这事是被一条评论点醒的。

前几天翻 dev.to,看到一篇讲 Loop Engineering 四个坑的文章,9 月 9 号发的,署名是代表 Google AI 的 Tilde A. Thurium,内容基于跟 Annie Wang 的对话。这种"N 个陷阱 + 对应修复"的整齐结构我一向警惕——整齐通常意味着在信息量上打了折。正文我看了个七八成同意,真正让我坐直的是评论区。

一个叫 Jo Do 的人,两句话把正文第二条的修复方案拆了,而且拆得对。

第二条讲的是未经核验的自主性,里面打了个比方,说让 agent 评判自己,等于让幼儿园小孩批改自己的作业。修复方案顺理成章:由一个 agent 检查另一个 agent 的工作。Jo Do 说,如果检查者和被检查者共享同一个模型、同一种上下文形态,那两边有一样的盲点。

我盯着这条看了两遍。因为我上个月刚干过这事,而且是它的反面教材。

联署会开得挺顺利

场景不复杂。一批埋点数据天天进来,字段格式经常是脏的,我懒得每次手工修,就起了一个自愈循环——跑清洗、跑校验、跑单测,哪一步红了就让 Claude Code 改代码,改完再跑,直到全绿。

改代码这步我其实不太担心,红了就改,改错了下一轮还会红。担心的是它改"对"了但是把事做错了,所以我在循环里塞了个 review 步骤:另开一个会话,同一个模型,读 diff、读需求描述,判断这次改动有没有问题。

然后它连着三轮说 OK。第四轮就是开头那个时区。

后来我把 review 那一步删了,换成断言。关键不在"加一道检查",在于检查器的判据不能是自然语言描述的"需求正确",得是一个真会失败的断言。

# 这不是检查器,这是一句愿望
assert "时区处理正确"

# 这是检查器
def test_dst_boundary():
    # 夏令时切换那天,本地时区聚合会碎成两段,UTC 不会
    out = aggregate(rows, tz="America/New_York")
    assert len(out) == 3

第一行那种我写过不少,就是写在注释或者 prompt 里的"请确保时区处理正确"。它不会失败,它只会被读到,然后被请求方和检查方一致认为没问题。

第二行那种才是检查器。它不"理解"任何东西,它只告诉你过还是不过,而且判定机制跟模型没关系——是 Python 在跑,是那个 3 在卡你。

能当检查器的,失败模式必须跟模型的理解不相关。类型系统、契约测试、拿线上旧输入做重放、拿一份已知正确的输出逐行比对,都算。这些的共同点是:盲点长在别的地方。

顺带一句,有人以为换个厂商的模型就独立了。相关性是降低了,但不是独立。两边读的还是同一份需求文档,你需求里没写的那部分,他们一样看不见。盲点不在权重里,在上下文里——换个模型换不掉上下文,就像换个同事读同一份没写清楚的交接文档,他一样会漏。

(正文第四个坑讲复杂度溢出、建议转向 Graph Engineering,这条我没什么可说的。我连一个循环都还没管明白,不配聊图。)

停止条件得长在循环自己身上

第二件改掉的事是停止条件。还是 Jo Do,他说硬停条件应该是循环自己能读到的值,不是外部的终止开关,否则你只能从账单上发现失控。

我读的时候心里咯噔一下。因为我第一版循环就是外部终止开关:我坐旁边看着,觉得不对就 Ctrl-C。

画外音:我当然没有坐在旁边看。

第一天晚上它跑到凌晨四点。我是从账单上知道的。钱不算多,但没有一分买到东西。

后来我把停止条件改成循环每轮开头自己检查的状态:

for step in range(MAX_STEPS):
    if spent_usd() > 8.0:
        record("budget")
        break
    if steps_since_progress >= 3:
        # diff 为空,或者测试没有从红变绿
        record("stalled")
        break

两个值都得是循环自己能读到的。"我盯着"不是停止规则,那是我以为我会一直醒着。

还有一件事我一开始没做,后来补上了:停止原因得记下来。超预算停的、卡住停的、正常跑完停的,这三种情况第二天早上需要的处理完全不同。不记,你第二天早上拿到的就是一堆 diff 和一句"它停了"。

评论区还有个单人跑 agent 的人,提了一张"不可自行批准"的升级清单,单子最上面是付款、法律身份变更、不可逆删除。这张单子我很喜欢,因为它比大部分 AI 安全的长篇大论实在——就是一张贴在显示器边上的便签。我自己的版本现在有三条:碰生产库、对外发任何东西、删掉任何我一次 undo 不回来的东西。循环撞上这三类,停下来等人,没有例外。

越跑得顺,越要警惕

整串评论里最狠的一条来自 Pushpendra。他说当目标本身描述不足的时候,向目标重试这套框架会出问题——agent 可能卡在一个"技术上成功但把事做错"的步骤上无限循环,因为成功和正确不是同一种检查。

翻译成人话:循环的终止条件写的是"测试通过",而测试通过只说明你没踩到你写下来的那几个断言,不说明你做对了事。

举个我自己的例子。有段时间我在磨一个接口的慢查询,验收标准写得很干脆:p99 压到 200ms 以下。它三天就把这事办成了——把缓存的 TTL 从五分钟拉到了二十四小时。断言全绿,因为那条断言就是我自己写的 p99。

这就引出一个很反直觉的结论,我自己也是踩了才认:循环跑得越顺,越要警惕。因为它不是在找正确答案,是在找能过关的答案。你把验收标准写得越死,它越精确地收敛到那几行断言上——那几行断言之外的东西,它一点都不 care,那不是它的目标。

所以"我写下了不可协商的判定标准"这句话,听着靠谱,风险恰恰在"不可协商"这四个字上。标准一旦写死、写窄,循环就会朝那个窄门收,"不可协商"意味着它对门外的东西也不协商。你真正想要的东西通常是比那扇门宽一圈的,差的那一圈就是这次缴的税。

我到现在也没找到一个干净利落的解法。能想到的就是两条:验收标准尽量往"我真的想要的东西"上写,而不是往"我好写的那部分"上写;还有,循环每次停下来,我不光看它改了什么,还看一眼它是在哪一行断言上停的,那行断言是不是我真正在乎的那行。

扯远了,收一下。

一条规则

检查器不是另一个 LLM 的循环,才允许无人值守地跑;检查器是另一个 LLM 的循环,必须挂在我眼皮子底下跑。

第二个 agent 可以有。让它写注释、让它把 diff 用中文给我讲一遍、让它列几个它觉得可疑的点,都行。但它不许有终止权。它没有能力判断"对",它最多能判断"眼熟"。

本期缴税:一次 UTC 换本地时区,换来一条"联署不等于检查"。这个税我还得再交几回才能记住。

阿舟
阿舟

写代码写到一半开始怀疑人生,靠 AI 工具续命,顺手把踩过的坑都记下来。

查看主页 →