第6篇:对话记忆怎么实现?Java版多轮上下文对话完整方案 (Java+AI落地实战系列 | 复制可用 | 生产可落地)

23 阅读9分钟

第6篇:对话记忆怎么实现?Java版多轮上下文对话完整方案

(Java+AI落地实战系列 | 复制可用 | 生产可落地)


大家好,我是三石。

上一篇我们搞定了流式输出,让AI对话从“憋半天一次性吐答案”变成了逐字打字的丝滑体验。

但很多同学跑通之后马上发现一个致命问题: AI就像金鱼,只有7秒记忆——上一句刚让它写了单例模式,下一句说“用枚举优化一下”,它直接懵了,根本不知道你在说啥。

这不是大模型笨,是它天生“无状态”。 每一次接口调用,对它来说都是全新的对话,它根本不记得刚才和你聊过什么。

所谓的“对话记忆”,本质就是每次提问时,把历史聊天记录一起打包发给大模型。 今天我们就从最简单的本地缓存版,讲到生产可用的Redis分布式版,手把手带你实现完整的多轮上下文对话。

先上一张对比图,一眼看懂区别: 在这里插入图片描述

一、先搞懂核心原理:记忆到底存在哪?

很多人以为大模型会记住你的对话,其实完全不是。 大模型本身没有记忆能力,每一次调用都是独立的。

我们说的“对话记忆”,全程都是我们自己的后端服务在维护

  1. 用户发第一条消息,后端存下来
  2. 调用大模型,把「系统人设+第一条用户消息」发过去
  3. 拿到AI回复,也存下来
  4. 用户发第二条消息,后端把「历史所有消息+第二条消息」一起打包发给大模型
  5. 大模型基于完整上下文生成答案

说白了就是:我们自己当“书记员”,每次把聊天记录全量复述一遍给大模型听。

这也是为什么对话越长,消耗的token越多、响应越慢——因为每次都在传完整的聊天记录。 在这里插入图片描述

二、基础版:本地内存实现会话记忆

先从最简单的版本开始,不用额外中间件,新建一个会话管理类就行,适合Demo、小工具、单机部署场景。

1. 会话上下文管理类

ConcurrentHashMap存会话,key是会话ID,value是历史消息列表,同时做最大条数限制,防止token爆炸。

@Component
public class ChatContextHolder {

    // 会话存储:key=sessionId,value=历史消息列表
    private final Map<String, List<Message>> contextMap = new ConcurrentHashMap<>();

    // 单会话最大保留10条消息(约5轮对话),避免token超限
    private static final int MAX_HISTORY_COUNT = 10;

    /**
     * 获取指定会话的历史消息
     */
    public List<Message> getHistory(String sessionId) {
        return contextMap.computeIfAbsent(sessionId, k -> new ArrayList<>());
    }

    /**
     * 添加消息到会话历史
     */
    public void addMessage(String sessionId, Message message) {
        List<Message> history = getHistory(sessionId);
        history.add(message);

        // 超出最大条数,移除最早的消息(保留system人设)
        if (history.size() > MAX_HISTORY_COUNT) {
            // 第0条是system,从第1条开始删
            history.remove(1);
        }
    }

    /**
     * 清空指定会话
     */
    public void clearSession(String sessionId) {
        contextMap.remove(sessionId);
    }
}

划重点:一定要保留第一条system消息! 它是AI的人设和规则,删掉之后AI就会“失控”,回答风格乱飘。裁剪历史的时候,永远动第一条。

2. 改造对话服务,支持多轮

在之前的单轮对话基础上,加入会话ID和历史消息逻辑。

@Service
public class QwenChatService {

    @Value("${ai.qwen.api-key}")
    private String apiKey;
    @Value("${ai.qwen.model}")
    private String model;
    @Value("${ai.qwen.temperature}")
    private Float temperature;
    @Value("${ai.qwen.max-tokens}")
    private Integer maxTokens;

    @Autowired
    private ChatContextHolder contextHolder;

