Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

第30章:构建你自己的 AI Agent — 从 Claude Code 模式到实战

定位:本章用 Rust 实现一个代码审查 Agent,综合应用全书 16 个命名模式,演示从分析到实战的迁移。前置依赖:第25-27章。适用场景:想动手实践的读者——本章用 Rust 实现一个代码审查 Agent,综合应用全书 16 个命名模式。

为什么需要这一章

为什么不是"构建你自己的 Claude Code"

读者可能会期待:既然前 29 章拆解了 Claude Code 的每个子系统,本章应该教你如何重新组装一个。但这恰恰是我们不会做的事情。

Claude Code 是一个产品——40+ 个工具、特定的 UI 交互、特定的会话格式、特定的计费集成。复刻这些实现细节没有意义:你的 Agent 不需要是编码助手,它可能是安全扫描器、数据管道监控、代码审查工具、或者客服机器人。如果我们教的是"如何实现 Claude Code 的 FileEditTool",换个场景就完全无法迁移。

本书前 29 章提炼的不是实现细节,而是模式——提示词分层、上下文预算、工具沙箱、渐进式权限、熔断重试、结构化可观测。这些模式不绑定特定产品形态,可以迁移到任何 Agent 场景。

所以本章要做的是:用一个完全不同的 Agent(代码审查,不是编码助手),完全不同的语言(Rust,不是 TypeScript),完全不同的运行方式(自己控制 Agent Loop,不是委托给 Claude Code)——来演示同样的 22 个模式如何组合应用。如果模式能在这种跨场景、跨语言、跨架构的迁移中存活下来,它们就不是 Claude Code 的专属知识,而是真正可复用的 Agent 工程原则。

模式的组合比单独理解更难

第 25-27 章提炼出 22 个命名模式和原则。但模式的价值不在于列举——而在于组合。单独理解"为一切设定预算"(详见第 26 章)不难,但当它需要同时与"告知而非隐藏"(详见第 26 章)配合、又不能破坏"缓存感知设计"(详见第 25 章)时,工程复杂度会陡然上升。

本章用一个真正可运行的项目(~800 行 Rust)演示如何把这些模式从分析结果变成你自己的代码。

我们的项目是一个 Rust 代码审查 Agent——输入一份 Git diff,输出结构化的审查报告。选择这个场景是因为它天然覆盖了 Agent 构建的核心维度:需要读取文件(上下文管理)、搜索代码(工具编排)、分析问题(提示词控制)、控制权限(安全约束)、处理故障(韧性)、追踪质量(可观测性)。而且,每个开发者都做过代码审查,场景不需要额外解释。

30.1 项目定义:代码审查 Agent

cc-sdk:Claude Code 的 Rust SDK

在介绍项目之前,先认识一下我们的核心依赖——cc-sdkGitHub)。这是一个社区维护的 Rust SDK,通过子进程方式与 Claude Code CLI 交互。它提供三种使用模式:

模式APIAgent Loop工具认证方式适合场景
完整 Agentcc_sdk::query()CC 内部CC 内置工具API key 或 CC 订阅需要 Agent 自主读写文件、执行命令
交互客户端ClaudeSDKClientCC 内部CC 内置工具API key 或 CC 订阅多轮对话、会话管理
LLM Proxycc_sdk::llm::query()你的代码无(全禁用)CC 订阅(无需 API key)输入已知,只需文本分析

LLM Proxy 模式(v0.8.1 新增)是本章的关键——它把 Claude Code CLI 当作纯粹的 LLM 代理,底层通过 --tools "" 禁用所有工具、PermissionMode::DontAsk 拒绝任何工具请求、max_turns: 1 限制为单轮。更重要的是,它走 Claude Code 订阅认证,不需要单独的 ANTHROPIC_API_KEY

项目定义

项目的输入、输出和约束如下:

  • 输入:一份 unified diff 文件(来自 git diff 或 PR)
  • 输出:结构化审查报告(JSON 或 Markdown),每条发现包含文件、行号、严重级别、分类和修复建议
  • 约束:只读(不修改被审查的代码)、有 Token 预算、可追踪

关键的架构决策是:Agent Loop 在我们自己的代码中,LLM 后端可插拔。通过 LlmBackend trait,同一个 Agent 可以用 Claude(cc-sdk)或 GPT(Codex 订阅)驱动,无需修改任何审查逻辑。

完整代码在本项目的 examples/code-review-agent/ 目录中。

flowchart TB
    A["Git Diff 输入"] --> B["Diff 解析 + 预算控制"]
    B --> C["逐文件 Agent Loop"]
    C --> C1["Turn 1: 审查 diff"]
    C1 --> C2["Turn 2: 决策"]
    C2 -->|"done"| C5["汇总 findings"]
    C2 -->|"use_tool: bash"| C3["执行 bash\n(read-only 沙箱)"]
    C3 --> C2
    C2 -->|"use_tool: skill"| C4["运行 skill\n(专项分析)"]
    C4 --> C5
    C2 -->|"review_related"| C6["审查关联文件"]
    C6 --> C5
    C5 -->|"下一个文件"| C
    C5 --> D["输出报告\nJSON/Markdown"]

    subgraph LLM["可插拔 LLM 后端"]
        L1["cc-sdk\nClaude 订阅"]
        L2["Codex\nGPT 订阅"]
        L3["WebSocket\n远程连接"]
    end

    C1 -.-> LLM
    C2 -.-> LLM
    C4 -.-> LLM
    C6 -.-> LLM

