capabilities 层:能力注册协议与五个能力的工具

2026-08-10

看一个目录叫 capabilities/,正常的预期是「这里放着这个产品对外的所有能力」。DeepTutor 不是这样。这个目录里住着五个东西,产品对外的能力清单里有七个,两边只重合两项,且这两份名单分别写在两个不同的文件里。先把这件事说清楚,后面的协议才读得通。

以下行号与数字都对应我们采集的仓库快照:HEAD 456f9c2,采集日 2026-08-10,deeptutor/__version__.py 里的版本号是 1.5.11。你自己 clone 之后行号可能已经漂了,文件名与标识符是稳的,照着搜即可。

一、两个注册表,两种「能力」

第一份名单在 deeptutor/capabilities/registry.py:13-19,常量名 LOOP_CAPABILITIES,共 5 项,顺序是 Mastery → Solve → Obsidian → Subagent → ExploreContext。这五个是回合级的循环能力,挂在聊天的答案循环上。

第二份名单在 deeptutor/runtime/bootstrap/builtin_capabilities.py:3-11,常量名 BUILTIN_CAPABILITY_CLASSES,共 7 项:chat、deep_solve、deep_question、deep_research、math_animator、visualize、mastery_path。这七个是运行时的顶层能力,对应的是「这一轮走哪条主线」。

关键在于两份名单的实现落点并不在同一处:七项里只有 deep_solvemastery_path 的实现类落在 deeptutor/capabilities/ 下,其余各项的实现类都不在这个目录里builtin_capabilities.py:4-10)。

所以「capability」在这个仓库里是一个被复用了两次的词。你在 deeptutor/capabilities/ 里翻不到 research、翻不到 visualize,不是它们缺失,是它们不属于这一层。要按功能找代码,先确认自己找的是哪一份名单里的那个「能力」。

目录规模上,deeptutor/capabilities/ 有 25 个 .py 文件、合计 3838 行;最大的单文件是 mastery/tools.py 721 行,其次是 explore_context/explorer.py 531 行、obsidian/tools.py 361 行。光这三个文件加起来就有 1613 行,接近整个目录的四成——剩下的 22 个文件平分另外六成,单个都不大。

二、LoopCapability 协议:六个成员

协议定义在 deeptutor/capabilities/protocol.py:19-84,它是一个 Protocol,不是基类——满足结构即可,不需要显式继承。

它要求两个字段、四个方法:

成员形态职责
name字段能力名
owned_tools字段本能力注册并在激活时贡献出来的工具名
is_active方法本回合是否参与
system_block方法可选的系统提示词片段
augment_kwargs方法为自己的工具注入服务端私有参数
pre_loop_seed方法追加到初始用户消息种子里的文本

表里「职责」一列不是我们的解读,是转述 protocol.py:19-84 那段 docstring 的原文,你照着行号打开就能逐条比对。

owned_tools 是静态的,这一点在协议里被单独强调:静态才能让设置页在没有回合上下文的情况下把工具按归属分组。配套的 capability_tool_owners()registry.py:38-44)就是干这件事的,它返回一份「工具名 → 所属能力名」的映射。

三、反直觉的第一处:协议是「只加不减」,而唯一的例外反着来

protocol.py:22-32 把普通 loop capability 的约定写得很死:它复用完整的聊天工具面,并在其上追加自己的工具;不裁剪、不压制既有工具面。换句话说,开了 solve 或 mastery 的那一轮,模型看到的内置工具和普通聊天那一轮是一样的,只是多了几个。

按直觉,「进入某个专门模式」通常意味着收窄——只留下这个模式该用的工具,免得模型跑偏。这里的默认恰恰相反:默认是加法

而例外只有一个,写在 protocol.py:87-102KnowledgeCapability。它把 exclusive_tools = True 写在类上,激活时用自己的工具 + ask_user 这个底线替换整个工具面。注意这里的措辞是「类上」——是否独占由类别归属决定,不是每个实例上的一个开关。

这个例外带出一个具体后果,源码里也标了对应 issue:owned_kbs()protocol.py:104-113)用来把本能力独占的知识库从 rag 这一面里排除掉。原因是当一个独占型能力接管了整轮,用户同时选中的其它普通知识库如果不做处理,就会被静默丢弃——rag 面已经不在工具面里了。排除之后,那些不属于本能力的 KB 仍然可以走 rag(源码标注 issue #650)。

排查方向由此确定:如果你发现某一轮里「我明明选了两个知识库,只有一个起作用」,先去看这一轮是不是有独占型能力激活,而不是先怀疑检索本身。

还有一个可选的异步钩子 pre_loopprotocol.py:34-54):在答案循环的第一次 LLM 调用之前 await 一次,返回的 block 折进用户消息种子。它是可选的,没实现的能力不受影响。

