本地大漠模型联调,阿里百炼token

This commit is contained in:
2026-04-05 18:23:04 +08:00
parent db0bde39e3
commit 6043dc2665
8 changed files with 109 additions and 32 deletions

View File

@@ -0,0 +1,11 @@
package com.rj.common;
/**
* 本地 OpenAI 兼容接口生成总结的文本与 token 用量(来自 LangChain4j / 服务端 usage 字段)。
*/
public record LocalLlmSummaryResult(
String rawContent,
Integer totalTokens,
Integer inputTokens,
Integer outputTokens) {
}

View File

@@ -720,16 +720,7 @@ public class AudioManagementController {
// 4. 通过业务层通用服务调用大模型生成总结并保存数据
AudioTextAnalysisFurniture furniture = audioTextAnalysisLlmService.generateSummaryAndSave(
sceneType,
recordingText,
audioManagement.getId(),
audioManagement.getSalesName(),
audioManagement.getSalesPhone(),
audioManagement.getCustomerName(),
audioManagement.getCustomerPhone()
);
// AudioTextAnalysisFurniture furniture = audioTextAnalysisLlmService.generateSummaryAndSave_V2(
// AudioTextAnalysisFurniture furniture = audioTextAnalysisLlmService.generateSummaryAndSave(
// sceneType,
// recordingText,
// audioManagement.getId(),
@@ -738,6 +729,15 @@ public class AudioManagementController {
// audioManagement.getCustomerName(),
// audioManagement.getCustomerPhone()
// );
AudioTextAnalysisFurniture furniture = audioTextAnalysisLlmService.generateSummaryAndSave_V2(
sceneType,
recordingText,
audioManagement.getId(),
audioManagement.getSalesName(),
audioManagement.getSalesPhone(),
audioManagement.getCustomerName(),
audioManagement.getCustomerPhone()
);
log.info("llm return furniture Analysis : ",furniture.toString() );
if (recordingText == null || recordingText.trim().isEmpty()) {
result.put("success", false);

View File

@@ -28,6 +28,6 @@ public class AiQaCustomerAskRequestDto {
@Schema(description = "可选系统提示词,不传则使用默认助手设定")
private String systemPrompt;
@Schema(description = "max_tokens不传则使用配置 ai.qa.local.max-tokens默认 512")
@Schema(description = "max_tokens不传则使用配置 ai.qa.local.max-tokens默认 2048")
private Integer maxTokens;
}

View File

@@ -189,6 +189,18 @@ public class AudioManagement implements Serializable {
@TableField("summary")
private String summary;
@Schema(description = "大模型调用总 token 数")
@TableField("total_tokens")
private Integer totalTokens;
@Schema(description = "大模型输入 token 数")
@TableField("input_tokens")
private Integer inputTokens;
@Schema(description = "大模型输出 token 数")
@TableField("output_tokens")
private Integer outputTokens;
/**
* 前端上传的录音文件
* 此字段不保存在数据库中,仅用于接收前端上传的文件

View File

@@ -1,6 +1,8 @@
package com.rj.service;
import com.alibaba.dashscope.aigc.generation.GenerationResult;
import com.rj.common.AudioAnalysisSceneType;
import com.rj.common.LocalLlmSummaryResult;
import com.rj.entity.AudioTextAnalysisFurniture;
/**
@@ -19,7 +21,7 @@ public interface IAudioTextAnalysisLlmService {
* @param recordingText 录音转写文本
* @return 大模型返回的原始内容(一般是 JSON 字符串或结构化文本)
*/
String generateSummaryByLLM(AudioAnalysisSceneType sceneType, String recordingText);
GenerationResult generateSummaryByLLM(AudioAnalysisSceneType sceneType, String recordingText);
/**
* 与 {@link #generateSummaryByLLM} 相同的提示词与场景逻辑,但通过本地 OpenAI 兼容接口调用大模型
@@ -28,9 +30,9 @@ public interface IAudioTextAnalysisLlmService {
*
* @param sceneType 业务场景类型
* @param recordingText 录音转写文本
* @return 大模型返回的原始内容
* @return 大模型返回的原始内容及 token 用量(若接口未返回 usage 则对应字段可能为 null
*/
String generateSummaryByLocalLLM(AudioAnalysisSceneType sceneType, String recordingText);
LocalLlmSummaryResult generateSummaryByLocalLLM(AudioAnalysisSceneType sceneType, String recordingText);
/**
* 处理完整的业务逻辑调用大模型生成总结解析JSON保存数据

View File

@@ -57,7 +57,7 @@ public class AiQaCustomerAskServiceImpl implements IAiQaCustomerAskService {
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) {
@Value("${ai.qa.local.max-tokens:2048}") int configMaxTokens) {
this.restTemplate = restTemplate;
this.objectMapper = objectMapper;
this.aiQaMainService = aiQaMainService;

View File

@@ -3,6 +3,7 @@ package com.rj.service.impl;
import com.alibaba.dashscope.aigc.generation.Generation;
import com.alibaba.dashscope.aigc.generation.GenerationParam;
import com.alibaba.dashscope.aigc.generation.GenerationResult;
import com.alibaba.dashscope.aigc.generation.GenerationUsage;
import com.alibaba.dashscope.common.Message;
import com.alibaba.dashscope.common.Role;
import com.alibaba.dashscope.exception.InputRequiredException;
@@ -12,6 +13,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rj.common.AudioAnalysisSceneType;
import com.rj.common.LocalLlmSummaryResult;
import com.rj.entity.AudioManagement;
import com.rj.entity.AudioTextAnalysisFurniture;
import com.rj.entity.AudioTextAnalysisSop;
@@ -24,6 +26,7 @@ import com.rj.service.ITodoItemService;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
import dev.langchain4j.model.output.TokenUsage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@@ -50,6 +53,9 @@ import java.util.concurrent.atomic.AtomicReference;
@Slf4j
public class AudioTextAnalysisLlmServiceImpl implements IAudioTextAnalysisLlmService {
/** LangChain4j 流式调用结束后的文本与(可选)服务端 usage */
private record LocalChatOutcome(String text, TokenUsage tokenUsage) {}
private static final Map<String, String> SYSTEM_PROMPT_CACHE = new ConcurrentHashMap<>();
private static final Map<String, String> USER_PROMPT_CACHE = new ConcurrentHashMap<>();
@@ -75,7 +81,7 @@ public class AudioTextAnalysisLlmServiceImpl implements IAudioTextAnalysisLlmSer
private String localDefaultModel;
/** 单次补全上限OpenAI max_tokens与「模型总上下文 context-length」不是同一概念 */
@Value("${ai.qa.local.max-tokens:512}")
@Value("${ai.qa.local.max-tokens:2048}")
private int localMaxTokens;
/** 本地模型上下文长度(与推理侧 max context 一致,用于避免 input+max_tokens 超过上限;默认 16384 */
@@ -117,7 +123,7 @@ public class AudioTextAnalysisLlmServiceImpl implements IAudioTextAnalysisLlmSer
}
@Override
public String generateSummaryByLLM(AudioAnalysisSceneType sceneType, String recordingText) {
public GenerationResult generateSummaryByLLM(AudioAnalysisSceneType sceneType, String recordingText) {
long startTime = System.currentTimeMillis();
log.info("开始调用大模型生成总结,场景: {}", sceneType);
@@ -145,8 +151,9 @@ public class AudioTextAnalysisLlmServiceImpl implements IAudioTextAnalysisLlmSer
try {
GenerationResult call = gen.call(param);
String rawContent = call.getOutput().getChoices().get(0).getMessage().getContent();
log.info("大模型生成总结完成,场景: {}, 结果长度: {}", sceneType, rawContent != null ? rawContent.length() : 0);
return rawContent;
log.info("大模型生成总结完成,场景: {}, 输入长度:{},结果长度: {}", sceneType, recordingText.length(),rawContent != null ? rawContent.length() : 0);
return call;
} catch (NoApiKeyException e) {
log.error("API密钥未配置", e);
throw new RuntimeException("API密钥未配置: " + e.getMessage(), e);
@@ -170,7 +177,7 @@ public class AudioTextAnalysisLlmServiceImpl implements IAudioTextAnalysisLlmSer
}
@Override
public String generateSummaryByLocalLLM(AudioAnalysisSceneType sceneType, String recordingText) {
public LocalLlmSummaryResult generateSummaryByLocalLLM(AudioAnalysisSceneType sceneType, String recordingText) {
long startTime = System.currentTimeMillis();
log.info("开始调用本地大模型生成总结,场景: {}", sceneType);
@@ -181,14 +188,25 @@ public class AudioTextAnalysisLlmServiceImpl implements IAudioTextAnalysisLlmSer
try {
int effectiveMaxTokens = computeEffectiveLocalMaxTokens(systemPrompt, userPrompt);
String rawContent = callLocalOpenAiChatCompletionsByLangChain4j(
LocalChatOutcome outcome = callLocalOpenAiChatCompletionsByLangChain4j(
systemPrompt,
userPrompt,
localDefaultModel,
effectiveMaxTokens);
String rawContent = outcome.text();
TokenUsage usage = outcome.tokenUsage();
Integer total = usage != null ? usage.totalTokenCount() : null;
Integer input = usage != null ? usage.inputTokenCount() : null;
Integer output = usage != null ? usage.outputTokenCount() : null;
log.info(
"本地大模型生成总结完成token花费总token: {}, 输入token: {}, 输出token: {}, 输出token详情: {}",
total,
input,
output,
usage != null ? usage.toString() : "");
log.info("本地大模型生成总结完成,场景: {}, 结果长度: {}",
sceneType, rawContent != null ? rawContent.length() : 0);
return rawContent;
return new LocalLlmSummaryResult(rawContent, total, input, output);
} catch (Exception e) {
log.error("调用本地大模型生成总结失败, 场景: {}", sceneType, e);
throw new RuntimeException("调用本地大模型失败: " + e.getMessage(), e);
@@ -208,9 +226,9 @@ public class AudioTextAnalysisLlmServiceImpl implements IAudioTextAnalysisLlmSer
/**
* 与 {@link AiQaCustomerAskServiceImpl} 中非流式问答路径一致LangChain4j + OpenAI 兼容接口,流式聚合为完整文本。
*/
private String callLocalOpenAiChatCompletionsByLangChain4j(
private LocalChatOutcome callLocalOpenAiChatCompletionsByLangChain4j(
String systemPrompt, String userContent, String model, int maxTokens) {
log.info("音频分析本地 LLMmodel={}, chatCompletionsUrl={}", model, chatCompletionsUrl);
log.info("音频分析本地 LLMmodel={}, chatCompletionsUrl={}, maxTokens={}", model, chatCompletionsUrl,maxTokens);
String openAiBaseUrl = normalizeOpenAiBaseUrl(chatCompletionsUrl);
OpenAiStreamingChatModel chatModel = OpenAiStreamingChatModel.builder()
.baseUrl(openAiBaseUrl)
@@ -223,6 +241,7 @@ public class AudioTextAnalysisLlmServiceImpl implements IAudioTextAnalysisLlmSer
StringBuilder answerBuffer = new StringBuilder();
CountDownLatch done = new CountDownLatch(1);
AtomicReference<Throwable> errorRef = new AtomicReference<>();
AtomicReference<TokenUsage> usageRef = new AtomicReference<>();
chatModel.chat(prompt, new StreamingChatResponseHandler() {
@Override
@@ -234,6 +253,9 @@ public class AudioTextAnalysisLlmServiceImpl implements IAudioTextAnalysisLlmSer
@Override
public void onCompleteResponse(ChatResponse completeResponse) {
if (completeResponse != null) {
usageRef.set(completeResponse.tokenUsage());
}
done.countDown();
}
@@ -262,7 +284,7 @@ public class AudioTextAnalysisLlmServiceImpl implements IAudioTextAnalysisLlmSer
if (answer.isEmpty()) {
throw new IllegalStateException("本地大模型流式调用返回空响应");
}
return answer;
return new LocalChatOutcome(answer, usageRef.get());
}
private String normalizeOpenAiBaseUrl(String rawUrl) {
@@ -429,8 +451,11 @@ public class AudioTextAnalysisLlmServiceImpl implements IAudioTextAnalysisLlmSer
String customerName,
String customerPhone) {
try {
GenerationResult generationResult = generateSummaryByLLM(sceneType, recordingText);
log.info("大模型生成总结完成token花费总token: {}, 输入token: {}, 输出token: {}, 输出token详情: {}",
generationResult.getUsage().getTotalTokens(),generationResult.getUsage().getInputTokens(),generationResult.getUsage().getOutputTokens(),generationResult.getUsage().getOutputTokensDetails());
// 1. 调用大模型生成总结
String rawContent = generateSummaryByLLM(sceneType, recordingText);
String rawContent = generationResult.getOutput().getChoices().get(0).getMessage().getContent();
if (rawContent == null || rawContent.trim().isEmpty()) {
log.warn("大模型返回内容为空");
return null;
@@ -443,10 +468,11 @@ public class AudioTextAnalysisLlmServiceImpl implements IAudioTextAnalysisLlmSer
// 3. 解析JSON并填充对象
applyStructuredResult(furniture, rawContent, parentId, ownerName, ownerPhone, customerName, customerPhone);
// 4. 更新AudioManagement的summary字段
// 4. 更新AudioManagement的summary与 token 用量
AudioManagement audioManagement = new AudioManagement();
audioManagement.setId(parentId);
audioManagement.setSummary(furniture.getSummarySentence());
audioManagement.setSummary(rawContent);
applyDashScopeUsageToAudioManagement(audioManagement, generationResult);
audioManagementService.updateById(audioManagement);
// 5. 保存或更新AudioTextAnalysisFurniture记录
@@ -491,21 +517,47 @@ public class AudioTextAnalysisLlmServiceImpl implements IAudioTextAnalysisLlmSer
String ownerPhone,
String customerName,
String customerPhone) {
// 1. 调用大模型生成总结
String rawContent = generateSummaryByLocalLLM(sceneType, recordingText);
LocalLlmSummaryResult llmResult = generateSummaryByLocalLLM(sceneType, recordingText);
String rawContent = llmResult.rawContent();
if (rawContent == null || rawContent.trim().isEmpty()) {
log.warn("大模型返回内容为空");
return null;
}
AudioTextAnalysisFurniture furniture = new AudioTextAnalysisFurniture();
// 4. 更新AudioManagement的summary字段
AudioManagement audioManagement = new AudioManagement();
audioManagement.setId(parentId);
audioManagement.setSummary(rawContent);
applyLocalLlmTokenCountsToAudioManagement(audioManagement, llmResult);
audioManagementService.updateById(audioManagement);
return furniture;
}
private void applyDashScopeUsageToAudioManagement(AudioManagement target, GenerationResult result) {
if (result == null || result.getUsage() == null) {
return;
}
GenerationUsage usage = result.getUsage();
target.setTotalTokens(usage.getTotalTokens());
target.setInputTokens(usage.getInputTokens());
target.setOutputTokens(usage.getOutputTokens());
}
private void applyLocalLlmTokenCountsToAudioManagement(AudioManagement target, LocalLlmSummaryResult result) {
if (result == null) {
return;
}
if (result.totalTokens() != null) {
target.setTotalTokens(result.totalTokens());
}
if (result.inputTokens() != null) {
target.setInputTokens(result.inputTokens());
}
if (result.outputTokens() != null) {
target.setOutputTokens(result.outputTokens());
}
}
/**
* 解析大模型返回的JSON结果并填充到AudioTextAnalysisFurniture对象
*

View File

@@ -3,7 +3,7 @@
字段要求:
1. style_type主题类型仅可填「会议纪要」「课堂纪要」「面试纪要」。
2. members团队成员描述 团队成员组成 ,比如: 经理,员工, 面试者,面试管,老师 重点反应每类人员的数量,姓名, 优点,贡献大小 等。
4. summary一句话总结应该简洁明了总结关键信息 重点列举5到15个重点信息 等关键信息, summary应该是树形结构总共分3层 ,比如 summary是第一层 ,第二层 类似: summary1summary2summary3summary[N] ,第三层 类似summary1_1,summary1_2,summary1_3,summary1_[N]。
4. summary一句话总结,生成的描述不少于200汉字,应该简洁明了总结关键信息, 重点列举5到15个重点信息 等关键信息, summary应该是树形结构总共分3层 ,比如 summary是第一层 ,第二层 类似: summary1summary2summary3summary[N] ,第三层 类似summary1_1,summary1_2,summary1_3,summary1_[N]。
5.summary1: 对summary 提供支持summary1是对summary的详细解释的第一个理由 summary1是对summary深入分析的一部分 。
6.summary1_1: 对summary1提供支持summary1_1是对summary1的详细解释的第一个理由 summary1_1是对summary1深入分析的一部分 。
7.summary1_[N]: 对summary1提供支持summary1_[N]是对summary1的详细解释的第一个理由 summary1_[N]是对summary1深入分析的一部分 ,[N] 这里的N不能超过数字 10。