如何构建一个生产级 AI Agent CLI —— 以 Claude Code 架构探索

如何构建一个生产级 AI Agent CLI ------ 以 Claude Code 架构探索

📂 基于 Claude Code v2.1.88 源码分析(1884 文件 / 51 万行 TypeScript)


一、背景:Agent CLI 为什么值得研究

2026 年是 AI Coding Agent 的爆发年。这个赛道上出现了三种产品形态:IDE 插件(Cursor、Copilot)、Web 平台(Devin、OpenHands)、以及终端 CLI(Claude Code、Aider)。

其中 CLI 形态最值得架构师关注------因为它是三种形态里工程约束最苛刻的

  • 没有浏览器的富交互,只有终端的字符流
  • 没有 WebSocket 的天然双向通道,要自己处理 SSE
  • 没有 IDE 的文件系统抽象,要直接操作磁盘
  • 用户对延迟极度敏感------终端里的 5 秒空白比网页上的 5 秒长得多
  • 权限模型比 IDE 插件复杂------终端里一个 rm -rf 是真的会执行

Claude Code 是 Anthropic 在这个约束集下交出的生产级答卷 。npm 包 @anthropic-ai/claude-code,v2.1.88 版本含 1884 个源文件、约 51 万行 TypeScript、40+ 内置工具、完整的 MCP 协议集成、三层上下文压缩、多 Agent 协作和 React/Ink 终端 UI。

本文的目标不是源码解读,而是从它的架构设计中提取可复用的方法论------如果你是下一个想做 Agent CLI 的人,这篇文章告诉你要解决哪些问题,以及每种问题有哪些解法。


二、整体思路:终端里的分布式系统

大多数人对 Agent CLI 的第一直觉是"一个 chat 循环 + 几个 tool 调用"。200 行代码就能跑。

但 Claude Code 的 51 万行告诉我们:一个生产级 Agent CLI 本质上是一个跑在终端里的分布式系统。它至少需要协调六个子系统:

复制代码
  ┌───────────────────────────────────────────────────────┐
  │                    入口 & UI 层                        │
  │  多模式入口 (REPL/headless/print/SDK)                  │
  │  React + Ink 组件树 (140+ 组件, 30+ 分组)              │
  │  80+ 斜杠命令                                          │
  └─────────────────────────┬─────────────────────────────┘
                            │
  ┌─────────────────────────▼─────────────────────────────┐
  │                   Agent 主循环                         │
  │  AsyncGenerator 全链路流式                             │
  │  while(true) { think → act → observe }                │
  │  7 个不可变状态快照 + 9 种退出路径                      │
  └───┬──────────────┬──────────────┬─────────────────────┘
      │              │              │
  ┌───▼───┐    ┌─────▼─────┐   ┌───▼──────────┐
  │ Tool  │    │  Context  │   │   MCP 协议     │
  │ System│    │  压缩层    │   │   集成层       │
  │       │    │           │   │               │
  │40+工具│    │3层压缩    │   │5种传输方式    │
  │5步鉴权│    │compact_   │   │动态工具发现    │
  │并发安全│   │boundary   │   │鉴权透传       │
  └───┬───┘    └─────┬─────┘   └───┬──────────┘
      │              │              │
  ┌───▼──────────────▼──────────────▼─────────────────────┐
  │                   状态 & 持久化层                       │
  │  AppState 全局状态树                                    │
  │  JSONL 追加日志 (用户消息阻塞写, Assistant 异步写)       │
  │  --continue / --resume / --fork-session                │
  └───────────────────────────────────────────────────────┘

这六个子系统不是堆叠关系,而是一组彼此咬合的齿轮

  • Agent 主循环负责"推理的节奏"------每一步该不该调用 Tool、什么时候退出
  • Tool System 负责"能力的边界"------能做什么、谁允许你做、并发安全吗
  • Context 压缩负责"记忆的容量"------窗口满了怎么办、旧的该记住还是忘记
  • MCP 负责"生态的入口"------外部工具如何进来、鉴权如何统一
  • State 持久化负责"时间的连续性"------崩溃重启后能继续吗、能回到过去吗
  • UI 负责"人机交互的体验"------REPL 流式渲染、权限弹窗、进度条

任何一环的缺失,都会在最坏的时候暴露出来。


三、竞品对比:三条路线,同一个问题

Agent CLI 领域目前有三条路线。它们之间的差异,本质上是对同一个问题的不同回答:

"自主性和可控性的边界画在哪里?"