Agent 的每个文件审查最多经历 3 轮 LLM 调用(review → decide → followup),加上最多 3 次工具调用。LLM 永远不直接执行工具——它输出 JSON 请求(AgentAction),我们的 Rust 代码决定是否执行以及如何执行。

为什么自己控制 Agent Loop? 委托给 Claude Code 的内置 Agent(cc_sdk::query)更简单,但你失去了精细控制:无法实现逐文件的熔断、预算分配、工具白名单和跨后端切换。自己控制循环意味着每个决策点都是显式的——这正是驾驭工程的核心。

项目的代码架构直接映射到我们要讨论的六层:

代码模块对应层级应用的核心模式
prompts.rsL1 提示词架构提示词即控制面、带外控制信道、工具级提示词
context.rsL2 上下文管理为一切设定预算、上下文卫生、告知而非隐藏
agent.rs + tools.rsL3 工具与搜索编辑前先读取、结构化搜索
llm.rs + tools.rsL4 安全与权限失败关闭、渐进式自主
resilience.rsL5 韧性有限重试预算、熔断失控循环、局部能力降维
agent.rs (tracing)L6 可观测性先观察再修复、结构化验证

接下来我们逐层拆解,每层先看 Claude Code 源码中的模式原型,再看 Rust 实现。

30.2 第一层:提示词架构

应用模式提示词即控制面(详见第 25 章)、带外控制信道(详见第 25 章)、工具级提示词(详见第 27 章)、范围匹配响应(详见第 27 章)

CC 源码中的模式

Claude Code 的提示词架构有一个关键设计:分离稳定部分和易变部分。稳定的部分被缓存(不破坏 prompt cache),易变的部分被显式标记为"危险的":

// restored-src/src/constants/systemPromptSections.ts:20-24
export function systemPromptSection(
  name: string,
  compute: ComputeFn,
): SystemPromptSection {
  return { name, compute, cacheBreak: false }
}
// restored-src/src/constants/systemPromptSections.ts:32-38
export function DANGEROUS_uncachedSystemPromptSection(
  name: string,
  compute: ComputeFn,
  _reason: string,
): SystemPromptSection {
  return { name, compute, cacheBreak: true }
}

DANGEROUS_ 前缀不是装饰——它是一个工程约束。任何需要每轮重算的提示词段落都必须通过这个函数创建,强迫开发者填写 _reason 参数解释为什么需要破坏缓存。这就是带外控制信道模式的体现:通过函数签名而非注释来约束行为。

Rust 实现

我们的代码审查 Agent 采用相同的分层思路,但用更简单的方式实现——一个"宪法"(Constitution)层和一个"运行时"(Runtime)层:

#![allow(unused)]
fn main() {
// examples/code-review-agent/src/prompts.rs:38-42
pub fn build_system_prompt(pr_info: &PrInfo) -> String {
    let constitution = build_constitution();
    let runtime = build_runtime_section(pr_info);
    format!("{constitution}\n\n---\n\n{runtime}")
}
}

宪法层是静态的——审查原则、严重级别定义、输出格式规范。这些内容在所有审查会话中完全相同:

#![allow(unused)]
fn main() {
// examples/code-review-agent/src/prompts.rs:45-84
fn build_constitution() -> String {
    r#"# Code Review Agent — Constitution

You are a code review agent. Your job is to review diffs and produce
a structured list of findings.

# Review Principles
1. **Correctness first**: Flag logic errors, off-by-one bugs...
2. **Security**: Identify injection vulnerabilities...
// ...

# Output Format
You MUST output a JSON array of finding objects..."#
        .to_string()
}
}

运行时层是动态的——当前 PR 的标题、变更文件列表、根据文件扩展名推断的语言特定规则:

#![allow(unused)]
fn main() {
// examples/code-review-agent/src/prompts.rs:113-154
fn infer_language_rules(files: &[String]) -> String {
    let mut rules = Vec::new();
    let mut seen_rust = false;
    // ...
    for file in files {
        if !seen_rust && file.ends_with(".rs") {
            seen_rust = true;
            rules.push("## Rust-Specific Rules\n- Check for `.unwrap()`...");
        }
        // TypeScript, Python 规则类似...
    }
    rules.join("\n\n")
}
}

范围匹配响应模式体现在输出格式的设计上:我们要求模型输出 JSON 数组而非自由文本,每条发现都有固定的字段结构。这不是为了美观——而是为了让下游的 parse_findings_from_response 能可靠地解析结果。

