从零开始两天构建一个 Claude Code:带你拆解 AI CLI 的每一层

本文详细介绍了使用 TypeScript 从零构建生产级 AI Agent CLI(类 Claude Code)的技术实践。文章深入拆解了六层架构设计,重点讲解了 Agent Loop 状态机、系统提示词分段缓存优化、内置工具链实现、安全权限控制、MCP 与 LSP 协议集成,以及多 Agent 协作与持久化记忆系统的核心原理。

Image

前两天突发奇想:一个生产级的 agentic CLI 到底需要哪些组件?每一层的具体怎么实现?SSE 缓冲区怎么管理、system prompt 怎么分段、工具权限怎么拦截、上下文满了怎么压缩。这些问题靠读文档回答不了,靠逆向混淆代码效率极低。

所以选择了另一条路:以 Claude Code 为参照系,从零重建一个功能等价的实现——纯 TypeScript,零框架,唯一的依赖是 fast-glob(因为原生 glob 在跨平台路径处理上有已知缺陷)。

两天之后的结果是 46 个文件,一万行 TypeScript。这篇文章记录的是这个过程中每一层的技术决策和实现细节。

Image

整体架构

在开始写任何代码之前,先要在脑子里跑通一个最小循环:用户输入一句话,CLI 怎么把它变成一次 API 调用,API 响应怎么变成终端输出或工具执行,工具结果怎么再送回模型。把这个循环画清楚,架构就基本定了。

核心流程如下:

用户输入 → 组装请求 → API 调用(SSE)→ 解析响应事件流 → 根据 stop_reason 决定分支:

  • end_turn:输出文本,结束本轮。
  • tool_use:执行工具调用 → 将 tool_result 追加到 messages → 反复迭代。

目录结构按层划分:

  • core/:引擎层,包含 Agent Loop、SSE 客户端、context 管理、compact 逻辑。
  • tools/:工具系统,包含所有内置工具的实现和 MCP 客户端。
  • ui/:终端渲染层,处理流式文本输出、进度指示、颜色主题。
  • plugins/:扩展系统,允许运行时注入工具和 hook。
  • skills/:技能库,对应 Claude Code 的 slash command 高级功能。
  • commands/:处理 / 前缀命令的解析和分发。

这六层之间的依赖是单向的,core/ 不依赖 ui/,tools/ 不依赖 skills/。

技术选型的核心理由是 Node.js 22 的原生能力。fetch 和 ReadableStream 在 22 版本中已经稳定,不需要 node-fetch 或任何 HTTP 客户端库。TextDecoder 处理 UTF-8 字节流,Buffer 处理二进制,child_process.exec 执行 Shell 命令——所有这些都是标准库。唯一无法绕过的是文件系统 glob,fast-glob 在处理 gitignore 规则和大型目录的性能上比原生实现好一个数量级,这个依赖是值得的。

多平台 LLM 兼容

默认支持御三家和自定义平台,自定义模型(OpenAI 兼容格式)是一个附加需求。部分本地模型(如 Ollama、LM Studio)暴露 OpenAI 兼容接口,事件格式与 其他格式不同。处理方式是在 SSE 客户端初始化时传入 format: 'openai' 参数,在事件解析层做格式适配,将 OpenAI 的 delta 结构翻译成统一的内部事件类型。这样 Agent Loop 层完全不感知 API 格式差异。

System Prompt 分段架构与 Prompt Caching

最直觉的 system prompt 写法是一个大字符串,把所有指令拼在一起传给 API。这个方式在原型阶段没问题,但在生产环境有两个显著缺陷:

  1. 每轮对话 system prompt 几乎不变,但如果以单字符串传入,API 缓存无法有效命中。
  2. 部分内容(如当前目录、Git 状态、CLAUDE.md 文件内容)每轮都会变化,与静态内容混在一起会污染缓存。

将 system 参数设为 block 数组,每个 block 可以独立设置 cache_control。按可变性将 system prompt 分为两类:

静态段

进程生命周期内不变,打上 cache_control 后首次写入缓存,后续命中。

  • 身份声明(参考龙虾的 User、Soul)
  • 工具使用规范(何时用 Bash vs 读文件、何时拒绝执行)
  • 编码风格(规范、注释原则)
  • 安全执行规则(禁止执行的命令类型)

这是官方订阅能节省 API 费用的核心原因,因为缓存命中率极高。

动态段

不带 cache_control,每轮重新计算。

  • 当前工作目录和系统环境(每次启动可能不同)
  • Git 仓库状态(git status 输出,每轮可能变化)
  • CLAUDE.md 内容(用户可随时修改)
  • MCP 服务器的自定义指令(运行时发现)

完整请求体的 system 字段最终是一个有序 block 数组,顺序固定:身份 → 工具指南 → 编码规范 → 安全规则 → 风格指南 → 环境信息 → Git 上下文 → CLAUDE.md → MCP 指令。Anthropic 的 prompt caching 按照 block 数组的前缀匹配来识别缓存,排在前面的静态内容越稳定,缓存命中率越高。

Agent Loop:while 循环背后的状态机

Image

Agent Loop 的骨架是一个有上限的 while 循环,最大迭代次数 25 次。25 轮足够完成大多数真实任务(读文件、分析、修改、验证通常在 10 轮内完成),同时防止 runaway loop 耗尽 API 额度。

