跳到主要内容

252 篇博文 含有标签「reliability」

查看所有标签

用户学会利用 Agent 超时机制套取退款

· 阅读需 10 分钟
Tian Pan
Software Engineer

某平台发布了一个针对长耗时智能体(agent)任务的 30 分钟实际时间上限,并配套了一项退款政策:任何达到超时上限且未产生交付成果的任务,其消耗的 token 费用将予以退还。其初衷是保护性的:挂起的智能体不应向客户收费。六个月后,超时率翻了一番,工程团队深陷“智能体可靠性”调查,而支持队列中挤满了抱怨智能体“不断超时”的用户——截图显示,用户的浏览器标签页在 29 分多钟时就被关闭了。

在财务模型从未命名的行为群体中,单位经济效益已悄然倒挂。退款人群并非质量不佳的人群。这是一种策略。

在网关层交换了两个用户上下文的 conversation_id 冲突

· 阅读需 10 分钟
Tian Pan
Software Engineer

收到一张读起来像幻觉一样的客户支持工单。用户附上了一张截图:一个他们从未问过的问题,顶部显示着他们的账户名,接着是引用了他们从未上传过的文件的模型回复。追踪记录看起来很干净。模型完全按照要求执行了任务。问题在于,这个提问完全来自另一个租户,而你的网关由于 conversation_id 值发生了碰撞,将两个对话路由到了同一个后端状态。

你在餐巾纸上算了算。UUID v4 有 122 位的熵。在 5000 万个对话的语料库中,发生任何碰撞的生日边界概率远低于五千万分之一。一年前设计系统时你跑过这个计算。数学是正确的。数学现在依然正确。改变的是你的两个后端层不再以相同的方式生成 ID,而数学所描述的概率从来就不是你实际运行中的概率。

当用户取消对话后,下游 API 却仍在继续写入

· 阅读需 11 分钟
Tian Pan
Software Engineer

用户点击停止。浏览器关闭了 SSE 连接。你的 AI SDK 触发了 onAbort。Agent 运行时检测到信号,停止向模型请求更多 token,并终止其循环。从你的代码库内部来看,这次取消显得非常利索。你所能看到的每个子系统都在执行正确的操作。

与此同时,两秒钟前,模型发出了一个工具调用(tool call)。运行时分发了它。工具的 execute 函数打开了一个连接到第三方 API 的 TCP 连接并发送了 payload。该 HTTP 请求仍在传输中,第三方的服务器仍在处理它,而第三方完全无法得知它所服务的对话已不存在。写入操作成功提交。用户的心智模型认为他们通过点击停止避开了该操作。下游系统的数据库则记录了完全不同的结果。

供应商上调 max_tokens 默认值,导致你的尾部响应长度翻倍

· 阅读需 13 分钟
Tian Pan
Software Engineer

你的事故时间线显示没有部署。你的代码没有变。你的流量组合没有变。你的提示词也没有变。然而,你的 p99 输出长度在一周内翻了一番,下游渲染层开始截断响应,而且在流量没有请求更长答案的情况下,你的输出 Token 账单增长了 38%。这种变化是真实的,回归是可以衡量的,但你的版本控制系统中没有记录——因为发生变动的值是你的代码从未发送过的。

供应商提高了一个隐式默认值。发布说明将其归类为“改进的长内容表现”。有问题的参数是 max_tokens,你的应用程序从第一天起就忽略了它,因为文档中记录的默认值很慷慨,而且你的输出很少接近这个值。默认值从 4096 移动到 8192,以适应供应商新模型中更长的推理过程。无论你是否想要,你的应用程序都获得了新的默认值,因为缺少参数本身就是一种配置选择——而供应商拥有更改其背后值的权利。

这种故障模式下,供应商侧的“无操作”(no-op)发布会作为行为变化、成本变化和用户体验(UX)变化同时在你的系统中传播,而你团队唯一的诊断信号是月底寄来的账单。

配额窗口机制重写后,这批夜间脚本是如何拖垮你的交互流量的

· 阅读需 13 分钟
Tian Pan
Software Engineer

