我们先用AiService工具类把调用ai大语言模型的代码写出来。因为AiService工具类中整合有记忆、Rag知识库、tools工具等,我们直接配置调用即可。
我用的是qwen-plus模型。
引入依赖:
<dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-open-ai-spring-boot-starter</artifactId><version>1.0.1-beta6</version></dependency>
<!-- langchain4j aiService工具类--><dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-spring-boot-starter</artifactId><version>1.0.1-beta6</version></dependency>
yaml文件:
langchain4j:open-ai:chat-model: # 会自动往容器中注入OpenAI Chat Model(阻塞式的)base-url: https://dashscope.aliyuncs.com/compatible-mode/v1api-key: 你的keymodel-name: qwen-pluslog-requests: truelog-responses: true
然后我们声明个接口,然后用@AiService注解去配置各种信息。
aiService工具类会自动调用,帮助我们创建该接口的代理对象,并注入到IoC容器中
//声明式接口
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,//手动装配代理对象chatModel = "openAiChatModel", //阻塞式调用的模型
)
public interface ConsultantService {String chat1(String message);
}
阻塞式是把答案全部准备好后在一次性返回给你; 非阻塞式(流式)是每次只返回2~3句话,返回很多次。
然后我们去写个controller去测试一下:
@RestController
@RequestMapping("/ai")
public class ChatController {@Autowiredprivate ConsultantService consultantService;@RequestMapping("/chat1")public String chat1(@RequestParam("message") String message){return consultantService.chat1(message);}
}
运行后,访问http://localhost:8080/ai/chat1?message=帮我写一篇500字文章
在转了1、2分钟的圈后,你会发现文章是一次性出现的。
下面我们看看流式调用:
引入依赖:
<!-- stream流式调用--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId></dependency><dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-reactor</artifactId><version>1.0.1-beta6</version></dependency>
首先我们需要现在yaml文件中添加流式调用的配置:
langchain4j:open-ai:chat-model: # 会自动往容器中注入OpenAI Chat Model(阻塞式的)base-url: https://dashscope.aliyuncs.com/compatible-mode/v1api-key: 你的keymodel-name: qwen-pluslog-requests: truelog-responses: truestreaming-chat-model: # 会自动往容器中注入OpenAI Streaming Chat Model(流式的)api-key: 你的keybase-url: https://dashscope.aliyuncs.com/compatible-mode/v1model-name: qwen-pluslog-requests: truelog-responses: truelogging:level:dev.langchain4j: debug
streaming-chat-model的配置与chat-model的配置完全一样。
返回到我们的ConsultantService接口中,配置流式调用的模型,写一个新的返回值为Flux的新接口:
@AiService( //aiService工具类会自动调用,帮助我们创建该接口的代理对象,并注入到IoC容器中wiringMode = AiServiceWiringMode.EXPLICIT,//手动装配代理对象chatModel = "openAiChatModel", //阻塞式调用的模型streamingChatModel = "openAiStreamingChatModel", //流式调用的模型
)
public interface ConsultantService {String chat1(String message);Flux<String> chat2(String message);
然后写一个新的controller方法:
@RestController
@RequestMapping("/ai")
public class ChatController {@Autowiredprivate ConsultantService consultantService;@RequestMapping("/chat1")public String chat1(@RequestParam("message") String message){return consultantService.chat1(message);}@RequestMapping(value = "/chat2",produces = "text/html;charset=utf-8") //produces属性指定编码类型public Flux<String> chat2(@RequestParam("message") String message){return consultantService.chat2(message);}
}
注解中要写produces = "text/html;charset=utf-8",不然返回的文本会乱码
我们再次访问 http://localhost:8080/ai/chat2?message=帮我写一篇500字文章
你会发现它是每次只返回2~3句话。
下面我们来看消息注解(写在接口方法的上面)
消息注解有俩种:
1、@SystemMessage 为系统注解,你可以理解为为ai大模型加一个系统前提,用于修饰ai的,可以用消息注解指明ai的身份,也可以让其只回答某方面的问题。
它有俩种使用方法:1、你可以直接把 系统前提 写在注解的后面
@SystemMessage("你是群众的好助手小团团,温柔且幽默") //消息注解
Flux<String> chat2(String message);
此时访问 http://localhost:8080/ai/chat2?message=你是谁
它回答就会是小团团,而不是通义百炼。
2、如果你的系统前提很多,你也可以把文本文件导在resources目录下,然后在接口注解中指明目录
@SystemMessage(fromResource = "system.txt") //如果注解的消息过长,可以指定对应的文本文件
Flux<String> chat2(String message);
2、@UserMessage 用户消息注解,每次提问时都会加上该内容,用于修饰我们提问的内容。
它也有俩种使用方法:1、直接把 前提 写在注解的后面。其中的{{it}}固定不变,指的是message
@UserMessage("你是群众的好助手小月月,温柔且幽默。{{it}}")
Flux<String> chat2(String message);
注意:如果这时访问该接口问他:你是谁 。它的回答是它是通义百炼,且你叫它小团团是认错了。
2、如果你不想用{{it}}你也可以换别的,但要用@V注解:
@UserMessage("你是群众的好助手小月月,温柔且幽默。{{msg}}")
Flux<String> chat2(@V("msg")String message);
会话记忆功能:
大模型没有记忆能力,我们需要在web后端建立一个存储对象,在下次发问题时,把聊天记录一起在发给大模型。
所以我们需要写一个配置类,来创建会话记忆对象注入到IoC容器中
@Configuration
public class CommonConfiguration {
//构建会话记忆对象@Beanpublic ChatMemory chatMemory(){return MessageWindowChatMemory.builder().maxMessages(20) //设置存储的最大消息数量.build();}
}
然后在我们的声明式接口中配置我们的记忆存储对象chatMemory
@AiService( //aiService工具类会自动调用,帮助我们创建该接口的代理对象,并注入到IoC容器中wiringMode = AiServiceWiringMode.EXPLICIT,//手动装配代理对象chatModel = "openAiChatModel", //阻塞式调用的模型streamingChatModel = "openAiStreamingChatModel", //流式调用的模型chatMemory = "chatMemory"//配置会话对象,为配置类中定义的记忆对象的方法名(公有的)
)
public interface ConsultantService {String chat1(String message);Flux<String> chat2(String message);
此时该大模型就能简单实现会话记忆的功能,但有个问题,当我们创建一个新的会话时,该新会话保留着原来旧会话的记忆,也就是说我们现在创建的记忆存储对象是所有的会话公有的,每一个会话都会从同一个chatMemory中获取记忆。
为了解决会话记忆隔离问题,我们需要引入ChatMemoryProvider对象,然后为每个会话设置个唯一标识。当创建了一个新的会话时,即不能从容器中获取到对应的id标识时,就会调用ChatMemoryProvider对象的get方法创建一个新的chatMemory对象。
创建好ChatMemoryProvider对象后,把它配置到声明式接口中,最后不要忘了该controller层的方法,要增加一个messageId参数。
1、定义ChatMemoryProvider对象
@Configuration
public class CommonConfiguration {
//大模型没有记忆能力,我们需要在web后端建立一个存储对象,在下次发问题时,把聊天记录一起在发给大模型//构建会话记忆对象@Beanpublic ChatMemory chatMemory(){return MessageWindowChatMemory.builder().maxMessages(20) //设置存储的最大消息数量.build();}//会话隔离//定义会话记忆对象提供者(但后端重启的话,记忆就没了)@Bean //如果没有从容器获取指定的id对象,就会调用该提供者的get方法创建一个新的chatMemory对象public ChatMemoryProvider chatMemoryProvider(){ChatMemoryProvider chatMemoryProvider = new ChatMemoryProvider() {@Overridepublic ChatMemory get(Object memoryId) {return MessageWindowChatMemory.builder().id(memoryId).maxMessages(20).build();}};return chatMemoryProvider;}
}
2、配置ChatMemoryProvider对象,也要修改接口方法
@AiService( //aiService工具类会自动调用,帮助我们创建该接口的代理对象,并注入到IoC容器中wiringMode = AiServiceWiringMode.EXPLICIT,//手动装配代理对象chatModel = "openAiChatModel", //阻塞式调用的模型streamingChatModel = "openAiStreamingChatModel", //流式调用的模型//chatMemory = "chatMemory"//配置会话对象,为配置类中定义的记忆对象的方法名(公有的)chatMemoryProvider = "chatMemoryProvider" //配置会话隔离,为配置类中定义的记忆对象提供者的方法名 (瑕疵版)
)
public interface ConsultantService {//有多个参数时,需要用注解分别表明其含义Flux<String> chat2(@UserMessage String message, @MemoryId String memoryId);//@MemoryId注解告诉它哪个参数是那个唯一标识}
3、修改controller层方法
@RestController
@RequestMapping("/ai")
public class ChatController {@Autowiredprivate ConsultantService consultantService;@RequestMapping(value = "/chat2",produces = "text/html;charset=utf-8") //produces属性指定编码类型public Flux<String> chat2(@RequestParam("message") String message,@RequestParam("memoryId") String memoryId){return consultantService.chat2(message,memoryId);}
}
现在我们就实现了会话记忆的功能了
但现在还有一个小瑕疵:当我们后端重启后,所有的会话记忆就消失了。
我们先看看现在它是把会话记忆存储在哪里了?
配置类中用的chatMemoryProvider对象存储的会话记忆,chatMemoryProvider对象创建新的chatMemory对象时用的是MessageWindowChatMemory类的build方法,我们进入MessageWindowChatMemory类找到它的store方法:
我们可以发现store为空时,调用的是SingleSlotChatMemoryStore对象的构造方法,我们进入SingleSlotChatMemoryStore对象
我们可以发现该对象是用List对象存储的会话记忆,所以是存储在服务器内存上的。
我们需要存储在redis或者mysql中,就能实现记忆持久化,并且我们自己写一个根据redis或mysql存储的ChatMemoryStore对象,并把该ChatMemoryStore对象传给MessageWindowChatMemory对象即可。
我以存储在redis为例:我是在本地运行的redis
1、首先我们引入redis依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>
2、然后在yaml文件中配置我们的redis的相关信息
spring:data:redis:host: localhostport: 6379password: 你的密码(没有可不写)
3、接下来我们写一个新的RedisChatMemoryStore对象让其实现 ChatMemoryStore接口,并重写它的三个方法:获取记忆、更新记忆、删除记忆。
@Repository //把该实现类对象交给IoC容器
public class RedisChatMemoryStore implements ChatMemoryStore {//用redis缓存记忆@Autowiredprivate StringRedisTemplate template;private String preName="langchain4j:";@Overridepublic List<ChatMessage> getMessages(Object memoryId) {//获取会话消息String str = template.opsForValue().get(preName+memoryId.toString());//把josn转为ListList<ChatMessage> list = ChatMessageDeserializer.messagesFromJson(str);return list;}@Overridepublic void updateMessages(Object memoryId, List<ChatMessage> list) {//更新会话记忆//1、把list中的记忆转为json格式String str = ChatMessageSerializer.messagesToJson(list);//2、把json格式记忆存入redis,并设置过期时间为1天template.opsForValue().set(preName+memoryId.toString(),str, Duration.ofDays(1));}@Overridepublic void deleteMessages(Object memoryId) {template.delete(preName+memoryId.toString());}
}
4、最后把创建好的RedisChatMemoryStore传给MessageWindowChatMemory对象
@Configuration
public class CommonConfiguration {@Autowiredprivate RedisChatMemoryStore store;//会话隔离//定义会话记忆对象提供者(但后端重启的话,记忆就没了)@Bean //如果没有从容器获取指定的id对象,就会调用该提供者的get方法创建一个新的chatMemory对象public ChatMemoryProvider chatMemoryProvider(){ChatMemoryProvider chatMemoryProvider = new ChatMemoryProvider() {@Overridepublic ChatMemory get(Object memoryId) {return MessageWindowChatMemory.builder().id(memoryId).maxMessages(20)//配置redis存储.chatMemoryStore(store).build();}};return chatMemoryProvider;}
}
这样就能实现把会话记忆根据memoryId存储在redis中了。