增加知识库的查询

This commit is contained in:
qinyong@9artedu.com 2026-06-23 17:35:04 +08:00
parent f71340f485
commit 569539d1f0
6 changed files with 308 additions and 23 deletions

View File

@ -49,5 +49,9 @@
<groupId>com.fasterxml.jackson.core</groupId> <groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId> <artifactId>jackson-databind</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -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;
}
}

View File

@ -1,12 +1,15 @@
package com.artedu.generation.controller; package com.artedu.generation.controller;
import com.artedu.common.result.Result; import com.artedu.common.result.Result;
import com.artedu.common.util.HttpUtils;
import com.artedu.generation.client.LLMClient; import com.artedu.generation.client.LLMClient;
import com.artedu.generation.config.QaConfig;
import com.artedu.generation.entity.ArchiveMessage; import com.artedu.generation.entity.ArchiveMessage;
import com.artedu.generation.mapper.ArchiveMessageMapper; import com.artedu.generation.mapper.ArchiveMessageMapper;
import com.artedu.generation.service.LlmModelService; import com.artedu.generation.service.LlmModelService;
import com.artedu.generation.service.PromptEngine; import com.artedu.generation.service.PromptEngine;
import com.artedu.generation.service.QianwenClient; import com.artedu.generation.service.QianwenClient;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@ -16,12 +19,16 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotBlank;
import java.io.IOException; import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.stream.Collectors;
@Slf4j @Slf4j
@Validated @Validated
@ -41,6 +48,9 @@ public class GenerationController {
@Autowired @Autowired
private ArchiveMessageMapper archiveMessageMapper; private ArchiveMessageMapper archiveMessageMapper;
@Autowired
private QaConfig qaConfig;
private final ExecutorService sseExecutor = Executors.newCachedThreadPool(); private final ExecutorService sseExecutor = Executors.newCachedThreadPool();
private static final List<Map<String, String>> STYLES = List.of( 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 = "roomid", required = false) String roomid,
@RequestParam(value = "staffName", required = false) String staffName) { @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[] promptPair = promptEngine.buildUtterancePrompt(chatContext, staffName); String answerNew = "";
String systemPrompt = promptPair[0]; //如果最后几条都是客户消息返回不为空,则直接返回结果,否则走原逻辑
String userPrompt = promptPair[1]; if (!StringUtils.isBlank(external_str)){ //不为空 发起知识库的查询
String answer = getAnswer(top15, external_str);
String generated = qianwenClient.generate(systemPrompt, userPrompt, 800); 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];
answerNew = qianwenClient.generate(systemPrompt, userPrompt, 800);
}
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
result.put("content", generated); result.put("content", answerNew);
result.put("source", "LLM"); result.put("source", "LLM");
result.put("model", qianwenClient.getCurrentModel() != null result.put("model", qianwenClient.getCurrentModel() != null
? qianwenClient.getCurrentModel().getModelName() : "default"); ? qianwenClient.getCurrentModel().getModelName() : "default");
@ -132,14 +180,47 @@ public class GenerationController {
return Result.success(result); 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; List<ArchiveMessage> messages;
boolean isGroupChat = roomid != null && !roomid.isEmpty(); boolean isGroupChat = roomid != null && !roomid.isEmpty();
// 查最近 今 天,避免今天没聊天就找不到记录 // 查最近 今 天,避免今天没聊天就找不到记录
long startTime = getTodayStartTime(); long startTime = getTodayStartTime();
long endTime = getTodayEndTime(); 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) { if (isGroupChat) {
messages = archiveMessageMapper.selectGroupChatMessages(roomid, startTime, endTime); messages = archiveMessageMapper.selectGroupChatMessages(roomid, startTime, endTime);
} else { } else {
@ -159,14 +240,10 @@ public class GenerationController {
} }
if (messages == null || messages.isEmpty()) { if (messages == null || messages.isEmpty()) {
return null; return new ArrayList<>();
} }
StringBuilder sb = new StringBuilder(); return messages;
for (ArchiveMessage msg : messages) {
sb.append(msg.getCreatedAt()).append(" ").append(msg.getFromRole()).append(msg.getContent()).append("\n");
}
return sb.toString().trim();
} }
private long getTodayStartTime() { private long getTodayStartTime() {
@ -201,7 +278,39 @@ public class GenerationController {
startEvent.put("styles", STYLES); startEvent.put("styles", STYLES);
emitter.send(SseEmitter.event().name("start").data(startEvent)); 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()) { if (chatContext == null || chatContext.isEmpty()) {
Map<String, Object> err = new HashMap<>(); Map<String, Object> err = new HashMap<>();
err.put("type", "error"); err.put("type", "error");
@ -210,13 +319,15 @@ public class GenerationController {
emitter.complete(); emitter.complete();
return; return;
} }
if(StringUtils.isBlank(generated)){
String systemPrompt = promptEngine.buildSystemPrompt(chatContext, staffName);
String userPrompt = promptEngine.buildMultiStyleUserPrompt(staffName);
String systemPrompt = promptEngine.buildSystemPrompt(chatContext, staffName); long startTime = System.currentTimeMillis();
String userPrompt = promptEngine.buildMultiStyleUserPrompt(staffName); generated = qianwenClient.generate(systemPrompt, userPrompt, 1200);
log.info("多风格话术生成完成, elapsed={}ms", System.currentTimeMillis() - startTime);
}
long startTime = System.currentTimeMillis();
String generated = qianwenClient.generate(systemPrompt, userPrompt, 1200);
log.info("多风格话术生成完成, elapsed={}ms", System.currentTimeMillis() - startTime);
List<String> utterances = parseMultiStyle(generated); List<String> utterances = parseMultiStyle(generated);
if (utterances.size() < 3) { if (utterances.size() < 3) {

View File

@ -31,8 +31,8 @@ public class PromptEngine {
sb.append("每条话术固定三要素,缺一不可:完整话术文本、预期效果、INTERNAL 语气;\n"); sb.append("每条话术固定三要素,缺一不可:完整话术文本、预期效果、INTERNAL 语气;\n");
sb.append("单条话术文本≤100 字,整体风格专业、自然、有温度。\n"); sb.append("单条话术文本≤100 字,整体风格专业、自然、有温度。\n");
sb.append("## 单条话术固定格式(严格遵守,不改动结构)\n"); sb.append("## 单条话术固定格式(严格遵守,不改动结构)\n");
sb.append("1、李先生您好,这个产品标准版 9800/年。不过不同企业的需求差别很大,方便说一下您这边大概的使用场景和人数吗?我可以帮您推荐最合适的版本,避免买多了浪费。\n"); sb.append("1、您好,第九联盟(点晴科技)提供线上课程11800元/8-10个月和线下课程33800元/6个月全日制,大学生可线上+寒暑假线下结合(按线下收费)。支持包括3年回放和答疑、重读机会,以及简历辅导、模拟面试、测试指导、校招群内推等服务。\n");
sb.append("2、价格方面我先给您一个参考——标准版 9800/年。上周有个和您同行做电商的客户,用了我们系统后客服效率提升了 40%。要不我发一份他们的使用案例给您看看?\n"); sb.append("2、您好,有什么可以帮助你吗\n");
return sb.toString(); return sb.toString();
} }
@ -164,4 +164,27 @@ public class PromptEngine {
public String buildSummaryPrompt(String context) { public String buildSummaryPrompt(String context) {
return "请对以下对话进行简要总结(50字以内),提炼学员核心需求和当前阶段:\n\n" + 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();
}
} }

View File

@ -35,6 +35,9 @@ llm:
api-key: sk-39ceb8d746014b349109e76f893acb18 api-key: sk-39ceb8d746014b349109e76f893acb18
model: qwen-plus model: qwen-plus
qa:
api-url: ${QA_API_URL:http://8.159.132.53:8000/api/qa/query}
mybatis-plus: mybatis-plus:
configuration: configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

View File

@ -91,6 +91,11 @@
<version>2.2.224</version> <version>2.2.224</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>