臭名昭著aTrust隔离计划1.臭名昭著aTrust隔离计划2.VPN原理与aTrust隔离网络实践3.docker-easyconnect到底做了什么4.TUN(tunnel-隧道-虚拟网卡)模式
基于Quartz搭建的个人博客1.Quartz个人博客使用教程2.使用 rsync 增量部署 Quartz 博客3.使用 GitHub Actions 自动部署 Quartz 博客5.域名绑定6.CDN加速Obsidian、双向链接与知识图谱
旅行
行程攻略青岛三日游行程攻略
前端
NginxNginx配置与反向代理入门
Nodenpm与npx的区别
中华财险-公司项目
对外接口文档
基础数据码表对外接口(仅支持rpc调用)业务码表对外接口应用主数据对外接口
基础运营查询版本更新日志详情消息中心对外接口站内信对外接口站内信模板配置手册
权限中心权限中心对外接口文档(最新)权限中心对外接口文档前端ACL-CORE FACADE依赖版本
审计中心审计中心对外接口文档audit-center-facade版本
审批中心工作流迁审批流现状工作流迁审批流API能力替换方案老审批流接口文档审批中心接口文档审批中心业务回调FAQ
账号中心内部系统对接单点登录内部系统对接认证中心三方网页应用登录授权账号中心对外接口文档前端账号中心RPC接口文档账户变更对外广播消息文档账户中心对外接口HRMS外部员工变更广播消息文档HRMS外部员工对外接口
组织员工岗位🔥机构映射SDK接口文档员工岗位变更通知说明组织机构管营一体概念与用法组织员工对外广播消息文档组织员工岗位对外接口文档组织员工岗位对外接口文档前端组织员工岗位数据模型组织员工主数据业务场景案例organization-facade版本
hrms(大型人力资源外包管理系统)
项目架构说明HRMS Maven模块与依赖说明
项目说明0.流程中心模块0.组织模块说明1.业务线模块2.计划模块3.项目模块4.协议模块5.供应商模块6.合约域模块7.外包人员模块*核心:外包人员生命周期
项目运维外包人员项目编制差异排查与修复2.HRMS相关问题排查4.修改externalId(externalId和accountId不一致)
需求-系分
1.内部转外包0723外包人员关联历史内部账号系分新增外包人员关联历史账号内容(紧急0723上线)需求
2.工作岗位0820工作岗位0820需求工作岗位0820需求-系分
3.用工模式调整0917内部人员转外包用工系统需求2内部人员转外包用工系统需求-系分
sso(账号中心-单点登录)
项目架构说明aboss-sso项目架构入门AOP统一日志打印链路Maven多模块项目高级知识OAuth2.0
项目说明1.SSO-OAuth2.0与IDaaS登录流程2.外部账号创建流水号并发问题分析
AI
使用说明
第三方插件&技能简介Archify使用与安装指南Ponytail使用指南
CodexCodex CLI与IDE区别及使用指南Codex Hook单独配置与提交通知Codex MCP安装与使用指南Codex第三方插件安装与使用指南
Agent开发1.LLM、Token、上下文窗口与模型参数2.大模型 API、请求参数与响应结构3.Spring AI ChatClient4.Prompt、System Prompt、Prompt 模板6.结构化输出、JSON Schema7.SSE 流式响应8.会话 ID、聊天记录、Redis9.超时、重试、限流、降级10.完成可运行聊天接口
GitGit常用命令与Obsidian推送排查
Java
面试题Java基础与集合面试题
AtomicInteger原子计数与并发安全ConcurrentHashMap并发安全与计数Java线程、线程池与Future
python
基础Python基础语法
HTTPXHRMS员工详情接口调用(Python HTTPX)

一、💡 一句话理解

第 8 天核心结论

大模型本身不会记住上一轮对话。后端要用一个稳定的会话 ID把同一段对话归类,把消息保存为聊天记录,并在下一次提问前取回最近上下文;本篇用 Redis Stack 保存这份短期记忆。

本篇严格对应 0-学习路线 第 8 天:会话 ID、聊天记录、Redis。它接在 3.Spring AI ChatClient7.SSE 流式响应 之后学习;今天先完成普通聊天接口的“记住上下文”能力,不提前学习 Tool Calling、RAG、限流或分布式锁。

完成后应能验证下面这段连续对话:

第 1 次:我叫小王。
第 2 次:我叫什么?
结果:模型能回答“小王”。

