FUST 与 DeepSeek:构建智能问答系统的实践探索

近年来,随着大型语言模型的快速发展,智能问答系统已经成为企业提升用户体验、降低客服成本的重要工具。本文将介绍如何利用知乎开源的FUST微服务框架和DeepSeek API,搭建一个高性能、可扩展的智能问答系统。

FUST 谐音 Fast,是知乎开发的一款基于Spring Boot的微服务开发框架,旨在帮助开发者快速构建高质量的微服务应用。它集成了主流组件,提供标准化的开发范式,并在知乎的大规模生产环境经受了考验。

我们将通过实现一个名为”Deep QA”的项目,展示如何将FUST框架与DeepSeek API结合,构建一个功能完善的智能问答系统。

二、技术栈概述

2.1 FUST框架

FUST是基于Spring Boot 3.x开发的微服务框架,具有以下优势:

  • 开箱即用:集成主流组件,提供标准化的开发范式
  • 高性能:基于高性能的Spring Boot 3.x和Armeria构建
  • 可观测性:内置完整的监控和追踪方案
  • 灵活扩展:提供丰富的扩展点,满足各种定制需求
  • 生产验证:在知乎大规模生产环境经受考验

2.2 DeepSeek API

DeepSeek提供了一系列强大的语言模型API,包括:

  • deepseek-chat:通用对话模型
  • deepseek-coder:代码生成和理解模型

三、项目架构

Deep QA采用经典的多模块微服务架构,主要包含以下两个核心模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
deep-qa/
├── deep-qa-api/ # HTTP API模块
│ └── src/
│ ├── main/java/com/deepqa/api/
│ │ ├── config/ # 配置类
│ │ ├── controller/ # 控制器
│ │ └── dto/ # 数据传输对象
│ └── main/resources/
│ ├── static/ # 静态资源
│ ├── templates/ # Thymeleaf模板
│ └── application.properties # 应用配置
├── deep-qa-business/ # 业务逻辑模块
│ └── src/
│ └── main/java/com/deepqa/business/
│ ├── client/ # 外部服务客户端
│ ├── dao/ # 数据访问对象
│ ├── model/ # 数据模型
│ └── service/ # 业务服务
│ └── impl/ # 服务实现
└── proto/ # Protocol Buffers定义
├── buf.yaml # Buf模块配置
└── hello.proto # 示例服务Proto定义

这种模块化设计有以下优势:

  1. 关注点分离:API层负责处理HTTP请求和响应,业务层负责核心业务逻辑
  2. 代码复用:业务逻辑可以被不同的接口层(HTTP、gRPC)复用
  3. 便于测试:各层可以独立测试
  4. 维护简单:模块边界清晰,降低了代码耦合度

四、环境准备

在开始项目开发前,需要准备以下环境:

  1. JDK 17+
  2. Maven 3.8+
  3. MySQL 5.7+
  4. Redis 6.0+
  5. DeepSeek API密钥

由于FUST框架尚未发布到Maven中央仓库,需要先将其发布到本地Maven仓库:

1
2
3
4
5
6
# 克隆FUST仓库
git clone https://github.com/zhihu/fust.git
cd fust

# 发布到本地Maven仓库
./gradlew publishToMavenLocal

五、项目实现

5.1 项目配置

首先,我们来看父项目的pom.xml配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.zhihu.fust</groupId>
<artifactId>fust-boot-bom</artifactId>
<version>0.1.0</version>
</parent>
<artifactId>deep-qa</artifactId>
<groupId>com.deepqa</groupId>
<packaging>pom</packaging>
<version>1.0</version>
<!-- 依赖配置... -->
<modules>
<module>deep-qa-business</module>
<module>deep-qa-api</module>
</modules>
</project>

注意这里使用了FUST框架的fust-boot-bom作为父项目,这样就可以统一管理FUST相关依赖的版本。

5.2 数据模型设计

deep-qa-business模块中,我们定义了以下主要数据模型:

5.2.1 聊天历史模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@Data
@Table(name = TABLE_NAME)
public class ChatHistoryModel {
public static final String TABLE_NAME = "t_chat_history";
/**
* 主键ID
*/
@Id
private Long id;

/**
* 创建时间
*/
@DbAutoColumn
private LocalDateTime createdAt;

/**
* 更新时间
*/
@DbAutoColumn
private LocalDateTime updatedAt;

/**
* 用户ID
*/
private Long userId;

/**
* 会话ID
*/
private String sessionId;

/**
* 角色(user/assistant)
*/
private String role;

/**
* 消息内容
*/
private String content;

/**
* 是否包含图片
*/
private Boolean hasImage;

/**
* 图片URL
*/
private String imageUrl;
}

