ChatPDF 知识库
RAG检索增强
由于训练大模型非常耗时,再加上训练语料本身比较滞后,所以大模型存在知识限制问题:
-
知识数据比较落后,往往是几个月之前的;不包含太过专业领域或者企业私有的数据;
-
为了解决这些问题,就需要用到RAG了。
RAG原理
RAG 的核心原理是将检索技术与生成模型相结合,结合外部知识库来检索相关信息来增强模型的输出,其实就是给大模型挂一个知识库
其核心工作流程分为三个阶段:
- 接收请求: 首先,系统接收到用户的请求(例如提出一个问题)
- 信息检索®: 系统从一个大型文档库中检索出与查询最相关的文档片段。这一步的目标是找到那些可能包含答案或相关信息的文档。这里不一定是从向量数据库中检索,但是向量数据库能反应相似度最高的几个文档(比如说法不同,意思相同),而不是精确查找
- 生成增强(A): 将检索到的文档片段与原始查询一起输入到大模型(如chatGPT)中,注意使用合适的提示词,比如原始的问题是XXX,检索到的信息是YY,给大模型的输入应该类似于: 请基于YYY回答XXXX。
- 输出生成(G): 大模型LLM 基于输入的査询和检索到的文档片段生成最终的文本答案,并返回给用户
注意:知识库不能写在提示词中,因为通常知识库数据量都是非常大的,而大模型的上下文是有大小限制的,那怎么办呢?
只要想办法从庞大的知识库中找到与用户问题相关的一小部分,组装成提示词,发送给大模型就可以了;那么该如何从知识库中找到与用户问题相关的内容呢?
- 全文检索?但在这里是不合适的,因为全文检索是文字匹配,而这里要求的是内容上的相似度;
- 而要从内容相似度来判断,这就不得不提到向量模型的知识了。
向量模型
向量是空间中有方向和长度的量,空间可以是二维,也可以是多维;向量既然是在空间中,那么两个向量之间就一定能计算距离;
向量之间的距离一般有两种计算方法:
欧几里得距离
在n维空间中,两点间的直线距离。它是两点间最直接的距离测量方式。很适合用于RGB色彩空间中衡量两种颜色之间的差异
颜色可以用 RGB 值表示,然后通过计算两种颜色 RGB 值之间的欧几里得距离来判断它们的相似度。
- R G B: 两个颜色的 RGB 分量(红色、绿色、蓝色)
- d: 两个颜色之间的欧几里得距离。
- 距离越小,表示颜色越相似; 距离越大,表示颜色越不同
余弦相似度
通过比较两个向量之间的夹角余弦值来衡量它们的方向是否相似,如果夹角余弦值越小,说明它们越相似,但这种方法不能考虑到向量的大小。
在颜色分析中,它可以用来比较颜色 色调的相似性,但是它对于亮度和饱和度的变化不敏感。
综上,如果能把文本转为向量,就可以通过向量距离来判断文本的相似度了;
现在有不少的专门的向量模型,就可以实现将文本向量化。一个好的向量模型,就是要尽可能让文本含义相似的向量,在空间中距离更近:
阿里云百炼平台就提供了这样的模型,用于将文本向量化:
这里选择通用文本向量-v3
,这个模型兼容OpenAI,所以我们依然采用OpenAI的配置;修改yml配置
spring:
application:
name: chart-robot
ai:
ollama:
# Ollama服务地址
base-url: http://localhost:11434
chat:
# 模型名称,可更改
model: deepseek-r1:14b
options:
# 模型温度,值越大,输出结果越随机
temperature: 0.8
openai:
base-url: https://dashscope.aliyuncs.com/compatible-mode
api-key: ${
OPENAI_API_KEY} #API key
chat:
options:
# 可选择的模型列表 https://help.aliyun.com/zh/model-studio/getting-started/models
model: qwen-plus
embedding:
options:
model: text-embedding-v3 #通用文本向量-v3
dimensions: 1024
向量模型测试
文本向量化以后,就可以通过向量之间的距离来判断文本相似度;接下来,我们来测试下阿里百炼提供的向量大模型;
在项目中写一个工具类,用以计算向量之间的欧氏距离和**余弦距离。**新建一个ai.util
包,在其中新建一个VectorDistanceUtils类:
public class VectorDistanceUtils {
// 私有构造函数:防止该工具类被实例化。
private VectorDistanceUtils() {}
// 浮点数计算精度阈值,用于判断浮点数是否接近零。
private static final double EPSILON = 1e-12;
/**
* 计算欧氏距离(Euclidean Distance)
* 欧氏距离是两个向量之间的直线距离,常用于衡量多维空间中两点的距离。
* @param vectorA 向量A(非空且与B等长)
* @param vectorB 向量B(非空且与A等长)
*/
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); // 返回平方和的平方根,即欧氏距离
}
/**
* 计算余弦距离(Cosine Distance)
* 余弦距离基于余弦相似度计算,表示两个向量在方向上的差异。距离范围为[0, 2],
* 其中0表示完全相同,2表示完全相反。
*/
public static double cosineDistance(float[] vectorA, float[] vectorB) {
// 校验输入向量的合法性
validateVectors(vectorA, vectorB);
double dotProduct = 0.0; // 点积
double normA = 0.0; // 向量A的模
double normB = 0.0; // 向量B的模
// 遍历向量的每个维度,计算点积和模的平方
for (int i = 0; i < vectorA.length; i++) {
dotProduct += vectorA[i] * vectorB[i]; // 点积累加
normA += vectorA[i] * vectorA[i]; // A模的平方累加
normB += vectorB[i] * vectorB[i]; // B模的平方累加
}
// 计算向量的模
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);
// 余弦距离 = 1 - 相似度,范围为[0, 2]
return 1.0 - similarity;
}
/**
* 参数校验统一方法
* 确保输入向量满足以下条件:
* 1. 不为空(null);
* 2. 长度相等;
* 3. 非空数组。
*/
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");
}
}
}
由于SpringBoot的自动装配能力,刚才配置的向量模型可以直接使用;
@SpringBootTest
...
// 自动注入向量模型
@Autowired
private OpenAiEmbeddingModel embeddingModel;
@Test
void contextLoads() {
// 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));
}
}
运行结果:
可以看到,向量相似度确实符合我们的预期。有了比较文本相似度的办法,知识库的问题就可以解决了;前面说了,知识库数据量很大,无法全部写入提示词,而且庞大的知识库中与用户问题相关的其实并不多;
所以,我们需要想办法从庞大的知识库中找到与用户问题相关的一小部分,组装成提示词,发送给大模型就可以了;
现但是新的问题来了:向量模型是生成向量的,如此庞大的知识库,谁来从中比较和检索数据呢? 这就需要用到向量数据库了
向量数据库
文本向量化
由于需要将已拆分的知识片段文本存储向量库,以便后续可以进行检索,而向量库存储的数据是向量不是文本
因此需要将文本进行向量化,即将一个字符串转换为一个N维数组,这个过程在自然语言处理(NLP)领域称为文本嵌入
不同的LLM对于文本嵌入的实现是不同的,ChatGPT的实现是基于transformer架构的,相关实现存储在服务端,每次嵌入都需要访问OpenAI的HTTP接口。
通过下面的例子可以看到OpenAi使用的模型是:text-embedding-ada-002,向量的维度是:1536
OpenAiEmbeddingModel embeddingModel = new OpenAiEmbeddingModel.OpenAiEmbeddingModelBuilder().apiKey(API_KEY).baseUrl(BASE_URL).build();
log.info