Java开发者LLM实战——使用LangChain4j构建本地RAG系统

1、引言

由于目前比较火的chatGPT是预训练模型,而训练一个大模型是需要较长时间(参数越多学习时间越长,保守估计一般是几个月,不差钱的可以多用点GPU缩短这个时间),这就导致了它所学习的知识不会是最新的,最新的chatGPT-4o只能基于2023年6月之前的数据进行回答,距离目前已经快一年的时间,如果想让GPT基于近一年的时间回复问题,就需要RAG(检索增强生成)技术了。

img

此外,对于公司内部的私有数据,为了数据安全、商业利益考虑,不能放到互联网上的数据,因此GPT也没有这部分的知识,如果需要GPT基于这部分私有的知识进行回答,也需要使用RAG技术。

img

本文将通过实战代码示例,意在帮助没有大模型实战经验的Java工程师掌握使用LangChain4j框架进行大模型开发。

2、基本概念

2.1 什么是RAG

RAG(Retrieval-Augmented Generation)的核心思想是:将传统的信息检索(IR)技术与现代的生成式大模型(如chatGPT)结合起来。

具体来说,RAG模型在生成答案之前,会首先从一个大型的文档库或知识库中检索到若干条相关的文档片段。再将这些检索到的片段作为额外的上下文信息,输入到生成模型中,从而生成更为准确和信息丰富的文本。

RAG的工作原理可以分为以下几个步骤:

1.接收请求:首先,系统接收到用户的请求(例如提出一个问题)。

2.*信息***检索(R) :系统从一个大型文档库中检索出与查询最相关的文档片段。这一步的目标是找到那些可能包含答案或相关信息的文档。

3.*生成***增强(A) :将检索到的文档片段与原始查询一起输入到大模型(如chatGPT)中,注意使用合适的提示词,比如原始的问题是XXX,检索到的信息是YYY,给大模型的输入应该类似于:请基于YYY回答XXXX。

4.*输出***生成(G) :大模型基于输入的查询和检索到的文档片段生成最终的文本答案,并返回给用户。

第2步骤中的信息检索,不一定必须使用向量数据库,可以是关系型数据库(如MySQL)或全文搜索引擎(如Elasticsearch, ES),

但大模型应用场景广泛使用向量数据库的原因是:在大模型RAG的应用场景中,主要是要查询相似度高的某几个文档,而不是精确的查找某一条(MySQL、ES擅长)。

相似度高的两个文档,可能不包含相同的关键词。 例如,句子1: “他很高兴。” 句子2: “他感到非常快乐。” 虽然都是描述【他】很开心快乐的心情,但是不包含相同的关键词;

包含相同的关键词的两个文档可能完全没有关联,例如:句子1: “他喜欢苹果。” 句子2: “苹果是一家大公司。” 虽然都包含相同的关键词【苹果】,但两句话的相似度很低。

2.2 LangChain4j简介

LangChain4j是LangChiain的java版本,

LangChain的Lang取自Large Language Model,代表大语言模型,

Chain是链式执行,即把语言模型应用中的各功能模块化,串联起来,形成一个完整的工作流。

它是面向大语言模型的开发框架,意在封装与LLM对接的细节,简化开发流程,提升基于LLM开发的效率。

2.3 大模型开发 vs. 传统JAVA开发

大模型开发——大模型实现业务逻辑:

开发前,开发人员关注数据准备(进行训练)、选择和微调模型(得到更好的效果,更能匹配业务预期),

开发过程中(大多数时候),重点在于如何有效的与大模型(LLM)进行沟通,利用LLM的专业知识解决特定的业务问题,

开发中更关注如何描述问题(提示工程 Propmt Engineering)进行有效的推理,关注如何将大模型的使用集成到现有的业务系统中。

传统的JAVA开发——开发者实现业务逻辑:

开发前,开发人员关注系统架构的选择(高并发、高可用),功能的拆解、模块化等设计。

