前言

这是我接单完成的一个AI应用项目。客户需要开发一个类似豆包智能体的AI角色扮演系统,要求使用Java技术栈,支持多个大模型API切换,实现纯文本的角色扮演对话功能。

本文将详细记录整个开发过程,包括需求分析、架构设计、核心功能实现、问题解决等,希望能给同样在学习AI应用开发的朋友一些参考和启发。

项目技术栈SpringBoot 3.2.0 + MySQL 8.0 + 通义千问API + DeepSeek API + WebSocket


一、项目背景与需求分析

1.1 项目来源

这个项目是我接的一个外包单子,客户提供了详细的需求文档(虽然现在已经过期),主要需求是开发一个AI角色扮演系统,类似于豆包智能体的功能。

1.2 核心需求

客户的需求非常明确:

需求类别 具体要求
技术栈 Java + SpringBoot + MySQL
开发范围 纯后端API开发,无需前端界面
AI模型 集成通义千问和DeepSeek两个大模型
测试方式 使用Postman等工具测试,能测通即可

核心功能清单

  • 角色管理(预设苏格拉底和魔法学徒两个角色)
  • 会话管理(支持创建会话并选择模型)
  • 消息交互(REST API + WebSocket双模式)
  • 多种对话风格(沉浸式、学术式、苏格拉底式)
  • 模型选择功能(运行时动态切换)

1.3 客户提供的资源

客户提供了以下关键资源:

# 通义千问API Key
# DeepSeek API Key  

此外还提供了:

  • 预设角色数据(苏格拉底、魔法学徒)
  • 数据库表结构要求
  • 角色人设的YAML格式定义

二、系统架构设计

2.1 技术选型

经过综合考虑,我选择了以下技术栈:

技术分类 选型方案 选择理由
后端框架 Spring Boot 3.2.0 最新稳定版,生态完善
数据库 MySQL 8.0 客户指定,成熟稳定
ORM框架 Spring Data JPA + Hibernate 简化数据库操作
HTTP客户端 WebFlux WebClient 支持异步调用大模型API
实时通信 WebSocket 支持流式对话
构建工具 Maven 依赖管理方便
JDK版本 Java 17 支持新特性如switch表达式

2.2 项目结构

采用经典的分层架构设计:

ai-roleplay/
├── src/main/java/com/example/airoleplay/
│   ├── controller/          # REST API控制器层
│   │   ├── CharacterController.java      # 角色管理接口
│   │   ├── SessionController.java        # 会话管理接口
│   │   └── HealthController.java         # 健康检查接口
│   ├── service/             # 业务逻辑层
│   │   ├── CharacterService.java         # 角色业务逻辑
│   │   ├── SessionService.java           # 会话业务逻辑
│   │   ├── LlmService.java               # LLM服务接口
│   │   ├── TongyiLlmService.java         # 通义千问实现
│   │   ├── DeepSeekLlmService.java       # DeepSeek实现
│   │   └── LlmServiceFactory.java        # 工厂模式选择模型
│   ├── entity/              # 数据库实体层
│   │   ├── Character.java                # 角色实体
│   │   ├── Session.java                  # 会话实体
│   │   ├── Message.java                  # 消息实体
│   │   └── Persona.java                  # 人设实体
│   ├── repository/          # 数据访问层
│   │   ├── CharacterRepository.java
│   │   ├── SessionRepository.java
│   │   └── MessageRepository.java
│   ├── dto/                 # 数据传输对象
│   │   ├── ChatMessage.java
│   │   └── CreateSessionRequest.java
│   ├── config/              # 配置类
│   │   └── WebSocketConfig.java          # WebSocket配置
│   └── websocket/           # WebSocket处理器
│       └── ChatWebSocketHandler.java     # 聊天WebSocket处理
└── schema.sql               # 数据库初始化脚本

2.3 系统架构图

客户端 Postman/WebSocket
Controller层
Service层
Repository层
MySQL数据库
LlmServiceFactory
TongyiLlmService
DeepSeekLlmService
通义千问API
DeepSeek API

三、核心功能实现

3.1 数据库设计

数据库设计是整个系统的基础,我按照客户需求文档设计了完整的表结构。

核心表结构

表名 说明 关键字段
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

