调用本地大模型, 已调通

This commit is contained in:
2026-03-31 07:53:20 +08:00
parent 454128c616
commit 48cd1115f2
2 changed files with 203 additions and 0 deletions

View 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;
}

View File

@@ -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 JSONAuthorization 留空不设置该头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.contentbody=" + raw);
}
log.info("OpenAI 兼容接口返回:" + content);
return content.asText("");
}
}