30.3 第二层:上下文管理

应用模式为一切设定预算(详见第 26 章)、上下文卫生(详见第 26 章)、告知而非隐藏(详见第 26 章)、保守估算(详见第 26 章)

CC 源码中的模式

Claude Code 对上下文管理有三层预算约束:单工具结果上限、单消息聚合上限、全局上下文窗口。关键常量定义在同一个文件中:

// restored-src/src/constants/toolLimits.ts:13
export const DEFAULT_MAX_RESULT_SIZE_CHARS = 50_000

// restored-src/src/constants/toolLimits.ts:22
export const MAX_TOOL_RESULT_TOKENS = 100_000

// restored-src/src/constants/toolLimits.ts:49
export const MAX_TOOL_RESULTS_PER_MESSAGE_CHARS = 200_000

当内容被截断时,CC 不是默默丢弃——它保留元信息,告诉模型完整内容在哪里:

// restored-src/src/utils/toolResultStorage.ts:30-34
export const PERSISTED_OUTPUT_TAG = '<persisted-output>'
export const PERSISTED_OUTPUT_CLOSING_TAG = '</persisted-output>'
export const TOOL_RESULT_CLEARED_MESSAGE = '[Old tool result content cleared]'

这就是告知而非隐藏——截断是不可避免的,但模型必须知道截断发生了,以及完整信息在哪里。

Rust 实现

我们的 Agent 实现了相同的双层预算:单文件上限 + 总预算。ContextBudget 结构体在分配前检查、分配后记账:

#![allow(unused)]
fn main() {
// examples/code-review-agent/src/context.rs:12-45
pub struct ContextBudget {
    pub max_total_tokens: usize,
    pub max_file_tokens: usize,
    pub used_tokens: usize,
}

impl ContextBudget {
    pub fn remaining(&self) -> usize {
        self.max_total_tokens.saturating_sub(self.used_tokens)
    }

    pub fn try_consume(&mut self, tokens: usize) -> bool {
        if self.used_tokens + tokens <= self.max_total_tokens {
            self.used_tokens += tokens;
            true
        } else {
            false
        }
    }
}
}

上下文卫生体现在 apply_budget 函数中——对每个文件,先检查总预算剩余,再应用单文件上限,超出的文件被跳过而非静默丢弃:

#![allow(unused)]
fn main() {
// examples/code-review-agent/src/context.rs:201-245
pub fn apply_budget(diff: &DiffContext, budget: &mut ContextBudget) -> (DiffContext, usize) {
    let mut files = Vec::new();
    let mut skipped = 0;

    for file in &diff.files {
        if budget.remaining() == 0 {
            warn!(file = %file.path, "Skipping file — total token budget exhausted");
            skipped += 1;
            continue;
        }
        let effective_max = budget.max_file_tokens.min(budget.remaining());
        let (content, was_truncated) = truncate_file_content(&file.diff, effective_max);
        // ...
    }
    (DiffContext { files }, skipped)
}
}

截断时注入元信息——当文件内容被截断,我们明确告诉模型原始大小:

#![allow(unused)]
fn main() {
// examples/code-review-agent/src/context.rs:100-102
truncated.push_str(&format!(
    "\n[Truncated: full file has {total_lines} lines, showing first {lines_shown}]"
));
}

Token 估算使用保守估算策略——基于字节长度除以 4(Rust 的 str::len() 返回字节数),对 ASCII 代码约等于字符数,对非 ASCII 内容则更加保守:

#![allow(unused)]
fn main() {
// examples/code-review-agent/src/context.rs:66-69
pub fn estimate_tokens(text: &str) -> usize {
    (text.len() + 3) / 4  // 保守估算:~4 字节/token
}
}

30.4 第三层:工具与搜索

应用模式编辑前先读取(详见第 27 章)、结构化搜索(详见第 27 章)

CC 源码中的模式

Claude Code 的 FileEditTool 有一个硬约束——如果你没有先读过文件,编辑会直接报错:

// restored-src/src/tools/FileEditTool/prompt.ts:4-6
function getPreReadInstruction(): string {
  return `\n- You must use your \`${FILE_READ_TOOL_NAME}\` tool at least once
    in the conversation before editing. This tool will error if you
    attempt an edit without reading the file. `
}

这不是建议,是强制执行。同时,搜索工具(Grep、Glob)被标记为安全的并发只读操作:

// restored-src/src/tools/GrepTool/GrepTool.ts:183-187
isConcurrencySafe() { return true }
isReadOnly() { return true }

Rust 实现

我们的 Agent 实现了自己的工具系统,受 just-bash 的启发——bash 本身就是万能的工具接口,LLM 天然会使用它。但与 just-bash 不同,我们的工具在只读沙箱中执行:

