第 38 章:调试的艺术

个人公众号

源码验证日期:2026-05-15,基于 commit 0d81bb6

上一章,你学会了写测试。测试告诉你”出错了”——但它通常不会告诉你”为什么出错”。从”测试失败了”到”找到 bug 的根因”,中间有一段路要走。这段路,就是调试。

调试是一门手艺。好的调试不是靠运气瞎猜,而是系统性地缩小问题范围。这一章,我们学习 Claude Code 内置的调试工具,以及如何运用它们追踪问题。


路线图

1
2
3
4
5
6
7
graph LR
CH37["第 37 章<br/>编写测试"] --> CH38["🔧 第 38 章<br/>调试的艺术"]
CH38 --> CH39["第 39 章<br/>从代码到贡献"]

style CH38 fill:#4CAF50,color:#fff,stroke:#333
style CH37 fill:#e8f5e9,stroke:#333
style CH39 fill:#e1f5fe,stroke:#333

认识内置的 debug 日志系统

Claude Code 有一个精心设计的调试日志系统,核心在 src/utils/debug.ts。它不是一个简单的 console.log 包装器——它有日志级别、过滤机制、缓冲写入、文件轮转。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 文件:src/utils/debug.ts
export function logForDebugging(
message: string,
{ level }: { level: DebugLogLevel } = { level: 'debug' },
): void {
// 检查日志级别
if (LEVEL_ORDER[level] < LEVEL_ORDER[getMinDebugLogLevel()]) {
return
}
// 检查是否应该记录这条消息
if (!shouldLogDebugMessage(message)) {
return
}
// 格式化并写入
const timestamp = new Date().toISOString()
const output = `${timestamp} [${level.toUpperCase()}] ${message.trim()}\n`
if (isDebugToStderr()) {
writeToStderr(output)
return
}
getDebugWriter().write(output)
}

这个函数做了三件事:

  1. 级别过滤——低于最低级别的消息直接丢弃。日志级别从低到高是:verbose < debug < info < warn < error
  2. 消息过滤——根据 debug 过滤器决定是否记录
  3. 输出路由——要么写到 stderr(实时),要么写到缓冲写入器(后台异步写入文件)

日志写到哪里?默认位置是:

1
~/.claude/debug/<session-id>.txt

同时维护一个 latest 符号链接,指向当前会话的日志文件。你可以随时用 tail -f ~/.claude/debug/latest 实时查看日志。


启动调试模式

有几种方式启用调试日志:

--debug-d——最简单的方式。启用调试日志并写入文件:

1
claude --debug

--debug=pattern——带过滤器的调试。只记录匹配特定分类的消息:

1
2
claude --debug=api,mcp       # 只看 api 和 mcp 相关的日志
claude --debug=!1p,!file # 排除 1p 和 file 类别的日志

过滤器的实现很有意思。src/utils/debugFilter.ts 定义了分类提取逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 文件:src/utils/debugFilter.ts
export function extractDebugCategories(message: string): string[] {
const categories: string[] = []
// "category: message" → 提取 category
const prefixMatch = message.match(/^([^:[]+):/)
if (prefixMatch) categories.push(prefixMatch[1].trim().toLowerCase())
// "[CATEGORY] message" → 提取 CATEGORY
const bracketMatch = message.match(/^\[([^\]]+)]/)
if (bracketMatch) categories.push(bracketMatch[1].trim().toLowerCase())
// "MCP server \"name\"" → 提取 mcp 和 name
const mcpMatch = message.match(/^MCP server ["']([^"']+)["']/)
if (mcpMatch) { categories.push('mcp'); categories.push(mcpMatch[1].toLowerCase()) }
return Array.from(new Set(categories))
}

日志消息的分类是根据消息格式自动提取的。比如 MCP server "github": connection established 会被分类为 mcpgithub

--debug-to-stderr-d2e——把日志直接写到标准错误输出:

1
claude --debug-to-stderr 2>&1 | tee debug-output.log

环境变量 DEBUG——经典的环境变量开关:

1
DEBUG=1 claude

用 logForDebugging 添加自定义日志

当你在开发自己的工具或插件时,可以导入并使用这个日志系统:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { logForDebugging } from '../utils/debug.js'

