LangChain4j 接入大模型时 @MemoryId 与 chatMemoryProvider 配置问题排查
一、问题背景
最近在重构一个 AI 个人记账助手项目时,我希望保留原项目中的 Agent 思路:
用户输入自然语言
↓
AgentController 接收请求
↓
AgentService 调度
↓
AgentPlanService 调用大模型解析用户意图
↓
AIService 返回 JSON
↓
ObjectMapper 转成 AgentPlan
↓
后续再进行意图判断和业务分发
其中,AIService 负责调用大模型,AgentPlanService 负责把大模型返回的 JSON 字符串转换成后端可用的 AgentPlan 对象。
最开始的核心代码如下:
String json = this.aiService.plan(sessionId, message);
System.out.println("AI Agent 计划结果:" + json);
json = cleanJson(json);
return this.objectMapper.readValue(json, AgentPlan.class);
但是测试时发现,代码执行到这一行:
String json = this.aiService.plan(sessionId, message);
就直接进入异常流程,后面的打印语句根本没有执行。
这说明问题并不是出在:
cleanJson()
ObjectMapper.readValue()
AgentPlan 字段映射
而是出在:
AIService.plan() 调用阶段
也就是 LangChain4j 代理对象调用大模型之前,就已经发生了异常。
二、接口设计
当时的 AIService 接口大致如下:
package com.test.service;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
public interface AIService {
@SystemMessage(fromResource = "agent-plan-prompt.txt")
String plan(@MemoryId String sessionId, @UserMessage String message);
String chat(@UserMessage String message);
}
这里使用了:
@MemoryId String sessionId
它的作用是让 LangChain4j 根据不同的 sessionId 区分不同会话的上下文记忆。
例如:
session_001 -> 一份聊天记忆
session_002 -> 另一份聊天记忆
session_003 -> 另一份聊天记忆
也就是说,@MemoryId 并不是普通参数注解,而是告诉 LangChain4j:
请根据这个参数的值,去获取对应的 ChatMemory。
三、错误配置写法
最开始我的配置类是这样写的:
package com.test.config;
import com.test.service.AIService;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.service.AiServices;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AgentServiceFactory {
@Bean
public AIService aiService(ChatModel chatModel) {
ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);
return AiServices.builder(AIService.class)
.chatModel(chatModel)
.chatMemory(chatMemory)
.build();
}
}
看起来这段代码没有问题:
ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);
意思是创建一个最多保留 10 条消息的会话记忆窗口。
但是问题出在:
.chatMemory(chatMemory)
这个配置表示:
整个 AIService 共用同一份固定 ChatMemory。
而我在 AIService 方法里又使用了:
@MemoryId String sessionId
这表示:
我希望不同 sessionId 使用不同 ChatMemory。
这两个思路是不一致的。
四、报错现象
取消 try-catch 后,接口返回:
{
"timestamp": "2026-05-29T02:44:54.978+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/agent/chat"
}
控制台日志中关键报错位置是:
java.lang.NullPointerException
at dev.langchain4j.service.memory.ChatMemoryService.getOrCreateChatMemory
at dev.langchain4j.service.DefaultAiServices$1.invoke
at jdk.proxy2.$Proxy80.plan
at com.test.service.impl.AgentPlanServiceImpl.parsePlan
这说明:
LangChain4j 在执行 AIService.plan() 时,
尝试根据 @MemoryId 获取或创建 ChatMemory,
但是由于配置不匹配,最终出现了空指针异常。
所以问题不是:
AI 额度不足
API Key 错误
提示词文件找不到
ObjectMapper 解析失败
而是:
@MemoryId 和 ChatMemory 配置方式不匹配。
五、chatMemory 和 chatMemoryProvider 的区别
1. chatMemory
.chatMemory(chatMemory)
表示给整个 AIService 配置一份固定记忆。
可以理解为:
所有用户、所有 sessionId 共用同一个聊天记录本。
这种方式适合简单单会话场景,例如:
String chat(@UserMessage String message);
也就是没有使用 @MemoryId 的情况。
2. chatMemoryProvider
.chatMemoryProvider(memoryId -> ...)
表示根据不同的 memoryId 动态提供不同的记忆对象。
可以理解为:
每个 sessionId 都有自己的聊天记录本。
例如:
session_001 -> ChatMemory A
session_002 -> ChatMemory B
session_003 -> ChatMemory C
这种方式适合多用户、多会话场景。
只要接口里使用了:
@MemoryId String sessionId
就应该优先使用:
.chatMemoryProvider(...)
而不是:
.chatMemory(...)
六、正确配置方式
修改后的配置类如下:
package com.test.config;
import com.test.service.AIService;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.service.AiServices;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Configuration
public class AgentServiceFactory {
private final Map<Object, ChatMemory> memoryStore = new ConcurrentHashMap<>();
@Bean
public AIService aiService(ChatModel chatModel) {
return AiServices.builder(AIService.class)
.chatModel(chatModel)
.chatMemoryProvider(memoryId ->
memoryStore.computeIfAbsent(
memoryId,
id -> MessageWindowChatMemory.withMaxMessages(10)
)
)
.build();
}
}
这段代码的核心是:
private final Map<Object, ChatMemory> memoryStore = new ConcurrentHashMap<>();
它用于保存不同会话的记忆。
例如:
session_test_backend_001 -> ChatMemory
session_web_001 -> ChatMemory
session_mobile_001 -> ChatMemory
然后通过:
memoryStore.computeIfAbsent(
memoryId,
id -> MessageWindowChatMemory.withMaxMessages(10)
)
实现:
如果当前 sessionId 已经有 ChatMemory,就直接复用;
如果没有,就创建一份新的 ChatMemory。
这样就能保证:
同一个 sessionId 能共享上下文;
不同 sessionId 之间互不干扰。
七、AIService 推荐写法
为了让计划解析和普通聊天都支持会话记忆,可以把 AIService 统一写成:
package com.test.service;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
public interface AIService {
@SystemMessage(fromResource = "agent-plan-prompt.txt")
String plan(@MemoryId String sessionId, @UserMessage String message);
String chat(@MemoryId String sessionId, @UserMessage String message);
}
其中:
@SystemMessage(fromResource = "agent-plan-prompt.txt")
表示读取 resources 目录下的提示词文件。
文件路径应该是:
src/main/resources/agent-plan-prompt.txt
plan() 方法负责让大模型返回结构化 JSON,例如:
{
"intent": "RECORD_EXPENSE",
"amount": 18,
"category": "饮品",
"description": "奶茶",
"valid": true,
"reason": ""
}
八、AgentPlanService 中的调用逻辑
修复记忆配置后,AgentPlanServiceImpl 可以继续保持如下逻辑:
package com.test.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.test.dto.AgentPlan;
import com.test.service.AIService;
import com.test.service.AgentPlanService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AgentPlanServiceImpl implements AgentPlanService {
@Autowired
private AIService aiService;
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public AgentPlan parsePlan(String sessionId, String message) {
try {
System.out.println("准备调用 AI,sessionId=" + sessionId + ",message=" + message);
String json = this.aiService.plan(sessionId, message);
System.out.println("AI Agent 计划结果:" + json);
json = cleanJson(json);
return this.objectMapper.readValue(json, AgentPlan.class);
} catch (Exception e) {
e.printStackTrace();
AgentPlan plan = new AgentPlan();
plan.setIntent(null);
plan.setValid(false);
plan.setReason("AI解析失败:" + e.getClass().getSimpleName() + ":" + e.getMessage());
return plan;
}
}
private String cleanJson(String json) {
if (json == null) {
return "{}";
}
json = json.trim();
json = json.replace("```json", "");
json = json.replace("```", "");
json = json.trim();
int start = json.indexOf("{");
int end = json.lastIndexOf("}");
if (start >= 0 && end >= 0 && end > start) {
return json.substring(start, end + 1);
}
return json;
}
}
这里要注意:
String json = this.aiService.plan(sessionId, message);
这一步如果成功,说明 LangChain4j 已经成功调用大模型。
后续才会进入:
cleanJson(json);
objectMapper.readValue(json, AgentPlan.class);
所以排查问题时要先判断异常发生在哪一行。
九、排查思路总结
本次问题的排查顺序如下:
1. 接口返回 500
2. 取消 try-catch,查看控制台完整异常
3. 发现异常发生在 aiService.plan()
4. 说明还没有拿到 AI 返回值
5. 排除 cleanJson 和 ObjectMapper 问题
6. 查看堆栈,发现 ChatMemoryService.getOrCreateChatMemory 报空指针
7. 判断是 @MemoryId 和 ChatMemory 配置方式不匹配
8. 将 .chatMemory(chatMemory) 改为 .chatMemoryProvider(...)
9. 问题解决
核心判断点是:
如果 System.out.println("AI Agent 计划结果:" + json) 没有执行,
说明问题发生在 AIService.plan() 调用阶段。
如果这句打印了,但 ObjectMapper 报错,
才说明问题发生在 JSON 格式或字段映射阶段。
十、最终结论
在 LangChain4j 中:
不使用 @MemoryId,可以使用 .chatMemory(chatMemory)
使用 @MemoryId,应该使用 .chatMemoryProvider(...)
两者的区别是:
.chatMemory(chatMemory)
表示整个 AIService 共用一份固定记忆。
.chatMemoryProvider(...)
表示根据 memoryId 动态获取或创建不同会话记忆。
本项目中前端请求包含:
{
"userId": 1,
"sessionId": "session_test_backend_001",
"message": "今天奶茶18"
}
因此后端 AIService 使用了:
@MemoryId String sessionId
这时配置类就应该使用:
.chatMemoryProvider(...)
否则 LangChain4j 在根据 sessionId 获取会话记忆时,可能会出现空指针异常。
本质上,这次问题不是大模型本身的问题,而是:
AIService 方法注解设计和 AiServices 配置方式没有匹配。
修复后,Agent 调用链路可以正常进入:
AIService.plan()
↓
大模型返回 JSON
↓
cleanJson()
↓
ObjectMapper 转 AgentPlan
↓
AgentService 后续调度
更多推荐

所有评论(0)