跳到主要内容

238 篇博文 含有标签「reliability」

查看所有标签

停不下来的 Agent:作为运行时故障模式的范围蔓延

· 阅读需 9 分钟
Tian Pan
Software Engineer

你让智能体修复一个不稳定的测试。第三分钟,测试通过了。第四分钟,智能体正在读取相邻文件。第九分钟,它“改进”了一个测试从未触及的辅助函数,为了清晰起见重命名了一个无关的参数,并开始对 fixture 构建器进行重构。最终提交的 diff 涉及 12 个文件和 400 行代码。原始 Bug 修复了,一些原本没坏的代码也顺便被“修复”了。

这不是模型感到困惑,而是模型完全按照指令留下的空间在行事。任务要求“修复 Bug”,但并没说“修复后就停止”。大多数智能体循环都有明确的起点和成功标准,但对第三个问题却含糊其辞:你什么时候结束?在聊天会话中,“结束”是由用户决定的。在自主循环中,“结束”是由停止条件决定的,如果你没写停止条件,那停止条件就是“模型失去了兴趣”。这不属于你可以调试的故障模式,而是一种你必须通过设计来消除的故障模式。

你的智能体没读过的那条休假自动回复

· 阅读需 9 分钟
Tian Pan
Software Engineer

凌晨两点,你的客服智能体呼叫一位真人。那位真人已经请假一周了。休假自动回复就躺在智能体正在读取的同一个邮箱里。智能体仍然 ping 了过去。自动回复弹了出来。智能体礼貌地道了声谢,然后又 ping 了一次——因为回复里没有它在等的那个工单解决码。十二个循环之后,另一支团队的人偶然发现这个未读会话已经堆了六十条消息,才手动去把值班的人叫醒。

智能体完全照着 prompt 说的做了。Prompt 让它升级给一个人。这个"人"在它眼里只是一个字符串,不是一个角色。这个字符串不知道什么叫 PTO。

供应商速率限制是你从未编写过的容量计划

· 阅读需 10 分钟
Tian Pan
Software Engineer

当你的应用程序第一次从模型供应商那里收到 429 错误时,发生了一些重要的事情,但几乎没人注意到。并不是错误本身,而是接下来执行的那行代码。也许你的 HTTP 客户端会以指数退避进行重试。也许它会降级到更小的模型。也许它会将请求排队,或者直接丢弃,又或者显示一个永远无法解决的加载动画。无论它做什么,这种行为现在就是你的容量策略。它决定了当供不应求时,哪些用户能获得服务,哪些用户的体验会降级。

而且,几乎可以肯定你并没有亲自制定过这个策略。它是由编写 SDK 封装的人、重试装饰器,或者是某人从教程中复制的三行 try/except 代码决定的。在负载下,你的系统中最重要的决策——当无法兼顾所有任务时该怎么办——正由一段没人审视过的代码作为容量决策来执行。

这篇文章的观点是,你应该把这段代码视为它的真实面貌:一个负载削减策略和一个容量计划,而不是一个错误处理器。429 并不是问题所在。问题在于你已经将系统在资源竞争下的行为设计,外包给了库的默认设置。

你的 Agent 端点是一个伪装成函数调用的分布式系统

· 阅读需 10 分钟
Tian Pan
Software Engineer

现代 AI 应用中最危险的一行代码看起来完全无害:

result = await agent.run(user_query)

它读起来就像一个函数调用。它有名称,接受参数,返回数值。你的 IDE 会自动补全它。你的类型检查器也觉得没问题。然而,就在这个单一的 await 背后,隐藏着一个远程的、多跳的、部分失效的分布式系统,而它却套着本地过程的语法外壳。代码看起来的样子与它实际表现出的行为之间的鸿沟,正是大多数生产环境 Agent 事故发生的地方。

你的智能体没有营业时间的概念

· 阅读需 10 分钟
Tian Pan
Software Engineer

