跳到主要内容

278 篇博文 含有标签「reliability」

查看所有标签

那个因等待另一个 Agent 而死锁的 Agent

· 阅读需 10 分钟
Tian Pan
Software Engineer

一个研究员智能体向一个检索智能体请求一份文档。检索智能体在任务执行到一半时,决定需要研究员智能体澄清查询意图后才能继续搜索。而正在等待文档的研究员智能体,在拿到文档之前不会做出响应。它们谁都没有出错,也没有陷入死循环。它们只是都在礼貌地、无限期地等待着对方——而你的编排器(orchestrator)根本没有“这两个智能体正在互相阻塞”的概念,它会一直愉快地维持这个状态,直到你从未配置过的超时设置最终触发,或者直到某个大活人注意到这个运行已经“进行中”整整 40 分钟了。

这就是死锁(Deadlock)。它是计算领域最古老的故障模式之一,且与你的模型有多聪明毫无关系。它是工作协调方式的一种属性,而不是工作执行方式的属性。过去一年多智能体研究中一个令人不安的发现是:在智能体集群中,大部分出问题的地方都在这里,即协调层,而不是任何单个智能体的推理能力。

单智能体思维永远无法暴露这些 Bug。当一个模型运行工具调用循环时,它最坏的情况也就是空转——而空转至少是肉眼可见的。一旦你拥有两个或更多可以互相等待的智能体,你就继承了分布式系统所有的病理特征:循环等待、活锁、丢包、过早终止、共享状态下的竞态条件。没有人会坐下来决定构建一个分布式系统,但在你添加第二个智能体的那一天,你就已经构建了一个分布式系统。

你的智能体从未执行过的补偿事务

· 阅读需 11 分钟
Tian Pan
Software Engineer

当你的 Agent 执行退款、发送邮件、关闭工单或写入数据行时,该操作便脱离了系统并进入了现实世界。现实世界没有回滚按钮。客户已经看到了退款,收件人已经阅读了邮件。而当 Agent 在三步之后走错了方向,你的恢复计划通常只是复盘会议中的一句话:“我们告诉它以后不要再那样做了。”

“以后不要再那样做”不是撤销(undo)。它是对未来的承诺,却被用来解决过去的问题。令人不安的事实是,大多数 Agent 架构根本没有撤销已完成副作用的机制——不是机制不好,而是根本没有机制。Agent 可以规划、调用工具并重试,但它无法回头。它只有前进挡,没有倒车挡。

你的智能体读不懂的弃用通知

· 阅读需 10 分钟
Tian Pan
Software Engineer

当你为人类开发人员弃用一个 API 时,你会为此举行一整套“仪式”。你提升版本号,在 OpenAPI 规范中添加 deprecated: true,发送 Sunset HTTP 响应头,向开发人员邮件列表发送邮件,发布变更日志,并给人们六个月的时间进行迁移。信号到达阅读它的开发人员,他们提交工单,并在旧路径消失之前更新他们的客户端。

现在,将同样的弃用通知指向一个智能体。调用你工具的模型不会阅读你的变更日志,它不会订阅你的邮件列表。它永远看不到 Sunset 响应头,除非你刻意将其放在模型会查看的地方,即便如此,它也没有可靠的习惯去据此行动。你精心编写的弃用通知落入了一个没有读者的信箱。智能体会一直调用工具的旧形式,直到该形式彻底消失,然后它就会失败——通常是静默失败,通常是在生产环境中,通常是在凌晨 2 点。

这就是为智能体而非人类构建工具时所存在的隐性不对称。我们在 API 演进的二十年里建立的每一项准则,都假设在弃用和迁移之间坐着一个人类。把人类拿掉,整个机制就会失效。

无人值守的人工升级路径

· 阅读需 9 分钟
Tian Pan
Software Engineer

每个智能体架构图都有相同的三个框。一个是“理想路径” (happy path),即模型给出回答,用户满意离开。一个是“自动兜底” (automatic fallback),即置信度低的回答触发重试、调用不同工具或返回预设的“让我查一下”。还有第三个框,通常画在最后且最小,标记为“升级到人工” (escalate to human)。在设计评审时,每个人都会对这个框点头表示认同。它看起来像是某种终结——一个让整个系统变得合理的安全阀。“别担心,如果智能体处理不了,人会接手。”

然后你上线了,却发现那个框是个谎言。不是技术上的谎言——代码能跑,工单能创建,对话会被标记。而是一个人员配置 (staffing) 上的谎言。那个标有“升级到人工”的箭头指向一个无人负责的队列,没有服务等级协议 (SLA),也不在任何人的轮值表上。智能体完全按照指令行事,它把问题抛给了一个从未同意接收问题的组织。

