22-RAG文档处理与异步入库

O泡李华 7

企业文档怎样变成可检索知识?解析、切分与异步入库

上传 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 表示文档可检索了吗?

答:只表示任务被接受,完成与发布状态需要另查。

问:有唯一任务键就不需要片段幂等吗?

答:任务恢复仍可能重复执行阶段,片段写入也应可重复。

参考资料