# 如果我来重新设计

这是一个思想实验：如果你从头设计一个类似 Claude Code 的系统，哪些设计决策会有所不同？

> 💡 **通俗理解**：这就像**装修完房子之后的复盘**——住进去才发现厨房插座不够用、卫生间门应该开在另一边、收纳空间设计得太少。不是说房子盖得不好，而是"如果重来一次，这些地方可以更好"。Claude Code 也是一样——很多设计在实践中暴露了改进空间。

这不是否定 Claude Code 的设计——很多决策有合理的历史原因。这是一个帮助深入理解权衡的练习。

> 🌍 **行业背景**：重新设计不是空想——业界已经有人在不同方向上做了尝试。**Cursor** 把 token 预算管理交给了编辑器层面，用户通过 UI 直观地看到"长上下文"和"短上下文"模式的区别，部分解决了本章重设计 1 讨论的 token 预算透明性问题。**Devin**（Cognition Labs）将 AI Agent 设计为完全自治的虚拟开发者，运行在云端沙箱中，其 Agent 协作图（本章重设计 2）是显式可见的——用户可以实时观察 Devin 在做什么。**Aider** 作为开源的终端 AI 编程工具，选择了极简路线：没有 hooks 系统、没有插件生态、没有多 Agent 编排，但代码库只有几千行，一个下午就能读完。这些不同选择说明：Claude Code 当前设计的每个"代价"背后，都有一条未被选择的替代路径。

---

## 重设计 1：把 token 预算做成显式的类型系统

> 💡 **"类型系统"是什么？** 简单说，就是给代码里的每样东西贴标签——"这是数字"、"这是文本"、"这是 token 预算"。如果你不小心把"文本"当"数字"用，系统在你写代码时就会报错，而不是等到程序运行出了事故才发现。"显式的类型系统"的意思是：把 token 预算也变成一种标签，让写错的人在第一时间被系统拦住。

Claude Code 在运行时大量考虑 token 成本（`omitClaudeMd`、`CacheSafeParams`、6 层压缩），但这些考虑散落在代码各处，没有一个统一的抽象。

**现在的问题**：
- 工程师在添加新功能时，需要主动了解"这会影响 cache 吗"、"这会增加多少 token"
- 这种知识是隐性的，不是代码层面可见的
- CacheSafeParams 的约束是注释，不是编译时检查

**如果重新设计**：引入 `TokenBudget<T>` 类型，把 token 消耗变成类型系统的一部分：

```typescript
// 伪代码
type TokenBudget = {
  systemPrompt: Budget
  tools: Budget
  history: Budget
}

function createSubAgent(
  prompt: CacheSafePrompt,  // 编译时确保与父请求参数相同
  budget: TokenBudget,
): SubAgent
```

这样 CacheSafeParams 的约束就不是注释里的"DO NOT"，而是类型错误。

> ⚠️ **可行性分析：TypeScript 类型系统的边界**
>
> 上面的伪代码展示了理想状态，但必须诚实面对一个技术事实：**token 数量是运行时属性，TypeScript 的结构化类型系统无法在编译期检查它**。一个字符串包含多少 token，取决于具体的 tokenizer（不同模型的 tokenizer 不同）、运行时的实际内容、甚至 API 的 pricing 规则变化。这和 Rust 的所有权系统有本质区别——所有权是固定的语义规则，可以在编译期完全确定；而 token 计数依赖运行时信息，无法静态推导。
>
> 更现实的方案是：**运行时预算追踪器 + 开发时 lint 规则（"lint"即自动代码检查工具——像 Word 的拼写检查，但检查的是代码规范）**的组合。具体来说：
> - 用一个 `TokenBudgetTracker` 类在运行时追踪各组件的 token 消耗，提供 `.allocate()` / `.consume()` / `.remaining()` API
> - 用 ESLint 自定义规则检查"是否每个构造 prompt 的函数都接受 budget 参数"
> - 在 CI 中跑集成测试，验证典型场景下各组件的 token 分配不超预算
>
> 讽刺的是，这恰恰就是 Claude Code 现在在做的事情的更结构化版本——`CacheSafeParams` 的注释约束升级为 lint 规则，分散的 token 计算集中到一个 tracker。改进是增量的，不是革命性的。

