跳到主要内容

那件风衣,其实穿在我自己身上

阿舟
阿舟

· 阅读约 7 分钟

上周翻一份旧 trace,翻到一半我把杯子放下了。

那是我去年写的 agent。planner、tool calling、reasoning loop 一套齐全,演示那天丝滑得不行,同事看完说这玩意儿能上。上线两周,同一个工单编号,周一跑出 A,周四跑出 B,周三那次直接超时。

三个晚上排查。最后不是靠调试器,是靠 grep 日志看出来的——所有成功跑完的 trace,路径都长一个样:

extract -> transform -> respond
extract -> transform -> respond
extract -> transform -> respond
...(四千多条,没有第二种)

出错的也没走别的路,就是这三步里某一步重试了几次。

我给它装了整整一个推理循环,它一次都没用过。

画外音:那天我把日志滚到底,愣是没找到一条能算"它自己决定"的痕迹。

后来刷到 dev.to 上 James Anderson 那篇,把这类系统比作穿着风衣的 if 语句。评论区有人统计了 400 条生产 trace,93% 走的是完全相同的提取、查询、响应三步,剩下 7% 是错误重试。数字跟我的几乎撞上。

我不孤独,但我也高兴不起来。

那个比喻我觉得反了

真正穿风衣的不是 agent。是我们。

翻领上绣着"这是智能体,它自己会决定下一步",风衣底下藏着的,是我没画流程图这件事。这里的因果得捋清楚:一旦你管一个系统叫 agent,画流程图这道工序就名正言顺地免了——它自己会决定嘛,画什么流程图。所以这个词最贵的地方,不是让人高估机器,是让人提前放过了自己。

评论区里那句"能在运行前画出流程图就不是 agent",被另一个人说是该钉在每个 AI 框架仓库的 README 顶部。我同意,但我觉得它还差半步——你能画出来,不代表它实际跑的就是那一条。这两件事之间的缝隙,就是我那三个晚上。

那个 original 作者自己承认,他没保留任何路径计数器。他说如果当时加了,第一天就会显示偏差约 0%,伪装当天就会被戳穿。

这句是整个帖子里最诚实的一句话,也是代价最高的一句。

加计数器要几行代码?

def record_path(steps: list[str]) -> None:
    metrics.incr("agent.path", tags={"path": ">".join(steps)})

就这。三个晚上换三行代码,这个比例我到现在都没消化。

为什么没人加。因为加上之后,那个"智能体"的智能当场就现原形了。你花了两个月搭出来的编排层,展示出来的是一条直线加几个重试。没人愿意主动伸手去拿这个数。我也一样——不是不会写,是没想过要写。演示那天它丝滑,我就默认它在做事。

"非确定性"被当成了免罪符

有个评论我觉得被严重低估了。大意是:agent 脚本本身是确定性的,随机性是它调用的 LLM 借给它的。

我去年不是这么想的。去年我把"非确定性"当成了这个架构的原罪,然后自己认了——非确定性嘛,测不了,只能靠线上观察。这个念头害了我半年。

不是的。控制流是我写的。分支条件是我写的。循环什么时候退出是我写的。不确定的只有中间那几个函数调用的返回值:

def classify(ticket: str) -> Category:
    raw = llm_call(CLASSIFY_PROMPT, ticket)   # ← 唯一不确定的地方
    return Category.model_validate_json(raw)  # ← 硬校验,不合法就抛

非确定性不是弥漫在整个系统里的雾,它被圈在几个格子里。圈起来的东西是可测的——固定一百条输入喂进那个函数,看输出分布,看有没有 schema 兜得住。真正测不了的是我让它自己决定下一步走哪,因为那等于把控制流也变成了随机变量。

所以解不是"给 agent 补测试",是"把随机性关进笼子,然后给笼子写测试"。顺序反了,怎么补都是白补。

装饰性推理

评论区里我最想单独拎出来的是这么一条:有人把规划和反思那两步迁到自托管的 4B 小模型上,成本又降了约 80%,而 trace 的多样性纹丝不动。

这句话的意思是——规划步骤的输出,跟你用哪个模型无关。因为不管用哪个,它每次输出的东西都几乎一样。你花在大模型上的那笔钱,买的是一段复读。

验证方法很土,我现在的脚本里就这几行:

outs = {hashlib.sha256(llm(p).encode()).hexdigest()[:8] for _ in range(100)}
print(len(outs))   # 12?3?还是 1?

如果你的 planner 步骤跑一百次,得到的是一个或两个 distinct hash,那它没有在推理,它是在念台词。你的 reasoning loop 是装饰性的,它的作用是让架构图好看。

我这里纠结过要不要把话说这么满——毕竟我只在自己的场景里验过。但既然我那个 planner 就是这么被我删掉的,我保留这个判断:大多数被叫 reflection 的步骤,输出是常量。

真正的问题不是名字,是没人量过。

把系统叫错名字只是个命名的毛病。命名的毛病通常不致命,除非它顺手替你把度量也省了。

那 400 条 trace 那个数据,比任何架构争论都说明问题。93% 走固定路径,剩下 7% 不是创造性的决策,是错误重试。也就是说,那套系统里"自主"部分的实际贡献是——重试。这事你写个 while 加 backoff 也能干。

所以我现在看一个自称 agent 的系统,不看它的编排图画得多花哨,只问一个问题:你把每一步的输入输出 hash 下来跑过一百次吗? 没跑过的,其余都先别谈。

那什么才真需要 agent

我认的场景不多:开放式研究、调一个未知的 bug、多跳任务、分支空间没法提前枚举的活。

但即便在这几个场景里,我现在也主张把自主性压到最小。原帖作者最后收口的方式我完全同意——只留一个 LLM 步骤处理真正模糊的输入,大概 5% 到 8%,剩下的怪情况直接转人工。别指望它在模糊地带临场发挥,它发挥出来的大概率是你没预料到的那种。

还有一条我要单独加:LLM 步骤只读不写。评论区有人提醒过,如果自由 agent 在第 3 步改了状态,比如写库或者打外部 REST,第 7 步又逻辑漂移了,那就是一场没有事务回滚的分布式状态污染。这个坑我在别的项目上缴过智商税,不想再交第二次。要写,走确定性代码,把动作从模型手里拿走。

最后

我重写完之后其实有点失落。线性流水线没什么可讲的,跑得快,便宜,可测,可断言,但它没有一段能在架构评审上拿去讲五分钟的故事。

这可能才是大家不肯画流程图的真实原因。不是因为不知道,是因为画出来太朴素。

所以现在的规矩只有两条:

一,写任何自称 agent 的东西之前,先把流程图用纸笔画出来,拍张照。画得出来的一律老老实实写流水线,画不出来的才有资格往下谈。

二,给每一个 LLM 调用点标一个数字——同一份输入跑一百次,几种输出。标不出来的,就是还没测过。

那条 5% 的模糊输入通道我留着。也许哪天我真的会需要它 😅。在那之前它就挂在那儿,像件不合身的风衣,我偶尔拿出来看一眼,提醒自己别乱穿。

阿舟
阿舟

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

查看主页 →