乐于分享
好东西不私藏

从 Claude Code 源码看生产级 Agent 系统的核心设计

从 Claude Code 源码看生产级 Agent 系统的核心设计

本文以 Anthropic 的 Claude Code 为研究对象,从 Agent 系统开发者的视角,系统性地分析其在 Agent 循环、工具系统、上下文管理、提示词工程、安全模型和多 Agent 协作等方面的设计经验


一、从 Chatbot 到 Agent

想象一个简单的场景:你对一个 LLM 说”帮我修一下这个 bug”

如果这是一个普通的 Chatbot,它会给你一段文字建议:“你可以在第 42 行加个空指针检查”。然后你自己去改代码、跑测试、看结果。LLM 只是一个”顾问”

但如果是一个 Agent,事情就完全不同了。它需要先读取相关代码文件,理解代码结构;定位到出问题的函数;搜索可能的依赖项;编辑代码文件;运行测试;如果测试失败,再分析失败原因、修改、重跑,可能要反复好几轮

这个跳跃带来了一系列 Chatbot 完全不需要面对的工程挑战:

  • 循环控制
    Agent 需要循环决策每一步该做什么,然后执行、观察结果、再决定。这个循环何时开始、何时结束、出错了怎么办?
  • 工具管理
    Agent 需要操作各种工具(读文件、写文件、执行命令、搜索代码等)。当工具数量达到几十个甚至更多,如何注册、校验、调度、并发控制?
  • 上下文管理
    LLM 的上下文窗口是有限的(当前主流约 200K token),但一个复杂任务可能产生上百轮对话。上下文满了怎么办?
  • 安全控制
    Agent 拥有操作真实环境的能力,一个错误的 rm -rf / 可能造成灾难性后果。如何在不过度限制能力的前提下保证安全?
  • 成本控制
    每次 LLM 调用都花钱。长对话、大工具集、频繁压缩都在消耗 token。如何在功能和成本之间找到平衡?

Claude Code 是目前市场上最成熟的 Coding Agent 产品之一,日活跃用户众多,已经在真实的生产环境中经受了考验。深入研究它的设计,不是为了复制它的做法,而是为了理解:一个在真实世界运行的 Agent 系统,到底需要解决哪些问题,以及它们之间如何互相制约


二、设计哲学极简调度 · 模型信任 · 安全纵深

在动手设计任何具体模块之前,有一个根本性的架构决策需要先做出:Agent 系统的”智能”应该放在哪里?

一种做法是让调度层(控制 Agent”做什么、怎么做”的决策逻辑)承载大量智能:规划模块、状态机管理流程、规则引擎校验结果。模型只是其中一个”组件”,调度层决定什么时候调模型、调哪个模型、怎么用模型的输出

Claude Code 走了另一条路,而且走得非常彻底:调度层极简——不规划、不判断、不纠错。 它的 Agent 循环本质上是一个 while(true),循环体里只有三步:调模型、执行模型说的工具、把结果喂回模型。循环里没有任何 if-else 来判断”模型是不是走错方向了”“任务是不是该换种策略了”——所有任务级决策全部交给模型自行判断

但”调度极简”绝不等于”系统简单”。恰恰相反——正因为调度层放弃了对模型的干预权,系统就必须在其他维度做得更深,才能保证整体可靠性。Claude Code 的架构本质是一个三角制衡:

“模型信任”在三个层面体现得淋漓尽致:

  • 循环无决策逻辑
    Agent 循环是纯粹的”调度器”,不包含任何业务判断
  • 工具选择靠模型
    调度层不会”建议”模型用哪个工具,也不会在模型选错工具时纠正。模型通过 tool_use 输出自主选择
  • 行为规范靠提示词
    代码风格、安全规则、输出格式、任务策略,全部编码在系统提示词中

这一设计理念源自两个核心假设:

  • 模型足够强:任何任务级决策都能通过提示词实现
  • 模型能力会进化:即使今天有模型做不好的决策,明天更强的模型可以做好;而极简调度的好处是切换模型几乎零成本,新模型发布后即插即用

“极简调度”也不意味着只有一个模型。Claude Code 在主循环之外部署了多个专用的辅助模型 Worker——用 Sonnet 做记忆文件的相关性筛选(从几十个记忆文件中选出与当前任务相关的注入上下文)、用 Haiku 做 Shell 命令的安全分类(替代用户手动确认)、用小模型生成工具调用的短标签摘要(用于 UI 展示)、用专用 prompt 驱动后台的会话摘要维护

不过,这样的设计也有明显局限,它假设模型能力”持续在线且只升不降”。但现实中有不少场景面临着无法使用强力模型的约束,还有些团队的路径是:先用最强的闭源模型验证商业模式,跑通后切换到更便宜的小模型或本地部署的开源模型以降低成本。在这些情况下,极简调度可能无法补偿模型能力的下降,而一些采用 Plan-and-Execute 或其他分治架构的 Agent 框架(如 BabyAGI、AutoGPT 的早期版本)则可以在一定程度上缓解弱模型的短板

更深层地,”信任模型”与”防止模型犯错”之间存在固有矛盾。Claude Code 在循环层面完全信任模型,但在安全层面用一套精密的权限管线来兜底。这种”信任决策、不信任执行”的分离策略,是理解后续所有设计的钥匙


三、Agent 循环

3.1 流程概览

Claude Code 的 Agent 循环基于 AsyncGenerator(异步生成器,一种可以逐个产出结果、按需拉取的异步编程模式)实现,循环执行以下动作:

  1. 预处理上下文:对历史消息做压缩、裁剪,确保不超出上下文窗口
  2. 构建完整提示:拼装系统提示词 + 用户上下文 + 当前轮附件
  3. 调用模型:通过流式 API 获取模型响应
  4. 解析响应:从响应文本中提取 tool_use block
  5. 执行工具:通过安全管线执行工具收集结果
  6. 喂回结果:将工具执行结果作为 tool_result消息追加到对话历史

如果模型的响应中没有 tool_use block,即模型只输出了文字而没有调用任何工具,则循环结束

3.2 终止条件

上述循环设计中有一个比较大胆的决策:循环的终止条件是”模型自觉”。没有显式的 task_complete 信号,没有状态机转换,没有额外的判断逻辑。模型不调用工具 = 模型认为任务完了

