Skip to main content

入库流水线

Written: 2026.06
第 16 章跟敲代码:codealong/chapters/ch16_ingestion_pipeline。 这部分代码是本章跟敲版,用来先跑通核心闭环;完整项目源码仍以本讲后文标注的 qa_core/scripts/ 等路径为准。
上一讲数据隔离与多租户设计
下一讲RAG 回归验收与入库质量

1. 本讲目标

  • 理解离线入库链路和在线问答链路的边界
  • 掌握文档加载、标准化、切分的完整流程
  • 理解 IndexManifest 的增量入库机制
  • 掌握 FAQ 从 CSV 到 Milvus 的完整流程
📖 前置阅读:如果你想深入理解 Parent-Child Chunking 的设计原理和 chunk size 的选择依据,请先阅读 附录G:文档切分策略

2. 前置知识 — 离线 vs 在线链路

2.1 清晰的工程边界

关键原则:在线问答不解析文件、不执行 OCR、不写入知识库。

2.2 为什么分开

如果把文档解析放在在线链路:
  • 用户提问时临时解析 PDF → 首 token 延迟增加 5-10 秒
  • 文件解析失败时用户看到的是”PDF 损坏”而非答案
  • 无法做质量报告(因为解析是即时的,没有机会检查)
如果把向量化放在在线链路:
  • 用户问题需要等 Embedding 模型加载(冷启动 10+ 秒)
  • 无法预热 Embedding 模型

2.3 知识库构建总链路

本项目的离线知识库构建不是单独的“文档切分”或“FAQ 入库”,而是一条完整的版本化构建链路。可以用一句话概括:
离线链路负责把原始资料变成“带版本、带权限、可检索、可回滚”的知识资产;在线链路只读取当前 active 版本来回答问题。
生产或演示时有两个常用入口: 如果在 Docker Compose 里执行入库命令,先确认项目根目录存在 .env.compose。仓库只提交 .env.compose.example,首次部署需要生成本地配置文件:
如果是新环境,或者 Milvus collection schema 变更后需要全量修复,优先使用批量脚本:
它会对 8 个冻结场景逐个执行“新建版本 → 强制入库 → 质量门禁 → 激活”,并在 --reset-collections 开启时删除旧 FAQ/Doc collection,确保 Milvus schema 按当前代码重新创建。 在 Docker Compose 模式下,对应命令是:
如果之前已经存在知识库,只是资料内容变化,批量重建全部 8 个场景时不加 --reset-collections
如果只重建一个场景,使用 scripts/rebuild_kb_version.py
Docker Compose 模式下,对应命令是:
如果 Milvus Collection 的 schema 发生过变化,例如 sparse 字段从普通 SparseVector 改成 BM25 Function 输出字段,需要加上 --reset-collections 删除旧集合并重新建表:
这里要区分两个参数:--force 只是忽略文件指纹、强制把资料重新写入新版本;--reset-collections 会删除 Milvus 里的 FAQ/Doc collection,让当前代码重新创建 schema。已有知识库只更新资料时,用 --force,不要默认加 --reset-collections 完整链路如下: 这条链路里有几个容易混淆的点: 所以第 16 讲后面的 FAQ 入库、文档加载、表格行入库、父子块切分、IndexManifest,都是这条总链路中的局部实现;第 14 讲负责解释版本状态机,第 17 讲负责解释质量门禁和评测。

3. 文档加载器注册表

3.1 注册表设计

3.2 扩展性

新增文件格式只需添加一个注册项:
注册表模式比 if/elif 分支更可维护。当文件类型增长时,if/elif 会变成几百行难以维护的代码。

4. 文档入库主流程

4.1 ingest_directory() 完整流程

_ingest_single_file() 负责单个文件的增量入库逻辑,被 ingest_directory 的循环调用:

4.2 normalize_documents 的作用


5. 表格 CSV / Excel 专用入库设计

5.1 为什么表格不能按普通文本切分

普通制度、流程、手册是一段段自然语言,适合用 Parent-Child Chunking 按章节和字符长度切分。 但 CSV / Excel 表格不是自然段,而是一条条行记录。一行里多个单元格共同表达一个完整业务事实:
如果把表格当普通文本递归切分,可能出现:
  • 检索命中了“施工照片”,但状态被切到另一个 chunk;
  • 检索命中了“金额”,但付款节点、责任人丢失;
  • 两行不同记录被拼到同一个 chunk,答案把 A 行状态说成 B 行状态;
  • 答案引用只能定位到文件,不能定位到工作表和行号。
