用 JDBC 持久化 Spring AI 会话记忆
教程基于 Spring AI 2.1.0-RC1,将会话事件存入 JDBC 仓库,再按调用构建上下文,并通过按轮次压缩控制提示长度。作者明确说明示例未连接真实数据库验证,且所用版本仍是候选版。
AI 深度解读
客服助手能接住上一句话,应用重启后却认不出同一个客户,问题通常出在会话状态没有持久化。Spring AI 2.1.0-RC1 的 session memory,尝试把「保存完整对话」和「给模型准备上下文」拆成两件事:历史进入数据库,每次调用只读取合适的部分。
这套设计值得关注,但原文是一篇基于文档的教程,没有生产部署记录,示例也没有连接真实数据库验证。读它最有价值的部分,是理解会话持久化、上下文压缩和用户权限分别该由谁负责。
保存完整历史,不等于每次都塞进提示词
传统 chat memory 通常是在 conversation ID 下存一串消息。短对话够用,一旦加入工具调用、长历史或工作流状态,单纯的消息列表就不太容易表达完整过程。
Spring AI 的 session model 使用追加式事件日志(append-only log)。Session 表示会话身份和元数据,event 记录发生过的事情;模型调用时,再从日志构造当前需要的视图(view)。
数据库保存的是对话历史,提示词只是某次调用使用的历史视图。
这样,压缩上下文不必删除原始记录。旧事件仍可用于审计、构造其他视图或后续恢复,但不会继续挤占当前提示词。
另一个关键是按轮次压缩(turn-aware compaction)。工具调用与返回结果应当完整保留,问题与回答也不宜随意拆开。否则模型可能只看到一个工具调用,却不知道执行结果。
不过,保留历史不代表所有历史都适合再次发送给模型。敏感字段、工具结果和不同事件类型如何进入上下文,仍要由应用决定。
JDBC 接入不复杂,20 轮和 10 轮却不是通用答案
原文要求通过 Spring AI BOM 使用 2.1.0-RC1 依赖集,并加入 org.springframework.ai:spring-ai-starter-model-session-jdbc。应用还需要 Spring Boot JDBC starter、数据库驱动和可用的 DataSource。
文档列出的 JdbcSessionRepository 支持 PostgreSQL、MySQL、MariaDB 和 H2。具备 DataSource 后,Boot 自动配置会创建 repository 和 SessionService。
本地 PostgreSQL 或 MySQL 开发环境可以使用 spring.ai.session.repository.jdbc.initialize-schema=always。生产环境是否自动初始化表结构,应服从团队的数据库迁移流程。
下面这段配置把会话管理接入 ChatClient,同时设置压缩触发条件和保留窗口:
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.session.SessionService;
import org.springframework.ai.session.advisor.SessionAdvisors;
import org.springframework.ai.session.compaction.TurnCountTrigger;
import org.springframework.ai.session.compaction.WindowCompactionStrategy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class AssistantConfiguration {
@Bean
ChatClient chatClient(ChatModel chatModel,
SessionService sessionService) {
return ChatClient.builder(chatModel)
.defaultAdvisors(SessionAdvisors.builder(sessionService)
.compaction(
new TurnCountTrigger(20),
WindowCompactionStrategy.builder()
.lastTurns(10)
.build())
.build())
.build();
}
}
重点看 new TurnCountTrigger(20) 和 .lastTurns(10):前者设置按轮次触发压缩,后者让窗口保留最近 10 轮。原文提醒,imports 和 builder 签名还要与 BOM 选定版本核对。
20 轮不是 token 上限。两轮包含大段工具输出的对话,也可能比十几轮短问答更长。轮数策略容易理解,但实际应用可能需要按 token 触发,或根据上下文预算定制策略。
压缩会在事件日志里记录边界,缩短的是活动视图。它解决上下文增长问题,却不保证窗口外的关键事实还能被模型想起来。
session ID 能串起对话,不能证明对话属于谁
连续对话的每次请求都要传入稳定的 session ID。Spring AI 不会在缺少标识时退回一个共享默认会话,以免不同用户的历史混在一起。
调用时通过 advisor 指定本轮会话:
String answer = chatClient.prompt()
.user("Can you summarize the issue I described earlier?")
.advisors(SessionAdvisors.session(sessionId))
.call()
.content();
需要重点检查 .advisors(SessionAdvisors.session(sessionId)) 之前的业务流程:服务端必须先确认当前用户有权访问该会话,再调用模型。
UUID 难猜,并不构成授权策略。浏览器可以保存一个不暴露内部含义的会话句柄,服务端收到后,先校验归属,再解析到 session。
Session 没有专门的 owner 字段。文档提供元数据,以及带索引的 SessionAttributes.PRINCIPAL_NAME 属性,方便按身份查找会话。但身份应来自服务端认证结果,不能直接相信请求体里的用户名或邮箱。
保留期限也有差别。通过服务创建的 session 使用请求指定的 TTL,或文档所述的默认 60 天;直接构造 Session 时,如果 builder 没设置过期时间,就不会自动过期。
持久化保证记录能存下来,权限检查决定记录会不会交给错误的人。
TTL 同样不能代替完整的数据保留政策。数据库备份、日志、导出记录,以及账号删除后的处理,都需要单独安排。
迁移旧会话,需要处理历史数据和后端差异
原文称,Spring AI 2.1.0 已弃用 ChatMemory、MessageWindowChatMemory、ChatMemoryRepository、MessageChatMemoryAdvisor 及若干 repository 实现,计划在 3.0.0 移除,目前仍可使用。
迁移并非改几个类名。新旧存储不共享表,SPRING_AI_CHAT_MEMORY 中的历史消息不会自动被 session 表读取。团队必须决定:迁移旧对话、保留旧数据只读入口,还是按明确政策让旧会话到期。
后端也未必能直接替换。对于 Redis、MongoDB、Neo4j、Cassandra 或其他 SQL 方言,原文要求核实 session repository 的实现状态,不能把「计划支持」当作已经可用。
测试可以先用 InMemorySessionRepository,通过 DefaultSessionService.builder()、.sessionRepository(InMemorySessionRepository.builder().build()) 和 .build() 构造服务。它适合验证会话创建与 advisor 接线,但进程停止后数据会丢失,多实例之间也不共享状态。
因此,还要保留使用部署数据库同一类型、同一 schema 策略的集成测试,检查重启后恢复、跨会话隔离、完整轮次压缩,以及过期和删除行为。
我的判断
我认同这套职责拆分。相比直接截断消息列表,事件日志加活动视图更适合需要追查历史、包含工具调用的助手;相比每次把全量历史送入模型,它也提供了控制上下文长度的位置。
但原文没有性能、成本或回答质量的对照数据,也没有真实数据库验证。能确认的是设计意图和文档示例,不能据此断言它已经适合所有生产团队。
它也不能替代检索。会话窗口处理有序对话中的上下文选择,从大型知识库找特定事实则是另一类问题。原文指出,VectorStoreChatMemoryAdvisor 仍然保留,因为 session memory 尚无对应的语义召回能力。
版本判断要保守。原文讨论的 2.1.0-RC1 是候选版本,并提到当时同时可用的稳定版本 2.0.2。这些是原文提供的版本信息,不能当成当前最新状态。采用前仍需核对发布状态、锁定版本并阅读升级说明。
能拿来做什么
- 如果你在做前端聊天界面与 Spring AI 后端,把会话句柄作为连续对话的必要字段,并在后端调用模型前校验归属;补上用户修改句柄后无法读取他人历史的测试。
- 如果助手会调用工具,先检查现有截断逻辑是否可能拆散调用与结果,再根据实际输入长度选择轮次或 token 策略,不要直接把 20 轮、保留 10 轮当成默认标准。
- 如果准备替换旧
ChatMemory,先盘点数据库后端、SPRING_AI_CHAT_MEMORY历史数据和保留期限,确定旧会话如何继续访问,再切换新请求入口。 - 如果目标是应用重启后继续对话,就用真实数据库集成测试验证这一行为,同时检查过期、删除和跨会话隔离,避免只验证 Bean 能成功启动。