使用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.UserMessageand no properties discovered to create BeanSerializer根本原因分析1. 首次对话 Redis 空数据问题新会话sessionId在 Redis 中无历史记录RedisChatMemoryStore.getMessages() 返回空列表某些 LLM API如智谱 AI要求消息列表不能为空2. Jackson 无法直接序列化 ChatMessageLangChain4j 的消息类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 ListChatMessage 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 { ListMessageWrapper wrappers objectMapper.readValue(json, objectMapper.getTypeFactory().constructCollectionType(List.class, MessageWrapper.class)); ListChatMessage 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, ListChatMessage messages) { if (messages null || messages.isEmpty()) { System.out.println([RedisChatMemoryStore] 会话 memoryId 尝试保存空消息列表跳过); return; } String key KEY_PREFIX memoryId.toString(); try { ListMessageWrapper 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对象而是将其转换为MapString, Objectif (msg instanceof AiMessage) {AiMessage aiMsg (AiMessage) msg;if (aiMsg.hasToolExecutionRequests()) {ListMapString, Object requestMaps aiMsg.toolExecutionRequests().stream().map(req - {MapString, 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 是一个 ListMapString, ObjectListMapString, Object requestMaps (ListMapString, Object) requests;ListToolExecutionRequest 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 ListChatMessage 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, ListChatMessage 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;// 在你的配置类中Beanpublic 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 AIMistralAiRole包含SYSTEM,USER,ASSISTANT,TOOL。Workers AIMessageRole包含system,ai(相当于 ASSISTANT),user。3、使用注意事项ChatMessage接口在 LangChain4j 中所有角色的消息都实现了ChatMessage接口这为处理不同类型的消息提供了统一的类型安全方式。大小写问题不同的 LLM 提供商对角色名的格式要求可能不同例如有的要求全部小写。在使用时需要注意框架的序列化逻辑是否会自动处理否则可能会遇到类似的问题。