Langchain4j笔记+总结 2
目录
会话记忆
构建向量数据库操作对象EmbeddingStore
切割文档、向量化并存储到向量数据库
检索
3.7.2.2.1 构建ContentRetriever对象
配置ContentRetriever对象
核心API
文档加载器
文档解析器
文档分割器
配置文本分割器对象
向量模型
配置向量模型
设置向量模型
向量数据库操作对象EmbeddingStore
准备向量数据库RediSearch
引入依赖
配置向量数据库连接信息
注入RedisEmbeddingStore对象使用
收尾工作
完整知识库
避免每次启动程序都做向量化的操作
Tools工具
MVC增删改查
Tools工具原理
准备工具方法
配置工具方法
会话记忆
根据配置的config
@Bean
public ChatMemoryProvider chatMemoryProvider() {ChatMemoryProvider chatMemoryProvider = new ChatMemoryProvider() {@Overridepublic ChatMemory get(Object memoryId) {return MessageWindowChatMemory.builder().id(memoryId)//id值.maxMessages(20)//最大会话记录数量.build();}};return chatMemoryProvider;
}
在service中注入
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,chatModel = "openAiChatModel",streamingChatModel = "openAiStreamingChatModel",//chatMemory = "chatMemory",chatMemoryProvider = "chatMemoryProvider"//配置会话记忆对象提供者
)
public interface ConsultantService {@SystemMessage(fromResource = "system.txt")public Flux<String> chat(@MemoryId String memoryId, @UserMessage String message);
}
controller接收参数
@RequestMapping(value = "/chat",produces = "text/html;charset=utf-8")
public Flux<String> chat(String memoryId,String message){Flux<String> result = consultantService.chat(memoryId,message);return result;
}
一些方法:
//把json字符串转化成List<ChatMessage>
List<ChatMessage> list = ChatMessageDeserializer.messagesFromJson(json);
//1.把list转换成json数据
String json = ChatMessageSerializer.messagesToJson(list);
会话记忆持久化 默认是存在内存 重启就没了 所以自定义方法增删改查
修改config

Rag
LangChain4j提供的ClassPathDocumentLoader可以让我们快速的将指定目录下的文档加载进内存中,并且每一个文档,都会对应的生成一个Document对象来记录文档的内容。这一部分工作需要在CommonConfig.java中完成。
@Bean public EmbeddingStore store(){
//1.加载文档进内存
List<Document> documents = ClassPathDocumentLoader.loadDocuments("content"); return null; }
构建向量数据库操作对象EmbeddingStore
其实在我们引入的依赖中已经提供了一个用于操作内存版本的向量数据库的类InmemoryEmbeddingStore,Inmemory是内存的意思,Embedding翻译过来是嵌入/向量的意思,Store是存储的意思,顾名思义,操作内存向量数据库。我们只要new出来一个对象即可。
@Bean public EmbeddingStore store(){
//1.加载文档进内存
List<Document> documents = ClassPathDocumentLoader.loadDocuments("content");
//2.构建向量数据库操作对象 操作的是内存版本的向量数据库
InMemoryEmbeddingStore store = new InMemoryEmbeddingStore();
return store; }
切割文档、向量化并存储到向量数据库
LangChain4j中给我们提供了一个类EmbeddingStoreIngestor,它把很多细节都封装起来了,可以帮我们快速的完成这一步的操作。
首先我们构建EmbeddingStoreIngestor对象,构建的时候告诉它我要把向量化的数据存储到哪里?也就是把第三步构建的EmbeddingStore设置给它,
接下来调用它的ingest方法,把需要存储数据的文档对象documents给它传递进去。
在这个方法的内部会使用它内置的文本分割器先分割,然后使用内置的向量模型完成向量化,最后再把向量存储到向量数据库中。
@Bean
public EmbeddingStore store(){//1.加载文档进内存List<Document> documents = ClassPathDocumentLoader.loadDocuments("content");//2.构建向量数据库操作对象 操作的是内存版本的向量数据库InMemoryEmbeddingStore store = new InMemoryEmbeddingStore();//3.构建一个EmbeddingStoreIngestor对象,完成文本数据切割,向量化, 存储EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder().embeddingStore(store).build();ingestor.ingest(documents);return store;
}
检索
3.7.2.2.1 构建ContentRetriever对象
LangChain4j提供的向量数据库检索对象叫做EmbeddingStoreContentRetriever,构建的时候我们可以设置三个内容。
第一个得调用embeddingStore方法告诉它从哪里检索,其实就是我们刚才构建的这个InmemoryEmbeddingStore给他即可;
第二个我们可以设置一下最小余弦相似度的值,之前我们讲过检索的时候会把用户的问题向量化,然后与向量数据库中已经存在的向量计算余弦相似度,值越大,相似度越高,这里通过minScore方法设置一个最低的相似度分数,可以确保检索出来的内容跟用户问题的相关度比较高;
第三个可以设置一个最大检索出来的片段数量值,因为将来如果检索出来的片段太多,一并发送给大模型,token的消耗是比较大的,而且分数低的片段你发送给大模型还会影响生成的结果,这里通过maxResults方法设置最大的片段数量后,它会保留分数最高的前几个片段使用。
这些操作也是在CommonConfig.java中完成
@Bean
public ContentRetriever contentRetriever(EmbeddingStore store){return EmbeddingStoreContentRetriever.builder().embeddingStore(store)//设置向量数据库操作对象.minScore(0.5)//设置最小分数.maxResults(3)//设置最大片段数量.build();
}
配置ContentRetriever对象
跟我们前面是类似的,在AiService注解中借助于contentRetriver这个属性完成配置即可。
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,//手动装配chatModel = "openAiChatModel",//指定模型streamingChatModel = "openAiStreamingChatModel",//chatMemory = "chatMemory",//配置会话记忆对象chatMemoryProvider = "chatMemoryProvider",//配置会话记忆提供者对象contentRetriever = "contentRetriever"//配置向量数据库检索对象
)
//@AiService
public interface ConsultantService {//用于聊天的方法//public String chat(String message);//@SystemMessage("你是东哥的助手小月月,人美心善又多金!")@SystemMessage(fromResource = "system.txt")//@UserMessage("你是东哥的助手小月月,人美心善又多金!{{it}}")//@UserMessage("你是东哥的助手小月月,人美心善又多金!{{msg}}")public Flux<String> chat(/*@V("msg")*/@MemoryId String memoryId, @UserMessage String message);
}