#![allow(unused)]
fn main() {
// examples/code-review-agent/src/tools.rs — 工具安全约束
const ALLOWED_COMMANDS: &[&str] = &[
    "cat", "head", "tail", "wc", "grep", "find", "ls", "sort", "awk", "sed", ...
];

const BLOCKED_COMMANDS: &[&str] = &[
    "rm", "mv", "curl", "python", "bash", "npm", ...
];
}

LLM 通过 AgentAction::UseTool 请求工具,我们的代码验证并执行:

#![allow(unused)]
fn main() {
// examples/code-review-agent/src/review.rs — Agent 决策
pub enum AgentAction {
    Done,
    ReviewRelated { file: String, reason: String },
    UseTool { tool: String, input: String, reason: String },
}
}

两种工具类型:

  • bash:只读命令(cat file | grep pattern),在子进程沙箱中执行
  • skill:专项分析提示词(security-auditperformance-reviewrust-idiomsapi-review),由我们的代码加载并通过当前 LLM 后端发送

这是结构化搜索模式的体现:LLM 提出需求("我想看这个函数的定义"),我们的代码决定如何满足(执行 grep -rn 'fn validate_input' src/)。工具执行结果回传给 LLM,LLM 继续分析。

30.5 第四层:安全与权限

应用模式失败关闭(详见第 25 章)、渐进式自主(详见第 27 章)

CC 源码中的模式

Claude Code 定义了 5 种外部权限模式(按字母序排列):

// restored-src/src/types/permissions.ts:16-22
export const EXTERNAL_PERMISSION_MODES = [
  'acceptEdits',
  'bypassPermissions',
  'default',
  'dontAsk',
  'plan',
] as const

按限制程度从高到低排列:plan(只规划不执行)> default(每次确认)> acceptEdits(自动接受编辑)> dontAsk(拒绝未预批准的工具)> bypassPermissions(完全自主)。这是渐进式自主——用户可以根据信任程度逐步放开权限。失败关闭体现在 default 模式的设计上:如果不确定是否应该允许,默认答案是"不"。

Rust 实现

我们的审查 Agent 实现了多层失败关闭

安全层机制效果
LLM 后端LlmBackend trait,纯文本接口LLM 不能直接执行任何操作
工具白名单ALLOWED_COMMANDS 只含只读命令bash 只能 cat/grep,不能 rm/curl
工具黑名单BLOCKED_COMMANDS 显式禁止危险命令双重保险
输出重定向阻止 > 操作符不能通过 bash 写文件
调用上限每文件最多 3 次工具调用防止 LLM 进入工具调用死循环
超时每次工具执行 30 秒超时防止命令挂起
输出截断工具输出限制 50KB防止大文件撑爆上下文

这是渐进式自主的实践——三种权限级别在同一个 Agent 中共存:

  1. Turn 1(审查):LLM 只看 diff,无工具访问
  2. Turn 2+(工具):LLM 可以请求只读 bash 或 skill,但我们的代码验证并执行
  3. MCP 模式:外部 Agent(如 Claude Code)可以调用我们的 Agent,形成嵌套授权

30.6 第五层:韧性

应用模式有限重试预算(详见第 6b 章)、熔断失控循环(详见第 26 章)、局部选模与能力降维(详见第 27 章)

CC 源码中的模式

Claude Code 的重试逻辑有两个关键约束——总重试次数和特定错误的重试上限:

// restored-src/src/services/api/withRetry.ts:52-54
const DEFAULT_MAX_RETRIES = 10
const FLOOR_OUTPUT_TOKENS = 3000
const MAX_529_RETRIES = 3

熔断器(Circuit Breaker)模式出现在自动压缩中——如果压缩连续失败 3 次,就停止尝试:

// restored-src/src/services/compact/autoCompact.ts:67-70
// Stop trying autocompact after this many consecutive failures.
// BQ 2026-03-10: 1,279 sessions had 50+ consecutive failures (up to 3,272)
// in a single session, wasting ~250K API calls/day globally.
const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3

源码注释中的数据表明了熔断器的必要性:在加入这个常量之前,1,279 个会话累计产生了超过 50 次连续失败,每天浪费约 25 万次 API 调用。这就是为什么熔断失控循环不是可选的。

Rust 实现

我们的 with_retry 函数实现了指数退避重试,上限为 30 秒。这是生产级重试的简化版——CC 的实现还包含 jitter(随机抖动)来避免多客户端同步重试的"惊群效应":

#![allow(unused)]
fn main() {
// examples/code-review-agent/src/resilience.rs:34-68
pub async fn with_retry<F, Fut, T>(config: &RetryConfig, mut operation: F) -> Result<T>
where
    F: FnMut() -> Fut,
    Fut: Future<Output = Result<T>>,
{
    for attempt in 0..=config.max_retries {
        match operation().await {
            Ok(value) => return Ok(value),
            Err(e) => {
                if attempt < config.max_retries {
                    let delay_ms = (config.base_delay_ms * 2u64.saturating_pow(attempt))
                        .min(MAX_BACKOFF_MS);
                    warn!(attempt, delay_ms, error = %e, "Operation failed, retrying");
                    tokio::time::sleep(Duration::from_millis(delay_ms)).await;
                }
                last_error = Some(e);
            }
        }
    }
    Err(last_error.expect("at least one attempt must have been made"))
}
}