二、🧭 理论:它是什么

2.1 会话 ID:给一段对话的“档案编号”

会话 ID(conversationId)是标识一段对话的字符串。例如:user-42:550e8400-e29b-41d4-a716-446655440000

它不是用户 ID,也不应该是前端随便传来的固定值:一个用户可以有多段对话;不同用户绝不能共用同一个 ID。

名称负责回答的问题示例
用户 ID“是谁在使用系统?”user-42
会话 ID“这是他的哪一段对话?”550e...000
实际存储键“Redis 到哪里找消息?”chat-memory:user-42:550e...000

Spring AI 的官方定义是:所有 ChatMemory 操作都按会话 ID 进行读、追加和删除。多用户系统应由服务器根据当前登录用户和会话生成 ID,避免串话。

2.2 聊天记录和聊天记忆不是一回事

概念白话解释本篇怎么处理
聊天记录(Chat History)所有消息的完整留档,用于审计、翻页和展示本篇不做长期归档;后续可写入 PostgreSQL。
聊天记忆(Chat Memory)本次请求真正放进 Prompt、让模型“看见”的上下文Redis 中保留一个最近消息窗口。

两者不能混为一谈。完整记录会越来越长,全部传给模型既增加 Token 成本,也会超过上下文窗口;而记忆只保留当前回答需要的最近几轮。

2.3 Redis 在这里做什么

Redis 是内存数据存储。这里不用它做“通用缓存”,而是把每个会话最近的消息存成可快速读取的短期记忆。

客户端携带 conversationId 和本轮问题

ChatClient 的 Memory Advisor 按 ID 读取 Redis 中的历史消息

历史消息 + 本轮问题 一起交给模型

模型回答后,用户消息和助手消息写回 Redis

官方事实

Spring AI 的 RedisChatMemoryRepository 使用 Redis Stack 的 Redis Query Engine 与 RedisJSON,将消息存为 JSON 文档,可设置 TTL;它不是普通 Redis Server 的无额外模块方案。

2.4 为什么还要设置“最近 10 条”与 TTL

MessageWindowChatMemory 是一个滑动窗口:超过 maxMessages 的旧消息会被移出记忆,系统消息会保留。maxMessages 是上限,不一定恰好等于最终条数,因为 Spring AI 会按完整对话轮次裁剪,避免把一次问答截断。

本篇设置 10 条消息和 24h TTL:前者控制每次模型调用携带的上下文,后者让临时会话在 24 小时后自动过期。它们都是示例值,应根据业务对“记住多久”和 Token 成本的要求调整。

三、⚙️ 理论:它是怎么工作的

3.1 从请求到写回 Redis 的完整链路

3.2 Spring AI 中各组件的职责

组件职责不负责什么
RedisChatMemoryRepository在 Redis Stack 中读写消息不决定保留多少消息。
MessageWindowChatMemory按窗口策略决定哪些消息留在记忆里不直接调用模型。
MessageChatMemoryAdvisor调用前取历史、调用后存消息不生成会话 ID。
ChatClient组织 Prompt 并调用模型不会自动猜测会话 ID。

每次带 Memory Advisor 调用 ChatClient 时,必须通过 ChatMemory.CONVERSATION_ID 传入会话 ID;省略它会在运行时抛出 IllegalArgumentException

3.3 会话 ID 的安全边界

示例为了便于用 curl 验证,允许请求带入 conversationId。真实项目中必须改为:从 JWT 或登录态取得当前用户 ID,在服务端生成 用户 ID + 新 UUID,并且查询、继续和删除会话时都校验归属。

错误:所有用户都使用 "default"
结果:A 的历史消息可能被送给 B 的模型请求 —— 串话与数据泄露。
 
正确:服务端生成 "当前用户 ID:随机会话 UUID"
结果:一人多会话,且用户之间隔离。

四、🚀 实践:从准备到验证

4.1 前置准备

4.1.1 沿用已有模型项目

先完成 3.Spring AI ChatClient 的 Spring Boot 项目与模型 API Key 配置。本篇不重复配置模型。以下示例以 Spring AI 官方当前稳定版 2.0.1 文档为准;如果现有项目使用其他 Spring AI 大版本,应先以该版本的官方文档核对 starter 名称和配置项。

4.1.2 启动 Redis Stack

RedisChatMemoryRepository 需要 Redis Stack 7.0+,不是只启动基础 Redis 镜像。开发机安装 Docker 后执行:

docker run -d --name ai-redis-stack -p 6379:6379 redis/redis-stack-server:latest

检查容器正在运行:

docker ps

预期能看到 ai-redis-stack,并且端口映射包含 6379。停止和重新启动它可分别使用 docker stop ai-redis-stackdocker start ai-redis-stack

4.1.3 在 pom.xml 增加 Redis 聊天记忆依赖

保留已有的模型 starter,另增加这一项:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-chat-memory-repository-redis</artifactId>
</dependency>

它会带入官方实现所需的 Jedis 客户端。修改后在 IntelliJ IDEA 重新加载 Maven 依赖。

4.1.4 配置 Redis 连接和记忆窗口

文件位置:src/main/resources/application.properties。不需要把密码写死;若 Redis 设置了密码,将它写为环境变量引用。

# Redis Stack 连接
spring.ai.chat.memory.repository.redis.host=localhost
spring.ai.chat.memory.repository.redis.port=6379
# spring.ai.chat.memory.repository.redis.password=${REDIS_PASSWORD}
 
# 会话记忆的 Redis 存储策略
spring.ai.chat.memory.repository.redis.key-prefix=chat-memory:
spring.ai.chat.memory.repository.redis.time-to-live=24h
spring.ai.chat.memory.repository.redis.initialize-schema=true

initialize-schema=true 会在启动时创建 Redis 搜索索引;生产环境应由部署流程统一管理这类结构变更,并视情况设为 false

4.1.5 注册 ChatMemory 和 ChatClient

这一步必须在调用聊天接口前完成:它把 Redis 记忆窗口注册给 ChatClient。文件位置:src/main/java/com/afyke/ai/config/ChatMemoryConfig.java。如果你的项目基础包名不是 com.afyke.ai,把 package 改成自己的基础包名。

package com.afyke.ai.config;
 
import java.time.Duration;
 
import redis.clients.jedis.RedisClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.memory.repository.redis.RedisChatMemoryRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class ChatMemoryConfig {
 
    @Value("${spring.ai.chat.memory.repository.redis.host:localhost}")
    private String redisHost;
 
    @Value("${spring.ai.chat.memory.repository.redis.port:6379}")
    private int redisPort;
 
    @Value("${spring.ai.chat.memory.repository.redis.key-prefix:chat-memory:}")
    private String redisKeyPrefix;
 
    @Value("${spring.ai.chat.memory.repository.redis.time-to-live:24h}")
    private Duration redisTimeToLive;
 
    @Bean
    RedisClient redisClient() {
        return RedisClient.builder().hostAndPort(redisHost, redisPort).build();
    }
 
    @Bean
    RedisChatMemoryRepository redisChatMemoryRepository(RedisClient redisClient) {
        return RedisChatMemoryRepository.builder()
                .jedisClient(redisClient)
                .keyPrefix(redisKeyPrefix)
                .timeToLive(redisTimeToLive)
                .build();
    }
 
    @Bean
    ChatMemory chatMemory(RedisChatMemoryRepository repository) {
        return MessageWindowChatMemory.builder()
                .chatMemoryRepository(repository)
                .maxMessages(10)
                .build();
    }
 
    @Bean
    ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) {
        return builder
                .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
                .build();
    }
}

这段配置先用 @Value 读取 application.properties 中的 Redis 地址、端口、键前缀和 TTL,再显式创建 RedisClientRedisChatMemoryRepository;最后把仓库交给“最多保留 10 条消息”的 ChatMemory,并创建已注册 MessageChatMemoryAdvisorChatClient

为什么不能只注入 RedisChatMemoryRepository

自定义了 ChatMemory Bean 时,Redis 自动配置可能因“已有 ChatMemory”条件而不再创建 RedisChatMemoryRepository,于是出现“找不到 Bean”。此时像上例一样显式注册仓库即可,不能只依赖自动配置。

4.2 可以拿来干什么

4.2.1 让模型记住刚刚说过的话

用途:用户分两次发送“我叫小王”“我叫什么”,第二次请求通过同一个会话 ID 取到第一次的记忆。

前置:ChatClient 已注册 MessageChatMemoryAdvisor,本次调用传入了 ChatMemory.CONVERSATION_ID

String answer = chatClient.prompt()
        .user("我叫什么?")
        .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
        .call()
        .content();

