feat: 升级话术生成能力,优化prompt工程与前端交互
1. 重构PromptEngine,拆分系统/用户提示词并新增多维度上下文支持 2. 为Doubao客户端新增temperature参数解析配置 3. 前端新增客户画像、对话历史、参考话术自动带入生成参数 4. 调整默认LLM模型为qwen-plus,优化返回结果格式与展示
This commit is contained in:
parent
46b6e0a358
commit
db8aa25da6
@ -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<Map<String, Object>> 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<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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,16 +34,26 @@ public class GenerationController {
|
||||
public Result<Map<String, Object>> 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<String, Object> 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);
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -33,7 +33,7 @@ spring:
|
||||
llm:
|
||||
qianwen:
|
||||
api-key: sk-39ceb8d746014b349109e76f893acb18
|
||||
model: qwen-turbo
|
||||
model: qwen-plus
|
||||
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
|
||||
@ -33,7 +33,7 @@ spring:
|
||||
llm:
|
||||
qianwen:
|
||||
api-key: sk-39ceb8d746014b349109e76f893acb18
|
||||
model: qwen-turbo
|
||||
model: qwen-plus
|
||||
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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 () => {
|
||||
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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user