Spring AI :RAG 全流程与核心特性详解
摘要:本文系统讲解 RAG 全流程与核心特性,帮助开发者深入理解 RAG 底层逻辑,掌握知识库检索优化核心特性,构建更精准、可控的问答体系。

下面按照流程依次讲解:文档收集和切割,向量转换和存储,文档过滤和检索,查询增强和关联
文档收集和切割 - ETL
文档收集和切割阶段,我们要对自己准备好的知识库文档进行处理,然后保存到向量数据库中。这个过程俗称 ETL(抽取、转换、加载),Spring AI 提供了对 ETL 的支持,参考 官方文档。
文档:什么是 Spring AI 中的文档呢?
文档不仅仅包含文本,还可以包含一系列元信息和多媒体附件:

ETL:
在 Spring AI 中,对 Document 的处理通常遵循以下流程:
-
读取文档:使用 DocumentReader 组件从数据源(如本地文件、数据库等)加载文档。
-
转换文档:根据需求将文档转换为适合后续处理的格式,比如去除冗余信息、分词、词性标注等,可以使用 DocumentTransformer 组件实现。
-
写入文档:使用 DocumentWriter 将文档以特定格式保存到存储中,比如将文档以嵌入向量的形式写入到向量数据库,或者以键值对字符串的形式保存到 Redis 等 KV 存储中。
流程如图:

我们利用 Spring AI 实现 ETL,核心就是要学习 DocumentReader、DocumentTransformer、DocumentWriter 三大组件。
抽取(Extract)
Spring AI 通过 DocumentReader 组件实现文档抽取,也就是把文档加载到内存中。
看下源码,DocumentReader 接口实现了 Supplier<List<Document>> 接口,主要负责从各种数据源读取数据并转换为 Document 对象集合。
public interface DocumentReader extends Supplier<List<Document>> {
default List<Document> read() {
return get();
}
}
实际开发中,我们可以直接使用 Spring AI 内置的多种 DocumentReader 实现类,用于处理不同类型的数据源:
-
JsonReader:读取 JSON 文档
-
TextReader:读取纯文本文件
-
MarkdownReader:读取 Markdown 文件
-
PDFReader:读取 PDF 文档,基于 Apache PdfBox 库实现
-
PagePdfDocumentReader:按照分页读取 PDF
-
ParagraphPdfDocumentReader:按照段落读取 PDF
-
-
HtmlReader:读取 HTML 文档,基于 jsoup 库实现
-
TikaDocumentReader:基于 Apache Tika 库处理多种格式的文档,更灵活
以 JsonReader 为例,支持 JSON Pointers 特性,能够快速指定从 JSON 文中提取字段和内容:
// 从 classpath 下的 JSON 文件中读取文档
@Component
class MyJsonReader {
private final Resource resource;
MyJsonReader(@Value("classpath:products.json") Resource resource) {
this.resource = resource;
}
// 基本用法
List<Document> loadBasicJsonDocuments() {
JsonReader jsonReader = new JsonReader(this.resource);
return jsonReader.get();
}
// 指定使用哪些 JSON 字段作为文档内容
List<Document> loadJsonWithSpecificFields() {
JsonReader jsonReader = new JsonReader(this.resource, "description", "features");
return jsonReader.get();
}
// 使用 JSON 指针精确提取文档内容
List<Document> loadJsonWithPointer() {
JsonReader jsonReader = new JsonReader(this.resource);
return jsonReader.get("/items"); // 提取 items 数组内的内容
}
}
此外,Spring AI Alibaba 官方社区提供了 更多的文档读取器,比如加载飞书文档、提取 B 站视频信息和字幕、加载邮件、加载 GitHub 官方文档、加载数据库等。
思考:如果让你自己实现一个 DocumentReader 组件,你会怎么实现呢?💡
当然是先看官方 开源的代码仓库 ,看看其他人是怎么实现的。
比如一个邮件文档读取器,核心代码就是解析邮件文档并且转换为 Document 列表:
public class MsgEmailParser {
private MsgEmailParser() {
// Private constructor to prevent instantiation
}
/**
* Convert MsgEmailElement to Document
* @param element MSG email element
* @return Document object
*/
public static Document convertToDocument(MsgEmailElement element) {
if (element == null) {
throw new IllegalArgumentException("MsgEmailElement cannot be null");
}
// Build metadata
Map<String, Object> metadata = new HashMap<>();
// Add metadata with null check
if (StringUtils.hasText(element.getSubject())) {
metadata.put("subject", element.getSubject());
}
// ... 省略更多元信息的设置
// Create Document object with content null check
String content = StringUtils.hasText(element.getText()) ? element.getText() : "";
return new Document(content, metadata);
}
}
转换(Transform)
Spring AI 通过 DocumentTransformer 组件实现文档转换。
看下源码,DocumentTransformer 接口实现了 Function<List<Document>, List<Document>> 接口,负责将一组文档转换为另一组文档。
public interface DocumentTransformer extends Function<List<Document>, List<Document>> {
default List<Document> transform(List<Document> documents) {
return apply(documents);
}
}
文档转换是保证 RAG 效果的核心步骤,也就是如何将大文档合理拆分为便于检索的知识碎片,Spring AI 提供了多种 DocumentTransformer 实现类,可以简单分为 3 类。
1)TextSplitter 文本分割器
其中 TextSplitter 是文本分割器的基类,提供了分割单词的流程方法:
protected List<String> doSplit(String text, int chunkSize) {
if (text != null && !text.trim().isEmpty()) {
List<Integer> tokens = this.getEncodedTokens(text);
List<String> chunks = new ArrayList();
int num_chunks = 0;
while(!tokens.isEmpty() && num_chunks < this.maxNumChunks) {
List<Integer> chunk = tokens.subList(0, Math.min(chunkSize, tokens.size()));
String chunkText = this.decodeTokens(chunk);
if (chunkText.trim().isEmpty()) {
tokens = tokens.subList(chunk.size(), tokens.size());
} else {
int lastPunctuation = Math.max(chunkText.lastIndexOf(46), Math.max(chunkText.lastIndexOf(63), Math.max(chunkText.lastIndexOf(33), chunkText.lastIndexOf(10))));
if (lastPunctuation != -1 && lastPunctuation > this.minChunkSizeChars) {
chunkText = chunkText.substring(0, lastPunctuation + 1);
}
String chunkTextToAppend = this.keepSeparator ? chunkText.trim() : chunkText.replace(System.lineSeparator(), " ").trim();
if (chunkTextToAppend.length() > this.minChunkLengthToEmbed) {
chunks.add(chunkTextToAppend);
}
tokens = tokens.subList(this.getEncodedTokens(chunkText).size(), tokens.size());
++num_chunks;
}
}
if (!tokens.isEmpty()) {
String remaining_text = this.decodeTokens(tokens).replace(System.lineSeparator(), " ").trim();
if (remaining_text.length() > this.minChunkLengthToEmbed) {
chunks.add(remaining_text);
}
}
return chunks;
} else {
return new ArrayList();
}
}
TokenTextSplitter 是其实现类,基于 Token 的文本分割器。它考虑了语义边界(比如句子结尾)来创建有意义的文本段落,是成本较低的文本切分方式。
@Component
class MyTokenTextSplitter {
public List<Document> splitDocuments(List<Document> documents) {
TokenTextSplitter splitter = new TokenTextSplitter();
return splitter.apply(documents);
}
public List<Document> splitCustomized(List<Document> documents) {
TokenTextSplitter splitter = new TokenTextSplitter(1000, 400, 10, 5000, true);
return splitter.apply(documents);
}
}
2)MetadataEnricher 元数据增强器
元数据增强器的作用是为文档补充更多的元信息(补充更多标签),便于后续检索,而不是改变文档本身的切分规则。包括:
-
KeywordMetadataEnricher:使用 AI 提取关键词并添加到元数据
-
SummaryMetadataEnricher:使用 AI 生成文档摘要并添加到元数据。不仅可以为当前文档生成摘要,还能关联前一个和后一个相邻的文档,让摘要更完整。
示例代码:
@Component
class MyDocumentEnricher {
private final ChatModel chatModel;
MyDocumentEnricher(ChatModel chatModel) {
this.chatModel = chatModel;
}
// 关键词元信息增强器
List<Document> enrichDocumentsByKeyword(List<Document> documents) {
KeywordMetadataEnricher enricher = new KeywordMetadataEnricher(this.chatModel, 5);
return enricher.apply(documents);
}
// 摘要元信息增强器
List<Document> enrichDocumentsBySummary(List<Document> documents) {
SummaryMetadataEnricher enricher = new SummaryMetadataEnricher(chatModel,
List.of(SummaryType.PREVIOUS, SummaryType.CURRENT, SummaryType.NEXT));
return enricher.apply(documents);
}
}
3)ContentFormatter 内容格式化工具
用于统一文档内容格式,由于官方对他的介绍较少,我们不妨看它的实现类 DefaultContentFormatter 的源码来了解他的功能:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.ai.document;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.util.Assert;
public final class DefaultContentFormatter implements ContentFormatter {
private static final String TEMPLATE_CONTENT_PLACEHOLDER = "{content}";
private static final String TEMPLATE_METADATA_STRING_PLACEHOLDER = "{metadata_string}";
private static final String TEMPLATE_VALUE_PLACEHOLDER = "{value}";
private static final String TEMPLATE_KEY_PLACEHOLDER = "{key}";
private static final String DEFAULT_METADATA_TEMPLATE = String.format("%s: %s", "{key}", "{value}");
private static final String DEFAULT_METADATA_SEPARATOR = System.lineSeparator();
private static final String DEFAULT_TEXT_TEMPLATE = String.format("%s\n\n%s", "{metadata_string}", "{content}");
private final String metadataTemplate;
private final String metadataSeparator;
private final String textTemplate;
private final List<String> excludedInferenceMetadataKeys;
private final List<String> excludedEmbedMetadataKeys;
private DefaultContentFormatter(Builder builder) {
this.metadataTemplate = builder.metadataTemplate;
this.metadataSeparator = builder.metadataSeparator;
this.textTemplate = builder.textTemplate;
this.excludedInferenceMetadataKeys = builder.excludedInferenceMetadataKeys;
this.excludedEmbedMetadataKeys = builder.excludedEmbedMetadataKeys;
}
public static Builder builder() {
return new Builder();
}
public static DefaultContentFormatter defaultConfig() {
return builder().build();
}
public String format(Document document, MetadataMode metadataMode) {
Map<String, Object> metadata = this.metadataFilter(document.getMetadata(), metadataMode);
String metadataText = (String)metadata.entrySet().stream().map((metadataEntry) -> {
return this.metadataTemplate.replace("{key}", (CharSequence)metadataEntry.getKey()).replace("{value}", metadataEntry.getValue().toString());
}).collect(Collectors.joining(this.metadataSeparator));
return this.textTemplate.replace("{metadata_string}", metadataText).replace("{content}", document.getText());
}
protected Map<String, Object> metadataFilter(Map<String, Object> metadata, MetadataMode metadataMode) {
if (metadataMode == MetadataMode.ALL) {
return new HashMap(metadata);
} else if (metadataMode == MetadataMode.NONE) {
return new HashMap(Collections.emptyMap());
} else {
Set<String> usableMetadataKeys = new HashSet(metadata.keySet());
if (metadataMode == MetadataMode.INFERENCE) {
usableMetadataKeys.removeAll(this.excludedInferenceMetadataKeys);
} else if (metadataMode == MetadataMode.EMBED) {
usableMetadataKeys.removeAll(this.excludedEmbedMetadataKeys);
}
return new HashMap((Map)metadata.entrySet().stream().filter((e) -> {
return usableMetadataKeys.contains(e.getKey());
}).collect(Collectors.toMap((e) -> {
return (String)e.getKey();
}, (e) -> {
return e.getValue();
})));
}
}
public String getMetadataTemplate() {
return this.metadataTemplate;
}
public String getMetadataSeparator() {
return this.metadataSeparator;
}
public String getTextTemplate() {
return this.textTemplate;
}
public List<String> getExcludedInferenceMetadataKeys() {
return Collections.unmodifiableList(this.excludedInferenceMetadataKeys);
}
public List<String> getExcludedEmbedMetadataKeys() {
return Collections.unmodifiableList(this.excludedEmbedMetadataKeys);
}
public static final class Builder {
private String metadataTemplate;
private String metadataSeparator;
private String textTemplate;
private List<String> excludedInferenceMetadataKeys;
private List<String> excludedEmbedMetadataKeys;
private Builder() {
this.metadataTemplate = DefaultContentFormatter.DEFAULT_METADATA_TEMPLATE;
this.metadataSeparator = DefaultContentFormatter.DEFAULT_METADATA_SEPARATOR;
this.textTemplate = DefaultContentFormatter.DEFAULT_TEXT_TEMPLATE;
this.excludedInferenceMetadataKeys = new ArrayList();
this.excludedEmbedMetadataKeys = new ArrayList();
}
public Builder from(DefaultContentFormatter fromFormatter) {
this.withExcludedEmbedMetadataKeys(fromFormatter.getExcludedEmbedMetadataKeys()).withExcludedInferenceMetadataKeys(fromFormatter.getExcludedInferenceMetadataKeys()).withMetadataSeparator(fromFormatter.getMetadataSeparator()).withMetadataTemplate(fromFormatter.getMetadataTemplate()).withTextTemplate(fromFormatter.getTextTemplate());
return this;
}
public Builder withMetadataTemplate(String metadataTemplate) {
Assert.hasText(metadataTemplate, "Metadata Template must not be empty");
this.metadataTemplate = metadataTemplate;
return this;
}
public Builder withMetadataSeparator(String metadataSeparator) {
Assert.notNull(metadataSeparator, "Metadata separator must not be empty");
this.metadataSeparator = metadataSeparator;
return this;
}
public Builder withTextTemplate(String textTemplate) {
Assert.hasText(textTemplate, "Document's text template must not be empty");
this.textTemplate = textTemplate;
return this;
}
public Builder withExcludedInferenceMetadataKeys(List<String> excludedInferenceMetadataKeys) {
Assert.notNull(excludedInferenceMetadataKeys, "Excluded inference metadata keys must not be null");
this.excludedInferenceMetadataKeys = excludedInferenceMetadataKeys;
return this;
}
public Builder withExcludedInferenceMetadataKeys(String... keys) {
Assert.notNull(keys, "Excluded inference metadata keys must not be null");
this.excludedInferenceMetadataKeys.addAll(Arrays.asList(keys));
return this;
}
public Builder withExcludedEmbedMetadataKeys(List<String> excludedEmbedMetadataKeys) {
Assert.notNull(excludedEmbedMetadataKeys, "Excluded Embed metadata keys must not be null");
this.excludedEmbedMetadataKeys = excludedEmbedMetadataKeys;
return this;
}
public Builder withExcludedEmbedMetadataKeys(String... keys) {
Assert.notNull(keys, "Excluded Embed metadata keys must not be null");
this.excludedEmbedMetadataKeys.addAll(Arrays.asList(keys));
return this;
}
public DefaultContentFormatter build() {
return new DefaultContentFormatter(this);
}
}
}
主要提供了 3 类功能:
-
文档格式化:将文档内容与元数据合并成特定格式的字符串,以便于后续处理。
-
元数据过滤:根据不同的元数据模式(MetadataMode)筛选需要保留的元数据项
-
ALL:保留所有元数据 -
NONE:移除所有元数据 -
INFERENCE:用于推理场景,排除指定的推理元数据 -
EMBED:用于嵌入场景,排除指定的嵌入元数据
-
-
自定义模板:支持自定义以下格式:
-
元数据模板:控制每个元数据项的展示方式
-
元数据分隔符:控制多个元数据项之间的分隔方式
-
文本模板:控制元数据和内容如何结合
-
加载(Load)
Spring AI 通过 DocumentWriter 组件实现文档加载(写入)。
DocumentWriter 接口实现了 Consumer<List<Document>> 接口,负责将处理后的文档写入到目标存储中:
public interface DocumentWriter extends Consumer<List<Document>> {
default void write(List<Document> documents) {
accept(documents);
}
}
Spring AI 提供了 2 种内置的 DocumentWriter 实现:
1)FileDocumentWriter:将文档写入到文件系统
@Component
class MyDocumentWriter {
public void writeDocuments(List<Document> documents) {
FileDocumentWriter writer = new FileDocumentWriter("output.txt", true, MetadataMode.ALL, false);
writer.accept(documents);
}
}
2)VectorStoreWriter:将文档写入到向量数据库
@Component
class MyVectorStoreWriter {
private final VectorStore vectorStore;
MyVectorStoreWriter(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
public void storeDocuments(List<Document> documents) {
vectorStore.accept(documents);
}
}
同时,你也可以同时将文档写入多个存储,只需要创建多个 Writer 或者自定义 Writer 即可。
ETL 流程示例
将上述 3 大组件组合起来,可以实现完整的 ETL 流程:
// 抽取:从 PDF 文件读取文档
PDFReader pdfReader = new PagePdfDocumentReader("knowledge_base.pdf");
List<Document> documents = pdfReader.read();
// 转换:分割文本并添加摘要
TokenTextSplitter splitter = new TokenTextSplitter(500, 50);
List<Document> splitDocuments = splitter.apply(documents);
SummaryMetadataEnricher enricher = new SummaryMetadataEnricher(chatModel,
List.of(SummaryType.CURRENT));
List<Document> enrichedDocuments = enricher.apply(splitDocuments);
// 加载:写入向量数据库
vectorStore.write(enrichedDocuments);
// 或者使用链式调用
vectorStore.write(enricher.apply(splitter.apply(pdfReader.read())));
通过这种方式,我们完成了从原始文档到向量数据库的整个 ETL 过程,为后续的检索增强生成提供了基础。
向量转换和存储 - VectorStore
Spring AI 官方 提供了向量数据库接口 VectorStore 和向量存储整合包,帮助开发者快速集成各种第三方向量存储,比如 Milvus、Redis、PGVector、Elasticsearch 等。
VectorStore 接口介绍
VectorStore 是 Spring AI 中用于与向量数据库交互的核心接口,它继承自 DocumentWriter,主要提供以下功能:
public interface VectorStore extends DocumentWriter {
default String getName() {
return this.getClass().getSimpleName();
}
void add(List<Document> documents);
void delete(List<String> idList);
void delete(Filter.Expression filterExpression);
default void delete(String filterExpression) { ... };
List<Document> similaritySearch(String query);
List<Document> similaritySearch(SearchRequest request);
default <T> Optional<T> getNativeClient() {
return Optional.empty();
}
}
这个接口定义了向量存储的基本操作,简单来说就是 "增删改查":
-
添加文档到向量库
-
从向量库删除文档
-
基于查询进行相似度搜索
-
获取原生客户端(用于特定实现的高级操作)
搜索请求构建
Spring AI 提供了 SearchRequest 类,用于构建相似度搜索请求:
SearchRequest request = SearchRequest.builder()
.query("什么是程序员鱼皮的编程导航学习网 codefather.cn?")
.topK(5) // 返回最相似的5个结果
.similarityThreshold(0.7) // 相似度阈值,0.0-1.0之间
.filterExpression("category == 'web' AND date > '2025-05-03'") // 过滤表达式
.build();
List<Document> results = vectorStore.similaritySearch(request);
SearchRequest 提供了多种配置选项:
-
query:搜索的查询文本
-
topK:返回的最大结果数,默认为4
-
similarityThreshold:相似度阈值,低于此值的结果会被过滤掉
-
filterExpression:基于文档元数据的过滤表达式,需要用到时查询 官方文档 了解语法即可
向量存储的工作原理
在向量数据库中,查询与传统关系型数据库有所不同。向量库执行的是相似性搜索,而非精确匹配,具体流程我们在上一节教程中有了解,可以再复习下。
-
嵌入转换:当文档被添加到向量存储时,Spring AI 会使用嵌入模型(如 OpenAI 的 text-embedding-ada-002)将文本转换为向量。
-
相似度计算:查询时,查询文本同样被转换为向量,然后系统计算此向量与存储中所有向量的相似度。
-
相似度度量:常用的相似度计算方法包括:
-
余弦相似度:计算两个向量的夹角余弦值,范围在-1到1之间
-
欧氏距离:计算两个向量间的直线距离
-
点积:两个向量的点积值
-
-
过滤与排序:根据相似度阈值过滤结果,并按相似度排序返回最相关的文档
支持的向量数据库
Spring AI 支持多种向量数据库实现,包括:

对于每种 Vector Store 实现,我们都可以参考对应的官方文档进行整合,开发方法基本上一致:先准备好数据源 => 引入不同的整合包 => 编写对应的配置 => 使用自动注入的 VectorStore 即可。
值得一提的是,Spring AI Alibaba 已经集成了阿里云百炼平台,可以直接使用阿里云百炼平台提供的 VectorStore API,无需自己再搭建向量数据库了。
参考 官方文档,主要是提供了 DashScopeCloudStore 类:

DashScopeCloudStore 类实现了 VectorStore 接口,通过调用 DashScope API 来使用阿里云提供的远程向量存储:

基于 PGVector 实现向量存储
PGVector 是经典数据库 PostgreSQL 的扩展,提供了存储和检索高维向量数据的能力。
为什么选择它来实现向量存储呢?因为很多传统业务都会把数据存储在这种关系型数据库中,直接给原有的数据库安装扩展就能实现向量相似度搜索、而不需要额外搞一套向量数据库,人力物力成本都很低,所以这种方案很受企业青睐,也是目前实现 RAG 的主流方案之一。
为了学习,我们采用更方便的方式 —— 使用现成的云数据库
1)打开 阿里云 PostgreSQL 官网,开通 Serverless 版本,按用量计费,对于学习来说性价比高:

开通 Serverless 数据库服务,填写配置:

2)开通成功后,进入控制台,创建账号和数据库:


进入插件管理,安装 vector 插件:

进入数据库连接,开通公网访问地址:

可以在本地使用 IDEA 自带的数据库管理工具,进行连接测试。

3)先引入 3 个依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store</artifactId>
<version>1.0.0-M6</version>
</dependency>
编写配置,建立数据库连接:
spring:
datasource:
url: jdbc:postgresql://改为你的公网地址/csdn_ai_agent
username: 改为你的用户名
password: 改为你的密码
然后编写配置类自己构造 PgVectorStore,不用 Starter 自动注入:
@Configuration
public class PgVectorVectorStoreConfig {
@Bean
public VectorStore pgVectorVectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel dashscopeEmbeddingModel) {
VectorStore vectorStore = PgVectorStore.builder(jdbcTemplate, dashscopeEmbeddingModel)
.dimensions(1536) // 不要盲目设置
.distanceType(COSINE_DISTANCE) // Optional: defaults to COSINE_DISTANCE
.indexType(HNSW) // Optional: defaults to HNSW
.initializeSchema(true) // Optional: defaults to false
.schemaName("public") // Optional: defaults to "public"
.vectorTableName("vector_store") // Optional: defaults to "vector_store"
.maxDocumentBatchSize(10000) // Optional: defaults to 10000
.build();
return vectorStore;
}
}
注意,在不确定向量维度的情况下,一定不要指定 dimensions 配置!否则很可能会报错! 如果你想使用特定的 Embedding 模型,必须到模型官网查看文档来了解模型支持的向量维度。💡
并且启动类要排除掉自动加载,否则也会报错:
@SpringBootApplication(exclude = PgVectorStoreAutoConfiguration.class)
public class YuAiAgentApplication {
public static void main(String[] args) {
SpringApplication.run(YuAiAgentApplication.class, args);
}
}
4)编写单元测试类验证:
@SpringBootTest
public class PgVectorVectorStoreConfigTest {
@Resource
VectorStore pgVectorVectorStore;
@Test
void test() {
List<Document> documents = List.of(
new Document("Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!!", Map.of("meta1", "meta1")),
new Document("The World is Big and Salvation Lurks Around the Corner"),
new Document("You walk forward facing the past and you turn back toward the future.", Map.of("meta2", "meta2")));
// 添加文档
pgVectorVectorStore.add(documents);
// 相似度查询
List<Document> results = pgVectorVectorStore.similaritySearch(SearchRequest.builder().query("Spring").topK(5).build());
Assertions.assertNotNull(results);
}
}
至此,PGVectorStore 整合成功,可用它来替换原本的本地 VectorStore,示例代码如下:
@Configuration
public class PgVectorVectorStoreConfig {
@Resource
private LoveAppDocumentLoader loveAppDocumentLoader;
@Bean
public VectorStore pgVectorVectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel dashscopeEmbeddingModel) {
VectorStore vectorStore = PgVectorStore.builder(jdbcTemplate, dashscopeEmbeddingModel)
.dimensions(1536)
.distanceType(COSINE_DISTANCE)
.indexType(HNSW)
.initializeSchema(true)
.schemaName("public")
.vectorTableName("vector_store")
.maxDocumentBatchSize(10000)
.build();
// 加载文档
List<Document> documents = loveAppDocumentLoader.loadMarkdowns();
vectorStore.add(documents);
return vectorStore;
}
}
注意:有些 Embedding 模型有加载文档的单批数量限制,可以通过 for 循环分为多批插入。
@Bean
public VectorStore pgVectorVectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel dashscopeEmbeddingModel) {
...
// 加载文档,分批添加(DashScope Embedding API 限制单次 batch size 不超过 10)
List<Document> documents = loveAppDocumentLoader.loadMarkdowns();
int batchSize = 10;
for (int i = 0; i < documents.size(); i += batchSize) {
int end = Math.min(i + batchSize, documents.size());
vectorStore.add(documents.subList(i, end));
}
return vectorStore;
}
文档过滤和检索
Spring AI 官方声称提供了一个 "模块化" 的 RAG 架构,用于优化大模型回复的准确性。
简单来说,就是把整个文档过滤检索阶段拆分为:检索前、检索中、检索后,分别针对每个阶段提供了可自定义的组件。
预检索阶段,系统接收用户的原始查询,通过查询转换等对其进行优化,输出增强的用户查询。
检索中阶段,系统使用增强的查询从知识库中搜索相关文档,涉及多检索源的合并,最终输出一组相关文档。
检索后阶段,系统对检索到的文档进行进一步处理,包括排序、选择最相关的子集以及压缩文档内容,输出经过优化的相关文档集。
预检索:优化用户查询
预检索阶段负责处理和优化用户的原始查询,以提高后续检索的质量,Spring AI 提供了多种查询处理的组件。
查询转换 - 查询重写
RewriteQueryTransformer 是查询重写转换器,核心作用是调用大语言模型优化用户原始查询,让问题更清晰、具体、无冗余信息,尤其适合用户问题模糊、包含无关内容的场景,能显著提升后续知识库检索的精准度。
Query query = new Query("什么是算法");
QueryTransformer queryTransformer = RewriteQueryTransformer.builder()
.chatClientBuilder(chatClientBuilder)
.build();
Query transformedQuery = queryTransformer.transform(query);
底层通过固定提示词模板引导大模型改写查询,模板要求模型针对检索目标优化语句、剔除无关内容,并保持简洁精准。
默认提示词模板如下:
private static final PromptTemplate DEFAULT_PROMPT_TEMPLATE = new PromptTemplate(
"Given a user query, rewrite it to provide better results when querying a {target}.\n" +
"Remove irrelevant information and ensure the query is concise and specific.\n" +
"\n" +
"Original query:\n" +
"{query}\n" +
"\n" +
"Rewritten query:\n"
);
查询转换 - 查询翻译
TranslationQueryTransformer 将查询翻译成嵌入模型支持的目标语言。如果查询已经是目标语言,则保持不变。这对于嵌入模型是针对特定语言训练而用户查询使用不同语言的情况非常有用,便于实现国际化应用。
示例代码如下:
Query query = new Query("hi, who is coder yupi? please answer me");
QueryTransformer queryTransformer = TranslationQueryTransformer.builder()
.chatClientBuilder(chatClientBuilder)
.targetLanguage("chinese")
.build();
Query transformedQuery = queryTransformer.transform(query);
语言可以随便指定,因为看源码我们会发现,查询翻译器也是通过给 AI 一段 Prompt 来实现翻译,当然也可以自定义翻译的 Prompt:

不过不太建议使用这个查询器,因为调用 AI 的成本远比调用第三方翻译 API 的成本要高,不如自己有样学样定义一个 QueryTransformer。
查询转换 - 查询压缩
CompressionQueryTransformer 使用大语言模型将对话历史和后续查询压缩成一个独立的查询,类似于概括总结,适用于对话历史较长且后续查询与对话上下文相关的场景。
示例代码如下:
// 构建带历史对话的查询
Query query = Query.builder()
.text("这个项目的登录功能怎么实现?")
.history(new UserMessage("苍穹外卖项目用了什么技术栈?"),
new AssistantMessage("苍穹外卖采用Spring Boot + MySQL + Redis实现"))
.build();
// 构建对话压缩查询增强器
QueryTransformer queryTransformer = CompressionQueryTransformer.builder()
.chatClientBuilder(chatClientBuilder)
.build();
// 执行压缩增强
Query transformedQuery = queryTransformer.transform(query);
查询扩展 - 多查询扩展
MultiQueryExpander 使用大语言模型将一个查询扩展为多个语义上不同的变体,有助于检索额外的上下文信息并增加找到相关结果的机会。就理解为我们在网上搜东西的时候,可能一种关键词搜不到,就会尝试一些不同的关键词。
示例代码如下:
MultiQueryExpander queryExpander = MultiQueryExpander.builder()
.chatClientBuilder(chatClientBuilder)
.numberOfQueries(3)
.build();
List<Query> queries = queryExpander.expand(new Query("算法是什么"));
上面这个查询可能被扩展为:请介绍算法的定义,并给出更多介绍
默认情况,会在扩展查询列表中包含原始查询。可以在构造时通过 includeOriginal 方法改变行为:
MultiQueryExpander queryExpander = MultiQueryExpander.builder()
.chatClientBuilder(chatClientBuilder)
.includeOriginal(false)
.build();
查看源码,会先调用 AI 得到查询扩展,然后按照换行符分割:
public List<Query> expand(Query query) {
Assert.notNull(query, "query cannot be null");
logger.debug("Generating {} query variants", this.numberOfQueries);
String response = this.chatClient.prompt().user((user) -> {
user.text(this.promptTemplate.getTemplate()).param("number", this.numberOfQueries).param("query", query.text());
}).call().content();
if (response == null) {
logger.warn("Query expansion result is null. Returning the input query unchanged.");
return List.of(query);
} else {
List<String> queryVariants = Arrays.asList(response.split("\n"));
if (!CollectionUtils.isEmpty(queryVariants) && this.numberOfQueries == queryVariants.size()) {
List<Query> queries = (List)queryVariants.stream().filter(StringUtils::hasText).map((queryText) -> {
return query.mutate().text(queryText).build();
}).collect(Collectors.toList());
if (this.includeOriginal) {
logger.debug("Including the original query in the result");
queries.add(0, query);
}
return queries;
} else {
logger.warn("Query expansion result does not contain the requested {} variants. Returning the input query unchanged.", this.numberOfQueries);
return List.of(query);
}
}
}
检索:提高查询相关性
检索模块负责从存储中查询检索出最相关的文档。
文档搜索【粗排阶段】
之前我们有了解过 DocumentRetriever 的概念,这是 Spring AI 提供的文档检索器。每种不同的存储方案都可能有自己的文档检索器实现类,比如 VectorStoreDocumentRetriever,从向量存储中检索与输入查询语义相似的文档。它支持基于元数据的过滤、设置相似度阈值、设置返回的结果数。
// 构建阿里云知识库检索器(CSDN 苍穹外卖专用)
DocumentRetriever retriever = VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.similarityThreshold(0.7)
.topK(5)
.filterExpression(new FilterExpressionBuilder()
.eq("module", "苍穹外卖")
.build())
.build();
// 执行检索(CSDN 业务问题)
List<Document> documents = retriever.retrieve(new Query("苍穹外卖项目如何实现登录验证?"));
上述代码中的 filterExpression 可以灵活地指定过滤条件。当然也可以通过构造 Query 对象的 FILTER_EXPRESSION 参数动态指定过滤表达式:
Query query = Query.builder()
.text("苍穹外卖如何实现订单超时取消?")
.context(Map.of(VectorStoreDocumentRetriever.FILTER_EXPRESSION, "module == '苍穹外卖'"))
.build();
List<Document> retrievedDocuments = documentRetriever.retrieve(query);
文档合并
Spring AI 内置了 ConcatenationDocumentJoiner 文档合并器,通过连接操作,将基于多个查询和来自多个数据源检索到的文档合并成单个文档集合。在遇到重复文档时,会保留首次出现的文档,每个文档的分数保持不变。
示例代码如下:
Map<Query, List<List<Document>>> documentsForQuery = ...
DocumentJoiner documentJoiner = new ConcatenationDocumentJoiner();
List<Document> documents = documentJoiner.join(documentsForQuery);
看源码发现实现原理简单,说是 "连接",其实就是把 Map 展开为二维列表、再把二维列表展开成文档列表,最后进行去重。
public List<Document> join(Map<Query, List<List<Document>>> documentsForQuery) {
Assert.notNull(documentsForQuery, "documentsForQuery cannot be null");
Assert.noNullElements(documentsForQuery.keySet(), "documentsForQuery cannot contain null keys");
Assert.noNullElements(documentsForQuery.values(), "documentsForQuery cannot contain null values");
logger.debug("Joining documents by concatenation");
return new ArrayList(((Map)documentsForQuery.values().stream().flatMap(Collection::stream).flatMap(Collection::stream).collect(Collectors.toMap(Document::getId, Function.identity(), (existing, duplicate) -> {
return existing;
}))).values());
}
检索后:优化文档处理
检索后:优化文档处理 检索后模块负责处理检索到的文档,以实现最佳生成结果。它们可以解决 "丢失在中间" 问题、模型上下文长度限制,以及减少检索信息中的噪音和冗余。
这些模块包括:
文档精排(Rerank):使用精排模型对向量检索结果重新打分排序
文档排序与筛选:选择最相关的 Top-K 文档
文档压缩:精简内容,适配模型上下文窗口 - 去重与冗余去除
查询增强和关联
生成阶段是 RAG 流程的最终环节,负责将检索到的文档与用户查询结合起来,为 AI 提供必要的上下文,从而生成更准确、更相关的回答。Spring AI 提供的 2 种实现 RAG 查询增强的 Advisor,分别是 QuestionAnswerAdvisor 和 RetrievalAugmentationAdvisor。
用 QuestionAnswerAdvisor:
- 本地向量库(PGVector、Redis);
- 单轮问答、快速验证;
- 不需要复杂查询处理和文档后处理。
用 RetrievalAugmentationAdvisor:
- 云知识库(阿里云、百度千帆等);
- 多轮对话、需要结合历史上下文;
- 需要查询重写、文档过滤 / 重排、自定义后处理;
- 生产环境、复杂 RAG 场景。
QuestionAnswerAdvisor
当用户问题发送到 AI 模型时,QuestionAnswerAdvisor 会先拦截并获取用户问题,根据该问题自动查询向量数据库获取相关文档,再将查询到的文档作为上下文附加到用户查询中,最后一并发送给大模型生成最终回答。
基本使用方式如下:
ChatResponse response = ChatClient.builder(chatModel)
.build().prompt()
.advisors(new QuestionAnswerAdvisor(vectorStore))
.user(userText)
.call()
.chatResponse();
我们可以通过建造者模式配置更精细的参数,比如文档过滤条件:
var qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
// 相似度阈值为 0.8,并返回最相关的前 6 个结果
.searchRequest(SearchRequest.builder().similarityThreshold(0.8d).topK(6).build())
.build();
此外,QuestionAnswerAdvisor 还支持动态过滤表达式,可以在运行时根据需要调整过滤条件:
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder().build())
.build())
.build();
// 在运行时更新过滤表达式
String content = this.chatClient.prompt()
.user("看着我的眼睛,回答我!")
.advisors(a -> a.param(QuestionAnswerAdvisor.FILTER_EXPRESSION, "type == 'web'"))
.call()
.content();
QuestionAnswerAdvisor 的实现原理是把用户提示词和检索到的文档等上下文信息拼成一个新的 Prompt,再调用 AI:
/**
* 核心:在调用 AI 之前执行(查询文档 + 拼接 Prompt)
*/
private AdvisedRequest before(AdvisedRequest request) {
// 1. 获取用户原始问题
String query = (new PromptTemplate(request.userText(), request.userParams())).render();
// 2. 去向量库检索相关文档
SearchRequest searchRequestToUse = SearchRequest.from(this.searchRequest)
.query(query)
.filterExpression(this.doGetFilterExpression(context))
.build();
List<Document> documents = this.vectorStore.similaritySearch(searchRequestToUse);
// 3. 把多个文档拼接成一段上下文
String documentContext = documents.stream()
.map(Document::getText)
.collect(Collectors.joining(System.lineSeparator()));
// 4. 拼接新 Prompt:用户问题 + 固定提示词 + 文档上下文
String advisedUserText = request.userText()
+ System.lineSeparator()
+ this.userTextAdvise;
// 5. 把文档上下文放入参数,传给 AI
Map<String, Object> advisedUserParams = new HashMap(request.userParams());
advisedUserParams.put("question_answer_context", documentContext);
// 6. 构建新的请求,交给 AI 模型
return AdvisedRequest.from(request)
.userText(advisedUserText)
.userParams(advisedUserParams)
.adviseContext(context)
.build();
}
RetrievalAugmentationAdvisor
Spring AI 提供的另一种 RAG 实现方式,基于 RAG 模块化架构,提供更多的灵活性和定制选项。
最简单的 RAG 流程可以通过以下方式实现:
Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
.documentRetriever(VectorStoreDocumentRetriever.builder()
.similarityThreshold(0.50)
.vectorStore(vectorStore)
.build())
.build();
String answer = chatClient.prompt()
.advisors(retrievalAugmentationAdvisor)
.user(question)
.call()
.content();
上述代码中,我们配置了 VectorStoreDocumentRetriever 文档检索器,用于从向量存储中检索文档。然后将这个 Advisor 添加到 ChatClient 的请求中,让它处理用户的问题。
RetrievalAugmentationAdvisor 还支持更高级的 RAG 流程,比如结合查询转换器:
VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.similarityThreshold(0.7)
.topK(5)
.build();
Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
.queryTransformers(RewriteQueryTransformer.builder()
.chatClientBuilder(chatClientBuilder.build().mutate())
.build())
.documentRetriever(VectorStoreDocumentRetriever.builder()
.similarityThreshold(0.50)
.vectorStore(vectorStore)
.build())
.build();
上述代码中,我们添加了一个 RewriteQueryTransformer,它会在检索之前重写用户的原始查询,使其更加明确和详细,从而显著提高检索的质量(因为大多数用户的原始查询是含糊不清、或者不够具体的)。
ContextualQueryAugmenter
默认情况下,RetrievalAugmentationAdvisor 不允许检索的上下文为空。当没有找到相关文档时,它会指示模型不要回答用户查询。这是一种保守的策略,可以防止模型在没有足够信息的情况下生成不准确的回答。
但在某些场景下,我们可能希望即使在没有相关文档的情况下也能为用户提供回答,比如即使没有特定知识库支持也能回答的通用问题。可以通过配置 ContextualQueryAugmenter 上下文查询增强器来实现。
示例代码如下:
Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
.documentRetriever(VectorStoreDocumentRetriever.builder()
.similarityThreshold(0.50)
.vectorStore(vectorStore)
.build())
.queryAugmenter(ContextualQueryAugmenter.builder()
.allowEmptyContext(true)
.build())
.build();
通过设置 allowEmptyContext(true),允许模型在没有找到相关文档的情况下也生成回答。
恭喜你学习完成!✿
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)