From 6043dc26653602d4fd97adfe075e91cef4c305f9 Mon Sep 17 00:00:00 2001 From: cst61 Date: Sun, 5 Apr 2026 18:23:04 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E5=A4=A7=E6=BC=A0=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E8=81=94=E8=B0=83=EF=BC=8C=E9=98=BF=E9=87=8C=E7=99=BE?= =?UTF-8?q?=E7=82=BCtoken?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/rj/common/LocalLlmSummaryResult.java | 11 +++ .../controller/AudioManagementController.java | 20 ++--- .../com/rj/dto/AiQaCustomerAskRequestDto.java | 2 +- .../java/com/rj/entity/AudioManagement.java | 12 +++ .../service/IAudioTextAnalysisLlmService.java | 8 +- .../impl/AiQaCustomerAskServiceImpl.java | 2 +- .../impl/AudioTextAnalysisLlmServiceImpl.java | 84 +++++++++++++++---- ...dio_text_analysis_summary_user_prompts.txt | 2 +- 8 files changed, 109 insertions(+), 32 deletions(-) create mode 100644 src/main/java/com/rj/common/LocalLlmSummaryResult.java diff --git a/src/main/java/com/rj/common/LocalLlmSummaryResult.java b/src/main/java/com/rj/common/LocalLlmSummaryResult.java new file mode 100644 index 0000000..efeeb2a --- /dev/null +++ b/src/main/java/com/rj/common/LocalLlmSummaryResult.java @@ -0,0 +1,11 @@ +package com.rj.common; + +/** + * 本地 OpenAI 兼容接口生成总结的文本与 token 用量(来自 LangChain4j / 服务端 usage 字段)。 + */ +public record LocalLlmSummaryResult( + String rawContent, + Integer totalTokens, + Integer inputTokens, + Integer outputTokens) { +} diff --git a/src/main/java/com/rj/controller/AudioManagementController.java b/src/main/java/com/rj/controller/AudioManagementController.java index bf68701..a94eff7 100644 --- a/src/main/java/com/rj/controller/AudioManagementController.java +++ b/src/main/java/com/rj/controller/AudioManagementController.java @@ -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); diff --git a/src/main/java/com/rj/dto/AiQaCustomerAskRequestDto.java b/src/main/java/com/rj/dto/AiQaCustomerAskRequestDto.java index 348113e..d9b5e44 100644 --- a/src/main/java/com/rj/dto/AiQaCustomerAskRequestDto.java +++ b/src/main/java/com/rj/dto/AiQaCustomerAskRequestDto.java @@ -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; } diff --git a/src/main/java/com/rj/entity/AudioManagement.java b/src/main/java/com/rj/entity/AudioManagement.java index d00eeab..9a4df27 100644 --- a/src/main/java/com/rj/entity/AudioManagement.java +++ b/src/main/java/com/rj/entity/AudioManagement.java @@ -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; + /** * 前端上传的录音文件 * 此字段不保存在数据库中,仅用于接收前端上传的文件 diff --git a/src/main/java/com/rj/service/IAudioTextAnalysisLlmService.java b/src/main/java/com/rj/service/IAudioTextAnalysisLlmService.java index 4ad75b2..756e8c5 100644 --- a/src/main/java/com/rj/service/IAudioTextAnalysisLlmService.java +++ b/src/main/java/com/rj/service/IAudioTextAnalysisLlmService.java @@ -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,保存数据 diff --git a/src/main/java/com/rj/service/impl/AiQaCustomerAskServiceImpl.java b/src/main/java/com/rj/service/impl/AiQaCustomerAskServiceImpl.java index de8afdc..d3ad86c 100644 --- a/src/main/java/com/rj/service/impl/AiQaCustomerAskServiceImpl.java +++ b/src/main/java/com/rj/service/impl/AiQaCustomerAskServiceImpl.java @@ -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; diff --git a/src/main/java/com/rj/service/impl/AudioTextAnalysisLlmServiceImpl.java b/src/main/java/com/rj/service/impl/AudioTextAnalysisLlmServiceImpl.java index cfe28e2..49ffac4 100644 --- a/src/main/java/com/rj/service/impl/AudioTextAnalysisLlmServiceImpl.java +++ b/src/main/java/com/rj/service/impl/AudioTextAnalysisLlmServiceImpl.java @@ -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 SYSTEM_PROMPT_CACHE = new ConcurrentHashMap<>(); private static final Map 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("音频分析本地 LLM:model={}, chatCompletionsUrl={}", model, chatCompletionsUrl); + log.info("音频分析本地 LLM:model={}, 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 errorRef = new AtomicReference<>(); + AtomicReference 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对象 * diff --git a/src/main/resources/prompts/audio_text_analysis_summary_user_prompts.txt b/src/main/resources/prompts/audio_text_analysis_summary_user_prompts.txt index b6fb263..6255d9e 100644 --- a/src/main/resources/prompts/audio_text_analysis_summary_user_prompts.txt +++ b/src/main/resources/prompts/audio_text_analysis_summary_user_prompts.txt @@ -3,7 +3,7 @@ 字段要求: 1. style_type:主题类型,仅可填「会议纪要」「课堂纪要」「面试纪要」。 2. members:团队成员,描述 团队成员组成 ,比如: 经理,员工, 面试者,面试管,老师 , 重点反应每类人员的数量,姓名, 优点,贡献大小 等。 -4. summary:一句话总结,应该简洁明了总结关键信息, 重点列举5到15个重点信息, 等关键信息, summary应该是树形结构,总共分3层 ,比如 summary是第一层 ,第二层 类似: summary1,summary2,summary3,summary[N] ,第三层 类似:summary1_1,summary1_2,summary1_3,summary1_[N]。 +4. summary:一句话总结,生成的描述不少于200汉字,应该简洁明了总结关键信息, 重点列举5到15个重点信息, 等关键信息, summary应该是树形结构,总共分3层 ,比如 summary是第一层 ,第二层 类似: summary1,summary2,summary3,summary[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。