第 15 章:完整 Coding Agent 集成

# 第 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,等待用户输入

```bash
claude                    # 进入交互 REPL
claude --resume           # 恢复上次会话(第 11 章)
claude --model claude-opus-4-5  # 指定模型
```

**一次性模式(`--print` / `-p`)**:执行单条指令后退出,适合脚本集成

```bash
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 可用的完整工具列表,由多个来源合并:

```typescript
// 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 失败 → 退出循环

### 验收

```bash
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` 再到 `工具执行` 的完整链路,现在已经贯通。

---

## 验收命令

```bash
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 模式)](16-background-agent)