# 把 AI 当乐高积木

Claude Code 代码库里，同一个 `query()` 函数（这是整个系统的"万能引擎"——所有需要调用 AI 模型的地方，无论是主对话、子 Agent、还是后台记忆整理，都通过它来执行）被用在了至少 7 个不同的场景里。

> 🌍 **行业背景**："一个通用执行引擎 + 参数化配置"的架构模式在软件工程中由来已久——GoF 设计模式中的策略模式（Strategy Pattern）和工厂方法模式（Factory Method Pattern）配合起来，就是这类架构的经典表达（GoF 23 个标准模式中没有"参数化工厂"这一项——此处是本书对"策略 + 工厂方法协作"这类架构的口语化概括）。在 AI 框架领域，不同产品在"多种 AI 调用场景"这个相同问题上做出了截然不同的架构选择：
>
> | 框架/产品 | 架构路线 | 核心机制 | 优势 | 代价 |
> |-----------|---------|---------|------|------|
> | **Claude Code** | 统一引擎 + 参数化 | 同一个 `query()` 函数，通过 `querySource`、权限、工具集等参数区分 7 种角色 | 所有角色自动继承引擎优化（流式、缓存、重试）；改一处惠及全局 | 单一路径复杂度上升；一个 bug 可能影响所有角色 |
> | **Cursor Agent** | 独立路径 + 共享基础设施 | Tab 补全、Inline Edit、Chat、Agent 模式各有独立调用路径，底层共享 LLM 通信层 | 每种模式可独立优化；故障隔离好 | 横切优化（即需要"一刀切"应用到所有路径的改进，如缓存策略）需要在多条路径上分别实现 |
> | **Aider** | 显式多模型协作 | 可配置 architect model + editor model，不同模型承担不同角色，各有独立的 system prompt 和输出解析 | 可以为不同角色选择最佳模型（如 GPT-4o 做架构、Claude 做编辑） | 模型间交互协议需要额外设计；模型切换增加延迟 |
> | **LangGraph** | 状态机编排 | 每个 Agent 是状态图中的一个节点，通过条件边连接，状态在节点间显式传递 | 工作流可视化、可回溯；复杂编排逻辑表达清晰 | 简单场景的样板代码多；状态图本身成为需要维护的抽象层 |
> | **OpenAI Agents SDK** | 对象实例化 + Handoff | 每个 `Agent` 是独立实例，通过 `handoff` 机制将控制权转移给另一个 Agent | 面向对象直觉清晰；Agent 边界明确 | 公共逻辑（错误重试、流式处理）需要在基类中维护；handoff 的状态传递需要显式设计 |
>
> 这些选择没有绝对的优劣——它们取决于团队规模、AI 角色之间的相似程度、以及迭代速度的优先级。Claude Code 选择极致统一路线，背后的工程上下文是：所有角色都调用同一个模型（Claude）、使用相同的消息格式和工具协议，差异主要在参数层面而非执行逻辑层面。在这种条件下，统一引擎的收益最大化、代价最小化。但如果你的系统需要跨模型供应商协作（如主 Agent 用 Claude、子 Agent 用 GPT），Aider 的多模型路线或 LangGraph 的状态机编排可能更合适。

---

## 七个 AI 实例

每次有人说 Claude Code 调用了 Claude，他们可能说的是以下任何一个：

1. **主循环 AI** — 响应用户输入的那个 (`querySource: 'repl_main_thread'`)
2. **子 Agent** — 执行 `Agent` 工具调用的 AI
3. **投机执行 AI** — 在用户打字时提前运行 (`querySource: 'speculation'`)
4. **SessionMemory AI** — 后台记笔记的 AI (`querySource: 'session_memory'`)
5. **Prompt Suggestion AI** — 预测用户下一条消息的 AI
6. **Hook Agent** — 用于 Stop 条件验证的 AI (`querySource: 'hook_agent'`)
7. **上下文压缩 AI** — 对话太长时进行摘要的 AI

这七个 AI 实例共享同一套工具系统、同一套权限系统、同一套消息格式——但各自有不同的 `querySource`、不同的 `ToolUseContext`、不同的权限限制。

> 💡 **通俗理解**：这就像一个**外卖平台的骑手调度系统**——主循环 AI 是调度中心总调度员，子 Agent 是各片区骑手，投机执行 AI 是提前出发的骑手（赌你会下单），SessionMemory AI 是记录每天送单情况的文员，Prompt Suggestion AI 是预测你下顿想吃什么的分析师。他们都骑同一款电动车（`query()` 函数），只是接的单不一样。

---

### 洞察：元提示词——用 AI 教 AI 怎么用 AI

"七个 AI 实例共享同一个引擎"是横向的复用。但 Claude Code 还有一个纵向的递归：**用提示词来生产提示词**——即元提示词（meta-prompting）架构。

