# DeepTutor 源码分析：一条「终身辅导」如何长成「统一学习运行时」

> **分析对象**：[HKUDS/DeepTutor](https://github.com/HKUDS/DeepTutor)（产品名 **DeepTutor**）  
> **基线**：tag **`v1.5.11`** · commit **`456f9c2`**（`release: v1.5.11`）· 版本单一真源 `deeptutor/__version__.py:9`  
> **许可**：Apache-2.0（`LICENSE`）  
> **代码规模（基线）**：`deeptutor/` **151 534** 行 Python（不含顶层 `tests/`）；`deeptutor_cli/` **5 147** 行；`web/` **108 133** 行 TS/TSX（464 文件）；全仓 **1 057** 个 `.py`  
> **产品一句话**：港大 HKUDS 开源的 **Agent 原生终身个性化辅导工作区**——Chat / Quiz / Research / Visualize / Solve / Mastery 共用同一条 agent 循环，知识库、书籍、记忆、Partners、编程 CLI 子智能体都挂在同一份上下文上。  
> **读者对象**：已读过本系列 Claude Code / MiroFish / OpenWorker / Reasonix 至少一份分析的产品经理与工程师。  
> **叙事方式**：按「**动机 → 约束 → 被否方案 → 选择 → 代价**」五段式展开。先回答“为什么做教育 Agent、脑子里那根主轴是什么”，再进代码；每处关键设计都尽量说清“不这么做会怎样”。  
> **本地基线路径**：`参考项目/DeepTutor @ 456f9c2`  
> **星标快照**：**35 022** star · **4 444** fork（2026-08-12）· 论文 [arXiv:2604.26962](https://arxiv.org/abs/2604.26962)  
> **本版修订（第二轮回核）**：修正 3 处事实错误（`preferences.md` 的写入语义、LoopCapability 数量与双身份、若干行号），补入 8 个此前漏写的承重墙：deferred tools 渐进披露、36 provider 兼容层与 DSML 回退、沙箱三档隔离、MCP 连接模型、101 个 CLI Apps 供应链、Codex OAuth、可重放 turn 协议、多用户授权矩阵。

---

## 目录

**Part I · 起点：开发者为什么做这个东西**  
1. [痛点与洞察：辅导不该是一堆孤立工具](#ch1)  
2. [产品位与动机证据：README / AGENTS / 论文里的原话](#ch2)  
3. [仓库地图与技术栈：数字形状与依赖方向](#ch3)  

**Part II · 思维导图：一条约束如何推出十几项设计**  
4. [开发者思维导图：从「切换目标不换引擎」到十二项设计](#ch4)  
5. [三层插件宪法：Tools × Capabilities × LoopCapability](#ch5)  
6. [UnifiedContext：一次 turn 的全部真相](#ch6)  

**Part III · 实现：从约束到代码**  
7. [ChatOrchestrator：三入口共用一张脸](#ch7)  
8. [双轨主循环：Chat 的「无工具即收工」× 深度能力的 Label 协议](#ch8)  
9. [工具挂载第一层：ToolMountFlags 让上下文决定工具面](#ch9)  
10. [工具挂载第二层：deferred tools 与 `load_tools` 渐进披露](#ch10)  
11. [Provider 兼容层：36 家能力位 · 优雅降级 · DSML 文本工具回退](#ch11)  
12. [三层 Memory：L1 痕迹 → L2 表面 → L3 综合（含四种综合模式）](#ch12)  
13. [多引擎 Knowledge：五种索引引擎 + 把 vault / CLI 当 KB](#ch13)  
14. [确定性骨架 vs 模型判断：Mastery / Solve / Obsidian 的工具分工哲学](#ch14)  
15. [ExploreContext：把「读懂材料」从「回答问题」里结构性剥离](#ch15)  
16. [Partners：同一大脑上的 IM 伴侣](#ch16)  
17. [Subagent：把编程 CLI 当成可会诊的知识库](#ch17)  
18. [工具生态四条腿：MCP · 101 个 CLI Apps · 沙箱三档 · Codex OAuth](#ch18)  
19. [可重放 turn 协议与多用户授权矩阵](#ch19)  
20. [Skills 与 BookEngine：渐进披露的知识与「活书」编译器](#ch20)  

**Part IV · 品味与边界**  
21. [十二项决策五段式复盘](#ch21)  
22. [横向对比（教育 / 通用 / 编程 Agent 桌）](#ch22)  
23. [诚实边界](#ch23)  
24. [源码导览索引与本地复现](#ch24)  

---

# Part I · 起点：开发者为什么做这个东西

<h2 id="ch1">第 1 章 痛点与洞察：辅导不该是一堆孤立工具</h2>

### 1.1 先回答「为什么」

读 DeepTutor 源码之前，先读它的定位句（`assets/README/README_CN.md:60-62`）：

> DeepTutor 是一个智能体原生的学习工作区，将辅导、解题、测验生成、研究、可视化和掌握度练习整合在一个可扩展的系统中。  
> **统一的运行时** — Chat、Quiz、Research、Visualize、Solve 和 Mastery Path 运行在同一个智能体循环上，**切换的是目标，而非引擎**，上下文始终随学习者流转。

这不是“又一个 ChatGPT 套皮”。出发点是：

1. **痛点**：现有 AI 学习产品把「聊天 / 出题 / 解题 / 做笔记 / 管知识库」拆成互不相通的工具；学习者每换一个模式就丢上下文，个性化无法跨任务累积。
2. **洞察**：真正的终身辅导需要的不是更多入口，而是 **同一条 agent 运行时 + 一份可流转的学习上下文**。
3. **推论**：产品形态必须是「工作区」，不是「单聊窗口」；架构形态必须是「插件化能力」，不是「每个功能写一套流水线」。

```mermaid
flowchart LR
    P["痛点：学习工具各自孤立<br/>换模式就丢上下文"] --> I["洞察：辅导 = 同一大脑<br/>换目标不换引擎"]
    I --> C["约束：一份 UnifiedContext<br/>+ 三层插件（Tool / Capability / LoopCapability）"]
    C --> D["推论：多引擎知识 · 三层记忆 · 渐进披露工具<br/>Partners · Subagent · Skills · 沙箱"]
```

### 1.2 三个可验证的工程主张

| 主张 | 在代码里长什么样 |
|---|---|
| **Agent-native** | CLI / WebSocket / SDK 都进 `ChatOrchestrator.handle`（`runtime/orchestrator.py:36-114`）；能力实现 `BaseCapability.run(context, stream)`（`core/capability_protocol.py:33-60`） |
| **切换目标不换引擎** | 顶层能力表只有 7 个名字，全部注册进同一 `CapabilityRegistry`（`runtime/bootstrap/builtin_capabilities.py:3-11`）；另有 5 个 LoopCapability 直接挂在 chat 循环上（`capabilities/registry.py:13-19`） |
| **上下文随学习者流转** | `UnifiedContext` 一次 turn 携带 KB / 附件 / memory / persona / skills / 历史（`core/context.py:34-84`） |

### 1.3 它和本系列前二十家不是同类

本系列绝大多数项目回答「**怎么让 AI 替我写代码 / 干任务**」。DeepTutor 换了个问题：

> **怎么让 AI 长期陪我学**——同一套脑子，跨聊天、做题、研究、可视化、掌握度练习，还记得我是谁。

唯一能对上「换轴」感觉的是 MiroFish（群体仿真）。DeepTutor 的轴是 **教育生命周期**，不是编码会话，也不是社会预演。

---

<h2 id="ch2">第 2 章 产品位与动机证据：README / AGENTS / 论文里的原话</h2>

### 2.1 动机证据表

| 证据 | 出处 | 它证明了什么 |
|---|---|---|
| 「Lifelong Personalized Tutoring」 | README 标题 | 产品轴心是终身个性化，不是单次问答 |
| 「One runtime for every mode … you switch the objective, not the engine」 | `README.md:195` | 统一运行时是卖点第一句 |
| 「a two-layer plugin model — single-shot **Tools** … and multi-stage **Capabilities**」 | `AGENTS.md:5-8` | 架构宪法写进给 Agent 看的说明书 |
| 「All capabilities emit on a shared `StreamBus`」 | `AGENTS.md:27-28` | 流式协议是横切基础设施 |
| 「Runtime settings live in `data/user/settings/*.json` — project-root `.env` files are intentionally ignored」 | `AGENTS.md:28-30` | 配置真源唯一，拒绝 `.env` 漂移 |
| 「Inspectable memory — L1 traces, L2 surface summaries, and L3 synthesis」 | `README.md:200` | 记忆必须可审计、可编辑 |
| 「consult a live coding CLI … from any turn」 | `README.md:197` | 编程 Agent 是被会诊的客体，不是宿主 |
| v1.4.0「every chat capability rebuilt on a single agentic engine」 | README Releases | 历史演进主动收敛到单引擎 |
| v1.4.5「a new loop-plugin framework」 | README Releases | 第三层插件（LoopCapability）是显式产品里程碑 |
| 论文 arXiv:2604.26962 | README News | 有学术设计陈述，不只是工程仓库 |

### 2.2 产品经理视角：它在卖什么体验

| 卖点 | 工程落点 |
|---|---|
| 一个工作区搞定学 / 练 / 研 / 画 | 7 个 Capability + 5 个 LoopCapability + BookEngine 并行 |
| 知识库可换引擎 | RAG factory 五种引擎（`services/rag/factory.py:216-256`） |
| 个性化看得见、能改 | Memory workbench + 整数脚注溯源（`services/memory/document.py:1-30`） |
| 手机微信也能问老师 | Partners 16 个 IM channel 自动发现（`partners/channels/registry.py:18-26`） |
| 需要写代码时请本地 Claude/Codex | Subagent 把 CLI 当「可会诊 KB」（`capabilities/subagent/`） |
| 装社区技能 / 装 101 个 CLI 工具 | Skills（ClawHub/EduHub）+ CLI Apps 快照（`services/cli_apps/vendor/catalog.json`） |
| 用自己的 ChatGPT 订阅 | Codex OAuth 逐账号登录（`services/codex_auth/`） |

> 🧠 **一句话**：别家在优化「工具调用多聪明」；DeepTutor 在优化「**学习生命周期里，上下文到底能不能一直跟着人走**」——并把这件事焊成 orchestrator + UnifiedContext + 三层插件。

---

<h2 id="ch3">第 3 章 仓库地图与技术栈：数字形状与依赖方向</h2>

### 3.1 顶层形状

```text
DeepTutor/
├── deeptutor/           # Python 后端核心（151 534 行）
│   ├── runtime/         # Orchestrator · launcher · registry（含 deferred_tools）· bootstrap
│   ├── core/            # UnifiedContext · StreamBus · agentic loop · tool/capability 协议
│   ├── agents/          # chat / research / question / visualize / math_animator / notebook / vision_solver
│   ├── capabilities/    # solve · mastery · subagent · obsidian · explore_context（LoopCapability 层）
│   ├── services/        # llm · rag · memory · skill · partners · subagent · mcp · sandbox · cli_apps …
│   ├── partners/        # IM bus + 16 channels
│   ├── book/            # Living Book 独立引擎（并行于 Orchestrator）
│   ├── knowledge/       # KB 管理面
│   ├── learning/        # 掌握度引擎（mastery / 间隔重复算术）
│   ├── multi_user/      # 身份 · 授权 · 审计（14 模块）
│   ├── api/             # FastAPI 33 个 router + unified WebSocket
│   └── tools/           # 内置工具实现
├── deeptutor_cli/       # Typer CLI（5 147 行）
├── web/                 # Next.js 16 前端（108 133 行 TS/TSX）
├── AGENTS.md / SKILL.md # Agent-native 说明书（给模型看的宪法）
└── pyproject.toml       # 包名 deeptutor · 版本 1.5.11
```

按包体量排序（Python，约数）：

| 包 | 约行数 | 角色 |
|---|---|---|
| `services/` | 65k | LLM / RAG / Memory / MCP / Skill / Sandbox / Subagent… |
| `agents/` | 17k | 各 Capability 的流水线实现 |
| `api/` | 16k | HTTP / WS 面 |
| `partners/` | 12k | IM 伴侣 |
| `book/` | 7.7k | 活书编译器 |
| `tools/` | 7.6k | 内置工具 |
| `learning/` | 4.7k | 掌握度算术 |
| `capabilities/` | 3.8k | LoopCapability 层 |
| `core/` + `runtime/` | 7.4k | 协议与编排心脏 |
| `multi_user/` | 2.0k | 授权矩阵 |

### 3.2 技术栈一句话

- **后端**：Python 3.11–3.13 · FastAPI · 自研 agentic loop · 36 provider 适配 · 5 种 RAG 引擎  
- **前端**：Next.js 16 · React 19；浏览器只连前端源，`/api/*` 与 `/ws/*` 由 `web/proxy.ts` 服务端转发（容器只需暴露 3782）  
- **入口**：`deeptutor` CLI（PyPI）· Web · Python SDK（`DeepTutorApp`）· Docker/GHCR  
- **配置**：`data/user/settings/*.json`，**故意忽略**项目根 `.env`（`AGENTS.md:28-30`）

### 3.3 依赖方向（读代码时的指南针）

```mermaid
flowchart TB
    CLI["deeptutor_cli"] --> ORCH["ChatOrchestrator"]
    WS["api/unified_ws<br/>可重放 turn 协议"] --> ORCH
    SDK["app.DeepTutorApp"] --> ORCH
    PART["Partners runtime"] --> ORCH
    ORCH --> CAPREG["CapabilityRegistry<br/>7 个顶层能力"]
    ORCH --> TOOLREG["ToolRegistry"]
    CAPREG --> CAP["BaseCapability.run"]
    CAP --> LOOP["AgentLoop（chat）<br/>run_agentic_loop（深度）"]
    LOOP --> LCAP["LOOP_CAPABILITIES<br/>mastery/solve/obsidian/subagent/explore"]
    LOOP --> TOOLS["BaseTool + deferred loader"]
    LOOP --> BUS["StreamBus"]
    TOOLS --> SBX["Sandbox 三档隔离"]
    TOOLS --> MCP["MCP manager"]
    CAP --> CTX["UnifiedContext"]
    CTX --> MEM["MemoryStore L1/L2/L3"]
    CTX --> RAG["RAG pipelines ×5"]
    CTX --> SK["SkillService"]
    BOOK["BookEngine"] -.复用 StreamBus/Registry.-> BUS
    MU["multi_user 授权"] --> CTX
```

BookEngine **刻意平行**于 Orchestrator（`book/__init__.py:5-9`）：复用 StreamBus / Registry，但不注册为 capability——它产出的是一本书，不是一轮流式回答。

---

# Part II · 思维导图：一条约束如何推出十几项设计

<h2 id="ch4">第 4 章 开发者思维导图：从「切换目标不换引擎」到十二项设计</h2>

```mermaid
mindmap
  root((切换目标<br/>不换引擎))
    统一入口
      ChatOrchestrator
      StreamBus 扇出
      可重放 turn 协议
    三层插件
      L1 Tools 单次调用
      L2 Capabilities 接管 turn
      L3 LoopCapability 挂在 chat 循环
    上下文决定工具面
      ToolMountFlags 情境旗标
      deferred tools + load_tools
      KnowledgeCapability 独占表面
    长期个性化
      L1 JSONL 痕迹
      L2 表面摘要
      L3 跨表面综合
      四种综合模式
    知识可插拔
      五种索引引擎
      Obsidian / Subagent 当 KB
    模型不可信之处
      ExploreContext 客观前置
      确定性骨架工具
      Provider 能力位 + 降级
    触达与安全
      Partners 16 IM
      沙箱三档隔离
      多用户授权矩阵
```

后文逐一拆开的十二项设计：Orchestrator 单入口 · 三层插件 · UnifiedContext · Chat「无工具即收工」· Label 协议循环 · ToolMountFlags · deferred tools · Provider 兼容与 DSML 回退 · 三层 Memory · 多引擎知识 · 确定性骨架工具 · 可重放 turn + 授权矩阵。

---

<h2 id="ch5">第 5 章 三层插件宪法：Tools × Capabilities × LoopCapability</h2>

> ⚠️ **本章是第二轮回核修正重点**：`AGENTS.md` 只讲了「两层插件」，但代码里真实存在**第三层**——`LOOP_CAPABILITIES`。而且 mastery / solve **同时**是第二层与第三层公民，这个双身份是理解 v1.4 之后架构的钥匙。

### 5.1 动机 → 约束

如果每个学习模式各自写一套「搜 → 想 → 答」流水线，模式之间无法共享工具、记忆、流式 UI。约束是：

> **单次动作是 Tool；多阶段接管 turn 的是 Capability。**（`AGENTS.md:5-8`，`core/capability_protocol.py:1-8`）

### 5.2 被否方案

| 被否 | 为什么否 |
|---|---|
| 每个模式一个独立 Agent 类 + 独立主循环 | 工具、记忆、流式协议会复制五份 |
| 只有 Tools、没有 Capability | Deep Research / Math Animator 这种多阶段管线塞不进「一次函数调用」 |
| 只有硬编码流水线、模型不能选工具 | 丢掉 agent-native，无法挂 MCP / Skills / CLI apps |

### 5.3 选择：三层

**Level 1 — Tools**（`core/tool_protocol.py:206` `BaseTool`）。内置工具在 `tools/builtin/__init__.py:1562-1607` 一张 `BUILTIN_TOOL_TYPES` 表里注册，再分成两类可见性：

| 类别 | 数量 | 名单 | 出处 |
|---|---|---|---|
| 用户可开关（`/settings/tools`） | **7** | `brainstorm` `web_search` `paper_search` `reason` `geogebra_analysis` `imagegen` `videogen` | `builtin/__init__.py:1627-1635` |
| 上下文自动挂载（用户视角「锁定开启」） | **15** | `rag` `kb_files` `code_execution` `read_source` `read_memory` `write_memory` `read_skill` `list_notebook` `write_note` `web_fetch` `github` `exec` `load_tools` `cron` `ask_user` | `builtin/__init__.py:1647-1663` |
| 能力独有（按能力激活挂载） | 5+3+9+1 | Mastery 5 / Solve 3 / Obsidian 9 / Subagent 1 | 各 `capabilities/*/tools.py` |
| Partner 专属（强制挂载，不可配置） | 3 | `partner_read` `partner_memorize` `partner_search` | `builtin/__init__.py:1601-1607` |

**Level 2 — Capabilities**（`BaseCapability` + `CapabilityManifest`），7 个，注册于 `runtime/bootstrap/builtin_capabilities.py:3-11`：

| 名字 | 阶段（manifest） |
|---|---|
| `chat` | exploring → responding |
| `mastery_path` | responding（Guided Learning） |
| `deep_solve` | planning → reasoning → writing |
| `deep_question` | ideation → generation |
| `deep_research` | rephrasing → decomposing → researching → reporting |
| `visualize` | analyzing → generating → reviewing |
| `math_animator` | concept_analysis → … → render_output |

**Level 3 — LoopCapability**，5 个，注册于 `capabilities/registry.py:13-19`：

```python
LOOP_CAPABILITIES: tuple[LoopCapability, ...] = (
    MasteryLoopCapability(),
    SolveLoopCapability(),
    ObsidianCapability(),
    SubagentCapability(),
    ExploreContextCapability(),
)
```

每轮由 `active_loop_capabilities(context)` 按**稳定注册顺序**筛出激活项（`registry.py:22-25`）。

### 5.4 两种截然不同的插件语义

`capabilities/protocol.py` 把第三层再分成两类，差别在「**加法还是替换**」：

| 类别 | 工具面语义 | 成员 | 出处 |
|---|---|---|---|
| `LoopCapability`（加法） | 复用**完整** chat 工具面，只把 `owned_tools` 加上去；**绝不**削减用户的 composer 开关 | Mastery / Solve / ExploreContext | `protocol.py:19-45` |
| `KnowledgeCapability`（替换） | **独占** turn：只有 `owned_tools` + `ask_user` 兜底，没有 chat 内置、没有用户开关 | Obsidian / Subagent | `protocol.py:87-105` |

独占性由**类别归属**决定，不是每实例的开关：继承 `KnowledgeCapability` 就等于设了 `exclusive_tools`（`protocol.py:98-105`）。管线用 `any_exclusive_capability_active()` 判断是否走独占分支，并顺带抑制 rag 脚手架（`registry.py:28-37`）。

### 5.5 双身份：mastery / solve 为什么两边都在

这是最容易读错的地方：

- `mastery_path` / `deep_solve` 在 **Level 2** 表里——用户在 UI 上「选中一个模式」时走这条路，能力自己 own 整个 turn；
- `MasteryLoopCapability` / `SolveLoopCapability` 在 **Level 3** 表里——当掌握路径 / 解题会话在**普通 chat turn** 上是活跃状态时，它们只是给 chat 循环**追加几个工具和一段系统块**。

换句话说：v1.4 之后「模式」不再是另起炉灶的引擎，而是同一条 chat 循环的**不同装配**。这才是 README 那句「switch the objective, not the engine」的代码含义。

### 5.6 代价

- 新人要同时装下三层插件语义，`AGENTS.md` 只写了两层，第三层得读 `capabilities/registry.py` 才知道；  
- 独占策略需要例外规则：与 LlamaIndex KB 并列选中时 `rag` / `kb_files` 必须共存（issue #650，`tool_composition.py:59-61,132-138`）。

---

<h2 id="ch6">第 6 章 UnifiedContext：一次 turn 的全部真相</h2>

### 6.1 动机

没有统一上下文对象，CLI 参数、WebSocket 帧、Partner 入站消息会各拼各的 prompt，个性化与权限必然漂移。

### 6.2 选择：一个 dataclass 流过整条链

`UnifiedContext`（`core/context.py:34-84`）携带：

| 字段 | 用途 | 值得注意的语义 |
|---|---|---|
| `user_message` / `conversation_history` | 本轮输入与历史 | OpenAI 消息格式 |
| `enabled_tools` | 用户开关 | **`None`（未指定）≠ `[]`（显式全关）**，注释明确写死 |
| `allowed_builtin_tools` | 内置自动挂载白名单 | `None` = 产品 chat 默认不设限；Partner 用它做减法 |
| `active_capability` | 选中的 L2 能力 | 空则回落 `chat` |
| `knowledge_bases` | RAG / Subagent / Obsidian 引用 | 三种异质对象共用一个槽位 |
| `memory_context` / `persona_context` | 注入系统提示 | persona 必须**从第一个 token 就生效**，故 eager 注入 |
| `skills_manifest` / `source_manifest` | 渐进披露的技能与附件清单 | 只放摘要行，全文靠工具拉 |
| `attachments` | 多模态附件 | 含 `extracted_text`（「模型看到的」office 文本） |
| `metadata` | 扩展缝 | `turn_id` / `_subagent_state` / `_min_loop_rounds` 等 |

### 6.3 代价

`metadata` 是有意留的「扩展缝垃圾桶」：协议稳定字段进 dataclass，实验字段进 metadata。灵活，但跨能力约定靠注释维持（例如 subagent 用 `_min_loop_rounds` 抬高循环预算，`capabilities/subagent/capability.py:56-60`）。

---

# Part III · 实现：从约束到代码

<h2 id="ch7">第 7 章 ChatOrchestrator：三入口共用一张脸</h2>

### 7.1 入口收敛

`ChatOrchestrator.handle`（`runtime/orchestrator.py:36-114`）：

1. 补 `session_id`  
2. 解析 `active_capability or "chat"`  
3. 从 `CapabilityRegistry` 取实现；取不到就发一条带 `turn_terminal` 的错误事件并正常收尾（`orchestrator.py:49-67`）  
4. 建 `StreamBus`，按 `turn_id` 注册到全局表（供 WS 重连订阅），后台跑 `capability.run(context, bus)`  
5. 对外 `async for` 扇出 `StreamEvent`  
6. `finally` 里必发 `DONE` 并 `close()`、注销 bus  
7. 结束后向全局 EventBus 发 `CAPABILITY_COMPLETE`（`orchestrator.py:116-134`）

```mermaid
sequenceDiagram
    participant U as CLI/WS/SDK/Partner
    participant O as ChatOrchestrator
    participant R as CapabilityRegistry
    participant C as Capability
    participant B as StreamBus
    U->>O: handle(UnifiedContext)
    O->>R: get(cap_name)
    O->>B: new StreamBus + register_bus(turn_id)
    O->>C: run(context, bus)
    C-->>B: stage / content / tool / sources
    B-->>U: StreamEvent…
    O->>B: DONE + close + unregister
    O->>O: publish CAPABILITY_COMPLETE
```

### 7.2 为什么这很关键

CLI 的 `deeptutor run`、Web 的 unified WS、Partner 入站、SDK 调用**最终都是同一张脸**。换 UI 不换行为——正是 `AGENTS.md:12-26` 那张架构图要钉死的事。

### 7.3 代价

Orchestrator 极薄（146 行），复杂度下沉到各 Capability / Pipeline。好处是入口永远读得懂；坏处是「真正的产品逻辑」散在 `agents/*` 与 `capabilities/*`，读者容易在 1 563 行的 `agentic_pipeline.py` 里迷路。

---

<h2 id="ch8">第 8 章 双轨主循环：Chat 的「无工具即收工」× 深度能力的 Label 协议</h2>

DeepTutor 最有品味的工程现实：**它没有强行统一成一种主循环**。

### 8.1 轨道 A — Chat `AgentLoop`：「这一轮没调用工具 = 答完了」

文件：`agents/chat/agent_loop.py`（模块注释 `1-24`，`AgentLoop` @ `171`，`run` @ `196`）。

规则极简：

- 有 tool calls → 该轮文本是 **narration**（工具工作的前言），循环继续；
- 无 tool calls → 该轮文本**就是**最终答案，`finish`；
- 每轮文本都**边生成边流给用户**，轮末再用 `call_role`（`narration` / `finish`）告诉前端怎么渲染——**不需要中途猜测文本去向**（`agent_loop.py:20-24`）；
- `ask_user` 可暂停并在同一 turn 内恢复；
- exploration 预算耗尽后进入最多 **3** 轮 settlement（`MAX_SETTLEMENT_ROUNDS = 3` @ `64`），仍不停则 `_forced_finish`（`agent_loop.py:492`：**关掉 tools**逼模型收尾）。总上限是 `exploration + 4`（`agent_loop.py:60-63` 注释算得很清楚）。

两个真实兜底细节值得抄走：

1. **截断续写**：provider 因长度上限停下（`finish_reason ∈ {length, max_tokens, max_output_tokens}`，`agent_loop.py:65,73-76`）时不当成「答完」，而是把可见前缀留在协议里继续续写；
2. **空 finish 轻推**：finish 轮文本为空时先发一次 nudge 让模型重说，只推一次（`nudged_empty_finish`，`agent_loop.py:361-386`）。

`ChatCapability` 只是薄壳（27 行），真正干活的是 `AgenticChatPipeline`（`run` @ `agentic_pipeline.py:326`）+ `AgentLoop`。

> 与 Claude Code / Codex「模型决定停不停」同构，但 DeepTutor 把「停」定义为 **工具调用空集**，而不是一个特殊的 `terminate` 工具——对辅导场景更自然：讲清楚了就该停。

### 8.2 轨道 B — `run_agentic_loop`：Label 驱动的协议状态机

文件：`core/agentic/loop.py`（`run_agentic_loop` @ `173`）。

模型每轮必须在第一行给出双反引号包裹的标签（`core/agentic/labels.py:3-10`）。`LabelProtocol`（`loop.py:39-64`）声明五个集合：`allowed` / `terminal` / `intermediate` / `final` / `tool_label`。

设计上最讲究的一点：**`final` 与 `terminal` 正交**——终止标签可以不流出正文（如 `REPLAN` 只把文本冒泡给上层），中间标签也可以流出正文（如 chat 的 `PAUSE`：向用户叙述而不结束 turn）（`loop.py:48-56`）。

循环本体 capability-agnostic，能力相关行为全部委托给 `LoopHost` Protocol（`loop.py:79-170`）：上下文窗口裁剪、逐轮 trace 元数据、并行工具分发、pause 处理、终止校验、协议违规修复文案、预算耗尽的强制收尾。其中三个钩子用 `getattr` 探测存在性（`before_iteration` / `on_intermediate`），**老 host 不写也不会坏**——这是很克制的扩展方式。

标签解析器本身也是防脆弱的（`labels.py:34-95`）：容忍一/三反引号变体、零宽字符前缀、缺失分隔符、以及"裸标签 + 分隔符"回退；超过 64 字符还没匹配就落 `UNKNOWN`。

Research / Question / Solve 走这条轨道——它们需要**显式阶段标签**（`THINK` / `TOOL` / `FINISH` / `APPEND` / `REPLAN`），不能只靠「有没有工具」判断阶段。

### 8.3 动机 → 选择 → 代价

| | Chat 轨道 | Label 轨道 |
|---|---|---|
| **动机** | 对话要低延迟、所见即所得 | 多阶段管线要可审计、可纠错 |
| **选择** | 无工具 = finish | 首行标签状态机 |
| **代价** | 两套循环心智 | 弱模型常写坏标签，需要 repair 消息回喂 |

仓库没有为了「架构好看」硬合并——辅导主路径要顺，深度研究路径要严。

---

<h2 id="ch9">第 9 章 工具挂载第一层：ToolMountFlags 让上下文决定工具面</h2>

### 9.1 问题

把全部工具 schema 每轮塞给模型会：费 token、诱导模型乱调、并且让 Partner 场景无法做权限减法。

### 9.2 选择

`ToolMountFlags` + `compose_enabled_tools`（`agents/_shared/tool_composition.py:82-99` 与 `101-160`）。条件挂载表是**单一真源**（`_CONDITIONAL_MOUNT_FLAGS`，`tool_composition.py:41-57`）：

| Flag | 挂载 |
|---|---|
| `has_kb` | `rag`, `kb_files` |
| `has_sources` | `read_source` |
| `has_memory` | `read_memory` |
| `has_notebooks` | `list_notebook`, `write_note` |
| `has_skills` | `read_skill` |
| `has_deferred_tools` | `load_tools` |
| `has_exec` / `has_code` | `exec`, `code_execution` |

组装顺序（`tool_composition.py:117-127`）：① 用户开关（过 `optional_whitelist` 过滤）→ ② 条件自动挂载 → ③ 激活能力的 owned tools → ④ 常在工具（`write_memory` / `web_fetch` / `github` / `ask_user` / `cron`）。

再叠三个 Partner 用的旋钮（`tool_composition.py:140-155`）：`builtin_whitelist`（**只减不增**）、`forced`（无条件追加，绕过白名单与情境门）、`suppressed`（最终移除）。`AUTO_MOUNTED_TOOLS` 直接派生自 `CONFIGURABLE_BUILTIN_TOOL_NAMES`（`tool_composition.py:39`），所以「设置页显示什么」和「管线挂什么」不可能漂移。

### 9.3 与本系列对照

| 项目 | 工具可见性策略 |
|---|---|
| Claude Code | 权限 + 前缀缓存友好排序 |
| Hermes | 74 工具极端插件化 |
| Reasonix | `use_capability` 单一代理隔离动态 MCP，保前缀稳定 |
| Open Design | 适配器即数据，按需加载 CLI |
| **DeepTutor** | **情境旗标自动挂载 + 用户开关 + 能力独占（再叠 deferred 第二层）** |

DeepTutor 的特殊之处：工具面是 **学习情境的函数**（有没有 KB / 笔记 / 技能 / 沙箱），不是纯权限函数。

---

<h2 id="ch10">第 10 章 工具挂载第二层：deferred tools 与 `load_tools` 渐进披露</h2>

> 🆕 **本章为第二轮回核补写**——上一版完全漏掉了这条，而它恰好是 DeepTutor 与 Reasonix `use_capability`、Claude Code「工具表稳定」最直接对话的设计。

### 10.1 动机与实测理由

即使有情境旗标，MCP 服务器 + 101 个 CLI Apps 也会让 schema 面爆炸。`runtime/registry/deferred_tools.py:1-16` 把动机写得很直白：

> 保持常在 schema 面很小，**这在弱模型上可测量地改善了工具选择**，同时让每个已连接工具都只差一次便宜的调用。

### 10.2 机制

1. 打了 `BaseTool.deferred` 的工具（**默认包含所有 MCP 工具**）不进本轮初始工具表；
2. 系统提示只带**一行摘要**（`render_deferred_tools_manifest`）；
3. 模型需要时用内置 `load_tools` 报出精确名字；
4. `DeferredToolLoader` 把完整 schema **追加进活的 `tool_schemas` 列表**——`run_agentic_loop` 每轮重读该列表，所以工具**立刻可调**；
5. 已加载的名字**按 chat session 持久化**，后续 turn 一开始就带上。

CLI App 工具做了特别处理：每个 App 自成一个 provider 且只有一个工具，逐 provider 写小标题会浪费 token，于是它们共享一个 `("cli","")` 分组、把 provider id 写在行内（`deferred_tools.py:36-40`）。

```mermaid
flowchart LR
    A["MCP / CLI App 工具<br/>deferred=True"] --> B["系统提示：一行摘要"]
    B --> C{模型判断需要?}
    C -- 否 --> D["schema 面保持小<br/>弱模型选得更准"]
    C -- 是 --> E["load_tools(精确名字)"]
    E --> F["schema 追加进活列表"]
    F --> G["下一轮即可调用"]
    G --> H["名字按 session 持久化"]
```

### 10.3 五段式

| 段 | 内容 |
|---|---|
| 动机 | 生态越大，常在 schema 面越毒 |
| 约束 | 工具必须「一次便宜调用」内可达，不能真的不可用 |
| 被否 | 全量塞（弱模型选错）／干脆不支持 MCP（丢生态） |
| 选择 | 一行 manifest + `load_tools` + 活 schema 列表 + session 记忆 |
| 代价 | 多一跳往返；模型得先「知道自己需要什么」；manifest 描述要限长（`MANIFEST_DESCRIPTION_MAX_CHARS`） |

---

<h2 id="ch11">第 11 章 Provider 兼容层：36 家能力位 · 优雅降级 · DSML 文本工具回退</h2>

> 🆕 **本章为第二轮回核补写**。

### 11.1 36 个 provider 是一张「能力位」表

`services/provider_registry.py:118` 的 `PROVIDERS` 元组共 **36** 条 `ProviderSpec`，其中 **11 条是网关**（gateway 优先，顺序即匹配优先级，`provider_registry.py:7,115-118`）：

| 类别 | 成员 |
|---|---|
| 网关 / 兼容层（11） | `custom` `custom_anthropic` `azure_openai` `openrouter` `edenai` `aihubmix` `siliconflow` `novita` `atlascloud` `volcengine` `byteplus`（含两个 coding-plan 变体） |
| 第一方 | `anthropic` `openai` `openai_codex` `github_copilot` `deepseek` `gemini` `zhipu` `dashscope` `moonshot` `minimax`(+`minimax_anthropic`) `mistral` `stepfun` `xiaomi_mimo` `groq` `qianfan` |
| 本地 | `vllm` `ollama` `lm_studio` `llama_cpp` `lemonade` `ovms` `nvidia_nim` |

关键在 `ProviderSpec` 的字段（`provider_registry.py:19-56`）——它不是「名字 + URL」，而是**逐家能力位**：`supports_prompt_caching` / `supports_stream_options` / `supports_max_completion_tokens` / `strip_model_prefix` / `is_oauth` / `is_local` / `thinking_style` / `reasoning_model_patterns` / `model_overrides`。加一家 provider = 加一条 spec，「环境变量、配置匹配、状态显示全部由此派生」（`provider_registry.py:3-5`）。

还有一张别名表 `PROVIDER_ALIASES`（`:77`）把 `azure` / `google` / `claude` / `openai-compatible` / `github-copilot` / `lm-studio` / `atlas` … 归一到规范名——用户填什么都能落地。

### 11.2 优雅降级：错误分类器驱动重试

`services/llm/request_compat.py:1` 一句话点题：「**provider-error classifiers used by retry and graceful-degradation paths**」。它把 provider 的报文正文小写后做子串匹配，判断三类不支持：

- `is_stream_options_unsupported`（`stream_options` / `unknown parameter` / `extra inputs are not permitted` …）  
- `is_tool_schema_unsupported`（→ 触发 DSML 回退，见下节）  
- `is_image_input_unsupported`（→ 多模态降级，`llm/multimodal.py:327` 注释说明降级要**跨迭代持久**，否则图片会被反复重发）

这类「先乐观发，被拒就精确降级并记住」的做法，比预先枚举每家 provider 的怪癖更耐老化。

### 11.3 DSML：当 provider 不肯做原生工具调用

`agents/chat/dsml_tool_calls.py:1-22` 是我在本系列里见过最具体的「兜底」案例：

某些 DeepSeek 部署（尤其本地/源码起的 OpenAI 兼容端点）**不宣告 function calling**，于是把工具调用当成标记写进 **content 通道**，形如：

```text
<｜｜DSML｜｜tool_calls>
  <｜｜DSML｜｜invoke name="exec">
    <｜｜DSML｜｜parameter name="command" string="true">python -c "..."</｜｜DSML｜｜parameter>
  </｜｜DSML｜｜invoke>
</｜｜DSML｜｜tool_calls>
```

**不解析会怎样**：这段标记会作为最终答案流给用户，而工具**根本没跑**（仓库直接挂了 issue #666）。

解法很克制：`extract_dsml_tool_calls` 是**纯函数**（无 I/O、无 LLM），标签前缀宽松匹配（`<[^>]*?invoke\s+name="`），只依赖稳定的 `invoke name` / `parameter name` 结构——**不赌那几个全角特殊 token 的确切字节**。同时 `DSMLStreamFilter`（`dsml_tool_calls.py:62`）在流式阶段就把标记从用户可见通道里滤掉，而原始文本照旧回喂给模型。

`InlineThinkFilter`（`agent_loop.py:83-95`）是它的姊妹设计，解决同一类问题的另一半：有些 provider 把推理写在 content 里用 `<think>` 包着，于是在**流式时**就地切分——「用户可见内容」在下游各处（实时气泡、持久化消息、循环的 finish 判定）**一处修好、处处干净**，而带标签的原文照旧回喂给模型。

### 11.4 代价

字符串匹配 provider 报文天生脆弱（改文案就漏判）；能力位表要人工维护；DSML 只覆盖了 DeepSeek 家的方言，别家若发明新方言还得再加分支。

---

<h2 id="ch12">第 12 章 三层 Memory：L1 痕迹 → L2 表面 → L3 综合（含四种综合模式）</h2>

### 12.1 布局

`services/memory/paths.py:1-8`：

```text
trace/<surface>/<YYYY-MM-DD>.jsonl        # L1 追加写
L2/<surface>.md                           # L2 每表面摘要
L3/{recent,profile,scope,preferences}.md  # L3 跨表面
backup/<timestamp>/...                    # v1 迁移归档
```

七个表面（`paths.py:47-56`）：`chat` `notebook` `quiz` `kb` `book` `partner` `cowriter`。

### 12.2 门面

`MemoryStore`（`store.py:68`）是**无状态**门面（进程级单例安全），按路径加写锁：

- `emit` → L1  
- `update_l2` / `update_l3` → consolidator  
- `read_l3_concat` → 供 `read_memory` 工具（无内容时返回明确提示串而非空，`store.py:53-55`）  
- `overwrite_doc` / `delete_entry` → 工作台里人直接改  
- `apply_ops_payload` → 「预览 → 应用」两步流（`store.py:165-193`）

### 12.3 ⚠️ 修正：`preferences.md` 到底谁能写

**上一版写错了**：说「偏好必须人可编辑、模型不能偷偷改写」。核对源码后的准确说法是：

1. `preferences.md` **不进自动综合**——`update_l3` 显式拒绝（`store.py:153-154`：`raise ValueError("preferences.md is not auto-consolidated")`）；
2. 但模型**能写**，只能走一条窄门：`write_preference`（`store.py:194-250`），注释写明「**`write_memory` 工具是唯一调用者**，`trace_id` 由 runtime 注入」；
3. 这条窄门自带两道保护：`op` 只有 `add` / `edit`；`add` 是**幂等**的——重复内容不再追加，而是报告已存在的条目（`detail="duplicate"`）。原因写在注释里：Guided Learning 的 turn 高度工具驱动且很长，模型会跨 turn 反复重发同一条 `write_memory`（issue #647），而 `preferences.md` 又没有自动综合去收拾重复。

所以真正的设计意图不是「不让模型写」，而是「**让模型只能通过一条带溯源与幂等保护的窄门写**」，且这类结论永不被自动摘要改写。

### 12.4 脚注溯源的精确形状

L2/L3 不是自由散文，而是有严格不变量的 Markdown（`document.py:1-30`）：

```markdown
## <section>
- <text> [^1][^2] <!--m_xxx-->

---

[^1]: notebook:abc
[^2]: chat:def
```

三个细节容易看漏：

- 脚注标签是**整数**，按全文 bullet 流的首次出现顺序分配；两条引用同一来源就**共享**标签，所以渲染视图里没有重复脚注行；
- 条目 id 藏在 bullet 尾部的 HTML 注释 `<!--m_xxx-->` 里，round-trip 存活，供 audit / dedup 行视图与 `DELETE /entry/{id}` 使用；
- 解析器**同时**接受旧格式（`[^m_xxx]: ref1, ref2`），下次保存时迁移——存量文档不会因升级读不了。

### 12.5 四种用户可见的综合模式

上一版只说「LLM consolidator」。实际有四种（`consolidator/modes/__init__.py:1-10`）：

| 模式 | 干什么 | 是否用 LLM |
|---|---|---|
| `update` | 分块增量抽取新事实 | 是 |
| `audit` | 对照原始证据做行级编辑（先渲染成带行号、去脚注的视图） | 是 |
| `dedup` | 全文行级迭代去重 | 是 |
| `merge` | 合并重复脚注引用 | **否**（纯确定性） |

并发上限也有明确契约：同一 `(layer, key)` 最多一个**活跃** run，第二个会抛 `RunBusyError`（`consolidator/runs.py:17,174-175`）。

### 12.6 Partner 记忆的越权缝

Partner 运行时可用 `memory_path_service_override`（`paths.py:27-44`）让 `read_memory` / `write_memory` 读到**主人**的记忆，而**其它服务**（rag / skills / notebooks）仍留在 partner 作用域。不这么做，IM 伴侣就永远「不认识你」。这是多租户 × 人格陪伴交叉时的细活。

### 12.7 五段式

| 段 | 内容 |
|---|---|
| 动机 | 个性化必须可检查，不能只藏在向量库里 |
| 约束 | 原始事件不可丢；摘要可编辑；每条结论可追溯 |
| 被否 | 单一 `PROFILE.md`（v1 已弃，见 `store.py:31` 的 `_V1_FILES`）／纯向量记忆 |
| 选择 | L1 JSONL + L2/L3 整数脚注 Markdown + 四模式 consolidator + 窄门偏好写入 |
| 代价 | 综合要花模型调用；三层 UI 有认知成本；摘要正确性依赖模型 |

---

<h2 id="ch13">第 13 章 多引擎 Knowledge：五种索引引擎 + 把 vault / CLI 当 KB</h2>

### 13.1 五种引擎

`services/rag/factory.py:216-256` 的引擎清单：

| 引擎 | 形态 | 备注 |
|---|---|---|
| **LlamaIndex** | 本地向量，默认 | 唯一用 DeepTutor 活跃 embedding 签名选/读**版本化索引**的引擎（`factory.py:67-68`） |
| **PageIndex** | 云端、**无向量** | 模型通过 PageIndex 的 **MCP 工具**读文档（`factory.py:227-228`）；因索引在云上，**故意不可 link**（`linked_kb.py:15,133-134`） |
| **GraphRAG** | 图 + 向量 | 输出 parquet 表，探针检查核心表存在（`index_probe.py:200`） |
| **LightRAG** | 图 + 向量，多模态 | HKUDS 自家 |
| **LightRAG Server** | 外置 HTTP | 无本地索引，naive/local/global/hybrid/mix 五种查询模式 |

配套三件套：统一 `RAGPipeline` Protocol（`pipelines/base.py:17-37`）、逐引擎 `engine_preflight`（`preflight.py:195`，**永不抛异常**）、`embedding_signature`（`:43-47` 明说图引擎错配会**静默失败**，所以要单独打戳）。

### 13.2 「知识库」是个产品语义槽位，不是数据结构

选中的 KB 可以是三种异质对象：

| 类型 | 行为 |
|---|---|
| 普通 RAG KB | 挂 `rag` / `kb_files` |
| Obsidian vault | `KnowledgeCapability` 独占 9 个 vault 工具 |
| 已连接的 Subagent | 独占 `consult_subagent`（第 17 章） |

把「资料」和「可咨询的专家进程」放进**同一个槽位**，是 DeepTutor 区别于「RAG 聊天机器人」的关键产品决策。

---

<h2 id="ch14">第 14 章 确定性骨架 vs 模型判断：Mastery / Solve / Obsidian 的工具分工哲学</h2>

> 🆕 **本章为第二轮回核补写**。这些 `tools.py` 的模块注释里藏着 DeepTutor 最清晰的一句工程哲学，上一版一句话带过太可惜。

### 14.1 同一句话，说了三遍

| 文件 | 原话 | 工具数 |
|---|---|---|
| `capabilities/mastery/tools.py:1-9` | 「**chat agent loop IS the tutor**；这些工具让它读门禁、记结果，而教学法——教什么、怎么问、何时讲解——**仍是模型的活**；算术（掌握度、门禁、间隔重复）留在引擎里」 | 5 |
| `capabilities/solve/tools.py:1-8` | 「**chat agent loop IS the solver**；三个工具给它一根确定性脊梁——一份它承诺的计划、逐步的 done 门禁、有界的 replan——而**怎么解**留给模型在循环里用共享内置工具做」 | 3 |
| `capabilities/obsidian/tools.py:1-8` | 九个工具全是纯 `vault` 操作的**薄包装**：六读（导航链接 / 搜索 / 读笔记 / 列标签）三写（create / append / set property） | 9 |

### 14.2 为什么这是一条值得抄的分界线

多数 Agent 项目在「让模型自由发挥」与「写死流水线」之间摇摆。DeepTutor 给了一条可操作的切法：

- **确定性的部分下沉成工具**：掌握度分数怎么算、间隔重复什么时候到期、计划的第几步算 done、replan 还剩几次——这些是**算术与状态机**，模型算不准也不该算；
- **判断的部分留给循环**：讲什么、怎么问、先解哪一步、要不要查资料——这些是**教学法与推理**。

于是 `learning/`（4.7k 行掌握度引擎）是纯算术模块，`mastery/tools.py` 只是它与 chat 循环之间的**缝**。

### 14.3 与本系列对照

| 项目 | 同类做法 |
|---|---|
| MiMo Code | Goal 独立裁判 + 四道死循环闸门（不信模型自述完成） |
| Reasonix | Delivery Profile 证据签收 + readiness 门禁 |
| grok-build | Goal Mode 五件套 |
| **DeepTutor** | **掌握度门禁 / 计划 done 门禁 / 有界 replan 沉进工具，教学法留给模型** |

共性是同一句话：**凡是能被算准的，就别让模型自由心证。**

---

<h2 id="ch15">第 15 章 ExploreContext：把「读懂材料」从「回答问题」里结构性剥离</h2>

> 🆕 **本章为第二轮回核补写**。这是全仓最"小"却最能说明品味的一个 LoopCapability。

### 15.1 两个非常具体的失败观察

`capabilities/explore_context/capability.py:1-30` 把动机写成了两条 bug 级观察：

1. **人格串味**：chat 循环把「理解附件」和「回答用户」熔在一个循环里。当附件是**用户和另一个 AI 的对话记录**时，模型在同一上下文里读到那些 `## Assistant` 轮次，于是**用那个 agent 的第一人称口吻**说话。把理解拆成一次客观（第三人称）前置调查，**从结构上**消掉这种混淆。
2. **弱模型根本不读**：原生 tool calling 下，弱模型经常**从不主动调用 `read_source`**。于是让专门的前置 pass 拥有读取权，**并把该工具从答案循环里彻底移除**——调查被迫发生在前面，而不是被跳过。

### 15.2 机制

- 激活条件：本轮带任何可读（非图片）附件源——文档、笔记条目、书章节、题库条目，或**被引用的会话历史**（动机案例）；
- 用可选的 `pre_loop` 钩子（`capabilities/protocol.py:34-53`），在**答案循环第一次 LLM 调用之前**跑；
- 它自己是个小 agentic 循环（`explore_context/explorer.py:149` 起），用 `read_source` 读该读的部分；
- 产出的客观调查**折进循环的 user-message 种子**（与 KB 种子并列，`agent_loop.py:205-217`）；
- 它**不 own 答案循环的任何工具**、**不贡献系统块**——近乎隐形。

```mermaid
flowchart LR
    A["turn 带可读附件<br/>（尤其：别的 AI 的对话记录）"] --> B["ExploreContext pre_loop<br/>只读 · 客观 · 第三人称"]
    B --> C["调查结论折进 user-message 种子"]
    C --> D["答案循环：不再有 read_source<br/>也不会串味成另一个 agent"]
```

### 15.3 五段式

| 段 | 内容 |
|---|---|
| 动机 | 同一循环里「读材料」污染「答问题」的人格与执行 |
| 约束 | 理解必须客观、第三人称，且**一定**发生 |
| 被否 | 靠提示词叮嘱模型「请先读附件、不要模仿对话里的助手」 |
| 选择 | 独立只读前置 pass + 从答案循环移除 `read_source` |
| 代价 | 多一次 LLM 往返（成本折进 turn 的 `usage` 里）；前置结论若跑偏会带偏全轮 |

---

<h2 id="ch16">第 16 章 Partners：同一大脑上的 IM 伴侣</h2>

### 16.1 是什么

Partners（前身 TutorBot，v1.4.3 更名并上「生产级 IM pipeline」）= 带 Soul 人格、挂 IM 频道、跑在**同一条 chat 循环**上的持久伴侣。

### 16.2 频道发现：零导入扫描 + 可诊断失败

`partners/channels/registry.py`：

- `discover_channel_names()` 用 `pkgutil` 扫描包，**零导入**列出内置频道名（`:18-26`），排除 `base` / `manager` / `registry`；
- `load_channel_class()` 导入模块并取第一个 `BaseChannel` 子类（`:29-38`）；
- 外部插件走 `entry_points(group="deeptutor.partners.channels")`，**内置名优先，插件不能遮蔽内置**（`:55-61`）；
- `discover_all_with_errors()` 额外返回 `errors` 字典——**「为什么 UI 里没有 X」可诊断**，而不是静默丢频道（`:63-78`）。

基线里内置 **16** 个频道模块：`telegram` `discord` `slack` `feishu` `wecom` `weixin` `dingtalk` `qq` `napcat` `mochat` `whatsapp` `matrix` `mattermost` `msteams` `zulip` `email`。

### 16.3 与主产品的关系

Partner turn 仍然构造 `UnifiedContext` 走 Orchestrator，但三处收紧：

- `allowed_builtin_tools` 做减法（主人可禁某些内置）；
- 强制挂 `PARTNER_BUILTIN_TOOL_NAMES` 三件套并**抑制** chat 的 `read_memory` / `write_memory`（`agentic_pipeline.py:64,624-625`）——这三件是**强制的、不可主人配置的**（`builtin/__init__.py:1601-1607`）；
- 记忆路径可 override 到主人作用域（第 12.6 节）。

### 16.4 代价

可选依赖多（`pip install deeptutor[partners]`，Matrix E2EE 还要 libolm）；每个频道的 Markdown / 流式 / 媒体语义都要单独磨（Telegram 得自己做 Markdown→HTML，`telegram.py:110`）。产品广度换来运维与测试矩阵膨胀。

---

<h2 id="ch17">第 17 章 Subagent：把编程 CLI 当成可会诊的知识库</h2>

### 17.1 反转宿主关系

本系列里 Open Design「把别人的 CLI 当引擎」；DeepTutor 换了个插法：

> **学习 Agent 是宿主；Claude Code / Codex / Gemini / Kimi / opencode / MiMo 是被会诊的子智能体，且入口是「知识库槽位」。**

`SubagentCapability`（`capabilities/subagent/capability.py:1-13,32-44`）：

- 当选中的 KB 是已连接 subagent 时激活（`is_active` → `connection_for_turn(context) is not None`）；
- 作为 `KnowledgeCapability` **独占** turn：只有 `consult_subagent` + `ask_user` 兜底；
- 连接信息（哪个后端、工作目录）、逐后端配置、turn 内预算与 session **全部服务端注入**，「**模型从不提供它们**」——防越权乱指工作目录；
- 用 `_min_loop_rounds = budget + 2` 抬高循环预算，保证最后一次会诊之后还有轮次写答案（`capability.py:25-29,56-60`）。

### 17.2 后端注册表与跨 turn 续接

`services/subagent/registry.py:24-35` 七个后端：`ClaudeCode` `Codex` `Gemini` `Kimi` `Opencode` `Mimo` `Partner`。`local_cli` 标志把「本机探测」和「从自己列表连接」分开——`detect_all()` 只探测 CLI，且**逐个失败不影响整体**（异常降级成 `available=False` + detail，`registry.py:51-77`）。

跨 turn 用 session registry 续上**同一个**本地 agent 会话（`capability.py:88-95`：首次会诊时从跨 turn 注册表 seed session id），所以侧边栏与聊天里的会诊共享上下文。

`consult_subagent` 每次调用把一个问题交给本地 CLI，并**把它的原生事件流经 dispatcher 的 `event_sink` 转出去**，所以侧边栏能看到那个 agent 真实的中间步骤（`capabilities/subagent/tools.py:1-8`）。多模态图片只在用户为该后端显式开了 `forward_images` 时才转发（`capability.py:96-100`）。

### 17.3 为什么这配得上「绝活」

编程 Agent 系列在教「怎么造引擎」；DeepTutor 示范「**教育产品如何把已有引擎当工具**」——与 Open Design 的插座哲学同构，但插座插在**知识库槽位**上，而不是设计流水线的步骤上。

---

<h2 id="ch18">第 18 章 工具生态四条腿：MCP · 101 个 CLI Apps · 沙箱三档 · Codex OAuth</h2>

> 🆕 **本章为第二轮回核补写**——上一版把这四件事压缩成了一张表的两行，实际上它们各自都有值得单独讲的设计。

### 18.1 MCP：每服务器一个专用连接任务

`services/mcp/manager.py:1-25` 讲了一个很多人踩过的坑：DeepTutor 的 chat 是**同一个 event loop 里的 per-turn task**，而 MCP session 必须在**同一个 task 内**打开和关闭（SDK 的 anyio cancel scope 是任务绑定的）。于是每个服务器拿到一个**专用连接任务**，端到端持有自己的 `AsyncExitStack`：

```text
connect → 在该 task 内进入 transport/session → 发布 adapter →
等待 shutdown 事件 → 在同一 task 内退出 stack
```

`ensure_started()` 是**懒的**（第一个 turn 付连接成本，有逐服务器超时上限），之后很便宜；`reload()` 对持久化配置做 diff。模块里还有 `oauth.py` / `secrets.py`（凭据移出沙箱可及范围，v1.5.7）/ `catalog/` / `session_state.py` / `pageindex_server.py`——**PageIndex 引擎本身就是通过 MCP 暴露给模型的**。

MCP 工具默认 `deferred`（第 10 章），所以接一堆服务器不会把 schema 面撑爆。

### 18.2 CLI Apps：101 个，钉在一个已审 commit 上

`services/cli_apps/`：

- 目录是**随包发布的快照** `vendor/catalog.json`（`catalog.py:31`），不是运行时去网上抓——「一次上游故障不该把整个目录带下水」（`catalog.py:3-9`）；
- 基线快照：**101 个 app**，来自 `HKUDS/CLI-Anything`，`commit = bc536c9`（2026-07-09），聚合两个 registry（`registry.json` + `public_registry.json`）；
- 快照里**没有 pinned commit 就拒绝提供安装**（`catalog.py:57`：`refusing to offer installs`）——供应链纪律写进代码；
- 每个 app 自成一个 provider 且只有一个工具，故在 deferred manifest 里共享 `cli` 分组（第 10.2 节）。

### 18.3 沙箱：三个后端、两级隔离、一条策略门

`services/sandbox/backends.py:1-16` 三个后端，各自**如实上报**自己真正提供的隔离级别：

| 后端 | 隔离级别 | 场景 |
|---|---|---|
| `RunnerSidecarBackend` | **SYSTEM** | 提交给独立 runner 容器（HTTP）——Docker 部署的正解：主应用保持最小权限，**永不执行不可信 shell** |
| `BwrapBackend` | **SYSTEM** | Linux 裸机 `bwrap` mount namespace |
| `RestrictedSubprocessBackend` | **APPLICATION** | 清过 env、cwd 受限的普通子进程；本地开发（如 macOS）的**降级**回退，**仅管理员可 opt-in，因为它不做 OS 隔离** |

`services/sandbox/service.py:1-13` 把这三档变成产品策略：

- `exec_capability_available()` → 决定 skill 的 `requires.sandbox` 门禁；
- `isolation_level()` → **策略门：SYSTEM 对所有人开放，APPLICATION 仅管理员**；
- `run()` → 执行，受**逐用户配额**约束（`quota.py` 的 `UserExecQuota` / `QuotaExceeded`）。

与本系列对照：Codex 是 Seatbelt/Landlock/Windows Token 三平台 OS 围栏，nanobot 用 bwrap，DeepTutor 的特色是**把「隔离强度」变成一个可查询的返回值，并用它做权限分级**——而不是假装所有部署一样安全。

### 18.4 Codex OAuth：用你自己的 ChatGPT 订阅

`services/codex_auth/`（`service.py:1`「Codex OAuth orchestration and managed model-catalog integration」）实现 v1.5.5 起的能力：用 ChatGPT 计划登录 Codex，v1.5.10 起**每个账号登入自己的 Codex**，v1.5.6 起支持 SSH 隧道后的远端登录。对应 provider 侧是 `openai_codex`（`is_oauth=True`，第 11.1 节）。

产品含义：学习者不必再买一份 API 额度——这类"接用户已有订阅"的设计在教育场景尤其重要。

---

<h2 id="ch19">第 19 章 可重放 turn 协议与多用户授权矩阵</h2>

> 🆕 **本章为第二轮回核补写**。上一版只写了「统一 WS」四个字，实际它是一份完整的 turn 生命周期协议。

### 19.1 一个端点，十一种消息

`api/routers/unified_ws.py:1-30`：单一 `/api/v1/ws`，用于「**turn-based execution and replayable streaming**」。客户端消息类型：

| 类型 | 作用 |
|---|---|
| `message` / `start_turn` | 起一个新 turn |
| `subscribe_turn` | 订阅已有 turn 的事件，**带 `after_seq`**（重放） |
| `subscribe_session` | 订阅某 session 当前活跃 turn |
| `resume_from` | **断线重连后接回在飞的 turn** |
| `unsubscribe` | 退订 |
| `cancel_turn` | 取消 |
| `submit_user_reply` | 把用户回答交给 `ask_user` 暂停的 turn，**在同一 turn 内恢复循环** |
| `user_input` | 把学习者答案投给 StreamBus（解 `wait_for_input`） |
| `regenerate` | 重跑最后一条用户消息为**全新 turn**，替换尾部助手消息，复用 session 存的能力/工具/偏好；可带 `overrides`（capability / tools / KB / language / config / notebook & history 引用）；错误码 `regenerate_busy` / `nothing_to_regenerate` |
| `check_active_turn` | 报告是否有活 turn；**并把陈旧的「running」持久化行标记为 cancelled** |

三件事撑起「重启安全」：事件带序号可 `after_seq` 重放、bus 按 `turn_id` 注册可再订阅（第 7.1 节）、陈旧状态自愈。README v1.4.0 说的 "restart-safe turn runtime" 就落在这里。

### 19.2 pause / resume 是一等公民

`ask_user` 不是「结束这轮，等下一轮」，而是**同 turn 暂停**：循环停在原地，`submit_user_reply` 回来后带着一条 `ask_user_resolved` 指令继续（`agentic_pipeline.py:873-911`），并把答案摊平成一条 tool 消息接回对话。对辅导产品这是必需品——「老师问你一句，你答了，课继续」不该丢上下文。

### 19.3 多用户：14 个模块的授权矩阵

`multi_user/` 不只是「可选多用户」，而是一张逐资源的授权矩阵：

| 模块 | 管什么 |
|---|---|
| `identity.py` / `context.py` / `paths.py` | 身份、请求上下文、逐用户工作区路径 |
| `grants.py` | 授权文档（v2 结构：`models.llm` / `knowledge_bases` / `skills` / partners…） |
| `model_access.py` / `personal_models.py` | 能用哪些模型 / 自带模型 |
| `knowledge_access.py` / `skill_access.py` / `tool_access.py` / `partner_access.py` | 逐类资源的可见与可用 |
| `audit.py` | 审计 |
| `router.py` | 管理面 API |

其中 partner 的授权语义写得很清楚：Partner 保持**管理员管理**（CRUD 路由 admin-gated），grant 只让用户**看见并咨询**指定 partner（`grants.py:22-28`）。另外 v1.4.10 起非管理员的 MCP 工具**默认拒绝**。

### 19.4 代价

协议表面大（11 种消息 + 33 个 REST router），前后端要一起维护语义；多用户是可选路径，个人工作区心智与多租户心智并存，读代码时要时刻问「这段跑在谁的 PathService 上」。

---

<h2 id="ch20">第 20 章 Skills 与 BookEngine：渐进披露的知识与「活书」编译器</h2>

### 20.1 Skills：渐进披露 + 双层影子

`services/skill/service.py:8-20` 说清了两件事：

1. **渐进披露**：技能**从不整份塞进系统提示**——提示里只有「一技能一行」的 manifest，模型匹配到任务时用 `read_skill` 取全文；例外是 frontmatter 里 `always: true` 的技能，其正文**eager 注入**（用于每轮都要生效的「家规」）；
2. **两层，user 影子 builtin**：`deeptutor/skills/builtin/` 随产品发布、运行时只读；`data/user/workspace/skills/` 由 API 创作。同名时用户层遮蔽内置层。

技能包结构是 `SKILL.md` + 可选 `references/`（等执行沙箱就绪还会有 `scripts/`）；`read_skill` 有硬上限防止巨大参考文件灌满上下文（`service.py:74`），也有路径逃逸校验（`service.py:182`）。社区技能可从 ClawHub 安装（v1.4.4，带安全门）。

> 对照：Claude Code 的 skills、Open Design 的 20 层提示词分带、Reasonix 的稳定前缀——三家都在做同一件事：**把"可能有用"的知识挪出常在上下文**。DeepTutor 在这条线上做了两次（skills 与 deferred tools）。

### 20.2 BookEngine：并行的第二个运行时

`book/__init__.py:5-9`：独立运行时，把聊天历史 / 笔记 / 知识库 / 意图**编译**成结构化、分块、可交互的「活书」；**平行于** `ChatOrchestrator`，复用 `ToolRegistry` / `CapabilityRegistry` / `StreamBus`。

阶段：Ideation → SourceExplorer → Spine（`spine_synthesizer` 已替代旧 `SpineAgent`）→ PagePlanner → `BookCompiler`。模型层是 Pydantic 的 `Book` / `Spine` / `Chapter` / `Page` / `Block` / `Progress`（`book/models.py`），编译器**刻意与 orchestrator 隔离**（`compiler.py:16`）。

为什么不做成一个 Capability？因为它的产出物是**一本可反复阅读、带进度的资产**，不是一轮流式回答——生命周期、存储（`book/storage.py`）、失败恢复都不一样。

### 20.3 其它承重墙（一句话档）

| 模块 | 作用 |
|---|---|
| `co_writer/` | 多文档协作写作 |
| `agents/math_animator/` | Manim 视频管线（可选 extra，需 LaTeX/ffmpeg） |
| `agents/notebook/` · `services/notebook/` | 笔记本与题库回流 |
| `services/persona/` | Soul / persona 模板 |
| `services/cron/` | 定时任务（`cron` 工具） |
| `services/imagegen` · `videogen` · `voice` | 图 / 视频 / 语音生成 |
| `services/parsing/` | 可插拔文档解析（MinerU / PyMuPDF4LLM / Docling…） |
| `agents/chat/context_budget.py:1-12` | 把**真实发出的那次请求**的窗口占用回传 UI；「重新推导会让读数与实际发送漂移」，且该读数**永不抛异常** |
| `core/trace.py` · `logging/` | 逐轮 trace 卡片与统计 |
| `i18n/` + `prompts/{en,zh}/*.yaml` | 状态文案与提示词双语外置 |

---

# Part IV · 品味与边界

<h2 id="ch21">第 21 章 十二项决策五段式复盘</h2>

| # | 决策 | 动机 | 被否 | 选择 | 主要代价 |
|---|---|---|---|---|---|
| 1 | 统一入口 | 三端行为一致 | 各端各写循环 | `ChatOrchestrator`（146 行） | 逻辑下沉到各 pipeline |
| 2 | 三层插件 | 单次动作 / 接管 turn / 装配循环 | 只两层或全流水线 | Tool + Capability + LoopCapability | 三套语义，文档只写了两层 |
| 3 | UnifiedContext | 防上下文漂移 | 每端私有 dict | 单一 dataclass + metadata 缝 | metadata 易膨胀 |
| 4 | Chat 无工具即停 | 辅导对话自然收尾 | 强制 `terminate` 工具 | `AgentLoop` + 3 轮 settlement | 与 Label 循环并存 |
| 5 | Label 深度循环 | 多阶段可审计 | 全用 Chat 循环 | `run_agentic_loop` + `LoopHost` | 弱模型要 repair |
| 6 | 情境挂载工具 | 降噪省 token | 全量 schema | `ToolMountFlags` 单一真源 | 旗标要维护 |
| 7 | deferred tools | 生态大但 schema 要小 | 全塞 / 不支持 MCP | manifest + `load_tools` + 活列表 | 多一跳往返 |
| 8 | Provider 能力位 + 降级 | 36 家怪癖各异 | 只支持 OpenAI 兼容 | spec 能力位 + 错误分类器 + DSML 回退 | 靠报文子串匹配，脆 |
| 9 | 三层记忆 | 可审计个性化 | 单文件 / 纯向量 | L1/L2/L3 + 整数脚注 + 四模式 | 综合成本、依赖模型质量 |
| 10 | 多引擎知识 | 不同资料不同索引 | 绑死一家 | factory + Protocol + preflight | 预检与签名复杂 |
| 11 | 确定性骨架工具 | 不让模型自由心证算术 | 全交给提示词 | 门禁/计划/replan 沉进工具 | 工具面变大 |
| 12 | 可重放 turn + 授权矩阵 | 断线不丢课、多租户可控 | 无状态请求 | `after_seq` 重放 + resume + grants v2 | 协议表面大 |

---

<h2 id="ch22">第 22 章 横向对比（教育 / 通用 / 编程 Agent 桌）</h2>

| 维度 | DeepTutor | MiroFish | OpenWorker | Claude Code / Codex | Open Design | Reasonix |
|---|---|---|---|---|---|---|
| 产品轴 | 终身学习工作区 | 群体事件预演 | AI 同事 | 本地编程 Agent | 设计宿主 | 省钱的终端 Agent |
| 主循环 | **双轨**（Chat + Label） | 仿真 tick | 收件箱异步 | 工具循环 | 不写主循环 | 单循环 + 三阶压缩 |
| 上下文 | UnifiedContext 跨模式 | 世界状态 | 会话 + 收件箱 | 会话 + AGENTS.md | 剧本 + 适配器 | 稳定前缀 + turn tail |
| 工具可见性 | 情境旗标 + deferred 两层 | — | 风险标签 | 权限 + 缓存排序 | 适配器即数据 | `use_capability` 代理 |
| 记忆 | L1/L2/L3 + 脚注可审计 | Agent 记忆 | SQLite | 文件 / 压缩 | 较少 | 压缩三阶梯 |
| 沙箱 | **隔离级别可查询 + 按级分权** | — | 权限档 | OS 三平台围栏 | — | 应用层 + OS |
| 外部引擎 | Subagent CLI 会诊（KB 槽位） | OASIS + Zep | 连接器 | 自身即引擎 | 25 个 CLI 适配 | MCP |
| 触达 | Web + CLI + 16 IM | Web | Slack 等 | TUI / IDE | Web | TUI/HTTP/桌面/ACP |
| 许可 | Apache-2.0 | AGPL-3.0 | 视底座 | 各异 | 视仓库 | MIT |

**一句话定位**：DeepTutor 是本系列里第一个把「Agent 工程」完整砸进 **教育产品骨架** 的样本——它证明统一运行时、可审计记忆、渐进披露、隔离分级这些手法并不只服务于写代码。

**三个别家都没有的组合**：

1. 「知识库」这个槽位可以是资料、可以是 vault、**也可以是一个活着的编程 Agent**；
2. 把「读懂材料」做成**独立客观前置 pass**，只因为观察到模型会串味成附件里的另一个 AI；
3. 沙箱把**隔离强度当返回值**，并据此把 `exec` 分成「所有人可用」与「仅管理员」。

---

<h2 id="ch23">第 23 章 诚实边界</h2>

1. **体量巨大**：15 万行后端 + 10.8 万行前端。本分析抓宪法与承重墙，不是逐行注释；`agentic_pipeline.py`（1 563 行）内部仍有大量未展开的分支。
2. **双轨循环增加认知负担**：读 chat 与读 research 是两套心智模型；三层插件语义又叠一层。
3. **Provider 兼容靠字符串匹配**：`request_compat` 判断"不支持"依赖报文子串，上游改文案就会漏判。
4. **RAG 多引擎 = 多真实失败模式**：embedding 错配（图引擎会**静默**失败）、云端 PageIndex、外置 LightRAG Server；preflight 很努力但运维仍重。
5. **个性化依赖 consolidator 质量**：L2/L3 由 LLM 写，脚注保证可追溯，不保证摘要正确。
6. **沙箱最弱一档不是 OS 隔离**：`RestrictedSubprocessBackend` 只是清 env + 限 cwd，仓库自己标注为管理员 opt-in 的降级路径——本地 macOS 开发场景要清楚这一点。
7. **Subagent 依赖本机 CLI**：没装 Claude/Codex 就没有会诊能力；容器部署更麻烦。
8. **教育效果评估不在仓库中心**：工程强度很高，但"是否真的学得更好"要靠论文与外部实验，不能由 star 数背书。
9. **多用户是可选路径**：默认个人工作区心智，上多租户需另读 `multi_user/` 全套。
10. **本文行号锚定 `456f9c2`**：上游高频发版（README 显示约每 2–3 天一个 release），行号会漂，回核请以符号名为准。

---

<h2 id="ch24">第 24 章 源码导览索引与本地复现</h2>

### 24.1 🔍 源码指路表

| 你想理解… | 先读 |
|---|---|
| 总架构宪法（给模型看的） | `AGENTS.md` · 根 `SKILL.md` |
| 统一入口 | `deeptutor/runtime/orchestrator.py` |
| 一次 turn 的数据 | `deeptutor/core/context.py` |
| Capability 协议（L2） | `deeptutor/core/capability_protocol.py` · `runtime/bootstrap/builtin_capabilities.py` |
| LoopCapability 协议（L3） | `deeptutor/capabilities/protocol.py` · `capabilities/registry.py` |
| Chat 主循环 | `deeptutor/agents/chat/agent_loop.py` |
| Chat 管线组装 | `deeptutor/agents/chat/agentic_pipeline.py` |
| Label 通用循环 | `deeptutor/core/agentic/loop.py` · `labels.py` · `labeled_step.py` |
| 工具挂载策略 | `deeptutor/agents/_shared/tool_composition.py` |
| 工具渐进披露 | `deeptutor/runtime/registry/deferred_tools.py` |
| 内置工具清单 | `deeptutor/tools/builtin/__init__.py:1562-1663` |
| Provider 能力位 | `deeptutor/services/provider_registry.py` |
| 兼容降级 / 文本工具回退 | `deeptutor/services/llm/request_compat.py` · `agents/chat/dsml_tool_calls.py` |
| 三层记忆 | `deeptutor/services/memory/{paths,store,document}.py` · `consolidator/modes/` |
| RAG 工厂与预检 | `deeptutor/services/rag/{factory,preflight,embedding_signature}.py` |
| 确定性骨架工具 | `deeptutor/capabilities/{mastery,solve,obsidian}/tools.py` |
| 客观前置调查 | `deeptutor/capabilities/explore_context/` |
| Subagent | `deeptutor/capabilities/subagent/` · `services/subagent/registry.py` |
| Partners 频道 | `deeptutor/partners/channels/registry.py` |
| MCP 连接模型 | `deeptutor/services/mcp/manager.py` |
| CLI Apps 快照 | `deeptutor/services/cli_apps/catalog.py` · `vendor/catalog.json` |
| 沙箱三档 | `deeptutor/services/sandbox/{backends,service,quota}.py` |
| Codex OAuth | `deeptutor/services/codex_auth/service.py` |
| 可重放 turn 协议 | `deeptutor/api/routers/unified_ws.py` |
| 多用户授权 | `deeptutor/multi_user/{grants,tool_access,knowledge_access}.py` |
| Skills | `deeptutor/services/skill/service.py` |
| 活书 | `deeptutor/book/{__init__,engine,compiler,models}.py` |
| 窗口占用读数 | `deeptutor/agents/chat/context_budget.py` |
| CLI | `deeptutor_cli/main.py` |

### 24.2 本地复现（只读分析）

```bash
cd "参考项目/DeepTutor"
git rev-parse --short HEAD   # 期望 456f9c2
git describe --tags          # 期望 v1.5.11

# 复核本文的几个关键数字
find deeptutor -name '*.py' -print0 | xargs -0 wc -l | tail -1        # 151534
python3 -c "import re,json;d=json.load(open('deeptutor/services/cli_apps/vendor/catalog.json'));print(len(d['apps']), d['meta']['commit'])"
python3 -c "import re;s=open('deeptutor/services/provider_registry.py').read();b=s[s.index('PROVIDERS: tuple'):];print(len(re.findall(r'ProviderSpec\(\s*name=\"',b)))"   # 36
ls deeptutor/partners/channels/*.py | wc -l                            # 20（含 base/manager/registry/__init__ → 16 个频道）
```

若要真的跑起来（非分析必需）：

```bash
python3 -m venv .venv && source .venv/bin/activate
pip install -e .
( cd web && npm ci --legacy-peer-deps )
deeptutor init && deeptutor start --dev
```

### 24.3 和本系列其它文档的衔接

- 关心「不写主循环、把别人 CLI 当引擎」→ 对照 [Open Design 源码分析](./open-design-源码分析.md) 与本文第 17 章（宿主关系反转）  
- 关心「渐进披露 / 缓存友好」→ 对照 [DeepSeek-Reasonix](./DeepSeek-Reasonix-源码分析.md) 的 `use_capability` 与本文第 10、20 章  
- 关心「不信模型自述」→ 对照 [MiMo Code](./MiMo-Code-源码分析.md) 的 Goal 裁判与本文第 14、15 章  
- 关心「沙箱与权限」→ 对照 [openai-codex](./openai-codex-源码分析.md) 的三平台围栏与本文第 18.3 节  
- 关心「换轴产品」→ 对照 [MiroFish](./MiroFish-源码分析.md)：一个换到社会仿真，一个换到终身学习  

---

## 附录 A · 版本基线指纹

| 项 | 值 |
|---|---|
| Repo | https://github.com/HKUDS/DeepTutor |
| Tag / Commit | `v1.5.11` / `456f9c2` |
| 版本真源 | `deeptutor/__version__.py:9` |
| License | Apache-2.0 |
| Stars / Forks（分析日） | 35 022 / 4 444（2026-08-12） |
| 后端 / CLI / 前端行数 | 151 534 / 5 147 / 108 133 |
| 顶层 Capability | 7 |
| LoopCapability | 5 |
| 用户可开关工具 / 情境自动挂载工具 | 7 / 15 |
| LLM Provider（其中网关） | 36（11） |
| RAG 引擎 | 5 |
| IM 频道 | 16 |
| Subagent 后端 | 7（6 个本地 CLI + partner） |
| CLI Apps 快照 | 101（`CLI-Anything` @ `bc536c9`，2026-07-09） |
| 沙箱后端 / 隔离档 | 3 / 2（SYSTEM、APPLICATION） |
| WS 客户端消息类型 | 11 |
| REST router | 33 |
| Docs / Paper | https://deeptutor.info/ · [arXiv:2604.26962](https://arxiv.org/abs/2604.26962) |

## 附录 B · 三个最独特设计（给总目录卡片用）

1. **切换目标不换引擎**：7 个顶层 Capability + 5 个 LoopCapability（其中 mastery/solve 双身份）共用 Orchestrator / StreamBus / UnifiedContext；工具面由情境旗标 + deferred 两层渐进披露决定。
2. **可审计三层记忆**：L1 JSONL 痕迹 → L2 表面摘要 → L3 综合，整数脚注 + `<!--m_xxx-->` 锚点逐条溯源，四种综合模式（含一种不用 LLM），偏好只能走幂等窄门写入。
3. **知识槽位可以变成专家**：RAG / Obsidian vault / 本地编程 CLI 共享「选中的知识库」这一产品语义；再配上「隔离强度可查询并据此分权」的沙箱与 101 个钉版 CLI Apps。

## 附录 C · 第二轮回核修订清单

| # | 类型 | 内容 |
|---|---|---|
| 1 | **纠错** | `preferences.md` 从「模型不能改写」修正为「不进自动综合，但 `write_memory` 经 `write_preference` 幂等窄门可写」（`store.py:153-154,194-250`） |
| 2 | **纠错** | 「两层插件」补为**三层**，列出 `LOOP_CAPABILITIES` 5 个成员，并说明 mastery/solve 的 L2/L3 双身份 |
| 3 | **纠错** | 行号校正：`README_CN.md:58-67`→`60-62`；`AGENTS.md:28-29`→`27-28`（StreamBus）与 `28-30`（settings）；`orchestrator.py:36-86`→`36-114` |
| 4 | 补写 | 第 10 章 deferred tools / `load_tools` 渐进披露 |
| 5 | 补写 | 第 11 章 36 provider 能力位、优雅降级、DSML 文本工具回退（issue #666） |
| 6 | 补写 | 第 14 章 Mastery/Solve/Obsidian 的「确定性骨架 vs 模型判断」分工哲学 |
| 7 | 补写 | 第 15 章 ExploreContext 的人格串味与「弱模型不读附件」两条动机 |
| 8 | 补写 | 第 18 章 MCP 连接任务模型 / 101 个 CLI Apps 供应链钉版 / 沙箱三档两级 / Codex OAuth |
| 9 | 补写 | 第 19 章 可重放 turn 协议（11 种消息、`after_seq`、resume、regenerate）与多用户授权矩阵 |
| 10 | 补充 | 精确计数：7/15 工具、36(11) provider、5 引擎、16 频道、7 后端、101 apps、33 router、四种综合模式 |
| 11 | 补充 | Chat 循环两处兜底（截断续写、空 finish 轻推）；Label 协议 `final`⊥`terminal`；标签解析容错 |
| 12 | 补充 | 附录 A 指纹表扩充为可逐条复核的数字表 + 24.2 节给出复核命令 |

## 附录 D · 分析范围声明

本分析基于 tag `v1.5.11` 浅克隆的静态阅读与关键路径逐条核对（含第二轮回核），**不覆盖**：全部测试用例、每个 IM channel 的协议实现细节、前端每个 page 的交互实现、`book/` 编译器与 `learning/` 掌握度算术的内部算法推导、生产多租户部署运维手册。行号相对该基线；上游若 rebase，请以符号名为准。