一个稳定运行了十个月的 cron 任务是你系统中最危险的任务,因为其中的代码没变,你的代码也没变,唯一改变的是别人发布的、你们团队没人读的发行说明(release notes)中的一句话。那个每晚在 00:05 UTC 启动、在十分钟内清空工作队列并重新进入休眠状态的每晚 embedding 刷新任务,曾是教科书般的范例。它通过在用户醒来前占用几分钟刚重置的每分钟配额,并在当天的剩余时间内保持在每日配额之内,从而与白天的交互式流量和谐共存。接着,服务商重写了每日窗口的核算方式,保持了每分钟窗口不变,并保留了你客户端测试的所有签名。批处理任务依然稳定运行。但每晚 00:13 UTC,交互界面开始返回 429 错误。团队一直在追查一个根本不存在的、本应在一周后才开始的上游维护窗口。

Bug 从不在你的代码里。Bug 在于“每日限制”不再是前一天的意思,而你的调度程序固定在了一个与旧定义对齐的时钟边界上。这篇文章讨论的是:速率限制核算(rate-limit accounting)作为一种服务商可以在不破坏任何签名的情况下修改的契约;两个独立正确的调度是如何组合成拒绝服务模式的;以及如何通过架构手段让 cron 任务不再是一个连接到别人时钟上的定时炸弹。

那些与实际限流不符的提供商频率限制响应头

· 阅读需 11 分钟
Tian Pan
Software Engineer

响应头显示你还有每分钟 480,000 个 token 的剩余额度。但在你仅消耗了 240,000 个之后,429 错误就降临了。你的调度器一直在根据一个运行环境根本不会遵守的数字进行自动扩缩,墙上的燃尽图显示的是文档里的理论值,而限流器执行的却是另一套完全不同的规则。

这种故障往往需要很长时间才能被察觉,因为路径上的每个组件都在执行其宣称的功能。供应商返回了一个响应头。你的客户端解析了它。你的调度器读取了它。你的仪表盘绘制了它。这些层级都没有损坏。真正出问题的是那个假设:即响应头是一份契约。

重试预算如何从你的仪表板中隐藏了供应商的真实错误率

· 阅读需 12 分钟
Tian Pan
Software Engineer

周会汇报的幻灯片上写着 99.9%。发票则显示账单翻了三倍。这两个数字在相邻的仪表盘上共存了数月,却没人发现它们衡量的是不同的世界。可靠性数值是重试后的结果——每一个最终返回 200 的调用都被计为成功——而成本数值则是客户端进行的每一次尝试,按 Token 计费。在两者之间,是一个慷慨的五次重试循环,以及一个尾部延迟正在悄悄恶化的供应商。第一次有人同时观察这两个数字是在一次故障期间,当时成本异常告警在可用性告警之前就触发了。

这就是整个模式。一个看起来像是可靠性机制的重试预算,本质上也是一个成本-质量调节旋钮,而那些只关注其中一面的团队,正在为一个发票最终会修正的可用性数字买单。

导致你的智能体重试机制失效两周的工具 Schema 迁移

· 阅读需 12 分钟
Tian Pan
Software Engineer

弃用通知是在周二发出的。下游团队更改了其搜索工具的响应结构 —— results[].snippet 变成了 results[].excerpt,这是一个干净的重命名,有六周的窗口期,文档中有横幅提醒,还给工程邮件列表发了三封提醒邮件。每一个人类用户都迁移了。但 Agent 没有,因为 Agent 不读邮件。在 14 天的时间里,重试循环静默地解析新的有效负载,发现它正在寻找的字段缺失了,抛出了一个 KeyError,并将其计为可重试的故障。重试命中了相同的端点,得到了相同的新结构,抛出了相同的错误,在尝试三次后放弃,并向用户返回了一条致歉消息。重试预算仪表盘在那段时间里一直显示为绿色 —— 重试次数从未 耗尽,它们只是在 预算内永久失败。在该路径上,从工具层测得的成功率为零。但没有人察觉,因为没有触发报警。

这是 2026 年最让工程师头疼的失败形式:不是那种戏剧性的停机,而是隐蔽的契约漂移(Contract Drift)。在这种情况下,面向人类的迁移已经顺利完成,而面向 Agent 的迁移甚至从未开始,因为没有人意识到还需要进行迁移。弃用流程完全按照设计运行,服务于它所设计的使用方。而 Agent 却是一个不在名单上的使用方。

你的评测套件也是生产负载:当每晚测试耗尽线上流量配额时

· 阅读需 12 分钟
Tian Pan
Software Engineer

