【SpringAI】第四弹:深入解析 Rag 检索增强工作流程、最佳实践和调优

【SpringAI】第四弹:深入解析 Rag 检索增强工作流程、最佳实践和调优

在这里插入图片描述
在这里插入图片描述

本节重点


以 Spri‏ng AI 框架为例,‏学习 RAG 知识库应‏用开发的核心特性和高级‏知识点,并且掌握 RA‌G 最佳实践和调优技巧。

具体内容包括:

  • RAG 核心特性
    • 文档收集和切割(ETL)
    • 向量转换和存储(向量数据库)
    • 文档过滤和检索(文档检索器)
    • 查询增强和关联(上下文查询增强器)
  • RAG 最佳实践和调优
  • RAG 高级知识
    • 检索策略
    • 大模型幻觉
    • 高级 RAG 架构

一、RAG 核心特性


Rag 检索增强工作流程


img

一、建立索引

(1) 文档预处理和切割 ETL

首先对文档进行结构优化,内容清洗,也就是让文档的每一个部分的内容,都有一个标题,来划分每个部分的内容,并对一些大模型无法识别的图片链接、超链接等,以及一些代码块进行清洗(避免在后续切割文档时,代码被切导致逻辑中断);使用固定大小、语义边界或递归分割等方法,将文档切割成多个文档切片;

(2) 向量转换和存储

调用 Embedding 模型,将文档切片转为高维向量,并存储到对应的向量数据库中;这些文档切片也会作为大模型回答用户特定问题的知识库;

二、检索增强

(1) 文档过滤和检索

用户问题向量化:将用户问题输入 Embedding 模型,将用户输入的提示词转为向量;相似度搜索和条件过滤:在向量数据库中进行相似度计算,查询与用户问题向量相似度接近的文档切片;将查询到的切片结果进行过滤,如用户问题的语义、关键词可以提炼出一些标签、元信息,那么就根据这些信息,来进一步过滤查询的切片;Rank 模型精排:调用 Rank 模型对检索到的相关文档切片进行精排(Re-ranking),使用更复杂的算法重新排序和评分;筛选出相似度分数最高的前 N 个切片;

(2) 查询增强和关联

构建增强提示词:将排序得出相似度分数最高的前 N 个切片,拼接到用户的提示词中,得到最终的增强提示词;生成最终答案:将增强提示词输入给大模型,大模型会基于知识库内容生成最终的结果;

上节教程中我们只是‏按照这个流程完成了入门级 RAG ‏应用的开发,实际上每个流程都有一些‏值得学习的特性;

Spring AI‏ 也为这些流程的技术实现提供了支持‌,下面让我们按照流程依次进行讲解。

  • 文档收集和切割
  • 向量转换和存储
  • 文档过滤和检索
  • 查询增强和关联

文档收集和切割 - ETL


文档收集和切割阶段,我们要对自己准备好的知识库文档进行处理,然后保存到向量数据库中。

这个过程俗称 ETL(抽取、转换、加载),Spring AI 提供了对 ETL 的支持,参考 官方文档


文档

什么是 Spring AI 中的文档呢?

文档不仅仅包含文本,还可以包含一系列元信息和多媒体附件:

null

查看源码:

image-20250830141925645

ETL

在 Spr‏ing AI 中,‏对 Documen‏t 的处理通常遵循‏以下流程:

  1. 读取文档:使用 DocumentReader 组件从数据源(如本地文件、网络资源、数据库等)加载文档。
  2. 转换文档:根据需求将文档转换为适合后续处理的格式,比如去除冗余信息、分词、词性标注等,可以使用 DocumentTransformer 组件实现。
  3. 写入文档:使用 DocumentWriter 将文档以特定格式保存到存储中,比如将文档以嵌入向量的形式写入到向量数据库,或者以键值对字符串的形式保存到 Redis 等 KV 存储中。

流程如图:

null

我们利用 Spring‏ AI 实现 ETL,核心就是要学习 Doc‏umentReader、DocumentTr‏ansformer、DocumentWrit‏er 三大组件。 ‌

完整的 E‏TL 类图如下,先‏简单了解一下即可,‏下面分别来详细讲解‏这 3 大组件:

null

抽取(Extract)

Sprin‏g AI 通过 D‏ocumentRe‏ader 组件实现‏文档抽取,也就是把‌文档加载到内存中。

image-20250830142257468

看下源码,DocumentReader 接口实现了 Supplier<List<Document>> 接口:主要负责从各种数据源读取数据并转换为 Document 对象集合。

