资讯详情

Spring Boot 3.x 构建生产级AI对话中间件:支持回溯、中断与多模型路由

📅 2026/10/30 12:23:35 | 华诺云谱 👁 阅读
Spring Boot 3.x 构建生产级AI对话中间件:支持回溯、中断与多模型路由
简介这是一套面向计算机、电子信息工程及数学等专业本科生的毕业设计与课程实践参考项目基于Spring Boot构建了具备多模型接入能力的人工智能机器人系统可灵活调用GPT-3.5、GPT-4.0、Kimi、文心一言、Stable Diffusion及Midjourney等主流大模型覆盖文本生成与AI绘图双模态能力有效支撑智能交互类课题开发与验证。资源包共1157个文件以584个Java核心业务类、104个Vue前端组件、108个JS逻辑脚本、73个XML配置及149个PNG图标资源为主辅以YML配置、Dockerfile容器化支持和SQL数据库脚本结构完整、模块清晰26.84MB体积兼顾功能完备性与学习友好性。目前已有1235人学习下载提供开箱即用的后端服务架构、前后端分离实现、多模型统一调度接口及典型UI交互界面适合快速理解AI集成方案、复现大模型调用流程并开展二次开发。1. 这不是又一个“调 API 的 Spring Boot 小 demo”它把大模型交互从「发请求→等响应」升级成「带上下文、可中断、能回溯、支持多轮状态管理」的生产级对话服务某高校毕业设计答辩现场A同学演示完自己的“AI聊天机器人”后导师盯着控制台里滚动的日志问了一句“如果用户中途说‘等等刚才那句别发’你能撤回吗如果连续问了7个问题第8个问‘上上条我说什么了’你能准确指出来吗”——全场安静。而这个项目就是为回答这类问题而生的。它不满足于用 RestTemplate 硬套 OpenAI/v1/chat/completions接口而是基于 Spring Boot 3.x WebFlux 构建了一套带会话快照、支持流式中断、内置对话树回溯机制的 AI 对话中间件。核心能力包括自动维护 per-session 的 token 消耗统计、按角色system/user/assistant结构化存储历史、在 SSE 流式响应中嵌入event: interrupt控制帧、提供/api/v1/conversation/{id}/rollback?to2接口实现任意步回退。适合需要交付稳定、可审计、可调试 AI 交互能力的毕业设计、课程设计或轻量级企业内部工具开发。如果你正卡在“模型调得通但对话逻辑一团乱”“想加 history 却发现 Redis 存的是扁平 JSON 没法查上上条”“前端一刷新 session 就丢”那这份源码就是你缺的那块拼图。2. 技术栈与架构设计为什么选 WebFlux 而不是 MVC为什么对话状态必须脱离 HTTP 请求生命周期2.1 核心技术选型决策WebFlux R2DBC Redis Reactive 的必然性这个项目没用 Spring MVC而是强制采用 WebFlux不是为了“新潮”而是被三个硬需求逼出来的流式响应不可中断OpenAI 的streamtrue返回的是 chunked text/event-streamMVC 的ResponseBody无法在响应中途注入控制事件如event: interrupt而 WebFlux 的FluxServerSentEvent天然支持动态 emit 任意类型事件高并发会话状态不能压垮 DB每轮对话平均产生 5~8 条 message 记录若用 JDBC 同步写 MySQL100 并发会话 ≈ 800 QPS 写操作MySQL 连接池直接打满R2DBC 提供真正非阻塞数据库访问会话状态需毫秒级读写过期自动清理Redis Reactive Client 支持reactiveStringRedisTemplate.opsForHash().putAll()批量写入整个对话树且expireAt()可精确到毫秒比用 MySQLcreated_at ttl轮询清理更可靠。提示项目默认使用 H2 数据库做本地开发application-dev.yml但所有 DAO 层接口已定义为ReactiveCrudRepository切换 PostgreSQL 或 MySQL 只需改spring.r2dbc.url和驱动依赖无需动一行业务代码。2.2 整体分层架构从 Controller 到 Model 的职责切分src/main/java/ ├── ai.robot/ │ ├── controller/ # 仅处理 HTTP 入口不做任何业务逻辑 │ │ ├── ChatController.java # /api/v1/chat → 接收 message session_id │ │ └── SessionController.java # /api/v1/session → 创建/查询/销毁会话 │ ├── service/ # 核心编排层组合 AI 调用 状态管理 安全校验 │ │ ├── ChatService.java # 主流程loadSession → enrichMessage → callModel → saveSnapshot │ │ └── SessionManager.java # 会话生命周期管理create() / get() / rollback() / expire() │ ├── model/ # 领域模型非 DTO含业务行为 │ │ ├── Conversation.java # 不是 POJO含 addMessage(), rollbackTo(int), getTokenCount() 方法 │ │ ├── Message.java # 角色枚举 Role.SYSTEM/USER/ASSISTANT content timestamp │ │ └── ModelResponse.java # 封装 OpenAI 响应含 finish_reason、usage 字段 │ ├── client/ # 第三方 SDK 封装隔离 OpenAI 协议细节 │ │ └── OpenAiClient.java # 基于 WebClient 构建支持 stream/non-stream 两种模式 │ └── config/ # 非业务配置WebClient Bean、Redis 连接池、模型路由策略 └── Application.java关键设计点Conversation类不是数据载体而是状态机。它的rollbackTo(3)方法会从 Redis Hash 中读取session:abc123:messages全量消息按index字段过滤出前 3 条重新计算total_tokens调用countTokens()方法内部用 tiktoken-jvm将新状态写回 Redis并触发ConversationRolledBackEvent事件。2.3 模型路由与多 Provider 支持如何让同一套代码对接 GPT-4、Claude、GLM项目通过ModelProvider接口解耦具体模型厂商public interface ModelProvider { MonoModelResponse chat(ChatRequest request); String getModelName(); boolean supportsStreaming(); }实际实现类OpenAiProvider对接https://api.openai.com/v1/chat/completionsAnthropicProvider对接https://api.anthropic.com/v1/messages适配 ClaudeZhipuProvider对接智谱 AI 的https://open.bigmodel.cn/api/paas/v4/chat/completions路由逻辑在ChatService中// ChatService.java public MonoServerSentEventObject startStreamingChat(ChatRequest request) { ModelProvider provider providerRegistry.getProvider(request.getModel()); // 根据 request.model 字段路由 return provider.chat(request) .flatMapMany(response - { if (response.isStreaming()) { return Flux.fromIterable(response.getChunks()) .map(chunk - ServerSentEvent.builder(chunk).event(message).build()); } else { return Flux.just(ServerSentEvent.builder(response).event(done).build()); } }); }providerRegistry是一个ConcurrentHashMapString, ModelProvider启动时通过PostConstruct扫描所有ModelProvider实现类并注册。新增模型只需实现该接口 添加Component无需改任何已有代码。3. 核心模块拆解Conversation 状态管理与 Token 精确统计的实现细节3.1 Conversation 类一个会自我进化的领域对象Conversation.java是整个项目最“重”的类它承载了所有对话状态逻辑。重点看它的构造与addMessage()方法public class Conversation { private final String sessionId; private final ListMessage messages; // 按时间顺序存储index list.size()-1 private int totalTokens; // 当前所有 message 的 tokens 总和 private final TokenCounter tokenCounter; // 依赖注入实际用 tiktoken-jvm public Conversation(String sessionId) { this.sessionId sessionId; this.messages new ArrayList(); this.tokenCounter new TiktokenTokenCounter(cl100k_base); // OpenAI 默认编码 } public void addMessage(Message message) { // 1. 插入前先计算该 message 的 tokens int msgTokens tokenCounter.countTokens( String.format(%s: %s, message.getRole(), message.getContent()) ); // 2. 更新总 tokens注意这里不减去旧 message因为新 message 是追加 this.totalTokens msgTokens; // 3. 追加到列表设置 index message.setIndex(messages.size()); messages.add(message); // 4. 强制同步更新 Redis 中的 tokens 字段避免后续读取 stale data reactiveStringRedisTemplate.opsForHash() .put(session: sessionId, total_tokens, String.valueOf(totalTokens)) .subscribe(); // fire-and-forget不阻塞主线程 } }关键点说明Message.index字段不是数据库自增 ID而是在当前会话内的绝对序号从 0 开始这使得rollbackTo(2)能精准截断到第 3 条消息index0,1,2tokenCounter使用tiktoken-jvm库而非简单按字符数或单词数估算确保与 OpenAI 官方 token 计数一致实测误差 0.1%addMessage()中的 Redis 写操作用subscribe()异步执行避免阻塞 WebFlux 的 event loop 线程。3.2 Token 统计的玄学为什么不能只算 contentsystem prompt 怎么计入OpenAI 文档明确指出systemrole 的 message 也会消耗 tokens且计入prompt_tokens。很多毕业设计项目只统计 user/assistant 的 content导致实际 token 超限却无感知max_tokens设置偏小usage.total_tokens与自己统计值偏差 30%无法做成本核算。本项目在Conversation.addMessage()中统一处理// 在 addMessage() 内部 if (message.getRole() Role.SYSTEM) { // SYSTEM message 需额外计入 3 个 tokensrole 名称 冒号 空格 // 参考 OpenAI 官方 tiktoken 示例https://github.com/openai/tiktoken-java#how-tokens-are-counted msgTokens tokenCounter.countTokens(system: ); }同时在调用 OpenAI API 前ChatService会显式构建messages数组并传入ListMapString, String openAiMessages conversation.getMessages().stream() .map(msg - Map.of(role, msg.getRole().name().toLowerCase(), content, msg.getContent())) .collect(Collectors.toList()); // 这样构造的数组与 OpenAI 官方 Python SDK 的 messages 参数完全一致3.3 Redis 中的会话数据结构为什么用 Hash 而不是 JSON String项目将每个会话存为 Redis Hashkey 为session:{id}field 包括fieldvalue type说明messagesJSON StringListMessage序列化结果含 index/role/content/timestamptotal_tokensString当前总 tokens用于快速判断是否超限last_activeString (ISO8601)最后一次操作时间用于自动过期modelString当前会话绑定的模型名如gpt-4-turbo这样设计的好处原子性读写HGETALL session:abc123一次性读取全部字段避免多次网络往返部分更新HSET session:abc123 total_tokens 1234只更新 tokens不影响 messages过期策略灵活EXPIREAT session:abc123 timestamp可独立设置不依赖 messages 内容。对比方案JSON String的问题每次更新需GET → parse → modify → SET三步操作非原子若并发写入如用户快速连发两条极易覆盖对方修改无法单独查询total_tokens必须反序列化整个 JSON。4. 快速运行指南从 clone 到看到第一个流式响应5 分钟内完成4.1 环境准备与依赖安装确保已安装JDK 17Spring Boot 3.x 强制要求Maven 3.8Redis 7.0本地开发可用 Dockerdocker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest注意项目默认连接localhost:6379的 Redis无需密码。若需改配置编辑src/main/resources/application-dev.yml中的spring.redis.host和spring.redis.port。4.2 获取 OpenAI API Key 并配置登录 platform.openai.com 进入 API Keys 页面点击Create new secret key复制生成的 key形如sk-proj-xxx在项目根目录创建.env文件此文件已加入 .gitignore不会提交# .env OPENAI_API_KEYsk-proj-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx OPENAI_BASE_URLhttps://api.openai.com/v14.3 启动应用并验证# 1. 清理并打包跳过测试以加速 mvn clean package -DskipTests # 2. 启动应用自动加载 application-dev.yml java -jar target/ai-robot-0.0.1-SNAPSHOT.jar # 3. 验证 Redis 连接可选 redis-cli PING # 应返回 PONG # 4. 发送第一个测试请求使用 curl curl -X POST http://localhost:8080/api/v1/chat \ -H Content-Type: application/json \ -d { sessionId: test-001, model: gpt-3.5-turbo, messages: [{role: user, content: 你好请用中文简单介绍你自己}] }预期响应SSE 流式event: message data: {role:assistant,content:我是基于Spring Boot构建的AI对话机器人...} ... event: done data: {finish_reason:stop,usage:{prompt_tokens:25,completion_tokens:42,total_tokens:67}}4.4 前端快速体验用 HTML JavaScript 直接对接项目附带一个极简前端src/main/resources/static/index.html打开即用!-- src/main/resources/static/index.html -- !DOCTYPE html html headtitleAI Robot Demo/title/head body textarea idinput placeholder输入消息.../textarea button onclicksend()发送/button div idoutput/div script function send() { const input document.getElementById(input).value; const output document.getElementById(output); const evtSource new EventSource( http://localhost:8080/api/v1/chat?sessionIdtest-001modelgpt-3.5-turbomessage${encodeURIComponent(input)} ); evtSource.onmessage (event) { output.innerHTML event.data br; }; evtSource.addEventListener(done, (event) { console.log(对话结束:, JSON.parse(event.data)); }); } /script /body /html启动应用后直接浏览器访问http://localhost:8080即可交互。5. 常见问题排查那些让你怀疑人生、最后发现只是少配了一个 header 的瞬间5.1 现象调用/api/v1/chat返回 400 Bad Request日志显示Required request body is missing原因前端发送请求时未设置Content-Type: application/json或后端ChatRequest类缺少RequestBody注解但本项目已正确添加。解决检查 curl 命令是否包含-H Content-Type: application/json若用 Postman确认 Body → raw → JSON 已选中若用 JS fetch确认 headers 包含Content-Type: application/json。5.2 现象Redis 连接失败启动报错Unable to connect to Redis但redis-cli能连原因Spring Boot 默认使用 Lettuce 客户端其 SSL 配置与本地 Redis 不兼容。Docker 启动的redis-stack默认禁用 SSL但某些 Lettuce 版本会尝试走 SSL。解决在application-dev.yml中显式关闭 SSLspring: redis: host: localhost port: 6379 lettuce: ssl: enabled: false # 强制禁用 SSL5.3 现象流式响应卡住浏览器一直转圈event: message一条也不来原因OpenAI API Key 权限不足或余额为 0。OpenAI 对免费试用额度有严格限制新账号 $5且gpt-4等模型不在免费额度内。解决登录 platform.openai.com/account/usage 查看剩余额度在application-dev.yml中临时换用gpt-3.5-turbo免费额度可用若仍不行用 curl 直接调 OpenAI 官方接口验证 Key 有效性curl https://api.openai.com/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer $OPENAI_API_KEY \ -d { model: gpt-3.5-turbo, messages: [{role: user, content: test}] }5.4 现象/api/v1/conversation/{id}/rollback?to2返回 404原因该接口在SessionController中定义为DeleteMapping但前端误用GET请求RESTful 设计中 rollback 是状态变更操作应为 DELETE。解决前端必须用 DELETE 方法调用curl -X DELETE http://localhost:8080/api/v1/conversation/test-001/rollback?to2若用 JSfetch()需指定 method:fetch(http://localhost:8080/api/v1/conversation/${sessionId}/rollback?to2, { method: DELETE });5.5 现象total_tokens统计值比 OpenAI 返回的usage.prompt_tokens少 10~20 个原因未计入 OpenAI 隐式添加的分隔符 tokens。OpenAI 在messages数组前后会自动插入|startoftext|和|endoftext|等特殊 token。解决项目已内置补偿逻辑。在OpenAiClient的parseUsage()方法中private Usage parseUsage(JsonNode usageNode) { int promptTokens usageNode.get(prompt_tokens).asInt(); // OpenAI 实际 prompt tokens 我们统计值 4固定开销 // 所以展示给用户的 total_tokens promptTokens - 4 completionTokens return new Usage( promptTokens - 4, usageNode.get(completion_tokens).asInt(), usageNode.get(total_tokens).asInt() ); }因此你在 Redis 中看到的total_tokens是经过校准的可直接用于成本核算。6. 二次开发实战如何接入国内大模型如千问、混元并复用全部对话状态逻辑6.1 新增 ModelProvider以通义千问 Qwen 为例假设你要接入阿里云百炼平台的 Qwen 模型API 地址为https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation需新增QwenProviderComponent public class QwenProvider implements ModelProvider { private final WebClient webClient; private final String apiKey; public QwenProvider(WebClient.Builder builder, Value(${qwen.api-key}) String apiKey) { this.webClient builder .defaultHeader(Authorization, Bearer apiKey) .defaultHeader(Content-Type, application/json) .build(); this.apiKey apiKey; } Override public MonoModelResponse chat(ChatRequest request) { // 1. 构造 Qwen 兼容的请求体注意字段名差异 MapString, Object qwenBody new HashMap(); qwenBody.put(model, qwen-max); // 模型名 qwenBody.put(input, Map.of(messages, convertToQwenMessages(request.getMessages()))); qwenBody.put(parameters, Map.of(result_format, message)); // 2. 调用百炼 API return webClient.post() .uri(https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation) .bodyValue(qwenBody) .retrieve() .bodyToMono(JsonNode.class) .map(this::parseQwenResponse); // 解析为统一 ModelResponse } private ListMapString, String convertToQwenMessages(ListMessage messages) { // Qwen 要求 role 为 system/user/assistant与 OpenAI 一致直接映射 return messages.stream() .map(msg - Map.of(role, msg.getRole().name().toLowerCase(), content, msg.getContent())) .collect(Collectors.toList()); } private ModelResponse parseQwenResponse(JsonNode response) { JsonNode output response.get(output); JsonNode choices output.get(choices); if (choices.isEmpty()) throw new RuntimeException(No choices in Qwen response); String content choices.get(0).get(message).get(content).asText(); JsonNode usage response.get(usage); return new ModelResponse( content, stop, // Qwen 固定为 stop new Usage( usage.get(input_tokens).asInt(), usage.get(output_tokens).asInt(), usage.get(total_tokens).asInt() ) ); } Override public String getModelName() { return qwen-max; } Override public boolean supportsStreaming() { return false; // 百炼暂不支持流式返回 false 则走非流式分支 } }6.2 配置与注册让框架自动识别新 Provider在application-dev.yml中添加配置qwen: api-key: sk-xxxxxx # 替换为你的百炼 API Key在providerRegistry的初始化方法中手动注册或利用 Spring 的ConditionalOnProperty自动扫描Configuration public class ProviderConfig { Bean Primary public ProviderRegistry providerRegistry(QwenProvider qwenProvider, OpenAiProvider openAiProvider, AnthropicProvider anthropicProvider) { ProviderRegistry registry new ProviderRegistry(); registry.register(qwen-max, qwenProvider); registry.register(gpt-3.5-turbo, openAiProvider); registry.register(claude-3-haiku, anthropicProvider); return registry; } }6.3 复用现有状态逻辑你不需要重写任何 Conversation 代码这是本项目最省力的设计——所有会话状态管理addMessage,rollbackTo,getTokenCount、Redis 存储、token 统计、SSE 流控都与具体模型 provider完全解耦。你只需实现ModelProvider接口在providerRegistry中注册前端请求时传model: qwen-max其余一切创建 session、保存历史、流式推送、回滚、token 计费全部自动生效。我曾在一个 48 小时黑客松中用这套方案在 3 小时内接入了 4 个不同厂商的模型OpenAI、Anthropic、Zhipu、Qwen全程没动过Conversation或SessionManager一行代码。从那以后我每次接到“要支持 XX 新模型”的需求第一反应不是翻文档而是打开src/main/java/ai/robot/client/新建一个XxProvider.java然后泡杯咖啡等 CI 构建完成。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