这个设计的优点是零协议开销——不需要教会模型发送特殊的终止信号,不需要设计额外的”完成度判断”逻辑。但代价是:如果模型”忘了”调用工具(比如它应该去验证修改结果,但直接给了文字回复),循环会错误地提前结束

对此,Claude Code 设计了三道间接防线:

  1. Stop Hooks
    每轮循环结束后执行用户配置的脚本(比如”检查测试是否通过”)。Hook 是 Claude Code 的可扩展拦截机制(第四章会详细讨论),可以向系统注入错误消息并重新执行
  2. Token Budget 继续
    当用户指定了 token 预算,如果模型只用了预算的不到 90%,系统会注入提示消息(“你只用了 X% 的预算,继续工作”)并让循环继续
  3. max_output_tokens 恢复
    如果模型输出被截断(达到单次输出上限),注入恢复消息让模型继续

3.3 流式执行与并发优化

当 Agent 想调用工具时,一个朴素的实现是:等 API 把整个响应都返回完,然后解析出所有工具调用,逐个执行。但 Claude Code 做了更激进的优化:在响应还在流式传输的过程中就开始执行工具

这通过一个”流式工具执行器”组件实现:每收到一个完整的 tool_use block,就立即判断能否开始执行。如果当前所有正在执行的工具和这个新工具都标记为”并发安全”,就立即启动执行。执行过程中还应用了”兄弟工具的级联取消”机制:如果一个 Shell 命令执行失败,执行器会向所有正在并行执行的兄弟工具发送取消信号(如果 npm install 失败了,并行运行的 npm test 也没有意义)

从源码看,流式工具执行器是否被真正使用受到一个功能性开关的控制,这意味着可能不是所有版本都应用了这一并发优化当功能未开启时,Agent 循环又回到了收集所有响应然后批量执行的路径

作为一个成熟的 C 端 Agent 系统,Claude Code 代码中充斥着很多类似的异步调用与并发执行,为系统行为尽可能争取到了更多”免费时间”,以优化用户体验

流式执行和并发优化提升了吞吐,但也引出了一个新问题:生产者和消费者的速率不匹配。Agent 循环是一个持续运行的流式系统,API 响应以不可预测的速率到达,多个并发工具可能在同一时刻完成并各自产出结果,而下游的消费者(UI 渲染、状态持久化、日志记录)各有各的处理节奏。如果生产者不受约束地持续推送事件,而消费者来不及处理,未处理的事件就会在内存中持续堆积,最终会耗尽内存

Claude Code 的解决方案不是”加一个限流器”,而是通过架构选择从根源上消除了这个问题:整个 Agent 循环基于 AsyncGenerator 实现,它以”拉取”的形式触发生产:消费者每处理完一个事件,主动调用 next() 拉取下一个;在消费者没有拉取时,生产者自动挂起,不会继续生产。这意味着内存中任何时刻最多只有一个待处理事件,无需显式的队列管理或限流逻辑

背压控制(backpressure):当消费者处理能力不足时,通过反馈机制使生产者主动限流,避免系统崩溃

3.4 协议执行器

Claude Code 的循环只通过两个抽象接口与外部世界交互:

  • ApiClient
    负责调用 LLM,输入是消息列表和系统提示词,输出是流式事件序列
  • ToolExecutor
    负责执行工具,输入是工具名和 JSON 参数,输出是文本结果

循环完全不知道:用的是 Anthropic 还是其他的 API、工具是本地 Bash 还是远程 MCP 服务

ApiClient 隔离了 LLM 提供商的差异,换一个模型提供商只需要写一个新的 ApiClient 实现,循环代码不需要任何修改;ToolExecutor 提供了工具执行的统一入口,所有工具调用在接口层实现统一的安全审计、权限检查、执行日志、并发控制等横切关注点(否则这些能力将散落在各工具内部,或者需要在循环中写大量分发逻辑)

3.5 错误恢复

生产环境中,API 调用不可能百分之百成功。但 Claude Code 的错误恢复远不止”捕获异常、重试”这么简单——它的 queryLoop 本质上是一张恢复图(recovery graph),有 7 个以上的”continue site”(循环继续点),每个对应一种轨迹修正策略,例如:

  • API 错误:429(速率限制)走指数退避重试;529(服务过载)连续 3 次失败则降级到更小的模型
  • 上下文溢出:先尝试折叠旧消息(保留结构信息);如果仍然超限则触发全量压缩
  • 输出截断:先尝试升级单次输出上限(8K → 64K)并重试相同请求;如果仍被截断,则注入恢复消息让模型从断点续写
  • Stop Hook 阻塞:用户配置的 Stop Hook 返回阻塞性错误时,将错误注入消息并强制循环继续

如果循环正常执行,也需要记录工具执行完成后的结果、更新状态,使循环的每一轮都能知道”自己从哪来”(比如是正常的工具执行后续轮,还是从一个压缩恢复中回来的)

循环维护的不是文本输出,而是一条合法的消息轨迹:每个 tool_use 必须有匹配的 tool_result,thinking block 不能在错误的边界被切断,恢复操作不能让 API 看到半成品状态


四、工具系统

上一章讨论了 Agent 循环如何调度工具执行,这也带来了一个关键问题:工具本身是怎么管理的?当工具数量达到 42 个以上,挑战远不止”把它们注册进去”这么简单

4.1 挑战

Claude Code 的工具集覆盖了文件操作(读、写、差量编辑)、代码搜索(Grep、Glob、LSP)、Shell 执行、Web 信息获取、任务分治(子 Agent 创建)、UX 交互(向用户提问、TODO 管理)、外部集成(MCP)等类别。当工具数量超过 10 个后,一些工程问题就变得不可回避:

  • Schema 膨胀
    42 个工具的 JSON Schema 定义可能吃掉 10K+ token,每次 API 调用都要携带,成本高昂
  • 权限异质性
    同一个 Shell 工具,执行 ls 是无害的读操作,执行 rm -rf / 是灾难性的写操作,需要对工具权限做动态管理
  • 并发安全性
    同时读 5 个文件可以并行,但写操作不行,具体该怎么判断?
  • 可扩展性
    用户通过 MCP 协议接入的外部工具要像内置工具一样无缝集成

