跳到主要内容

233 篇博文 含有标签「observability」

查看所有标签

流式推理中的海勒姆定律:节奏、停顿和中间 Token 是未成文的契约

· 阅读需 12 分钟
Tian Pan
Software Engineer

一个团队从前沿模型升级到了其更快的后继版本。评估套件(eval suite)全绿。最终答案一致。工具调用的 Schema 完全相同。结构化输出通过了与以往一样的 JSON Schema 验证。他们发布了。不到一天,支持票据就堆积如山:“助手感觉太匆忙了”,“它不再真正思考了”,“感觉不对劲”。产品经理调取了遥测数据,发现任务完成率没有变化。工程团队反复检查了评估和 Schema,没发现任何问题。投诉是真实的,但团队定义的契约——就如团队所定义的那样——依然完好无损。

改变的是流的纹理(texture)。旧模型在调用工具前会停顿 800 毫秒,发出一句“让我查一下……”的前导词,并以每秒约 35 个 Token 的速度输出,在子句边界处有自然的节奏。新模型以每秒 90 个 Token 的速度输出,从不停顿,且完全跳过了前导词。这些都没有出现在任何文档记录的契约中。但所有这些都是不可或缺的“承重”部分。

这就是海勒姆定律(Hyrum's Law),而流式传输(streaming)让它的表面积变得巨大。系统的任何可观察行为都会被某人所依赖——而流式 AI 界面暴露的可观察行为远比团队意识到的要多。

多维 Agent 二分查找:当回归出现在交互中时

· 阅读需 12 分钟
Tian Pan
Software Engineer

质量在一夜之间下降了。值班工程师打开仪表盘,追踪了几个异常会话,并开始进行显而易见的二分定位:模型提供商在 UTC 时间 02:00 切换到了新的快照,于是将模型回退到固定的旧别名。评估套件仍然显示红色。回滚昨天的提示词更改。仍然是红色。将检索索引固定回上周的版本。仍然是红色。每个负责团队都在孤立地回滚自己的维度,并报告“不是我们的问题”。三个小时过去了,没有人负责诊断,因为没有人负责回归真正存在的交互面(interaction surface)——新模型以一种旧模型绝不会采取的方式,解释了新的工具描述。

这就是单轴工具无法解决的失败模式。git bisect 之所以有效,是因为搜索空间是一维的:提交记录的线性序列。而 Agent 没有单一的时间线。它有四到五个并行运行的时间线——模型快照、系统提示词、工具目录、检索索引、采样配置——每个都有自己的负责人、自己的部署节奏,以及自己的“回滚”按钮,只能将其自身的轴恢复到已知状态。你正在追踪的回归通常是一个双因素交互作用,沿着任何单一轴进行二分都会返回假阴性结果,因为该 bug 仅在“新模型遇上新工具描述”的交叉乘积单元格中触发。

工具行为漂移:Schema 没变,语义却变了

· 阅读需 12 分钟
Tian Pan
Software Engineer

你的契约测试通过了。Schema 校验器显示正常。工具返回的数据结构与上个季度完全一致。然而,面向用户的回答已经悄无声息地错了六个星期。

这就是契约测试从未设计用来捕捉的故障模式。契约测试验证的是传输格式没有改变——比如 search() 是否仍然返回 { results: [{ id, title, score }] }create_event 是否仍然接受 ISO 8601 字符串,地理编码器是否仍然输出 { lat, lng }。它们无法捕捉到的是:搜索端点开始按新近度而非相关性排序的时刻;日历 API 在欧盟地区静默地将你 14:07 的开始时间吸附到 14:00;地理编码器在同一个模糊的多边形内选择了一个不同的点;或者作为工具的 LLM 分类器在稳定的端点后升级到了新模型,导致你的评估集从未采样过的某个类别中误报率上升了四个百分点。Schema 没变,但行为变了。你的智能体继续读取着代表通过的绿色勾选,并产生了没有任何错误日志捕捉到的退化答案。

工具延迟尾部:为什么 p99 重塑了智能体架构而 p50 掩盖了问题

· 阅读需 11 分钟
Tian Pan
Software Engineer

我上个季度合作过的一个团队发布了一个包含七个步骤的智能体(agent),并以显而易见的方式构建了其延迟预算:搜索返回耗时 200ms,SQL 查询耗时 80ms,电子邮件发送耗时 150ms,链条中的其他环节以此类推。将中位数相加,再加入一些缓冲,数学计算表明该智能体能轻松地保持在两秒的 SLA 之内。仪表板连续几周证实了这一点。中值延迟(median latency)表现优异。接着,客户开始抱怨该功能慢得无法使用,而仪表板看起来依然是代表正常的绿色。

他们互相转述的故事是错误的,因为他们是围绕 sum(p50) 构建的架构,而用户体验到的却是 sum(p99)。经过三到四个步骤后,链条中的 任何 环节掉入其自身尾部概率(tail probability)的可能性就不再微不足道了。经过七个步骤后,这种概率接近于掷硬币。没有任何单项工具的仪表板变红,因为没有任何单项服务表现异常 —— 问题在于没有人负责这种乘法复合(multiplicative composition)效应。

这并不是什么新教训。分布式系统研究人员已经为此撰写了四十年的文章。新鲜的是,每个构建智能体的团队都在最后期限的压力下,痛苦地重新发现这一点。

当工具撒谎时:智能体默认信任的“伪成功”失败模式

· 阅读需 12 分钟
Tian Pan
Software Engineer

智能体自信地告诉用户:“我已经发送了确认邮件,并将退款退回到你的账户。”追踪记录很干净:两次工具调用,均返回 {"success": true},模型生成了精练的摘要,对话在 3.2 秒内结束。一周后,客户发起投诉,因为邮件从未送达,退款也从未入账。审计日志中全是绿色的勾选标记。没有任何环节失败——除了实际的工作本身。

这就是在大多数智能体技术栈中尚未被命名的故障模式:撒谎的工具。这里的“撒谎”并非恶意——它们返回了其契约规定的响应。这种谎言是结构性的。HTTP 层返回 “200 OK” 是因为请求被接受了,而不是因为操作完成了。邮件服务商返回 success: true 是因为消息进入了发送队列,而不是因为它已经发出了。数据库写入返回且无报错,是因为它写入了一个从未同步的副本。被训练成“乐于助人”且在“绿色代表完成”的示例上训练过的模型,将这些信号编织成一份自信的摘要,然后继续下一步。

挂钟时间截止日期漂移:为什么你的智能体认为它还有时间但实际上没有

· 阅读需 11 分钟
Tian Pan
Software Engineer

用户点击发送。智能体被配置了 30 秒的时间配额。规划器(planner)检查任务,发现一条耗时约 12 秒的“深度研究”路径和一条耗时 3 秒的“快速查询”路径,并自信地选择了深度路径,因为“我们有充足的时间”。28 秒后,响应返回,比团队上季度发布的 SLA 晚了 2 秒。仪表盘显示,智能体的推理是正确的,重试逻辑是正确的,工具调用也成功了。没有人能解释为什么用户的加载动画转了 46 秒。

这个 bug 不在任何单一组件中。它存在于组件之间的缝隙中,存在于一个系统从未想过要刷新的值里:智能体对于还剩多少时间的认知。在请求受理与模型的下一个规划步骤之间,发生了一次透明重试,挂钟时间在流逝,但截止时间的元数据却没有更新。模型现在正根据它在 15 秒前就已经花掉的预算进行推理,而它自己对此一无所知。

Agent 循环容量计算:为什么你的预置吞吐量只有你想象的一半

· 阅读需 13 分钟
Tian Pan
Software Engineer

我曾合作过的一个团队发布了一个他们称之为 “小规模” 的功能:一个供几百名分析师使用的内部研究助手。他们的容量模型认为一次用户请求等于一次模型调用,因此他们根据峰值用户 QPS,并预留了标准的 30% 突发余量来设定预置吞吐量(provisioned throughput)的大小。发布当天,他们在不到一小时内就遇到了 429 错误,原本应该只消耗 40% 预留容量的流量却让容量达到了 100% 的饱和状态。事后复盘发现了一个之前没有人算进去的数字:平均每个请求触发了 11 次模型调用,而不是 1 次。

这是我在 Agent 推广过程中看到的最常见的容量估算失误。其中的数学逻辑并不复杂,失败模式也并不罕见。团队问错了单位问题——他们以用户请求为单位进行规划,而计费表是按模型调用次数计次的——他们支付真金白银买下的预留容量,在一种如果换成聊天产品本应被视为轻负载的压力下化为乌有。

智能体状态差异对比 (Agent State Diff):为什么肉眼对比两条追踪路径无法规模化

· 阅读需 11 分钟
Tian Pan
Software Engineer

一个回归错误流入了生产环境。团队选取了导致失败的输入,针对上周的提示词进行回放,却得到了不同的输出。现在他们必须查明原因——而答案埋藏在 3 MB 大小的文本差异、分歧的工具调用序列以及被打乱的检索块中,人类根本无法有效地进行比对(diff)。于是,他们将两份记录粘贴到左右分栏的查看器中,滚动查看了二十分钟,得出结论“模型今天感觉不太一样”,然后发布了一个并没有解决根本原因的热修复,因为他们从未找到真正的原因。

这就是 Agent 状态差异问题,也是通用工程工具在处理 Agent 系统时失效的首要环节。传统的回归二分查找(bisect)针对的是确定性代码:相同的输入产生相同的输出,git bisect 遍历历史记录,直到你找到破坏代码的提交。但 Agent 的运行不是确定性的,输入也不仅仅是一个字符串,其“历史”是一个多轴的包(envelope)——模型快照、采样配置、检索到的上下文、工具目录、框架标志——其中任何一个变量都可以独立地改变行为。

审计追踪的不匹配:当用户、智能体和工具各有各的日志时

· 阅读需 12 分钟
Tian Pan
Software Engineer

一名监管人员给你发了一封邮件,只问了一个问题:该用户是否授权了这笔交易?六小时后,三名工程师正在聊天频道里,试图将聊天界面的对话日志、规划代理(planner agent)的推理追踪以及工具的 API 记录关联起来。聊天日志记录了轮次 ID(turn ID)和用户可见的消息,但没有工具调用的细节。规划器追踪记录了工具调用的记录,其时间戳与聊天日志相差几百毫秒。工具日志记录了 API 调用及其自身的关联 ID(correlation ID),而该 ID 在代理的记录中无处寻觅。下游服务的日志则有另一个 ID,且没有回溯链接。团队最终通过关联用户 ID 和大致的时间戳重建了答案,祈祷没有什么关键信息因错过一个轮次而产生偏差,最后向法务部门提交了一份 PDF 文件。

这就是审计追踪不匹配(audit trail mismatch)。每一层的负责人(owner)都认为自己的日志没问题——而且从单体来看,它们确实没问题。缺失的产物是那个本应存在的“关联视图(joined view)”,并且没人为它的缺失负责。团队只有在发生事故、客户升级投诉或监管机构强制要求关联数据时,才会发现它并不存在。

上下文膨胀:你无法用 Grep 搜寻的 AI 内存泄漏

· 阅读需 14 分钟
Tian Pan
Software Engineer

一个长时间运行的智能体(agent)会话最初以 2K 上下文开启,现在却在为 40K token 的“死状态”买单。第三轮的检索结果、智能体早已跳过的目录列表、工具调用返回的 JSON 转储(即便其答案只是一个整数)—— 所有这些都在随后的每一次推理调用中如影随形,全额计费,并拖累注意力。这种模式在结构上与内存泄漏完全一致:无引用的数据无限制增长。但没有剖析器(profiler)能发现它,因为泄漏并不存在于进程内存中。它存在于对话历史里,而大多数智能体框架在发布时都没有配备回收机制。

成本同时体现在两个地方。token 账单呈二次方增长 —— 一个 20 步的循环,每一步贡献 1,000 个 token,累计产生约 210,000 个输入 token,而不是 20,000 个,因为之前的每一轮对话都会在后续的每一次调用中重新计费。而且模型本身也开始退化:当积累了 50K token 的噪声时,即使是拥有 1M token 窗口的模型,在实际任务上的准确度也会出现两位数的下降。你在花更多的钱,让模型更差地去思考它在三轮前就已经解决的问题。

昼夜延迟:为什么你的 AI 功能在东部时间上午 9 点最慢

· 阅读需 10 分钟
Tian Pan
Software Engineer

在上个季度的某个时候,你团队的一名工程师在 Slack 上发了一个帖子,开头是“模型变慢了”。他们展示了一张图表:你的助手功能的 p95 延迟从早上 7 点开始稳步攀升,在东部时间上午 10 点左右达到顶峰,午餐期间处于平台期,并在下午 5 点后悄然恢复。这种形态在第二天、第三天不断重复。团队追溯了他们的部署记录,指责了分词器(tokenizer)的更改,接着是上下文长度的退化,最后发现没什么是特别确定的。修复方案从未落地,因为 Bug 根本不在你的代码里。

顶尖模型提供商运行着共享的推理集群。当你的用户醒来时,北美其他地区也醒了,再加上欧洲的下午,以及每一家购买了相同 API 的公司的每一个内部工具。提供商端的队列深度翻倍,GPU 竞争加剧,你的 p95 延迟也随之翻倍——而你的代码库没发生一行代码变更。这是你技术栈中最可预测的生产事故,但几乎没有团队会为此建立仪表板。

隐藏的 SDK 重试机制:为什么你付了两倍的钱却浑然不知

· 阅读需 12 分钟
Tian Pan
Software Engineer

打开 OpenAI Python SDK 的源代码,你会发现一行安静的代码:DEFAULT_MAX_RETRIES = 2。Anthropic SDK 也采用了同样的默认设置。大多数 TypeScript SDK 也是如此。两次重试,指数级退避(exponential backoff),在连接错误、408、409、429 以及任何 5xx 错误时自动触发——这些都在你的代码看到失败之前就执行了。你没有配置它。你没有选择加入。你通常甚至不知道它的发生,因为你的应用记录的指标是 request_count(请求数),而不是 attempt_count(尝试数),并且你的追踪器(tracer)唯一能看到的 span 是 SDK 在最后一次尝试后关闭的最外层 span。

这在大多数情况下都没问题,直到出问题为止。如果在该 SDK 调用之上再添加一个应用级的重试装饰器——那种每个团队在遇到第一个 429 错误后都会写的代码——你就构建了一个 3x3 的风暴:SDK 尝试三次,你的包装层围绕 SDK 又尝试三次,在服务商降级期间,一个单一的用户请求会扇出为九次推理调用。服务商的账单会计算每一次尝试。而你的仪表盘只记录了一次。当最终有人进行账实对账时,那将是一场谁都不会喜欢的季度末谈话。