那些由于模型选择了不同的 Token 而无法复现的 Bug
用户提交了一个 bug。你的智能体生成的摘要掉了一段关键内容,或者 JSON 返回格式错误,或者回答一本正经地胡说八道。你打开工单,复制请求,然后重放(replay)。结果正常。你又重放了一次。依然正常。于是你把工单标记为“无法复现”并继续处理其他事情。
Bug 依然在那儿。真实用户依然在遇到它。你之所以关闭工单,是因为你的调试工具链默认了固定的输入会产生固定的输出——而你正在调试的组件实际上是从概率分布中进行采样的。
用户提交了一个 bug。你的智能体生成的摘要掉了一段关键内容,或者 JSON 返回格式错误,或者回答一本正经地胡说八道。你打开工单,复制请求,然后重放(replay)。结果正常。你又重放了一次。依然正常。于是你把工单标记为“无法复现”并继续处理其他事情。
Bug 依然在那儿。真实用户依然在遇到它。你之所以关闭工单,是因为你的调试工具链默认了固定的输入会产生固定的输出——而你正在调试的组件实际上是从概率分布中进行采样的。
传统的容量规划(capacity planning)建立在一个默认的假设之上:请求在大体上是可以互换的。Web 服务器处理登录、搜索、结账 —— 尽管这些操作有所不同,但它们的差异都在一个范围之内。你衡量每秒请求数(RPS),观察 p50 和 p99 延迟,乘以安全系数,然后进行资源配置。这个模型之所以有效,是因为工作的基本单位 —— 单个请求 —— 具有稳定的成本。
Agent(智能体)的工作负载从根本上打破了这个假设。你对 Agent 的一个查询可能通过一次简单的生成就解决了:300 个 token 输入,200 个输出,两秒钟搞定。但下一个查询,表面上看起来一模一样,却可能触发一个规划步骤,分发出 40 个工具调用(tool calls),在每一轮对话中重新读取不断增长的上下文,并在四分钟内消耗掉 120 万个 token。同样的端点(endpoint),同一个用户,同一条代码路径。单个请求的成本差异可能达到三个数量级,而且请求中没有任何信息能预先告诉你接下来会遇到哪种情况。
打开任何一份 AI 功能评论,你都会听到关于这三个数字的辩论:延迟、Token 成本和准确率。有人调出 P95 图表,有人计算单千次请求的成本,还有人争论评估分数 (eval score) 已经好到可以发布了。没人提到能源。没人提到碳排放。正因为没人提及,该功能的环保足迹仍然被决定了——由谁赢得了那场关于金额的争论所含蓄地决定。
这就是 AI 可持续性中沉默的问题。并不是团队故意选择了高碳设计,而是他们根本没有做出选择。碳足迹成了成本决策的副作用,而成本与碳排放之间仅存在松散的关联。在支出仪表盘上看起来大获全胜的路由规则,可能会悄无声息地让排放量翻倍,而会议室里没人会知道,因为那个能告诉他们真相的数字从未出现在仪表盘上。
这篇文章将能源和碳排放视为它们的真实身份:AI 系统的一个可测量、可掌控的属性,与延迟和成本处于同等地位。它不是企业价值的脚注,而是一个明细项。
一个工具调用失败了。你的 Agent 框架使用指数退避(exponential backoff)重试了三次。第三次尝试成功了。追踪记录(trace)显示一个绿色的对勾。没人收到报警,错误计数器没有增加,用户得到了他们的答案。根据你所有的仪表盘,系统运行正常。
事实并非如此。工具失败是因为 Agent 传递了一个格式错误的参数,而第三次尝试之所以成功,仅仅是因为 Agent 在每次采样时表现不同,刚好在第三次尝试时正确表述了调用。你并没有从瞬时故障(transient fault)中恢复。你只是在玩老虎机直到它中奖,然后记录下中奖结果,并扔掉了那两次告诉你 Agent 已经坏掉的拉杆记录。
这就是重试逻辑悄悄腐蚀 Agent 系统的方式。重试是为“调用者正确且网络不稳定”的世界设计的。而 Agent 颠覆了这个假设:网络通常是正常的,而调用者才是不可靠的部分。当你把为第一种世界构建的重试策略应用到第二种世界时,它就不再是一种恢复机制,而变成了一种将 Bug “洗”成绿色对勾的手段。
每天,你的智能体(Agent)都会把失败案例打包成“礼物”发还给你。一名用户点击了“踩”。另一名用户读完答案后一言不发,直接关掉了标签页。第三名用户将同一个问题改写了三次,直到智能体终于答对。每一个都是带标签的失败案例 —— 真实的输入、真实的上下文、系统失误的真实时刻 —— 由那些最希望系统运行正常的人免费提供给你。
大多数团队都会把这些信息全部丢掉。并非故意为之。点击“踩”只是增加了仪表盘上的一个计数;放弃使用表现为留存图表中的一次下滑;改写问题看起来就像普通的日常使用。没有任何东西能将信号及其产生的上下文一并捕捉,因此也就无法进行回放、分选(Triage)或转化为测试用例。你所拥有的最丰富的评估数据源正擦肩而过,而团队却还在继续手动编写合成的评估(Eval)案例。
这就是你从未构建的智能体反馈循环。它不是你忘记购买的某种工具,而是一条流水线 —— 从用户信号,到分选后的失败案例,再到新的评估案例 —— 它之所以未能建立,与技术本身关系不大。
询问你的智能体上季度的流失率,它会用一个简洁的句子回答 4.2%。这个数字看起来很合理。周围的文字描述也显得很有信心。然而,当有人最终检查仪表盘时,显示的却是 6.8%。智能体根本没有进行任何查询——它只是产生了一个符合“流失率”特征的 Token 序列,因为对于语言模型来说,口述一个数字和计算一个数字在输出过程中看起来是一模一样的。
这是一种能躲过所有 Demo 的隐形失败模式。一个虚构的工具名称会抛出你可以捕获的错误。一个格式错误的参数会通不过 Schema 校验。但是,一个表达流畅的虚构 数值,会穿透你的整个流水线,看起来与真实数值毫无二致。没有异常,没有日志记录,没有红字。唯一的错误信号是某个恰好知道正确答案的人——而使用智能体的初衷本就是为了让人们不必亲自去查。
关于 Agent 可观测性的标准建议只有三个词:记录完整 trace。捕获每一次工具调用、每一个 prompt、每一条模型响应、每一次内存读写。团队照做了。接着第一个真实故障发生了,工程师打开 trace,发现它有 40 层工具调用深,20 万个 token 宽。从技术层面看,trace 是完整的;但从实践层面看,它完全不可读。
接下来是熟悉的仪式。工程师不断滚动屏幕。他们展开一个 span,看到 5 万个字符的 JSON,折叠它,再次滚动。十分钟后,他们终于找到了那个模型选错工具的回合——它被埋在 37 个完全符合预期的回合之间。原本旨在让故障清晰可见的 trace,反而增加了排查成本。
你做了负责任的事。你检查了你的 agent,识别了那些可能造成真正损失的操作——发放退款、删除记录、发送外部邮件、部署配置更改——并将它们路由给人工审批。风险分级门控(Risk-tiered gating)。教科书级的做法。评审委员会也签署通过了。
然后,三周后收到了一个客户投诉:一个 agent 任务自上周二以来一直处于“进行中”。没有失败。没有报错。只是停留在一个人工审批队列中,而事实证明,根本没人在关注那个队列。Agent 完成了它的工作,将危险操作挡在门后并等待。而这扇门没有负责人。任务在没有仪表盘指向、没有警报触发的地方默默地老化。
当下游 API 开始出现波动时,人类操作员在任何事情真正崩溃之前,就会通过十几种方式察觉到。状态页变为黄色。变更日志邮件飞进收件箱。提供商的仪表板上出现警告横幅。值班频道因有人在日志中发现 429 错误而热闹起来。队友发帖询问:“还有人看到写入变慢吗?”这些都不是对请求的响应。它们是围绕 API 的环境运行信号,人类几乎是被动地吸收了这些信号。
调用同一个 API 的智能体(agent)只收到一样东西:它刚刚发出的请求的响应。状态码、Header、Body。这就是全部的渠道。它没有收件箱,没有仪表板,没有 Slack,没有外围视野。它察觉不到最后十个调用每个耗时都是之前十个的两倍。它读不了状态页,因为没人给它 URL,它也没有查看状态页的常规指令。当依赖项降级时,智能体是系统中最后一个知情方——而且它通常是通过失败才知晓的。
这种不对称性并非模型能力问题。更聪明的模型也解决不了。智能体对运行信号是盲目的,因为底层设施(plumbing)从未传递过这些信号,而且大多数智能体架构在出厂时,甚至没人注意到缺失了这些底层设施。
你发布了一个模型升级。评估套件的分数从 87% 提升到了 91%。更新日志信手拈来,领导层纷纷鼓掌,然而那些真正重要的仪表盘——用户满意度、工单升级率、点踩率——却毫无动静。毫无起色。甚至可能略微变差了。
这是 AI 工程中最令人迷惑的失败模式之一,因为表面上没有任何东西坏掉。评估运行正常。数字是真实的。在你测试的 600 个示例中,模型确实有所改进。问题在于,这 600 个示例是你构建套件那一周的流量快照,而在那之后的几个月里,你的用户已经走出了画面。
这张故障工单具有只有真实事故才具备的典型特征。在 02:14,支持代理关闭了一个本应进入 30 天宽限期的客户账户。客户发现了。工单落到你的桌面上,“复现步骤”一栏下面只有一行字:未知。
你打开追踪记录。你看到代理调用了 close_account 而不是 set_grace_period。你看到工具执行成功了。你看不出的是模型为什么选择了那个分支 —— 而且当你通过同一个代理重新运行同一条客户消息时,它做出了正确的选择。做了两次。现在的事故复盘报告中,原本该写根本原因的地方出现了一个段落大小的空洞,而你唯一能诚实写下的只有“无法复现”。