> **为什么 Claude Code 现在没这么做？** token 是运行时属性、LLM pricing 随时变化、不同模型的 tokenizer 不同——这意味着任何编译期方案都必然不完整。Claude Code 团队选择了更务实的路线：用注释约束 + code review 来管理 token 相关逻辑，把工程精力投入到更紧迫的功能迭代上。这个决策在快速迭代阶段是合理的——代价是新工程师上手时容易踩坑（不知道某个改动会破坏 cache），但收益是团队不需要维护一套复杂的 budget 基础设施。
>
> **迁移成本**：引入集中式 budget tracker 需要重构所有 prompt 构造路径（至少涉及 `src/services/api/claude.ts` 中的多个函数），估计 2-4 周的工程量。主要风险不是代码改动量，而是 budget 分配策略本身需要大量调参——分配多了浪费，分配少了截断，而最优分配因用户场景而异。

> 📚 **课程关联**：将运行时约束提升为可检查的规则，借鉴了**依赖类型**（Dependent Types）的思想。但需要注意理想与现实的距离：学术界的依赖类型可以编码任意约束，而工业界的 TypeScript 只能在结构层面提供有限保证。

---

## 重设计 2：把 AI 实例设计成明确的有向图

现在系统里存在多类 AI 实例（主 AI、子 Agent、SessionMemory、Hook Agent、Speculation、Compact、以及若干 sideQuery 类小查询；实际源码中 `querySource` 字段可枚举出数十个取值，以官方仓库为准），它们的关系隐藏在 `querySource` 字段和代码流程里。主 AI 可以创建子 Agent，子 Agent 可以再创建子 Agent；SessionMemory AI 是后台运行的；Hook Agent 是按需创建的。

**现在的问题**：
- 没有一个地方可以看到"当前有多少 AI 在运行，它们的关系是什么"
- 子 Agent 创建子 Agent 的深度没有明确的限制（除了隐式的 token 和时间限制）
- 调试多 AI 协作时，很难追踪哪个 AI 做了什么

**如果重新设计**：把 AI 协作建模为有向图（DAG）：

```
MainAgent
├── SpeculationAgent (background)
├── SessionMemoryAgent (background, post-sampling)
└── SubAgent[0]
    ├── SubAgent[0.0]
    └── HookAgent (stop condition)
```

这个图应该是运行时可见的（通过 `/agents` 命令或类似接口），每个节点有明确的生命周期、资源限制、取消传播规则。

Claude Code 已经有 `AppState.tasks` 来追踪子 Agent，并且 `querySource` 字段已经为每种 AI 实例提供了身份标识——这说明团队已经意识到 Agent 可追溯性的需求并做了初步实现。重设计方案应该基于这个现有基础提出增量改进，而非假设从零开始。

> ⚠️ **类比校准：调用栈而非容器编排**
>
> 需要修正一个容易误导的类比。Claude Code 的 Agent 不是长期运行的服务——它们是**短暂的、按需创建的 LLM 调用序列**。一个 SubAgent 的生命周期通常只有几秒到几十秒，创建和销毁的开销很轻。把这种模式类比为 Kubernetes Pod 编排或 Airflow DAG 调度，是把轻量级的函数调用类比成了重量级的容器编排，量级上差了好几个数量级。
>
> 更合适的类比是**函数调用栈（call stack）或 async task tree**：主 Agent 调用子 Agent，子 Agent 可能再调用子 Agent，完成后结果向上返回——这就是递归函数调用的模式。DAG 可视化仍然有价值（就像调试器的 call stack 视图），但它的作用是帮助开发者理解调用关系，而非做 Kubernetes 那种资源调度和容器编排。

