2026年7月7日/ AI Engineering Notes

企业知识库文档入库流程:解析、清洗、切片、向量化和索引状态

企业知识库 RAG 的效果,很大一部分在文档入库阶段就决定了。本文整理文档入库流程怎么设计:文件接收、hash 去重、解析、清洗、切片、元数据、embedding、索引状态、失败重试和回滚。

AIRAG知识库Document IngestionEngineering Practice

这篇是《如何搭建企业知识库 RAG 系统》里“文档入库流程”部分的展开。

企业知识库 RAG 不要把“上传文件”当成一个简单接口。文件能传上来,不代表知识已经能被正确检索。

真正的入库链路应该是:

接收文件
-> 计算 hash
-> 解析结构
-> 清洗正文
-> 抽取元数据
-> 切片
-> 生成 embedding
-> 写入向量索引和全文索引
-> 更新索引状态
-> 记录失败原因

这条链路出问题,后面的检索、rerank、prompt 都会被拖下水。

入库不要同步做完

第一版就建议把文档入库做成异步任务。

上传接口只负责三件事:

  • 保存原始文件。
  • 创建 document 记录。
  • 投递 indexing job。

不要在上传接口里同步解析 PDF、切片、调 embedding 模型、写向量库。原因很现实:PDF 可能很慢,OCR 可能失败,embedding 可能超时,向量库也可能暂时不可用。

推荐状态流:

UPLOADED
-> PARSING
-> PARSED
-> CHUNKING
-> EMBEDDING
-> INDEXING
-> INDEXED

失败状态也要拆开:

PARSE_FAILED
CHUNK_FAILED
EMBEDDING_FAILED
INDEX_FAILED

只写一个 FAILED 后面会很难排查。运营或管理员只知道“失败了”,不知道该换文件、重试任务,还是等模型服务恢复。

后台看板可以先做得很朴素,但字段要够用:

文档 来源 当前状态 chunk 数 失败原因 操作
Redis 运维手册.pdf upload INDEXED 128 - 查看切片
客服 FAQ.docx upload EMBEDDING_FAILED 64 embedding timeout 重试 embedding
制度汇编.pdf upload PARSE_FAILED 0 scanned pdf without OCR 重新上传 / 开 OCR

这张表的目的不是好看,而是让人一眼知道失败卡在哪一步。

document 表怎么存

document 不要只存标题和正文。它是整个入库任务的主状态。

id
title
source_type
source_uri
file_type
file_size
content_hash
parser_version
chunk_strategy
embedding_model
index_status
error_code
error_message
created_by
created_at
updated_at
indexed_at

几个字段很关键:

字段 作用
content_hash 判断文件内容是否变化,避免重复索引
parser_version 解析器升级后知道哪些文档需要重跑
chunk_strategy 切片策略变更后可以对比效果
embedding_model 模型变更后判断是否要重建向量
index_status 给前端和任务系统显示当前进度
error_code 失败重试和人工处理的依据

source_type 可以先简单区分:

upload
url
confluence
notion
git
database

第一版只做 upload 也没问题,但字段先留出来,后续接企业系统会轻松很多。

投递给任务队列的消息也要小而明确:

{
  "job_id": "job_20260708_001",
  "document_id": "doc_001",
  "source_uri": "s3://kb-upload/redis-ops.pdf",
  "content_hash": "sha256:9f2c...",
  "parser_version": "pdf-parser-v1",
  "chunk_strategy": "section-500-overlap-100",
  "embedding_model": "text-embedding-3-large",
  "requested_by": "user_001"
}

不要把文件内容塞进消息体。队列里只传定位信息和处理参数,原始文件放对象存储或文件服务。

hash 去重和版本

文档入库第一步先算 content_hash

同一个来源 + 同一个 hash -> 不重复入库
同一个来源 + 新 hash -> 新版本或重新索引

不要只按文件名判断。企业里经常出现这种情况:

Redis运维手册.pdf
Redis运维手册-最新版.pdf
Redis运维手册-最终版.pdf
Redis运维手册-最终版2.pdf

文件名没有意义,hash 才能判断内容有没有变。

版本策略有两种:

策略 做法 适合场景
覆盖版本 同一个 document id 下更新 chunk 和索引 内部手册、制度文档
保留版本 每次更新创建新 version 合同、审计、合规文档

第一版可以先做覆盖版本,但要保留 versioncontent_hash,否则以后无法解释“为什么昨天还能搜到,今天搜不到”。

文档解析

解析的目标不是拿到一大段纯文本,而是尽量保留结构。

至少要拿到:

  • 标题
  • 段落
  • 列表
  • 表格
  • 页码
  • 章节层级
  • 图片或附件说明

不同文件格式要分开处理:

格式 重点
Markdown / HTML 保留标题层级、代码块、表格
Word 保留标题、列表、表格和页内顺序
PDF 区分文本 PDF 和扫描 PDF,必要时 OCR
Excel 不要直接转成长文本,要按 sheet 和表头组织
PPT 保留页标题、正文、备注和页码

