Spring Boot + MySQL:多轮聊天与 ChatMemory 持久化
这是一条最适合业务系统起步的链路:
- 直接使用
ai4j-spring-boot-starter - 用
JdbcChatMemory把会话落到 MySQL - 让同一个
sessionId在多次请求之间自动续上上下文
如果你现在还不需要完整 Agent runtime,而只是想先把“多轮聊天 + 会话持久化”做好,这页就是最短路径。
1. 适用场景
适合:
- Web 聊天页
- 企业问答助手
- 多轮客服机器人
- 同一用户会话需要跨实例恢复
不适合:
- 需要自动工具循环和复杂推理状态
- 需要 planning / handoff / trace / code execution
这类场景应直接看:
2. 依赖
最常见的 Spring Boot 组合如下:
<dependencies>
<dependency>
<groupId>io.github.lnyo-cly</groupId>
<artifactId>ai4j-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
如果你项目里已经用了 ai4j-bom,这里可以不再单独写版本。
3. application.yml
下面用 OpenAI 作为示例模型服务,你也可以换成 DeepSeek、Doubao、Ollama 等其它已接入 provider。
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/ai4j?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
ai:
openai:
api-key: ${OPENAI_API_KEY}
api-host: https://api.openai.com/
这一页的重点不在 Spring Session,而在 JdbcChatMemory:
DataSource由 Spring Boot 提供JdbcChatMemory直接复用这个DataSource- 默认会自动建表
ai4j_chat_memory
4. 先封装一个会话 Memory 工厂
import io.github.lnyocly.ai4j.memory.ChatMemory;
import io.github.lnyocly.ai4j.memory.JdbcChatMemory;
import io.github.lnyocly.ai4j.memory.JdbcChatMemoryConfig;
import io.github.lnyocly.ai4j.memory.MessageWindowChatMemoryPolicy;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
@Component
public class ChatMemoryFactory {
private final DataSource dataSource;
public ChatMemoryFactory(DataSource dataSource) {
this.dataSource = dataSource;
}
public ChatMemory create(String sessionId) {
return new JdbcChatMemory(
JdbcChatMemoryConfig.builder()
.dataSource(dataSource)
.sessionId(sessionId)
.policy(new MessageWindowChatMemoryPolicy(20))
.build()
);
}
}
这里用了一个非常实用的默认值:
- 只保留最近 20 条 memory item
这样做的好处是:
- 不需要一上来就做复杂压缩
- 可以先控制上下文成本
- 行为稳定,可预期
5. 聊天服务
import io.github.lnyocly.ai4j.memory.ChatMemory;
import io.github.lnyocly.ai4j.platform.openai.chat.entity.ChatCompletion;
import io.github.lnyocly.ai4j.platform.openai.chat.entity.ChatCompletionResponse;
import io.github.lnyocly.ai4j.service.IChatService;
import io.github.lnyocly.ai4j.service.PlatformType;
import io.github.lnyocly.ai4j.service.factory.AiService;
import org.springframework.stereotype.Service;
@Service
public class ChatSessionService {
private final AiService aiService;
private final ChatMemoryFactory chatMemoryFactory;
public ChatSessionService(AiService aiService, ChatMemoryFactory chatMemoryFactory) {
this.aiService = aiService;
this.chatMemoryFactory = chatMemoryFactory;
}
public String chat(String sessionId, String userMessage) throws Exception {
IChatService chatService = aiService.getChatService(PlatformType.OPENAI);
ChatMemory memory = chatMemoryFactory.create(sessionId);
if (memory.getItems().isEmpty()) {
memory.addSystem("你是一个简洁、准确的 AI4J 产品助手。");
}
memory.addUser(userMessage);
ChatCompletion request = ChatCompletion.builder()
.model("gpt-4o-mini")
.messages(memory.toChatMessages())
.build();
ChatCompletionResponse response = chatService.chatCompletion(request);
String answer = response.getChoices().get(0).getMessage().getContent().getText();
memory.addAssistant(answer);
return answer;
}
}
这里最关键的是两行:
memory.addUser(userMessage);
memory.toChatMessages();
它们意味着:
- 当前轮用户输入会先写入持久化 memory
- 发给模型的是“本轮 + 历史轮次”的完整上下文
返回后再执行:
memory.addAssistant(answer);
这样下一轮请求来时,这条回答已经在库里了。
6. 提供一个最小 Controller
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
import java.util.Map;
@RestController
@RequestMapping("/chat")
public class ChatController {
private final ChatSessionService chatSessionService;
public ChatController(ChatSessionService chatSessionService) {
this.chatSessionService = chatSessionService;
}
@PostMapping("/message")
public Map<String, Object> message(@RequestHeader("X-Session-Id") String sessionId,
@RequestBody Map<String, String> body) throws Exception {
String answer = chatSessionService.chat(sessionId, body.get("message"));
return Collections.<String, Object>singletonMap("answer", answer);
}
}
推荐用请求头、Cookie 或网关层 token 来确定 sessionId,不要让前端随意拼一个公共固定值。
7. 验证方式
第一次请求:
curl -X POST "http://127.0.0.1:8080/chat/message" ^
-H "Content-Type: application/json" ^
-H "X-Session-Id: demo-session-001" ^
-d "{\"message\":\"请用一句话介绍 AI4J\"}"
第二次请求继续同一个 sessionId:
curl -X POST "http://127.0.0.1:8080/chat/message" ^
-H "Content-Type: application/json" ^
-H "X-Session-Id: demo-session-001" ^
-d "{\"message\":\"再补一段关于 MCP 的说明\"}"
如果第二轮回答能承接第一轮语境,说明 JdbcChatMemory 已经生效。
8. 同一份 ChatMemory 也能给 Responses 用
如果你后面要把基础对话切到 Responses,不用重新维护另一套 history:
import io.github.lnyocly.ai4j.platform.openai.response.entity.Response;
import io.github.lnyocly.ai4j.platform.openai.response.entity.ResponseRequest;
import io.github.lnyocly.ai4j.service.IResponsesService;
import io.github.lnyocly.ai4j.service.PlatformType;
IResponsesService responsesService = aiService.getResponsesService(PlatformType.DOUBAO);
ChatMemory memory = chatMemoryFactory.create(sessionId);
memory.addUser("请继续总结刚才的重点");
ResponseRequest request = ResponseRequest.builder()
.model("doubao-seed-1-8-251228")
.input(memory.toResponsesInput())
.build();
Response response = responsesService.create(request);
也就是说:
ChatMemory不是只能给 Chat API 用- 同一份会话上下文可以同时兼容
Chat和Responses
9. 上线建议
sessionId最好绑定用户或设备,不要纯前端自生成后长期信任- 如果你只是普通聊天,优先使用
MessageWindowChatMemoryPolicy - 如果会话非常长,再考虑更上层的摘要压缩或迁移到
AgentMemory JdbcChatMemory解决的是上下文持久化,不替你做会话治理、鉴权和分库分表