> **为什么 Claude Code 现在没这么做？** Agent 的动态创建模式决定了图的拓扑在运行前不可知——主 Agent 根据用户请求的复杂度决定是否创建子 Agent，子 Agent 根据执行情况决定是否继续分裂。强制要求静态声明 Agent 拓扑会限制这种灵活性。此外，Claude Code 的 Agent 通常只有 2-3 层嵌套深度，调试复杂度有限——在大多数场景下，现有的 `querySource` 追踪机制已经够用。
>
> **迁移成本**：添加运行时 Agent 拓扑视图（类似 `/agents` 命令）相对轻量——主要是在 `AppState` 中维护一棵 Agent 树，在创建/销毁时更新节点。估计 1-2 周工程量。但真正的成本在后续维护：每次修改 Agent 创建逻辑都需要同步更新拓扑追踪，且可视化 UI 需要处理 Agent 快速创建/销毁时的渲染抖动。

> 📚 **课程关联**：把 AI 实例的运行时关系建模为可查询的数据结构，对应**操作系统**课程中的进程树（`pstree` 命令）和**分布式系统**课程中的 trace propagation（如 OpenTelemetry 的 span tree）。Claude Code 当前的隐式 Agent 关系，相当于在一个看不到"任务管理器"的电脑上排查哪个程序占满了内存——你知道出了问题，但看不到是谁在搞事。

---

## 重设计 3：把 CLAUDE.md 设计成版本化的合约

CLAUDE.md 现在是一个被动文件——Claude 每次启动时读取它。但这带来了一些问题：

**现在的问题**：
- CLAUDE.md 修改后，Claude 在下一次会话才会使用新内容
- 没有机制告诉 Claude"这条规则比那条规则更重要"
- 多个 CLAUDE.md 文件的优先级通过加载顺序来决定——源码注释（`src/utils/claudemd.ts`）明确写道："Files are loaded in reverse order of priority, i.e. the latest files are highest priority with the model paying more attention to them"。也就是说，高优先级文件被放在 prompt 的更后面位置，依赖模型对后出现内容给予更多注意力。这种机制是确定性的（加载顺序：全局 → 用户 → 项目根 → 当前目录，越近越后加载），但优先级的"执行力度"取决于模型的注意力分配行为，而非显式的优先级指令——这确实存在脆弱性

**如果重新设计**：把 CLAUDE.md 变成有版本、有优先级、有冲突检测的合约系统：

```yaml
# .claude/contracts/security.md
version: "1.2"
priority: critical  # critical / normal / hint
scope: workspace    # global / workspace / directory
conflicts_with:     # 如果与这些合约冲突，报告
  - "performance"
rules:
  - Never commit to main directly
  - Always run tests before committing
```

这样系统可以在加载时检测冲突，可以按优先级合并，可以在用户修改规则时检测破坏性变更。

> ⚠️ **诚实的承认**：这个方案的核心——版本号、优先级、scope、冲突检测——在 JavaScript 工具链中并不新鲜。ESLint 的 cascade config、Prettier 的 config 合并、甚至 2012 年的 `.editorconfig` 都实现了目录级 scope 和规则合并。这里的真正贡献不是发明新模式，而是把成熟的配置管理实践应用到 AI Agent 的指令系统中。

> **为什么 Claude Code 现在没这么做？** CLAUDE.md 的核心价值在于**零门槛**——它就是一个 Markdown 文件，任何人都能编辑，不需要学习 YAML schema、priority 语法或冲突检测规则。加入合约化机制会引入学习成本：用户需要理解 `critical` 和 `normal` 的区别、`scope` 的继承逻辑、以及冲突报告的含义。对于大多数只在项目根目录放一个 CLAUDE.md 的用户来说，这些复杂性是不必要的。Claude Code 团队选择了"简单文件 + 确定性加载顺序"的方案，用最低的用户门槛覆盖了 90% 的使用场景。
>
> **迁移成本**：从 Markdown 文件迁移到 YAML 合约需要设计迁移工具（自动将现有 CLAUDE.md 转换为新格式）、提供向后兼容（新系统能读旧格式）、以及更新所有文档和教程。更大的隐性成本是社区适应——现有的 CLAUDE.md 最佳实践、社区模板、团队规范都需要重写。