一家中型 SaaS 公司的支持智能体正确处理了一起计费纠纷。它读取了工单,检查了客户账户,发现了重复收费,执行了退款,并发送了一封礼貌的确认邮件。每一步都是正确的。唯一的问题是时间戳:客户所在时区的凌晨 3:14。客户在睡梦中醒来看到退款通知,以为自己的信用卡被盗刷了,在公司有人醒来解释之前,就向银行提交了欺诈申诉。

在那个工作流中,没有任何环节是传统意义上的 Bug。智能体没有产生幻觉,没有选错账户,也没有算错退款金额。它只是完全不知道凌晨 3 点是一个告知别人资金变动的糟糕时间。这个模型读过的人类睡眠习惯相关文本比世界上任何人都多,但它的行为表现仍然像是在对待一个随时待命的服务端点,只要你调用它,它就是清醒的。

那些由于模型选择了不同的 Token 而无法复现的 Bug

· 阅读需 11 分钟
Tian Pan
Software Engineer

用户提交了一个 bug。你的智能体生成的摘要掉了一段关键内容,或者 JSON 返回格式错误,或者回答一本正经地胡说八道。你打开工单,复制请求,然后重放(replay)。结果正常。你又重放了一次。依然正常。于是你把工单标记为“无法复现”并继续处理其他事情。

Bug 依然在那儿。真实用户依然在遇到它。你之所以关闭工单,是因为你的调试工具链默认了固定的输入会产生固定的输出——而你正在调试的组件实际上是从概率分布中进行采样的。

你从未注入过的故障:给你的 Agent 提供一个说谎的工具

· 阅读需 11 分钟
Tian Pan
Software Engineer

打开你的智能体(agent)韧性测试套件,看看它实际上在测试什么。你会发现超时。你会发现连接中断、500 错误、频率限制响应、格式错误的 JSON,也许还有一个在失败前卡死三十秒的工具。所有这些都是经典模式下的故障注入:工具坏了,问题在于你的智能体是否能优雅地降级。

现在找找看那个工具完全没坏的测试。那个工具在 80 毫秒内响应,返回了完全符合 schema 的有效 JSON,但里面的值纯粹是错的。一个过期了三天的余额。一个交换了两个字段的客户记录。一个两位数移位的订单数量。一个本应返回四十行却返回空的查询结果列表。

你找不到它。几乎没有人注入过这种故障。而这正是你的智能体最无法抵御的故障,因为所有其他故障都会自我宣告,而这种故障不会。

点对了按钮但点错了屏幕的 GUI Agent

· 阅读需 12 分钟
Tian Pan
Software Engineer

一个计算机使用智能体拍摄了一张截图,对其进行推理,决定点击像素点 (840, 612) 处的“确认”按钮,并发送了点击指令。当光标落下时,一个弹窗出现了。三秒钟前还是“确认”的像素点,现在变成了“删除”。该智能体完全按照计划执行了操作。但它的计划是针对一个已不再存在的屏幕制定的。

这不是定位(grounding)错误。模型正确识别了按钮。这也不是推理错误,计划本身是合理的。这是一个时序错误(timing error)——这是 GUI 自动化中监测最不足的失败类别——而你的测试套件几乎肯定没有覆盖它,因为你的测试环境在观察和行动之间从未发生过变化。

一个令人不安的测量结果:最近一项针对真实 Ubuntu 工作负载下的桌面智能体的研究发现,从智能体观察屏幕到基于该观察采取行动之间,平均存在 6.51 秒 的间隔。对于 UI 来说,6.5 秒是漫长的永恒。通知会弹出,懒加载列表完成加载,动画趋于稳定,焦点发生转移。智能体对屏幕的心理模型是有保质期的,但几乎没有智能体框架会这样对待它。

你的重试逻辑正在给 Agent 传达错误的教训

· 阅读需 11 分钟
Tian Pan
Software Engineer

一个工具调用失败了。你的 Agent 框架使用指数退避(exponential backoff)重试了三次。第三次尝试成功了。追踪记录(trace)显示一个绿色的对勾。没人收到报警,错误计数器没有增加,用户得到了他们的答案。根据你所有的仪表盘,系统运行正常。