// 在你的工具逻辑中
async call(input) {
logForDebugging(`MyTool called with input: ${JSON.stringify(input)}`)

// ... 核心逻辑 ...

logForDebugging(`MyTool result: ${result}`, { level: 'info' })

// 出错时
logForDebugging(`MyTool error: ${error.message}`, { level: 'error' })

return { data: result }
}

注意几个要点:

  • 不要在非 debug 模式下依赖日志——shouldLogDebugMessage 在非 debug 模式下返回 false,日志不会被写入。logForDebugging 不会影响生产性能。
  • 使用合适的级别——verbose 用于高频诊断信息,debug 用于常规调试,info 用于重要事件,warn 用于异常但可恢复的情况,error 用于真正的错误。
  • CLAUDE_CODE_DEBUG_LOG_LEVEL=verbose 启用最高级别——默认级别是 debugverbose 级别的消息会被过滤掉。

使用内置的 /debug 命令

Claude Code 有一个内建的 /debug 命令,用于调试当前会话的问题:

1
/debug [问题描述]

它做了以下事情:

  1. 如果你之前没有启用 debug 日志,它会立即启用(但之前的日志已经丢失了)
  2. 读取当前会话的 debug 日志的最后 20 行
  3. 分析日志中的 [ERROR][WARN] 条目
  4. 给出诊断结论和修复建议

源码在 src/skills/bundled/debug.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 文件:src/skills/bundled/debug.ts
export function registerDebugSkill(): void {
registerBundledSkill({
name: 'debug',
description: 'Enable debug logging for this session and help diagnose issues',
allowedTools: ['Read', 'Grep', 'Glob'],
async getPromptForCommand(args) {
const wasAlreadyLogging = enableDebugLogging()
const debugLogPath = getDebugLogPath()

// 读取日志文件的最后 64KB
const stats = await stat(debugLogPath)
const readSize = Math.min(stats.size, TAIL_READ_BYTES)
// ... 读取并格式化最后 20 行 ...

return [{
type: 'text',
text: `# Debug Skill\n\n日志路径: ${debugLogPath}\n\n${logInfo}\n\n## 问题\n${args || '请描述你遇到的问题'}`
}]
},
})
}

这是一个很好的调试流程参考:先启用日志,让用户重现问题,然后分析日志


性能分析——启动性能

当你觉得 Claude Code 启动太慢时,可以使用内置的启动性能分析器:

1
CLAUDE_CODE_PROFILE_STARTUP=1 claude

分析器会在 src/utils/startupProfiler.ts 中记录一系列检查点:

1
2
3
4
5
6
7
// 文件:src/utils/startupProfiler.ts
const PHASE_DEFINITIONS = {
import_time: ['cli_entry', 'main_tsx_imports_loaded'],
init_time: ['init_function_start', 'init_function_end'],
settings_time: ['eagerLoadSettings_start', 'eagerLoadSettings_end'],
total_time: ['cli_entry', 'main_after_run'],
}

分析结果输出到 ~/.claude/startup-perf/<session-id>.txt,报告长这样:

1
2
3
4
5
6
7
8
9
STARTUP PROFILING REPORT

0.0ms +0.0ms cli_entry heap: 12.3MB
45.2ms +45.2ms cli_dump_system_prompt_path
89.7ms +44.5ms main_tsx_imports_loaded heap: 28.1MB
...
312.5ms +12.3ms main_after_run heap: 45.6MB

Total startup time: 312.5ms

每一行显示了:相对起始时间、与上一个检查点的差值、检查点名称、堆内存使用量。如果某个步骤的耗时异常高,你一眼就能看到。


性能分析——查询性能

运行时的查询性能是另一回事。Claude Code 有一个专门的查询分析器:

1
CLAUDE_CODE_PROFILE_QUERY=1 claude

这个分析器追踪了完整的查询生命周期:

1
2
3
4
5
6
7
8
query_user_input_received          # 用户输入开始
query_context_loading_start/end # 系统提示词加载
query_tool_schema_build_start/end # 工具 schema 构建
query_api_request_sent # HTTP 请求发出
query_first_chunk_received # 第一个流式数据块(TTFT)
query_api_streaming_end # 流式传输完成
query_tool_execution_start/end # 工具执行
query_end # 查询结束

