Some checks failed
Java Maven 3.9.9 & JDK 26 CI/CD Pipeline / build-and-deploy (push) Failing after 9m3s
228 lines
9.4 KiB
Java
228 lines
9.4 KiB
Java
package com.l.tracecd.service;
|
||
|
||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||
import com.l.tracecd.config.DeepSeekConfig;
|
||
import com.l.tracecd.constant.Constants;
|
||
import com.l.tracecd.dto.DeepSeekRequest;
|
||
import com.l.tracecd.dto.DeepSeekResponse;
|
||
import org.slf4j.Logger;
|
||
import org.slf4j.LoggerFactory;
|
||
import org.springframework.stereotype.Service;
|
||
import org.springframework.web.client.RestClient;
|
||
|
||
import java.time.LocalDateTime;
|
||
import java.time.format.DateTimeFormatter;
|
||
import java.util.ArrayList;
|
||
import java.util.List;
|
||
import java.util.Map;
|
||
|
||
/**
|
||
* DeepSeek 大模型服务
|
||
* 负责与 DeepSeek API 交互,包括 function call 处理
|
||
*/
|
||
@Service
|
||
public class LlmService {
|
||
|
||
private static final Logger log = LoggerFactory.getLogger(LlmService.class);
|
||
|
||
private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||
|
||
private final RestClient deepSeekRestClient;
|
||
private final DeepSeekConfig.DeepSeekProperties deepSeekProperties;
|
||
private final ObjectMapper objectMapper;
|
||
|
||
public LlmService(RestClient deepSeekRestClient, DeepSeekConfig.DeepSeekProperties deepSeekProperties,
|
||
ObjectMapper objectMapper) {
|
||
this.deepSeekRestClient = deepSeekRestClient;
|
||
this.deepSeekProperties = deepSeekProperties;
|
||
this.objectMapper = objectMapper;
|
||
}
|
||
|
||
/**
|
||
* 分析用户语音文本,返回 LLM 响应
|
||
* 可能包含文本回复或 function call 请求
|
||
*
|
||
* @param userText ASR 识别后的文本
|
||
* @return LLM 的响应消息
|
||
*/
|
||
public DeepSeekResponse.ResponseMessage analyze(String userText) {
|
||
long start = System.currentTimeMillis();
|
||
|
||
DeepSeekRequest request = new DeepSeekRequest();
|
||
request.setModel(deepSeekProperties.getModel());
|
||
|
||
List<DeepSeekRequest.Message> messages = new ArrayList<>();
|
||
messages.add(new DeepSeekRequest.Message("system", buildSystemPrompt()));
|
||
messages.add(new DeepSeekRequest.Message("user", userText));
|
||
request.setMessages(messages);
|
||
|
||
// 设置 function call 工具
|
||
request.setTools(buildTools());
|
||
|
||
log.info("调用 DeepSeek API,用户输入: {}", userText);
|
||
DeepSeekResponse response = deepSeekRestClient.post()
|
||
.body(request)
|
||
.retrieve()
|
||
.body(DeepSeekResponse.class);
|
||
|
||
long elapsed = System.currentTimeMillis() - start;
|
||
log.info("DeepSeek 响应完成,耗时: {}ms", elapsed);
|
||
|
||
if (response == null || response.getChoices() == null || response.getChoices().isEmpty()) {
|
||
throw new RuntimeException("DeepSeek 返回空响应");
|
||
}
|
||
|
||
return response.getChoices().getFirst().getMessage();
|
||
}
|
||
|
||
/**
|
||
* 将 function call 结果返回给 LLM,获取最终回复
|
||
*
|
||
* @param messages 完整对话历史(包含 tool call 和 tool 结果)
|
||
* @return LLM 的最终响应
|
||
*/
|
||
public DeepSeekResponse.ResponseMessage continueWithToolResult(
|
||
List<DeepSeekRequest.Message> messages) {
|
||
long start = System.currentTimeMillis();
|
||
|
||
DeepSeekRequest request = new DeepSeekRequest();
|
||
request.setModel(deepSeekProperties.getModel());
|
||
request.setMessages(messages);
|
||
request.setTools(buildTools());
|
||
|
||
log.debug("继续与 DeepSeek 对话,messages 数量: {}", messages.size());
|
||
DeepSeekResponse response = deepSeekRestClient.post()
|
||
.body(request)
|
||
.retrieve()
|
||
.body(DeepSeekResponse.class);
|
||
|
||
long elapsed = System.currentTimeMillis() - start;
|
||
log.info("DeepSeek 二次响应完成,耗时: {}ms", elapsed);
|
||
|
||
if (response == null || response.getChoices() == null || response.getChoices().isEmpty()) {
|
||
throw new RuntimeException("DeepSeek 返回空响应");
|
||
}
|
||
|
||
return response.getChoices().get(0).getMessage();
|
||
}
|
||
|
||
/**
|
||
* 构建 System Prompt
|
||
*/
|
||
private String buildSystemPrompt() {
|
||
String now = LocalDateTime.now().format(DT_FMT);
|
||
return """
|
||
你是一个日常记账助手。当前时间是 %s。
|
||
|
||
你的任务是分析用户的输入,判断意图并做出相应处理:
|
||
|
||
## 意图判断
|
||
1. **录入(record)**: 用户描述了在某个时间、地点、花费金额做了某事。
|
||
例如:"我花20块钱在小区门口张三面馆吃了一碗肉丝面"
|
||
2. **查询(query)**: 用户想查询已录入的事项。
|
||
例如:"我昨天中午12点做了什么"、"我今天一共花了多少钱"、"我上次吃面是什么时候"
|
||
3. **聊天(chat)**: 用户输入与记账无关。
|
||
例如:"你好"、"今天天气怎么样"
|
||
|
||
## 录入意图处理
|
||
当用户意图是录入时,分析内容并提取字段,生成标准 MySQL INSERT 语句。
|
||
- person: 人物,默认为"我"
|
||
- record_time: 事项发生时间,如用户未明确时间则用当前时间,格式 yyyy-MM-dd HH:mm:ss
|
||
- location: 地点
|
||
- content: 事项内容概述(一句话)
|
||
- category: 分类,2-4个字,如"吃饭"、"修车"、"旅游"、"加油"、"租房"、"水电"、"话费"、"购物"、"剪头"等
|
||
- amount: 花费金额(数字)
|
||
|
||
只输出 SQL 语句,不要用```sql```包裹,不要有任何其他内容。
|
||
格式举例:INSERT INTO t_daily_record (person, record_time, location, content, category, amount) VALUES ('我', '2025-01-15 12:00:00', '小区门口张三面馆', '吃了一碗肉丝面', '吃饭', 20.00)
|
||
|
||
## 查询意图处理
|
||
当用户意图是查询时,**必须调用 query_daily_records 函数**执行 SQL 查询。
|
||
- 人物、日期、内容、金额相关字段查询
|
||
- 查询完成后根据返回数据生成回复:
|
||
* 多条数据用 markdown 表格展示,表格上方展示金额汇总
|
||
* 一条数据直接描述
|
||
* 无数据告知用户未找到
|
||
* 回复要友好自然
|
||
|
||
## 聊天意图处理
|
||
当用户输入与记账无关时,直接友好回复。
|
||
|
||
## 重要规则
|
||
- 只输出 SQL 语句或友好回复,不要输出分析过程
|
||
- 时间推算要准确:"昨天"推算为具体日期,"中午12点"设为12:00:00
|
||
- 如果用户没有提及金额,amount 设为 0
|
||
- 分类必须是2-4个中文字符
|
||
""".formatted(now);
|
||
}
|
||
|
||
/**
|
||
* 构建 Function Call 工具定义
|
||
*/
|
||
private List<DeepSeekRequest.Tool> buildTools() {
|
||
Map<String, Object> sqlProperty = Map.of(
|
||
"type", "object",
|
||
"properties", Map.of(
|
||
"sql", Map.of(
|
||
"type", "string",
|
||
"description", "合法的 MySQL SELECT 语句,仅允许 SELECT,表名限定为 t_daily_record"
|
||
)
|
||
),
|
||
"required", List.of("sql")
|
||
);
|
||
|
||
DeepSeekRequest.ToolFunction func = new DeepSeekRequest.ToolFunction(
|
||
Constants.FUNCTION_QUERY_RECORDS,
|
||
"执行 SQL SELECT 查询获取事项数据。仅支持 SELECT 语句,表名限定为 t_daily_record。查询结果以 JSON 数组返回。",
|
||
new DeepSeekRequest.Parameters(
|
||
Map.of("sql", sqlProperty.get("properties")),
|
||
List.of("sql")
|
||
)
|
||
);
|
||
|
||
// Fix: properly set the properties type
|
||
func.getParameters().setProperties(Map.of(
|
||
"sql", Map.of(
|
||
"type", "string",
|
||
"description", "合法的 MySQL SELECT 语句,仅允许 SELECT,表名限定为 t_daily_record"
|
||
)
|
||
));
|
||
|
||
return List.of(new DeepSeekRequest.Tool("function", func));
|
||
}
|
||
|
||
/**
|
||
* 将 LLM 的 INSERT SQL 再次发送给 LLM 要求修正
|
||
*/
|
||
public DeepSeekResponse.ResponseMessage retryInsert(String userText, String errorMessage) {
|
||
String prompt = """
|
||
之前生成的 SQL 语句不合法。错误信息: %s
|
||
|
||
用户原始输入: %s
|
||
|
||
请重新生成合法的 INSERT INTO t_daily_record 语句。
|
||
只输出 SQL 语句,不要有其他内容。
|
||
""".formatted(errorMessage, userText);
|
||
|
||
DeepSeekRequest request = new DeepSeekRequest();
|
||
request.setModel(deepSeekProperties.getModel());
|
||
request.setMessages(List.of(
|
||
new DeepSeekRequest.Message("system", buildSystemPrompt()),
|
||
new DeepSeekRequest.Message("user", prompt)
|
||
));
|
||
// 重试时不带 tools
|
||
|
||
log.info("重试 SQL 生成,错误: {}", errorMessage);
|
||
DeepSeekResponse response = deepSeekRestClient.post()
|
||
.body(request)
|
||
.retrieve()
|
||
.body(DeepSeekResponse.class);
|
||
|
||
if (response == null || response.getChoices() == null || response.getChoices().isEmpty()) {
|
||
throw new RuntimeException("DeepSeek 返回空响应");
|
||
}
|
||
return response.getChoices().get(0).getMessage();
|
||
}
|
||
}
|