CircuitBreaker 使用原子计数器追踪连续失败次数。在我们的逐文件审查循环中,它直接集成到 Agent Loop 中——连续 3 个文件失败就停止审查剩余文件,避免无意义的 API 浪费:

#![allow(unused)]
fn main() {
// examples/code-review-agent/src/main.rs:107-130
let circuit_breaker = CircuitBreaker::new(3);

for file in &constrained_diff.files {
    if !circuit_breaker.check() {
        warn!("Circuit breaker OPEN — skipping remaining files");
        break;
    }
    // ... call LLM with retry ...
    match result {
        Ok(response_text) => { circuit_breaker.record_success(); /* ... */ }
        Err(e) => { circuit_breaker.record_failure(); /* ... */ }
    }
}
}

CircuitBreaker 本身的实现:

#![allow(unused)]
fn main() {
// examples/code-review-agent/src/resilience.rs:74-118
pub struct CircuitBreaker {
    max_failures: u32,
    failures: AtomicU32,
}

impl CircuitBreaker {
    pub fn check(&self) -> bool {
        self.failures.load(Ordering::Relaxed) < self.max_failures
    }
    pub fn record_failure(&self) { /* 原子递增,到阈值时 warn */ }
    pub fn record_success(&self) { self.failures.store(0, Ordering::Relaxed); }
}
}

局部能力降维体现在上下文管理层——当文件超过单文件 Token 预算时,Agent 不是放弃审查,而是降级为只审查截断后的部分(见 30.3 的截断逻辑)。这比"要么全审要么不审"更实用。

30.7 第六层:可观测性

应用模式先观察再修复(详见第 25 章)、结构化验证(详见第 27 章)

CC 源码中的模式

Claude Code 的事件日志有一个独特的类型安全设计——logEvent 函数的 metadata 参数使用 LogEventMetadata 类型,该类型的值只允许 boolean | number | undefined,从类型定义层面排除了 string,防止意外将代码或文件路径写入遥测日志:

// restored-src/src/services/analytics/index.ts:133-144
export function logEvent(
  eventName: string,
  // intentionally no strings unless
  // AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
  // to avoid accidentally logging code/filepaths
  metadata: LogEventMetadata,
): void {
  if (sink === null) {
    eventQueue.push({ eventName, metadata, async: false })
    return
  }
  sink.logEvent(eventName, metadata)
}

注释中的标记类型名 AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 作为代码审查时的视觉警示——如果你需要显式传入字符串,你必须经过这个类型的"声明"。

Rust 实现

我们使用 tracing crate 在关键节点记录结构化事件。review_startedreview_completed 两个事件覆盖了 Agent 的完整生命周期:

#![allow(unused)]
fn main() {
// examples/code-review-agent/src/main.rs:68-75
info!(
    diff = %cli.diff.display(),
    max_tokens = cli.max_tokens,
    max_file_tokens = cli.max_file_tokens,
    "review_started"
);

// 逐文件审查时的事件
info!(file = %file.path, tokens = file.estimated_tokens, "Reviewing file");
info!(file = %file.path, findings = findings.len(), "File review complete");

// 汇总事件
info!(summary = %report.summary_line(), "review_completed");
}

ReviewReport 本身就是一个可观测性结构——它记录了审查的覆盖率(reviewed vs skipped)、Token 消耗、耗时和成本:

#![allow(unused)]
fn main() {
// examples/code-review-agent/src/review.rs:46-59
pub struct ReviewReport {
    pub files_reviewed: usize,
    pub files_skipped: usize,
    pub total_tokens_used: u64,
    pub duration_ms: u64,
    pub findings: Vec<Finding>,
    pub cost_usd: Option<f64>,
}
}

这些指标让你可以回答关键问题:审查覆盖了多少文件?跳过了多少?每个发现消耗了多少 Token?整个审查花了多少钱?先观察再修复意味着在优化任何东西之前,你首先要能看到它。

30.8 实战演示:Agent 审查自己的代码

最好的测试是让 Agent 审查它自己。我们用 git diff --no-index 生成包含全部 5 个 Rust 源文件的 diff(1261 行新增代码),然后运行审查:

$ cargo run -- --diff /tmp/new-code-review.diff

review_started  diff=/tmp/new-code-review.diff max_tokens=50000
Parsed diff into per-file chunks  file_count=5
Budget applied  files_to_review=5 files_skipped=0 tokens_used=10171
Reviewing file  file=context.rs   tokens=2579
File review complete  file=context.rs   findings=5
Reviewing file  file=main.rs      tokens=1651
File review complete  file=main.rs      findings=5
Reviewing file  file=prompts.rs   tokens=1722
File review complete  file=prompts.rs   findings=5
Reviewing file  file=resilience.rs tokens=1580
File review complete  file=resilience.rs findings=5
Reviewing file  file=review.rs    tokens=2639
File review complete  file=review.rs    findings=5
review_completed  25 findings (0 critical, 10 warnings, 15 info) across 5 files in 128.3s

