源码解析 · 终身辅导 · v1.5.11 @ 456f9c2 · 第二轮回核

切换目标
不换引擎

DeepTutor 不是又一个 ChatGPT 套皮学习机器人——港大 HKUDS 把它做成 Agent 原生学习工作区:Chat / Quiz / Research / Visualize / Solve / Mastery 共用同一条运行时,知识库、三层记忆、Partners、本地编程 CLI 都挂在 UnifiedContext 上;工具面靠情境旗标 + deferred 两层渐进披露,沙箱把「隔离强度」当返回值来分权。全文按「动机 → 约束 → 被否方案 → 选择 → 代价」五段式展开,标注 文件:行号,可回源码核对。

35k
GitHub stars
151k
后端 Python 行
36
LLM Provider(11 网关)
101
钉版 CLI Apps
Part I

起点:开发者为什么做这个东西

Chapter 01

痛点与洞察:辅导不该是一堆孤立工具

1.1 先回答「为什么」

读 DeepTutor 源码之前,先读它的定位句(assets/README/README_CN.md:60-62):

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

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

  1. 痛点:现有 AI 学习产品把「聊天 / 出题 / 解题 / 做笔记 / 管知识库」拆成互不相通的工具;学习者每换一个模式就丢上下文,个性化无法跨任务累积。
  2. 洞察:真正的终身辅导需要的不是更多入口,而是 同一条 agent 运行时 + 一份可流转的学习上下文
  3. 推论:产品形态必须是「工作区」,不是「单聊窗口」;架构形态必须是「插件化能力」,不是「每个功能写一套流水线」。
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.handleruntime/orchestrator.py:36-114);能力实现 BaseCapability.run(context, stream)core/capability_protocol.py:33-60
切换目标不换引擎 顶层能力表只有 7 个名字,全部注册进同一 CapabilityRegistryruntime/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 的轴是 教育生命周期,不是编码会话,也不是社会预演。


Chapter 02

产品位与动机证据:README / AGENTS / 论文里的原话

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 + 三层插件。


Chapter 03

仓库地图与技术栈:数字形状与依赖方向

3.1 顶层形状

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故意忽略项目根 .envAGENTS.md:28-30

3.3 依赖方向(读代码时的指南针)

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

思维导图:一条约束如何推出十几项设计

Chapter 04

开发者思维导图:从「切换目标不换引擎」到十二项设计

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 + 授权矩阵。


Chapter 05

三层插件宪法:Tools × Capabilities × LoopCapability

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

5.1 动机 → 约束

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

单次动作是 Tool;多阶段接管 turn 的是 Capability。AGENTS.md:5-8core/capability_protocol.py:1-8

5.2 被否方案

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

5.3 选择:三层

Level 1 — Toolscore/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 — CapabilitiesBaseCapability + 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

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_toolsprotocol.py:98-105)。管线用 any_exclusive_capability_active() 判断是否走独占分支,并顺带抑制 rag 脚手架(registry.py:28-37)。

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

这是最容易读错的地方:

  • mastery_path / deep_solveLevel 2 表里——用户在 UI 上「选中一个模式」时走这条路,能力自己 own 整个 turn;
  • MasteryLoopCapability / SolveLoopCapabilityLevel 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)。

Chapter 06

UnifiedContext:一次 turn 的全部真相

6.1 动机

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

6.2 选择:一个 dataclass 流过整条链

UnifiedContextcore/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

实现:从约束到代码

Chapter 07

ChatOrchestrator:三入口共用一张脸

7.1 入口收敛

ChatOrchestrator.handleruntime/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 里必发 DONEclose()、注销 bus
  7. 结束后向全局 EventBus 发 CAPABILITY_COMPLETEorchestrator.py:116-134
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 里迷路。


Chapter 08

双轨主循环:Chat 的「无工具即收工」× 深度能力的 Label 协议

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

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

文件:agents/chat/agent_loop.py(模块注释 1-24AgentLoop @ 171run @ 196)。