核心API
为了梳理RAG的核心API,我们再来回顾一下知识库的存储流程.

首先我们需要在项目中准备存储数据的文档,这些文档需要使用文档加载器 Document Loader 加载进内存,由于加载的过程中需要解析文档的内容,所以还要使用到文档解析器来解析文档的内容,最后在内存中生成一个一个的Document对象用于记录文档的内容。
由于每个Document对象中记录的是对应文档中的全部内容,如果我们直接把整个文档的内容一次性向量化存储到向量数据库中,不利于检索,所以这些文档对象,需要使用文档分割器 Document Splitter分割成一个一个的文本片段,而每一个文本片段只是记录整个文档中的一小部分内容,这样将来根据用户问题检索相关片段的时候就会更精准。
这些文本片段需要使用向量模型转化为一个一个向量,之前讲过其实就是一串一串的数字记录的是不同维度的坐标,LangChain4j中提供了Embedding对象用于记录这些坐标,因此这里得到的是一个一个的Embedding对象。
最后再使用EmbeddingStore这种向量数据库操作对象将向量和对应的文本片段存储到向量数据库中。
在整个流程中,主要用到了文档加载器、文档解析器、文档分割器、向量模型以及向量数据库操作对象这五类API,等会儿咱们挨个讲解。其中有关文档分割器、向量模、还有向量数据库操作对象的具体方法的调用都被封装到了EmbeddingStoreIngestor中了,对于咱们来说无需过多关注,我们主要关注的是使用哪种文档分割器、哪种向量模型、哪种向量数据库操作对象即可,将来用哪种把哪种交给EmbeddingStoreIngestor就可以了。
文档加载器
文档加载器的作用是把磁盘或者网络中的数据加载进程序。LangChain4j给我们提供了多个文档加载器,其中常见的有以下三种:
-
FileSystemDocumentLoader, 根据本地磁盘绝对路径加载
-
ClassPathDocumentLoader,相对于类路径加载
-
UrlDocumentLoader,根据url路径加载
-
……
大家可以把之前代码中的ClassPathDocumentLoader替换为FileSYstemDocumentLoader做一个尝试。
@Bean
public EmbeddingStore store(){//1.加载文档进内存//List<Document> documents = ClassPathDocumentLoader.loadDocuments("content");List<Document> documents = FileSystemDocumentLoader.loadDocuments("C:\\Users\\Administrator\\ideaProjects\\consultant\\src\\main\\resources\\content");//2.构建向量数据库操作对象 操作的是内存版本的向量数据库InMemoryEmbeddingStore store = new InMemoryEmbeddingStore();//3.构建一个EmbeddingStoreIngestor对象,完成文本数据切割,向量化, 存储EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder().embeddingStore(store).build();ingestor.ingest(documents);return store;
}
文档解析器
文档解析器就是用于解析文档中的内容,把原本非纯文本数据转化成纯文本。比如初始的文档是pdf格式的,它的内容就不是纯文本的,此时需要借助于文档解析器将非纯文本数据转化成纯文本。在LangChain4j中提供了几个常用的文档解析器:
-
TextDocumentParser,解析纯文本格式的文件
-
ApachePdfBoxDocumentParser,解析pdf格式文件
-
ApachePoiDocumentParser,解析微软的office文件,例如DOC、PPT、XLS
-
ApacheTikaDocumentParser(默认),几乎可以解析所有格式的文件
由于默认的ApacheTikaDocumentParser虽然可以解析所有格式的文件,但是它可能在纯PDF文件方面的表现没有那么优秀,或者使用起来没有那么方便,此时我们可以将默认的解析器切换成ApachePdfBoxDocumentParser,具体的操作如下:
A. 准备pdf格式的数据
将资料中准备的《西北大学.pdf》拷贝到resourcces/content目录下,删除原来的《西北大学.md》。
B. 引入依赖
<dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-document-parser-apache-pdfbox</artifactId><version>1.0.1-beta6</version></dependency>
C. 指定解析器
@Bean
public EmbeddingStore store(){//1.加载文档进内存//List<Document> documents = ClassPathDocumentLoader.loadDocuments("content");//加载文档的时候指定解析器List<Document> documents = ClassPathDocumentLoader.loadDocuments("content",new ApachePdfBoxDocumentParser());//2.构建向量数据库操作对象 操作的是内存版本的向量数据库InMemoryEmbeddingStore store = new InMemoryEmbeddingStore();//3.构建一个EmbeddingStoreIngestor对象,完成文本数据切割,向量化, 存储EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder().embeddingStore(store).build();ingestor.ingest(documents);return store;
}
文档分割器
文档分割器主要用于把一个大的文档切割成一个一个的小片段。在langchain4j中提供了多种文档分割器,大概有以下7种:
-
DocuemntByParagraphSplitter,按照段落分割文本
-
DocumentByLineSplitter,按照行分割文本
-
DocumentBySentenceSplitter,按照句子分割文本
-
DocumentByWordSplitter,按照词分割文本
-
DocumentByCharacterSplitter,按照固定数量的字符分割文本
-
DocumentByRegexSplitter,按照正则表达式分割文本
-
DocumentSplitters.recursive(…)(默认),递归分割器,优先段落分割,再按照行分割,再按照句子分割,再按照词分割
先说第一种按照段落分割文本,举个例子,假设我们文本中的内容是一片散文,总共由6个段落组成。
那么DocumentByParagraphSplitter就会把文档分割成6个部分,但是这里大家要注意的是这每一部分并不是将来进行向量化的文本片段,文本片段是根据这6部分的内容组合而成的。通常情况下LangChain4j是允许我们指定文本片段的字符容量的,假设我指定单个文本片段的字符容量为300,那么在组合文本片段的时候,第一部分的自然段和第二部分的自然段的字符总和不到300,可以放到同一个文本片段中,但是加上第三部分的自然段,字符总和超过了300,那么第三部分的自然段就不能再放到这个文本片段中了,而是放到下一个新的文本片段中。
当然除了按照段落分割文本,LangChain4j还提供了按行分割、按句子分割、按单词分割、按固定数量的字符分割等等不同方式的文档分割器,都可以使用。这里我们关注一下最后一种文本分割器,它是通过一个静态方法recursive创建出来的,叫做递归分割器,它组合了段落分割器、行分割器、句子分割器以及词分割器,它会按照优先级进行分割文档,先按照段落分割,再按照行,再按照句子,最后按照词,有什么用呢?
咱们刚才按段落分割,第三个自然段是不是放不下了?此时如果是递归分割器的话它会继续使用行分割器,把第三个自然段进一步分割,尝试把得到的内容放到当前文本片段中,如果还是不行,再按照句子分割,这就是它的作用。
咱们默认使用的也是这种递归分割器,默认使用的单个文本最大字符个数就是300,当然了,我不想使用这个默认的切割器,我觉得300个字符太少了,我想多设置一点儿,行不行呢?也可以,接下来我们看应该如何操作。
构建文本分割器对象
DocumentSplitter documentSplitter = DocumentSplitters.recursive(
每个片段最大容纳的字符,
两个片段之间重叠字符的个数
);
构建的时候需要指定每个片段最大容纳的字符数量和两个片段之间重叠字符的个数,第一个好理解,给大家解释一下第二个是什么意思。
假如我有一篇以高考为题目的散文需要存储到向量数据库中,将来分割后得到的两个文本片段,第一个片段里写到高考.....而第二个片段中完全没有出现高考相关的字眼,那到时候我去检索高考相关的内容时第二个片段将不会被检索出来,但实质上按照语义它是应该被检索出来的。
我们解决的办法就是让两个片段存储的内容有重叠的部分,上一个片段的末尾与下一个片段的开头重复,这样就可以保持语义的连贯性了。比如我把高考不是重点, 而是起点...这句话存储到第二个片段的开头就能解决这个问题,咱们第二个参数就是用于指定重叠部分字符的数量。

