从Claude Code源码看Agent设计
tool不是函数的映射,而是标准化的协议对象。同一份定义同时服务:模型调用、权限判断、并发调度、UI呈现、结果回流。
Part 1: Runtime 内核
从CLI到Agent
Claude Code的核心定位:外壳是CLI,但内核是一整套本地的Agent。CLI只是入口,真正干活的是后面这条完整的执行链。
程序入口链图
- CLI.tsx 启动
- 进入main.tsx
- 执行init/up安全初始化
- 接入REPL交互环境
- 进入任务队列执行
分支逻辑:
- main.tsx:负责整体
主流程调度 - 安全初始化:搭建基于ln与react的TTY交互界面
main.tsx伪代码的执行流程:
- 做
安全初始化(确认目录可信、加载用户配置) - 根据启动
参数分流:--print: 走headless后台模式--ratio: 走CI模式--remote: 走远程模式- 默认路径:装配工具池(MCP工具、内置skills等所有Agent定义) → 打开REPL
设计原则:所有运行态(REPL/SDK/后台)共用一条queue内核,不存在两套实现。
不管是在终端里直接聊天,还是通过SDK嵌入别的程序调用,走的都是同一段执行循环。
Queue循环:Agent的本质形态
Agent循环的本质是一个While循环,反复调用模型,模型每轮决定是继续用工具还是停下来:
┌──────────────────────────────────────┐
│ Agent执行循环 │
│ │
│ start → API Core → 判断stop reason │
│ ↓ │
│ 是 tool use? │
│ ↓ ↓ │
│ 是 否 │
│ ↓ ↓ │
│ execute tools end turn │
│ ↓ ↓ │
│ append result │
│ ↓ │
│ 回到 API Core │
└──────────────────────────────────────┘
Claude Code把每一轮拆成了6个阶段,比简单的While循环更复杂:
preAPIface: 装配attachment、skill、memoryAPI request phase: 流式调用API并重试process response: 把模型输出解析成transition状态字execute tools: 按工具并发性分批执行,加权限,加hooksrun stop hooks: 轮次结束后的钩子检查list: 判断要不要收工,否则turn count加一进入下一轮
特性:整条循环底层做成流式生成,模型还在生成时,界面就已经流式显示,下游也开始消化和执行工具,不用等完整输出。
Transition状态字:
continue: 正常进下一轮tool use: 要执行工具turn: 模型主动停止stop sequence: 等等
7级错误恢复机制
Agent跑长任务过程中会有各种失败:网络中断、上下文塞满、模型话说一半被截断、token预算耗尽等
Claude Code把这些失败按严重程度从轻到重分成7级,每一级都有专门的恢复策略:
| 级别 | 场景 | 恢复策略 |
|---|---|---|
| L1 | 流式连接中断 |
自动切成等诊断答完再统一处理的非流式模式,并重试 |
| L2 | 上下文快塞满(预防) | 主动把旧的非重要工具输出折叠成摘要(claude context压缩) |
| L3 | 上下文真的爆了(抢救) | 完整压缩,把历史压成一条摘要加几个关键附件 |
| L4 | 模型输出配额不够 | 把输出配额往上再升,让它接着说 |
| L5 | 仍在半句截断 | 在下一轮明确告诉它"接着说" |
| L6 | stop hook否决退出 | 拦住不让停,执行用户/插件配置的stop hook检查 |
| L7 | 整体token预算耗尽 | 直接抛error,但在都token消耗时写好状态,给一个可以稍后review的接口 |
设计思想:把长任务可能死的所有方式都铺成了梯子,不只是处理一两种异常。这也是Claude Code能跑半小时或1小时以上复杂长任务的前提
Part 2: 状态连续性
Tool协议对象
在很多Agent框架里,tool就是一个函数加一段description,模型说调就调。但Claude Code把tool做成了一个协议对象
interface Tool {
// 基本属性
name: string;
description: string;
input_schema: any;
// 扩展函数
execute: Function;
// 关键属性(必须自己声明)
read_only?: boolean; // 是否只读
concurrency_safe?: boolean; // 是否并发安全
permission_callback?: Function; // 权限检查回调
}
tool_build_factory是这套协议的工厂
设计:
- 默认:非并发、非只读、非破坏性,权限默认放行
- 未声明就视为:非并发、非只读
- 新工具默认进入**
串行执行 + 权限检查**的通道,绕不开治理
理念:安全与并发不是外包给管理层去管,而是写进协议里,变成每一个工具作者都必须面对的设计责任。
总结:tool不是函数的映射,而是标准化的协议对象
同一份定义同时服务:模型调用、权限判断、并发调度、UI呈现、结果回流
Tool执行管线与并发策略
模型输出 assistant message
↓
tool orchestration(按并发现分批)
↓
tool execution(逐个执行)
↓
├── sma校验
├── validate input
├── tool hooks
└── permission询问/拒绝
↓
tool call(工具调用)
↓
result回流 → 规范成用户端的tool result message
↓
进入下一轮API
并发策略:模型一次可能输出7个tool use(A、B、C、D、E、F、G)
┌────────────────────────────────────────────────┐
│ A(读) ─┐ │
│ B(读) ─┼─→ 并发执行(都是只读,并发安全) │
│ C(图) ─┘ │
│ │
│ D(写) ──→ 独占串行(edit是写操作,并发不安全) │
│ │
│ E(写) ─┐ │
│ F(写) ─┼─→ 并发执行(都是只读,并发安全) │
│ G(读) ─┘ │
└────────────────────────────────────────────────┘
Streaming Tool Exec
4个状态(pending、executing、completed、failed)追踪每个tool,同时接受tool use边接受边启动,不需要等模型完整输出
这就是Claude Code跑起来快的原因——底层是流式调度在帮模型抢时间
System Prompt的架构
Claude Code把给模型的输入分成三类:
| 类型 | 说明 |
|---|---|
| System Prompt | 主为镜策的长期行为协议(身份、基础规则、工具使用方式、输出风格、记忆等),几乎不变 |
| User/System Context | 运行态里附加的上下文(如claudeude.md、当前日期、git状态等),不是用户当前输入,而是runtime自己塞进去的现场信息 |
| Task-specific Prompts | 后台任务的专用协议(如compact session memory),每个任务都有自己的工具版名单、轮格式约束、轮次限制 |
System Prompt组装器优先级链:
override(最高优先级) → coordinator → agent → custom → default
override: 外部显式传入,直接强制覆盖coordinator: 多Agent协调模式的专用身份agent: Agent自带的身份定义custom: 用户在配置里自定义的default: Claude Code出厂自带的身份规则
优先级逻辑:有override就用override,没有就看coordinator,意思往下走。
这解释了为什么coordinator模式或者subagent能换一条身份运行——它就是替换了system prompt这个槽,不是模型变了,是模型看到自己是谁变了
上下文压缩策略
长任务跑着跑着token窗口会被工具输出挤爆
Claude Code的答案是5级压缩策略
| 级别 | 名称 | 策略 |
|---|---|---|
| 1 | claude dream |
把价值低的旧工具输出直接删掉,不做摘要(做摘要本身也要花token) |
| 2 | microcomp |
主要清理旧的tool result和cached edit,目标减少体积同时保住前缀稳定 |
| 3 | context collapse |
把多轮相似操作折叠成一段结构化摘要(如连续读了十几个文件,折成"读了哪些文件,关键发现是什么") |
| 4 | auto compact阈值触发 |
达到阈值后主循环停一下,用专门的compact prompt单独发一次请求,让模型总结当前会话历史,然后替换 |
| 5 | session memory compact |
复用后台已经抽好的session memory,避免再调一次总结模型 |
执行时机:每一轮的API request phase开始前都会做一次上下文治理,按梯子顺序依次尝试:claude dream → microcomp → context collapse → auto compact
有session memory就直接复用之后才进API request
设计:
-
轻量策略优先:前面的策略如果已经把上下文占用降到阈值以下,就不会进入完整的auto compact
-
完整compact不是删历史,而是用
summary + post compact attachment重建工作台:压缩后要重新选模型的一些信息(工具声明、文件上下文、计划状态),如果只是简单截段,模型就会忘了自己刚干到哪一步
System Prompt的缓存优化
┌────────────────────────────────────────────┐
│ 默认的System Prompt │
├──────────────────────┬─────────────────────┤
│ 静态主干 │ 动态边界 │
│ │ │
│ • 系统规则 │ • actions │
│ • 任务规则 │ • 环境信息 │
│ • 工具使用方式 │ • 语言偏好 │
│ • 语气等 │ • 输入风格 │
│ │ │
│ 几乎不变 │ 每次会变 │
└──────────────────────┴─────────────────────┘
↓ ↓
放最前面 放后面
为什么这么排:服务器端的system prompt cache是按前缀匹配的,前缀越稳定越长,在重复请求时命中率越高,token费用越便宜。稳定的放前面会变的放后面。
Claude Code同时维护本地section cache和服务器端system prompt cache:
本地这份:避免每次请求都重新组装一遍prompt服务器那份:省钱降延迟
两层cache一起治理才能把成本压下来
Memory体系
Claude Code把长期状态拆分成了文件索引、作用率和召回机制
┌─────────────────────────────────────────────────────────────────┐
│ Memory三大分类 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Auto Memory(自动记忆) │
│ ├── 存:用户编号、项目背景、跨会话的长期约束 │
│ ├── 写入时机:轮次结束后,后台调用extract memories自动抽取 │
│ └── 注入方式:不是全量灌,在queue请求前按相关性召回少量 │
│ │
│ 2. Session Memory(会话记忆) │
│ ├── 存:当前会话的滚动摘要(不是长期偏好) │
│ ├── 触发条件:会话达到token或tool call的阈值时 │
│ └── 优先复用,避免再总结一次 │
│ │
│ 3. Memory Agent(记忆Agent) │
│ ├── 存:某一类agent的长期经验 │
│ └── 条件:只有定义里声明了memory字段的agent才有 │
│ │
│ 补充:Team Memory(团队同步层) │
│ │
└─────────────────────────────────────────────────────────────────┘
Memory.md不是正文仓库,而是入口索引:
- 每条记忆只记一个链接加一行描述
- 整个文件硬限制:200行、25KB以内
设计:不是全量注入,按需召回
请求前扫描每个记忆文件的文件头元数据,生成清单,让一个轻量模型从中选最多5个,把这5个的正文代入本次请求。这个流程在Claude Code里叫**relevant recall**
Transcript与Resume机制
Transcript和resume是支撑长任务能够继续执行、关闭后第二天还能打开从中断位置接着跑的底座。
Transcript概念:不是聊天记录数组,而是一个append-only事件流日志
每一个事件追加到文件,带有唯一ID(UUID)并指向前一个event ID,所有事件靠这两个字段连成一条链
三个特点:
-
主链有边界:只有四类事件能进transcript(用户输入、模型输出、附加内容、系统消息)。其余如工具执行进度消息等不参与主链 -
追加日志:一行一个event,批量flush到磁盘(不是每条消息同步写盘,而是先进内存队列,由后台批量刷到磁盘文件) -
尾部重挂元数据:标题、标签、模式、work tree、agent设置等信息会周期性重挂到transcript文件尾部
反复重挂的原因是REPL列表页用的是轻量读取,只扫文件尾部一小段窗口。会话越变越长,早期写下的元数据会被新内容挤出尾部窗口
Resume四步重建管线:
- 加载日志:读取整个JSON文件,按
事件类型分列到不同数组
↓ - 重建主链:把最新消息当做叶子节点,沿着parent ID一路向前回溯,得到当前可继续的对话链
↓ - 修复断点:处理链被打断的典型场景
├── 早期版本残留的进度状态信息要清理掉
└── 比如把中间信息删了之后,它的parent UID就指向一个空洞,要往前找还活着的祖先重新挂上
↓ - 恢复运行态:plan计划状态、文件读取历史、context状态、agent和当前模式都要全部挂回内存
↓
控制权交还给REPL
Part 3: 安全与拓展
Sandbox与Permission的关系
每一条bash命令落到宿主机之前要经过几道关卡:能不能执行、怎么执行、执行后怎么清理,串成一条执行链:
Sandbox管线四步
- 逐条路由:每条bash命令判断是否进入sandbox,不符合则走普通路径
- 配置翻译:整合用户配置,转化为底层隔离环境可执行规则
- 隔离运行:底层将命令封装至隔离环境中执行
- 收尾清理:清除临时文件与影响宿主机的残留状态
为什么已有sandbox还需要permission?
-
互补不是替代:
permission先回答"这条命令能不能执行",sandbox处理"已经允许的命令怎么被进一步限制"。两层各干各的 -
配置翻译是核心:用户和项目设置里写的allow/directory读写目录、运名单白名单,被**
翻译成隔离环境真实可执行的限制**。这意味着sandbox不是软规则,而是硬边界 -
保护控制平面:把各类setting文件(.claude/、.config/目录等)都列入禁止写入范围,防止一个被污染的命令去改agent自己的配置。不让坏命令通过修改自我来扩大影响
补充:sandbox不是唯一防线
真正执行命令的其实是tool,它在进入sandbox之前自己还有一层前置检查:命令语法和危险模式识别、基于规则的permission判断、以及危险命令的识别和分流
MCP生态接入
MCP(Model Context Protocol)是外部工具入职流程。MCP server提供的工具,Claude Code把这些tool标准化成自己内建的tool对象,然后进入工具池再进入执行链。
设计:MCP工具不走旁路。它进来之后继续走schema、permission、做result回流这一整套。不为MCP单独开一条执行通道。
意味着:
- MCP工具受到和内置工具一样的治理
- 一样要声明并发性
- 一样要走权限检查
- 一样要写进transcript
Skills任务协议
Skill不是tool,它是一个可复用的prompt package。本质上就是:markdown + 元数据 + 可选脚本。
Front Matter声明:名称、描述、可用工具、触发路径等
调用方式:把这些拼入任务上下文
按需激活:skill不是全部常驻到prompt里等模型用,而是有3条入口:
| 入口 | 说明 |
|---|---|
| 用户主动调用 | 用户明确要求使用某个skill |
| 模型自主选择 | 模型根据上下文判断需要 |
| 条件触发 | 声明了path字段的skill会在操作匹配的文件时自动注入(如改.py文件时自动激活python相关skill) |
两条重要设计:
-
tool search延迟加载:当工具数量太多时(如装了一堆MCP),不会把所有接口的描述一次性塞进prompt,而是先暴露一个精简的发现入口,再按任务需要展开具体工具的边界。这直接降低了prompt体积,提高了cache命中
-
统一治理:MCP把外部能力接入tool pool,skills把任务方法直接注入上下文。两者的入口不同,但依然受到同一套权限和上下文预算的约束。真正要执行外部动作时仍然沿用这一条工具执行力。
多Agent分层架构
┌─────────────────────────────────────────────────────────────────┐
│ 多Agent分层架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 第三层:Swarm模式 │
│ ├── team boxes:靠inbox文件实现teammate之间的通信 │
│ ├── task board:autonomous时teammate自己去扫未认领的任务 │
│ ├── team file:记录团队信息的花名册 │
│ └── 权限回流:teammate使用敏感工具时权限请求统一回到leader确认 │
│ │
│ 第二层:Coordinator │
│ ├── 不是把sub agent多开几个,而是把主agent的system prompt换掉 │
│ ├── 让主agent自己变成coordinator,负责派工、综合续写多个worker │
│ ├── 结果以task notification(任务通知)形式回流 │
│ └── 典型流程:research并行 + implementation分派 │
│ │
│ 第一层:Subagent(单入测链) │
│ ├── 复用queue kernel,半隔离transcript │
│ ├── 子上下文只包含一条task prompt │
│ ├── 独立完成工具调用和结果回收 │
│ └── 返回的不是自己的上下文,而是一条摘要 │
│ │
└─────────────────────────────────────────────────────────────────┘
Swarm模式下的Autonomous机制:
当teammate空闲时自己找活的流程:
┌─────────────────────────────────────────────────────────────────┐
│ Autonomous生命周期 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ IDLE状态:timer倒数,没有人叫他 │
│ ↓ │
│ 超时后自己启动,去taskboard上读任务列表 │
│ ↓ │
│ 把自己的名字写进任务的own字段(避免多个teammate抢同一个任务) │
│ ↓ │
│ 开始处理这个task → 进入WORK状态 │
│ ↓ │
│ 干完后变灰,回到IDLE │
│ ↓ │
│ 下一轮timer重新开始 │
│ │
└─────────────────────────────────────────────────────────────────┘
Subagent的context隔离原理:子上下文检测到task后,拿到的不是父的全部历史,而是一份全新的message数组,只包含一条task prompt。它在自己的上下文里调用tool,独立完成工具调用和结果回收。干完后返回的不是自己的上下文,而是一条摘要。然后子上下文被抛弃。父拿到了这一条摘要。
设计价值:用一份干净的子上下文做隔离,防止主链被工具产物申报。
Coordinated Agent(do agent)是多agent的统一入口:本质上就是Claude Code暴露给模型的那个发起agent委派的tool。同一个tool通过参数不同,可以决定走普通的subagent、coordinator派出的worker,或者swarm里的teammate这几种形态。虽然入口分了层,底下真正执行时并没有换一套内核,最后还是回到同一个runtime、同一条queue执行链。
三条设计原则
1. 把模型能力交给runtime承接
模型输出本身只是一段文字,它怎么变成真实的工程行动是runtime层的queue loop、tool协议、permission、流式执行共同决定的。模型不是总指挥,runtime才是。
2. 把长任务状态做成可治理的对象
prompt、context、memory、transcript以及resume各自都要有明确的分工:怎么保存、怎么压缩、怎么召回、怎么恢复。不是事后补丁。
3. 把拓展与协作纳入同一治理面
MCP、skills、subagent、coordinator、swarm入口形态各不相同,但进来之后都不能绕过权限、执行与权限边界
Runtime Map
┌─────────────────────────────────────────────────────────────────┐
│ Claude Code Runtime Map │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 能力入口(最上层) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌───────────────────────┐ │ │
│ │ │ Tool │ │ MCP │ │ Skills & Subagent │ │ │
│ │ └──────────┘ └──────────┘ └───────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 运行治理(中间层) │ │
│ │ ┌────────────┐ ┌────────────┐ ┌──────────────────┐ │ │
│ │ │ Queue │ │ Tool Exec │ │ Prompt/Context │ │ │
│ │ │ Loop │ │ Pipeline │ │ Management │ │ │
│ │ └────────────┘ └────────────┘ └──────────────────┘ │ │
│ │ ┌────────────┐ ┌────────────┐ ┌──────────────────┐ │ │
│ │ │ Permission │ │ Sandbox │ │ Transition │ │ │
│ │ │ │ │ │ │ State │ │ │
│ │ └────────────┘ └────────────┘ └──────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 状态底座(最下层) │ │
│ │ ┌────────────┐ ┌────────────┐ ┌──────────────────┐ │ │
│ │ │ Memory │ │ Transcript │ │ Resume │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ └────────────┘ └────────────┘ └──────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
运转逻辑:
模型意图 → runtime通过tool协议、权限和sandbox → 变成真正可执行但不会越界的本地操作
↓
这些操作被状态底座完整记下来
↓
任务即使中断,后面还能接着跑
Harness Engineering
Claude Code这类系统的定位是Harness Engineering,由早期的Prompt Engineering上升到Context Engineering,再过渡到现在的Harness Engineering。
所有设计的核心围绕两件事:
-
安全可靠:让大语言模型的自主性行为是安全的,不会随便删重要文件、不会搞乱系统 -
承认
上下文始终有限:在上下文有限的约束下,用最好的机制实现最优的prompt送给模型。所有设计都围绕这个约束进行,而且这个约束不会变——即使上下文变长,计算成本和注意力分散问题依然存在。
Subagent的本质:几乎只是为了解决有限上下文的问题。subagent走的还是同一个API,只是prompt被大幅精简了。
中间产生的大量上下文累积对主context无影响,所以只需要拿到结果就可以。subagent是为有限上下文的设计服务的
Engineer和Research的融合:Harness Engineering既是一个engineering对象,也可以变成research的对象:
- Auto Harnessness:如何
自动化harness - 为harness的agent构建
Benchmark - 用强模型
协助弱模型在harness体系下表现更好 - 甚至调整小模型
权重,让它在harness范式里表现更好
Claude Code、Open AI API的关系:两者都是Harness Engineering,都是包在语言模型API上的一层结构,解决安全可靠、稳定执行、完成任务的问题。Claude Code能接所有AI模型,因为它最后就是把prompt塞给API
-
Subagent的多Agent关系:
不同subagent最终掉的还是同一个API,它并不是因为是一个子Agent就会掉别的一片 -
Claude Code的模型无关性:它不存在只能接特定模型的问题,
搞清楚原理就知道逻辑 -
Engineering和Research应该浑然一体,做有影响力的事情让社区共创
更多推荐




所有评论(0)