关键设计点

  1. UUID主键:使用UUID作为主键,便于分布式扩展

    id VARCHAR(36) PRIMARY KEY DEFAULT (UUID())
    
  2. 对话模式枚举sessions表中的mode字段支持三种模式

    mode ENUM('immersive', 'academic', 'socratic') DEFAULT 'immersive'
    
    • immersive:沉浸式,AI完全进入角色
    • academic:学术式,以学术讨论方式交流
    • socratic:苏格拉底式,通过提问引导思考
  3. 模型选择字段model_name字段记录使用的AI模型

    model_name VARCHAR(50) DEFAULT 'tongyi'
    

预设数据

数据库初始化时插入了两个角色:

INSERT INTO characters (id, name, locale, tags, brief, popularity) VALUES 
('char-socrates', '苏格拉底', 'zh-CN', '["哲学家", "古希腊", "智者"]', 
 '古希腊哲学家,以问答法著称', 100),
('char-wizard', '魔法学徒', 'zh-CN', '["魔法", "学生", "冒险"]', 
 '霍格沃茨的年轻魔法学徒', 85);

3.2 多模型适配器模式

这是项目的核心设计亮点。为了支持多个大模型的灵活切换,我采用了策略模式 + 工厂模式的组合设计。

设计思路

tongyi
deepseek
SessionController
LlmServiceFactory
modelName?
TongyiLlmService
DeepSeekLlmService
LlmService接口

LLM服务接口定义

public interface LlmService {
    String generateResponse(String text);
    String generateResponse(String text, Session session);
    void streamChat(String text, String sessionId, WebSocketSession webSocketSession);
}

工厂类实现模型选择

@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接口,无需修改现有代码
  • 运行时动态选择:根据会话配置动态选择模型,无需重启服务
  • 代码解耦:每个模型的实现互不影响,便于维护
  • 易于测试:可以轻松mock不同的LLM服务进行单元测试

3.3 通义千问API集成

通义千问使用阿里云的DashScope API,这是阿里云提供的大模型服务平台。

核心实现代码

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();
}

踩坑记录

⚠️ 坑点1:通义千问的响应格式有两种

  • 旧版格式:output.text
  • 新版格式:output.choices[0].message.content
  • 解决方案:需要兼容处理两种格式

⚠️ 坑点2:错误处理方式不同

  • 通义千问失败时检查code字段
  • 而不是常见的error字段
// 错误检查代码
if (node.has("code")) {
    throw new RuntimeException("API调用失败: " + node.path("message").asText());
}

3.4 DeepSeek API集成

DeepSeek使用标准的OpenAI兼容接口,实现相对简单,这也是我选择它的原因之一。

核心实现代码

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();
        
    JsonNode node = objectMapper.readTree(response);
    return node.path("choices").get(0)
               .path("message").path("content").asText();
}

API对比

对比项 通义千问 DeepSeek
接口标准 阿里云自定义 OpenAI兼容
请求格式 input.messages messages
响应格式 output.choices choices
流式输出 incremental_output stream
实现难度 中等 简单

3.5 角色扮演Prompt工程

这是项目的另一个核心技术点。为了让AI真正"扮演"角色而不是简单回答问题,我设计了动态Prompt构建逻辑。

Prompt构建策略

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();
}

实际效果示例

场景1:苏格拉底 + socratic模式

用户:什么是正义?
AI:让我们先澄清概念。你认为正义是什么呢?是对所有人一视同仁,还是给予每个人应得的?

场景2:魔法学徒 + immersive模式

用户:请介绍一下魔法学院的生活
AI:这听起来像是一场冒险!在霍格沃茨,每天早上我都要赶去上魔药课,格兰芬多的同学们总是...

场景3:苏格拉底 + academic模式

用户:什么是真理?
AI:从哲学角度来看,真理是符合客观实际的认识。在古希腊哲学传统中,我们认为...

3.6 WebSocket实时通信

为了支持实时流式对话,我实现了WebSocket功能,让用户可以实时看到AI的回复过程。

WebSocket处理器实现

@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());
    
    // 异步调用LLM,避免阻塞WebSocket线程
    new Thread(() -> llmService.streamChat(chatMessage.getText(), sessionId, session)).start();
}

连接方式

# WebSocket连接URL
ws://localhost:8080/ws/chat?sessionId=<会话ID>

# 发送消息格式(JSON)
{
    "text": "你好,请介绍一下你自己"
}

