Java开发者の模型召唤术:LangChain4j咏唱指南(二)
Java开发者の模型召唤术:LangChain4j咏唱指南(二)
往期回顾:
Java开发者の模型召唤术:LangChain4j咏唱指南(一)_langchain4j 千问-CSDN博客
上期博客中简单的为大家介绍了langchain4j是什么、java 集成 langchain4j、集成阿里云百炼平台的各个模型以及本地安装的ollama模型的调用流程。
那么本期教程将会与大家分享常规的Spring-boot集成、流式输出、记忆对话、function-call、预设角色等更深层次的知识,码字不易,各位喜欢请一键三连~
1.springboot 项目集成langchain4j
1.1创建项目
不再过多介绍,重要的事情说三遍
! JDK选择17
! JDK选择17
! JDK选择17
本人spring-boot 版本使用的是3.1.5
1.2引入依赖
spring-boot-starter-web
spring-boot-starter-test
spring相关的基础依赖不在过多赘述
核心依赖包有以下两个:
<langchain4j.version>1.0.0-beta2</langchain4j.version>
<!--阿里云百炼平台qwen-boot-starter-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!--langchain4j基础依赖-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>${langchain4j.version}</version>
</dependency>
同时引入依赖版本自动化管理
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 自动化langchain4j-community依赖版本管理-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-bom</artifactId>
<version>${langchain4j.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
1.3引入配置
配置文件 application.properties
server.port=8888
# langchain4j-qwen
langchain4j.community.dashscope.chat-model.api-key=sk-XXXXX
langchain4j.community.dashscope.chat-model.model-name=qwen-max
#sse qianwen
langchain4j.community.dashscope.streaming-chat-model.api-key=sk-XXXX
## qwq-32b deepseek 都不支持function call(deepseek-v3)
langchain4j.community.dashscope.streaming-chat-model.model-name= qwen-max
1.4编写controller
1.4.1 最简单的例子
编写controller 进行简单的对话测试,那么先来个最简单的叭,代码示例
@RestController
@RequestMapping("/ai/simple")
public class LangChain4jController {
@Resource
private QwenChatModel qwenChatModel;
@GetMapping("/doAsk")
public String langChain4j(@RequestParam(defaultValue = "你是谁?") String question) {
return qwenChatModel.chat(question);
}
}
可以看到能够请求到qwen,得到返回内容
1.4.2 自定义注解实现预设角色
基础大模型是没有目的性的, 你聊什么给什么,但是如果我们开发的是一个星座智能助手, 我需要他以一个星座专家的角色跟我对话, 那么就需要进行一些预设角色的实现与设定。
各位小伙伴但凡是接触各大智能体API调用的时候,一般是使用promt进行与各大只能体平台进行交互,那么在交互过程中一般会通过promt指定模型角色,然后在调用过程中通过传入到SystemMessage中,搭配用户的问题传入userMessage中实现与模型有效的沟通。
langchain4j其实是有类似于SystemMessage的注解,标注模型是什么角色,通过**@SystemMessage**注解进行使用。但是此处的话,我们进行自定义注解的方式进行自定义这一行为
首先自定义一个注解SystemPrompt
/**
* @version 1.0
* @Author jerryLau
* @Date 2025/3/24 8:45
* @注释 自定义注解 用于指定系统提示词
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SystemPrompt {
String systemPrompt() default "你是一个星座专家";
}
定义aiService接口,定义执行方式
/**
* @version 1.0
* @Author jerryLau
* @Date 2025/3/24 8:41
* @注释 aiService 接口
*/
public interface AiService {
/***
* 提问 ai 方法 同步方法
* @param question
* @return
* @throws NoSuchMethodException
*/
@SystemPrompt()
ChatResponse doAsk(String question) throws NoSuchMethodException;
}
创建impl实现具体的调用方式
/**
* @version 1.0
* @Author jerryLau
* @Date 2025/3/24 10:46
* @注释
*/
@Service
public class AiServiceImpl implements AiService {
@Resource
private QwenChatModel qwenChatModel;
/***
* 提问 ai 方法 同步阻塞方法
* @param question 问题
* @return
* @throws NoSuchMethodException
*/
@Override
public ChatResponse doAsk(String question) throws NoSuchMethodException {
//反射获取注释中的内容
Method doAsk = AiService.class.getMethod("doAsk", String.class);
SystemPrompt annotation = doAsk.getAnnotation(SystemPrompt.class);
if (annotation != null) {
String s = annotation.systemPrompt();
List<ChatMessage> messages = new ArrayList<>();
//传入SystemMessage和userMessage
messages.add(SystemMessage.from(s));
messages.add(UserMessage.from(question));
ChatResponse chat = qwenChatModel.chat(messages);
//返回回答结果
return chat;
} else {
throw new NoSuchMethodException("no annotation");
}
}
}
这一套下来,可以在controller中添加请求方法
@GetMapping("/doAsk2")
public String langChain4j2(@RequestParam(defaultValue = "你是谁?") String question) throws NoSuchMethodException {
System.out.println("进入doAsk2方法");
ChatResponse aiMessageResponse = aiService.doAsk(question);
System.out.println(aiMessageResponse);
return aiMessageResponse.aiMessage().text();
}
当我们再次请求“你是谁”的时候,会发现智能体的回答不同于1.3.1中简单的回答,已经发生了一些变化。
1.4.3 非阻塞式调用
好多小伙伴可能不大了解什么是阻塞什么是非阻塞,那么我们先观察一下下面的两个gif图对比一下
可以比较直观的看到第一个gif图在请求的时候,响应过程是 -> -> -> boom 一下子蹦出整个响应内容,而第二个过程感觉像是这样的 ->A…->B… ->C…
哈哈哈 上面是比较通俗的感觉,那么什么是阻塞调用什么是非阻塞调用呢?
其实在各个智能体API调用时,一般都会分为阻塞和非阻塞,小伙伴们应该都了解,在智能体调用时,有个叫做token的东西,简单理解就类似于字符这样,通常一句话可能被分开成为好几个部分,比如“你好,我是千问,我是你的ai智慧助手”,会在智能体回复你之前被划分为【你好;我;是;千问;我是;你的;ai ; 智慧助手;】每次智能体响应会相应其中的一个词语。阻塞式响应的话,智能体回答我们的问题时,一般是收集所有的片段,等待片段收集完成后,归接在一起返回相应给我们。而非阻塞式响应的话,就是实时的返回智能体的响应,不做最后的等待归集。因此就会出现上面两个gif中示例中感官上的区别。
那么应该怎实现sse即流式实时响应呢?
首先要实现sse对于spring-boot项目而言,当然是先引入依赖了,先引入web flux的依赖,支持流式响应:
<!-- springboot flux-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
在之前aiService中添加流式请求的方法
/***
* 提问 ai 方法 sse方法
* @param question
* @return
* @throws NoSuchMethodException
*/
Flux<String> doAskSSE(String question) ;
impl实现具体的流式响应的方法
/***
* 提问 ai 方法 sse方法
* @param question
* @return
* @throws NoSuchMethodException
*/
@Override
public Flux<String> doAskSSE(String question) {
Flux<String> flux = Flux.create(emitter -> {
qwenStreamingChatModel.chat(question, new StreamingChatResponseHandler() {
@Override
public void onPartialResponse(String partialResponse) {
emitter.next(partialResponse);
}
@Override
public void onCompleteResponse(ChatResponse completeResponse) {
emitter.complete();
}
@Override
public void onError(Throwable error) {
emitter.error(error);
}
});
});
return flux;
}
至此大功告成,运行后在浏览器调用,效果应该和上述第二个gif效果一致。
2.记忆对话
简单的springboot集成、阻塞式调用及流式调用已经大功告成。不知道小伙伴们有没有发现下面这个问题:
问题分析:
以上述阻塞案例为例子,我将代码搬过来,加了调用次数,换了接口路径为:/ai/memoryChart/doAsk
我现在有以下调用流程:
1-调用接口告诉qwen 我是jerry
2-调用qwen 问我是谁?
第一次调用:
第二次调用:
很显然,qwen不知道我的名字,但是我明明在第一次对话中告诉过他了呀;可见对于qwen来说,他暂时是没有记忆的,我们要进行一点改造使得他知道我是谁。
那么改造的方式呢有两种,第一种是将第一次的上下文再通过一定的方式再喂给它,具体的形式如下
2.1手动获取上下文并传递给模型
我们创建一个新的方法,书写如下代码,通过写死用户消息及系统打印的方式,打印模型给到我们的返回值,将第一次的返回消息及问题一起放入第二次的请求参数中,手动喂给qwen上下文:
@GetMapping("/doAskWithMemory1")
public String langChain4j_memory() {
UserMessage userMessage = new UserMessage("halo 我是 jerry");
ChatResponse chat1Resp = qwenChatModel.chat(userMessage);
AiMessage aiMessage = chat1Resp.aiMessage();
System.out.println("第一次调用返回:" + aiMessage.text());
System.out.println("--------------------");
String question2 = "我是谁?";
ChatResponse chat2Resp = qwenChatModel.chat(userMessage, aiMessage, UserMessage.from(question2));
System.out.println("第二次调用返回:" + chat2Resp.aiMessage().text());
return "langChain4j_memory方法结束";
}
我们看看执行结果:
由于我们直接进行系统打印,请求返回将不在重要,直接看控制台out输出即可
此时发现,通过手动给qwen模型喂上下文的方式,使得他拥有了一些记忆,也知道了我是谁。但是大家不妨想一想,如果有一些复杂的逻辑结构,难道要将每一次的问题结果做收集,再喂给大模型嘛?这样未免太麻烦了叭。
所以这个方式,可以但是不推荐。⚠️
2.2Chat 记忆缓存组件✅
记忆缓存是聊天系统中的一个重要组件,用于存储和管理对话的上下文信息。它的主要作用是让AI助手能够”记住”之前的对话内容,从而提供连贯和个性化的回复。
ChatMemory是一个用于管理聊天上下文的组件,它可以解决以下问题:
- 防止上下文超出大模型的token限制
- 隔离不同用户的上下文信息
- 简化ChatMessage的管理
其核心可以理解为以下几点:
- 容器管理: 管理ChatMessage的生命周期
- 淘汰机制: 防止存储过多消息
- 持久化: 避免聊天上下文丢失
摘自[Chat 记忆缓存 - 零基础入门Java AI](https://javaai.pig4cloud.com/docs/07Chat 记忆)
对于ChatMemory而言,Langchain4j提供了两种实现方式,每一种呢都有自己的特点与应用方式
1.MessageWindowChatMemory
开箱即用,简单实现✅
MessageWindowChatMemory是一个基于消息数量的简单实现。维护一个固定大小的消息窗口,保存最近的N条交互信息,确保系统不会处理过长的上下文,同时保留关键的历史信息。如果窗口大小是5,那么保存最近的5条消息,新的消息进来后,最旧的那条会被丢掉。这样做的好处是防止内存或处理负担过重,尤其是在长时间对话中,避免累积太多数据
特点:
- 易于理解和实现
- 基于消息数量进行管理
- 适用于不需要精确token控制的场景
那么对于性能优化的小伙伴来说,可能得注意以下几点
实践:
- 评估窗口大小:通过A/B测试选择最佳N值,例如监控任务完成率与响应时间。
- 异常处理:窗口大小为0时禁用历史记录,或抛出配置错误。
- 性能监控:在高并发场景下,监测内存使用与消息处理延迟,优化数据结构(如使用循环缓冲区)
2.TokenWindowChatMemory
TokenWindowChatMemory以token数量为限制的窗口。也就是说,它不会限制消息的数量,而是限制总token数,动态维护上下文窗口,确保总Token数不超过预设阈值(如LLM模型的最大输入限制),这样能更精确地控制内存和模型输入的长度。
特点:
- Token容量限制
- 设定总Token上限(如4096 Token),新消息加入时,若总Token数超过阈值,按时间顺序淘汰最早的消息,直至满足限制。
- 相比固定消息数量的窗口,更精准适配模型输入长度限制(如GPT-4的上下文窗口)。
- 动态调整
- 消息可能因Token长度差异被部分保留(如截断长消息)或完全移除,需权衡完整性与容量。
实践:
- 大语言模型(LLM)集成:适配模型的固定Token输入限制(如GPT-3的2048 Token)。
- 变长消息处理:消息长度差异大时(如用户发送长文本或短指令),避免固定条数窗口导致的Token不可控。
- 成本敏感场景:按Token计费的API调用(如OpenAI),精确控制输入长度以降低成本。
2.3记忆实现-共享记忆
那么在此我们简单的实现一下基于MessageWindowChatMemory的记忆存储,让我们的大模型拥有记忆。
那么官方提供了如下的集成代码,首先呢声明一个配置类
/**
* @version 1.0
* @Author jerryLau
* @Date 2025/3/26 13:33
* @注释 ai配置类
*/
@Configuration
public class AIConf {
}
然后再配置类中声明一个ChatAssistant接口
/***
* 聊天助手接口
*/
public interface ChatAssistant {
/***
* 普通聊天 非隔离上下文
* @param question
* @return
*/
String chat(String question);
/***
* 流式输出 非隔离上下文
* @param question
* @return
*/
TokenStream streamChat(String question);
}
然后通过@Bean注解,将这个ChatAssitant当成一个SpringBean
/***
* 注入聊天服务 非隔离上下文
* @param chatLanguageModel
* @param streamingChatLanguageModel
* @return
*/
@Bean
public ChatAssistant chatAssistant(ChatLanguageModel chatLanguageModel,
StreamingChatLanguageModel streamingChatLanguageModel,
ToolService toolService) {
//使用简单的ChatMemory - MessageWindowChatMemory来保存聊天记录
MessageWindowChatMemory messageWindowChatMemory = MessageWindowChatMemory.builder()
.maxMessages(10).build(); //最多保存10条记录
ChatAssistant build = AiServices.builder(ChatAssistant.class)
.chatLanguageModel(chatLanguageModel)
.streamingChatLanguageModel(streamingChatLanguageModel)
.chatMemory(messageWindowChatMemory)
.build();
return build;
}
那具体的实现逻辑解释如下:
1-当Spring Boot应用启动时,会加载配置类AIConf,并尝试创建Bean。被注释的@Bean方法会接收ChatLanguageModel和StreamingChatLanguageModel作为参数,这两个是与AI模型交互的组件,像前面用到的qwenChatModel,qwenStreamingChatModel分别实现了这两个接口。
2-创建了一个MessageWindowChatMemory实例,设置最大保存10条消息
3-通过LangChain4j 的 AiServices
类负责为接口创建动态代理。核心逻辑在 build()
,这里使用 JDK 动态代理 (Proxy.newProxyInstance
),所有接口方法的调用会被路由到自定义的 InvocationHandler
,当Controller中调用 chatAssistant.chat(question)
时,动态代理会触发 InvocationHandler.invoke()
,其中会判断当前传入的是什么模型以及是否存在记忆存储,在底层拼装记忆,并存储新的记忆后,调用传入模型的chat方法进行请求得到结果。
那么我们就可以定义controller进行使用定义的ChatAssistant
@Resource
private AIConf.ChatAssistant chatAssistant;
/***
* 记忆调用 2
* 使用langchain4j 自带的记忆化工具进行上下文记忆
* @param
* @return
*/
@GetMapping("/doAskWithMemory2")
public String langChain4j_memory2(@RequestParam(defaultValue = "halo 我是jerry") String question) {
String chat = chatAssistant.chat(question);
return chat;
}
可以看到,qwen现在知道我是谁了
但是伴随着上述方式的实现,又出现了一个问题,小伙伴们可以再想想看,我上面两次都是通过idea插件apifox实现的接口调用,qwen知道我是谁了,那么如果我用浏览器调用这个接口,模拟一个新用户进行使用,发送请求询问“我是谁”,有会出现什么样的结果呢
通过实践我们发现,即使是一个新用户,我询问qwen我是谁,qwen仍然知道我是之前的jerry,这显然是不大合理的。那么对于上述的记忆的代码实现而言,虽然实现了记忆存储但是不同的用户之间没有隔离,怎么实现不同用户之间的记忆隔离呢,继续往下看~
2.4记忆实现-非共享记忆
在进行记忆存储时,会有一个类似于数据库主键的id,叫做memoryId,在进行记忆实现时,对于不同德用户而言,带一个唯一的id,在记忆存储时,根据这个id作为唯一标识进行存储,这样一来是不是实现了不同用户的记忆分离。那我们来实现一下
chatAssistant 添加一个新的方法,参数包括一个memoryId 和 一个用户消息
/***
* 普通聊天 隔离上下文 通过id 进行上下文隔离 达到不通用户聊天互不影响的效果
* @param id 上下文id
* @param question 用户问题
* @return
*/
String chat(@MemoryId int id, @UserMessage String question);
注入的bean需要做id的配置
/***
* 注入聊天服务 隔离上下文
* @param chatLanguageModel
* @param streamingChatLanguageModel
* @return
*/
@Bean
public ChatAssistant chatAssistant(ChatLanguageModel chatLanguageModel, StreamingChatLanguageModel streamingChatLanguageModel) {
//使用简单的ChatMemory - MessageWindowChatMemory来保存聊天记录
// MessageWindowChatMemory.builder().maxMessages(10).id(memoryId).build()
ChatAssistant build = AiServices.builder(ChatAssistant.class)
.chatLanguageModel(chatLanguageModel)
.streamingChatLanguageModel(streamingChatLanguageModel)
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.builder().maxMessages(10).id(memoryId).build())
.build();
return build;
}
通过
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.builder().maxMessages(10).id(memoryId).build())
实现自定义memoryId作为用户记忆消息id,实现用户消息记忆的分离存储,接下来编写controller层代码进行实现调用
/***
* 记忆调用 2
* 使用langchain4j 自带的记忆化工具进行上下文记忆
* @param
* @return
*/
@GetMapping("/doAskWithMemory2")
public String langChain4j_memory2(@RequestParam(defaultValue = "halo 我是jerry") String question, @RequestParam long id) {
String chat = chatAssistant.chat(id, question);
return chat;
}
那我们模拟1号玩家,先告诉模型我叫jerry,再接着问我是谁
再模拟2号玩家,接着问我是谁,发现模型不知道我是谁,在像模型介绍我是谁后,模型知道了我是谁
至此呢,我们实现了用户的隔离,即不同的用户可以单独的运用模型进行相关操作。
2.5记忆持久化存储
那么对于上面的记忆相关的内容,是存在内存变量中的,程序在启停时,内存变量中的数据也会被刷掉。于是我们想到的能不能将这些记忆话的内容进行持久化的存储,比如说存储到量数据库。
而此处我们选择将数据存储在一个mapDB中,要实现自定义的记忆持久化,首先要实现langchain4j提供的记忆持久化的接口ChatMemoryStore
//ChatMemoryStore源码,提供了获取,更新,删除等基本操作
import dev.langchain4j.data.message.ChatMessage;
import java.util.List;
public interface ChatMemoryStore {
List<ChatMessage> getMessages(Object var1);
void updateMessages(Object var1, List<ChatMessage> var2);
void deleteMessages(Object var1);
}
我们自定义一个PersistentChatMemoryStore 实现ChatMemoryStore接口
/***
* @version 1.0
* @Author jerryLau
* @Date 2025/3/26 13:33
* @注释 持久化内存
*/
public class PersistentChatMemoryStore implements ChatMemoryStore {
// 创建数据库chat-memory.db
DB db = DBMaker.fileDB("langchain4j-springBoot-demo/src/main/java/com/jerry/langchain4jspringbootdemo/store/db/chat-memory.db")
.transactionEnable()
.make();
//创建集合 messages,数据key为int,value为 string类型
private final Map<Integer, String> map = db.hashMap("messages", INTEGER, STRING).createOrOpen();
/***
* 从map 中获取记忆信息
* key 是 memoryId
*/
@Override
public List<ChatMessage> getMessages(Object memoryId) {
String json = map.get((int) memoryId);
return messagesFromJson(json);
}
/***
* 更新map 中记忆信息
* key 是 memoryId
*/
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
String json = messagesToJson(messages);
map.put((int) memoryId, json);
db.commit();
}
/***
* 从map 中删除记忆信息
* key 是 memoryId
*/
@Override
public void deleteMessages(Object memoryId) {
map.remove((int) memoryId);
db.commit();
}
那么在aiConf中将设置好的PersistentChatMemoryStore注解进入
/***
* 注入聊天服务 隔离上下文 - 持久化聊天上下文记录-mapdb
* @param chatLanguageModel
* @param streamingChatLanguageModel
* @return
*/
@Bean
public ChatAssistant chatAssistantWithStore(ChatLanguageModel chatLanguageModel, StreamingChatLanguageModel streamingChatLanguageModel) {
//使用mapdb 来保存聊天记录
PersistentChatMemoryStore persistentChatMemoryStore = new PersistentChatMemoryStore();
ChatMemoryProvider chatMemoryProvider = (memoryId) -> {
return MessageWindowChatMemory.builder()
.maxMessages(10)
.id(memoryId)
.chatMemoryStore(persistentChatMemoryStore)
.build();
};
ChatAssistant build = AiServices.builder(ChatAssistant.class)
.chatLanguageModel(chatLanguageModel)
.streamingChatLanguageModel(streamingChatLanguageModel)
.chatMemoryProvider(chatMemoryProvider)
.build();
return build;
}
controller中的请求方法还是与langChain4j_memory2一致,在发送请求后,看到在相应的目录路径下生成了 一个名为chat-memory.db的数据库文件
但是有个问题哈 这个数据库文件 应该怎么打开,本人试着用dateDrip打开但是没打开,有没有大佬指导一下⚠️⚠️⚠️
最后实在是没办法了,我直接写了个main,将数据读了出来,代码供各位参考:
/**
* @version 1.0
* @Author jerryLau
* @Date 2025/3/26 17:12
* @注释 读取mapdb文件
*/
public class MapDBViewer {
public static void main(String[] args) {
// 打开数据库文件(只读模式,避免意外修改)
DB db = DBMaker
.fileDB("langchain4j-springBoot-demo/src/main/java/com/jerry/langchain4jspringbootdemo/store/db/chat-memory.db")
.readOnly()
.make();
// 获取存储的Map(假设数据存储在名为"myMap"的HTreeMap中)
HTreeMap<Integer, String> map = db.hashMap("messages")
.keySerializer(org.mapdb.Serializer.INTEGER)
.valueSerializer(org.mapdb.Serializer.STRING)
.createOrOpen();
// 遍历并打印所有键值对
map.forEach((key, value) -> System.out.println("Key: " + key.toString() + ", Value: " + value));
// 关闭数据库
db.close();
}
}
读取到的内容如下:
Key: 11112, Value: [{“contents”:[{“text”:“为我讲个笑话吧”,“type”:“TEXT”}],“type”:“USER”},{“text”:“当然可以,接下来是一个轻松的笑话:\n\n为什么电脑经常生病?\n\n因为它的窗户(Windows)总是开着!”,“type”:“AI”}]
3.Function-call(Tools)
LangChain4j 的 Tools 机制 是一种让开发者将自定义 Java 方法动态暴露给大语言模型(LLM)的框架,允许 LLM 调用外部工具(如计算、API 接口、数据库查询等),突破纯文本生成的限制。
例如,我们知道大型语言模型本身并不擅长数学。如果您的用例偶尔涉及数学计算,您可能希望为 LLM 提供一个“数学工具”。通过在向 LLM 发出的请求中声明一个或多个工具,它就可以在认为合适时调用其中一个。给定一个数学问题以及一组数学工具,LLM 可能会决定为了正确回答该问题,它应该首先调用所提供的数学工具之一。
3.1简单算数
例如我们提供两个数学方法:
@Service
public class ToolService {
@Tool("Sums 2 given numbers")
double sum(double a, double b) {
return a + b;
}
@Tool("Returns a square root of a given number")
double squareRoot(double x) {
return Math.sqrt(x);
}
}
修改aiConf,为注入的bean 添加tools,
@Bean
public ChatAssistant chatAssistant(ChatLanguageModel chatLanguageModel,
StreamingChatLanguageModel streamingChatLanguageModel,
ToolService toolService) {
//使用简单的ChatMemory - MessageWindowChatMemory来保存聊天记录
MessageWindowChatMemory messageWindowChatMemory = MessageWindowChatMemory.builder()
.maxMessages(10).build(); //最多保存10条记录
ChatAssistant build = AiServices.builder(ChatAssistant.class)
.chatLanguageModel(chatLanguageModel)
.streamingChatLanguageModel(streamingChatLanguageModel)
.chatMemory(messageWindowChatMemory)
.tools(toolService) //添加tools
.build();
return build;
}
继续使用langChain4j_memory2方法调用,会发现LLM通过toolsService的sum方法返回3.2,再结合LLM进行结果输出,存在控制台输出以及结果输出
正如你 所见,当大型语言模型能够使用工具时,它可以在适当的时候决定调用其中一个工具。
3.2更加复杂的案例
@Tools非常强大的功能。在上个简单的示例中,我们为语言模型提供了基本的数学工具,但想象一下,如果我们给它提供例如 sendEmail(发送电子邮件)这样的工具,并且给出这样的查询:“我的朋友想了解人工智能领域的近期新闻。将简短摘要发送至 friend@email.com”,那么它就可以使用 googleSearch 工具查找近期新闻,然后进行总结,并通过 sendEmail 工具将摘要发送出去。
我们简单的实现这个例子,优先创建一个tool
@Tool("Send the short recent news in any field to the user.")
public String generateAI(@P("mailAddr") String mailAddress, @P("filed") String filed) throws MessagingException {
System.out.println("输入:" + mailAddress);
System.out.println("输入:" + filed);
UserMessage userMessage = new UserMessage("generate the short recent news in " + filed + " field");
ChatResponse chat1Resp = qwenChatModel.chat(userMessage);
String text = chat1Resp.aiMessage().text();
boolean b = mailService.sendEmail(
"XXXX@qq.com", "QQ邮箱的授权码", mailAddress
, "Short recent news in " + filed + " filed", text);
//mailAddress
if (b) {
System.out.println("邮件发送成功!");
}
return "邮件发送成功!";
}
具体实现发送邮件的方法mailService.sendEmail
/**
* @version 1.0
* @Author jerryLau
* @Date 2025/3/28 11:18
* @注释 邮件服务
*/
@Service
public class MailService {
public boolean sendEmail(
String addr, String password, // 此处 password 实际是授权码
String toAddress, String subject, String message)
throws MessagingException {
Properties props = new Properties();
props.put("mail.smtp.host", "smtp.qq.com");
props.put("mail.smtp.port", "465");
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.ssl.enable", "true"); // 开启ssl 验证
props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
props.put("mail.smtp.socketFactory.port", "465");
// props.put("mail.debug", "true"); // 启用调试日志
Authenticator authenticator = new Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(addr, password); // 密码参数传入授权码
}
};
Session session = Session.getInstance(props, authenticator);
try {
Message mimeMessage = new MimeMessage(session);
mimeMessage.setFrom(new InternetAddress(addr));
mimeMessage.setRecipient(Message.RecipientType.TO, new InternetAddress(toAddress));
mimeMessage.setSubject(subject);
mimeMessage.setText(message);
Transport.send(mimeMessage);
System.out.println("邮件发送成功!");
return true;
} catch (MessagingException e) {
throw new RuntimeException(e);
}
}
}
查看邮件发现,邮件也正常收到了
4.预设角色SystemMessage
在1.4.2中其实已经通过自定义注解的方式实现了系统角色预定。其实Langchain4j也提供了自己的注解配置实现系统预定角色–@SystemMessage
那么至此呢我们已经学习完了Spring-boot集成Langchain4j以及阿里云百炼模型。感兴趣的同学可以自己体会应用啦
码字不易,各位观众老爷如果喜欢,希望一键三连哈~~~
参考文档:
Introduction | LangChain4j官方文档
Langchain4j中文版本文档
个人代码demo
后面有时间了,我再整理放一下~~~