---

## 重设计 4：Hooks 的能力需要更细粒度的权限

现在的 hooks 是二元的：要么被信任（可以执行任意 shell 命令——"shell 命令"即在终端/命令行里输入的指令，比如删除文件、安装软件等，就像文字版的电脑操作），要么不被信任（完全禁用）。

**现在的问题**：
- 你不能说"这个 hook 只能读取文件，不能网络请求"
- 你不能说"这个 hook 的 shell 命令超时是 5 秒而不是 10 分钟"
- 插件 hooks 和用户自定义 hooks 有相同的能力边界

**如果重新设计**：给 hooks 添加能力声明：

```json
{
  "PreToolUse": [{
    "matcher": "Edit",
    "hooks": [{
      "type": "command",
      "command": "lint.sh $CLAUDE_FILE_PATHS",
      "capabilities": {
        "network": false,
        "filesystem": "read-only",
        "timeout": 5000
      }
    }]
  }]
}
```

理想情况下，系统执行 hook 时在沙箱里运行，强制执行声明的能力边界。这让 hooks 的权限模型更清晰，减少恶意 hooks 的影响范围。

> ⚠️ **实现复杂度不应被低估**
>
> 上面的 JSON 配置看起来简洁优雅，但"声明一个能力字段"和"在运行时强制执行这个能力"之间存在巨大的工程鸿沟。要做到进程级的文件系统只读隔离：
> - 在 **macOS** 上，需要使用 seatbelt profiles（`sandbox-exec`）—— 但 Apple 已经将其标记为 deprecated，且文档极其有限
> - 在 **Linux** 上，需要 seccomp-bpf 过滤器或 namespace 隔离，配置复杂且不同发行版行为不一致
> - 在 **Windows** 上，进程隔离机制完全不同，需要单独的实现路径
>
> 此外还有**性能代价**：每次 hook 执行都启动沙箱环境，会增加显著的延迟。对于 `PreToolUse` 这种在关键路径上的 hook，增加 50-200ms 的沙箱启动开销是否可接受？在用户执行频繁编辑操作时，这些延迟会累积。
>
> 一个更渐进的方案是：先实现**声明式能力检查**（只声明、不强制，用于审计和提示），再逐步引入**轻量级隔离**（如 `--read-only` 挂载的 tmpdir 而非完整沙箱）。这就是 Claude Code 现有权限系统的演进思路——先做 fail-closed 的用户确认，再考虑自动化沙箱。

> **为什么 Claude Code 现在没这么做？** Hooks 系统本身还处于早期阶段（从 settings.json 配置的简洁性可以看出团队有意保持低复杂度）。在用户还在探索 hooks 的使用模式时，过早引入沙箱机制会增加配置复杂度、降低调试便利性、并可能因为沙箱限制过严而阻碍合法用途。当前的"信任用户配置"策略和 Claude Code 的整体安全哲学一致：用户显式配置的 hook 等同于用户在终端里输入的命令，其安全责任在用户侧。
>
> **迁移成本**：沙箱实现需要跨平台适配（macOS/Linux/Windows 三套隔离机制），估计 4-8 周的重度系统编程工作。更大的挑战是长期维护——操作系统的沙箱 API 经常变更（如 macOS 的 seatbelt deprecation），需要持续跟进。

---

## 重设计 5：上下文压缩需要用户参与

现在的上下文压缩是完全自动的，用户看不见它何时发生、丢失了什么。