开发过程中(大多数时候)是根据特定的业务问题,设计特定的算法、数据存储等以实现业务逻辑,以编码为主。

3. 实战经验

3.1 环境搭建

3.1.1 向量库(Chroma)

Windows:

先安装python,参考: docs.python.org/zh-cn/3/usi…

PS:注意需要配置环境变量

验证-执行:

css 代码解读复制代码python --version

img

再安装chroma,参考:docs.trychroma.com/getting-sta…

验证-执行:

arduino 代码解读复制代码chroma run

img

Mac:

现先安装python

 代码解读复制代码brew install python

或者下载安装: www.python.org/downloads/m…

验证-执行:

css 代码解读复制代码python --version

img

安装chroma(同上),参考:docs.trychroma.com/getting-sta…

验证-执行:

arduino 代码解读复制代码chroma run

img

3.1.2 集成LangChain4j

xml 代码解读复制代码<properties>
        <langchain4j.version>0.31.0</langchain4j.version>
</properties>
<dependency>
	<groupId>dev.langchain4j</groupId>
	<artifactId>langchain4j-core</artifactId>
	<version>${langchain4j.version}</version>
</dependency>
<dependency>
	<groupId>dev.langchain4j</groupId>
	<artifactId>langchain4j</artifactId>
	 <version>${langchain4j.version}</version> 
</dependency>
<dependency>
	<groupId>dev.langchain4j</groupId>
	<artifactId>langchain4j-open-ai</artifactId>
	 <version>${langchain4j.version}</version> 
</dependency>
<dependency>
	<groupId>dev.langchain4j</groupId>
	<artifactId>langchain4j-embeddings</artifactId>
	 <version>${langchain4j.version}</version> 
</dependency>
<dependency>
	<groupId>dev.langchain4j</groupId>
	<artifactId>langchain4j-chroma</artifactId>
	 <version>${langchain4j.version}</version> 
</dependency>
<dependency>
	<groupId>io.github.amikos-tech</groupId>
	<artifactId>chromadb-java-client</artifactId>
	 <version>${langchain4j.version}</version> 
</dependency>

3.2 程序编写

3.2.1 项目结构

LangChain ├── core │ ├── src │ │ ├── main │ │ │ ├── java │ │ │ │ └── cn.jdl.tech_and_data.ka │ │ │ │ ├── ChatWithMemory │ │ │ │ ├── Constants │ │ │ │ ├── Main │ │ │ │ ├── RagChat │ │ │ │ └── Utils │ │ │ ├── resources │ │ │ │ ├── log4j2.xml │ │ │ │ └── 笑话.txt │ │ ├── test │ │ │ └── java │ ├── target ├── pom.xml ├── parent [learn.langchain.parent] ├── pom.xml

3.2.2 知识采集

一般是公司内网的知识库中或互联网上进行数据采集,获取到的文本文件、WORD文档或PDF文件,本文使用resources目录下的【笑话.txt】作为知识采集的结果文件

ini 代码解读复制代码URL docUrl = Main.class.getClassLoader().getResource("笑话.txt");
if(docUrl==null){
    log.error("未获取到文件");
}
Document document = getDocument(docUrl);
if(document==null){
    log.error("加载文件失败");
}
typescript 代码解读复制代码private static Document getDocument(URL resource) {
    Document document = null;
    try{
        Path path = Paths.get(resource.toURI());
        document = FileSystemDocumentLoader.loadDocument(path);
    }catch (URISyntaxException e){
        log.error("加载文件发生异常", e);
    }
    return document;
}

3.2.3 文档切分

使用dev.langchain4j.data.document.splitter.DocumentSplitters#recursize

它有三个参数:分段大小(一个分段中最大包含多少个token)、重叠度(段与段之前重叠的token数)、分词器(将一段文本进行分词,得到token)

其中,重叠度的设计是为了减少按大小拆分后切断原来文本的语义,使其尽量完整。

img

ini 代码解读复制代码DocumentSplitter splitter = DocumentSplitters.recursive(150,10,new OpenAiTokenizer());
splitter.split(document);

