Claude Code 解析-核心设计

Claude Code 解析-核心设计
mengnankkzhouClaude code设计
背景
claude code具体是干什么的就不用跟大家说了:
那我们从工程的角度去拆解他:
其产品本质可以理解为:
CLI/TUI 外壳
-> 输入编译器
-> 多轮 Agent Loop
-> 模型调用适配层
-> 工具执行器
-> Hook/Permission
-> Session/Transcript/Plan/FileHistory
设计进化
我们从chatbot到真正的agent就是有了工具调用!
但是工具调用又会带来一些问题:
- 工具注册与发现:谁来管理工具的注册表?如何让模型知道有哪些工具可用?如何在不重启系统的情况下动态添加新工具?
- 参数校验:谁来校验工具调用的参数?模型可能传递错误类型的参数、缺少必填字段、或传入超出范围的值。校验逻辑放在哪一层?
- 权限管控:谁来决定某个工具调用是否应该被执行?模型可能要求执行
rm -rf /,这显然不应该被允许。但有些操作在特定上下文中是安全的——如何平衡安全性与效率? - 错误恢复:工具执行可能失败,API 调用可能超时,LLM 的输出可能不符合预期格式。每一种错误场景都需要有对应的恢复策略,否则 Agent 会陷入”出错-重试-再出错”的死循环。
- 状态一致性:多个工具调用可能操作同一份资源。如何在多轮调用之间维护状态一致性?如何避免”读到过期数据”的问题?
- 并发与调度:某些工具调用可以并行执行(如同时读取多个文件),某些必须串行(如先创建目录再写文件)。如何智能地调度这些调用以最大化效率?
这样就引出了我们的Agent Harness的封装
简单封装的思路 是:调用 LLM API,解析输出中的工具调用指令,执行工具,把结果拼回 Prompt,再次调用 LLM API。这是一个典型的 while 循环。
这种思路的问题在于它假设了一个理想化的世界:API 调用不会超时,模型输出永远格式正确,用户不需要实时看到进度,上下文窗口是无限的,所有操作都是安全的。但在真实的生产环境中,每一个假设都会被打破。
Agent Harness 的思路 则需要考虑以下问题:
- 流式输出:LLM 的响应是流式的,用户需要实时看到 Agent 的思考过程,而不是等待整个响应完成。如何在不阻塞主线程的情况下实现增量渲染?如果使用回调,会导致”回调地狱”;如果使用 Promise 链,会失去中途取消的能力;如果使用事件发射器,会增加内存管理的复杂度。
- 权限管控:LLM 可能要求执行
rm -rf /,这显然不应该被允许。权限系统应该在哪一层介入?如果在外层统一拦截,无法处理工具特定的权限逻辑(如 Bash 命令的风险评估);如果在每个工具内部检查,会导致重复代码和权限策略不一致。如何平衡安全性与效率? - 上下文管理:随着对话的进行,上下文窗口会被填满。何时触发上下文压缩?压缩策略如何保证不丢失关键信息?压缩后的上下文如何与缓存系统协同?错误的压缩策略可能导致 Agent “忘记”关键信息,从而做出错误的决策。
- 错误恢复:工具执行可能失败,API 调用可能超时,LLM 的输出可能不符合预期格式。每一种错误场景都需要有对应的恢复策略。没有统一的错误恢复框架,每种错误都需要单独处理,代码会迅速膨胀到不可维护的程度。
- 状态持久化:用户中断会话后如何恢复?多个子智能体如何共享状态?状态更新如何保证不可变性?如果状态管理混乱,Agent 可能在恢复后产生不一致的行为。
- 可扩展性:如何让第三方开发者安全地注册新工具?如何支持 MCP(Model Context Protocol)等外部协议?如果没有清晰的扩展接口,Agent 的能力将永远受限于原始开发者的想象力。
基本设计
claude code的迭代循环:
- 构建 API 请求(系统 Prompt + 消息历史 + 工具定义)
- 调用 LLM API 并流式接收响应
- 解析响应中的工具调用指令
- 通过权限管线校验每个工具调用
- 执行被允许的工具调用
- 将工具结果作为新消息注入历史
- 决定是否继续循环(如果 LLM 返回了新的工具调用)或终止(如果 LLM 返回了纯文本回复)
状态管理:
跨迭代状态被封装在一个不可变的 State 对象中,每次迭代时通过整体替换而非逐字段修改。这种不可变状态流转的模式使得状态的每次变化都是可追溯的。
工具管理基石:
工具类型模块定义了 Claude Code 中所有工具必须遵循的类型契约。这是一个典型的”接口即架构”的案例——通过定义清晰的类型接口,工具系统的架构约束被编译器强制执行。
这个类型定义中蕴含了丰富的架构决策:
- 工具的执行方法接收权限检查回调,意味着权限检查被内嵌到了工具执行流程中,而非外部拦截。
- 进度回调支持工具执行过程的增量进度报告,这是流式 UI 的基础。
- 并发安全声明标记工具是否可以并行执行,这影响调度策略。
- 中断行为定义用户中断时的行为(取消还是阻塞),这是用户体验的关键细节。
- 破坏性标记标识工具是否执行不可逆操作,这是权限系统的重要输入。
工具注册:
工具注册模块中的核心函数返回所有可用工具的完整列表,是工具系统的”单一事实来源”(Single Source of Truth)。值得注意的设计细节:
- 条件注册:某些工具通过 Feature Flag 控制,如 REPL 工具只在内部版本中可用,定时任务工具只在特定功能模式下启用。这使得同一份代码可以服务于不同的产品形态。
- 延迟加载:部分工具使用动态导入而非静态导入来避免循环依赖和减少启动时间。
- 工具过滤:工具过滤函数在将工具列表发送给 LLM 之前,会根据权限上下文过滤掉被禁止的工具,确保模型甚至无法”看到”它不应该使用的工具。
Agent Harness设计
异步流式优先
不可变状态流转
渐进式能力扩展
安全边界内嵌
缓存感知设计
是我们设计的Agent Harness的五大原则
异步流式优先
传统的 LLM 调用通常采用请求-响应模式:发送 Prompt,等待完整响应,处理结果。但 Agent 的交互模式是根本不同的——Agent 可能在一个用户请求中执行多轮工具调用,每一轮都可能产生需要实时展示给用户的中间状态(思考过程、工具调用计划、执行进度)。
AsyncGenerator 完美地匹配了这个需求:
- 增量输出:通过
yield逐步产出流式事件,上层代码可以实时渲染。 - 可中断性:调用者可以随时通过
generator.return()或generator.throw()终止生成器。 - 背压控制:如果消费者处理速度跟不上生产速度,生成器会自动暂停,避免内存溢出。
这种设计使得 Claude Code 的对话循环成为一个”事件流”而非”请求-响应对”。上层代码只需要一个 for await...of 循环就可以消费整个对话过程的所有事件。
AsyncGenerator 是唯一同时满足”流式输出”、”可取消”和”类型安全”三个需求的方案。
安全边界内嵌
Claude Code 的权限系统不是一个附加的安全层,而是被内嵌到了架构的核心管线中。工具调用从被 LLM 提出到最终执行,需要经过多个安全检查点:
- 工具可见性过滤:在将工具列表发送给 LLM 之前,根据权限规则过滤掉被禁止的工具。模型甚至无法”看到”它不应该使用的工具。
- 输入校验:工具的
validateInput方法在权限检查之前执行,拒绝格式不合法的参数。 - 权限决策:
canUseTool回调综合考量权限模式(默认/Auto/Bypass)、工具的危险等级、用户的历史决策等因素,做出允许/拒绝/询问的决策。 - 运行时防护:即使通过了上述检查,工具执行过程中仍有沙箱限制、超时控制、输出大小限制等防护措施。
这个四阶段管线的设计哲学是”纵深防御”(Defense in Depth)——没有单一的安全检查点是”银弹”,但每一层都可以独立短路,阻止不安全的操作。
为什么不使用简单的白名单? 白名单方案看似简单,但存在致命缺陷:它假设所有操作都可以事先分类为”安全”或”不安全”。但现实中,安全性是上下文相关的——rm -rf node_modules 在开发环境中是安全的,但 rm -rf /etc 是危险的。同一个 Bash 工具、同一个命令模式,在不同参数下的风险等级完全不同。四阶段管线允许每一层根据上下文做出更精细的判断。
缓存感知设计
在 LLM 的 API 计价模型中,Prompt 缓存(Prompt Caching)可以显著降低成本和延迟。Claude Code 的架构从多个层面考虑了缓存友好性:
- 系统 Prompt 稳定性:系统 Prompt 的构建方式被精心设计,确保在工具列表不变的情况下,Prompt 的字节内容保持一致,从而命中 API 侧的 Prompt 缓存。
- 子智能体的缓存共享:Fork 模式下的子智能体会继承父智能体的
renderedSystemPrompt,避免重新生成可能因配置变化而不同的 Prompt,保证缓存命中率。 - 消息历史的不可变性:已发送给 API 的消息不会被修改,只有追加新消息的操作,这保证了缓存键的稳定性。
缓存感知设计的影响是深远的:它不仅降低了 API 成本,还通过减少重复计算提高了响应速度,这对于需要频繁与 LLM 交互的 Agent 系统尤为关键。
如果不这样设计会怎样? 一个不关心缓存的 Agent 系统可能在每次 API 调用时都重新构建系统 Prompt,导致:(1) 每次 API 调用都需要处理完整的 Prompt,增加延迟和成本;(2) 微小的配置变化(如工具列表的排序变化)可能导致缓存全面失效;(3) 在子智能体场景下,父子智能体之间无法共享已缓存的 Prompt,导致重复计算。对于每天执行数百次 API 调用的生产级 Agent,这些浪费会迅速累积成可观的成本。
渐进式能力扩展
Claude Code 提供了四级扩展模型,从内建到外部、从简单到复杂:
| 扩展级别 | 机制 | 适用场景 | 扩展者角色 |
|---|---|---|---|
| 工具(Tool) | 实现 Tool 类型接口 |
添加新的原子操作能力 | 核心开发者 |
| 技能(Skill) | Markdown + 脚本的声明式工具 | 封装可复用的任务模板 | 高级用户 |
| 插件(Plugin) | 带生命周期的工具包 | 组织相关工具和配置 | 生态开发者 |
| MCP 服务器 | 标准化协议的外部工具集成 | 第三方工具生态 | 第三方开发者 |
这四级扩展模型的设计哲学是”渐进增强”:对于简单的需求,声明一个 Skill 就够了;对于复杂的集成,可以通过 MCP 协议连接外部服务。每一级都建立在前一级的基础之上,而非替代它。
特别值得关注的是 MCP(Model Context Protocol)的集成方式。Claude Code 不是一个封闭系统——它通过 MCP 协议可以动态发现和调用外部工具服务器提供的工具,这使得 Agent 的能力边界不是由开发者预设的,而是可以在运行时动态扩展的。
如果不采用渐进式扩展会怎样? 两种极端都不可取。如果只有”工具”一级扩展,第三方开发者必须修改核心代码才能添加新能力,这会严重限制生态系统的生长。如果只提供 MCP 一级扩展,简单的自定义需求也需要搭建一个完整的工具服务器,门槛过高。渐进式扩展让每一类贡献者都能在最适合自己的抽象层次上工作。
不可变状态流转
Claude Code 的状态管理借鉴了 Redux/Zustand 的不可变状态模式。核心状态存储由一个极简的 store 实现完成。这个实现虽然简洁,但蕴含了重要的设计决策:
- Updater 函数模式:状态更新接收一个
(prev: T) => T函数,而非新状态值本身。这确保了状态的每次更新都基于前一个状态,避免了竞态条件。 - 引用相等性检查:通过引用比较确保只有真正发生变化时才触发通知,避免不必要的重渲染。
- 订阅/取消订阅模式:监听器通过集合管理,返回清理函数,防止内存泄漏。
在对话循环层面,不可变性同样被严格遵守:跨迭代状态通过整体替换的方式更新,每次迭代开始时从状态对象解构出需要的字段,确保读操作使用的是不可变快照。
不可变状态流转的好处是多方面的:状态变化可预测、可追溯、可调试;在子智能体场景下,父智能体可以安全地将状态快照传递给子智能体而不担心被意外修改;在推测执行(Speculation)场景下,状态回滚变得简单而安全。
如果不这样设计会怎样? 如果使用可变状态(直接修改对象的字段),在并发场景下会出现经典的竞态条件:工具 A 的执行修改了状态,但工具 B 在读取状态时看到的是修改了一半的不一致数据。在子智能体场景下更危险——子智能体可能意外修改父智能体的状态,导致主循环的行为变得不可预测。这类 bug 极难复现和调试,因为它取决于异步操作的具体调度顺序。







