# Claude Code 工作原理科普：一段 Prompt 的奇幻漂流

> 写给计算机初学者的 Agent 工作流入门指南
> 本文基于 `claude-code` 源码（约 2026 年 3 月版本）的实际实现写成，所有关键结论都标注了源码位置，方便有兴趣的读者顺藤摸瓜。

---

## 目录

1. [先搞懂概念：什么是 AI Agent？](#一先搞懂概念什么是-ai-agent)
2. [全景图：Claude Code 的五层架构](#二全景图claude-code-的五层架构)
3. [旅程开始：启动时软件做了什么](#三旅程开始启动时软件做了什么)
4. [按下回车的那一刻：输入的分流之旅](#四按下回车的那一刻输入的分流之旅)
5. [打包行李：发往 AI 的请求是怎么拼出来的](#五打包行李发往-ai-的请求是怎么拼出来的)
6. [心脏地带：Agent 主循环（最重要的一章）](#六心脏地带agent-主循环最重要的一章)
7. [AI 的"手"：工具系统与权限闸门](#七ai-的手工具系统与权限闸门)
8. [记忆与遗忘：上下文压缩](#八记忆与遗忘上下文压缩)
9. [分身术与急刹车：子 Agent 与中断机制](#九分身术与急刹车子-agent-与中断机制)
10. [总流程图：一图看完全程](#十总流程图一图看完全程)
11. [附录：源码导览索引](#附录源码导览索引)

---

## 一、先搞懂概念：什么是 AI Agent？

### 1.1 大模型本身只会"说话"

我们先从一个常见的误解说起。很多人以为 ChatGPT、Claude 这类 AI 是"什么都会做的智能程序"。其实**大语言模型（LLM）本身只会做一件事：读一段文字，然后接着往下写文字**。

你可以把它想象成一个被关在房间里的超级博学的人：

- 他读过全世界的书，知识渊博；
- 但他**没有手、没有眼睛、没有电话**，不能碰任何东西；
- 你和它交流的唯一方式，是从门缝里递纸条进去，他写张纸条递出来。

所以，光有大模型，它是**没法帮你"改代码文件"的**——它只能告诉你"你应该把第 3 行改成 xxx"，改文件这个动作它自己做不了。

### 1.2 Agent = 大脑 + 手 + 循环

**Agent（智能体）** 就是给这个"房间里的人"配上手和眼睛的一套软件系统：

| 比喻 | 在 Claude Code 里对应什么 |
|---|---|
| 🧠 大脑 | Claude 大模型（通过 API 远程调用） |
| ✋ 手 | **工具（Tools）**：读写文件、执行命令、搜索网页…… |
| 👀 眼睛 | 工具执行后返回的结果（文件内容、命令输出） |
| 🔁 神经系统 | **主循环**：把结果喂回给大脑，让大脑决定下一步 |
| 🛡️ 保安 | 权限系统：危险动作要先征得你同意 |

Agent 的核心思想一句话就能说完：

> **模型说话 → 软件替它动手 → 把动手的结果再告诉模型 → 模型继续说 → 直到事情办完。**

这个"说话 → 动手 → 反馈"的循环，就是所谓的 **Agent 工作流（Agentic Loop）**。Claude Code 整个软件，本质上就是围绕这个循环造的一台精密机器。

### 1.3 一个最小例子

假设你对 Claude Code 说："*看看 src 目录里有什么文件，然后告诉我哪个文件最大*"。软件内部实际发生的对话是这样的（简化版）：

```mermaid
sequenceDiagram
    participant 你
    participant 软件 as Claude Code 软件
    participant 模型 as Claude 大模型

    你->>软件: 看看 src 里有什么文件，哪个最大？
    软件->>模型: 用户问：src 里有什么文件……
    模型-->>软件: 我需要用 Bash 工具执行 `ls src`
    软件->>软件: 真的去执行 ls src（动手！）
    软件->>模型: 工具结果：a.ts b.ts c.ts
    模型-->>软件: 我需要用 Bash 工具执行 `du -h src/*`
    软件->>软件: 执行 du -h src/*
    软件->>模型: 工具结果：a.ts 2K, b.ts 8K, c.ts 1K
    模型-->>软件: 不需要工具了。最终回答：b.ts 最大，8KB
    软件->>你: 📄 b.ts 最大（8KB）
```

注意看：模型说了三次话，前两次都是"我要用工具"，第三次才是真正的答案。**软件在中间替它动了两次手**。这就是 Agent 的全部秘密。剩下的所有复杂性，都是为了让这个循环更安全、更快、更省 token。

---

## 二、全景图：Claude Code 的五层架构

在深入细节之前，先建立一张地图。Claude Code 从外到内可以分成五层：

```mermaid
flowchart TB
    subgraph L1["🖥️ 第 1 层：界面层（终端 UI）"]
        UI["输入框 / 文本渲染 / 权限弹窗<br/><i>src/components · src/screens · src/ink</i>"]
    end
    subgraph L2["🚦 第 2 层：输入分流层"]
        ROUTER["判断输入是命令 / shell / 普通提问<br/><i>src/utils/handlePromptSubmit.ts · processUserInput.ts</i>"]
    end
    subgraph L3["📦 第 3 层：上下文组装层"]
        CTX["系统提示词 + 记忆文件 + git 状态<br/><i>src/constants/prompts.ts · src/context.ts</i>"]
    end
    subgraph L4["🔁 第 4 层：Agent 主循环（心脏）"]
        LOOP["请求模型 → 执行工具 → 追加结果 → 再请求<br/><i>src/query.ts · src/services/tools/</i>"]
    end
    subgraph L5["🌐 第 5 层：API 通信层"]
        API["流式 HTTP 请求 / 重试 / 中断<br/><i>src/services/api/claude.ts</i>"]
    end

    UI --> ROUTER --> CTX --> LOOP --> API
    LOOP -.->|调用| TOOLS["🔧 工具箱：Bash / Read / Write / Edit / Grep ...<br/><i>src/tools/</i>"]
    API -.->|HTTPS| CLAUDE["☁️ Anthropic 服务器上的 Claude 模型"]
```

- **第 1 层（界面）**：你在终端里看到的输入框、打字效果、"是否允许执行此命令？"的弹窗，都是用 React 渲染在终端里的（一个叫 Ink 的库，魔改版在 `src/ink/`）。
- **第 2 层（分流）**：你敲回车后，软件先判断这段话是发给 AI 的，还是本地命令（比如 `/help` 这种斜杠命令、`!ls` 这种直接跑 shell）。
- **第 3 层（打包）**：确认要发给 AI 后，把系统提示词、你的记忆文件（CLAUDE.md）、git 状态等"背景资料"打包。
- **第 4 层（循环）**：真正的心脏——反复请求模型、执行工具、喂回结果。
- **第 5 层（通信）**：负责和 Anthropic 的服务器说话，流式传输、断线重试、随时可中断。

接下来，我们跟着一段 prompt 从出生到答案诞生，一层一层走一遍。

---

## 三、旅程开始：启动时软件做了什么

你在终端敲下 `claude` 命令、按下回车，到输入框出现之间，软件其实悄悄做了很多事：

```mermaid
flowchart LR
    A["敲下 claude 命令"] --> B["分拣员 cli.tsx<br/>特殊参数走快速通道"]
    B --> C["主程序 main.tsx<br/>解析命令行参数"]
    C --> D["全局初始化 init.ts<br/>读配置/预连服务器"]
    D --> E["会话初始化 setup.ts<br/>确定工作目录/加载命令表"]
    E --> F{"目录信任对话框<br/>❓信任此文件夹吗？"}
    F -->|信任| G["渲染交互界面<br/>输入框出现！"]
    F -->|拒绝| H["退出"]
```

（源码位置：`src/entrypoints/cli.tsx` → `src/main.tsx` → `src/entrypoints/init.ts` → `src/setup.ts` → `src/replLauncher.tsx`）

挑几件有意思的事说说：

1. **"分拣员"设计**：程序的入口 `cli.tsx` 像一个快递分拣员。如果你输入的是 `claude --version` 这种简单请求，它不会加载整个庞大的对话系统，打印个版本号就退出了——所以 `--version` 响应飞快。
2. **偷偷提前握手**：初始化时软件会提前和 Anthropic 服务器做一次网络握手（`preconnectAnthropicApi()`），就像打电话先拨号放着，等你真的说话时不用等拨号音，省下 100~200 毫秒。
3. **信任对话框是安全边界**：第一次在某个文件夹里运行，它会问"你信任这个目录吗？"。这是有讲究的——项目目录里可能藏着别人写的配置文件（hooks、CLAUDE.md），恶意项目可能借此让你执行危险命令。你点"信任"之后，这些配置才会生效。
4. **预热上下文**：信任通过后，软件立刻**在后台**去读 git 状态和你的 CLAUDE.md 记忆文件，而不是等你问第一个问题时才现读。所以你第一个问题的响应会更快。

---

## 四、按下回车的那一刻：输入的分流之旅

现在输入框出现了。你打了一段字，比如 `帮我看看这个函数有没有 bug`，然后按下回车。

### 4.1 从按键到文本

你在键盘上敲的每个键，都要经历一段旅程（源码：`src/ink/components/App.tsx` → `src/hooks/useTextInput.ts`）：

1. 终端把你的按键变成原始字节流；
2. 软件用一个状态机把字节解析成"结构化按键"（识别出这是字母、那是方向键、那是回车）；
3. 每个按键送进一个"文本状态机"，更新输入框里显示的内容和光标位置；
4. 回车键到达时，调用 `onSubmit(你输入的完整文本)`——**你的文本正式离开输入框**。

### 4.2 分拣台：这段话是发给谁的？

回车之后，软件**不会**立刻把文本发给 AI。它先在一个"分拣台"上做判断（源码：`src/screens/REPL.tsx` 的 `onSubmit` → `src/utils/handlePromptSubmit.ts` → `src/utils/processUserInput/processUserInput.ts`）：

```mermaid
flowchart TD
    INPUT["你敲下的文本"] --> Q1{"是空白的吗？"}
    Q1 -->|是| DROP1["🗑️ 丢弃"]
    Q1 -->|否| Q2{"是 exit/quit 吗？"}
    Q2 -->|是| EXIT["本地执行退出"]
    Q2 -->|否| Q3{"以 ! 开头？<br/>（如 !ls -la）"}
    Q3 -->|是| BASH["💻 本地直接执行 shell<br/>结果显示给你看<br/><b>不发 AI</b>"]
    Q3 -->|否| Q4{"以 / 开头？<br/>（如 /help、/commit）"}
    Q4 -->|是| CMD{"查命令表，看命令类型"}
    CMD -->|本地型<br/>如 /clear、/config| LOCAL["⚙️ 本地执行<br/><b>不发 AI</b>"]
    CMD -->|提示词型<br/>如 /commit、各种技能| PROMPT["📝 把命令展开成一段提示词<br/><b>发给 AI</b>"]
    CMD -->|查无此命令| UNKNOWN["提示'未知命令'<br/><b>不发 AI</b>"]
    Q4 -->|否| Q5{"AI 正在忙吗？"}
    Q5 -->|在忙| QUEUE["📥 排队，等当前回合结束再处理"]
    Q5 -->|空闲| TEXT["✉️ 包装成'用户消息'<br/><b>发给 AI</b>"]
```

几个初学者容易困惑的点，这里一并解答：

- **`/help` 为什么不消耗 AI 额度？** 因为它是"本地型命令"，软件自己就知道怎么显示帮助，根本不用问模型。
- **`!ls` 是干嘛的？** 开头敲 `!` 进入 bash 模式，命令直接在你电脑上执行，输出只给你看，不发给模型。适合你自己想快速跑个命令的场景。
- **"AI 正在忙时我发的消息去哪了？"** 进了一个队列。等当前回合结束，队列里的消息会被取出来重新走一遍流程。
- **历史记录（按 ↑ 键调出以前输入）会发给 AI 吗？** 不会。那份历史存在本地 `~/.claude/history.jsonl`，只服务你的"上箭头召回"，和发给模型的对话历史是两码事。

另外，在真正发出之前，还有一道用户可以自己安装的"安检门"：**UserPromptSubmit 钩子（hook）**。如果你在配置里写了钩子脚本，它可以在这里拦截你的输入、改写它、或者追加额外资料。

### 4.3 附件：@文件 是怎么塞进去的

如果你输入里写了 `@src/main.ts`，或者你的 IDE 里有选中的代码，这些会在这个阶段被收集成"附件消息"，跟在你的问题后面一起发给模型（源码：`src/utils/attachments.ts`）。图片粘贴也是在这里被压缩、转成模型能读的图片块。

### 4.4 补充：分拣台的规则——纯字符串匹配，零 AI 参与

很多人以为分拣台会"智能理解"你的意图，其实它只用 **3 条写死的规则**，全部是最朴素的字符串比较：

**规则 1：看第一个字符**（`src/components/PromptInput/inputModes.ts:16`）

```ts
export function getModeFromInput(input: string): HistoryMode {
  if (input.startsWith('!')) {   // 首字符是 ! → bash 模式
    return 'bash'
  }
  return 'prompt'                // 否则 → 候选发给 AI
}
```

斜杠命令同理：`processUserInput.ts:536` 的 `inputString.startsWith('/')`。

**规则 2：退出词是一份写死的名单**（`src/utils/handlePromptSubmit.ts:196`）

```ts
if (['exit', 'quit', ':q', ':q!', ':wq', ':wq!'].includes(input.trim())) {
  // 改写成 /exit 命令，退出程序
}
```

注意是 `.includes()`——必须**精确等于**名单中的某个词。你说"帮我退出这个循环"这种自然语言完全不匹配，会照常发给 AI。（`:q`、`:wq` 是照顾 Vim 用户肌肉记忆的彩蛋。）

**规则 3：斜杠命令是查字典，也是精确匹配**（`src/commands.ts:688`）

```ts
export function findCommand(commandName: string, commands: Command[]) {
  return commands.find(
    cmd =>
      cmd.name === commandName ||              // 名字精确相等
      getCommandName(cmd) === commandName ||   // 或全名精确相等
      cmd.aliases?.includes(commandName),      // 或命中别名列表
  )
}
```

查不到就老老实实回 `Unknown command: xxx`，绝不会"猜"你是不是想输别的命令。

**为什么要设计得这么"笨"？** 这是刻意的工程取舍：

1. **确定性**：你的输入该不该发给 AI，涉及隐私和费用，必须 100% 可预测。若用"智能分类"判断，一旦误判，你的本地私语可能就被上传了；
2. **快**：字符串比较是纳秒级，零延迟、零 token 消耗；
3. **可调试**：规则写在代码里，行为完全可复现。

真正"理解"你输入的环节，发生在分拣**之后**——通过检查的文本才会交给大模型。分拣台只是个按章办事的门卫：不认识人，只看证件。

---

## 五、打包行李：发往 AI 的请求是怎么拼出来的

好，你的问题被确认"要发给 AI"了。接下来软件要打包一个巨大的请求。你以为发给 AI 的只有"帮我看看这个函数有没有 bug"这一句话？其实远远不止。每次请求都像寄出一个大包裹：

```mermaid
flowchart TB
    subgraph PACKAGE["📦 发往 Anthropic API 的一个完整请求"]
        direction TB
        SYS["1️⃣ 系统提示词（System Prompt）<br/>几千字的'员工手册'：你是谁、怎么做事、<br/>工具使用规范、语气要求、可用技能清单、环境信息"]
        USER_CTX["2️⃣ 用户上下文（伪装成第一条用户消息）<br/>你的 CLAUDE.md 记忆文件内容 + 今天的日期"]
        GIT["3️⃣ 系统上下文（追加在系统提示词末尾）<br/>当前 git 分支、未提交的改动、最近 5 条 commit"]
        HISTORY["4️⃣ 对话历史<br/>你之前说过的每句话 + AI 的每次回复 + 每次工具执行结果"]
        NEW["5️⃣ 你的新消息<br/>'帮我看看这个函数有没有 bug' + 附件"]
        TOOLS["6️⃣ 工具清单<br/>约 40 个工具的说明书：名字、用途、参数格式"]
    end
```

（源码：`src/constants/prompts.ts` 的 `getSystemPrompt`、`src/context.ts` 的 `getUserContext`/`getSystemContext`、`src/utils/api.ts` 的 `prependUserContext`/`appendSystemContext`、`src/services/api/claude.ts` 的请求组装）

逐件拆开看：

1. **系统提示词**：这是 Anthropic 写给模型的"员工手册"，每次请求都带。里面写着"你是 Claude Code，一个编程助手""修改文件前必须先读文件""回答要简洁"等几十条规范，还有当前可用的技能清单、你的操作系统/ shell / 模型版本等环境信息。
2. **CLAUDE.md 记忆文件**：软件会从你的项目目录一路向上找到根目录，收集所有 `CLAUDE.md`（还有 `~/.claude/CLAUDE.md` 全局记忆），拼成一段文字。它被包在一个 `<system-reminder>` 标签里，伪装成对话最早的一条"用户消息"发出去——模型看得到，界面上不显示。
3. **git 状态**：当前分支、有没有未提交的改动、最近 5 条 commit——让模型知道代码仓库的现状。
4. **对话历史**：之前每一轮的"你说的话 + AI 回复 + 工具结果"原封不动全部带上。**这是理解大模型的关键：模型本身没有记忆，所谓"它记得你说过的话"，其实是软件每轮都把全部聊天记录重新发了一遍。**
5. **你的新消息**：这次真正的问题，外加 `@文件` 附件。
6. **工具清单**：每个工具一份"说明书"（名字 + 用途描述 + 参数格式），模型就靠这个知道"世界上存在 Bash 这个工具，调用时要给一个 command 字符串"。

然后，软件通过 HTTPS 向 Anthropic 服务器发起一个**流式请求**——服务器不用等全文写完，写一点就传一点，所以你看到 AI 的回答是一个字一个字"打"出来的（源码：`src/services/api/claude.ts`，`messages.create({ stream: true })`）。

### 5.1 深入：CLAUDE.md 的收集规则（源码细节版）

CLAUDE.md 不是"只读一个文件"，而是一套有严格优先级和顺序的收集流程（源码：`src/utils/claudemd.ts` 的 `getMemoryFiles`，第 790 行起）。软件按下面的顺序**依次收集 5 个来源**，全部拼在一起：

```mermaid
flowchart TD
    A["开始收集记忆文件"] --> B["1️⃣ Managed（企业管控）<br/>/etc/claude-code/CLAUDE.md<br/>+ 管控版 rules 目录<br/><i>无条件最先加载，公司IT用来下发强制规范</i>"]
    B --> C["2️⃣ User（用户全局）<br/>~/.claude/CLAUDE.md<br/>+ ~/.claude/rules/*.md<br/><i>你个人对所有项目的偏好</i>"]
    C --> D["3️⃣ Project + Local（项目）<br/>从你的当前目录出发，<b>逐级向上走到根目录</b><br/>每一层都找这 4 个目标："]
    D --> E["&nbsp;&nbsp;&nbsp;· CLAUDE.md（项目规范，会提交进 git）<br/>&nbsp;&nbsp;&nbsp;· .claude/CLAUDE.md（同上，另一种放法）<br/>&nbsp;&nbsp;&nbsp;· .claude/rules/*.md（拆成多条的规则文件）<br/>&nbsp;&nbsp;&nbsp;· CLAUDE.local.md（你的私人项目笔记，不提交 git）"]
    E --> F["4️⃣ --add-dir 额外目录<br/><i>需环境变量开启，默认关闭</i>"]
    F --> G["5️⃣ 自动记忆 / 团队记忆<br/><i>实验特性，需开关</i>"]
    G --> H["全部拼成一大段文字"]
```

这套规则里有几个值得知道的细节：

1. **向上查找意味着"就近叠加"**：如果你在 `项目A/src/utils` 里启动，那么根目录、项目A、src、utils 每一层的 CLAUDE.md 都会被收集——越靠近你位置的文件越具体，全部生效、互不覆盖（模型自己判断哪条更适用）。
2. **每个文件都会带上"身份标签"**（`getClaudeMds`，第 1153 行）：拼进上下文时，每个文件的内容前面会标注它的路径和类型说明，比如 `Contents of /path/CLAUDE.md (project instructions, checked into the codebase):`——这样模型知道哪条是公司强制规范、哪条是你的私人笔记，会区别对待。
3. **@导入语法**：CLAUDE.md 里可以写 `@路径/另一个文件.md` 来导入别的文件（`processMemoryFile`，第 618 行）。软件会**递归展开**导入，但有安全限制：最多递归 5 层（`MAX_INCLUDE_DEPTH`）、只允许文本扩展名（防止把图片 PDF 塞进记忆）、项目级文件默认不允许导入项目目录**之外**的文件（除非你在信任对话框里批准过）——防止别人提交的恶意 CLAUDE.md 偷读你的私人文件。
4. **去重**：所有文件路径进一个 `processedPaths` 集合，同一文件（包括软链接指向的）只加载一次。
5. **单文件上限 40000 字符**（`MAX_MEMORY_CHARACTER_COUNT`，第 92 行），超长的会被标记出来。
6. **可以整体关停**：环境变量 `CLAUDE_CODE_DISABLE_CLAUDE_MDS` 或 `--bare` 模式可以跳过自动发现（`src/context.ts:165`）。

### 5.2 深入：上下文组装的"摆放位置"规则

收集齐材料后，每样材料放进请求里的**位置**是有讲究的（源码：`src/utils/api.ts`）：

| 材料 | 放在哪 | 源码 | 为什么这样放 |
|---|---|---|---|
| 员工手册（系统提示词） | 请求的 `system` 字段 | `getSystemPrompt` | API 规定的"最高指令"位置 |
| git 状态 | **追加在系统提示词末尾**，形如 `gitStatus: <内容>` | `appendSystemContext`（api.ts:437） | 属于"环境状况"，和指令放一起 |
| CLAUDE.md + 今天日期 | **伪装成对话最早的一条 user 消息**，包在 `<system-reminder>` 标签里 | `prependUserContext`（api.ts:449） | 见下文 |

第三条值得展开。CLAUDE.md 实际上是以这样的形式发出的：

```
<system-reminder>
As you answer the user's questions, you can use the following context:
# claudeMd
（所有记忆文件的内容）
# currentDate
Today's date is 2026-07-23.

IMPORTANT: this context may or may not be relevant to your tasks.
You should not respond to this context unless it is highly relevant to your task.
</system-reminder>
```

三个设计巧思：

1. **`isMeta: true` 标记**——这条消息模型看得见，但**不会显示在你的终端界面上**，所以你不会觉得对话框里多了一段奇怪的话；
2. **`IMPORTANT: ... may or may not be relevant`**——明确告诉模型"这是背景资料，不是用户的提问"，防止模型把 CLAUDE.md 当成需要回答的问题；
3. **缓存友好**：git 状态和 CLAUDE.md 在一次会话中**只读取一次**（`memoize` 缓存，`src/context.ts:116/155`），每轮请求重复发送相同的内容——这恰好能命中 API 的 prompt cache，省钱省时。这也是为什么要"预热"：启动时就在后台提前读好（见第三章）。

**一句话总结这套规则**：*管控优先、全局其次、项目逐层叠加；导入可递归但有深度和权限闸门；组装时各就各位，指令进 system、状态跟在指令后、记忆伪装成首条用户消息。*

---

## 六、心脏地带：Agent 主循环（最重要的一章）

这一章是整个软件的灵魂。如果你能理解这一章，就理解了所有 AI Agent（不止 Claude Code）的工作方式。

### 6.1 循环长什么样

源码在 `src/query.ts` 的 `queryLoop` 函数，核心结构出奇的简单——就是一个 `while (true)` 死循环：

```mermaid
flowchart TD
    START(["带着打包好的请求进入循环"]) --> COMPACT["① 检查对话历史会不会太长<br/>太长就先压缩（第八章细讲）"]
    COMPACT --> CALL["② 向模型发起流式请求<br/><i>src/services/api/claude.ts</i>"]
    CALL --> COLLECT["③ 边接收边收集：<br/>模型这次回复里有没有'我要用工具'（tool_use）？"]
    COLLECT --> CHECK{"有没有<br/>工具调用？"}
    CHECK -->|"没有（模型直接给了答案）"| DONE["🎉 本回合结束<br/>把答案显示给用户"]
    CHECK -->|"有（比如：我要用 Bash 跑 ls）"| EXEC["④ 执行工具（第七章细讲）<br/>权限检查 → 真的执行 → 拿到结果"]
    EXEC --> APPEND["⑤ 把'AI 的回复 + 工具执行结果'<br/>追加到对话历史末尾"]
    APPEND --> LOOP(["回到 ①，带着更长的历史再请求一次模型"])
    LOOP -.-> COMPACT
```

用一句话概括：

> **只要模型还想用工具，循环就不停；模型什么时候不用工具了，答案就出来了。**

### 6.2 一个真实的循环轨迹

还是用"看看 src 里哪个文件最大"的例子，循环实际转了 **3 圈**：

| 圈数 | 发给模型的历史 | 模型的回复 | 软件的动作 |
|---|---|---|---|
| 第 1 圈 | 你的问题 | "我要用 Bash 跑 `ls src`" | 执行 `ls`，把输出追加进历史 |
| 第 2 圈 | 问题 + ls 请求 + ls 结果 | "我要用 Bash 跑 `du -h src/*`" | 执行 `du`，把输出追加进历史 |
| 第 3 圈 | 问题 + 两轮工具对话 | "b.ts 最大（8KB）"——没有工具调用 | **循环结束**，显示答案 |

注意历史是**越滚越长的**：第 3 圈请求里包含了前两圈的全部对话。这就是为什么长对话会越来越慢、越来越贵，也是为什么需要后面要讲的"压缩"。

### 6.3 模型"想用工具"这件事，在数据里长什么样？

模型不会真的打电话，它只是在回复的文字流里生成一个特殊结构（叫 `tool_use` 块），大致是：

```json
{ "type": "tool_use", "name": "Bash", "input": { "command": "ls src" } }
```

软件收到这个块，就知道"模型要动手了"，于是替它执行。执行结果再包成另一个特殊结构（`tool_result` 块）发回去：

```json
{ "type": "tool_result", "content": "a.ts\nb.ts\nc.ts" }
```

整个 Agent 循环，说穿了就是软件在当这个"翻译官 + 跑腿"：**把模型的"文字意图"翻译成真实操作，再把真实操作的结果翻译回文字。**

### 6.4 一个精巧的优化：边收边干活

模型生成回复是一个字一个字流式传过来的。Claude Code 有个聪明的设计（`StreamingToolExecutor`）：**工具调用的参数一旦收齐，不等模型把后面的话说完，就立刻开始执行工具**。比如模型前半段是"我要跑 `npm test`"，后半段的解释文字还在传输中，`npm test` 已经在你的电脑上跑起来了。等模型说完，测试结果可能都已经出来一半了。

还有些执行纪律：

- **只读工具（如读文件、搜索）可以并行跑**（最多 10 个一起）；
- **写操作（改文件、跑命令）严格排队一个一个来**，避免互相踩脚；
- 如果一条 Bash 命令失败了，会**取消同批的其他命令**——因为命令之间往往有依赖（`mkdir` 都失败了，后面往里面写文件也没意义）。

### 6.5 循环什么时候会停？

除了"模型不再调工具"这个正常出口，循环还有很多紧急出口（源码里叫 `Terminal` 状态）：

| 停止原因 | 什么时候发生 |
|---|---|
| ✅ `completed` | 模型正常说完，没有工具调用 |
| 🛑 用户按 ESC | 随时可中断（第九章细讲） |
| 📏 对话太长且压缩失败 | 上下文窗口实在装不下了 |
| 🔁 达到最大轮数 | SDK 调用时设置的 `maxTurns` 上限 |
| 💥 API 出错 | 网络故障等，重试也失败 |
| 🪝 钩子阻止 | 你配置的 Stop hook 说"停" |

---

## 七、AI 的"手"：工具系统与权限闸门

### 7.1 工具箱里有什么

模型能用的每个能力都是一个"工具（Tool）"对象（定义在 `src/Tool.ts`），约 40 个内置工具（`src/tools/`）。最常用的：

| 工具 | 干什么 | 危险程度 |
|---|---|---|
| `Read` | 读文件内容 | 🟢 安全 |
| `Glob` / `Grep` | 按文件名 / 内容搜索 | 🟢 安全 |
| `Edit` / `Write` | 修改 / 创建文件 | 🟡 会改你的文件 |
| `Bash` | 执行任意终端命令 | 🔴 理论上什么都能干 |
| `WebFetch` / `WebSearch` | 抓网页 / 联网搜索 | 🟢 基本安全 |
| `Agent` | 派一个"分身"子 agent 去干活 | 🟡 |
| `TodoWrite` | 维护待办清单（模型自己列计划用的） | 🟢 |

每个工具由几部分组成（一个生动的对应关系）：

- **名字 + 说明书**（`name`、`prompt`）：写给模型看，告诉它什么时候该用、怎么用；
- **参数格式**（`inputSchema`，用 Zod 定义）：规定调用时必须给什么参数，比如 Bash 必须给 `command` 字符串。模型乱给参数会被直接打回；
- **执行体**（`call`）：真正干活的函数；
- **权限检查**（`checkPermissions`）：这个工具自己判断"这个具体操作危不危险"；
- **界面渲染**：在终端里怎么显示这个工具的执行过程（比如 Bash 显示 `$ 命令`，Edit 显示代码 diff）。

### 7.2 从"模型想用 Bash"到"真的执行"的完整链路

这是最精彩的一条链路，我们走一遍（源码：`src/services/tools/toolExecution.ts` 的 `runToolUse` → `checkPermissionsAndCallTool`）：

```mermaid
flowchart TD
    A["模型说：我要用 Bash 跑<br/>'rm -rf node_modules'"] --> B["1. 按名字找到 Bash 工具<br/><i>找不到？→ 把错误回给模型让它自我纠正</i>"]
    B --> C["2. Zod 校验参数格式<br/><i>command 是字符串吗？</i>"]
    C --> D["3. PreToolUse 钩子<br/><i>用户配置的脚本可拦截/改写命令</i>"]
    D --> E{"4. 权限裁决<br/><i>src/utils/permissions/permissions.ts</i>"}
    E -->|命中'禁止规则'| F["❌ 拒绝<br/>把原因告诉模型"]
    E -->|命中'始终允许'规则<br/>（如你之前点过'始终允许 npm *'）| G["✅ 直接执行"]
    E -->|默认情况| H["❓ 弹窗问你：<br/>允许一次 / 始终允许 / 拒绝"]
    H -->|你选了允许| G
    H -->|你选了拒绝| F
    G --> I["5. 真正执行：<br/>起 shell 进程，收集输出"]
    I --> J["6. PostToolUse 钩子<br/>可对结果追加反馈"]
    J --> K["7. 结果包成 tool_result 消息<br/>追加进对话历史"]
    K --> L["8. 主循环带着新历史<br/>再次请求模型"]
```

这个设计有几个精妙之处，值得初学者体会：

1. **模型犯错不会搞崩系统**。模型瞎编工具名、给错参数，软件不会崩溃，而是把"你错了，错在 xxx"作为工具结果回喂给模型——模型读到后通常会自我纠正，换正确的姿势再来一次。
2. **权限是一道真正的闸门，不是提示词**。模型没有任何办法绕过权限系统直接执行命令——它只能"提出申请"，批不批在软件（和你）。这就是为什么 Claude Code 敢把 Bash 这种大杀器交给模型。
3. **"始终允许"会记住**。你在弹窗里选"始终允许 `git *`"，这条规则会写进配置文件，以后 git 开头的命令就不再烦你。
4. **连弹窗都能智能化**。有些场景下软件会让一个小模型先给命令"预判危险等级"，高置信度安全的直接放行，减少打扰。

### 7.3 技能（Skill）和斜杠命令：教 AI 新把戏

- **斜杠命令**（`src/commands/`，约 100 个）：`/commit`（自动生成提交信息）、`/review`（代码审查）……本质是"预制提示词模板"或本地功能。
- **技能**（`src/skills/`）：一个 `SKILL.md` 文件就是一个技能，里面写着"什么时候用我、怎么用我"。软件只在对话里注入一份技能**清单**（省 token），模型决定要用某个技能时，才通过 `Skill` 工具把完整说明书加载进来。这种设计叫**渐进披露**——就像书架上只放书名，用到哪本抽哪本。

---

## 八、记忆与遗忘：上下文压缩

### 8.1 为什么需要压缩

前面说过：**每轮请求都要带上全部历史**。而模型的"脑容量"（上下文窗口）是有限的，比如 20 万 token（大约 15 万汉字）。一次大型重构，对话里会塞满几百个文件的内容、无数的命令输出——很快就会撞到天花板。

而且就算撞不到，历史越长：响应越慢、费用越高、模型还容易被无关的旧信息干扰。

### 8.2 多层防线

Claude Code 在**每一圈循环的开头**都会做一轮"瘦身检查"（源码：`src/query.ts` 开头部分 + `src/services/compact/`）：

```mermaid
flowchart LR
    A["每圈循环开头"] --> B["① 超大工具结果<br/>落盘存文件，历史里只留'摘要+路径'"]
    B --> C["② 清理陈旧的旧工具结果<br/><i>microcompact</i>"]
    C --> D{"③ token 数接近上限？<br/>（上限 − 13000 的警戒线）"}
    D -->|还没| E["✅ 正常请求模型"]
    D -->|接近| F["④ 自动压缩 autocompact<br/>见下文"]
    F --> E
```

### 8.3 自动压缩是怎么做的？——让 AI 自己写读书笔记

最有意思的是压缩的方式：**不是简单粗暴地砍掉前半截，而是派一个"一次性分身"，把整段对话读一遍，写一份摘要**（源码：`src/services/compact/compact.ts`，内部复用了同一个 query 循环，只跑一轮）。

压缩后的对话历史变成：

```
[摘要消息：之前对话的要点总结] + [一道"压缩边界"标记] + [边界之后的原始对话]
```

从此之后，模型只能看到边界**之后**的内容加那份摘要——就像你考试前把整本书浓缩成一页笔记，然后只看笔记和最后几章。

为了防止压缩本身也失败（极端情况），还有熔断器：连续压缩失败 3 次就放弃并报错，不会无限烧钱重试。

### 8.4 被动救场

如果预防没拦住、API 真的返回了"内容太长"错误（413），软件还有**被动压缩**：悄悄把错误藏起来（不让你看到吓人的报错），紧急压缩一次再重发请求。只有所有手段都失败了，才会告诉你"对话太长了，请用 `/compact` 手动压缩或开新对话"。

---

## 九、分身术与急刹车：子 Agent 与中断机制

### 9.1 子 Agent：一个循环，递归复用

当任务很大时，模型可以调用 `Agent` 工具派一个"分身"去干子任务（比如"你去把整个代码库搜一遍，找出所有用到了这个函数的地方"）。

架构上有个非常优雅的点（源码：`src/tools/AgentTool/runAgent.ts`）：**子 agent 跑的就是主循环本身**——同一个 `query()` 函数，只是换了一份自己的系统提示词、一个缩水的工具箱、一个身份标记。主 agent、子 agent、写摘要的分身，全是同一台发动机的复用。

子 agent 有两种跑法：

```mermaid
flowchart TD
    A["模型调用 Agent 工具"] --> B{"前台还是后台？"}
    B -->|前台（同步）| C["主循环<b>暂停等待</b><br/>子 agent 跑完后，结果作为工具结果回来"]
    B -->|后台（异步）| D["子 agent 独立运行<br/>主循环<b>立刻继续</b>干别的"]
    D --> E["子 agent 干完后<br/>往主循环的消息队列里塞一条'任务完成通知'<br/>主循环下一圈读到"]
```

为什么要分前后台？想象你让助理去查资料：前台模式是你坐在那等他回来；后台模式是你继续干自己的事，他查完了发微信告诉你。

### 9.2 ESC：随时拉下的急刹车

你随时可按 ESC 中断 AI。这个"急刹车"做得很彻底（源码：`src/hooks/useCancelRequest.ts`）：

1. 中断信号一路传到网络层，**HTTP 请求被真正掐断**——不是"装没看见结果"，是连接真的断了，立刻停止计费；
2. 循环里有两个检查点：流式接收中途被断、或工具执行中途被断，都会优雅收尾；
3. 有个容易忽略但关键的细节：如果模型已经发出了工具调用请求但被中断，软件会**自动补一条"被用户打断"的假工具结果**。为什么必须补？因为 API 规定：每个"工具调用"必须配对一条"工具结果"，缺了下轮请求会直接报错。就像写信必须有回信，哪怕回信写的是"对方不想聊了"。

还有层级之分：前台子 agent 和主 agent 共享同一个刹车（ESC 一起停）；**后台子 agent 有自己独立的刹车**——你按 ESC 停下当前的活，后台任务还活着（不然太反直觉了），想杀它要用专门的快捷键（Ctrl+X Ctrl+K）。

---

## 十、总流程图：一图看完全程

把前面所有章节串起来，这就是你发出一段 prompt 之后的完整旅程：

```mermaid
flowchart TD
    subgraph 你的电脑["💻 你的电脑（Claude Code 软件）"]
        A["⌨️ 你敲字，按下回车"] --> B["分流判断：<br/>/命令？!shell？普通提问？"]
        B -->|本地命令| C["⚙️ 本地执行，直接出结果"]
        B -->|普通提问| D["📦 打包请求：<br/>系统提示词 + CLAUDE.md + git状态<br/>+ 全部对话历史 + 工具清单"]
        D --> E{"🔁 主循环 while(true)"}
        E --> F["检查历史长度，必要时压缩"]
        F --> G["流式请求模型"]
        G --> H{"模型回复里有<br/>工具调用吗？"}
        H -->|有| I["权限检查 ❓<br/>（可能弹窗问你）"]
        I -->|被拒| J["把'用户拒绝了'<br/>回给模型"]
        I -->|放行| K["🔧 真的执行工具"]
        K --> L["结果追加进历史"]
        J --> L
        L --> E
        H -->|没有| M["🎉 显示最终答案"]
        N["ESC 急刹车"] -.随时.-> G
        N -.随时.-> K
    end
    G -.->|HTTPS 流式| O["☁️ Anthropic 服务器<br/>Claude 大模型"]
    O -.->|逐字返回| G
    P["🤖 子 agent<br/>（复用同一个循环）"] -.被 Agent 工具唤起.-> E
```

---

## 附录：源码导览索引

如果你想亲自翻源码，这里是本文提到的关键文件地图：

| 主题 | 文件 | 看什么 |
|---|---|---|
| 程序入口 | `src/entrypoints/cli.tsx` | "分拣员" main() |
| 主程序 | `src/main.tsx` | 命令行解析、启动流程 |
| 全局初始化 | `src/entrypoints/init.ts` | 配置、预连 API |
| 界面渲染 | `src/ink/`、`src/components/`、`src/screens/REPL.tsx` | 终端 UI、输入框 |
| 输入分流 | `src/utils/handlePromptSubmit.ts`、`src/utils/processUserInput/` | 命令/shell/提问的分发 |
| 上下文组装 | `src/constants/prompts.ts`、`src/context.ts` | 系统提示词、CLAUDE.md、git 状态 |
| **主循环（心脏）** | **`src/query.ts` 的 `queryLoop`** | `while (true)` agentic 循环 |
| API 通信 | `src/services/api/claude.ts` | 流式请求、SSE 解析 |
| 工具定义 | `src/Tool.ts`、`src/tools/` | 约 40 个工具 |
| 工具执行 | `src/services/tools/toolExecution.ts`、`toolOrchestration.ts` | 分发、串并行调度 |
| 权限系统 | `src/utils/permissions/permissions.ts` | deny/allow/ask 裁决 |
| 钩子系统 | `src/utils/hooks.ts`、`src/services/tools/toolHooks.ts` | 用户自定义脚本介入点 |
| 上下文压缩 | `src/services/compact/` | 自动/被动压缩 |
| 子 Agent | `src/tools/AgentTool/`、`src/Task.ts`、`src/tasks/` | 分身与后台任务 |
| 中断机制 | `src/hooks/useCancelRequest.ts`、`src/utils/abortController.ts` | ESC 急刹车 |

> **关于这份源码的小知识**：这是一份内部构建版本的 TypeScript 源码快照（带 sourcemap 还原），函数名基本保留，注释详尽。你可能会看到 `feature('XXX')` 这样的编译期开关和少量缺失文件，都属于构建系统的正常痕迹，不影响理解主流程。

---

## 结语：三句话带走

1. **大模型只会说话，Agent 软件是它的手和眼睛**——Claude Code 的全部工作就是"把模型的意图翻译成操作，把操作的结果翻译回文字"。
2. **心脏是一个 `while` 循环**：请求模型 → 有工具调用就执行并追加结果 → 再请求 → 直到模型不再要工具，答案就出来了。
3. **安全靠架构而非自觉**：权限闸门、参数校验、随时可断的急刹车，都是软件层面的硬约束，模型想做什么都得"申请批准"。

理解了这三点，你再看市面上任何 AI Agent 产品（Cursor、Devin、各种自动化助手），都会发现它们跳不出这个基本框架——差别只在工具箱的丰富程度、循环的精细程度、和安全机制的严格程度。
