RAG知识库
由于训练大模型非常耗时,再加上训练语料本身比较滞后,所以大模型存在知识限制问题:
-
知识数据比较落后,往往是几个月之前的
-
不包含太过专业领域或者企业私有的数据
为了解决这些问题,我们就需要用到RAG了。下面我们简单回顾下RAG原理
1. RAG原理
要解决大模型的知识限制问题,其实并不复杂。
解决的思路就是给大模型外挂一个知识库,可以是专业领域知识,也可以是企业私有的数据。
不过,知识库不能简单的直接拼接在提示词中。
因为通常知识库数据量都是非常大的,而大模型的上下文是有大小限制的,早期的GPT上下文不能超过2000token,现在也不到200k token,因此知识库不能直接写在提示词中。
怎么办?
思路很简单,庞大的知识库中与用户问题相关的其实并不多。
所以,我们需要想办法从庞大的知识库中找到与用户问题相关的一小部分,组装成提示词,发送给大模型就可以了。
那么问题来了,我们该如何从知识库中找到与用户问题相关的内容呢?
可能有同学会相到全文检索,但是在这里是不合适的,因为全文检索是文字匹配,这里我们要求的是内容上的相似度。
而要从内容相似度来判断,这就不得不提到向量模型的知识了。
1.1.向量模型
先说说向量,向量是空间中有方向和长度的量,空间可以是二维,也可以是多维。
向量既然是在空间中,两个向量之间就一定能计算距离。
我们以二维向量为例,向量之间的距离有两种计算方法:

通常,两个向量之间欧式距离越近,我们认为两个向量的相似度越高。(余弦距离相反,越大相似度越高)
所以,如果我们能把文本转为向量,就可以通过向量距离来判断文本的相似度了。
现在,有不少的专门的向量模型,就可以实现将文本向量化。一个好的向量模型,就是要尽可能让文本含义相似的向量,在空间中距离更近:

接下来,我们就准备一个向量模型,用于将文本向量化。
阿里云百炼平台就提供了这样的模型:

这里我们选择通用文本向量-v3,这个模型兼容OpenAI,所以我们依然采用OpenAI的配置。
修改application.yaml,添加向量模型配置:
spring:
application:
name: ai-demo
ai:
openai:
base-url: https://dashscope.aliyuncs.com/compatible-mode
api-key: ${OPENAI_API_KEY}
chat:
options:
model: qwen-max # 模型名称
temperature: 0.8 # 模型温度,值越大,输出结果越随机
embedding: #添加的向量模型
options:
model: text-embedding-v3
dimensions: 1024
写一个测试了来看看成功没
package com.ai;
import com.itheima.ai.util.VectorDistanceUtils;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiEmbeddingModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Arrays;
import java.util.List;
@SpringBootTest
class AiDemoApplicationTests {
// 自动注入向量模型
@Autowired
private OpenAiEmbeddingModel embeddingModel;
@Test
public void testEmbedding() {
// 1.测试数据
// 1.1.用来查询的文本,国际冲突
String query = "global conflicts";
// 1.2.用来做比较的文本
String[] texts = new String[]{
"哈马斯称加沙下阶段停火谈判仍在进行 以方尚未做出承诺",
"土耳其、芬兰、瑞典与北约代表将继续就瑞典“入约”问题进行谈判",
"日本航空基地水井中检测出有机氟化物超标",
"国家游泳中心(水立方):恢复游泳、嬉水乐园等水上项目运营",
"我国首次在空间站开展舱外辐射生物学暴露实验",
};
// 2.向量化
// 2.1.先将查询文本向量化
float[] queryVector = embeddingModel.embed(query);
// 2.2.再将比较文本向量化,放到一个数组
List<float[]> textVectors = embeddingModel.embed(Arrays.asList(texts));
// 3.比较欧氏距离
// 3.1.把查询文本自己与自己比较,肯定是相似度最高的
System.out.println(VectorDistanceUtils.euclideanDistance(queryVector, queryVector));
// 3.2.把查询文本与其它文本比较
for (float[] textVector : textVectors) {
System.out.println(VectorDistanceUtils.euclideanDistance(queryVector, textVector));
}
System.out.println("------------------");
// 4.比较余弦距离
// 4.1.把查询文本自己与自己比较,肯定是相似度最高的
System.out.println(VectorDistanceUtils.cosineDistance(queryVector, queryVector));
// 4.2.把查询文本与其它文本比较
for (float[] textVector : textVectors) {
System.out.println(VectorDistanceUtils.cosineDistance(queryVector, textVector));
}
}
}
运行结果:
0.0
1.0722205301828829
1.0844350869313875
1.1185223356097924
1.1693257901084286
1.1499045763089124
------------------
0.9999999999999998
0.4251716163869882
0.41200032867283726
0.37445397231274447
0.3163386320532005
0.3388597327534832
可以看到,向量相似度确实符合我们的预期。
OK,有了比较文本相似度的办法,知识库的问题就可以解决了。
前面说了,知识库数据量很大,无法全部写入提示词。但是庞大的知识库中与用户问题相关的其实并不多。
所以,我们需要想办法从庞大的知识库中找到与用户问题相关的一小部分,组装成提示词,发送给大模型就可以了。
现在,利用向量大模型就可以帮助我们比较文本相似度。
但是新的问题来了:向量模型是帮我们生成向量的,如此庞大的知识库,谁来帮我们从中比较和检索数据呢?
这就需要用到向量数据库了。
1.2.向量数据库
向量数据库的主要作用有两个:
-
存储向量数据
-
基于相似度检索数据
刚好符合我们的需求。
SpringAI支持很多向量数据库,并且都进行了封装,可以用统一的API去访问:
-
Azure Vector Search - The Azure vector store.
-
Apache Cassandra - The Apache Cassandra vector store.
-
Chroma Vector Store - The Chroma vector store.
-
Elasticsearch Vector Store - The Elasticsearch vector store.
-
GemFire Vector Store - The GemFire vector store.
-
MariaDB Vector Store - The MariaDB vector store.
-
Milvus Vector Store - The Milvus vector store.
-
MongoDB Atlas Vector Store - The MongoDB Atlas vector store.
-
Neo4j Vector Store - The Neo4j vector store.
-
OpenSearch Vector Store - The OpenSearch vector store.
-
Oracle Vector Store - The Oracle Database vector store.
-
PgVector Store - The PostgreSQL/PGVector vector store.
-
Pinecone Vector Store - PineCone vector store.
-
Qdrant Vector Store - Qdrant vector store.
-
Redis Vector Store - The Redis vector store.
-
SAP Hana Vector Store - The SAP HANA vector store.
-
Typesense Vector Store - The Typesense vector store.
-
Weaviate Vector Store - The Weaviate vector store.
-
SimpleVectorStore - A simple implementation of persistent vector storage, good for educational purposes.
这些库都实现了统一的接口:VectorStore,因此操作方式一模一样,大家学会任意一个,其它就都不是问题。
不过,除了最后一个库以外,其它所有向量数据库都是需要安装部署的。每个企业用的向量库都不一样,这里我就不一一演示了。
1.2.1.SimpleVectorStore
想用这个需要添加一个VectorStore的Bean:
还有引入一个依赖
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-vector-store</artifactId>
<version>1.0.0</version>
</dependency>
@Bean
public VectorStore vectorStore(OpenAiEmbeddingModel embeddingModel) {
return SimpleVectorStore.builder(embeddingModel).build();
}
1.2.2.VectorStore接口
接下来,你就可以使用VectorStore中的各种功能了,可以参考SpringAI官方文档:
Vector Databases :: Spring AI Reference
这是VectorStore中声明的方法:
public interface VectorStore extends DocumentWriter {
default String getName() {
return this.getClass().getSimpleName();
}
// 保存文档到向量库
void add(List<Document> documents);
// 根据文档id删除文档
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();
}
}
注意,VectorStore操作向量化的基本单位是Document,我们在使用时需要将自己的知识库分割转换为一个个的Document,然后写入VectorStore.
那么问题来了,我们该如何把各种不同的知识库文件转为Document呢?
1.3.文件的读取和转换
前面说过,知识库太大,是需要拆分成文档片段,然后再做向量化的。而且SpringAI中向量库接收的是Document类型的文档,也就是说,我们处理文档还要转成Document格式。
不过,文档读取、拆分、转换的动作并不需要我们亲自完成。在SpringAI中提供了各种文档读取的工具,可以参考官网:
ETL Pipeline :: Spring AI Reference
比如PDF文档读取和拆分,SpringAI提供了两种默认的拆分原则:
-
PagePdfDocumentReader:按页拆分,推荐使用 -
ParagraphPdfDocumentReader:按pdf的目录拆分,不推荐,因为很多PDF不规范,没有章节标签
当然,大家也可以自己实现PDF的读取和拆分功能。
这里我们选择使用PagePdfDocumentReader。
首先,我们需要在pom.xml中引入依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
然后就可以利用工具把PDF文件读取并处理成Document了。
我们写一个单元测试(别忘了配置API_KEY):
@Test
public void testVectorStore(){
Resource resource = new FileSystemResource("中二知识笔记.pdf");
// 1.创建PDF的读取器
PagePdfDocumentReader reader = new PagePdfDocumentReader(
resource, // 文件源
PdfDocumentReaderConfig.builder()
.withPageExtractedTextFormatter(ExtractedTextFormatter.defaults())
.withPagesPerDocument(1) // 每1页PDF作为一个Document
.build()
);
// 2.读取PDF文档,拆分为Document
List<Document> documents = reader.read();
// 3.写入向量库
vectorStore.add(documents);
// 4.搜索
SearchRequest request = SearchRequest.builder()
.query("论语中教育的目的是什么")
.topK(1)
.similarityThreshold(0.6)
.filterExpression("file_name == '中二知识笔记.pdf'")
.build();
List<Document> docs = vectorStore.similaritySearch(request);
if (docs == null) {
System.out.println("没有搜索到任何内容");
return;
}
for (Document doc : docs) {
System.out.println(doc.getId());
System.out.println(doc.getScore());
System.out.println(doc.getText());
}
}
1.4.原理总结
OK,现在我们有了这些工具:
-
PDFReader:读取文档并拆分为片段
-
向量大模型:将文本片段向量化
-
向量数据库:存储向量,检索向量
让我们梳理一下要解决的问题和解决思路:
-
要解决大模型的知识限制问题,需要外挂知识库
-
受到大模型上下文限制,知识库不能简单的直接拼接在提示词中
-
我们需要从庞大的知识库中找到与用户问题相关的一小部分,再组装成提示词
-
这些可以利用文档读取器、向量大模型、向量数据库来解决。
所以RAG要做的事情就是将知识库分割,然后利用向量模型做向量化,存入向量数据库,然后查询的时候去检索:
第一阶段(存储知识库):
-
将知识库内容切片,分为一个个片段
-
将每个片段利用向量模型向量化
-
将所有向量化后的片段写入向量数据库
第二阶段(检索知识库):
-
每当用户询问AI时,将用户问题向量化
-
拿着问题向量去向量数据库检索最相关的片段
第三阶段(对话大模型):
-
将检索到的片段、用户的问题一起拼接为提示词
-
发送提示词给大模型,得到响应

