RAG增强
由于大模型训练比较耗时,且训练语料本身存在滞后性问题,所以提问大模型问题,可能会出现两个问题
- 知识数据比较落后,往往是几个月之前的
- 不包含太过专业领域或者企业私有的数据
比如让本地的大模型背诵《出师表》

很明显大模型回答的不对。再或者问大模型比较专业的知识比如:
医疗用毒性药品A 型肉毒毒素的管理

这里生成的结果问题比较多,如“处方需留存至少 5 年“,根据《处方管理办法》,医疗用毒性药品处方的保存期限为2 年。还有“零售药店除非持有特殊资质(如部分特殊药品的零售许可)可销售”这一描述也有问题,正确的是根据《医疗用毒性药品管理办法》,医疗用毒性药品的零售有严格限制,而A 型肉毒毒素明确禁止零售(无论是否有 “特殊资质”),仅能由具备资质的医疗机构凭处方使用,零售药店一律不得销售。
解决方案也很简单。
我们可以给大模型外挂个知识库进行解决不过,知识库不能简单的直接拼接在提示词中。因为通常知识库数据量都是非常大的,而大模型的上下文是有大小限制的这时候就需要用到了向量模型。
向量模型
先说说向量,向量是空间中有方向和长度的量,空间可以是二维,也可以是多维。
向量既然是在空间中,两个向量之间就一定能计算距离。
我们以二维向量为例,向量之间的距离有两种计算方法:
通常,两个向量之间欧式距离越近,我们认为两个向量的相似度越高。(余弦距离相反,越大相似度越高)
所以,如果我们能把文本转为向量,就可以通过向量距离来判断文本的相似度了。
现在,有不少的专门的向量模型,就可以实现将文本向量化。一个好的向量模型,就是要尽可能让文本含义相似的向量,在空间中距离更近:
接下来,我们就准备一个向量模型,用于将文本向量化。
阿里云百炼平台就提供了这样的模型:
这里我们选择通用文本向量-v3,这个模型兼容OpenAI,所以我们依然采用OpenAI的配置。
修改application.yaml,添加向量模型配置:
openai:
base-url: https://dashscope.aliyuncs.com/compatible-mode # OpenAI 服务的访问地址,这里使用的第三方代理商:智增增
api-key: ${BAILIAN_API_KEY} # 阿里云的 API Key,
chat:
options:
model: qwen-plus # 模型名称
temperature: 0.7 # 温度值
embedding:
options:
model: text-embedding-v4
dimensions: 1024
接下来,我们就来测试下阿里百炼提供的向量大模型好不好用。
首先,我们在项目中写一个工具类,用以计算向量之间的欧氏距离和余弦距离。
package xiaojiuaijc.top.xiaoxiaojiuai.utils;
public class VectorDistanceUtils {
// 防止实例化
private VectorDistanceUtils() {}
// 浮点数计算精度阈值
private static final double EPSILON = 1e-12;
/**
* 计算欧氏距离
* @param vectorA 向量A(非空且与B等长)
* @param vectorB 向量B(非空且与A等长)
* @return 欧氏距离
* @throws IllegalArgumentException 参数不合法时抛出
*/
public static double euclideanDistance(float[] vectorA, float[] vectorB) {
validateVectors(vectorA, vectorB);
double sum = 0.0;
for (int i = 0; i < vectorA.length; i++) {
double diff = vectorA[i] - vectorB[i];
sum += diff * diff;
}
return Math.sqrt(sum);
}
/**
* 计算余弦距离
* @param vectorA 向量A(非空且与B等长)
* @param vectorB 向量B(非空且与A等长)
* @return 余弦距离,范围[0, 2]
* @throws IllegalArgumentException 参数不合法或零向量时抛出
*/
public static double cosineDistance(float[] vectorA, float[] vectorB) {
validateVectors(vectorA, vectorB);
double dotProduct = 0.0;
double normA = 0.0;
double normB = 0.0;
for (int i = 0; i < vectorA.length; i++) {
dotProduct += vectorA[i] * vectorB[i];
normA += vectorA[i] * vectorA[i];
normB += vectorB[i] * vectorB[i];
}
normA = Math.sqrt(normA);
normB = Math.sqrt(normB);
// 处理零向量情况
if (normA < EPSILON || normB < EPSILON) {
throw new IllegalArgumentException("Vectors cannot be zero vectors");
}
// 处理浮点误差,确保结果在[-1,1]范围内
double similarity = dotProduct / (normA * normB);
similarity = Math.max(Math.min(similarity, 1.0), -1.0);
return similarity;
}
// 参数校验统一方法
private static void validateVectors(float[] a, float[] b) {
if (a == null || b == null) {
throw new IllegalArgumentException("Vectors cannot be null");
}
if (a.length != b.length) {
throw new IllegalArgumentException("Vectors must have same dimension");
}
if (a.length == 0) {
throw new IllegalArgumentException("Vectors cannot be empty");
}
}
}
@Test
public void testEmbedding() {
// 1.测试数据
// 1.1.用来查询的文本,国际冲突
String query = "牙膏";
// 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));
}
}
然后进行简单的测试可以看出牙膏和牙刷的关联度是比较高的,其他的比较低。

有了比较文本相似度的办法,知识库的问题就可以解决了。
前面说了,知识库数据量很大,无法全部写入提示词。但是庞大的知识库中与用户问题相关的其实并不多。
所以,我们需要想办法从庞大的知识库中找到与用户问题相关的一小部分,组装成提示词,发送给大模型就可以了。
现在,利用向量大模型就可以帮助我们比较文本相似度。
但是新的问题来了:向量模型是帮我们生成向量的,如此庞大的知识库,谁来帮我们从中比较和检索数据呢?
这就需要用到向量数据库了。
向量数据库
向量数据库的主要作用有两个:
- 存储向量数据
- 基于相似度检索数据
刚好符合我们的需求。
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,因此操作方式一模一样,大家学会任意一个,其它就都不是问题。
这里我使用SimpleVectorStore进行测试
首先加入依赖
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
<version>${spring-ai.version}</version>
</dependency>
然后编写代码进行测试
@Test
public void testVectorStore(){
Resource resource = new FileSystemResource("2025年《药事管理与法规》三色笔记.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("医疗用毒性药品A 型肉毒毒素的管理")
.topK(1)
.similarityThreshold(0.6)
.filterExpression("file_name == '2025年《药事管理与法规》三色笔记.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());
}
}

这样我们就根据RAG正确检索到了想要的知识,在和大模型进行交互的时候可以吧这个传给大模型,让大模型回答的更精确
让我们梳理一下要解决的问题和解决思路:
- 要解决大模型的知识限制问题,需要外挂知识库
- 受到大模型上下文限制,知识库不能简单的直接拼接在提示词中
- 我们需要从庞大的知识库中找到与用户问题相关的一小部分,再组装成提示词
- 这些可以利用文档读取器、向量大模型、向量数据库来解决。
所以RAG要做的事情就是将知识库分割,然后利用向量模型做向量化,存入向量数据库,然后查询的时候去检索:
第一阶段(存储知识库):
- 将知识库内容切片,分为一个个片段
- 将每个片段利用向量模型向量化
- 将所有向量化后的片段写入向量数据库
第二阶段(检索知识库):
- 每当用户询问AI时,将用户问题向量化
- 拿着问题向量去向量数据库检索最相关的片段
第三阶段(对话大模型):
- 将检索到的片段、用户的问题一起拼接为提示词
- 发送提示词给大模型,得到响应


Comments NOTHING