feat: 升级话术生成能力,优化prompt工程与前端交互

1. 重构PromptEngine,拆分系统/用户提示词并新增多维度上下文支持
2. 为Doubao客户端新增temperature参数解析配置
3. 前端新增客户画像、对话历史、参考话术自动带入生成参数
4. 调整默认LLM模型为qwen-plus,优化返回结果格式与展示
This commit is contained in:
jiao 2026-06-05 15:39:59 +08:00
parent 46b6e0a358
commit db8aa25da6
23 changed files with 241 additions and 18 deletions

View File

@ -59,6 +59,11 @@ public class DoubaoClient implements LLMClient {
if (maxTokens != null) { if (maxTokens != null) {
requestBody.put("max_tokens", maxTokens); requestBody.put("max_tokens", maxTokens);
} }
// 从 configJson 读取 temperature,默认 0.7
Double temperature = parseTemperature();
if (temperature != null) {
requestBody.put("temperature", temperature);
}
try { try {
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers); HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
@ -100,4 +105,25 @@ public class DoubaoClient implements LLMClient {
log.warn("豆包客户端暂不支持embedding,返回空向量"); log.warn("豆包客户端暂不支持embedding,返回空向量");
return List.of(); return List.of();
} }
/**
* 从 configJson 中解析 temperature 参数
*/
private Double parseTemperature() {
try {
String configJson = model.getConfigJson();
if (configJson != null && !configJson.isEmpty()) {
Map<String, Object> 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;
}
} }

View File

@ -34,16 +34,26 @@ public class GenerationController {
public Result<Map<String, Object>> generateUtterance( public Result<Map<String, Object>> generateUtterance(
@NotBlank(message = "message不能为空") @RequestParam("message") String message, @NotBlank(message = "message不能为空") @RequestParam("message") String message,
@RequestParam(value = "context", required = false) String context, @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 = "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[] promptPair = promptEngine.buildUtterancePrompt(
String generated = qianwenClient.generate(prompt, 500); 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<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
result.put("content", generated); result.put("content", generated);
result.put("source", "LLM"); result.put("source", "LLM");
result.put("model", "qwen-turbo"); result.put("model", qianwenClient.getCurrentModel() != null
? qianwenClient.getCurrentModel().getModelName() : "default");
return Result.success(result); return Result.success(result);
} }

View File

@ -7,18 +7,124 @@ import org.springframework.stereotype.Service;
@Service @Service
public class PromptEngine { 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(); StringBuilder sb = new StringBuilder();
sb.append("你是第九联盟(CG美术培训机构)的课程顾问助手。请根据学员的咨询消息,生成一段专业、友好、有说服力的回复话术。\n\n"); sb.append("你是「小九」,第九联盟(CG美术培训)的资深课程顾问,从业8年。");
sb.append("【机构背景】\n"); sb.append("第九联盟专注CG数字艺术教育10年,累计培养8000+学员,合作企业包括腾讯、网易、米哈游等500+家。");
sb.append("- 第九联盟专注CG数字艺术教育10年\n"); sb.append("6大校区:上海、西安、厦门、武汉、青岛、合肥。");
sb.append("- 累计培养8000+学员,与腾讯、网易、米哈游等500+企业有就业合作\n"); sb.append("\n\n回复原则:\n");
sb.append("- 6大校区:上海、西安、厦门、武汉、青岛、合肥\n\n"); sb.append("1. 语气像微信聊天一样自然亲切,不要机械生硬、不要堆砌数据\n");
sb.append("【当前对话阶段】").append(stage).append("\n"); sb.append("2. 结合学员具体情况给针对性建议,不要泛泛而谈\n");
sb.append("【学员意图】").append(intent).append("\n"); sb.append("3. 适当用学员成功案例或真实数据增强说服力,点到为止\n");
sb.append("【对话上下文】\n").append(context != null ? context : "无").append("\n\n"); sb.append("4. 长度控制在150-250字,简洁有力\n");
sb.append("【学员最新消息】\n").append(message).append("\n\n"); sb.append("5. 如知道学员真实姓名则称呼姓名,不知道真实姓名时绝对不要编造名字,可以直接用自然的方式开头\n");
sb.append("请直接输出回复话术,不要包含任何解释或前缀。话术要自然、专业、符合顾问身份。"); 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(); return sb.toString();
} }

View File

@ -33,7 +33,7 @@ spring:
llm: llm:
qianwen: qianwen:
api-key: sk-39ceb8d746014b349109e76f893acb18 api-key: sk-39ceb8d746014b349109e76f893acb18
model: qwen-turbo model: qwen-plus
mybatis-plus: mybatis-plus:
configuration: configuration:

View File

@ -33,7 +33,7 @@ spring:
llm: llm:
qianwen: qianwen:
api-key: sk-39ceb8d746014b349109e76f893acb18 api-key: sk-39ceb8d746014b349109e76f893acb18
model: qwen-turbo model: qwen-plus
mybatis-plus: mybatis-plus:
configuration: configuration:

View File

@ -76,6 +76,33 @@ export default function ScriptRecommend({ userInfo, customerId: propCustomerId }
} }
} }
// 阶段/意图代码映射为中文描述
const getStageLabel = (stage?: string) => {
const map: Record<string, string> = {
'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<string, string> = {
'INT-COURSE-01': '课程咨询',
'INT-GREET-01': '问候',
'INT-NEED-01': '需求探询',
'INT-PRICE-01': '价格咨询',
'INT-EMPLOY-01': '就业咨询',
'INT-OTHER': '其他咨询'
}
return map[intent || ''] || intent || '未知'
}
const handleGenerate = async () => { const handleGenerate = async () => {
if (!customerMsg.trim()) { if (!customerMsg.trim()) {
Toast.show({ content: '请输入学员消息', icon: 'fail' }) Toast.show({ content: '请输入学员消息', icon: 'fail' })
@ -83,10 +110,64 @@ export default function ScriptRecommend({ userInfo, customerId: propCustomerId }
} }
setGenerating(true) setGenerating(true)
try { 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({ const params = new URLSearchParams({
message: customerMsg, message: customerMsg,
intent: 'INT-COURSE-01', 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 res = await fetch(`/api/v1/generation/utterance?${params.toString()}`, { method: 'POST' })
const data = await res.json() const data = await res.json()