4.2 工具行为监控

许多 Agent 框架用静态元数据标记工具的权限级别,如 required_permission : "dangerous"。但 Claude Code 走了一条更精细的路:工具的行为属性是关于输入的函数,而非静态常量。Tool 接口包含大量接受 input 参数的方法:

  • isReadOnly(input):这次调用是只读的吗?(ls 是,rm 不是)
  • isConcurrencySafe(input):这次调用可以与其他工具并发吗?
  • isDestructive(input):这次调用是否有破坏性?
  • checkPermissions(input, context):这次调用需要什么级别的权限?

以 Shell 工具为例:isReadOnly(input) 会解析命令管道中的每个子命令,判断是否全部为只读命令;而 isConcurrencySafe(input) 直接委托给 isReadOnly——只有只读命令才可以并发

这里有个重要的设计细节:工具的工厂函数为所有可选方法注入保守默认值,并发安全性默认为”不安全”(串行执行),只读性默认为”会写”(需要权限)。如果开发者忘记声明某个行为属性,系统会走更安全的路径

4.3 工具执行管线

一个工具调用从模型发出到最终产出结果,经过一条 7 步管线:

  1. 工具解析:通过名称(含别名回退)找到工具定义
  2. Schema 校验:用结构化校验器解析模型生成的 JSON 参数,格式错误则返回提示
  3. 语义校验:工具自定义的深层校验(如文件路径合法性、密钥检测)
  4. PreToolUse Hook:执行用户配置的前置钩子(可拦截、修改输入、注入额外行为)
  5. 权限决策:合并 Hook 结果 + 全局规则 + 工具自身的细粒度权限检查 + 必要时询问用户
  6. 工具执行:调用 tool.call()
  7. PostToolUse Hook + 后处理:后置钩子、大结果持久化、结果消息构建

这条管线的核心设计思想是面向切面编程(AOP)——安全检查、审计日志、Hook 拦截都不在各个工具内部实现,而是作为管线的”切面”横切所有工具。这意味着无论是内置的文件读取工具还是外部 MCP 服务器提供的数据库查询工具,都必须经过同一道安全关卡

4.4 成本控制

42 个工具的 Schema 全部放进 API 请求会消耗大量 token。Claude Code 的解决方案分两个层面:

  • 延迟加载:某些工具被标记为”可延迟”,首次请求不发送完整 Schema,模型会在需要时调用 ToolSearch 工具通过关键字匹配找到它们。这避免了低频工具(如 Jupyter Notebook 编辑)常驻 prompt 的成本

  • 分区排序:内置工具按名称排序作为固定前缀,MCP 外部工具按名称排序作为后缀。这保证了内置工具的位置永远稳定:添加或删除一个 MCP 工具不会改变内置工具在 prompt 中的位置,以便于 prompt cache 的前缀持续命中(cache 命中比 miss 便宜约 12 倍)

4.5 大结果持久化

工具输出是 Agent 上下文的最大不确定性来源。一个 cat 大文件的结果可能有几万个 token,直接塞进对话历史,一两次就能撑爆上下文窗口

对此,Claude Code 为每个工具定义了结果大小上限,超过阈值的结果持久化到磁盘,并返回预览 + 文件路径。模型如果需要完整内容,可以通过文件读取工具去查看

4.6 Hook 系统与 MCP

一个功能再完备的 Agent,也无法满足所有场景。企业需要在工具执行前做合规审查、团队需要集成内部 API、个人用户想连接自己的数据库……

Claude Code 用两个互补的机制解决扩展问题:

控制面的 Hook 系统: 覆盖约 27 种生命周期事件(工具调用前后、会话开始/结束、压缩前后、权限请求等),支持命令行脚本、HTTP 回调、Agent 子模型审核、SDK 函数四种实现类型。Hook 的能力可以分为三个层次,从基本到高级:

  • 拦截与放行: Hook 最基础的能力是在工具执行前决定”让不让做”。通信协议极其朴素:退出码 0 = 成功(放行),退出码 1 = 非阻塞警告(放行但注入提示),退出码 2 = 阻塞性拦截(直接拒绝)。任何能返回退出码的程序都可以成为 Hook

  • 修改输入与输出: Hook 还可以修改工具行为。比如 PreToolUse 可以改写工具的输入参数(自动添加安全标志)、注入额外的上下文信息;PostToolUse 可以替换 MCP 工具的输出结果(比如对敏感数据脱敏)、附加额外的分析结果

  • 流程控制: Stop Hook 在每轮对话结束后执行,决定是否强制 Agent 继续工作;异步 Hook 运行某种后台任务,完成后把结果注入 Agent 循环上下文;Agent 型 Hook 用另一个 LLM 实例来审核主 Agent 的行为,解决那些规则和脚本无法表达的复杂安全策略

Hook 的聚合规则遵循一票否决制:同一事件上挂了多个 Hook,只要有一个返回 deny,最终结果就是 deny。这保证了安全性——你不能通过添加一个”总是返回 allow”的 Hook 来绕过其他 Hook 的拦截。

数据面的 MCP: MCP 是一个开放协议,定义了模型与外部工具/资源服务器之间的标准通信接口。Claude Code 作为 MCP 客户端,可以连接任意符合协议的 MCP 服务器,运行时动态发现并使用其提供的工具

MCP 的关键设计是动态工具发现:工具列表不是静态配置,而是通过 tools/list 请求从服务器获取,服务器可以在运行期间通过通知更新列表。Claude Code 进一步扩展了一个 notifications/claude/channel 方法,允许 MCP 服务器主动向 Agent 推送消息(如 Slack 新消息、GitHub webhook 事件)。推送内容被包装为附件注入对话队列,Agent 在下一轮循环中可以即时响应。这把 MCP 从”工具箱”升级为”消息通道”,让 Agent 不仅能调用外部能力,还能被外部事件驱动


五、上下文管理

前面几章讨论了 Agent 如何循环决策和执行工具,但有一个无法回避的物理约束始终悬在头顶:LLM 的上下文窗口是有限的