**更好的设计应该**：
- 在压缩发生前通知用户（就像 Notion AI 提示"这个对话太长了，我要压缩一下"）
- 让用户标记"这条信息很重要，不要压缩"
- 压缩后显示"摘要是什么"（不仅仅是隐式的继续）

SessionMemory 是一个部分解决方案，但它的 9 个固定节结构不一定能捕捉所有用户认为重要的信息。

> ⚠️ **这是 UX 改进，不是架构重设计**
>
> 需要诚实承认：上面提到的"压缩前通知""让用户标记重要信息""显示摘要"，本质上是产品经理的需求描述，不是架构级别的重设计。如果要把它提升到架构层面，真正需要回答的问题是：**如何设计一个支持"选择性压缩"的上下文管理架构？** 比如分层的 context ring buffer——热数据在 L1（完整保留）、温数据在 L2（摘要）、冷数据在 L3（仅关键词索引）。用户的"重要"标记相当于把特定信息钉在 L1 层。但这又引出新问题：如果用户标记了 80% 的内容为"重要"，压缩还有意义吗？

> **为什么 Claude Code 现在没这么做？** 上下文压缩的自动化是有意为之的设计决策——它降低了用户的认知负担。大多数用户不想（也不应该需要）理解 token 预算和上下文窗口的技术细节。每次压缩前弹出通知会打断工作流，让用户做一个他们可能没有足够信息来做好的决策（"这段对话的哪些部分对后续任务最重要？"）。Claude Code 选择让 SessionMemory 自动提取关键信息，是在"用户控制"和"用户体验"之间做了偏向后者的权衡。
>
> **迁移成本**：UI 层面的通知和标记功能相对轻量（1-2 周）。但底层的选择性压缩架构（分层 context ring buffer）需要重新设计整个上下文管理管线，涉及 prompt 构造、token 计算、cache 策略等多个模块的联动改造，估计 6-10 周。

---

## 总结：好的设计决策和可以更好的

**值得保留和借鉴的**：
- 工具接口（`Tool.ts`）的设计清晰、可扩展
- 权限系统的多层防线和 fail-closed 原则
- `querySource` 区分不同 AI 实例的追踪机制
- Hooks 系统的事件覆盖广度

**值得改进的**：
- 让 token 预算成为可见的、可推理的系统约束
- 让 AI 协作图成为显式的运行时模型
- 给 Hooks 添加能力声明和沙箱
- 上下文压缩需要更多的用户参与

这些改进不是否定当前设计，而是在当前设计的基础上，让系统的约束和行为更加显式、可观察、可推理。需要诚实承认的是：以上每个"重设计"都带有事后诸葛的成分。Claude Code 团队在快速迭代的压力下做出了务实的选择——先让系统跑起来、服务好用户，再逐步优化架构。这是工程现实中的合理策略。我们提出的这些改进方向，有些（如类型化 token 预算）在现有 TypeScript 类型系统下实现难度不小，有些（如 Agent DAG 可视化）需要额外的运行时开销。理想设计和工程现实之间的张力，本身就是软件工程中最核心的课题之一。

---

## 代码落点

本章讨论的重设计目标对应的现有实现：

- `src/services/api/claude.ts` — 重设计 1（token 预算显式化）：当前 token 预算计算分散在此文件的多个函数中
- `src/utils/forkedAgent.ts` — 重设计 2（Agent 协作图）：当前子 Agent 关系是隐式的，缺乏运行时拓扑模型
- `src/hooks/` — 重设计 3（Hook 沙箱）：当前 Hook 执行无能力声明、无沙箱隔离
- `src/Tool.ts` — 值得保留的设计：工具接口清晰、可扩展，是架构中最成功的抽象之一

---

## 超越视角：从 Prompt Engineering 到 AI 行为工程