3.1 IDE 插件路线:Cursor / GitHub Copilot

复制代码
  ┌────────────────────────────────────┐
  │  人在回路,AI 辅助                   │
  │                                    │
  │  User → 写代码 → AI 建议 → User 确认 │
  │                                    │
  │  工具: 代码补全、内联建议、chat 面板  │
  │  自主性: 低(需要人工触发)           │
  │  安全: 高(人拍板)                  │
  │  上下文: IDE 自动收集                │
  └────────────────────────────────────┘

IDE 插件把边界画在**"AI 只建议,人做决策"**。优势是安全可控,代价是自主性低------AI 不能主动执行多步操作,每次 Tool 调用都需要用户确认。这让它在"修改一个函数"时体验流畅,但在"重构一个模块"时显得笨拙。

3.2 轻量开源路线:Aider

复制代码
  ┌────────────────────────────────────┐
  │  最小内核,LLM 驱动                  │
  │                                    │
  │  User → 描述需求 → LLM 输出 diff    │
  │         → 自动 apply → User review │
  │                                    │
  │  工具: 文件读写、git、shell         │
  │  自主性: 中(自动 apply + 人工 review)│
  │  安全: 中(git 可回滚)             │
  │  上下文: 手动管理                   │
  └────────────────────────────────────┘

Aider 比 IDE 插件更自主------LLM 生成的 diff 自动 apply,用户事后 review。核心代码量小(几千行 Python),架构清晰透明。但它不做上下文压缩、不支持多 Agent、没有 MCP 生态、没有会话持久化。不是做不到,而是这些都不是"最小可用"的范畴

3.3 全栈 CLI 路线:Claude Code

复制代码
  ┌────────────────────────────────────┐
  │  自治 Agent + 可扩展生态             │
  │                                    │
  │  User → 描述目标 → Agent 自主规划    │
  │         → 多轮 Tool 调用 → 完成     │
  │                                    │
  │  工具: 40+ 内置 + MCP 外部 + 子Agent│
  │  自主性: 高(Agent 自主决策多步)    │
  │  安全: 分层(5步鉴权链 + 权限模式)  │
  │  上下文: 3层自动压缩                │
  └────────────────────────────────────┘

Claude Code 把边界画在**"Agent 可以自主执行,但每类操作有独立的权限策略"**。它不是"全信任"或"全拒绝",而是给每一步 Tool 调用都套上可配置的决策链。代价是 51 万行代码的工程复杂度------但这不是过度设计,而是"自主性每提高一级,安全性就要多一层"的必然结果。

3.4 三条路线的本质

复制代码
  自主性
    ▲
    │                    ● Claude Code
    │                   (分层鉴权,生态扩展)
    │
    │          ● Aider
    │         (自动apply,git回滚)
    │
    │    ● Cursor/Copilot
    │   (人拍板)
    │
    └──────────────────────────────▶ 可控性

三条路线不是在比"谁更好",而是在比"谁愿意为更高的自主性付出更多的工程代价"。Aider 用 git 回滚做安全网,因此可以自动 apply。Claude Code 用 5 步鉴权链做安全网,因此可以让 Agent 自主规划执行。

核心结论:Agent CLI 的工程复杂度 = 自主性 × 安全性。提高任何一个维度,另一个维度都要同步加固。


四、展开:五个关键子系统的设计决策

4.1 Agent 主循环:AsyncGenerator + 不可变状态

Agent CLI 最核心的代码就是主循环。Claude Code 的主循环签名为 query(): AsyncGenerator ------ 每一步推理都是一个 yield,全链路流式。

复制代码
  ┌─────────────────────────────────────────────┐
  │              Agent 主循环                    │
  │                                             │
  │  IDLE                                       │
  │    │                                        │
  │    ▼                                        │
  │  SETUP (初始化上下文、工具、权限)             │
  │    │                                        │
  │    ▼                                        │
  │  while(true) {                             │
  │    ┌─ PRE-API (构建 message)               │
  │    ├─ API Call (SSE streaming)             │
  │    ├─ Tool Execution (到达即执行)            │
  │    ├─ contextCompact (超阈值自动压缩)        │
  │    └─ Continue or Exit (9种退出路径)        │
  │    state = { ...state, newFields }  ◀ 不可变 │
  │  }                                          │
  └─────────────────────────────────────────────┘