你的智能体忘记发送的幂等键

· 阅读需 10 分钟
Tian Pan
Software Engineer

你的智能体中最昂贵的 Bug 不是幻觉。而是重试。

在你的技术栈中,总有一个工具负责扣款、发送邮件、关闭工单或写入数据行。智能体调用它,调用耗时过长导致超时,于是智能体——作为一个优秀的、具有弹性的软件——再次调用它。棘手之处在于,第一次调用其实已经成功了,只是响应从未返回。结果你扣了客户两次款,无论你如何提示“请在支付时保持谨慎”,都无法阻止这种情况发生。

这是分布式系统中最古老的故障模式,只是披上了一件新装。十年前,我们通过幂等键(idempotency keys)为 HTTP API 解决了这个问题。但大多数智能体技术栈通过将原本为读取设计的重试逻辑直接用于执行写入的工具,且从未发送那个能让重试变得安全的字段,从而重新引入了这个问题。

你从未进行过压测的每分钟 Token 上限

· 阅读需 11 分钟
Tian Pan
Software Engineer

Demo 成功了。Beta 测试也通过了。接着,正式发布带来的流量是平时的十倍,直接撞上了你从未进行过压测的每分钟 Token 配额,而在产品最受瞩目的时刻,所有超出上限的用户都收到了 429 错误。

这是无人排练过的故障模式。团队会偏执地对自己的服务器进行压测——副本、连接池、数据库索引——然后将每个请求都路由到一个存在于别人账户中的供应商配额,而那个上限他们从未真正触及过。限流不是一个在 try 块中捕获的错误。它是一个硬性的产品约束,如果你没有围绕它进行规划,那么产品发布之时,就是你第一次发现坑在哪里的时刻。

无人负责的对话:问责制如何在智能体移交链中消散

· 阅读需 10 分钟
Tian Pan
Software Engineer

一个支持请求发送了进来:“我的订阅被扣了两次费,请取消其中一个并退款。”一个路由代理将其归类为计费问题并进行了移交。一位计费专员查看了账户,确认了两笔扣费,并将退款操作移交给了一个工具调用子代理。该子代理执行了一次退款,随后一个总结代理给客户回信:“搞定了——你的重复扣费已退还。”每一个步骤单独看都是正确的。但实际上客户被扣了三次费,而不是两次。其中一笔重复扣费仍在他们的卡上,而总结代理无从得知这一点,因为当对话传导到它那里时,“ 2 ”这个数字已经在上游的四个步骤中固化成了事实。

让我们进行一次复盘。这是谁的 Bug?路由代理分类正确。计费专员读取了它收到的两笔扣费。工具代理准确地按照指令进行了退款。总结代理进行了准确的总结。单独调出每个代理的 Trace,每一个都通过了。系统失败了,但没有任何组件出错。这就是“无主对话”,它是多代理架构中定义性的操作性失败——不是模型太笨,而是责任在移交链条中不断扩散,直到没有任何一个代理需要对用户实际体验到的结果负责。

嘈杂邻居竟是你:当失控的 Agent 让共享账户中的其他人全都触发 429 错误

· 阅读需 12 分钟
Tian Pan
Software Engineer

事故的开端和大多数此类事件如出一辙:周二下午两点,一个面向客户的功能在生产环境中不断抛出 429 错误。没有新代码部署,没有流量激增,该功能自身的日志中也没有任何解释。值班工程师盯着仪表盘看了二十分钟,直到另一个频道的某人随口提到,他们刚刚启动了一个“快速回填(backfill)”任务,要对几十万份旧文档重新生成摘要。两个团队,两套代码库,两组值班轮换——而它们之间共用一个 API Key 的速率限制。回填任务吃光了配额,聊天机器人则被“饿死”了。

这就是“吵闹的邻居”(noisy neighbor)问题。而让模型 API 变得如此危险的转折点在于,这个“邻居”并不是共享云硬件上的某个匿名租户,而是你自己公司里的另一个团队。你们之间的那道墙比任何人想象的都要薄。

没有根本原因的事后分析

· 阅读需 10 分钟
Tian Pan
Software Engineer

事故排查会议(incident bridge)安静得有些异样,这意味着大家都卡住了。一份支持工单显示,智能体(agent)告诉客户其退款已获批准,而事实并非如此。你有完整的追踪记录:提示词(prompt)、检索到的账户记录、工具调用、模型的推理过程以及最终的消息。你重放了一遍,智能体的操作是正确的。你又重放了一遍,依然正确。十次中有九次,产生该事故的追踪记录反而生成了正确的结果。会议上终于有人提出了那个复盘模板(retro template)无法处理的问题:那么,根本原因是什么?

