diff --git a/backend/generation-service/src/main/java/com/artedu/generation/client/DoubaoClient.java b/backend/generation-service/src/main/java/com/artedu/generation/client/DoubaoClient.java index 219e863..76f2bad 100644 --- a/backend/generation-service/src/main/java/com/artedu/generation/client/DoubaoClient.java +++ b/backend/generation-service/src/main/java/com/artedu/generation/client/DoubaoClient.java @@ -59,6 +59,11 @@ public class DoubaoClient implements LLMClient { if (maxTokens != null) { requestBody.put("max_tokens", maxTokens); } + // 从 configJson 读取 temperature,默认 0.7 + Double temperature = parseTemperature(); + if (temperature != null) { + requestBody.put("temperature", temperature); + } try { HttpEntity> entity = new HttpEntity<>(requestBody, headers); @@ -100,4 +105,25 @@ public class DoubaoClient implements LLMClient { log.warn("豆包客户端暂不支持embedding,返回空向量"); return List.of(); } + + /** + * 从 configJson 中解析 temperature 参数 + */ + private Double parseTemperature() { + try { + String configJson = model.getConfigJson(); + if (configJson != null && !configJson.isEmpty()) { + Map config = JsonUtils.fromJsonMap(configJson); + if (config != null && config.get("temperature") != null) { + Object temp = config.get("temperature"); + if (temp instanceof Number) { + return ((Number) temp).doubleValue(); + } + } + } + } catch (Exception e) { + log.warn("解析 temperature 配置失败: {}", e.getMessage()); + } + return 0.7; + } } 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 90bb25a..abeeb62 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,16 +34,26 @@ public class GenerationController { public Result> generateUtterance( @NotBlank(message = "message不能为空") @RequestParam("message") String message, @RequestParam(value = "context", required = false) String context, + @RequestParam(value = "customerProfile", required = false) String customerProfile, + @RequestParam(value = "conversationContext", required = false) String conversationContext, + @RequestParam(value = "referenceUtterances", required = false) String referenceUtterances, @NotBlank(message = "intent不能为空") @RequestParam("intent") String intent, - @NotBlank(message = "stage不能为空") @RequestParam("stage") String stage) { + @NotBlank(message = "stage不能为空") @RequestParam("stage") String stage, + @RequestParam(value = "stageLabel", required = false) String stageLabel, + @RequestParam(value = "intentLabel", required = false) String intentLabel) { - String prompt = promptEngine.buildUtterancePrompt(message, context, intent, stage); - String generated = qianwenClient.generate(prompt, 500); + String[] promptPair = promptEngine.buildUtterancePrompt( + message, context, customerProfile, conversationContext, + referenceUtterances, intent, stage, stageLabel, intentLabel); + String systemPrompt = promptPair[0]; + String userPrompt = promptPair[1]; + String generated = qianwenClient.generate(systemPrompt, userPrompt, 800); Map result = new HashMap<>(); result.put("content", generated); result.put("source", "LLM"); - result.put("model", "qwen-turbo"); + result.put("model", qianwenClient.getCurrentModel() != null + ? qianwenClient.getCurrentModel().getModelName() : "default"); return Result.success(result); } 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 13d3065..2e135d8 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 @@ -7,18 +7,124 @@ import org.springframework.stereotype.Service; @Service public class PromptEngine { - public String buildUtterancePrompt(String message, String context, String intent, String stage) { + /** + * 构建生成话术的系统提示词(角色设定) + */ + public String buildSystemPrompt(String stageLabel, String intentLabel) { StringBuilder sb = new StringBuilder(); - sb.append("你是第九联盟(CG美术培训机构)的课程顾问助手。请根据学员的咨询消息,生成一段专业、友好、有说服力的回复话术。\n\n"); - sb.append("【机构背景】\n"); - sb.append("- 第九联盟专注CG数字艺术教育10年\n"); - sb.append("- 累计培养8000+学员,与腾讯、网易、米哈游等500+企业有就业合作\n"); - sb.append("- 6大校区:上海、西安、厦门、武汉、青岛、合肥\n\n"); - sb.append("【当前对话阶段】").append(stage).append("\n"); - sb.append("【学员意图】").append(intent).append("\n"); - sb.append("【对话上下文】\n").append(context != null ? context : "无").append("\n\n"); - sb.append("【学员最新消息】\n").append(message).append("\n\n"); - sb.append("请直接输出回复话术,不要包含任何解释或前缀。话术要自然、专业、符合顾问身份。"); + sb.append("你是「小九」,第九联盟(CG美术培训)的资深课程顾问,从业8年。"); + sb.append("第九联盟专注CG数字艺术教育10年,累计培养8000+学员,合作企业包括腾讯、网易、米哈游等500+家。"); + sb.append("6大校区:上海、西安、厦门、武汉、青岛、合肥。"); + sb.append("\n\n回复原则:\n"); + sb.append("1. 语气像微信聊天一样自然亲切,不要机械生硬、不要堆砌数据\n"); + sb.append("2. 结合学员具体情况给针对性建议,不要泛泛而谈\n"); + sb.append("3. 适当用学员成功案例或真实数据增强说服力,点到为止\n"); + sb.append("4. 长度控制在150-250字,简洁有力\n"); + sb.append("5. 如知道学员真实姓名则称呼姓名,不知道真实姓名时绝对不要编造名字,可以直接用自然的方式开头\n"); + if (stageLabel != null && !stageLabel.isEmpty()) { + sb.append("6. 当前处于「").append(stageLabel).append("」阶段,回复要匹配该阶段的沟通目标\n"); + } + return sb.toString(); + } + + /** + * 构建生成话术的用户提示词(具体任务) + */ + public String buildUserPrompt(String message, String context, + String customerProfile, String conversationContext, + String referenceUtterances, + String intent, String stage, + String stageLabel, String intentLabel) { + StringBuilder sb = new StringBuilder(); + + // 学员画像(精简) + if (customerProfile != null && !customerProfile.isEmpty()) { + sb.append("【学员画像】\n").append(customerProfile).append("\n\n"); + } + + // 对话历史(最多3轮) + String history = null; + if (conversationContext != null && !conversationContext.isEmpty()) { + history = conversationContext; + } else if (context != null && !context.isEmpty()) { + history = context; + } + if (history != null && !history.isEmpty()) { + sb.append("【最近对话】\n").append(history).append("\n\n"); + } + + // 阶段和意图 + String stageDisplay = (stageLabel != null && !stageLabel.isEmpty()) ? stageLabel : stage; + String intentDisplay = (intentLabel != null && !intentLabel.isEmpty()) ? intentLabel : intent; + sb.append("【当前阶段】").append(stageDisplay).append("\n"); + sb.append("【学员意图】").append(intentDisplay).append("\n\n"); + + // 参考话术(Few-shot,仅作为风格参考) + if (referenceUtterances != null && !referenceUtterances.isEmpty()) { + sb.append("【参考话术风格】(学习以下话术的表达方式,不要照搬内容)\n"); + sb.append(referenceUtterances).append("\n\n"); + } + + // 学员最新消息 + 任务 + sb.append("学员最新消息:\"").append(message).append("\"\n\n"); + sb.append("请直接生成回复内容,不要加任何前缀、标记或解释。"); + + return sb.toString(); + } + + /** + * 构建完整话术 Prompt(system + user 分离) + * 返回 String[]{systemPrompt, userPrompt} + */ + public String[] buildUtterancePrompt(String message, String context, + String customerProfile, String conversationContext, + String referenceUtterances, + String intent, String stage, + String stageLabel, String intentLabel) { + String system = buildSystemPrompt(stageLabel, intentLabel); + String user = buildUserPrompt(message, context, customerProfile, conversationContext, + referenceUtterances, intent, stage, stageLabel, intentLabel); + return new String[]{system, user}; + } + + /** + * 旧版兼容方法:返回合并后的单条 prompt(当模型不支持 system 分离时使用) + */ + public String buildUtterancePromptLegacy(String message, String context, + String customerProfile, String conversationContext, + String referenceUtterances, + String intent, String stage, + String stageLabel, String intentLabel) { + StringBuilder sb = new StringBuilder(); + sb.append("你叫「小九」,是第九联盟(CG美术培训)的资深课程顾问,从业8年。"); + sb.append("第九联盟专注CG数字艺术教育10年,累计培养8000+学员。6大校区:上海、西安、厦门、武汉、青岛、合肥。\n\n"); + + if (customerProfile != null && !customerProfile.isEmpty()) { + sb.append("【学员画像】\n").append(customerProfile).append("\n\n"); + } + if (conversationContext != null && !conversationContext.isEmpty()) { + sb.append("【最近对话】\n").append(conversationContext).append("\n\n"); + } else if (context != null && !context.isEmpty()) { + sb.append("【最近对话】\n").append(context).append("\n\n"); + } + + String stageDisplay = (stageLabel != null && !stageLabel.isEmpty()) ? stageLabel : stage; + String intentDisplay = (intentLabel != null && !intentLabel.isEmpty()) ? intentLabel : intent; + sb.append("【当前阶段】").append(stageDisplay).append("\n"); + sb.append("【学员意图】").append(intentDisplay).append("\n\n"); + + if (referenceUtterances != null && !referenceUtterances.isEmpty()) { + sb.append("【参考话术】\n").append(referenceUtterances).append("\n\n"); + } + + sb.append("学员最新消息:\"").append(message).append("\"\n\n"); + sb.append("请生成一段自然、专业、有说服力的回复。要求:\n"); + sb.append("1. 称呼学员姓名,结合其具体情况给针对性建议\n"); + sb.append("2. 语气自然亲切,像微信聊天,不要机械\n"); + sb.append("3. 适当用数据或案例增强说服力,点到为止\n"); + sb.append("4. 150-250字,简洁有力\n"); + sb.append("5. 直接输出回复内容,不要加任何前缀或解释"); + return sb.toString(); } diff --git a/backend/generation-service/src/main/resources/application.yml b/backend/generation-service/src/main/resources/application.yml index 0f1b0cb..2c72491 100644 --- a/backend/generation-service/src/main/resources/application.yml +++ b/backend/generation-service/src/main/resources/application.yml @@ -33,7 +33,7 @@ spring: llm: qianwen: api-key: sk-39ceb8d746014b349109e76f893acb18 - model: qwen-turbo + model: qwen-plus mybatis-plus: configuration: diff --git a/backend/generation-service/target/classes/application.yml b/backend/generation-service/target/classes/application.yml index 0f1b0cb..2c72491 100644 --- a/backend/generation-service/target/classes/application.yml +++ b/backend/generation-service/target/classes/application.yml @@ -33,7 +33,7 @@ spring: llm: qianwen: api-key: sk-39ceb8d746014b349109e76f893acb18 - model: qwen-turbo + model: qwen-plus mybatis-plus: configuration: diff --git a/backend/generation-service/target/classes/com/artedu/generation/GenerationServiceApplication.class b/backend/generation-service/target/classes/com/artedu/generation/GenerationServiceApplication.class index a773fc9..c97fc17 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/GenerationServiceApplication.class and b/backend/generation-service/target/classes/com/artedu/generation/GenerationServiceApplication.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/client/BailianClient.class b/backend/generation-service/target/classes/com/artedu/generation/client/BailianClient.class index db32f54..ea4b949 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/client/BailianClient.class and b/backend/generation-service/target/classes/com/artedu/generation/client/BailianClient.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/client/DoubaoClient.class b/backend/generation-service/target/classes/com/artedu/generation/client/DoubaoClient.class index 2b1e611..831798f 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/client/DoubaoClient.class and b/backend/generation-service/target/classes/com/artedu/generation/client/DoubaoClient.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/client/HunyuanClient.class b/backend/generation-service/target/classes/com/artedu/generation/client/HunyuanClient.class index 16a7601..0fc8ee8 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/client/HunyuanClient.class and b/backend/generation-service/target/classes/com/artedu/generation/client/HunyuanClient.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/client/LLMClient.class b/backend/generation-service/target/classes/com/artedu/generation/client/LLMClient.class index f74e3cb..b724616 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/client/LLMClient.class and b/backend/generation-service/target/classes/com/artedu/generation/client/LLMClient.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/client/LLMClientFactory.class b/backend/generation-service/target/classes/com/artedu/generation/client/LLMClientFactory.class index fe3ef45..6976415 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/client/LLMClientFactory.class and b/backend/generation-service/target/classes/com/artedu/generation/client/LLMClientFactory.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/controller/EmbedRequest.class b/backend/generation-service/target/classes/com/artedu/generation/controller/EmbedRequest.class index a070a4b..8dd733c 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/controller/EmbedRequest.class and b/backend/generation-service/target/classes/com/artedu/generation/controller/EmbedRequest.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/controller/GenerateRequest.class b/backend/generation-service/target/classes/com/artedu/generation/controller/GenerateRequest.class index afe82bc..8c294f8 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/controller/GenerateRequest.class and b/backend/generation-service/target/classes/com/artedu/generation/controller/GenerateRequest.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/controller/GenerationController.class b/backend/generation-service/target/classes/com/artedu/generation/controller/GenerationController.class index 3e7cadc..40286a4 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/controller/GenerationController.class and b/backend/generation-service/target/classes/com/artedu/generation/controller/GenerationController.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/controller/LlmModelController.class b/backend/generation-service/target/classes/com/artedu/generation/controller/LlmModelController.class index 99e0867..ce732ea 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/controller/LlmModelController.class and b/backend/generation-service/target/classes/com/artedu/generation/controller/LlmModelController.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/entity/LlmModel.class b/backend/generation-service/target/classes/com/artedu/generation/entity/LlmModel.class index 1451498..01056bf 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/entity/LlmModel.class and b/backend/generation-service/target/classes/com/artedu/generation/entity/LlmModel.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/mapper/LlmModelMapper.class b/backend/generation-service/target/classes/com/artedu/generation/mapper/LlmModelMapper.class index a66c634..a0e9f9b 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/mapper/LlmModelMapper.class and b/backend/generation-service/target/classes/com/artedu/generation/mapper/LlmModelMapper.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/service/LlmModelService.class b/backend/generation-service/target/classes/com/artedu/generation/service/LlmModelService.class index d9ce4e4..d49d8fe 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/service/LlmModelService.class and b/backend/generation-service/target/classes/com/artedu/generation/service/LlmModelService.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/service/PromptEngine.class b/backend/generation-service/target/classes/com/artedu/generation/service/PromptEngine.class index 163b8c4..ce0ea19 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/service/PromptEngine.class and b/backend/generation-service/target/classes/com/artedu/generation/service/PromptEngine.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/service/QianwenClient.class b/backend/generation-service/target/classes/com/artedu/generation/service/QianwenClient.class index 7f7cf2c..84fa7e0 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/service/QianwenClient.class and b/backend/generation-service/target/classes/com/artedu/generation/service/QianwenClient.class differ diff --git a/backend/generation-service/target/generation-service-1.0.0-SNAPSHOT.jar b/backend/generation-service/target/generation-service-1.0.0-SNAPSHOT.jar index 979ed91..d979f8b 100644 Binary files a/backend/generation-service/target/generation-service-1.0.0-SNAPSHOT.jar and b/backend/generation-service/target/generation-service-1.0.0-SNAPSHOT.jar differ diff --git a/backend/generation-service/target/generation-service-1.0.0-SNAPSHOT.jar.original b/backend/generation-service/target/generation-service-1.0.0-SNAPSHOT.jar.original index 679ec4a..3ab62f9 100644 Binary files a/backend/generation-service/target/generation-service-1.0.0-SNAPSHOT.jar.original and b/backend/generation-service/target/generation-service-1.0.0-SNAPSHOT.jar.original differ diff --git a/frontend/sidebar/src/components/ScriptRecommend.tsx b/frontend/sidebar/src/components/ScriptRecommend.tsx index 0ac0536..52595ef 100644 --- a/frontend/sidebar/src/components/ScriptRecommend.tsx +++ b/frontend/sidebar/src/components/ScriptRecommend.tsx @@ -76,6 +76,33 @@ export default function ScriptRecommend({ userInfo, customerId: propCustomerId } } } + // 阶段/意图代码映射为中文描述 + const getStageLabel = (stage?: string) => { + const map: Record = { + 'STAGE-01': '开场白', + 'STAGE-02': '需求挖掘', + 'STAGE-03': '方案呈现', + 'STAGE-04': '异议处理', + 'STAGE-05': '促成签约', + 'STAGE-06': '跟进维护', + 'STAGE-07': '转介绍', + 'STAGE-08': '沉默激活' + } + return map[stage || ''] || stage || '未知' + } + + const getIntentLabel = (intent?: string) => { + const map: Record = { + 'INT-COURSE-01': '课程咨询', + 'INT-GREET-01': '问候', + 'INT-NEED-01': '需求探询', + 'INT-PRICE-01': '价格咨询', + 'INT-EMPLOY-01': '就业咨询', + 'INT-OTHER': '其他咨询' + } + return map[intent || ''] || intent || '未知' + } + const handleGenerate = async () => { if (!customerMsg.trim()) { Toast.show({ content: '请输入学员消息', icon: 'fail' }) @@ -83,10 +110,64 @@ export default function ScriptRecommend({ userInfo, customerId: propCustomerId } } setGenerating(true) try { + // 并行获取:客户画像 + 对话历史 + 参考话术 + const [profileRes, contextRes] = await Promise.all([ + fetch(`/api/v1/intent/profile/${encodeURIComponent(customerId)}?corpId=${corpId}`), + fetch(`/api/v1/conversations/${sessionId}/context`).catch(() => null) + ]) + + // 解析客户画像 + let customerProfileStr = '' + const profileData = await profileRes.json() + if (profileData.code === 0 && profileData.data) { + const p = profileData.data + const courses = p.interestedCourses ? (typeof p.interestedCourses === 'string' ? JSON.parse(p.interestedCourses) : p.interestedCourses) : [] + const lines: string[] = [] + if (p.currentOccupation) lines.push(`- 身份/职业:${p.currentOccupation}`) + if (p.studentType) lines.push(`- 学员类型:${p.studentType}`) + if (p.skillLevel) lines.push(`- 基础水平:${p.skillLevel}`) + if (p.intentLevel) lines.push(`- 意向度:${p.intentLevel}(${p.intentScore != null ? Math.round(p.intentScore) : 0}分)`) + if (p.concernFocus) lines.push(`- 关注方向:${p.concernFocus}`) + if (p.decisionStage) lines.push(`- 决策阶段:${p.decisionStage}`) + if (courses.length > 0) lines.push(`- 意向课程:${courses.join('、')}`) + if (p.budgetHint) lines.push(`- 预算范围:${p.budgetHint}`) + if (p.preferredCity) lines.push(`- 意向城市:${p.preferredCity}`) + if (p.age) lines.push(`- 年龄:${p.age}岁`) + if (p.education) lines.push(`- 学历:${p.education}`) + customerProfileStr = lines.join('\n') + } + + // 解析对话历史 + let conversationContextStr = '' + if (contextRes) { + const contextData = await contextRes.json() + if (contextData.code === 0 && contextData.data && contextData.data.messages) { + const msgs = contextData.data.messages as Array<{ role: string; content: string }> + // 取最近 5 条 + const recent = msgs.slice(-5) + conversationContextStr = recent.map(m => { + const roleName = m.role === 'student' ? '学员' : '顾问' + return `${roleName}:${m.content}` + }).join('\n') + } + } + + // 参考话术:取话术库推荐的前 3 条 + let referenceUtterancesStr = '' + if (scripts.length > 0) { + const top3 = scripts.slice(0, 3) + referenceUtterancesStr = top3.map((s, i) => `${i + 1}. "${s.content}"`).join('\n') + } + const params = new URLSearchParams({ message: customerMsg, intent: 'INT-COURSE-01', - stage: 'STAGE-03' + stage: 'STAGE-03', + stageLabel: getStageLabel('STAGE-03'), + intentLabel: getIntentLabel('INT-COURSE-01'), + ...(customerProfileStr ? { customerProfile: customerProfileStr } : {}), + ...(conversationContextStr ? { conversationContext: conversationContextStr } : {}), + ...(referenceUtterancesStr ? { referenceUtterances: referenceUtterancesStr } : {}) }) const res = await fetch(`/api/v1/generation/utterance?${params.toString()}`, { method: 'POST' }) const data = await res.json()