为什么是 AsyncGenerator 而不是回调? 因为 Agent 的每一步都可能涉及异步操作------LLM API 调用是异步的、MCP 工具调用是异步的、子 Agent 执行是异步的。回调会导致"回调地狱",事件会导致状态管理混乱。AsyncGenerator 让主循环的每一轮都是一个 await + yield 的干净交接。

为什么用不可变状态? 主循环有 7 个 continue 点,每个点都可能因为异步操作而交错执行。如果状态用可变字段 this.state.field = x,两个异步分支可能互相覆盖。不可变 state = { ...state, field: x } 保证了每个分支读到的都是自己的快照。

为什么要 9 种退出路径? 因为 Agent 的结束不只有"任务完成"一种:

退出原因 触发条件 处理方式
completed LLM 输出 stop_reason 返回最终结果
hook_stopped PreToolUse Hook 返回 false 停止执行
blocking_limit 权限被用户拒绝 中断并告知
max_turns 达到最大轮次 优雅降级
prompt_too_long 上下文超限 压缩后重试
model_error API 返回错误 重试或退出
aborted 用户中断 保存状态

设计启示:Agent 循环的复杂度不在"正常路径",而在"异常路径"。200 行 Demo 和 50 万行产品的差距,80% 在异常处理上。


4.2 工具系统:注册 != 安全,权限需要一条决策链

工具系统的设计思路可以分成两层:能力层 (注册什么工具)和安全层(谁可以调用、什么时候可以调用)。

复制代码
  ┌────────────────────────────────────────────────────────┐
  │                 工具权限决策链                          │
  │                                                       │
  │  ① validateInput(input)  ── 输入格式校验              │
  │          │                                            │
  │          ▼                                            │
  │  ② PreToolUse Hooks  ── 前置钩子(可拦截)             │
  │          │                                            │
  │          ▼                                            │
  │  ③ Permission Rules  ── 规则匹配                      │
  │     alwaysDeny > alwaysAllow > alwaysAsk              │
  │          │                                            │
  │          ▼                                            │
  │  ④ Interactive Prompt  ── 交互式确认                  │
  │          │                                            │
  │          ▼                                            │
  │  ⑤ checkPermissions  ── 最终检查                      │
  │     · 路径沙箱检查                                     │
  │     · Agent 黑名单检查                                 │
  │     · MCP 工具鉴权透传                                 │
  └────────────────────────────────────────────────────────┘

为什么需要 5 步而不是 1 步? 因为不同场景需要不同的安全级别:

  • Plan 模式:只读工具自动放行(FileRead、Grep),写入工具需确认
  • Auto 模式:让 LLM 自己判断危险等级并请求确认
  • Bypass 模式:跳过所有检查(仅限受信任的自动化脚本)

并发安全是工具自己的属性。 每个 Tool 声明 isConcurrencySafe() ------ FileRead 可以 10 个并发执行(读操作安全),FileEdit 必须串行(防止写冲突)。执行器根据这个标记决定调度策略。

设计启示:权限不是二进制的"允许/拒绝"。它是一个可配置的决策链------每多一层配置就多一层灵活性,但同时也多一层代码复杂度。你需要的是"刚好够用的决策链",既要防止意外的 rm -rf,又要避免每次 grep 都弹窗。


4.3 上下文压缩:不是"能不能记住",而是"该记住什么"

Agent CLI 的最大工程挑战不是 LLM 调用,而是上下文窗口管理。每轮对话都在消耗 token,一旦超出模型限制,Agent 就会"失忆"。

Claude Code 用了三层压缩策略:

复制代码
  ┌───────────────────────────────────────────────┐
  │              上下文压缩三层策略                 │
  │                                               │
  │  ① autoCompact ── 主动压缩                    │
  │     触发: 当前 token 数 > (窗口 - 13K buffer)  │
  │     动作: 调用 Claude 总结旧消息,              │
  │           插入 compact_boundary 标记           │
  │                                               │
  │  ② snipCompact ── 清理过期标记                 │
  │     触发: autoCompact 之前                     │
  │     动作: 删除旧的 compact_boundary,           │
  │           只保留最新的一条                     │
  │                                               │
  │  ③ contextCollapse ── 被动重组                 │
  │     触发: API 返回 413 (prompt_too_long)       │
  │     动作: 将部分上下文持久化到 collapse store,  │
  │           不在 REPL 中显示                     │
  └───────────────────────────────────────────────┘

