跳到主要内容

31 篇博文 含有标签「debugging」

查看所有标签

多模态追踪:当各种模态必须共享一个 ID

· 阅读需 12 分钟
Tian Pan
Software Engineer

一位用户拨通了你的客服 Agent。他们说话,Agent 倾听,用户在通话中途上传了一张错误截图,Agent 同时对图片和转写文本进行推理,最后通话以一封总结修复方案的邮件收尾。三天后用户投诉过来:修复没有生效,邮件也从未送达。你打开可观测性栈,发现三个独立 UI 里躺着三条互不相干的追踪。语音流水线给你一条 ASR 追踪。视觉流水线给你一段图片上传的 span。LLM 调用给你一条带 token 数和工具调用的聊天追踪。这些仪表盘里没有任何东西告诉你:它们其实是同一次对话。

这就是没人愿意写的那种复盘。不是因为数据缺失——每一个模态都老老实实记录了它该记录的东西——而是因为跨模态的"接合"从来就没建起来。每条流水线都从自家模型供应商默认配置里长出了自己的追踪约定,而把它们绑在一起的那一次对话轮次,只存在于设计这个 Agent 的那位工程师的脑子里。

从 Bug 到行为率:没有复现步骤的 AI 事后分析

· 阅读需 10 分钟
Tian Pan
Software Engineer

用户提交了一个工单。智能体告诉一位付费客户,他们的退款将在 7 小时内处理,而文档中记录的 SLA 是 7 天。附带了截图。你调取了追踪记录,找到了准确的提示词(prompt)、准确的工具调用、准确的模型和种子值(seed)。你进行了复现。模型说是 7 天。你再次复现。7 天。你复现了 100 次。其中 98 次说是 7 天,2 次说是“今天结束前”,但从未说过 7 小时。截图是明确无误的。复现结果却不一致。周五截止的复盘报告现在有一个“根本原因”栏,但你却填不出任何根本原因。

这就是大多数进入复盘阶段的 AI 事故的形态。不是那种明显的宕机——那些会有堆栈追踪和 500 错误率图表,并以每个 SRE 都受训过的方式恢复。棘手的是那些产生了一个错误输出、留下了受害者、在退出时抹除了自身条件,且在你召唤它时拒绝再次出现的单次事件。你使用过的每一个复盘模板都假设存在一个可复现用例。但智能体并不给你提供这些。

那些由于模型选择了不同的 Token 而无法复现的 Bug

· 阅读需 11 分钟
Tian Pan
Software Engineer

用户提交了一个 bug。你的智能体生成的摘要掉了一段关键内容,或者 JSON 返回格式错误,或者回答一本正经地胡说八道。你打开工单,复制请求,然后重放(replay)。结果正常。你又重放了一次。依然正常。于是你把工单标记为“无法复现”并继续处理其他事情。

Bug 依然在那儿。真实用户依然在遇到它。你之所以关闭工单,是因为你的调试工具链默认了固定的输入会产生固定的输出——而你正在调试的组件实际上是从概率分布中进行采样的。

难以调试的庞大 Agent 追踪:当记录了一切却读不懂任何内容时

· 阅读需 12 分钟
Tian Pan
Software Engineer

关于 Agent 可观测性的标准建议只有三个词:记录完整 trace。捕获每一次工具调用、每一个 prompt、每一条模型响应、每一次内存读写。团队照做了。接着第一个真实故障发生了,工程师打开 trace,发现它有 40 层工具调用深,20 万个 token 宽。从技术层面看,trace 是完整的;但从实践层面看,它完全不可读。

接下来是熟悉的仪式。工程师不断滚动屏幕。他们展开一个 span,看到 5 万个字符的 JSON,折叠它,再次滚动。十分钟后,他们终于找到了那个模型选错工具的回合——它被埋在 37 个完全符合预期的回合之间。原本旨在让故障清晰可见的 trace,反而增加了排查成本。

没有复现步骤的故障工单:可复现性是工程化的结果

· 阅读需 11 分钟
Tian Pan
Software Engineer

这张故障工单具有只有真实事故才具备的典型特征。在 02:14,支持代理关闭了一个本应进入 30 天宽限期的客户账户。客户发现了。工单落到你的桌面上,“复现步骤”一栏下面只有一行字:未知

你打开追踪记录。你看到代理调用了 close_account 而不是 set_grace_period。你看到工具执行成功了。你看不出的是模型为什么选择了那个分支 —— 而且当你通过同一个代理重新运行同一条客户消息时,它做出了正确的选择。做了两次。现在的事故复盘报告中,原本该写根本原因的地方出现了一个段落大小的空洞,而你唯一能诚实写下的只有“无法复现”。

在智能体交接处中断的分布式链路追踪

· 阅读需 12 分钟
Tian Pan
Software Engineer

你打开一个失败运行的追踪(trace)。Span 树非常漂亮:用户请求、规划者 Agent 的推理、三次工具调用、Token 计数、延迟,所有这些都整齐地嵌套在一起。然后规划者交接给一个专家 Agent —— 追踪到此结束。并不是出现了错误 Span。它只是停止了。接下来的内容是来自专家 Agent 的另一个、无根的追踪,它从思考的中途开始,没有父级,没有可见的输入,也与导致它的请求没有任何联系。