四、反直觉的第二处:有一个能力,一个循环工具都不拥有

explore_context 这个能力不拥有任何答案循环工具,也不贡献系统 block,它只通过 pre_loop 钩子工作;read_source 这个工具只存在于它的前置循环里deeptutor/capabilities/explore_context/capability.py:25-28)。

也就是说,按第二节那张表去衡量,它的 owned_tools 是空的、system_block 不产出——一个「什么都不注册」的能力。它做的全部事情发生在答案循环开始之前。

它存在的理由被直接写进了 docstring(explore_context/capability.py:13-23),两条:其一,聊天循环把「理解材料」和「回答用户」混在一起时,模型读到另一个 AI 的 ## Assistant 轮次,会采用对方的第一人称口吻;其二,弱模型经常根本不主动调 read_source

这段 docstring 值得单独拎出来,是因为它把一个架构决策的动机写成了可读的文字,而不是留给读代码的人去猜。至于这个前置循环实际改善了什么,explorer.py 那 531 行我们没有读,也没有运行过这个项目,不作任何效果判断。

五、五个能力各自的工具

能力工具数工具名出处
mastery5mastery_status / mastery_quiz / mastery_grade / mastery_assess / mastery_buildmastery/tools.py:61-67
obsidian9obsidian_search / _read / _list / _backlinks / _links / _tags / _create_note / _append / _set_propertyobsidian/tools.py:26-36
solve3solve_plan / solve_finish_step / solve_replansolve/tools.py:26-30
subagent1consult_subagentsubagent/tools.py:34
explore_context0—(只有前置循环里的 read_sourceexplore_context/capability.py:25-28

这张表最该读的是分布本身:从 9 到 0。obsidian 是典型的「一堆细粒度读写工具」,subagent 只有一个入口,explore_context 是零。工具数量多少不代表哪个能力更重要,它反映的是这个能力和模型之间的交互形状——是让模型自己反复挑动作,还是只给它一个口子。

几个各自的常量,都能在源码里直接核到:

  • solve 的会话常量:DEFAULT_MAX_REPLANS = 2_MAX_STEPS = 12(步骤列表按这个数被截断)、_MAX_SESSIONS = 256solve/session.py:21-22,50,80)。
  • subagent 的 _FINISH_HEADROOM = 2subagent/capability.py:29),是在 consult 预算之外,留给「写最终答案」的轮次余量。这个名字本身说明了一件事:预算不是一路花到零,得给收尾留位置。

关于 consult_subagent 需要如实补一句:它的另一端是一个外部连接的 agent。知识库类型里对应的是 SUBAGENT_KB_TYPE = "subagent"——指向一个连接的 agent,磁盘上没有路径,没有可索引可检索的内容(deeptutor/knowledge/kb_types.py:63)。DeepTutor 会去驱动你本机已装的 agent CLI,这部分实现在 services 一侧,我们另有一篇专门讲;在本文的语境里你只需要记住:这条链的另一端不是仓库内的一次纯函数调用,涉及本机执行环境,是否启用请自行评估。

六、mastery 这条线:服务端握着题目,模型不许改

mastery 是 5 个能力里代码量最大的一个(mastery/tools.py 721 行),也是第一节说的两处重合之一——另一处是 solve:七项顶层能力里,只有 deep_solvemastery_path 的实现类落在 deeptutor/capabilities/ 下(builtin_capabilities.py:4-10)。换句话说,读 mastery 这条线,你会同时踩到两份名单。它的 manifest 写在 mastery/capability.py:58-67name="mastery_path"stages=["responding"]cli_aliases=["mastery"]tools_used 是 5 个 mastery 工具再加 rag / read_source / ask_user

这一层有两处「服务端不放手」的设计:

其一,路径 id 由服务端注入。 _mastery_path_id 是服务端注入的,模型永远不提供;而且每次调用新建 store 与 service,以避免并发共享对象(mastery/tools.py:10-13)。它的取值顺序在 mastery/capability.py:26-54:显式 mastery_path_id → 第一个 book reference → session_id,非 [A-Za-z0-9_-] 的字符统一替换成 _

