法不净空,觉无性也。

第15章 可观测性与调试

2026.07.29

调试自主 agent 不同于调试普通程序——LLM 的输出不可复现,错误的因果链可能跨越多步工具调用。


模式层

15.1 Agent 可观测性的独特挑战

传统软件的可观测性(日志、指标、追踪)在面对 agent 系统时暴露出三个不足:

第一,非确定性输出。 普通程序在相同输入下产生相同输出(确定性)。Agent 系统在相同输入下可能产生不同输出——LLM 的不确定性使得"复现 bug"成为难题。用户说"重构模块 A",agent 第一次做的方案和第二次做的方案可能完全不同。调试时无法通过"再跑一次"来确认问题是否修复。

第二,因果链跨越多步工具调用。 Agent 的一个决策可能涉及 5 次工具调用、3 次 LLM 往返、1 次子 agent 调用。如果最终输出有问题,根因可能在第一步——但第一步的推理过程已经被后续的上下文覆盖了。传统日志的"按时间戳搜索"模式在跨越多步的因果链面前效率很低。

第三,LLM 输出的内容不可解析。 传统日志的结构化事件("用户登录成功"、"数据库查询返回 0 行")可以被精确解析和过滤。LLM 的 Reasoning token 是自然语言——无法被程序结构化解析。你不能 grep "错误原因" 来找到 agent 决策出错的原因。

15.2 可观测性三要素

针对上述挑战,CodeCoder 的可观测性体系围绕三个要素设计:

结构化事件流。 所有 agent 事件(NewToken、ToolStarted、ToolFinished、MilestoneDone、StatusUpdate)都作为结构化 JSON 事件输出。每个事件包含时间戳、事件类型、相关上下文。结构化事件可以被程序解析、过滤、采样。

实时可观测。 Headless 模式下,事件流同时写入 stderr 和 .ccd.bg.ndjson。交互式模式下,事件流通过 daemon 的事件通道广播到所有连接的 client。不需要等待 run 完成就可以查看进展。

事后可追溯。 Session 文件保存完整的事件历史。BgObserver 写入的 ndjson 文件保留从上一次 truncate 到当前运行的完整事件流。事后分析时,不需要重新运行 agent——读取 ndjson 文件即可。


案例层

15.3 BgObserver + bg_ledger

BgObserver 是 headless 模式下的可观测性组件:

struct BgObserver {
    ndjson_writer: BufWriter<File>,
    events_seen: usize,
    start_time: Instant,
}

impl BgObserver {
    fn new(project_root: &Path) -> Result<Self> {
        let path = project_root.join(".ccd.bg.ndjson");
        // Truncate at start of each round
        let file = fs::OpenOptions::new()
            .write(true)
            .truncate(true)
            .create(true)
            .open(&path)?;
        Ok(Self {
            ndjson_writer: BufWriter::new(file),
            events_seen: 0,
            start_time: Instant::now(),
        })
    }

    fn observe(&mut self, event: &BgEvent) {
        // Write to ndjson
        let json = serde_json::to_string(event)?;
        writeln!(self.ndjson_writer, "{}", json)?;
        self.ndjson_writer.flush()?;
        // Also write to stderr
        eprintln!("{}", json);
        self.events_seen += 1;
    }
}

bg_ledger 是 BgObserver 的扩展——它记录每个里程碑的耗时和结果:

struct BgLedger {
    milestone_times: Vec<MilestoneRecord>,
    tool_counts: HashMap<String, usize>,
    total_tokens: usize,
}

struct MilestoneRecord {
    name: String,
    started_at: Instant,
    completed_at: Option<Instant>,
    status: MilestoneStatus,
    attempts: usize,
}

15.4 Accountability Chain

Accountability Chain 是 CodeCoder 可观测性体系中的高级功能——将 agent 的决策与其产生的具体输出链接起来。

