业务梗概
基于spring-ai的对接deepseek对话
目前带有历史对话默认是带有10次的记忆。
业务暂时规定每天可请求无记忆的ai50次。
有记忆的ai20次。
接口
GET 流式聊天API
GET /ai/deepseek/stream/chat
使用Server-Sent Events (SSE) 实现实时流式响应
请求参数
名称 | 位置 | 类型 | 必选 | 说明 |
---|---|---|---|---|
message | query | string | 是 | 用户消息 |
返回示例
GET 流式聊天API(JSON格式)
GET /ai/deepseek/stream/chat-json
返回JSON格式的流式数据
请求参数
名称 | 位置 | 类型 | 必选 | 说明 |
---|---|---|---|---|
message | query | string | 是 | 用户消息 |
返回示例
GET streamChatMemory 有历史对话
GET /ai/deepseek/stream/chat-memory
请求参数
名称 | 位置 | 类型 | 必选 | 说明 |
---|---|---|---|---|
message | query | string | 是 | none |
返回示例
200 Response
技术详解
采用了redis跟spring-ai的deepseek
xml
<!-- Redis 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-deepseek</artifactId>
</dependency>
使用起来只需要配置apikey
yml
spring:
ai:
deepseek:
api-key: xx
首先先看流聊天的
java
/**
* 流式聊天API
* 使用Server-Sent Events (SSE) 实现实时流式响应
*
* @param message 用户消息
* @return 流式响应
*/
@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChat(@RequestParam String message) {
log.info("收到流式聊天请求:{}", message);
return smartGeneratorService.streamChat(message)
.filter(chunk -> chunk != null && !chunk.trim().isEmpty()) // 过滤空内容
.doOnNext(chunk -> log.debug("原始数据块: '{}'", chunk))
.map(chunk -> chunk.trim()) // 只清理空白字符
.filter(chunk -> !chunk.isEmpty()) // 再次过滤空内容
.concatWith(Flux.just("[DONE]"))
.doOnSubscribe(subscription -> log.info("开始流式响应"))
.doOnComplete(() -> log.info("流式响应完成"))
.doOnError(error -> log.error("流式响应出错", error))
.onErrorReturn("[ERROR] 流式响应出现错误");
}
这里用到了[DONE]表示结束。
在看具体的service核心代码就是
设置聊天模型参数(如温度、最大长度等)
java
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
.temperature(0.9)
.maxTokens(800)
.build();
调用底层大模型的 stream 方法
java
return chatModel.stream(new Prompt(prompt.getInstructions(), options))
.map(response -> response.getResult().getOutput().getText());
这个 chatModel.stream(...)
就是关键点 —— 它返回一个 FluxString,每个 String
是大模型逐步生成的一小段回复(chunk)。
那么带有记忆的是如何实现的呢?
java
@GetMapping(value = "/chat-memory", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChatMemory(@RequestParam String message) {
Long userId = LoginHelper.getCurrentAppUserId();
return smartGeneratorService.streamChatWithMemory(userId.toString(), message)
.concatWith(Flux.just("[DONE]"));
}
我们用户了一个唯一标识,就是用户的id
具体的service中。
1. 获取聊天历史
java
ReactiveListOperations<String, String> listOps = redisOps.opsForList();
String redisKey = "chat:history:" + sessionId;
- 根据
sessionId
获取 Redis 中对应会话的历史记录。 - Redis 列表结构类似队列,适合按顺序存储聊天记录。
java
listOps.range(redisKey, 0, MAX_HISTORY - 1)
- 获取最近的 N 条历史记录。
2. 拼接 Prompt
java
StringBuilder sb = new StringBuilder();
sb.append("你是一位友好、有帮助的AI助手。\n");
for (String past : historyList) {
sb.append("用户:").append(past).append("\n");
}
sb.append("用户:").append(message);
- 把之前的对话和当前消息拼接成一个 Prompt。
- 这样模型就能“看到”上下文。
注意:这里只拼接了“用户”的话,假设之前的 AI 回复没有存进去(也可以扩展成存
用户-助手
对儿)。
3. 聊天后更新历史
java
responseFlux.concatWith(Mono.defer(() -> {
return listOps.leftPush(redisKey, message)
.flatMap(len -> listOps.trim(redisKey, 0, MAX_HISTORY - 1))
.then(Mono.empty());
}));
- 在流式响应 完成后:
- 将当前用户输入
message
存入 Redis 的头部。 - 再通过
trim()
保留最近的 N 条,裁剪老数据。
- 将当前用户输入
redis的结构就是这样的。