输入是本轮问题与 conversationId;Advisor 自动补上历史消息;输出仍是本轮模型回答。不要手动把历史字符串拼进每一个 Prompt,这会让角色、顺序和窗口裁剪难以统一维护。

4.2.2 给短期对话自动过期

用途:客服临时咨询、未登录体验等不需要永久保存的对话,可以在一段时间后由 Redis 自动清理。

前置:已配置 spring.ai.chat.memory.repository.redis.time-to-live=24h。24 小时内有存储的消息可用于记忆;过期后该会话不再有该部分上下文。

spring.ai.chat.memory.repository.redis.time-to-live=24h

TTL 不是数据合规策略的替代品。涉及用户隐私或审计时,还要明确完整聊天记录的存储地点、访问权限、删除流程和保留期限。

4.3 完整实践:带 Redis 记忆的聊天接口

目标:从注册 Redis 记忆到新增 POST /api/chat 接口,完整跑通连续对话。首次不传 conversationId 时,后端生成一个;之后客户端带回这个 ID,就能继续同一段对话。

4.3.1 配置 ChatMemory 和 ChatClient

前面的 4.1.5 已经说明了为什么这一步必须先做;为保证本节可以从头独立完成,这里再次给出完整配置。文件位置:src/main/java/com/afyke/ai/config/ChatMemoryConfig.java。如果你的项目基础包名不是 com.afyke.ai,把 package 改成自己的基础包名。

package com.afyke.ai.config;
 
import java.time.Duration;
 
import redis.clients.jedis.RedisClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.memory.repository.redis.RedisChatMemoryRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class ChatMemoryConfig {
 
    @Value("${spring.ai.chat.memory.repository.redis.host:localhost}")
    private String redisHost;
 
    @Value("${spring.ai.chat.memory.repository.redis.port:6379}")
    private int redisPort;
 
    @Value("${spring.ai.chat.memory.repository.redis.key-prefix:chat-memory:}")
    private String redisKeyPrefix;
 
    @Value("${spring.ai.chat.memory.repository.redis.time-to-live:24h}")
    private Duration redisTimeToLive;
 
    @Bean
    RedisClient redisClient() {
        return RedisClient.builder().hostAndPort(redisHost, redisPort).build();
    }
 
    @Bean
    RedisChatMemoryRepository redisChatMemoryRepository(RedisClient redisClient) {
        return RedisChatMemoryRepository.builder()
                .jedisClient(redisClient)
                .keyPrefix(redisKeyPrefix)
                .timeToLive(redisTimeToLive)
                .build();
    }
 
    @Bean
    ChatMemory chatMemory(RedisChatMemoryRepository repository) {
        return MessageWindowChatMemory.builder()
                .chatMemoryRepository(repository)
                .maxMessages(10)
                .build();
    }
 
    @Bean
    ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) {
        return builder
                .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
                .build();
    }
}

这段代码把 Redis 记忆窗口注册给 ChatClient:调用前自动按会话 ID 取历史消息,调用后自动写回用户消息和模型回答。

4.3.2 编写请求、响应与 Controller

文件位置:src/main/java/com/afyke/ai/controller/ChatController.java

package com.afyke.ai.controller;
 
// 校验 @RequestBody 中的字段规则。
import jakarta.validation.Valid;
// 声明字符串不能为空、不能只包含空格。
import jakarta.validation.constraints.NotBlank;
// 调用大模型的高级客户端。
import org.springframework.ai.chat.client.ChatClient;
// 读取 Spring AI 规定的会话 ID 参数名。
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
 
import java.util.UUID;
 
// 声明这是 REST Controller,方法返回值会自动转成 JSON。
@RestController
// 为本类所有接口统一加上 /api/chat 路径前缀。
@RequestMapping("/api/chat")
public class ChatController {
 
    // 复用已注册聊天记忆 Advisor 的 ChatClient。
    private final ChatClient chatClient;
 
    // Spring 自动注入配置类创建的 ChatClient,无须手动 new。
    public ChatController(ChatClient chatClient) {
        this.chatClient = chatClient;
    }
 
