Appearance
第18章 设计模式与架构决策
"Good architecture is not about doing everything right the first time. It's about making decisions that are easy to change." -- Martin Fowler, Patterns of Enterprise Application Architecture
本章要点
- Generator 驱动的流式管道:为什么 AsyncGenerator 是 AI Agent 系统的最佳编排原语
- 自描述工具模式:工具即对象、Schema 即文档、运行时校验三位一体
- 多模式权限模型:从五种权限模式的设计推演出分层安全架构
- 并行预加载与启动优化:Side-effect Imports、并行预读与特性标志死代码消除
- 协议优先的扩展性:MCP 标准协议如何解耦 Agent 的能力与实现
- 上下文窗口经济学:Token 预算管理、自动压缩与结果截断的协同策略
- 安全与自由的平衡术:分层安全检查、沙箱隔离与用户信任级别
- 构建你自己的 Agent 系统:从 Claude Code 提炼的架构原则与实现路线图
在前面十七章中,我们从 CLI 启动、Query 引擎、流式处理、工具系统、权限模型、MCP 协议到终端 UI,逐一拆解了 Claude Code 的每一个核心子系统。但正如一座建筑的价值不仅在于砖瓦的品质,更在于整体的结构设计——Claude Code 的真正精妙之处,在于这些子系统背后一以贯之的设计模式与架构决策。
本章是全书的收官。我们将站在更高的抽象层次上,从 Claude Code 源码中萃取出七个可迁移的设计模式。这些模式不仅适用于 Claude Code 本身,也适用于任何需要构建 AI Agent 系统的工程团队。对于每一个模式,我们都会追问三个问题:它解决了什么问题?它在源码中如何实现?如果我要构建自己的 Agent 系统,该如何复用它?
18.1 Generator 驱动的流式管道
Claude Code 中 AsyncGenerator 贯穿了从 API 响应到 UI 渲染的完整数据流。下图展示了 Generator 管道的核心组合模式:
18.1.1 模式描述与动机
在任何 AI Agent 系统中,最核心的架构挑战是如何编排一个多轮、流式、可中断的执行循环。这个循环需要同时满足以下约束:
- 流式输出:模型的响应是逐 token 流出的,UI 需要实时渲染
- 工具穿插:模型可能在响应中途请求调用工具,工具执行完毕后循环继续
- 可中断性:用户随时可能按下 Ctrl+C 中断当前操作
- 错误恢复:API 错误、Token 超限、模型降级都需要在循环内处理
- 多消费者:同一个循环的输出需要同时送给 UI 渲染、SDK 回调、日志系统
传统的异步编程范式在面对这些约束时往往力不从心。回调地狱(Callback Hell)让控制流碎片化;Promise 链无法表达"暂停并等待外部输入"的语义;RxJS 的 Observable 虽然强大,但引入了沉重的概念负担和运行时依赖。
Claude Code 选择了 JavaScript 的 AsyncGenerator 作为核心编排原语。这个选择看似简单,却深刻地影响了整个系统的架构风格。
18.1.2 在 Claude Code 中的应用
Claude Code 的核心查询循环 query() 函数就是一个异步生成器:
typescript
// 文件:src/query.ts
export async function* query(
params: QueryParams,
): AsyncGenerator<
| StreamEvent
| RequestStartEvent
| Message
| TombstoneMessage
| ToolUseSummaryMessage,
Terminal
> {
const consumedCommandUuids: string[] = []
const terminal = yield* queryLoop(params, consumedCommandUuids)
for (const uuid of consumedCommandUuids) {
notifyCommandLifecycle(uuid, 'completed')
}
return terminal
}这个函数的返回类型 AsyncGenerator<YieldType, ReturnType> 精确地编码了两层信息:YieldType 是循环过程中流出的中间产物(流式事件、消息、墓碑标记),ReturnType 是循环结束时的终态(成功、错误、中断的原因)。
核心的 queryLoop 是一个 while(true) 循环,通过 yield 将中间产物推送给消费者:
typescript
// 文件:src/query.ts
async function* queryLoop(
params: QueryParams,
consumedCommandUuids: string[],
): AsyncGenerator<...> {
let state: State = {
messages: params.messages,
toolUseContext: params.toolUseContext,
autoCompactTracking: undefined,
maxOutputTokensRecoveryCount: 0,
hasAttemptedReactiveCompact: false,
// ...
}
while (true) {
let { toolUseContext } = state
yield { type: 'stream_request_start' }
// 1. 消息预处理流水线
// 2. API 调用与流式接收
for await (const message of deps.callModel({...})) {
yield yieldMessage // 逐条推送给消费者
}
// 3. 工具执行
// 4. 停止条件判断
// 5. state 转移 -> continue 或 return
}
}这个模式在工具编排层同样得到了一致的应用。toolOrchestration.ts 中的 runTools 函数也是一个异步生成器:
typescript
// 文件:src/services/tools/toolOrchestration.ts
export async function* runTools(
toolUseMessages: ToolUseBlock[],
assistantMessages: AssistantMessage[],
canUseTool: CanUseToolFn,
toolUseContext: ToolUseContext,
): AsyncGenerator<MessageUpdate, void> {
let currentContext = toolUseContext
for (const { isConcurrencySafe, blocks } of partitionToolCalls(
toolUseMessages, currentContext,
)) {
if (isConcurrencySafe) {
for await (const update of runToolsConcurrently(blocks, ...)) {
yield { message: update.message, newContext: currentContext }
}
} else {
for await (const update of runToolsSerially(blocks, ...)) {
yield { message: update.message, newContext: currentContext }
}
}
}
}