    /**
     * 带上下文的多轮对话
     * @param sessionId 会话ID,区分不同用户/对话
     * @param userMessage 用户当前提问
     * @return AI回答
     */
    public String chatWithContext(String sessionId, String userMessage) {
        // 1. 获取当前会话的历史消息
        List<Message> messages = contextHolder.getHistory(sessionId);

        // 2. 首次对话,初始化系统人设
        if (messages.isEmpty()) {
            messages.add(Message.builder()
                    .role(Role.SYSTEM)
                    .content("你是专业的Java开发助手,回答精准简洁,代码附带注释,优先给出最佳实践。")
                    .build());
        }

        // 3. 把用户当前提问加入历史
        Message userMsg = Message.builder()
                .role(Role.USER)
                .content(userMessage)
                .build();
        messages.add(userMsg);

        // 4. 构建请求,调用大模型
        DashScope dashScope = new DashScope(apiKey);
        GenerationRequest request = GenerationRequest.builder()
                .model(model)
                .input(MessageInput.builder().messages(messages).build())
                .temperature(temperature)
                .maxTokens(maxTokens)
                .resultFormat(ResultFormat.MESSAGE)
                .build();

        try {
            GenerationResult result = dashScope.call(request);
            String reply = result.getOutput().getChoices().get(0).getMessage().getContent();

            // 5. 把AI回复也加入历史,完成一轮对话
            Message assistantMsg = Message.builder()
                    .role(Role.ASSISTANT)
                    .content(reply)
                    .build();
            contextHolder.addMessage(sessionId, assistantMsg);

            return reply;
        } catch (Exception e) {
            e.printStackTrace();
            return "AI调用失败:" + e.getMessage();
        }
    }
}

3. 对外接口层

加一个sessionId参数,用来区分不同的对话。

@RestController
@RequestMapping("/api/chat")
public class ChatController {

    @Autowired
    private QwenChatService qwenChatService;

    /**
     * 多轮对话接口
     */
    @PostMapping("/sendWithContext")
    public Result<String> chatWithContext(@RequestBody ChatRequest request) {
        String reply = qwenChatService.chatWithContext(
                request.getSessionId(),
                request.getMessage()
        );
        return Result.success(reply);
    }

    /**
     * 清空会话接口
     */
    @PostMapping("/clear")
    public Result<String> clearSession(String sessionId) {
        contextHolder.clearSession(sessionId);
        return Result.success("会话已清空");
    }

    // 请求实体
    @Data
    static class ChatRequest {
        private String sessionId; // 会话ID,前端生成并维护
        private String message;
    }
}

4. 测试验证

用同一个sessionId连续发两条消息:

  • 第一条:用Java写一个单例模式
  • 第二条:用枚举方式改写一下

你会发现AI能理解上下文,基于上一轮的单例代码进行改写,多轮记忆就实现了。


三、进阶版:Redis实现分布式会话记忆

本地内存版虽然简单,但有两个致命问题:

  1. 服务重启,所有会话记忆全没了
  2. 集群部署时,请求落到不同实例上,会话就串了

生产环境集群部署,必须用Redis来存会话历史,所有实例共享数据。

1. 引入Redis依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.7.18</version>
</dependency>

2. Redis会话管理实现

用Redis的List结构存历史消息,设置过期时间,自动清理长时间不活跃的会话。

@Component
public class RedisChatContextHolder {

    @Autowired
    private StringRedisTemplate redisTemplate;

    // 会话key前缀
    private static final String SESSION_PREFIX = "ai:chat:session:";
    // 会话过期时间:30分钟无交互自动失效
    private static final long EXPIRE_MINUTES = 30;
    // 最大历史消息数
    private static final int MAX_HISTORY_COUNT = 10;

    /**
     * 获取历史消息
     */
    public List<Message> getHistory(String sessionId) {
        String key = SESSION_PREFIX + sessionId;
        List<String> jsonList = redisTemplate.opsForList().range(key, 0, -1);

        List<Message> messages = new ArrayList<>();
        if (CollectionUtils.isEmpty(jsonList)) {
            return messages;
        }
        // JSON反序列化为Message对象
        for (String json : jsonList) {
            messages.add(JSON.parseObject(json, Message.class));
        }
        return messages;
    }

    /**
     * 添加消息并刷新过期时间
     */
    public void addMessage(String sessionId, Message message) {
        String key = SESSION_PREFIX + sessionId;
        redisTemplate.opsForList().rightPush(key, JSON.toJSONString(message));

        // 超出最大长度,移除最早的消息(保留第0条system)
        Long size = redisTemplate.opsForList().size(key);
        if (size != null && size > MAX_HISTORY_COUNT) {
            redisTemplate.opsForList().remove(key, 1, 1);
        }

        // 刷新过期时间
        redisTemplate.expire(key, EXPIRE_MINUTES, TimeUnit.MINUTES);
    }