这里我们使用了@DbAutoColumn注解来标记自动填充的字段,FUST框架会自动处理这些字段的创建和更新。

5.3 数据访问层

数据访问层使用MyBatis实现,FUST框架提供了TemplateDao接口,简化了常见的CRUD操作。以ChatHistoryDao为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Mapper
public interface ChatHistoryDao extends TemplateDao<ChatHistoryModel> {

/**
* 通过用户ID和会话ID查询对话记录
*/
@Select({"SELECT * FROM ", ChatHistoryModel.TABLE_NAME,
" WHERE user_id = #{userId} AND session_id = #{sessionId} ORDER BY created_at ASC"})
@ResultMap("ChatHistoryModel")
List<ChatHistoryModel> findByUserIdAndSessionId(@Param("userId") Long userId, @Param("sessionId") String sessionId);

/**
* 删除会话记录
*/
@Delete({"DELETE FROM ", ChatHistoryModel.TABLE_NAME, " WHERE user_id = #{userId} AND session_id = #{sessionId}"})
int deleteByUserIdAndSessionId(@Param("userId") Long userId, @Param("sessionId") String sessionId);

/**
* 按创建时间获取用户最近的会话列表(分组查询不同会话)
*/
@Select({"SELECT DISTINCT session_id FROM ", ChatHistoryModel.TABLE_NAME,
" WHERE user_id = #{userId} ORDER BY MAX(created_at) DESC LIMIT #{limit}"})
List<String> findRecentSessionsByUserId(@Param("userId") Long userId, @Param("limit") int limit);
}

通过继承TemplateDao,我们自动获得了常见的CRUD操作,同时还可以根据业务需求自定义其他查询方法。

5.4 业务服务层

业务服务层是应用的核心,负责实现业务逻辑。以ChatService接口及其实现类为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public interface ChatService {
/**
* 发送消息
*/
ChatHistoryModel sendMessage(Long userId, String sessionId, String message);

/**
* 获取聊天历史
*/
List<ChatHistoryModel> getChatHistory(Long userId, String sessionId);

/**
* 获取用户会话列表
*/
List<String> getUserSessions(Long userId, int limit);

/**
* 创建新会话
*/
String createNewSession(Long userId);

/**
* 删除会话
*/
boolean deleteSession(Long userId, String sessionId);
}

实现类ChatServiceImpl负责具体的业务逻辑实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@Service
@Slf4j
public class ChatServiceImpl implements ChatService {

@Autowired
private ChatHistoryDao chatHistoryDao;

@Autowired
private ConfigService configService;

@Autowired
private DeepSeekClient deepSeekClient;

@Override
public ChatHistoryModel sendMessage(Long userId, String sessionId, String message) {
// 保存用户消息
ChatHistoryModel userMessage = new ChatHistoryModel();
userMessage.setUserId(userId);
userMessage.setSessionId(sessionId);
userMessage.setRole("user");
userMessage.setContent(message);
userMessage.setHasImage(false);
chatHistoryDao.create(userMessage);

// 获取用户DeepSeek配置
ConfigModel config = configService.getUserConfig(userId);

try {
// 获取历史对话记录用于上下文
List<ChatHistoryModel> historyList = chatHistoryDao.findByUserIdAndSessionId(userId, sessionId);

// 将历史转换为DeepSeek API需要的格式 (限制最近10条消息)
List<Map<String, String>> conversationHistory = new java.util.ArrayList<>();
int startIndex = Math.max(0, historyList.size() - 10);

for (int i = startIndex; i < historyList.size(); i++) {
ChatHistoryModel historyItem = historyList.get(i);
if (!"user".equals(historyItem.getRole()) && !"assistant".equals(historyItem.getRole())) {
continue;
}

Map<String, String> messageMap = new java.util.HashMap<>();
messageMap.put("role", historyItem.getRole());
messageMap.put("content", historyItem.getContent());
conversationHistory.add(messageMap);
}

// 调用DeepSeek API获取回复
String aiReply = deepSeekClient.generateResponse(config, message, conversationHistory);

// 保存AI回复
ChatHistoryModel aiMessage = new ChatHistoryModel();
aiMessage.setUserId(userId);
aiMessage.setSessionId(sessionId);
aiMessage.setRole("assistant");
aiMessage.setContent(aiReply);
aiMessage.setHasImage(false);
chatHistoryDao.create(aiMessage);

return aiMessage;
} catch (Exception e) {
log.error("Failed to get response from DeepSeek API", e);

// 保存错误消息
ChatHistoryModel errorMessage = new ChatHistoryModel();
errorMessage.setUserId(userId);
errorMessage.setSessionId(sessionId);
errorMessage.setRole("assistant");
errorMessage.setContent("抱歉,AI助手暂时无法回复。错误信息: " + e.getMessage());
errorMessage.setHasImage(false);
chatHistoryDao.create(errorMessage);

return errorMessage;
}
}

// 其他方法实现...
}