根本没有。至少不是模板所指的那种。五个为什么(five-whys)的链条是这样运作的:“智能体告诉了客户错误的信息” → “因为模型生成了批准” → “因为它采样了一个断言批准的 Token 序列” → “因为……这是概率分布所允许的。”最后一个“为什么”最终以耸耸肩告终。“模型采样了一个错误的 Token”在技术上是正确的,但在操作上毫无用处。它没有指明修复方案,没有分配负责人,也没有弥补差距。你可以把它写进报告,但每个读到它的人都知道,你记录的是一个巧合,而不是一个原因。

两个模型厂商,一个功能:用冗余换来的连贯性噩梦

· 阅读需 12 分钟
Tian Pan
Software Engineer

你接入了第二个模型供应商,因为第一个挂了。那是一个星期二,主供应商的状态页在几个小时内一片通红,你的功能也随之陷入瘫痪。所以你做了件负责任的事:添加了一个备选方案。现在,如果 OpenAI 无法访问,你就路由到 Anthropic。如果 Anthropic 对你进行了频率限制,你就回退到 Gemini。架构图看起来干净且成熟。可靠性问题,解决了。

可惜你添加的不是一个副本。你添加的是第二种意见。而操作“第二种意见”与操作“第二个副本”是完全不同的两回事。

你引入的心理模型来自“无状态服务”的策略:在负载均衡器后运行三个相同的实例,如果一个挂了,另外两个会提供完全相同的响应。这之所以有效,是因为副本之间在字节级别是完全等效的。但来自两个供应商的两个语言模型并非如此。它们是在不同的数据上训练的,针对不同的目标进行了微调,而且它们在用户最容易察觉的输入点上——会产生系统性的、而非随机的——分歧。

对你的工具接收的内容保持严格:波斯塔尔法则在智能体系统中失效

· 阅读需 12 分钟
Tian Pan
Software Engineer

“发送时要保守,接收时要开放。”波斯塔尔法则(Postel's Law)无疑是网络历史上最成功的设计原则——正是依靠它,来自不同厂商的 TCP 实现才得以在 20 世纪 80 年代实现互操作,并塑造了四十多年来的协议和 API 设计。然而,根据 IETF 自身的判断,这一原则会随着时间的推移而变质:RFC 9413 源于一份标题直白为《稳健性原则的有害后果》(The Harmful Consequences of the Robustness Principle)的草案,认为开放式的接收在短期内有助于互操作性,但从长远来看,却在悄悄腐蚀整个生态系统。

智能体(Agent)系统将这种长期的腐蚀缩短到了几周。当“发送者”是一个发出工具调用(tool calls)的大语言模型时,每一次开放式的接收——将 "5" 强制转换为 5、丢弃未知字段、对枚举拼写错误进行模糊匹配——都会破坏维持系统健康所需的精确信号。在智能体架构中,工具边界是唯一一处“大声报错”即是可靠性特性的地方,而大多数团队却反其道而行之。

不稳定的测试毒化 Agent 循环的速度比伤害人类时快得多

· 阅读需 11 分钟
Tian Pan
Software Engineer

一个人类工程师在看到一个明明不可能由改动引起的测试失败时,会做一件智能体做不到的事:耸耸肩。他们会点击重新运行,对着 CI 大神嘀咕几句,然后继续工作。这种耸肩行为包含了多年积累的上下文 —— 这个测试从 3 月份开始就一直不稳定(Flaky),那个服务的预发布环境每逢周一就会崩溃,没人信任 WebSocket 的测试套件。而编码智能体完全没有这些。它看到一个红色的 X,就将其视为绝对真理,因为它的所有训练和提示词都在告诉它:测试失败意味着代码是错误的。

接下来的环节才是最昂贵的部分。智能体不会耸耸肩 —— 它会采取行动。它会“修复”那些从未损坏的代码。它会因为应用改动后测试套件变红而回滚一个正确的改动。它会消耗大量的 Token 预算去追赶一个幻影,在原本正常的代码路径中添加重试、等待和防御性检查,直到那个不稳定测试碰巧通过,智能体便得出结论:它最后的突变就是解药。测试基座中的非确定性一直是人类注意力的一种负担。而对于智能体循环来说,它更糟糕:它是直接注入到决策系统中的受损训练信号,而系统正以机器速度基于此做出反应。