# 接收消息格式(JSON)
{
    "delta": "我是苏格拉底...",
    "done": false
}

技术要点

  1. 异步处理:使用新线程处理LLM调用,避免阻塞WebSocket主线程
  2. 会话验证:连接时验证sessionId的有效性
  3. 错误处理:捕获异常并记录日志
  4. 线程安全:使用synchronized保证消息发送的线程安全

四、完整测试流程

4.1 环境准备

  1. 安装MySQL 8.0,创建数据库
  2. 执行schema.sql初始化数据
  3. 修改application.properties中的数据库连接信息
  4. 运行AiRoleplayApplication.java启动服务

4.2 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"
}

关键操作:复制返回的id字段,这是后续对话需要的sessionId

测试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

返回内容:该会话的所有对话记录

4.3 WebSocket测试

  1. 连接WebSocket:ws://localhost:8080/ws/chat?sessionId={sessionId}
  2. 发送JSON消息:
{
    "text": "这是通过WebSocket发送的消息"
}
  1. 实时接收AI的流式回复

五、开发中遇到的问题与解决

5.1 问题1:AI回复中自报家门

现象:使用通义千问时,AI回复"我是通义千问…",没有进入角色

原因:创建会话时保存了modelName字段到数据库,但调用LLM服务时没有从数据库读取会话信息,导致Prompt没有正确构建

解决方案

// 修改前
String response = llmServiceFactory.getService(session.getModelName())
                                   .generateResponse(text);

// 修改后
String response = llmServiceFactory.getService(session.getModelName())
                                   .generateResponse(text, session); // 传入完整会话对象

5.2 问题2:WebClient依赖缺失

现象:编译时找不到WebClient类

解决方案:在pom.xml中添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

5.3 问题3:UUID生成策略问题

现象:JPA保存实体时ID为null

解决方案:使用Hibernate的UUID生成器

@Id
@GeneratedValue(generator = "uuid2")
@GenericGenerator(name = "uuid2", strategy = "org.hibernate.id.UUIDGenerator")
private String id;

六、项目亮点总结

6.1 技术亮点

  1. 架构设计:采用分层架构 + 策略模式,代码结构清晰,易于维护和扩展
  2. 多模型支持:通过工厂模式实现运行时动态切换AI模型
  3. Prompt工程:根据角色和对话模式动态构建Prompt,实现真实的角色扮演效果
  4. 双通信模式:同时支持REST API和WebSocket,满足不同场景需求
  5. 完整的数据持久化:所有对话记录都保存到数据库,支持历史查询

6.2 技术收获

通过这个项目,我深入学习了:

  • Spring Boot 3.x的新特性和最佳实践
  • 大模型API的调用方式和差异
  • WebSocket在实时通信中的应用
  • 设计模式在实际项目中的运用
  • Prompt工程的基本技巧

七、项目交付

7.1 交付内容

最终交付内容:

  • ✅ 完整的源代码(Maven项目)
  • ✅ 数据库初始化脚本(schema.sql)
  • ✅ 详细的测试文档(Word格式)
  • ✅ API接口说明
  • ✅ 运行环境配置说明

客户通过Postman测试所有接口后,确认功能完全符合需求,项目顺利验收。

7.2 后续优化方向

如果有时间,可以考虑以下优化:

  1. 添加用户认证和权限管理(JWT)
  2. 实现真正的流式输出(SSE或WebSocket分片传输)
  3. 添加对话上下文管理(目前每次调用都是独立的)
  4. 集成更多大模型
  5. 添加敏感词过滤和内容审核
  6. 实现对话摘要和关键词提取

八、相关资源


结语

这是我第一次独立完成AI应用的后端开发,从需求分析到架构设计,从编码实现到测试交付,整个过程让我对Spring Boot和大模型应用有了更深的理解。

项目的核心价值

  • 💡 掌握了多模型集成的设计模式
  • 💡 学会了Prompt工程的实践技巧
  • 💡 积累了真实的项目开发经验
  • 💡 提升了问题分析和解决能力

希望这篇文章能帮助到正在学习AI应用开发的同学们!如果你有任何问题或建议,欢迎私信或者在评论区交流讨论!


关键词SpringBoot AI角色扮演 通义千问 DeepSeek WebSocket 大模型应用 Prompt工程 设计模式

原创不易,如果觉得有帮助,请点赞👍收藏⭐关注💖支持一下!

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