所以本项目对表格资料的原则是:
一行表格 = 一个完整业务语义单元。

5.2 文件读取策略

表格文件在 Loader 注册表中作为独立类型接入:
读取规则: Excel 会逐个 sheet 处理,避免把多个业务表混成一张表。

5.3 表格清洗

表格入库前先做轻量清洗:
清洗目标不是复杂 ETL,而是保证表格行进入 RAG 时不会因为空行、空列表头、Unnamed 列名造成检索噪声。 单元格值也会转成适合检索的短文本:
这样 1000.0 会变成 1000,金额、编号、数量类问题更容易命中。

5.4 每行转换为 Document

表格 loader 会把每一行转换成一个 LangChain Document
生成后的正文类似:
这样做有两个好处:
  1. 语义完整:同一行的字段和值不会被拆散;
  2. 适合向量检索和 BM25:既有自然语言标签,也有明确的列名和值。

5.5 metadata 设计

表格行必须携带可追溯 metadata:
字段含义:

5.6 表格行不再递归切分

split_documents() 会识别 content_type=table_row
也就是说,表格行不会再进入普通字符切分器。 原因是:表格行已经是完整业务单元,再切一次反而会破坏“列名 -> 单元格值”的关系。

5.7 检索策略中的 prefer_table

表格入库只是第一步。检索时还要识别用户是否在问表格问题。 本项目通过 is_table_query() 判断问题是否包含表格、清单、台账、字段、行号、工作表、状态、金额、责任人等表达:
prefer_table=True 时:
  • 扩大 doc_top_k,多召回一些候选表格行;
  • 扩大 final_context_top_n,给表格证据更多上下文空间;
  • 设置 faq_direct_exact_only=True,禁止相似 FAQ 直接回答;
  • 上下文构建时把表格行排在普通正文前。
为什么要禁用相似 FAQ 直出?

5.8 答案引用和兜底

表格资料的答案必须能回到原始证据。当前项目在来源标签中追加工作表和行号:
另外,表格类问题经常涉及状态、金额、责任人、日期等精确值。LLM 有时会概括回答而漏掉某个关键单元格,所以项目里增加了表格行兜底:
如果模型回答没有覆盖表格行里的核心字段,系统会追加:
这不是替代 LLM,而是对表格精确字段的一层确定性保护。

5.9 面试话术

如果面试官问“Excel 和 CSV 怎么入库”,可以这样回答:
Excel 和 CSV 不能按普通文本切分。我们把每一行转成一个带表头、工作表、行号和单元格键值的 LangChain Document,并写入 content_type=table_row。切分阶段识别到表格行后不会再递归切分;检索阶段如果问题命中表格、清单、台账、金额、状态等关键词,会启用 prefer_table,扩大文档召回并优先保留表格行。答案引用会展示文件、工作表和行号,如果模型漏掉关键单元格,系统会追加表格行要点,保证表格类问题能追溯、能复核、字段不丢。

5.10 表格入库练习

这组练习用于确认“表格读取 → 行级 Document → 检索偏好 → 答案引用”已经闭环。 准备一个最小 CSV:
建议把它放到工程项目资料问答场景的数据目录中,并按常规知识库重建流程入库。学习时重点观察四件事: 可以在页面或接口中提问:
理想回答应该包含:
  • 状态是“待补交”;
  • 责任人是“项目经理”;
  • 引用来源能定位到 CSV/Excel 的对应行;
  • 如果模型遗漏状态或责任人,系统会追加“表格行要点”。
这个练习的目的不是测试模型文采,而是验证表格证据没有在切分和生成阶段丢失。

5.11 当前边界

一期表格入库只覆盖“规范二维表”。复杂 Excel 能力不能无边界扩散,否则会把 RAG 项目变成 Office 解析项目。 面试时可以这样说:
我们一期支持的是规范 CSV/Excel 的行级语义入库,不追求解析所有复杂 Office 特性。这样做是为了保证 RAG 主链路清晰可控:表格行能召回、字段能引用、来源能复核。合并单元格、截图表格、图表解释这类复杂资料会进入后续 OCR/VLM 和资料治理链路,而不是塞进普通表格 loader 里。