PDF 是最容易出问题的格式。常见坑:

  • 页眉页脚混进正文。
  • 多栏排版顺序错乱。
  • 表格被拆散。
  • 扫描件没有文字层。
  • 页码和段落位置丢失。

所以解析结果最好先落一份中间产物,而不是直接切片。

{
  "document_id": "doc_001",
  "elements": [
    {
      "type": "title",
      "text": "Redis 连接超时排查",
      "page": 3,
      "level": 2
    },
    {
      "type": "paragraph",
      "text": "先检查连接地址、端口和网络策略。",
      "page": 3
    },
    {
      "type": "table",
      "text": "配置项 | 默认值 | 说明 ...",
      "page": 4
    }
  ]
}

有了中间结构,后续切片、引用、重跑任务都会更稳。

解析完成后,可以先抽样看 3 类材料:

抽样材料 看什么
原始页截图 / 原始段落 判断解析顺序有没有乱
parser output JSON 判断标题、表格、页码有没有保住
chunk preview 判断切片后还能不能独立读懂

如果这三类材料都不看,后面发现召回差,很容易误判成 embedding 或模型问题。

清洗规则

清洗不是把文本变短,而是去掉会污染检索的东西。

常见清洗项:

  • 重复页眉页脚
  • 目录页
  • 水印
  • 空行和乱码
  • 重复版权说明
  • 无意义页码
  • PDF 换行导致的断句
  • OCR 识别出来的孤立字符

但不要过度清洗。下面这些内容经常是有用信息:

  • 错误码
  • 配置项
  • 接口路径
  • 命令行
  • 版本号
  • 表格表头
  • 章节编号

错误清洗例子:

原文:redis.connect.timeout=3000
错误清洗后:redis connect timeout

对 RAG 来说,配置项里的点号、下划线、等号都有检索价值。不要为了“看起来像自然语言”把它们清掉。

元数据抽取

每个 chunk 都要带元数据。否则后面做权限、过滤、引用、排查都难。

建议至少保留:

document_id
document_title
source_type
source_uri
version
section_path
page_number
chunk_index
content_hash
visibility
department_id
created_at
updated_at

section_path 是最容易被低估的字段。

运维手册 / Redis / 连接超时排查 / 连接池配置

它有三个作用:

  • 检索时补充上下文。
  • 引用时让用户知道答案来自哪里。
  • 排查时判断 chunk 是不是被切断了。

如果只存 chunk 正文,回答引用就会变成“参考资料:某某 PDF”,用户无法验证。

切片策略

切片的目标不是把文本平均切开,而是让每个 chunk 都能独立支撑一个检索命中。

建议顺序:

先按文档结构切
再按段落切
最后才按 token 或字符长度兜底

一个可用的默认策略:

按标题层级分组
每个 chunk 目标长度 500 到 800 中文字
保留 80 到 120 字 overlap
列表和步骤尽量不拆
表格保留表头
每个 chunk 前附加文档标题和 section_path

不要迷信固定 chunk size。不同资料差异很大:

文档类型 切片重点
FAQ 一问一答尽量放同一个 chunk
运维手册 一个故障场景或一个步骤组一块
接口文档 一个接口路径、参数、返回值一块
制度文档 一个条款或一组相关条款一块
表格 保留表头,必要时按行组切

坏切片通常长这样:

chunk_01: Redis 连接超时排查包括以下步骤:1. 检查地址
chunk_02: 和端口。2. 检查连接池配置。3. 检查网络策略。

第一块语义不完整,第二块缺标题。检索命中任何一块都不舒服。

更好的切片:

title: Redis 运维手册
section: 连接超时排查
content:
Redis 连接超时排查包括:
1. 检查连接地址和端口。
2. 检查连接池超时配置。
3. 检查服务端最大连接数。
4. 检查网络和防火墙策略。

表格怎么处理

企业知识库里,表格很重要。

常见表格内容:

  • 配置项
  • 错误码
  • 权限矩阵
  • 参数说明
  • 产品规格
  • 流程状态

不要把表格直接拍平成一段没有表头的文本。

较好的 chunk 格式:

section: Redis 参数说明
table: 连接池配置

配置项: max-active
默认值: 8
说明: 最大活跃连接数

配置项: max-wait
默认值: -1
说明: 获取连接最大等待时间

如果表格很大,不要一个 chunk 塞完整张表。可以按行组切,但每个 chunk 都重复表头和表格标题。

表格标题 + 表头 + 第 1 到 20 行
表格标题 + 表头 + 第 21 到 40 行

这样召回到任意一块时,模型都知道每一列是什么意思。

embedding 写入

生成 embedding 时要保存模型和版本。

chunk_id
embedding_model
embedding_dimension
embedding_vector
embedding_version
created_at

不要只存向量。

换 embedding 模型后,老向量和新向量不能混着用。它们不在同一个语义空间里。正确做法是:

新模型 -> 新 embedding_version -> 重建索引 -> 跑评测 -> 切流量

如果需要平滑迁移,可以保留两套向量:

embedding_v1: old_model
embedding_v2: new_model

检索时按版本选择索引,评测通过后再切默认版本。

全文索引也要入库