Bug 就存在于那个间隙中。一直以来都是如此。交接是一个 Agent 的假设与另一个 Agent 的理解相遇的地方,也是你的追踪无法跟随的唯一地方。

这不是日志记录的问题。你的 Agent 可能在两端都正确地发出了 Span。问题在于追踪上下文(trace context)—— 将 Span 缝合成一个故事的线程 ID —— 没能在从调用者到被调用者的跳转中幸存下来。你技术栈中的每个 HTTP 客户端和 gRPC 存根都会免费传播该上下文。但你的 Agent 交接没有这样做,因为没有人告诉它去这样做。

Agent 调试器没有断点:为什么追踪优先工作流正在取代单步执行

· 阅读需 11 分钟
Tian Pan
Software Engineer

当你第一次尝试像调试服务那样调试 Agent 时,你会发现以往的肌肉记忆完全派不上用场。你设置了一个假设的断点——虽然 IDE 中没有面板可以放置它,但你在脑海中想象了一个——就在 planner 选错工具的那一步。你使用相同的输入重新运行。这一次,planner 选择了正确的工具。你再次运行。它又选了一个你从未见过的第三种工具。Bug 是真实存在的,你的同事今天早上复现了两次,而你用了十五年的调试器突然间变成了博物馆里的陈列品。

这里失效的心智模型并不是“使用调试器”,而是背后更深层的假设:即一个程序在给定相同输入的情况下,会产生相同的执行过程。现代调试器中的每一项功能——断点、单步跳过 (step-over)、观测表达式 (watch expressions)、条件断点、热重载——都是建立在这种确定性之上的。你暂停执行是因为暂停是有意义的。你向前单步执行是因为下一步是可预知的。你检查一个变量是因为它的值是一个事实,而不是从某种分布中随机抽取的结果。

重跑反模式:为什么再次运行并不能发现 Bug

· 阅读需 11 分钟
Tian Pan
Software Engineer

当 AI 功能表现异常时,大多数工程师做的第一件事就是再次点击“运行(run)”。这种思路认为,模型是具有随机性的,所以这次运行可能只是运气不好。当第二次尝试产生看起来合理的结果时,工单就被关闭了。团队继续前进。而真正的 Bug——过期的工具响应、检索缺失、仅在包含特定 token 的输入时才触发的系统提示词冲突——仍然完好无损地留在生产环境中,等待下一个用户触发它。

这就是“重跑反模式(rerun antipattern)”,它是 AI 团队从聊天机器人时代继承下来的最昂贵的调试习惯。它看起来很严谨,因为模型确实是非确定性的。它看起来像是一种方差探测。但几乎没有人在重新运行之前写下假设,没有人预先决定多少次运行才算证据,也没有人考虑 token 的成本。正在发生的事情更接近于“老虎机式调试”:你不断拉动杠杆,直到红灯停止闪烁,然后你走开,并确信机器没问题。

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

· 阅读需 12 分钟
Tian Pan
Software Engineer

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

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

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

· 阅读需 11 分钟
Tian Pan
Software Engineer

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

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

解读智能体堆栈跟踪:在模型、工具与 Harness 之间定位故障

· 阅读需 11 分钟
Tian Pan
Software Engineer

用户报告 Agent 给出了错误答案。你打开 Trace。模型的推理过程看起来没问题。工具调用全部返回 200 OK。Harness 日志显示没有重试、没有截断、没有异常。然而,答案就是错的。于是你花了接下来的两个小时,将三个具有不同格式、不同时钟的独立日志流缝合在一起,最终发现某个工具针对特定的查询形状静默返回了 {"result": null},模型将这个 null 合理化为一个听起来合乎逻辑的事实,而 Harness 则愉快地将这个幻觉转发给了用户。这三个层级中的任何一个都没有单独记录任何警报。故障发生在连接处。

这是生产级 Agent 系统中最主要的故障模式,而大多数团队都在使用单层工具进行调试。模型团队归咎于工具。工具团队归咎于模型。平台团队归咎于 Harness。每个人都部分正确,因为 Agent 故障几乎从来不是单一组件的 Bug —— 它是三个组件之间的失配,而每个组件都在不同的“步骤”心理模型上运行。在你的 Trace 基础设施反映这一现实之前,你将不断为披着不同外衣的同类事故买单。

LLM 自我调试:解释何时是信号,何时是谎言

· 阅读需 9 分钟
Tian Pan
Software Engineer

当你的 LLM 智能体失败时,最诱人的事情莫过于问它为什么。它会给出流畅、具体、看似充满自我意识的回答。它可能会说:"我误解了用户的意图,检索了关于 X 的文档,而实际上应该定向到 Y。"听起来就像是根本原因。你把它记下来,打开提示编辑器,然后花四十分钟追查一个错误的问题。

这就是 LLM 自我调试的核心陷阱。模型的解释和模型实际的失败机制是两回事。有时两者重叠,但经常并不重合。在采取行动之前判断自己处于哪种情况,是区分快速调试和昂贵弯路的关键所在。