知识库的类型模型与 `manifest` 清单:一个拒绝回答「索引了几篇」的模块

2026-08-10

多数带知识库的项目,「知识库」只有一种:把文件丢进去,切块、嵌入、建索引,然后检索。DeepTutor 不是这样——它的知识库条目上有一个 type 字段,这个字段决定的不是”用哪个引擎”,而是”这个条目到底归不归 DeepTutor 管”。搞不清这一层,你会在排查时问出一堆没有答案的问题:为什么这个库没有 raw/ 目录、为什么重建索引对它没反应、为什么文件清单里它显示”无法枚举”。

以下所有行号与常量都对应我们采集的仓库快照:HEAD 456f9c2,采集日 2026-08-10,deeptutor/__version__.py 里的版本号是 1.5.11。行号会随版本漂,文件名和常量名是稳的,照着搜即可。

一、类型模型:默认那一种是不写 type

先说最容易踩空的一点:默认类型在磁盘上没有标记deeptutor/knowledge/kb_types.py:3-7 的模块 docstring 写明,多数 KB 是默认的 indexed 类型,条目里不带 type 字段。也就是说,你去 kb_config.json 里翻,看到一个条目没有 type,那不是数据缺失,那就是标准的本地索引型知识库。

type 的是另外五种,源码统称 connected(连接型 / 指针型):

常量type关键字段内容在哪
OBSIDIAN_KB_TYPEobsidianvault_path用户自己的 Markdown 目录,不建索引
LINKED_KB_TYPElinkedexternal_path用户在别处已建好的引擎索引,跳过索引步骤
SUBAGENT_KB_TYPEsubagent—(磁盘上没有路径)一个连接的 agent,无可索引可检索内容
LIGHTRAG_SERVER_KB_TYPElightrag_serverserver_url + 可选 api_key检索转发到该服务器的 /query 端点
IMA_KB_TYPEimaclient_id + api_key + knowledge_base_id检索转发到腾讯 IMA 的 search_knowledge OpenAPI

行号分别是 kb_types.py:53:58:63:69:75

这五种里有两件事必须如实说清楚,不能当成”另一种数据源”轻描淡写过去:

  • lightrag_serverima 这两种,检索请求会离开你的机器,发往一个外部服务;ima 那种连凭据(client_id / api_key)都要写进 KB 条目里。
  • subagent 型指向的是一个连接的 agent。deeptutor/services/subagent/__init__.py:1-7 写明,这个子系统把用户本机已装的 agent CLI 当作子代理来用。换句话说,挂一个 subagent 型 KB,等于把一条检索路径接到本机上会真正被拉起来的外部程序上。这条链路具体怎么驱动,我们另有一篇专门讲;这里只强调它不是”读一个文件夹”那么轻。

要不要用这几种,取决于你自己对数据外发与本机执行的接受度,本文不给建议。

二、CONNECTED_KB_TYPES 是一个总开关

上面五个常量被塞进一个 frozenset:CONNECTED_KB_TYPESkb_types.py:77-87)。这个集合不是用来做展示分组的,它是一个行为开关——属于它,就跳过索引流水线、孤儿清理与 embedding 对账这三条链路。

配套有两个访问器,是判断”这个 KB 该怎么对待”的正门:

  • external_root_of()kb_types.py:105-113):返回连接型 KB 指向的外部根路径。对 subagent / lightrag_server / ima 三种返回 None——因为它们根本不落到本地路径上。
  • supports_local_raw_files()kb_types.py:95-102):连接型 KB 不参与 DeepTutor 的 raw 文件上传与管理 API。

这两条合起来解释了一个高频困惑:为什么某个知识库没法上传文件、也找不到它的本地目录。答案不在上传逻辑里,在 type 字段上。排查这类问题的第一动作,是去 kb_config.json 看这个条目有没有 type、值是什么,而不是去翻上传接口的报错。

