当前位置: 首页 > news >正文

使用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 AIMistralAiRole包含SYSTEM,USER,ASSISTANT,TOOL
Workers AIMessageRole包含system,ai(相当于 ASSISTANT),user

3、使用注意事项

  • ChatMessage接口:在 LangChain4j 中,所有角色的消息都实现了ChatMessage接口,这为处理不同类型的消息提供了统一的类型安全方式。

  • 大小写问题:不同的 LLM 提供商对角色名的格式要求可能不同(例如,有的要求全部小写)。在使用时,需要注意框架的序列化逻辑是否会自动处理,否则可能会遇到类似的问题。

http://www.jsqmd.com/news/1069533/

相关文章:

  • 无人机电力营销落地瓶颈深度解析|四大核心壁垒、运维营销业务差异化、实景落地案例、全套YOLOv8电力AI视觉工程实现
  • 从零剖析十路充电桩嵌入式源码----软件开发环境搭建【3.1】
  • ivs-nat与nginx四层代理区别
  • eclipse设置豆沙绿背景色
  • 做 excel 表格用哪个智谱清言软件文档导出,AI 导出鸭专业适配表格导出,结构精准无需手动调整
  • 字符串的格式化问题 字符串的常规操作
  • deepspeed,vllm,llamafactory的使用
  • Kimi Work 来了:月之暗面发布桌面 Agent,知识工作者的“Vibe Working“时代开启
  • AI 时代,每个人都需要构建自己的操作系统
  • 【流形学习多模态语言变量分析基础】王阳明代数讲义之元认知透镜
  • 杰理ac791 wifi_camera 工程排坑手册
  • 云耀计算AI-Claura,在树莓派运行的AI
  • 【AI应用实战-WorkBuddy】工作流搭建:从需求到自动化全流程(十三)
  • 第二章 数字类型及其操作3
  • IntelliGit 项目个人工作总结
  • 模型配置篇(子篇)《DeepSeek API Key 获取实操指南:手把手教你拿到“大龙虾”的通行证》
  • 计算机毕业设计之村级技能培训管理系统
  • 微分几何中的等参超曲面与焦点流形稳定性分析
  • 从 Receiver Agreement 看懂 SAP PI/PO 出站路由的最后一公里
  • 秋招倒计时两个月,AI能力要从“会用工具”变成“能讲案例”
  • 为什么很多公司禁用 MyBatis 二级缓存?看完你就不敢乱开了
  • Python 3正则表达式完全指南:从入门到精通
  • 基于 Harmony 6.0 应用的游戏时长统计与防沉迷提醒应用首页实现
  • 金融事件序列建模:PRAGMA Transformer模型解析与应用
  • 2026 AI 开发者生存指南(5):AI Agent 框架对比——LangChain、LangGraph、CrewAI、Dify 怎么选?
  • Tiny Time Mixers (TTMs): Fast Pre-trained Models for Enhanced Zero/Few-Shot Forecasting of Multivari
  • 基于LLM的文本相关性评估:从RAG优化到可持续性分析的工程实践
  • Spring AI 接入 MCP:DeepSeek 连接 Filesystem Server 读取本地文件
  • 复杂流体系统实时控制:模型降阶与滚动时域优化实践
  • DINOv3+LoRA:基于视觉基础模型的图像篡改检测新范式