上周翻一份旧 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% 的模糊输入通道我留着。也许哪天我真的会需要它 😅。在那之前它就挂在那儿,像件不合身的风衣,我偶尔拿出来看一眼,提醒自己别乱穿。