导读
这份教程研究的不是“Claude Code 有哪些功能”,而是“它如何成为一个可长时运行、可调用工具、可恢复上下文、可拆分子任务的 Agent 系统”。
两条学习主线
主线一:Agent 如何被构造
一个 Agent 不等于一段 Prompt。它至少由身份、指令、上下文、可用工具、模型策略、权限边界和可变状态组成。前七章会把这些部分从外到内拆开。
主线二:Agent 如何运行
一次任务不等于一次 API 调用。用户输入会先被编译成模型上下文,模型可能返回工具意图,本地运行时执行后再把结果回填。期间还有取消、重试、降级、压缩和 Hook。第八到第十三章会说清这个状态机。
后续三组问题
- 能力从哪里来:Tools、ToolSearch、Skills、MCP、Plugins 和 Hooks 分别扮演什么角色?
- 能力如何受控:Plan Mode、工具可见性、参数权限、Sandbox 和 Worktree 如何层层收紧边界?
- 任务如何延续和拆分:Memory、压缩、Subagent、Fork、后台 Agent 和 Agent Team 各自解决什么问题?
每章怎样读
每章会尽量回答六个问题:为什么需要它?在哪一层?输入和输出是什么?什么时候发生?设计精妙在哪里?它不负责什么?
源码路径只放在章末作为证据,正文不进入函数和类。若某项能力受功能开关、平台或内部构建限制,会明确标记“条件启用”。
请始终区分两件事:源码存在某种能力,不等于它在每次运行中都会启用。工具还会经过平台、功能开关、Agent 类型和权限规则的再次筛选。
全书最核心的结论:Claude Code 是一个把本地环境、指令、历史、能力和策略编译成模型请求,再把模型返回的意图编译成受控本地操作的 Agent 运行时。
总体架构与四个运行平面
Claude Code 表面上是一个终端应用,架构上却更接近一个本地 Agent 运行时。它要同时协调模型、本地工具、安全策略、会话状态和持久化资料。
理解整个系统,最有效的方法不是记住源码目录,而是把它分成四个运行平面。
flowchart TB
U["用户与外部事件"] --> C["控制面<br/>模式、策略、会话、调度"]
C --> M["模型面<br/>Prompt 编译、API 请求、流式响应"]
M --> E["执行面<br/>工具、权限、Sandbox、Hook"]
E --> M
C <--> P["持久化面<br/>会话、Memory、任务、配置"]
E <--> P
M --> U
控制面:决定这一刻的 Agent 是谁
控制面不直接完成用户任务,而是决定任务应以什么方式运行。它维护:
- 当前是主 Agent、某种 Subagent,还是特殊协调器;
- 处于 Default、Plan、Accept Edits 还是其他权限模式;
- 使用哪个模型、思考策略和备用模型;
- 哪些工具应对当前 Agent 可见;
- 当前回合是继续、等待、压缩、恢复还是终止;
- 子 Agent、后台任务和团队消息如何调度。
可以把控制面理解为 Agent 的“操作系统内核”:它本身不写文件、不搜索代码,但决定谁可以在什么边界内做这些事。
模型面:把本地世界编译成请求
模型面负责两次编译:
- 请求编译:把系统指令、用户原话、会话历史、运行时附件和工具能力组装成 Messages API 请求。
- 响应编译:把模型返回的文本、思考、工具意图和停止原因,转换成本地运行时能处理的事件。
模型并不直接看到本地世界。它看到的只是被选中的 system、messages和 tools。本地上有一个文件,不等于该文件已经发给模型;只有它被指令加载、附件选中或工具读取后,内容才会进入请求。
执行面:把模型意图变成受控操作
模型返回 tool_use 只是表达意图。真正的本地动作由执行面完成:
- 查找对应工具并校验输入;
- 询问权限策略是允许、拒绝还是请求用户确认;
- 在需要时使用操作系统沙盒执行 Shell;
- 调用工具前后的 Hook;
- 产生进度、成功、失败或取消结果;
- 把结果封装成后续请求的
tool_result。
这里的重要边界是:模型提议动作,本地运行时决定动作能否发生。因此安全不是只靠 Prompt 中的“请勿”,还有模型之外的硬边界。
持久化面:保存运行所需的跨回合事实
持久化面不只是“保存聊天记录”。它还承载:
- 会话与转录;
- 用户、项目和本地配置;
- CLAUDE.md 与 Rules 这类指令文件;
- 自动 Memory、Agent Memory 和 Session Memory;
- Plan 文件、任务状态、后台 Agent 输出和团队邮箱;
- 插件、Skill、MCP 和 Hook 配置。
持久化的内容也不会全部常驻模型上下文。控制面在合适的生命周期节点选取它们,模型面再将被选中的部分编译进请求。
四个平面如何组成闭环
sequenceDiagram
participant U as 用户
participant C as 控制面
participant M as 模型面
participant E as 执行面
participant P as 持久化面
U->>C: 提交需求
C->>P: 加载会话、指令和配置
C->>M: 交付当前 Agent 策略
M->>M: 编译 system + messages + tools
M->>E: 返回工具意图
E->>E: 权限、Sandbox、Hook、执行
E->>P: 落盘状态或文件
E->>M: 回填 tool_result
M->>C: 结束或请求继续
C->>P: 保存会话状态
C->>U: 展示结果
这个闭环说明了一个精妙的架构选择:不让模型成为系统本身。模型是决策引擎,但上下文选择、工具执行、安全边界、持久化和恢复都留在本地运行时。这使同一个模型可在不同 Agent 类型中获得不同能力,也让模型失败时仍然可以重试、降级或恢复。
常见误解
- “终端 UI 就是主系统”:UI 只是一个入口和观察面,SDK、远程、非交互会话也可驱动同一类运行时。
- “模型知道项目里的所有事”:模型只知道本轮被编译进请求的内容。
- “工具调用就是远程执行”:工具意图先回到 Claude Code,真正执行主要发生在本地或已连接的 MCP 服务器。
- “有了 Sandbox 就不需要权限系统”:权限决定动作是否可以发起,Sandbox 限制已允许进程的实际能力。
源码定位
- 主回合与控制流:
src/query.ts、src/query/ - 模型 API 边界:
src/services/api/claude.ts - 工具集合与执行:
src/tools.ts、src/services/tools/ - 权限与沙盒:
src/utils/permissions/、src/utils/sandbox/ - 会话与状态:
src/state/、src/context.ts、src/services/compact/ - 持久记忆:
src/memdir/、src/services/SessionMemory/
Agent 的完整生命周期
一个 Claude Code Agent 不是每次请求都从零开始,也不是一次启动后就永久不变。它有两层生命周期:
- 会话生命周期:从创建或恢复会话,到退出、清理或转移。
- 回合生命周期:从收到一次用户输入,到这次任务的工具链和模型决策结束。
回合可以包含很多次 API 请求;会话又可以包含很多回合。
生命周期全景
stateDiagram-v2
[*] --> 启动
启动 --> 加载配置
加载配置 --> 发现能力
发现能力 --> 构造Agent
构造Agent --> 等待输入
等待输入 --> 编译上下文
编译上下文 --> 请求模型
请求模型 --> 执行工具: tool_use
执行工具 --> 编译上下文: tool_result
请求模型 --> 结束回合: 最终文本/停止
请求模型 --> 恢复: 错误/过长
恢复 --> 编译上下文: 重试/压缩/降级
结束回合 --> 保存状态
保存状态 --> 等待输入
等待输入 --> 退出
退出 --> 清理
清理 --> [*]
阶段一:启动与环境建模
运行时先确定“我在哪里运行”:当前工作目录、平台、Shell、Git 状态、终端形态、交互或非交互入口、用户身份与 API 提供方。
这个阶段的输出不是一个 API 请求,而是一组后续构造 Agent 所需的环境事实。其中有些会进入 system,有些只影响本地工具和权限。
阶段二:加载策略与发现能力
运行时接着合并多个范围的配置和策略:托管策略、用户配置、项目配置、本地配置、命令行参数和当前会话变更。
同时发现可用能力:
- 内置工具与平台工具;
- 已连接 MCP Server 的工具、资源和 Skill;
- 项目、用户、托管和插件提供的 Skills、Agents 和 Hooks;
- CLAUDE.md、Rules、Memory 和会话记录。
“被发现”只代表候选。它们还要通过功能开关、平台、权限、Agent 类型和信任边界的筛选。
阶段三:构造当前 Agent
环境和候选能力就绪后,运行时才能构造当前 Agent。构造结果包含:
- 身份与专用系统提示;
- 模型、思考方式和备用策略;
- 经过裁剪的工具池;
- 用户与项目指令;
- 权限模式与硬性策略;
- 会话历史、读取缓存、取消信号和任务状态等运行时状态。
这一阶段是全书的第一条主线,后面五章会继续拆解。
阶段四:进入一个用户回合
收到用户输入后,运行时不会立即把原文发给 API。它还要:
- 收集当前时刻需要附加的指令、记忆、模式和文件变化;
- 将附件转换为内部消息;
- 检查上下文预算,必要时先清理或压缩;
- 归一化角色顺序和工具配对;
- 重新完成工具池与权限状态的当前快照。
这是一个精妙的时序设计:动态事实不是在会话创建时一次性冻结,而是在每轮的正确时点重新编译。
阶段五:模型与工具循环
一次请求后,模型可能直接返回最终文本,也可能返回一个或多个工具调用。如果有工具,本地运行时会执行权限与工具流程,再把结果追加到历史中。
因此一个用户回合实际上是:
请求模型 → 解析意图 → 本地执行 → 回填结果 → 再请求模型。
它会循环到模型不再请求工具、用户中断、本地策略终止,或发生无法恢复的错误。
阶段六:恢复不是一条统一重试线
不同失败需要不同恢复:
- 短暂的传输或限流错误可等待后重试;
- 上下文过长需要清理、微压缩或完整压缩;
- 媒体过大需要处理特定内容,而不是盲目重发;
- 模型不可用时可切换备用模型;
- 工具失败通常作为结果返回模型,让模型决定下一步;
- 用户中断和 Hook 阻止代表控制决策,不应被自动重试抵消。
这种分流设计比“任何错误都再调一次 API”更稳健,也避免在确定性错误上重复消耗 Token。
阶段七:结束回合与保留连续性
当模型给出最终答复或运行时决定停止时,本轮的临时资源会被清理,但会话不一定结束。历史、任务、文件变更、Memory 和必要的会话笔记会为下一轮提供连续性。
对 Subagent 而言,结束时还要清理 Agent 局部的 Skill 调用状态、调试记录和独立取消资源,再把最终结果回传主 Agent。
三个容易混淆的“结束”
| 结束类型 | 结束的是什么 | 仍然保留什么 |
|---|---|---|
| 一次 API 请求结束 | 当前流式响应 | 当前用户回合可继续执行工具 |
| 一次用户回合结束 | 当前任务的工具循环 | 会话历史和持久状态 |
| 一个 Agent 结束 | 该 Agent 的独立查询链和局部资源 | 已落盘文件与回传结果 |
这个区分非常重要:“模型停止生成”不代表“本地任务已经完成”,“子 Agent 结束”也不代表“主会话结束”。
源码定位
- 启动与会话入口:
src/entrypoints/、src/bootstrap/ - 环境与用户上下文:
src/context.ts、src/utils/systemPrompt.ts - 主回合生命周期:
src/query.ts - 附件与消息归一化:
src/utils/attachments.ts、src/utils/messages.ts - 错误与压缩恢复:
src/query.ts、src/services/compact/ - Subagent 生命周期:
src/tools/AgentTool/、src/tasks/
Agent 构造公式
在 Claude Code 中,Agent 不是“一段不同的人设 Prompt”。一个可运行 Agent 至少是七个部分的组合:
Agent = 身份 + 指令 + 上下文 + 能力 + 模型策略 + 权限 + 可变状态
这七部分不会被拼成一个巨大字符串。它们分布在 API 请求、本地运行时和持久化系统中。
flowchart LR
I["身份<br/>主 Agent / Agent 类型"] --> A["Agent 实例"]
P["指令<br/>system / append / Skill"] --> A
C["上下文<br/>历史 / CLAUDE.md / 附件"] --> A
T["能力<br/>经裁剪的 tools"] --> A
M["模型策略<br/>模型 / thinking / fallback"] --> A
G["权限<br/>模式 / 规则 / Sandbox"] --> A
S["可变状态<br/>会话 / 取消 / 缓存 / 任务"] --> A
A --> R["system + messages + tools<br/>与本地执行上下文"]
身份:决定它为什么存在
身份先定义 Agent 的职责,再影响后面的指令和能力。
- 主 Agent 承担用户会话、最终决策和结果交付。
- Explore Agent 主要做只读检索和代码建模。
- Plan Agent 主要形成方案,不应拥有普通写入能力。
- 自定义 Agent 可以定义专用职责、指令、工具、模型、Skills、Hooks 和 MCP 需求。
- 特殊内部 Agent 可服务于压缩、验证、会话记忆等系统任务;它们可能受功能开关或构建类型限制。
身份不是界面标签,而是能力边界的输入。例如只读 Agent 不只会在 Prompt 中收到“不要写文件”,它的工具池也会直接移除写入能力。
指令:定义决策规则
指令包含不同生命周期的内容:
| 指令类型 | 典型内容 | 生命周期 |
|---|---|---|
| 基础 system | 身份、安全原则、任务方法、工具使用和表达风格 | Agent 构造时选择 |
| Agent 专用指令 | 某类 Agent 的职责和边界 | 该 Agent 生命周期 |
| 附加 system | 用户显式要求附加的高层指令 | 当前会话或 Agent |
| 项目指令 | CLAUDE.md、Rules | 随工作目录和文件范围变化 |
| 运行时提醒 | Plan Mode、Skill、Memory、Hook、任务状态 | 在特定回合附加 |
这些指令并不都进入顶层 system。动态指令往往通过 user 消息中的 <system-reminder> 进入正确时序位置。
上下文:定义它此刻知道什么
上下文与指令不同。指令规定“应该怎样做”,上下文提供“当前世界是什么样”。
上下文可以来自:
- 用户原话与会话历史;
- 工具返回的文件、命令、网页和 MCP 结果;
- 当前工作目录、Git、日期、模型与平台信息;
- 项目指令、Memory、已调用 Skill 和压缩摘要;
- 子 Agent、后台任务、Hook 和 IDE 产生的事件。
上下文必须被选择。把本地所有内容都塞给模型不但不可能,还会破坏时序、缓存和注意力分配。
能力:定义它真正能做什么
能力由当前工具池表达,而不只是由 Prompt 声称。但最终工具池并不是一条对所有来源都适用的简单交集,它有两条汇合路径:
- 基础装配池:内置工具和会话运行时已注册的 MCP 工具,按当前 worker 自己的权限模式重新装配,再经过平台、全局可见性、Agent 规则、前后台规则与延迟加载裁剪。普通 Subagent 不继承父 Agent 已裁剪后的最终数组;只有 Fork 精确复制父级工具快照。
- Agent 专属 MCP:Agent 自己声明的 MCP Server 在上述 Agent 裁剪完成后增量接入,再与第一条路径汇合。
两条路径得到的是“模型可提议的能力”。具体调用仍要经过运行时权限:本地进程工具在适用时继续受 Sandbox,远程 MCP 则受外部服务自身边界。这个非对称结构很重要:基础池工具受 Agent 的 tools / disallowedTools 约束;专属 MCP 不再回穿这层可见性裁剪,但仍不能绕过执行时 deny 和权限边界。
因此,同一模型、同一用户、同一项目,在主 Agent、Plan Agent 和后台 Agent 中也可能拥有不同能力。
模型策略:定义由哪个决策引擎思考
模型策略不只是一个模型名,还可包含:
- 主模型与备用模型;
- 思考模式与思考预算;
- 输出上限与结构化输出;
- 快速模式、模型可用性和提供方适配;
- 某些特殊 Agent 使用的专用模型。
条件启用的 Agent 还可为自己指定模型或推理力度。但提供方、账户策略和当前模型可用性仍是外层约束。
权限:定义能力在什么条件下可以使用
工具存在不等于任何参数都能执行。权限是一个独立的运行时输入:
- 权限模式给出默认决策风格;
- 托管、用户、项目和会话规则给出允许、询问和拒绝范围;
- 工具对参数做语义分析,例如命令、路径或域名;
- Sandbox 对已被允许的进程继续加上操作系统边界。
权限主要留在本地,不会作为一个顶层 API 字段交给模型。
可变状态:让 Agent 不是一次性函数
最后一部分是模型请求之外的可变状态:
- 当前会话、历史和压缩边界;
- 当前权限模式、模型和工具池;
- 读取文件缓存、文件变化和工具追踪;
- 取消信号、后台任务、队友消息和通知;
- 已调用 Skills、连续失败、备用模型和压缩跟踪。
它们决定了“下一次请求怎样构造”,却不一定以原始形式发给模型。
构造不是一次性完成
Agent 构造有两个时间尺度:
- 初始构造:选择身份、system、模型、初始工具和会话状态。
- 回合重建:根据新的权限模式、已加载工具、附件、压缩结果、文件变化和子 Agent 消息更新请求快照。
这是 Agent 构造最精妙的地方之一:身份相对稳定,能力和上下文按回合编译。这既保持行为一致,又允许它在 Plan Mode、Skill 调用、ToolSearch 加载和压缩后更新能力。
源码定位
- Agent 定义与内置类型:
src/tools/AgentTool/、src/commands/agents/ - 系统提示选择:
src/utils/systemPrompt.ts、src/constants/prompts.ts - 用户与运行时上下文:
src/context.ts、src/utils/attachments.ts - 工具池组装与裁剪:
src/tools.ts、src/tools/AgentTool/agentToolUtils.ts - 查询期间的可变状态:
src/query.ts、src/types/
系统 Prompt 的分层与选择
Claude Code 的 Prompt 不是一篇永远不变的长文,也不是每轮把所有信息重新拼成一个字符串。它是一套带有选择优先级、分块和缓存边界的指令系统。
先区分四条 Prompt 通道
| 通道 | API 位置 | 承载的内容 |
|---|---|---|
| 基础行为 | 顶层 system[] | Agent 身份、安全原则、任务方法、工具原则、环境、自动 Memory 机制;Agent 自身的 Memory 规则与内容也可追加在这里 |
| 对话与运行时事实 | messages[] | 用户原话、历史、工具结果、初始 CLAUDE.md / 日期,以及按时序出现的 Plan、相关或嵌套 Memory、Skill、Hook 等提醒 |
| 能力说明 | 顶层 tools[] | 工具名、用途、输入结构与缓存标记 |
| 推理与输出控制 | 请求顶层字段 | 模型、思考、输出上限、工具选择和实验能力 |
四条通道共同影响模型,但生命周期不同。把它们区分开,才能同时保持指令稳定、时序正确和缓存有效。
交互主线的 system 选择树
系统提示词不是把所有候选顺序叠加,而是先选择基础,再决定是否追加。交互主线的选择树是:
- 完整覆盖接口:如果内部入口真的传入 override,直接使用它,不再追加其他 system。当前源码能确认这条选择接口存在,但不能据此宣称普通用户入口一定会触发它。
- 协调器提示:只有协调条件成立,并且没有主线程 Agent 定义时才选择。
- 主线程 Agent 提示:有主线程 Agent 定义时采用该 Agent 的提示。Proactive / KAIROS 是特例:Agent 指令追加到默认提示,而不是替换默认提示。
- 命令行自定义提示:没有以上分支时,显式自定义基础提示胜出。
- 默认提示:其余情况使用 Claude Code 默认提示。
普通 append system 放在已选基础之后;完整覆盖分支则不追加。这里的精妙之处不是优先级数字,而是互斥选择、条件例外和追加语义被明确分开。
Headless / SDK 有自己的选择顺序
非交互入口不是机械复用交互 REPL 的优先级。在 Headless / SDK 路径中,显式 system prompt 高于 Agent prompt;只有没有显式 system 时,才回退到 Agent 或默认提示。
因此,“自定义 Agent 一定覆盖显式 system”或“所有入口都遵循同一优先级”都不成立。架构上共享的是 system 的分块协议,不是每个入口的产品选择策略。
默认 system 不是一块内容
默认 system 可以按职责理解为下列区段:
- Agent 身份与基本安全原则;
- 如何理解并完成软件工程任务;
- 如何对待高风险、不可逆和对外可见操作;
- 如何优先使用专用工具、并行工具和任务跟踪;
- 何时使用 Subagent 与 Skill;
- 如何组织面向用户的最终表达;
- 会话级能力和自动 Memory 规则;
- 工作目录、Git、平台、Shell、模型等环境信息;
- 可选的语言、输出风格、MCP 指令、临时目录与上下文管理说明。
某一段是否出现,可能取决于工具集、模型、平台、输出风格、功能开关或运行入口。因此不存在一份永远字节级相同的默认 Prompt。
内容顺序稳定,但全局缓存边界有条件
系统 Prompt 最精妙的设计之一,是不只考虑“模型要看什么”,还考虑“哪些前缀可被缓存复用”。
flowchart LR
S1["稳定 system<br/>身份、原则、工具方法"] --> B["候选缓存边界"]
B --> S2["动态 system<br/>环境、风格、会话特性"]
S2 --> U["messages<br/>用户、附件、工具结果"]
U --> T["tools<br/>当前能力快照"]
相对稳定的行为原则放在前面,易变的用户、项目和回合信息放在后面或 messages。这是始终成立的内容排序原则。
但“稳定段一定进入跨组织共享的全局缓存”并不成立。物理缓存作用域还取决于:基础提示是否带有默认提示的稳定标记、调用是否具备第一方全局缓存资格,以及渲染后的非延迟用户 MCP 工具是否迫使缓存降级。自定义提示或 Agent 提示通常没有默认标记;第三方资格、标记缺失或用户 MCP 也可能只使用组织级缓存。
因此要区分两件事:把稳定内容放在前面是 Prompt 架构;这段前缀最终使用哪一级缓存是请求时策略。前者为后者创造机会,但不保证结果。
这个设计的代价是上下文不再是一段可直观阅读的大文本,而是需要经过编译的分块协议。
<system-reminder> 是消息内协议,不是唯一注入通道
源码中至少有三条不同路径,不能把所有动态知识都概括成“先变成附件”:
- 初始 user context:CLAUDE.md 层级与日期在会话开始时直接组成靠前的 meta
user消息。 - system 绑定内容:自动 Memory 的使用机制属于默认顶层 system;Agent 自身的 Memory 规则和已加载内容可追加到 Agent system。
- 运行时附件:Plan 状态、后续路径指令、相关或嵌套 Memory、Skill、Hook、文件变化等在发生时转成消息内提醒。
这些内容都有指令意义,但时序不同。如果把动态部分全部放进顶层 system:
- 无法表达“调用 EnterPlanMode 之后才生效”;
- 难以和工具结果保持正确时序;
- 动态变化会频繁破坏前缀缓存;
- 压缩后难以按仍有效的状态重建。
运行时附件通常投影为 user.content[] 中的文本块;如果它紧随一批工具结果,还可能折入相邻 tool_result.content,以保持工具配对合法,而不是永远形成独立消息。<system-reminder> 只是 Claude Code 与模型之间的内部语义标签,不是 Messages API 新角色,也不改变底层 role。
工具说明为什么不应塞进 system
工具的名称、用途和输入结构是结构化 Prompt,它们位于 tools[]。这样模型可以产生可校验的 tool_use,运行时也可以对工具做增删和延迟加载。
如果工具只是 system 里的一段文字,模型可能理解用途,却没有可靠的参数协议,本地运行时也难以把“能力可见”变成硬性边界。
普通 Subagent 与 Fork 的 Prompt 差异
- 普通 Subagent 使用该 Agent 类型自己的 system,并从委派 Prompt 开始一条新消息链。
- Fork 在构建能力开启、交互入口且非协调器场景中,为了复用 Prompt 缓存,可以继承已经渲染的 system、主对话前缀和精确工具数组;它不是所有运行形态都存在的通用分支。
这说明 Prompt 本身也是多代理隔离的一部分。普通 Subagent 优先上下文独立,Fork 优先前缀复用;两者不应被视为同一种启动方式。
常见误解
- “所有指令都在 system”:运行时指令大量存在于带内部标签的
user内容。 - “system-reminder 比 user 角色优先级高”:在 API 协议中它仍是 user 文本块;特殊性来自模型与 Claude Code 对该标签的协议理解。
- “主 Agent 和 Subagent 只是 user Prompt 不同”:它们的 system、tools、模型策略和可变状态都可能不同。
- “追加 Prompt 等于完整覆盖”:前者保留基础行为,后者替换基础行为。
源码定位
- 默认 system 区段:
src/constants/prompts.ts、src/constants/systemPromptSections.ts - system 选择优先级:
src/utils/systemPrompt.ts - system 分块与 API 转换:
src/utils/systemPromptType.ts、src/services/api/claude.ts - 运行时附件:
src/utils/attachments.ts - Fork 前缀复用:
src/tools/AgentTool/forkSubagent.ts、src/utils/forkedAgent.ts
上下文编译
Claude Code 不是把本地世界直接暴露给模型,而是先把分散的事实编译成一个合法、有时序、可压缩的消息序列。
这个过程可以称为上下文编译。它介于本地状态与 Messages API 之间,是 Claude Code 最核心的架构边界之一。
flowchart LR
S1["system context<br/>Git 等启动快照"] --> Y["system[]"]
S2["会话历史"] --> N["内部消息序列"]
S3["初始 user context<br/>CLAUDE.md / 日期"] --> U["前置 meta user"]
U --> N
S4["运行时事件<br/>Plan / Skill / Memory / Hook / IDE / 任务"] --> A["附件收集"]
A --> N
N --> M["移动附件与合并角色"]
M --> T["修复 tool_use / tool_result"]
T --> C["清理不适合 API 的块"]
C --> R["Messages API 请求"]
Y --> R
上下文不是一个数组,而是多种信息源
| 来源 | 解决的问题 | 常见进入方式 |
|---|---|---|
| 用户原话 | 这一轮要完成什么 | 普通 user 内容 |
| 会话历史 | 前面做过什么、模型曾说什么 | user / assistant 交替历史 |
| CLAUDE.md 与 Rules | 用户、项目和路径级规则 | <system-reminder> |
| 工具结果 | 本地世界的新观测 | tool_result |
| Plan、Skill、相关或嵌套 Memory | 当前工作方式、专用流程和按时点激活的跨会话事实 | meta user 消息或相邻工具结果中的提醒 |
| Hook、IDE、文件变化 | 运行中新出现的本地状态 | 动态附件 |
| 任务和 Agent 消息 | 后台任务、队友或子 Agent 的进展 | 通知或任务附件 |
不同来源的信任程度不同。项目指令可能是受信任配置,MCP 或网页内容则是外部输入。编译的任务是放到正确位置,不是把所有文本都提升成同等权威。
user context 与 system context 是两条通道
Claude Code 在会话中区分两类背景信息:
- user context 主要包含 CLAUDE.md 层级和当前日期等面向对话的事实。它被包成
<system-reminder>,放在消息历史前部。 - system context 主要包含会话启动时的 Git 状态等环境快照。它被附加到系统 Prompt。
两者都影响 Prompt 缓存,但在线上位置不同。不同入口也可以有不同策略:例如完整自定义 system 的 SDK 场景可以跳过默认 system context,却仍然保留 user context。
CLAUDE.md 是按路径逐步激活的
指令加载不是“启动时扫描整个磁盘”。
- 托管、用户、项目和本地范围的 CLAUDE.md 会按层级组合;
- 当 Agent 进入更深目录、读取某个文件或触发路径规则时,可以加入更具体的指令;
- 只读 Explore/Plan Agent 在条件启用的瘦身策略下,可移除 CLAUDE.md 和启动 Git 快照,但不等于没有任何日期或环境上下文。
这是一种渐进式上下文设计:指令随 Agent 的实际工作范围展开,不在一开始浪费上下文窗口。
附件是一套运行时事件协议
“附件”不只指用户上传的文件。它是 Claude Code 用来向对话注入动态事件的统一中间形态。
可以把附件分成四层:
- 用户触发附件:
@file、MCP resource、@agent和条件启用的 Skill 发现。 - Agent 线程附件:日期变化、工具变化、文件变化、嵌套 Memory、Plan、Todo,以及按
agentId发给当前普通 Agent 的待处理消息。 - 团队条件附件:只有 Agent Teams 与对应 query 条件成立时,才加入 teammate mailbox 和 team context。
- 主线专用附件:IDE 选区、诊断、输出风格、统一任务、Token 预算和某些验证提醒。
附件提供者之间尽量隔离:一个次要附件失败,不应阻塞整个用户回合。收集本身也有时间边界,避免某个外部来源无限拖延首次模型请求。
时序决定附件放在哪里
首轮用户附件可以跟随用户原话进入消息。但工具回合中的新附件必须等本批工具结果收齐后才能追加。
原因不是 UI 编排,而是 Messages API 的对话协议:一组 tool_use 需要紧随可配对的 tool_result,不能在中间随意插入一条普通用户消息。
后台命令队列也按 Agent 身份分流:主 Agent 只消费主线用户输入,Subagent 只消费发给自己的任务通知,不会偷走用户给主线的新 Prompt。
内部 transcript 不等于 API messages
为了 UI、恢复和审计,内部 transcript 可以保留:
- 进度消息;
- 附件消息;
- 本地命令输出;
- 被拆成多块的流式 assistant 响应;
- 不需要发给模型的虚拟与展示消息。
请求前才会把它们投影成 API messages。这个投影是有损的:
- 附件被移到合法时序位置;
- 纯 UI 进度和虚拟消息被过滤;
- 连续
user消息被合并,以适配要求角色交替的提供方; - 同一次 API 响应的 assistant 分块被重新合并;
- 工具名和工具输入被归一化;
- 孤立思考块、空 assistant、非法媒体和不配对的工具结果被清理或修复。
这种分离非常精妙:transcript 优先保留运行真相,API projection 优先满足协议和缓存稳定。如果强迫两者共用一种消息形状,UI、恢复和模型协议会相互牵制。
日期变化展示了上下文编译的核心思想
会话首次的日期会被保持,跨过零点时不会回头改写对话前部的日期。新日期作为对话尾部的变化附件进入。
这样既保留“时间已经变化”的语义,又不会改写整段旧历史并让 Prompt cache 失效。上下文编译的目标不仅是信息正确,还是在正确时点以稳定前缀表达信息。
源码定位
- user/system context:
src/context.ts、src/utils/api.ts - 用户输入处理:
src/utils/processUserInput/ - 附件收集与转换:
src/utils/attachments.ts - API 消息投影:
src/utils/messages.ts、src/services/api/claude.ts - 主回合中的附件时序:
src/query.ts
工具能力图
模型能否读文件、运行命令或创建 Subagent,不是由一句“你可以”决定的,而是由当前请求的 tools[] 与本地执行策略共同决定。
因此,工具不是一张静态清单,而是一张会随 Agent、平台、权限和运行状态变化的能力图。
工具能力有两条装配路径
flowchart LR
A["内置工具"] --> P["当前 worker 基础装配池"]
B["会话运行时已注册 MCP"] --> P
P --> F1["平台 / 功能开关<br/>全局可见性 deny"]
F1 --> F2["Agent allow / deny<br/>前台 / 后台规则"]
F2 --> H["继承能力"]
C["Agent 专属 mcpServers"] --> D["连接并在裁剪后追加"]
D --> H
H --> F3["ToolSearch 延迟展开"]
F3 --> V["模型可见 tools[]"]
V --> X["执行时参数权限"]
X --> Z["Sandbox / 外部服务边界"]
第一层:候选来源
基础候选池主要由内置工具与会话运行时已经注册的 MCP 工具组成。普通 Subagent 会按自己的权限模式重新装配这一基础池,不接收父 Agent 已裁剪后的最终数组;Fork 才精确继承父线程工具快照。通常模式下,MCP 能力以 mcp__服务名__工具名 暴露;SDK 的 no-prefix 模式是明确例外,因此不能靠名称格式识别所有 MCP 工具。
基础装配池会把内置与 MCP 分区稳定排序,内置工具在前。这不只是为了美观:新增 MCP 工具时,尽量不打乱内置工具的字节前缀。Agent 专属 MCP 在裁剪后直接追加,并不会再次执行这套分区排序,所以排序是装配池的规律,不是所有最终 Agent 工具数组的绝对规律。
第二层:运行环境
工具可能因下列原因不进入候选:
- 当前平台不支持,例如特定 Shell 工具;
- 功能开关或内部构建未开启;
- 非交互或简化运行形态不需要该能力;
- MCP Server 尚未连接、尚未认证或没有提供工具。
因此“源码目录里有这个工具”不能证明它已经出现在当前请求中。
全局可见性与执行时 deny
明确禁止的工具可在请求前直接从能力图移除。这样模型不会先决定调用一个注定被拒绝的能力,也减少了无意义的工具协议开销。
参数级权限则不能全在这里处理。例如 Bash 可能允许某些命令而禁止另一些;这种判断必须等模型给出具体输入后再做。
Agent 自身的允许与禁止
Agent 定义可以提供工具白名单或禁止列表。但主线 Agent 与普通 Subagent 的语义并不完全相同:
- 作为主线启动的 Agent 保留主会话运行能力,再应用自身 allow/deny;
- 普通 Subagent 会先移除子 Agent 全局禁止能力,然后应用自身规则;
permissionMode=plan只让ExitPlanMode越过普通 Subagent / 后台默认裁剪,并不会自动移除写工具;- 内置 Plan Agent 是另一层身份策略:它通过自身禁止列表同时移除写工具与
ExitPlanMode,因为它只负责形成方案,不能自行退出主线的 Plan Mode; - Fork 仅在对应能力与入口条件成立时,为保持缓存字节一致而精确复用父线程工具数组,递归限制改在调用时检查。
这说明“Agent 类型”不只改变 Prompt,还参与实际能力裁剪。
基础装配池中名称以 mcp__ 开头的 MCP 工具有一个窄特例:它们越过普通 Subagent 默认禁用表和后台异步白名单,但仍受该 Agent 的 tools / disallowedTools 裁剪。SDK no-prefix MCP 不会命中这项名称特例。
前台与后台调度边界
后台 Agent 不能依赖随时弹出的主 UI 对话。它的工具池因此会再收紧,例如避免直接向用户提问、操作主线 Plan Mode 或不受控地继续生成子 Agent。
条件启用的进程内队友可以获得部分团队工具和同步委派能力,但仍会防止无限后台扩张。
Agent 专属 MCP 是后置增量
Agent 可以在父级 MCP 能力之外声明自己需要的 MCP Server:
- 对已存在的全局连接,可以复用;
- 对 Agent 自己创建的动态连接,应在 Agent 结束时清理;
- 要求必备 MCP 的 Agent,只有在对应 Server 已真正提供工具时才应可用;
- 用户控制的 Agent 配置不能在严格信任策略下悄悄引入任意 MCP。
这条路径发生在 Agent 基础工具裁剪完成之后,所以新追加的专属 MCP 不再经过该 Agent 的 allow/deny,也不再经过基础装配池此前的 blanket-deny 可见性过滤。这不等于获得无条件执行权:具体调用仍要经过运行时权限与 deny。可见性后置追加和执行授权是两套边界。
ToolSearch 的延迟暴露
延迟加载并不只在“工具很多”时发生。默认策略会优先把多数 MCP / shouldDefer 工具设为 deferred,alwaysLoad 可使其常驻;只有 auto 模式才主要依据工具规模阈值决定是否启用。最终选择还受模型、提供方、功能开关和当前可见性影响。
模型初始只知道延迟工具的名称与可发现性,使用 ToolSearch 返回引用后,对应完整输入结构才进入后续请求。
工具能力图因此不仅是“有/无”,还有“可发现但尚未展开”状态。
工具定义和本地工具不是同一个东西
模型看到的工具是一份协议:
- 名称;
- 用途说明;
- JSON 输入结构;
- 可选的延迟加载与缓存标记。
本地运行时则还持有:
- 输入归一化和验证;
- 权限决策;
- 执行器;
- 并发安全性和取消语义;
- 进度、结果和上下文更新规则。
模型看到的只是“对外协议”,不是本地实现和安全策略本身。
可见性与授权必须分开
工具可见性回答“模型能不能提议使用”。执行时权限回答“这一次具体输入能不能发生”。
一个 Bash 工具可以对模型可见,但一条具体命令仍可被自动允许、要求用户确认或直接拒绝。如果被允许,它还可能被 Sandbox 限制文件和网络范围。
这是工具能力图最重要的设计取舍:用粗粒度可见性减少无效决策,用细粒度权限保留同一工具的灵活性。
源码定位
- 内置与 MCP 工具池:
src/tools.ts - Agent 工具裁剪:
src/tools/AgentTool/agentToolUtils.ts、src/constants/tools.ts - Agent 专属 MCP:
src/tools/AgentTool/runAgent.ts、src/tools/AgentTool/loadAgentsDir.ts - ToolSearch 与延迟工具:
src/tools/ToolSearchTool/、src/utils/toolSearch.ts - API 工具投影与 schema 缓存:
src/utils/api.ts、src/utils/toolSchemaCache.ts - 执行时权限:
src/utils/permissions/、src/services/tools/
状态模型
一个可持续工作的 Agent 必须记得自己正在做什么,但“记得”不能全部交给模型上下文。
Claude Code 把状态分在多个作用域中,每个作用域有不同的所有者、生命周期和共享边界。这是理解主 Agent 与 Subagent 隔离的基础。
六个状态作用域
| 作用域 | 典型内容 | 主要生命周期 |
|---|---|---|
| 进程与全局状态 | 功能开关、客户端、插件与 MCP 注册、全局配置 | 进程 |
| 会话状态 | session ID、历史、当前模型、权限模式、基础工作目录 | 会话 |
| Agent 实例 / 对话线程状态 | agentId、专属 transcript、有效 CWD 覆盖、已调用 Skill、读取与内容替换状态、专属 MCP、取消资源 | Agent 实例或线程 |
| 用户回合状态 | 当前消息、压缩跟踪、转移原因、备用模型、继续次数 | 一个 agentic turn |
| 工具执行状态 | tool use ID、进度、当前输入的权限决策与取消结果 | 工具或一批工具 |
| 持久状态 | transcript、配置、Memory、任务、邮箱、文件和 Git | 跨回合、跨会话或跨进程 |
状态分层的核心原则是:只在确实需要的范围共享可变状态。如果把“当前 Agent”、“当前 CWD”和“当前取消器”都写成单一全局变量,多个后台 Agent 并发时会立即串号。
Agent 类型与 Agent 实例必须分开
Claude Code 中有三个容易被混淆的身份:
- agentType:逻辑角色,用于选择指令、工具和模型策略;同一类型可创建很多实例。
- agentId:一次真实运行实例的标识,用于 transcript、Skill 状态、通知路由和清理。
- 团队身份:需要跨进程寻址和重连时,使用可预测的队友名和团队名。
逻辑角色可复用,运行实例必须隔离,团队身份又必须可寻址。用一个字段表达三者,会同时伤害重用、隔离和重连。
会话状态是主线容器,不是唯一真相
主会话需要展示消息、权限对话框、任务、通知和模型状态。因此它有一份面向 UI 和主循环的可变状态。
但不是所有事实都应该只存在这里:
- 真正的团队邮箱和任务列表需要跨进程保存;
- 子 Agent 的完整历史应放在独立 sidechain transcript;
- 工具进度属于运行中事件,不需要全部发给模型;
- 文件和 Git 才是工作产物的持久真相。
因此主会话状态更像运行时观察与控制容器,而不是全系统唯一数据库。
查询状态是一个显式状态机
一个用户回合中,主查询循环需要带着下列状态继续迭代:
- 当前内部消息序列;
- 工具执行上下文;
- 是否已压缩、是否尝试响应式恢复;
- 最大输出恢复次数;
- 当前回合计数与任务 Token 预算;
- 上一次为什么继续,下一次要替换哪部分状态。
使用显式状态机而不是递归调用,可以更清楚地表达“工具结果后继续”、“压缩后重试”、“模型降级后继续”和“Hook 阻止后给模型再一次机会”等不同转移。
读取缓存与内容替换状态
上下文管理不能每轮对同一段旧内容作出不同处理,否则 Prompt 前缀会不断漂移。因此运行时需要记住:
- 某文件在什么时候被读过;
- 读取后文件是否变化;
- 某个旧工具结果是否已被清理或摘要;
- 一次内容替换决策是否应在后续回合保持。
这些不是一次工具调用结束就丢弃的临时状态,而属于会话对话线程或 Agent 实例,会跨工具回合保持。普通 Subagent 通常获得自己的读取缓存和内容替换状态。Fork 可以复制父线程的决策快照,但后续不与父线程共用同一可变容器。
并发身份和 CWD 不应使用单一全局变量
多个后台 Agent 可在同一进程并发等待 API、文件和工具。主会话持有基础 CWD,但具体 Agent 链可以用异步上下文覆盖自己的有效 CWD。在这种架构下,“当前 Agent ID”、“当前队友身份”和“当前有效工作目录”必须跟随各自的异步执行链,而不能被所有任务改写同一份全局状态。
这个设计让同进程 Agent 能共享 API 配置、注册表和任务系统,同时保持 telemetry、路径、Skill 状态和消息路由不串线。
任务状态与前后台状态是正交的
一个 Agent 任务可以是 pending、running、completed、failed 或 killed。但“前台/后台”不是这条状态链中的终态:
running + 前台表示当前用户回合正在等它;running + 后台表示它仍在运行,但主 Agent 可继续其他工作;completed / failed / killed才表示生命周期终止。
前台转后台因此是调度所有权的改变,不应被误解为 Agent 类型改变或任务已完成。
持久化是状态生命周期的最后一步
需要恢复的状态不能只存在内存:
- 主会话与 Subagent sidechain 保存消息历史;
- Agent metadata 保存类型、工作树路径和描述等恢复信息;
- 团队任务和邮箱使用跨进程文件与锁;
- Memory 和会话笔记保存被选中的长期或长会话事实;
- 文件系统和 Git 保存工作产物。
恢复时不会盲目复制最后的内存。运行时会清理未完结工具、孤立思考块和空响应,再使用当前仍存在的 Agent 定义重建能力。这使恢复是“从持久事实重建合法运行状态”,而不是内存镜像回放。
状态分层的精妙与代价
精妙之处:
- 主 Agent 可查看任务,又不需共享子 Agent 的完整可变上下文;
- 后台 Agent 可独立取消,不会因主回合 ESC 而自动全部消失;
- 条件启用且入口允许时,Fork 可复用请求前缀,又在分叉后独立推进状态;
- Worktree 可只隔离文件工作副本,不必复制整个进程和配置。
付出的代价:
- 同一个事实可能在 UI 镜像、运行任务和持久文件中有不同表示;
- 生命周期结束时必须对称清理 Agent 专属 MCP、Hook、Skill、缓存和 transcript 资源;
- 正确性依赖清晰的状态所有权,不能随意在模块级全局状态中加字段。
源码定位
- 主会话与应用状态:
src/state/、src/bootstrap/state.ts - 查询回合状态:
src/query.ts - Agent 并发身份:
src/utils/agentContext.ts、src/utils/teammateContext.ts - CWD 作用域:
src/utils/cwd.ts - 任务状态:
src/Task.ts、src/tasks/ - Subagent 持久与恢复:
src/tools/AgentTool/runAgent.ts、src/tools/AgentTool/resumeAgent.ts
主查询循环
Claude Code 的核心不是“向模型发一次请求”,而是一个显式的状态机。它在一个用户回合内可以多次调用 API、执行工具、吸收新附件、压缩历史并恢复错误。
三层循环嵌套
| 层 | 负责什么 | 持续多久 |
|---|---|---|
| 会话宿主 | 接收输入、串行化用户回合、保存 transcript、向 UI 或 SDK 输出事件 | 整个会话 |
| Agent 查询循环 | 管理一个 agentic turn 中的多次 API、工具、压缩、Hook 和终止 | 一次用户回合 |
| Messages API 请求 | 编译一份 payload、消费响应流、处理请求级重试 | 一次模型请求 |
用户只提交一次需求,不代表只有一次 API 请求;一次 API 流结束,也不代表这个用户回合已经结束。
外层先保证一个会话只有一个主回合
在交互会话中,回合锁会在用户输入预处理之前取得。这一点很重要:本地 Shell 模式和斜杠命令也可能异步等待,如果只在调用模型前加锁,会话仍然可以在预处理阶段重入。
当主回合正在运行时,新的用户输入不会启动第二个主查询循环,而是进入队列,在正确的工具与附件边界被吸收。
这种串行化不会禁止 Subagent 并发;它只保证同一主对话的用户时序不被两个主回合同时改写。
一个 agentic turn 的状态机
stateDiagram-v2
[*] --> 请求前准备
请求前准备 --> 模型流: 上下文可用
请求前准备 --> 终止: 硬上限/压缩失败
模型流 --> 工具批次: 收到 tool_use
模型流 --> 恢复决策: 无 tool_use
模型流 --> 终止: 中断/不可恢复
工具批次 --> 请求前准备: 结果+附件
工具批次 --> 终止: 中断/Hook/turn 上限
恢复决策 --> 请求前准备: 压缩/续写/降级/Hook 反馈
恢复决策 --> 终止: 完成/恢复耗尽
终止 --> [*]
每次迭代的固定次序
- 更新查询链跟踪;条件启用 Skill 搜索时,启动可与模型流重叠的 Skill 预取;
- 只投影最近压缩边界之后仍需要的消息;
- 应用工具结果大小预算;
- 按从便宜到昂贵的顺序执行历史清理、微压缩、上下文折叠和自动完整压缩;
- 把 system context 加到 system,把 user context 作为 meta user 提醒放在消息前部;
- 根据当前模式和恢复状态选择模型,检查硬性上限;
- 编译并调用 Messages API,消费流式响应;
- 收集真实
tool_use块,条件启用时边流式生成边启动工具; - 无工具时进入恢复、Stop Hook 和终止判断;
- 有工具时收齐结果、吸收附件和队列消息、刷新 MCP 能力,再构造下一迭代。
这个次序就是 Agent 的运行时协议。改变顺序可能导致上下文超限、工具结果断链或 Prompt cache 无法复用。
为什么继续信号是 tool_use,不是 stop reason
流式过程中,真实工具块可能在整条 assistant 消息的 stop reason 最终确定前就已经完成。另外,不同提供方和异常流对 stop reason 的完整性不一定相同。
因此主循环用“是否真正收到 tool_use 内容块”判断是否需要工具回合,而不是把 stop reason 当成唯一依据。
这个精妙设计把状态机绑定在已观测的协议事件上,而不是某个可能延迟或缺失的摘要字段上。
Continue 不只有“工具完成”
| 继续原因 | 下一迭代改变什么 |
|---|---|
| 工具批次完成 | 追加 assistant、tool results 和新附件 |
| 折叠结果重试 | 使用更小的细粒度消息投影 |
| 响应式压缩 | 用压缩后消息替换旧历史 |
| 输出上限升级 | 在条件启用时扩大当前请求输出上限 |
| 输出被截断后续写 | 保留已生成内容,追加“从中断处继续”提醒 |
| Stop Hook 阻止 | 把 Hook 反馈追加给模型,允许其修正 |
| Token 预算要求继续 | 追加继续提醒,保持同一用户回合 |
当状态机显式记录转移原因时,下一迭代可以精确地只替换必要部分,而不是把整个 Agent 从零重启。
Terminal 是运行时判断
主循环可因模型最终回答、上下文恢复失败、媒体或模型错误、用户中断、Hook 停止、Agent 回合上限,以及宿主级费用或结构化输出限制而结束。
核心循环的终止原因还要被 UI 或 SDK 宿主映射成最终结果类型。所以“查询结束原因”与“SDK 最终结果类型”不是同一层概念。
为什么主循环是系统的脊柱
Prompt 在每次迭代前被重新编译;Tools 在流中被发现、在本地被执行、在下一迭代变成消息;Memory 和 Skill 可以预取;Sandbox 在工具执行阶段生效;Subagent 本身又是另一条同类查询链。
主循环的精妙在于:它不需要理解每个工具的业务细节,只维护统一协议、状态转移和生命周期边界。
源码定位
- 交互会话宿主:
src/screens/REPL.tsx、src/utils/handlePromptSubmit.ts - SDK 会话宿主:
src/QueryEngine.ts - Agent 主查询循环:
src/query.ts - Messages API 层:
src/services/api/claude.ts - 工具执行:
src/services/tools/ - 压缩与恢复:
src/services/compact/
API 请求编译
每次模型请求前,Claude Code 都要把内部丰富的运行时状态投影成 Messages API 能接受的结构。这不是字段搬运,而是一次请求编译。
编译的输入与输出
flowchart LR
A["内部 transcript"] --> N["消息归一化"]
B["system 区段"] --> S["system 分块+缓存边界"]
C["当前工具池"] --> T["API tool schema"]
D["模型/思考/输出策略"] --> P["顶层参数"]
N --> R["Messages API 请求"]
S --> R
T --> R
P --> R
E["权限/Sandbox/UI/本地缓存"] -. 留在本地 .-> L["执行上下文"]
最终请求大致由下列部分组成:
| 顶层部分 | 承载内容 |
|---|---|
model | 当前迭代真正使用的模型,可因 fallback 改变 |
system[] | 已选择的基础 system、动态区段与缓存标记 |
messages[] | 归一化后的用户、assistant、附件与工具回合 |
tools[] | 当前真正向模型暴露的工具结构 |
max_tokens / thinking | 输出和推理预算 |
metadata | 请求级元数据 |
| 可选控制字段 | tool_choice、betas、temperature、context_management、output_config、speed |
权限规则、Sandbox 执行器、UI 状态、文件读取缓存和取消器不是 API 顶层字段,它们留在本地用于执行模型意图。
消息归一化是协议边界
内部消息允许 UI 进度、附件、本地系统消息、分块 assistant 响应和恢复占位。API 请求则必须符合更严格的角色和工具配对协议。
请求前会:
- 过滤仅供 UI 和运行时使用的消息;
- 把附件投影成一个或多个 meta
user消息; - 合并连续
user角色,以适配要求对话交替的提供方; - 按同一 API message ID 重新合并流式 assistant 分块;
- 归一化工具别名和输入;
- 清理不完整思考块、空消息和非法媒体;
- 修复或拒绝不合法的
tool_use/tool_result对。
这是一个有损投影:transcript 中存在的内容不一定会进入请求,但为了恢复和审计仍可留在本地历史中。
user context 与 system context 的线上位置不同
- system context 被序列化后附加到顶层 system;
- user context 被包成
<system-reminder>,作为历史前部的 metauser消息。
二者都是 Prompt cache key 的一部分,但不能因为名字里都有 context,就认为它们位于同一 API 字段。
工具数组是当前请求的快照
- ToolSearch 是否开启取决于当前模型、提供方、模式与功能开关;默认策略会延迟多数 MCP /
shouldDefer工具,只有 auto 模式主要使用工具规模阈值; - 延迟工具只有在历史中被
tool_reference发现后,才带完整 schema 进入当前请求; - MCP Server 仍在连接时,ToolSearch 可以保留,以便能力稍后出现;
- 不支持工具搜索的模型会移除 ToolSearch 及相关字段。
工具的基础 schema 在会话内保持稳定,延迟加载和缓存标记作为请求级覆盖。这避免功能开关或 MCP 重连让整个工具前缀无效。
system 分块同时服务语义和缓存
在支持的路径中:
- 稳定前缀可使用更宽的缓存作用域;
- 动态部分不进入跨组织的全局缓存;
- 实际渲染的用户级 MCP 工具会改变信任边界,system 缓存作用域因此收紧;
- 自定义 Agent Prompt 如果没有默认动态边界,不能假定也能获得相同的全局分段缓存。
缓存作用域不只是性能参数,还与内容是否包含用户专属能力相关。
请求 retry 保持模型不变,只重算允许变化的参数
请求编译不会在进入重试器前完全冻结。每次请求级尝试可以重新计算最大输出、思考配置、快速模式、beta 和部分输出配置,但仍使用这次模型调用已经选定的同一模型。
真正切换模型属于上层的 fallback 状态转移:主循环更新当前模型,再重新编译一次请求。这样请求级恢复可以调整传输参数,却不会与模型降级混成同一层,也不需要丢弃上层 Agent 回合状态。
真正的出网边界
在这份源码快照中,请求最终通过 anthropic.beta.messages.create 发出,主路径使用流式响应。位置是 src/services/api/claude.ts。
在它之前,仍然是 Claude Code 本地的请求编译;在它之后,才进入模型提供方的 Messages API。具体物理主机、认证与传输适配取决于提供方和客户端配置,不应把这个逻辑出口误解为永远直连某一固定域名。
请求 ID 属于消息链
并发的主 Agent、Subagent 和队友不应共享一个“最后 API request ID”全局变量。当前请求的父级跟踪信息可从当前消息链中最后一条 assistant 消息推导。
这样撤销消息会自然撤销跟踪边,并发 Agent 也不会互相覆盖 ID。这是“让关联状态附着在它所属的数据链上”的精妙例子。
源码定位
- 内部消息归一化:
src/utils/messages.ts - system 与工具 API 投影:
src/utils/api.ts、src/utils/toolSchemaCache.ts - 请求参数、缓存和最终 API:
src/services/api/claude.ts - 请求前上下文组装:
src/query.ts - 提供方与认证客户端:
src/services/api/
流式响应与工具回合
模型响应不是一个最终字符串,而是由文本、思考、工具意图、使用量和停止信号组成的流式事件序列。
工具回合的核心是:模型只产生结构化意图,Claude Code 在本地执行后,用 tool_result 把新世界状态送回模型。
流式内容块是运行时的交付粒度
流式层会逐步建立 assistant 响应:
- 消息开始时建立请求级上下文和初始使用量;
- 内容块开始时根据类型创建累加状态;
- 文本、思考、签名和工具 JSON 分别累加;
- 一个内容块结束后,就可以产生一个完整的内部 assistant 消息块;
- 消息尾部再更新最终使用量和 stop reason。
同一次 API 响应的多个内容块,在 transcript 里可以是多个内部消息,但共享同一个 API message ID。下次请求前,它们会被重新合并为一条 assistant 消息。
这个设计让 UI 可以尽早展示内容,工具也可以尽早启动,同时保证下一次 API 请求仍是合法消息形状。
一个工具回合的完整时序
sequenceDiagram
participant M as 模型
participant Q as 主查询循环
participant P as 权限与 Hook
participant T as 本地/MCP 工具
M-->>Q: assistant content + tool_use
Q->>P: 验证输入并请求权限
P-->>Q: 允许 / 拒绝 / 用户确认
Q->>T: 执行工具
T-->>Q: progress
T-->>Q: success / error / cancelled
Q->>Q: 封装 tool_result
Q->>Q: 收齐本批结果并追加附件
Q->>M: assistant tool_use + user tool_result
M-->>Q: 继续调工具或给出最终文本
tool_result 为什么属于 user 角色
Messages API 把工具结果建模为“外部世界对 assistant 工具请求的回答”:
- assistant 消息包含
tool_use; - 后续
user消息包含对应的tool_result; tool_use_id把两者严格关联。
这不代表工具结果是用户手写的,而是 API 用角色表达这一次外部观测进入对话。
工具输入错误也必须返回 tool_result
工具名不存在、输入不符合 schema、权限被拒绝、执行抛出错误,以及工具被取消,都不应让协议断裂。它们通常会被封装成错误类型的 tool_result。
模型因此能看见失败事实,重新选择参数、改用其他工具或向用户说明。如果只抛出本地异常,下次请求会留下一个永远没有结果的 tool_use。
工具配对有三层保障
正常执行层
每个工具调用都会产生携带原始 ID 的结果,无论成功、失败还是权限拒绝。
中断与恢复补洞层
如果模型流在产生工具块后中断,或者模型 fallback 需要丢弃当前尝试,查询循环会为缺失结果的调用生成可识别的错误结果或墓石。
API 边界修复层
发送前再做一次双向检查:
- 工具调用没有结果,插入合法的错误结果;
- 结果引用了不存在的调用,移除孤立结果;
- 重复的工具 ID 被去重;
- 会话从半个工具回合恢复时,清理开头的孤立结果并保持角色交替。
对需要严格训练轨迹的条件启用路径,可以改为发现不配对就直接失败,而不注入合成上下文。
三层保障的精妙在于:正常路径保持语义完整,异常路径保持对话可恢复,API 边界则保证不把非法协议发出。
进度不等于工具结果
长时间工具可以持续产生 progress,用于 UI、后台任务或 SDK 观察。但模型的下一轮决策需要的是终态 tool_result。
进度和结果分开,可以让本地系统实时可观测,又不会把大量短暂日志当成模型永久历史。
工具之后才能吸收新附件
任务通知、新用户队列输入、文件变化和新 MCP 状态可以在工具执行期间出现,但必须等本批 tool_result 收齐后才追加。
这样既能在同一 agentic turn 中吸收新事件,又不会把普通 user 内容插进一组工具结果之间。
源码定位
- 流式响应聚合:
src/services/api/claude.ts - 工具发现与回合:
src/query.ts - 工具执行:
src/services/tools/toolExecution.ts - 流式工具执行:
src/services/tools/StreamingToolExecutor.ts - 工具配对修复:
src/utils/messages.ts
并行、取消与终止
一条 assistant 响应可以同时包含多个工具调用。如果所有工具都完全串行,读三个独立文件也要等三次;如果所有工具都盲目并行,两个写操作又可能相互覆盖。
因此 Claude Code 不是只有“是否并行”一个开关,而是为每种工具定义并发安全性、中断语义和上下文更新边界。
两种工具调度路径
| 路径 | 启动时机 | 并发原则 |
|---|---|---|
| 流式工具执行 | 一个 tool_use 内容块完成后立即尝试 | 可并发工具可以边采样边运行;非安全工具必须独占 |
| 响应后批量执行 | 整条模型响应完成后 | 连续安全调用成批并行,非安全调用分开串行 |
流式路径是条件启用的能力,而且在一次查询开始时快照。这避免流正在运行时,功能开关突然变化,导致前半批与后半批使用不同调度语义。
并发安全是工具协议的一部分
可并发工具通常是相互独立的读取或搜索。非安全工具通常可能修改文件、工作目录或共享运行时状态。
并发分类必须来自工具本身,不能靠模型猜测。模型可以在一条响应里提出多个意图,但本地调度器才知道哪些可以同时运行。
工具还可以带来上下文修改,例如更新工作目录或读取缓存。这类修改必须有可序列化语义,否则并发完成顺序会让最终状态不可预测。
进度通道和结果通道分开
流式工具执行器会分开管理:
- progress:可随时唤醒消费者,提供实时可观测性;
- final result:只在工具进入成功、错误或取消终态时产生,用于下一次模型请求。
分开后,UI 不需要等所有工具完成才更新,模型历史又不会被短暂进度刷屏。
取消是一棵有明确边界的信号树
一个主回合的父取消信号会向下传递到 API 流、工具执行、Hook 和压缩。条件启用的流式工具执行路径还拥有一层兄弟信号,再为各工具建立执行信号:
查询父信号 → 流式批次的兄弟信号 → 每个工具的执行信号
当前可观察语义包括:取消整个用户回合;流式路径中的 Bash 失败取消同批兄弟工具;以及工具自身声明的允许取消或阻塞中断。响应结束后的普通批量执行路径没有 Bash 专属的兄弟取消策略。
这棵树并不提供一个通用的“任意点名单独取消某个工具、其他工具照常继续”控制接口。每工具信号主要用于传播正确的生命周期与清理语义,不能把内部信号层级误读成对外调度能力。
用户中断的两个阶段
模型流阶段
中断后优先停止响应流,但必须先为已经产生的工具意图补齐结果,避免 transcript 留下半个工具回合。
工具阶段
中断后工具执行器会尝试收齐已完成结果,并为未完成调用生成取消结果,然后主循环才以“工具阶段被中断”结束。
工具默认可以声明“阻塞中断”或“允许取消”。这个声明很重要:某些修改了外部状态的动作,在不可预知的中间点强制终止可能比让它完成更危险。
interrupt 与普通取消的区别
当中断原因是新用户消息接管时,运行时可以不再追加一条冗余的“已中断”可见消息,因为下一条用户输入已经表达了接管。
这是一个小但精妙的协议设计:取消不仅是布尔值,还携带意图,从而决定会话应该如何呈现。
从流转为非流的重复副作用风险
条件启用时,流式请求失败可以用同一模型的非流式请求重试。但如果部分流已经启动了工具,新的非流请求可能再次产生同一意图。
运行时可以用墓石清理旧 assistant 尝试、丢弃旧执行器的后续结果,但已经发生的外部副作用无法靠消息撤回。
因此流转非流是一项带明确取舍的恢复策略,并提供关闭边界。这也说明为什么外部副作用最好具有幂等键或重复检测。
终止前的最后检查
工具批次完成不代表回合必然继续。主循环还要检查:
- 取消信号是否已触发;
- Hook 是否要求停止继续;
- Agent 是否达到最大回合数;
- 工具结果是否保持协议配对;
- 是否还有需要在下一迭代注入的附件或队列消息。
并发、取消和终止不是三个独立功能,而是同一调度状态机的三个维度。
源码定位
- 流式工具调度:
src/services/tools/StreamingToolExecutor.ts - 响应后批量执行:
src/services/tools/toolOrchestration.ts - 查询级取消与终止:
src/query.ts - 流式恢复与非流 fallback:
src/services/api/claude.ts - 查询入口功能快照:
src/query/config.ts
错误分类与恢复
一个长时运行的 Agent 必然会遇到网络抖动、限流、模型过载、上下文过长、输出被截断、工具失败和用户中断。
恢复设计的关键不是“多重试几次”,而是先判断失败发生在哪一层,再选择不会扩大副作用的恢复方式。
四层恢复不能混为一谈
| 层级 | 解决什么 | 在同一用户回合内改变什么 |
|---|---|---|
| 请求 retry | 短暂失败、凭证刷新和可在请求层纠正的参数问题 | 保持同一模型,重建允许变化的请求参数后再次尝试 |
| 传输模式 fallback | 流式端点或流程不完整 | 保持同一模型,改用非流式传输 |
| 模型 fallback | 符合策略的连续 529/overloaded_error | 清理失败尝试,改用备用模型重新编译当前请求 |
| 语义恢复 | API 已返回有意义的“过长”、“输出截断”等结果 | 改变历史投影或输出策略后继续循环 |
如果不区分这四层,很容易用请求 retry 反复发送一个确定过长的 Prompt,或用完整回合重跑来处理一次短暂断线。
请求 retry:同一模型内的短暂失败与参数纠正
可重试范围主要包含:
- 连接错误与超时;
- 某些请求冲突、限流和服务器错误;
- 访问令牌撤销、OAuth 或云凭证过期;
- 需要刷新 API 客户端或凭证缓存的情况;
- 快速模式被限流或不受支持,以及旧式输入长度加输出上限超过窗口等可在请求层纠正的参数问题。
重试使用指数退避、上限和抖动,并尊重服务器给出的等待提示。具体次数和等待时间可被运行环境调整,不应被理解为 Agent 语义的固定常量。
后台摘要、标题和分类器不一定与前台主任务使用相同过载重试策略。对次要后台工作立即失败,可以防止过载时这些任务放大服务压力。
流式转非流式:传输恢复不等于换模型
下列情况可以触发同一模型的非流式 fallback:
- 流迭代过程抛出传输异常;
- 流很长时间没有任何新块,且条件启用的 watchdog 主动中止;
- 流结束但没有形成有效 assistant 响应;
- 中间代理不支持当前流式端点。
它的风险是已发生副作用可能重复,因此这项能力可以被显式关闭。消息层能撤销旧 assistant 尝试,却不能撤销已经发到外部系统的动作。
模型降级(fallback):只针对合资格的连续过载
模型过载不会在第一次失败时立即换模型。运行时会先应用请求级退避,只有连续出现符合资格的 529/overloaded_error、存在备用模型,且账户与模型策略允许时,才把当前 Agent 迭代转到 fallback model。模型访问失败、404 或任意“不可用”错误不能被笼统归入这条路径。
切换前还要处理当前尝试中已产生的工具块,避免旧工具结果在新模型响应中突然回流。
API 错误也是一种内部 assistant 消息
多数重试耗尽的 API 错误不会立即穿透为未分类异常,而是被转成带有类型的合成 assistant 错误,例如:
- timeout 与 connection;
- rate limit;
- prompt too long;
- PDF、图片或请求大小错误;
- tool pairing;
- 认证、模型访问、计费与权限错误。
当主循环看见最后一条是 API 错误时,不会再运行 Stop Hook,避免“API 错误 → Hook 要求继续 → 同样错误”的死循环。
恢复中的错误会先被扣留
如果 SDK 消费者一看到 error 就立即关闭,那么 prompt too long、媒体过大和 max output tokens 这些仍可恢复的中间状态,会被误认为最终失败。
因此查询循环会先扣留这些错误,在压缩、清理、升级输出上限或续写确实无法恢复后,才把原错误当作终态向外交付。
这是一个值得复用的精妙设计:内部需要看见恢复信号,外部只应看见最终成功或真正耗尽的失败。
输出被截断的恢复
当模型因输出上限停止时,条件启用的恢复顺序是:
- 当前仍使用默认上限且用户没有显式覆盖时,先扩大当前请求的输出上限;
- 仍然被截断时,把已生成内容保留在历史中,追加“从中断处继续”的 meta user 提醒;
- 续写次数达到上限后,才返回原始截断错误。
这不是重新从头生成,而是把已产生内容作为新的对话事实让模型续写。
prompt too long 与媒体过大不走同一恢复链
prompt too long 的恢复顺序大致是:
- 如果细粒度上下文折叠已有待消费结果,先排空并重试一次;
- 仍失败时,尝试条件启用的响应式压缩;
- 同一用户回合只允许一次响应式压缩尝试,避免无限压缩螺旋;
- 恢复失败时直接交付原错误,不再运行 Stop Hook。
媒体过大在相应构建能力与运行时开关同时启用时,可以走剥离媒体的响应式恢复契约;该条件能力未启用时不能假定存在这条路径。它与普通文本历史折叠是不同的恢复问题。
工具失败通常不属于 API 重试
工具失败已经是一个有语义的外部观测,通常会作为 tool_result 返给模型,让模型决定是修正参数、切换工具还是停止。
把工具失败自动隐藏并在本地重试,可能使模型看不见重要环境变化,也可能重复副作用。
恢复系统的总原则
- 传输错误在传输层处理;
- 合资格的连续模型过载在模型 fallback 层处理;
- 上下文和输出问题在主回合语义层处理;
- 工具失败作为环境事实返回模型;
- 用户中断和 Hook 阻止是控制决策,不被自动重试抵消。
这种分层让每种恢复只改变必要状态,避免一个通用“重来”按钮重复所有计算和副作用。
源码定位
- 请求重试:
src/services/api/withRetry.ts - 流/非流恢复与错误分类:
src/services/api/claude.ts、src/services/api/errors.ts - 模型 fallback 与语义恢复:
src/query.ts - 压缩恢复:
src/services/compact/ - 工具失败投影:
src/services/tools/toolExecution.ts
上下文预算与压缩
Agent 的上下文会随工具回合不断增长。如果只在到达模型硬上限后做一次全文总结,既浪费已经可以清理的大块工具结果,又可能因为 Prompt 已经太大而连总结请求都无法发出。
Claude Code 因此使用一套从便宜、细粒度到昂贵、高粒度的分层上下文管理。
请求前的固定顺序
flowchart LR
A["工具结果大小预算"] --> B["历史截剪"]
B --> C["微压缩"]
C --> D["细粒度上下文折叠"]
D --> E["自动完整压缩"]
E --> F["请求模型"]
F --> G["响应式压缩<br/>只在真实过长后恢复"]
顺序很重要:先让丢失最小的机制尝试释放空间,如果已经足够,就不必过早把整段对话替换成摘要。
第一层:限制工具结果的成本
文件读取、搜索、Shell 和网页结果往往是上下文中增长最快的部分。它们的原始内容对刚完成的决策很重要,但不一定需要永久保留。
因此运行时可以对工具结果设置预算,把过大内容截断、清理或替换为稳定占位说明。目标是保留“曾经执行过并得到某类结果”的协议骨架,而不是永久重复全量数据。
第二层:历史截剪与微压缩
微压缩不是完整对话摘要,而是对可压缩工具结果和思考数据做局部处理。当前源码快照可确认三类边界:
按时间清理旧结果
条件启用时,当主线会话经历长时间空闲且 Prompt cache 已经变冷,可以清理更旧的可压缩工具结果,保留最近若干个。
这个设计的精妙点是利用了“缓存已经冷却”这个时机:既然下一次本来就很难命中旧前缀,便可用更小历史重新开始。
使用 API 缓存编辑
在支持的模型与主线路径中,微压缩可以不改写本地 transcript,而是在下一次 API 请求中要求删除缓存里某些工具结果。只有 API 确认实际删除量后,本地才记录压缩边界。
它把“审计 transcript”和“本次模型实际需要的缓存内容”进一步分开。
API 端上下文管理
条件启用时,请求可携带清理旧思考或工具结果的策略。这与本地微压缩是不同层次:一个决定本地如何投影历史,另一个请求服务器管理已缓存上下文。
某些缓存微压缩与上下文折叠的内部模块不在当前源码快照中,因此本教程只陈述可验证的调用条件、输出契约和状态转移,不推测其内部删取算法。
第三层:自动完整压缩
当便宜清理仍不足以释放空间时,主线可以在达到硬上限之前主动进行完整压缩。
压缩阈值不能等于模型全部上下文窗口。它还要为压缩摘要本身、下一次输出和安全缓冲预留空间。
自动完整压缩还有多个保护:
- 用户可以关闭自动压缩;
- 专用压缩或 Session Memory 查询自身不再递归自动压缩;
- 响应式专用模式或上下文折叠可以抑制主动完整压缩;
- 连续压缩失败会触发断路器,避免每次迭代反复消耗注定失败的 API 请求。
压缩的输出不只是一段摘要
完整压缩后的上下文大致由下列部分重建:
压缩边界 → 摘要消息 → 必须保留的近期消息 → 恢复的附件 → Hook 结果
需要恢复的不只是文件路径,还包括:
- 最近的关键文件上下文;
- 当前 Plan 与 Plan Mode;
- 已调用且仍有效的 Skills;
- 异步 Agent 与任务状态;
- 动态工具、Agent 列表和 MCP 变化。
压缩因此不是“把聊天记录换成一段总结”,而是一次运行状态重建。
压缩摘要也尽量复用主线 Prompt cache
条件允许时,压缩摘要会使用一个受限 Fork,复用主线已渲染的 system、工具和历史前缀。该 Fork 不执行工具,只运行一轮摘要,并且避免为它的私有尾巴写入新缓存。
这里甚至要避免随意改变最大输出,因为输出上限可能间接改变思考预算,从而让本来相同的前缀无法命中缓存。
第四层:真实过长之后的响应式压缩
主动阈值是根据本地使用量预估,可能与提供方的真实接受边界有差异。因此当 API 实际返回 prompt too long 时,条件启用的响应式压缩作为最后恢复层。
它只在真实错误后运行,并且同一用户回合仅尝试一次,防止“压缩 → 仍然过长 → 再压缩”的无限螺旋。
当前源码快照没有包含该模块的内部删取算法,因此只能确认其进入条件、结果形状、单次保护和失败类型。
Prompt cache 与压缩是同一设计问题
压缩会改变历史,而 Prompt cache 依赖字节前缀稳定。因此上下文管理必须同时考虑:
- system 稳定段与动态段的缓存作用域;
- 消息级只保留一个有效 cache marker;
- 普通请求把 marker 放在最后消息,不写入缓存的 Fork 放在最后一个共享前缀点;
- 某些 beta、快速模式和缓存策略在会话内使用粘性快照,避免中途翻转破坏 cache key;
- 动态 Agent 列表、日期变化和 MCP 变化尽量以尾部 delta 表达,而不回头改写旧前缀。
这是整个压缩架构最精妙的地方:它不是单纯追求“Token 更少”,而是在语义连续、协议合法、缓存稳定和恢复成本之间取得平衡。
条件启用的任务 Token 预算如何穿过压缩
当调用方提供 API 任务 Token 预算时,压缩摘要会把大量旧历史隐藏在更小文本后面。如果任务预算只根据压缩后可见 Token 重新计算,就会“忘记”压缩前已经消耗的预算。
因此运行时会在压缩时记住压缩前最后一次 API 给出的权威上下文使用量,从剩余预算中扣除,再在后续请求中显式携带。多次压缩会继续累计,不会重置任务消耗。
压缩后的连续性是重建出来的
自动压缩后仍能继续 Plan Mode、Skills 和异步 Agent,不是因为它们的所有原始文本都还在模型窗口。而是因为运行时在压缩后重新附加仍有效的状态和指令。
这是理解“长会话连续性”的关键:连续性来自被精心选择并重建的状态,不是无限上下文。
源码定位
- 压缩调度顺序:
src/query.ts - 微压缩边界:
src/services/compact/microCompact.ts、src/services/compact/apiMicrocompact.ts - 自动完整压缩:
src/services/compact/autoCompact.ts、src/services/compact/compact.ts - 压缩后消息重建:
src/services/compact/compact.ts - Prompt cache 与缓存编辑:
src/services/api/claude.ts、src/utils/api.ts
内置工具全景
Claude Code 的工具不是一张永远不变的菜单。源码中的工具定义只是能力候选;真正发送给 API 的 tools[],还要经过构建版本、平台、会话形态、功能开关、权限规则、Agent 身份、MCP 连接状态和延迟加载共同裁剪。
因此,理解工具系统要同时回答三个问题:源码里是否存在、当前 Agent 是否看得见、这一次具体输入是否被允许执行。
flowchart LR
A["内置工具候选"] --> D["构建与平台门控"]
B["运行时 MCP 工具"] --> P["工具池装配"]
D --> P
P --> V["会话 / Agent / deny 可见性裁剪"]
V --> L["ToolSearch 常驻或延迟展开"]
L --> API["本轮 API 的 tools[]"]
API --> C["模型提出 tool_use"]
C --> R["参数校验、权限、Hook<br/>本地进程在适用时再受 Sandbox"]
R --> E["执行与 tool_result"]
一张表读懂四种“存在”
| 状态 | 含义 |
|---|---|
| 已定义 | 源码中有工具协议与本地执行能力 |
| 已注册 | 当前构建和平台把它放入候选池 |
| 已暴露 | 当前 Agent 与请求可以看到完整 schema,或可通过 ToolSearch 发现 |
| 可执行 | 本次具体参数通过权限、Hook、Sandbox 与外部系统边界 |
“源码里有”只证明第一层;不能据此推断当前模型一定能调用。
会话、计划与用户沟通
| 工具 | 架构职责 | 可见条件与边界 |
|---|---|---|
Agent(兼容旧名 Task) | 把一个目标交给独立 Agent 上下文,可同步运行、后台运行、恢复,或在团队模式下创建可通信队友;也可请求 worktree 隔离 | 主线程通常可见;普通 Subagent 默认禁止继续递归委派,内部构建和特定进程内队友有窄例外;Agent 类型、后台形态与权限会再次裁剪其能力 |
AskUserQuestion | 用结构化选项向用户收集决策,而不是让模型在文本里假装已经得到确认 | 依赖可交互界面;远程 channel 场景会关闭,普通 Subagent 也默认拿不到;属于可延迟工具 |
SendUserMessage(兼容旧名 Brief) | 在 Brief/助手形态中充当用户主要可见回复通道,并可携带附件 | 不是普通 Claude Code 会话的固定输出方式;只有对应构建能力、用户资格和显式启用条件同时成立才出现。启用后不延迟,因为它是主沟通通道 |
Config | 让 Agent 读取或修改受支持的 Claude Code 设置 | 当前注册表只在内部用户构建中加入;它不是任意配置文件编辑器,也不是外部发行版的通用能力 |
EnterPlanMode | 请求把主会话权限状态切入 Plan Mode | 需要相应交互路径;在无法完成退出审批的 channel 场景中与退出工具一起关闭,避免进入后无法离开 |
ExitPlanMode | 提交计划、请求批准并恢复执行态;队友需要计划审批时可转交团队负责人 | 对主会话通常需要用户交互;队友可走负责人审批路径。它改变权限状态,但“Plan Mode”本身不等同于内置 Plan Agent 的工具白名单 |
StructuredOutput | 在要求 JSON Schema 的非交互请求中,把最终答案收束为唯一的结构化结果 | 不属于普通用户可选工具;只有非交互结构化输出条件成立时才在常规筛选之后追加,并携带该请求专属 schema |
Sleep | 在主动式运行中明确等待下一次唤醒,避免用空文本回合轮询 | 只在主动式/助手相关构建与运行状态中出现;普通交互会话不需要它 |
Skill | 以名字调用已发现的 Skill,把声明式工作流引入当前上下文或独立 Fork | 只接受实际已加载且允许模型调用的 Skill;它不会凭名字执行任意 slash command。Skill 的完整边界见“扩展系统”一章 |
这里最精妙的设计是:用户沟通、计划切换和结构化输出都被建模为工具协议。模型必须显式表达“提问”“交计划”“提交结构化答案”,运行时才有机会在统一的权限和状态机中处理它们。
文件、代码与本地检索
| 工具 | 架构职责 | 可见条件与边界 |
|---|---|---|
Read | 读取文本、图片、PDF、Notebook 等工作区内容,并为后续精确编辑建立文件状态 | 读取范围仍受路径权限、工作目录和文件大小预算约束;读取不是写入授权 |
Edit | 对既有文件做精确替换 | 依赖已经掌握的文件状态,并检查目标是否被外部修改;受写权限、路径与敏感文件规则约束 |
Write | 创建或整体覆盖文件 | 适合新文件或明确的全文替换;同样经过写权限、路径与敏感内容边界 |
NotebookEdit | 以单元格为边界修改 Jupyter Notebook | 与普通文本编辑分离,避免把 Notebook 的结构误当成纯文本;属于可延迟工具 |
Glob | 按路径和文件名模式寻找候选文件 | 只回答“哪些路径匹配”,不搜索文件内容;在带嵌入式搜索能力的内部构建中可被 Shell 内的快速搜索替代,因此不一定注册 |
Grep | 按正则搜索文件内容,可限定类型、目录和输出形态 | 只回答“哪些内容匹配”,不负责互联网或工具发现;与 Glob 一样可能被内部嵌入式搜索替代 |
LSP | 通过语言服务器获取定义、引用、符号、悬停与调用层次等语义信息 | 只有环境开关允许且语言服务器已连接时才可见;它是语义索引,不是纯文本搜索的替代品 |
文件工具共享的是工作区边界,却故意拆成不同意图:发现路径、搜索内容、读取上下文、局部修改、整体写入、结构化 Notebook 修改。拆分让权限提示、审计记录和模型选择都更准确。
Shell、网页与运行环境
| 工具 | 架构职责 | 可见条件与边界 |
|---|---|---|
Bash | 执行必须通过 Shell 完成的系统命令,可在条件允许时转为后台任务 | 默认本地 Shell 能力;具体命令还要经过命令语义、规则权限和 Sandbox。后台任务关闭时,后台参数不会暴露给模型 |
PowerShell | 在 Windows 上提供与 PowerShell 语义和权限规则一致的命令通道 | 仅 Windows;内部构建默认启用但可关闭,外部构建需显式启用。它不是 macOS/Linux 上 Bash 的别名 |
WebFetch | 已知 URL 后抓取并提炼页面内容 | 是“取回指定页面”,不是搜索引擎;私有或需登录页面应优先使用对应 MCP。主机权限仍可要求确认;属于可延迟工具 |
WebSearch | 在外部互联网中寻找当前信息 | 依赖 API 提供方与模型是否支持服务端网页搜索;第一方、受支持的 Vertex/Foundry 路径可用,其他提供方不应假定存在;属于可延迟工具 |
REPL | 在内部 REPL 形态中用一个受控入口包裹 Bash、文件和检索等原语 | 只在内部用户构建且 REPL 模式启用时注册;启用后原始 Bash、Read、Edit、Write、Glob、Grep、NotebookEdit、Agent 会从模型直连工具中隐藏,避免两套入口并存 |
专用工具优先于 Shell,不只是使用体验问题。专用工具有更窄的输入协议、更清晰的权限语义和更稳定的结果结构;Shell 是无法被专用能力表达时的通用逃生口。
任务、后台进程与 Agent Teams
任务体系有两层,不能混为一谈:一层是 Agent 自己的工作清单,另一层是已经启动的后台执行单元。
| 工具 | 架构职责 | 可见条件与边界 |
|---|---|---|
TodoWrite | 维护旧版的会话内待办清单 | 仅在新版 Task 列表未启用时出现 |
TaskCreate | 在新版任务列表中创建带主题、描述、状态和依赖关系的任务 | 新版 Task 模式启用时出现;交互模式默认使用,新版能力也可在非交互模式显式开启 |
TaskGet | 按任务 ID 读取完整任务 | 同上;只读 |
TaskList | 查看任务列表与阻塞关系 | 同上;只读 |
TaskUpdate | 更新状态、负责人、依赖与元数据,也可删除任务 | 同上;在 Agent Teams 中还承担认领任务和通知新负责人的协作语义 |
TaskOutput | 读取后台 Agent 或 Shell 任务的状态和输出 | 外部构建仍保留兼容入口,但描述已标为弃用,首选直接读取任务输出文件;兼容旧名 AgentOutputTool、BashOutputTool |
TaskStop | 终止正在运行的后台任务 | 兼容旧名 KillShell;只管理已经注册的后台任务,不是任意进程终止器 |
SendMessage | 在 Agent Teams 中点对点或广播消息,并承载关机、计划审批等结构化协议 | 只有 Agent Teams 总开关成立时启用;普通消息与结构化控制消息的路由边界不同,跨会话桥接还要额外授权 |
TeamCreate | 建立团队上下文、负责人身份和共享任务命名空间 | 仅 Agent Teams;创建的是协作平面,不等于立即生成所有队友 |
TeamDelete | 在所有成员结束后清理团队、任务目录和关联 worktree | 仅 Agent Teams;存在活动成员时拒绝清理,防止把仍在运行的协作状态拔掉 |
TodoWrite 与 TaskCreate 等不是同一批工具的两个名字。它们是两套任务模型之间的运行时切换;而 TaskOutput/TaskStop 管的是后台执行生命周期。
Worktree 与调度工具
| 工具 | 架构职责 | 可见条件与边界 |
|---|---|---|
EnterWorktree | 创建隔离的 Git worktree,并把当前会话的有效工作目录切过去 | 当前快照中 worktree 能力全局开启;真正进入仍要求可创建 worktree 的仓库或配置 Hook |
ExitWorktree | 返回原工作目录,并选择保留或清理 worktree | 清理前检查未提交与未合并状态;需要显式确认危险清理,不是简单的目录切换 |
CronCreate | 为当前 Agent 安排一次性或周期性 Prompt;可选择仅会话内或持久化 | 只有 Agent Trigger 构建能力和运行时 Cron 开关都成立才出现;队友创建的任务会回到对应队友 |
CronList | 列出当前可见的定时任务 | 同上;只读 |
CronDelete | 删除指定定时任务 | 同上;只删除当前调度命名空间里的任务 |
RemoteTrigger | 管理并运行远端 Agent Trigger | 需要远端 Trigger 构建能力、运行时开关和远程会话策略同时允许;它管理远端调度,不等于本地 Cron* |
Worktree 是文件系统隔离层,Cron/RemoteTrigger 是时间与唤醒层。它们都改变 Agent 的运行环境,但不会替代权限系统。
MCP 工具与资源工具
| 工具 | 架构职责 | 可见条件与边界 |
|---|---|---|
动态 mcp__服务__工具 | 把 MCP Server 的工具声明适配成 Claude 工具协议 | Server 连接并返回工具后动态产生;通常加完全限定前缀,SDK 的显式 no-prefix 模式是例外。alwaysLoad 元数据可阻止延迟加载 |
mcp__服务__authenticate | 在需要认证而真实工具尚不可用时,向模型暴露 OAuth 启动能力 | 只对处于需认证状态且传输类型支持相应流程的 Server 生成;认证完成后真实工具会替换它 |
ListMcpResourcesTool | 枚举已连接 MCP Server 提供的资源 URI 和元信息 | 只有至少一个已连接 Server 声明 resources 能力时追加;全局只需一组帮助工具 |
ReadMcpResourceTool | 按 Server 和 URI 读取一个 MCP 资源 | 与资源列表工具成对出现;资源是数据对象,不是可执行工具 |
内部 MCPTool 适配模板 | 为动态 MCP 工具提供统一权限、进度、结果和 UI 契约 | 它本身的占位名不会作为普通业务工具发送;每个 Server 工具会覆盖名称、schema 与描述后进入工具池 |
MCP 的关键边界是:Server 决定“提供什么能力”,Claude Code 决定“如何命名、何时暴露、怎样授权、如何接回 tool result”。MCP 不能绕过本地 deny 与 Hook;本地 Sandbox 只约束适用的本地执行路径,远端 Server 的副作用还要依赖 MCP 权限和外部系统自身授权。Server 提供的只读/破坏性注解也只是决策信号,不是最终授权。
ToolSearch:工具目录的搜索入口
ToolSearch 不搜索文件、代码或互联网。它搜索的是当前已知但尚未展开 schema 的工具。
默认策略会把多数 MCP 工具以及标记为可延迟的内置工具保留为名称,把 ToolSearch 本身常驻。模型选中匹配项后,结果中的工具引用使完整 schema 进入后续请求。具体启用还受模型能力、提供方、禁用开关、ToolSearch 自身可见性与模式影响;只有自动模式才主要按工具定义占上下文的比例决定是否延迟。
下一章会单独展开它与 Glob、Grep、WebSearch、MCP resources 的区别。
仅在条件构建中出现的注册项
当前 src/tools.ts 还引用一组按内部用户、环境变量或构建 Feature 裁剪的工具:Tungsten、SuggestBackgroundPR、WebBrowser、OverflowTest、CtxInspect、TerminalCapture、VerifyPlanExecution、Workflow、Monitor、SendUserFile、PushNotification、SubscribePR、Snip、ListPeers。
这些模块没有全部包含在当前源码快照中。能从现有源码确认的是它们的注册条件和在工具池中的位置,不能可靠还原的执行语义不在本教程中推测。TestingPermission 则明确只服务测试环境,用来验证“总是要求权限”的端到端流程,不属于生产能力。
这组条件注册项说明:Claude Code 会在构建期就裁掉不属于某发行形态的能力,而不只是运行时把它们隐藏起来。
三种会让工具池整体变形的模式
简化模式
简化模式把能力缩到 Bash、Read、Edit;若 REPL 模式同时启用,则只保留 REPL 包装入口。协调者模式可额外保留必要的委派和停止能力。
协调者模式
协调者只保留 Agent、TaskStop、SendMessage、StructuredOutput 等编排能力,把实际读写和命令执行交给 Worker。这不是“协调者自觉不写文件”,而是请求中的能力图已经移除了执行工具。
Agent 与后台模式
Subagent 会按自己的权限模式从会话候选重新装配基础池,再按定义、同步/后台身份和团队身份裁剪;只有 Fork 精确复制父工具快照。基础池 MCP 与 Agent 专属 MCP 还有不同装配路径,详见“工具能力图”和“Subagent 构造”。
设计结论
Claude Code 没有把所有动作塞进一个万能终端工具,而是把工具系统分成四层:
- 小而稳定的内置协议;
- 按构建、平台与会话形态裁剪的候选池;
- 由 MCP、Plugin、Agent 与结构化输出动态追加的能力;
- 在每次调用时生效的权限、Hook 与 Sandbox。
这样既能让模型获得开放扩展能力,又不会把“模型看得见工具”错误地等同于“模型拥有执行权”。
源码定位
- 工具注册表与条件门控:
src/tools.ts - 工具公共协议:
src/Tool.ts - 工具池合并、排序与协调者裁剪:
src/hooks/useMergedTools.ts、src/utils/toolPool.ts - Agent 工具裁剪:
src/tools/AgentTool/agentToolUtils.ts、src/constants/tools.ts - 各内置工具:
src/tools/ - Shell 与平台门控:
src/utils/shell/shellToolUtils.ts - MCP 动态工具、认证与资源:
src/services/mcp/client.ts、src/tools/MCPTool/、src/tools/McpAuthTool/、src/tools/ListMcpResourcesTool/、src/tools/ReadMcpResourceTool/ - 结构化输出工具:
src/tools/SyntheticOutputTool/SyntheticOutputTool.ts、src/main.tsx - ToolSearch:
src/tools/ToolSearchTool/、src/utils/toolSearch.ts
搜索体系与 ToolSearch
Claude Code 里有多个名字带“搜索”的能力,但它们搜索的对象完全不同。最常见的误解,是把 ToolSearch 当成代码搜索,或把 WebFetch 当成搜索引擎。
先记住一句话:路径用 Glob,内容用 Grep,语义用 LSP,未知网页用 WebSearch,已知网页用 WebFetch,工具 schema 用 ToolSearch,MCP 数据对象用 resources。
flowchart TD
Q["我缺少什么信息?"] --> P{"本地文件在哪里?"}
P -->|是| G["Glob:按路径模式找文件"]
P -->|否| C{"本地内容在哪里出现?"}
C -->|是| R["Grep:按正则找内容"]
C -->|否| S{"代码符号关系?"}
S -->|是| L["LSP:定义、引用、符号"]
S -->|否| W{"互联网信息?"}
W -->|未知页面| WS["WebSearch:找网页"]
W -->|已知 URL| WF["WebFetch:取网页"]
W -->|否| T{"缺少某个工具的参数协议?"}
T -->|是| TS["ToolSearch:展开工具 schema"]
T -->|否| M["MCP resources:列出或读取外部数据对象"]
七种能力的边界
| 能力 | 搜索空间 | 输入线索 | 返回的是什么 | 不负责什么 |
|---|---|---|---|---|
Glob | 本地目录树 | 路径通配模式、起始目录 | 匹配的文件路径 | 不读取文件内容,不理解符号语义 |
Grep | 本地文件内容 | 正则、文件类型、目录、输出模式 | 匹配行、文件名或计数 | 不发现工具,不访问互联网 |
LSP | 已连接语言服务器的代码索引 | 文件位置、符号或查询类型 | 定义、引用、符号、悬停、调用层次 | 不保证覆盖未被语言服务器索引的文本 |
WebSearch | 外部互联网搜索服务 | 自然语言查询、域名过滤 | 搜索结果与引用内容 | 不保证访问登录后页面,不读取本地文件 |
WebFetch | 一个已知 URL | URL 与提炼目标 | 页面内容的提取结果 | 不替用户发现 URL;不适合认证站点 |
ToolSearch | 当前会话的 deferred tool 目录 | 精确工具名或能力关键词 | 工具引用,使完整 schema 可用 | 不执行命中的工具,不搜索业务数据 |
| MCP resources | 已连接 MCP Server 的资源目录 | Server 名、资源 URI | 资源元信息或资源内容 | 不等同 MCP tool,也不搜索 Claude 的工具 schema |
这张表体现了 Claude Code 的一个核心取舍:不使用一个“大搜索工具”包办所有事情,而是让每种索引都保留自己的权限、结果形态和失败语义。
Glob:先缩小路径空间
Glob 的问题是“哪些文件名或路径符合模式”。它适合在不知道精确文件位置时建立候选集,例如找所有某类配置、测试或组件文件。
它返回的是路径,不是文件内容。找到路径后,通常还要交给 Read、Grep 或 LSP。在包含嵌入式快速搜索的内部构建中,专用 Glob 可不注册;这是能力入口变化,不是路径搜索需求消失。
Grep:在候选内容中找证据
Grep 的问题是“这段文本或正则在哪些文件里出现”。它可以控制搜索目录、文件类型、上下文行、计数和只返回文件名等输出形态。
它最适合回答调用点、配置键、错误信息和文本引用等问题。Grep 不知道某个标识符是否真是语言符号;需要定义、引用或调用层次时,LSP 的语义索引更准确。
WebSearch 与 WebFetch:发现和读取分离
WebSearch 负责从未知信息空间中发现页面,能力是否存在取决于 API 提供方与模型。WebFetch 负责读取已经知道的 URL,并根据 Prompt 提炼内容。
两者分离有两个好处:
- 搜索结果可以保留“为什么选中这个来源”的发现语义;
- 对指定主机的访问许可可以落在
WebFetch的 URL 边界上。
遇到 GitHub、文档系统或企业服务的私有页面,架构上应转向有认证能力的 MCP,而不是让 WebFetch 反复失败。
MCP resources:外部系统的数据目录
MCP Server 可以同时提供 tools、prompts/skills 和 resources。resources 是可列举、可按 URI 读取的数据对象,例如文档、记录或二进制内容。
当至少一个已连接 Server 声明 resources 能力时,Claude Code 才追加一组全局 ListMcpResourcesTool 与 ReadMcpResourceTool。前者发现 URI,后者读取指定 URI;它们不是每个 Server 各复制一套。
资源目录回答“外部系统里有什么数据”,ToolSearch 回答“Claude 当前还能调用什么工具”。两者看起来都有“列出再选择”,但索引对象完全不同。
ToolSearch 解决的是 Prompt 体积问题
每个工具在 API 请求中都需要名称、描述和输入 JSON Schema。连接大量 MCP Server 后,即使本轮一个都不用,完整工具定义也会长期占据上下文,并让工具列表变化更容易破坏 Prompt cache。
ToolSearch 把工具能力拆成两级:
- 初始请求只让模型知道 deferred 工具的名称;
- 模型真正需要时,再取回少数完整 schema。
stateDiagram-v2
[*] --> 候选池
候选池 --> 常驻工具: 非 deferred 或 alwaysLoad
候选池 --> 延迟目录: MCP 或 shouldDefer
常驻工具 --> 本轮请求
延迟目录 --> 名称提醒
名称提醒 --> ToolSearch调用
ToolSearch调用 --> 工具引用
工具引用 --> 已发现集合
已发现集合 --> 后续请求完整Schema
后续请求完整Schema --> 正常工具调用
哪些工具会被延迟
延迟分类先尊重 alwaysLoad:一个 MCP 工具若声明必须常驻,就不会进入 deferred 集合。
除此之外:
- MCP 工具默认可延迟;
- 内置工具只有显式标记
shouldDefer才可延迟; ToolSearch自己永远常驻,否则模型没有入口取回其他 schema;- 作为当前主要交互通道的
SendUserMessage不延迟; - 条件启用的 Fork-first
Agent也可被强制常驻,保证首轮即可委派。
所以,“MCP 一定延迟”也不准确:alwaysLoad 是明确例外。
ToolSearch 何时真正启用
源码把“候选时可能启用”和“发请求时最终启用”分开。
最终决策至少检查:
- 当前模式是始终启用、自动阈值,还是标准全量工具模式;
- 模型是否支持
tool_reference; ToolSearch是否仍在当前 Agent 的工具池中;- 实验性 beta 是否被总开关关闭;
- 第一方代理地址是否明确支持相应协议,或用户是否显式声明支持;
- 自动模式下,deferred 工具的名称、描述和 schema 是否超过上下文窗口的一定比例。
默认模式倾向于始终延迟 MCP 与 shouldDefer 工具;只有自动模式主要依据体积阈值。不能把 ToolSearch 简化成“工具超过某个数量才开启”。
一次 ToolSearch 如何改变后续请求
ToolSearch 支持两种查询意图:
- 精确选择:直接指定一个或多个工具名;
- 关键词检索:根据工具名拆词、描述和搜索提示排序,并可要求某些关键词必须命中。
返回值不是业务数据,而是 tool_reference。API 用这个引用展开对应工具的完整定义;本地历史也会扫描这些引用,重建“已经发现的工具集合”。
这意味着工具发现状态不是隐藏的全局变量,而是尽量落在可重放的消息历史里。完整压缩会把压缩前已发现集合带到压缩边界,避免长会话压缩后突然忘记已经加载过的工具。
工具目录变化有两条条件路径
MCP Server 可以在会话中连接、断开或通知工具列表变化。如果每次都回头改写旧 Prompt,缓存前缀会频繁失效。
在内部构建或相应功能开关开启时,运行时会计算 deferred 工具目录的新增与移除,并以持久的尾部提醒表达变化。旧历史保持不动,新请求只看到增量;工具定义缓存也按工具身份复用。
该条件路径未开启时,不会写入这种目录增量附件,而是在每次请求前临时前置当前全部可用 deferred 工具名称。它仍能让模型发现能力,但不应被描述成“目录变化一定被持久化为增量”。
增量路径与动态 Agent 列表、Skill 发现、Memory 附件使用同一原则:**动态事实尽量追加,不重写已经缓存的前缀。**全量临时路径则用更简单的请求时快照换取相同行为可见性。
ToolSearch 不会扩大权限
取回 schema 只把工具从“知道名字”变成“知道怎样提出调用”。它不会:
- 把被 Agent 白名单移除的工具重新加回来;
- 绕过全局 deny;
- 自动批准某个具体输入;
- 绕过 PreToolUse Hook 或 Sandbox;
- 让断开的 MCP Server 恢复连接。
因此工具有三个不同状态:不可见、可发现但未展开、已展开但调用仍需授权。ToolSearch 只负责第二个状态到第三个状态的转换。
设计结论
Claude Code 的搜索体系不是按“命令叫什么”分类,而是按索引对象分类:文件树、文本、语言语义、互联网、已知页面、工具协议和外部资源。
ToolSearch 最值得借鉴的地方,是把开放工具生态的上下文成本从“连接多少就永久支付多少”改成“本轮需要多少才展开多少”,同时用消息历史保持发现状态可恢复。
源码定位
- 路径搜索:
src/tools/GlobTool/ - 内容搜索:
src/tools/GrepTool/ - 语义搜索:
src/tools/LSPTool/ - 网页发现与读取:
src/tools/WebSearchTool/、src/tools/WebFetchTool/ - ToolSearch 协议与排序:
src/tools/ToolSearchTool/ - ToolSearch 模式、阈值、已发现集合与目录增量:
src/utils/toolSearch.ts - MCP 资源发现与读取:
src/tools/ListMcpResourcesTool/、src/tools/ReadMcpResourceTool/ - MCP tools/resources 动态装配:
src/services/mcp/client.ts - deferred 工具提醒:
src/utils/attachments.ts、src/utils/messages.ts - API 工具投影与 schema 缓存:
src/utils/api.ts、src/utils/toolSchemaCache.ts
扩展系统
Claude Code 的扩展能力由六个不同概念组成:Command、Skill、Tool、Plugin、MCP 和 Hook。它们可以被同一个 Plugin 打包,也可能在一次任务中互相调用,但职责并不重叠。
最容易记的边界是:Command 是用户入口,Skill 是可加载的方法说明,Tool 是模型动作协议,Plugin 是分发容器,MCP 是外部能力协议,Hook 是生命周期拦截器。
flowchart TD
P["Plugin 分发容器"] --> C["Commands"]
P --> S["Skills"]
P --> A["Agents"]
P --> H["Hooks"]
P --> M["MCP Servers"]
P --> L["LSP / 设置 / 输出样式"]
U["用户输入 /name"] --> C
C -->|Prompt 型| Q["编译为消息并查询模型"]
C -->|本地型| X["直接改变 CLI / UI 状态"]
S --> ST["Skill 工具或 /skill 调用"]
ST -->|inline| Q
ST -->|fork| F["独立 Agent 上下文"]
M --> T["动态 Tools / Prompts / Skills / Resources"]
T --> Q
H --> E["会话、Prompt、Tool、Agent、压缩等事件"]
E --> Q
六个概念的职责边界
| 模块 | 谁触发 | 核心产物 | 是否直接成为 API tool | 典型用途 |
|---|---|---|---|---|
| Command | 用户输入 slash command,或 CLI 自身 | 本地操作结果、UI,或一组要交给模型的消息 | 否 | 配置、登录、会话管理、把预制 Prompt 送入对话 |
| Skill | 用户通过 /skill,或模型通过 Skill 工具 | 一段带元数据、资源和约束的任务方法 | Skill 本身不直接成为独立 tool;统一经 Skill 工具调用 | 复用领域知识和多步工作流 |
| Tool | 模型生成 tool_use | 一个有名称、描述和输入 schema 的动作协议 | 是 | 文件、Shell、网页、Agent、MCP 等可执行动作 |
| Plugin | 用户/组织安装并启用 | 一组 Commands、Skills、Agents、Hooks、MCP、LSP、设置等组件 | Plugin 本身不是 tool | 分发、版本、依赖和策略管理 |
| MCP | 连接外部进程或服务 | 动态 Tools、Prompts/Skills、Resources | 其 tools 会动态成为 API tools | 接入外部系统和认证数据 |
| Hook | 生命周期事件自动触发 | 允许/拒绝、改写输入、附加上下文、停止、重试或通知 | 否 | 安全策略、自动化、审计与上下文注入 |
这套分层避免了一个常见架构错误:把“扩展包”“模型能力”“用户命令”“生命周期回调”都叫作插件,然后让权限和执行顺序变得不可解释。
Command:CLI 的入口层
Command 首先是 Claude Code 的控制平面。它有三种形态:
- 本地 Command:直接完成无模型参与的本地操作;
- 本地 UI Command:打开交互界面并回传结果;
- Prompt Command:把预制内容、参数和附件编译成消息,再进入查询循环。
因此 /config、/mcp 一类控制命令与 Skill 看起来都使用 slash 入口,但架构语义不同。模型只能通过 Skill 调用允许模型调用的 Prompt 型能力,不能猜测并执行任意内置 Command。
Command 还有独立的可用性边界:认证提供方、交互/非交互模式、功能开关、远程模式和是否对用户隐藏。它们决定“用户能否看到和运行这个入口”,不等同工具执行权限。
Skill:声明式能力包
Skill 本质上是带 frontmatter 的 Markdown 能力包。正文描述如何完成任务,frontmatter 描述何时可见、谁能调用、用什么模型、允许哪些工具、是否 Fork、绑定哪些 Hook,以及适用哪些路径。
Claude Code 不在启动时把所有 Skill 正文塞进 Prompt。发现阶段只把名称、描述和适用条件等轻量元数据暴露给模型;只有被调用或被 Agent 预加载时,正文才进入 Agent 上下文。这样 Skill 数量不会直接变成常驻 Prompt 成本。
stateDiagram-v2
[*] --> 发现来源
发现来源 --> 元数据索引: 读取名称、描述、when_to_use、paths
元数据索引 --> 相关提醒: 自动匹配任务或路径
元数据索引 --> Skill调用: 用户或模型显式选择
Skill调用 --> Inline: 默认
Skill调用 --> Fork: context=fork
Inline --> 当前对话后续回合
Fork --> 独立上下文与预算
Fork --> 当前对话后续回合: 返回摘要结果
元数据索引 --> Agent预加载: Agent frontmatter 声明 skills
Agent预加载 --> Agent初始上下文
Skill 从哪里被发现
当前源码把多个来源汇入统一 Command/Skill 索引:
- 组织托管的 Skills;
- 用户级 Skills;
- 项目及从工作目录向上的
.claude/skills; --add-dir指定目录;- 兼容旧版
.claude/commands; - Bundled Skills;
- 已启用 Plugin 提供的 Skills;
- 条件启用的 MCP Skills;
- 会话中因访问嵌套目录而动态发现的 Skills。
同一个实体可能经符号链接或多条目录路径被发现,加载器按真实路径去重。裸模式会跳过大部分自动目录遍历,只保留显式来源与单独注册的 Bundled 能力;组织策略还可以把 Skills 限制为仅 Plugin 来源。
路径条件与嵌套发现
Skill 可以声明 paths。这类 Skill 启动时只进入候选集合;当 Read、Edit 或 Write 触及匹配文件后才激活。
文件操作还会从目标文件目录向当前工作目录回溯,发现更深层的 .claude/skills。越靠近目标文件的 Skill 具有更具体的作用域。新发现结果通过尾部提醒进入后续回合,而不是回写旧 Prompt。
用户可调用与模型可调用是两条开关
user-invocable 决定用户能否用 slash 入口看到并调用;disable-model-invocation 决定模型能否通过 Skill 工具选择它。
因此可以存在:只给用户的 Skill、只给模型的 Skill、两者都可调用的 Skill。隐藏 UI 不等于禁止模型调用,反之亦然。
Skill 的三种进入 Agent 的方式
| 方式 | 上下文归属 | 正文如何进入 | allowed-tools | Skill frontmatter Hooks |
|---|---|---|---|---|
| Inline 调用 | 当前 Agent | 作为元用户消息加入当前对话 | 追加为当前调用后的权限允许规则 | 在当前会话作用域注册 |
| Fork 调用 | 新的子 Agent | 作为 Fork 的起始任务 | 追加到 Fork 的权限上下文 | 当前快照的 Fork 分支不经过 Inline 的 Skill Hook 注册路径 |
| Agent 预加载 | 新 Agent 的初始上下文 | Agent 定义的 skills 列表并行加载正文 | 不自动应用 | 不自动注册 |
这个差异非常关键。**预加载 Skill 等于给 Agent 教材,不等于执行一次 Skill。**它不会自动获得 Skill 声明的权限,也不会因为正文被放入初始上下文就安装 Skill Hook。
Fork Skill 也不是“大号 Inline”:它选择一个 Agent 定义,在独立上下文和 Token 预算中运行,只把最终结果带回调用方。它适合输出噪音大、步骤多、需要隔离上下文的流程。
allowed-tools 是权限增量,不是能力安装
Skill 的 allowed-tools 会把声明的规则加入该次 Inline 或 Fork 的允许上下文。它解决的是“这个已知工作流被允许使用哪些现有工具”,而不是:
- 向工具池安装一个不存在的工具;
- 让被 Agent 白名单移除的工具重新可见;
- 覆盖更高优先级的 deny;
- 绕过 Sandbox、路径边界或外部服务授权。
这让 Skill 可以携带完成工作所需的最小权限提示,同时仍服从系统的硬边界。
Skill Hooks 是有生命周期的临时扩展
Inline Skill 被实际调用时,它的 Hooks 注册到当前会话作用域,并与 Skill 根目录绑定。Hook 策略可以限制非受信来源注册 Hook;压缩时,已调用 Skill 的必要内容会按 Agent 作用域恢复,防止不同 Agent 的 Skill 状态互相泄漏。
Hooks 不属于 Skill 正文。正文影响模型决策,Hook 在确定事件点影响运行时决策;两者必须分别审计。
Plugin:组件的分发与治理容器
Plugin 的职责不是“运行”,而是把多个组件作为一个可安装、可启用、可升级和可治理的单元交付。
一个 Plugin 可以声明:
- Commands 与 Skills;
- Agent 定义;
- Hooks;
- MCP Servers 与 channel;
- LSP Servers;
- 输出样式;
- 默认设置和用户可配置项。
加载过程先解析 manifest 与约定目录,再合并 marketplace 元数据、启用状态和组织策略。缺少依赖的 Plugin 会从 enabled 集合降级;组织可以强制禁用 Plugin,跨 marketplace 自动依赖还有单独信任边界。
Plugin 组件会带来源命名空间,MCP Server 也会增加 Plugin scope,避免两个 Plugin 的同名 Skill、Command 或 Server 静默覆盖。热重载通过缓存失效重新装配组件,而不是让旧 Hook、旧 MCP 和新命令各自漂移。
MCP:独立于 Plugin 的运行时协议
Plugin 可以携带 MCP 配置,但 MCP 不依赖 Plugin 才能存在。用户、项目、SDK、Plugin 或动态 Agent 都可以提供 MCP Server 配置。
连接成功后,一个 Server 可以贡献:
- 动态工具;
- Prompt Commands;
- 条件启用的 MCP Skills;
- Resources;
- Server instructions 与通知。
需要认证时,真实工具尚不可见,运行时可先暴露该 Server 的认证工具。工具列表变化后会刷新工具池,并通过 ToolSearch 增量提醒模型。
所以 Plugin 解决“如何分发一组能力”,MCP 解决“如何在运行时与外部能力服务协商”。
Hook:不经过模型选择的拦截平面
Hook 在事件发生时自动运行,来源可以是设置、Plugin、Skill、Agent frontmatter 或 SDK 回调。它不是 Tool,模型不能通过编造一个 Hook 名称来调用它。
flowchart LR
I["生命周期事件"] --> M["按事件与 matcher 选 Hook"]
M --> H["命令 / Prompt / Agent / HTTP / 回调 Hook"]
H --> O{"结果"}
O --> A["附加上下文或系统提示"]
O --> P["允许、询问、拒绝或改写输入"]
O --> S["停止、重试、异步唤醒"]
O --> R["替换 MCP 输出"]
Hook 覆盖的事件包括会话开始与结束、用户 Prompt 提交、工具调用前后与失败、权限请求和拒绝、Subagent 开始与结束、压缩前后、通知、MCP elicitation、任务与队友状态、工作目录和文件变化等。
Hook 能做什么
PreToolUse可以建议允许、询问或拒绝,附加上下文,也可改写工具输入;PermissionRequest可以对真正的权限询问给出结构化决定;PostToolUse可以附加结果上下文,对 MCP 结果还可以给出替换值;- 失败、停止、压缩、Subagent 等事件可以阻止继续或补充后续消息;
- 异步 Hook 可以在后台完成并选择是否重新唤醒会话。
Hook 的“允许”不是超级权限。工具仍要经过设置中的 deny/ask 规则;Hook 也不能绕过 Sandbox。这个优先级防止低信任扩展通过一个 PreToolUse 回调推翻管理员策略。
Hook 的作用域与信任
设置级 Hook、Plugin Hook、Skill Hook 和 Agent Hook 会在统一事件总线上合并,但保留来源根目录和会话/Agent 作用域。相同命令只在相同来源上下文内去重,避免两个 Plugin 使用相同模板时互相吞掉。
Plugin-only 或 managed-only 策略会在注册点阻止不受信的 Skill/Agent Hooks,而不是事后笼统禁掉所有运行时 Hook。这样管理员提供的 Plugin Hook 与 SDK 内部 Hook仍能正常工作。
一次 Plugin Skill 调用穿过哪些边界
sequenceDiagram
participant PL as Plugin加载器
participant IDX as Command/Skill索引
participant MD as 模型
participant ST as Skill工具
participant HK as Hook总线
participant Q as 查询循环
PL->>IDX: 注册带命名空间的Skill元数据
IDX-->>MD: 只暴露相关Skill名称与说明
MD->>ST: 选择Skill与参数
ST->>ST: 校验来源、可调用性与执行上下文
alt Inline
ST->>HK: 注册该Skill的会话级Hooks
ST->>Q: 追加正文、附件与权限增量
else Fork
ST->>Q: 创建独立Agent上下文与权限增量
Q-->>ST: 返回Fork最终结果
end
Q-->>MD: 继续正常工具回合
这条链路说明,Skill 不是直接执行 Markdown。它先经过来源发现和调用资格,再选择上下文形态,最后才影响查询循环;Plugin、Hook 和权限各有自己的边界。
设计结论
Claude Code 扩展架构最值得借鉴的不是“支持很多插件”,而是把扩展拆成六个互相正交的维度:
- 入口与 UI 属于 Command;
- 方法与上下文属于 Skill;
- 动作协议属于 Tool;
- 分发与治理属于 Plugin;
- 外部能力协商属于 MCP;
- 生命周期控制属于 Hook。
当这些维度分开后,系统才能分别回答:谁安装、谁看见、谁触发、在哪个上下文运行、获得什么权限、何时被拦截。
源码定位
- Command 类型、可用性与统一装配:
src/types/command.ts、src/commands.ts - slash command 到消息:
src/utils/processUserInput/processSlashCommand.tsx - Skill 来源、frontmatter、路径条件与动态发现:
src/skills/loadSkillsDir.ts - Skill 调用、Inline 与 Fork:
src/tools/SkillTool/SkillTool.ts、src/utils/forkedAgent.ts - Agent Skill 预加载:
src/tools/AgentTool/loadAgentsDir.ts、src/tools/AgentTool/runAgent.ts - Plugin manifest 与组件模型:
src/types/plugin.ts、src/utils/plugins/schemas.ts - Plugin 加载、策略与依赖:
src/utils/plugins/pluginLoader.ts、src/utils/plugins/pluginPolicy.ts、src/utils/plugins/dependencyResolver.ts - Plugin Commands/Skills/Hooks/MCP:
src/utils/plugins/loadPluginCommands.ts、src/utils/plugins/loadPluginHooks.ts、src/utils/plugins/mcpPluginIntegration.ts - MCP 连接与能力适配:
src/services/mcp/client.ts、src/services/mcp/config.ts - Hook 协议与事件编排:
src/schemas/hooks.ts、src/types/hooks.ts、src/utils/hooks.ts、src/services/tools/toolHooks.ts
Memory 体系:五种持久化不能混为一谈
Claude Code 并没有一个笼统的“记忆模块”。它把长期规则、跨会话经验、角色专属经验、当前会话摘要和完整审计记录拆成五条通道。它们都落盘,但作用域、进入模型的位置和保留目的完全不同。
flowchart LR
A["CLAUDE.md<br/>项目与用户规则"] --> U["会话开头的 user 上下文提醒"]
B["自动记忆<br/>跨会话经验"] --> S["系统 Prompt 中的记忆规则"]
B --> U
B --> T["按需的尾部记忆附件"]
C["Agent Memory<br/>某类 Agent 的经验"] --> AS["该 Agent 的系统 Prompt"]
D["Session Memory<br/>当前会话工作摘要"] --> CP["压缩后的摘要消息"]
E["Transcript<br/>完整事件账本"] --> R["恢复、审计与历史检索"]
先看清五条边界
| 机制 | 解决的问题 | 作用域 | 进入模型的位置 | 持久化边界 |
|---|---|---|---|---|
| CLAUDE.md 体系 | Claude 在这个环境里必须怎样工作 | 管理级、用户级、项目级、本地项目级 | 会话开头的 user 角色元提醒;进入 claudeMd 用户上下文 | 独立文件;项目文件可随仓库共享,本地文件只留在本机 |
| 自动记忆 | 从过去会话保留用户偏好、反馈和非代码事实 | 默认按规范化项目根目录;同一仓库的 worktree 共用 | 记忆行为规则在系统 Prompt;索引通常随初始用户上下文,条件启用时相关正文作为尾部附件 | 跨会话目录,直到被更新或删除 |
| Agent Memory | 让某一种 Agent 延续自己的专门经验 | 自动记忆总开关开启后,按 Agent 类型 × user、project 或 local 范围 | 直接附加到该 Agent 的系统 Prompt | 跨该 Agent 的多次运行;不同 Agent 类型彼此分开 |
| Session Memory | 让一条很长的当前会话在压缩后仍可继续 | 当前项目目录 × 当前 sessionId | 平时不逐轮注入;主要在压缩时成为摘要消息 | 随该会话保存,可服务同一会话恢复,不作为未来会话的通用经验 |
| Transcript | 保存实际发生过什么 | 主会话一份,Subagent 各有侧链文件 | 不默认整体发送给模型;恢复时重建消息链,必要时检索 | JSONL 会话账本,受会话持久化与清理周期控制 |
CLAUDE.md 是规则层,不是经验数据库
源码把 CLAUDE.md 家族分为四级:
- 管理级规则:组织或设备统一下发;
- 用户级规则:对该用户的所有项目生效;
- 项目级规则:项目根路径上的
CLAUDE.md、.claude/CLAUDE.md与.claude/rules/*.md; - 本地项目规则:
CLAUDE.local.md,只属于当前用户和当前项目。
发现过程从当前工作目录向上遍历,越靠近当前目录的内容越晚装入、注意力优先级越高。规则还可以通过 @ 引入其他文本文件;带路径条件的规则只在访问匹配文件时按需补入,而不是把整棵目录的规则一次性塞进上下文。
它的注入位置很容易被误解:CLAUDE.md 内容不是默认系统 Prompt 的一个静态段。运行时先把它编译进用户上下文,再变成会话最前部的 user 角色元消息,并包在 <system-reminder> 中。这样它在语义上仍是高优先级环境规则,同时保持系统 Prompt 的缓存边界稳定。
自动记忆是跨会话经验层
自动记忆默认位于配置目录下按项目根路径隔离的 memory/ 目录。项目身份优先使用规范化 Git 根目录,因此同一仓库的多个 worktree 不会形成互不相识的记忆孤岛。
目录采用“索引 + 主题文件”结构:
MEMORY.md只保存简短索引,受行数和字节上限约束;- 具体内容放在独立主题文件中;
- 记忆按语义主题组织,不按聊天时间顺序堆积;
- 当前源码把可保存内容约束为用户信息、反馈、项目上下文和外部参考;能从代码或 Git 直接得到的事实不应重复保存。
自动记忆有两条不同的注入路径:
- “怎样保存、何时读取、过期内容必须复核”等行为规则,放入默认系统 Prompt 的动态记忆段;
MEMORY.md的实际索引通常跟随 CLAUDE.md 一起进入初始用户上下文。条件功能启用时,索引不再常驻,而是先根据当前问题从主题文件中挑选少量相关记忆,再以relevant_memories附件追加到对话尾部。
这意味着自动记忆不是把整个历史知识库永久塞进 Prompt,而是“一个小索引 + 按需召回”。主 Agent 可以直接写记忆;条件启用时,完整回合结束后还会有一个受限 Fork 从新增对话中提取遗漏内容。这个 Fork 复用主会话缓存,但写权限只开放给自动记忆目录,且若主 Agent 已经写过记忆,本轮不会重复提取。
Agent Memory 是角色专属的长期经验
Agent Memory 只有在**自动记忆总开关开启,并且 Agent 定义显式声明 memory**时才成立。只写 frontmatter 不能越过全局关闭状态。目录以 Agent 类型命名,所以同一项目里的“代码审查 Agent”和“测试 Agent”不会共享一锅经验。
它有三种范围:
user:跨项目复用,适合该 Agent 的通用经验;project:位于项目的.claude/agent-memory/,可随版本控制共享;local:位于.claude/agent-memory-local/,只属于当前项目与本机。
它与自动记忆最大的架构差异是注入位置。Agent Memory 的规则和 MEMORY.md 内容在 Agent 构造时一起附加到该 Agent 的系统 Prompt,而不是进入主会话的用户上下文。
如果 Agent 本来使用显式工具 allowlist,构造阶段会补入维护记忆所需的读写能力;这只是 allowlist 补全,后续仍受 disallowedTools、全局 deny 和正常权限边界约束。Memory 声明不能强行恢复被明确禁止的工具。
因此 Agent Memory 绑定的是“角色”,自动记忆绑定的是“用户与项目”。主 Agent 知道某条用户偏好,不代表每种专用 Agent 都应把它写进自己的角色经验;反过来也一样。
Session Memory 是当前会话的滚动工作摘要
Session Memory 保存到当前项目会话目录下的 session-memory/summary.md。它记录正在做什么、关键文件、失败与修正、工作流、结果和工作日志,目的是帮助一条长会话继续,而不是形成跨会话知识库。
在条件功能开启、自动压缩可用且处于主 REPL 线程时,采样后的后台钩子会按上下文增长和工具调用阈值触发一个隔离 Fork。这个 Fork 只能编辑这一份摘要文件,不会污染主 Agent 的读取缓存,也不会在 Subagent 或 teammate 路径上自动运行。
Session Memory 平时不会作为固定块加入每一次 API 请求。它的核心消费点是上下文压缩:运行时等待正在进行的摘要更新,在摘要有效时把它转换成压缩后的 user 摘要消息,再保留边界之后的近期消息、Plan 附件和 Hook 结果。若功能未启用、摘要仍为空、边界无法确认或压缩后仍超预算,则回退到普通压缩流程。
因此它的语义是“这次会话目前进行到哪里”,而不是“以后遇到同类问题都应该记住什么”。
Transcript 是事实账本,不是 Prompt
Transcript 以 JSONL 保存会话消息及其父子关系、模式变化、压缩边界、工作树状态等元数据。主会话写入项目会话文件;Subagent 的侧链写入当前会话目录下各自的 agent-<id>.jsonl,避免把独立执行历史混进主链。
它主要承担三件事:
/resume或会话恢复时重建合法的消息链;- 为历史检索、审计和统计提供原始依据;
- 在压缩后仍保留模型当前窗口之外的完整历史引用。
短暂的工具进度不是 transcript 消息,不应进入长期账本。持久化也可以被关闭;cleanupPeriodDays 控制保留周期,设为 0 时既不再写新 transcript,也会在启动清理旧记录。
不要把 transcript 当作模型每轮都能看到的无限记忆。模型只看到当前请求编译出的消息窗口;想使用旧 transcript,必须经过恢复、搜索或摘要路径重新投影。
这套设计精妙在哪里
五条通道分别回答五个问题:
- 必须遵守什么:CLAUDE.md;
- 跨会话学到了什么:自动记忆;
- 某类 Agent 学到了什么:Agent Memory;
- 当前长会话进行到哪里:Session Memory;
- 实际发生过什么:Transcript。
如果把它们合成一个“大记忆文件”,规则与事实会互相污染,所有 Agent 会错误共享角色经验,长会话摘要会永久化,完整日志还会吞噬上下文。Claude Code 的做法是让每类持久化只在最需要的位置进入模型,并让 transcript 保持为可恢复的事实来源,而不是默认上下文。
源码定位
- CLAUDE.md 分层、发现、条件规则与自动记忆索引:
src/utils/claudemd.ts - 用户上下文与实际
user角色提醒:src/context.ts、src/utils/api.ts - 自动记忆目录、项目根归一化与开关:
src/memdir/paths.ts - 自动记忆 Prompt、索引约束与内容组织:
src/memdir/memdir.ts、src/memdir/memoryTypes.ts - 相关记忆选择与尾部附件:
src/memdir/findRelevantMemories.ts、src/utils/attachments.ts、src/utils/messages.ts - 回合结束后的受限记忆提取:
src/services/extractMemories/extractMemories.ts - Agent Memory 的范围与系统 Prompt 注入:
src/tools/AgentTool/agentMemory.ts、src/tools/AgentTool/loadAgentsDir.ts - Session Memory 的更新、边界与压缩消费:
src/services/SessionMemory/sessionMemory.ts、src/services/SessionMemory/sessionMemoryUtils.ts、src/services/compact/sessionMemoryCompact.ts - Transcript 路径、主链与 Subagent 侧链:
src/utils/sessionStorage.ts、src/utils/sessionStoragePortable.ts
模式、权限、Sandbox 与 Worktree
Claude Code 的安全架构不是一个“是否允许”的总开关,而是四个职责不同的边界。前一层通过,不代表后一层失效;把它们混成一个概念,最容易得出“跳过权限就等于关闭沙盒”或“worktree 就是安全容器”这类错误结论。
flowchart LR
M["模型准备调用能力"] --> V{"1. 工具可见?"}
V -- 否 --> X1["模型没有该工具定义"]
V -- 是 --> P{"2. 参数级权限?"}
P -- deny --> X2["拒绝"]
P -- ask --> H["用户、Hook 或分类器决策"]
P -- allow --> S{"3. Shell 是否进入 OS Sandbox?"}
H -- 允许 --> S
S --> W["4. 当前 CWD 是否是独立 Worktree?"]
W --> E["在最终边界内执行"]
第一层:工具可见性决定模型能提出什么动作
基础工具池先由内置工具、会话运行时已注册的 MCP 工具、平台能力和功能开关组装,再根据主线程或 Subagent、同步或后台、Agent 的 tools 与 disallowedTools、ToolSearch 延迟加载策略进行投影。
Agent 自己通过 mcpServers 引入的专属 MCP 是后置增量:它在上述 Agent 可见性裁剪完成后追加,不再回穿该 Agent 的 allow/deny 或基础池的 blanket-deny 可见性过滤,但具体调用仍受执行时权限与 deny 约束。只有最终进入 API tools 数组的定义,模型才能原生发出对应 tool_use。
这一层属于能力面裁剪:例如内置 Plan Agent 根本看不到写文件工具,后台 Agent 也看不到不适合异步执行的交互工具。它可以显著缩小误用空间,但不能替代权限检查,因为同一个可见工具的不同参数可能具有完全不同的风险。
第二层:权限判断针对一次具体调用
模型给出工具与参数后,运行时才进入参数级权限管线。核心顺序是:
- 校验工具输入;
- 匹配整工具与内容级
deny、ask、allow规则; - 让工具执行自己的语义检查,例如路径边界、Shell 子命令和危险配置文件;
- 应用当前 permission mode;
- 必要时交给用户、PermissionRequest Hook 或条件启用的分类器;
- 只有得到
allow才进入真正执行。
显式拒绝优先于方便模式。当前源码中,内容级 ask、安全路径检查以及必须与用户交互的工具,都位于普通 bypass 快速放行之前。这说明 permission mode 是默认决策策略,不是抹掉所有硬规则的布尔值。
Permission modes 各自只改变一部分策略
| 模式 | 架构语义 | 不代表什么 |
|---|---|---|
default | 按规则自动允许安全调用,其余请求用户确认 | 不等于所有操作都询问 |
acceptEdits | 自动允许工作目录范围内通过安全检查的文件编辑 | 不会自动允许任意 Bash、MCP 或目录外写入 |
plan | 把主会话切到规划状态,并配合 Plan 提醒与计划文件通道 | 本身不会从工具池删除所有写工具,也不是 OS 只读沙盒 |
dontAsk | 把原本需要询问的结果直接转成拒绝 | 不是自动允许模式 |
bypassPermissions | 跳过常规权限询问并快速允许,但仍晚于显式规则、内容级询问、安全检查和强交互要求 | 不会关闭 OS Sandbox |
源码还包含条件开放的 auto:它用分类器处理原本需要询问的动作,并带连续拒绝控制;这不是对外始终存在的稳定模式。bubble 是内部 Agent 权限传递语义,不在用户可选择的模式集合中。
Plan Mode 是一套会话协议,不只是一个枚举值
模型主动调用 EnterPlanMode 时,它走普通权限管线:默认交互策略下通常会询问用户,但整工具 allow 规则或可用的 bypass 策略可以直接允许,因此不能把确认写成工具本身的无条件语义。进入后,运行时切换 permissionMode,并向对话尾部加入 Plan Mode 元提醒;完整提醒之后会改用稀疏提醒,压缩后还会重新恢复。它在 API 中属于 user 角色的 <system-reminder> 内容,而不是替换顶层系统 Prompt。
ExitPlanMode 不同:它承担展示计划和请求批准的显式产品协议。进入时是否弹出权限确认,与退出时的计划审批不是同一层机制。
计划文件是一个受控例外:当前会话对应的 plan 路径被识别为内部可编辑路径,主 Agent 可以逐步写计划;其他文件仍由“只读规划”的提醒约束和正常权限管线共同保护。
Plan Mode 并不是文件系统层面的硬只读:当会话原本具有 bypass 能力时,plan 模式下的权限管线仍可能沿用快速放行。因此“不要改其他文件”首先是一条会话协议,再叠加工具权限;真正的 OS 强制边界仍属于 Sandbox。
flowchart TD
E["进入 Plan Mode"] --> P1["阶段一:理解需求<br/>读代码、提问、并行 Explore"]
P1 --> P2["阶段二:设计<br/>调用一个或多个 Plan Agent"]
P2 --> P3["阶段三:复核<br/>核对关键文件与用户意图"]
P3 --> P4["阶段四:定稿<br/>只把推荐方案写入 plan 文件"]
P4 --> P5["阶段五:ExitPlanMode<br/>请求用户批准"]
P5 -- 批准 --> I["恢复执行模式"]
P5 -- 拒绝或需修改 --> C["留在 Plan Mode<br/>按反馈回到相应阶段"]
标准五阶段
- 初步理解:读取相关代码、寻找可复用模式、澄清需求;这阶段要求只使用 Explore 类型 Subagent,并按复杂度选择最少必要数量。
- 方案设计:把探索得到的文件路径、调用链、约束与需求交给 Plan Agent;复杂任务可以并行获得多个视角。
- 方案复核:主 Agent 自己回读关键文件,检查方案是否符合原始意图,未决问题用 AskUserQuestion 澄清。
- 最终计划:主 Agent 把唯一推荐方案写入 plan 文件,标出关键文件、应复用的现有能力和验证方法。当前源码还通过实验分组控制计划长度与结构,但不改变阶段职责。
- 退出申请:用 ExitPlanMode 展示计划并请求批准。计划批准不能用普通文本或 AskUserQuestion 代替。
条件启用的访谈式实验流
另一条条件路径不强制先集中 Explore、再集中 Design,而是循环执行:
探索少量关键文件 → 立即更新计划骨架 → 遇到只能由用户决定的问题就提问 → 带着答案继续探索
第一轮应尽快形成计划骨架并开始访谈,而不是长时间闭门搜索。只有当改什么、改哪些文件、复用什么以及怎样验证都已明确时,才调用 ExitPlanMode。这个实验改变的是规划编排方式,不改变“只有 plan 文件可编辑、最终必须显式批准”的边界。
permissionMode: plan 与内置 Plan Agent 必须分开
| 对象 | 它是什么 | 工具边界 | 能否写 plan 文件 | 谁负责退出 |
|---|---|---|---|---|
| 主会话 Plan Mode | 主 Agent 的会话状态与工作协议 | 主工具池仍可能包含写工具;提醒与权限共同约束行为 | 可以 | 主 Agent 调用 ExitPlanMode |
| 内置 Plan Agent | 阶段二中负责提出设计的专用 Subagent | 明确移除 Agent、ExitPlanMode、Edit、Write、NotebookEdit,并使用 Explore 工具集 | 不可以 | 返回设计给主 Agent 后结束 |
处于 plan permission mode 的普通 Agent | 权限模式被设为 plan 的 Agent 实例 | ExitPlanMode 可绕过普通 Subagent 与后台工具过滤,以支持 teammate 的批准流;其他能力仍按 Agent 定义裁剪 | 取决于其可见工具与权限,不由 mode 自动决定 | 依具体运行角色而定 |
最关键的结论是:Plan Agent 的只读性来自专门的工具裁剪和系统 Prompt;主会话 Plan Mode 的只读规划来自会话提醒、计划文件例外与权限管线。两者不是同一个机制。
第三层:OS Sandbox 约束 Shell 实际能碰什么
参数权限回答“这次调用是否获准”,Sandbox 回答“获准的 Shell 进程在操作系统层面实际能访问什么”。Claude Code 通过 @anthropic-ai/sandbox-runtime 适配器把配置转换为文件系统读写限制、网络域名规则、代理与 Unix socket 限制,再包装 Bash 或 PowerShell 命令。
平台边界如下:
- macOS 使用系统
sandbox-exec所代表的 Seatbelt 机制; - Linux 与 WSL2 使用 bubblewrap,也就是
bwrap,网络能力还依赖相应代理组件; - WSL1 不受支持;
- 原生 Windows 不支持这套 POSIX 沙盒。若策略要求 sandbox 且禁止非沙盒命令,PowerShell 会直接拒绝执行,而不是静默降级。
沙盒只有在设置已启用、平台受支持、平台允许且依赖检查通过时才真正生效。sandbox.enabled: true 但环境不可用时:
failIfUnavailable: false(默认)会给出警告,命令可能退回非沙盒执行;failIfUnavailable: true把沙盒变成启动硬门槛,REPL 与非交互入口都会拒绝启动。
这一区分非常重要:仅仅在配置中写了“启用”不等于安全边界已经建立,failIfUnavailable 才决定“缺依赖时降级”还是“缺边界就失败”。
为什么 bypass permission 不等于关闭 sandbox
bypassPermissions 只改变第二层的应用级决策。Shell 是否进入沙盒由独立的沙盒选择策略决定,它检查沙盒是否真实启用、该命令是否被排除,以及本次调用是否显式请求 dangerouslyDisableSandbox 且组织策略允许非沙盒命令。
因此可以同时出现“应用权限不询问,但命令仍在 Seatbelt/bwrap 内运行”。反方向也成立:某个命令得到普通权限批准,却可能因为平台或依赖不可用而无法获得 OS 沙盒保护。
条件开启 autoAllowBashIfSandboxed 时,确定会进入沙盒的 Bash 可以减少应用层常规询问;这只是两层之间的联动优化,不是把权限系统与沙盒合并成一个边界。
第四层:Worktree 隔离代码工作区,不隔离操作系统
Worktree 为会话或 Agent 创建独立 Git checkout 和分支,并把其有效 CWD 切到新目录。这样写文件、运行测试和提交默认落在独立工作树里,不直接污染主工作区,也减少并行 Agent 互相覆盖文件的概率。
Claude Code 有两种主要入口:
- 会话级 EnterWorktree/ExitWorktree:整条主会话切入或退出工作树,并同步刷新依赖 CWD 的 Prompt、计划与 Memory 缓存;
- Agent 的
isolation: "worktree":只给该 Agent 一个临时工作树。没有变化时可以自动清理,有变化时保留路径和分支供主 Agent 或用户接管。
删除工作树采用失败关闭:无法可靠判断状态,或发现未提交文件、未合并提交时,不会在没有显式 discard_changes 的情况下删除。
但 Worktree 不是安全沙盒。Worktree 机制本身不会创建新的机器、用户身份或进程安全主体;即使其他编排另起进程,也不是 Worktree 提供的隔离。只要权限与 OS Sandbox 允许,Agent 仍可能访问工作树之外的路径。它隔离的是代码副本、分支和并发写入,不是凭据、网络、进程或整个文件系统。
四层如何共同处理一次危险命令
以 Agent 想在仓库中执行一个会删除文件的 Shell 命令为例:
- Bash 不在该 Agent 的可见工具集合中,调用根本不会被模型生成;
- Bash 可见时,命令字符串仍要经过 deny/ask/allow、危险语义和当前模式判断;
- 调用获准后,若沙盒真实可用,操作系统继续限制它可写的路径和可访问的网络;
- 若 Agent 位于 worktree,仓库内允许发生的修改落在独立 checkout,而不是主工作区。
每层防御一种不同失败:能力暴露过宽、参数过险、进程越界、并发工作区污染。缺少任何一层,都不能靠另一层完整替代。
源码定位
- 主工具池与 Agent 工具裁剪:
src/hooks/useMergedTools.ts、src/utils/toolPool.ts、src/tools/AgentTool/agentToolUtils.ts - 权限模式定义与切换:
src/types/permissions.ts、src/utils/permissions/PermissionMode.ts、src/utils/permissions/permissionSetup.ts - 参数级权限顺序与 bypass 边界:
src/utils/permissions/permissions.ts、src/utils/permissions/filesystem.ts - Plan Mode 完整提醒、五阶段、访谈流与稀疏提醒:
src/utils/messages.ts、src/utils/planModeV2.ts - Enter/Exit Plan 的状态转换:
src/tools/EnterPlanModeTool/EnterPlanModeTool.ts、src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts - 内置 Plan Agent 的独立工具边界:
src/tools/AgentTool/built-in/planAgent.ts - Sandbox 配置转换、平台与依赖判断:
src/utils/sandbox/sandbox-adapter.ts、src/entrypoints/sandboxTypes.ts - Shell 是否进入 Sandbox:
src/tools/BashTool/shouldUseSandbox.ts、src/tools/PowerShellTool/PowerShellTool.tsx - Worktree 生命周期与安全删除:
src/utils/worktree.ts、src/tools/EnterWorktreeTool/EnterWorktreeTool.ts、src/tools/ExitWorktreeTool/ExitWorktreeTool.ts - Agent 独立 worktree:
src/tools/AgentTool/AgentTool.tsx、src/tools/AgentTool/runAgent.ts
Subagent 的构造
Subagent 不是主 Agent 换一段人设后继续说话,而是从主运行时派生出的另一条 Agent 查询链。它复用必要的基础设施,但重新决定 system、messages、tools、状态所有权和终止方式。
先区分类型、实例和任务
| 概念 | 回答的问题 | 是否可复用 |
|---|---|---|
| Agent 类型 | 这个角色负责什么、允许什么 | 可创建多个实例 |
| Agent 实例 | 这一次真实运行是谁 | 每次独立 agentId |
| 运行任务 | 主线现在如何等待、观察或停止它 | 可前台,也可后台 |
同一 Explore 类型可以并发运行多个实例;同一实例从前台转后台,也没有因此变成另一种 Agent 类型。
普通 Subagent 的构造链
flowchart LR
A["Agent 工具收到委派"] --> B["选择 Agent 类型"]
B --> C["生成独立 agentId"]
C --> D["选择 system 与模型"]
D --> E["重新装配并裁剪 tools"]
E --> F["新建局部状态<br/>按运行路径复制必要快照"]
F --> G["以任务 Prompt 开始新消息链"]
G --> H["独立 query 循环"]
H --> I["结果成为父级 tool_result"]
H --> J["sidechain transcript"]
这条构造链解决了两个矛盾:既要让 Subagent 使用同一套文件、认证和执行基础设施,又不能让它直接改写主 Agent 的对话和当前工具状态。
system:使用角色规则,不复制主线全文
普通 Subagent 使用所选 Agent 定义的 system,并补充它自己的环境上下文。Explore、Plan 等内置角色还可采用更瘦的上下文策略,避免把与只读任务无关或可能过时的启动快照带入。
它不是主 Agent system 的字节级副本。普通 Subagent 优先获得职责清晰的独立上下文,而不是优先命中主线 Prompt cache。
messages:只接收任务,不共享主对话
普通 Subagent 默认从委派任务开始,不共享主对话,也不继承主 Agent 后续收到的新消息。它仍可重新获得当前目录适用的 CLAUDE.md、日期和环境事实,因此“不继承主对话”不等于“没有项目上下文”。
以下内容可在它自己的消息链中增量出现:
- SubagentStart Hook 提供的附加上下文;
- Agent 定义预加载的 Skills;
- 路径触发的嵌套指令与 Memory;
- 工具结果、任务通知,以及在该实例可由注册名或合法
agentId寻址时专门发给它的消息。
父级只在自己的 transcript 中保留 Agent 工具调用、进度和最终结果,不把子链的全部思考与工具历史平铺进主对话。
tools:重新装配,不照抄父级
普通 Subagent 会按自己的权限模式重新获得候选池,再应用内置 Subagent 边界、Agent tools / disallowedTools 和前后台规则。因此父级看得见某个工具,不代表子级也看得见;父级工具被临时裁剪,也不必然改变 worker 的候选来源。
同步 Subagent 可以使用比后台 Subagent 更宽的交互能力;后台实例会进一步移除需要主 UI、递归扩张或主线控制权的工具。Agent 自己声明的 MCP Server 则走专属后置增量路径,仍受调用时权限控制。
读取状态:普通 Agent 从空开始,Fork 才复制父级快照
普通 Subagent 不带父级消息前缀,因此从空的读取文件状态开始;它在自己的查询链中重新建立“读过什么、之后是否变化”。这避免父 Agent 的读取事实被误当成子 Agent 已经亲自观察过。
它的内容替换状态仍从父上下文复制成独立容器,但因为普通 Subagent 没有父级消息中的工具调用标识,这份快照通常没有对象可匹配。复制容器不等于继承父对话语义。
Fork 不同:它复制父级消息前缀,所以同时复制父级读取状态和内容替换决策,保证旧工具结果在两条链中的投影一致。复制后仍各自推进,不共享同一个可变容器。
它还会新建:
- Agent 身份与查询追踪链;
- 嵌套 Memory、Skill 发现和调用状态;
- 专属消息数组与 sidechain transcript;
- Agent 自己创建的 MCP 连接与清理责任;
- 工具拒绝计数、进度和局部取消资源。
主线 UI 控制回调通常不向后台 Agent 开放,但任务注册仍必须能到达根任务表,否则后台 Shell 或 Agent 将无法被观察和终止。这是“隔离普通状态、保留生命周期控制”的精妙分层。
同步与后台改变的是调度所有权
同步 Subagent
主 Agent 的当前工具回合等待它结束,结果直接成为 Agent 工具的 tool_result。它通常与父回合共享取消命运,也可以把权限确认冒泡到交互界面。
后台 Subagent
主 Agent 先得到任务标识,随后通过任务通知、TaskOutput 或定向消息取得进展和结果。定向消息要求目标仍能由注册名或合法 agentId 解析;运行中进入内存待处理队列,停止后只有 sidechain transcript 仍存在时才能尝试续跑。后台任务使用独立取消器;主线一次 ESC 不会自动杀死它,需要显式 TaskStop 或生命周期清理。
前台任务被转为后台时,当前源码的可观察语义是以原始任务参数重新启动后台查询,不是把正在生成的内存上下文无缝搬过去。因此“转后台”不应被理解为冻结并迁移同一个调用栈。
Fork 是另一种构造策略
Fork 仅在构建能力开启、交互入口且非协调器等条件满足时存在。它的目标不是角色隔离,而是复制主线请求前缀并最大化缓存复用:
- 优先复用父级已渲染的 system 字节;如果该快照缺失则回退重建,此时字节同一性只是最佳努力;
- 复制父级历史和当前完整工具批次;
- 精确继承 tools、模型、thinking 与交互标志;
- 克隆读取和内容替换决策,分叉后各自推进;
- 追加只属于当前 Fork 的末尾任务指令。
Fork 不等于共享 live conversation。创建以后,父级新消息不会自动流入 Fork,Fork 的新消息也不会写回父级历史。它共享的是创建时前缀,不是持续可变状态。
作用域化 CWD 与 Worktree 是两件事
普通 Subagent 与 Fork 默认仍操作同一 checkout。显式 CWD 覆盖只改变该 Agent 异步链看到的有效目录,目标可以是同一 checkout 的子目录或任意已有目录;它不会创建工作副本。
只有 isolation: worktree 才创建另一 checkout,并沿该 Agent 的异步链把 CWD 覆盖到那里。它不会自动复制进程、认证、MCP、环境变量或外部服务。任务结束时,无变化的临时工作树可清理;存在修改、提交或无法安全判断时应保留并把路径返回父级。
Fork 叠加 Worktree 时,继承前缀里的路径和读取快照仍来自父 checkout,因此运行时会追加路径翻译和重新读取提醒。Worktree 提供文件副本,不能让旧上下文天然变成新副本的事实。
结果、记录与清理
一次 Subagent 结束时需要完成三件事:
- 将最终文本、错误或取消事实投影为父级可消费的结果;
- 把完整子链写入父 session 下独立的 sidechain,以支持查看和条件续跑;
- 清理 Agent 专属 MCP、Skill 调用状态、Hook、取消监听和临时 worktree。
父级得到的是经过边界收敛的结果,不是子 Agent 整个内存堆。这使委派既可审计,又不会把主上下文迅速撑大。
源码定位
- Agent 选择与调度:
src/tools/AgentTool/AgentTool.tsx - Subagent 构造与生命周期:
src/tools/AgentTool/runAgent.ts - 工具裁剪:
src/tools/AgentTool/agentToolUtils.ts、src/constants/tools.ts - 状态克隆与 Fork 查询:
src/utils/forkedAgent.ts - Fork 路由:
src/tools/AgentTool/forkSubagent.ts - 后台 Agent 任务:
src/tasks/LocalAgentTask/ - sidechain 与恢复:
src/utils/sessionStorage.ts、src/tools/AgentTool/resumeAgent.ts
多代理协作
Claude Code 的“多代理”不是一个模式,而是几种解决不同问题的机制。理解它们最好的方式,是把执行载体、上下文继承和文件隔离看成三条正交轴。
三条轴决定真实拓扑
flowchart TD
A["执行载体"] --> A1["同进程 query 链"]
A --> A2["tmux / iTerm2 独立进程"]
B["上下文关系"] --> B1["新任务链"]
B --> B2["复制父级前缀 Fork"]
C["文件边界"] --> C1["同一 checkout"]
C --> C2["显式 CWD"]
C --> C3["Worktree checkout"]
“后台”只描述等待方式,“Team”只描述协调域,“Worktree”只描述工作副本。任何一个词都不能单独推出另外两条轴。
六种协作形态解决六类问题
| 形态 | 主要解决的问题 | 默认上下文 | 默认文件边界 |
|---|---|---|---|
| 同步 Subagent | 当前回合中的专门委派 | 新消息链 | 同一 checkout |
| 后台 Subagent | 让主线与长任务在时间上解耦 | 新消息链 | 同一 checkout |
| Fork | 并行利用主线完整前缀与 Prompt cache | 复制父级前缀 | 同一 checkout |
| 同进程 teammate | 长期、有名字的协作者与共享任务协调 | 独立长期历史 | 同一 checkout |
| pane teammate | 独立进程、独立 UI 的长期协作者 | 独立会话 | 默认同一路径 |
| Worktree Agent | 避免多个写任务直接争用同一工作副本 | 取决于所叠加 Agent | 独立 checkout |
普通同步/后台 Subagent、Fork 和同进程 teammate 都复用当前进程里的 Agent 查询能力。只有 tmux / iTerm2 teammate 启动完整的新 Claude Code 进程。
TeamCreate 建立协调域,不直接创建队友
Agent Teams 条件启用时,TeamCreate 负责建立:
- team 配置和 leader 身份;
- 共享任务列表的命名空间;
- mailbox 的命名空间与寻址基础;成员 inbox 文件在首次实际写入时按需建立;
- leader 侧的团队控制状态。
真正的 teammate 由带 name 和 team 上下文的 Agent 委派创建。团队拓扑是扁平的:teammate 不能再生成 teammate;同进程 teammate 可在受限条件下使用同步 Subagent,但不能无限生成后台层级。
同进程 teammate:长期 runner,而非一次工具调用
同进程 teammate 有自己的团队身份、累积消息历史、每轮查询和压缩节奏。它使用主 Agent 基础 system 加 teammate 协议补充,再按可选自定义 Agent 收窄职责。
它不继承 leader 对话。初始任务直接交给 runner;后续消息通过共享 mailbox 到达。普通最终文本也不会自动出现在 leader 对话中,teammate 必须用 SendMessage 把需要共享的结论发回。
这种设计刻意避免“所有人的完整历史互相广播”。共享的是任务和必要消息,私有的是每个 Agent 的推理链。
pane teammate:协作协议跨过进程边界
tmux / iTerm2 teammate 是完整新进程,拥有自己的 AppState、查询循环、缓存、MCP 生命周期、Skill 状态、取消器和 transcript。leader 只保存观察与控制镜像。
初始任务不塞进进程启动命令,而是由 mailbox 投递。这使第一条任务和后续协作消息遵循同一寻址协议,也避免把任意 Prompt 暴露在命令行参数中。
新进程独立初始化。spawn 会显式传入已经解析的模型:只有配置选择 inherit 或 teammate 默认策略跟随 leader 时,才使用 leader 当前模型;否则可以是单独指定或默认模型。权限也只转发被支持的选定模式,Plan 约束走独立的 required-plan 协议。必要配置/代理变量按白名单转发,不能笼统说它复制了父进程全部环境。parentSessionId 只建立血缘和记录关联,不会共享 API conversation 或 Prompt cache。
三个“任务”平面必须分开
flowchart LR
R["Runtime task<br/>进程内观察与取消"]
T["Team task list<br/>带锁 JSON 协调工作"]
M["Mailbox<br/>带锁 JSON 传递消息"]
R -. 状态镜像 .-> T
T -. 分配通知 .-> M
M -. 新一轮输入 .-> R
Runtime task
位于各进程的 AppState,用来观察后台 Agent、Shell、远程任务和 teammate,适合 UI 进度与取消。它不是跨进程数据库。
Team task list
位于共享配置目录,TaskCreate / TaskUpdate / TaskList 通过文件锁协调 owner、blocker、完成状态与认领。它才是 team 中可跨进程读取的工作清单。
Mailbox
同样位于共享配置目录,SendMessage、任务分配和 shutdown 协议通过它寻址。读、写和标记已读都要避免并发覆盖;只有消息已提交给 Agent 或可靠进入本地队列后,才应标记已读。
普通后台 Subagent 的定向消息不是 team mailbox:目标能被注册名或合法 agentId 寻址时,运行中使用进程内 pending messages;结束后只有 transcript 仍存在才可尝试 sidechain resume。两者都叫“发消息”,却属于不同持久化平面。
前后台通知遵循优先级
后台 Agent 完成后先把任务置为终态,让等待者立即解锁;分类、Git 检查和通知美化可以随后处理。完成通知进入主命令队列时优先级低于真实用户输入,避免一批后台结果让用户的新需求饿死。
Team mailbox 同样有消费优先级:shutdown 与 leader 指令应高于普通 peer chatter,空闲 teammate 才尝试领取未阻塞任务。
取消有三种粒度
| 粒度 | 影响 |
|---|---|
| 主线回合取消 | 终止当前 leader turn;同步 Subagent 通常随之终止 |
| teammate 当前工作取消 | 停止这一轮,teammate 回到 idle 等待下一条消息 |
| Agent 生命周期终止 | 结束后台任务、runner 或独立 pane |
后台 Subagent 和 teammate 通常拥有独立生命周期,所以 leader 按一次 ESC 不等于全团队关机。Fork 在正常启用后台能力时被强制异步并使用独立取消器;如果全局关闭后台任务而退回同步,它会随父回合取消。跨进程 teammate 的优雅退出还是一个 mailbox 请求/同意协议;强制关闭才由 pane backend 执行。
Worktree 不会由 Team 自动提供
Team 当前创建队友的路径默认使用 leader 的工作目录,不会因为“多人协作”自动建 worktree。多个 teammate 因此可能同时修改同一文件。
需要文件隔离时,必须显式选择 Agent worktree 或其他独立 CWD。Worktree 只隔离 checkout 和分支,仍共享 Git object store、配置目录、网络和外部系统;它不是容器。
值得注意的条件边界
- Agent Teams、Fork、后台自动化和某些 teammate backend 都是条件启用能力;源码存在不代表当前产品形态必定可见。
- 全局关闭后台任务时,原本会异步的路径也可能退回同步。
- SendMessage 对普通后台 Agent 的续跑能力依赖工具可见、目标可寻址,并且停止后的 sidechain transcript 仍存在。
- 同进程 EnterWorktree 改变的是 session 级工作目录,不应拿它代替 Agent 专属的异步 CWD 隔离。
- 从前台转后台是调度重启边界,不是上下文无损迁移。
源码定位
- Team 建立与开关:
src/tools/TeamCreateTool/、src/utils/agentSwarmsEnabled.ts - teammate 创建:
src/tools/shared/spawnMultiAgent.ts - 同进程 runner:
src/utils/swarm/spawnInProcess.ts、src/utils/swarm/inProcessRunner.ts - pane backend:
src/utils/swarm/backends/ - 团队任务:
src/utils/tasks.ts、src/tools/TaskCreateTool/、src/tools/TaskUpdateTool/ - mailbox:
src/utils/teammateMailbox.ts、src/hooks/useInboxPoller.ts - 普通后台任务与消息:
src/tasks/LocalAgentTask/、src/utils/messageQueueManager.ts - Worktree:
src/utils/worktree.ts、src/utils/cwd.ts
主 Agent 与子 Agent 的共享和隔离
多 Agent 最容易产生的误解,是把“同进程”“同工作区”和“同上下文”当成同一件事。Claude Code 实际采用最小共享:共享昂贵且稳定的基础设施,复制需要一致起点的快照,隔离会并发变化的可变状态。
先定义三种关系
- 共享:访问同一个物理资源或可变对象,一方变化可能立即被另一方看见。
- 复制:创建时取得快照,之后各自变化,不会自动同步。
- 独立:由该 Agent 新建并独立拥有。
“都能读同一个文件”属于共享物理资源;“都知道父级已读过该文件”可能只是复制缓存;“都调用同一模型”也不代表共享 API 对话。
完整共享/隔离矩阵
| 维度 | 主 Agent | 同步 Subagent | 后台 Subagent | Fork | 同进程 teammate | pane teammate | Worktree Agent |
|---|---|---|---|---|---|---|---|
| OS 进程 | 主进程 | 共享 | 共享 | 共享 | 共享 | 独立进程 | 与所叠加 Agent 相同 |
| 物理文件 | 主 checkout | 默认同文件 | 默认同文件 | 默认同文件 | 默认同文件 | 默认同路径/同文件 | 独立 checkout,Git 仓储仍有关联 |
| 有效 CWD | session CWD | 默认继承 | 默认继承 | 默认继承 | 默认继承 | 启动时指定,默认同路径 | Agent 异步链覆盖到 worktree |
| 环境变量 | 进程环境 | 共享 | 共享 | 共享 | 共享 | 只保证显式转发的配置集合 | 同进程时共享 |
| API 认证与客户端基础 | 主配置 | 共享基础 | 共享基础 | 共享基础 | 共享基础 | 自己初始化,来源可相同 | 与所叠加 Agent 相同 |
| API conversation | 主查询链 | 独立 | 独立 | 独立链,复制父前缀 | 独立长期历史 | 独立 session | 取决于所叠加 Agent |
| 模型选择 | 主线当前模型 | 按 Agent 策略解析 | 按 Agent 策略解析 | 精确继承父级 | teammate 自身策略 | spawn 解析;仅 inherit/跟随策略才取 leader 模型 | Worktree 本身不改变模型 |
| system Prompt | 主线选择结果 | Agent 专用 | Agent 专用 | 优先复制已渲染父级字节;缺失时重建 | 主 Prompt + teammate 协议 | 新进程主 Prompt + teammate 协议 | Worktree 本身不改变选择策略 |
| 主对话 messages | 原始所有者 | 不共享 | 不共享 | 创建时复制前缀 | 不共享 | 不共享 | 取决于普通或 Fork |
| 工具池 | 主线当前快照 | 重新装配与裁剪 | 再加后台裁剪 | 精确复制父级工具 | 组装后按 teammate 边界裁剪 | 新进程完整组装 | 取决于所叠加 Agent |
| 权限硬约束 | 当前规则 | 从 live 父上下文派生并可向用户确认 | 默认避免弹窗;bubble/显式开放可送父终端 | 常用 bubble;同步回退时服从父路径 | 独立 worker 模式,可向 leader 确认 | 独立初始化,只转发受支持模式 | Worktree 不绕过权限 |
| 一般 AppState 写入 | 主所有者 | 当前同步路径共享父 setter | 隔离为 no-op | 异步时隔离;同步回退时共享 | runner 写根镜像,内部查询受隔离 | 完全独立,leader 仅镜像 | 与执行载体相同 |
| 根 runtime task 注册 | 共享根表 | 可写 | 可写 | 可写 | 可写 | 各进程独立,leader 有控制镜像 | 与执行载体相同 |
| 读取文件状态 | 主状态 | 从空状态开始 | 从空状态开始 | 复制父级快照后独立 | 每轮重新建立,不跨轮保留自身缓存 | 独立进程状态 | 普通 Agent 从空开始;Fork 复制旧 checkout 快照并收到重读提醒 |
| 内容替换决策 | 主状态 | 复制后独立 | 复制后独立 | 精确复制后独立 | 自己跨轮保持 | 独立 | 与对应 Agent 相同 |
| Skill 调用状态 | 以主 agentId 归属 | 同模块但按 agentId 隔离 | 同左,结束清理 | 同左 | 同进程 Map,按身份隔离 | 独立进程 Map | registry 可共享,调用状态仍按 Agent 隔离 |
| 取消信号 | 主 turn 控制 | 随父 turn | 独立 task controller | 异步时独立;后台总开关关闭导致同步时随父 turn | 生命周期与当前 turn 两层 | 独立进程 controller | 与执行载体相同 |
| Team task list | 条件启用时读写 | 普通 Subagent 不以此为主 | 普通后台 runtime task 不等于它 | 同左 | 共享带锁 JSON | 共享同一带锁 JSON | 配置目录仍共享 |
| Team mailbox | leader 地址 | 默认不用 | 可寻址时走普通 pending queue;不是 mailbox | 同左 | 共享 mailbox 文件 | 共享 mailbox 文件 | Worktree 不隔离 mailbox |
| transcript | 主 transcript | 独立 sidechain | 独立 sidechain | 含父前缀副本的 sidechain | runner 历史 + sidechain/UI 镜像 | 独立主 session transcript | metadata 额外记录 worktree |
| MCP | 主连接集 | 可复用父连接并增量创建 | 同左 | 精确工具前缀 | 同进程连接基础可共享 | 自己管理连接生命周期 | 外部能力并未被 worktree 隔离 |
| 外部网络/服务 | 主凭证边界 | 默认共享可达性 | 默认共享可达性 | 默认共享可达性 | 默认共享可达性 | 取决于显式环境与凭证 | 不因 worktree 自动隔离 |
共享的核心:基础设施和物理世界
同进程 Agent 通常共享:
- 进程、环境与模块注册表;
- API 认证基础和父级已建立的部分 MCP 连接;
- 根任务观察能力与同一个物理文件系统;
- 团队配置目录、网络和外部服务可达性。
共享这些资源减少了重复启动和连接成本,但也意味着同 checkout 并发写入、外部副作用和模块级全局状态都必须被谨慎管理。
复制只发生在确实需要一致前缀的状态
普通 Subagent 的读取缓存从空开始;内容替换状态仍复制成独立容器,但由于它没有父级消息中的工具调用标识,这份快照通常无从匹配。Fork 同时复制读取缓存、内容替换决策和消息前缀,三者才真正共同参与父级前缀复用;复制后也不会在并发完成时互相改写。
同进程 teammate 跨轮保留自己的累积消息与内容替换状态,但不会保留每轮产生的读取缓存。Fork 则把复制策略推到极致:system 优先复用已渲染字节,tools、model、thinking 与 messages prefix 尽量一致;system 快照缺失时会回退重建,所以缓存同一性是最佳努力。分叉点以后仍是两条独立消息链。
隔离的核心:会并发变化的控制状态
以下状态必须按 Agent 或 query chain 隔离:
- agentId、查询跟踪和 telemetry 归因;
- 消息历史、压缩边界和请求 ID;
- Skill 调用、嵌套 Memory 与发现状态;
- 当前工具进度、权限拒绝计数和取消资源;
- 专属 MCP、Hook 和 transcript 生命周期。
同进程并不妨碍这种隔离。Agent 身份、teammate 身份和有效 CWD 跟随各自异步链传播,避免多个并发任务争抢一个“当前 Agent”全局变量。
Worktree 的隔离边界最容易被高估
Worktree 隔离的是工作副本和分支,不隔离:
- 进程与环境变量;
- API 认证、MCP 和网络;
- team task、mailbox 与配置;
- Git common objects、refs 和 hooks;
- 数据库、云服务或其他外部副作用。
所以 Worktree 适合减少源码写冲突,不是安全 Sandbox,更不是完整租户隔离。
三个关键推论
- 同一个 checkout 不代表上下文共享:普通 Subagent 可修改主文件,却仍看不到主对话。
- 复制主对话不代表状态共享:Fork 复制前缀,但创建后不自动同步消息、缓存和取消状态。
- 独立进程不代表文件隔离:pane teammate 有独立内存和 API session,默认仍能修改 leader 的同一目录。
这套设计的精妙之处,是让每种隔离都对应一个明确问题,而不为“多 Agent”一次性复制整台运行环境。代价是调用方必须明确自己需要的是对话隔离、生命周期隔离,还是文件隔离。
源码定位
- Subagent 状态所有权:
src/utils/forkedAgent.ts、src/tools/AgentTool/runAgent.ts - Agent 并发身份:
src/utils/agentContext.ts、src/utils/teammateContext.ts - 有效 CWD:
src/utils/cwd.ts - 后台任务:
src/tasks/LocalAgentTask/ - 同进程 teammate:
src/utils/swarm/inProcessRunner.ts - 独立进程 teammate:
src/tools/shared/spawnMultiAgent.ts - 任务与 mailbox:
src/utils/tasks.ts、src/utils/teammateMailbox.ts - sidechain 与团队 transcript:
src/utils/sessionStorage.ts - Worktree:
src/utils/worktree.ts
最值得复用的精妙设计
Claude Code 的价值不在于“接上模型和工具”这一表层组合,而在于它如何处理动态上下文、硬权限、长会话和并发 Agent。下面这些设计可以迁移到其他 Agent 系统。
一、把 Agent 拆成七个可独立演化的输入
Agent 不是一个 Prompt,而是身份、指令、上下文、能力、模型策略、权限和可变状态的组合。任何一项变化都不要求重建其他所有层。
这使“换角色”“换模型”“进入 Plan Mode”“发现新工具”和“恢复会话”成为不同的状态转移,而不是不断复制一份越来越大的 Agent 配置。
二、用四个运行平面隔离职责
控制面决定下一步,模型面负责推理,执行面把工具意图变成现实动作,持久化面保存需要恢复的事实。模型无法直接执行本地操作,UI 状态也不会自动变成 API 字段。
这条边界让安全、恢复和多 Agent 不必侵入模型协议本身。
三、稳定 Prompt 前缀与动态状态分离
身份、原则和通用工具方法尽量放在稳定前部;日期、文件变化、Plan、Skill 和任务消息按时点进入后部。物理缓存作用域再根据默认标记、调用资格和用户 MCP 信任边界决定。
关键取舍是:先为缓存创造稳定前缀,但不把“内容稳定”误写成“一定获得全局缓存”。
四、把上下文当成编译产物
内部 transcript、system context、初始 user context 和运行时附件走不同入口,最终才投影成合法的 system[] 与 messages[]。
这种编译层可以过滤 UI 事件、重排附件、合并角色、修复工具配对并重建压缩后的状态。模型协议因此不需要承担本地运行时的全部复杂度。
五、以真实协议事件驱动状态机
主循环根据是否真正收到 tool_use 决定进入工具回合,而不是只相信 stop reason。流式摘要字段可能迟到或缺失,已经观测到的内容块才是可靠事实。
这是一条通用原则:长连接系统应让状态转移依赖可验证事件,而不是依赖最终统计标签。
六、分开“模型看得见”和“本次允许执行”
工具可见性先缩小模型的决策空间;具体调用再按参数匹配 deny、ask、allow 与工具语义;本地进程在适用时继续受 OS Sandbox。
因此一个 Bash 工具可以可见,但某条命令仍被拒绝。粗粒度能力图提高决策质量,细粒度权限保留灵活性。
七、工具来源允许非对称装配
基础工具池按 worker 自己的模式重建和裁剪;Agent 专属 MCP 在基础裁剪后增量加入;Fork 则为了缓存精确继承父工具快照。
三种路径服务不同目标:普通 Agent 优先能力隔离,专属 MCP 优先角色扩展,Fork 优先请求前缀复用。用一条统一交集表达它们反而会丢失架构事实。
八、条件启用 ToolSearch 时渐进暴露能力
当模式、模型、阈值以及延迟工具或待连接 MCP 等条件满足时,初始请求只常驻核心工具;延迟工具先公布名称与可发现性,模型搜索后,完整 schema 才进入后续请求。未启用时,ToolSearch 自身会被移除,其他工具直接以内联 schema 暴露。
这样既避免海量 MCP schema 挤占上下文,又不牺牲能力发现。ToolSearch 搜的是“可调用能力”,不是文件、代码或网页内容。
九、为工具回合设置三层配对保障
正常执行层为每个 tool_use 产生结果;中断和恢复层为半途调用补错误结果;API 边界再清理孤立、重复或不合法配对。
它把“正常语义完整”“异常可恢复”和“出网协议合法”分开负责,比把所有补救压在执行器里更稳健。
十、错误恢复按失败层分流
连接与限流由请求 retry 处理;流式链路问题可转非流;持续模型不可用才触发模型降级;Prompt 过长、输出截断等语义错误由主回合改变上下文后继续。
分层恢复只替换必要状态,减少重复外部副作用。工具失败则作为环境事实返给模型,而不是默认在本地悄悄重放。
十一、压缩与 Prompt cache 联合设计
Claude Code 先限制大工具结果,再做局部清理和微压缩,随后才完整摘要;真实超限后还有一次响应式恢复。压缩后重新附加仍有效的 Plan、Skill、任务和指令。
压缩不是“历史变短”这么简单,而是在语义连续、工具协议、缓存前缀和恢复成本之间做状态重建。
十二、多 Agent 只共享必要资源
普通 Subagent 不共享主对话,却可使用同一物理文件;Fork 复制父级请求前缀,却不共享分叉后的可变状态;pane teammate 有独立进程,却默认仍可能操作同一目录。
身份与 CWD 跟随异步链,读取缓存和替换决策复制后独立,任务与 mailbox 按跨进程需要持久化。这比“每个 Agent 完整复制一套环境”成本更低,也比“所有 Agent 共用全局状态”更安全。
十三、把观察、协调和消息拆成三个平面
Runtime task 负责进程内进度与取消,Team task list 负责跨进程工作分配,mailbox 负责有地址的消息传递。后台 Subagent 的 pending queue 又与 Team mailbox 分开。
这避免 UI 镜像被误当作任务真相,也避免“发送消息”与“修改任务 owner”耦合成同一种存储操作。
十四、功能存在与运行时可用严格区分
一个工具、Fork、Agent Teams、自动记忆提取或压缩模块出现在源码中,不代表当前构建、平台、模型和会话一定启用。教程和 Agent 本身都必须保留条件语气。
这是最容易忽略、也最值得复用的工程纪律:描述当前真实能力图,而不是源码目录的理论上限。
一张总图
flowchart TD
A["稳定身份与规则"] --> B["按回合编译上下文与能力"]
B --> C["结构化 API 请求"]
C --> D["流式协议事件"]
D --> E["本地权限与工具执行"]
E --> F["结果、附件与持久事实"]
F --> B
B --> G["预算不足时分层压缩"]
D --> H["按错误层恢复"]
E --> I["Subagent / Team 独立查询链"]
I --> F
源码定位
- Prompt 与上下文:
src/constants/prompts.ts、src/context.ts、src/utils/messages.ts - 查询状态机:
src/query.ts - API 请求与缓存:
src/services/api/claude.ts、src/utils/api.ts - 工具能力与权限:
src/tools.ts、src/utils/toolSearch.ts、src/utils/permissions/ - 工具执行与恢复:
src/services/tools/、src/services/api/withRetry.ts - 压缩:
src/services/compact/ - 多 Agent:
src/tools/AgentTool/、src/utils/swarm/ - 持久化:
src/utils/sessionStorage.ts、src/utils/tasks.ts、src/utils/teammateMailbox.ts
最终 API 请求与源码索引
这一章回答最具体的问题:Claude Code 最后到底把什么发给模型,Plan Mode 和工具回合在请求中长什么样,以及这条边界在源码哪里。
先区分三种形状
| 形状 | 用途 | 是否直接出网 |
|---|---|---|
| 内部 transcript | UI、恢复、进度、附件和 sidechain 的完整事件账本 | 否 |
| 归一化消息 | 已过滤、重排、合并并修复工具配对的 messages[] | 作为请求的一部分 |
| Messages API payload | system、messages、tools、模型与控制字段的最终快照 | 是 |
权限规则、Sandbox、读取缓存、任务表、取消器和 UI 状态不会因为影响 Agent 行为,就自动成为 API 顶层字段。
最终出网边界在哪里
主查询循环在 src/query.ts 维护一次 agentic turn;src/utils/messages.ts 投影内部消息,src/utils/api.ts 投影 system 与工具;最终请求在 src/services/api/claude.ts 汇合。
该文件先形成请求参数,再在流式主路径调用 Anthropic Beta Messages API,并在最后一层加入 stream: true。因此:
src/services/api/claude.ts中的anthropic.beta.messages.create(...)是本教程所说的最终 API 调用位置。
它是逻辑出网边界,不等于永远直连固定域名;Bedrock、Vertex、第一方或其他兼容提供方仍可由客户端配置决定物理传输路径。
一份完整请求大致长这样
下面选择一个明确分支作为架构等价的脱敏示意:正常主线程、Prompt caching 开启、ToolSearch 已启用、thinking 开启且模型支持 adaptive thinking。实际文本、模型名、工具数量和字段会随入口、Agent、功能开关和提供方变化。
{
"model": "当前迭代使用的模型标识",
"max_tokens": 32000,
"system": [
{
"type": "text",
"text": "相对稳定的身份、原则与工具使用规则",
"cache_control": {
"type": "ephemeral"
}
},
{
"type": "text",
"text": "当前工作目录、平台、风格、记忆机制等动态系统上下文"
}
],
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "<system-reminder>当前项目适用的 CLAUDE.md 与日期</system-reminder>"
},
{
"type": "text",
"text": "用户的真实需求",
"cache_control": {
"type": "ephemeral"
}
}
]
}
],
"tools": [
{
"name": "Read",
"description": "读取文件",
"input_schema": {
"type": "object",
"properties": {
"file_path": {
"type": "string"
}
},
"required": [
"file_path"
]
}
},
{
"name": "ToolSearch",
"description": "发现延迟加载的可调用工具",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string"
}
},
"required": [
"query"
]
}
}
],
"thinking": {
"type": "adaptive"
},
"metadata": {
"user_id": "包含脱敏设备、账户与会话关联信息的序列化字符串"
},
"stream": true
}
这份示意最重要的不是示例数字,而是四条通道同时存在:system[] 放基础与系统上下文,messages[] 放时序化对话,tools[] 放结构化能力,其他顶层字段控制本次推理与传输。
这里同时选择了缓存开启分支,所以 system 和 message 都展示了缓存标记。正常主请求会在最后一条 message 的最后一个内容块放置恰好一个 message 级断点;不写缓存的 Fork 等特殊路径会把它移到最后一个共享前缀点。system[] 也不是固定两块:它可因归因前缀、默认 Prompt 动静边界和 global-cache 资格形成不同数量的块,缓存作用域可能是组织级、全局级或无标记。上面的两块只是一种代表快照。
若 ToolSearch 条件不成立,示例中的 ToolSearch 会从 tools[] 移除,其他工具直接带完整 schema;若 thinking 关闭或模型不支持,该字段缺席,支持预算式 thinking 的模型则使用带 Token 预算的 enabled 形态。
主 Agent 已处于五阶段 Plan 分支时,请求尾部是什么样
如果主 Agent 的用户回合开始时已经处于标准五阶段 Plan 分支,运行时会收集 Plan 附件,转成 <system-reminder>,再与相邻 user 内容归一化。忽略更早历史后,请求尾部大致是:
{
"role": "user",
"content": [
{
"type": "text",
"text": "用户的真实需求"
},
{
"type": "text",
"text": "<system-reminder>Plan Mode 已激活:除计划文件外只读;按理解、设计、复核、定稿、ExitPlanMode 五阶段完成规划</system-reminder>"
}
]
}
这正是最容易误解的地方:五阶段 Plan 指令没有替换顶层 system,也没有成为一个新的 API role;它仍是 user.content[] 中由运行时追加的文本块。访谈式 Plan 实验会换成迭代规划提醒,Subagent 也有自己的 Plan 提醒,不能把这段五阶段文本视为所有 Plan 请求的固定模板。
完整提醒不会在每个工具回合重复。后续会按人类回合节流,并在完整与稀疏提醒之间切换;压缩后仍会重建有效的 Plan 状态。
模型先调用 EnterPlanMode 时,会多一个工具回合
若当前并不在 Plan Mode,模型先从 tools[] 中调用 EnterPlanMode。它走普通权限管线:默认交互策略下可能询问并获得许可,allow/bypass 条件也可能直接放行。调用成功后,下一次 API 请求的消息尾部大致如下;示例继续选择主 Agent 的五阶段分支:
{
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "请先设计一个可靠方案,不要开始实现"
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_enter_plan",
"name": "EnterPlanMode",
"input": {}
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_enter_plan",
"content": "已经进入 Plan Mode,接下来应专注于探索与设计。\n\n<system-reminder>Plan Mode 已激活及五阶段规划指令</system-reminder>"
}
]
}
]
}
本地权限界面是否要求用户点击,不会被伪装成一句“用户已同意”发给 API。成功工具结果表达的是模式已经进入;Plan 附件在工具批次完成后出现。对上面这条普通字符串结果,纯文本 reminder 会直接拼入同一个 tool_result.content 字符串。其他工具结果形状或折叠受限时,reminder 才可能表现为内容数组或同一 user 消息中的相邻块。
普通工具调用如何形成下一次请求
以 Read 为例,模型先产生结构化意图,本地读取文件,再把结果作为 user 角色的 tool_result 回填:
{
"messages": [
{
"role": "assistant",
"content": [
{
"type": "text",
"text": "我先读取架构入口。"
},
{
"type": "tool_use",
"id": "toolu_read_arch",
"name": "Read",
"input": {
"file_path": "/项目/ARCHITECTURE.md"
}
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_read_arch",
"content": "文件内容或经过预算处理后的读取结果"
}
]
}
]
}
下一轮是否继续不靠“看起来像工具调用”的文本,而是靠真实 tool_use 内容块。权限拒绝、输入错误、取消和执行异常也应形成对应的错误 tool_result,以保持协议闭环。
哪些顶层字段是条件出现的
| 字段 | 何时出现或变化 |
|---|---|
tool_choice | 宿主需要限制或指定工具选择时 |
betas | 当前提供方和功能需要相应实验能力时 |
temperature | thinking 关闭时发送,默认 1 且可覆盖;thinking 开启时省略 |
context_management | 上下文管理能力、beta 和当前策略同时满足时 |
output_config | 推理力度、任务 Token 预算或结构化输出启用时 |
speed | 快速模式当前可用、受支持且未处于冷却时 |
同一模型内的 request retry 可以重新计算最大输出、快速模式等允许变化的动态字段。持续合资格过载触发模型 fallback 时,请求层抛出转移信号,由主查询循环更新模型并重新构造一次模型调用;它不是同一个请求闭包里悄悄改 model。两条路径都不会因此丢弃外层用户回合状态。
全书源码证据索引
| 章节 | 核心证据 |
|---|---|
| 01 总体架构 | src/main.tsx、src/screens/REPL.tsx、src/query.ts |
| 02 Agent 生命周期 | src/bootstrap/、src/query.ts、src/services/tools/ |
| 03 Agent 构造 | src/tools/AgentTool/runAgent.ts、src/utils/forkedAgent.ts |
| 04 系统 Prompt | src/utils/systemPrompt.ts、src/constants/prompts.ts、src/utils/api.ts |
| 05 上下文编译 | src/context.ts、src/utils/attachments.ts、src/utils/messages.ts |
| 06 工具能力图 | src/tools.ts、src/tools/AgentTool/agentToolUtils.ts、src/utils/toolSearch.ts |
| 07 状态模型 | src/bootstrap/state.ts、src/utils/agentContext.ts、src/utils/cwd.ts |
| 08 主查询循环 | src/query.ts、src/QueryEngine.ts |
| 09 API 请求编译 | src/services/api/claude.ts、src/utils/api.ts |
| 10 工具回合 | src/services/tools/toolExecution.ts、src/services/tools/StreamingToolExecutor.ts |
| 11 并行与取消 | src/services/tools/toolOrchestration.ts、src/utils/abortController.ts |
| 12 错误恢复 | src/services/api/withRetry.ts、src/services/api/errors.ts、src/query.ts |
| 13 压缩 | src/services/compact/、src/utils/messages.ts |
| 14 内置工具 | src/tools.ts、src/tools/、src/constants/tools.ts |
| 15 搜索体系 | src/tools/GlobTool/、src/tools/GrepTool/、src/tools/ToolSearchTool/、src/services/mcp/ |
| 16 扩展系统 | src/commands/、src/skills/、src/plugins/、src/services/mcp/、src/hooks/ |
| 17 Memory | src/utils/claudemd.ts、src/memdir/、src/services/SessionMemory/ |
| 18 安全边界 | src/utils/permissions/、src/utils/sandbox/、src/utils/worktree.ts |
| 19 Subagent 构造 | src/tools/AgentTool/、src/tasks/LocalAgentTask/ |
| 20 多代理协作 | src/utils/swarm/、src/utils/tasks.ts、src/utils/teammateMailbox.ts |
| 21 共享与隔离 | src/utils/forkedAgent.ts、src/utils/cwd.ts、src/utils/sessionStorage.ts |
| 22 精妙设计 | src/query.ts、src/services/api/claude.ts、src/tools/AgentTool/ |
| 23 最终 API | src/services/api/claude.ts、src/utils/messages.ts、src/utils/api.ts |
最后用一句话收束
Claude Code 每轮真正发出的不是“用户问题 + 一段大 Prompt”,而是:
经过当前 Agent 身份、上下文编译、能力裁剪、缓存策略和恢复状态共同生成的一份 Messages API 请求快照。
模型返回结构化意图,本地运行时执行并观察,再把结果编译进下一份请求;Agent 就在这个闭环中持续运行。
源码定位
- 请求状态来源:
src/query.ts - 消息归一化:
src/utils/messages.ts - system 与工具投影:
src/utils/api.ts - 最终参数与 Messages API 调用:
src/services/api/claude.ts - Plan Mode 附件和五阶段提醒:
src/utils/attachments.ts、src/utils/messages.ts