配置文本分割器对象
真正分割文本的操作被封装到EmbeddingStoreIngestor中了,所以我们需要在构建该对象的时候,通过documentSplitter方法告诉它将来使用哪个文本分割器。
@Bean
public EmbeddingStore store(){//1.加载文档进内存//List<Document> documents = ClassPathDocumentLoader.loadDocuments("content");//加载文档的时候指定解析器List<Document> documents = ClassPathDocumentLoader.loadDocuments("content",new ApachePdfBoxDocumentParser());//2.构建向量数据库操作对象 操作的是内存版本的向量数据库InMemoryEmbeddingStore store = new InMemoryEmbeddingStore();//构建文档分割器对象DocumentSplitter ds = DocumentSplitters.recursive(500,100);//3.构建一个EmbeddingStoreIngestor对象,完成文本数据切割,向量化, 存储EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder().embeddingStore(store).documentSplitter(ds) .build();ingestor.ingest(documents);return store;
}

向量模型
向量模型的作用是把分割后的文本片段向量化或者把用户消息向量化。
public interface EmbeddingModel {default Response<Embedding> embed(String text) {return this.embed(TextSegment.from(text));}default Response<Embedding> embed(TextSegment textSegment) {}Response<List<Embedding>> embedAll(List<TextSegment> texts);default int dimension() {return ((Embedding)this.embed("test").content()).dimension();}}
LangChain4j中提供了EmbeddingModel接口用于定义有关向量模型的方法,例如有embed、embedall等等方法用于把文本片段向量化。LangChain4j提供了一个内存版本的向量模型实现方案,而咱们快速入门中使用的就是这个向量模型,只是咱们当时并没有指定这个向量模型,因为它被封装到EmbeddingStoreIngestor中了,所以我们并没有看到。

