trace/src/main/java/com/l/tracecd/service/LlmService.java
Lee c5dee7e444
Some checks failed
Java Maven 3.9.9 & JDK 26 CI/CD Pipeline / build-and-deploy (push) Failing after 9m3s
初始化项目
2026-06-08 15:04:18 +08:00

228 lines
9.4 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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