Skip to Content
AI 时代的 CLI 工具开发实战附录 B:从 Claude Code 源码学到的 10 个设计模式

附录 B:从 Claude Code 源码学到的 10 个设计模式

以下分析基于 2025 年 5 月 Claude Code 公开源码版本。Claude Code 处于活跃开发中,部分实现细节可能已经更新。

Claude Code 是 Anthropic 推出的 AI 编程 CLI 工具,也是目前 Node.js + TypeScript 技术栈下最复杂的 CLI 应用之一。它的代码库展示了很多值得借鉴的架构模式。以下 10 个模式提炼自其公开源码和技术文档。


1. React-in-Terminal 架构

Claude Code 用 React 构建终端界面,但没有直接使用 Ink 的默认 reconciler,而是实现了自定义的 React reconciler。

标准 Ink 的渲染策略是”状态变化时重新计算整个可见区域的字符矩阵”。对于静态内容这没问题,但 Claude Code 的核心场景是流式输出——AI 的回复逐 token 到达,每秒可能触发几十次状态更新。如果每次都完整重绘,性能开销不可接受。

Claude Code 的自定义 reconciler 做了两件关键优化:一是增量追加——流式文本直接 append 到输出缓冲区的末尾,不触发全局重绘;二是脏区域标记——只有被修改的组件节点才参与 diff 计算。这和游戏引擎的”脏矩形”渲染技术是同一个思路。

此外,Claude Code 内置了一个 ANSI 转义序列解析器。终端中的子进程输出(如 git diff、cat 等命令的结果)包含颜色转义码,需要正确解析才能在 React 组件中渲染。解析器把 ANSI 序列拆分为文本片段和样式信息,再由 React 组件分别渲染。

借鉴点:当标准框架的性能不满足需求时,可以在 reconciler 层面做定制,而不是放弃声明式架构回退到命令式。


2. 中间件链式权限系统

Claude Code 在每次工具调用(读文件、写文件、执行命令等)前后都执行一系列检查。这些检查以中间件的形式组织,形成一条处理链。

核心概念是两个 Hook:PreToolUse 和 PostToolUse。每个 Hook 是一个函数数组,按顺序执行。PreToolUse 在工具执行前运行,可以放行、拦截或要求用户确认。PostToolUse 在工具执行后运行,可以审计日志或修改输出。

用户请求 → PreToolUse[权限检查] → PreToolUse[路径校验] → PreToolUse[用户确认] ↓ 工具执行 ↓ PostToolUse[审计日志] ← PostToolUse[输出过滤] ← PostToolUse[结果校验]

中间件可以返回三种结果:allow(放行,继续执行下一个中间件)、deny(拒绝,终止链并返回错误)、ask(暂停链,弹出交互式确认界面,用户同意后继续)。

这种设计的好处是权限逻辑和业务逻辑完全分离。添加新的安全规则只需要注册一个新的 PreToolUse 中间件,不需要修改任何工具的实现代码。

借鉴点:对于需要权限控制的 CLI 工具,中间件模式比在每个命令中写 if-else 更优雅、更可维护。


3. 四级权限模型

Claude Code 把工具分为四个安全等级,每个等级有独立的权限策略:

等级工具默认策略
只读Read(读文件)、WebFetch(网络请求)自动放行
受限写入Edit(编辑文件)首次需确认,确认后同类操作自动放行
命令执行Bash(执行 shell 命令)每次需确认,支持白名单规则
危险操作删除文件、force push 等始终需要确认,不支持自动放行

权限规则支持通配符配置。用户可以在项目的 .claude/settings.json 中定义规则:

{ "permissions": { "allow": [ "Bash(npm test)", "Bash(git *)", "Edit(src/**)" ], "deny": [ "Bash(rm *)", "Edit(.env*)" ] } }