但是这种内置的向量模型内有时候功能没有那么强大,说白了就是支持的向量维度太少,检索的时候没有那么精准,所以有些情况下我们需要替换它,使用一些功能更强大的向量模型。
阿里云百炼平台也提供了专门用于向量化的向量模型text-embedding-v3,接下来我们看应该如何把我们程序中内存版本的向量模型替换成阿里云百炼提供的向量模型。
配置向量模型
和咱们之前配置文本模型类似,只不过这里不再是chat-model或者streaming-chat-model,而是embedding-model,其它的配置一样,也需要配置url、apikey、modelname以及日志相关的配置。
langchain4j:open-ai:embedding-model:base-url: https://dashscope.aliyuncs.com/compatible-mode/v1api-key: ${API-KEY}model-name: text-embedding-v3log-requests: truelog-responses: true
设置向量模型
当我们配置完毕后,LangChain4j会自动的根据我们的配置信息往IOC容器中注入一个EmbeddingModel对象供我们使用,所以接下来我们只需要把这个EmbeddingModel对象交给EmbeddingStoreIngestor和EmbeddingStoreContentRetriever即可,一个是存储的时候使用,一个是检索的时候使用。
@Autowired
private EmbeddingModel embeddingModel;@Bean
public EmbeddingStore store(){//1.加载文档进内存//List<Document> documents = ClassPathDocumentLoader.loadDocuments("content");//加载文档的时候指定解析器List<Document> documents = ClassPathDocumentLoader.loadDocuments("content",new ApachePdfBoxDocumentParser());//2.构建向量数据库操作对象 操作的是内存版本的向量数据库InMemoryEmbeddingStore store = new InMemoryEmbeddingStore();//构建文档分割器对象DocumentSplitter ds = DocumentSplitters.recursive(500,100);//3.构建一个EmbeddingStoreIngestor对象,完成文本数据切割,向量化, 存储EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder().embeddingStore(store).documentSplitter(ds) .embeddingModel(embeddingModel).build();ingestor.ingest(documents);return store;
}@Bean
public ContentRetriever contentRetriever(EmbeddingStore store){return EmbeddingStoreContentRetriever.builder().embeddingStore(store).minScore(0.5).maxResults(3).embeddingModel(embeddingModel).build();
}