6. IndexManifest 增量机制

6.1 为什么需要增量入库

假设知识库有 500 个 PDF 文件,每次修改一个文件就要全部重新入库:
  • 耗时:500 个 PDF 全部解析、切分、向量化 → 可能 20-30 分钟
  • 浪费:499 个未变化的文件被重复处理
  • 风险:重新入库过程中如果出错,旧数据也会被删除
增量入库:只处理变化的文件,未变化的跳过。

6.2 Manifest 文件结构

6.3 核心方法

6.4 文件指纹计算

前置知识:如果你不熟悉 SHA256 哈希和增量检测原理,请先阅读 附录B:SHA256 内容指纹与增量检测
基于路径、修改时间和大小的指纹确保文件元数据变化时被检测到,比全文 SHA256 更快且适合大文件场景(极端情况下内容变化但 mtime/size 不变时使用 —force 重建)。

7. FAQ 入库流程

7.1 CSV 格式

FAQ 使用 CSV 文件管理,每行一个问答对:

7.2 入库实现

存储策略
  • page_content = FAQ 标准问题 → 用于向量检索
  • metadata.answer = 标准答案 → 检索命中后直接取 metadata 返回
  • metadata.source = 当前场景 valid_sources 中的标准分类 → 用于 Milvus 过滤和数据隔离
这样 FAQ 直出时不需要再调用 LLM,直接从 metadata 读取答案即可。 normalize_faq_source() 只依赖当前场景包的 valid_sourcessource_patterns。如果 CSV 中的分类无法映射到当前场景,系统会直接报错,而不是偷偷写入 Milvus。这样可以保证 FAQ 入库的业务边界和场景配置一致。

8. 清理与维护

8.1 清理已删除的本地文件

当本地文档被删除时,Milvus 中的旧 chunk 不会自动消失。需要运行清理脚本:

8.2 cleanup_missing_document_chunks 原理

默认 dry-run:先预览再执行,防止误删。

9. 复杂图文资料入库治理

9.1 这属于多模态吗

导入文档中同时存在文字、图片、截图、扫描页、流程图、设备照片时,本质上已经进入了多模态资料处理范围。 但在当前一期项目里,它应该被定位为:
多模态入库治理,不是多模态在线问答。
两者区别如下: 这样设计的原因是:在线问答必须稳定、低延迟、可追踪;图片解析、OCR、VLM 描述成本高且失败率高,如果直接塞进在线链路,会让 RAG 主流程变慢、变重、变不可控。

9.2 为什么不能“图片 OCR 一下就入库”

真实企业资料中的图片经常包含:
  • 合同扫描件;
  • 审批截图;
  • 设备告警截图;
  • 流程图;
  • 验收照片;
  • 表格截图;
  • 盖章文件;
  • 票据和单证照片。
这些内容的风险不只是“能不能识别出文字”,而是: 所以复杂图文资料不能简单走“OCR -> 普通文本切分 -> 入库”。正确流程应该是:

9.3 三类资料的处理策略

当前项目已有离线 OCR 脚本:
第一条命令只生成待复核资料,第二条命令才把复核后的 Markdown 提升到场景资料目录。提升后仍然要执行知识库版本重建、入库质量检查和RAG 回归验收。

9.4 image_text_block 推荐结构

对于图片和正文强相关的资料,不应该把 OCR 文本当成普通段落直接切分,而应该生成专门的图文块:
关键字段说明:

9.5 分块策略

图文混排资料的切分原则是:
  1. 正文按章节或父子块切分:继续复用当前 split_documents() 的 Parent-Child Chunking。
  2. 表格按行切分:CSV/Excel 仍然使用 content_type=table_row,不参与普通递归切分。
  3. 图片 OCR 文本不单独裸切:必须绑定页码、图片编号和附近正文。
  4. 未复核图文块不进 active:只能作为候选资料进入复核区或治理报告。
  5. 低置信度图文块阻断激活:避免把错误金额、日期、合同号写入正式知识库。
也就是说,图文资料的最小语义单元不是“识别出的一行字”,而是:

9.6 检索策略

图文块进入知识库后,也不应该和普通正文完全同权。 推荐策略:
  • 普通知识问题:优先使用文本 chunk 和表格 chunk。
  • 用户问题包含“图片、截图、扫描件、照片、图中、流程图、告警面板”等表达时,提高 image_text_block 权重。
  • 如果命中的图文块 review_status != reviewed,回答必须标记“未确认”,不能把它当成正式证据。
  • 来源展示必须包含文件名、页码和图片编号。
这样既能让图文资料参与 RAG,又不会让未确认图片内容污染正式答案。

9.7 面试话术

如果面试官问“你们项目支持多模态吗”,可以这样回答:
我们一期没有做实时多模态对话,而是把多模态能力收敛在知识库入库治理侧。图片、扫描件、图文 PDF 会先通过离线 OCR 或 VLM 生成可复核文本,再绑定页码、图片编号、附近正文和置信度。只有人工复核通过的图文块才会以 image_text_block 形式进入知识库,并继续经过入库质量检查、版本激活和回归评测。这样既能处理企业资料中的多模态信息,又不会让在线问答链路变重、变慢、变不稳定。
这段内容的重点不是展示 OCR 接入,而是理解企业 RAG 中多模态资料必须经过治理、复核、入库质量检查和版本化上线。

10. data_packs 与企业资料增强包

项目里除了 scenarios/,还有一个 data_packs/ 目录。它们不是同一种东西,学习时要先区分清楚。

10.1 scenarios 是主链路数据源

scenarios/ 是当前 8 个冻结业务场景的正式资料目录。执行下面命令时,默认读取的就是 scenarios/
单场景重建也是一样:
所以第一遍跑通主链路时只需要关心 scenarios/,它保证主链路足够稳定、可控、可复现。

10.2 clean_overlay 是增强候选,不自动入库

data_packs/enterprise_realistic_pack/clean_overlay/ 用来模拟更真实的企业资料,例如:
  • 区域差异和例外规则;
  • 角色权限和金额阈值;
  • 审批链和补签流程;
  • 合同付款风险;
  • 跨境单证金额变更;
  • 理赔材料不一致;
  • SaaS 企业客户账单和集成问题。
它不是 active 知识库的一部分,也不会被 rebuild_scenarios.py 自动读取。这样设计是为了避免“增强资料还没治理完,就污染正式知识库”。 clean overlay 的正确流程是:
常用命令:

10.3 dirty_samples 只用于治理演示

data_packs/enterprise_realistic_pack/dirty_samples/ 不能直接入库。它里面放的是用来讲资料治理风险的样本,例如: dirty samples 的正确流向是:
对应分析命令:

10.4 三者关系总结

面试话术:
我们没有把所有资料都直接塞进 active 知识库。scenarios/ 是当前正式数据源,保证主链路稳定;clean_overlay 是企业仿真增强候选,需要先通过预检、就绪检查和回归评测;dirty_samples 只用于演示真实企业资料治理问题,不能直接入库。这样既能保持教学主链路可控,又能讲清企业资料从脏数据到可上线知识资产的治理过程。

11. 入库失败排查手册

入库链路牵涉 MySQL、Milvus、Embedding、Reranker、场景配置、质量门禁和版本激活。排查时不要直接猜原因,按下面顺序查,速度最快。

11.1 先确认当前 active 版本

页面提示“信息不足”、检索结果为空、或者刚重建后仍然回答旧内容时,先查 active 版本:
判断:

11.2 再确认 Milvus collection 是否存在且有数据

如果 collection 不存在,说明入库没有真正写到 Milvus;如果 collection 存在但实体数量很少或为 0,需要回看入库日志。 常见原因:

11.3 schema 不兼容时使用 reset-collections

如果日志出现:
通常表示复用了旧 schema collection。处理方式是删除旧 collection 并重建:
排查口径:
只手动删除 collection 不会自动生成新知识库版本。必须重新跑入库脚本,让脚本重新创建 collection、写入 FAQ/Doc、生成质量报告并激活版本。

11.4 质量门禁失败先看报告,不要直接跳过

