22-RAG文档处理与异步入库
企业文档怎样变成可检索知识?解析、切分与异步入库
上传 PDF 后能检索到内容,却找不到表格里的例外;导入超时后重新上传,又出现重复片段。文档处理需要保留语义、版本与任务状态,而不只是调用一次 Embedding。
Spring AI 接入片段使用 Java 17、Spring Boot 3.5.3 与 Spring AI 1.1.8;协议与第三方平台示例另行标明适用范围。
1. 入库与问答是两条独立链路
原文、片段与引用应有稳定标识。
入库与问答是两条链
入库:原文件 → 解析 → 清洗 → 切分 → 元数据 → Embedding → 索引
问答:身份与问题 → 查询处理 → 已授权检索 → 选证据 → 生成 → 引用检查
Embedding 将文本转换为向量;向量检索寻找表示接近的片段;生成阶段读取片段并组织回答。入库时的文档版本、权限、页码和切分位置,应一直保留到最终引用。
原始 RAG 研究说明了将检索与生成结合的思路,实际应用还需要自己的文档处理、权限与评估体系。RAG 论文
先保留可追踪的 Document
每个片段需要稳定 ID、原文内容和元数据,至少关联 docId、revision、chunkIndex、来源及定位。解析 PDF 时的页码与原文件页码要定义清楚,不能把数组下标直接当页面编号。
只有向量而没有原文,模型就没有可阅读证据;只有原文而没有版本,引用就可能指向后来已修改的内容。
Spring AI 的 ETL 使用 DocumentReader、DocumentTransformer 和 DocumentWriter 组织处理,但文件解析成功不等于内容完整。Spring AI 1.1.8 ETL
一个公开资料的最小检索示例
沿用Java 服务变慢怎样定位?从 CPU、GC 到内存保留链 Spring AI 1.1.8 项目,增加以下受 BOM 管理的依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-vector-store</artifactId>
</dependency>
给 OpenAI 适配的 Embedding 配置可用模型:spring.ai.openai.embedding.options.model=${AI_EMBEDDING_MODEL}。聊天模型与 Embedding 模型不是同一个用途,应分别确定权限和能力。
demo.RagConfig 注册内存向量库和问答客户端:
package demo;
import java.util.List;
import java.util.Map;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RagConfig {
@Bean
VectorStore vectorStore(EmbeddingModel embeddings) {
return SimpleVectorStore.builder(embeddings).build();
}
@Bean
ChatClient ragClient(ChatClient.Builder builder) {
return builder.build();
}
@Bean
CommandLineRunner load(VectorStore store) {
return args -> store.add(List.of(
new Document("guide-v1-0", "演示产品支持七天内申请退货,但已激活的数字商品除外。",
Map.of("docId", "guide", "revision", "v1", "source", "演示手册第1节")),
new Document("guide-v1-1", "演示产品的退货申请需要提供订单编号和未使用证明。",
Map.of("docId", "guide", "revision", "v1", "source", "演示手册第2节"))));
}
}
这两段是虚构产品规则,用于机制说明,不是实际退货建议。入库会调用 Embedding 服务,查询也会生成查询向量;本批没有实际执行。
SimpleVectorStore 用于演示,未配置保存时重启会重新加载,不能据此宣称已经实现生产持久化。对应实现
2. 切分首先保护上下文完整性
长度只是约束之一,规则、表格和来源位置同样重要。
Chunk 是检索与引用的基本单位
太小的片段容易丢主语、条件和标题;太大的片段包含多个主题,检索表示可能不够集中,生成时也增加 Token。
| 内容 | 更需要保留的结构 |
|---|---|
| 业务制度 | 标题、条件、例外和生效范围 |
| API 文档 | 接口名、参数、返回与错误码 |
| 表格 | 表头、单位和行列关系 |
| 代码 | 类或方法边界、依赖和注释 |
| 长 FAQ | 问题与对应答案 |
不应直接给所有文档套一个固定字符数。可以先按标题、段落或语义单元划分,再对过长部分继续切分。
字符、Token 和语义边界不同
字符长度便于实现,但不同语言与 tokenizer 的 Token 数不一样。Spring AI TokenTextSplitter 使用特定分词规则,也不意味着它自动识别所有业务条件、表格和代码语义。Spring AI ETL 与切分
模型上下文预算还包括 system、问题、历史、工具定义和输出预留。只让每个 Chunk 不超限,拼接十个 Chunk 后仍可能超过总预算。
中文文本按 Java String 下标切分还涉及 UTF-16,可能在扩展字符的代理对中间切断。演示算法应说明计数单位。
重叠能解决所有上下文问题吗?
重叠帮助边界附近的信息共同出现,但如果例外距离很远,增加少量重叠也无济于事。重叠过大还会重复索引、增加 Embedding 费用和检索冗余。
可以把章节标题作为检索文本前缀,为子片段补充父级主题;也可以用小片段检索、再按权限与版本取回邻近内容或父段落。这些方法要分别管理检索单元与送入模型的证据单元。
检索阶段去重时,不要把只在例外条件上不同的条款误合并。内容相似不等于业务等价。
表格和 OCR 为什么需要单独检查?
OCR 可能把“不超过”识别成“超过”,表格解析可能把金额与项目错配。切分算法无法修复原文已经错误的事实。
先抽查提取结果,再决定表格转为结构化行、Markdown 或带表头的独立片段。对每个片段保留原页与区域,便于回到原文件核对。
目录、页眉和页脚若重复出现在很多 Chunk 中,也可能干扰向量表示,应清理但保留真实标题信息。
切分方案怎样比较?
固定同一文档版本、Embedding 模型、问题集和检索参数,分别比较小片段、较大片段、结构切分,以及不同重叠。观察正确证据是否被命中、例外是否保留、重复率、Token 和引用准确性。
问题集应包含跨段条件、表格、短事实、长解释和不存在问题。不能只选择某一种切分特别擅长的样本。本文提供比较方法,没有实际测量哪一组最优。
若同时改变 Chunk 大小、模型和 topK,即使答案变好,也无法确认是谁起了作用。每次比较尽量隔离一个主要变量。
3. 异步任务怎样支持恢复
任务、租约、阶段与版本发布分开,让半完成内容不进入正式检索。
上传接口先提交任务
上传先校验用户权限、类型、大小和文件内容,再保存原始文件及文档版本。数据库提交导入任务后返回 202 Accepted 与任务编号,表示已接受处理,不表示已完成索引。
原始文件存储和数据库不在一个事务里时,需要清理未被引用的上传对象,或按暂存、确认方式管理。不能假设两个系统一起成功。
任务绑定具体文档版本、解析配置和 Embedding 配置。稍后切换模型,不应让同一任务前半段用旧模型、后半段用新模型。
任务表需要哪些字段?
以下为 MySQL 8.0 结构示例:
CREATE TABLE import_task (
id BIGINT NOT NULL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
document_version_id BIGINT NOT NULL,
pipeline_revision VARCHAR(40) NOT NULL,
status VARCHAR(20) NOT NULL,
stage VARCHAR(20) NOT NULL,
attempts INT NOT NULL DEFAULT 0,
max_attempts INT NOT NULL,
next_run_at DATETIME(6) NOT NULL,
lease_until DATETIME(6) NULL,
claim_version BIGINT NOT NULL DEFAULT 0,
worker_id VARCHAR(64) NULL,
error_code VARCHAR(64) NULL,
updated_at DATETIME(6) NOT NULL,
UNIQUE KEY uk_version_pipeline (tenant_id, document_version_id, pipeline_revision),
KEY idx_pick (status, next_run_at)
);
同一文档版本与同一 Pipeline 的重复请求复用任务;更改解析或 Embedding 方案时生成新的 Pipeline 修订。内容摘要用于发现重复内容,但不能跨租户暴露“别人已经上传过这份文件”。
领取任务不能只先查再改
多个 Worker 会竞争任务。可以使用短事务锁定可领取行,配合 SKIP LOCKED 等能力领取队列式工作,再更新状态、租约、Worker 和 claim_version。MySQL 锁定读。
事务提交后才进行耗时解析与模型请求,不长时间持有数据库锁。Worker 定期续租,并在写入进度时核对 claim_version,避免旧执行者覆盖新执行者的状态。
租约失效允许恢复者接管,但不证明旧 Worker 已经停止。对向量写入使用稳定片段 ID 和幂等 upsert;即使重复执行,最终不会累积多份相同片段。
阶段与发布如何分离?
处理阶段可以是 PARSE、SPLIT、EMBED、INDEX、VALIDATE,任务终态为成功、失败或取消。进度表示已完成的工作,不用没有依据的百分比伪装精确。
向量记录包含租户、文档版本、片段编号和 Pipeline 修订。先写入暂存版本,完成校验后切换业务元数据中的有效版本,再清理旧索引。
保存原文 → 解析结果 → 切分结果 → 分批向量化 → 索引校验
↓
发布有效版本指针
批次记录可以支持从已完成阶段恢复,但缓存结果必须与内容和配置版本一致。旧切分不能在改了规则后继续复用。
重试、取消与删除怎样配合?
网络超时、临时限流适合受控重试,采用退避、随机抖动和整体尝试上限。格式不支持、文件损坏或权限已撤销应停止,并给出可修改原因。
用户删除文档时先撤销可见状态,再取消正在运行的任务。Worker 在发布之前重新检查文档是否仍可发布,防止一个迟到任务把已删除内容重新恢复可见。
取消可以停止后续批次,但已经产生的费用和远端写入不一定撤销。记录残留资源并安排清理,不能只将状态改为 CANCELLED 就认为全部完成。
4. 设计判断与适用边界
先检查解析结果,再调整 Chunk 大小。错序表格与遗漏文本在向量阶段不会自动修复。保留标题路径、原文位置与版本,必要时为片段增加来自原文的上下文说明,并通过样本检验它是否真的帮助召回。
异步导入使用稳定任务与片段标识,按固定 Pipeline 修订执行。重试可以重复阶段,但不能累积重复片段或让已删除文档重新发布。向量化费用、解析资源和发布状态都需要观测,不以 HTTP 上传成功代表知识已经可用。
Contextual Retrieval 讨论为片段补充文档上下文。是否适用仍要通过本项目样本比较,本文没有移用其效果指标。 Anthropic:Introducing Contextual Retrieval。
5. 问答
问:RAG 是否让模型学会了整个知识库?
答:通常是每次检索后提供选中片段,不等于训练模型记住全部内容。
问:有来源列表就证明回答正确吗?
答:还需检查具体结论是否由引用支持,来源本身也要合法有效。
问:Chunk 越小,检索一定越准确吗?
答:主题更集中,但可能丢条件、主语与例外,要看完整证据覆盖。
问:加重叠就能保证条款完整吗?
答:只能改善边界问题,远距离依赖仍需结构或父子片段方案。
问:202 表示文档可检索了吗?
答:只表示任务被接受,完成与发布状态需要另查。
问:有唯一任务键就不需要片段幂等吗?
答:任务恢复仍可能重复执行阶段,片段写入也应可重复。