Merge branch 'master' of ssh://git-a.9artedu.com:2222/jqb/agent_9art
This commit is contained in:
commit
984b3dba70
@ -49,5 +49,9 @@
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@ -0,0 +1,139 @@
|
||||
package com.artedu.common.util;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
public class HttpUtils {
|
||||
|
||||
private static final OkHttpClient OK_HTTP_CLIENT = new OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(30, TimeUnit.SECONDS)
|
||||
.build();
|
||||
|
||||
private static final MediaType JSON_MEDIA_TYPE = MediaType.get("application/json; charset=utf-8");
|
||||
private static final MediaType FORM_MEDIA_TYPE = MediaType.get("application/x-www-form-urlencoded; charset=utf-8");
|
||||
|
||||
public static String get(String url) throws IOException {
|
||||
return get(url, null);
|
||||
}
|
||||
|
||||
public static String get(String url, Map<String, String> headers) throws IOException {
|
||||
Request.Builder requestBuilder = new Request.Builder().url(url).get();
|
||||
|
||||
if (headers != null) {
|
||||
for (Map.Entry<String, String> entry : headers.entrySet()) {
|
||||
requestBuilder.addHeader(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
Request request = requestBuilder.build();
|
||||
return executeRequest(request);
|
||||
}
|
||||
|
||||
public static String post(String url, String jsonBody) throws IOException {
|
||||
return post(url, jsonBody, null);
|
||||
}
|
||||
|
||||
public static String post(String url, String jsonBody, Map<String, String> headers) throws IOException {
|
||||
RequestBody body = RequestBody.create(jsonBody, JSON_MEDIA_TYPE);
|
||||
Request.Builder requestBuilder = new Request.Builder().url(url).post(body);
|
||||
|
||||
if (headers != null) {
|
||||
for (Map.Entry<String, String> entry : headers.entrySet()) {
|
||||
requestBuilder.addHeader(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
Request request = requestBuilder.build();
|
||||
return executeRequest(request);
|
||||
}
|
||||
|
||||
public static String postForm(String url, Map<String, Object> formParams) throws IOException {
|
||||
return postForm(url, formParams, null);
|
||||
}
|
||||
|
||||
public static String postForm(String url, Map<String, Object> formParams, Map<String, String> headers) throws IOException {
|
||||
FormBody.Builder formBodyBuilder = new FormBody.Builder();
|
||||
if (formParams != null) {
|
||||
for (Map.Entry<String, Object> entry : formParams.entrySet()) {
|
||||
String value = entry.getValue() != null ? String.valueOf(entry.getValue()) : "";
|
||||
formBodyBuilder.add(entry.getKey(), value);
|
||||
}
|
||||
}
|
||||
|
||||
RequestBody body = formBodyBuilder.build();
|
||||
Request.Builder requestBuilder = new Request.Builder().url(url).post(body);
|
||||
|
||||
if (headers != null) {
|
||||
for (Map.Entry<String, String> entry : headers.entrySet()) {
|
||||
requestBuilder.addHeader(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
Request request = requestBuilder.build();
|
||||
return executeRequest(request);
|
||||
}
|
||||
|
||||
public static String put(String url, String jsonBody) throws IOException {
|
||||
return put(url, jsonBody, null);
|
||||
}
|
||||
|
||||
public static String put(String url, String jsonBody, Map<String, String> headers) throws IOException {
|
||||
RequestBody body = RequestBody.create(jsonBody, JSON_MEDIA_TYPE);
|
||||
Request.Builder requestBuilder = new Request.Builder().url(url).put(body);
|
||||
|
||||
if (headers != null) {
|
||||
for (Map.Entry<String, String> entry : headers.entrySet()) {
|
||||
requestBuilder.addHeader(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
Request request = requestBuilder.build();
|
||||
return executeRequest(request);
|
||||
}
|
||||
|
||||
public static String delete(String url) throws IOException {
|
||||
return delete(url, null);
|
||||
}
|
||||
|
||||
public static String delete(String url, Map<String, String> headers) throws IOException {
|
||||
Request.Builder requestBuilder = new Request.Builder().url(url).delete();
|
||||
|
||||
if (headers != null) {
|
||||
for (Map.Entry<String, String> entry : headers.entrySet()) {
|
||||
requestBuilder.addHeader(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
Request request = requestBuilder.build();
|
||||
return executeRequest(request);
|
||||
}
|
||||
|
||||
private static String executeRequest(Request request) throws IOException {
|
||||
log.debug("HTTP request: {} {}", request.method(), request.url());
|
||||
|
||||
try (Response response = OK_HTTP_CLIENT.newCall(request).execute()) {
|
||||
if (!response.isSuccessful()) {
|
||||
String errorBody = response.body() != null ? response.body().string() : "";
|
||||
log.error("HTTP request failed: {} {} - {} - {}",
|
||||
request.method(), request.url(), response.code(), errorBody);
|
||||
throw new IOException("HTTP request failed: " + response.code() + " " + errorBody);
|
||||
}
|
||||
|
||||
ResponseBody body = response.body();
|
||||
String result = body != null ? body.string() : "";
|
||||
log.debug("HTTP response: {} {} - {}", request.method(), request.url(), result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public static OkHttpClient getClient() {
|
||||
return OK_HTTP_CLIENT;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package com.artedu.generation.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "qa")
|
||||
public class QaConfig {
|
||||
|
||||
private String apiUrl = "http://localhost:8000/api/qa/query";
|
||||
}
|
||||
@ -1,12 +1,15 @@
|
||||
package com.artedu.generation.controller;
|
||||
|
||||
import com.artedu.common.result.Result;
|
||||
import com.artedu.common.util.HttpUtils;
|
||||
import com.artedu.generation.client.LLMClient;
|
||||
import com.artedu.generation.config.QaConfig;
|
||||
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 com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
@ -16,12 +19,16 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Validated
|
||||
@ -41,6 +48,9 @@ public class GenerationController {
|
||||
@Autowired
|
||||
private ArchiveMessageMapper archiveMessageMapper;
|
||||
|
||||
@Autowired
|
||||
private QaConfig qaConfig;
|
||||
|
||||
private final ExecutorService sseExecutor = Executors.newCachedThreadPool();
|
||||
|
||||
private static final List<Map<String, String>> STYLES = List.of(
|
||||
@ -115,16 +125,54 @@ public class GenerationController {
|
||||
@RequestParam(value = "roomid", required = false) String roomid,
|
||||
@RequestParam(value = "staffName", required = false) String staffName) {
|
||||
|
||||
String chatContext = buildChatContext(fromUser, toUser, roomid);
|
||||
List<ArchiveMessage> messages = buildChatContext(fromUser, toUser, roomid);
|
||||
// 遍历消息:只取前三条是客户的消息,组装 查询知识库
|
||||
String external_str = ""; //原始问题
|
||||
// 历史消息上下文
|
||||
List<ArchiveMessage> top15 = (messages == null || messages.isEmpty())
|
||||
? new ArrayList<>()
|
||||
: messages.stream().limit(15).collect(Collectors.toList());
|
||||
StringBuilder sb = new StringBuilder();
|
||||
boolean is_external = true;
|
||||
int count = 0;
|
||||
for (ArchiveMessage msg : messages) {
|
||||
sb.append(msg.getCreatedAt()).append(" ").append(msg.getFromRole()).append(" ").append(msg.getContent()).append("\n");
|
||||
if ("EXTERNAL".equals(messages.get(0).getFromRole())){ //第一条消息是客户的消息 则继续
|
||||
if(count<3){
|
||||
if ("EXTERNAL".equals(msg.getFromRole()) && is_external){ //判断判断前三条是否只包含客户消息
|
||||
external_str += msg.getContent()+",";
|
||||
}
|
||||
if (!"EXTERNAL".equals(msg.getFromRole())){ //判断前三条是否包含非客户的消息,如果包含,则不需要后续的消息
|
||||
is_external = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
count++;
|
||||
}
|
||||
|
||||
String answerNew = "";
|
||||
//如果最后几条都是客户消息返回不为空,则直接返回结果,否则走原逻辑
|
||||
if (!StringUtils.isBlank(external_str)){ //不为空 发起知识库的查询
|
||||
String answer = getAnswer(top15, external_str);
|
||||
if(!StringUtils.isBlank(answer)){
|
||||
answer = answer.replaceAll("\"", "").replace("[","").replace("]","");
|
||||
if(!StringUtils.isBlank(answer)){
|
||||
String promptNew = promptEngine.buildAnswerPrompt(answer);
|
||||
answerNew = qianwenClient.generate(promptNew, 800);
|
||||
}
|
||||
}
|
||||
}
|
||||
//没有命中知识库,则根据当天聊天内容生成
|
||||
if(StringUtils.isBlank(answerNew)){
|
||||
String chatContext = sb.toString();
|
||||
String[] promptPair = promptEngine.buildUtterancePrompt(chatContext, staffName);
|
||||
String systemPrompt = promptPair[0];
|
||||
String userPrompt = promptPair[1];
|
||||
|
||||
String generated = qianwenClient.generate(systemPrompt, userPrompt, 800);
|
||||
|
||||
answerNew = qianwenClient.generate(systemPrompt, userPrompt, 800);
|
||||
}
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("content", generated);
|
||||
result.put("content", answerNew);
|
||||
result.put("source", "LLM");
|
||||
result.put("model", qianwenClient.getCurrentModel() != null
|
||||
? qianwenClient.getCurrentModel().getModelName() : "default");
|
||||
@ -132,14 +180,47 @@ public class GenerationController {
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
private String buildChatContext(String fromUser, String toUser, String roomid) {
|
||||
private String getAnswer(List<ArchiveMessage> messages, String external_str){
|
||||
List<String> msgInfo = messages.stream()
|
||||
.map(msg -> String.join(" ",
|
||||
String.valueOf(msg.getCreatedAt()),
|
||||
String.valueOf(msg.getFromRole()),
|
||||
String.valueOf(msg.getContent())
|
||||
) + "\n").collect(Collectors.toList()) ;
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("external_str", external_str);
|
||||
params.put("messages_top15", msgInfo);
|
||||
try {
|
||||
String jsonBody = com.artedu.common.util.JsonUtils.toJson(params);
|
||||
String response = HttpUtils.post(qaConfig.getApiUrl(), jsonBody);
|
||||
log.info("知识库答案:{}",response);
|
||||
return response;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private List<ArchiveMessage> buildChatContext(String fromUser, String toUser, String roomid) {
|
||||
List<ArchiveMessage> messages;
|
||||
boolean isGroupChat = roomid != null && !roomid.isEmpty();
|
||||
|
||||
// 查最近 7 天,避免今天没聊天就找不到记录
|
||||
long startTime = get7DaysAgoStartTime();
|
||||
// 查最近 今 天,避免今天没聊天就找不到记录
|
||||
long startTime = getTodayStartTime();
|
||||
long endTime = getTodayEndTime();
|
||||
|
||||
//// 指定日期:2026-05-22 todo 测试需要
|
||||
// LocalDate targetDate = LocalDate.of(2026, 6, 18);
|
||||
//
|
||||
//// 当天 00:00:00 毫秒时间戳
|
||||
// long startTime = targetDate.atStartOfDay()
|
||||
// .atZone(ZoneId.systemDefault())
|
||||
// .toInstant()
|
||||
// .toEpochMilli();
|
||||
//
|
||||
//// 当天 23:59:59 毫秒时间戳
|
||||
// long endTime = LocalDateTime.of(targetDate, java.time.LocalTime.of(23, 59, 59))
|
||||
// .atZone(ZoneId.systemDefault())
|
||||
// .toInstant()
|
||||
// .toEpochMilli();
|
||||
if (isGroupChat) {
|
||||
messages = archiveMessageMapper.selectGroupChatMessages(roomid, startTime, endTime);
|
||||
} else {
|
||||
@ -159,14 +240,10 @@ public class GenerationController {
|
||||
}
|
||||
|
||||
if (messages == null || messages.isEmpty()) {
|
||||
return null;
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
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();
|
||||
return messages;
|
||||
}
|
||||
|
||||
private long getTodayStartTime() {
|
||||
@ -201,7 +278,39 @@ public class GenerationController {
|
||||
startEvent.put("styles", STYLES);
|
||||
emitter.send(SseEmitter.event().name("start").data(startEvent));
|
||||
|
||||
String chatContext = buildChatContext(fromUser, toUser, roomid);
|
||||
List<ArchiveMessage> messages = buildChatContext(fromUser, toUser, roomid);
|
||||
// 遍历消息:只取前三条是客户的消息,组装 查询知识库
|
||||
String external_str = ""; //原始问题
|
||||
// 历史消息上下文
|
||||
List<ArchiveMessage> top15 = (messages == null || messages.isEmpty())
|
||||
? new ArrayList<>()
|
||||
: messages.stream().limit(15).collect(Collectors.toList());
|
||||
StringBuilder sb = new StringBuilder();
|
||||
boolean is_external = true;
|
||||
for (ArchiveMessage msg : messages) {
|
||||
sb.append(msg.getCreatedAt()).append(" ").append(msg.getFromRole()).append(" ").append(msg.getContent()).append("\n");
|
||||
if ("EXTERNAL".equals(messages.get(0).getFromRole())){ //第一条消息是客户的消息 则继续
|
||||
if ("EXTERNAL".equals(msg.getFromRole()) && is_external){ //判断只要消息不是内部人员,则后续不考虑
|
||||
is_external = false;
|
||||
external_str += msg.getContent()+",";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String generated = "";
|
||||
//如果最后几条都是客户消息返回不为空,则直接返回结果,否则走原逻辑
|
||||
if (!StringUtils.isBlank(external_str)){ //不为空 发起知识库的查询
|
||||
String answer = getAnswer(top15, external_str);
|
||||
if(!StringUtils.isBlank(answer)){
|
||||
answer = answer.replaceAll("\"", "").replace("[","").replace("]","");
|
||||
if(!StringUtils.isBlank(answer)){
|
||||
String promptNew = promptEngine.buildAnswerPrompt(answer);
|
||||
generated = qianwenClient.generate(promptNew, 800);
|
||||
log.info("原问题:{},改写后的问题:{},命中知识库结果:{}", external_str, answer, generated);
|
||||
}
|
||||
}
|
||||
}
|
||||
String chatContext = sb.toString();
|
||||
if (chatContext == null || chatContext.isEmpty()) {
|
||||
Map<String, Object> err = new HashMap<>();
|
||||
err.put("type", "error");
|
||||
@ -210,13 +319,15 @@ public class GenerationController {
|
||||
emitter.complete();
|
||||
return;
|
||||
}
|
||||
|
||||
if(StringUtils.isBlank(generated)){
|
||||
String systemPrompt = promptEngine.buildSystemPrompt(chatContext, staffName);
|
||||
String userPrompt = promptEngine.buildMultiStyleUserPrompt(staffName);
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
String generated = qianwenClient.generate(systemPrompt, userPrompt, 1200);
|
||||
generated = qianwenClient.generate(systemPrompt, userPrompt, 1200);
|
||||
log.info("多风格话术生成完成, elapsed={}ms", System.currentTimeMillis() - startTime);
|
||||
}
|
||||
|
||||
|
||||
List<String> utterances = parseMultiStyle(generated);
|
||||
if (utterances.size() < 3) {
|
||||
|
||||
@ -31,8 +31,8 @@ public class PromptEngine {
|
||||
sb.append("每条话术固定三要素,缺一不可:完整话术文本、预期效果、INTERNAL 语气;\n");
|
||||
sb.append("单条话术文本≤100 字,整体风格专业、自然、有温度。\n");
|
||||
sb.append("## 单条话术固定格式(严格遵守,不改动结构)\n");
|
||||
sb.append("1、李先生您好,这个产品标准版 9800/年。不过不同企业的需求差别很大,方便说一下您这边大概的使用场景和人数吗?我可以帮您推荐最合适的版本,避免买多了浪费。\n");
|
||||
sb.append("2、价格方面我先给您一个参考——标准版 9800/年。上周有个和您同行做电商的客户,用了我们系统后客服效率提升了 40%。要不我发一份他们的使用案例给您看看?\n");
|
||||
sb.append("1、您好,第九联盟(点晴科技)提供线上课程11800元/8-10个月和线下课程33800元/6个月全日制,大学生可线上+寒暑假线下结合(按线下收费)。支持包括3年回放和答疑、重读机会,以及简历辅导、模拟面试、测试指导、校招群内推等服务。\n");
|
||||
sb.append("2、您好,有什么可以帮助你吗\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@ -164,4 +164,27 @@ public class PromptEngine {
|
||||
public String buildSummaryPrompt(String context) {
|
||||
return "请对以下对话进行简要总结(50字以内),提炼学员核心需求和当前阶段:\n\n" + context;
|
||||
}
|
||||
/**
|
||||
* 构建话术改写提示词
|
||||
*/
|
||||
public String buildAnswerPrompt(String answer) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("你是一位经验丰富的课程顾问,擅长用亲切、自然、像朋友聊天的方式跟学生/家长沟通。\n\n\n");
|
||||
|
||||
sb.append("【任务】把\"原内容信息\"改写成温暖、有说服力、让人愿意继续聊下去的回复。\n\n");
|
||||
sb.append("【执行流程】\n");
|
||||
sb.append("1. 首先检查是否有\"原内容信息\":\n");
|
||||
sb.append("- 如果没有提供,或内容为空,或内容仅为系统提示本身 → 必须直接输出空,不输出任何其他文字。\n");
|
||||
sb.append("- 如果提供了 → 执行步骤2\n");
|
||||
sb.append("2. 润色原内容,保持字数相近,只输出润色结果。\n");
|
||||
sb.append("【约束】\n");
|
||||
sb.append("- 严禁在原内容为空时编造、引导、解释。\n");
|
||||
sb.append("- 输出字数必须与原信息字数相近。\n");
|
||||
sb.append("- 只输出润色结果,禁止输出\"好的\"\"请提供\"等额外文字。\n");
|
||||
sb.append("【原内容】\n");
|
||||
sb.append(answer);
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,6 +35,9 @@ llm:
|
||||
api-key: sk-39ceb8d746014b349109e76f893acb18
|
||||
model: qwen-plus
|
||||
|
||||
qa:
|
||||
api-url: ${QA_API_URL:http://8.159.132.53:8000/api/qa/query}
|
||||
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
|
||||
@ -91,6 +91,11 @@
|
||||
<version>2.2.224</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
<version>4.12.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user