跳到主要内容 Java 接入 AI 大模型个人实践:多轮对话与流式输出实现 | 极客日志
Java AI java
Java 接入 AI 大模型个人实践:多轮对话与流式输出实现 本文介绍了在 Java 项目中接入通义千问大模型的个人实践,涵盖模型选择、数据库表结构设计、多轮对话与流式输出的代码实现。文章详细展示了如何使用 DashScope SDK 构建 GenerationParam,通过 Spring WebFlux 处理流式响应,以及如何利用 MySQL 存储历史对话上下文。内容包含 Controller、Service、Mapper 层的完整代码逻辑,并补充了 Maven 依赖、配置文件及后续优化建议,为开发者提供了一套可参考的后端集成方案。
佛系玩家 发布于 2025/2/6 更新于 2026/4/21 1 浏览
Java 接入 AI 大模型个人实践:多轮对话与流式输出实现
一、前言 随着 AI 技术的日益成熟,将大模型能力集成到个人项目或企业应用中已成为趋势。本文旨在探讨如何在 Java 项目中接入国产大模型(以通义千问为例),实现基础的多轮对话及流式输出功能,并分享相关的数据库设计与代码实现思路。
二、模型选择与 Demo 测试
1. 模型选择 本着实用与成本可控的原则,本次接入选择通义千问 作为目标模型。接入大模型的通用流程基本一致,核心在于业务场景的设计与上下文管理。
2. 接入准备
开通服务 :在阿里云 DashScope 控制台开通服务并创建 API-KEY。
引入 SDK :在 Maven 项目中引入 dashscope-sdk-java 依赖。
交互模式 :支持单轮、多轮、流式输出等。本实践采用多轮对话 + 流式输出 模式,这是目前市面上最常见的交互方式。
通过 Demo 代码可以看出,要实现多轮对话,需要将历史对话的上下文传递给大模型。观察 GenerationParam 方法可知,只需将连续的对话以 Message 集合的形式传入即可。
private static GenerationParam buildGenerationParam (Message userMsg) {
return GenerationParam.builder()
.model("qwen-turbo" )
.messages(Arrays.asList(userMsg))
.resultFormat(GenerationParam.ResultFormat.MESSAGE)
.topP(0.8 )
.incrementalOutput(true )
.build();
}
3. Message 属性分析 为了设计合理的表结构,需了解 Message 类的关键属性:
public class Message {
String role;
String content;
@SerializedName("tool_calls")
List<ToolCallBase> toolCalls;
@SerializedName("tool_call_id")
String toolCallId;
@SerializedName("name")
String name;
}
role :身份标识。常见类型包括 USER (用户), ASSISTANT (助手), SYSTEM (系统), BOT, ATTACHMENT, TOOL 等。
content :输出的具体内容。
三、流程及表结构设计思考
1. 设计思考
历史存储 :必须将对话历史存储在本地数据库,以便构建上下文。
会话控制 :需要控制会话的生命周期,包括上下文 Token 限制和会话超时时间。
基于以上两点,设计两张数据表:一张用于存储用户提问 (chat_question_history),另一张用于存储模型回复 (chat_answer_history)。新增 session_id 属性用于关联同一时间段内的连续对话。
2. 表结构设计 两表通过 question_id 关联,确保一问一答对应。使用 user_id 和 session_id 区分不同用户的会话历史。
CREATE TABLE `chat_answer_history` (
`id` varchar (50 ) NOT NULL COMMENT '主键 id' ,
`user_id` bigint NOT NULL COMMENT '用户 id' ,
`session_id` varchar (30 ) NOT NULL COMMENT '会话 id' ,
`question_id` bigint NOT NULL COMMENT '问题 id' ,
`answer` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '答案' ,
`message_role` varchar (30 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '角色' ,
`message_tool_calls` varchar (30 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL ,
`message_tool_call_id` varchar (64 ) DEFAULT '' ,
`message_name` varchar (64 ) DEFAULT '' ,
`create_time` datetime DEFAULT NULL COMMENT '创建时间' ,
`create_by` varchar (255 ) DEFAULT NULL COMMENT '创建者' ,
PRIMARY KEY (`id`),
KEY `session_id_index` (`session_id`) USING BTREE,
KEY `question_id_index` (`question_id`) USING BTREE,
KEY `user_id_index` (`user_id`) USING BTREE
) ENGINE= InnoDB DEFAULT CHARSET= utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT= '答案历史对话表' ;
CREATE TABLE `chat_question_history` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键 id' ,
`user_id` bigint NOT NULL COMMENT '用户 id' ,
`session_id` varchar (30 ) NOT NULL COMMENT '会话 id' ,
`question` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '问题' ,
`question_file_name` varchar (30 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '文件名称' ,
`question_file_url` varchar (30 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '文件地址' ,
`model` varchar (30 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '模型类型' ,
`assistant` varchar (30 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '定义模型方向' ,
`create_time` datetime DEFAULT NULL COMMENT '创建时间' ,
`create_by` varchar (255 ) DEFAULT NULL COMMENT '创建者' ,
`message_role` varchar (10 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '角色' ,
PRIMARY KEY (`id`),
KEY `session_id_index` (`session_id`) USING BTREE,
KEY `user_id_index` (`user_id`) USING BTREE
) ENGINE= InnoDB AUTO_INCREMENT= 215 DEFAULT CHARSET= utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT= '提问历史对话表' ;
四、多轮对话 + 流式输出代码实现
1. Maven 依赖配置 确保项目中包含 DashScope SDK 及相关 Web 依赖。
<dependency >
<groupId > com.alibaba.dashscope</groupId >
<artifactId > dashscope-sdk-java</artifactId >
<version > latest</version >
</dependency >
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-webflux</artifactId >
</dependency >
2. 请求接口 Controller 使用 Spring WebFlux 的 Flux 处理流式响应,避免阻塞线程。
import com.alibaba.dashscope.aigc.generation.GenerationResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
@RestController
@RequestMapping("/chat")
public class ChatController {
@Autowired
private IChatQuestionHistoryService questionHistoryService;
@PostMapping(path = "/getChat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<GenerationResult> gptChat (@RequestBody ChatQuestionVo chatQuestionVo) {
return questionHistoryService.gptChat(chatQuestionVo);
}
@GetMapping("/historyList")
public TableDataInfo historyList () {
Long userId = SecurityUtils.getUserId();
List<ChatHistoryVo> list = questionHistoryService.historyList(userId);
list.sort(Comparator.comparing(ChatHistoryVo::getCreateTime));
return getDataTable(list);
}
}
@Data
public class ChatQuestionVo {
private String question;
private String sessionId;
private String model;
private String assistant;
private String questionFileName;
private String questionFileUrl;
}
3. 接口实现 Service Service 层负责调用 SDK、管理会话状态及持久化数据。
@Service
public class ChatQuestionHistoryServiceImpl implements IChatQuestionHistoryService {
@Autowired
private ChatQuestionHistoryMapper chatQuestionHistoryMapper;
@Autowired
private ChatAnswerHistoryMapper chatAnswerHistoryMapper;
@Value("${chat.accessKeyAli}")
private String accessKeyAli;
@Override
public Flux<GenerationResult> gptChat (ChatQuestionVo question) {
StringBuilder contentBuilder = new StringBuilder ();
Constants.apiKey = accessKeyAli;
Generation gen = new Generation ();
Long userId = SecurityUtils.getUserId();
String username = SecurityUtils.getUsername();
if (StringUtils.isEmpty(question.getSessionId())) {
question.setSessionId(String.valueOf(System.currentTimeMillis()));
} else {
long sessionTime = Long.parseLong(question.getSessionId());
if (System.currentTimeMillis() - sessionTime > 5 * 60 * 1000 ) {
question.setSessionId(String.valueOf(System.currentTimeMillis()));
}
}
List<Message> messages = conversationHistory(question.getSessionId());
messages.add(Message.builder().role(Role.USER.getValue()).content(question.getQuestion()).build());
GenerationParam param = buildGenerationParam(messages);
return Flux.create(sink -> {
try {
gen.streamCall(param, new ResultCallback <GenerationResult>() {
@Override
public void onEvent (GenerationResult message) {
String newContent = message.getOutput().getChoices().get(0 ).getMessage().getContent();
contentBuilder.append(newContent);
if ("stop" .equals(message.getOutput().getChoices().get(0 ).getFinishReason())) {
saveHistory(history, answerHistory, userId, username, question, message, contentBuilder);
contentBuilder.setLength(0 );
}
sink.next(message);
}
@Override
public void onError (Exception err) {
sink.error(err);
}
@Override
public void onComplete () {
sink.complete();
}
});
} catch (Exception e) {
sink.error(e);
}
});
}
private void saveHistory (ChatQuestionHistory history, ChatAnswerHistory answerHistory,
Long userId, String username, ChatQuestionVo question,
GenerationResult message, StringBuilder contentBuilder) {
ChatQuestionHistory qHistory = new ChatQuestionHistory ();
BeanUtils.copyProperties(question, qHistory);
qHistory.setUserId(userId);
qHistory.setCreateBy(username);
qHistory.setMessageRole(Role.USER.getValue());
qHistory.setCreateTime(new Date ());
chatQuestionHistoryMapper.insertChatQuestionHistory(qHistory);
ChatAnswerHistory aHistory = new ChatAnswerHistory ();
aHistory.setId(message.getRequestId());
aHistory.setAnswer(contentBuilder.toString());
aHistory.setUserId(userId);
aHistory.setSessionId(question.getSessionId());
aHistory.setQuestionId(qHistory.getId());
aHistory.setMessageRole(message.getOutput().getChoices().get(0 ).getMessage().getRole());
aHistory.setCreateBy(username);
aHistory.setCreateTime(new Date ());
chatAnswerHistoryMapper.insertChatAnswerHistory(aHistory);
}
private GenerationParam buildGenerationParam (List<Message> messages) {
return GenerationParam.builder()
.model(ChatModelConstants.Models.QWEN_MAX)
.messages(messages)
.resultFormat(GenerationParam.ResultFormat.MESSAGE)
.topP(0.8 )
.incrementalOutput(true )
.build();
}
private List<Message> conversationHistory (String sessionId) {
List<Message> messages = new ArrayList <>();
if (StringUtils.isEmpty(sessionId)) {
return messages;
}
messages = chatAnswerHistoryMapper.conversationHistory(SecurityUtils.getUserId(), sessionId);
return messages;
}
@Override
public List<ChatHistoryVo> historyList (Long userId) {
return chatQuestionHistoryMapper.historyList(userId);
}
}
4. Mapper SQL 查询 使用 UNION ALL 合并问题和回答记录,按时间排序取最新 N 条。
<select id ="conversationHistory" resultType ="com.alibaba.dashscope.common.Message" >
SELECT
content,
role
FROM
(
SELECT
question AS content,
message_role AS role,
create_time
FROM
kim_chat_question_history
WHERE
user_id = #{userId}
AND session_id = #{sessionId}
UNION ALL
SELECT
answer AS content,
message_role AS role,
create_time
FROM
kim_chat_answer_history
WHERE
user_id = #{userId}
AND session_id = #{sessionId}
) AS combined_data
ORDER BY
create_time
LIMIT 10
</select >
<select id ="historyList" resultType ="com.kingoffice.system.domain.vo.ChatHistoryVo" >
SELECT
t1.question,
t1.id AS questionId,
t1.create_time AS createTime,
t2.answer,
t2.id AS answerId
FROM
kim_chat_question_history t1
INNER JOIN kim_chat_answer_history t2 ON t1.id = t2.question_id
WHERE
t1.user_id = #{userId}
ORDER BY
t1.create_time DESC
</select >
五、总结与后续优化 至此,后端已实现多轮对话以及流式输出功能。代码中仍有一些待完善的地方,例如对于 Token 数量的精确控制、参数校验、会话时长管理等都是需要考虑的问题。
此外,SpringAI 组件和阿里微服务组件提供了更快速的大模型调用方案,感兴趣的同学可以深入研究。后续计划在前端部分展示如何调用后端流式接口,实现 SSE 实时渲染效果。
配置示例 (application.yml) chat:
accessKeyAli: your_api_key_here
timeout: 30s
注意事项
Token 限制 :实际生产中需计算输入输出 Token 总数,防止超出模型限制。
异常处理 :网络波动或 API 错误时需有重试机制。
安全性 :API Key 应存储在环境变量或配置中心,严禁硬编码。
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
RSA密钥对生成器 生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
Mermaid 预览与可视化编辑 基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online