报告会计算关键指标:

1
2
3
4
5
6
7
8
9
Total TTFT: 1234ms
- Pre-request overhead: 234ms (19.0%)
- Network latency: 1000ms (81.0%)

PHASE BREAKDOWN:
Context loading 45ms
Tool schemas 120ms
Message normalization 35ms
Network TTFB 1000ms

如果 “Tool schemas” 占了很大比例,可能是工具数量太多。如果 “Network TTFB” 占主导,瓶颈在 API 调用。


慢操作检测

除了主动的性能分析,Claude Code 还有一个被动的慢操作检测系统:

1
2
3
4
5
// 文件:src/utils/slowOperations.ts
export function jsonStringify(value, replacer?, space?) {
using _ = slowLogging`JSON.stringify(${value})`
return JSON.stringify(value, replacer, space)
}

using _ = slowLogging\…` 是一个精巧的 JavaScript 模式——using 是 ECMAScript 的资源管理语法,slowLogging返回一个Disposable` 对象,在代码块结束时计算耗时并决定是否记录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class AntSlowLogger {
constructor(args) {
this.startTime = performance.now()
this.err = new Error() // 捕获调用栈
}
[Symbol.dispose]() {
const duration = performance.now() - this.startTime
if (duration > SLOW_OPERATION_THRESHOLD_MS) {
logForDebugging(
`[SLOW OPERATION DETECTED] ${description} (${duration.toFixed(1)}ms)`
)
}
}
}

阈值通过环境变量控制:

1
2
# 开发环境默认 20ms
CLAUDE_CODE_SLOW_OPERATION_THRESHOLD_MS=50 claude --debug

这个系统帮你捕捉到你可能根本不知道存在的性能问题。


VS Code 断点调试

日志和性能分析是”事后分析”。断点调试是”现场分析”——在代码执行的时候暂停,查看变量状态。

回忆第 29 章的 .vscode/launch.json 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug CLI (interactive)",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/cli.js",
"args": [],
"console": "integratedTerminal"
},
{
"name": "Debug CLI (query profiling)",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/cli.js",
"args": [],
"console": "integratedTerminal",
"env": {
"CLAUDE_CODE_PROFILE_QUERY": "1",
"CLAUDE_CODE_DEBUG_LOG_LEVEL": "verbose"
}
}
]
}

在 VS Code 里按 F5,选择对应的配置,就能启动调试。因为 cli.js.map 存在,VS Code 可能会把断点映射回 TypeScript 源码。试试在 src/tools/GlobTool/GlobTool.tscall() 方法里设断点。


调试策略——系统化缩小范围

工具千千万,思路只一条:系统化地缩小问题范围

第一步:复现。 你必须能稳定地复现问题。用 --debug-to-stderr 启动,记录完整的会话日志。

第二步:定位层级。 问题发生在哪一层?

  • 输入层——AI 传过来的参数不对?检查 inputSchema.parse() 的输出
  • 权限层——操作被拒绝了?检查 checkPermissions() 的返回值
  • 执行层——call() 内部出错了?在 call() 里加日志
  • 输出层——结果格式不对?检查 mapToolResultToToolResultBlockParam()

第三步:加日志。 在问题层的入口和出口各加一行 logForDebugging

1
2
3
4
5
6
async call(input) {
logForDebugging(`[MyTool] enter call, input=${JSON.stringify(input)}`)
// ... 核心逻辑 ...
logForDebugging(`[MyTool] exit call, result=${JSON.stringify(result)}`)
return result
}

如果 enter 出现了但 exit 没有,说明中间抛了异常。如果 exit 的 result 和期望不一样,说明逻辑有 bug。

第四步:二分查找。 如果问题在一段长逻辑中,用二分法缩小范围:在中间加日志,看前半段还是后半段有问题。

第五步:固定和验证。 找到问题后,先写一个测试复现它(防止回归),然后修复,最后确认测试通过。

实战:调试那个贯穿全书的死循环 Bug

咱们把五步策略用在那个从卷零就缠着我们的 bug 上——Agent 反复读 src/auth.ts 25 次。

