1、langchain4j分为高阶API和低阶API
- 低阶API:按照我的理解,就是大部分功能封装好了(调用大模型等操作),但是一些配置信息,还是需要自己定制,配置
- 高阶API:按照我的理解,就是所有功能基本都封装好了类似mybatis-plus,减少重复造轮子
低阶API、需要引入的maven坐标
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>1.0.0-beta3</version>
</dependency>
高阶API,需要引入的maven坐标
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>1.0.0-beta3</version>
</dependency>
也可以引入bom物料清单,进行版本控制
<dependencyManagement>
<dependencies>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-bom</artifactId>
<version>1.0.0-beta3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2、低阶API调用方式
- 首先配置聊天模型,这里我用的是阿里的百炼
- System.getenv("aliAi-key"):把阿里的百炼KEY,配置到系统环境变量,防止泄漏
@Bean
public ChatModel chatModel() {
return OpenAiChatModel.builder()
.apiKey(System.getenv("aliAi-key"))
.modelName("qwen-plus")
.logRequests(true)
.logResponses(true)
.baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
.build();
}
- 通过模型对象,直接调用
@Autowired
private ChatModel chatModel;
@RequestMapping(value = "/prompt/chat3")
public String chat3(String prompt) throws IOException {
PromptTemplate promptTemplate = PromptTemplate.from("根据中国{{legal}}法律,解答以下问题:{{question}}");
Prompt apply = promptTemplate.apply(Map.of("legal", "知识产权", "question", "什么是知识产权?"));
UserMessage userMessage = apply.toUserMessage();
PromptTemplate promptTemplate2 = PromptTemplate.from("你是一个专业的法律助手,请根据用户输入的问题会打法律相关信息");
//填写对应的模板参数
Prompt apply2 = promptTemplate2.apply("");
SystemMessage systemMessage = apply2.toSystemMessage();
//如果不需要模板,也可以直接创建
SystemMessage systemMessage1 = new SystemMessage("你是一个专业的法律助手,请根据用户输入的问题会打法律相关信息");
ChatResponse chatResponse = chatModel.chat(systemMessage,userMessage);
return chatResponse.aiMessage().text();
}
3、高阶API调用
- 定义接口
- 这里的UserMessage、SystemMessage注解,就是提示词,告诉大模型这个助手的基本信息
- SystemMessage:系统提示词,一般是我们自己定义,最好不要让用户可以自定义,不然就会出现“sql注入”,用户可以改变这个系统提示词的的含义,大模型就会理解错误
- UserMessage:一般就是用户自己输入的问题了,也可以像我一样,通过模板的方式,能更好的让大模型理解
- @V:这个注解,就是把函数的入参,与模板的占位符匹配,最后替换成用户的问题,发送给大模型
public interface ChatAssistant {
@SystemMessage("你是一位专业的中国法律顾问,只回答与中国有关的法律问题。输出限制:对于其他领域的问题禁止回答,直接返回‘抱歉,我只能回答中国法律相关问题。’")
@UserMessage("请回答以下法律问题:{{question}},字数控制在{{length}}以内,以{{format}}格式输出")
String chat(@V("question") String question, @V("length") int length, @V("format") String format); // userMessage 包含 "{{country}}" 模板变量
String chat2(LawPrompt lawPrompt); // userMessage 包含 "{{country}}" 模板变量
}
- 添加接口和模型直接关联的配置
- langchain4j,会自动生成一个代理对象,实现接口的所有方式
@Bean("chat")
public ChatAssistant chatAssistant(ChatModel chatModel) {
return AiServices.create(ChatAssistant.class, chatModel);
}
- 实现调用
@RequestMapping(value = "/prompt/chat")
public String chatImage(String prompt) throws IOException {
log.info("prompt1: {}", prompt);
String chat = chatAssistant.chat("什么是知识产权?", 100, "md");
String chat2 = chatAssistant.chat("什么是java?", 100, "md");
String chat3 = chatAssistant.chat("介绍一下水果西瓜和芒果", 100, "md");
String chat4 = chatAssistant.chat("飞机发动机原理", 100, "md");
return "answer01:" + chat + "\nanswer2" + chat2 + "\nanswer3" + chat3 + "\nanswer4" + chat4;
}
调用日志截图
- langchain4j会把系统消息,用户消息打包成不同的角色参数发送给大模型
- 更进一步:@AiService 方式实现:与spring-boot集成
- 引入maven坐标
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
<version>1.0.0-beta3</version>
</dependency>
- 配置文件配置参数
langchain4j:
open-ai:
chat-model:
api-key: ${aliAi-key}
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
model-name: qwen-plus
- 直接使用:无需过多配置:springboot-starter简直是好东西,减少了一堆配置
@AiService
interface Assistant {
@SystemMessage("You are a polite assistant")
String chat(String userMessage);
}
@RestController
@Slf4j
public class LangChain4JBootController {
@Autowired
private ChatAssistant chatAssistant;
@RequestMapping("/lc4f/boot/advanced")
public String hello(@RequestParam(value = "question", defaultValue = "你是谁") String question) {
String result = chatAssistant.chat(question);
log.info("result:{}", result);
return result;
}
}
4、想加上记忆功能怎么加?
LangChain4j提供了2种开箱即用的记忆功能
- MessageWindowChatMemory:保留最近的N条消息
- TokenWindowChatMemory:保留最近的N个令牌
定义接口,@MemoryId int memoryId,就是用户的ID,为每个用户提供记忆功能
public interface ChatMemoryAssistant {
String chatWithMemory(@MemoryId int memoryId, @UserMessage String userMessage);
}
配置如下
- 通过chatMemoryProvider设置记忆模式,其中TokenWindowChatMemory的记忆模式,需要设置一个Tokenizer来计算每个ChatMessage中的令牌数,用来制定淘汰哪些数据。
@Bean("chatMemoryWindowChatMemory")
public ChatMemoryAssistant chatMemoryWindowChatMemory(ChatModel chatModel) {
return AiServices
.builder(ChatMemoryAssistant.class)
.chatModel(chatModel)
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
.build();
}
@Bean("chatMemoryTokenWindowChatMemory")
public ChatMemoryAssistant chatMemoryTokenWindowChatMemory(ChatModel chatModel) {
return AiServices
.builder(ChatMemoryAssistant.class)
.chatModel(chatModel)
.chatMemoryProvider(memoryId -> TokenWindowChatMemory.withMaxTokens(1000,new OpenAiTokenCountEstimator("gpt-4")))
.build();
}
调用方式
- 这样,大模型即记住了你的历史聊天记录
@RequestMapping(value = "/memory/chat3")
public String chatMemoryTokenWindowChatMemory(String prompt) throws IOException {
log.info("prompt3: {}", prompt);
String chat = chatMemoryTokenWindowChatMemory.chatWithMemory(1, "你好,我叫java");
String s = chatMemoryTokenWindowChatMemory.chatWithMemory(1, "我叫什么啊?");
String chat2 = chatMemoryTokenWindowChatMemory.chatWithMemory(2, "你好,我叫Cpp");
String s2 = chatMemoryTokenWindowChatMemory.chatWithMemory(2, "我叫什么啊?");
return "answer03:" + chat + "\nanswer4" + s + "\nanswer5" + chat2 + "\nanswer6" + s2;
}
让我们来看看,大模型是怎么记忆的?
- 可以看到,是把之前发送的消息,通通放在一起,再一次发送给大模型,大模型就知道之前用户发送了什么消息,就是这么纯粹,我把所有消息都告诉你,你不就记住了!!!哈哈哈
- 但是这样就会有个后果:如果你配置的withMaxMessages数值过大,那么用户的消息在一直增加,到最后发送给大模型的消息就会是一个超长的文本,这样消耗的token太多,以及响应肯定会慢很多
- 我想到的解决办法是,把最近的几次用户的消息让大模型总结一下,“压缩用户的消息”,这样应该能解决部分问题
5、怎么持久化用户的记忆呢?
通过观察源码发现:默认使用的事内存存储,系统只要关机重启,用户的历史记忆就消失了
通过redis持久化存储 添加上chatMemoryStore存储功能的实现
- 在配置聊天接口代理实现的时候,添加存储实现类
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
//让前面配置生效
template.afterPropertiesSet();
return template;
}
- 实现redis存储:主要实现ChatMemoryStore类:提供消息的CRUD功能
- getMessages:获取消息
- updateMessages:更新消息
- deleteMessages:删除消息
package paperfly.persistence;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.ChatMessageDeserializer;
import dev.langchain4j.data.message.ChatMessageSerializer;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class RedisChatMemoryStore implements ChatMemoryStore {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String KEY_PREFIX = "chat_memory:";
@Override
public List<ChatMessage> getMessages(Object memoryId) {
String retValue = redisTemplate.opsForValue().get(KEY_PREFIX + memoryId);
return ChatMessageDeserializer.messagesFromJson(retValue);
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
String retValue = ChatMessageSerializer.messagesToJson(messages);
redisTemplate.opsForValue().set(KEY_PREFIX + memoryId, retValue);
}
@Override
public void deleteMessages(Object memoryId) {
redisTemplate.delete(KEY_PREFIX + memoryId);
}
}
@Bean("chatPersistenceWindowChatMemory")
public ChatPersistenceAssistant chatPersistenceWindowChatMemory(ChatModel chatModel, RedisChatMemoryStore store) {
return AiServices
.builder(ChatPersistenceAssistant.class)
.chatModel(chatModel)
.chatMemoryProvider(memoryId -> {
return MessageWindowChatMemory
.builder()
.id(memoryId)
.maxMessages(10)
.chatMemoryStore(store)
.build();
})
.build();
}