当前主流模型的上下文窗口约 200K token,这对于 Coding Agent 来说并不算多:解决复杂任务时,轻易就会累积几十轮对话、读取数十个文件、执行上百次工具调用。一个中等复杂度的代码文件约 3K-5K token,读取 20 个文件就吃掉了 60K-100K。再加上每轮对话的推理文本和工具调用参数,200K token 在长任务中很快见底

5.1 压缩策略

第一层:MicroCompact(微压缩)

可以重新获取的工具输出被替换为一个占位符 [Old tool result content cleared],比如文件读取(可以再读一次)、Grep 搜索(可以再搜一次)、Shell 命令(可以再执行一次)。一般能回收 10K-50K 上下文

Anthropic API 的 prompt cache 有一个 TTL(约 5 分钟),MicroCompact 会在不同的 cache 冷热状态下走不同的压缩路径

  • 冷缓存路径:cache 已过期,直接修改本地消息内容(反正整个 prompt 都要重新计算,趁机瘦身)
  • 热缓存路径:cache 还新鲜,则不修改本地消息,而是生成 cache_edits指令,让 API 服务端直接从缓存副本中“删除”指定内容 

cache_edits 是 Anthropic API 的专有机制(需要在请求中设置特定的 beta header 才能启用),其他模型提供商目前没有等价功能。它能够进一步提高缓存命中、降低成本,但原理尚不明确。从目前泄露的代码注释推测,Anthropic 内部的推理缓存系统使用了分页管理缓存的局部注意力,这也许意味着删除早期的某些 cache 可能不会影响后续缓存

Claude Code 源码中还有一个由功能开关控制的 History Snip(历史裁剪)机制,它与 MicroCompact 的区别在于:后者保留了消息结构,只替换工具结果的文本内容;Snip 则直接从消息列表中移除整段消息对(tool_use + tool_result),其效果更激进,释放的 token 量通常也更大,但目前并非所有构建版本都启用

第二层:Context Collapse(上下文折叠)

MicroCompact 只清除工具结果的文本内容,但消息结构本身(tool_use + tool_result 对)仍然占据上下文空间。当对话足够长时,即使内容清空了,这些”空壳”的结构开销也很可观。Context Collapse 解决的就是这个问题:它将旧的对话轮次折叠为压缩后的摘要块(本地依然保留完整的原始消息,随时可恢复)

Context Collapse 还引入了渐进式折叠(progressive folding):不是一次性折叠所有旧消息,而是从最旧的消息开始,逐批折叠,直到上下文用量降到目标阈值以下。每次折叠的边界对齐到”API round”(一轮完整的 assistant 响应 + 对应的 tool results),确保语义完整性

在遇到 prompt-too-long 等错误时,MicroCompact 先清除可恢复的工具输出(低损操作),如果仍不够,则启动 Context Collapse 开始折叠旧轮次(中等损失,但本地可恢复)

第三层:Session Memory Compact(会话记忆快捷压缩)

Full Compact 需要调用 LLM 生成摘要,意味着额外的 API 调用成本和数秒延迟。Session Memory Compact 是一个投机优化:后台有一直有一个异步 Agent 持续维护会话摘要。当需要压缩时,检查这个摘要是否”足够新鲜”(基于摘要覆盖到第多少条消息判断),若是便直接用它替换旧消息

第四层:Full Compact(完整 LLM 压缩)

这是最重量级的压缩手段,但也是质量最高的。其 prompt 设计得非常结构化,要求模型生成包含 9 个明确章节的摘要:主要请求与意图、关键技术概念、涉及的文件和代码片段、遇到的错误与修复、问题解决过程、所有用户消息、待完成任务、当前进行的工作、可选的下一步行动

长文本压缩是一个较为困难的任务,为了提高结果的质量,Claude Code 采用了analysis scratchpad 机制:要求模型先在 <analysis> 块中”打草稿”——组织思路、检查遗漏,然后再写正式的 <summary>。最终处理时,<analysis> 被完整剥离,不进入压缩后的上下文

5.2 自动压缩的工程保障

自动压缩的触发逻辑需要精细的阈值控制和故障防护:

阈值计算:对于 200K 上下文的模型,有效窗口约 180K(扣除输出预留),auto-compact 阈值设在约 167K(再减去 13K 缓冲,确保压缩请求本身有足够空间)。

断路器:连续 3 次压缩失败后停止重试,防止不可恢复的大上下文反复锤击 API。在实际生产中曾出现过上千个会话连续失败 50+ 次、每天浪费约 25 万次 API 调用的情况

API 不变式保护:压缩后的消息序列必须符合 API 约,每个 tool_result 必须有对应的 tool_use,来自同一流式响应的 assistant 消息必须一起保留

消息分组:截断和压缩操作以”API round”(一轮 assistant 响应 + tool results)为原子单位,保证被处理的部分是语义完整的对话轮次,不会出现半截的工具调用

5.3 关键信息恢复

Full Compact 完成后,系统不是简单地把摘要放进去就结束了,后续会执行一系列恢复操作:

  • 最近读取的 5 个文件(50K token 预算,每个最多 5K)作为附件注入
  • 已加载的 Skills 被重新注入(25K token 预算)
  • 活动的 plan 内容被恢复
  • SessionStart hooks 被重新运行
  • 延迟工具的 delta、Agent 列表、MCP 指令等被重新追加

这解释了为什么 Full Compact 的压缩率可以高达 80-95%——它不是”保留所有重要信息”,而是”保留能让模型继续工作的最小信息” + 按需重新获取其他内容。这是一个”激进压缩 + 懒加载恢复”的设计模式

如果最近读取的文件已被删除、MCP 服务器已断开,恢复可能失败,造成严重的信息丢失。应用类似设计模式必须考虑可靠性问题

5.4 会话持久化

前面讨论的所有压缩策略都是针对 LLM 看到的活跃上下文,但用户可能还需要恢复之前的工作会话继续操作、回顾历史对话中的关键决策、或者从某个节点分支出新的尝试方向,光靠压缩后的摘要无法还原完整对话

Claude Code 将完整的对话历史以 JSONL 格式存储在本地文件系统中(~/.claude/projects/...),每条记录是一个独立的 JSON 对象,类型包括消息、摘要、标签、Agent 元数据、压缩产物等。对于从某个节点 fork 出去的新会话,使用 parentUuid 形成的链表结构来记录会话关系,便于路径恢复