在这个实现中,我们可以看到:

  1. 保存用户消息到数据库
  2. 获取历史对话作为上下文
  3. 调用DeepSeek API获取AI回复
  4. 保存AI回复到数据库
  5. 异常处理机制

5.5 DeepSeek API集成

我们通过DeepSeekClient类与DeepSeek API进行交互:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@Component
public class DeepSeekClient {

private static final Logger LOGGER = LoggerFactory.getLogger(DeepSeekClient.class);

/**
* 发送消息到DeepSeek API获取回复
*/
public String generateResponse(ConfigModel config, String prompt, List<Map<String, String>> history) {
try {
DeepSeekApi deepSeekApi = new DeepSeekApi(config.getServerUrl(), config.getApiKey());

// 构建选项
Map<String, Object> options = new HashMap<>();
options.put("temperature", config.getTemperature());
options.put("top_p", config.getTopP());
options.put("context_window", config.getContextSize());

return deepSeekApi.generateCompletion(config.getModelName(), prompt, history, options);
} catch (Exception e) {
LOGGER.error("Error calling DeepSeek API", e);
return "调用AI服务出错: " + e.getMessage();
}
}

/**
* 获取可用模型列表
*/
public List<String> getAvailableModels(String serverUrl, String apiKey) {
try {
DeepSeekApi deepSeekApi = new DeepSeekApi(serverUrl, apiKey);
List<DeepSeekApi.Model> models = deepSeekApi.listModels();

return models.stream()
.map(DeepSeekApi.Model::getName)
.collect(Collectors.toList());
} catch (Exception e) {
LOGGER.error("Error fetching DeepSeek models", e);
return List.of("deepseek-chat", "deepseek-coder", "deepseek-lite");
}
}

/**
* 测试与DeepSeek API的连接
*/
public boolean testConnection(String serverUrl, String apiKey) {
try {
DeepSeekApi deepSeekApi = new DeepSeekApi(serverUrl, apiKey);
return deepSeekApi.testConnection();
} catch (Exception e) {
LOGGER.error("Failed to connect to DeepSeek API: {}", serverUrl, e);
return false;
}
}
}

这个客户端类提供了三个主要功能:生成回复、获取可用模型列表和测试连接。

5.6 Web控制器层

Web控制器层负责处理HTTP请求和响应,项目中主要有三个控制器:ChatControllerConfigControllerHomeController。以ChatController为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
@RestController
@RequestMapping("/api/chat")
public class ChatController {

private static final int MAX_SESSIONS_COUNT = 10;
private static final int SESSION_TITLE_LENGTH = 8;

private final ChatService chatService;

@Autowired
public ChatController(ChatService chatService) {
this.chatService = chatService;
}

/**
* 发送消息
*/
@PostMapping("/send")
public ResponseDTO<ChatMessageDTO> sendMessage(@RequestBody SendMessageRequestDTO request) {
// 简单验证
if (request.getMessage() == null || request.getMessage().trim().isEmpty()) {
return ResponseDTO.error("消息内容不能为空");
}

// 模拟用户认证 (实际应从会话或令牌中获取用户ID)
Long userId = 1L;

// 如果没有会话ID,创建新会话
String sessionId = request.getSessionId();
if (sessionId == null || sessionId.trim().isEmpty()) {
sessionId = chatService.createNewSession(userId);
}

// 发送消息并获取回复
ChatHistoryModel response = chatService.sendMessage(userId, sessionId, request.getMessage());

// 转换为DTO
ChatMessageDTO messageDTO = convertToDTO(response);

return ResponseDTO.success(messageDTO);
}

/**
* 获取会话历史记录
*/
@GetMapping("/history/{sessionId}")
public ResponseDTO<List<ChatMessageDTO>> getChatHistory(@PathVariable String sessionId) {
// 模拟用户认证
Long userId = 1L;

List<ChatHistoryModel> history = chatService.getChatHistory(userId, sessionId);
List<ChatMessageDTO> historyDTO = history.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());

return ResponseDTO.success(historyDTO);
}

// 其他API方法...

/**
* 将ChatHistoryModel转换为ChatMessageDTO
*/
private ChatMessageDTO convertToDTO(ChatHistoryModel model) {
ChatMessageDTO dto = new ChatMessageDTO();
dto.setId(model.getId().toString());
dto.setRole(model.getRole());
dto.setContent(model.getContent());

if (model.getCreatedAt() != null) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
dto.setTime(model.getCreatedAt().format(formatter));
}

