提示词完善

This commit is contained in:
2026-04-05 15:10:40 +08:00
parent 7fb2ed53b7
commit db0bde39e3
7 changed files with 293 additions and 14 deletions

View File

@@ -15,8 +15,8 @@ public enum AudioAnalysisSceneType {
"qwen-plus" "qwen-plus"
), ),
SCENARIO_SOFT_SALE( SCENARIO_SOFT_SALE(
"prompts/audio_text_analysis_furniture_system.txt", "prompts/audio_text_analysis_soft_sale_system_prompts.txt",
"prompts/audio_text_analysis_furniture_user.txt", "prompts/audio_text_analysis_soft_sale_user_prompts.txt",
"qwen-plus" "qwen-plus"
), ),
@@ -24,9 +24,9 @@ public enum AudioAnalysisSceneType {
* 会议纪要/会议分析场景 * 会议纪要/会议分析场景
* 提示词文件和模型可根据实际需要进行调整。 * 提示词文件和模型可根据实际需要进行调整。
*/ */
SCENARIO_COMMON_SALE( SCENARIO_SUMMARY(
"prompts/audio_text_analysis_meeting_system.txt", "prompts/audio_text_analysis_summary_system_prompts.txt",
"prompts/audio_text_analysis_meeting_user.txt", "prompts/audio_text_analysis_summary_user_prompts.txt",
"qwen-plus" "qwen-plus"
), ),

View File

@@ -49,7 +49,7 @@ public class AudioManagementController {
private static final String SCENARIO_FURNITURE_SALE = "FURNITURE"; private static final String SCENARIO_FURNITURE_SALE = "FURNITURE";
private static final String SCENARIO_MEETING_SUMMARY = "MEETING_SUMMARY"; private static final String SCENARIO_MEETING_SUMMARY = "MEETING_SUMMARY";
private static final String SCENARIO_CAR_SALE = "CAR_SALE"; 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"; //租赁模式 private static final String SCENARIO_SPEAKING_TRAINING= "SPEAKING_TRAINING"; //租赁模式
@@ -510,6 +510,7 @@ public class AudioManagementController {
customerToUpdate.setDetailedAddress(audioManagement.getRemarks()); customerToUpdate.setDetailedAddress(audioManagement.getRemarks());
customerToUpdate.setSalesPhone(audioManagement.getSalesPhone()); customerToUpdate.setSalesPhone(audioManagement.getSalesPhone());
customerToUpdate.setSalesName(audioManagement.getSalesName()); customerToUpdate.setSalesName(audioManagement.getSalesName());
customerToUpdate.setRecordingCount(customerToUpdate.getRecordingCount()+1);
customerToUpdate.setUpdateTime(LocalDateTime.now()); customerToUpdate.setUpdateTime(LocalDateTime.now());
customerManagementService.updateById(customerToUpdate); customerManagementService.updateById(customerToUpdate);
log.info("同步更新客户信息成功客户ID: {}", customerToUpdate.getId()); log.info("同步更新客户信息成功客户ID: {}", customerToUpdate.getId());
@@ -728,6 +729,15 @@ public class AudioManagementController {
audioManagement.getCustomerName(), audioManagement.getCustomerName(),
audioManagement.getCustomerPhone() 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() ); log.info("llm return furniture Analysis : ",furniture.toString() );
if (recordingText == null || recordingText.trim().isEmpty()) { if (recordingText == null || recordingText.trim().isEmpty()) {
result.put("success", false); result.put("success", false);
@@ -762,8 +772,8 @@ public class AudioManagementController {
if (SCENARIO_FURNITURE_SALE.equals(s)) { if (SCENARIO_FURNITURE_SALE.equals(s)) {
return AudioAnalysisSceneType.SCENARIO_FURNITURE_SALE; return AudioAnalysisSceneType.SCENARIO_FURNITURE_SALE;
} }
if (SCENARIO_COMMON_SALE.equals(s)) { if (SCENARIO_SUMMARY.equals(s)) {
return AudioAnalysisSceneType.SCENARIO_COMMON_SALE; return AudioAnalysisSceneType.SCENARIO_SUMMARY;
} }
if (SCENARIO_CAR_SALE.equals(s)) { if (SCENARIO_CAR_SALE.equals(s)) {
return AudioAnalysisSceneType.SCENARIO_CAR_SALE; return AudioAnalysisSceneType.SCENARIO_CAR_SALE;

View File

@@ -197,7 +197,7 @@ public class SalesManagementController {
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
try { try {
LambdaQueryWrapper<SalesManagement> queryWrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<SalesManagement> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SalesManagement::getLoginAccount, loginAccount); queryWrapper.like(SalesManagement::getLoginAccount, loginAccount);
List<SalesManagement> salesList = salesManagementService.list(queryWrapper); List<SalesManagement> salesList = salesManagementService.list(queryWrapper);
result.put("success", true); result.put("success", true);
result.put("message", "查询成功"); result.put("message", "查询成功");

View File

@@ -21,6 +21,17 @@ public interface IAudioTextAnalysisLlmService {
*/ */
String generateSummaryByLLM(AudioAnalysisSceneType sceneType, String recordingText); 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保存数据 * 处理完整的业务逻辑调用大模型生成总结解析JSON保存数据
* 此方法用于家具场景的完整业务处理 * 此方法用于家具场景的完整业务处理

View File

@@ -21,10 +21,16 @@ import com.rj.service.IAudioTextAnalysisFurnitureService;
import com.rj.service.IAudioTextAnalysisLlmService; import com.rj.service.IAudioTextAnalysisLlmService;
import com.rj.service.IAudioTextAnalysisSopService; import com.rj.service.IAudioTextAnalysisSopService;
import com.rj.service.ITodoItemService; 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 lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import jakarta.annotation.PostConstruct;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -33,6 +39,9 @@ import java.util.Map;
import java.util.Scanner; import java.util.Scanner;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; 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 @Autowired
private IAudioTextAnalysisSopService sopService; 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 @Override
public String generateSummaryByLLM(AudioAnalysisSceneType sceneType, String recordingText) { public String generateSummaryByLLM(AudioAnalysisSceneType sceneType, String recordingText) {
long startTime = System.currentTimeMillis(); 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("音频分析本地 LLMmodel={}, 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) { private String buildPrompt(String template, String recordingText) {
if (template == null) { if (template == null) {
return recordingText != null ? recordingText : ""; return recordingText != null ? recordingText : "";
@@ -245,10 +491,18 @@ public class AudioTextAnalysisLlmServiceImpl implements IAudioTextAnalysisLlmSer
String ownerPhone, String ownerPhone,
String customerName, String customerName,
String customerPhone) { String customerPhone) {
// 1. 调用大模型生成总结
String rawContent = generateSummaryByLocalLLM(sceneType, recordingText);
if (rawContent == null || rawContent.trim().isEmpty()) {
log.warn("大模型返回内容为空");
return null;
}
AudioTextAnalysisFurniture furniture = new AudioTextAnalysisFurniture(); 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; return furniture;
} }

View File

@@ -16,7 +16,11 @@ ai:
local: local:
chat-url: http://192.168.1.44:8000/v1/chat/completions chat-url: http://192.168.1.44:8000/v1/chat/completions
default-model: Qwen2.5-7B-Instruct default-model: Qwen2.5-7B-Instruct
max-tokens: 512 # 模型总上下文(与推理服务一致);勿与 max-tokens、min-output-tokens 混用同一数值
context-length: 16384
# 单次补全上限。过小(如 512会导致课堂纪要等 JSON 总结被截断、字段残缺或不准;建议 20484096须保证 input+max_tokens 不超过 context-length
max-tokens: 2048
min-output-tokens: 128
langchain4j: langchain4j:
open-ai: open-ai:

View File

@@ -1,4 +1,4 @@
请阅读以下录音文本,从中提取客户信息并生成 JSON。必须覆盖所有字段缺失信息请基于语境合理推断或标注"暂无信息"。 请阅读以下录音文本,从中提取重要关键信息并生成 JSON。必须覆盖所有字段缺失信息请基于语境合理推断或标注"暂无信息"。
字段要求: 字段要求:
1. style_type主题类型仅可填「会议纪要」「课堂纪要」「面试纪要」。 1. style_type主题类型仅可填「会议纪要」「课堂纪要」「面试纪要」。