关于Token(标记)

Token是经过分词后的文本单位,即将一个文本分词后得到的词、子词等的个数,具体取决于分词器(Tokenizer),

比如:我喜欢吃苹果,可以拆分成我/喜欢/吃/苹果,token数量=4, 也可以拆分成我/喜/欢/吃/苹果,token数量=5

chatGPT使用的是BPE(Byte Pair Encoding)算法进行分词,

对于上面文本的分词结果如下:

ini 代码解读复制代码18:17:29.371 [main] INFO  TokenizerTest - 待分词的文本:我喜欢吃苹果
18:17:30.055 [main] INFO  cn.jdl.tech_and_data.ka.Utils - 当前的模型是:gpt-4o
18:17:31.933 [main] INFO  TokenizerTest - 分词结果:我 / 喜欢 / 吃 / 苹果

关于token与字符的关系:GPT-4o的回复:

img

关于文档拆分的目的

由于与LLM交互的时候输入的文本对应的token长度是有限制的,输入过长的内容,LLM会无响应或直接该报错,

因此不能将所有相关的知识都作为输入给到LLM,需要将知识文档进行拆分,存储到向量库,

每次调用LLM时,先找出与提出的问题关联度最高的文档片段,作为参考的上下文输入给LLM。

入参过长,LLM报错:

img

虽然根据响应,允许输入1048576个字符=1024K个字符=1M个字符,

但官网文档给的32K tokens,而一般1个中文字符对应1-2个Token,因此字符串建议不大于64K,实际使用中,为了保障性能,也是要控制输入不要过长。

如下是常见LLM给定的token输入上限:

模型名称Token 输入上限(最大长度)
GPT-3 (davinci)4096 tokens
GPT-3.5 (text-davinci-003)4096 tokens
GPT-4 (8k context)8192 tokens
GPT-4 (32k context)32768 tokens
LLaMA (7B)2048 tokens
LLaMA (13B)2048 tokens
LLaMA (30B)2048 tokens
LLaMA (65B)2048 tokens
讯飞星火(SparkDesk)8192 tokens
文心一言(Ernie 3.0)4096 tokens
智源悟道(WuDao 2.0)2048 tokens
阿里巴巴 M62048 tokens
华为盘古(Pangu-Alpha)2048 tokens
言犀大模型(ChatJd)2048 tokens

文档拆分的方案langchain4j中提供了6种:

img

1、基于字符的:逐个字符(含空白字符)分割

2、基于行的:按照换行符(\n)分割

3、基于段落的:按照连续的两个换行符(\n\n)分割

4、基于正则的:按照自定义正则表达式分隔

5、基于句子的(使用Apache OpenNLP,只支持英文,所以可以忽略😂)

img

6、基于字的:将文本按照空白字符分割

文档切分的流程如下,其中segments是最终输出的拆分结果,类型是:List。

先使用基于段落的方案将整个文档切成若干段(分段):parts,

再对每个段落part按照其他的某个方案(如:分句),并在满足【分段大小(一个分段中最大包含多少个token)】的条件下进行,同时计算重叠部分,按照【重叠度(段与段之前重叠的token数)】补充重叠信息。

img

3.2.4 文本向量化

由于需要将已拆分的知识片段文本存储向量库以便后续可以进行检索,而向量库存储的数据是向量不是文本,

因此需要将文本进行向量化,即将一个字符串转换为一个N维数组,这个过程在自然语言处理(NLP)领域称为文本嵌入(Words Embedding)。

不同的LLM对于文本嵌入的实现是不同的,ChatGPT的实现是基于transformer架构的,相关实现存储在服务端,每次嵌入都需要访问OpenAI的HTTP接口。

通过下面的例子可以看到OpenAi使用的模型是:text-embedding-ada-002,向量的维度是:1536。

