跳到主要内容

330 篇博文 含有标签「observability」

查看所有标签

代理墙钟预算:一场与工具超时机制的赛跑

· 阅读需 12 分钟
Tian Pan
Software Engineer

有一种 Agent 漏洞,当你孤立地观察任何单个组件时,它都不会出现。模型没问题,工具没问题,重试策略也没问题。纸面上的超时值甚至可以说很慷慨。然而,一个通常在 8 秒内完成的工具,却总是在一个已经在 7.9 秒时将其宣告为失败的 Agent 面前折戟。Agent 围绕一个从未发生过的“错误”重新规划,并启动了第二次调用,而第一次调用的结果即将与其发生碰撞。

漏洞不在任何一个框框里。它存在于两个没人同意应该同步的时钟之间的缝隙中。

引用索引失效:当你的分块器开始添加行号前缀时,偏移了一位

· 阅读需 12 分钟
Tian Pan
Software Engineer

分块器开始在每个块前添加 [line N]。Eval 变绿了(通过了)。从那天起,模型生成的每一条引用都指向了实际证据前的一个段落,这种情况出现在该产品所服务的受监管行业的每一份文档中。团队并不是通过评估发现这个问题的,而是通过一位审计人员发现的。审计人员查看了引用的句子,阅读后指出,该句子与其本应支持的断言完全矛盾。

这种回归错误(regression)能躲过代码审查、对三个示例文档的手动 QA 测试以及功能开关(feature-flag)的逐步推送。孤立地看,这些检查都没有错。它们都在问同一个问题——在预期的地方是否出现了引用——但没有一个检查在问审计人员问的问题,即:引用是否指向了断言来源的那个句子。这两个问题之间的差距,正是那个“差一错误”(off-by-one)长期潜伏的地方。

这种失效模式之所以值得专门写篇文章,不在于 Bug 本身。差一错误是陈年旧事了。有趣的地方在于,这个失效是由两个系统共同产生的:它们在整数的结构上保持一致,却在整数的含义上产生了无声的分歧。

你的 Agent 每一轮都在重新生成对话摘要,只因缓存键包含了一个时间戳

· 阅读需 12 分钟
Tian Pan
Software Engineer

一个只被写入却从未被读取的缓存算不上缓存。它只是一个增加了额外延迟、按 KB 计费的日志系统。而这种失效模式最残酷的版本是,从每个角度看缓存都是健康的:set 调用成功,get 调用返回迅速,键(key)格式正确,值(value)有效,TTL 设置合理。唯一的问题是,没有任何一次 get 调用能找到之前 set 调用写入的键,因为键中的一个字段在每次计算时都会发生变化。

这是一个关于调试过程的故事:为了“能分辨出我正在看的是哪条缓存记录”,一位工程师在缓存键中添加了一个时间戳。结果,在没人察觉的两个星期里,系统悄悄地为每场对话多支付了 14 次额外的 LLM 调用费用。

那个将你的系统提示词泄露到客户审计日志中的调试日志器

· 阅读需 11 分钟
Tian Pan
Software Engineer

一位具有安全意识的客户拉取了其租户的审计导出文件,打开 JSON,从一个名为 llm.request.system 的字段中读到了逐字记录的拒绝策略、检索流水线结构以及一些内部产品标识符。没有漏洞利用。没有提示词注入。没有越狱。仅仅是你的平台团队在六个月前添加的一个日志字段,目的是让工程师能够将提示词版本与事件关联起来——结果通过你的企业团队出于 SOC 2 合规原因单独向租户开放的摘要(feed)泄露了出来。

泄露发生在周三下午。你的安全团队是由客户呼叫(paged)的,而不是报警系统。事件时间线显示泄露当天并没有部署——错误配置是在审计摘要扩大其字段白名单的那天发布的,那是另一个团队、另一个迭代周期(sprint)和另一张工单。两名评审员都批准了他们所看到的内容。但没有人从全局组合的角度去审视。

那场无需部署就让你检索召回率减半的 Embedding 弃用事件

