知识库存到哪里,名字为什么会被拒
「知识库存到哪里」和「名字为什么会被拒」,看上去是两个不相干的问题,在 DeepTutor 的源码里其实是同一个问题的两头:你给知识库起的那个名字,会直接变成磁盘上的一个目录名。所以名字校验不是输入框的礼貌性检查,它是落盘约束的前置条件。
下面的行号与常量都对应我们采集的仓库快照:HEAD 456f9c2,采集日 2026-08-10,deeptutor/__version__.py 里的版本号是 1.5.11。你 clone 之后行号可能已经漂了,文件名与常量名是稳的,照着搜即可。
一、名字校验:四条规则写在三行常量里
规则集中在 deeptutor/knowledge/naming.py,整个文件只有 40 行,把这三行常量看完,你就知道自己那个名字为什么过不去了:
_MAX_KB_NAME_LENGTH = 120(naming.py:10)——长度上限 120。_FORBIDDEN_CHARS = set('<>:"/\\|?*#%')(naming.py:9)——禁用字符集合。- 控制字符正则
[\x00-\x1f\x7f](naming.py:8),命中即拒(naming.py:29)。
外加一条在函数体里:名字先做 Unicode NFC 归一化再 strip,空名以及 .、.. 一律报错(naming.py:20-24)。
这里有一处值得单独拎出来说的地方:禁用字符集里除了 < > : " / \ | ? * 这批文件系统保留字,还多了 # 和 % 两个(naming.py:9)。这两个字符在 Windows 与 Linux 上都能出现在合法文件名里,按「这只是个目录名」的直觉不该被拦。它们为什么也被收进这个集合,我们不做推断;对使用者来说,能用的结论只有一条:判断名字合不合法,唯一标准是 naming.py:9 里那个集合,别拿操作系统自己的文件名规则去套。一个带 # 或 % 的名字,你在本机新建同名文件夹毫无问题,放进 DeepTutor 就是会被拒——这也是这条规则最容易让人愣住的地方。
NFC 归一化这一条也容易吃暗亏:你从别处粘贴过来的名字,若带的是分解形式的音标或组合字符,落盘前会被规整成合成形式。也就是说,你输入的字符串与最终变成目录名的字符串未必逐字节相同。这条对中文用户影响不大,但如果你的库名里混了带变音符号的拉丁字母、或者带兼容字符的日韩文本,值得留意。
判断自己是不是撞上了这条:把名字里的每个字符逐个对照 naming.py:8-10 那三行看一遍。四条规则各自单独报错,报错信息本身就够定位到是哪一条。如果四条都不沾边,那问题不在名字校验这一层,别在这里继续排查。
二、名字过了之后,东西落在哪
KnowledgeBaseManager.__init__ 的 base_dir 参数默认值是 "./data/knowledge_bases"(deeptutor/knowledge/manager.py:247)。注意它是相对路径——相对的是进程的当前工作目录,不是仓库目录,也不是用户主目录。
这个基目录下有一个配置文件 kb_config.json(manager.py:252,常量名在 deeptutor/services/rag/kb_paths.py:21),所有知识库条目都登记在里面。
单个 KB 目录下有几个固定子路径:
| 子路径 | 装什么 | 出处 |
|---|---|---|
raw | 原始文档 | manager.py:978-991 |
images | 图片 | manager.py:978-991 |
content_list | 内容清单 | manager.py:978-991 |
llamaindex_storage | 默认引擎的索引存储 | initializer.py:42-43 |
KnowledgeBaseInitializer 里同时写明 raw_dir = kb_dir / "raw"(deeptutor/knowledge/initializer.py:42-43),与清单侧的口径一致:普通 KB 的文档根就是 <kb_dir>/raw(deeptutor/knowledge/manifest.py:142-154)。所以「我把文件放进去了但系统看不到」这类问题,第一步是确认文件到底在不在这一层,而不是先怀疑索引。
索引的版本目录是平铺的 version-N:前缀常量 VERSION_PREFIX = "version-",识别正则是 ^version-(\d+)$,新版本号取现有目录里的最大值加一(deeptutor/services/rag/index_versioning.py:38,43,111-113)。平铺意味着 version-1、version-2 是并列的兄弟目录,不嵌套,你 ls 一眼就能看出建过几版。
还有一个遗留目录名叫 rag_storage。如果某个 KB 的 provider 是默认引擎,且目录里只剩这个遗留目录,它会被标成 needs_reindex,原本的 ready 状态会被改写掉(manager.py:313-326)。看到状态莫名其妙从 ready 变成 needs_reindex,先去看这个 KB 目录下有没有 version-N。
三、反直觉的那一处:路径不是拼出来的
读到这里你大概已经在心里拼好了一条路径:<base_dir>/<kb_name>。这在多数情况下确实成立,但源码明确禁止你这么写代码。
deeptutor/services/rag/kb_paths.py 的模块 docstring 把话说得很硬:resolve_kb_dir() 是所有 pipeline 找 KB 存储根的唯一入口,pipeline 不得自己去算 Path(kb_base_dir) / kb_name,否则 linked KB 会解析到一个不存在的本地目录,然后静默返回空结果(kb_paths.py:8-11,24-34)。
「静默」是这处最咬人的地方。不是报错、不是找不到目录的异常,而是检索正常跑完、返回零条。对上层来说,这和「知识库里确实没有相关内容」长得一模一样。
为什么会这样,看一眼知识库的类型模型就明白了(deeptutor/knowledge/kb_types.py):多数 KB 是默认的 indexed 类型,条目上不带 type 字段(kb_types.py:3-7);而另外五种被归进 CONNECTED_KB_TYPES 这个 frozenset(kb_types.py:77-87),它们压根不按「基目录加库名」的规矩落盘。本文只取其中与落盘位置直接相关的那条线索:
- 落在别处:
linked带external_path字段,指向用户在别处已经建好的引擎索引(kb_types.py:58);obsidian带vault_path字段(kb_types.py:53)。这两种的内容都躺在你自己指定的外部目录里。 - 本机没有落点:
subagent(kb_types.py:63)、lightrag_server(kb_types.py:69)、ima(kb_types.py:75)三种在磁盘上根本没有对应路径——配套的external_root_of()对这三种直接返回None(kb_types.py:105-113)。
external_root_of() 返回 None 这一行,正是上面那条禁令的注脚:存储根在不同类型下压根不是同一套算法,你自己拼 <base_dir>/<kb_name> 只是碰巧在 indexed 类型上蒙对了。这五种类型各自还带哪些字段、检索请求分别转发到哪里、以及属于 CONNECTED_KB_TYPES 的条目怎么被索引流水线、孤儿清理与 embedding 对账整体跳过,本文不展开——知识库的类型模型与清单,我们另有一篇专门讲。
所以「知识库存到哪里」这个问题没有单一答案。只有默认的 indexed 类型才真的落在 <base_dir>/<kb_name>,其余五种要么在别处、要么根本不在磁盘上。排查「文件夹是空的」之前,先去 kb_config.json 里看这个条目的 type 是什么——类型不对,你在默认基目录下翻多久都是空的。
顺带提醒一句:lightrag_server 与 ima 两种类型的条目字段里带 api_key(kb_types.py:69,75),这属于本机上的敏感凭据。怎么保管、要不要给配置文件单独收紧权限,取决于你自己的环境,本文不给方案,也不做任何「这样配就安全」的承诺。
四、两处基目录口径,说完就停
同一件事在仓库里有两个位置给出取值:
KnowledgeBaseManager.__init__的默认base_dir是"./data/knowledge_bases"(manager.py:247);add_documents.py:30与initializer.py:32里的默认值与它一致。- 服务端的路径服务里,KB 根目录是
<workspace_root>/knowledge_bases(deeptutor/services/path_service.py:116-117)。
两处是不同的表达。按纪律,我们只陈述差异并标明位置,不推断哪个是「对的」,也不据此评价什么。实际排查时以你这次运行走的是哪条入口为准——CLI 直跑与服务端两条路谁调了谁,我们没有核实,不下结论。
五、写盘这一侧的三个细节
配置写入是原子的。 _save_config 走 atomic_write_json(临时文件加 os.replace),源码注释说明了旧实现直接 open(..., "w") 会在拿到锁之前就把文件截断(manager.py:343-350)。这解释了为什么并发写配置时不会看到半截 JSON。
孤儿清理有 60 秒宽限。 _ORPHAN_PRUNE_GRACE_SECONDS = 60:一个条目对应的目录缺失,要超过 60 秒才会被 list_knowledge_bases 当作孤儿清掉(manager.py:50-55)。也就是说,你手动删了目录之后立刻去列表里看,条目可能还在——这不是没生效,是还没到宽限期。
PocketBase 只是镜像。 项目支持可选的 PocketBase 同步,但源码写明本地 JSON 文件仍然是唯一的事实源,PocketBase 拿到的是一份镜像副本(manager.py:255-260)。两边对不上时,以本地文件为准。
注册型 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)。以 Obsidian 为例,它写进条目的字段是 path / type / vault_path / description / status="ready" / created_at / updated_at(manager.py:712-720)——注册完直接就是 ready,因为它本来就不需要建索引。
六、状态字符串的一处不一致
manager.py:395 处文档写的状态字符串是 "initializing"、"processing"、"ready"、"error" 四个,而代码里另有 "needs_reindex" 分支(manager.py:1171)。清单侧的状态标签本地化表则覆盖 ready / needs_reindex / processing / initializing / error 五个键,且只有 en / zh 两种语言(manifest.py:229-244)。
两处不一致,标明位置到此为止,以我们实读的仓库状态为准。对你的实际影响只有一条:做状态判断时别按四个值写死分支。
另外 README.md:734 的 CLI 表把 deeptutor kb ... 描述为 “Manage LlamaIndex knowledge bases”,而代码里检索引擎 provider 常量有 6 个、connected 类型有 5 种(deeptutor/services/rag/factory.py:30-48、kb_types.py:79-87)。同样只陈述差异。
七、可以照着核的五步
- 打开
deeptutor/knowledge/naming.py:8-10,把你的库名逐字符对照三行常量;再看:20-24确认NFC归一化与./..的处理。 - 打开
deeptutor/knowledge/manager.py:247与:252,确认基目录默认值是相对路径,配置文件叫kb_config.json。 - 在
kb_config.json里找到你这个条目,先看它有没有type字段;有的话对照kb_types.py:53-75判断它属于哪一种 connected 类型。 - 进到 KB 目录
ls一下:看有没有raw、有没有version-N、是不是只剩rag_storage——第三种情况对应manager.py:313-326的needs_reindex改写。 - 在仓库里搜
resolve_kb_dir,确认自己写的扩展代码没有绕过它去拼路径(kb_paths.py:24-34)。
需要说明的边界:manager.py 全文 1861 行,我们只精读了构造、配置读写、路径访问器、register_* 与状态更新这几段;get_info、delete_knowledge_base、link_folder 系列只看了签名。add_documents.py(480 行)与 initializer.py(348 行)只读了前 70 行,增量索引与建库的完整流程我们没有核实。embedding 配置与 index_probe.py、preflight.py 未读,所以关于「重建索引何时触发」,本文只覆盖了 manager 里能看到的规则(manager.py:313-326 的遗留目录改写这一条),不代表全部。上面出现的所有默认值都是源码中的默认配置,不是运行结果的保证。
本文依据 DeepTutor 官方仓库(github.com/HKUDS/DeepTutor)的 README、AGENTS.md、
pyproject.toml 与 deeptutor/ 下的源码整理,核对日 2026-08-10,对应仓库快照 456f9c2(版本 1.5.11)。
本文内容为仓库源码与文档口径,我们没有安装、部署或运行过该项目,也没有调用过其中任何一个模型 API,
因此不涉及生成质量、响应速度与教学效果的任何描述。
参数与默认值随版本变动,请以仓库最新代码与 --help 的实际输出为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。