跳到主要内容

278 篇博文 含有标签「reliability」

查看所有标签

当时钟成为工具:Agent、时区以及那个只在午夜发生的 Bug

· 阅读需 10 分钟
Tian Pan
Software Engineer

询问大型语言模型现在几点,你得到的回答虽然语气自信,但几乎肯定是不准确的。这并非因为模型坏了,而是因为它的内部没有时钟。Transformer 是一种无状态的文本补全引擎:它将 token 映射到 token。在整个流水线中,没有任何地方会接收到“现在是 14:32 UTC”这样的信号。模型感知不到当前时刻 —— 这是你必须在每一轮对话中主动提供给它的东西,否则它就会从陈旧的训练数据中臆造一个时间。

这种无声的失败往往在最糟糕的时刻浮出面。你的智能体认为现在是星期一,因为会话是在星期一开启的,于是它在星期二、星期三依然坚信这一点,直到它为一个已经过去的日子设定了“明天早上”的提醒。它利用数小时前就已冻结的 now 来分析“过去 24 小时”的日志。它在转换跨时区的会议时间时,因为误判了夏令时的边界而导致一小时的偏差。这些在传统意义上都不像是“幻觉”。其输出流畅、合理且逻辑自洽,只是它锚定在了一个不再存在的时刻。

3% 的回归不会让报警器响起:应对统计性故障的轮值工作

· 阅读需 12 分钟
Tian Pan
Software Engineer

你的轮值制度(on-call rotation)最初是为了捕获一种特定类型的故障,但绝非那种真正会让你的 AI 产品崩溃的故障。它监控的是停止响应的服务、飙升的延迟曲线、超过 1% 的错误率。这些都是阶跃函数(step functions):原本运行正常的东西突然失效了,这种不连续性足以在凌晨 3 点叫醒一名工程师。整个体系——阈值、运维手册(runbooks)、升级策略——都假设故障会自我宣告。

对于一个包含模型在环(model in the loop)的系统,真正关键的失效模式恰恰相反。上周二,你的提取流水线准确率为 94%。这周二,它变成了 91%。没有服务崩溃。每个请求都返回了 200。延迟平稳。输出依然是格式良好的 JSON。但你 3% 的用户现在得到了微妙的错误答案,而且他们不会提交 Bug,因为答案看起来是正确的。由于没有触发条件,寻呼机(pager)保持沉默。等到有人注意到时——通常是愤怒的客户,通常是在几周后——这种回归(regression)在整个过程中一直在静默地累积恶化。

用户学会利用 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"},用于填充缺失字段,并应用在了一个该包装服务无法识别的错误之上。状态码是正确的,请求头是正确的,响应体是错误的,而响应体正是客户端读取的部分。