每次迭代的流程:

  1. 检查是否需要 compact(压缩)。
  2. 构建完整 prompt。
  3. 发起流式请求。
  4. 实时处理事件流。
  5. 检查 stop_reason:tool_use 则执行工具,end_turn 或 max_tokens 则结束循环。
  6. 构建 tool_result message,追加到 messages 数组,进入下一轮。

工具执行管线是 Agent Loop 中最复杂的部分,共六个阶段:

  1. renderToolCall:在终端展示将要执行的工具名和参数。
  2. permissionCheck:根据工具类型和参数决定是否需要用户确认。
  3. preHook:插件系统的前置拦截点。
  4. checkpoint:对于破坏性操作,在执行前快照相关文件状态。
  5. executeTool:调用实际工具函数。
  6. postHook:插件后置钩子。

自动 compact 机制处理上下文窗口溢出。在每轮迭代开始时,估算当前 messages 数组的 token 数(总字符数除以 4),如果超过模型上下文限制的 85%,触发压缩——发起一次独立的 API 调用生成摘要,用摘要替换原来的 messages 数组。

21 个内置工具

Image

工具系统的入口是 TOOL_DEFINITIONS,一个 JSON Schema 数组,描述每个工具的名称、用途和参数结构。模型通过这个数组“知道”有哪些工具可以调用。

核心工具的实现细节:

  • Read:读取文件后加行号前缀,支持 offset 和 limit 参数分页读取。
  • Write:写入前确保父目录存在。
  • Edit:精确字符串替换,关键约束是 old_string 必须在文件中唯一出现,否则报错,迫使模型提供精确的定位。
  • Bash:执行 Shell 命令,120 秒超时,输出过长时进行截断。
  • Grep:自实现 regex 引擎,支持多种输出模式,不依赖系统 grep。
  • WebFetch / WebSearch:处理 HTML 剥离与截断,搜索走 DuckDuckGo。

Deferred Tools 是一个性能优化。低频工具不放入每次请求的 tools 数组,而是标记为 deferred。模型需要时先通过 ToolSearch 查询获取完整 schema,再在下一轮调用。这降低了约 40% 的固定开销。

权限系统:三种模式与两阶段分类器

Image

权限系统是安全核心,设计目标是在用户体验与安全性之间取得平衡。

三种模式

  • default:safe 类工具自动执行,dangerous 和 write 类需要用户确认。
  • auto:绕过所有交互式提示,适合 CI 环境,但底线规则(deny rules)仍生效。
  • plan:只读沙箱,仅放行 safe 工具,用户查看计划后再切换模式。

两阶段分类器

  • Stage 1(模式匹配):维护已知安全/危险命令规则表,覆盖 90% 情况,零延迟。
  • Stage 2(模型辅助):处理模糊情况,将命令发给轻量模型(如 Haiku)判断。

MCP 动态工具与 LSP 集成

MCP(Model Context Protocol)让工具变成可以独立部署的进程。协议层是 JSON-RPC 2.0 over stdio。McpManager 管理多个 server 的生命周期,工具名通过前缀进行命名空间隔离。

LSP 集成则为模型提供实时的代码诊断信息。LspClient 实现 LSP 客户端侧,Write/Edit 执行后自动通知语言服务器,诊断结果作为 lsp_diagnostics 注入下一轮 system prompt。这让模型能立即看到编译器反馈,缩短了修复路径。

插件系统与 Skills

Image

插件系统允许在不修改核心代码的情况下扩展能力。每个插件通过 plugin.json 声明扩展点:skills、agents、hooks、commands、mcpServers 和 lspServers。

Skills 是参数化的 prompt 模板。例如输入 /commit 时,系统展开模板、注入上下文并提交给 agent loop。内置技能包括 Git 操作(commit/pr/review)、项目初始化(init)、代码审查(simplify)等。

Agent Teams:多 Agent 协作

当任务可以分解时,多 Agent 并行能显著缩短时间。子 Agent 通过 executeSubAgent() 运行,是主循环的简化版。

  • 隔离模式:可选 worktree 隔离,子 Agent 在独立分支操作,由主 Agent 决定是否合并。
  • 后台执行:支持 run_in_background,完成后以 tool_result 形式通知。
  • 协作流程:主 Agent 创建团队 → 分发任务 → 等待/汇总结果 → 释放资源。

Image

Auto Memory:跨会话记忆

Auto Memory 用文件系统模拟持久记忆,存储在项目专属目录下,分为四种 Markdown 类型:

  • user:用户偏好。
  • feedback:行为纠正。
  • project:项目约定。
  • reference:外部资源指针。

MEMORY.md 是索引文件,对话开始时注入 system prompt,模型按需读取。记忆的读写完全复用现有工具链,用户也可以直接编辑文本。

总结与思考

整个项目验证了一个认知:Claude Code 的工程质量确实极高。其分段 prompt 的三层缓存设计展示了极强的工程意识。

构建 Agent 工具的核心难点在于 Harness Engineering(治理工程)。调用 API 只是基础,真正的挑战在于如何正确反馈工具结果、处理流式交互以及在长任务中实现错误恢复。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
icebearminer
icebearminer
江湖只有他的大名,没有他的介绍。