**第一层：AgentTool 的"提示词写作课"**

`src/tools/AgentTool/prompt.ts` 里的 `getPrompt()` 函数不仅描述了 AgentTool 能做什么，还包含了一整节 `## Writing the prompt`，直接教 Claude 怎么给子 Agent 写好提示词：

```
Brief the agent like a smart colleague who just walked into the room —
it hasn't seen this conversation, doesn't know what you've tried,
doesn't understand why this task matters.
- Explain what you're trying to accomplish and why.
- Describe what you've already learned or ruled out.
- Give enough context about the surrounding problem that the agent
  can make judgment calls rather than just following a narrow instruction.
```

这不是工具描述，这是**提示词写作规范**。Claude 在调用 AgentTool 时，同时也在被 system prompt 培训"什么是好的子 Agent 提示词"。提示词在教 AI 如何生产提示词。

注意那句"Terse command-style prompts produce shallow, generic work"（"简短命令式提示词只会产生浅薄的泛化输出"，源码出处 `src/tools/AgentTool/prompt.ts:110`）——这条断言被编码进了工具描述的教学内容，对每次使用 AgentTool 的 Claude 实例生效；它背后的支撑是否来自公开实验记录，以 Anthropic 官方公告为准。

**第二层：AGENT_CREATION_SYSTEM_PROMPT——把 Claude 变成"AI 架构师"**

`src/components/agents/generateAgent.ts` 走得更远。当用户要创建一个自定义 Agent 时，系统用 `AGENT_CREATION_SYSTEM_PROMPT` 调用 Claude，而这个 system prompt 开头写道：

```
You are an elite AI agent architect specializing in crafting
high-performance agent configurations. Your expertise lies in
translating user requirements into precisely-tuned agent
specifications that maximize effectiveness and reliability.
```

Claude 被赋予了一个新角色——不是"助手"，而是"AI 智能体架构师"。它要完成五项任务：提取核心意图、设计专家人格、构建综合指令、优化性能、生成标识符。输出是一个 `GeneratedAgent` 对象（包含 `identifier`、`whenToUse`、`systemPrompt` 字段），这个对象会成为下一个 AI 实例的配置——**Claude 生产了供 Claude 消费的提示词**。

这个 system prompt 本身还示范了"何时调用 AgentTool"的行为示例（带 `<commentary>` 标签），是递归到第三层的元提示词：用示例教 Claude 如何写示例教其他 Claude 如何行动。

**"乐高积木"的逻辑极端**

如果说"七个 AI 实例共享一个引擎"是把 AI 当乐高积木（同一块积木，不同颜色和尺寸），那么元提示词架构是把 AI 当**积木设计工具**——不只是复用同一个引擎，而是让引擎学会设计自己的积木规格。

用户描述一个需求 → Claude（作为架构师）设计 Agent 配置 → 生成的配置驱动另一个 Claude 实例 → 该实例在执行任务时调用子 Agent（使用 AgentTool 学到的提示词写作技巧）→ 子 Agent 可能再触发新的生成循环。

这不是嵌套调用，这是**能力的自我扩展**：系统通过元提示词机制，把 Anthropic 工程师对"什么是好 Agent"的判断知识，传递给每一个运行中的 Claude 实例，让它们也能生产高质量的 Agent 配置。

> 💡 **通俗理解**：这就像**一个烹饪学校请了一位大厨（Claude）来教厨师培训课程（AgentTool 提示词写作课）**，同时这个大厨还能根据客人需求设计新菜系（AGENT_CREATION_SYSTEM_PROMPT），而设计好的菜系又被其他大厨学习并发扬——知识在 AI 实例之间流动，而非停留在人类工程师手里。

---

## 它们是怎么组合起来的

**模块化的关键不是继承，而是参数**。

```typescript
// query() 函数的入参（简化）
type QueryParams = {
  messages: Message[]
  systemPrompt: SystemPrompt
  querySource: QuerySource    // 区分是谁在调用
  toolUseContext: ToolUseContext  // 包含 AppState 和权限
  canUseTool: CanUseToolFn    // 每个调用者可以覆盖权限检查
  tools?: Tools               // 可以限制可用工具集
}
```

每个 AI 实例通过不同的参数配置来实现不同的行为：

| 实例 | 特殊配置 |
|------|---------|
| 主循环 | 完整权限、完整工具、完整 system prompt |
| 子 Agent | `setAppState` 是 no-op（无法修改父状态） |
| 投机执行 | `CacheSafeParams`（必须与主循环参数相同以共享缓存） |
| SessionMemory | `isNonInteractiveSession: true`、只能用 Edit 工具 |
| Hook Agent | `mode: 'dontAsk'`、额外添加 SyntheticOutputTool |
| 压缩 AI | 更小的模型（Haiku）、只输出摘要 |

