目录
来源:从泄露源码看 Agent 设计(技术分享视频字幕)
一句话总结:Claude Code 的全部工程复杂度,本质都在解决两个问题——让模型的自主行为安全可靠,以及在有限上下文约束下把长任务跑完。
开场
Claude Code 的源码不久前被 Anthropic 意外泄露。这是第一次,可以从代码层面去看一个真实生产级 Agent 是怎么搭出来的。
大多数人盯着 prompt 模板、tool 封装、workflow 编排在打转,但代码里展示的工程模式很反直觉:模型不是总指挥,Runtime 才是;Tool 不是函数,而是协议对象;上下文不是”装得越多越好”,而是要主动压缩、按需召回;多 Agent 不是为了角色扮演,而是为了隔离上下文。
那么这些设计背后,是不是有一条统一的主线?
答案是一句话:所有设计都在回答两个问题——安全可靠 + 有限上下文。下面分四层展开。
金字塔总览
mindmap
root((Agent 设计<br/>安全 × 压缩))
一·Runtime 内核
CLI 只是壳
统一 query 循环
七级错误恢复
流式调度抢时间
二·工具与执行
Tool 是协议对象
并发性自我声明
每层都有治理点
默认走慢路径
三·上下文与状态
Prompt 分层缓存
五级压缩梯子
Memory 索引化召回
Transcript append-only
四·安全与扩展
Sandbox + Permission 互补
MCP/Skills 统一治理
多 Agent 三层架构
Sub-agent = 上下文隔离
一、Runtime 内核:CLI 只是入口,所有形态共用一条执行链
1.1 外壳与内核:一切都汇聚到 query 循环
Claude Code 的 CLI 只是入口,真正干活的是后面的本地 Agent Runtime。
- 启动链:
cli.tsx→main.tsx主流程调度 →init/setup安全初始化(确认目录可信、加载用户配置)→launch repl→ 进入query函数 - 启动参数分流:
--print走 headless 后台、--remote走远程模式、默认走完整路径(装配工具池、MCP、内建 skills、Agent 定义) - 关键事实:REPL / headless / SDK 嵌入 / sub-agent,走的都是同一段 query 执行循环,不存在两套实现
金句:CLI 只是壳,内核是一整套本地 Agent Runtime;主 Agent 派出的子 Agent 跑的也是同一个内核,只是上下文做了隔离。
1.2 query 循环:本质上是一个 while
剥掉所有工程外壳,Agent 的核心就是一个反复调用模型的 while:
flowchart LR
A[user message] --> B[API call]
B --> C{stop_reason?}
C -->|tool_use| D[execute tool]
D --> E[append result]
E --> B
C -->|end_turn| F[exit loop]
每一轮,模型决定继续用工具还是停下来。Claude Code 在工程上把每一轮拆成了六个阶段:
| 阶段 | 干什么 |
|---|---|
pre_api_phase | 装配 attachment / skill / memory |
api_request_phase | 流式调用 API,带重试 |
process_response | 把模型输出解析成 transition 状态字 |
execute_tools | 按并发性分批跑,加权限、加 hooks |
stop_hooks | 轮次结束后的钩子,可否决退出 |
next_turn | 判断收工,否则 turn_count++ |
transition 是状态机的语义灵魂:continue / tool_use / end_turn / stop_sequence,每个值对应一种”接下来干嘛”的决策。
1.3 流式调度:跑起来丝滑的底层原因
整条循环是流式生成——模型还在生成,界面已经在显示,下游已经开始消化、开始执行工具,不用等整段回答完。这是 Claude Code “感觉很快”的真正原因,不是模型快,是调度在帮模型抢时间。
1.4 七级错误恢复:长任务不死的真正秘密
Agent 跑长任务会失败:网络断、上下文塞满、话说一半被截、token 预算耗尽……Claude Code 把这些按严重程度分了 L1 ~ L7 七级,每级一条恢复策略:
| 级别 | 场景 | 策略 |
|---|---|---|
| L1 | 流式连接中断 | 切非流式模式重试 |
| L2 | 上下文快塞满 | 预防式折叠旧工具输出(context clipse) |
| L3 | 上下文已爆 | 完整压缩历史成摘要 |
| L4 | 输出预算用完 | 提升单轮输出配额 |
| L5 | 半句被截 | 下一轮明确告诉它”接着说” |
| L6 | stop_hook 拦截 | 外部检查没过,强制再跑一轮 |
| L7 | token 预算耗尽 | 写好状态,给 resume 接口,不要直接抛 |
金句:七级恢复不是处理一两种异常,是把”长任务可能死的所有方式”都铺成了梯子——这才是 Claude Code 能跑半小时长任务的前提。
二、工具与执行:Tool 不是函数,而是协议对象
2.1 Tool 接口:把治理写进协议
很多框架里 Tool 就是”一个函数 + 一段 description,模型说调就调”。Claude Code 把 Tool 做成了协议对象,每个 Tool 必须显式声明:
name/description/inputSchema/call- 是否只读
- 是否并发安全
- 权限检查回调
这些属性直接决定了:能不能并发跑、需不需要走权限弹窗、UI 上如何呈现、能不能修改文件。
默认值是悲观的:未声明 = 非并发 + 非只读 + 必走权限。换句话说,新工具默认走慢路径——串行执行、加权限检查、绕不开治理。只有显式声明”我是只读、并发安全”,才升级到快路径。
金句:安全和并发不是外包给一层管理层去管,而是写进协议里,变成每一个工具作者必须面对的设计责任。
2.2 执行管线:每一层都有治理点
模型输出一段 assistant_message,里面带一个或多个 tool_use block。Query 收到后不是直接调函数,而是走一整条治理链:
flowchart LR
A[tool_use block] --> B[tool_orchestration<br/>按并发性分批]
B --> C[validate_input_schema]
C --> D[tool_hooks<br/>可拦截]
D --> E[permission<br/>可询问/拒绝]
E --> F[tool_core<br/>真正执行]
F --> G[tool_result message]
G --> H[下一轮 API]
关键不在层多,而在每一层都有显式的治理点——可拒绝、可改写、可补充上下文。模型不是想调就调,每一步都能被喊停。
2.3 并发分批:按声明拆批起跑
举例:模型一次输出七个 tool_use(A~G):
| 工具 | 并发性 | 调度 |
|---|---|---|
| read_a, b, grep_c | 只读,安全 | 并发执行 |
| file_edit_d | 写操作,不安全 | 独占串行 |
| glob_e, web_fetch_f | 只读,安全 | 并发执行 |
| bash_g | 不安全 | 独占串行 |
安全的并发起跑,不安全的退化成单条。底层还有一个 streaming tool executor,用 queued / executing / completed / yielded 四状态追踪——边接收 tool_use 边启动,不等模型整段输出完。
三、上下文与状态:在永远有限的窗口里活下来
这是整个 Runtime 的”灵魂层”。所有压缩、记忆、恢复机制,都在回答同一个问题:上下文永远是有限的,怎么用得最值。
3.1 三段式 Prompt:稳定的放前面,会变的放后面
输入侧分成三类来源:
| 类型 | 内容 | 变化频率 |
|---|---|---|
system_prompt | 身份、规则、工具使用方式、输出风格 | 几乎不变 |
user_system_context | CLAUDE.md、当前日期、git 状态 | 每次会变 |
task_specific_prompt | compact / session memory 等后台任务专用 | 任务相关 |
System prompt 内部又切成「静态主干 + 动态边界」两段——前者放前面,后者放后面。为什么这么排? 因为服务器端的 prompt cache 按前缀匹配,前缀越稳定越长,重复请求命中率越高、token 越便宜。排序本身就是工程优化。
身份槽(system_prompt)还有一条优先级链:override > coordinator > agent > custom > default。这条链解释了一件事——sub-agent 不是模型变了,是模型”看到自己是谁”变了。
3.2 五级压缩梯子:每轮都做上下文治理
上下文会被工具输出挤爆。Claude Code 的压缩从轻到重分五级:
| 级别 | 名字 | 做什么 |
|---|---|---|
| 最轻 | snip | 把旧的、价值低的工具输出直接删掉,不做摘要(因为做摘要本身要花 token) |
| 轻 | micro_compact | 清理旧 tool_result,保住前缀稳定不破坏 cache |
| 中 | context_clipse | 把多轮相似操作折叠成一段结构化摘要 |
| 重 | auto_compact | 阈值触发的完整压缩,单独请求模型总结历史 |
| 最重 | session_memory_compact | 复用后台已抽取的 session memory,避免再调一次总结模型 |
两条关键设计:
- 轻量优先:每轮 API 请求前都按梯子顺序尝试,前面的策略已经把占用降到阈值以下,就不进入完整 compact——因为 auto_compact 自己也会失败、成本更高
- 完整压缩 ≠ 删历史:是
summary+post_compact_attachment重建工作台(重新塞回工具声明、文件上下文、计划状态),否则模型会忘了自己刚干到哪一步
3.3 Memory:入口索引,不是正文仓库
memory.md 不是正文仓库,而是入口索引——每条记忆只记一个链接 + 一行描述,整个文件限制在 200 行 / 25 KB 以内。
为什么?因为如果让 memory 直接进 prompt,几次会话下来 prompt 就被长期状态彻底挤满了。所以走的是另一条思路——不是全量注入,而是按需召回:
flowchart LR
A[请求前扫描<br/>memory 文件头元数据] --> B[生成清单]
B --> C[轻量模型<br/>从中选最多 5 个]
C --> D[只把这 5 个的正文<br/>代入本轮请求]
这个流程叫 relevant_recall。Memory 还分三类:auto_memory(用户偏好,后台抽取)/ session_memory(当前会话滚动摘要)/ agent_memory(特定 Agent 长期经验,声明了 memory 字段才有)。
3.4 Transcript:append-only 事件流,不是聊天数组
Claude Code 关掉再打开还能接着跑,靠的是 transcript。它不是聊天记录数组,而是 append-only 事件流:
- 每个事件有唯一
uuid,并指向前一条parent_uuid,靠这两个字段连成一条链 - 主链有边界:只有四类事件能进——用户输入、模型输出、附加内容、系统消息
- 写盘是 append-only + 批量 flush(内存队列 → 后台刷盘)
- 文件尾部周期性重挂元数据(标题、模式、worktree),因为 resume 列表页用的是轻量读取器,只扫尾部一小段窗口
Resume 是一条四步重建管线:
- 加载日志:读整个 JSONL,按事件类型分桶
- 重建主链:从叶子节点沿
parent_uuid一路回溯 - 修复断点:处理被 snip 删掉的中间节点造成的空洞,往前找还活着的祖先重新挂上
- 恢复运行态:plan、文件读取历史、context clipse 状态、Agent 模式,全部挂回内存,再把控制权交还给 REPL
四、安全与扩展:硬边界 + 统一治理
4.1 Sandbox 与 Permission:互补,不是替代
很多人会问:既然有 sandbox,为什么还要 permission?答案是两层各干各的事:
- Permission:回答”这条命令能不能执行”
- Sandbox:处理”已经允许的命令怎么被进一步限制”
每条 bash 命令到达宿主机之前要走四步:
- 逐条路由:判断要不要进沙箱,不符合就走普通路径
- 配置翻译:把用户配置(allowlist、读写目录)翻译成隔离环境真实可执行的限制
- 隔离运行:shell 层把命令包装到隔离环境里跑
- 收尾清理:清掉临时文件、可能影响后续的残留状态
金句:Sandbox 不是软规则,是硬边界。配置不是拿来对照检查,而是被翻译成系统级限制。
另一个重要细节:设置文件 / .claude 目录 / skill 目录列入禁止写入范围——防止一个被攻陷的命令通过修改 Agent 自己的配置来扩大影响。
4.2 MCP 与 Skills:统一治理,不开旁路
外部能力接入有两条路径,但都不开旁路:
| 入口 | 是什么 | 怎么治理 |
|---|---|---|
| MCP | 外部工具入池 | 标准化成内建 Tool 对象,继续走 schema / permission / tool_result 整套链 |
| Skills | 任务协议注入 | markdown + frontmatter + 可选脚本,按需激活而非全量常驻 |
Skills 的激活有三条入口:用户主动调用 / 模型自主选择 / path 字段条件触发(比如改 .py 文件时自动激活 Python 相关 skill)。目标只有一个——避免一堆备用技能把上下文撑爆。
Tool search 还做了延迟加载:工具数量多时(一堆 MCP),不会一次性把所有接口描述塞进 prompt,而是先暴露一个轻量发现入口,按任务需要再展开具体工具边界。这直接降低 prompt 体积、提高 cache 命中。
4.3 多 Agent:三层架构,统一内核
| 层 | 形态 | 解决什么 |
|---|---|---|
| Sub-agent | 单入子链,复用 Runtime,隔离 transcript | 防止主链被工具产物撑爆 |
| Coordinator | 不是多开 sub-agent,而是把主 Agent 的 system_prompt 换掉,让它变成派工人 | 任务编排 |
| Swarm | mailbox + task_board + team_file | 团队协作、空闲自找活 |
Sub-agent 的本质很容易被误解。看起来像”不同角色干不同事”,实际上它几乎只是为了解决有限上下文:
- Sub-agent 调的还是同一个 API,不是别的模型
- 它的 prompt 被大幅精简,中间产生的 tool_use 累积上下文对主 Agent 完全屏蔽
- 主 Agent 只拿到 sub-agent 返回的一条摘要(典型动画:3 条原始消息 → 1 条摘要)
所有这些多 Agent 入口虽然分了层,但底下还是同一个 Runtime、同一条 query 执行链。
五、收口:所有设计只为两件事
把整套架构压成一句话:模型意图 → 受治理的本地行动 → 可恢复的长期任务。
从下到上三层:
flowchart BT
A[状态底座<br/>Memory · Transcript · Resume] --> B[运行治理<br/>Query Loop · Tool · Permission · Sandbox]
B --> C[能力入口<br/>Tools · MCP · Skills · Agent]
再往上抽,所有设计都在回答两个问题:
5.1 安全可靠
让大模型的自主行为是相对安全的——不会随便删你的重要文件、不会搞乱系统。Permission 默认放行但走串行慢路径、Sandbox 把配置翻译成硬边界、控制平面禁止写入、MCP/Skills 不开旁路——这些设计累加起来,才让一个能执行 shell 的 Agent 敢交给普通用户用。
5.2 有限上下文
上下文永远是有限的,而且这个约束不会消失——就算未来上下文真的无限了,模型也很难在那么长的输入里找到重点(密集注意力计算撑不住,也没有重心)。所以:
- Prompt 分静态/动态两段是为了 cache 命中
- 五级压缩梯子是为了在不丢工作台的前提下腾空间
- Memory 做成索引 + 召回是为了不挤占 prompt
- Transcript append-only 是为了任务可恢复
- Sub-agent 几乎只是为了上下文隔离——这点最容易被想成”多角色协作”
一页速览
| 层级 | 内容 | 一句话核心 |
|---|---|---|
| 核心结论 | 安全可靠 + 有限上下文 | Claude Code 全部复杂度都在回答这两个问题 |
| Runtime 内核 | CLI 只是壳 | 所有形态共用 query 执行循环 + 七级错误恢复 |
| 工具与执行 | Tool 是协议对象 | 治理写进协议,默认走慢路径 |
| 上下文与状态 | Prompt / 压缩 / Memory / Transcript | 永远有限的窗口里要活下来 |
| 安全与扩展 | Sandbox + Permission 互补 | MCP/Skills 不开旁路,多 Agent 统一内核 |
一句话收束
模型不是总指挥,Runtime 才是。所有看起来复杂的 Agent 工程,本质都是在用最少的 token 把最多的事情做安全。