From 70c6f1c789591e58e4cf69efcfd354b86b041a05 Mon Sep 17 00:00:00 2001 From: "qinyong@9artedu.com" Date: Mon, 3 Aug 2026 09:51:54 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8F=90=E7=A4=BA=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/GenerationController.java | 18 ++-- .../generation/service/PromptEngine.java | 97 +++++++++++++++---- 2 files changed, 88 insertions(+), 27 deletions(-) diff --git a/backend/generation-service/src/main/java/com/artedu/generation/controller/GenerationController.java b/backend/generation-service/src/main/java/com/artedu/generation/controller/GenerationController.java index ecb3921..1f7f440 100644 --- a/backend/generation-service/src/main/java/com/artedu/generation/controller/GenerationController.java +++ b/backend/generation-service/src/main/java/com/artedu/generation/controller/GenerationController.java @@ -34,6 +34,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; import java.util.stream.Collectors; @Slf4j @@ -62,6 +63,9 @@ public class GenerationController { private final ExecutorService sseExecutor = Executors.newCachedThreadPool(); private final ExecutorService llmExecutor = Executors.newCachedThreadPool(); + /** SSE Emitter 工厂:默认创建 5 分钟超时的真实 Emitter;单测可注入可控实现以脱离 HTTP 客户端 */ + private Supplier emitterFactory = () -> new SseEmitter(600000L); + private static final List> STYLES = List.of( Map.of("id", "friendly", "name", "亲和风格", "desc", "温暖亲切,像朋友聊天"), Map.of("id", "professional", "name", "专业风格", "desc", "数据驱动,专业权威"), @@ -289,7 +293,7 @@ public class GenerationController { @RequestParam(value = "staffName", required = false) String staffName) { log.info("流式生成多风格话术参数:{},{},{},{}", fromUser, toUser, roomid, staffName); // 生成过程可能超过 30s,SSE 总超时放宽到 5 分钟 - SseEmitter emitter = new SseEmitter(300000L); + SseEmitter emitter = emitterFactory.get(); sseExecutor.execute(() -> { try { @@ -434,12 +438,12 @@ public class GenerationController { * 在后台线程执行 LLM 生成,返回最终话术文本 */ private String generateUtteranceText(List top15, String chatContext, - String external_str, String staffName) throws Exception { + String externalStr, String staffName) throws Exception { String generated = ""; //如果最后几条都是客户消息返回不为空,则直接返回结果,否则走原逻辑 - if (!StringUtils.isBlank(external_str)) { //不为空 发起知识库的查询 - String answer = getAnswer(top15, external_str); - log.info("原问题:{},命中知识库结果:{}", external_str, answer); + if (!StringUtils.isBlank(externalStr)) { //不为空 发起知识库的查询 + String answer = getAnswer(top15, externalStr); + log.info("原问题:{},命中知识库结果:{}", externalStr, answer); if (!StringUtils.isBlank(answer)) { ObjectMapper mapper = new ObjectMapper(); List list = mapper.readValue(answer, new TypeReference>(){}); @@ -453,7 +457,7 @@ public class GenerationController { String[] parts = generated.split("\\|\\|\\|"); if (parts.length < 3) { for (int i = 0; i < 3 - parts.length; i++) { - String systemPrompt = promptEngine.buildPrompt(chatContext, staffName); + String systemPrompt = promptEngine.buildPrompt(chatContext, staffName, externalStr); String generatedNew = qianwenClient.generate(systemPrompt, "", 1200); generated = generated + generatedNew; } @@ -464,7 +468,7 @@ public class GenerationController { } if (StringUtils.isBlank(generated)) { - String systemPrompt = promptEngine.buildSystemPrompt(chatContext, staffName); + String systemPrompt = promptEngine.buildSystemNewPrompt(chatContext, staffName, externalStr); String userPrompt = promptEngine.buildMultiStyleUserPrompt(staffName); long startTime = System.currentTimeMillis(); diff --git a/backend/generation-service/src/main/java/com/artedu/generation/service/PromptEngine.java b/backend/generation-service/src/main/java/com/artedu/generation/service/PromptEngine.java index 84422a0..ade87a1 100644 --- a/backend/generation-service/src/main/java/com/artedu/generation/service/PromptEngine.java +++ b/backend/generation-service/src/main/java/com/artedu/generation/service/PromptEngine.java @@ -36,32 +36,89 @@ public class PromptEngine { return sb.toString(); } /** - * 构建生成话术的提示词(角色设定) + * 构建生成话术的系统提示词(角色设定) */ - public String buildPrompt(String chatHistoryContext, String staffName) { + public String buildSystemNewPrompt(String chatHistoryContext, String staffName, String externalStr) { StringBuilder sb = new StringBuilder(); - sb.append("你是第九联盟培训机构资深销售,仅输出符合规范的 3 条推荐回复话术,全程禁止输出任何对话分析、客户判断、前置解读、总结说明文字,任何分析类文字一律不写,正文只保留 3 条话术条目。\n"); + sb.append("你是第九联盟培训机构资深销售顾问。你的唯一任务是:基于「最新的问题」生成 3 条推荐回复话术。\n\n"); + + sb.append("## 输入数据(按优先级处理)\n"); + sb.append("【最高优先级 · 必须直接回应】\n"); + sb.append("最新的问题:").append(externalStr).append("\n\n"); + sb.append("【参考上下文 · 仅辅助理解背景】\n"); + sb.append("对话历史:").append(chatHistoryContext).append("\n"); + sb.append("(说明:INTERNAL=销售,EXTERNAL=客户)\n\n"); + + sb.append("## 核心任务\n"); + sb.append("1. 首先判断「最新的问题」是否为结束语(如'好的谢谢'、'我先考虑下'、'再见'、'不用了'等)。\n"); + sb.append(" - 若是结束语:生成 3 条礼貌、得体、留有余地的结束回复,不强行推销。\n"); + sb.append(" - 若不是结束语:针对最新问题直接回应,对话历史仅用于避免重复、保持连贯,不得让历史话题覆盖最新问题。\n"); + sb.append("2. 内部自动完成 5 项分析(客户意图、情绪状态、购买阶段、关键信号、未解决问题),仅用于生成话术,严禁输出。\n\n"); + + sb.append("## 输出铁律(违反任何一条即不合格)\n"); + sb.append("- 仅输出 3 条话术条目,严禁输出任何分析、解读、总结、标题、过渡段、前置说明。\n"); + sb.append("- 单条话术≤100 字,整体风格专业、自然、有温度,像真人销售顾问在沟通。\n"); + sb.append("- 禁止输出:客户情况总结、对话解读、阶段判断、需求分析、铺垫文字、总述段落。\n\n"); + if (staffName != null && !staffName.isEmpty()) { - sb.append("## 重要约束\n"); - sb.append("你的名字是「").append(staffName).append("」,在回复中如果需要自称或介绍自己,必须使用这个名字,绝对禁止编造任何其他名字(如小王、小李、小张等)。\n"); + sb.append("## 身份约束\n"); + sb.append("你的名字是「").append(staffName).append("」。如需自称或介绍,必须使用此名,绝对禁止编造其他名字(如小王、小李等)。\n\n"); } - sb.append("## 对话历史\n"); + sb.append("## 内容规范\n"); + sb.append("1. 严禁编造具体金额、折扣比例、名额数量、截止日期等精确数值;缺乏准确数据时,使用模糊表述(如'一定减免'、'额外优惠')或引导私信/电话确认。\n"); + sb.append("2. 若用户附带参考文本:优先基于参考文本,再用自有知识补充,不得与参考文本事实冲突。\n"); + sb.append("3. 必须引用价格时,使用【具体金额以官方最新政策为准】等占位符,禁止直接生成数字。\n"); + sb.append("4. 回答紧扣用户最新提问,不主动脑补用户未提出的需求,不随意延伸无关内容。\n"); + return sb.toString(); + } + /** + * 构建生成话术的提示词(角色设定) + */ + public String buildPrompt(String chatHistoryContext, String staffName, String externalStr) { + StringBuilder sb = new StringBuilder(); + + sb.append("你是第九联盟培训机构的资深销售顾问。你的唯一任务是:根据下方信息,输出 3 条推荐回复话术。\n\n"); + +// === 核心优先级:最新问题永远是第一响应目标 === + sb.append("## 最高优先级:最新问题(必须优先直接回应)\n"); + sb.append(externalStr); + sb.append("\n\n"); + +// === 对话历史降级为参考 === + sb.append("## 参考上下文:对话历史(仅作背景参考,不优先回应历史话题)\n"); sb.append(chatHistoryContext); - sb.append("\n"); - sb.append("## 说明\n"); - sb.append("其中INTERNAL表示销售,EXTERNAL表示客户\n"); - sb.append(" ## 内置分析逻辑(仅 AI 内部运算,绝对不能打印 / 展示出来)\n"); - sb.append("内部自动完成 5 项分析,仅用于生成话术,不对外输出:1. 客户意图;2. 情绪状态;3. 购买阶段;4. 关键信号;5. 未解决问题\n"); - sb.append("## 硬性输出铁律(违规判定:出现任何分析文字直接不合格)\n"); - sb.append("禁止输出:客户情况总结、对话解读、阶段判断、需求分析、前置铺垫、过渡介绍、标题说明、总述段落;\n"); - sb.append("单条话术文本≤100 字,整体风格专业、自然、有温度。\n"); - sb.append("##规范"); - sb.append("1. 你可以调用自身储备的可靠知识进行回答、推理、拓展。\n" + - "2. 区分信息可信度:确定的常识/成熟理论直接输出;存在多种说法、没有统一结论的内容,主动说明存在不同观点。\n" + - "3. 严禁编造不存在的数据、案例、文档、人名、规则、专业定义。不确定、没有把握的内容,直接说明“暂无确切信息”,禁止依靠想象强行给出答案。\n" + - "4. 如果用户附带参考文本:优先基于参考文本内容,再使用自有知识补充拓展,不能和参考文本事实冲突。\n" + - "5. 不要随意延伸无关内容,回答紧扣用户提问,不主动脑补用户未提出的需求。"); + sb.append("\n\n"); + +// === 身份约束 === + if (staffName != null && !staffName.isEmpty()) { + sb.append("## 身份约束\n"); + sb.append("你的名字是「").append(staffName).append("」,在回复中如需自称或介绍自己,必须使用此名字,绝对禁止编造其他任何名字(如小王、小李、小张等)。\n\n"); + } + +// === 内置分析(不可输出) === + sb.append("## 内置分析逻辑(仅 AI 内部运算,严禁输出)\n"); + sb.append("请在内部自动完成以下 5 项分析,仅用于生成话术,不对外输出:\n"); + sb.append("1. 客户意图;2. 情绪状态;3. 购买阶段;4. 关键信号;5. 未解决问题。\n\n"); + +// === 结束语识别分支 === + sb.append("## 结束语识别规则\n"); + sb.append("若最新问题属于结束语(如“好的”“谢谢”“我先考虑一下”“回头再说”“拜拜”“再见”等),则输出 3 条简短、礼貌、得体的结束回复,不强行推销、不追问、不延伸新话题。\n\n"); + +// === 输出铁律 === + sb.append("## 硬性输出铁律(违规即不合格)\n"); + sb.append("- 严禁输出:客户情况总结、对话解读、阶段判断、需求分析、前置铺垫、过渡介绍、标题说明、总述段落、任何分析类文字。\n"); + sb.append("- 正文只保留 3 条话术条目,每条话术文本 ≤100 字。\n"); + sb.append("- 整体风格:专业、自然、有温度,像真人销售顾问在沟通。\n\n"); + +// === 内容规范 === + sb.append("## 内容规范\n"); + sb.append("1. 严禁编造具体金额、折扣比例、名额数量、截止日期等精确数值;如缺乏准确数据,禁止填写任何数字,使用模糊表述(如“一定减免”“额外优惠”“具体金额我帮您确认”)或引导用户私信/电话确认细节。\n"); + sb.append("2. 若话术包含任何未经核实的数字,全部替换为模糊表述或占位符。\n"); + sb.append("3. 必须引用价格时,使用【具体金额以官方最新政策为准】等占位符,禁止直接生成数字。\n"); + sb.append("4. 若用户附带参考文本:优先基于参考文本内容,再使用自有知识补充拓展,不得与参考文本事实冲突。\n"); + sb.append("5. 不要随意延伸无关内容,回答紧扣用户最新提问,不主动脑补用户未提出的需求。\n"); + return sb.toString(); }