Bash(npm test) 表示允许执行 npm test 命令无需确认;Edit(src/**) 表示允许编辑 src 目录下任何文件无需确认;Bash(rm *) 表示禁止任何以 rm 开头的命令。

借鉴点:权限不是全有或全无的开关。按操作类型分级,配合通配符规则,可以在安全性和使用体验之间取得平衡。


4. 流式输出组件化

AI 回复是逐 token 流式到达的,Claude Code 用一个专门的 StreamingText 组件处理这个场景。

这个组件的核心挑战不是”逐字显示”——那很简单。难点在于:

  1. Markdown 渲染的时机:AI 回复是 Markdown 格式,但在流式到达过程中,Markdown 结构是不完整的——代码块可能只有开头的 ``` 还没有结尾。组件需要判断何时可以安全地渲染 Markdown(段落结束、代码块闭合),何时只能暂时作为纯文本展示。

  2. 暂停和恢复:用户可以按 Escape 暂停流式输出(AI 继续生成但不显示),再按一次恢复。这需要一个缓冲区来存储暂停期间的文本。

  3. 取消:用户按 Ctrl+C 取消当前生成。组件需要立即停止渲染,清理已建立的 SSE 连接,并在界面上显示”已取消”状态。

  4. 性能:长回复可能有几千 token。如果每个 token 都触发 React 状态更新和重渲染,帧率会显著下降。Claude Code 的做法是用 requestAnimationFrame 风格的批处理——累积多个 token 后一次性更新状态。

借鉴点:流式输出不是简单的字符串拼接。把它封装成一个有状态的组件,内部处理缓冲、暂停、取消等复杂逻辑,外部使用者只需要传入一个 stream。


5. 多层配置合并

Claude Code 的配置来源有五层,按优先级从低到高:

硬编码默认值 < 全局配置 < 企业配置 < 项目配置 < 环境变量 < 命令行参数
  • 硬编码默认值:代码中定义的 fallback
  • 全局配置:~/.claude/settings.json,用户级偏好
  • 企业配置:企业部署时通过 MDM 下发的策略,不可被用户覆盖
  • 项目配置:.claude/settings.json,项目级设置,可以提交到 Git
  • 环境变量:CLAUDE_* 前缀的环境变量
  • 命令行参数:--model、--verbose 等

合并逻辑不是简单的 Object.assign。不同类型的配置有不同的合并策略:

  • 标量值(字符串、数字、布尔):高优先级覆盖低优先级
  • 数组值(如权限规则的 allow 列表):合并(concat),不是覆盖
  • 对象值:递归合并

企业配置有特殊地位——它的某些字段(如允许的模型列表、网络访问策略)标记为 enforced,即使项目配置或命令行参数也不能覆盖。这确保了企业安全策略的强制执行。

借鉴点:多层配置合并是 CLI 工具的常见需求。关键是明确定义每一层的优先级和合并策略,特别是数组类型字段是”覆盖”还是”合并”要想清楚。


6. 错误上报 fallback-channel

Claude Code 的错误上报不是简单的 try-catch + 日志。它实现了一个 fallback-channel 机制:当主通道异常时自动切换到备用通道。

正常情况下,错误通过结构化日志写入本地文件(~/.claude/logs/),并在用户同意的情况下上报到远程监控服务。但如果主上报通道本身出了问题——比如网络断开、磁盘满了、日志文件被锁定——错误信息会丢失。

fallback-channel 的设计是一个降级链:

远程上报 → 本地日志文件 → stderr 输出 → /tmp 临时文件

每一层失败后自动降级到下一层。最后一层(/tmp 临时文件)几乎不可能失败(除非整个文件系统只读)。即使所有持久化方案都失败,至少 stderr 能让用户看到错误信息。

这个机制还处理了一个微妙的问题:错误处理代码本身的错误。如果 try-catch 的 catch 块里的代码抛异常,通常会导致无限递归或静默丢失。fallback-channel 通过逐层降级避免了这个问题——上层的异常被下层捕获处理。

借鉴点:生产级 CLI 工具需要考虑”如果错误处理失败了怎么办”。降级链是一个简洁有效的解决方案。


7. 约定式命令发现

Claude Code 支持项目级自定义命令:把 Markdown 文件放在 .claude/commands/ 目录下,自动注册为可调用的斜杠命令。

.claude/ commands/ review.md → /project:review deploy.md → /project:deploy fix-lint.md → /project:fix-lint

文件名就是命令名,文件内容就是命令的 prompt 模板。不需要写 JavaScript、不需要注册、不需要配置——放进去就能用。

这种”约定式”设计降低了扩展门槛:任何团队成员都可以创建新命令,不需要了解 CLI 框架的内部实现。命令文件可以提交到 Git 仓库,团队共享。

命令发现的实现很简单:

  1. 启动时扫描 .claude/commands/ 目录
  2. 读取所有 .md 文件
  3. 文件名转换为命令名(去掉 .md 后缀,- 保留)
  4. 文件内容作为命令的 prompt,支持 $ARGUMENTS 变量占位符

这个模式不限于 AI 工具。任何 CLI 都可以用类似的方式支持用户自定义扩展——约定一个目录,扫描其中的文件,按文件名注册功能。repox 的插件系统也可以参考这种方式实现轻量级扩展。

借鉴点:约定优于配置。文件系统本身就是最好的注册表。


8. 沙箱隔离执行

Claude Code 的 Bash 工具(执行 shell 命令)运行在操作系统级别的沙箱中。这不是一个简单的命令过滤器,而是利用 OS 安全原语实现的强隔离。

macOS 上使用 Seatbelt(sandbox-exec),通过 .sb 配置文件定义允许和禁止的操作。Linux 上组合使用 Bubblewrap(文件系统命名空间隔离)、Landlock(用户态文件系统访问控制)和 seccomp(系统调用过滤)。

沙箱的核心原则是最小权限:

  • 文件系统:只允许读写项目目录和必要的系统目录(/usr、/tmp)
  • 网络:默认禁止所有网络访问(防止数据外泄)
  • 进程:禁止创建特权进程
  • 设备:禁止访问硬件设备

沙箱配置是动态的——用户可以通过权限规则放宽限制(比如允许 npm install 的网络访问),但不能超出 Claude Code 定义的最大权限边界。

实现中的一个细节:沙箱创建有性能开销,特别是 Bubblewrap 需要设置新的命名空间。Claude Code 通过预创建沙箱环境来减少延迟——在 AI 开始思考时就准备好沙箱,等 AI 决定执行命令时沙箱已经就绪。

借鉴点:应用层的命令过滤不可靠(总有绕过方式),OS 级沙箱才能提供真正的安全保证。但沙箱的配置复杂度很高,建议渐进式实现——先做应用层白名单,再逐步引入 OS 级隔离。


9. 上下文压缩

AI 对话有 token 上限(Claude 的上下文窗口是 200K tokens)。长时间使用 Claude Code 后,对话历史会逼近这个限制。Claude Code 的做法不是简单地截断历史,而是压缩。

压缩流程:

  1. 监测当前对话的 token 数量
  2. 当接近上限(比如超过 80%)时触发压缩
  3. 把历史消息发送给 AI,要求生成一个”摘要”——保留关键信息(文件路径、决策依据、未完成的任务),丢弃冗余内容(重复的代码片段、已解决的讨论)
  4. 用摘要替换原始历史,释放 token 空间

这比简单截断好得多。截断会丢失早期的重要上下文(比如用户在对话开头定义的需求),而压缩可以保留要点。

压缩还有一个副作用的好处:它相当于定期”刷新”AI 的注意力。长对话中 AI 可能被中间的无关讨论分散注意力,压缩后的摘要重新聚焦在核心任务上。

对 CLI 开发的启示:任何涉及长时间 AI 对话的工具都需要考虑上下文管理。不只是 token 限制——即使窗口足够大,过长的上下文也会降低 AI 的回答质量(“大海捞针”问题)。

借鉴点:资源受限时,压缩优于截断。保留结构化摘要而不是随机丢弃内容。


10. MCP 集成

Claude Code 同时扮演两个 MCP 角色:

作为 MCP Client:Claude Code 可以连接外部 MCP Server,调用第三方工具。比如连接一个数据库 MCP Server 查询数据,或连接一个 Jira MCP Server 读取任务信息。这使得 Claude Code 的能力可以无限扩展,而不需要在自身代码中集成每一个外部服务。

作为 MCP Server:其他 AI 应用可以通过 MCP 协议调用 Claude Code 的能力(文件读写、代码搜索、命令执行等)。这使得 Claude Code 可以被嵌入更大的 AI 工作流中。

MCP 集成的实现要点:

  1. 工具发现:启动时从配置文件读取 MCP Server 列表,连接每个 Server,获取可用工具的 schema。这些工具和内置工具合并在一起,AI 可以根据需要选择调用。

  2. 生命周期管理:MCP Server 作为子进程启动,Claude Code 负责管理它们的生命周期——启动、健康检查、崩溃重启、退出清理。

  3. 权限透传:通过 MCP 调用的外部工具同样经过权限系统的检查。一个恶意的 MCP Server 不能绕过 Claude Code 的安全策略。

  4. 协议版本协商:MCP 协议在快速演进中,不同 Server 可能实现了不同版本的协议。Claude Code 在连接时进行版本协商,确保兼容性。

借鉴点:MCP 不只是一个”对外暴露接口”的方案,更是一个”引入外部能力”的方案。一个 CLI 工具同时作为 MCP Client 和 Server,就有了连接整个 AI 工具生态的能力。

本书资源

继续阅读 · 同作者其他书

Last updated on