2026年7月7日/ AI Engineering Notes
企业知识库文档入库流程:解析、清洗、切片、向量化和索引状态
企业知识库 RAG 的效果,很大一部分在文档入库阶段就决定了。本文整理文档入库流程怎么设计:文件接收、hash 去重、解析、清洗、切片、元数据、embedding、索引状态、失败重试和回滚。
这篇是《如何搭建企业知识库 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 | 合同、审计、合规文档 |
第一版可以先做覆盖版本,但要保留 version 或 content_hash,否则以后无法解释“为什么昨天还能搜到,今天搜不到”。
文档解析
解析的目标不是拿到一大段纯文本,而是尽量保留结构。
至少要拿到:
- 标题
- 段落
- 列表
- 表格
- 页码
- 章节层级
- 图片或附件说明
不同文件格式要分开处理:
| 格式 | 重点 |
|---|---|
| Markdown / HTML | 保留标题层级、代码块、表格 |
| Word | 保留标题、列表、表格和页内顺序 |
| 区分文本 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 太多。
最小实现顺序
第一版不用一次做完所有格式。
建议按这个顺序:
- 先支持 Markdown、TXT、HTML。
- 建 document / chunk / embedding / index_job 表。
- 做异步入库任务和状态流转。
- 保存解析后的 elements 中间产物。
- 做结构化切片,保留 section_path。
- 生成 embedding,写向量索引。
- 同时写全文索引。
- 增加失败重试和错误码。
- 增加入库报告。
- 再支持 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.