Agent 在 128 秒内逐文件审查了 5 个文件,发现了 25 个问题。以下是几个有代表性的发现:

diff 解析的边界 bug(Warning):

splitn(2, " b/") 在文件路径包含空格时会错误拆分。例如 diff --git a/foo b/bar b/foo b/bar 会在 a/ 路径中的 b/ 处断开。

原子计数器溢出(Warning):

record_failurefetch_add(1, Relaxed) 没有溢出保护。如果持续调用,计数器会从 u32::MAX 回绕到 0,意外关闭熔断器。

JSON 解析脆弱性(Warning):

extract_json_array 的括号匹配不处理 JSON 字符串值内的 [],可能导致提前匹配或匹配过晚。

性能优化建议(Info):

content.lines().count() 遍历一次计总行数,然后 for 循环再遍历一次。对大文件是冗余的两次遍历。

切换到 Codex(GPT-5.4)后端审查同样的代码,Agent 展示了跨文件跟进能力——审查 agent.rs 时自主决定也查看 llm.rs(因为 trait 依赖),8 个文件 39 个 findings,67 秒。同一套 Agent Loop,换个 LLM 后端就能对比不同模型的审查质量。

自举:Agent 发现并修复自己的安全漏洞

最有说服力的测试是让 Agent 审查自己的工具系统代码(tools.rs)。Codex 后端返回了 2 个 Critical:

Shell 命令注入(Critical):bash 工具通过 sh -c 执行命令,所以 shell 元字符会被解释——即使第一个 token 在白名单里。cat file; uname -agrep foo $(id) 或反引号替换都能绕过 is_command_allowed

这是一个真实的安全漏洞。修复方式:

  1. 不再用 sh -c——改为 Command::new(program).args(args) 直接执行,不经过 shell 解释器
  2. 阻止所有 shell 元字符;|&$(){}`,在命令到达执行前就拦截
  3. 新增 5 个注入攻击测试:分号链接、管道、子 shell、反引号、&& 链接
#![allow(unused)]
fn main() {
// 修复前:sh -c 会解释 shell 元字符
let mut cmd = Command::new("sh");
cmd.arg("-c").arg(command);  // ← "cat file; rm -rf /" 会执行两条命令

// 修复后:直接执行,不经过 shell
const SHELL_METACHARACTERS: &[char] = &[';', '|', '&', '`', '$', '(', ')'];
if command.contains(SHELL_METACHARACTERS) {
    return blocked("Shell metacharacters not allowed");
}
let mut cmd = Command::new(program);
cmd.args(args);  // ← 参数不会被 shell 解释
}

这个过程演示了完整的 Agent 驱动开发循环:Agent 审查 → 发现漏洞 → 开发者修复 → Agent 验证修复。更重要的是,它展示了为什么代码审查 Agent 的安全层(第四层)本身也需要被审查——没有任何系统能仅靠设计保证安全,持续的审查循环才是防线。

Agent 工作机制全景

下面的序列图展示了 Agent 审查两个有依赖关系的文件时的完整交互过程——包括初始审查、工具调用、跨文件跟进和最终汇总:

sequenceDiagram
    participant U as 用户/CLI
    participant A as Agent Loop<br/>(agent.rs)
    participant T as 工具系统<br/>(tools.rs)
    participant L as LLM 后端<br/>(llm.rs)
    participant B as Bash 沙箱

    U->>A: --diff my.diff --backend codex
    activate A
    Note over A: 加载 diff → 预算控制<br/>5 文件, 10K tokens

    rect rgb(230, 245, 255)
        Note over A,L: 文件 1: agent.rs (Turn 1 — 审查)
        A->>L: complete(system_prompt, diff_of_agent.rs)
        L-->>A: findings: [{severity: Warning, ...}, ...]
    end

    rect rgb(255, 243, 224)
        Note over A,L: 文件 1: agent.rs (Turn 2 — 决策)
        A->>L: complete(system_prompt, followup_prompt)
        L-->>A: {"action": "use_tool", "tool": "bash",<br/>"input": "grep -rn 'LlmBackend' src/"}
    end

    rect rgb(232, 245, 233)
        Note over A,B: 文件 1: agent.rs (Turn 3 — 工具执行)
        A->>T: execute_tool("bash", "grep -rn ...")
        T->>T: 白名单检查 ✓<br/>元字符检查 ✓
        T->>B: Command::new("grep").args(["-rn", ...])
        B-->>T: src/llm.rs:50: pub trait LlmBackend ...
        T-->>A: ToolResult { success: true, output: ... }
    end

    rect rgb(255, 243, 224)
        Note over A,L: 文件 1: agent.rs (Turn 4 — 继续决策)
        A->>L: complete(prompt + tool_results)
        L-->>A: {"action": "review_related",<br/>"file": "llm.rs", "reason": "trait 依赖"}
    end

    rect rgb(243, 229, 245)
        Note over A,L: 文件 1: agent.rs (Turn 5 — 跨文件审查)
        A->>L: complete(system_prompt, diff_of_llm.rs)
        L-->>A: cross_file_findings: [...]
    end

    Note over A: 文件 1 完成: 合并 findings

    rect rgb(230, 245, 255)
        Note over A,L: 文件 2: tools.rs (Turn 1 — 审查)
        A->>L: complete(system_prompt, diff_of_tools.rs)
        L-->>A: [{severity: Critical,<br/>message: "sh -c shell 注入"}]
    end

    rect rgb(255, 243, 224)
        Note over A,L: 文件 2: tools.rs (Turn 2 — 决策)
        A->>L: complete(followup_prompt)
        L-->>A: {"action": "use_tool", "tool": "skill",<br/>"input": "security-audit"}
    end

    rect rgb(252, 228, 236)
        Note over A,L: 文件 2: tools.rs (Turn 3 — Skill 分析)
        A->>A: find_skill("security-audit")<br/>加载专项提示词
        A->>L: complete(security_audit_prompt,<br/>diff_of_tools.rs)
        L-->>A: deep_security_findings: [...]
    end

    Note over A: 文件 2 完成: 合并 findings

    A->>A: 汇总所有 findings<br/>构建 ReviewReport
    A-->>U: JSON/Markdown 报告
    deactivate A