通读 Claude Code 的全部提示词模板（本书 Part 2「Prompt 原文集」章节做了全量整理，涵盖 P001–P183 主编号 + P101a/P101b 子编号 + 6 个外部 .txt 引用）之后，有一个更宏观的演变值得独立讨论：Anthropic 在 Claude Code 中实践的，已经不是通常意义上的"Prompt Engineering"（提示词工程），而是更系统化的东西——**AI 行为工程（AI Behavioral Engineering）**。

**传统 Prompt Engineering 的特征**：写一段话，测试效果，改一改，再测试。技术含量低，不可复现，强依赖个人直觉，没有回归机制。这是 2022-2023 年大多数团队的真实工作方式。

**Claude Code 源码揭示的新范式**，体现在四个维度：

**1. 反模式的穷举式枚举（anti-pattern enumeration）**

以验证 Agent 的系统提示词为例，它不是告诉 AI"要谨慎"，而是穷举列出所有观察到的失败模式，配上具体示例和反例。这种"负面知识库"的构建方式更接近**安全工程**（威胁建模）的思维，而非写作的思维——先把所有可能出错的地方列出来，再逐一防御。

**2. 人格设计（persona design）**

探索 Agent 和规划 Agent 的 system prompt 不只是功能描述——它们构造了一个完整的**认知框架**：这个 AI 的价值观是什么、它面对模糊性时应该有什么默认倾向、什么情况下应该停止执行而非继续推进。这是人格心理学的方法论被应用到 AI 行为设计上。

**3. 认知科学映射（cognitive science mapping）**

Dream 系统的提示词（autoDream/promptTemplates.ts）使用了神经科学隐喻来描述记忆整合过程——不只是功能规范，而是构建了一个关于"AI 应该如何模拟人类记忆整合"的认知模型。将认知科学概念映射到 AI 行为设计，是跨学科的、有意为之的方法论选择。

**4. 量化验证（quantitative validation）**

如前一章所述，`src/memdir/memoryTypes.ts` 中的 eval 注释（`H1: 0/2 → 3/3`、`H2: 0/2 → 3/3` 等）表明 Anthropic 对每次提示词改动都进行量化评估。这是把科学实验方法引入了软件工程——一个在传统软件开发中几乎不存在的实践（你不会在普通函数的注释里写"此算法在 benchmark 测试集上 F1 从 0.67 提升到了 0.91"）。

**这个转变意味着什么**

| 维度 | Prompt Engineering（旧） | AI Behavioral Engineering（新） |
|------|------------------------|-------------------------------|
| 知识形态 | 隐性的个人技巧 | 显式的可编码规则 |
| 验证方式 | 主观感受 | eval 评估集量化 |
| 设计方法 | 由内而外（"我觉得这样好"） | 由外而内（先定义失败模式，再防御） |
| 跨学科借鉴 | 几乎没有 | 安全工程、认知科学、人格心理学 |
| 迭代记录 | 无 | 注释中保留历史准确率和已知失败 |

这个演变不只是"更专业的 Prompt Engineering"——它代表了一种工程文化的转变：**从"我写了段话，感觉很好"到"我有 12 个 eval 用例，改前 3/12 通过，改后 11/12 通过，剩余 1 个失败案例已记录在 Known gap 中"**。

对于正在构建 AI 产品的团队，这个转变有直接的实践意义：不是"雇一个会写 prompt 的人"，而是**建立一个 eval 驱动的提示词迭代机制**——哪怕从 5 个测试用例开始，也比零强。Claude Code 源码展示的，是这个机制在生产规模下的完整形态。

> 💡 **通俗理解**：传统 Prompt Engineering 就像**凭经验调味的厨师**——炒一锅菜，尝一口，"嗯，再加点盐"。AI 行为工程更像**研发部门做产品配方**——设计 20 个口味测试员、记录每次配方调整前后的打分、在配方卡上注明"第 3 号配料从 5g 改到 7g，盲测平均分从 6.2 提升到 8.1，但对坚果过敏人群（已知缺口）仍需改进"。两者都是在"调味"，但一个是手艺，一个是科学。