第一步:复现。 启动 debug 模式,输入”帮我修复 src/auth.ts 中的 login 问题”,观察输出:

1
2
claude --debug-to-stderr
> 帮我修复 src/auth.ts 中的 login 问题

从日志中提取每次工具调用:

1
2
grep "tool_use" ~/.claude/debug/latest | grep -c "Read"
# 输出: 25

确认了:25 次 Read,0 次 Edit。

第二步:定位层级。 发生在这个 bug 中涉及的层级:

  • 执行层(toolExecution.ts):Read 工具每次执行正确,返回文件内容 → 不是这里
  • API 层(claude.ts):API 正常返回内容,模型收到完整上下文 → 不是这里
  • 模型决策层:模型每轮都输出 tool_use: Read 而不是 tool_use: Edit是这里

第三步:加日志。queryLoop 中给每轮的工具调用计数:

1
2
3
4
5
6
7
8
9
10
11
// → 在 queryLoop 中添加
const toolCallHistory = new Map<string, number>()
// 每轮记录
for (const block of response.content) {
if (block.type === "tool_use") {
const key = `${block.name}:${JSON.stringify(block.input)}`
const count = (toolCallHistory.get(key) ?? 0) + 1
toolCallHistory.set(key, count)
logForDebugging(`[Loop] tool_call #${turnCount}: ${key} (第${count}次)`)
}
}

重新运行,看到:

1
2
3
4
[Loop] tool_call #1: Read:{"file_path":"src/auth.ts"} (第1次)
[Loop] tool_call #2: Read:{"file_path":"src/auth.ts"} (第2次)
[Loop] tool_call #3: Read:{"file_path":"src/auth.ts"} (第3次)
...完全相同直到第25次

第四步:二分查找。 问题不在代码层面(代码正确执行了),而在模型行为层面。二分法不适用——我们需要改分析方向。

第五步:固定和验证。 Root cause 找到了:模型没有”意识到”自己在重复。解决不是修代码,而是在 system prompt 中加一条指令,并在卷五 ch63 实现代码级的循环检测。等 ch63 实现后,用这个 bug 做回归测试——确保连续 3 次相同工具调用后 Agent 会停下来问用户。


当调试不够用 — 结构化可观测性

调试是”手动调查一个问题”。但当你把 Agent 跑在服务器上、处理成百上千个任务时,手动调试不够用了。你需要可观测性——系统在运行时的自我报告能力。

结构化日志

console.log 的问题在于不可搜索、不可聚合。结构化日志用统一的格式记录,让你能按字段查询和统计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// → 结构化 Logger(最小实现 ~25 行)
type LogLevel = "debug" | "info" | "warn" | "error"

interface LogEntry {
timestamp: string
level: LogLevel
sessionId: string
message: string
context?: Record<string, unknown>
}

class StructuredLogger {
constructor(private sessionId: string) {}

log(level: LogLevel, message: string, context?: Record<string, unknown>) {
const entry: LogEntry = {
timestamp: new Date().toISOString(),
level,
sessionId: this.sessionId,
message,
context,
}
// 输出为 JSON Lines(每行一个 JSON,方便 grep + jq 处理)
process.stderr.write(JSON.stringify(entry) + "\n")
}

debug(msg: string, ctx?: Record<string, unknown>) { this.log("debug", msg, ctx) }
info(msg: string, ctx?: Record<string, unknown>) { this.log("info", msg, ctx) }
warn(msg: string, ctx?: Record<string, unknown>) { this.log("warn", msg, ctx) }
error(msg: string, ctx?: Record<string, unknown>) { this.log("error", msg, ctx) }
}

console.log 的对比:

1
2
3
4
5
6
7
8
❌ console.log:
"查询失败: tool_use timeout"
→ 哪个会话?哪一轮?哪个工具?不知道。

✅ 结构化日志:
{"timestamp":"2026-05-17T10:30:00Z","level":"error","sessionId":"sess-42",
"message":"tool execution timeout","context":{"tool":"Bash","turn":3,"durationMs":30001}}
→ 按 sessionId 聚合、按 tool 统计、按 turn 过滤。

