通关我贼溜
155.11M · 2026-02-19
在 AI 应用爆发的今天,多轮对话和会话管理是构建生产级聊天应用的核心能力。然而,很多开发者在入门 Spring AI 时,往往只实现了简单的单次问答,缺乏对上下文管理、会话持久化、流式输出等关键特性的深入理解。
本文将带你从零开始,基于 Spring Boot 3.x + Spring AI + R2DBC 技术栈,构建一个支持多会话管理、上下文记忆、流式/非流式双模式的智能聊天服务。通过本文,你将掌握:
本项目实现了一个完整的 AI 聊天服务,具备以下核心能力:
| 特性 | 说明 |
|---|---|
| 多会话管理 | 支持创建、查询、删除多个独立会话 |
| 上下文记忆 | 自动携带历史消息,支持多轮对话 |
| 上下文截断 | 智能控制历史消息数量,防止 Token 超限 |
| 双模式输出 | 支持同步响应(非流式)和 SSE 流式输出 |
| 响应式架构 | 全面使用 WebFlux + R2DBC,高并发友好 |
| 配置外部化 | 系统提示词、上下文长度等参数可配置 |
| 技术 | 版本 | 用途 |
|---|---|---|
| Spring Boot | 3.5.10 | 基础框架 |
| Spring AI | 1.0.0-SNAPSHOT | AI 能力封装 |
| Spring WebFlux | 3.5.10 | 响应式 Web 框架 |
| Spring Data R2DBC | 3.5.10 | 响应式数据库访问 |
| PostgreSQL | 14+ | 数据持久化 |
| R2DBC PostgreSQL | - | 响应式 PostgreSQL 驱动 |
| Java | 25 | 编程语言 |
┌─────────────────────────────────────────────────────────────┐
│ Client (Postman/Curl) │
└───────────────────────┬─────────────────────────────────────┘
│ HTTP/SSE
┌───────────────────────▼─────────────────────────────────────┐
│ ChatController (RESTful API) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ POST /chat │ │POST /stream │ │ Session Management │ │
│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
└─────────┼───────────────┼───────────────────┼──────────────┘
│ │ │
┌─────────▼───────────────▼───────────────────▼──────────────┐
│ ChatService │
│ (业务逻辑 + 上下文管理 + 消息持久化) │
└─────────┬───────────────────────┬──────────────────────────┘
│ │
┌─────────▼──────────┐ ┌─────────▼──────────┐
│ ChatClient │ │ R2DBC Repository │
│ (Spring AI) │ │ (Session/Message) │
└─────────┬──────────┘ └─────────┬──────────┘
│ │
┌─────────▼──────────┐ ┌─────────▼──────────┐
│ OpenAI API │ │ PostgreSQL │
│ (或兼容 API) │ │ (chatdb) │
└────────────────────┘ └────────────────────┘
-- 创建数据库
CREATE DATABASE chatdb;
-- 创建用户(如需要)
CREATE USER chatuser WITH PASSWORD 'your_password';
GRANT ALL PRIVILEGES ON DATABASE chatdb TO chatuser;
创建 Maven 项目,添加以下依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 d">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>spring-ai-chat</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>25</maven.compiler.source>
<maven.compiler.target>25</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.10</version>
</parent>
<!-- Spring AI 仓库配置 -->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url></url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url></url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- WebFlux - 响应式 Web 框架 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- Spring MVC(可选,用于兼容) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Spring AI OpenAI Starter -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<!-- R2DBC 响应式数据库 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<!-- PostgreSQL R2DBC 驱动 -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>r2dbc-postgresql</artifactId>
</dependency>
</dependencies>
</project>
在深入代码之前,先理解几个关键概念:
| 组件 | 作用 |
|---|---|
ChatClient | AI 聊天的核心入口,封装了模型调用逻辑 |
Prompt | 提示词对象,包含完整的对话上下文 |
Message | 单条消息,分为 SystemMessage、UserMessage、AssistantMessage |
ChatModel | 底层模型接口,由 Spring AI 自动配置 |
Spring WebFlux 使用 Reactor 作为响应式编程库:
Mono<T>:表示 0 或 1 个元素的异步序列(适合单条数据)Flux<T>:表示 0 到 N 个元素的异步序列(适合流式数据、列表)为什么用响应式?
大模型有 Token 限制(如 GPT-3.5 是 4K/16K,GPT-4 是 8K/32K),需要控制发送的历史消息数量:
策略:滑动窗口,保留最近 N 轮对话
完整历史:[Msg1, Msg2, Msg3, ..., Msg50] (50条 = 25轮)
↓ 截断,保留最近 20 轮
发送给AI:[SystemMsg, Msg11, Msg12, ..., Msg50] (41条 = 1系统+20轮)
src/main/java/org/example/
├── SpringAiJcStart.java # 启动类
├── config/
│ └── ChatProperties.java # 配置属性类
├── controller/
│ └── ChatController.java # REST API 控制器
├── entity/
│ ├── ConversationSession.java # 会话实体
│ └── ConversationMessage.java # 消息实体
├── exception/
│ ├── ChatException.java # 自定义异常
│ ├── ErrorResponse.java # 错误响应
│ └── GlobalExceptionHandler.java # 全局异常处理
├── repository/
│ ├── ConversationSessionRepository.java # 会话数据访问
│ └── ConversationMessageRepository.java # 消息数据访问
└── service/
└── ChatService.java # 核心业务逻辑
src/main/resources/
├── application.yml # 应用配置
└── schema.sql # 数据库初始化脚本
-- schema.sql
-- 创建会话表
CREATE TABLE IF NOT EXISTS conversation_sessions (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- 创建消息表
CREATE TABLE IF NOT EXISTS conversation_messages (
id BIGSERIAL PRIMARY KEY,
session_id BIGINT NOT NULL REFERENCES conversation_sessions(id) ON DELETE CASCADE,
role VARCHAR(50) NOT NULL, -- 'user' 或 'assistant'
content TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- 创建索引优化查询
CREATE INDEX IF NOT EXISTS idx_messages_session_id ON conversation_messages(session_id);
CREATE INDEX IF NOT EXISTS idx_sessions_updated_at ON conversation_sessions(updated_at DESC);
package org.example.entity;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
import java.time.LocalDateTime;
/**
* 会话实体 - 对应 conversation_sessions 表
*
* 使用 Java Record 简化定义,自动包含:
* - 构造方法
* - getter 方法
* - equals/hashCode/toString
*/
@Table("conversation_sessions")
public record ConversationSession(
@Id
Long id,
@Column("title")
String title,
@Column("created_at")
LocalDateTime createdAt,
@Column("updated_at")
LocalDateTime updatedAt
) {
/**
* 工厂方法:创建新会话
*/
public static ConversationSession create(String title) {
LocalDateTime now = LocalDateTime.now();
return new ConversationSession(
null, // id 由数据库自动生成
title,
now,
now
);
}
/**
* 更新时间戳
*/
public ConversationSession withUpdatedTime() {
return new ConversationSession(
this.id,
this.title,
this.createdAt,
LocalDateTime.now()
);
}
}
package org.example.entity;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
import java.time.LocalDateTime;
/**
* 消息实体 - 对应 conversation_messages 表
*/
@Table("conversation_messages")
public record ConversationMessage(
@Id
Long id,
@Column("session_id")
Long sessionId,
@Column("role")
String role, // "user" 或 "assistant"
@Column("content")
String content,
@Column("created_at")
LocalDateTime createdAt
) {
/**
* 工厂方法:创建新消息
*/
public static ConversationMessage of(Long sessionId, String role, String content) {
return new ConversationMessage(
null,
sessionId,
role,
content,
LocalDateTime.now()
);
}
}
package org.example.repository;
import org.example.entity.ConversationSession;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Flux;
/**
* 会话数据访问接口
*/
public interface ConversationSessionRepository
extends ReactiveCrudRepository<ConversationSession, Long> {
/**
* 按更新时间倒序查询所有会话
*/
Flux<ConversationSession> findAllByOrderByUpdatedAtDesc();
}
package org.example.repository;
import org.example.entity.ConversationMessage;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 消息数据访问接口
*/
public interface ConversationMessageRepository
extends ReactiveCrudRepository<ConversationMessage, Long> {
/**
* 按时间正序查询会话的所有消息
*/
Flux<ConversationMessage> findBySessionIdOrderByCreatedAtAsc(Long sessionId);
/**
* 删除会话的所有消息
*/
Mono<Void> deleteBySessionId(Long sessionId);
}
# application.yml
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY:your-api-key-here}
base-url: # 或使用兼容 API
chat:
options:
model: gpt-3.5-turbo
retry:
max-attempts: 3
backoff:
initial-interval: 1000
multiplier: 2
max-interval: 10000
server:
port: 8080
# R2DBC 数据库配置
r2dbc:
url: r2dbc:postgresql://localhost:5432/chatdb
username: postgres
password: root
pool:
enabled: true
initial-size: 5
max-size: 20
# 自动执行 schema.sql
sql:
init:
mode: always
# 自定义配置
app:
chat:
# 系统提示词 - 定义AI助手的行为准则
system-prompt: "你是一个友好、专业的AI助手,请用简洁清晰的语言回答用户的问题。"
# 最大保留的对话轮数(一对 = user + assistant)
max-context-pairs: 20
package org.example.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 聊天服务配置属性
*
* 配置项前缀: app.chat
* 支持在 application.yml 中自定义配置
*/
@Configuration
@ConfigurationProperties(prefix = "app.chat")
public class ChatProperties {
/**
* 系统提示词 - 定义AI助手的行为准则
*/
private String systemPrompt = "你是一个友好、专业的AI助手,请用简洁清晰的语言回答用户的问题。";
/**
* 最大保留的对话轮数(一对 = user + assistant)
* 默认20对 = 40条消息,加上系统消息共41条
*/
private int maxContextPairs = 20;
// Getters and Setters
public String getSystemPrompt() {
return systemPrompt;
}
public void setSystemPrompt(String systemPrompt) {
this.systemPrompt = systemPrompt;
}
public int getMaxContextPairs() {
return maxContextPairs;
}
public void setMaxContextPairs(int maxContextPairs) {
this.maxContextPairs = maxContextPairs;
}
/**
* 获取最大保留的消息条数(不含系统消息)
*/
public int getMaxContextMessages() {
return maxContextPairs * 2;
}
}
package org.example.service;
import org.example.config.ChatProperties;
import org.example.entity.ConversationMessage;
import org.example.entity.ConversationSession;
import org.example.repository.ConversationMessageRepository;
import org.example.repository.ConversationSessionRepository;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.MessageType;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.retry.TransientAiException;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.util.ArrayList;
import java.util.List;
/**
* 聊天服务 - 支持多会话管理和上下文长度控制
*
* 核心设计原则:
* 1. 无状态设计 - 不保存当前会话状态,所有操作都需要明确的 sessionId
* 2. 上下文控制 - 限制历史消息数量,防止token超限
* 3. 响应式编程 - 全面使用 Mono/Flux 进行异步操作
* 4. 配置外部化 - 系统提示词和上下文长度通过配置文件管理
*/
@Service
public class ChatService {
private final ChatClient chatClient;
private final ConversationSessionRepository sessionRepository;
private final ConversationMessageRepository messageRepository;
private final ChatProperties chatProperties;
public ChatService(
ChatModel chatModel,
ConversationSessionRepository sessionRepository,
ConversationMessageRepository messageRepository,
ChatProperties chatProperties
) {
this.chatClient = ChatClient.builder(chatModel).build();
this.sessionRepository = sessionRepository;
this.messageRepository = messageRepository;
this.chatProperties = chatProperties;
}
// ... 业务方法将在下文展开
}
/**
* 普通聊天(非流式)
*
* 执行流程:
* 1. 验证 sessionId 有效性
* 2. 构建对话历史(带上下文截断)
* 3. 添加当前用户消息
* 4. 调用 AI 模型
* 5. 保存用户消息和 AI 回复到数据库
* 6. 返回 AI 回复
*
* @param sessionId 会话ID,必须提供
* @param userMessage 用户消息
* @return AI回复
*/
public Mono<String> chat(Long sessionId, String userMessage) {
// 参数校验
if (sessionId == null) {
return Mono.error(new IllegalArgumentException("sessionId 不能为空,请先创建会话"));
}
return validateSessionExists(sessionId)
.flatMap(sid -> buildConversationHistory(sid)
.flatMap(history -> {
// 添加用户消息到历史
history.add(new UserMessage(userMessage));
// 构建 Prompt
Prompt prompt = new Prompt(new ArrayList<>(history));
// 调用 AI(使用 boundedElastic 线程池避免阻塞)
return Mono.fromCallable(() ->
chatClient.prompt(prompt)
.call()
.content()
)
.subscribeOn(Schedulers.boundedElastic())
.flatMap(response -> {
if (response != null && !response.isEmpty()) {
// 先保存用户消息,再保存AI回复
return saveMessage(sid, MessageType.USER.getValue(), userMessage)
.then(saveMessage(sid, MessageType.ASSISTANT.getValue(), response))
.thenReturn(response);
}
return Mono.justOrEmpty(response);
})
// 错误处理:AI 服务暂时不可用
.onErrorResume(TransientAiException.class, e -> {
String errorMsg = extractErrorMessage(e);
return Mono.just("【AI服务暂时不可用】" + errorMsg);
})
// 错误处理:其他异常
.onErrorResume(Exception.class, e ->
Mono.just("【请求失败】" + e.getMessage())
);
})
);
}
/**
* 流式聊天(SSE)
*
* 执行流程:
* 1. 验证 sessionId 有效性
* 2. 构建对话历史
* 3. 添加当前用户消息
* 4. 调用 AI 流式接口
* 5. 实时返回流式数据
* 6. 流完成后异步保存消息
*
* @param sessionId 会话ID,必须提供
* @param userMessage 用户消息
* @return 流式AI回复(Flux<String>)
*/
public Flux<String> chatStream(Long sessionId, String userMessage) {
if (sessionId == null) {
return Flux.error(new IllegalArgumentException("sessionId 不能为空,请先创建会话"));
}
return validateSessionExists(sessionId)
.flatMapMany(sid -> buildConversationHistory(sid)
.flatMapMany(history -> {
// 添加用户消息到历史
history.add(new UserMessage(userMessage));
Prompt prompt = new Prompt(new ArrayList<>(history));
StringBuilder fullResponse = new StringBuilder();
return chatClient.prompt(prompt)
.stream() // 启用流式输出
.content() // 获取内容流
.doOnNext(fullResponse::append) // 累积完整回复
.doOnComplete(() -> {
// 流完成后异步保存消息
if (!fullResponse.isEmpty()) {
saveMessage(sid, MessageType.USER.getValue(), userMessage)
.then(saveMessage(sid, MessageType.ASSISTANT.getValue(), fullResponse.toString()))
.subscribeOn(Schedulers.boundedElastic())
.subscribe(); // 异步执行,不阻塞响应
}
})
.onErrorResume(TransientAiException.class, e -> {
String errorMsg = extractErrorMessage(e);
return Flux.just("【AI服务暂时不可用】" + errorMsg);
})
.onErrorResume(Exception.class, e ->
Flux.just("【请求失败】" + e.getMessage())
);
})
);
}
/**
* 构建对话历史(带上下文长度控制)
*
* 策略:
* 1. 始终包含系统消息(从配置读取)
* 2. 从数据库获取该会话的所有历史消息
* 3. 如果消息数量超过限制,保留最近的 maxContextMessages 条
*
* @param sessionId 会话ID
* @return 构建好的消息列表(包含系统消息)
*/
private Mono<List<Message>> buildConversationHistory(Long sessionId) {
return messageRepository.findBySessionIdOrderByCreatedAtAsc(sessionId)
.collectList()
.map(messages -> {
List<Message> history = new ArrayList<>();
// 1. 添加系统消息(从配置读取)
history.add(new SystemMessage(chatProperties.getSystemPrompt()));
// 2. 处理历史消息(上下文截断)
List<ConversationMessage> contextMessages = messages;
int maxContextMessages = chatProperties.getMaxContextMessages();
// 如果消息过多,只保留最近的 maxContextMessages 条
if (messages.size() > maxContextMessages) {
contextMessages = messages.subList(
messages.size() - maxContextMessages,
messages.size()
);
}
// 3. 转换为 Spring AI Message 对象
for (ConversationMessage msg : contextMessages) {
if (MessageType.USER.getValue().equals(msg.role())) {
history.add(new UserMessage(msg.content()));
} else if (MessageType.ASSISTANT.getValue().equals(msg.role())) {
history.add(new AssistantMessage(msg.content()));
}
// 系统消息已在开头添加,数据库中的系统消息忽略
}
return history;
});
}
/**
* 创建新会话
*
* @param title 会话标题(可选,默认为"新会话")
* @return 新会话ID
*/
public Mono<Long> createNewSession(String title) {
String sessionTitle = (title == null || title.isBlank()) ? "新会话" : title;
return sessionRepository.save(ConversationSession.create(sessionTitle))
.map(ConversationSession::id);
}
/**
* 获取所有会话列表(按更新时间倒序)
*
* @return 会话列表
*/
public Flux<ConversationSession> getAllSessions() {
return sessionRepository.findAllByOrderByUpdatedAtDesc();
}
/**
* 获取指定会话的所有消息
*
* @param sessionId 会话ID
* @return 消息列表
*/
public Flux<ConversationMessage> getSessionMessages(Long sessionId) {
if (sessionId == null) {
return Flux.error(new IllegalArgumentException("sessionId 不能为空"));
}
return messageRepository.findBySessionIdOrderByCreatedAtAsc(sessionId);
}
/**
* 删除会话及其所有消息
*
* @param sessionId 要删除的会话ID
* @return 操作完成信号
*/
public Mono<Void> deleteSession(Long sessionId) {
if (sessionId == null) {
return Mono.error(new IllegalArgumentException("sessionId 不能为空"));
}
// 先删除消息,再删除会话(利用外键级联删除也可)
return messageRepository.deleteBySessionId(sessionId)
.then(sessionRepository.deleteById(sessionId));
}
/**
* 验证会话是否存在
*/
private Mono<Long> validateSessionExists(Long sessionId) {
return sessionRepository.existsById(sessionId)
.flatMap(exists -> {
if (exists) {
return Mono.just(sessionId);
}
return Mono.error(new RuntimeException("会话不存在: " + sessionId));
});
}
/**
* 保存消息到数据库
*/
private Mono<Void> saveMessage(Long sessionId, String role, String content) {
return messageRepository.save(ConversationMessage.of(sessionId, role, content))
.then();
}
package org.example.controller;
import org.example.entity.ConversationMessage;
import org.example.entity.ConversationSession;
import org.example.service.ChatService;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
/**
* 聊天控制器 - RESTful API
*
* API 设计原则:
* 1. 所有聊天操作都需要提供 sessionId,明确指定操作哪个会话
* 2. 会话管理与会话操作分离
* 3. 支持流式和非流式两种聊天模式
*/
@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final ChatService chatService;
public ChatController(ChatService chatService) {
this.chatService = chatService;
}
// ==================== 聊天 API ====================
/**
* 普通聊天(非流式)
*
* @param request 包含 sessionId 和 message
* @return AI 回复
*/
@PostMapping
public Mono<String> chat(@RequestBody ChatRequest request) {
return chatService.chat(request.sessionId(), request.message());
}
/**
* 流式聊天(SSE)
*
* @param request 包含 sessionId 和 message
* @return 流式 AI 回复
*/
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chatStream(@RequestBody ChatRequest request) {
return chatService.chatStream(request.sessionId(), request.message());
}
// ==================== 会话管理 API ====================
/**
* 创建新会话
*
* @param request 可选的会话标题
* @return 新创建的会话ID
*/
@PostMapping("/sessions")
public Mono<CreateSessionResponse> createSession(@RequestBody(required = false) CreateSessionRequest request) {
String title = (request != null) ? request.title() : null;
return chatService.createNewSession(title)
.map(CreateSessionResponse::new);
}
/**
* 获取所有会话列表
*
* @return 按更新时间倒序排列的会话列表
*/
@GetMapping("/sessions")
public Flux<ConversationSession> getAllSessions() {
return chatService.getAllSessions();
}
/**
* 获取指定会话的详细信息
*
* @param sessionId 会话ID
* @return 会话信息
*/
@GetMapping("/sessions/{sessionId}")
public Mono<ConversationSession> getSession(@PathVariable Long sessionId) {
return chatService.getSession(sessionId);
}
/**
* 获取指定会话的所有消息
*
* @param sessionId 会话ID
* @return 消息列表
*/
@GetMapping("/sessions/{sessionId}/messages")
public Mono<List<ConversationMessage>> getSessionMessages(@PathVariable Long sessionId) {
return chatService.getSessionMessages(sessionId).collectList();
}
/**
* 删除会话及其所有消息
*
* @param sessionId 要删除的会话ID
*/
@DeleteMapping("/sessions/{sessionId}")
public Mono<Void> deleteSession(@PathVariable Long sessionId) {
return chatService.deleteSession(sessionId);
}
// ==================== 请求/响应记录 ====================
/**
* 聊天请求
*
* @param sessionId 会话ID(必填)
* @param message 用户消息
*/
public record ChatRequest(Long sessionId, String message) {}
/**
* 创建会话请求
*
* @param title 会话标题(可选)
*/
public record CreateSessionRequest(String title) {}
/**
* 创建会话响应
*
* @param sessionId 新创建的会话ID
*/
public record CreateSessionResponse(Long sessionId) {}
}
| 方法 | 端点 | 说明 | 请求体 | 响应 |
|---|---|---|---|---|
| POST | /api/chat | 非流式聊天 | {"sessionId": 1, "message": "你好"} | String |
| POST | /api/chat/stream | 流式聊天(SSE) | {"sessionId": 1, "message": "你好"} | Flux<String> |
| POST | /api/chat/sessions | 创建会话 | {"title": "会话标题"}(可选) | {"sessionId": 1} |
| GET | /api/chat/sessions | 获取所有会话 | - | Flux<ConversationSession> |
| GET | /api/chat/sessions/{id} | 获取会话详情 | - | ConversationSession |
| GET | /api/chat/sessions/{id}/messages | 获取会话消息 | - | List<ConversationMessage> |
| DELETE | /api/chat/sessions/{id} | 删除会话 | - | Void |
# 1. 确保 PostgreSQL 已启动且数据库已创建
# 2. 设置环境变量(或在 application.yml 中配置)
export OPENAI_API_KEY=your-api-key
# 3. 启动应用
mvn spring-boot:run
curl -X POST
-H "Content-Type: application/json"
-d '{"title": "技术讨论"}'
响应:
{"sessionId": 1}
curl -X POST
-H "Content-Type: application/json"
-d '{
"sessionId": 1,
"message": "你好,请介绍一下 Spring AI"
}'
响应:
你好!Spring AI 是 Spring 官方推出的 AI 应用开发框架...
curl -X POST
-H "Content-Type: application/json"
-d '{
"sessionId": 1,
"message": "用 Java 写个 Hello World"
}'
响应(逐字输出):
当当然!以下是一...
# 第二轮对话(携带上下文)
curl -X POST
-H "Content-Type: application/json"
-d '{
"sessionId": 1,
"message": "刚才说的内容能再详细点吗?"
}'
AI 能够根据之前的对话内容继续回答,证明上下文记忆生效。
curl
响应:
[
{"id": 1, "sessionId": 1, "role": "user", "content": "你好,请介绍一下 Spring AI", ...},
{"id": 2, "sessionId": 1, "role": "assistant", "content": "你好!Spring AI 是...", ...},
{"id": 3, "sessionId": 1, "role": "user", "content": "刚才说的内容能再详细点吗?", ...},
{"id": 4, "sessionId": 1, "role": "assistant", "content": "当然!Spring AI 提供了...", ...}
]
现象:
Cannot connect to localhost:5432
解决方案:
chatdb 是否已创建schema.sql 中的表名与实体类 @Table 注解是否一致现象: 请求响应慢,并发量高时系统卡顿
解决方案:
// 使用 Schedulers.boundedElastic() 避免阻塞
return Mono.fromCallable(() ->
chatClient.prompt(prompt).call().content()
)
.subscribeOn(Schedulers.boundedElastic()) // 在弹性线程池执行
现象:
This model's maximum context length is 4097 tokens
解决方案:
app.chat.max-context-pairs 配置,减少保留的历史轮数现象: SSE 流在浏览器中显示异常
解决方案:
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)EventSource 正确解析 SSE 格式server:
netty:
connection-timeout: 2s
onErrorResume 提供友好的错误提示通过本文,我们学习了:
ChatClient、Prompt、Message 的使用Mono 和 Flux 在 AI 应用中的实践基于本项目,你可以进一步实现:
| 功能 | 实现思路 |
|---|---|
| 用户认证 | 集成 Spring Security,会话关联用户ID |
| 消息分页 | 使用 Pageable 实现历史消息分页加载 |
| 多模型支持 | 配置多个 ChatModel,支持切换不同 AI |
| 消息编辑/重发 | 支持修改历史消息并重新生成回复 |
| 文件上传 | 集成 Spring AI 的文档解析能力 |
| 前端界面 | 使用 Vue/React 构建聊天界面 |
[建议:此处插入 GitHub/GitCode 仓库链接]
https://github.com/yourusername/spring-ai-chat-service
| 依赖 | 版本 |
|---|---|
| Spring Boot | 3.5.10 |
| Spring AI | 1.0.0-SNAPSHOT |
| Java | 25 |
| PostgreSQL | 14+ |
原创声明:本文为原创教程,转载请注明出处。
欢迎在评论区交流讨论!如果你有任何问题或建议,欢迎留言。
为什么选择 32ai?
低至 0.56 : 1 比率
快速访问: 点击访问 — 直连、无需魔法。