调用本地大模型, 已调通
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