rag 检索管线:52 个文件拆成了几段
读检索层最怕的是一头扎进 chunk 策略和融合权重里,读了两天还说不清「我配的那个管线到底有没有被选上」。DeepTutor 的 deeptutor/services/rag/ 恰好把这两件事分得很干净:选管线的逻辑集中在一个工厂文件里,各管线的算法各自躲在子包里。本文只读前者——因为前者是你排查问题时真正会撞上的那一层。
先把锚点交代清楚:以下所有行号、文件数、常量名都对应我们采集的仓库快照 HEAD 456f9c2,采集日 2026-08-10,deeptutor/__version__.py 里的版本号是 1.5.11。你自己 clone 之后行号可能已经漂了,文件名与常量名是相对稳的,照着搜即可。
一、52 个文件在 services 这一层的什么位置
deeptutor/services/ 整层是 323 个文件、Python 代码合计 65456 行,一级子目录 26 个。在这 26 个里,rag 是 7539 行 / 52 个文件,规模排第二,只低于 llm 的 9263 行 / 36 文件。
这两个数字放一起有个直观的含义:rag 的文件数比 llm 多出 16 个,总行数却更少。也就是说这一层是被切得比较碎的——平均每个文件不到 150 行。我们在事实核对里读到的一条具体路径是 deeptutor/services/rag/pipelines/llamaindex/vector_store.py,可见管线代码在 pipelines/ 之下还有一层目录。至于这一层碎成这样是不是全都由目录形态决定的,我们没有把 52 个文件逐个列出来核,不下结论。
这一层的模块 docstring 只有一句话,卡里记的是「RAG 管线导出」(rag/__init__.py:1)。
至于谁在调它:层级总数我们用 grep -rn 'from deeptutor.services' 统计,api 层引用 services 共 201 处;按子模块拆分则换成 grep -rho 'from deeptutor\.services\.[a-z_]*' | sort | uniq -c 来数,rag 被引 23 次,与 memory 并列第二,仅次于 config 的 34 次。两个数的统计命令不是同一条,你自己复现时别混用。所以 rag 不是一个内部小工具,它是 api 层的高频依赖。
二、拆成几段:工厂文件里的六个常量
rag/factory.py 里定义了 6 个管线 provider 常量(rag/factory.py:30-48):
| provider 名 | 备注 |
|---|---|
llamaindex | 默认 |
pageindex | |
graphrag | 需 pip install 可选依赖 |
lightrag | 需 pip install 可选依赖 |
lightrag-server | HTTP 客户端形态 |
ima | HTTP 客户端形态(Tencent IMA) |
对应的分发函数是 _build_pipeline,按名字懒 import 五个具名管线类:PageIndexPipeline、GraphRagPipeline、LightRagPipeline、LightRagServerPipeline、ImaPipeline;都不命中时落到同样是函数内 import 的 LlamaIndexPipeline——六个分支全部是懒 import(rag/factory.py:120-160)。
「懒 import」这个写法带来的效果,可以从解析层那份同款工厂上看到依据:parsing/engines/factory.py:1-7 的文件头注释一边写明自己 “mirrors the RAG pipeline factory (services/rag/factory.py)“,一边说明引擎模块懒 import 第三方依赖、导入注册表本身不会因为缺少可选依赖而失败。rag 这一侧的写法与之同构,但 rag 的工厂文件里并没有对应的注释来自陈这一点,所以这句只能算解析层的原话,不能当成 rag 自己的声明。
顺带说清这一层的边界:六个管线各自的检索算法我们没有读。chunk 怎么切、BM25 融合权重是多少、GraphRAG 与 LightRAG 的 mode 各是什么语义、SUPPORTED_MODES 与 DEFAULT_MODE 的实际取值——这些我们都没有打开对应文件确认,本文一律不下结论。
三、反直觉的那一处:名字写错不会报错
这是这一层最值得单独拎出来讲的设计。
rag/factory.py:54-59 处理了「未知或历史遗留的 provider 字符串」,做法是 collapse 回默认,注释写明这样做是为了让 “stale config 不会选到已不存在的管线”。
按大多数人的直觉,配置里写了个不存在的管线名,应该在启动或首次检索时炸一下。这里恰恰相反:它悄悄换成默认的 llamaindex 继续跑。这个选择本身是有明确目标的——用户升级之后旧配置里残留的老名字不会把功能整个卡死。但它给排查带来一个具体后果:
「我明明配了 graphrag,为什么表现和之前一样」这类问题,在这一层不会给你任何错误信号。
所以判定动作要落在配置这一侧,而不是等报错:
- 先把你配置里的 provider 字符串逐字抄出来,和
rag/factory.py:30-48里那 6 个常量逐字符比对——注意lightrag-server是连字符,不是下划线,也不是空格。 - 再看
_build_pipeline(rag/factory.py:120-160)里的分支名,确认你要的那个类确实有对应分支。 - 如果两处都对得上,问题就不在”选没选中”这一层,往下走去看依赖装没装、凭据配没配,别在工厂文件里继续耗。
反过来说,如果你配的名字压根不在这 6 个里,那么无论下游怎么调,跑的都是默认管线——这一点先确认,能省掉一整轮无效排查。
四、第二处反直觉:configured: True 不等于能用
list_pipelines() 给 UI 返回的字段包括 configured、requires_api_key、modes(rag/factory.py:217-275),其中 modes 并非每条都有——这一点以你自己打开那段代码看到的为准。
这里有一处需要照实说明的语义:lightrag-server 与 ima 这两个恒为 configured: True,原因是它们只是 HTTP 客户端,凭据按知识库(KB)逐个配置,所以工厂这一层压根没有”全局配好没有”这个状态可报。而 graphrag 与 lightrag 则要求 pip install 可选依赖,它们的 configured 才反映依赖是否就位。
也就是说,同一个字段名在六个管线上的含义并不是同一件事。看到 configured: True 时,你要先分清它属于哪一类:是”依赖装好了”,还是”这个字段对它恒真、真正的凭据在 KB 那一侧”。这不是文档里的坑,是这两类管线形态天然不同带来的结果,代码注释已经把原因写在那了。
还有一件必须如实说明的事:lightrag-server 与 ima 是 HTTP 客户端形态,实际检索由外部服务端承担。把知识库内容交给外部服务是否合适,取决于你的内容与合规要求,请自行评估,本文不给建议。
五、管线实例的缓存键:(kb_base_dir, provider)
管线实例是按 (kb_base_dir, provider) 缓存的(rag/factory.py:51、:171-179),并且传自定义 kwargs 时跳过缓存。
这两句话有三个可直接用上的推论,都在字面语义范围内:
- 同一个 provider,不同知识库目录,拿到的是不同实例——缓存键里带着
kb_base_dir。 - 同一个知识库目录换 provider,也是不同实例。
- 一旦你走了带自定义 kwargs 的调用路径,就不吃缓存了。这条在你怀疑”改了参数怎么没生效”时值得先看一眼:走没走缓存,取决于你有没有传 kwargs。
至于实例被创建之后什么时候失效、进程内会不会长期驻留,我们没有逐行读它的失效逻辑,不做推断。
六、向量库那处 NotImplementedError
rag/pipelines/llamaindex/vector_store.py 里有两处一模一样的抛错,原文照抄:
raise NotImplementedError("FAISS only supports local storage for now.")
位置分别是 :149 与 :158。
按仓库原样记录:这是默认管线(llamaindex)向量存储侧的一条明确边界,for now 是代码里自己写的措辞。你如果打算把向量库放到本地磁盘之外的地方,这两行是必须先读的。它具体挡住的是哪几条调用路径,我们没有沿着调用方向追,只记录抛错文本与行号。
七、同一件事在 docstring、注册表与 README 里对不上
写这一层时会撞上一组可核实的口径差异。按纪律,我们只陈述差异并标明位置,不推断原因,也不据此评价项目:
rag/factory.py:3的模块 docstring 写的是 “Three pipelines ship today”,而紧接着的 bullet 列表列了 6 条(:6-19);KNOWN_PROVIDERS实际包含 6 个(:39-48),_build_pipeline也实现了 6 个分支(:120-160)。- README 的检索引擎清单少 2 个。
README.md:198写的是 “versioned RAG libraries across LlamaIndex, PageIndex, GraphRAG, LightRAG, or a linked Obsidian vault”,未提代码中同样注册的lightrag-server与ima(rag/factory.py:34-35、:250-274)。 - 以我们实读的仓库状态为准:注册表里是 6 个。
说完就停。这里唯一需要你带走的操作性结论是:判断”有哪些管线可选”,去读 rag/factory.py:30-48,不要以 docstring 或 README 的清单为准。
八、向量化那一侧的一个已知取值陷阱
检索链路的另一半是 embedding,它不在 rag/ 目录里,而在 services/embedding/ 与 services/config/provider_runtime.py。这里只提一条与配置直接相关的:
config/provider_runtime.py:60-65 的注释指出,default_api_base 自 v1.3.0 起是完整的 embedding 端点 URL,而不是 base,适配器会”逐字使用配置的 URL,不追加路径”。也就是说,如果你按老习惯只填了一个 base 前缀,适配器不会替你补后半截。EMBEDDING_PROVIDERS 共 12 个键,适配器实现有 8 个文件。
其余 embedding 细节我们另有专门的篇目在讲,这里不展开。
九、你可以照着核的五步
- 打开
rag/factory.py:30-48,把 6 个 provider 常量抄下来,和你自己的配置逐字符比。 - 打开
rag/factory.py:54-59,确认未知名字的处理是 collapse 回默认而不是抛错,并读一遍那句 “stale config” 注释。 - 打开
rag/factory.py:120-160,确认六个分支是懒 import 的;想知道这种写法要解决什么,去读parsing/engines/factory.py:1-7那段自陈的注释。 - 打开
rag/factory.py:217-275,看configured、requires_api_key、modes这几个字段各自出现在哪些管线上,确认lightrag-server与ima的configured恒为 True。 - 打开
rag/pipelines/llamaindex/vector_store.py:149与:158,把那句NotImplementedError的原文抄进你自己的技术备忘里。
十、本文的边界
我们只读了 rag/ 这一层的工厂文件、模块 docstring 与向量存储的两处抛错,没有逐行读完 52 个文件中的任何一个千行级文件;六个管线各自的检索算法、chunk 策略、融合权重、mode 语义均未核实。上面出现的所有常量、字段与文件路径都来自源码文本,不代表这些能力在实际环境中已经被验证可用;凡是涉及”检索出来的结果好不好”的部分,本文一律不下结论。
本文依据 DeepTutor 官方仓库(github.com/HKUDS/DeepTutor)的 README、AGENTS.md、
pyproject.toml 与 deeptutor/ 下的源码整理,核对日 2026-08-10,对应仓库快照 456f9c2(版本 1.5.11)。
本文内容为仓库源码与文档口径,我们没有安装、部署或运行过该项目,也没有调用过其中任何一个模型 API,
因此不涉及生成质量、响应速度与教学效果的任何描述。
参数与默认值随版本变动,请以仓库最新代码与 --help 的实际输出为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。