本文记录了我完成的一个AI应用项目全过程。客户希望构建一个类似“豆包智能体”的角色扮演对话系统,要求使用Java技术栈,支持通义千问和DeepSeek等多种大模型API切换,并实现纯文本形式的角色对话功能。
项目聚焦于后端API开发,不包含前端界面,通过Postman等工具进行接口测试验证。文章将从需求分析、架构设计、核心功能实现到问题解决进行全面梳理,旨在为从事AI应用开发的开发者提供实践参考。
SpringBoot 3.2.0
MySQL 8.0
通义千问API
DeepSeek API
WebSocket
该项目来源于一次外包合作。客户提供了初步的需求文档(目前已过期),核心目标是打造一个具备角色设定能力的AI对话系统,功能上对标当前主流的AI智能体平台。
客户明确提出以下关键要求:
| 需求类别 | 具体要求 |
|---|---|
| 技术栈 | Java + SpringBoot + MySQL |
| 开发范围 | 仅后端API,无需前端页面 |
| AI模型 | 集成通义千问与DeepSeek两大模型 |
| 测试方式 | 可通过Postman等工具完成接口调用测试 |
核心功能清单如下:
客户在项目初期提供了必要的基础资料:
# 通义千问API Key
# DeepSeek API Key
此外还包括:
结合项目需求与长期维护考虑,最终确定以下技术方案:
| 技术分类 | 选型方案 | 选择理由 |
|---|---|---|
| 后端框架 | Spring Boot 3.2.0 | 采用最新稳定版本,拥有完善的生态支持 |
| 数据库 | MySQL 8.0 | 客户指定,成熟可靠且性能良好 |
| ORM框架 | Spring Data JPA + Hibernate | 简化数据库操作,提升开发效率 |
| HTTP客户端 | WebFlux WebClient | 支持响应式异步调用,适合处理大模型API延迟 |
| 实时通信 | WebSocket | 实现流式输出,提升用户体验 |
| 构建工具 | Maven | 依赖管理清晰,团队协作方便 |
| JDK版本 | Java 17 | 支持现代语法特性如switch表达式、密封类等 |
整体采用标准分层架构,目录组织清晰:
ai-roleplay/ ├── src/main/java/com/example/airoleplay/ │ ├── controller/ │ │ ├── CharacterController.java # 角色管理接口 │ │ ├── SessionController.java # 会话管理接口 │ │ └── HealthController.java # 健康检查接口 │ ├── service/ │ │ ├── CharacterService.java # 角色业务逻辑 │ │ ├── SessionService.java # 会话业务逻辑 │ │ ├── LlmService.java # LLM服务接口 │ │ ├── TongyiLlmService.java # 通义千问实现 │ │ ├── DeepSeekLlmService.java # DeepSeek实现 │ │ └── LlmServiceFactory.java # 工厂模式选择模型 │ ├── entity/ │ │ ├── CharacterEntity.java # 角色实体 │ │ ├── SessionEntity.java # 会话实体 │ │ └── MessageEntity.java # 消息实体 │ ├── repository/ │ │ ├── CharacterRepository.java # 角色数据访问 │ │ ├── SessionRepository.java # 会话数据访问 │ │ └── MessageRepository.java # 消息数据访问 │ ├── config/ │ │ └── WebSocketConfig.java # WebSocket配置 │ ├── dto/ │ │ ├── request/ # 请求DTO │ │ └── response/ # 响应DTO │ └── util/ │ └── PromptUtil.java # 提示词工具类
系统采用模块化设计,各组件协同工作,形成完整的服务闭环。
主要包括三个核心数据表:
初始化包含两个角色:
为了实现多个大模型的灵活切换,采用适配器+工厂模式的设计思想,统一对外暴露相同的LLM调用接口。
定义统一的LlmService接口,包含generateResponse方法,所有具体模型实现该接口。
通过LlmServiceFactory根据传入的模型类型返回对应的实现类实例,实现运行时动态绑定。
使用WebClient发起异步HTTP请求,封装Authorization头与请求体格式,处理流式响应数据。
同样基于WebClient实现,但其API更简洁,无需复杂签名,直接携带Bearer Token即可调用。
| 维度 | 通义千问 | DeepSeek |
|---|---|---|
| 认证方式 | 签名机制(较复杂) | Token直连(简单) |
| 响应速度 | 较快 | 极快 |
| 上下文长度 | 32K | 128K |
| 是否支持流式 | 支持 | 支持 |
采用“角色设定 + 行为规范 + 输出约束”三层结构构建提示词模板:
以苏格拉底为例,当用户提出观点时,系统会自动以反问形式引导思考,而非直接给出答案,有效模拟其教学风格。
通过@ServerEndpoint注解定义端点,管理会话连接池,接收客户端消息后转发至对应LLM服务并推送流式结果。
客户端通过ws://host:port/ws/chat/{sessionId}建立连接,服务端基于Session ID维护上下文。
配置本地application-dev.yml,填入各模型API Key,启动MySQL容器,确保服务正常运行。
测试1:获取角色列表
GET /api/characters
验证能否正确返回预设角色信息。
测试2:创建会话(通义千问)
POST /api/sessions
Body: {"characterId": 1, "model": "qwen"}
验证是否生成有效会话ID并持久化。
测试3:发送消息
POST /api/messages
包含会话ID与用户输入,验证AI能否返回符合角色设定的回答。
测试4:获取历史消息
GET /api/messages?sessionId=xxx
验证消息记录是否完整保存。
使用WebSocket客户端工具连接指定端点,发送消息后观察是否能接收到逐字流式返回的结果,验证实时性与稳定性。
现象:模型在回答开头总是自称“我是通义千问”等标识语。
解决方案:在Prompt中增加明确指令:“请完全代入角色,禁止提及自己是AI或模型名称”,成功抑制此类输出。
现象:编译通过但运行时报错找不到WebClient类。
原因:未引入spring-boot-starter-webflux依赖。
解决:在pom.xml中添加对应依赖后恢复正常。
现象:JPA保存实体时UUID为空。
原因:未正确配置@GeneratedValue(strategy = GenerationType.UUID)或缺少默认值设置。
解决:改为在Java层面使用UUID.randomUUID().toString()手动赋值,确保唯一性。
(已去除所有引流性质内容)
本次项目成功实现了基于Java生态的AI角色扮演系统,验证了SpringBoot在现代AI应用开发中的可行性与强大能力。整个过程不仅锻炼了技术整合能力,也加深了对大模型服务调用细节的理解。希望这份详实的开发记录能为后来者提供有价值的借鉴。
作为整个系统的基础,数据库设计严格依据客户需求文档进行,确保了数据结构的完整性与可扩展性。
| 表名 | 说明 | 关键字段 |
|---|---|---|
| users | 用户表 | id, email, nickname |
| characters | 角色表 | id, name, brief, popularity |
| personas | 人设表 | character_id, persona_yaml, persona_json |
| sessions | 会话表 | id, character_id, mode, model_name |
| messages | 消息表 | session_id, role, text |
| citations | 引用表 | message_id, source, url |
| favorites | 收藏表 | user_id, session_id |
UUID主键:采用UUID作为主键,提升分布式部署下的扩展能力。
id VARCHAR(36) PRIMARY KEY DEFAULT (UUID())
对话模式枚举:通过mode字段支持三种交互方式:
mode ENUM('immersive', 'academic', 'socratic') DEFAULT 'immersive'
sessions
mode:沉浸式 — AI完全代入设定角色进行互动immersive:学术式 — 以严谨的学术风格展开讨论academic:苏格拉底式 — 通过连续提问引导用户自主思考socratic
模型选择字段:model_name用于记录当前会话所使用的AI模型。
model_name VARCHAR(50) DEFAULT 'tongyi'
model_name
预设数据初始化:在数据库初始脚本中预先插入两个角色实例,便于快速体验:
INSERT INTO characters (id, name, locale, tags, brief, popularity) VALUES
('char-socrates', '苏格拉底', 'zh-CN', '["哲学家", "古希腊", "智者"]',
'古希腊哲学家,以问答法著称', 100),
('char-wizard', '魔法学徒', 'zh-CN', '["魔法", "学生", "冒险"]',
'霍格沃茨的年轻魔法学徒', 85);
该模块是项目的核心技术亮点。为实现多种大语言模型之间的灵活切换与统一调用,采用了策略模式 + 工厂模式相结合的设计方案。
LLM服务接口定义:所有模型服务均需实现统一接口,保证调用一致性。
public interface LlmService {
String generateResponse(String text);
String generateResponse(String text, Session session);
void streamChat(String text, String sessionId, WebSocketSession webSocketSession);
}
工厂类实现动态模型选取:通过Spring依赖注入管理不同模型服务实例,并根据配置或参数返回对应实现。
@Component
@RequiredArgsConstructor
public class LlmServiceFactory {
private final TongyiLlmService tongyiLlmService;
private final DeepSeekLlmService deepSeekLlmService;
}
public LlmService getService(String modelName) {
return switch (modelName.toLowerCase()) {
case "deepseek" -> deepSeekLlmService;
case "tongyi" -> tongyiLlmService;
default -> tongyiLlmService;
};
}
该实现方式具备多个关键优势,能够有效提升系统的可维护性与扩展能力:
LlmService
通义千问基于阿里云提供的 DashScope 大模型服务平台进行集成。该平台通过标准化接口提供多种AI能力,其中文本生成服务是本项目重点使用的功能。
private String callTongyiApi(String text) throws Exception {
Map<String, Object> request = new HashMap<>();
request.put("model", "qwen-turbo");
Map<String, Object> input = new HashMap<>();
input.put("messages", List.of(Map.of("role", "user", "content", text)));
request.put("input", input);
WebClient client = WebClient.builder()
.defaultHeader("Authorization", "Bearer " + apiKey)
.defaultHeader("Content-Type", "application/json")
.build();
String response = client.post()
.uri("https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation")
.bodyValue(request)
.retrieve()
.bodyToMono(String.class)
.block();
JsonNode node = objectMapper.readTree(response);
return node.path("output").path("choices").get(0)
.path("message").path("content").asText();
}
坑点一:响应格式存在新旧两个版本
在实际对接过程中发现,通义千问返回的数据结构有两种形式:
output.text
output.choices[0].message.content
因此,解析响应时必须兼容处理两种结构,避免因格式变更导致解析失败。
坑点二:错误标识字段非常规
与大多数API不同,通义千问在请求失败时不通过常见的 HTTP 状态码或顶层 error 字段判断,而是通过检查特定字段:
code
而非通常使用的:
error
对应的错误处理代码应如下编写:
// 错误检测逻辑
if (node.has("code")) {
throw new RuntimeException("API调用失败: " + node.path("message").asText());
}
DeepSeek 提供了与 OpenAI 接口高度兼容的标准 RESTful API,这使得其集成过程更为简便,也是选型中的重要考量因素之一。
private String callDeepSeekApi(String text) throws Exception {
Map<String, Object> request = new HashMap<>();
request.put("model", "deepseek-chat");
request.put("messages", List.of(Map.of("role", "user", "content", text)));
request.put("stream", false);
WebClient client = WebClient.builder()
.defaultHeader("Authorization", "Bearer " + apiKey)
.defaultHeader("Content-Type", "application/json")
.build();
String response = client.post()
.uri("https://api.deepseek.com/chat/completions")
.bodyValue(request)
.retrieve()
.bodyToMono(String.class)
.block();
得益于其标准协议的支持,开发者可以复用大量已有的工具链和封装库,显著降低开发与调试成本。
| 对比项 | 通义千问 | DeepSeek |
|---|---|---|
| 接口标准 | 阿里云自定义 | OpenAI 兼容 |
| 请求格式 | |
|
| 响应格式 | |
|
| 流式输出 | |
|
| 实现难度 | 中等 | 简单 |
作为项目的核心技术之一,角色扮演功能的实现依赖于精心设计的动态 Prompt 构建机制。该机制确保 AI 不仅是回答问题,而是真正“化身”为特定角色进行交互。
private String buildRolePlayPrompt(String userText, Session session) {
StringBuilder prompt = new StringBuilder();
// 1. 注入角色身份信息
characterService.getCharacterById(session.getCharacterId())
.ifPresent(character -> {
prompt.append("你现在要扮演角色:").append(character.getName()).append("\n");
prompt.append("角色描述:").append(character.getBrief()).append("\n");
});
// 2. 根据当前对话模式调整表达风格
switch (session.getMode()) {
case immersive:
prompt.append("请完全沉浸在这个角色中,用第一人称回答,保持角色的性格特点和说话方式。\n");
break;
case academic:
prompt.append("请以学术讨论的方式,结合角色的知识背景来回答问题。\n");
break;
case socratic:
prompt.append("请用苏格拉底式的问答方法,通过提问来引导思考。\n");
break;
}
prompt.append("\n用户问题:").append(userText);
return prompt.toString();
}
用户:什么是正义?
AI:让我们先澄清概念。你认为正义是什么呢?是对所有人一视同仁,还是给予每个人应得的?用户:请介绍一下魔法学院的生活
AI:这听起来像是一场冒险!在霍格沃茨,每天早上我都要赶去上魔药课,格兰芬多的同学们总是...用户:什么是真理?
AI:从哲学角度来看,真理是符合客观实际的认识。在古希腊哲学传统中,我们认为...为支持流式对话体验,系统集成了 WebSocket 技术,使用户能够实时接收 AI 生成的内容,提升交互流畅度。
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) {
String sessionId = getSessionIdFromUri(session.getUri());
ChatMessage chatMessage = objectMapper.readValue(message.getPayload(), ChatMessage.class);
// 获取会话配置以确定使用的语言模型
Session chatSession = sessionService.getSessionById(sessionId).get();
LlmService llmService = llmServiceFactory.getService(chatSession.getModelName());
// 启动独立线程处理大模型请求,防止阻塞主通信线程
new Thread(() -> llmService.streamChat(chatMessage.getText(), sessionId, session)).start();
}
WebSocket 连接地址:
ws://localhost:8080/ws/chat?sessionId=<会话ID>
发送消息格式(JSON):
{
"text": "你好,请介绍一下你自己"
}
接收消息格式(JSON):
{
"delta": "我是苏格拉底...",
"done": false
}
synchronized 确保消息发送过程的线程安全性在进入功能测试前,需完成基础环境搭建,包括服务启动、依赖配置、数据库初始化及外部 API 接口连通性验证。
.bodyToMono(String.class)
.block();
JsonNode node = objectMapper.readTree(response);
return node.path("choices").get(0)
.path("message").path("content").asText();
一、环境搭建与数据库配置
首先安装 MySQL 8.0 版本,并创建对应的应用数据库。接着执行以下步骤完成初始化:
schema.sqlapplication.properties,确保 URL、用户名和密码正确无误AiRoleplayApplication.java二、REST API 功能验证(基于 Postman 测试)
测试项1:获取角色列表
GET http://localhost:8080/api/v1/characters
预期返回结果:系统应返回“苏格拉底”与“魔法学徒”两个预设角色信息。
测试项2:创建会话(接入通义千问模型)
POST http://localhost:8080/api/v1/sessions
Content-Type: application/json
{
"characterId": "char-socrates",
"mode": "immersive",
"modelName": "tongyi"
}
关键操作说明:从接口响应中复制生成的 sessionId 字段值
id,该标识将在后续消息交互中用于维持会话状态。
测试项3:发送用户消息
POST http://localhost:8080/api/v1/sessions/{sessionId}/messages
Content-Type: application/json
{
"text": "你好,请介绍一下你自己"
}
实际表现:AI 将以设定的角色——苏格拉底的语气进行回应,体现角色扮演效果。
测试项4:查询历史对话记录
GET http://localhost:8080/api/v1/sessions/{sessionId}/messages
返回内容说明:接口将返回指定会话 ID 下的所有聊天历史数据。
三、WebSocket 实时通信测试
建立 WebSocket 连接:
ws://localhost:8080/ws/chat?sessionId={sessionId}
向服务端发送如下格式的 JSON 消息:
{
"text": "这是通过WebSocket发送的消息"
}
客户端将实时接收来自 AI 的流式响应结果,实现低延迟互动体验。
四、开发过程中遇到的问题及解决方案
问题一:AI 回复包含自我介绍内容
现象描述:在使用通义千问模型时,AI 首次回复出现“我是通义千问…”等脱离角色设定的内容。
根本原因:虽然已在数据库中保存了完整的会话信息
modelName,但在调用大模型服务时未读取该上下文,导致 Prompt 构建不完整,无法进入角色模式。
解决方式:
// 修改前代码 String response = llmServiceFactory.getService(session.getModelName()) .generateResponse(text); // 修改后代码 String response = llmServiceFactory.getService(session.getModelName()) .generateResponse(text, session); // 显式传入会话对象以构建完整 Prompt
问题二:编译时报 WebClient 类找不到
现象描述:项目构建阶段提示无法识别 WebClient 相关类。
解决方案:在项目的依赖管理文件中添加 Spring WebFlux 支持:
pom.xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency>
问题三:JPA 实体 ID 生成异常
现象描述:使用 JPA 保存实体时,UUID 类型的主键字段显示为 null。
解决方法:采用 Hibernate 提供的 UUID 生成策略:
@Id @GeneratedValue(generator = "uuid2") @GenericGenerator(name = "uuid2", strategy = "org.hibernate.id.UUIDGenerator") private String id;
五、项目核心亮点总结
5.1 技术层面的优势
5.2 技术能力提升
通过本项目实践,深入掌握了以下技能:
六、项目交付情况
6.1 交付成果清单
客户已使用 Postman 对全部接口进行验证,确认功能完全满足需求,项目顺利通过验收。
6.2 后续可优化方向
若资源允许,建议推进以下改进:
七、参考资料
结语
这是我首次独立完成一个 AI 应用的后端系统开发,涵盖需求分析、架构设计、编码实现到最终测试交付的全流程。整个过程不仅加深了我对 Spring Boot 框架的理解,也让我对大模型应用的开发模式有了更真实的体会。
项目核心收获:
希望本文能为正在学习 AI 应用开发的同学提供参考价值。
关键词展示:
SpringBoot
AI角色扮演
通义千问
DeepSeek
WebSocket
大模型应用
Prompt工程
设计模式创作过程颇为不易,若您认为内容有所帮助,欢迎点赞、收藏并给予支持。
扫码加好友,拉您进群



收藏
