
最近折腾 Claude Code Agent,踩了几个坑,这篇把调试方法说清楚。
大多数 Agent 调试问题源于把 AI 执行当成同步代码来对待。开发者习惯性地加 console.log、单步调试,然后纳闷为什么本地好好的 Agent 上线就挂了。执行模型根本不一样:Agent 在多次 LLM(大语言模型)调用中做出非确定性决策,每次决策都受到上下文影响,而上下文在每次运行之间是变化的。
传统调试假设确定性行为。设个断点,检查状态,复现问题。Agent 执行打破了这三个假设。同样的输入产生不同的工具调用。上下文窗口静默溢出。模型凭空编造出不存在的字段名。等错误暴露出来的时候,导致问题的决策轨迹已经消失了。
解决办法是捕获完整的执行路径:每一次工具调用、每一个模型决策、每一次上下文状态转换。Agent 需要执行转录,不仅展示发生了什么,还要展示 Agent 为什么选择每个动作。这个区别很关键。没有推理链,调试就变成了考古——在日志里挖来挖去,试图重建本质上属于概率性的决策。
这个区别把调试从被动救火变成了系统性根因分析。这篇聊几个关键模式:阅读 Claude Code 转录、追踪工具执行、识别常见失败模式,以及构建在上线前就能捕获问题的可观测性系统。
Agent 转录揭示了从用户输入到最终输出的完整决策序列。每条转录包含对话历史、工具调用及其输入输出、以及模型在每一步的推理。有效阅读这些转录需要理解 Claude Code 捕获了什么,又遗漏了什么。
转录结构遵循线性的轮次序列。每轮包含用户消息或助手消息,可选带有工具调用。工具调用包括函数名、参数和结果。关键信息藏在三个地方:助手调用工具前的推理、揭示模型理解内容的工具参数、以及展示执行是否成功的工具结果。
大多数调试失败发生在开发者跳过推理步骤的时候。他们看到工具调用的参数错了,就以为模型做了错误的决策。推理过程揭示了真正的问题:模型缺少关于有效参数值的上下文,或者工具描述有歧义,或者前面的工具结果包含了误导信息。
上下文溢出在转录中表现为模型忘记早期的指令或工具结果。转录显示所有消息,但 Claude Code 不会在上下文窗口接近极限时提示。开发者必须手动计算 Token 数量,并观察症状:模型重复已经问过的问题、忽略对话早期的工具结果、或者做出与既定上下文矛盾的决策。
这里的意思是,转录长度和调试难度正相关。3-5 次工具调用的短对话分析起来很简单。20+ 次工具调用的对话需要系统性分析:识别执行可能分叉的决策点,检查每个工具结果是否影响了下一个决策,并验证关键上下文在整个过程中是否始终可访问。
工具调用追踪捕获 Agent 执行偏离预期行为的确切时刻。工具名、参数和结果构成一个三元组,既揭示了 Agent 尝试了什么,也揭示了是否成功。有效追踪需要结构化日志,保留整个执行过程中的这个三元组。
interface ToolCall { id: string; name: string; arguments: Record<string, unknown>; result: { success: boolean; data?: unknown; error?: string; }; timestamp: number; contextTokens: number;
} class AgentTracer { private calls: ToolCall[] = []; logToolCall(call: ToolCall): void { this.calls.push(call); // Detect immediate failure patterns if (!call.result.success) { this.analyzeFailure(call); } // Detect hallucinated arguments const schema = this.getToolSchema(call.name); const invalidArgs = this.findInvalidArguments(call.arguments, schema); if (invalidArgs.length > 0) { console.warn(`Hallucinated arguments in ${call.name}:`, invalidArgs); } } private analyzeFailure(call: ToolCall): void { const recentCalls = this.calls.slice(-5); const sameToolFailures = recentCalls.filter( c => c.name === call.name && !c.result.success ); if (sameToolFailures.length >= 2) { console.error(`Reasoning loop detected: ${call.name} failed ${sameToolFailures.length} times`); } } private findInvalidArguments( args: Record<string, unknown>, schema: Record<string, { type: string; required?: boolean }> ): string[] { return Object.keys(args).filter(key => !(key in schema)); } getExecutionSummary(): string { const total = this.calls.length; const failed = this.calls.filter(c => !c.result.success).length; const avgTokens = this.calls.reduce((sum, c) => sum + c.contextTokens, 0) / total; return `${total} tool calls, ${failed} failures, ${avgTokens.toFixed(0)} avg tokens`; }
}
这个追踪器在工具调用发生时捕获它们,并立即检查两种常见失败模式:同一工具反复失败、以及参数不在工具 Schema 中。这两种模式都表明 Agent 卡住了,如果不干预就不太可能恢复。
工具参数幻觉发生在模型编造看起来合理但与 Schema 不匹配的字段名时。模型看到 searchDocuments 有个 query 参数,就想当然认为一定有 requireUnique 或 maxResults 参数,因为类似工具都有这些。工具执行时会因为校验失败而报错,但模型把错误理解为查询问题,而不是 Schema 误解。
这种失败模式很隐蔽,但代价很高。Agent 用不同的查询值重试,白白消耗 Token 和延迟,而实际修复只需要删除那个幻觉出来的字段。要早点检测到这个问题,需要在执行前对比参数和已知 Schema,并在出现意外字段时发出警告。
三种失败模式占了大多数生产 Agent 问题:导致模型忘记关键信息的上下文溢出、校验失败的幻觉字段、以及 Agent 反复重试同样失败方法的推理循环。
上下文溢出发生在对话历史加工具结果超过模型上下文窗口时。Claude Code 不会在这种情况下抛出错误。更早的消息会被静默截断。模型继续处理,但无法访问更早的上下文。依赖那些上下文的决策会变得不连贯。
症状表现为不一致的行为:Agent 询问已经收到过的信息、忽略最初 Prompt 中指定的约束、或者做出与对话开头工具结果矛盾的决策。开发者看到这些症状会觉得模型不可靠,而实际问题其实是机械性的:上下文容量不足。
修复需要监控整个执行过程中的上下文 Token 数,并在达到限制前实现摘要策略。当 Token 接近最大值的 75% 时,将较早的消息摘要成压缩上下文,保留关键信息。这很重要,因为上下文溢出是可预测的——Token 数量是确定性的——但没有显式追踪就看不见。
字段幻觉出现在工具 Schema 描述不足时,或者模型遇到类似但 Schema 不同的工具时。模型根据训练时学到的模式生成看似合理的参数,但那些模式与实际工具接口不匹配。
interface ToolSchema { name: string; description: string; parameters: { type: "object"; properties: Record<string, { type: string; description: string; enum?: string[]; }>; required: string[]; };
} function validateToolCall( call: { name: string; arguments: Record<string, unknown> }, schema: ToolSchema
): { valid: boolean; errors: string[] } { const errors: string[] = []; const validProps = new Set(Object.keys(schema.parameters.properties)); // Check for hallucinated arguments Object.keys(call.arguments).forEach(arg => { if (!validProps.has(arg)) { errors.push(`Unexpected argument '${arg}' not in schema for ${call.name}`); } }); // Check for missing required arguments schema.parameters.required.forEach(req => { if (!(req in call.arguments)) { errors.push(`Missing required argument '${req}' in ${call.name}`); } }); // Check enum violations Object.entries(call.arguments).forEach(([key, value]) => { const prop = schema.parameters.properties[key]; if (prop?.enum && !prop.enum.includes(String(value))) { errors.push(`Invalid value '${value}' for ${key}, must be one of: ${prop.enum.join(", ")}`); } }); return { valid: errors.length === 0, errors };
}
执行前校验能防止幻觉参数到达工具。Agent 立即收到关于 Schema 违规的反馈,而不是神秘的执行错误。这把调试时间从分析错误信息缩短到修复 Schema 描述或约束参数生成。
推理循环发生在 Agent 遇到失败、尝试用微小改动重试、再次失败、然后继续这种模式时。每次迭代消耗 Token 和延迟,却没有向解决方案推进。循环继续直到上下文溢出或用户介入。
检测需要追踪最近的工具调用模式。如果同一工具用类似参数失败三次,Agent 很可能卡住了。打破循环需要外部干预:注入一条系统消息,明确禁止进一步重试该工具,或者升级到人工操作员,他可以提供替代上下文。
LLM 擅长在大规模转录量上的模式识别。与其手动审查数百条失败的 Agent 运行,开发者可以用第二个 LLM 分析转录并识别常见失败模式。这种元分析模式需要结构化 Prompt,把症状描述和根因推断分开。
interface TraceAnalysisPrompt { systemPrompt: string; traceContext: { toolCalls: ToolCall[]; conversationLength