跳到主要内容
94% 的时间它在打同样四个接口,剩下那 6% 才要命

94% 的时间它在打同样四个接口,剩下那 6% 才要命

阿舟
阿舟

· 阅读约 8 分钟

有人在 dev.to 上干了件我一直想干又没动手的事:把一个对外自称 agent 的系统摁住,盯一段时间,看它到底在干什么。结论朴素得很——94% 的时间它在按同一个顺序打同样四个接口,剩下 6% 在重试。

A -> B -> C -> D -> done
on failure -> retry(A)

画外音:这不是 agent,这是一条披着对话外衣的流水线。

作者是 James Anderson,这篇接着上一篇写。上一篇我读过,主题一句话——大部分挂“AI agent”招牌的东西,是装了 LLM 调用的固定流水线。骂得爽。爽完手里是空的:知道它是马甲,然后呢?我自己的系统该不该上 agent,照旧靠拍脑袋。

这篇跟进不一样,它开始算账。核心那句我抄下来了:自主性是一笔要还的成本,不是一项默认能力。翻成人话——你得先证明这条流程非自主不可,而不是先上了再想办法解释。

这句我认。而且我认为它是整轮讨论里唯一能带回工位的东西。

上次那次数据库迁移之后,我给自己立过一条:凡是能写成 if 的,就别让它自己“想一想”。问题是这条规矩只有方向没有边界。什么算“能写成 if”,全靠手感。手感这东西,上线前都挺准,上线后谁说得准。

这次算是把那条线往下画了一点。

作者给了三条。我按自己的理解重新压了一遍,所以顺序和权重跟原文不完全一致,这个我认。

第一条,环境得会回话。意思是下一步取决于一个系统不动作就永远拿不到的回应。他举的例子是有活人在对面的场面——谈判、教练、陪聊,回复分叉你猜不到;还有 API、爬虫撞上改版的 DOM、以及那些会莫名其妙挂掉的工具。

我第一反应是“那我不沾”,第二反应是觉得这条写反了。API 超时返回 429,这算不算回话?算。但 429 是我提前就能列出来的回应,所以不算。他在测试标准里自己补了这刀:如果对话本身只是固定的几个工具调用,那环境就没在回话。所以这条的严格版本不是“环境有反馈”,是“环境的反馈长什么样,你提前写不出来”。差别很大——前者满大街都是,后者我几乎没见到。

第二条,路径是发现出来的,不是设计出来的。例子是开放式 debug:第二步取决于第一步吐出来的 traceback。测试标准我觉得是三条里最利落的——这条路径里,能不能出现一条没人写过的?如果每条路径都能倒推回你自己写的代码,那你就不是在产生分支,你是穿着 agent 的马甲在写 if。

这条我熟,也栽过。我手上有个跑爬虫的活,站点改版之前我一直觉得它就是“环境会回话”的典范:DOM 一变,它自己想办法。后来我认真数了数它那段时间真做过的判断,八成全是我能提前写死的——找不到就换 selector,换不到重试,重试两次告警。

for sel in ["#price", "#price-v2", "[data-testid=price]"]:
    if hit(sel):
        break
else:
    alert_and_skip()

剩下那两成,我到现在也没完全想明白该不该算“发现”。所以我用了最怂的办法:八成写成上面那样,两成留给它。这个后面还要说。

第三条,分支空间得真的没法穷举。不是“三个 case 的 if”。工具 A、失败调 B、再不行升到 C、退避重试、五个已知路由——在他看来这些全算 pipeline,因为分支是提前列好的。

读到这儿我停了一下,把前两条跟第三条对着看,发现它们其实是同一句话的两种说法。环境不会回话,分支就必然是提前列好的;路径是设计出来的,分支空间就必然可穷举。反过来也成立。

所以我把三条压成了一条半:这条流程的下一步,我事先写不写得出来;以及如果写不出来,那块写不出来的面积有多大

要不要把人家三条砍成一条半,我纠结了一会儿,有点抬杠的意思。但这不是抬杠——条件少一条,判断成本就低一截,我才可能在每个 PR 上真去用它,而不是读的时候点头、写的时候忘掉。清单超过三条我就记不住,这是我的问题,不是清单的问题。