企业知识库不要只建向量索引。

这些内容更适合全文检索:

  • 错误码
  • 配置项
  • 类名
  • 方法名
  • 接口路径
  • 编号
  • 产品型号
  • 专有名词

入库时最好同时写两类索引:

vector_index: 语义召回
fulltext_index: 精确匹配

后面检索时做 hybrid search,再 rerank。

如果第一版只做向量检索,至少也要在数据模型里留出全文索引位置。否则后面加 hybrid search 会改动很大。

索引状态和回滚

索引不要边删边写。

错误做法:

删除旧 chunk
开始解析新文件
生成新 chunk
写入新索引

如果中途失败,线上知识会直接缺一块。

更稳的做法:

保留旧版本可用
新版本写入 staging
新版本全部索引成功
切换 active_version
延迟清理旧版本

状态可以这样设计:

document.active_version = v1
index_job.target_version = v2
v2 全部成功后 active_version = v2

这样即使 v2 解析失败,v1 仍然可检索。

失败重试

入库任务一定要能重试,但不是所有失败都应该无限重试。

失败类型 是否自动重试 处理方式
模型接口超时 指数退避重试
向量库连接失败 重试并告警
PDF 解析失败 标记失败,提示换文件或走 OCR
文件损坏 标记失败,提示重新上传
权限不足 检查同步账号权限
chunk 过大 调整切片策略后重跑

重试任务要幂等。

同一个 job_id 重跑时,不能生成重复 chunk,也不能写出两份 active 索引。

可以用这几个键控制幂等:

document_id
content_hash
parser_version
chunk_strategy
embedding_version

这几个值都一样,说明是同一次索引逻辑,重复执行应该得到同一批 chunk 和向量。

入库报告

每次入库完成后,建议生成一份简单报告。

document_id: doc_001
status: INDEXED
file_type: pdf
pages: 18
elements: 246
chunks: 42
embedding_model: text-embedding-xxx
embedding_version: v3
parser_version: pdf_parser_v2
chunk_strategy: heading_recursive_600
duration: 46s
warnings:
  - page 7 table extraction fallback to plain text
  - page 12 OCR confidence low

这份报告很有用。用户说“这个文档搜不到”时,你可以先看:

  • 有没有解析成功。
  • chunk 数量是否异常。
  • 表格有没有降级。
  • embedding 有没有写入。
  • active version 是否切换。

没有报告,就只能翻日志。

如果要给管理员保存结构化报告,可以像这样:

{
  "document_id": "doc_001",
  "title": "Redis 运维手册.pdf",
  "status": "INDEXED",
  "parser_version": "pdf-parser-v1",
  "chunk_strategy": "section-500-overlap-100",
  "embedding_model": "text-embedding-3-large",
  "pages": 18,
  "elements": 246,
  "chunks": 73,
  "tables": 6,
  "warnings": [
    "page 12 table converted to markdown",
    "page 17 footer removed as repeated text"
  ],
  "duration_ms": {
    "parse": 1820,
    "chunk": 116,
    "embedding": 4380,
    "index": 920
  }
}

之后检索效果不好,可以先回头看是不是 chunk 数异常、表格丢失、解析耗时过长或 warning 太多。

最小实现顺序

第一版不用一次做完所有格式。

建议按这个顺序:

  1. 先支持 Markdown、TXT、HTML。
  2. 建 document / chunk / embedding / index_job 表。
  3. 做异步入库任务和状态流转。
  4. 保存解析后的 elements 中间产物。
  5. 做结构化切片,保留 section_path。
  6. 生成 embedding,写向量索引。
  7. 同时写全文索引。
  8. 增加失败重试和错误码。
  9. 增加入库报告。
  10. 再支持 Word、PDF、Excel、PPT。

先从简单格式开始,是为了把主链路跑稳。PDF 可以晚一点做,因为 PDF 的解析复杂度很容易把第一版拖乱。

上线前检查

入库链路上线前至少检查这些问题:

  • 同一个文件重复上传会不会重复建索引。
  • 文档更新后旧版本是否还能被错误召回。
  • 新版本索引失败时旧版本是否仍可用。
  • 删除文档后 chunk、向量、全文索引是否同步失效。
  • 表格是否保留表头。
  • chunk 是否带 document title 和 section_path。
  • embedding 模型变更后是否能重建索引。
  • 大文件是否会阻塞上传接口。
  • 失败任务是否能重试。
  • 管理端是否能看到失败原因。

结论

RAG 的入库流程不是“文件转文本再切块”。

最小闭环应该是:

原始文件可追溯
解析结构可检查
chunk 可复现
embedding 有版本
索引状态可回滚
失败原因可重试

这一步做好,后面的检索和评测才有稳定基础。否则系统看起来能回答,但一换文档、一改切片、一升级 embedding,就很容易变成不可解释的黑盒。

参考资料

Brief

RAG quality is often decided before retrieval starts, in the ingestion pipeline.

A production ingestion flow needs parsing, cleaning, structure-aware chunking, metadata, embedding, index status and retry design.

Do not treat document upload as a single synchronous API call.