compact_boundary 是三层策略的"书签"。 它是一条特殊的系统消息,标记"此处发生过压缩"。只有 boundary 之后的消息发给 API;boundary 之前的被摘要替代。如果发生多次压缩,旧的 boundary 会被 snipCompact 清理。

为什么压缩前后有一层 13K buffer? 因为压缩本身消耗 token------你需要给 LLM 足够的空间来"读旧消息 + 写摘要"。不留 buffer 的结果是:刚要压缩就超限,API 返回 413,被动重试,用户体验断崖式下跌。

设计启示:上下文管理占 Claude Code 工程复杂度的 30% 以上。这不是 LLM 的问题,而是"人机对话天然会变长"的问题。你的 Agent 如果不做压缩,迟早会遇到 413------不是在开发时,而是在用户用到第 100 轮的时候。


4.4 多 Agent 协作:隔离是安全的前提

Claude Code 支持多 Agent,但不是"让多个 Agent 共享一套状态"------而是三种隔离级别,越危险越隔离

复制代码
  ┌──────────────────────────────────────────────────────┐
  │                子 Agent 三种隔离模式                   │
  │                                                      │
  │  fork (子进程)           │  Agent B 有独立进程         │
  │  · 独立消息历史          │  · 共享文件磁盘缓存         │
  │  · 权限降级             │  · 无 AppState 写入         │
  │                          │                            │
  │  remote (Bridge)         │  Agent B 在远端执行         │
  │  · 完全隔离              │  · 通过网络通信             │
  │  · 独立权限模型          │  · 独立文件系统             │
  │                          │                            │
  │  in-process (ALS)        │  Agent B 同进程不同上下文   │
  │  · AsyncLocalStorage     │  · setAppState 改为 no-op  │
  │  · 工具池过滤            │  · 禁止危险工具             │
  └──────────────────────────────────────────────────────┘

子 Agent 的两个安全机制:

  1. 权限降级 :子 Agent 的 setAppState 是一个 no-op。它不能修改主 Agent 的全局状态。只有 setAppStateForTasks 能写入根 Store------且这个函数受额外的权限检查。

  2. 工具池过滤 :子 Agent 拿到的工具列表是主 Agent 的子集。危险工具(如 rm -rfsudo)被移除。

Swarm 团队模式 更进一步:Leader Agent + 多个 Teammate,共享一个 Task Board。Task Board 是 Agent 之间唯一的通信媒介------没有直接的 sendMessage(agentB, "hello"),只能通过 Board 认领任务和提交结果。这种"通过共享数据结构通信"的模式比"直接对话"更容易审计和回滚。

设计启示:多 Agent 协作的难点不是通信协议,而是隔离。增加的每一个 Agent 都是一个"不可信的外部实体"------即使它跑在同一个进程里。权限降级和工具池过滤不是在过度设计,而是在防止"子 Agent 污染主会话"。


4.5 状态持久化:不是存下来就行,是怎么存才能不丢也不慢

Agent CLI 的会话可能持续数小时、数十轮对话。如果崩溃后一切归零,用户体验会从"好用的工具"变成"不敢用的工具"。

Claude Code 的持久化方案是 JSONL 追加日志 + 差异写入策略

