目前,很多企业会利用大模型来做企业内部知识库系统,比如智能客服系统,做法是把企业内部的整理的产品手册、常见问题手册,做成智能知识库系统,客户可以直接向知识库系统用自然语言提问,知识库系统能理解客户的问题并基于内部知识给出客户想要的答案,此时就会用到文本向量化。
什么是向量
一个二维向量可以理解为平面坐标轴中的一个坐标点(x,y),在编程领域,一个二维向量就是一个大小为二的float类型的数组。
文本向量化
所谓文本向量化是指,利用大模型可以把一个字、一个词或一段话映射为一个多维向量,比如我们可以直接在LangChain4j中来调用向量模型来对一句话进行向量化体验:
java
package com.timi;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.openai.OpenAiEmbeddingModel;
import dev.langchain4j.model.output.Response;
public class _05_Vector {
public static void main(String[] args) {
OpenAiEmbeddingModel embeddingModel = OpenAiEmbeddingModel.builder()
.baseUrl("http://langchain4j.dev/demo/openai/v1")
.apiKey("demo")
.build();
Response<Embedding> embed = embeddingModel.embed("你好,我叫司马懿");
System.out.println(embed.content().toString());
System.out.println(embed.content().vector().length);
}
}
代码执行结果为:
java
Embedding { vector = [-0.00991016, -0.009099855, ...] }
1536
从结果可以知道"你好,我叫司马懿"这句话经过OpenAiEmbeddingModel向量化之后得到的一个长度为1536的float数组。注意,1536是固定的,不会随着句子长度而变化。
那么,我们通过这种向量模型得到一句话对应的向量有什么作用呢?非常有用,因为我们可以基于向量来判断两句话之间的相似度。
向量相似度
前面提到,向量相当于就是坐标点,如果两个坐标点靠得近,那么就表示这两个向量相似,所以,如果两句话对应的向量相似,那么就表示这两句话语义比较相似,当然这中间最关键的就是向量模型,因为向量是它生成的,向量模型也是经过大量机器学习训练之后产生的,向量模型效果越好,就表示它对于自然语言理解的程度越好,同时也就表示它生成出来的向量越准备,越能反映出语义的相似度。
比如可以通过计算两个坐标的余弦相似度(代码是ChatGPT生成的):
java
import java.util.*;
public class CosineSimilarity {
// 计算两个向量的点积
public static double dotProduct(double[] vectorA, double[] vectorB) {
double dotProduct = 0;
for (int i = 0; i < vectorA.length; i++) {
dotProduct += vectorA[i] * vectorB[i];
}
return dotProduct;
}
// 计算向量的模
public static double vectorMagnitude(double[] vector) {
double magnitude = 0;
for (double component : vector) {
magnitude += Math.pow(component, 2);
}
return Math.sqrt(magnitude);
}
// 计算余弦相似度
public static double cosineSimilarity(double[] vectorA, double[] vectorB) {
double dotProduct = dotProduct(vectorA, vectorB);
double magnitudeA = vectorMagnitude(vectorA);
double magnitudeB = vectorMagnitude(vectorB);
if (magnitudeA == 0 || magnitudeB == 0) {
return 0; // 避免除以零
} else {
return dotProduct / (magnitudeA * magnitudeB);
}
}
public static void main(String[] args) {
// 示例向量
double[] vectorA = {1, 2, 3};
double[] vectorB = {3, 2, 1};
// 计算余弦相似度
double similarity = cosineSimilarity(vectorA, vectorB);
System.out.println("Cosine Similarity: " + similarity);
}
}
向量数据库
对于向量模型生成出来的向量,我们可以持久化到向量数据库,并且能利用向量数据库来计算两个向量之间的相似度,或者根据一个向量查找跟这个向量最相似的向量。
在LangChain4j中,EmbeddingStore表示向量数据库,它有20个实现类:
- AstraDbEmbeddingStore
- AzureAiSearchEmbeddingStore
- CassandraEmbeddingStore
- ChromaEmbeddingStore
- ElasticsearchEmbeddingStore
- InMemoryEmbeddingStore
- InfinispanEmbeddingStore
- MemoryIdEmbeddingStore
- MilvusEmbeddingStore
- MinimalEmbeddingStore
- MongoDbEmbeddingStore
- Neo4jEmbeddingStore
- OpenSearchEmbeddingStore
- PgVectorEmbeddingStore
- PineconeEmbeddingStore
- QdrantEmbeddingStore
- RedisEmbeddingStore
- VearchEmbeddingStore
- VespaEmbeddingStore
- WeaviateEmbeddingStore
其中有我们熟悉的几个数据库都可以用来存储向量,比如Elasticsearch、MongoDb、Neo4j、Pg、Redis。
那么我们使用Redis来演示对于向量的增删查改,首先添加依赖:
xml
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-redis</artifactId>
<version>${langchain4j.version}</version>
</dependency>
然后需要注意的是,普通的Redis是不支持向量存储和查询的,需要额外的redisearch模块,我这边是直接使用docker来运行一个带有redisearch模块的redis容器的,命令为:
java
docker run -p 6379:6379 redis/redis-stack-server:latest
注意端口6379不要和你现有的Redis冲突了。
然后就可以使用以下代码把向量存到redis中了:
java
RedisEmbeddingStore embeddingStore = RedisEmbeddingStore.builder()
.host("127.0.0.1")
.port(6379)
.dimension(1536)
.build();
// 生成向量
Response<Embedding> embed = embeddingModel.embed("我是司马懿");
// 存储向量
embeddingStore.add(embed.content());
dimension表示要存储的向量的维度,所以为1536,如果你不是使用OpenAiEmbeddingModel得到的向量,那么维度可能会不一样。
可以使用以下命令来清空:
java
redis-cli FT.DROPINDEX embedding-index DD
匹配向量
在上面我们存储了"我是司马懿"的向量到Redis,接下来我们来使用"我的名字叫司马懿"来查找,看能不能查找到"我是司马懿",最好新建一个类,新增和查询分为两个类比较方便:
java
package com.timi;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.openai.OpenAiEmbeddingModel;
import dev.langchain4j.model.output.Response;
import dev.langchain4j.store.embedding.EmbeddingMatch;
import dev.langchain4j.store.embedding.redis.RedisEmbeddingStore;
import java.util.List;
public class _05_Vector_Search {
public static void main(String[] args) {
OpenAiEmbeddingModel embeddingModel = OpenAiEmbeddingModel.builder()
.baseUrl("http://langchain4j.dev/demo/openai/v1")
.apiKey("demo")
.build();
RedisEmbeddingStore embeddingStore = RedisEmbeddingStore.builder()
.host("127.0.0.1")
.port(6379)
.dimension(1536)
.build();
// 生成向量
Response<Embedding> embed = embeddingModel.embed("我的名字叫司马懿");
List<EmbeddingMatch<TextSegment>> result = embeddingStore.findRelevant(embed.content(), 4);
for (EmbeddingMatch<TextSegment> embeddingMatch : result) {
System.out.println(embeddingMatch.score());
}
}
}
代码执行结果为:
java
0.9765566289425
这就是"我是司马懿"和"我的名字叫司马懿"两句话之间的语义相似度分数,假如我们换成"今天天气很好",得到的分数为
java
0.8937962949275
看上去似乎分数差别不大,但是要注意小数位其实是很多的,也就是这个分数精度是比较高的,所以两个分数还是有一定距离的。
我们不妨再来演示一种场景:
java
// 生成向量
TextSegment textSegment1 = TextSegment.textSegment("客服电话是400-8558558");
TextSegment textSegment2 = TextSegment.textSegment("客服工作时间是周一到周五");
TextSegment textSegment3 = TextSegment.textSegment("客服投诉电话是400-8668668");
Response<Embedding> embed1 = embeddingModel.embed(textSegment1);
Response<Embedding> embed2 = embeddingModel.embed(textSegment2);
Response<Embedding> embed3 = embeddingModel.embed(textSegment3);
// 存储向量
embeddingStore.add(embed1.content(), textSegment1);
embeddingStore.add(embed2.content(), textSegment2);
embeddingStore.add(embed3.content(), textSegment3);
我往向量数据库中添加了三条客户相关的知识,并且通过TextSegment把原始文本也存入了Redis,相当于Redis中现在存储了三条原始文本以及对应的向量,然后我们来查询:
java
// 生成向量
Response<Embedding> embed = embeddingModel.embed("客服电话多少");
// 查询
List<EmbeddingMatch<TextSegment>> result = embeddingStore.findRelevant(embed.content(), 5);
for (EmbeddingMatch<TextSegment> embeddingMatch : result) {
System.out.println(embeddingMatch.embedded().text() + ",分数为:" + embeddingMatch.score());
}
代码执行结果为:
java
客服电话是400-8558558,分数为:0.9529553055763
客服投诉电话是400-8668668,分数为:0.9520588517189
客服工作时间是周一到周五,分数为:0.9305278658864999
从这就更容易看出向量的好处,能够基于向量快速的得到和文本相似的文本,这样就能非常适合用来做RAG,也就是检索增强生成。
本节总结
本节我们学习了什么是文本向量化、向量数据库、以及文本相似度等概念,下一节我们将通过完成一个智能客服系统来掌握向量等技术在RAG中的使用,以及掌握到底什么是RAG。