当 agent 做出一个决定(如"修改文件 A 的接口"),Accountability Chain 记录:

  • 决策的触发条件(用户输入)
  • 决策的推理过程摘要(从 Reasoning 中提取的关键点)
  • 决策执行的工具调用序列
  • 执行结果(文件修改的 diff)

这些记录在事后分析时,可以帮助回答"为什么 agent 做了这个修改"——而不是"agent 做了什么修改"。

struct AccountabilityEntry {
    timestamp: Instant,
    trigger: String,          // Input that triggered the decision
    rationale: String,        // Reasoning process summary
    actions: Vec<ToolCall>,   // Executed tool calls
    outcome: String,          // Execution result summary
    diff: Option<String>,     // File modification diff (if any)
}

15.5 调试方法论

调试自主 agent 的方法论可以总结为三个步骤:

第一步:隔离 LLM 输出。 使用 StubClientScriptedProvider 替代真实 LLM provider。StubClient 返回固定的响应,ScriptedProvider 从预先录制的响应序列中播放。这使得 agent 在调试时的行为是确定性的——每次运行产生相同的输出。

第二步:确定性回放。 从 Session 文件或 ndjson 文件中读取事件序列,在不连接 LLM provider 的情况下回放。回放时,agent 的输入来自录制的事件,而不是 LLM 的实时输出。可以快速定位"在哪个事件之后 agent 开始出现异常行为"。

fn replay(session: &Session, context: &Context) -> Result<()> {
    for event in &session.events {
        match event {
            Event::NewToken(_) => {}  // Ignore token output
            Event::ToolStarted { tool, args } => {
                // Check if tool call is reasonable
                validate_tool_call(tool, args)?;
            }
            Event::ToolFinished { tool, result } => {
                // Validate tool result
                validate_tool_result(tool, result)?;
            }
            Event::MilestoneDone { name, .. } => {
                // Validate milestone completion conditions
                validate_milestone(name)?;
            }
        }
    }
    Ok(())
}

第三步:结构化事件追踪。 使用事件过滤工具(如 jq)从 ndjson 文件中提取特定类型的事件进行分析:

# Extract all tool call events
jq 'select(.event == "ToolStarted")' .ccd.bg.ndjson

# Extract all failed tool calls
jq 'select(.event == "ToolFinished" and .result.status == "error")' .ccd.bg.ndjson

# Extract all milestone completion events, sorted by time
jq 'select(.event == "MilestoneDone") | {name, timestamp}' .ccd.bg.ndjson

ADR 深度阅读

BgObserver 与 bg_ledger 的引入动机

ADR 0039 记录了 BgObserver 和 bg_ledger 的引入过程。

在 BgObserver 出现之前,headless 模式的输出只有退出码和 stderr 上的错误信息。如果 headless run 失败了(退出码 2 或 3),用户需要重新运行一次才能看到"哪里出错了"。重新运行可能产生不同的结果(LLM 的不确定性),使得"复现 bug"变得困难。

BgObserver 的引入将 headless 的运行过程从"黑盒"变为"透明盒"。每个事件都写入 ndjson 文件,用户可以 tail -f 实时观察,也可以在 run 结束后离线分析。ndjson 格式(每行一个 JSON 事件)确保文件可以被流式处理——不需要等到 run 结束才能开始分析。

bg_ledger 在 BgObserver 的基础上增加了里程碑级别的聚合统计。它不是记录每个事件,而是记录每个里程碑的耗时、工具调用次数、重试次数。这使得"这个 run 为什么慢"的根因分析变得可行——如果某个里程碑的次数远超平均值,可以针对性地查看该里程碑的详细事件。

Accountability Chain 是后来增加的功能。它的引入动机是:当 agent 做出一个错误的修改(如删除了不该删的代码),现有的日志系统只能告诉你"agent 删除了代码",不能告诉你"agent 为什么认为这段代码应该被删除"。Accountability Chain 把决策的推理过程摘要与决策的执行结果关联起来,使得事后审查时可以看到"动机"而非仅看到"行为"。


下一章,CodeCoder 的测试策略——三层测试金字塔、隔离测试、行为验证。