Spring AI RAG 生产级实战:从 PDF 解析到精准回答的完整流水线指南

引言

大模型的知识截止日期和幻觉问题,使得 RAG(检索增强生成)成为企业落地 AI 应用的标配架构。然而,从"能跑"到"好用",中间横亘着无数细节:文档如何解析?分段策略怎么选?检索和生成如何无缝衔接?

本文将以一个基于 Spring AI 1.1.2 + Redis + DashScope 的生产级 RAG 项目为例,手把手拆解完整流水线,涵盖从 PDF 上传、云端解析、分层分段、向量化存储,到查询扩展、多库并发检索、上下文增强、精准回答的全链路实战经验。


1. 整体架构概览

整个 RAG 流水线分为两大阶段:

image.png


2. 文档解析:从 PDF 到结构化内容

2.1 挑战

PDF 是一种呈现格式而非数据结构。直接提取文本会丢失:

  • 表格结构:行列关系、表头、合并单元格
  • 图片信息:图表、截图、流程图
  • 版面布局:多栏、页眉页脚、标题层级
  • 公式:数学符号、化学方程式

2.2 方案:MinerU 云端解析

image.png

我们采用 MinerU API 进行云端解析,分两步:

Step 1 — 上传 + 获取解析任务 ID

// MinerUtil.uploadAndParse()
// 1. 获取预签名上传URL
// 2. PUT上传PDF文件
// 3. 返回batchId用于轮询

Step 2 — 轮询获取结果

// MinerUtil.getZipUrl()
// 轮询直到状态为 DONE,下载结果ZIP包

返回的 _content_list.json 包含结构化内容项,每个项有 type 字段:

类型 处理策略
TEXT / EQUATION 截断至2000字符,相邻小段合并
TABLE 保留表题和表注,内容截断至2000字符
LIST 列表项拼接,遇2000字符拆分
IMAGE 调用 VLM(qwen3.5-flash)生成结构化描述
PAGE_NUMBER / HEADER 直接过滤

2.3 图片理解

对于文档中的图片,单纯 OCR 提取文字会丢失视觉信息(图表趋势、流程图走向)。我们用多模态大模型描述图片:

// 使用线程池 parallelProcessPool (CPU核心数*2)
// 超时10分钟,每张图片调用 VLM 生成描述

2.4 经验总结

PDF 超过 200 页时预处理拆分,避免超时
表格和图片是 RAG 的薄弱环节,需专项处理
云端解析 vs 本地解析:云端精度高但依赖网络,本地速度快但版面分析能力有限


3. 分段策略:父子分层结构

3.1 为什么用父子模式?

LLM 的上下文窗口虽在增长,但实验表明检索分段远优于检索全文。关键在于找到精准度上下文完整性的平衡点。

对标 Dify 的父子模式,我们实现了类似的 双层分段结构

image.png

3.2 实现细节

父分段生成TextUtil.textSegmentsFromMiner()):

  • MinerU 返回的每个 ContentItem 视为一个父分段
  • 同一页内相邻的小内容块(<500字符)合并
  • 每个父分段携带:id, bookId, page, content, bbox(边界框坐标)

子分段生成TextUtil.strengthenWithSpilt()):

  • 使用 BreakIterator.getSentenceInstance(Locale.CHINESE) 按句切分
  • 中文场景必须指定中文Locale,否则分句不准
  • 过滤掉 < 10 字符的短句(通常是标点残留)
  • 子分段通过 parentId = "{bookId}_{parentId}" 链接回父分段

3.3 推荐配置

参数 推荐值 说明
父段最大长度 2000 chars 对应 MinerU 的 blockMaxSize
子段最小长度 10 chars 过滤无意义短句
相邻合并阈值 500 chars 同页相邻小段合并
重叠策略 无重叠 父子结构天然覆盖

3.4 为什么这不只是"按段落分段"?

