调用本地大模型, 已调通
This commit is contained in:
18
src/main/java/com/rj/service/IAiQaCustomerAskService.java
Normal file
18
src/main/java/com/rj/service/IAiQaCustomerAskService.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package com.rj.service;
|
||||
|
||||
import com.rj.dto.AiQaCustomerAskRequestDto;
|
||||
import com.rj.dto.AiQaCustomerAskResponseDto;
|
||||
|
||||
/**
|
||||
* AI 问答子表:客户提问并调用大模型的业务
|
||||
*/
|
||||
public interface IAiQaCustomerAskService {
|
||||
|
||||
/**
|
||||
* 校验入参、拼装 messages、调用本地 OpenAI 兼容接口,返回回答数据。
|
||||
*
|
||||
* @throws IllegalArgumentException 入参不合法(非受检,调用方需捕获或转为 HTTP 400)
|
||||
* @throws Exception HTTP 或解析失败、模型返回错误等
|
||||
*/
|
||||
AiQaCustomerAskResponseDto askCustomer(AiQaCustomerAskRequestDto request) throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package com.rj.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.rj.dto.AiQaCustomerAskRequestDto;
|
||||
import com.rj.dto.AiQaCustomerAskResponseDto;
|
||||
import com.rj.entity.AiQaItem;
|
||||
import com.rj.entity.AiQaMain;
|
||||
import com.rj.service.IAiQaCustomerAskService;
|
||||
import com.rj.service.IAiQaItemService;
|
||||
import com.rj.service.IAiQaMainService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 客户 AI 问答:本地 OpenAI 兼容 /v1/chat/completions
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AiQaCustomerAskServiceImpl implements IAiQaCustomerAskService {
|
||||
|
||||
private static final String DEFAULT_SYSTEM_PROMPT = "你是专业、友好的助手,请简洁准确地回答用户问题。";
|
||||
|
||||
private static final int MAIN_TITLE_MAX_LEN = 32;
|
||||
private static final int ITEM_QUEST_SRC_MAX_LEN = 500;
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final IAiQaMainService aiQaMainService;
|
||||
private final IAiQaItemService aiQaItemService;
|
||||
|
||||
private final String chatCompletionsUrl;
|
||||
private final String configDefaultModel;
|
||||
private final int configMaxTokens;
|
||||
|
||||
public AiQaCustomerAskServiceImpl(
|
||||
RestTemplate restTemplate,
|
||||
ObjectMapper objectMapper,
|
||||
IAiQaMainService aiQaMainService,
|
||||
IAiQaItemService aiQaItemService,
|
||||
@Value("${ai.qa.local.chat-url:http://192.168.1.44:8000/v1/chat/completions}") String chatCompletionsUrl,
|
||||
@Value("${ai.qa.local.default-model:Qwen2.5-7B-Instruct}") String configDefaultModel,
|
||||
@Value("${ai.qa.local.max-tokens:512}") int configMaxTokens) {
|
||||
this.restTemplate = restTemplate;
|
||||
this.objectMapper = objectMapper;
|
||||
this.aiQaMainService = aiQaMainService;
|
||||
this.aiQaItemService = aiQaItemService;
|
||||
this.chatCompletionsUrl = chatCompletionsUrl;
|
||||
this.configDefaultModel = configDefaultModel;
|
||||
this.configMaxTokens = configMaxTokens;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public AiQaCustomerAskResponseDto askCustomer(AiQaCustomerAskRequestDto request) throws Exception {
|
||||
validate(request);
|
||||
|
||||
boolean newMain = request.getParentId() == null || request.getParentId().trim().isEmpty();
|
||||
String parentId = newMain ? UUID.randomUUID().toString() : request.getParentId().trim();
|
||||
String questionType = request.getQuestionType().trim();
|
||||
String questionContent = request.getQuestionContent().trim();
|
||||
|
||||
String model = request.getModel() != null && !request.getModel().trim().isEmpty()
|
||||
? request.getModel().trim()
|
||||
: configDefaultModel;
|
||||
String systemContent = request.getSystemPrompt() != null && !request.getSystemPrompt().trim().isEmpty()
|
||||
? request.getSystemPrompt().trim()
|
||||
: DEFAULT_SYSTEM_PROMPT;
|
||||
|
||||
int maxTokens = request.getMaxTokens() != null && request.getMaxTokens() > 0
|
||||
? request.getMaxTokens()
|
||||
: configMaxTokens;
|
||||
|
||||
String userMessage = "问题类型:" + questionType + "\n用户问题:" + questionContent;
|
||||
String answer = callLocalOpenAiChatCompletions(systemContent, userMessage, model, maxTokens);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (newMain) {
|
||||
AiQaMain main = new AiQaMain();
|
||||
main.setId(parentId);
|
||||
main.setTitle(truncate(questionContent, MAIN_TITLE_MAX_LEN));
|
||||
main.setQaType(questionType);
|
||||
main.setAskContent(questionContent);
|
||||
main.setCreateTime(now);
|
||||
main.setUpdateTime(now);
|
||||
aiQaMainService.save(main);
|
||||
}
|
||||
AiQaItem item = new AiQaItem();
|
||||
item.setId(UUID.randomUUID().toString());
|
||||
item.setParentId(parentId);
|
||||
item.setQuestSrc(truncate(userMessage, ITEM_QUEST_SRC_MAX_LEN));
|
||||
item.setAnswerText(buildAnswerJson(questionType, questionContent, answer, model));
|
||||
item.setCreateTime(now);
|
||||
item.setUpdateTime(now);
|
||||
aiQaItemService.save(item);
|
||||
|
||||
return AiQaCustomerAskResponseDto.builder()
|
||||
.parentId(parentId)
|
||||
.questionType(questionType)
|
||||
.questionContent(questionContent)
|
||||
.answer(answer)
|
||||
.model(model)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static String truncate(String s, int maxLen) {
|
||||
if (s == null) {
|
||||
return null;
|
||||
}
|
||||
return s.length() <= maxLen ? s : s.substring(0, maxLen);
|
||||
}
|
||||
|
||||
private JsonNode buildAnswerJson(String questionType, String questionContent, String answer, String model) {
|
||||
var node = objectMapper.createObjectNode();
|
||||
node.put("questionType", questionType);
|
||||
node.put("questionContent", questionContent);
|
||||
node.put("answer", answer);
|
||||
node.put("model", model);
|
||||
return node;
|
||||
}
|
||||
|
||||
private void validate(AiQaCustomerAskRequestDto request) {
|
||||
if (request == null) {
|
||||
throw new IllegalArgumentException("请求体不能为空");
|
||||
}
|
||||
if (request.getQuestionType() == null || request.getQuestionType().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("客户问题类型(questionType)不能为空");
|
||||
}
|
||||
if (request.getQuestionContent() == null || request.getQuestionContent().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("客户问题内容(questionContent)不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAI 标准:POST JSON,Authorization 留空(不设置该头),Content-Type application/json。
|
||||
*/
|
||||
private String callLocalOpenAiChatCompletions(String systemPrompt, String userContent, String model, int maxTokens)
|
||||
throws Exception {
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("model", model);
|
||||
List<Map<String, String>> messages = new ArrayList<>();
|
||||
Map<String, String> sys = new LinkedHashMap<>();
|
||||
sys.put("role", "system");
|
||||
sys.put("content", systemPrompt);
|
||||
messages.add(sys);
|
||||
Map<String, String> user = new LinkedHashMap<>();
|
||||
user.put("role", "user");
|
||||
user.put("content", userContent);
|
||||
messages.add(user);
|
||||
body.put("messages", messages);
|
||||
body.put("max_tokens", maxTokens);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(chatCompletionsUrl, entity, String.class);
|
||||
String raw = response.getBody();
|
||||
if (raw == null || raw.isEmpty()) {
|
||||
throw new IllegalStateException("本地大模型返回空响应体");
|
||||
}
|
||||
|
||||
JsonNode root = objectMapper.readTree(raw);
|
||||
if (root.has("error")) {
|
||||
String msg = root.path("error").path("message").asText(root.path("error").toString());
|
||||
throw new IllegalStateException("本地大模型错误: " + msg);
|
||||
}
|
||||
JsonNode content = root.path("choices").path(0).path("message").path("content");
|
||||
if (content.isMissingNode() || content.isNull()) {
|
||||
throw new IllegalStateException("响应缺少标准字段 choices[0].message.content,body=" + raw);
|
||||
}
|
||||
log.info("OpenAI 兼容接口返回:" + content);
|
||||
return content.asText("");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user