👉一键直达《JavaAI应用开发实战》知识库
👉一键获取《JavaAI应用开发实战》代码仓库
👉一键获取访问密码
当前以下视频系列课程正持续更新中:
- 《做实验学 Elasticsearch》
- 《小编说 ES 教程》
- 《Java AI 应用开发实战》
- …… 更多内容陆续上线中!
💡 为什么值得订阅?
所有资料均由小编精心整理,保证代码可运行、可调试,省去你东拼西凑的时间成本,专注核心技能提升。
各位同学,大家好!上节课咱们给 AI 加上了“记忆”——比如你告诉它:“你叫小编”,它就真的记住了,下次聊天还能喊你“小编”。是不是觉得挺酷?
来,咱们想象一个场景:
你的 AI 客服同时接待两位用户——张三和李四。
张三说:“我叫张三,订单号是 123。”
紧接着,李四问:“我叫什么?我的订单呢?”
结果 AI 一脸认真地回答:“你叫张三,订单号 123。”
……是不是瞬间冷汗直流?!
这可不是小 bug,这是用户数据串台!轻则体验崩坏,重则隐私泄露、法律风险!所以,真正的生产级 AI 应用,必须做到:一人一套记忆,互不干扰!
那问题来了:怎么实现?自己手写 Map 套 Map?加锁?分布式缓存?别急——LangChain4j 已经帮你想好了,一行注解 + 几行配置,直接搞定!
今天这节课,咱们就手把手,从“翻车现场”到“稳如老狗”,彻底掌握 多用户记忆隔离 的实战方案!
【理论:为什么记忆会“串台”?核心思路是什么?】
首先,咱们得搞清楚:为什么上节课的记忆会串?
很简单——因为咱们用的是全局共享的记忆池!所有用户的对话都塞进同一个地方,AI 取的时候根本不分你是谁、他是谁,自然就乱套了。
就像你去银行办业务,结果柜员拿着隔壁老王的存折给你算账……这能行吗?
所以,解决思路非常直白:给每个用户发一个专属“小账本”!
- 张三说话 → 记在张三的本子上
- 李四提问 → 只看李四的本子
- 谁也别碰谁的,清清楚楚!
这个“小账本”的钥匙,就是我们今天要讲的 MemoryId(记忆 ID)。它可以是:
- 用户的唯一 ID(比如 user_id = 1001)
- 或者一次会话的 session_id(比如 UUID)
只要这个 ID 能唯一区分用户或会话,就行!
而 LangChain4j 给咱们提供了一个超级好用的机制:@MemoryId 注解 + ChatMemoryProvider,自动根据 ID 创建、查找、更新对应的“小账本”。
底层其实就是一个 Map> ——
- Key 是用户 ID
- Value 是这个用户的对话历史(不是字符串,而是带角色、内容、时间戳的
Message对象)
【实战:手把手写代码】
好,理论懂了,现在咱们动手写代码!目标:实现一个接口,不同用户问“我是谁”,AI 能正确回答各自的名字。
第一步:定义 AI 助手接口
咱们先定义一个接口,叫 AssistantUnique,里面放两个方法:
// 定义支持多用户隔离的 AI 助手接口
public interface AssistantUnique {
// 普通聊天方法
String chat(
@MemoryId int memoryId, // ← 关键!用这个 ID 区分用户
@UserMessage String userMessage // 用户说的话
);
// 流式响应(用于实时打字效果)
TokenStream stream(
@MemoryId int memoryId,
@UserMessage String userMessage
);
}
重点来了:
@MemoryId:标记哪个参数是“记忆 ID”@UserMessage:标记用户输入的内容
这两个注解,就是 LangChain4j 的“眼睛”,让它知道怎么拆解你的参数。
你可能会想:为啥用 int?能不能用 String?当然可以!比如传用户的 UUID 字符串更安全。这里用 int 只是为了演示方便。
第二步:注册 Bean,配置记忆提供者
接下来,在 Spring Boot 里把这个接口变成一个可用的 Bean。咱们写在一个配置类里,比如 AiConfig.java:
@Bean
public AssistantUnique assistantUnique(
ChatModel qwenChatModel,
StreamingChatModel qwenStreamingChatModel
) {
return AiServices.builder(AssistantUnique.class)
.chatModel(qwenChatModel) // 普通聊天模型
.streamingChatModel(qwenStreamingChatModel) // 流式模型
.chatMemoryProvider(memoryId ->
MessageWindowChatMemory.builder()
.maxMessages(10) // ← 最多记 10 条,防内存爆炸!
.id(memoryId) // ← 绑定当前用户的 ID
.build()
)
.build();
}
敲黑板!这里最关键的是 .chatMemoryProvider(...) 这一段:
- 它是一个函数:输入 memoryId,输出一个专属的 ChatMemory
- 每次调用
chat()时,LangChain4j 会自动:
-
- 拿到你传的
memoryId - 调这个函数,拿到对应用户的“小账本”
- 把历史消息 + 新消息拼成完整上下文,喂给大模型
- 把新对话再存回去
- 拿到你传的
而且用的是 MessageWindowChatMemory —— 它会自动只保留最近 N 条消息(这里是 10 条),避免对话太长把内存撑爆。这在生产环境里极其重要!
小贴士:你用的模型不限于通义千问!换成 OpenAI、文心一言、讯飞星火都行,LangChain4j 都封装好了,接口通用。
第三步:写个测试接口,验证效果
现在,咱们写个简单的 HTTP 接口来测试:
@RestController
@RequestMapping("/ai")
public class AiController {
@Autowired
private AssistantUnique assistantUnique; // 注入我们配好的助手
@GetMapping("/memory_chat")
public String memoryChat(
@RequestParam(defaultValue = "我是谁") String message,
@RequestParam Integer userId // ← 用 userId 当 memoryId
) {
return assistantUnique.chat(userId, message);
}
}
启动服务后,咱们用浏览器或 Postman 来测试三轮:
✅ 测试 1:用户 1 告诉 AI 自己叫“小编”
GET /ai/memoryId_chat?message=我叫小编&userId=1
→ AI 回:“好的,我知道了!”
(此时,userId=1 的“小账本”里记下了这条)
❌ 测试 2:用户 2 问“我叫什么?”
GET /ai/memoryId_chat?message=我叫什么&userId=2
→ AI 回:“抱歉,我不知道您的名字,因为我没有之前的聊天记录。”
(完美!没串台!userId=2 的账本是空的)
✅ 测试 3:用户 1 再问“我叫什么?”
GET /ai/memoryId_chat?message=我叫什么&userId=1
→ AI 回:“您刚才告诉我您叫小编!”
(精准命中!说明记忆隔离成功!)
【收尾:本节课的核心收获】
好,咱们来总结一下,今天你真正掌握了什么:
- 为什么必须做记忆隔离?
→ 防止用户数据串台,避免隐私泄露,这是生产系统的底线! - 核心思路是什么?
→ 给每个用户/会话分配唯一MemoryId,实现“一人一本小账本”。 - LangChain4j 怎么帮你?
→ 用@MemoryId注解 +chatMemoryProvider,自动管理隔离记忆。 - 生产环境要注意什么?
→ 用MessageWindowChatMemory限制消息数量(比如 maxMessages=10),防止内存溢出! - 底层原理是什么?
→ 本质是Map<MemoryId, List<Message>>,通过 ID 精准读写,绝不混淆。
最后留个思考题:
现在记忆是存在内存里的,如果服务重启,所有“小账本”就丢了。
那怎么把记忆存到 Redis 或数据库里,做到“断电不丢”?
别急,下节课咱们就讲 《聊天记忆持久化:让 AI 真正“不忘事”》!
今天的课就到这里,记得动手敲一遍代码,跑通测试!有问题随时留言,咱们下节课见!👋
1万+

被折叠的 条评论
为什么被折叠?