交互式版本点击查看动画可视化 — 可以逐步播放、暂停、调速,点击每个步骤查看详细解释。

这张图清晰地展示了六层架构在运行时的协作:

  • L1 提示词system_promptfollowup_prompt 控制每轮 LLM 调用的行为
  • L2 上下文:预算控制决定了哪些文件被加载、哪些被截断
  • L3 工具:Agent 自主决定调用 bash(查找代码)或 skill(深入分析)
  • L4 安全:工具系统验证白名单和元字符后才执行
  • L5 韧性:每个 LLM 调用都有 retry + 熔断保护(图中省略)
  • L6 可观测:每个彩色区块都有对应的 tracing 事件

这些发现验证了六层架构的协同工作:提示词层的 Constitution 定义了审查原则和输出格式,上下文层的预算控制确保文件都在预算内,Agent Loop 逐文件循环并通过工具系统深入分析,韧性层的重试和熔断保护了整个循环,可观测层的 tracing 事件让我们能看到每个文件的审查进度、工具调用和发现数量。

30.9 闭环:让 Claude Code 使用你的 Agent

构建 Agent 的终极验证是让它被其他 Agent 使用。我们的代码审查 Agent 可以通过 MCP(Model Context Protocol,详见第 22 章技能系统)暴露为 Claude Code 的一个工具。

只需添加 --serve 参数,Agent 就从 CLI 切换到 MCP Server 模式,通过 stdio 与 Claude Code 通信:

#![allow(unused)]
fn main() {
// examples/code-review-agent/src/mcp.rs(核心定义)
#[tool(description = "Review a unified diff file for bugs, security issues, and code quality.")]
async fn review_diff(&self, Parameters(req): Parameters<ReviewDiffRequest>) -> String {
    // 复用完整的 Agent Loop:加载 diff → 预算控制 → 逐文件 LLM → 汇总
    match self.do_review(req).await {
        Ok(report) => serde_json::to_string_pretty(&report).unwrap_or_default(),
        Err(e) => format!("{{\"error\": \"{e}\"}}"),
    }
}
}

在 Claude Code 的 settings.json 中注册:

{
  "mcpServers": {
    "code-review": {
      "command": "cargo",
      "args": ["run", "--manifest-path", "/path/to/Cargo.toml", "--", "--serve"]
    }
  }
}

之后,Claude Code 就可以自然地调用 review_diff 工具——你在对话中说"帮我审查这个 diff",CC 会调用你的 Agent,拿到结构化的 findings,然后逐个修复。这形成了一个完整的闭环:

flowchart LR
    U["开发者"] -->|"帮我审查"| CC["Claude Code"]
    CC -->|"MCP: review_diff"| RA["你的 Review Agent"]
    RA -->|"LLM Proxy"| LLM["Claude (订阅)"]
    LLM --> RA
    RA -->|"25 findings JSON"| CC
    CC -->|"逐个修复"| U

从 CC 源码中学习模式,用这些模式构建自己的 Agent,再让 CC 来使用——这就是驾驭工程的实践意义。

30.10 完整架构回顾

将六层叠加起来,形成完整的 Agent 架构:

graph TB
    subgraph L6["第六层:可观测性"]
        O1["tracing 事件"]
        O2["ReviewReport 指标"]
    end

    subgraph L5["第五层:韧性"]
        R1["with_retry 指数退避"]
        R2["CircuitBreaker 熔断器"]
        R3["截断降级"]
    end

    subgraph L4["第四层:安全"]
        S1["bash 白名单 + 黑名单"]
        S2["工具调用上限(3次/文件)"]
        S3["输出截断 + 超时"]
    end

    subgraph L3["第三层:工具"]
        T1["bash (read-only 沙箱)"]
        T2["skill (专项分析提示词)"]
        T3["AgentAction 决策分发"]
    end

    subgraph L2["第二层:上下文"]
        C1["ContextBudget 双层预算"]
        C2["truncate + 元信息"]
    end

    subgraph L1["第一层:提示词"]
        P1["Constitution 静态层"]
        P2["Runtime 动态层"]
    end

    L6 --> L5 --> L4 --> L3 --> L2 --> L1

    style L1 fill:#e3f2fd
    style L2 fill:#e8f5e9
    style L3 fill:#fff3e0
    style L4 fill:#fce4ec
    style L5 fill:#f3e5f5
    style L6 fill:#e0f2f1

下表汇总了每层对应的 CC 模式和本书章节:

层级核心模式来源CC 源码关键文件
L1 提示词提示词即控制面、带外控制信道、工具级提示词、范围匹配响应ch25, ch27systemPromptSections.ts
L2 上下文为一切设定预算、上下文卫生、告知而非隐藏、保守估算ch26toolLimits.ts, toolResultStorage.ts
L3 工具编辑前先读取、结构化搜索ch27FileEditTool/prompt.ts, GrepTool.ts
L4 安全失败关闭、渐进式自主ch25, ch27types/permissions.ts
L5 韧性有限重试预算、熔断失控循环、局部能力降维ch6b, ch26, ch27withRetry.ts, autoCompact.ts
L6 可观测先观察再修复、结构化验证ch25, ch27analytics/index.ts

22 个命名模式中,本章覆盖了 16 个(73%)。未覆盖的 6 个——缓存感知设计、A/B 测试一切、保留重要内容、防御性 Git、双重看门狗、缓存中断检测——要么需要更大的系统规模才有意义(A/B 测试),要么与特定子系统强绑定(缓存中断检测、防御性 Git)。

模式提炼

模式:六层 Agent 构建栈

  • 解决的问题:Agent 构建缺乏系统性方法论,开发者往往只关注"调用模型"而忽略周边工程
  • 核心做法:从提示词 → 上下文 → 工具 → 安全 → 韧性 → 可观测性逐层叠加,每层有明确的职责边界
  • 前置条件:理解本书前 29 章的单层模式,尤其是第 25-27 章的原则提炼
  • CC 映射:每层直接对应 Claude Code 的一个子系统——systemPromptcompactiontoolSystempermissionsretrytelemetry

模式:模式组合而非模式叠加

  • 解决的问题:22 个模式单独看都有价值,但组合使用时可能冲突
  • 核心做法:识别模式间的关系——互补还是张力
    • 互补:上下文卫生 + 告知而非隐藏(截断内容但保留元信息,见 30.3)
    • 互补:失败关闭 + 渐进式自主(默认锁定 + 按需升级,见 30.5)
    • 张力:为一切设定预算 vs 缓存感知设计(截断可能破坏缓存断点)
    • 张力:编辑前先读取 vs Token 预算(读取完整文件可能超预算)
  • 解决张力的方式:为截断行为添加元信息注入,让预算系统在截断时仍保持模型的知情权

用户能做什么

六条建议,每条对应一层:

  1. 提示词层:把系统提示词拆成静态"宪法"和动态"运行时"两部分。宪法部分写入版本控制,运行时部分在每次调用时生成。这不仅是代码组织——如果你将来接入 prompt cache,静态部分可以直接复用。

  2. 上下文层:为 Agent 的每个输入通道设定明确的 Token 预算,并在截断时注入元信息(见 30.3 的实现)。让模型知道自己看到的不是全部,比默默截断要好得多。

  3. 工具层:学习 just-bash 的洞察——bash 是 LLM 天然会用的万能工具接口。但务必加沙箱:白名单允许的命令、阻止输出重定向、限制调用次数。让 LLM 请求工具(AgentAction::UseTool),你的代码验证并执行。另外,skill 不需要依赖外部系统——它们就是专项分析的提示词模板,由你的 Agent 自己管理和加载。

  4. 安全层:多层失败关闭比单层更可靠。我们的 Agent 有 7 层安全约束(白名单 + 黑名单 + 重定向阻止 + 调用上限 + 超时 + 输出截断 + LLM 不直接执行工具)。任何一层被绕过,其他层仍然有效。

  5. 韧性层:为重试设上限,为连续失败设熔断器。没有上限的重试不是韧性——是浪费。CC 源码中的注释(见 30.6)表明,未熔断的重试循环会造成惊人的资源浪费。

  6. 可观测层:在写第一行业务逻辑之前,先接入 tracing。review_startedreview_completed 这两个事件就够你回答"Agent 在做什么"和"做得怎么样"。后续的优化都建立在观测数据之上——没有数据的优化是猜测。