复制代码
  ┌──────────────────────────────────────────────────────┐
  │              JSONL 会话持久化                          │
  │                                                      │
  │  存储路径: ~/.claude/projects/<hash>/                 │
  │            sessions/<session-id>.jsonl               │
  │                                                      │
  │  格式: 每行一个 JSON 对象                             │
  │  {"type":"user","message":{"role":"user",...}}       │
  │  {"type":"assistant","message":{"role":"assistant"...│
  │  {"type":"system","subtype":"compact_boundary",...}  │
  │                                                      │
  │  写入策略 (差异化的):                                  │
  │  · user 消息:   同步写入 (block, 防崩溃丢数据)        │
  │  · assistant:   异步写入 (fire-and-forget, 性能)      │
  │  · progress:    内联去重 (不追加重复行)                │
  └──────────────────────────────────────────────────────┘

为什么 JSONL 而不是 SQLite? 因为 Agent 的会话是"追加为主"的日志------每轮对话追加一行,几乎不需要 UPDATE 或 DELETE。JSONL 的追加性能是 O(1),SQLite 的 B-tree 写是 O(log n)。对于"追加"这个操作,JSONL 是最快的。

为什么用差异写入策略? 因为不同类型的消息对可靠性和延迟的要求不同:

  • 用户消息丢失 = 用户白打了,必须可靠(同步写入)
  • Assistant 消息丢失 = 下次 --resume 时重跑一轮,可以容忍(异步写入)
  • Progress 消息重复 = 界面抖动,需要去重(内联合并)

三种会话恢复模式:

命令 效果
--continue 恢复最近的会话
--resume <id> 恢复指定会话
--fork-session 克隆会话历史,新 ID 继续

设计启示:持久化的复杂度不在"存什么",而在"怎么存才能同时满足可靠性和性能"。同步 + 异步 + 去重的三层策略,是对不同类型消息的"差异 QoS 保障"。


五、总结:生产级 Agent CLI 的五个必要条件

从 Claude Code 的架构往回看,一个生产级 Agent CLI 至少要解决五个问题。不是"最好有",而是"必须有"。

① 流式全链路

复制代码
API SSE → AsyncGenerator → Tool Executor → Ink UI
  │            │               │              │
  └────────────┴───────────────┴──────────────┘
          每个环节都在流式传递,Tool 到达即执行

如果 API 是流式的但 Tool 执行是批量的,你只省了"等 LLM 输出"的时间,没省"等 Tool 执行"的时间。Claude Code 的 Tool 是到达即执行------LLM 输出第一个 Tool call 的瞬间就开始执行,不需要等所有 Tool call 都输出完。

② 安全分层,可配置不绑架

"要么全信任,要么全拒绝"不是安全模型,是偷懒。Agent 的危险程度是连续的------greprm -rf 安全得多,应该有不同的确认策略。5 步鉴权链的价值不在于"5 步",而在于每一步都可以独立配置

③ 上下文管理是核心,不是附加功能

不要等到窗口满了再想怎么办。autoCompact + snipCompact + contextCollapse 三层策略是"预防 + 治疗 + 抢救"。13K token buffer 是"手术安全空间"------不让压缩本身消耗的 token 撑爆窗口。

④ 状态可恢复,崩溃无感

--continue 不是锦上添花,是用户的信任锚点。如果一次崩溃就回到原点,没人敢在重要项目上用 Agent。JSONL + 差异写入策略,用极低的工程成本换取了极高的用户信任。

⑤ 可扩展但不失控

MCP 协议让外部工具进入 Agent 的工具池。但每进入一个新工具,就多一个安全风险。Claude Code 的做法是:MCP 工具走同一套鉴权链,子 Agent 权限自动降级。扩展性和安全性不是矛盾的------只要在架构设计阶段就考虑二者的咬合关系。


最后

Claude Code 的 51 万行代码不是一次性堆出来的。它经历了从"一个 chat 循环"到"一个终端里的分布式系统"的演化。这个演化过程中,每一步的驱动力都是同一个问题:

"用户会怎么用错这个功能?"

正是因为每次都先问这个问题,才催生了 5 步鉴权链、三层上下文压缩、不可变状态快照、子 Agent 权限降级、差异写入策略------这些在 Demo 阶段看起来"过度设计"的东西,在生产阶段恰好是"保底设计"。

生产级不是"功能更多"。生产级是"每个功能被用错的可能性,都有一条防御路径"。


相关推荐
知识领航员1 小时前
蘑兔AI音乐深度实测:功能拆解、实测表现与适用场景
java·c语言·c++·人工智能·python·算法·github
cskywit1 小时前
【CVPR2024】用Diffusion“造”遥感分割数据:SatSynth论文解读
人工智能·深度学习·计算机视觉
virtaitech1 小时前
算力浪费与算力饥渴并存,OrionX社区版免费开放能否破解这一困局?
大数据·人工智能·gpu算力
火山引擎开发者社区1 小时前
业务团队也能“手搓”应用?火山 Supabase 助力猿辅导对话式 Agent 落地
人工智能
薛定e的猫咪1 小时前
因果推理研究方向综述笔记
人工智能·笔记·深度学习·算法
happyprince1 小时前
03-FlagEmbedding 推理模块深度分析
人工智能
段一凡-华北理工大学1 小时前
高炉炼铁领域炉温监测、预警、调控智能体设计与应用】~系列文章19:项目实战:从0到1搭建系统
人工智能·高炉炼铁·工业智能体·炉温监测·炉温预警
冬奇Lab2 小时前
RAG 系列(十五):CRAG——检索结果不好时自动纠偏
人工智能·llm
冬奇Lab2 小时前
一天一个开源项目(第100篇):Easy-Vibe - Datawhale 出品的 AI 时代编程入门教程
人工智能·开源·资讯