OpenAiEmbeddingModel embeddingModel=newOpenAiEmbeddingModel.OpenAiEmbeddingModelBuilder().apiKey(API_KEY).baseUrl(BASE_URL).build();
log.info("当前的模型是: {}", embeddingModel.modelName());
String text = "两只眼睛";
Embedding embedding = embeddingModel.embed(text).content();
log.info("文本:{}的嵌入结果是:\n{}", text, embedding.vectorAsList());
log.info("它是{}维的向量", embedding.dimension());
ini 代码解读复制代码

3.2.5 向量库存储

向量数据库,也称为向量存储或向量搜索引擎,是一种专门设计用于存储和管理向量(固定长度的数字列表)及其他数据项的数据库。

这些向量是数据点在高维空间中的数学表示,其中每个维度对应数据的一个特征。向量数据库的主要目的是通过近似最近邻(ANN)算法实现高效的相似性搜索。

在使用向量库前,需要先启动chromdb,再通过LangChain4j封装的SDK连接到向量库,并创建数据存储容器,即集合(Collection)中(相当于MySQL的表)

ini 代码解读复制代码Client client = new Client(CHROMA_URL);
EmbeddingFunction embeddingFunction = new OpenAIEmbeddingFunction(API_KEY, OPEN_AI_MODULE_NAME);
client.createCollection(CHROMA_DB_DEFAULT_COLLECTION_NAME,null,true, embeddingFunction);

嵌入完成后,通过SDK连接到向量库,将向量(Embedding)与文本段(TextSegment)绑定,一并存储到向量库中。

ini 代码解读复制代码EmbeddingStore<TextSegment> embeddingStore = ChromaEmbeddingStore.builder().baseUrl(CHROMA_URL).collectionName(CHROMA_DB_DEFAULT_COLLECTION_NAME).build();
segments.forEach(segment->{
  Embedding embedding = embeddingModel.embed(segment).content();
  embeddingStore.add(embedding, segment);
});

3.2.6 向量库检索

为了在向量库中查询到相似的知识片段,作为查询的文本也需要进行向量化,方法同上。

ini 代码解读复制代码Embedding queryEmbedding = embeddingModel.embed(qryText).content(); 

检索时,向量库通过ANN算法查找与查询向量(queryEmbedding)距离最近的若个(取决于查询入参)个文本片段。

ini 代码解读复制代码EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder().queryEmbedding(queryEmbedding).maxResults(1).build();
EmbeddingSearchResult<TextSegment> embeddedEmbeddingSearchResult = embeddingStore.search(embeddingSearchRequest);
List<EmbeddingMatch<TextSegment>> embeddingMatcheList = embeddedEmbeddingSearchResult.matches();
EmbeddingMatch<TextSegment> embeddingMatch = embeddingMatcheList.get(0);
TextSegment TextSegment = embeddingMatch.embedded();

查询方法有4个入参:查询文本嵌入向量(queryEmbedding)、最大查询数量(最多查询多少个距离最近的向量)、最小分值(通过该值过滤一些候选值)、元数据过滤器(根据元数据过滤一些候选值)

img

3.2.7 与LLM交互

定义Prompt模板,告知LLM通过给定的上下文知识(context,即:查询向量库获取到的相关知识)回答提出的问题(question):

css 代码解读复制代码基于如下信息进行回答:\n{{context}}\n提问:\n{{question}}
ini 代码解读复制代码PromptTemplate promptTemplate = PromptTemplate.from("基于如下信息进行回答:\n" +
                "{{context}}\n" +
                "提问:\n" +
                "{{question}}");
Map<String, Object> variables = new HashMap<>();
variables.put("context", textSegment.text());
variables.put("question", QUESTION);
prompt = promptTemplate.apply(variables);

将Prompt信息组合到请求LLM的入参中:

ini 代码解读复制代码OpenAiChatModel openAiChatModel =  OpenAiChatModel.builder().apiKey(API_KEY).baseUrl(BASE_URL).modelName(OPEN_AI_MODULE_NAME).temperature(TEMPERATURE_NO_RANDOM).build();
UserMessage userMessage = prompt.toUserMessage();
Response<AiMessage> aiMessageResponse = openAiChatModel.generate(userMessage);
String response = aiMessageResponse.content();

