使用langchain4j遇到的难题(暂记)
目录
一、在 Spring Boot 项目中集成 LangChain4j 框架,使用 Redis 持久化聊天历史
问题
根本原因分析
解决方案:
二、Jackson 无法序列化 ToolExecutionRequest 对象(循环调用Tools)
问题:
根本原因分析:
解决方案:
注意:langchain4j对于序列化有专门的工具处理,如下:
同时对于版本较高的可以使用官方的redis模块:
三、处理消息role时遇到的问题
问题:
解决方案:
一、在 Spring Boot 项目中集成 LangChain4j 框架,使用 Redis 持久化聊天历史
问题
{"error":{"code":"1214","message":"输入不能为空"}}
以及 Jackson 序列化异常No serializer found for class dev.langchain4j.data.message.UserMessage
and no properties discovered to create BeanSerializer
根本原因分析
1. 首次对话 Redis 空数据问题
新会话(sessionId)在 Redis 中无历史记录
RedisChatMemoryStore.getMessages() 返回空列表
某些 LLM API(如智谱 AI)要求消息列表不能为空
2. Jackson 无法直接序列化 ChatMessage
LangChain4j 的消息类(UserMessage、AiMessage、SystemMessage)没有标准的 getter 方法,导致 Jackson 序列化失败。
3. 消息类混淆陷阱
容易错误导入 dev.ai4j.openai4j.chat.AssistantMessage,而实际应使用 dev.langchain4j.data.message.AiMessage。
解决方案:
核心思路:创建中间包装类
通过自定义的 MessageWrapper 类作为桥梁,实现 ChatMessage 与 JSON 的双向转换。
package com.demo.javaaitest.utile; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import dev.langchain4j.data.message.AiMessage; import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.data.message.SystemMessage; import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.store.memory.chat.ChatMemoryStore; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; @Component public class RedisChatMemoryStore implements ChatMemoryStore { private static final String KEY_PREFIX = "chat:memory:"; private static final long TTL_HOURS = 24; @Autowired private StringRedisTemplate redisTemplate; private final ObjectMapper objectMapper = new ObjectMapper(); @Override public List<ChatMessage> getMessages(Object memoryId) { String key = KEY_PREFIX + memoryId.toString(); String json = redisTemplate.opsForValue().get(key); if (json == null || json.trim().isEmpty()) { System.out.println("[RedisChatMemoryStore] 会话 " + memoryId + " 无历史记录(新会话)"); return new ArrayList<>(); } try { List<MessageWrapper> wrappers = objectMapper.readValue(json, objectMapper.getTypeFactory().constructCollectionType(List.class, MessageWrapper.class)); List<ChatMessage> messages = new ArrayList<>(); for (MessageWrapper wrapper : wrappers) { ChatMessage message = deserializeMessage(wrapper); if (message != null) { messages.add(message); } } System.out.println("[RedisChatMemoryStore] 会话 " + memoryId + " 加载了 " + messages.size() + " 条历史消息"); return messages; } catch (JsonProcessingException e) { System.err.println("[RedisChatMemoryStore] 反序列化失败, memoryId: " + memoryId + ", 错误: " + e.getMessage()); return new ArrayList<>(); } } @Override public void updateMessages(Object memoryId, List<ChatMessage> messages) { if (messages == null || messages.isEmpty()) { System.out.println("[RedisChatMemoryStore] 会话 " + memoryId + " 尝试保存空消息列表,跳过"); return; } String key = KEY_PREFIX + memoryId.toString(); try { List<MessageWrapper> wrappers = new ArrayList<>(); for (ChatMessage message : messages) { wrappers.add(serializeMessage(message)); } String json = objectMapper.writeValueAsString(wrappers); redisTemplate.opsForValue().set(key, json, TTL_HOURS, TimeUnit.HOURS); System.out.println("[RedisChatMemoryStore] 会话 " + memoryId + " 已保存 " + messages.size() + " 条消息到 Redis"); } catch (JsonProcessingException e) { System.err.println("[RedisChatMemoryStore] 序列化失败, memoryId: " + memoryId + ", 错误: " + e.getMessage()); } } @Override public void deleteMessages(Object memoryId) { String key = KEY_PREFIX + memoryId.toString(); redisTemplate.delete(key); System.out.println("[RedisChatMemoryStore] 会话 " + memoryId + " 已删除"); } /** * 将 ChatMessage 转换为可序列化的 Wrapper 对象 */ private MessageWrapper serializeMessage(ChatMessage message) { MessageWrapper wrapper = new MessageWrapper(); if (message instanceof UserMessage) { wrapper.setType("USER"); wrapper.setContent(((UserMessage) message).text()); } else if (message instanceof AiMessage) { wrapper.setType("ASSISTANT"); wrapper.setContent(((AiMessage) message).text()); } else if (message instanceof SystemMessage) { wrapper.setType("SYSTEM"); wrapper.setContent(((SystemMessage) message).text()); } else { wrapper.setType("UNKNOWN"); wrapper.setContent(""); } return wrapper; } /** * 从 Wrapper 对象还原为 ChatMessage */ private ChatMessage deserializeMessage(MessageWrapper wrapper) { if (wrapper.getContent() == null) { return null; } switch (wrapper.getType()) { case "USER": return UserMessage.from(wrapper.getContent()); case "ASSISTANT": return AiMessage.from(wrapper.getContent()); case "SYSTEM": return SystemMessage.from(wrapper.getContent()); default: System.err.println("[RedisChatMemoryStore] 未知的消息类型: " + wrapper.getType()); return null; } } /** * 消息包装类,用于 JSON 序列化 */ private static class MessageWrapper { private String type; private String content; public String getType() { return type; } public void setType(String type) { this.type = type; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } } }二、Jackson 无法序列化 ToolExecutionRequest 对象(循环调用Tools)
问题:
使用@Tool注解,工具调用必须保存toolExecutionRequests列表时进行序列化失败
根本原因分析:
因为它的字段是private且没有标准的 getter 方法(或者 Jackson 找不到可序列化的属性)。我们需要将toolExecutionRequests转换为 Jackson 能理解的格式(比如Map)来存储,读取时再反向构造。
解决方案:
在updateMessages中,不要直接存储ToolExecutionRequest对象,而是将其转换为Map<String, Object>:
if (msg instanceof AiMessage) {
AiMessage aiMsg = (AiMessage) msg;
if (aiMsg.hasToolExecutionRequests()) {
List<Map<String, Object>> requestMaps = aiMsg.toolExecutionRequests().stream()
.map(req -> {
Map<String, Object> reqMap = new HashMap<>();
reqMap.put("id", req.id());
reqMap.put("name", req.name());
reqMap.put("arguments", req.arguments());
return reqMap;
})
.collect(Collectors.toList());
map.put("toolExecutionRequests", requestMaps);
} else {
map.put("text", aiMsg.text());
}
}
在getMessages中,读取时反向转换
case "AI":
Object requests = map.get("toolExecutionRequests");
if (requests != null) {
// requests 是一个 List<Map<String, Object>>
List<Map<String, Object>> requestMaps = (List<Map<String, Object>>) requests;
List<ToolExecutionRequest> toolRequests = requestMaps.stream()
.map(reqMap -> ToolExecutionRequest.builder()
.id((String) reqMap.get("id"))
.name((String) reqMap.get("name"))
.arguments((String) reqMap.get("arguments"))
.build())
.collect(Collectors.toList());
return new AiMessage(toolRequests);
} else {
String text = (String) map.get("text");
return text != null ? new AiMessage(text) : null;
}
注意:langchain4j对于序列化有专门的工具处理,如下:
package com.demo.javaaitest.redis; 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.StringRedisTemplate; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @Component public class RedisChatMemoryStore implements ChatMemoryStore { @Autowired private StringRedisTemplate redisTemplate; private static final String KEY_PREFIX = "chat:"; @Override public List<ChatMessage> getMessages(Object memoryId) { String key = KEY_PREFIX + memoryId.toString(); String json = redisTemplate.opsForValue().get(key); if (json == null || json.isEmpty()) { return new ArrayList<>(); } try { // 使用官方反序列化工具,一行代码搞定 return ChatMessageDeserializer.messagesFromJson(json); } catch (Exception e) { e.printStackTrace(); return new ArrayList<>(); } } @Override public void updateMessages(Object memoryId, List<ChatMessage> messages) { String key = KEY_PREFIX + memoryId.toString(); try { // 使用官方序列化工具,一行代码搞定 String json = ChatMessageSerializer.messagesToJson(messages); redisTemplate.opsForValue().set(key, json, 24, TimeUnit.HOURS); } catch (Exception e) { e.printStackTrace(); } } @Override public void deleteMessages(Object memoryId) { String key = KEY_PREFIX + memoryId.toString(); redisTemplate.delete(key); } }同时对于版本较高的可以使用官方的redis模块:
import dev.langchain4j.store.memory.chat.RedisChatMemoryStore;
// 在你的配置类中
@Bean
public ChatMemoryStore chatMemoryStore(RedisClient redisClient) {
// RedisChatMemoryStore 的构造方法可能因版本而异,请参考官方文档
return new RedisChatMemoryStore(redisClient);
}
三、处理消息role时遇到的问题
问题:
不同的 LLM 提供商对角色名的格式要求可能不同
解决方案:
在 LangChain4j 中,Role主要用于标识对话中不同消息的发送者。框架本身和不同的模型提供商都定义了各自的角色枚举,但核心概念是相通的。
1、核心角色 (OpenAI 风格)
在 LangChain4j 的核心抽象中,最常使用的Role枚举(通常与 OpenAI 模型对应)包含以下几种:
SYSTEM: 用于设定AI助手的背景、行为或人格。这条消息通常位于对话的最开始,用来指导模型后续的所有回复。USER: 代表最终用户或应用程序发出的消息,即用户提出的问题或指令。ASSISTANT: 代表AI模型生成回复的消息。在多轮对话中,之前的AI回复会以这个角色继续参与上下文。TOOL/FUNCTION: 用于表示工具调用或函数执行的结果。当AI决定调用一个工具(如查询数据库)时,工具的执行结果会以这个角色返回给模型。其中FUNCTION角色已被标记为@Deprecated(弃用),推荐使用TOOL。
2、特定模型提供商 (Provider-specific) 的角色
除了上述通用角色,LangChain4j 在为不同模型提供商(如 Anthropic, Mistral, WorkersAI)做适配时,也定义了各自的角色枚举。虽然名称可能略有不同,但语义是基本一致的:
| 模型提供商 | 对应角色枚举 (Enum) | 包含的角色 |
|---|---|---|
| Anthropic(Claude) | AnthropicRole | 与核心角色类似,包含SYSTEM,USER,ASSISTANT等。 |
| Mistral AI | MistralAiRole | 包含SYSTEM,USER,ASSISTANT,TOOL。 |
| Workers AI | MessageRole | 包含system,ai(相当于 ASSISTANT),user。 |
3、使用注意事项
ChatMessage接口:在 LangChain4j 中,所有角色的消息都实现了ChatMessage接口,这为处理不同类型的消息提供了统一的类型安全方式。大小写问题:不同的 LLM 提供商对角色名的格式要求可能不同(例如,有的要求全部小写)。在使用时,需要注意框架的序列化逻辑是否会自动处理,否则可能会遇到类似的问题。