这种”活跃上下文激进压缩 + 持久化层完整保留”的分层策略,让系统在 LLM 侧追求效率,在用户侧保证完整性,两者互不干扰

5.5 跨会话记忆系统

前面四节讨论的都是”一个会话内如何管理上下文”。但还有一个更长时间尺度的问题:Agent 能否跨会话积累经验?

想象一个场景:开发者在 Session A 中让 Agent 学到了项目的编码规范、常见的 bug 模式、测试偏好。Session B 开始时,如果 Agent 完全”失忆”,用户又要重复教一遍。前面会话持久化解决的是”恢复之前的工作状态”,但不解决”从过去经验中提取通用知识”的问题

Claude Code 为此构建了一套分层的跨会话记忆系统(memdir),与第六章将要讨论的 CLAUDE.md 配置文件系统形成互补。CLAUDE.md 是用户主动编写的指令,而跨会话记忆是 Agent 自动积累的经验

写入:Agent 在工作过程中通过专用的记忆工具将重要发现写入本地文件系统(~/.claude/memory/ 目录下按项目组织),每条记忆都带有时间戳和来源会话标识。写入的记忆不是随意的自由文本,而是只允许记录以下四类信息(避免标签自由生长导致召回时模糊匹配):

    • user:用户身份与偏好

    • feedback:对 Agent 行为的纠正,如”不要在响应末尾添加总结”

    • project:项目进展与架构决策
    • reference:外部系统的定位信息,如”Bug 追踪在 Linear 的 INGEST 项目”

    读取:每个新会话启动时,系统执行一次记忆召回:用一个辅助模型(不是主对话模型)扫描所有记忆文件,筛选出与当前任务相关的记忆,注入到系统提示词中

    一个积累了几百条记忆的长期项目,每次只需注入其中最相关的几条

    跨会话记忆系统还有团队级别的扩展:Team Memory 允许同一个团队的多个 Agent 实例共享记忆池,一个 Agent 学到的经验可以被其他成员查询。这在 Swarm 模式(第八章)下尤其有价值——Leader Agent 可以将任务分解策略写入团队记忆,Worker Agent 可以查询

    但需要指出的是,这套跨会话记忆系统目前更接近一个”结构化笔记本”而非真正的长期学习系统——它依赖 Agent 的判断来决定”什么值得记住”,且没有遗忘机制(记忆只增不减,直到用户手动清理)


    六、系统提示词组装

    回到第二章提出的三角制衡:如果调度层不承载决策逻辑,那”教模型怎么做”的知识存在哪里?答案是系统提示词

    但当此处实际涉及的工作量远超”写几句角色描述”。在 Claude Code 中,系统提示词是一个分层构建、缓存感知、多来源合并的工程系统,它需要同时满足完整性(告诉模型所有需要知道的)、稳定性(利于 prompt cache 命中)、可定制性(支持多种使用模式和用户配置),以及可维护性(新增功能时不破坏已有结构)

    6.1 三层上下文组装

    调用 LLM API 时,只能从 system 参数和 messages 数组两个位置注入信息。但模型需要知道的信息远不止”你是谁、遵循什么规则”,它还需要知道当前项目的环境快照(git 状态、操作系统、工作目录)、用户的自定义偏好(CLAUDE.md)、以及每一轮的即时上下文(用户选中的代码、新发现的相关 Skill、当前 token 消耗等)

    这些信息有截然不同的变化频率:有的整个 session 不变,有的每轮都变。如果把它们不加区分地塞进同一个位置,每轮变化的部分会拖着不变的部分一起重新计算,打穿 prompt cache

    Claude Code 的解决方案是把这些信息按变化频率拆分成三层,分别放入 API 的不同位置:

    第一层 System Prompt: 注入 system 参数, 整个 session 只构建一次,具体包括:

    • 通用指令:身份声明、系统规则、任务指导、工具使用指引、语调风格等
    • 会话级配置:运行环境(操作系统、工作目录、模型名)、git 状态、语言偏好、MCP 服务器指令、自动记忆系统使用说明等

    第二层 User/System Context: 注入对话历史的第一条 user message, 以 <system-reminder> 标签包裹,包含 CLAUDE.md 内容和当前日期。通过记忆化缓存,session 内只计算一次

    为什么不把这些内容也放进 system ?因为 system 的内容要尽可能稳定以利于跨用户的 global cache 命中。CLAUDE.md 和 git status 是用户/项目级的,无法复用

    第三层 Attachments: 注入当前 turn 的 user message。每轮 API 调用前动态收集 30+ 种附件,例如 TODO 提醒、IDE 中用户选中的代码行、与当前任务相关的 Skills、新目录的规则文件、当前 token 使用量等

    6.2 缓存感知分段

    上一节中,System Prompt 包含了通用指令与会话级配置,前者可以跨用户复用 prompt cache,所以还需要在其内部对两类提示做好区分。Claude Code 的方案是用一个哨兵字符串作为分界线。系统提示词的构建函数返回一个字符串数组,哨兵把它分成两段:

    • 静态段:身份声明、系统规则、任务指导、工具使用指引、语调风格。这些对所有用户完全相同,标记为 scope: ‘global’做跨用户、跨组织缓存
    • 动态段:环境信息、语言偏好、MCP 指令、记忆文件。因 session 而异

    发送 API 请求前,系统在哨兵位置将数组切分成多段,各自设置不同的缓存策略。哨兵本身被丢弃,不进入最终请求。静态段命中 global cache 意味着数十万用户共享同一份 KV cache 前缀,带来极大的成本节省

    这套利用哨兵来区分上下文类型的方案,似乎有很高的维护成本,因为判断一个 section 是否”静态”需要人工审查。Claude Code 的开发历史中有过多次因为在静态段中错误引入了运行时依赖而破坏 global cache 的 bug更健壮的方案也许是让每个 section 显式声明自己的 scope(全局/会话/每轮),由框架自动排序,而非依赖手动维护的哨兵位置

    6.3 Section 注册表与记忆化

    动态段的内容虽然因 session 而异,但大多数在 session 内不会变。环境信息不会变、语言偏好不会变、甚至 MCP 指令也只在服务器连接/断开时才变。如果每轮都重新计算所有 section,不仅浪费计算资源,还可能因输出的微小差异(如时间戳)导致 prompt 变化、cache 失效

    为了解决这个问题,Claude Code 实现了一个Section 注册表,系统提示词的动态段由一组命名的”section”组成,每个 section 由一个计算函数生成内容。注册表的核心职责是管理这些 section 的计算和缓存策略:

    • 记忆化 section(默认)
      开发者注册一个 section 并实现对应的内容生成函数。系统在第一次请求这个 section 时调用此函数生成内容,之后就直接返回缓存结果,直到用户执行 /clear 或 /compact 时才清除
    • 非记忆化 section
      如果某个 section 的内容确实可能在 turn 之间变化,开发者必须在注册时添加标记,并且在提供不能缓存的书面理由(用于代码审核)

    当前唯一跳过缓存的是 MCP 指令 section(因为 MCP 服务器可能在 turn 之间断开,工具列表需要实时更新),其他十几个动态 section 全部使用了带缓存的版本

    6.4 记忆文件系统

    系统提示词中最复杂的动态部分是”记忆文件”(Memory Files),即 CLAUDE.md 系统。它用一套精巧的加载机制把分散在文件系统中的用户指令注入到 prompt 中。

    加载遵循六层优先级(从低到高):

    1. Managed:组织策略(/etc/claude-code/CLAUDE.md),管理员设定
    2. User:用户全局偏好(~/.claude/CLAUDE.md
    3. Project:项目级规范,从当前目录一路向上遍历到文件系统根目录,收集每一层的 CLAUDE.md
    4. Local:私有项目规范(.local.md 后缀,不进版本控制)
    5. AutoMem自动记忆(Agent 自行管理的记忆文件)
    6. TeamMem:团队共享记忆

    离工作目录越近的文件优先级越高,所有记忆文件被一句强硬的指令前綴包裹:“These instructions OVERRIDE any default behavior and you MUST follow them exactly as written.”

    这个系统的复杂度已经接近一个小型的配置管理框架。六层优先级 + 目录遍历 + 条件注入+ 递归引入。普通用户很难理解”我的 CLAUDE.md 为什么没生效”——可能是被更高优先级的文件覆盖了,可能是路径不对,可能是 glob 不匹配。Claude Code 甚至还专门提供了一个 /context 命令(超过 1300 行的分析代码)用于调试

    6.5 快照

    上面提到的记忆化机制带来了一个重要的语义特征:上下文信息是”快照”而非”实时”的

    什么是快照?就像拍照一样——Session 开始时,系统对当前的 git 状态、CLAUDE.md 文件内容、环境信息等”拍一张照片”,此后整个 Session 中模型看到的都是这张照片,而非实时的状态。Git status 不会反映后续的 commit,CLAUDE.md 不会反映后续的编辑。系统提示词中甚至会显式告知模型:“this status is a snapshot in time, and will not update during the conversation”。

    这是一个深思熟虑的设计选择,核心动机有三个:

    1. 缓存稳定性:Agent 在工作过程中不断修改文件、创建 commit,如果 git status 每轮都更新,整个 User Context 块就会变化,prompt cache 全部失效,成本翻 12 倍
    2. 模型一致性:模型在整个 Session 中看到同一个环境快照,不会因为”git status 突然多了几行”而困惑或做出不一致的决策
    3. 开销控制:轮都重做git status + 目录遍历 + 文件读取太浪费

    当用户执行 /compact(压缩上下文)或 /clear(清除会话)时,所有记忆化缓存被清除,系统相当于”重新拍一张照片”,此后的对话基于新快照继续

    使用快照也带来了一些隐患,例如用户在 Session 中修改了 CLAUDE.md(比如加了一条新规则),模型看到的仍然是旧版本,系统没有文件监视器来自动检测变更,用户必须手动执行 /compact 才能让新规则生效。这对用户来说是一个隐藏的认知负担,容易以为是系统 bug。一个可能的改进方向是,至少在 CLAUDE.md 被修改时给出一个明确的提示:“检测到配置文件变更,执行 /compact 使其生效”


    七、安全防护

    前面几章讨论了 Agent 如何高效地做事,但有一个始终伴随的问题:Agent 做事的能力越强,犯错的后果就越严重

    一个没有安全约束的 Coding Agent 本质上是一个拥有 shell 访问权的自主程序,它可以删文件、执行网络请求、安装恶意软件、修改系统配置。这不是假想风险:恶意仓库的配置文件可以指示 Agent 执行危险操作、模型的幻觉可能产生破坏性命令、prompt injection 可以诱导 Agent 做攻击者想做的事

    因此,我们要解决的根本问题是:如何在不过度限制 Agent 能力的前提下,防止它做出不可逆的有害行为?

    7.1 有序短路管线

    Claude Code 的权限裁决是一个有序短路管线:多个检查步骤按固定顺序执行,任何一步给出确定结果即停止,后续步骤不再执行。这条管线按照由严格到最宽松的顺序排列:

    1. 黑名单拦截:配置中明确禁止的工具,直接拒绝,不问任何问题
    2. 需确认拦截:配置要求对某工具弹出确认框。但有一个例外:如果 Shell 命令运行在沙箱中(内核级隔离)则可以跳过确认,因为沙箱已经限制了最坏情况下的损害范围
    3. 工具自身检查:每个工具可以根据输入内容定义自己的权限逻辑。比如 Shell 工具会解析命令,判断 ls 只需读权限而 rm -rf 需要最高权限;文件工具会检查路径是否在允许范围内
    4. 特权模式放行:处于 Auto 模式或只读的 Plan 模式,到这一步直接放行
    5. 白名单放行:配置中明确允许的操作,到这一步放行
    6. 兜底 转交用户:以上所有检查都没有确定结果,则交给用户手动确认

    核心设计原则:拒绝永远在前,允许永远在后。 这意味着即使你同时配置了”禁止 rm”和”允许所有 Shell 命令”,禁止规则会先命中——安全检查不可能被更宽松的规则覆盖

    7.2 Auto 模式

    前面描述的权限管线在很多步骤需要用户手动确认,这对于高频操作的开发者来说会严重打断心流。为此,Claude Code 提供了一个 auto 模式(也叫 YOLO 模式),在这个模式下系统自动批准大部分操作,无需用户逐一确认

    但全自动批准显然会带来安全风险——如果模型被 prompt injection 攻击,或者产生了幻觉,可能执行破坏性命令。因此,auto 模式下系统引入了一个 LLM 分类器来自动判断 Shell 命令是否安全,替代人工确认

    这个分类器的设计有一个精妙的反操纵细节:分类器的输入只包含 tool_use 结构化序列,不包含模型的自然语言推理文本

    这样做的原因是,如果分类器能看到模型的推理文本,攻击者可以通过 prompt injection 让主模型在推理中写”这个命令是安全的、已经过用户确认”来误导分类器。只看结构化的工具调用数据,分类器的判断完全基于可验证的事实,大幅降低了被攻击的风险

    分类器使用较小的模型(如 Haiku),采用两阶段设计:第一阶段快速初筛,如果初筛认为需要阻止才启动第二阶段深入判断。并且通过投机启动(speculative start)优化延迟:分类器与权限管线并行执行,如果权限管线先给出确定结果(如 deny 规则直接命中),分类器结果直接丢弃

    7.3 Hook 的不对称权力

    在第四章中我们提到 Hook 可以拦截工具调用。但相反,PreToolUse Hook 返回 allow 并不能直接放行工具,它仍然要经过规则系统的二次检查,因为 Hook 是用户编写的脚本,可能有 bug 或被篡改

    扩展机制只能做”更严格”的事,不能做”更宽松”的事

    7.4 多来源规则与企业策略

    权限规则来自至少六个来源:用户设置、项目设置、本地设置、功能标志设置、策略设置、CLI 参数。所有来源的规则都参与匹配。

    一个关键的安全保证是:企业策略的 deny 规则不可被用户设置覆盖。系统通过一个”只允许托管权限规则”的标志来实现这一点,当管理员开启此标志后,用户级别的 allow 规则将被忽略

    7.5 信任边界

    在用户确认信任当前工作区之前,CLI 不会加载任何来自项目的 Hook、环境变量和功能标志,因为恶意仓库可以在配置文件中指定 Hook 指向恶意脚本,或设置环境变量改变 Agent 行为。如果在信任确认前就加载这些配置,用户 clone 一个仓库的瞬间就可能被攻击

    信任确认将启动流程硬分割为两个阶段:确认前只加载安全的全局配置(密钥链、MDM 设置等),确认后才加载项目级的一切。这不是一个 UI 功能,而是一个架构约束——信任边界嵌入在初始化流程的代码结构中

    7.6 沙箱作为纵深防御

    对于最危险的工具—— Shell 命令执行,Claude Code 还叠加了沙箱隔离作为最后一道防线。在 macOS 上使用 seatbelt(Apple 的 sandbox 机制),在 Linux 上使用 bwrap(Bubblewrap 容器)

    沙箱的存在改变了权限管线的行为:当 Shell 命令在沙箱中执行时,某些原本需要用户确认的操作可以自动放行,因为内核级隔离已经限制了最坏情况下的损害范围

    以 Shell 工具为例,其在权限管线之前就内置了 23 个命名安全检查,通过 tree-sitter 将命令解析为 AST 进行结构化分析,覆盖了 11 种命令替换注入模式、Zsh 特有的攻击向量(如 =(...) 进程替换、$(<file) 文件读取)、Unicode 同形字伪装路径、以及针对 sed 等高危命令的白名单验证。这些验证构成了权限管线与沙箱之间的额外防线

    这是”纵深防御”(defense in depth)的教科书示例:prompt 指导模型行为(第一层)、工具内置的结构化安全检查做前置过滤(第二层)、权限管线在运行时拦截(第三层)、沙箱在操作系统级限制(第四层)。任何一层被突破,后面还能兜底


    八、多 Agent 协作

    前面七章讨论的都是”一个 Agent 如何做事”。但当任务复杂到一个 Agent 无法在单个上下文窗口中完成时,比如同时重构前端和后端、同时修复多个相关但独立的 bug,就需要多个 Agent 协作

    Claude Code 提供了多种多 Agent 协作模式,从简单的任务委派到复杂的团队协作,解决的问题层次不同

    8.1 子 Agent 模式

    最基本的多 Agent 形式是通过一个”创建子 Agent”的专用工具来实现任务委派。主 Agent 把一个子任务委派给子 Agent,子 Agent 拥有独立的上下文窗口,完成后将结果返回给主 Agent

    这解决了一个实际问题:主 Agent 的上下文已经很满了,再执行一个大搜索或大文件读取可能导致上下文溢出。委派给子 Agent 后,可以用它自己的上下文窗口处理,只把精炼后的结果返回,大幅降低了主 Agent 的上下文压力

    子 Agent 有几个关键的设计约束:

    • 完全的上下文隔离: 子 Agent 看不到父 Agent 的对话历史。父 Agent 必须在委派 prompt 中包含子 Agent 需要的所有信息——文件路径、行号、错误消息、完成标准

    • 工具集可以不同: 子 Agent 被限制使用父 Agent 工具集的子集。例如,一个”只读研究”子 Agent 可能只有文件读取和搜索工具,不能修改文件

    • 系统提示词独立构建: 子 Agent 的系统提示词由各自的定义独立构建,走独立的构建路径。这里有一个值得警惕的安全风险:父 Agent 的安全规则(特别是来自 CLAUDE.md 的用户自定义规则)不会自动继承给子 Agent,容易造成安全规则的丢失

    Fork Subagent(分叉子 Agent) 是子 Agent 模式的变体,它继承父 Agent 的完整对话历史前缀作为初始上下文。这带来两个好处:一是不需要父 Agent 重复描述已有的背景信息,减少了 prompt 构建的冗余;二是由于子 Agent 的 prompt 前缀与父 Agent 相同,prompt cache 可以直接命中,大幅降低了创建子 Agent 的首次调用成本。这是一个典型的”用缓存换效率”的设计,代价是可能浪费宝贵的上下文空间

    8.2 Coordinator-Worker 模式

    更高级的协作形式是 Coordinator 模式:主 Agent 不再直接做任何”动手”的工作,只能作为纯粹的任务编排者调用「创建 Agent、发送消息、停止任务」这三个工具,把所有实际工作委派给 Worker Agent

    这种分离的好处是:Coordinator 的上下文不会被工具输出填满,可以专注于高层次的任务分解和进度跟踪。Worker 各自拥有独立的上下文窗口,可以并行处理各自的子任务

    8.3 Swarm/Agent Teams 模式

    最复杂的协作形式是 Swarm 模式,支持多个 Agent 作为”团队”协作,每个 Agent 运行在独立的终端窗口或独立进程中。团队间的通信通过一个基于文件的邮箱系统实现:

    ~/.claude/teams/{team-name}/├── config.json            # 团队配置└── inboxes/    ├── team-lead.json     # Leader 的收件箱    ├── researcher.json    # 成员 A 的收件箱    └── test-runner.json   # 成员 B 的收件箱

    消息类型包括:普通文本消息、广播消息、空闲通知(“我做完了/卡住了/失败了”)、权限委托请求/响应、计划审批请求、关闭请求等

    并发控制通过 lockfile 实现,避免多个 Agent 实例同时往同一个收件箱写消息

    选择文件系统而非 IPC/WebSocket 作为通信机制,有几个务实的理由:跨进程天然可用(不需要共享内存或网络端口)、崩溃恢复简单(文件在进程重启后仍然存在)、调试友好(直接查看 JSON 文件就能理解通信状态)。代价是延迟较高(文件 I/O + 轮询),但对于 Agent 间的协调消息(通常是秒级响应时间),这个延迟完全可以接受

    8.4 对抗”自我欺骗”

    多 Agent 协作中还有一个容易被忽视的设计:Verification Agent(验证 Agent)。这是一个专门用于”二次验证”的内置子 Agent 变体,它的设计动机源于一个 LLM 的已知弱点:自我一致性偏差

    当主 Agent 写了一段代码并自行运行测试后,它倾向于认为自己的实现是正确的,即使测试覆盖不全或存在边界情况。这不是恶意,而是 LLM 的 attention 机制天然倾向于与自己之前的推理保持一致。Verification Agent 解决这个问题的方式很直接:用一个独立上下文的 Agent 实例来审查主 Agent 的工作成果,从零开始评估

    这个设计模式的价值不在于技术复杂度(实际上它只是子 Agent 的一个特化用法),而在于它承认了一个架构层面的事实:单个 LLM 实例不适合同时担任”执行者”和”审查者”角色。在传统软件工程中,这对应于”代码审查不能由作者自己完成”的原则


    九、启发与反思

    9.1 值得借鉴的设计模式

    1. 有序短路管线

    这是 Claude Code 中出现频率最高的模式。它同时出现在:权限裁决(黑名单 → 需确认 → 工具自检 → 特权放行 → 白名单 → 兜底)、上下文预处理(结果截断 → 旧结果清理 → 微压缩 → 摘要折叠 → 完整 LLM 压缩)、错误恢复(429 重试 → 529 降级 → prompt-too-long 压缩 → 致命退出)。核心思想是:多个处理步骤按固定顺序执行,任何一步给出确定结果即停止,顺序本身携带语义

    2. 缓存感知设计

    Prompt cache 直接影响成本(命中 = 约 1/12 的成本)和延迟(命中 = 跳过预填充)。Claude Code 围绕缓存做了大量设计:系统提示词的静态/动态分段、工具列表的分区排序、确保影响缓存键的请求参数在 session 内保持不变、MicroCompact 的热/冷缓存路径、section 的默认记忆化。在设计任何 LLM 应用组件时,先问”这会不会导致缓存失效”是一个值得养成的习惯

    3. Fail-closed 默认值

    安全相关的默认值永远是限制性的:并发安全性默认”不安全”,只读性默认”会写”。开发者需要主动声明”安全”来解锁行为,而非主动声明”危险”来加锁

    4. 安全在调度层统一执行

    所有工具调用都经过同一条执行管线中的安全切面,无论是内置工具还是外部 MCP 工具。安全策略不由各工具自行实现,避免了遗漏和不一致

    5. 消融实验基础设施

    一个在商业产品中不常见但极具借鉴价值的设计是 Claude Code 的 ABLATION_BASELINE(消融基线)机制。这是一个编译时开关,当开启时,系统会禁用一系列经验性优化(如记忆系统、高级压缩策略、辅助 Worker 等),回退到一个”最小功能”的基线版本

    为什么这很重要?Agent 系统有大量”我们觉得有用”的优化:记忆召回、Context Collapse、辅助模型分类等。但这些优化是否真的提升了最终效果,还是只增加了复杂度?消融实验通过对比”有 X”和”没有 X”的结果差异来量化每个优化的实际贡献。没有这个基础设施,团队只能凭直觉判断”这个功能有没有用”——而直觉在复杂系统中是极不可靠的

    这提供了一个值得借鉴的工程纪律:每个重要的优化设计,在引入时就应该预留”关闭它”的开关,以便未来用数据验证其价值

    9.2 值得商榷的设计选择

    Claude Code 是一个成功的商业产品,但不意味着它的每个设计都是最佳实践。以下几个方面值得批判性思考:

    自动化 Plan-and-Execute 的缺失:虽然有 Plan 模式(用户手动触发的只读规划阶段)和 Coordinator 模式(模型即兴做任务分解),但架构层面没有自动化的”先规划再执行”流程。对于跨文件重构、多模块修改等长程任务,缺乏结构化规划可能导致模型走弯路

    模型降级时的脆弱性:三角制衡的”模型信任”顶点依赖模型足够强。当不得不使用弱模型时,缺乏调度层的补偿机制——更严格的决策校验、更详细的提示词引导、更保守的工具调用策略

    配置的爆炸式增长:六层记忆文件优先级 + 约 27 种 Hook 事件 + 多种权限模式 + MCP 服务器配置 + 五层设置合并。对于新用户和运维人员,配置空间的可理解性是一个挑战

    Session Memory 的递归质量损失:后台摘要 Agent 的上下文可能已被压缩,基于压缩后上下文生成的摘要再被用于压缩主会话,信息损失可能叠加。缺乏对这一风险的显式监控

    子 Agent 的安全继承:系统提示词的替换语义意味着子 Agent 不自动继承父 Agent 的安全规则,需要自行确保安全规则的完整性