向量数据库操作对象EmbeddingStore
EmbeddingStore是用来操作向量数据库的API,将来不管是存储还是检索都需要借助于它来完成。LangChain4j提供的EmbeddingStore接口中提供了两组方法,分别是add用于存储数据,search用于检索数据。
同时LangChain4j还提供了一个实现方案InMemoryEmbeddingStore,也就是咱们之前一直使用的方案,但是这它操作的是内存向量数据库,有些情况下不能满足实际开发中的需求。
大家可以想一下,如果我们使用内存向量数据库,一旦服务器重启数据就丢失了,又得重新加载文档、重新向量化,这样每次启动都会比较耗时,还有就是每次启动都会使用百炼平台提供的向量模型完成向量化,它是收费的,每次都这么干那是跟钱过不去,没必要对吧。

所以咱们得考虑把向量化后的数据存储到外部的向量数据库中。之前给大家介绍过常见的向量数据库有Milvus、Chroma、Pinecone、RediSearch以及pgvector, 用哪一种都行,LangChain4j对这些向量数据库都做了支持。
咱们本次课程中采用redisearch存储向量数据。接下来我们看看具体的操作。
准备向量数据库RediSearch
这一块我们依然使用docker来部署redisearch,由于redisearch是redis扩展的一个功能,所以我们得把之前部署的redis先卸载掉,然后部署一个扩展了redissearch的redis即可。这里我们需要执行三条命令:
docker stop redis # 停止原有的redis镜像 docker rm redis #删除原有的redis镜像 docker run --name redis-vector -d -p 6379:6379 redislabs/redisearch #安装扩展redisearch功能的redis
引入依赖
<dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-community-redis-spring-boot-starter</artifactId><version>1.0.1-beta6</version>
</dependency>
配置向量数据库连接信息
大家要注意的是这里的配置和我们之前配置的redis不相干,这里配置的是langchain4j.community下的,而之前配置的是spring.data下的。
langchain4j:community:redis:host: localhostport: 6379
当引入的起步依赖检测我们这一段配置信息后,会自动的往IOC容器中注入一个RedisEmbeddingStore对象,这个对象实现了EmbeddingStore接口,封装了操作redissearch的API,我们可以直接使用。
注入RedisEmbeddingStore对象使用
和之前一样,将IOC容器中的RedisEmbeddingStore对象分别设置给EmbeddingStoreIngestor和EmbeddingStoreContentRetriever,用于存储和检索。
@Autowired
private RedisEmbeddingStore redisEmbeddingStore;@Bean
public EmbeddingStore store(){//embeddingStore的对象, 这个对象的名字不能重复,所以这里使用store//1.加载文档进内存//List<Document> documents = ClassPathDocumentLoader.loadDocuments("content");List<Document> documents = ClassPathDocumentLoader.loadDocuments("content",new ApachePdfBoxDocumentParser());//List<Document> documents = FileSystemDocumentLoader.loadDocuments("C:\\Users\\Administrator\\ideaProjects\\consultant\\src\\main\\resources\\content");//2.构建向量数据库操作对象 操作的是内存版本的向量数据库//InMemoryEmbeddingStore store = new InMemoryEmbeddingStore();//构建文档分割器对象DocumentSplitter ds = DocumentSplitters.recursive(500,100);//3.构建一个EmbeddingStoreIngestor对象,完成文本数据切割,向量化, 存储EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()//.embeddingStore(store).embeddingStore(redisEmbeddingStore).documentSplitter(ds).embeddingModel(embeddingModel).build();ingestor.ingest(documents);return redisEmbeddingStore;
}@Bean
public ContentRetriever contentRetriever(/*EmbeddingStore store*/){return EmbeddingStoreContentRetriever.builder().embeddingStore(redisEmbeddingStore).minScore(0.5).maxResults(3).embeddingModel(embeddingModel).build();
}