2.配置ChatClient
接下来就是最后的环节了,实现RAG的对话流程。
理论上来说,我们每次与AI对话的完整流程是这样的:
-
将用户的问题利用向量大模型做向量化 OpenAiEmbeddingModel
-
去向量数据库检索相关的文档 VectorStore
-
拼接提示词,发送给大模型
-
解析响应结果
不过,SpringAI同样基于AOP技术帮我们完成了全部流程,用到的是一个名QuestionAnswerAdvisor的Advisor。我们只需要把VectorStore配置到Advisor即可。
给ChatPDF也单独定义一个ChatClient:
先映入pom.xml依赖
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>
@Bean
public ChatClient pdfChatClient(OpenAiChatModel model, ChatMemory chatMemory, VectorStore vectorStore) {
return ChatClient
.builder(model)
.defaultSystem("请根据提供的上下文回答问题,不要自己猜测。")
.defaultAdvisors(
new SimpleLoggerAdvisor(),//配置日志
MessageChatMemoryAdvisor.builder(chatMemory).build(),//会话记忆
QuestionAnswerAdvisor
.builder(vectorStore)// 向量库
.searchRequest(SearchRequest.builder()// 向量检索的请求参数
.similarityThreshold(0.6)// 相似度阈值
.topK(2)// 返回的文档片段数量
.build())
.build()
)
.build();
}
3.对话接口
@RequestMapping(value = "/chat", produces = "text/html;charset=UTF-8")
public Flux<String> chat(String prompt, String chatId) {
chatRepository.addChatId("pdf", chatId);
Resource file = fileRepository.getFile(chatId);
return pdfChatClient
.prompt(prompt)
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId))
.advisors(a -> a.param(QuestionAnswerAdvisor.FILTER_EXPRESSION, "file_name == '"+file.getFilename()+"'"))
.stream()
.content();
}
这样我们就能正确的使用上向量数据库,跟向量模型了,ai的回答,也会根据向量库里面去查找出来。
更多推荐


所有评论(0)