提示词完善
This commit is contained in:
@@ -15,8 +15,8 @@ public enum AudioAnalysisSceneType {
|
||||
"qwen-plus"
|
||||
),
|
||||
SCENARIO_SOFT_SALE(
|
||||
"prompts/audio_text_analysis_furniture_system.txt",
|
||||
"prompts/audio_text_analysis_furniture_user.txt",
|
||||
"prompts/audio_text_analysis_soft_sale_system_prompts.txt",
|
||||
"prompts/audio_text_analysis_soft_sale_user_prompts.txt",
|
||||
"qwen-plus"
|
||||
),
|
||||
|
||||
@@ -24,9 +24,9 @@ public enum AudioAnalysisSceneType {
|
||||
* 会议纪要/会议分析场景
|
||||
* 提示词文件和模型可根据实际需要进行调整。
|
||||
*/
|
||||
SCENARIO_COMMON_SALE(
|
||||
"prompts/audio_text_analysis_meeting_system.txt",
|
||||
"prompts/audio_text_analysis_meeting_user.txt",
|
||||
SCENARIO_SUMMARY(
|
||||
"prompts/audio_text_analysis_summary_system_prompts.txt",
|
||||
"prompts/audio_text_analysis_summary_user_prompts.txt",
|
||||
"qwen-plus"
|
||||
),
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ public class AudioManagementController {
|
||||
private static final String SCENARIO_FURNITURE_SALE = "FURNITURE";
|
||||
private static final String SCENARIO_MEETING_SUMMARY = "MEETING_SUMMARY";
|
||||
private static final String SCENARIO_CAR_SALE = "CAR_SALE";
|
||||
private static final String SCENARIO_COMMON_SALE = "COMMON_SALE";
|
||||
private static final String SCENARIO_SUMMARY = "SUMMARY";
|
||||
private static final String SCENARIO_SPEAKING_TRAINING= "SPEAKING_TRAINING"; //租赁模式
|
||||
|
||||
|
||||
@@ -510,6 +510,7 @@ public class AudioManagementController {
|
||||
customerToUpdate.setDetailedAddress(audioManagement.getRemarks());
|
||||
customerToUpdate.setSalesPhone(audioManagement.getSalesPhone());
|
||||
customerToUpdate.setSalesName(audioManagement.getSalesName());
|
||||
customerToUpdate.setRecordingCount(customerToUpdate.getRecordingCount()+1);
|
||||
customerToUpdate.setUpdateTime(LocalDateTime.now());
|
||||
customerManagementService.updateById(customerToUpdate);
|
||||
log.info("同步更新客户信息成功,客户ID: {}", customerToUpdate.getId());
|
||||
@@ -728,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);
|
||||
@@ -762,8 +772,8 @@ public class AudioManagementController {
|
||||
if (SCENARIO_FURNITURE_SALE.equals(s)) {
|
||||
return AudioAnalysisSceneType.SCENARIO_FURNITURE_SALE;
|
||||
}
|
||||
if (SCENARIO_COMMON_SALE.equals(s)) {
|
||||
return AudioAnalysisSceneType.SCENARIO_COMMON_SALE;
|
||||
if (SCENARIO_SUMMARY.equals(s)) {
|
||||
return AudioAnalysisSceneType.SCENARIO_SUMMARY;
|
||||
}
|
||||
if (SCENARIO_CAR_SALE.equals(s)) {
|
||||
return AudioAnalysisSceneType.SCENARIO_CAR_SALE;
|
||||
|
||||
@@ -197,7 +197,7 @@ public class SalesManagementController {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
LambdaQueryWrapper<SalesManagement> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(SalesManagement::getLoginAccount, loginAccount);
|
||||
queryWrapper.like(SalesManagement::getLoginAccount, loginAccount);
|
||||
List<SalesManagement> salesList = salesManagementService.list(queryWrapper);
|
||||
result.put("success", true);
|
||||
result.put("message", "查询成功");
|
||||
|
||||
@@ -21,6 +21,17 @@ public interface IAudioTextAnalysisLlmService {
|
||||
*/
|
||||
String generateSummaryByLLM(AudioAnalysisSceneType sceneType, String recordingText);
|
||||
|
||||
/**
|
||||
* 与 {@link #generateSummaryByLLM} 相同的提示词与场景逻辑,但通过本地 OpenAI 兼容接口调用大模型
|
||||
* (配置项与客户问答本地模型一致:{@code ai.qa.local.chat-url}、{@code ai.qa.local.default-model}、{@code ai.qa.local.max-tokens};
|
||||
* 另支持 {@code ai.qa.local.context-length}、{@code ai.qa.local.token-margin}、{@code ai.qa.local.min-output-tokens}、{@code ai.qa.local.input-tokens-per-char} 以避免超出本地模型上下文。)
|
||||
*
|
||||
* @param sceneType 业务场景类型
|
||||
* @param recordingText 录音转写文本
|
||||
* @return 大模型返回的原始内容
|
||||
*/
|
||||
String generateSummaryByLocalLLM(AudioAnalysisSceneType sceneType, String recordingText);
|
||||
|
||||
/**
|
||||
* 处理完整的业务逻辑:调用大模型生成总结,解析JSON,保存数据
|
||||
* 此方法用于家具场景的完整业务处理
|
||||
|
||||
@@ -21,10 +21,16 @@ import com.rj.service.IAudioTextAnalysisFurnitureService;
|
||||
import com.rj.service.IAudioTextAnalysisLlmService;
|
||||
import com.rj.service.IAudioTextAnalysisSopService;
|
||||
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 lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -33,6 +39,9 @@ import java.util.Map;
|
||||
import java.util.Scanner;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* 音频文本分析 - 大模型通用服务实现
|
||||
@@ -59,6 +68,54 @@ public class AudioTextAnalysisLlmServiceImpl implements IAudioTextAnalysisLlmSer
|
||||
@Autowired
|
||||
private IAudioTextAnalysisSopService sopService;
|
||||
|
||||
@Value("${ai.qa.local.chat-url:http://192.168.1.44:8000/v1/chat/completions}")
|
||||
private String chatCompletionsUrl;
|
||||
|
||||
@Value("${ai.qa.local.default-model:Qwen2.5-7B-Instruct}")
|
||||
private String localDefaultModel;
|
||||
|
||||
/** 单次补全上限(OpenAI max_tokens);与「模型总上下文 context-length」不是同一概念 */
|
||||
@Value("${ai.qa.local.max-tokens:512}")
|
||||
private int localMaxTokens;
|
||||
|
||||
/** 本地模型上下文长度(与推理侧 max context 一致,用于避免 input+max_tokens 超过上限;默认 16384) */
|
||||
@Value("${ai.qa.local.context-length:16384}")
|
||||
private int localContextLength;
|
||||
|
||||
/** 预留余量,避免服务端计数与本地估算不一致 */
|
||||
@Value("${ai.qa.local.token-margin:80}")
|
||||
private int localTokenMargin;
|
||||
|
||||
/**
|
||||
* 截断转写时希望保留的「最小输出预算」启发值(须显著小于 context-length,且不得大于 max-tokens,
|
||||
* 因为实际 effMax 恒为 min(max-tokens, 剩余上下文))。
|
||||
*/
|
||||
@Value("${ai.qa.local.min-output-tokens:128}")
|
||||
private int configuredLocalMinOutputTokens;
|
||||
|
||||
/** {@link #configuredLocalMinOutputTokens} 经与 max-tokens 对齐后的实际取值 */
|
||||
private int effectiveLocalMinOutputTokens;
|
||||
|
||||
/**
|
||||
* 对完整 prompt 字符串做输入 token 的保守上界估算(偏大会多截断、偏少仍可能 400)。
|
||||
* 中文为主可保持默认;英文偏多时可酌情调低(如 0.35)。
|
||||
*/
|
||||
@Value("${ai.qa.local.input-tokens-per-char:1.25}")
|
||||
private double localInputTokensPerChar;
|
||||
|
||||
@PostConstruct
|
||||
void clampLocalMinOutputTokens() {
|
||||
effectiveLocalMinOutputTokens = Math.max(1, Math.min(configuredLocalMinOutputTokens, localMaxTokens));
|
||||
if (effectiveLocalMinOutputTokens < configuredLocalMinOutputTokens) {
|
||||
log.warn(
|
||||
"ai.qa.local.min-output-tokens={} 大于 ai.qa.local.max-tokens={}:有效输出 effMax 恒 ≤ max-tokens,已钳制为 {}。"
|
||||
+ " 切勿将 min-output-tokens 配成与 context-length 相同。",
|
||||
configuredLocalMinOutputTokens,
|
||||
localMaxTokens,
|
||||
effectiveLocalMinOutputTokens);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateSummaryByLLM(AudioAnalysisSceneType sceneType, String recordingText) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
@@ -112,6 +169,195 @@ public class AudioTextAnalysisLlmServiceImpl implements IAudioTextAnalysisLlmSer
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateSummaryByLocalLLM(AudioAnalysisSceneType sceneType, String recordingText) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
log.info("开始调用本地大模型生成总结,场景: {}", sceneType);
|
||||
|
||||
String systemPrompt = getSystemPrompt(sceneType);
|
||||
String userPromptTemplate = getUserPromptTemplate(sceneType);
|
||||
String recordingForPrompt = fitRecordingTextToLocalContext(systemPrompt, userPromptTemplate, recordingText);
|
||||
String userPrompt = buildPrompt(userPromptTemplate, recordingForPrompt);
|
||||
|
||||
try {
|
||||
int effectiveMaxTokens = computeEffectiveLocalMaxTokens(systemPrompt, userPrompt);
|
||||
String rawContent = callLocalOpenAiChatCompletionsByLangChain4j(
|
||||
systemPrompt,
|
||||
userPrompt,
|
||||
localDefaultModel,
|
||||
effectiveMaxTokens);
|
||||
log.info("本地大模型生成总结完成,场景: {}, 结果长度: {}",
|
||||
sceneType, rawContent != null ? rawContent.length() : 0);
|
||||
return rawContent;
|
||||
} catch (Exception e) {
|
||||
log.error("调用本地大模型生成总结失败, 场景: {}", sceneType, e);
|
||||
throw new RuntimeException("调用本地大模型失败: " + e.getMessage(), e);
|
||||
} finally {
|
||||
long endTime = System.currentTimeMillis();
|
||||
long durationMillis = endTime - startTime;
|
||||
double durationSeconds = durationMillis / 1000.0;
|
||||
double durationMinutes = durationSeconds / 60.0;
|
||||
log.info("调用本地大模型耗时: {} 毫秒, {} 秒, {} 分钟, 场景: {}",
|
||||
durationMillis,
|
||||
String.format("%.2f", durationSeconds),
|
||||
String.format("%.2f", durationMinutes),
|
||||
sceneType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 与 {@link AiQaCustomerAskServiceImpl} 中非流式问答路径一致:LangChain4j + OpenAI 兼容接口,流式聚合为完整文本。
|
||||
*/
|
||||
private String callLocalOpenAiChatCompletionsByLangChain4j(
|
||||
String systemPrompt, String userContent, String model, int maxTokens) {
|
||||
log.info("音频分析本地 LLM:model={}, chatCompletionsUrl={}", model, chatCompletionsUrl);
|
||||
String openAiBaseUrl = normalizeOpenAiBaseUrl(chatCompletionsUrl);
|
||||
OpenAiStreamingChatModel chatModel = OpenAiStreamingChatModel.builder()
|
||||
.baseUrl(openAiBaseUrl)
|
||||
.apiKey("not-used")
|
||||
.modelName(model)
|
||||
.maxTokens(maxTokens)
|
||||
.build();
|
||||
|
||||
String prompt = buildFullLocalLlmPrompt(systemPrompt, userContent);
|
||||
StringBuilder answerBuffer = new StringBuilder();
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
AtomicReference<Throwable> errorRef = new AtomicReference<>();
|
||||
|
||||
chatModel.chat(prompt, new StreamingChatResponseHandler() {
|
||||
@Override
|
||||
public void onPartialResponse(String partialResponse) {
|
||||
if (partialResponse != null && !partialResponse.isEmpty()) {
|
||||
answerBuffer.append(partialResponse);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleteResponse(ChatResponse completeResponse) {
|
||||
done.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
errorRef.set(error);
|
||||
done.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
boolean finished = done.await(120, TimeUnit.SECONDS);
|
||||
if (!finished) {
|
||||
throw new IllegalStateException("本地大模型流式调用超时(120s)");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("本地大模型流式调用被中断", e);
|
||||
}
|
||||
|
||||
if (errorRef.get() != null) {
|
||||
throw new IllegalStateException("本地大模型流式调用失败: " + errorRef.get().getMessage(), errorRef.get());
|
||||
}
|
||||
|
||||
String answer = answerBuffer.toString();
|
||||
if (answer.isEmpty()) {
|
||||
throw new IllegalStateException("本地大模型流式调用返回空响应");
|
||||
}
|
||||
return answer;
|
||||
}
|
||||
|
||||
private String normalizeOpenAiBaseUrl(String rawUrl) {
|
||||
if (rawUrl == null || rawUrl.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("ai.qa.local.chat-url 不能为空");
|
||||
}
|
||||
String normalized = rawUrl.trim();
|
||||
while (normalized.endsWith("/")) {
|
||||
normalized = normalized.substring(0, normalized.length() - 1);
|
||||
}
|
||||
String suffix = "/chat/completions";
|
||||
if (normalized.endsWith(suffix)) {
|
||||
normalized = normalized.substring(0, normalized.length() - suffix.length());
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private String buildFullLocalLlmPrompt(String systemPrompt, String userContent) {
|
||||
return "系统指令:" + systemPrompt + "\n\n用户输入:" + userContent;
|
||||
}
|
||||
|
||||
private int estimateLocalPromptInputTokens(String fullPrompt) {
|
||||
if (fullPrompt == null || fullPrompt.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
return (int) Math.ceil(fullPrompt.length() * localInputTokensPerChar);
|
||||
}
|
||||
|
||||
private int computeEffectiveLocalMaxTokens(String systemPrompt, String userPrompt) {
|
||||
String full = buildFullLocalLlmPrompt(systemPrompt, userPrompt);
|
||||
int estIn = estimateLocalPromptInputTokens(full);
|
||||
int capByContext = localContextLength - localTokenMargin - estIn;
|
||||
int effective = Math.min(localMaxTokens, capByContext);
|
||||
return Math.max(1, effective);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在不超过本地模型上下文的前提下,尽量保留更多转写文本;必要时从尾部截断。
|
||||
*/
|
||||
private String fitRecordingTextToLocalContext(
|
||||
String systemPrompt, String userPromptTemplate, String recordingText) {
|
||||
String recording = recordingText != null ? recordingText : "";
|
||||
if (recording.isEmpty()) {
|
||||
return recording;
|
||||
}
|
||||
String userFull = buildPrompt(userPromptTemplate, recording);
|
||||
String full = buildFullLocalLlmPrompt(systemPrompt, userFull);
|
||||
int est = estimateLocalPromptInputTokens(full);
|
||||
int effMax = Math.min(localMaxTokens, localContextLength - localTokenMargin - est);
|
||||
if (effMax >= effectiveLocalMinOutputTokens) {
|
||||
return recording;
|
||||
}
|
||||
int lo = 0;
|
||||
int hi = recording.length();
|
||||
int best = 0;
|
||||
while (lo <= hi) {
|
||||
int mid = (lo + hi) >>> 1;
|
||||
String sub = recording.substring(0, mid);
|
||||
userFull = buildPrompt(userPromptTemplate, sub);
|
||||
full = buildFullLocalLlmPrompt(systemPrompt, userFull);
|
||||
est = estimateLocalPromptInputTokens(full);
|
||||
effMax = Math.min(localMaxTokens, localContextLength - localTokenMargin - est);
|
||||
if (effMax >= effectiveLocalMinOutputTokens) {
|
||||
best = mid;
|
||||
lo = mid + 1;
|
||||
} else {
|
||||
hi = mid - 1;
|
||||
}
|
||||
}
|
||||
if (best >= recording.length()) {
|
||||
return recording;
|
||||
}
|
||||
if (best <= 0) {
|
||||
userFull = buildPrompt(userPromptTemplate, "");
|
||||
full = buildFullLocalLlmPrompt(systemPrompt, userFull);
|
||||
int estEmpty = estimateLocalPromptInputTokens(full);
|
||||
int effMaxEmpty = Math.min(localMaxTokens, localContextLength - localTokenMargin - estEmpty);
|
||||
log.warn(
|
||||
"本地 LLM 上下文过紧:无转写时估算剩余输出预算 effMax≈{}(max-tokens={}),仍小于 effective min-output-tokens={}"
|
||||
+ "(配置 min-output-tokens={})。请减小提示词、提高 context-length,或降低 min-output-tokens / 提高 max-tokens。将仍尝试调用。",
|
||||
effMaxEmpty,
|
||||
localMaxTokens,
|
||||
effectiveLocalMinOutputTokens,
|
||||
configuredLocalMinOutputTokens);
|
||||
return "";
|
||||
}
|
||||
log.warn(
|
||||
"录音转写过长,已按本地模型上下文截断:原长度 {} 字符,保留 {} 字符(context-length={},max-tokens={})",
|
||||
recording.length(),
|
||||
best,
|
||||
localContextLength,
|
||||
localMaxTokens);
|
||||
return recording.substring(0, best);
|
||||
}
|
||||
|
||||
private String buildPrompt(String template, String recordingText) {
|
||||
if (template == null) {
|
||||
return recordingText != null ? recordingText : "";
|
||||
@@ -245,10 +491,18 @@ public class AudioTextAnalysisLlmServiceImpl implements IAudioTextAnalysisLlmSer
|
||||
String ownerPhone,
|
||||
String customerName,
|
||||
String customerPhone) {
|
||||
|
||||
// 1. 调用大模型生成总结
|
||||
String rawContent = generateSummaryByLocalLLM(sceneType, recordingText);
|
||||
if (rawContent == null || rawContent.trim().isEmpty()) {
|
||||
log.warn("大模型返回内容为空");
|
||||
return null;
|
||||
}
|
||||
AudioTextAnalysisFurniture furniture = new AudioTextAnalysisFurniture();
|
||||
furniture.setRecordingText(recordingText);
|
||||
|
||||
// 4. 更新AudioManagement的summary字段
|
||||
AudioManagement audioManagement = new AudioManagement();
|
||||
audioManagement.setId(parentId);
|
||||
audioManagement.setSummary(rawContent);
|
||||
audioManagementService.updateById(audioManagement);
|
||||
|
||||
return furniture;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user