第 15 章:完整 Coding Agent 集成

每一章都是一块砖;架构是知道该把它放在哪里。


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 ◄────────────────────────────────────────┘

你从 stdinLLM 再到 工具执行 的完整链路,现在已经贯通。


验收命令

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,自主完成三步任务

下一章

第 16 章:后台智能框架(forked Agent 模式)