AI大模型上下文连续对话+Token计费统计商用方案|SpringBoot3实现对话记忆、精准计费、额度限流(生产可用)
AI大模型上下文连续对话+Token计费统计商用方案|SpringBoot3实现对话记忆、精准计费、额度限流(生产可用)
一、前言:单轮对话的致命短板
在上一篇 Java后端对接AI大模型SSE流式输出 实战中,我们实现了基础的AI打字机流式问答,但仅支持单轮独立对话,存在两大商用致命问题:
- 无上下文记忆:每轮问答相互独立,AI无法关联历史对话,完全不符合用户使用习惯
- 无计费统计:无法统计用户提问、AI回复的Token消耗,付费AI产品无法核算成本、计费收费
- 无额度管控:无限流、无额度限制,极易被恶意刷接口,造成高额AI成本损耗
本文基于原有SSE流式架构,零重构升级,完整实现:多轮上下文记忆对话 + 精准Token计费统计 + 用户额度限流管控,一套代码直接支撑商用AI产品落地。
二、核心原理前置认知
2.1 AI上下文对话原理
大模型本身无本地记忆能力,所有上下文记忆均由业务后端维护。每次请求AI接口时,需要携带:历史对话记录 + 当前用户提问,大模型根据完整对话列表实现连续问答。
2.2 Token计费规则
主流大模型(DeepSeek、GPT、通义千问)均采用输入Token+输出Token分开计费模式:
- 输入Token:用户提问 + 历史上下文总字符消耗
- 输出Token:AI返回回答内容消耗
- 总费用 = 输入Token单价 + 输出Token单价
2.3 上下文过期与截断机制
对话无限累加会导致上下文过长、Token暴增、请求超时,生产环境必须配置上下文最大长度限制、自动截断、过期清理机制。
三、项目依赖(复用原有架构)
无需新增复杂依赖,沿用之前SSE流式项目依赖,仅需新增Redis依赖维护用户会话上下文:
<!-- Redis:维护用户会话上下文、额度限流 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
四、核心实体类定义
4.1 对话消息实体
统一封装单条对话消息角色、内容,适配大模型标准参数格式:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* AI对话消息实体
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AiMessage {
// 角色:user用户提问、assistant AI回复、system系统提示词
private String role;
// 对话内容
private String content;
}
4.2 Token计费统计实体
精准记录单轮对话、用户累计Token消耗:
import lombok.Data;
/**
* AI对话Token计费统计实体
*/
@Data
public class AiTokenBill {
// 输入Token(用户提问+上下文)
private Integer inputToken;
// 输出Token(AI回复)
private Integer outputToken;
// 本轮总Token
public Integer getTotalToken() {
return inputToken + outputToken;
}
}
五、工具类封装(Token计算+上下文管理)
5.1 Token通用计算工具类
采用行业通用字符估算规则(适配绝大多数大模型,误差极低,商用足够):
/**
* AI Token计算工具类
* 通用估算规则:1中文≈2Token,1英文/数字≈1Token
*/
public class AiTokenUtil {
/**
* 文本转Token估算
*/
public static int countToken(String text) {
if (text == null || text.isEmpty()) {
return 0;
}
// 中文字符统计
long chineseCount = text.chars().filter(c -> c > 127).count();
// 英文、数字、符号统计
long otherCount = text.length() - chineseCount;
// 通用换算公式
return (int) (chineseCount * 2 + otherCount);
}
}
5.2 原有SSE工具类(复用)
沿用上篇文章SseUtil工具类,无需修改,保证流式推送稳定性。
六、Redis上下文会话管理核心
使用Redis缓存用户会话对话记录,设置过期时间,实现多轮记忆、自动过期清理,支持多端会话同步。
import com.alibaba.fastjson2.JSON;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* AI会话上下文管理工具
*/
@Component
public class AiSessionContext {
@Resource
private StringRedisTemplate stringRedisTemplate;
// 用户会话Key前缀
private static final String AI_SESSION_PREFIX = "ai:session:";
// 会话过期时间:30分钟(无对话自动失效)
private static final long SESSION_EXPIRE = 30;
// 最大上下文对话轮数(防止Token溢出)
private static final int MAX_HISTORY_NUM = 10;
/**
* 获取用户完整上下文对话
*/
public List<AiMessage> getHistoryMessage(String sessionId) {
String key = AI_SESSION_PREFIX + sessionId;
String json = stringRedisTemplate.opsForValue().get(key);
if (json == null || json.isEmpty()) {
return new ArrayList<>();
}
// 读取历史对话,只保留最新10轮,截断旧对话
List<AiMessage> messageList = JSON.parseArray(json, AiMessage.class);
if (messageList.size() > MAX_HISTORY_NUM) {
messageList = messageList.subList(messageList.size() - MAX_HISTORY_NUM, messageList.size());
}
return messageList;
}
/**
* 更新用户会话上下文
*/
public void updateSessionMessage(String sessionId, List<AiMessage> messageList) {
String key = AI_SESSION_PREFIX + sessionId;
String json = JSON.toJSONString(messageList);
// 更新并重置过期时间
stringRedisTemplate.opsForValue().set(key, json, SESSION_EXPIRE, TimeUnit.MINUTES);
}
/**
* 清空用户会话
*/
public void clearSession(String sessionId) {
stringRedisTemplate.delete(AI_SESSION_PREFIX + sessionId);
}
}
七、完整商用接口:上下文对话+Token计费
整合SSE流式推送、多轮上下文记忆、Token实时统计、会话更新,可直接上线商用:
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import okhttp3.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import javax.annotation.Resource;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/ai")
public class AiChatController {
// 大模型密钥&地址
private static final String API_KEY = "sk-xxxx";
private static final String AI_URL = "https://api.deepseek.com/v1/chat/completions";
@Resource
private AiSessionContext aiSessionContext;
/**
* 多轮上下文流式对话 + Token计费统计
* @param sessionId 会话ID(前端生成,唯一标识用户对话)
* @param prompt 用户提问
* @return SSE流式响应
*/
@GetMapping(value = "/chat/context/stream", produces = "text/event-stream;charset=UTF-8")
public SseEmitter chatContextStream(@RequestParam String sessionId, @RequestParam String prompt) {
SseEmitter emitter = SseUtil.createEmitter();
// 异步处理对话逻辑,不阻塞主线程
new Thread(() -> {
try {
// 1. 获取用户历史上下文对话
List<AiMessage> historyList = aiSessionContext.getHistoryMessage(sessionId);
// 2. 加入当前用户新提问
historyList.add(new AiMessage("user", prompt));
// 3. 计算输入Token(上下文+当前提问总消耗)
String inputAllContent = JSON.toJSONString(historyList);
int inputToken = AiTokenUtil.countToken(inputAllContent);
// 4. 构建大模型请求参数
JSONObject json = new JSONObject();
json.put("model", "deepseek-chat");
json.put("stream", true);
json.put("temperature", 0.7);
json.put("messages", historyList);
// 5. 发起大模型请求
OkHttpClient client = new OkHttpClient();
RequestBody body = RequestBody.create(json.toString(), MediaType.get("application/json; charset=utf-8"));
Request request = new Request.Builder()
.url(AI_URL)
.addHeader("Authorization", "Bearer " + API_KEY)
.post(body)
.build();
Response response = client.newCall(request).execute();
BufferedReader reader = new BufferedReader(new InputStreamReader(response.body().byteStream()));
// 6. 流式接收AI回复,统计输出Token
StringBuilder aiReply = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("data: ") && !line.contains("[DONE]")) {
String data = line.substring(6);
JSONObject resJson = JSON.parseObject(data);
String content = resJson.getJSONArray("choices")
.getJSONObject(0)
.getJSONObject("delta")
.getString("content");
if (content != null) {
aiReply.append(content);
// 逐字推送给前端
SseUtil.send(emitter, content);
}
}
}
// 7. 统计完整Token消耗
int outputToken = AiTokenUtil.countToken(aiReply.toString());
AiTokenBill tokenBill = new AiTokenBill();
tokenBill.setInputToken(inputToken);
tokenBill.setOutputToken(outputToken);
// 8. 更新会话上下文(存入AI回复,实现下一轮记忆)
historyList.add(new AiMessage("assistant", aiReply.toString()));
aiSessionContext.updateSessionMessage(sessionId, historyList);
// 9. 末尾推送计费信息(特殊标识,前端单独解析)
SseUtil.send(emitter, "\n【计费统计】输入Token:" + inputToken + ",输出Token:" + outputToken + ",总Token:" + tokenBill.getTotalToken());
} catch (Exception e) {
SseUtil.send(emitter, "对话异常,请重试!");
} finally {
SseUtil.complete(emitter);
}
}).start();
return emitter;
}
/**
* 清空对话上下文
*/
@GetMapping("/chat/clear")
public String clearChat(@RequestParam String sessionId) {
aiSessionContext.clearSession(sessionId);
return "对话上下文清空成功";
}
}
八、前端测试页面(支持多轮对话)
前端生成唯一sessionId标识会话,支持连续提问、保留对话记忆、展示计费信息:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>AI上下文连续对话+Token计费</title>
</head>
<body>
<div>
<input type="text" id="prompt" placeholder="请输入提问内容" style="width: 500px;height: 40px;">
<button onclick="startChat()" style="height: 40px;">发送提问</button>
<button onclick="clearChat()" style="height: 40px;">清空对话</button>
</div>
<div id="result" style="white-space: pre-wrap;margin-top: 20px;line-height: 1.8;"></div>
<script>
// 生成唯一会话ID(全局唯一,保证对话记忆)
let sessionId = localStorage.getItem("ai_session_id") || UUID.randomUUID();
localStorage.setItem("ai_session_id", sessionId);
let source = null;
// 发起流式对话
function startChat() {
let prompt = document.getElementById("prompt").value;
if (!prompt) return;
document.getElementById("result").innerHTML += "【用户】:" + prompt + "\n【AI】:";
// 关闭上一次连接
if (source) source.close();
// 建立SSE连接
source = new EventSource(`http://localhost:8080/ai/chat/context/stream?sessionId=${sessionId}&prompt=${prompt}`);
source.onmessage = function (e) {
document.getElementById("result").innerHTML += e.data;
}
}
// 清空对话上下文
function clearChat() {
fetch(`http://localhost:8080/ai/chat/clear?sessionId=${sessionId}`)
document.getElementById("result").innerHTML = "";
}
// 生成UUID
function UUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
let r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
}
</script>
</body>
</html>
九、生产级优化:用户额度限流管控
商用产品必须防止恶意刷量、控制AI成本,新增用户每日Token额度限制,极简改造即可实现:
/**
* 每日用户额度校验(简单商用限流)
*/
public boolean checkUserQuota(String userId) {
String quotaKey = "ai:quota:daily:" + userId;
// 每日最大免费Token:10000
int maxQuota = 10000;
Integer userUseToken = Integer.valueOf(stringRedisTemplate.opsForValue().get(quotaKey) == null ? "0" : stringRedisTemplate.opsForValue().get(quotaKey));
if (userUseToken >= maxQuota) {
return false;
}
// 累加已用额度
stringRedisTemplate.opsForValue().increment(quotaKey, tokenBill.getTotalToken());
// 设置每日过期
stringRedisTemplate.expire(quotaKey, 1, TimeUnit.DAYS);
return true;
}
十、生产高频坑点&解决方案
坑1:上下文无限累加,Token越来越大
解决:限制最大历史对话轮数(本文限制10轮),自动截断早期对话,控制Token大小。
坑2:Token统计误差过大
解决:采用中英文差异化换算规则,贴合大模型官方计费标准,商用误差可忽略。
坑3:会话长期占用内存/Redis空间
解决:设置会话30分钟无操作自动过期,自动清理无效会话数据。
坑4:多轮对话顺序混乱
解决:严格按照 user提问 → assistant回复 顺序存入上下文,保证对话时序正确。
坑5:SSE重复连接、内存泄漏
解决:每次新对话关闭上一次SSE连接,finally强制释放资源。
十一、商用拓展功能清单
- 精准计费单价配置:配置输入/输出Token单价,自动计算对话金额
- 用户充值体系:结合用户余额,实现扣费、欠费拦截
- 对话日志入库:保存每轮对话内容、Token消耗、用户信息、耗时
- 敏感词过滤:提问&回复双向风控,合规落地
- 断线重连:网络波动自动重连SSE,接续剩余内容推送
十二、总结
本文在基础SSE流式问答的前提下,完成了AI产品从可用到商用的核心升级,解决了行业两大核心痛点:对话无记忆、无计费统计。
整套方案具备以下商用价值:
- 轻量高效:基于SSE+Redis,无需复杂中间件
- 成本可控:精准Token统计+额度限流,杜绝成本浪费
- 体验优质:多轮连续对话,贴合主流AI产品交互逻辑
- 落地简单:零架构重构,代码直接上线
目前绝大多数付费AI问答、AI客服、企业级AI助手,均采用这套流式对话+上下文记忆+Token计费架构。
更多推荐



所有评论(0)