知识库的类型模型与 `manifest` 清单:一个拒绝回答「索引了几篇」的模块
多数带知识库的项目,「知识库」只有一种:把文件丢进去,切块、嵌入、建索引,然后检索。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_TYPE | obsidian | vault_path | 用户自己的 Markdown 目录,不建索引 |
LINKED_KB_TYPE | linked | external_path | 用户在别处已建好的引擎索引,跳过索引步骤 |
SUBAGENT_KB_TYPE | subagent | —(磁盘上没有路径) | 一个连接的 agent,无可索引可检索内容 |
LIGHTRAG_SERVER_KB_TYPE | lightrag_server | server_url + 可选 api_key | 检索转发到该服务器的 /query 端点 |
IMA_KB_TYPE | ima | client_id + api_key + knowledge_base_id | 检索转发到腾讯 IMA 的 search_knowledge OpenAPI |
行号分别是 kb_types.py:53、:58、:63、:69、:75。
这五种里有两件事必须如实说清楚,不能当成”另一种数据源”轻描淡写过去:
lightrag_server与ima这两种,检索请求会离开你的机器,发往一个外部服务;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_TYPES(kb_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_base、register_obsidian_vault、register_linked_kb、register_subagent_connection、register_lightrag_server_kb、register_ima_kb(分别在 manager.py:674,691,725,779,832,883)。一个默认型加五个连接型,正好对上前面那张表——所以你想确认某种类型到底写了哪些字段,直接读对应的那个 register_* 就行。以 Obsidian 为例,写入的条目字段是 path / type / vault_path / description / status="ready" / created_at / updated_at(manager.py:712-720)。
顺带一提名字这一侧的约束,都在 deeptutor/knowledge/naming.py:长度上限 _MAX_KB_NAME_LENGTH = 120(naming.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 行的目录里不算大,但它是”清单”这件事的唯一实现。两个数据类:
KbDocument(manifest.py:69-78):字段只有两个,name是相对文档根的 POSIX 路径,size是字节数,stat 失败时记 0——不是抛错,是记 0。KbManifest(manifest.py:80-108):字段name/provider/status/kb_type/total/matched/documents/pattern/unavailable,外加enumerable、omitted两个属性。
这里的字段命名值得读两遍:total 与 matched 是分开的两个数,documents 是被截断后的那一截,omitted 是”被截掉了多少”。也就是说,这个结构在设计上就承认”我给你的列表是不完整的”,并且把不完整的程度作为一个显式字段暴露出来,而不是让调用方自己去猜。
截断的上限有三个常量:
MANIFEST_NOTE_LIMIT = 20(manifest.py:46)——系统提示词里每个 KB 最多列 20 个文档;KB_FILES_DEFAULT_LIMIT = 200、KB_FILES_MAX_LIMIT = 1000(manifest.py:50-51)——kb_files工具的默认值与上限。
20 与 200 差一个数量级,这个差不是随手写的:前者要跟着每一轮对话进提示词,后者是用户主动查清单时才走。你自己接这套数据时,也得按这两条路径分开考虑,别拿同一个上限套。
四、反直觉的那一处:它明确拒绝报告”进索引的文档数”
如果你只从这个模块拿一个信息走,就是这一条。
manifest.py:18-25 的源码里写明了一件”故意不报告”的事:进入索引的文档数。清单能告诉你库里有多少个文件、都叫什么、多大,但它不告诉你”这些文件里有多少篇真的被索引了”。
给出的理由是三条数据源各有各的不可靠:
docstore.json是 chunk 级的——它里面的条目数是切块数,不是文档数;metadata.json里的file_hashes只在增量添加这条路径上写——走别的路径进来的文档不在里面;last_indexed_count只是最后一批的大小——它是一次批处理的规模,不是累计总量。
三个看起来都能当”索引了几篇”用的字段,逐个都不成立。于是这个模块的选择是:不给这个数,而不是从三个里挑一个近似的报出去。
这与人的直觉相反。一般写清单功能,看到手边有个 last_indexed_count 就会顺手拿来当”已索引数”填进返回体,反正数量级差不多——但差不多的数字一旦进了提示词,模型就会拿它当事实用,而使用者也不会知道这个数是怎么来的。这里的取舍是宁可留白。
同样的思路在”不可枚举”的处理上也出现了一次。三个原因码写在 manifest.py:55-57:UNAVAILABLE_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>/raw;linked 与 obsidian 就地枚举它们指向的外部目录;远端型返回 None。注意普通 KB 数的是 raw 而不是整个 KB 目录——同级还有别的子目录,不在清单口径内。
其三,pattern 怎么匹配。 规则在 manifest.py:202-214:过滤串里含 *、?、[ 三种通配符之一时按 glob 匹配,否则按大小写不敏感的子串匹配。这一条很容易想当然:你写 report,它不是精确匹配也不是前缀匹配,是子串;你写 *.md,才走 glob。两种模式的切换是隐式的,取决于你有没有打通配符。
还有一个小口径要记住:total 永远是全量计数,忽略 pattern;matched 才是过滤后的数(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)——如果你在做第三种语言的界面,这张表是要自己补的地方之一。
六、两处文档与代码对不上的地方
按纪律,只陈述差异、标明位置,不推断原因,也不据此评价项目。
deeptutor/knowledge/kb_types.py的模块 docstring 说连接型”这些 flavours 存在于今天”并列了 5 种;而manifest.py:142-148的document_rootdocstring 只提到”一个远端 LightRAG server 与一个连接的 subagent 没有本地文档集”,未提ima——代码里的_NON_DOCUMENT_KB_TYPES是包含ima的(manifest.py:62-66,146-147)。README.md:734的 CLI 表把deeptutor kb ...描述为 “Manage LlamaIndex knowledge bases”;而代码里检索引擎 provider 常量有 6 个、连接型 KB 有 5 种(deeptutor/services/rag/factory.py:30-48、kb_types.py:77-87)。
以我们实读的仓库状态为准。说完就停。
七、你可以照着核的五步
- 打开
deeptutor/knowledge/kb_types.py,从第 53 行往下读到第 87 行,把五个*_KB_TYPE常量和CONNECTED_KB_TYPES抄一遍; - 打开你自己的
kb_config.json(默认在<base_dir>/kb_config.json),逐条看有没有type字段——没有的是默认索引型; - 把上一步看到的
type值回到manifest.py:62-66的_NON_DOCUMENT_KB_TYPES里比一遍,就知道这个库为什么”列不出文档”; - 在仓库里
grep -n "MANIFEST_NOTE_LIMIT\|KB_FILES_DEFAULT_LIMIT\|KB_FILES_MAX_LIMIT" deeptutor/knowledge/manifest.py,确认 20 / 200 / 1000 这三个上限; - 读
manifest.py:18-25那段说明,确认”进索引文档数不报告”是显式选择,以及它给出的三条理由分别指向docstore.json、metadata.json的file_hashes与last_indexed_count。
需要说明的边界:deeptutor/knowledge/manager.py 有 1861 行,本文只用到了注册型 API 与 Obsidian 条目字段那几段,get_info、delete_knowledge_base、link_folder 系列我们没有逐行核实;add_documents.py(480 行)与 initializer.py(348 行)只读了前 70 行,增量索引与建库的具体流程不在本文范围;REST API 层如何调用这些模型我们没有读。本文出现的所有上限与默认值都是源码中的默认配置,不是运行结果的保证。
本文依据 DeepTutor 官方仓库(github.com/HKUDS/DeepTutor)的 README、AGENTS.md、
pyproject.toml 与 deeptutor/ 下的源码整理,核对日 2026-08-10,对应仓库快照 456f9c2(版本 1.5.11)。
本文内容为仓库源码与文档口径,我们没有安装、部署或运行过该项目,也没有调用过其中任何一个模型 API,
因此不涉及生成质量、响应速度与教学效果的任何描述。
参数与默认值随版本变动,请以仓库最新代码与 --help 的实际输出为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。