if (model.getHasImage() && model.getImageUrl() != null) {
dto.setHasImage(true);
dto.setImageUrl(model.getImageUrl());
} else {
dto.setHasImage(false);
}

return dto;
}
}

FUST框架提供了统一的响应格式ResponseDTO,让API接口返回的数据更加规范和一致。

5.7 应用启动类

最后,我们看一下应用的入口类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@SpringBootApplication
@Import(ServiceConfiguration.class)
@ComponentScan(basePackages = {"com.deepqa.api", "com.deepqa.business"})
@EntityScan(basePackages = {"com.deepqa.business.model"})
public class DeepQAMain {

public static void main(String[] args) {
// 设置默认环境变量
if (System.getProperty("env.name") == null && System.getenv("ENV_NAME") == null) {
System.setProperty("env.name", "dev");
}

TelemetryInitializer.init();
SpringApplication application = new SpringApplication(DeepQAMain.class);
application.setAdditionalProfiles("api");
application.run(args);
}
}

注意这里调用了TelemetryInitializer.init(),这是FUST框架提供的遥测初始化方法,用于收集应用指标和监控数据。

六、项目部署与运行

FUST框架提供了一系列脚本来简化项目的构建和运行过程。本文 demo 项目代码地址访问 deep-qa

6.1 项目构建

首先,编译Maven项目:

1
mvn clean package

然后,使用构建脚本:

1
bash build.sh

6.2 项目运行

根据不同的环境,使用不同的环境配置脚本:

开发环境

1
2
3
4
5
# 使用开发环境配置
source ./dev-env.sh

# 然后运行HTTP服务
bash run.sh deep-qa-api/target

生产环境

1
2
3
4
5
6
7
8
9
# 设置生产环境数据库密码(敏感信息不要硬编码)
export DB_USER="production_username"
export DB_PASSWORD="production_password"

# 使用生产环境配置
source ./prod-env.sh

# 然后运行HTTP服务
bash run.sh deep-qa-api/target

6.3 服务访问

API服务启动后,可以通过以下URL访问:

  • HTTP API: http://localhost:8080/api/chat/send

  • 示例调用:

    1
    2
    3
    curl -X POST "http://localhost:8080/api/chat/send" \
    -H "Content-Type: application/json" \
    -d '{"message":"你好,请介绍一下你自己"}'

七、FUST框架的企业级优势

通过Deep QA项目的实现,我们可以看到FUST框架在企业级开发中的一些显著优势:

7.1 结构化的项目组织

FUST框架提供了清晰的项目结构和模块划分,使得代码组织更加合理,便于团队协作和维护。

7.2 统一的异常处理

FUST提供了全局异常处理机制和统一的响应格式,使得API接口的错误处理更加规范和一致。

7.3 内置的可观测性

通过TelemetryInitializer,FUST框架提供了内置的监控和跟踪功能,便于问题排查和性能优化。

7.4 灵活的环境配置

FUST框架支持通过环境变量或系统属性来配置不同的运行环境,使得应用可以在不同环境中无缝切换。

7.5 高性能的服务框架

FUST基于高性能的Armeria服务器构建,提供了出色的性能和吞吐能力,特别适合高并发场景。

7.6 多协议支持

FUST同时支持HTTP和gRPC协议,使得服务可以同时为不同类型的客户端提供接口。

7.7 企业级数据库访问

FUST提供了对MyBatis的增强支持,简化了数据库访问层的开发,提高了代码的可维护性。

八、总结与展望

通过本文,我们详细介绍了如何使用FUST框架和DeepSeek API构建一个功能完整的智能问答系统。这个系统具有以下特点:

  1. 模块化架构:清晰的模块划分和责任分配
  2. 扩展性强:可以方便地添加新功能和扩展现有功能
  3. 高性能:基于FUST和Armeria的高性能服务框架
  4. 可靠稳定:完善的异常处理和错误恢复机制
  5. 可观测性:内置的监控和跟踪功能

未来,我们可以考虑在以下方面进行扩展和优化:

  1. 多模态支持:支持图像识别和处理
  2. 用户认证与权限管理:完善用户管理功能
  3. 多语言支持:支持更多语言和地区
  4. 对话记忆优化:改进对话上下文管理
  5. 模型微调:根据特定领域需求对模型进行微调

九、参考资料

  1. FUST框架官方文档
  2. DeepSeek API文档
  3. Spring Boot官方文档
  4. MyBatis官方文档
  5. Armeria服务器文档

通过本文的学习,相信读者已经对如何使用FUST框架构建企业级应用有了深入的理解,并能够将这些知识应用到自己的项目中。