3.3 测试验证

ini 代码解读复制代码17:47:57.060 [main] INFO  cn.jdl.tech_and_data.ka.Utils - 当前的模型是:gpt-4o
17:47:57.067 [main] INFO  cn.jdl.tech_and_data.ka.RagChat - [without RAG]userMessage=UserMessage { name = null contents = [TextContent { text = "请给我讲一个关于冰淇淋的笑话" }] }
17:48:00.129 [main] INFO  cn.jdl.tech_and_data.ka.RagChat - 不使用RAG,直接询问AI:[请给我讲一个关于冰淇淋的笑话]
 得到的回答是:[当然!这里有一个关于冰淇淋的笑话,希望你会喜欢:

为什么冰淇淋总是很开心?

因为它有很多“甜蜜”的朋友!]
17:48:00.269 [main] INFO  cn.jdl.tech_and_data.ka.RagChat - version of chroma db is : 0.5.0
17:48:02.550 [main] INFO  cn.jdl.tech_and_data.ka.RagChat - text:听说夏天和冰淇淋更配哦?不存在的,冰淇淋在我嘴里早就化了,连味道都没来得及品尝!, embeddingId=bd89a581-e764-47fc-967c-68899d6be904
17:48:03.811 [main] INFO  cn.jdl.tech_and_data.ka.RagChat - text:有一次,一个女孩因为太想要一支冰激凌而得不到,结果她竟然在超市的冰激凌货架前大声呼唤:“天啊,我渴望你!你能把冰激凌赐给我吗?”你猜怎么着?她得到了那支冰激凌,但是当她回家后发现里面全是空的!, embeddingId=6f84ab62-635f-4bd0-a3d2-ff55d00c4b01
17:48:05.534 [main] INFO  cn.jdl.tech_and_data.ka.RagChat - text:冰淇淋的冷,你绝对想象不到,它不仅冷静而且淡定的在我嘴里说着:“我是你的心头痛。”, embeddingId=c9d15213-2146-4f7b-8b23-c95d858b4ded
17:48:06.843 [main] INFO  cn.jdl.tech_and_data.ka.RagChat - text:半价冰淇淋妈妈带着小明去买冰淇淋,小明兴致勃勃地挑选了一个自己喜欢的口味。妈妈结账时,发现价格是双倍的,于是问店员为什么。店员说:“这个冰淇淋是两种口味的双球冰淇淋。” 小明听了,恍然大悟:“难怪它长了两只眼睛!”, embeddingId=042ee87a-bc0c-4ba2-aea5-dbef45c32d33
17:48:08.452 [main] INFO  cn.jdl.tech_and_data.ka.RagChat - text:有一次我家的冰箱坏了,里面的冰激凌都开始融化。我看到了一个很棒的创意——把所有的冰激凌球放在一个碗里,然后放入冰箱冷冻。结果我得到了一个巨大的冰激凌球混合物,这可是一道奇特的甜点!

