第 21 章 排错与调试实战
本章要解决的问题
线上 Agent 出问题了,只知道「答得不对」——怎么一步步定位到是提示词、工具、还是模型的问题?
章节大纲
- 21.1 Agent 全链路追踪(Trace 到每步思考/工具调用)
- 21.2 常见故障分类:幻觉、循环、上下文溢出、工具错配、成本爆炸
- 21.3 线上问题定位 SOP
- 🛠 解决方案:Agent 故障排查手册(40+ 真实问题对照表)
21.1 Agent 全链路追踪
21.1.1 为什么 Agent 排错比传统软件难 10 倍
传统软件的 bug 是可复现的(同一输入 → 同一错误),Agent 的故障是概率性的:这次答错,下次可能对;换个人问可能就正常。没有追踪,你甚至不知道"错的那次"发生了什么。
第 6 章强调过"每步留痕"——本章把它升级为生产级的全链路追踪。
21.1.2 追踪的最小数据模型
每个 Agent 任务(trace)包含一组事件(span),每个事件记录一次调用:
图 1:Trace 数据模型
TRACE = {
"trace_id": "t-20260807-001",
"task": "查询订单 A123 物流",
"user_id": "u-1001",
"started_at": "...", "ended_at": "...",
"total_cost": 0.0123, "total_tokens": 3200,
"spans": [
{
"span_id": "s1",
"type": "llm", # 模型调用
"model": "deepseek-chat",
"input": "系统提示词 + 历史…", # prompt 摘要
"output": "调用工具 query_logistics",
"tool_calls": [{"name": "query_logistics",
"args": {"order_id": "A123"}}],
"tokens": 1200, "cost": 0.004, "latency_ms": 850,
},
{
"span_id": "s2",
"type": "tool", # 工具执行
"tool": "query_logistics",
"args": {"order_id": "A123"},
"result": {"status": "shipped", "eta": "明天"}, # 成功
"latency_ms": 120,
},
{
"span_id": "s3",
"type": "llm", # 最终回答
"input": "...工具结果回填...",
"output": "您的订单 A123 已发货,预计明天送达。",
"latency_ms": 600,
},
],
}
排错的本质 = 顺着 trace 找到"哪个 span 出了问题"——是模型决策错(s1)、工具执行失败(s2)、还是最终生成错(s3)。
21.1.3 追踪工具的落地
| 层级 | 工具 | 说明 |
|---|---|---|
| 快速起步 | 自研 JSON 日志 | 每 span 一条日志,零依赖 |
| 专业方案 | Langfuse(推荐) | LLM 专用:trace/评测/成本面板 |
| 生态绑定 | LangSmith | LangChain 生态 |
| 统一监控 | OpenTelemetry | 与现有监控体系统一 |
落地顺序:先自研日志跑起来 → 数据积累后接 Langfuse 可视化——别一上来就上重工具。
21.2 常见故障分类
21.2.1 五大故障族
生产 Agent 的故障可以归为五类,先分类再定位:
图 2:五大故障族
| 故障族 | 表现 | 高频根因 |
|---|---|---|
| 幻觉 | 编造事实/数据/参数 | 知识缺失硬答、温度高、无约束(呼应 13/17 章) |
| 循环 | 反复调用、无进展 | 无停止条件、无进展检测(呼应 6 章) |
| 上下文溢出 | 答非所问、爆窗 | 历史无压缩、结果全量回填(呼应 3 章) |
| 工具错配 | 调错工具/参数乱填 | Schema 描述弱、温度高(呼应 5/14 章) |
| 成本爆炸 | 单任务费用异常 | 无限重试、无预算封顶(呼应 23/24 章) |
21.2.2 每种故障的 trace 特征
看 trace 就能初步判断故障族:
| 故障 | trace 特征 |
|---|---|
| 幻觉 | 无工具调用 span,模型直接回答;或答案无法对应任何工具结果 |
| 循环 | 多个相同的 tool_calls span 重复出现;result 相同 |
| 上下文溢出 | 单个 llm span 的 input 特别长(接近窗口上限) |
| 工具错配 | tool_calls 里的工具名/参数与需求不符 |
| 成本爆炸 | spans 数量异常多;cost 累计超限 |
排错口诀:先看 trace,再猜原因——trace 会告诉你"哪里错了",别对着线上输出瞎猜。
21.3 线上问题定位 SOP
21.3.1 五步定位流程
[1 收集现场] 找到出问题的 trace_id(用户报错时间 / 日志检索)
↓
[2 看全貌] 整个 trace 有多少 span?哪一步开始不对劲?
↓
[3 定位环节] 问题在 模型决策 / 工具执行 / 上下文组织 / 最终生成?
↓
[4 复现验证] 用同一输入 + 同版本参数跑本地,确认可复现率
↓
[5 修复回归] 修复后跑评测集回归(第 20 章),确认无退化
图 3:线上定位五步 SOP
21.3.2 定位到具体环节后的处理
| 定位结果 | 下一步 |
|---|---|
| 模型决策错(该调工具没调) | 检查工具描述/Schema(第 5、14 章) |
| 工具执行失败 | 检查工具代码、权限、上游系统 |
| 上下文组织问题 | 检查历史压缩/按需投喂(第 3 章) |
| 最终生成错 | 检查提示词约束、温度(第 4、24 章) |
| 成本异常 | 检查重试/循环/缓存(第 23、24 章) |
21.3.3 可复现率:概率故障的验证方法
Agent 故障往往不能 100% 复现,用可复现率判断:
图 4:可复现率判断
同一输入跑 N 次(N≥10):
失败次数/N = 可复现率
├─ 高复现率(>70%)→ 确定性 bug,对照 trace 排查
└─ 低复现率(<30%)→ 概率性故障,检查温度/随机性/竞态
21.4 真实排错案例复盘(3 个完整案例)
把前面所有方法串起来,用 3 个真实场景演示"从现象到根因到修复"的完整路径。
案例一:客服 Agent"答非所问"(高复现率)
现象:某客服 Agent 对"物流到哪了"的回答经常驴唇不对马嘴,复现率 80%。
排查过程(五步 SOP):
[1 收集现场] 找到 trace_id,看该任务全部 span
[2 看全貌] 发现只有 2 个 span:1 次模型调用 → 直接回答(没调工具)
[3 定位环节] 问题在"模型决策"——该调物流工具但没调
[4 复现验证] 同一问题跑 10 次,8 次没调工具(复现率 80%)
[5 修复] 检查工具 Schema 描述
根因:query_logistics 工具的 description 写的是"查询订单",模型不知道"物流进度"应该用它。
修复:描述改为"查询订单物流状态。当用户询问发货/配送/物流/到哪了时使用。"——重跑评测,工具调用率从 20% 升到 95%。
教训:高复现率"该调没调",先看工具描述是否覆盖用户问法(第 5/14 章:描述是工具的灵魂)。
案例二:Agent 偶发死循环(低复现率)
现象:某个 Agent 偶尔(约 15% 概率)陷入循环,反复调用同一个工具,直到超时。
排查过程:
[1 收集现场] trace 显示 8+ 个相同 tool_calls span,结果相同
[2 看全貌] 循环特征明显:同一工具、同一参数、结果不变
[3 定位环节] 问题在"循环控制"——无进展检测缺失
[4 复现验证] 低复现率(15%),与温度/seed 相关
[5 修复] 加无进展检测 + 调用去重
根因:工具返回"查询中"这类中间状态,模型收到后不知道该不该继续,偶尔"原地打转"。
修复:
# 1. 相同调用去重(第6章)
def dedup(tool_calls, history):
sig = (call.name, json.dumps(call.args, sort_keys=True))
return sig not in history # 相同签名不重复执行
# 2. 无进展检测(第6章)
def no_progress(history, threshold=3):
recent = [h["result"] for h in history[-threshold:]]
return len(set(recent)) == 1 # 连续结果相同 → 终止
教训:低复现率 + 相同调用重复 = 典型的"无进展循环",用无进展检测根治(第 6 章 6.3.2)。
案例三:RAG 问答"查得到但答不对"(高复现率)
现象:知识库问答,检索命中率 90%,但答案经常漏掉关键信息,复现率接近 100%。
排查过程:
[1 收集现场] trace 显示检索命中了正确文档(命中率 OK)
[2 看全貌] 上下文里有 8 个 chunk,关键信息在第 6 个
[3 定位环节] 问题在"上下文组织"——Top-K 太大,关键信息被淹没
[4 复现验证] 稳定复现(确定性高)
[5 修复] 缩小 Top-K + 加重排
根因:Top-8 全塞进上下文,模型注意力被无关 chunk 稀释(第 3 章注意力稀释)。
修复:混合检索召回 Top-20 → Cross-Encoder 重排取 Top-3(第 8 章)——只喂精排后的 3 个块,答案完整率显著提升。
教训:检索命中率高但答不对 → 问题在"上下文组织"而非"检索"——用第 8 章定位口诀"先检索、后生成"判断。
案例复盘总结
| 案例 | 复现率 | 故障族 | 关键排查点 | 修复 |
|---|---|---|---|---|
| 答非所问 | 80% | 工具错配 | trace 发现"该调没调" | 改工具描述 |
| 偶发循环 | 15% | 循环 | 相同调用重复 | 无进展检测 |
| 查得到答不对 | 100% | 上下文 | 命中率高但组织差 | 重排取 Top-3 |
三个案例的共同点:都是"先看 trace 定位环节 → 再用对症工具修复"——没有 trace 全靠猜,有了 trace 全是套路。
🛠 解决方案:Agent 故障排查手册(40+ 真实问题对照表)
幻觉类(10 例典型)
| 现象 | 根因 | 修复 |
|---|---|---|
| 编造订单号 | 无工具/参数幻觉 | 强制走工具 + 参数校验(5 章) |
| 编造政策条款 | 知识缺失硬答 | 接 RAG + "没有就说没有"(8 章) |
| 编造数据来源 | 无约束 | 加"仅基于材料"约束 + 温度调低 |
| 反思修不好幻觉 | 知识型错误 | 反思修疏忽,幻觉用工具/检索(13 章) |
循环类(8 例典型)
| 现象 | 根因 | 修复 |
|---|---|---|
| 反复调同一工具 | 无进展检测 | 相同签名去重 + 无进展终止(6 章) |
| 计划来回改 | 重规划无上限 | 重规划上限 + 历史去重(15 章) |
| 反思不停 | 无停止条件 | 三停止条件(13 章) |
| 步数不够就超时 | 任务复杂 | 调 max_iterations + 查任务拆解 |
上下文类(8 例典型)
| 现象 | 根因 | 修复 |
|---|---|---|
| 聊几轮失忆 | 无压缩 | 摘要 + 滑动窗口(3 章) |
| 答非所问 | 注意力稀释 | 指令前置 + 裁剪 |
| 爆窗 400 错误 | 超窗口 | 压缩 + 精简回填 |
| 答案不完整 | 输出预留不足 | 预留 20% + max_tokens(24 章) |
工具类(8 例典型)
| 现象 | 根因 | 修复 |
|---|---|---|
| 调错工具 | 描述不清 | 重写描述(14 章) |
| 参数乱填 | 温度高/Schema 弱 | 低温度 + 严格 Schema(5 章) |
| 一直失败 | 无熔断 | 重试上限 + 熔断(24 章 B1) |
| 注入调危险工具 | 无输入过滤 | 注入检测 + 敏感确认(17/22 章) |
成本类(6 例典型)
| 现象 | 根因 | 修复 |
|---|---|---|
| 单任务成本飙升 | 无限循环 | 停止条件 + 无进展检测(6 章) |
| 重试成本爆炸 | 无上限 | 重试 2~3 + 熔断(5 章) |
| 缓存失效成本高 | 未开缓存 | Prompt 缓存 + 结果缓存(23 章) |
实战提示
- 排错先看 trace:顺着 span 找"哪里开始不对劲",别对着输出猜。
- 故障先分类:幻觉/循环/上下文/工具/成本五大类,先归类再定位。
- 可复现率说话:概率故障跑 10 次算复现率,别被单次复现误导。
- 修复必回归:改完跑评测集(第 20 章),防止修好 A 弄坏 B。
- 排查手册当团队资产:每个新故障沉淀进手册,越用越全。