代码仓库地址:https://github.com/Liucc-123/ai-agent
项目目标:通过项目实战掌握AI 大模型应用开发的常见知识,包括有:掌握AI 应用平台使用、AI 大模型接入、开发框架(Spring AI + LangChain4j)、本地部署、Prompt 工程、多模态特性、RAG 知识库、工具调用、MCP 服务开发、智能体原理和开发、服务化部署等技术。
本节重点
通过给AI恋爱大师应用添加知识库问答功能,入门并实战RAG知识库场景相关知识,掌握基于SpringAI框架实现RAG的两种方式。
具体内容包括有:
- AI恋爱知识问答需求分析
- RAG概念(重点是理解RAG核心步骤)
- 实战:SpringAI+本地知识库
- 实战:SpringAI+云知识库
一、AI 知识问答需求分析
AI知识问答应用场景
首先大模型本身就拥有一些通用的知识,对于不知道的知识,也可通过互联网进行获取。但这些知识都是公开的,对于企业来说,数据就是石油、数据就是命脉,不会轻易暴漏到公共空间里。那么AI又该怎么获取这些知识呢?通过知识库+RAG来为企业应用进行赋能。
如果不给 AI 提供特定领域的知识库,AI 可能会面临这些问题:
- 知识有限:AI 不知道你的最新课程和内容
- 编故事:当 AI 不知道答案时,它可能会 “自圆其说” 编造内容
- 无法个性化:不了解你的特色服务和回答风格
- 不会推销:不知道该在什么时候推荐你的付费课程和服务
那么如何让 AI 利用自己的知识库进行问答呢?这就需要用到 AI 主流的技术 —— RAG。
二、RAG 概念
什么是RAG?
RAG(Retrieval-Augmented Generation,检索增强生成)是一种信息检索和内容生成的混合架构,可以解决大模型的知识时效性和幻觉问题。
从技术上看,RAG在大模型回复内容之前,会从外部知识库中检索用户相关问题作为上下文信息传递给大模型,使得大模型能够回复更相关、更准确的内容。
通过 RAG 技术改造后,AI 就能:
- 准确回答关于特定内容的问题
- 在合适的时机推荐相关课程和服务
- 用特定的语气和用户交流
- 提供更新、更准确的建议
RAG工作流程
RAG 技术实现主要包含以下 4 个核心步骤:
- 文档收集和切割
- 向量转换和存储
- 文档过滤和检索
- 查询增强和关联
1、文档收集和切割
文档收集:从各种来源(网页、PDF、数据库等)收集原始文档
文档预处理:清洗、标准化文本格式
文档切割:将长文档切割为适当大小的文本片段(这一过程也成为chunks),常有以下几种方式:
- 基于固定大小(比如512个token)
- 基于语义边界(比如基于一级标题、段落)
- 基于递归分割策略(如递归字符 n-gram 切割)
2、向量转换和存储
向量转换:使用Embedding模型将文本块转换为高维向量数据,可以捕获到文本的语义特征
向量存储:将生成的向量和对应文本存入到向量数据库中,支持高效的相似性搜索。
Embedding 模型的作用就是将原本非结构化的文本数据映射到一个高维的、连续的向量空间中。在这个空间里,语义相似的文本(或词语)会彼此靠近,而语义不相似的则会距离较远。这就是为什么 Embedding 能够“捕获到文本的语义特征”。
3、文档过滤和检索
查询处理:将用户问题也转换为向量表示
过滤机制:基于元数据、关键词或自定义规则进行过滤
相似度搜索:在向量数据库中根据查找与问题向量最相似的文档块,常用的相似度搜索算法有余弦相似度、欧氏距离等
组装上下文:将检索到的多个文档块组装成连贯上下文
4、查询增强和关联
提示词组装:将检索到的相关文档切片及用户query组装成增强提示词发送给大模型
上下文融合:大模型基于增强提示词生成回答
源引用:在回答中添加信息来源引用
后处理:格式化、摘要或其他处理以优化最终输出
完整的RAG工作流程
RAG相关概念
Embedding 和 Embedding 模型
Embedding 嵌入是将高维离散数据(如文字、图片)转换为低维连续向量的过程。这些向量能在数学空间中表示原始数据的语义特征,使计算机能够理解数据间的相似性。
Embedding 模型是执行这种转换算法的机器学习模型,如 Word2Vec(文本)、ResNet(图像)等。不同的 Embedding 模型产生的向量表示和维度数不同,一般维度越高表达能力更强,可以捕获更丰富的语义信息和更细微的差别,但同样占用更多存储空间。
向量数据库
向量数据库是专门存储和检索向量数据的数据库系统。通过高效索引算法实现快速相似性搜索,支持 K 近邻查询等操作。
注意,并不是只有向量数据库才能存储向量数据,只不过与传统数据库不同,向量数据库优化了高维向量的存储和检索。
AI 的流行带火了一波向量数据库和向量存储,比如 Milvus、Pinecone 等。此外,一些传统数据库也可以通过安装插件实现向量存储和检索,比如 PGVector、Redis Stack 的 RediSearch 等。
用一张图来了解向量数据库的分类:
召回
召回是信息检索的第一阶段,目标是从大规模数据集中快速筛选出可能相关的候选项子集。强调速度和广度,而非精确度。为后续粗略排序和精细排序提供候选集。
精排和 Rank 模型
精排(精确排序)是搜索 / 推荐系统的最后阶段,使用计算复杂度更高的算法,考虑更多特征和业务规则,对少量候选项进行更复杂、精细的排序
比如,短视频推荐先通过召回获取数万个可能相关视频,再通过粗排缩减至数百条,最后精排阶段会考虑用户最近的互动、视频热度、内容多样性等复杂因素,确定最终展示的 10 个视频及顺序。
Rank 模型(排序模型)负责对召回阶段筛选出的候选集进行精确排序,考虑多种特征评估相关性。
现代 Rank 模型通常基于深度学习,如 BERT、LambdaMART 等,综合考虑查询与候选项的相关性、用户历史行为等因素。举个例子,电商推荐系统会根据商品特征、用户偏好、点击率等给每个候选商品打分并排序。
混合检索策略
混合检索策略结合多种检索方法的优势,提高搜索效果。常见组合包括关键词检索、语义检索、知识图谱等。
比如在 AI 大模型开发平台 Dify 中,就为用户提供了 “基于全文检索的关键词搜索 + 基于向量检索的语义检索” 的混合检索策略,用户还可以自己设置不同检索方式的权重。
在了解了RAG的基本概念后,我们可以通过程序来开发实现RAG。想要在程序中让 AI 使用知识库,首先建议利用一个 AI 开发框架,比如 Spring AI;然后可以通过 2 种模式进行开发 —— 基于本地知识库或云知识库服务实现 RAG。下面分别实现这两种方式。
三、实战:SpringAI+本地知识库
Spring AI 框架为我们实现 RAG 提供了全流程的支持,我们参考标准的RAG开发步骤并进行一定的简化,来实现基于本地知识库的AI恋爱知识问答应用。
标准的RAG开发步骤:
- 文档手机和切割(chunk)
- 向量转换和存储
- 切片过滤和检索
- 查询增强和关联
简化后的RAG步骤:
- 文档准备
- 文档读取
- 向量转换和存储
- 查询增强
1、文档准备
首先准备给AI知识库提供知识的文档,推荐使用markdown格式的,尽量结构化。
这里准备了三篇《恋爱常见问题和回答》文档,附件如下:
在工程resources
目录下新建document
文件夹,将知识库文档放到此文件夹下,如下:
我们在学习RAG的过程中,也可以通过AI来为我们生成markdown文档,我的提示此如下:
帮我生成 3 篇 Markdown 文章,主题是【恋爱常见问题和回答】,3 篇文章的问题分别针对单身、恋爱、已婚的状态,内容形式为 1 问 1 答,每个问题标题使用 4 级标题,每篇内容需要有至少 5 个问题,要求每个问题中推荐一个相关的课程,课程链接都是 https://www.codefather.cn
2、文档读取
首先,我们要对自己准备好的知识库文档进行处理,然后保存到向量数据库中。这个过程俗称 ETL(抽取、转换、加载),Spring AI 提供了对 ETL 的支持,参考 官方文档。
ETL管道的三大核心组件:
DocumentReader
实现Supplier<List<Document>>
,提供来自不同来源的文档源。DocumentTransformer
实现Function<List<Document>, List<Document>>
,将一批文档作为处理工作流程的一部分进行转换DocumentWriter
实现了Consumer<List<Document>>
,管理 ETL 过程的最终阶段,准备文档以供存储
Document
类的内容是由 PDF 文件、文本文件和其他文档类型在 DocumentReader
的帮助下创建的。
下面的类图说明了 ETL 接口和实现:
刚开始接触RAG,先对RAG的整体流程与核心组件有了初步的印象即可,下面就准备通过SpringAI读取准备好的Markdown文档,为写入到向量数据库做准备。
1)引入依赖
SpringAI提供了多种DocumentReader,用于读取不同类型的文件
引入MarkdownDocumentReader组件来读取markdown文档
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-markdown-document-reader</artifactId>
<version>1.0.0</version>
</dependency>
2)在根目录下新建 rag
包,编写文档加载器类 LoveAppDocumentLoader
,负责读取所有 Markdown 文档并转换为 Document
列表
@Component
@Slf4j
public class LoveAppDocumentLoader {
private final ResourcePatternResolver resourcePatternResolver;
public LoveAppDocumentLoader(ResourcePatternResolver resourcePatternResolver) {
this.resourcePatternResolver = resourcePatternResolver;
}
public List<Document> loadMarkdowns() {
List<Document> documents = new ArrayList<>();
try {
Resource[] resources = resourcePatternResolver.getResources("classpath:document/*.md");
for (Resource resource : resources) {
MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder()
.withHorizontalRuleCreateDocument(true)
.withIncludeCodeBlock(false)
.withIncludeBlockquote(false)
.withAdditionalMetadata("filename", resource.getFilename())
.build();
MarkdownDocumentReader reader = new MarkdownDocumentReader(resource, config);
List<Document> list = reader.get();
documents.addAll(list);
}
} catch (IOException e) {
log.error("Markdown 文档加载失败", e);
}
return documents;
}
}
在上述代码中,通过<font style="color:rgb(31, 35, 40);">MarkdownDocumentReaderConfig</font>
文档加载配置来指定读取文档的各种细节,比如是否根据水平线来切块、是否包含代码块、是否包含引用块,尤其是通过文件名<font style="color:rgb(31, 35, 40);">filename</font>
作为文档的元信息,可以方便后续知识库实现更精确的检索。
3、向量转换和存储
为了简单起见,我们先使用SpringAI内置的、基于内存读写的向量数据库<font style="color:rgb(31, 35, 40);">SimpleVectorStore</font>
来保存文档。
<font style="color:rgb(31, 35, 40);">SimpleVectorStore</font>
实现了<font style="color:rgb(31, 35, 40);">VectorStore</font>
接口,而<font style="color:rgb(31, 35, 40);">VectorStore</font>
接口继承了<font style="color:rgb(31, 35, 40);">DocumentWriter</font>
接口,所以<font style="color:rgb(31, 35, 40);">SimpleVectorStore</font>
具备文档写入的能力
简单看下<font style="color:rgb(31, 35, 40);">SimpleVectorStore</font>
的源码:在将文档写入到数据库前,会先调用EmbeddingModel将文档转化为向量数据,实际保存到数据库中的是向量类型的数据。
在<font style="color:rgb(31, 35, 40);">rag</font>
包下新建<font style="color:rgb(31, 35, 40);">LoveAppVectorStoreConfig</font>
配置类,注入我们刚刚编写的markdown文档读取器<font style="color:rgb(31, 35, 40);">LoveAppDocumentLoader</font>
对象,向量模型我们就使用阿里提供的<font style="color:rgb(31, 35, 40);">dashscopeEmbeddingModel</font>
,实现向量数据库初始化及保存到数据库中的方法。如下:
/**
* 向量存储配置类
*/
@Configuration
public class LoveAppVectorStoreConfig {
@Resource
private LoveAppDocumentLoader loveAppDocumentLoader;
@Bean
public VectorStore loveAppVectorStore(EmbeddingModel dashscopeEmbeddingModel) {
// 加载文档
List<Document> markdowns = loveAppDocumentLoader.loadMarkdowns();
SimpleVectorStore simpleVectorStore = SimpleVectorStore.builder(dashscopeEmbeddingModel).build();
// 分段文档存储到向量数据库中
simpleVectorStore.add(markdowns);
return simpleVectorStore;
}
}
4、查询增强
Spring AI 通过 Advisor 特性提供了开箱即用的 RAG 功能。主要是 <font style="color:rgb(31, 35, 40);">QuestionAnswerAdvisor</font>
问答拦截器和 <font style="color:rgb(31, 35, 40);">RetrievalAugmentationAdvisor</font>
检索增强拦截器,前者更简单易用、后者更灵活强大。
查询增强的原理其实很简单:向量数据库种存放着AI大模型不知道的知识,当用户问题发送给大模型时,<font style="color:rgb(31, 35, 40);">QuestionAnswerAdvisor</font>
会从向量数据库种查找和用户问题相关的document,相关document会被附加到用户prompt,为AI大模型提供上下文信息,帮助其回答。
查看<font style="color:rgb(31, 35, 40);">QuestionAnswerAdvisor</font>
的源码,可以看到其让大模型基于知识库回答的prompt:
在LoveApp
类,编写基于知识库问答的对话方法,将QuestionAnswerAdvisor
添加到顾问配置里:
@Resource
private VectorStore loveAppVectorStore;
/**
* 基于 RAG 的聊天
*
* @param message
* @param chatId
* @return
*/
public String doChaWithRag(String message, String chatId) {
String content = chatClient.prompt()
.user(message)
.advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)
.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10))
// 开启日志记录顾问
.advisors(new MyLoggerAdvisor())
// 基于本地知识库问答(自动开启RAG功能)
.advisors(new QuestionAnswerAdvisor(loveAppVectorStore))
.call()
.chatResponse()
.getResult()
.getOutput()
.getText();
return content;
}
单元测试
@Test
void doChaWithRagTest() {
String message = "你好,我是tiga";
String chatId = UUID.randomUUID().toString();
loveApp.doChaWithRag(message, chatId);
loveApp.doChaWithRag("我和我的女朋友是异地恋,我该怎么维持这段关系呢?", chatId);
}
在MyLoggerAdvisor
给请求对象request对象打断点可以看到:相关切片文档的metadata及文本内容。命中的是恋爱篇文档、四级目录、目录标题是"1. 如何维系异地恋关系?"
控制台输出用户请求和模型响应:
知识库文档:
可以看到,大模型的确是基于我们上传的文档进行回复的。
四、实战:SpringAI+云知识库
处理本地知识库,还有云知识库的方式,第三方平台会提供很多现成的服务,比如智能切片、元信息管理、程序接入等。缺点就是会有相应的费用,下来进行实战。
1、准备云知识库
1)准备数据。在应用数据导入数据
上传我们的本地文档,等待平台对文档进行解析:
解析成功:
点击“详情”按钮,可以查看解析后的具体内容,但这里知识效果看起来好像就只是把换行符给删掉了???
给文档添加标签,提高后续大模型检索文档的精确度:
这里意思就是大模型除了通过比较向量数据之间的距离来检索和用户问题最相关的文档外,还可以通过标签快速检索到符合条件的文档。
2、进入阿里云百炼平台的知识库,创建知识库,这里选择推荐配置:
:::info
这里的知识库名称很重要,在对接程序时会用到这个值!
:::
3、导入数据到知识库中,我们选择导入所有的文件
导入数据时,选择【智能切分】chunk方式,配置选择默认就行。
解释这里的几种概念:
metadata抽取:用户可以自定义一些切片的元信息,有定义常量、变量、大模型来取值、正则、关键词搜素等方式来给元信息取值,方便大模型进一步检索到文档;
最大分段长度:一个分片中包含的最大内容长度
分段重叠长度:当前分片与上一个分片重叠的文本长度,这样做是为了保证切片之间的内容具备连贯性。
等待导入完成,
点击“查看切片”,如果对默认的切片内容感觉不合理,可以手动调整切片内容。
至此,我们的云知识库就准备完成啦。下来开始编写代码,让程序和云知识库建立联系。
2、RAG开发
有了知识库,接下来就可以用程序进行对接,实现很简单,可以参考SpringAI Alibaba官方文档。
Spring AI Alibaba 利用了 Spring AI 提供的文档检索特性(DocumentRetriever),自定义了一套文档检索的方法,使得程序会调用阿里灵积大模型 API 来从云知识库中检索文档,而不是从内存中检索。
使用下列代码就可以创建一个文档检索器并发起查询:
// 调用大模型的 API
var dashScopeApi = new DashScopeApi("DASHSCOPE_API_KEY");
// 创建文档检索器
DocumentRetriever retriever = new DashScopeDocumentRetriever(dashScopeApi,
DashScopeDocumentRetrieverOptions.builder()
.withIndexName("你的知识库名称")
.build());
// 测试从云知识库中查询
List<Document> documentList = retriever.retrieve(new Query("谁是鱼皮"));
这就需要使用 Spring AI 提供的另一个 RAG Advisor —— RetrievalAugmentationAdvisor 检索增强顾问,可以绑定文档检索器、查询转换器和查询增强器,更灵活地构造查询。
示例代码如下,目前可以先仅作了解即可,:
Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
.queryTransformers(RewriteQueryTransformer.builder()
.chatClientBuilder(chatClientBuilder.build().mutate())
.build())
.documentRetriever(VectorStoreDocumentRetriever.builder()
.similarityThreshold(0.50)
.vectorStore(vectorStore)
.build())
.build();
String answer = chatClient.prompt()
.advisors(retrievalAugmentationAdvisor)
.user(question)
.call()
.content();
1)回归到我们的项目中,先编写一个配置类,用于初始化基于云知识库的检索增强顾问 Bean:
/**
* 基于云知识库的RAG
*/
@Configuration
public class LoveAppRagCloudAdvisorConfig {
@Value("${spring.ai.dashscope.api-key}")
private String dashscopeApiKey;
@Bean
public Advisor loveAppRagCloudAdvisor() {
// 初始化DocumentRetriever对象(这里使用阿里的)
DashScopeApi dashScopeApi = new DashScopeApi(dashscopeApiKey);
DocumentRetriever retriever = new DashScopeDocumentRetriever(dashScopeApi,
DashScopeDocumentRetrieverOptions.builder()
// 注意使用的是知识库名称,而不是知识库id
.withIndexName("恋爱知识问答")
.build());
Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
.documentRetriever(retriever)
.build();
return retrievalAugmentationAdvisor;
}
}
注意:注意使用的是知识库名称,而不是知识库id
2)然后在 LoveApp
中使用该 Advisor:
@Resource
private Advisor loveAppRagCloudAdvisor;
/**
* 基于云知识库的知识问答
* @param message
* @param chatId
* @return
*/
public String doChaWithCloudRag(String message, String chatId) {
String content = chatClient.prompt()
.user(message)
.advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)
.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10))
// 开启日志记录顾问
.advisors(new MyLoggerAdvisor())
// 基于云知识库的增强检索
.advisors(loveAppRagCloudAdvisor)
.call()
.chatResponse()
.getResult()
.getOutput()
.getText();
return content;
}
3)单元测试
@Test
void doChaWithCloudRagTest(){
String message = "你好,我是tiga";
String chatId = UUID.randomUUID().toString();
loveApp.doChaWithCloudRag(message, chatId);
loveApp.doChaWithCloudRag("我和我的女朋友是异地恋,我该怎么维持这段关系呢?", chatId);
}
通过Debug请求,可以发现切片数量有很多,每一个切片有很多的metadata
用户query也被改写为基于检索到的知识库内容进行问答:
至此,我们就学完了 RAG 知识库的基本开发,在下一章节中我们会实战更多RAG的高级特性和最佳实践。
扩展思路
1)利用 RAG 知识库,实现 “通过用户的问题推荐可能的恋爱对象” 功能。
参考思路:新建一个恋爱对象文档,每行数据包含一位用户的基本信息(比如年龄、星座、职业)。
总结
本章节主要学习了:
- RAG的工作流程和原理
- 基于SpringAI+本地知识库实现RAG
- 基于SpringAI+云知识库实现RAG