事实并非如此。工具失败是因为 Agent 传递了一个格式错误的参数,而第三次尝试之所以成功,仅仅是因为 Agent 在每次采样时表现不同,刚好在第三次尝试时正确表述了调用。你并没有从瞬时故障(transient fault)中恢复。你只是在玩老虎机直到它中奖,然后记录下中奖结果,并扔掉了那两次告诉你 Agent 已经坏掉的拉杆记录。

这就是重试逻辑悄悄腐蚀 Agent 系统的方式。重试是为“调用者正确且网络不稳定”的世界设计的。而 Agent 颠覆了这个假设:网络通常是正常的,而调用者才是不可靠的部分。当你把为第一种世界构建的重试策略应用到第二种世界时,它就不再是一种恢复机制,而变成了一种将 Bug “洗”成绿色对勾的手段。

记住你 Bug 的智能体:为什么修复 Bug 是一次内存失效事件

· 阅读需 11 分钟
Tian Pan
Software Engineer

几个月前,你的一个下游 API 返回了一个格式错误的时间戳——在应当显示毫秒的地方返回了秒,或者在 Schema 承诺返回字符串的地方返回了 null。你的智能体(agent)遇到了这个问题,分析了故障原因,并制定了一个修复方案:乘以 1000,或者回退到默认值,或者使用不同的端点重试。它解决了眼前的麻烦。然后,它做了一件产生深远影响的事:它记下了这个变通方案(workaround)。

也许它在长期记忆中保存了一条笔记:“计费 API 返回的时间戳单位是秒;使用前需转换。” 也许这次交互被卷入了一个微调(fine-tuning)数据集,于是这个变通方案变成了一个习得的反射行为。无论哪种方式,智能体现在都对世界产生了一种认知。而就在上周,API 团队发布了一个修复补丁。现在时间戳正确了。但没人告诉这个智能体。

无人清理的审批队列

· 阅读需 10 分钟
Tian Pan
Software Engineer

你做了负责任的事。你检查了你的 agent,识别了那些可能造成真正损失的操作——发放退款、删除记录、发送外部邮件、部署配置更改——并将它们路由给人工审批。风险分级门控(Risk-tiered gating)。教科书级的做法。评审委员会也签署通过了。

然后,三周后收到了一个客户投诉:一个 agent 任务自上周二以来一直处于“进行中”。没有失败。没有报错。只是停留在一个人工审批队列中,而事实证明,根本没人在关注那个队列。Agent 完成了它的工作,将危险操作挡在门后并等待。而这扇门没有负责人。任务在没有仪表盘指向、没有警报触发的地方默默地老化。

智能体从未接收到的服务降级信号

· 阅读需 10 分钟
Tian Pan
Software Engineer

当下游 API 开始出现波动时,人类操作员在任何事情真正崩溃之前,就会通过十几种方式察觉到。状态页变为黄色。变更日志邮件飞进收件箱。提供商的仪表板上出现警告横幅。值班频道因有人在日志中发现 429 错误而热闹起来。队友发帖询问:“还有人看到写入变慢吗?”这些都不是对请求的响应。它们是围绕 API 的环境运行信号,人类几乎是被动地吸收了这些信号。

调用同一个 API 的智能体(agent)只收到一样东西:它刚刚发出的请求的响应。状态码、Header、Body。这就是全部的渠道。它没有收件箱,没有仪表板,没有 Slack,没有外围视野。它察觉不到最后十个调用每个耗时都是之前十个的两倍。它读不了状态页,因为没人给它 URL,它也没有查看状态页的常规指令。当依赖项降级时,智能体是系统中最后一个知情方——而且它通常是通过失败才知晓的。

这种不对称性并非模型能力问题。更聪明的模型也解决不了。智能体对运行信号是盲目的,因为底层设施(plumbing)从未传递过这些信号,而且大多数智能体架构在出厂时,甚至没人注意到缺失了这些底层设施。