收尾工作
完整知识库
将资料中提供的所有pdf文档,全部拷贝到reouserces/content目录下,重新启动测试,让向量数据库保存所有的数据。
避免每次启动程序都做向量化的操作
由于咱们准备向量数据库的操作是在CommonConfig配置类中完成的,在该类中我们提供了一个store方法,方法上添加了一个@Bean注解,所以每次启动程序,该方法都会执行一遍,文档就会重新加载,重新向量化,不合适。
所以当我们把所有文档拷贝到content目录中,启动测试一遍后,redis中就已经存好了所有的数据,接下来把store方法上的@Bean注解注释掉,可以避免每次启动都做向量化的操作。
//@Bean
public EmbeddingStore store(){//.......return redisEmbeddingStore;
}
Tools工具
在咱们的AI志愿填报顾问中,将来要做这么一个功能,每次回答完用户的问题后,都会在答案的最后附上这么一句话: 志愿填报需要考虑的因素有很多,如果要得到专业的志愿填报指导,建议您预约一个一对一的指导服务,是否需要预约?
当用户表达出需要预约的意愿并提交了姓名, 性别, 电话等信息后,我们的程序就需要数据库中添加一条信息,记录预约详情。
所以开发这个功能的前提是我们得先准备好mysql数据库环境,把crud的代码开发好,将来当用户提交了考生信息后才能调用这些代码往数据库中添加数据。
准备实体类
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.time.LocalDateTime;@Data
@NoArgsConstructor
@AllArgsConstructor
public class Reservation {private Long id;private String name;private String gender;private String phone;private LocalDateTime communicationTime;private String province;private Integer estimatedScore;
}
MVC增删改查
.....
Tools工具原理
Tools工具,以前也叫做function calling,翻译过来叫做函数调用,如果在我们的程序中添加了function calling功能,,那整个工作流程会发生一些改变,我们简单的看一看。

