chore: add .gitignore for all backend modules and update generation service
1. 为所有后端子模块添加.gitignore文件,忽略target编译目录 2. 重构LLM客户端接口,新增支持关闭推理过程的生成方法 3. 为各模型客户端实现新的重载generate方法 4. 重构PromptEngine,替换原有话术生成逻辑为对话历史分析模式 5. 新增对话历史存档实体类和Mapper,实现单聊/群聊历史查询接口 6. 简化GenerationController,移除SSE流式生成相关代码
This commit is contained in:
parent
efed845b6f
commit
5e33db1ad3
2
backend/analytics-service/.gitignore
vendored
Normal file
2
backend/analytics-service/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
target/
|
||||
target
|
||||
2
backend/archive-service/.gitignore
vendored
Normal file
2
backend/archive-service/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
target/
|
||||
target
|
||||
2
backend/auth-service/.gitignore
vendored
Normal file
2
backend/auth-service/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
target/
|
||||
target
|
||||
2
backend/common/.gitignore
vendored
Normal file
2
backend/common/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
target/
|
||||
target
|
||||
2
backend/conversation-service/.gitignore
vendored
Normal file
2
backend/conversation-service/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
target/
|
||||
target
|
||||
2
backend/gateway/.gitignore
vendored
Normal file
2
backend/gateway/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
target/
|
||||
target
|
||||
2
backend/generation-service/.gitignore
vendored
Normal file
2
backend/generation-service/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
target/
|
||||
target
|
||||
@ -32,6 +32,11 @@ public class BailianClient implements LLMClient {
|
||||
|
||||
@Override
|
||||
public String generate(String system, String prompt, Integer maxTokens) {
|
||||
return generate(system, prompt, maxTokens, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generate(String system, String prompt, Integer maxTokens, boolean thinkingDisabled) {
|
||||
if (model == null || model.getApiKey() == null || model.getApiKey().isEmpty()) {
|
||||
log.error("百炼模型配置不完整");
|
||||
return null;
|
||||
@ -65,6 +70,13 @@ public class BailianClient implements LLMClient {
|
||||
if (maxTokens != null) {
|
||||
parameters.put("max_tokens", maxTokens);
|
||||
}
|
||||
|
||||
if (thinkingDisabled) {
|
||||
Map<String, Object> thinking = new HashMap<>();
|
||||
thinking.put("type", "disabled");
|
||||
parameters.put("thinking", thinking);
|
||||
}
|
||||
|
||||
requestBody.put("parameters", parameters);
|
||||
|
||||
try {
|
||||
|
||||
@ -33,6 +33,11 @@ public class DoubaoClient implements LLMClient {
|
||||
|
||||
@Override
|
||||
public String generate(String system, String prompt, Integer maxTokens) {
|
||||
return generate(system, prompt, maxTokens, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generate(String system, String prompt, Integer maxTokens, boolean thinkingDisabled) {
|
||||
if (model == null || model.getApiKey() == null || model.getApiKey().isEmpty()) {
|
||||
log.error("豆包模型配置不完整");
|
||||
return null;
|
||||
@ -59,12 +64,18 @@ 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);
|
||||
}
|
||||
|
||||
if (thinkingDisabled) {
|
||||
Map<String, Object> thinking = new HashMap<>();
|
||||
thinking.put("type", "disabled");
|
||||
requestBody.put("thinking", thinking);
|
||||
}
|
||||
|
||||
try {
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||
Map<String, Object> response = restTemplate.postForObject(url, entity, Map.class);
|
||||
|
||||
@ -32,6 +32,11 @@ public class HunyuanClient implements LLMClient {
|
||||
|
||||
@Override
|
||||
public String generate(String system, String prompt, Integer maxTokens) {
|
||||
return generate(system, prompt, maxTokens, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generate(String system, String prompt, Integer maxTokens, boolean thinkingDisabled) {
|
||||
if (model == null || model.getApiKey() == null || model.getApiKey().isEmpty()) {
|
||||
log.error("混元模型配置不完整");
|
||||
return null;
|
||||
@ -59,6 +64,12 @@ public class HunyuanClient implements LLMClient {
|
||||
requestBody.put("MaxTokens", maxTokens);
|
||||
}
|
||||
|
||||
if (thinkingDisabled) {
|
||||
Map<String, Object> thinking = new HashMap<>();
|
||||
thinking.put("type", "disabled");
|
||||
requestBody.put("thinking", thinking);
|
||||
}
|
||||
|
||||
try {
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||
Map<String, Object> response = restTemplate.postForObject(url, entity, Map.class);
|
||||
|
||||
@ -26,6 +26,17 @@ public interface LLMClient {
|
||||
*/
|
||||
String generate(String system, String prompt, Integer maxTokens);
|
||||
|
||||
/**
|
||||
* 文本生成(支持system消息和关闭推理过程)
|
||||
*
|
||||
* @param system 系统提示词
|
||||
* @param prompt 用户提示词
|
||||
* @param maxTokens 最大token数
|
||||
* @param thinkingDisabled 是否关闭推理过程
|
||||
* @return 生成的文本
|
||||
*/
|
||||
String generate(String system, String prompt, Integer maxTokens, boolean thinkingDisabled);
|
||||
|
||||
/**
|
||||
* 获取当前使用的模型配置
|
||||
*/
|
||||
|
||||
@ -2,24 +2,20 @@ package com.artedu.generation.controller;
|
||||
|
||||
import com.artedu.common.result.Result;
|
||||
import com.artedu.generation.client.LLMClient;
|
||||
import com.artedu.generation.entity.ArchiveMessage;
|
||||
import com.artedu.generation.mapper.ArchiveMessageMapper;
|
||||
import com.artedu.generation.service.LlmModelService;
|
||||
import com.artedu.generation.service.PromptEngine;
|
||||
import com.artedu.generation.service.QianwenClient;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@Slf4j
|
||||
@Validated
|
||||
@ -36,31 +32,21 @@ public class GenerationController {
|
||||
@Autowired
|
||||
private LlmModelService llmModelService;
|
||||
|
||||
private final ExecutorService sseExecutor = Executors.newCachedThreadPool();
|
||||
|
||||
private static final List<Map<String, String>> STYLES = List.of(
|
||||
Map.of("id", "friendly", "name", "亲和风格", "desc", "温暖亲切,像朋友聊天"),
|
||||
Map.of("id", "professional", "name", "专业风格", "desc", "数据驱动,专业权威"),
|
||||
Map.of("id", "urgent", "name", "促单风格", "desc", "制造紧迫感,促成决策")
|
||||
);
|
||||
@Autowired
|
||||
private ArchiveMessageMapper archiveMessageMapper;
|
||||
|
||||
@PostMapping("/utterance")
|
||||
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,
|
||||
@RequestParam(value = "stageLabel", required = false) String stageLabel,
|
||||
@RequestParam(value = "intentLabel", required = false) String intentLabel) {
|
||||
@RequestParam(value = "fromUser", required = false) String fromUser,
|
||||
@RequestParam(value = "toUser", required = false) String toUser,
|
||||
@RequestParam(value = "roomid", required = false) String roomid) {
|
||||
|
||||
String[] promptPair = promptEngine.buildUtterancePrompt(
|
||||
message, context, customerProfile, conversationContext,
|
||||
referenceUtterances, intent, stage, stageLabel, intentLabel);
|
||||
String chatContext = buildChatContext(fromUser, toUser, roomid);
|
||||
|
||||
String[] promptPair = promptEngine.buildUtterancePrompt(chatContext);
|
||||
String systemPrompt = promptPair[0];
|
||||
String userPrompt = promptPair[1];
|
||||
|
||||
String generated = qianwenClient.generate(systemPrompt, userPrompt, 800);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
@ -72,6 +58,43 @@ public class GenerationController {
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
private String buildChatContext(String fromUser, String toUser, String roomid) {
|
||||
List<ArchiveMessage> messages;
|
||||
boolean isGroupChat = roomid != null && !roomid.isEmpty();
|
||||
|
||||
long startTime = getTodayStartTime();
|
||||
long endTime = getTodayEndTime();
|
||||
|
||||
if (isGroupChat) {
|
||||
messages = archiveMessageMapper.selectGroupChatMessages(roomid, startTime, endTime);
|
||||
} else {
|
||||
if (fromUser == null || fromUser.isEmpty() || toUser == null || toUser.isEmpty()) {
|
||||
log.warn("单聊模式需要fromUser和toUser参数");
|
||||
return null;
|
||||
}
|
||||
toUser = "[\""+toUser+"\"]";
|
||||
messages = archiveMessageMapper.selectSingleChatMessages(fromUser, toUser, startTime, endTime);
|
||||
}
|
||||
|
||||
if (messages == null || messages.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (ArchiveMessage msg : messages) {
|
||||
sb.append(msg.getCreatedAt()).append(" ").append(msg.getFromRole()).append(msg.getContent()).append("\n");
|
||||
}
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
private long getTodayStartTime() {
|
||||
return java.time.LocalDate.now().atStartOfDay(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli();
|
||||
}
|
||||
|
||||
private long getTodayEndTime() {
|
||||
return java.time.LocalDate.now().plusDays(1).atStartOfDay(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli() - 1;
|
||||
}
|
||||
|
||||
@PostMapping("/summary")
|
||||
public Result<Map<String, Object>> generateSummary(
|
||||
@NotBlank(message = "context不能为空") @RequestParam("context") String context) {
|
||||
@ -85,131 +108,6 @@ public class GenerationController {
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 流式生成多风格话术(SSE)
|
||||
* 页面打开即自动推荐 3 条不同风格话术
|
||||
*/
|
||||
@GetMapping(value = "/utterance-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public SseEmitter streamUtterance(
|
||||
@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,
|
||||
@RequestParam(value = "stageLabel", required = false) String stageLabel,
|
||||
@RequestParam(value = "intentLabel", required = false) String intentLabel) {
|
||||
|
||||
SseEmitter emitter = new SseEmitter(120000L); // 2分钟超时
|
||||
|
||||
sseExecutor.execute(() -> {
|
||||
try {
|
||||
// 1. 发送开始事件,告知前端3种风格
|
||||
Map<String, Object> startEvent = new HashMap<>();
|
||||
startEvent.put("type", "start");
|
||||
startEvent.put("styles", STYLES);
|
||||
emitter.send(SseEmitter.event().name("start").data(startEvent));
|
||||
|
||||
// 2. 构建 system + user prompt
|
||||
String systemPrompt = promptEngine.buildSystemPrompt(stageLabel, intentLabel);
|
||||
String userPrompt = promptEngine.buildMultiStyleUserPrompt(
|
||||
message, context, customerProfile, conversationContext,
|
||||
referenceUtterances, intent, stage, stageLabel, intentLabel);
|
||||
|
||||
// 3. 调用 LLM 生成(非流式,后台一次完成)
|
||||
long startTime = System.currentTimeMillis();
|
||||
String generated = qianwenClient.generate(systemPrompt, userPrompt, 1200);
|
||||
log.info("多风格话术生成完成, elapsed={}ms", System.currentTimeMillis() - startTime);
|
||||
|
||||
// 4. 解析出3条话术
|
||||
List<String> utterances = parseMultiStyle(generated);
|
||||
if (utterances.size() < 3) {
|
||||
// 如果解析失败,用回退方案:把整个内容当一条
|
||||
while (utterances.size() < 3) {
|
||||
utterances.add(generated != null ? generated : "生成失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 逐条逐字流式推送给前端(伪流式,打字机效果)
|
||||
for (int i = 0; i < Math.min(3, utterances.size()); i++) {
|
||||
String text = utterances.get(i);
|
||||
String styleId = STYLES.get(i).get("id");
|
||||
|
||||
// 先发送该条话术的起始事件
|
||||
Map<String, Object> itemStart = new HashMap<>();
|
||||
itemStart.put("type", "item-start");
|
||||
itemStart.put("styleId", styleId);
|
||||
itemStart.put("index", i);
|
||||
emitter.send(SseEmitter.event().name("item-start").data(itemStart));
|
||||
|
||||
// 逐字推送(每20-40ms推送一个字,模拟打字速度)
|
||||
for (int j = 0; j < text.length(); j++) {
|
||||
String ch = text.substring(j, j + 1);
|
||||
Map<String, Object> chunk = new HashMap<>();
|
||||
chunk.put("type", "chunk");
|
||||
chunk.put("styleId", styleId);
|
||||
chunk.put("text", ch);
|
||||
chunk.put("index", i);
|
||||
emitter.send(SseEmitter.event().name("chunk").data(chunk));
|
||||
Thread.sleep(25); // 25ms/字,约 40字/秒
|
||||
}
|
||||
|
||||
// 该条话术完成
|
||||
Map<String, Object> complete = new HashMap<>();
|
||||
complete.put("type", "complete");
|
||||
complete.put("styleId", styleId);
|
||||
complete.put("fullText", text);
|
||||
complete.put("index", i);
|
||||
emitter.send(SseEmitter.event().name("complete").data(complete));
|
||||
}
|
||||
|
||||
// 6. 全部完成
|
||||
Map<String, Object> done = new HashMap<>();
|
||||
done.put("type", "done");
|
||||
done.put("model", qianwenClient.getCurrentModel() != null
|
||||
? qianwenClient.getCurrentModel().getModelName() : "default");
|
||||
emitter.send(SseEmitter.event().name("done").data(done));
|
||||
emitter.complete();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("流式生成失败", e);
|
||||
try {
|
||||
Map<String, Object> error = new HashMap<>();
|
||||
error.put("type", "error");
|
||||
error.put("message", e.getMessage());
|
||||
emitter.send(SseEmitter.event().name("error").data(error));
|
||||
} catch (IOException ignored) {}
|
||||
emitter.completeWithError(e);
|
||||
}
|
||||
});
|
||||
|
||||
return emitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析多风格话术,按 "|||" 分隔
|
||||
*/
|
||||
private List<String> parseMultiStyle(String text) {
|
||||
List<String> result = new ArrayList<>();
|
||||
if (text == null || text.isEmpty()) return result;
|
||||
|
||||
// 去掉常见的AI输出前缀
|
||||
String cleaned = text.replaceAll("^(风格一[::]?|风格二[::]?|风格三[::]?|亲和风格[::]?|专业风格[::]?|促单风格[::]?)", "").trim();
|
||||
|
||||
// 按 ||| 分隔
|
||||
String[] parts = cleaned.split("\\|\\|\\|");
|
||||
for (String part : parts) {
|
||||
String trimmed = part.trim();
|
||||
if (!trimmed.isEmpty() && trimmed.length() > 5) {
|
||||
// 去掉可能的风格标签前缀
|
||||
trimmed = trimmed.replaceAll("^(风格[一二三四五][::]?|【.*?】[::]?)", "").trim();
|
||||
result.add(trimmed);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用文本生成接口(供其他服务调用)
|
||||
*/
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
package com.artedu.generation.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("archive_messages")
|
||||
public class ArchiveMessage {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String msgid;
|
||||
private Long seq;
|
||||
private String corpId;
|
||||
private String action;
|
||||
private String fromUser;
|
||||
private String fromRole;
|
||||
private String toUser;
|
||||
private String tolist;
|
||||
private String roomid;
|
||||
private String msgtype;
|
||||
private Long msgtime;
|
||||
private String content;
|
||||
private String mediaData;
|
||||
private Integer decryptStatus;
|
||||
private String sessionId;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
package com.artedu.generation.mapper;
|
||||
|
||||
import com.artedu.generation.entity.ArchiveMessage;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ArchiveMessageMapper extends BaseMapper<ArchiveMessage> {
|
||||
|
||||
@Select("<script>" +
|
||||
"(SELECT msgtime,created_at, from_role, content FROM archive_messages " +
|
||||
"WHERE from_user = #{fromUser} AND to_user = #{toUser} " +
|
||||
"AND msgtime >= #{startTime} AND msgtime < #{endTime} AND msgtype='text' " +
|
||||
"AND content IS NOT NULL AND content != '' limit 100) " +
|
||||
"UNION ALL " +
|
||||
"(SELECT msgtime,created_at, from_role, content FROM archive_messages " +
|
||||
"WHERE from_user = #{toUser} AND to_user = #{fromUser} " +
|
||||
"AND msgtime >= #{startTime} AND msgtime < #{endTime} AND msgtype='text' " +
|
||||
"AND content IS NOT NULL AND content != '' limit 100)" +
|
||||
"ORDER BY msgtime DESC" +
|
||||
"</script>")
|
||||
List<ArchiveMessage> selectSingleChatMessages(@Param("fromUser") String fromUser,
|
||||
@Param("toUser") String toUser,
|
||||
@Param("startTime") Long startTime,
|
||||
@Param("endTime") Long endTime);
|
||||
|
||||
@Select("SELECT created_at, from_role, content FROM archive_messages " +
|
||||
"WHERE roomid = #{roomid} " +
|
||||
"AND msgtime >= #{startTime} AND msgtime < #{endTime} AND msgtype='text' " +
|
||||
"AND content IS NOT NULL AND content != '' " +
|
||||
"ORDER BY msgtime DESC")
|
||||
List<ArchiveMessage> selectGroupChatMessages(@Param("roomid") String roomid,
|
||||
@Param("startTime") Long startTime,
|
||||
@Param("endTime") Long endTime);
|
||||
}
|
||||
@ -10,20 +10,24 @@ public class PromptEngine {
|
||||
/**
|
||||
* 构建生成话术的系统提示词(角色设定)
|
||||
*/
|
||||
public String buildSystemPrompt(String stageLabel, String intentLabel) {
|
||||
public String buildSystemPrompt(String chatHistoryContext) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
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");
|
||||
}
|
||||
sb.append("资深销售教练,仅输出符合规范的 3 条推荐回复话术,全程禁止输出任何对话分析、客户判断、前置解读、总结说明文字,任何分析类文字一律不写,正文只保留 3 条话术条目。\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("全文只能存在 3 条话术条目,除此以外无任何文字,不能加开头、结尾、备注类语句;\n");
|
||||
sb.append("每条话术固定三要素,缺一不可:完整话术文本、预期效果、INTERNAL 语气;\n");
|
||||
sb.append("单条话术文本≤100 字,整体风格专业、自然、有温度。\n");
|
||||
sb.append("## 单条话术固定格式(严格遵守,不改动结构)\n");
|
||||
sb.append("1、李先生您好,这个产品标准版 9800/年。不过不同企业的需求差别很大,方便说一下您这边大概的使用场景和人数吗?我可以帮您推荐最合适的版本,避免买多了浪费。\n");
|
||||
sb.append("2、价格方面我先给您一个参考——标准版 9800/年。上周有个和您同行做电商的客户,用了我们系统后客服效率提升了 40%。要不我发一份他们的使用案例给您看看?\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@ -76,14 +80,11 @@ public class PromptEngine {
|
||||
* 构建完整话术 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);
|
||||
public String[] buildUtterancePrompt(String chatHistoryContext) {
|
||||
String system = buildSystemPrompt(chatHistoryContext);
|
||||
// String user = buildUserPrompt(message, context, customerProfile, conversationContext,
|
||||
// referenceUtterances, intent, stage, stageLabel, intentLabel);
|
||||
String user = "";
|
||||
return new String[]{system, user};
|
||||
}
|
||||
|
||||
@ -128,59 +129,6 @@ public class PromptEngine {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建多风格话术的用户提示词
|
||||
* 一次生成 3 条不同风格的话术,用特定分隔符
|
||||
*/
|
||||
public String buildMultiStyleUserPrompt(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");
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
if (referenceUtterances != null && !referenceUtterances.isEmpty()) {
|
||||
sb.append("【参考话术风格】\n").append(referenceUtterances).append("\n\n");
|
||||
}
|
||||
|
||||
sb.append("学员最新消息:\"").append(message).append("\"\n\n");
|
||||
sb.append("请为这条学员消息生成3种不同风格的回复话术,每种风格完全不同,让学员有更多选择:\n\n");
|
||||
sb.append("风格一【亲和风格】:语气温暖、亲切、像朋友聊天,拉近距离,适合建立好感\n");
|
||||
sb.append("风格二【专业风格】:用数据、案例、专业分析增强说服力,适合建立信任\n");
|
||||
sb.append("风格三【促单风格】:制造紧迫感,强调限时优惠或名额有限,适合促成决策\n\n");
|
||||
sb.append("要求:\n");
|
||||
sb.append("- 每条话术150-250字\n");
|
||||
sb.append("- 直接输出话术内容,不要加任何前缀或解释\n");
|
||||
sb.append("- 每条话术独立成行,用「|||」分隔\n\n");
|
||||
sb.append("输出格式示例:\n");
|
||||
sb.append("亲和风格话术内容...\n");
|
||||
sb.append("|||\n");
|
||||
sb.append("专业风格话术内容...\n");
|
||||
sb.append("|||\n");
|
||||
sb.append("促单风格话术内容...");
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public String buildSummaryPrompt(String context) {
|
||||
return "请对以下对话进行简要总结(50字以内),提炼学员核心需求和当前阶段:\n\n" + context;
|
||||
}
|
||||
|
||||
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.
2
backend/intent-service/.gitignore
vendored
Normal file
2
backend/intent-service/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
target/
|
||||
target
|
||||
2
backend/kb-admin-service/.gitignore
vendored
Normal file
2
backend/kb-admin-service/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
target/
|
||||
target
|
||||
2
backend/recommendation-service/.gitignore
vendored
Normal file
2
backend/recommendation-service/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
target/
|
||||
target
|
||||
Loading…
Reference in New Issue
Block a user