类型模型在写入侧也是对齐的。deeptutor/knowledge/manager.py 里的注册型 API 一共 6 个:register_knowledge_baseregister_obsidian_vaultregister_linked_kbregister_subagent_connectionregister_lightrag_server_kbregister_ima_kb(分别在 manager.py:674,691,725,779,832,883)。一个默认型加五个连接型,正好对上前面那张表——所以你想确认某种类型到底写了哪些字段,直接读对应的那个 register_* 就行。以 Obsidian 为例,写入的条目字段是 path / type / vault_path / description / status="ready" / created_at / updated_atmanager.py:712-720)。

顺带一提名字这一侧的约束,都在 deeptutor/knowledge/naming.py:长度上限 _MAX_KB_NAME_LENGTH = 120naming.py:10),禁用字符集合 _FORBIDDEN_CHARS = set('<>:"/\\|?*#%')naming.py:9),控制字符正则 [\x00-\x1f\x7f] 被拒(naming.py:8,29),名称先做 Unicode NFC 归一化再 strip,空名、... 一律报错(naming.py:20-24)。禁用字符集合里同时包含 /\: 这类路径分隔与盘符字符。

三、manifest.py:把”库里有什么”当成事实来算

deeptutor/knowledge/manifest.py 只有 429 行,在这个 8 文件 3556 行的目录里不算大,但它是”清单”这件事的唯一实现。两个数据类:

  • KbDocumentmanifest.py:69-78):字段只有两个,name 是相对文档根的 POSIX 路径,size 是字节数,stat 失败时记 0——不是抛错,是记 0。
  • KbManifestmanifest.py:80-108):字段 name / provider / status / kb_type / total / matched / documents / pattern / unavailable,外加 enumerableomitted 两个属性。

这里的字段命名值得读两遍:totalmatched 是分开的两个数,documents 是被截断后的那一截,omitted 是”被截掉了多少”。也就是说,这个结构在设计上就承认”我给你的列表是不完整的”,并且把不完整的程度作为一个显式字段暴露出来,而不是让调用方自己去猜。

截断的上限有三个常量:

  • MANIFEST_NOTE_LIMIT = 20manifest.py:46)——系统提示词里每个 KB 最多列 20 个文档;
  • KB_FILES_DEFAULT_LIMIT = 200KB_FILES_MAX_LIMIT = 1000manifest.py:50-51)——kb_files 工具的默认值与上限。

20 与 200 差一个数量级,这个差不是随手写的:前者要跟着每一轮对话进提示词,后者是用户主动查清单时才走。你自己接这套数据时,也得按这两条路径分开考虑,别拿同一个上限套。

四、反直觉的那一处:它明确拒绝报告”进索引的文档数”

如果你只从这个模块拿一个信息走,就是这一条。

manifest.py:18-25 的源码里写明了一件”故意不报告”的事:进入索引的文档数。清单能告诉你库里有多少个文件、都叫什么、多大,但它不告诉你”这些文件里有多少篇真的被索引了”。

给出的理由是三条数据源各有各的不可靠:

  1. docstore.jsonchunk 级的——它里面的条目数是切块数,不是文档数;
  2. metadata.json 里的 file_hashes 只在增量添加这条路径上写——走别的路径进来的文档不在里面;
  3. last_indexed_count 只是最后一批的大小——它是一次批处理的规模,不是累计总量。

三个看起来都能当”索引了几篇”用的字段,逐个都不成立。于是这个模块的选择是:不给这个数,而不是从三个里挑一个近似的报出去。

这与人的直觉相反。一般写清单功能,看到手边有个 last_indexed_count 就会顺手拿来当”已索引数”填进返回体,反正数量级差不多——但差不多的数字一旦进了提示词,模型就会拿它当事实用,而使用者也不会知道这个数是怎么来的。这里的取舍是宁可留白。

同样的思路在”不可枚举”的处理上也出现了一次。三个原因码写在 manifest.py:55-57UNAVAILABLE_REMOTE = "remote"UNAVAILABLE_AGENT = "agent"UNAVAILABLE_MISSING = "missing"。远端型与 agent 型 KB 拿不到本地文件列表,它们不会被报成”0 个文档”,而是带上原因码报成不可枚举——enumerable 属性就是 unavailable 为空时才为真。0 和”我没法数”是两件事,这套模型把它们分开了。

所以,你在使用侧看到”该知识库无法列出文档”时,先别当成故障:去看条目的 type,如果是那三种连接型之一,这就是设计内的返回,不是索引坏了。