在 Agent Loop 中埋点

Agent 的每一步都应该产生日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// → 在 AgentLoop 中埋点
class AgentLoop {
private logger: StructuredLogger

async *run(input: string): AsyncGenerator<AgentEvent> {
this.logger.info("agent.start", { input: input.substring(0, 100) })

for (let turn = 1; turn <= this.config.maxTurns; turn++) {
const turnStart = Date.now()
this.logger.debug("turn.start", { turn })

const response = await this.client.createMessage({ /*...*/ })
this.logger.debug("turn.api_response", {
turn,
stopReason: response.stop_reason,
tokens: response.usage,
durationMs: Date.now() - turnStart,
})

if (response.stop_reason === "tool_use") {
for (const block of response.content) {
if (block.type === "tool_use") {
const toolStart = Date.now()
try {
const result = await this.toolExecutor.execute(block)
this.logger.debug("turn.tool_success", {
turn,
tool: block.name,
durationMs: Date.now() - toolStart,
})
yield { type: "tool_result", data: result }
} catch (err) {
this.logger.error("turn.tool_error", {
turn,
tool: block.name,
error: err.message,
})
}
}
}
continue
}

this.logger.info("agent.complete", {
turns: turn,
totalTokens: this.totalTokens,
totalDurationMs: Date.now() - this.sessionStart,
})
yield { type: "response", data: response }
return
}

this.logger.warn("agent.max_turns", { maxTurns: this.config.maxTurns })
}
}

这一层埋点之后,你可以回答以下问题,而无需手动检查任何代码:

  • “平均一个任务消耗几轮 loop?”
  • “哪个工具最容易超时?”
  • “API 调用的 P50/P95/P99 延迟是多少?”

OpenTelemetry 入门

结构化日志是第一步。当你的 Agent 框架被多个服务使用,你需要分布式追踪——把一个请求在多个系统之间的完整路径串起来。

OpenTelemetry(OTel)是分布式追踪的开放标准。Claude Code 使用了 OTel(src/services/analytics/),但被抽象在内部。你不需要一个完整的 OTel 集群来开始——最小集成只需 20 行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// → 最简 OTel 集成(控制台输出)
import { trace, SpanStatusCode } from "@opentelemetry/api"
import { BasicTracerProvider, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base"
import { ConsoleSpanExporter } from "@opentelemetry/sdk-trace-node"

// 初始化
const provider = new BasicTracerProvider()
provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()))
provider.register()

const tracer = trace.getTracer("my-agent")

// 使用:包裹 Agent run
async function* runWithTrace(agent: AgentLoop, input: string) {
return tracer.startActiveSpan("agent.run", async (span) => {
span.setAttribute("agent.model", agent.model)
span.setAttribute("agent.input", input.substring(0, 200))

try {
for await (const event of agent.run(input)) {
if (event.type === "response") {
span.setAttribute("agent.turns", agent.turnCount)
span.setAttribute("agent.tokens", agent.totalTokens)
span.setStatus({ code: SpanStatusCode.OK })
}
yield event
}
} catch (err) {
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message })
throw err
} finally {
span.end()
}
})
}

Agent 专属监控面板

当日志和追踪就绪后,你需要的是一套能快速发现问题的问题指标面板。

核心指标

指标类型含义告警阈值建议
Turn CountHistogram每任务的平均轮次> 15 轮告警(可能陷入循环)
Token / TurnGauge每轮消耗的 token> 32K 告警(接近窗口上限)
Tool Latency (P95)Histogram95% 的工具调用在多久内完成> 10s 告警
API Latency (P95)Histogram95% 的 API 调用在多久内完成> 5s 告警
Error RateGauge工具调用失败率> 5% 告警
Cache Hit RateGaugePrompt Cache 的命中率< 60% 告警(缓存配置有问题)
Cost / TaskGauge每任务平均成本> 预设上限告警