image-20250830143710788

实现了 Supplier<List<Document>> 接口,就可以调用 Document 就可以调用父类的定义的通用方法: get()

image-20250830145207633

查看 DocumentReader 的具体实现类的源码,查看它们是如何抽取文档,并转换为 Document 对象的:

image-20250830145111915

也就是说,在抽取文档的过程中,调用 DocumentReader 对应格式的实现类,抽取文档中的文本,如果不是 textReader,就可能包含一些媒体类型的信息,需要将这些信息一并抽取,并设置一些默认的元信息;

将文档内容(文本、媒体信息)、元信息作为参数实例一个 Document 对象;


实际开发中,我们可以直接使用 Spring AI 内置的多种 DocumentReader 实现类,用于处理不同类型的数据源:

image-20250830142343585
  1. JsonReader:读取 JSON 文档
  2. TextReader:读取纯文本文件
  3. MarkdownReader:读取 Markdown 文件
  4. PDFReader:读取 PDF 文档,基于 Apache PdfBox 库实现
    • PagePdfDocumentReader:按照分页读取 PDF
    • ParagraphPdfDocumentReader:按照段落读取 PDF
  5. HtmlReader:读取 HTML 文档,基于 jsoup 库实现
  6. TikaDocumentReader:基于 Apache Tika 库处理多种格式的文档,更灵活

以 Json‏Reader 为例,支‏持 JSON Poin‏ters 特性,能够快‏速指定从 JSON 文‌档中提取哪些字段和内容:

{

​ “帅哥”:{

​ “name”: “小雷”

​ }

}

Spring AI 的 Json‏Reader 可以通过 JSON Poin‏ters 特性,通过帅哥.name 一次性读取到 name 中的值;
// 从 classpath 下的 JSON 文件中读取文档@ComponentclassMyJsonReader{ privatefinalResource resource;MyJsonReader(@Value("classpath:products.json")Resource resource){ this.resource = resource;}// 基本用法List<Document>loadBasicJsonDocuments(){ JsonReader jsonReader =newJsonReader(this.resource);return jsonReader.get();}// 指定使用哪些 JSON 字段作为文档内容List<Document>loadJsonWithSpecificFields(){ JsonReader jsonReader =newJsonReader(this.resource,"description","features");return jsonReader.get();}// 使用 JSON 指针精确提取文档内容List<Document>loadJsonWithPointer(){ JsonReader jsonReader =newJsonReader(this.resource);return jsonReader.get("/items");// 提取 items 数组内的内容}}

更多的文档读取器等用到的时候再了解用法即可。

此外,Spring AI Alibaba 官方社区提供了 更多的文档读取器,比如加载飞书文档、提取 B 站视频信息和字幕、加载邮件、加载 GitHub 官方文档、加载数据库等等。


💡 思考‏:如果让你自己实现‏一个 Docume‏ntReader ‏组件,你会怎么实现‌呢?

当然是先看官方 开源的代码仓库 ,看看大佬们是怎么实现的:

image-20250830145849835

比如一个邮‏件文档读取器的实现‏其实并不难,核心代‏码就是解析邮件文档‏并且转换为 Doc‌ument 列表:

image-20250830150346101

邮件解析器的实现:

image-20250830150948884
publicclassMsgEmailParser{ privateMsgEmailParser(){ // Private constructor to prevent instantiation}/** * Convert MsgEmailElement to Document * @param element MSG email element * @return Document object */publicstaticDocumentconvertToDocument(MsgEmailElement element){ if(element ==null){ thrownewIllegalArgumentException("MsgEmailElement cannot be null");}// Build metadataMap<String,Object> metadata =newHashMap<>();// Add metadata with null checkif(StringUtils.hasText(element.getSubject())){  metadata.put("subject", element.getSubject());}// ... 省略更多元信息的设置// Create Document object with content null checkString content =StringUtils.hasText(element.getText())? element.getText():"";returnnewDocument(content, metadata);}}

邮件解析器获取文档的原理,就是以邮件中的一些信息(收信人、发信人等…)作为元信息,抽取出邮件的文本,结合元信息,实例一个 Document 对象;这个对象作为解析结果返回;

邮‏件文档读取器中的 get() 方法,会接收所有的解析结果,集成一个列表对象,作为文档对象列表,并返回,就实现了单次抽取一些邮件,转为 Document 的功能;


转换(Transform)

Sprin‏g AI 通过 D‏ocumentTr‏ansformer‏ 组件实现文档转换‌。看下源码:

image-20250830151230666

DocumentTransformer 接口实现了 Function<List<Document>, List<Document>> 接口,负责将一组文档转换为另一组文档。

publicinterfaceDocumentTransformerextendsFunction<List<Document>,List<Document>>{ defaultList<Document>transform(List<Document> documents){ returnapply(documents);}}

文档转换是保证 R‏AG 效果的核心步骤,也就是如何将大‏文档,合理拆分为便于检索的知识碎片,S‏pring AI 提供了多种 Doc‏umentTransformer 实‌现类,可以简单分为 3 类。


1. TextSplitter 文本分割器

其中 Te‏xtSplitte‏r 是文本分割器的‏基类,提供了分割单‏词的流程方法:

image-20250830151453543

TokenTex‏tSplitter 是其实现类‏,基于 Token 的文本分‏割器。

它考虑了语义边界(比如句子‏结尾)来创建有意义的文本段落,‌是成本较低的文本切分方式。

@ComponentclassMyTokenTextSplitter{ publicList<Document>splitDocuments(List<Document> documents){ TokenTextSplitter splitter =newTokenTextSplitter();return splitter.apply(documents);}publicList<Document>splitCustomized(List<Document> documents){ TokenTextSplitter splitter =newTokenTextSplitter(1000,400,10,5000,true);return splitter.apply(documents);}}

Token‏TextSplit‏ter 提供了两种‏构造函数选项:

  1. TokenTextSplitter():使用默认设置创建分割器。
  2. TokenTextSplitter(int defaultChunkSize, int minChunkSizeChars, int minChunkLengthToEmbed, int maxNumChunks, boolean keepSeparator):使用自定义参数创建分割器,通过调整参数,可以控制分割的粒度和方式,适应不同的应用场景。

参数说明(无需记忆):

  • defaultChunkSize:每个文本块的目标大小(以 token 为单位,默认值:800)。
  • minChunkSizeChars:每个文本块的最小大小(以字符为单位,默认值:350)。
  • minChunkLengthToEmbed:要被包含的块的最小长度(默认值:5)。
  • maxNumChunks:从文本中生成的最大块数(默认值:10000)。
  • keepSeparator:是否在块中保留分隔符(如换行符)(默认值:true)。

官方文档有‏对 Token 分‏词器工作原理的详细‏解释,可以简单了解‏下:

  1. 使用 CL100K_BASE 编码将输入文本编码为 token。
  2. 根据 defaultChunkSize 将编码后的文本分割成块。
  3. 对于每个块:
    • 将块解码回文本。
    • 尝试在 minChunkSizeChars 之后找到合适的断点(句号、问号、感叹号或换行符)。
    • 如果找到断点,则在该点截断块。
    • 修剪块并根据 keepSeparator 设置选择性地删除换行符。
    • 如果生成的块长度大于 minChunkLengthToEmbed,则将其添加到输出中。
  4. 这个过程会一直持续到所有 token 都被处理完或达到 maxNumChunks 为止。
  5. 如果剩余文本长度大于 minChunkLengthToEmbed,则会作为最后一个块添加。

2. MetadataEnricher 元数据增强器

元数据增强‏器的作用是为文档补‏充更多的元信息,便‏于后续检索,而不是‏改变文档本身的‌切分规则。包括:

  • KeywordMetadataEnricher:使用 AI 提取关键词并添加到元数据
  • SummaryMetadataEnricher:使用 AI 生成文档摘要并添加到元数据。不仅可以为当前文档生成摘要,还能关联前一个和后一个相邻的文档,让摘要更完整。

示例代码:

@ComponentclassMyDocumentEnricher{ privatefinalChatModel chatModel;MyDocumentEnricher(ChatModel chatModel){ this.chatModel = chatModel;}// 关键词元信息增强器List<Document>enrichDocumentsByKeyword(List<Document> documents){ KeywordMetadataEnricher enricher =newKeywordMetadataEnricher(this.chatModel,5);return enricher.apply(documents);}// 摘要元信息增强器List<Document>enrichDocumentsBySummary(List<Document> documents){ SummaryMetadataEnricher enricher =newSummaryMetadataEnricher(chatModel,List.of(SummaryType.PREVIOUS,SummaryType.CURRENT,SummaryType.NEXT));return enricher.apply(documents);}}

3. ContentFormatter 内容格式化工具

用于统一文‏档内容格式。官方对‏这个的介绍少的可怜‏,感觉像是个孤‏儿功能。。。

image-20250830152253519

我们不妨看它的实现类 DefaultContentFormatter 的源码来了解他的功能:

image-20250830152605147

主要提供了 3 类功能:

  1. 文档格式化:将文档内容与元数据,合并成特定格式的字符串,以便于后续处理。
  2. 元数据过滤:根据不同的元数据模式(MetadataMode)筛选需要保留的元数据项:
    • ALL:保留所有元数据
    • NONE:移除所有元数据
    • INFERENCE:用于推理场景,排除指定的推理元数据
    • EMBED:用于嵌入场景,排除指定的嵌入元数据
  3. 自定义模板:支持自定义以下格式:
    • 元数据模板:控制每个元数据项的展示方式
    • 元数据分隔符:控制多个元数据项之间的分隔方式
    • 文本模板:控制元数据和内容如何结合

该类采用 Builder 模式创建实例,使用示例:

DefaultContentFormatter formatter =DefaultContentFormatter.builder().withMetadataTemplate("{key}: {value}").withMetadataSeparator("\n").withTextTemplate("{metadata_string}\n\n{content}").withExcludedInferenceMetadataKeys("embedding","vector_id").withExcludedEmbedMetadataKeys("source_url","timestamp").build();// 使用格式化器处理文档String formattedText = formatter.format(document,MetadataMode.INFERENCE);

在 RAG‏ 系统中,这个格式‏化器可以有下面的作‏用,了解即可:

  1. 提供上下文:将元数据(如文档来源、时间、标签等)与内容结合,丰富大语言模型的上下文信息
  2. 过滤无关信息:通过排除特定元数据,减少噪音,提高检索和生成质量
  3. 场景适配:为不同场景(如推理和嵌入)提供不同的格式化策略
  4. 结构化输出:为 AI 模型提供结构化的输入,使其能更好地理解和处理文档内容

加载(Load)

Sprin‏g AI 通过 D‏ocumentWr‏iter 组件实现‏文档加载(写入)

DocumentWriter 接口实现了 Consumer<List<Document>> 接口,负责将处理后的文档写入到目标存储中:


Sprin‏g AI 提供了 ‏2 种内置的 Do‏cumentWri‏ter 实现:

1. File‏DocumentWri‏ter:将文档写入到文‏件系统 ‏ ‌

@ComponentclassMyDocumentWriter{ publicvoidwriteDocuments(List<Document> documents){ FileDocumentWriter writer =newFileDocumentWriter("output.txt",true,MetadataMode.ALL,false); writer.accept(documents);}}

2. Vec‏torStoreW‏riter:将文档‏写入到向量数据库

@ComponentclassMyVectorStoreWriter{ privatefinalVectorStore vectorStore;MyVectorStoreWriter(VectorStore vectorStore){ this.vectorStore = vectorStore;}publicvoidstoreDocuments(List<Document> documents){  vectorStore.accept(documents);}}

当然,你也‏可以同时将文档写入‏多个存储,只需要创‏建多个 Write‏r 或者自定义 W‌riter 即可。


ETL 流程示例

将上述 3 大组件组合起来,可以实现完整的 ETL 流程:

// 抽取:从 PDF 文件读取文档PDFReader pdfReader =newPagePdfDocumentReader("knowledge_base.pdf");List<Document> documents = pdfReader.read();// 转换:分割文本并添加摘要TokenTextSplitter splitter =newTokenTextSplitter(500,50);List<Document> splitDocuments = splitter.apply(documents);SummaryMetadataEnricher enricher =newSummaryMetadataEnricher(chatModel,List.of(SummaryType.CURRENT));List<Document> enrichedDocuments = enricher.apply(splitDocuments);// 加载:写入向量数据库 vectorStore.write(enrichedDocuments);// 或者使用链式调用 vectorStore.write(enricher.apply(splitter.apply(pdfReader.read())));

通过这种方‏式,我们完成了从原‏始文档到向量数据库‏的整个 ETL 过‏程,为后续的检索增‌强生成提供了基础。


向量转换和存储


上一节教程中有介绍过,向量存储是 RAG 应用中的核心组件,它将文档转换为向量(嵌入)并存储起来,以便后续进行高效的相似性搜索。

Spring AI 官方 提供了向量数据库接口 VectorStore 和向量存储整合包,帮助开发者快速集成各种第三方向量存储,比如 Milvus、Redis、PGVector、Elasticsearch 等。


VectorStore 接口介绍

VectorS‏tore 是 Spring‏ AI 中用于与向量数据库‏交互的核心接口,它继承自 ‏DocumentWrite‌r,主要提供以下功能:

publicinterfaceVectorStoreextendsDocumentWriter{ defaultStringgetName(){ returnthis.getClass().getSimpleName();}voidadd(List

Read more

RustDesk 服务端完整安装部署教程

RustDesk 服务端完整安装部署教程(2025 版) 一、环境准备 1. 服务器要求 * 操作系统:Ubuntu 20.04/22.04(推荐)、Debian、CentOS 等 * 硬件配置: * 测试环境:1 核 2G 以上 * 生产环境:2 核 4G+,50G + 存储空间 * 网络要求:公网 IP(如需外网访问) 2. 防火墙配置 开放 RustDesk 服务端所需端口: # Ubuntu/Debiansudo ufw allow 21115:21119/tcp sudo ufw allow 21116/

By Ne0inhk
Flutter 组件 postgres_crdt 的适配 鸿蒙Harmony 实战 - 驾驭分布式无冲突复制数据类型、实现鸿蒙端高性能离线对等同步架构方案

Flutter 组件 postgres_crdt 的适配 鸿蒙Harmony 实战 - 驾驭分布式无冲突复制数据类型、实现鸿蒙端高性能离线对等同步架构方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 postgres_crdt 的适配 鸿蒙Harmony 实战 - 驾驭分布式无冲突复制数据类型、实现鸿蒙端高性能离线对等同步架构方案 前言 在鸿蒙(OpenHarmony)生态的分布式协作编辑器、多端同步的即时通讯资产库以及需要实现“本地优先(Local-first)”架构的各类大型数字化政务应用开发中,“数据一致性的最终收敛”是系统稳定性的灵魂。面对由 5 台鸿蒙设备在不同地点、不同弱网环境下同时对同一份 JSON 资产执行的交叉修改。如果依然采用基于“锁”或“版本号覆盖”的传统同步逻辑。不仅会导致频繁出现的由于并发冲突引发的“保存失败”报错,更会因为无法处理跨设备的时序漂移,引发严重的资产状态错乱。 我们需要一种“逻辑守恒、冲突自愈”的存储艺术。 postgres_crdt 是一套专注于将 PostgreSQL 生态的严谨性与无冲突复制数据类型(

By Ne0inhk
MySQL(Windows)压缩包安装与配置指南(超详细版)

MySQL(Windows)压缩包安装与配置指南(超详细版)

在 Windows 环境下安装 MySQL,除了使用官方安装器之外,更常见也更灵活的方式是使用 zip 压缩包解压安装。这种方式的优点是目录结构清晰、迁移方便、不会写入过多系统组件,适合学习、开发环境以及需要多版本共存的场景。 本文记录一次完整的 MySQL 8.0.28(winx64)压缩包安装流程,包括环境变量配置、my.ini 编写、初始化、注册系统服务、启动、登录与修改 root 密码,并附带常见报错处理方法。 一、安装包准备 下载地址:https://dev.mysql.com/downloads/mysql/ 我比较喜欢用zip 本次使用版本为: * MySQL Community Server 8.0.28 * Windows x64

By Ne0inhk
Flutter 三方库 actors 鸿蒙超算平台底层架构适配真知:搭建轻量级并发处理结构强力引入无状态内存安全的传递协议,全景释放超密集系统多核全量调配效力-适配鸿蒙 HarmonyOS ohos

Flutter 三方库 actors 鸿蒙超算平台底层架构适配真知:搭建轻量级并发处理结构强力引入无状态内存安全的传递协议,全景释放超密集系统多核全量调配效力-适配鸿蒙 HarmonyOS ohos

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 actors 鸿蒙超算平台底层架构适配真知:搭建轻量级并发处理结构强力引入无状态内存安全的传递协议,全景释放超密集系统多核全量调配效力 在高性能并发编程中,共享内存锁竞争是导致系统卡顿的主要诱因。actors 库采用了经典的 Actor 语言模型(类似 Erlang/Akka),为 Dart 开发者提供了一种无锁的异步任务处理方案。本文将详细探讨该库在 OpenHarmony 上的适配与应用实践。 前言 什么是 Actor 模式?在这种模式下,每个 Actor 都是一个独立、自治的逻辑单元,它们之间只能通过“消息传递”进行通信,而不共享任何内部状态。在鸿蒙这个强调极速流畅和多线程(TaskPool)调度的系统中,actors 库能显著提升应用处理大规模计算密集型任务时的稳定性。 一、原理解析 1.1 基础概念 每一个

By Ne0inhk