最小自主面这条他说得比我狠:就算满足了条件,也要把自主的面积压到最小的那个点上。只把真正需要运行时判断的那一个决策交给模型,其余全用确定性编排和可测的代码兜住。设计问题不是“要不要 agent”,是“决策边界画在哪个节点”,而答案通常是一个节点,不是一整张图。

他还补了句挺老实的:按这个设计做出来的“真 agent”,身体大部分仍然是 pipeline。

我读到这儿反而松了口气。因为这意味着我不必在“上 agent”和“不上 agent”之间二选一,我可以在一个函数里上。爬虫那个活我就是这么收的——整条流程是死的,只有“换不换选择器”这一个点是活的,其余全是硬编码。

前面那三条我大致能接受,也能在评审会上拿去问别人。但它们有个共同点:都能在白板上讨论。白板上讨论不出来的东西,才是我凌晨两点会撞上的东西。

有个评论写了一句大意是,没有可观测性的自主,就是按溢价买来的不确定性。这句可以直接贴我显示器上。

另一个接得更要命:验证不仅要有记录,还得记下来是谁验的。一个没有署名的批准,等于没有批准。

这句我认得有点疼。因为“agent 说它验过了”这种话,我说过不止一次。晨会上一句“那个修复验证通过了”,讲得跟真的一样,回头出事你问是谁验的——是它自己。它给自己的改动当裁判,我在旁边鼓掌。

所以我现在给每条自主分支加了个字面上的要求:它得能报出四样东西。

observation   它看见了什么
action        它选了什么动作
guardrail     它凭什么觉得自己可以这么做
fallback      它不行的时候退回哪

少一样,这条分支就没资格自己拍板,我把它降级成 if。这个测试的好处是它不需要任何架构讨论——打开日志,看它说不说得出来。说不出来就是说不出来,没有解释空间。

坏的 trace 长这样,我见过不止一次:

{"action": "cleaned_up", "reason": "model decided"}

这不叫 trace,这叫“相信我”。多出来的那行 reviewer 才是我要的,而且它得是个人名,不是一个 agent id。

作者后来也认了这条,说验证得排在条件零:一条验证不了的自主分支,在讨论它够不够开放之前就已经挂了。这个排序我同意,甚至觉得它是全文最重要的一处修正——前面那些条件决定“你该不该上”,条件零决定“你上了会不会出事”。顺序不一样。

底下还补了个工程细节,我觉得比上面这些都实在:每条自主决策除了那四样,还得带一个能扛过重试的幂等键。否则你分不清一个动作是跑了两遍,还是跑了一遍被记了两笔。这坑我不算陌生——上次迁移那档子事,表面上是它多删了索引,根子上是我没有任何一个地方能说清楚“这个动作到底执行了几次”。

还有个评论提了个我没想过的指标,分叉率。大意是,什么时候才值得上真 agent,界线在这里——连重试策略本身都需要判断了,那 6% 才变成真正的分叉。这条线以下,agent 是纯粹的开销;这条线以上,写死 pipeline 是不承认现实。他问的是,怎么在选架构之前先把分叉率量出来。

作者回他说,这是他见过最锋利的一个提法。我也觉得锋利,但想补一句不太好听的:分叉率这东西,不先把日志和 trace 铺上去,是量不出来的。也就是说,“先量了再决定要不要上 agent”这条看起来最理性的路,前提恰恰是先上一套只为观测、不做决策的 agent 骨架。你想省的那笔钱,得先花出去。

有人泼了盆冷水,说可验证这条在实践里最难,因为不是每个自主决策都配得上一个干净的绿勾。一半同意。确实不是每条都有绿勾——DOM 改版这种事,“修好了没”本身就带主观。但我不觉得这是“所以可以放宽”的理由,这是你欠下的债。判断标准不好写,说明你还没想清楚这个决策该怎么验,而不是它不用验。真要放宽,也该是显式写进设计文档、并且有人签字的那种放宽。

一个决策能不能被一个署了名的人复核,决定它配不配自主。剩下的都是能力问题,能力问题早晚能解决,责任问题不能。

至于那三条架构条件,我不是不信,是觉得它们得排在后面。先翻日志看有没有那个署名,再谈要不要上 agent。反过来的顺序我踩过,代价不便宜。

如果你手上真有一个满足这几条的生产例子——不是 demo——说出来,我请你喝咖啡。demo 就算了,我见过太多 demo,它们都很有礼貌,从不报错。

阿舟
阿舟

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

查看主页 →