【Spring AI】一文带你快速上手 RAG
一篇关于如何快速上手RAG的文章
目录
RetrievalAugmentationAdvisor 查询增强
1.什么是 RAG?
RAG(Retrieval-Augmented Generation)即 “检索增强生成”。它是一种将信息检索 系统与大语言模型LLM 的生成能力相结合的技术框架。
比如说:
当用户提出一个问题时,RAG 不会让 LLM 凭空回答。而是先从外部知识库(如公司文档、数据库、网页等)中检索与问题最相关的信息片段,然后将这些检索到的信息 和用户的原始问题 一起打包,发送给 LLM,指令 LLM 基于这些提供的可靠信息来组织语言、生成答案。
2.RAG完整流程
具体如图所示:
可以概括为四个步骤:
阶段一:线下
1)数据预处理
2)数据向量存储
阶段二:线上
3)文档检索
4)查询增强
详细介绍可以去看我写的面试题解:【秋招必看】RAG 面试热题(一)-CSDN博客
3.如何在程序中使用RAG?
我们已经知道了RAG开发的完整流程,我们可以自己实现一个ai应用。
需求:用户输入一段提示词,后端会跟据提示词从知识库里面检索相关的信息,将这些信息与用户的提示词相结合全部交给LLM,LLM返回信息给用户
环境准备:jdk21、springboot3.x、springAI1.0.0-M6
1)准备知识库
本地:
可以问ai,不过提示词一定要规范,内容也要格式统一
示例提示词:
帮我生成 3 篇 Markdown 文章,主题是【恋爱常见问题和回答】,3 篇文章的问题分别针对单身、恋爱、已婚的状态,内容形式为 1 问 1 答,每个问题标题使用 4 级标题,每篇内容需要有至少 5 个问题,要求每个问题中推荐一个相关的课程,课程链接都是 xxx
云知识库:
选择 阿里云百炼,因为 Spring AI Alibaba 可以和它轻松集成,简化 RAG 开发。
2)数据预处理
这个阶段主要是对文档进行读取、切片(分块)、标注元数据
文档读取
Spring AI 通过 DocumentReader 组件实现文档抽取。
如图,DocumentReader的实现类
因为是markdown格式,因此采用MarkdownDocumentReader读取器,这个读取器可以同时实现文档读取、切分和元数据标注
文档切片
可以通过 Spring AI 的 ETL Pipeline 提供的 DocumentTransformer 来调整切分规则,但是不推荐,因为手动切分很难把控,效果不太好。所以我这里直接使用MarkdownDocumentReader读取器配置切分规则即可,虽然也是手动切分。。。
当然也可以选择使用阿里云百炼的知识库进行智能切分,这是百炼经过大量评估后总结出的推荐策略
元数据标注
可以为文档添加丰富的结构化信息,俗称元信息,形成多维索引,便于后续向量化处理和精准检索。比如document类的元数据:
这就是一个map结构来存储元数据
1)手动添加元信息(单个文档)(不推荐)
2)利用 DocumentReader 批量添加元信息(手动,如配置MarkdownDocumentReader)
3)基于AI自动添加元信息
Spring AI 提供了生成元信息的 Transformer 组件,可以基于 AI 自动解析关键词并添加到元信息中。
/**
* 基于 AI 的文档元信息增强器(为文档补充元信息)
* 本项目该增强器暂时只应用在本地的RAG知识库
*/
@Component
class MyKeywordEnricher {
@Resource
private ChatModel dashscopeChatModel;
//主要功能是为每个文档生成关键词并添加到其元数据中
List<Document> enrichDocuments(List<Document> documents) {
// 参数1: 使用的AI聊天模型(此处为注入的dashscopeChatModel)
// 参数2: 5 - 表示希望为每个文档生成的关键词数量
KeywordMetadataEnricher enricher = new KeywordMetadataEnricher(this.dashscopeChatModel, 5);
return enricher.apply(documents);
}
}
综合示例代码:
1.整体逻辑不难,使用spring的资源解析类ResourcePatternResolver 的getResources方法读取所有文档的路径并且每个文档都放在一个Resource集合,
2.使用循环的方式取出每个文档进行解析,配置好读取器MarkdownDocumentReaderConfig 之后(如切片规则、元数据标注),
3.将配置和文档都传给MarkdownDocumentReader()进行解析,
4.注意,如果这里上面没有进行文档切片和元数据标注等配置,可以在返回最终数据前使用自主切分和自动补充关键词元信息,
5.最后返回解析(分片后的文档)
//spring的资源解析类
private final ResourcePatternResolver resourcePatternResolver;
/**
* 加载多篇 Markdown 文档
*/
public List<Document> loadMarkdowns() {
List<Document> allDocuments = new ArrayList<>();
try {
Resource[] resources = resourcePatternResolver.getResources("classpath:document/*.md");
for (Resource resource : resources) {
String filename = resource.getFilename();
// // 提取文档倒数第 3 和第 2 个字作为标签(太生硬,不推荐)
// String status = filename.substring(filename.length() - 6, filename.length() - 4);
// 配置Markdown文档读取器
MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder()//创建一个配置建造器
.withHorizontalRuleCreateDocument(true)//按章节分割成多个小文档,如遇到 (--- 或 ***)
.withIncludeCodeBlock(false)//设置解析时不包含代码块(``` 中的内容)
.withIncludeBlockquote(false)//设置解析时不包含引用块 (> 开头的内容)
.withAdditionalMetadata("filename", filename)//向生成的每个 Document 对象中添加一个额外的元数据键值对,键是 "filename"
// .withAdditionalMetadata("status", status)//为每篇文章添加特定标签,例如"状态"
.build();//生成一个 MarkdownDocumentReaderConfig 配置对象
// 创建Markdown文档读取器实例
MarkdownDocumentReader reader = new MarkdownDocumentReader(resource, config);
allDocuments.addAll(reader.get());
}
} catch (IOException e) {
log.error("Markdown 文档加载失败", e);
}
return allDocuments;
}
3)数据向量化存储
这个阶段就是将分块后的文档进行数字化存储
关于VectorStore
VectorStore 是 Spring AI 中用于与向量数据库交互的核心接口,它继承自 DocumentWriter
这个接口定义了向量存储的基本操作,简单来说就是 “增删改查”:
- 添加文档到向量库
- 从向量库删除文档
- 基于查询进行相似度搜索
- 获取原生客户端(用于特定实现的高级操作)
向量化存储可以先存在内存里,也可以选择存在pgVector
方法一:存储在本地内存(推荐)
示例代码:
1.主要思路/步骤就是使用Embedding模型构建向量存储实例SimpleVector,
2.使用文档加载器加载所有Markdown文档,
3.期间可以选择进行自主切分和自动补充关键词元信息(其实也没必要,因为前面MarkdownDocumentReaderConfig 已经配置好了,这里相当于重复了),
4.然后将Document集合(切分后的文档)添加到向量存储中,文档会被自动转换为向量并建立索引,这样,一个存储在内存中的向量数据库就完成了(因添加了Bean注解,这个 VectorStore
实例会一直存在,直到Spring应用上下文关闭)
// 注入文档加载器组件,用于加载Markdown文档
@Resource
private LoveAppDocumentLoader loveAppDocumentLoader;
@Resource
private MyTokenTextSplitter myTokenTextSplitter;
@Resource
private MyKeywordEnricher myKeywordEnricher;
/**
* 创建并配置向量存储Bean
* @param dashscopeEmbeddingModel 嵌入模型,用于将文本转换为向量表示
* @return 配置好的向量存储实例
*/
@Bean // 声明该方法返回一个由Spring管理的Bean
VectorStore loveAppVectorStore(EmbeddingModel dashscopeEmbeddingModel) {
// 构建简单的向量存储实例,传入嵌入模型
SimpleVectorStore simpleVectorStore = SimpleVectorStore.builder(dashscopeEmbeddingModel)
.build();
// 使用文档加载器加载所有Markdown文档
List<Document> documents = loveAppDocumentLoader.loadMarkdowns();
// //自主切分(不推荐)
// List<Document> splitDocuments = myTokenTextSplitter.splitCustomized(documents);
// // 自动补充关键词元信息(可选)
// List<Document> enrichedDocuments = myKeywordEnricher.enrichDocuments(documents);
// 将加载的文档添加到向量存储中,文档会被自动转换为向量并建立索引
simpleVectorStore.add(enrichedDocuments);
// 返回配置好的向量存储实例
return simpleVectorStore;
}
方法二:存储在云数据库postgres
大致流程都是一致的,先创建pgVector实例,然后配置参数,将文档添加进去(注意,这里的pgsql是云数据库,因此添加文档只要执行一次即可,否则每次都添加相同的文档)
@Resource
private LoveAppDocumentLoader loveAppDocumentLoader;
/**
* 创建并配置PgVector向量存储Bean
*
* @param jdbcTemplate Spring JDBC模板,用于数据库操作
* @param dashscopeEmbeddingModel 嵌入模型,用于将文本转换为向量表示
* @return 配置好的VectorStore实例
*/
@Bean
public VectorStore pgVectorVectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel dashscopeEmbeddingModel) {
// 使用建造者模式创建PgVectorStore实例
VectorStore vectorStore = PgVectorStore.builder(jdbcTemplate, dashscopeEmbeddingModel)
.dimensions(1536) // 可选: 向量维度,默认为模型尺寸或1536
.distanceType(COSINE_DISTANCE) // 可选:向量相似度计算方式,默认为COSINE_DISTANCE(余弦距离)
.indexType(HNSW) // 可选:向量索引类型,默认为HNSW(分层可导航小世界算法)
.initializeSchema(true) // 可选:是否自动初始化数据库表结构,默认为false
.schemaName("public") // 可选:数据库schema名称,默认为"public"
.vectorTableName("vector_store") // 可选:向量存储表名,默认为"vector_store"
.maxDocumentBatchSize(10000) // 可选:批量处理文档的最大数量,默认为10000
.build();
// // 可选择加载本地文档
// List<Document> documents = loveAppDocumentLoader.loadMarkdowns();
// vectorStore.add(documents);
return vectorStore;
}
方法三:使用阿里云百炼内置向量存储
使用阿里云百炼创建知识库的时候直接选用内置的向量存储。。。
这时候已经可以得到一个完整的VectorStore实例,在需要的类里面注入即可,可以在查询增强器里面使用了,可以查看最终查询示例
4)文档检索
这个阶段就是从向量数据库中检索需要的文档的阶段,也是最重要的一个阶段
整个文档过滤检索阶段拆分为:检索前、检索时、检索后
查询重写(可选)
对用户的查询重写和翻译可以使查询更加精确和专业,但是要注意保持查询的语义完整性。
/**
* 查询重写器组件
* 该类负责使用AI模型对用户输入的查询进行重写优化,使其更适合后续的检索或处理。
*/
@Component
public class QueryRewriter {
// 查询转换器接口,用于执行具体的查询重写逻辑
private final QueryTransformer queryTransformer;
//构造函数,通过依赖注入获取ChatModel实例并初始化查询转换器
public QueryRewriter(ChatModel dashscopeChatModel) {
// 使用注入的AI模型构建ChatClient构造器
ChatClient.Builder builder = ChatClient.builder(dashscopeChatModel);
//初始化查询转换器,RewriteQueryTransformer是QueryTransformer的一个具体的实现
queryTransformer = RewriteQueryTransformer.builder()
.chatClientBuilder(builder)
.build();
}
//查询重写
public String doQueryRewrite(String prompt) {
// 原始查询字符串包装成Query对象,然后执行查询重写转换
Query transformedQuery = queryTransformer.transform(new Query(prompt));
// 从重写后的Query对象中提取文本内容并返回
return transformedQuery.text();
}
}
过滤器配置
运用 Spring 内置的文档检索器提供的 filterExpression 配置过滤规则,主要用来跟据元数据信息来过滤部分文档,提升检索质量。
Filter.Expression expression = new FilterExpressionBuilder()
.eq("status", status)
.build();
具体使用可以看这篇【Spring AI】Filter 简单使用_filterexpressionbuilder-CSDN博客
检索器配置
之前我们有了解过 DocumentRetriever 的概念,这是 Spring AI 提供的文档检索器。每种不同的存储方案都可能有自己的文档检索器实现类,比如 VectorStoreDocumentRetriever
,从向量存储中检索与输入查询语义相似的文档。它支持基于元数据的过滤、设置相似度阈值、设置返回的结果数。
// 创建文档检索器,配置检索参数
// VectorStoreDocumentRetriever是DocumentRetriever的一个实现类
DocumentRetriever documentRetriever = VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore) // 设置向量存储源
// .filterExpression(expression) // 设置过滤条件:按状态过滤文档
.similarityThreshold(0.5) // 设置相似度阈值:只返回相似度大于0.5的文档(0-1范围)
.topK(3) // 设置返回文档数量:最多返回3个最相关的文档
.build(); // 构建DocumentRetriever实例
5)查询增强
这个阶段就是将用户的提示词和检索回来的文档进行适当融合,然后再喂给LLM
经过前面的文档检索,系统已经获取了与用户查询相关的文档。此时,大模型需要根据用户提示词和检索内容生成最终回答。然而,返回结果可能仍未达到预期效果,需要进一步优化。
主要有两种增强方式:
QuestionAnswerAdvisor 查询增强
这个增强器简单好用,new QuestionAnswerAdvisor(vectorStore)直接完成
示例
ChatResponse response = ChatClient.builder(chatModel)
.build().prompt()
.advisors(new QuestionAnswerAdvisor(vectorStore))
.user(userText)
.call()
.chatResponse();
RetrievalAugmentationAdvisor 查询增强
这个增强器更加灵活,也很好用
示例代码:
Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
.documentRetriever(VectorStoreDocumentRetriever.builder()
.similarityThreshold(0.50)
.vectorStore(vectorStore)
.build())
.build();
String answer = chatClient.prompt()
.advisors(retrievalAugmentationAdvisor)
.user(question)
.call()
.content();
错误处理机制
使用ContextualQueryAugmenter
在上面的RetrievalAugmentationAdvisor增强查询中,我们可以加入空上下文时的处理
因为空上下文也是一种错误,而且还有其他各种错误,我们干脆新建一个错误处理类工厂:
/**
* 自定义错误处理工厂类
* 工厂模式:负责创建和配置特定的 ContextualQueryAugmenter 实例
*/
public class LoveAppContextualQueryAugmenterFactory {
public static ContextualQueryAugmenter createInstance() {
// 当用户的问题与恋爱主题无关时,使用此模板回复用户
PromptTemplate emptyContextPromptTemplate = new PromptTemplate("""
你应该输出下面的内容:
抱歉,我只能回答恋爱相关的问题,别的没办法帮到您哦,
有问题可以联系管理员
""");
// 使用建造者模式构建并配置 ContextualQueryAugmenter 实例
return ContextualQueryAugmenter.builder()
.allowEmptyContext(false)//不允许空上下文
.emptyContextPromptTemplate(emptyContextPromptTemplate)//设置空上下文时的回复模板
.build();
}
}
然后只需要在RetrievalAugmentationAdvisor构建时引入即可(最终查询示例里面用到)
// 创建并返回检索增强顾问
// 该顾问将在AI生成回答时,自动从vectorStore中检索相关文档作为上下文
return RetrievalAugmentationAdvisor.builder()
.documentRetriever(documentRetriever) // 设置文档检索器
.queryAugmenter(LoveAppContextualQueryAugmenterFactory.createInstance())//设置空上下文时的回复
.build(); // 构建RetrievalAugmentationAdvisor实例
最终查询示例
写一个工厂类 LoveAppRagCustomAdvisorFactory,根据用户查询需求生成对应的 advisor:
1.先是配置过滤器Filter,具体使用很简单,可以看我另一篇文章【Spring AI】Filter 简单使用_filterexpressionbuilder-CSDN博客
2.创建文档检索器VectorStoreDocumentRetriever,设置向量存储vectorStore、相似度阈值、返回的文档数量
3.创建检索/查询增强顾问RetrievalAugmentationAdvisor,设置文档检索器、设置空上下文时的回复,最后构建,返回一个Advisors
/**
* 创建自定义的 RAG 检索增强顾问的工厂
* 工厂模式
*/
@Slf4j
public class LoveAppRagCustomAdvisorFactory {
/**
* 创建LoveApp自定义RAG检索增强顾问
* 该方法用于构建一个根据状态过滤的文档检索增强顾问
*
* @param vectorStore 向量存储实例,用于文档检索
* @param filename 文档过滤条件,用于筛选特定条件的文档
* @return 配置好的RetrievalAugmentationAdvisor实例,可用于增强AI对话的检索能力
*/
public static Advisor createLoveAppRagCustomAdvisor(VectorStore vectorStore, String filename) {
// 构建过滤表达式:只检索filename字段
Filter.Expression expression = new FilterExpressionBuilder()
.nin("filename", filename) // 添加不包含条件:字段"filename"的值不能包含参数filename
.build(); // 构建过滤表达式
// 创建文档检索器,配置检索参数
// VectorStoreDocumentRetriever是DocumentRetriever的一个实现类
DocumentRetriever documentRetriever = VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore) // 设置向量存储源
// .filterExpression(expression) // 设置过滤条件:按状态过滤文档
.similarityThreshold(0.5) // 设置相似度阈值:只返回相似度大于0.5的文档(0-1范围)
.topK(3) // 设置返回文档数量:最多返回3个最相关的文档
.build(); // 构建DocumentRetriever实例
// 创建并返回检索增强顾问
// 该顾问将在AI生成回答时,自动从vectorStore中检索相关文档作为上下文
return RetrievalAugmentationAdvisor.builder()
.documentRetriever(documentRetriever) // 设置文档检索器
.queryAugmenter(LoveAppContextualQueryAugmenterFactory.createInstance())//设置空上下文时的回复
.build(); // 构建RetrievalAugmentationAdvisor实例
}
}
通过ChatClient与LLM交互
实际应用App类,调用下面的doChatWithRag,
1.先是执行构造器App,使用建造者模式创建一个ChatClient对象,配置好默认的系统提示词、开启自定义日志(这个自己写,就不细说了)
2.chatClient初始化完成后,可以选择将用户的提示词重写一遍,然后开始配置详细调用详细,比如添加用户提示词.user()、配置顾问.advisors(如对话记忆顾问、RAG 检索增强服务顾问),然后通过call()方法调用大模型
3.将返回的信息chatResponse 转化为String类型,最后返回给用户
private final ChatClient chatClient;
/**
* 构造器会自动初始化
* 这部分代码会在 Spring 创建 Bean 时执行
*/
public App(ChatModel dashscopeChatModel) {
chatClient = ChatClient.builder(dashscopeChatModel)
.defaultSystem(SYSTEM_PROMPT)//默认系统提示词(自己写)
.defaultAdvisors(
// 自定义日志 Advisor,可按需开启
new MyLoggerAdvisor()
// // 自定义推理增强 Advisor,可按需开启
// new ReReadingAdvisor()
)
.build();
}
/**
* 基于RAG本地知识库的多轮对话
*/
public String doChatWithRag(String message, String chatId) {
//查询重写
String rewrittenMessage = queryRewriter.doQueryRewrite(message);
ChatResponse chatResponse = chatClient
.prompt()
.user(rewrittenMessage)//添加用户提示词(问题)
.advisors(spec -> spec
.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)// 对话ID
.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10)// 检索的历史消息数量
)//配置对话记忆顾问
// .advisors(new QuestionAnswerAdvisor(loveAppVectorStore))// 1.应用本地知识库问答
// .advisors(new QuestionAnswerAdvisor(pgVectorVectorStore))// 2.应用 RAG 检索增强服务(基于 PgVector 向量存储)
.advisors(LoveAppRagCustomAdvisorFactory.createLoveAppRagCustomAdvisor(
loveAppVectorStore, "过滤条件xxx")
)// 3.应用自定义的 RAG 检索增强服务
.call()
.chatResponse();
chatClient.prompt().call().content();
String content = chatResponse.getResult().getOutput().getText();
return content;
}
测试
/**
* 测试基于RAG本地知识库的问答
*/
@Test
void doChatWithRag() {
String chatId = UUID.randomUUID().toString();
String message = "你好,我是程序员yunikno,我已经结婚了,但是婚后关系不太亲密,怎么办?";
String answer = loveApp.doChatWithRag(message, chatId);
Assertions.assertNotNull(answer);
}
我们可以看到每个文档被切成15份,并且元数据也符合我们的配置filenname
接下来看用户请求request的处理前的文本userText,可以看到已经将检索回来的文档加入到用户的提示词中(对比了原文档内容基本一致),并且用户的提示词也被重写(变成了“婚后关系不亲密怎么版?”)
再看看最终回复,基本跟知识库提供的信息一致
看到这里了,如果对你有帮助,可以点个赞么~

为武汉地区的开发者提供学习、交流和合作的平台。社区聚集了众多技术爱好者和专业人士,涵盖了多个领域,包括人工智能、大数据、云计算、区块链等。社区定期举办技术分享、培训和活动,为开发者提供更多的学习和交流机会。
更多推荐
所有评论(0)