其二,题目的展示数据由服务端替换。 mastery loop 在 ask_user 调用时,用服务端持久化的挂起问题替换模型自拟的展示数据,防止模型改题号或重排 A/B/C(mastery/loop.py:16-43)。

这两处放在一起看是同一个思路:凡是「后面要拿来比对的东西」,都不经模型往返。至于对应的学习状态怎么落盘、掌握度怎么算、复习怎么排期,都在 deeptutor/learning/ 下,我们另有专文,本文不展开。

系统提示词方面,mastery 只有 en / zh 两个文件,用 wc -l 数出来各 14 行。提示词原文里有两条硬要求(mastery/prompts/en/system.md:2,8):目标背后是一道 “HARD mastery gate”,未清关不得前进;test-out 不是静默跳过,必须过闸门并记录。

这里必须说明边界:以上闸门规则、题型处理与 id 注入顺序,都是这个项目在代码里做出的实现选择,不是经过验证的教学结论。本文只陈述这些规则写在哪一行、怎么运作,不评价它们在学习科学上的有效性,也不承诺任何学习效果。工具层的题型白名单顺带记一下:_QUESTION_TYPES = ("choice", "short", "open"),题库类型映射是 choicechoiceopenwritten、其余→short_answermastery/tools.py:69,93-99)。

七、三处文档与代码对不上的地方

按纪律,只陈述差异、标明两处位置,不推断原因,也不据此评价项目。

  1. deeptutor/capabilities/__init__.py:3-5 的 docstring 写「每个 loop capability 住在自己的子包下(solvemastery)」,括号里给了两个;registry.py:13-19LOOP_CAPABILITIES 是 5 个,子包另有 obsidian / subagent / explore_context。
  2. AGENTS.md:69 写所有能力汇聚到 deeptutor/capabilities/_shared.pyemit_capability_result(),但该文件不存在;实际实现在 deeptutor/agents/_shared/capability_result.py,引用点见 deeptutor/agents/chat/agent_loop.py:35
  3. AGENTS.md:71 写提示词位于 capabilities/prompts/{en,zh}/<name>.yaml,但 deeptutor/capabilities/prompts 这个目录不存在(ls 报 No such file);capabilities 下的提示词是每个能力自带的 <cap>/prompts/{en,zh}/,例如 deeptutor/capabilities/mastery/prompts/en/system.md

以我们实读的仓库状态为准。说完就停。

八、你可以照着核的五步

  1. 打开 deeptutor/capabilities/registry.py:13-19deeptutor/runtime/bootstrap/builtin_capabilities.py:3-11,把 5 项和 7 项两份名单并排放着,确认只有 deep_solvemastery_path 落在 capabilities/ 下。
  2. 打开 deeptutor/capabilities/protocol.py:22-32,确认「复用完整工具面 + 只加不减」这条约定是写在协议 docstring 里的,不是推断出来的。
  3. 打开 protocol.py:87-102104-113,确认 exclusive_tools = True 写在 KnowledgeCapability 类上,以及 owned_kbs() 排除逻辑的注释里引用的 issue 编号。
  4. 打开 deeptutor/capabilities/explore_context/capability.py:13-28,确认这个能力的 owned_tools 为空、只走 pre_loop,并把那段解释动机的 docstring 读完。
  5. deeptutor/capabilities/ 下逐个 grep 五个能力的工具名常量(mastery/tools.py:61-67obsidian/tools.py:26-36solve/tools.py:26-30subagent/tools.py:34),和第五节那张表对一遍。

需要说明的核对边界:mastery/tools.py 721 行我们只读了前 110 行与工具名、参数名的 grep 结果,五个工具各自的返回结构与参数校验细节未逐行核实;explore_context/explorer.py(531 行)以及 obsidian / subagent / solve 三个 tools.py 的实现体我们没有读。本文出现的所有常量都是源码中的默认配置,不是运行结果的保证。


本文依据 DeepTutor 官方仓库(github.com/HKUDS/DeepTutor)的 README、AGENTS.mdpyproject.tomldeeptutor/ 下的源码整理,核对日 2026-08-10,对应仓库快照 456f9c2(版本 1.5.11)。 本文内容为仓库源码与文档口径,我们没有安装、部署或运行过该项目,也没有调用过其中任何一个模型 API, 因此不涉及生成质量、响应速度与教学效果的任何描述。 参数与默认值随版本变动,请以仓库最新代码与 --help 的实际输出为准。 安全相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。