如何在生产环境中调试 AI 智能体(实战指南)
调试生产环境中的 AI 智能体,主要是隔离出是哪一层出了问题——提示词、工具、模型还是编排。我用追踪 ID 记录每一步,重放完全相同的输入,然后二分定位。在我的智能体中,约 70% 的“AI 故障”最终都是管道故障,而非模型故障。
每周三。28,400+ 读者。纯干货。
✓ 请查收邮箱 — 点击确认链接以完成订阅。
✓ 订阅成功!
✓ 您已在订阅列表中。
目录
2026 年 6 月更新。
TL;DR: 调试生产环境中的 AI 智能体,主要是隔离出是哪一层出了问题——提示词、工具调用、模型输出还是编排。我用追踪 ID 记录每一步,重放完全相同的输入,并从那里开始二分定位。在我的智能体中,看似“AI 故障”的问题大约有 70% 最终都是管道问题:格式错误的工具结果、被截断的输入、被悄悄吞掉的异常。
操作者视角: 我运行着 100 多个生产智能体——Pickleland 的预订流程、内容流水线、收件箱分拣器。它们会以一切软件都会坏掉的方式坏掉,再加上几种新的方式。这是我当初希望自己拥有的实战指南:如何在不盯着一堵 token 墙的情况下找到出故障的层。
当智能体在生产环境中出问题时,本能反应是怪罪模型。“Claude 产生了幻觉。”有时确实如此。通常不是。模型只是五六层堆栈中的一层,而 bug 远更常出现在你自己写的那一层,而不是 Anthropic 交付的那一层。这篇文章讲的就是我找出它的系统化方法。
在调试任何东西之前,先让每次运行都可追踪
你无法调试你看不见的东西。你能做的最具杠杆效应的事情——在任何具体 bug 出现之前——就是给每次智能体运行附上一个追踪 ID,并记录它走过的每一步。
“一步”是指任何跨越边界的东西:传入的触发器、每次模型调用(带完整的消息数组)、每次工具调用(带参数)、每个工具结果,以及最终输出。把它们记录为以追踪 ID 为键的结构化 JSON。
function logStep(traceId: string, step: string, payload: unknown) {
console.log(JSON.stringify({
traceId,
step, // "trigger" | "model_call" | "tool_call" | "tool_result" | "output"
ts: Date.now(),
payload,
}));
}在 Cloudflare Workers 上,我把这些发送到队列并写入一张表;在本地它们输出到 stdout。规则是绝对的:如果某一步没有被记录,那么就调试而言它就没有发生过。这与我在我使用的智能体技术栈中描述的埋点一致——追踪 ID 是其他一切都挂靠其上的脊柱。
隔离出那一层:提示词、工具、模型还是编排
一旦你有了追踪,调试就变成了二分查找。一共有四层,而大多数情况下 bug 恰好只住在其中一层。
1. 输入层(最常见的元凶)
把进入失败模型调用的那个完全相同的 messages 数组拉出来。不是重建——而是日志里逐字逐句的载荷。然后像一个陌生人那样去读它。我那些“模型无视了指令”的 bug 中,有一半实际上是:
- 一个工具结果因为某处字符串化出错而返回成了
"[object Object]"。 - 一个输入因为撑爆了上下文窗口、又被一次粗暴的切片砍断,从而在句子中途被截断。
- 一个变量被插值成了
undefined,悄无声息地毒化了提示词。
如果输入是错的,那么模型在垃圾之上完美地完成了它的工作。去修管道。
2. 工具层
如果输入看起来干净,就检查是否有工具返回了一个被智能体当作成功对待的错误。一个经典案例:API 返回 200,正文却是 { "error": "rate limited" },而你的工具包装器没有检查正文,于是智能体自信满满地基于一条错误信息采取行动。把工具结果原样记录下来,并断言它的形状。
3. 模型层
只有在排除 1 和 2 之后,我才会怀疑模型。即便如此,“模型 bug”通常意味着“我的提示词有歧义”。把那个完全相同的失败输入拿来,丢进一个针对相同模型和温度的一次性脚本,看它是否复现。如果复现,修复手段是提示词工作或一次更严格的 eval,而不是慌张地换模型。
4. 编排层
如果单独一步孤立来看没问题,但多步运行却失败,那么 bug 就在交接处——步骤之间丢失的状态、一个竞态条件、一次重新运行了非幂等操作的重试。这些是最棘手的,我在多智能体编排模式中讲解了相关模式。
复现非确定性,而不是与它搏斗
让智能体感觉无法调试的,是非确定性:相同的输入在不同运行间产生不同的输出。你可以驯服它。
第一,能固定的就固定。 调试期间设置 temperature: 0。这不会让 Claude 完全确定,但会大幅收窄方差,让你能把真正的 bug 与采样噪声区分开。
第二,跑它 N 次。 如果某个故障是每 20 次运行复现 1 次,就把那个完全相同的输入循环 50 次,并捕获每一个输出。现在你有了一个样本,而不是一则逸闻。一个 5% 概率触发的 bug 是真正的 bug——你只是需要足够的量才能看见它。
for i in $(seq 1 50); do
node replay.mjs --trace=abc123 >> runs.jsonl
done
# 然后统计失败数
grep -c '"status":"fail"' runs.jsonl第三,对通过的运行和失败的运行做差异比对。 在固定温度且输入相同的情况下,输出的差异意味着存在一个你尚未发现的输入差异——提示词里的一个时间戳、一个会变化的工具结果、一个发生了变化的检索文档。
搭建一个重放装置,从此不再在生产环境中调试
通过重新触发线上智能体来调试既慢又危险——它会发出真实的邮件、预订真实的场地。相反,捕获追踪并在离线状态下重放它。
重放装置加载一条已记录的追踪,重建对任意一步的完全相同的输入,并仅针对模型重新运行那一步。因为你记录了完整的 messages 数组,所以你根本不需要上游系统。这把生产环境中一次 10 分钟的往返变成了一个 2 秒的本地循环,是我调试工作流中最大的提速。
一个好的重放装置还让你能够变异并重新运行:改动系统提示词的一行,重放那同样的 50 条失败追踪,看看现在有多少条能通过。这就是从调试通向 eval 的桥梁——一旦你有了一批失败追踪的语料,你就有了一套回归测试的雏形。
关注那些真正能预测崩坏的指标
有些故障从不抛出异常。智能体照常运行,返回某个看似合理的东西,却悄悄做了错事。要抓住这些,你得关注行为指标,而不只是错误率:
- 工具调用成功率(按每个工具)。这里的下降往往先于一次可见的故障。
- 输出 schema 有效性——有多少 % 的输出能针对预期结构成功解析。我用 Zod 校验每一个输出,并在有效性下滑时告警。
- 循环长度——每次运行的平均步数。突然的飙升通常意味着智能体卡在重试里了。
- 每次运行的成本——一个失控的循环会先表现为成本飙升,然后才表现为一条投诉。(当成本重要时,Haiku 对 Sonnet 的算账值得了解。)
我追踪这些指标的方式和我追踪其他一切的方式一样——见我如何衡量一个 AI 智能体是否真的在起作用。一个能抓住沉默故障的指标,抵得上十个只能抓住吵闹故障的指标。
5 分钟分诊清单
当一个智能体崩了、而我又赶时间时,我会按顺序跑这套:
- 拿到失败运行的追踪 ID。
- 读失败那一步的完全相同的输入。 它格式良好吗?(在这里能解决约 50% 的情况。)
- 在那条追踪里检查工具结果,找出伪装成成功的错误。
- 在
temperature: 0下离线重放那一步。 它复现吗? - 如果复现, 那是提示词/模型问题——修复并重跑追踪语料。如果不复现, 那是非确定性或状态/编排 bug——循环它 50 次来刻画它。
有纪律的隔离每一次都胜过巧妙的提示词。模型很少是问题所在;问题通常出在它周围的系统。
常见问题
我该如何调试一个只是偶尔失败的 AI 智能体?
从一条已记录的追踪中捕获那个完全相同的输入,在温度 0 下重放它 50 次以上。间歇性故障是触发率很低的真正 bug——量会把逸闻变成一个可复现的样本,让你能够比对并修复。
bug 通常在模型里还是在我的代码里?
在我的生产智能体中,看似的“AI 故障”大约有 70% 是管道问题:格式错误的工具结果、被截断的输入、被吞掉的异常,或步骤之间丢失的状态。在怀疑模型之前,先排除输入层和工具层。
调试智能体我需要的最低限度日志是什么?
每次运行一个追踪 ID,外加触发器、每次模型调用(完整消息数组)、每次工具调用及其原始结果、以及最终输出的结构化日志。如果一步没被记录,你就没法调试它。
我该如何不再对着线上生产环境调试?
搭建一个重放装置,它加载一条已记录的追踪,并使用捕获的输入在离线状态下重新运行任意单独的一步。它把一次缓慢、危险的生产往返变成一个快速的本地循环,并成为你回归测试套件的种子。
每周三。28,400+ 读者。纯干货。
✓ 请查收邮箱 — 点击确认链接以完成订阅。
✓ 订阅成功!
✓ 您已在订阅列表中。
将AI实战手册发送到您的邮箱
每周三。28,400+ 读者。纯干货。
请查收邮箱。
我们已向您发送确认邮件 — 点击其中的链接以完成订阅。如果一分钟内没收到,请检查垃圾邮件。
订阅成功。
欢迎 — 下一期很快就会送达您的邮箱。
您已在订阅列表中 — 每周三留意查收。