五、“文档”到底怎么算:三条口径

清单里的数怎么来的,取决于三条口径,都写在同一个文件里。

其一,什么算文档。 非隐藏的普通文件才算(manifest.py:111-139)。以 . 开头的条目在每一层都被跳过——.DS_Store.obsidian/ 这类都不计入;并且不跟随符号链接目录。这两条直接决定了你的数字:一个 Obsidian vault 里的 .obsidian/ 配置目录不会被算进去,而如果你靠软链接把另一个目录挂进来,它也不会被走进去。

其二,从哪个根开始数。 文档根的解析规则是(manifest.py:142-154):普通 KB 是 <kb_dir>/rawlinkedobsidian 就地枚举它们指向的外部目录;远端型返回 None。注意普通 KB 数的是 raw 而不是整个 KB 目录——同级还有别的子目录,不在清单口径内。

其三,pattern 怎么匹配。 规则在 manifest.py:202-214:过滤串里含 *?[ 三种通配符之一时按 glob 匹配,否则按大小写不敏感的子串匹配。这一条很容易想当然:你写 report,它不是精确匹配也不是前缀匹配,是子串;你写 *.md,才走 glob。两种模式的切换是隐式的,取决于你有没有打通配符。

还有一个小口径要记住:total 永远是全量计数,忽略 patternmatched 才是过滤后的数(manifest.py:80-108 的字段注释)。看到”总数 300、匹配 4”,不要以为库缩水了。

文件大小的渲染单位链是 B → KB → MB → GB → TB(manifest.py:404-412)。状态标签的本地化表只覆盖 en / zh 两种语言,键是 ready / needs_reindex / processing / initializing / error 五个(manifest.py:229-244)——如果你在做第三种语言的界面,这张表是要自己补的地方之一。

六、两处文档与代码对不上的地方

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

  1. deeptutor/knowledge/kb_types.py 的模块 docstring 说连接型”这些 flavours 存在于今天”并列了 5 种;而 manifest.py:142-148document_root docstring 只提到”一个远端 LightRAG server 与一个连接的 subagent 没有本地文档集”,未提 ima——代码里的 _NON_DOCUMENT_KB_TYPES包含 ima 的(manifest.py:62-66,146-147)。
  2. README.md:734 的 CLI 表把 deeptutor kb ... 描述为 “Manage LlamaIndex knowledge bases”;而代码里检索引擎 provider 常量有 6 个、连接型 KB 有 5 种(deeptutor/services/rag/factory.py:30-48kb_types.py:77-87)。

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

七、你可以照着核的五步

  1. 打开 deeptutor/knowledge/kb_types.py,从第 53 行往下读到第 87 行,把五个 *_KB_TYPE 常量和 CONNECTED_KB_TYPES 抄一遍;
  2. 打开你自己的 kb_config.json(默认在 <base_dir>/kb_config.json),逐条看有没有 type 字段——没有的是默认索引型;
  3. 把上一步看到的 type 值回到 manifest.py:62-66_NON_DOCUMENT_KB_TYPES 里比一遍,就知道这个库为什么”列不出文档”;
  4. 在仓库里 grep -n "MANIFEST_NOTE_LIMIT\|KB_FILES_DEFAULT_LIMIT\|KB_FILES_MAX_LIMIT" deeptutor/knowledge/manifest.py,确认 20 / 200 / 1000 这三个上限;
  5. manifest.py:18-25 那段说明,确认”进索引文档数不报告”是显式选择,以及它给出的三条理由分别指向 docstore.jsonmetadata.jsonfile_hasheslast_indexed_count

需要说明的边界:deeptutor/knowledge/manager.py 有 1861 行,本文只用到了注册型 API 与 Obsidian 条目字段那几段,get_infodelete_knowledge_baselink_folder 系列我们没有逐行核实;add_documents.py(480 行)与 initializer.py(348 行)只读了前 70 行,增量索引与建库的具体流程不在本文范围;REST API 层如何调用这些模型我们没有读。本文出现的所有上限与默认值都是源码中的默认配置,不是运行结果的保证。


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

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