第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-sdk(GitHub)。这是一个社区维护的 Rust SDK,通过子进程方式与 Claude Code CLI 交互。它提供三种使用模式:
| 模式 | API | Agent Loop | 工具 | 认证方式 | 适合场景 |
|---|---|---|---|---|---|
| 完整 Agent | cc_sdk::query() | CC 内部 | CC 内置工具 | API key 或 CC 订阅 | 需要 Agent 自主读写文件、执行命令 |
| 交互客户端 | ClaudeSDKClient | CC 内部 | CC 内置工具 | API key 或 CC 订阅 | 多轮对话、会话管理 |
| LLM Proxy | cc_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.rs | L1 提示词架构 | 提示词即控制面、带外控制信道、工具级提示词 |
context.rs | L2 上下文管理 | 为一切设定预算、上下文卫生、告知而非隐藏 |
agent.rs + tools.rs | L3 工具与搜索 | 编辑前先读取、结构化搜索 |
llm.rs + tools.rs | L4 安全与权限 | 失败关闭、渐进式自主 |
resilience.rs | L5 韧性 | 有限重试预算、熔断失控循环、局部能力降维 |
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-audit、performance-review、rust-idioms、api-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 中共存:
- Turn 1(审查):LLM 只看 diff,无工具访问
- Turn 2+(工具):LLM 可以请求只读 bash 或 skill,但我们的代码验证并执行
- 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_started 和 review_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_failure的fetch_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 -a、grep foo $(id)或反引号替换都能绕过is_command_allowed。
这是一个真实的安全漏洞。修复方式:
- 不再用
sh -c——改为Command::new(program).args(args)直接执行,不经过 shell 解释器 - 阻止所有 shell 元字符:
;|&$(){}`,在命令到达执行前就拦截 - 新增 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_prompt和followup_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, ch27 | systemPromptSections.ts |
| L2 上下文 | 为一切设定预算、上下文卫生、告知而非隐藏、保守估算 | ch26 | toolLimits.ts, toolResultStorage.ts |
| L3 工具 | 编辑前先读取、结构化搜索 | ch27 | FileEditTool/prompt.ts, GrepTool.ts |
| L4 安全 | 失败关闭、渐进式自主 | ch25, ch27 | types/permissions.ts |
| L5 韧性 | 有限重试预算、熔断失控循环、局部能力降维 | ch6b, ch26, ch27 | withRetry.ts, autoCompact.ts |
| L6 可观测 | 先观察再修复、结构化验证 | ch25, ch27 | analytics/index.ts |
22 个命名模式中,本章覆盖了 16 个(73%)。未覆盖的 6 个——缓存感知设计、A/B 测试一切、保留重要内容、防御性 Git、双重看门狗、缓存中断检测——要么需要更大的系统规模才有意义(A/B 测试),要么与特定子系统强绑定(缓存中断检测、防御性 Git)。
模式提炼
模式:六层 Agent 构建栈
- 解决的问题:Agent 构建缺乏系统性方法论,开发者往往只关注"调用模型"而忽略周边工程
- 核心做法:从提示词 → 上下文 → 工具 → 安全 → 韧性 → 可观测性逐层叠加,每层有明确的职责边界
- 前置条件:理解本书前 29 章的单层模式,尤其是第 25-27 章的原则提炼
- CC 映射:每层直接对应 Claude Code 的一个子系统——
systemPrompt、compaction、toolSystem、permissions、retry、telemetry
模式:模式组合而非模式叠加
- 解决的问题:22 个模式单独看都有价值,但组合使用时可能冲突
- 核心做法:识别模式间的关系——互补还是张力
- 互补:上下文卫生 + 告知而非隐藏(截断内容但保留元信息,见 30.3)
- 互补:失败关闭 + 渐进式自主(默认锁定 + 按需升级,见 30.5)
- 张力:为一切设定预算 vs 缓存感知设计(截断可能破坏缓存断点)
- 张力:编辑前先读取 vs Token 预算(读取完整文件可能超预算)
- 解决张力的方式:为截断行为添加元信息注入,让预算系统在截断时仍保持模型的知情权
用户能做什么
六条建议,每条对应一层:
-
提示词层:把系统提示词拆成静态"宪法"和动态"运行时"两部分。宪法部分写入版本控制,运行时部分在每次调用时生成。这不仅是代码组织——如果你将来接入 prompt cache,静态部分可以直接复用。
-
上下文层:为 Agent 的每个输入通道设定明确的 Token 预算,并在截断时注入元信息(见 30.3 的实现)。让模型知道自己看到的不是全部,比默默截断要好得多。
-
工具层:学习 just-bash 的洞察——bash 是 LLM 天然会用的万能工具接口。但务必加沙箱:白名单允许的命令、阻止输出重定向、限制调用次数。让 LLM 请求工具(
AgentAction::UseTool),你的代码验证并执行。另外,skill 不需要依赖外部系统——它们就是专项分析的提示词模板,由你的 Agent 自己管理和加载。 -
安全层:多层失败关闭比单层更可靠。我们的 Agent 有 7 层安全约束(白名单 + 黑名单 + 重定向阻止 + 调用上限 + 超时 + 输出截断 + LLM 不直接执行工具)。任何一层被绕过,其他层仍然有效。
-
韧性层:为重试设上限,为连续失败设熔断器。没有上限的重试不是韧性——是浪费。CC 源码中的注释(见 30.6)表明,未熔断的重试循环会造成惊人的资源浪费。
-
可观测层:在写第一行业务逻辑之前,先接入 tracing。
review_started和review_completed这两个事件就够你回答"Agent 在做什么"和"做得怎么样"。后续的优化都建立在观测数据之上——没有数据的优化是猜测。