传统"按段落分段"是扁平结构:每个分段独立存储、独立检索。缺点是:

  • 段落太短 → 上下文不够
  • 段落太长 → 噪音多、精准度下降

父子分层的优势:

  1. 检索粒度:子段(句子)匹配用户问题,精准定位
  2. 回答粒度:父段(段落)提供给 LLM,上下文完整
  3. 得分传递:子段的相似度得分传递给父段,父段按子段得分排序

4. 向量化存储:Redis + HNSW 索引

4.1 向量数据库选型

我们选择 Redis(RediSearch 模块) 作为向量存储,原因:

  • 部署简单:无需额外组件,Redis 已是基础设施
  • 性能优秀:HNSW 算法,COSINE 距离度量
  • Spring AI 原生支持spring-ai-redis-store 开箱即用

4.2 索引配置

// FT.CREATE 索引参数
HNSW: M=16, EF_CONSTRUCTION=200
Metric: COSINE
Type: FLOAT32
Dimensions: 512-2048(按向量库配置)

4.3 Embedding 模型

使用 DashScope 的 text-embedding-v4,向量维度可根据需要调整。

注意: 同一索引的向量维度必须一致。如果修改维度,需重建索引。

4.4 元数据索引

除了向量,我们还索引了以下 TAG 字段用于过滤:

  • bookId:文档来源
  • page:页码(支持定位到原文)
  • parentId:父子分段关联
  • id, text, bbox

5. 检索增强生成(在线阶段)

这一阶段是 RAG 的核心,直接决定回答质量。

image.png

5.1 查询扩展(Query Expansion)

痛点:用户的问题通常简短、模糊,直接检索向量库效果差。

方案:在检索之前,先用 LLM 对问题进行扩展。

// MyQueryExpander.expand()
// 1. 截取最近10条对话历史
// 2. 调用LLM理解完整意图
// 3. 解析代词指代(那、这、它、其等)
// 4. 生成4个完整通顺的查询句子

为什么是 4 个? 实验表明 3-5 个扩展查询能在召回率计算成本之间取得最佳平衡。太少则扩展不充分,太多则引入噪声且耗时线性增长。

5.2 多库并发检索

痛点:一个智能体可能关联多个知识库。

方案:自定义 MultiVectorStoreDocumentRetriever,针对每个扩展查询变体,并发查询所有关联的向量库。

CompletableFuture.supplyAsync(() ->
    vectorStore.similaritySearch(queryVariant)
)

并发度:N 个向量库 × 5 个查询变体 = 5N 个并发任务。使用 CompletableFuture 实现,无需额外线程池配置。

5.3 子句检索 → 父段召回

这是我们的核心创新之一,也是区别于普通 RAG 的关键。

image.png

为什么这么做?

  • 直接检索父段:句子级的精确匹配被段落中的噪声稀释
  • 直接返回子段:LLM 上下文不足,且无法引用完整来源
  • 子段查 + 父段回:两全其美

5.4 排序与截断

MyDocumentPostProcessor 对所有召回文档排序并截取 TopK:

  1. 按相似度得分降序排列
  2. 截取前 topK 个文档(来自智能体配置)

注意:我们没有引入独立的 Reranker 模型。如果你的场景对精度要求更高(如法律、医疗),建议在排序后、截断前加入一个 cross-encoder reranker。

5.5 上下文增强(Query Augmentation)

这是 RAG 生成的最后一道关口

Spring AI 的一个坑:原版 ContextualQueryAugmenter.augment() 会创建新的 Query 对象,丢失对话历史

我们的修复:自定义 MyContextualQueryAugmenter,用 query.mutate().text(augmentedText).build() 保留历史。

增强后的查询格式:

用户问题:{query}

相关上下文(每段内容开头 <ID>数字</ID> 是文档唯一ID):
---------------------
<ID>1</ID>段落内容...
<ID>2</ID>段落内容...
---------------------