    // 接收 POST /api/chat 请求。
    @PostMapping
    // 正常处理时返回 HTTP 200。
    @ResponseStatus(HttpStatus.OK)
    // @Valid 启用字段校验;@RequestBody 将请求 JSON 转为 ChatRequest。
    public ChatResponse chat(@Valid @RequestBody ChatRequest request) {
        // 先取得客户端带回的 ID;新旧两个变量分开,避免后续 Lambda 捕获一个被重新赋值的变量。
        String requestedConversationId = request.conversationId();
 
        // Lambda 中使用的局部变量必须是 final 或“只赋值一次”。
        final String conversationId;
        if (requestedConversationId == null || requestedConversationId.isBlank()) {
            // 新对话:生成随机 ID。实际项目应把 demo-user 替换为登录态中的用户 ID。
            conversationId = "demo-user:" + UUID.randomUUID();
        } else {
            // 继续已有对话:使用客户端带回的 ID。
            conversationId = requestedConversationId;
        }
 
        // 发起本轮模型调用;记忆 Advisor 会在调用前后自动处理 Redis 中的聊天记录。
        String answer = chatClient.prompt()
                // 放入用户本轮问题。
                .user(request.message())
                // 指明本轮属于哪个会话;缺少它,Advisor 无法读取或保存对应记忆。
                .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
                // 执行同步模型调用。
                .call()
                // 从模型响应中取出纯文本回答。
                .content();
 
        // 把会话 ID 和回答一起返回;客户端必须保存 ID,下一次继续携带它。
        return new ChatResponse(conversationId, answer);
    }
 
    // 请求 JSON:conversationId 可省略(表示新建对话),message 必须非空。
    public record ChatRequest(String conversationId, @NotBlank String message) {
    }
 
    // 响应 JSON:返回本次会话 ID 和模型回答。
    public record ChatResponse(String conversationId, String answer) {
    }
}

输入 JSON 包含 message 和可选的 conversationId。首次请求生成 ID;每次调用都把 ID 交给 Advisor;返回 JSON 让客户端保存 ID。@NotBlank 防止空问题进入模型调用。若项目尚未有校验依赖,在 pom.xml 增加 spring-boot-starter-validation

4.3.3 启动并验证

先确认 Redis Stack 已运行,再从 IDEA 启动 Spring Boot 应用。打开新终端,第一次请求不要带会话 ID:

curl -X POST 'http://localhost:8080/api/chat' \
  -H 'Content-Type: application/json' \
  -d '{"message":"我叫小王,请记住。"}'

预期响应结构如下;其中 conversationId 每次新建会话都不同:

{
  "conversationId": "demo-user:550e8400-e29b-41d4-a716-446655440000",
  "answer": "好的,我记住了,你叫小王。"
}

复制上一步的真实 conversationId,发起第二次请求:

curl -X POST 'http://localhost:8080/api/chat' \
  -H 'Content-Type: application/json' \
  -d '{"conversationId":"替换为上一步真实的 conversationId","message":"我叫什么?"}'

预期:answer 能识别“小王”。若换成一个全新的 conversationId,模型没有前一轮消息可用,不能期待它知道名字。

4.3.4 常见报错排查

现象常见原因处理方式
启动时连不上 RedisRedis Stack 未运行、端口不是 6379执行 docker ps,核对容器和 application.properties
RedisChatMemoryRepository 找不到 Bean未添加 starter、Maven 未刷新,或自定义 ChatMemory 使自动配置退让核对 4.1.3 的依赖;按 4.1.5 显式创建 RedisClientRedisChatMemoryRepository
提示缺少 conversationId调用中漏了 .advisors(...) 参数传入 ChatMemory.CONVERSATION_ID,不要只在请求对象中保存它。
第二次回答不记得第一次两次请求的 ID 不同、Redis 被清空或 TTL 已过复制同一个真实 ID,检查 Redis 容器与 TTL。
模型没有正常回答模型 API Key、模型地址或额度问题回到 2.大模型 API、请求参数与响应结构 检查既有模型配置。

五、📌 总结

  • 会话 ID 是同一段对话的隔离键;生产环境由服务端根据登录用户与会话生成并校验归属。
  • 聊天记录是完整留档,聊天记忆是给模型的有限上下文;不要把两者混为一谈。
  • Redis Stack + RedisChatMemoryRepository 负责消息存储,MessageWindowChatMemory 负责最近消息窗口。
  • MessageChatMemoryAdvisor 在调用前取历史、调用后保存消息;每次必须传 ChatMemory.CONVERSATION_ID
  • 今天的验收标准是:两次请求使用同一个 ID 时,模型能记住第一轮信息。

一句话复述

用“用户隔离的会话 ID”从 Redis 取回最近聊天记录,再随本轮问题交给模型,模型才会表现得像在持续对话。

六、🔗 官方资料