规则极简:

  • 有 tool calls → 该轮文本是 narration(工具工作的前言),循环继续;
  • 无 tool calls → 该轮文本就是最终答案,finish
  • 每轮文本都边生成边流给用户,轮末再用 call_rolenarration / finish)告诉前端怎么渲染——不需要中途猜测文本去向agent_loop.py:20-24);
  • ask_user 可暂停并在同一 turn 内恢复;
  • exploration 预算耗尽后进入最多 3 轮 settlement(MAX_SETTLEMENT_ROUNDS = 3 @ 64),仍不停则 _forced_finishagent_loop.py:492关掉 tools逼模型收尾)。总上限是 exploration + 4agent_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_finishagent_loop.py:361-386)。

ChatCapability 只是薄壳(27 行),真正干活的是 AgenticChatPipelinerun @ agentic_pipeline.py:326)+ AgentLoop

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

8.2 轨道 B — run_agentic_loop:Label 驱动的协议状态机

文件:core/agentic/loop.pyrun_agentic_loop @ 173)。

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

设计上最讲究的一点:finalterminal 正交——终止标签可以不流出正文(如 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 消息回喂

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


Chapter 09

工具挂载第一层:ToolMountFlags 让上下文决定工具面

9.1 问题

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

9.2 选择

ToolMountFlags + compose_enabled_toolsagents/_shared/tool_composition.py:82-99101-160)。条件挂载表是单一真源_CONDITIONAL_MOUNT_FLAGStool_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_NAMEStool_composition.py:39),所以「设置页显示什么」和「管线挂什么」不可能漂移。

9.3 与本系列对照

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

DeepTutor 的特殊之处:工具面是 学习情境的函数(有没有 KB / 笔记 / 技能 / 沙箱),不是纯权限函数。


Chapter 10

工具挂载第二层:deferred tools 与 `load_tools` 渐进披露

🆕 本章为第二轮回核补写——上一版完全漏掉了这条,而它恰好是 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)。

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

Chapter 11

Provider 兼容层:36 家能力位 · 优雅降级 · DSML 文本工具回退

🆕 本章为第二轮回核补写

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

services/provider_registry.py:118PROVIDERS 元组共 36ProviderSpec,其中 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_unsupportedstream_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 通道,形如:

<||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 的确切字节。同时 DSMLStreamFilterdsml_tool_calls.py:62)在流式阶段就把标记从用户可见通道里滤掉,而原始文本照旧回喂给模型。

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

11.4 代价

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


Chapter 12

三层 Memory:L1 痕迹 → L2 表面 → L3 综合(含四种综合模式)

12.1 布局

services/memory/paths.py:1-8

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 门面

MemoryStorestore.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-154raise ValueError("preferences.md is not auto-consolidated"));
  2. 但模型能写,只能走一条窄门:write_preferencestore.py:194-250),注释写明「write_memory 工具是唯一调用者trace_id 由 runtime 注入」;
  3. 这条窄门自带两道保护:op 只有 add / editadd幂等的——重复内容不再追加,而是报告已存在的条目(detail="duplicate")。原因写在注释里:Guided Learning 的 turn 高度工具驱动且很长,模型会跨 turn 反复重发同一条 write_memory(issue #647),而 preferences.md 又没有自动综合去收拾重复。

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

12.4 脚注溯源的精确形状

L2/L3 不是自由散文,而是有严格不变量的 Markdown(document.py:1-30):

## <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,第二个会抛 RunBusyErrorconsolidator/runs.py:17,174-175)。

12.6 Partner 记忆的越权缝

Partner 运行时可用 memory_path_service_overridepaths.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 有认知成本;摘要正确性依赖模型

Chapter 13

多引擎 Knowledge:五种索引引擎 + 把 vault / CLI 当 KB

13.1 五种引擎

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

引擎 形态 备注
LlamaIndex 本地向量,默认 唯一用 DeepTutor 活跃 embedding 签名选/读版本化索引的引擎(factory.py:67-68
PageIndex 云端、无向量 模型通过 PageIndex 的 MCP 工具读文档(factory.py:227-228);因索引在云上,故意不可 linklinked_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_preflightpreflight.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 聊天机器人」的关键产品决策。


Chapter 14

确定性骨架 vs 模型判断:Mastery / Solve / Obsidian 的工具分工哲学

🆕 本章为第二轮回核补写。这些 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 沉进工具,教学法留给模型

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


Chapter 15

ExploreContext:把「读懂材料」从「回答问题」里结构性剥离

🆕 本章为第二轮回核补写。这是全仓最"小"却最能说明品味的一个 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 答案循环的任何工具不贡献系统块——近乎隐形。
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 里);前置结论若跑偏会带偏全轮

Chapter 16

Partners:同一大脑上的 IM 伴侣

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_memoryagentic_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)。产品广度换来运维与测试矩阵膨胀。