    /**
     * 清空会话
     */
    public void clearSession(String sessionId) {
        redisTemplate.delete(SESSION_PREFIX + sessionId);
    }
}

好处很明显:

  • 集群部署通用,所有实例共享会话
  • 服务重启不丢数据
  • 自带过期时间,自动清理无效会话,不会内存泄漏
  • 可以做会话持久化,用户下次进来还能继续聊 在这里插入图片描述

四、新手必踩的4个坑,提前帮你避了

坑1:只存用户消息,不存AI回复

这是新手最常犯的低级错误。 只把用户的提问存进历史,AI的回答不存,结果大模型永远只看得到用户的问题,看不到自己之前的回答,上下文自然对不上。

正确做法:用户消息和AI回复必须成对存入历史,一轮对话两条消息。

坑2:历史消息不裁剪,token费用直接爆

有人觉得“记忆越长越好”,直接存几十条历史。 结果就是每次调用都传几千个token,账单蹭蹭涨,还容易触发大模型的长度限制直接报错。

解决方案:普通对话场景保留8-12条消息(4-6轮)完全够用,超出就裁剪最早的对话。

坑3:sessionId不隔离,不同用户串消息

测试的时候图省事,所有人都用同一个sessionId,结果A和B的对话串到一起了,AI回答乱七八糟。

解决方案:前端给每个用户、每个对话窗口生成唯一的sessionId(比如UUID),严格隔离。

坑4:本地版部署到集群,会话丢失

本地内存版在单机上跑得好好的,一上生产集群就时灵时不灵。 本质就是请求负载均衡到不同实例上,这个实例存了会话,下个请求落到另一个实例上就取不到了。

解决方案:生产环境直接上Redis版,不要用本地内存。


五、生产环境还能怎么优化?

1. 按用户配额限制会话数

防止恶意用户创建大量会话占用内存,每个用户最多同时保留10个会话。

2. 摘要式长记忆

如果真的需要很长的对话历史,不要全量传。 可以定期让大模型把之前的对话总结成一段摘要,用摘要代替完整历史,大幅节省token。

3. 敏感内容校验

历史消息入库前,用户提问和AI回复都要过敏感词校验,避免违规内容沉淀。

4. 流式输出+对话记忆结合

本篇为了方便理解用了同步接口,实际项目里可以把第三篇的流式输出和本篇的会话记忆结合起来,体验直接拉满。


六、本篇小结

今天我们彻底搞懂了对话记忆的本质,实现了两个版本:

  • 本地内存版:简单快速,适合Demo和单机小工具
  • Redis分布式版:生产可用,支持集群部署

核心逻辑永远不变:后端维护历史消息,每次全量传给大模型。 剩下的就是存储介质、裁剪策略、过期策略的工程化优化。

到这里,我们已经搞定了「基础对接→流式输出→多轮记忆」,一个完整可用的AI对话系统已经成型了。


下篇预告

对话功能虽然完整了,但还有个老大难问题: AI张嘴就胡说八道,问业务问题十句有八句是编的,根本不敢用在企业场景。

这就是RAG知识库要解决的问题——让AI只基于企业内部文档回答,从根源杜绝幻觉,而且全程可以内网部署,数据不流出。

所以下一篇我们讲:

第7篇:从零搭建本地RAG知识库,内网可用,零API费用

内容会覆盖:

  • RAG核心原理,一句话讲明白
  • 本地开源大模型部署
  • 文档切片+向量化存储
  • 相似度检索+增强生成
  • 完整可复制代码

🎁 粉丝福利

本篇完整代码已整理进系列源码包,包含:

  • 本地内存版会话记忆完整实现
  • Redis分布式版会话记忆完整实现
  • 历史消息自动裁剪逻辑
  • 配套Controller接口

关注 Java-AI工程师,后台回复 系列源码 即可免费领取。 每更新一篇,我都会往资料包里新增对应源码,跟着系列就能从零搭出完整的企业级AI系统。

最新已更:mp.weixin.qq.com/s/S_eQR2d5x…