麒麟飞到北极会变成什么? 答案:冰淇淋。, embeddingId=e7436694-821e-4559-89d3-2f09be2e172a
17:48:08.453 [main] INFO  cn.jdl.tech_and_data.ka.RagChat - 通过关键词:【两只眼睛】查询向量库
17:48:10.252 [main] INFO  cn.jdl.tech_and_data.ka.RagChat - 查询到的文档信息是 matchedTextSegment: TextSegment { text = "半价冰淇淋妈妈带着小明去买冰淇淋,小明兴致勃勃地挑选了一个自己喜欢的口味。妈妈结账时,发现价格是双倍的,于是问店员为什么。店员说:“这个冰淇淋是两种口味的双球冰淇淋。” 小明听了,恍然大悟:“难怪它长了两只眼睛!”" metadata = {absolute_directory_path=/Users/qihaizhi/newJavaEngineerOrientation/LangChain/core/target/classes, file_name=笑话.txt, index=3} }
17:48:10.258 [main] INFO  cn.jdl.tech_and_data.ka.RagChat - [with RAG]userMessage=UserMessage { name = null contents = [TextContent { text = "请基于如下信息进行回答,尽量使用信息中的所有词语:
半价冰淇淋妈妈带着小明去买冰淇淋,小明兴致勃勃地挑选了一个自己喜欢的口味。妈妈结账时,发现价格是双倍的,于是问店员为什么。店员说:“这个冰淇淋是两种口味的双球冰淇淋。” 小明听了,恍然大悟:“难怪它长了两只眼睛!”
提问:
请给我讲一个关于冰淇淋的笑话" }] }
17:48:16.663 [main] INFO  cn.jdl.tech_and_data.ka.RagChat - 使用RAG,询问AI:[请给我讲一个关于冰淇淋的笑话],
 得到的回答是:[有一天,妈妈带着小明去买冰淇淋。小明兴致勃勃地挑选了一个自己喜欢的口味。妈妈结账时,发现价格是双倍的,于是问店员为什么。店员说:“这个冰淇淋是两种口味的双球冰淇淋。” 小明听了,恍然大悟:“难怪它长了两只眼睛!”

这个笑话是不是很有趣呢?冰淇淋不仅美味,还能带来欢乐!]

4、总结与展望

通过本文的实战示例,详细介绍了如何使用LangChain4j框架来实现基于RAG技术的本地大模型应用。

通过不断探索和优化,RAG技术在大模型应用中的潜力将得到更充分的发挥,为各种业务场景提供更智能和高效的解决方案。

希望本文能为Java工程师们提供一个清晰的实战指南,帮助大家在大模型开发的道路上走得更远。

如何学习AI大模型?

我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。

我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在人工智能学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多互联网行业朋友无法获得正确的资料得到学习提升,故此将并将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。

在这里插入图片描述

第一阶段: 从大模型系统设计入手,讲解大模型的主要方法;

第二阶段: 在通过大模型提示词工程从Prompts角度入手更好发挥模型的作用;

第三阶段: 大模型平台应用开发借助阿里云PAI平台构建电商领域虚拟试衣系统;

第四阶段: 大模型知识库应用开发以LangChain框架为例,构建物流行业咨询智能问答系统;

第五阶段: 大模型微调开发借助以大健康、新零售、新媒体领域构建适合当前领域大模型;

第六阶段: 以SD多模态大模型为主,搭建了文生图小程序案例;

第七阶段: 以大模型平台应用与开发为主,通过星火大模型,文心大模型等成熟大模型构建大模型行业应用。

在这里插入图片描述

👉学会后的收获:👈
• 基于大模型全栈工程实现(前端、后端、产品经理、设计、数据分析等),通过这门课可获得不同能力;

• 能够利用大模型解决相关实际项目需求: 大数据时代,越来越多的企业和机构需要处理海量数据,利用大模型技术可以更好地处理这些数据,提高数据分析和决策的准确性。因此,掌握大模型应用开发技能,可以让程序员更好地应对实际项目需求;

• 基于大模型和企业数据AI应用开发,实现大模型理论、掌握GPU算力、硬件、LangChain开发框架和项目实战技能, 学会Fine-tuning垂直训练大模型(数据准备、数据蒸馏、大模型部署)一站式掌握;

• 能够完成时下热门大模型垂直领域模型训练能力,提高程序员的编码能力: 大模型应用开发需要掌握机器学习算法、深度学习框架等技术,这些技术的掌握可以提高程序员的编码能力和分析能力,让程序员更加熟练地编写高质量的代码。

在这里插入图片描述

1.AI大模型学习路线图
2.100套AI大模型商业化落地方案
3.100集大模型视频教程
4.200本大模型PDF书籍
5.LLM面试题合集
6.AI产品经理资源合集

👉获取方式:
😝有需要的小伙伴,可以保存图片到wx扫描二v码免费领取【保证100%免费】🆓

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值