--quality-gate 失败时,说明资料里可能存在空文件、重复 FAQ、source 无效、FAQ/正文冲突或低质量 chunk。 排查顺序:
不要为了让命令通过就直接去掉 --quality-gate。这会让低质量资料进入 active 知识库,后面在线问答会变成“能检索,但答得不可靠”。

11.5 重建后页面还是旧答案

按这个顺序检查:
  1. 页面右侧当前状态里的知识库版本是否变成新版本。
  2. .env.composeACTIVE_SCENARIO_ID 是否是你刚重建的场景。
  3. API 容器是否重新加载了 .env.compose
  1. 是否有多个 Milvus 实例:宿主机脚本连的是 127.0.0.1:19530,容器内脚本连的是 http://milvus:19530。要确认两者指向同一个 Docker Compose 服务。

11.6 八场景全量初始化的推荐命令

如果你希望在新环境中一次性把全部 8 个场景初始化到可演示状态,使用:
如果之前已经存在知识库,只是资料内容变化,重建全部 8 个场景时不要删除 collection:
如果容器镜像里还没有最新脚本,执行 docker compose --env-file .env.compose build api 后再运行入库命令。

12. 本讲实践闭环

通过标准:资料能从文件进入可检索知识库,metadata、版本、隔离字段完整,失败可按排查手册定位。

12.1 本讲从 0 到 1 实现闭环

这一讲是离线链路的完整交付:把文件、FAQ、表格资料变成线上可检索的数据。实现顺序如下:
  1. 先实现文件 Loader 注册表,让不同后缀走不同解析器。
  2. 再实现标准化,把 source、scenario、kb_version、DataScope 写入 metadata。
  3. 然后实现 chunking,把长文档切成适合检索的片段。
  4. FAQ 单独入库:问题作为检索文本,标准答案放 metadata。
  5. 最后由 ingest_directory() 串起加载、标准化、切分、写入 Milvus、更新 Manifest。
实现完成后,相关代码结构应该是下面这张图: 来源:真实代码调用点,见 qa_core/indexing/document_loaders.py
标准化阶段是离线链路和在线链路的接口。在线检索依赖的过滤字段,必须在这里写全。 来源:真实代码调用点,见 qa_core/indexing/document_normalizer.py
文档入库主流程要能重复执行。没变化的文件通过 Manifest 跳过;文件变化、Embedding 版本变化或 chunk schema 变化时,要先删除旧 chunk,再重新解析和写入。 来源:真实代码逻辑压缩版,对应 qa_core/indexing/service.py::ingest_directory()_ingest_single_file()
FAQ 入库不能把“答案”也作为主要检索文本,否则标准答案过长时会稀释问题语义。本项目让问题参与检索,答案作为 metadata 被命中后直出。 来源:真实代码调用点,见 qa_core/indexing/faq_ingestion.py
单场景验收用 rebuild_kb_version.py,全量初始化 8 个场景用 rebuild_scenarios.py 来源:命令行验收,对应 scripts/rebuild_kb_version.pyscripts/rebuild_scenarios.py
已有知识库只刷新资料内容时,使用:
闭环验证重点: 验收重点:文件能进入 Milvus,active 版本能更新,metadata、版本、隔离字段完整,失败时能用排查手册定位。

13. 重点掌握

14. 本讲小结

  • 离线入库 ≠ 在线问答:入库负责解析文件、切分、向量化、写入 Milvus;问答只做检索和生成
  • 注册表模式管理文件格式→Loader 的映射,扩展新格式只需添加注册项
  • 表格 CSV/Excel 按行入库:每行是一个 table_row,保留表头、工作表、行号和单元格键值,不再递归切分
  • IndexManifest 记录每个文件的指纹和 chunk ID,实现增量入库(只处理变化的文件)
  • FAQ 入库将标准问题作为检索内容、标准答案存储在 metadata 中,检索命中后直接返回
  • 复杂图文资料属于多模态入库治理:OCR/VLM 结果必须绑定上下文、人工复核、入库质量检查和版本激活后才能进入 active 知识库
  • data_packs 不是默认数据源clean_overlay 是增强候选,dirty_samples 是治理演示样本,默认 8 场景初始化只读取 scenarios/
  • 清理脚本默认 dry-run,先预览再执行,防止误删
下一讲RAG 回归验收与入库质量 — 入库质量报告、评测指标、回归验收体系、Bad Case 闭环