· 阅读需 12 分钟
Tian Pan
Software Engineer

在一个 RAG 系统中,可能上线的代价最高昂的嵌入 (embedding) Bug,是那种你的代码库没有任何变化、检索代码没变、索引没变、查询路径也没变的 Bug。然后在第六周的某个周二,有人注意到答案的质量不如从前了。

服务商为你十二个月前构建索引时所使用的嵌入系列发布了停用公告。平台团队将其归档在了一个拥有一年缓冲期的停用仪表盘中,然后就继续处理其他事情了。停用路径并不是一个生硬的截止——而是一个悄无声息的质量退化:被停用的端点开始路由到一个“兼容性”继任者,它返回相同维度的向量,但语义几何空间却有微妙的不同。查询嵌入开始与你一年前嵌入的语料库发生漂移。在六周的时间里,你的常规评估中的 Recall@10 下降了 47%。团队直到一个无关的质量仪表盘达到阈值时才追溯到原因,迫使一名高级工程师进行根因分析,最终发现问题指向了一个在这一年里没人动过的嵌入端点。

团队上线了新提示词模板,评估框架却还在测昨天的旧版本

· 阅读需 10 分钟
Tian Pan
Software Engineer

事件时间线清晰可见。9:02,你的平台团队将 prompt-template@v38 推送到了配置服务。11:14,你的仪表板显示一切正常。16:51,支持团队有人标记了升级件数的激增。17:03,你打开了评估套件,发现回归分数为 0.34,于是进行了回滚。复盘报告称:“在 8 小时内捕获,除了 0.04% 看到该问题的客户外,未造成进一步损害。”工程领导层对响应速度表示赞赏。

但这是错的。回归在 0 小时内就被捕获了。17:03 运行的评估套件与 09:03 运行的是同一个。它一直指向的是 v37。评估框架在进程启动时从配置服务加载了模板,将渲染后的 Prompt 以 Python 对象的形式缓存到了模块级作用域中,并且从未重新读取源文件。你的线上流量在上午 9 点切换到了 v38。而你的评估直到 17:03 有人重启了 Worker 池来“重新运行回归”时才发生变化。在长达 8 小时的时间里,客户交互是基于从未经过评估打分的 Prompt 进行的,而评估系统却一直在给生产环境中根本没人在用的 Prompt 打分。

JSON Schema 校验通过了,但下游消费者因语义漂移拒绝了你的输出

· 阅读需 11 分钟
Tian Pan
Software Engineer

JSON Schema 验证的是输出的形状(shape)。它并不验证该形状内数值的含义。在长达 9 个月的时间里,你的 AI 流水线产生的每一条输出都顺利通过了校验,监控显示 Schema 合规率为 100%,你的团队也理所当然地认为符合 Schema 的响应在契约层面就是正确的。接着,一次模型升级发布了,每一条输出依然能通过校验,但你的 Slack 告警频道却在一夜之间从每天 50 条消息飙升到了 800 条。

Schema 没有出问题,出问题的是其内部数值的分布。这就是大多数 AI 团队在生产环境中发现的鸿沟:JSON 契约是一个类型系统(type system),而非行为系统(behavior system),而下游消费者一直依赖于某种契约从未被要求强制执行的数值分布。

KV 缓存预热 Cron 任务只在蓝环境运行而从未进入绿环境,原因竟是主机绑定从未迁移

· 阅读需 12 分钟
Tian Pan
Software Engineer

事故复盘将十二天前的一次部署确定为支出增加 3.6 倍的原因,而当时在场的参与者中,没有一个人在变更发布时参与其中。部署过程非常常规:蓝绿切换,流量按计划转移到绿色环境,蓝色环境停用,流水线变绿,发布工程师关闭了工单。生产环境的 SLO 都没有触发。应用层的告警也没有响起。系统运行得完全符合设计。