当用户把问题发送给AI应用,在AI应用的内部需要组织提交给大模型的数据,而这些数据中需要描述清楚我们的AI应用中有哪些函数能够被大模型调用。
每一个函数的描述都包含三个部分,方法名称、方法作用、方法入参。
当AI应用把这些数据发送给大模型后,大模型会先根据用户的问题以及上下文拆解任务,从而判断是否需要调用函数,如果有函数需要调用,则把需要调用的函数的名称,以及调用时需要使用的参数准备好一并响应给AI应用。AI应用接收到响应后需要执行对应的函数,得到对应的结果,接下来把得到的结果和之前信息一块组织好再发送给大模型。
这里需要注意的是由于在一次任务的处理过程中可能需要根据顺序调用多个函数,所以当大模型接收到AI应用发送的数据继续拆解任务,如果发现还需要调用其他的函数,则会重复4.1~4.4这几个步骤,直到无需调用函数,最终把生成的结果响应该AI应用,并由AI应用发送给用户。
这就是增加了function calling 或者 Tools工具后整个AI应用的工作流程,比之前要复杂不少,不过好消息是下面的这些工作LangChain4j都能帮我们自动的完成,对于咱们来说只需要按照LangChain4j的规则描述清楚有哪些方法可以被大模型调用,方法名的名字是什么、有什么作用、以及都需要哪些参数?
准备工具方法
LangChain4j提供了Tool注解用于对方法的作用进行描述,还有P注解用于对方法的参数进行描述,将来LangChain4j就能通过反射的方式获取到Tool注解中的作用描述、P注解中的参数描述、以及方法的名称,组织数据,一并发送给大模型。
这里需要注意,ReservationTool需要注入到IOC容器对象中。
import com.itheima.consultant.pojo.Reservation;
import com.itheima.consultant.service.ReservationService;
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import java.time.LocalDateTime;@Component
public class ReservationTool {@Autowiredprivate ReservationService reservationService;//1.工具方法: 添加预约信息@Tool("预约志愿填报服务")public void addReservation(@P("考生姓名") String name,@P("考生性别") String gender,@P("考生手机号") String phone,@P("预约沟通时间,格式为: yyyy-MM-dd'T'HH:mm") String communicationTime,@P("考生所在省份") String province,@P("考生预估分数") Integer estimatedScore){Reservation reservation = new Reservation(null,name,gender,phone, LocalDateTime.parse(communicationTime),province,estimatedScore);reservationService.insert(reservation);}//2.工具方法: 查询预约信息@Tool("根据考生手机号查询预约单")public Reservation findReservation(@P("考生手机号") String phone){return reservationService.findByPhone(phone);}
}
配置工具方法
配置的方法和之前的类似,在AiService注解中过一个叫做tools的属性完成配置,值写上包含了工具方法的Bean对象的名字即可。
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,//手动装配chatModel = "openAiChatModel",//指定模型streamingChatModel = "openAiStreamingChatModel",//chatMemory = "chatMemory",//配置会话记忆对象chatMemoryProvider = "chatMemoryProvider",//配置会话记忆提供者对象contentRetriever = "contentRetriever",//配置向量数据库检索对象tools = "reservationTool"
)
//@AiService
public interface ConsultantService {//用于聊天的方法//public String chat(String message);//@SystemMessage("你是东哥的助手小月月,人美心善又多金!")@SystemMessage(fromResource = "system.txt")//@UserMessage("你是东哥的助手小月月,人美心善又多金!{{it}}")//@UserMessage("你是东哥的助手小月月,人美心善又多金!{{msg}}")public Flux<String> chat(/*@V("msg")*/@MemoryId String memoryId, @UserMessage String message);
}
功能已经实现完毕了,测试的时候注意观察IDEA控制台的信息,langchain4j给大模型发送消息的时候会使用tools参数告诉大模型,有哪些函数可以调用。