---

## 为什么选择统一引擎

传统做法是把每种 AI 调用写成独立的函数：`callSessionMemoryAI()`、`callSpeculationAI()`、`callHookAgentAI()`……结果是大量重复代码，每个函数都有自己的错误处理、流式处理、工具调用循环。

当然，还有一条**中间路线**：把公共的流式处理、缓存、错误重试提取到基类或 mixin，然后每种 AI 角色继承或组合这些公共能力。这种方案同样实现了 DRY，但每个角色有更清晰的关注点分离。Cursor 的架构更接近这条路线——不同模式（Tab 补全、Chat、Agent）有独立的调用路径，但共享底层的 LLM 通信基础设施。

Claude Code 选择了更激进的统一路线：让 `query()` 成为通用的 **AI 执行引擎**，任何需要 AI 能力的组件都通过参数配置来使用它。这个选择之所以在 Claude Code 的场景下可行，是因为其 7 种 AI 角色之间的执行逻辑高度相似——都是"发消息、调工具、处理响应"的循环，差异集中在参数层面而非流程层面。

> 📚 **课程关联**：这个架构选择直接对应**软件工程**课程中的 **DRY（Don't Repeat Yourself）** 原则。它也**部分**符合**开闭原则（Open-Closed Principle）**——当新角色只需要不同的参数组合时，确实可以不修改 `query()` 就添加新角色（如新增一个只读分析 AI，只需传入受限的工具集和对应的 system prompt）。但这个"开闭"是有边界的：如果新角色需要全新的执行逻辑（比如完全不同的流式处理方式或错误重试策略），你仍然需要修改 `query()` 内部——此时"统一引擎"的开闭性就不再成立。这不是 Claude Code 设计的缺陷，而是参数化模式的固有边界：**参数能表达的变化维度是有限的**。

---

## 两种隔离：内存状态 vs 文件系统

当子 Agent 被创建时，它的 `ToolUseContext` 里：

```typescript
setAppState: () => {}  // no-op
```

这意味着子 Agent **无法修改父 Agent 的内存状态**（如 UI 状态、会话元数据等）。但需要明确：**这只是内存级别的隔离，不是 I/O 隔离**。子 Agent 仍然可以读文件、写文件、执行命令——这些文件系统和外部环境的副作用是**真实发生的、不可通过 `setAppState` 回滚的**。

Claude Code 在不同场景下使用了不同粒度的隔离策略：

| 隔离层级 | 机制 | 适用场景 | 隔离范围 |
|----------|------|---------|---------|
| **内存状态隔离** | `setAppState: () => {}` | 所有子 Agent（AgentTool 调用） | 父 Agent 的应用状态不被子 Agent 修改，但文件写入是真实的 |
| **文件系统隔离** | Overlay 文件系统 | 仅投机执行场景 | 文件操作在虚拟层进行，预测错误时可以完整回滚 |
| **权限隔离** | `canUseTool` + 工具集限制 | SessionMemory（只能用 Edit）、Hook Agent（`dontAsk` 模式） | 从源头限制可执行的操作类型 |

换句话说，普通子 Agent 通过 AgentTool 调用时，如果它写入了错误的文件内容，主 Agent 无法自动回滚——文件系统的变更是真实的。只有投机执行场景才有 overlay 文件系统提供的完整隔离。这是一个重要的工程权衡：为所有子 Agent 都提供文件系统级隔离（如沙箱或 overlay）成本太高，`setAppState` 的内存隔离是"够用且廉价"的折中方案。

> 💡 **通俗理解**：`setAppState: () => {}` 就像你派助手去帮你办事，但不给他你的银行卡密码（内存状态）——他不能动你的存款。但你给了他你家的钥匙（文件系统权限），他可以进出你的房子、移动家具。投机执行的 overlay 文件系统则更像"先在模型房里演练一遍，确认没问题再搬到真正的房子里"。

---

## 应用到你自己的 AI 系统

如果你在构建多 AI 协作的系统，需要在三条路线中做选择：

| 路线 | 适用条件 | 代表 |
|------|---------|------|
| **统一引擎 + 参数化** | 角色少（<10）、执行逻辑相似、单一模型、小团队 | Claude Code |
| **独立路径 + 共享基础设施** | 角色的执行逻辑有显著差异、需要独立优化和故障隔离 | Cursor |
| **状态机 / 对象编排** | 角色间有复杂的协作流程、需要可视化和回溯 | LangGraph、OpenAI Agents SDK |

如果你的场景适合统一引擎路线，Claude Code 的模式值得借鉴：