原本的设计是一个每五分钟运行一次的 Cron 任务,它每五分钟针对稳定的系统提示词前缀 (system-prompt prefix) 预热提供商的 Prompt 缓存。这种预热为团队在冷启动时带来了 91% 的缓存命中率,并在每个会话的第一次请求中获得了大约 4 倍的成本优势。该 Cron 任务是一年前首次引入蓝绿模式时编写的,其主机选择器 (host selector) 被固定在蓝色池 (blue pool),以避免在重叠窗口期间运行两次预热。当绿色环境变成活跃环境而蓝色环境消失时,该 Cron 任务失去了它的主机,并从“每五分钟运行一次”悄无声息地转变为“永不运行”。随着提供商缓存的 TTL 使预热的前缀过期,缓存命中率在接下来的 36 小时内逐渐下降。成本仪表盘计算的是每日窗口内的平均单次请求成本,平滑了趋势,直到下一个计费周期让问题变得显而易见。

供应商移除的 Logprobs 字段如何静默地破坏了你的置信度路由

· 阅读需 13 分钟
Tian Pan
Software Engineer

在事故复盘报告中,最昂贵的代码行反而是没人写出的那一行:一个字段缺失的 200 OK。原本应该将难题上报(Escalate)给更强模型的路由,在整整六周的时间里上报的流量为零。成本看板在欢庆,质量看板却在下滑——但仅限于那组被现有评估集低估的难题切片。直到客户投诉系统以前能正确处理的特定问题时,一切看起来都还像是场胜利。

起因是协议栈上层的一个响应结构变化。供应商的中级方案取消了逐 Token 的 logprobs,这在发行说明中被称作“特定层级的特性对等调整”。客户端收到的仍是有效的 JSON,HTTP 状态码依然是 200,响应中的模型标识符与请求中的模型标识符也完全匹配。唯一的变化是,路由用来做上报决策的字段消失了,而一年前在一次事故中添加的防御性默认设置,悄然变成了每个请求的生产环境默认行为。

供应商将你的模型标识符重定向到特定租户的微调模型,而其他人使用的却是基础模型

· 阅读需 12 分钟
Tian Pan
Software Engineer

客户支持团队升级了一个问题:“你的助手以前能正确处理退款资格问题。但上周开始出错了。”值班工程师调取了对话记录,在开发账号中使用相同的模型标识符回放了完全相同的提示词(prompt),得到了正确的回答,于是以“无法复现”为由关闭了工单。两周后,另一名客户提出了同样的投诉。工程师再次在同一个开发账号中进行回放,结果依然正确。团队开始归咎于没人做过的提示词更改。

请求中的模型标识符从未改变。响应字段中的字符串与请求字段中的字符串匹配。评估套件在六周内一直保持绿色。生产流量使用的模型权重与评估套件使用的模型权重是两套不同的集合,而且在该账号的整个生命周期中一直如此——直到过去这六周,它们变成了同一套权重,而团队注意到这一点仅仅是因为客户先发现了。

那个按会话分桶并导致 A/B 测试分群漂移的模型发布标志

· 阅读需 12 分钟
Tian Pan
Software Engineer

事后分析会以一句房间里所有人都希望是真的话开头:新模型在满意度上赢得了 4 %,p 小于 0.01,发布吧。一个月后,一项更冷静的分析发现,这种提升其实是一个混杂因素,模型表现实际上持平或略差,而团队在中间几周一直在争论哪个 prompt 更改“导致”了这一胜利。模型本身并没有导致任何结果。实验衡量错了对象,因为标志服务(flag service)和分析流水线在静默状态下对“分群(cohort)”的定义产生了分歧。

这是 A/B 测试中最昂贵的故障模式之一,因为系统中没有任何东西是损坏的。标志服务工作正常。实验追踪器工作正常。仪表板能正常渲染。统计数据是根据接收到的数据正确计算的。故障存在于三个组件之间的缝隙中,每个组件对身份都有不同的假设,而且除非你主动寻找,否则这个缝隙是不可见的。

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

· 阅读需 13 分钟
Tian Pan
Software Engineer

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

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