一个团队最成功的 AI 功能在周二凌晨 2:14 宕机了。传呼机显示模型 API 在稳态下返回 429 错误。模型是健康的。供应商是健康的。团队自身的生产流量也是正常的。蚕食额度的是每晚运行的评测套件(eval suite)——正是团队在前一周引以为傲并进行扩展的那个套件。评测系统和产品共享同一个组织密钥(organization key),在那个夜晚,评测系统成了那个打破室友宁静的“吵闹邻居”。

评测系统并没有异常行为。它正在按照开发者的设计运行:针对生产模型标识符(identifier)进行一千个案例的测试,按节奏、按计划运行——这个计划因为已经静默运行了两年,早就被大家遗忘了。这次最终导致超限的扩展增加了三百个案例。该 PR 经过了评测负责人和 Prompt 负责人的审核。评审线程中没有一个人想到要问:这会消耗多少每日 Token 额度?

那些响应体显示 OK 且被客户端信以为真的 429 错误

· 阅读需 10 分钟
Tian Pan
Software Engineer

故障始于 14:03,服务商返回了 429 错误,并带有一个 JSON 响应体,内容为 {"status": "ok", "data": null}。这个客户端库是六个月前由一个被坑过两次的人匆忙写成的——一次是因为网关返回了带有 error 字段的 HTTP 200,另一次是因为服务商在请求实际成功时返回了 HTTP 500。所以,这个库学会了信任响应体,而不是状态码。状态码要求限流,响应体却说继续。客户端相信了响应体,发出了下一个请求,又得到了一个带有 ok 的 429,再次发送,到 14:11 时,服务商的熔断器已将该账户在该小时的剩余时间内列入了黑名单。

服务商并没有完全撒谎。429 是真实的。但在响应流水线的某个环节,一个默认的封装层覆盖了限流负载——这是一个来自包装服务的通用 {"status": "ok"},用于填充缺失字段,并应用在了一个该包装服务无法识别的错误之上。状态码是正确的,请求头是正确的,响应体是错误的,而响应体正是客户端读取的部分。

那个基于已被你的上下文剪枝器丢弃的事实进行分支的智能体计划

· 阅读需 12 分钟
Tian Pan
Software Engineer

一个运行时间较长的 Agent 在第 3 步生成了一个计划。计划的内容大致是:“如果第 1 步中 get_order 返回的订单状态为 shipped,则向客户发送一封物流追踪邮件;否则开启退款工单。”Agent 自信地选择了邮件分支。但客户从未收到追踪号码,因为订单实际上处于 pending 状态。你查看 Trace,期望能发现幻觉。但你发现的情况更糟:第 1 步的工具结果已经不在上下文中了。Pruner 在第 2 步和第 3 步之间将其剔除了——因为它在最近性排名中较低,而且为了给 12KB 的对话记录腾出空间。计划仍在运行。分支仍被选中。现在的决策指向了一个根本不存在的证据。

这在通常意义上并不是模型失败。模型生成了语法正确的计划,按顺序执行,并做出了分支决策。分支是基于一个曾经在上下文中但现在已不在其中的事实做出的。思维链编码了条件(if status == "shipped");而实际的状态在传递到需要它的步骤时被丢弃了。计划看起来是确定性的,但它已经被悄悄地从证据中剥离了。

增加更多工具后,你的智能体“不知道”率为何反而下降了

· 阅读需 10 分钟
Tian Pan
Software Engineer

你添加了搜索工具,接着是日历工具,然后是 CRM 工具,接着是四个数据库封装器(wrappers)和一个计算器。仪表盘的变化正如你所愿:任务完成率上升,延迟保持稳定,“我不知道”率从 14% 下降到 4%。这看起来像是能力的提升。事实并非如此。规划器并没有学到更多知识;它只是学会了减少拒答(abstention)。现在每个问题看起来都是可以回答的,因为总有 某个 工具的模式能与查询充分匹配并被调用。你消除的那 10 个百分点的“我不知道”并没有转化为正确的答案 —— 它们变成了自信的错误答案,分布在无人仔细评审的长尾场景中。

这就是工具表面扩展带来的“伪能力陷阱”(false-competence trap)。这是团队在庆祝改进时,发布了性能退化(regression)最常见的方式。评估准则衡量的是 Agent 是否尝试了任务并产生了一个形状合理的答案;它并不衡量 Agent 是否应该拒绝回答。拒答并非没有成本,但它是目前能获得的最廉价的正确行为,而一旦你的工具集变得足够大,以至于总有 某个 工具会被触发,你就再也看不见这种行为了。