每一章都是一块砖;架构是知道该把它放在哪里。
15.1 核心问题
前 14 章各自解决了一个子问题:
| 章节 | 解决的子问题 |
|---|---|
| 第 1 章 | 进程如何接收输入、产生输出 |
| 第 2 章 | 终端 UI 如何实时渲染 |
| 第 3 章 | CLI 参数如何路由到功能 |
| 第 4 章 | 工具如何注册与调用 |
| 第 5 章 | 如何流式调用 LLM |
| 第 6 章 | 多轮对话如何管理与压缩 |
| 第 7 章 | Agentic Loop 如何驱动自主执行 |
| 第 8 章 | 危险操作如何获得用户授权 |
| 第 9 章 | 上下文如何动态注入 System Prompt |
| 第 10 章 | 记忆与文件历史如何跨会话持久化 |
| 第 11 章 | 会话如何存档与恢复 |
| 第 12 章 | 斜杠命令如何扩展 Agent 控制面 |
| 第 13 章 | 子 Agent 如何并行执行任务 |
| 第 14 章 | MCP 工具如何按需扩展能力 |
现在的问题是:如何把它们拼接成一个真正可用的 Coding Agent?
拼接不只是 import ——它涉及:
- 启动顺序:哪些东西必须先于其他东西初始化?
- 错误边界:任何层的错误不能让整个 Agent 崩溃
- 信号与关闭:Ctrl-C 时如何有序停止所有子进程
- 模式切换:
--print(一次性)vs 交互式 REPL
15.2 原理讲解
15.2.1 Claude Code 的启动顺序
src/entrypoints/cli.tsx 是 Claude Code 的真正入口,启动顺序严格有序:
进程启动
│
├─ 0. 超早期副作用(模块导入阶段)
│ ├─ startMdmRawRead() ← 并行读取企业 MDM 配置(macOS plutil)
│ └─ startKeychainPrefetch() ← 并行读取钥匙串(macOS keychain)
│
├─ 1. CLI 参数解析(Commander)
│ └─ 命令路由(repl / --print / --resume / mcp / ...)
│
├─ 2. init()(src/entrypoints/init.ts,memoize 保证只执行一次)
│ ├─ enableConfigs() ← 验证并启用配置系统
│ ├─ applySafeConfigEnv() ← 注入安全的环境变量
│ ├─ setupGracefulShutdown() ← 注册 SIGINT/SIGTERM 处理器
│ ├─ preconnectAnthropicApi() ← 提前建立 TCP 连接(降低 TTFT)
│ └─ initializeRemoteSettings() ← 异步加载企业远端配置
│
├─ 3. 信任检查
│ └─ checkHasTrustDialogAccepted() → 否则弹出信任对话框
│
├─ 4. 上下文构建
│ ├─ getSystemContext() ← git 状态、工作目录等(第 9 章)
│ ├─ getTools() ← 内置工具 + MCP 工具注册(第 4/14 章)
│ └─ loadMemoryPrompt() ← CLAUDE.md + MEMORY.md(第 9/10 章)
│
├─ 5. UI 启动(仅交互模式)
│ └─ launchRepl(root, appProps, replProps, renderAndRun)
│ └─ <App><REPL /></App> ← Ink 渲染(第 2 章)
│
└─ 6. 事件循环(Agentic Loop,第 7 章)
15.2.2 两种运行模式
交互模式(REPL):默认模式,启动 Ink UI,等待用户输入
claude # 进入交互 REPL
claude --resume # 恢复上次会话(第 11 章)
claude --model claude-opus-4-5 # 指定模型
一次性模式(--print / -p):执行单条指令后退出,适合脚本集成
claude -p "找出所有 TODO 注释"
claude --print "生成 README" > README.md
两种模式共享相同的 QueryEngine(Agentic Loop),差异只在 UI 层:
- 交互模式:Ink 渲染 → 用户输入 → 下一轮
- 一次性模式:直接输出结果 →
process.exit(0)
15.2.3 错误边界策略
Claude Code 对不同层级的错误有不同处理策略:
API 错误(网络/限流/认证)
└─ withRetry():指数退避重试(第 5 章)
工具执行错误
└─ 捕获后作为 tool_result 返回给 LLM,让 LLM 自行修正
权限拒绝
└─ 转为 PermissionDenial 消息,追加到消息历史(第 8 章)
Fatal 错误(配置损坏/进程崩溃)
└─ gracefulShutdownSync():
├─ 停止所有 MCP Server 子进程
├─ 关闭 LSP Server
├─ flushSessionStorage()(写入会话档案,第 11 章)
└─ process.exit(1)
15.2.4 优雅关闭:gracefulShutdown.ts
src/utils/gracefulShutdown.ts 注册了 SIGINT(Ctrl-C)和 SIGTERM 处理器:
Ctrl-C 信号
│
├─ 第一次:发送 AbortSignal(中断当前工具调用)
│
└─ 第二次:gracefulShutdownSync()
├─ cleanupTerminalModes() ← 恢复终端状态(取消 Kitty 键盘模式等)
├─ runCleanupFunctions() ← 执行注册的清理函数
├─ flushSessionStorage() ← 保存会话档案
└─ 退出
registerCleanup(fn) 是各模块注册清理函数的标准方式,MCP Server 在连接时注册 client.close(),保证进程退出时子进程被正确杀死。
15.2.5 上下文构建:系统提示词的组装
每轮调用前,fetchSystemPromptParts() 动态组装系统提示词:
System Prompt = [
核心角色定义(硬编码)
+ 工具使用规范
+ getUserContext():git 状态、操作系统、工作目录
+ getSystemContext():CLAUDE.md 内容
+ loadMemoryPrompt():MEMORY.md + 内存文件
+ MCP Server 描述(如已连接)
+ 当前会话特定指令(如 --system-prompt 传入的)
]
这些部分每轮都可能变化(git status 会变、MEMORY.md 会被更新),所以每轮重新计算而不是缓存。
15.2.6 工具注册表的组装
getTools() 返回当前 Agent 可用的完整工具列表,由多个来源合并:
// src/tools.ts(简化)
export function getTools(mcpConnections: ConnectedMCPServer[]): Tool[] {
return [
// 内置工具(第 4 章)
BashTool,
ReadFileTool,
WriteFileTool,
GlobTool,
GrepTool,
// ...
// 代理工具(第 13 章)
AgentTool,
// MCP 工具(第 14 章,动态注入)
...mcpConnections.flatMap(conn => conn.tools),
]
}
LLM 看到的 tools[] 数组就是这个列表的 Schema 表示。
15.2.7 完整调用链回顾
以"用户输入一条指令"为例,完整路径如下:
用户输入(键盘 / stdin)
│
▼ 第 2 章(Ink TextInput)/ 第 1 章(readline)
用户消息追加到 messages[]
│
▼ 第 6 章(ConversationManager)
裁剪/压缩消息历史(如超过 Token 上限)
│
▼ 第 9 章(fetchSystemPromptParts)
组装最新 System Prompt(含最新 MEMORY.md)
│
▼ 第 5 章(流式 LLM 客户端)
POST /v1/messages(流式)
│
├─ TextBlock → 第 2 章实时渲染到终端
│
└─ ToolUseBlock
│
▼ 第 8 章(权限仲裁)
检查权限(auto-approve / ask / deny)
│
▼ 第 4 章(Tool.call())
执行工具(bash / file / MCP / Agent...)
│
▼ 第 13 章(若为 AgentTool)
启动子 Agent,递归执行子任务
│
ToolResultBlockParam 追加到 messages[]
│
▼ 回到 Agentic Loop 顶部(第 7 章)
继续下一轮 API 调用...
│
stop_reason = "end_turn"
│
▼ 第 11 章
recordTranscript()(写入会话档案)
│
▼ 第 10 章
(后台)检测新记忆 → 写入 MEMORY.md
15.3 源码索引
| 文件 | 关键函数 | 作用 |
|---|---|---|
src/entrypoints/cli.tsx |
main() |
整个 CLI 的真正入口,参数解析与模式路由 |
src/entrypoints/init.ts |
init()(memoized) |
一次性初始化:配置、环境变量、优雅关闭、遥测 |
src/main.tsx |
模块顶层副作用 | 超早期并行预热:MDM 读取、钥匙串预取 |
src/replLauncher.tsx |
launchRepl() |
启动 Ink App + REPL 界面 |
src/QueryEngine.ts |
QueryEngine(class) |
Agentic Loop 核心:多轮对话 + 工具调度 |
src/query.ts |
query() |
单次 LLM 调用 + 流式解析 + 工具结果处理 |
src/context.ts |
getSystemContext()、getUserContext() |
动态上下文(git 状态、工作目录等) |
src/tools.ts |
getTools() |
工具注册表:内置 + MCP 动态合并 |
src/utils/gracefulShutdown.ts |
setupGracefulShutdown()、gracefulShutdownSync() |
SIGINT/SIGTERM 处理,有序退出 |
src/utils/queryContext.ts |
fetchSystemPromptParts() |
每轮系统提示词动态组装 |
src/utils/sessionStorage.ts |
recordTranscript()、flushSessionStorage() |
会话存档写入(第 11 章) |
15.4 最小化产出物
代码骨架位于
../chapters/15/src/,参考实现位于../chapters/15/solution/。 前置条件:需要ANTHROPIC_API_KEY环境变量。
本章要实现什么
本章是集成章,没有单一的骨架文件需要填写。
任务是将前 14 章的产出物整合为一个完整的 Coding Agent。参考 solution/ 目录中的完整实现,理解各章如何组合在一起:
solution/
├── main.ts ← 完整集成入口(Agentic Loop + 权限 + 流式输出)
├── tools/
│ ├── registry.ts ← 工具注册表(内置 + MCP)
│ ├── bash.ts ← BashTool(第 4/8 章)
│ ├── files.ts ← ReadFileTool / WriteFileTool
│ └── agent.ts ← AgentTool(第 13 章)
├── context/
│ ├── system.ts ← 系统上下文(第 9 章)
│ └── memory.ts ← 记忆加载(第 10 章)
├── session/
│ └── storage.ts ← 会话存档(第 11 章)
├── mcp/
│ └── connect.ts ← MCP 连接(第 14 章)
└── commands/
└── slash.ts ← 斜杠命令(第 12 章)
学习目标:
- 理解各章模块如何通过 import 组合在一起
- 理解启动顺序:工具注册 → MCP 连接 → 会话初始化 → Agentic Loop
- 理解错误边界:工具失败 → tool_result;API 失败 → 退出循环
验收
cd docs/chapters/15
npm install
npm test
验收脚本检查 solution/ 目录的完整性,并验证各模块能正常导入和工作。
15.5 本章小结
集成的本质
集成不是把 14 个文件 import 在一起——它是明确每个部分的边界和依赖顺序:
初始化阶段(顺序严格):
配置 → 工具注册 → MCP 连接 → 会话开启 → UI 启动
每轮调用阶段(并发安全):
历史截断 ‖ 系统提示重建 → LLM 调用 → 工具执行 → 结果追加
退出阶段(顺序严格):
中断信号 → 会话写盘 → 子进程关闭 → 进程退出
Claude Code vs 本章产出物的差异
| 特性 | Claude Code | 本章产出物 |
|---|---|---|
| UI 框架 | Ink(React for CLI) | readline(纯 Node.js) |
| 流式渲染 | 实时 Ink 重渲染 + 打字机效果 | 直接 process.stdout.write |
| 对话压缩 | buildPostCompactMessages()(智能 Token 压缩) |
简单截断早期消息 |
| 权限系统 | 多维度(tool + path + pattern)+ 持久化白名单 | 单维度(工具级)+ 运行时白名单 |
| 记忆提取 | 后台 forked Agent 自动分析对话 | 仅手动调用 appendMemory() |
| 错误处理 | withRetry()(指数退避)+ 多种错误类型 |
简单 try-catch |
| 多 Agent | 真正并行(Promise.all + worktree 隔离) | 串行执行子 Agent |
| 启动性能 | MDM/钥匙串预热 + 懒加载模块 | 无优化 |
15 章架构总览
第 1 章 进程 I/O ─────────────────────────────────────────────────┐
第 2 章 终端 UI (Ink/readline) ───────────────────────────────────┤
第 3 章 CLI 路由 (Commander) ─────────────────────────────────────┤
│
第 4 章 工具引擎 ──────────────────────────────────────────────────┤
第 5 章 流式 LLM 客户端 ───────────────────────────────────────────┤
第 6 章 对话管理(压缩) ──────────────────────────────────────────┤
第 7 章 Agentic Loop ◄─────────────────────────────────────────── ┤ ← 核心驱动器
第 8 章 权限仲裁 ──────────────────────────────────────────────────┤
第 9 章 上下文/System Prompt ──────────────────────────────────────┤
第 10 章 记忆系统 ──────────────────────────────────────────────────┤
第 11 章 会话持久化 ────────────────────────────────────────────────┤
第 12 章 斜杠命令 ──────────────────────────────────────────────────┤
第 13 章 多 Agent 调度 ─────────────────────────────────────────────┤
第 14 章 MCP 插件 ──────────────────────────────────────────────────┤
│
第 15 章 完整 Coding Agent ◄────────────────────────────────────────┘
你从 stdin 到 LLM 再到 工具执行 的完整链路,现在已经贯通。
验收命令
cd chapters/15
npm install
# 一次性模式(--print):统计 TypeScript 文件
npx tsx src/main.ts --print "用 bash 统计当前目录下的 .ts 文件数量"
# 预期:Agent 执行 bash find . -name "*.ts" | wc -l,返回统计结果
# 交互模式:完整 REPL
npx tsx src/main.ts
# 输入:找出项目中所有 TODO 注释(使用 grep 工具)
# 预期:Agent 调用 bash 执行 grep -r "TODO" .,返回匹配结果
# 斜杠命令验收
# 输入:/help → 显示帮助
# 输入:/memory → 显示记忆内容(~/.mini-agent/memory/MEMORY.md)
# 输入:/sessions → 列出历史会话
# 多步任务(整合验收)
# 输入:"读取 README.md 文件,然后用 bash 统计其中单词数量,最后把结果写入 stats.txt"
# 预期:Agent 依次调用 read_file → bash → write_file,自主完成三步任务