Chapter 17

Subagent:把编程 CLI 当成可会诊的知识库

17.1 反转宿主关系

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

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

SubagentCapabilitycapabilities/subagent/capability.py:1-13,32-44):

  • 当选中的 KB 是已连接 subagent 时激活(is_activeconnection_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 Partnerlocal_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 的插座哲学同构,但插座插在知识库槽位上,而不是设计流水线的步骤上。


Chapter 18

工具生态四条腿:MCP · 101 个 CLI Apps · 沙箱三档 · Codex OAuth

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

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

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

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.jsoncatalog.py:31),不是运行时去网上抓——「一次上游故障不该把整个目录带下水」(catalog.py:3-9);
  • 基线快照:101 个 app,来自 HKUDS/CLI-Anythingcommit = bc536c9(2026-07-09),聚合两个 registry(registry.json + public_registry.json);
  • 快照里没有 pinned commit 就拒绝提供安装catalog.py:57refusing 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.pyUserExecQuota / 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_codexis_oauth=True,第 11.1 节)。

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


Chapter 19

可重放 turn 协议与多用户授权矩阵

🆕 本章为第二轮回核补写。上一版只写了「统一 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 上」。


Chapter 20

Skills 与 BookEngine:渐进披露的知识与「活书」编译器

20.1 Skills:渐进披露 + 双层影子

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

  1. 渐进披露:技能从不整份塞进系统提示——提示里只有「一技能一行」的 manifest,模型匹配到任务时用 read_skill 取全文;例外是 frontmatter 里 always: true 的技能,其正文eager 注入(用于每轮都要生效的「家规」);
  2. 两层,user 影子 builtindeeptutor/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 / Progressbook/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

品味与边界

Chapter 21

十二项决策五段式复盘

# 决策 动机 被否 选择 主要代价
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 协议表面大

Chapter 22

横向对比(教育 / 通用 / 编程 Agent 桌)

维度 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 分成「所有人可用」与「仅管理员」。

Chapter 23

诚实边界

  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),行号会漂,回核请以符号名为准。

Chapter 24

源码导览索引与本地复现

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 本地复现(只读分析)

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 个频道)

若要真的跑起来(非分析必需):

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 源码分析 与本文第 17 章(宿主关系反转)
  • 关心「渐进披露 / 缓存友好」→ 对照 DeepSeek-Reasonixuse_capability 与本文第 10、20 章
  • 关心「不信模型自述」→ 对照 MiMo Code 的 Goal 裁判与本文第 14、15 章
  • 关心「沙箱与权限」→ 对照 openai-codex 的三平台围栏与本文第 18.3 节
  • 关心「换轴产品」→ 对照 MiroFish:一个换到社会仿真,一个换到终身学习

附录 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

附录 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_memorywrite_preference 幂等窄门可写」(store.py:153-154,194-250
2 纠错 「两层插件」补为三层,列出 LOOP_CAPABILITIES 5 个成员,并说明 mastery/solve 的 L2/L3 双身份
3 纠错 行号校正:README_CN.md:58-6760-62AGENTS.md:28-2927-28(StreamBus)与 28-30(settings);orchestrator.py:36-8636-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 协议 finalterminal;标签解析容错
12 补充 附录 A 指纹表扩充为可逐条复核的数字表 + 24.2 节给出复核命令

附录 D · 分析范围声明

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

附录 A

附录 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
附录 B

附录 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

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

# 类型 内容
1 纠错 preferences.md 从「模型不能改写」修正为「不进自动综合,但 write_memorywrite_preference 幂等窄门可写」(store.py:153-154,194-250
2 纠错 「两层插件」补为三层,列出 LOOP_CAPABILITIES 5 个成员,并说明 mastery/solve 的 L2/L3 双身份
3 纠错 行号校正:README_CN.md:58-6760-62AGENTS.md:28-2927-28(StreamBus)与 28-30(settings);orchestrator.py:36-8636-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 协议 finalterminal;标签解析容错
12 补充 附录 A 指纹表扩充为可逐条复核的数字表 + 24.2 节给出复核命令
附录 D

附录 D · 分析范围声明

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