最简指标收集器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// → 最简指标收集器(Counter + Histogram,~30 行)
class MetricsCollector {
private counters = new Map<string, number>()
private histograms = new Map<string, number[]>()

counter(name: string, value = 1): void {
this.counters.set(name, (this.counters.get(name) ?? 0) + value)
}

histogram(name: string, value: number): void {
if (!this.histograms.has(name)) this.histograms.set(name, [])
this.histograms.get(name)!.push(value)
}

report(): string {
const lines: string[] = []
for (const [name, count] of this.counters) {
lines.push(`COUNT ${name}: ${count}`)
}
for (const [name, values] of this.histograms) {
values.sort((a, b) => a - b)
const p50 = values[Math.floor(values.length * 0.5)]
const p95 = values[Math.floor(values.length * 0.95)]
const avg = values.reduce((a, b) => a + b, 0) / values.length
lines.push(`HIST ${name}: avg=${avg.toFixed(1)} p50=${p50} p95=${p95} n=${values.length}`)
}
return lines.join("\n")
}
}

// 使用
const metrics = new MetricsCollector()
metrics.histogram("turn.count", agent.turnCount)
metrics.histogram("token.per_turn", usage.output_tokens)
metrics.histogram("tool.latency_ms", Date.now() - toolStart)
console.log(metrics.report())

常见错误

常见错误检查方法
debug 日志文件太大定期清理:find ~/.claude/debug/ -name "*.txt" -mtime +7 -delete
source map 断点不生效确认 cli.js.mapcli.js 在同一目录,检查 VS Code 的 source map 设置
--debug 模式下性能下降--debug=pattern 过滤,只看特定类别的日志
慢操作日志没出现必须同时启用 debug 模式:CLAUDE_CODE_SLOW_OPERATION_THRESHOLD_MS=10 claude --debug
结构化日志太多导致性能下降生产环境只开 warn + error 级别;debug 只用于开发
OTel Span 未导出检查 SpanProcessor 和 Exporter 是否正确注册;确认 provider.register() 已调用
指标 P95 为空需要至少 20 个数据点才能计算 P95

试试看

  1. 启用并查看日志。claude --debug 启动一个会话,执行几条命令,然后检查 ~/.claude/debug/latest 文件。
  2. 过滤日志。claude --debug=mcp 启动,触发一个 MCP 工具调用。日志里应该只包含 MCP 相关的消息。
  3. 查询性能分析。CLAUDE_CODE_PROFILE_QUERY=1 claude --debug 启动,发送一条查询,在 debug 日志中找到 QUERY PROFILING REPORT
  4. 添加自定义日志。 在你创建的工具里加一行 logForDebugging,用 --debug 模式运行,确认你的日志出现在输出中。
  5. 实现结构化 Logger。 在你的 Agent 框架中实现 StructuredLogger 类,在 AgentLoop 的关键点埋日志。
  6. 构建指标面板。MetricsCollector 收集 10 次 Agent 运行的指标,生成一份报告。

检查点

  • 日志系统——logForDebugging 是核心入口,支持五个级别、分类过滤、缓冲写入
  • 启动方式——--debug--debug=pattern--debug-to-stderrDEBUG=1 四种方式
  • 日志过滤——--debug=pattern 支持正向和反向过滤,分类从消息格式自动提取
  • 内置命令——/debug 可以中途启用日志并自动分析问题
  • 启动性能——CLAUDE_CODE_PROFILE_STARTUP=1 记录完整启动时间线
  • 查询性能——CLAUDE_CODE_PROFILE_QUERY=1 记录从输入到首字返回的每个阶段
  • 慢操作检测——slowLogging 包装常见操作,超过阈值自动报告
  • 断点调试——VS Code + cli.js.map 实现源码级断点
  • 调试策略——复现、定位层级、加日志、二分、写测试、修复
  • 结构化可观测性——JSON Lines 格式日志 + sessionId/turn/tool 埋点,可搜索聚合
  • OpenTelemetry——最简 20 行集成,Span 包裹 Agent.run,自动导出到控制台
  • 监控面板——七个核心指标(Turn Count / Token / Latency / Error Rate / Cache Hit / Cost),P50/P95 统计

调试的核心不是工具,是思维方式:不要猜,要观察。不要一次改多处,要一次只改一处。每一步都要有验证。

可观测性扩展了这条原则:不只是一个人观察一个会话,而是让系统在所有会话中持续观察自己。


上一章:编写测试 | 下一章:从代码到贡献