**定义一个通用执行函数**，入参里有：
- `querySource` — 用于日志、权限、计费区分
- `canUseTool` — 每个调用者定义自己的权限边界
- `setAppState` — 是否允许这个 AI 修改全局状态（通常子 AI 不应该），但要记住这只隔离内存状态，不隔离 I/O 副作用

**把 AI 的"个性"放在参数里**，而不是放在函数实现里。System prompt、工具集、权限——这些是配置，不是代码。

**警惕统一引擎的临界点**：
- 当你发现 `query()` 内部开始出现大量 `if (querySource === 'xxx')` 分支时，说明参数化已经不够用，考虑拆分
- 当新角色需要不同的模型供应商、不同的流式处理方式时，统一引擎的抽象会变成束缚
- 当团队并行开发多个 AI 角色时，共享代码路径的合并冲突会拖慢所有人

---

## 类比

需要区分两种不同的设计模式：

**Unix 管道**是**组合（composition）**——`cat | grep | sort` 把多个**不同的程序**通过标准接口串联，每个程序做不同的事。Claude Code 的 `query()` 并不是这种模式——它不是把多个不同程序组合起来，而是**同一个程序通过不同参数服务于多种场景**。

更准确的 Unix 类比是 `curl` 命令：`curl -X GET` 发送 GET 请求，`curl -X POST -d '...'` 发送 POST 请求，`curl -H 'Authorization: ...'` 带认证头。同一个程序，通过不同的 flag 组合，服务于各种 HTTP 场景。Claude Code 的 `query()` 正是这种**参数化（parameterization）**模式：同一个执行引擎，通过 `querySource`、`tools`、`canUseTool` 等参数的不同组合，变身为主循环 AI、子 Agent、投机执行 AI 等不同角色。

> 💡 **通俗理解**：这就像一台**多功能料理机**——你不需要分别买榨汁机、搅拌机、研磨机，而是一个电机底座配不同的刀头和杯体。换上榨汁刀头就是榨汁机，换上搅拌杯就是搅拌机。`query()` 就是那个电机底座，参数就是不同的刀头和杯体。

> 📚 **课程关联**：从**软件工程**设计模式的视角看，`query()` 最贴切的对应是**策略模式（Strategy Pattern）**——执行引擎的核心流程（流式处理、工具调用循环、错误重试）是固定的"骨架"，而 system prompt、工具集、权限函数等参数充当可替换的"策略"。另一个视角是**依赖注入（Dependency Injection）**——每个 AI 角色不是自己构造执行环境，而是由调用方注入所需的配置。这两种分析都比编译原理类比更准确：不同的 `querySource` 并不对应"不同的源语言"——它们都使用相同的消息格式和工具协议，差异仅在于参数配置，而非语法或语义的根本不同。

---

## 代码落点

- `src/services/api/claude.ts`：统一的 `query()` 函数——所有 7 种 AI 调用共用的执行入口
- `src/tools/AgentTool.ts`：子 Agent 的工具定义——通过参数化 `query()` 实现 Agent 分身
- `src/services/sideQuery.ts`：旁路查询——用于记忆检索、Yolo 分类等非主对话场景

## 权衡与反思

共用 `query()` 的设计收益是真实的，但风险同样真实：

**故障半径扩大**。当 7 种调用场景共用同一条执行路径时，为投机执行优化 `CacheSafeParams` 的参数处理逻辑，可能意外影响 SessionMemory AI 的行为。调试时第一个问题永远是"这是哪种 `querySource` 的问题"。如果 `query()` 内部出现了大量 `if (querySource === 'xxx')` 分支，那么"统一引擎"在代码层面可能已经退化为"同一个文件里的多个函数"——物理位置合并了但逻辑并未统一。

**何时应该拆分？** 如果未来 AI 角色增加到 15 个、20 个，`query()` 的参数列表会膨胀到什么程度？判断框架是：**当新角色需要的不仅是参数差异，而是执行流程差异时**（比如完全不同的流式处理方式、不同的模型供应商、不同的错误重试策略），统一引擎的维护成本会急剧上升。此时 Cursor 的"独立路径 + 共享基础设施"路线或 LangGraph 的状态机编排可能更合适。

**不该照搬的场景**：
- 当团队超过一定规模时，所有人共享同一条代码路径意味着合并冲突会成为瓶颈
- 当你的 AI 角色需要不同的模型供应商（如主 Agent 用 Claude、子 Agent 用 GPT-4o），统一引擎的抽象会变得牵强——Aider 的多模型路线此时更自然
- 当 AI 角色之间的执行逻辑差异超过参数能表达的范围时，硬塞进同一个函数反而降低可读性

Claude Code 的统一 `query()` 是在特定工程上下文（单一模型、小团队、角色执行逻辑高度相似）下的正确选择。理解这个选择的**条件**，比模仿这个选择的**形式**更有价值。