回答规则:
1. 严格基于上下文回答
2. 每句话如果使用了上下文,必须在句尾标注对应的文档ID
3. 如果上下文不足以回答问题,如实说明

5.6 文档来源追溯

回答中的 <ID>xxx</ID> 标签让前端可以:

  • 高亮回答中每个句子对应的原文区域
  • 用户点击 ID 跳转到文档对应位置
  • 实现可溯源的可信 AI 回答

6. 智能体架构设计

6.1 可配置的智能体

每个 Agent 通过 t_agent 表配置,支持运行时调整:

字段 范围 说明
temperature 0.0 - 1.0 回答创造力
topK 0 - 100 召回文档数
similarity 0.0 - 1.0 相似度阈值
maxMessage 0 - 100 历史对话窗口
prompt - 系统提示词

6.2 多知识库关联

一个 Agent 可以关联多个 Vector Store(通过 t_agent_vector 关联表)。这意味着:

  • 可以给一个 Agent 同时挂载"产品文档"+"技术手册"+"FAQ"
  • 检索时自动并行查询所有库,结果合并

6.3 缓存策略

我们使用 Caffeine 缓存,避免重复初始化开销:

缓存对象 容量 过期策略
Agent 实例 1000 30分钟访问过期
Vector Store 实例 1000 30分钟访问过期
异步任务状态 - 60分钟写入过期

7. 性能优化实战

7.1 数据 ingestion 优化

  • 全局信号量(Semaphore=1):同一时间只处理一个文件,避免 MinerU API 过载
  • 批量入库:每 10 个 Document 一批写入 Redis
  • 异步处理:上传后立即返回 taskId,前端轮询进度

7.2 在线检索优化

  • 并发检索:多个查询变体 × 多个向量库同时搜索
  • 历史截断:只保留最近 10 条对话记录(可配置)
  • Streaming 响应:Flux 实现流式输出,用户无需等待全量生成

7.3 脏数据处理

  • 过滤页眉页脚、页码
  • 替换连续空格和制表符
  • 短句过滤(< 10 字符)
  • URL 和邮箱可选删除

8. 通用模式 vs 父子模式对比

维度 通用模式(扁平分段) 父子模式(分层分段)
分段形式 独立分段 父子双层嵌套
检索粒度 段落级 句子级检索 + 段落级回答
上下文完整度 取决于段落长度 高(父段提供完整上下文)
精准度 一般(长段落噪声多) 高(精确匹配子段)
实现复杂度 简单 中等
推荐场景 QA 对、短文本 长文档、技术手册、医疗报告

10. 总结与展望

生产级 RAG 的关键要点

  1. 文档解析是地基:PDF 排版复杂,建议使用专业解析服务(MinerU 等)
  2. 分段策略决定上限:父子分层结构经实践验证是兼顾精度和上下文的最佳方案
  3. 查询扩展弥补用户输入稀疏性:LLM 扩写比传统词向量扩展效果显著更好
  4. 并发检索是性能保障:多变体 × 多库并发,充分利用计算资源
  5. 可溯源的回答建立信任:文档 ID 引用让 AI 回答可验证、可追溯

后续优化方向


写在最后

RAG 系统的落地远不止"文档分段 + 向量检索"这么简单。从本文的实践可以看到,一个生产级的 RAG 流水线需要在文档解析、分段策略、检索增强、生成控制四个环节上精雕细琢。其中,父子分层分段、LLM 查询扩展、并发多库检索、文档 ID 溯源等策略,是我们在实际业务中验证过的有效手段。

如果你的项目也正从"Demo 阶段"走向"生产阶段",希望本文的架构设计与踩坑经验能为你提供一些参考。RAG 技术仍在快速演进,Graph RAG、Agentic RAG 等新范式正在涌现,保持学习,持续迭代。


posted @ 2026-06-16 21:09  PC2005-cloud  阅读(39)  评论(0)    收藏  举报