Browse Source

新增App,集成客服咨询和商品信息抽取功能,并实现基于文件和基于内存的两种对话记忆持久化方案。

master
hygl 1 year ago
parent
commit
3035278135
  1. 12
      pom.xml
  2. 62
      src/main/java/com/wok/supportbot/advisor/MyLoggerAdvisor.java
  3. 53
      src/main/java/com/wok/supportbot/advisor/ReReadingAdvisor.java
  4. 76
      src/main/java/com/wok/supportbot/app/AssistantApp.java
  5. 64
      src/main/java/com/wok/supportbot/app/ProductInfoApp.java
  6. 88
      src/main/java/com/wok/supportbot/chatmemory/FileBasedChatMemory.java
  7. 2
      src/main/java/com/wok/supportbot/demo/invoke/SpringAiAiInvoke.java
  8. 14
      src/main/java/com/wok/supportbot/record/AssistantReport.java
  9. 14
      src/main/java/com/wok/supportbot/record/ProductInfo.java
  10. 62
      src/test/java/com/wok/supportbot/SupportBotApplicationTests.java

12
pom.xml

@ -52,6 +52,18 @@
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId> <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.4.0</version> <version>4.4.0</version>
</dependency> </dependency>
<!-- 支持结构化输出 -->
<dependency>
<groupId>com.github.victools</groupId>
<artifactId>jsonschema-generator</artifactId>
<version>4.38.0</version>
</dependency>
<!-- 支持文件会话记忆持久化的序列化 -->
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>5.6.2</version>
</dependency>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>

62
src/main/java/com/wok/supportbot/advisor/MyLoggerAdvisor.java

@ -0,0 +1,62 @@
package com.wok.supportbot.advisor;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Flux;
import org.springframework.ai.chat.client.advisor.api.AdvisedRequest;
import org.springframework.ai.chat.client.advisor.api.AdvisedResponse;
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisor;
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisorChain;
import org.springframework.ai.chat.client.advisor.api.StreamAroundAdvisor;
import org.springframework.ai.chat.client.advisor.api.StreamAroundAdvisorChain;
import org.springframework.ai.chat.model.MessageAggregator;
/**
* 自定义日志 Advisor
* 打印 info 级别日志只输出单次用户提示词和 AI 回复的文本
*/
@Slf4j
public class MyLoggerAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {
@Override
public String getName() {
return this.getClass().getSimpleName();
}
@Override
public int getOrder() {
return 0;
}
private AdvisedRequest before(AdvisedRequest request) {
log.info("AI Request: {}", request.userText());
return request;
}
private void observeAfter(AdvisedResponse advisedResponse) {
log.info("AI Response: {}", advisedResponse.response().getResult().getOutput().getText());
}
@Override
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
advisedRequest = before(advisedRequest);
AdvisedResponse advisedResponse = chain.nextAroundCall(advisedRequest);
observeAfter(advisedResponse);
return advisedResponse;
}
@Override
public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {
advisedRequest = before(advisedRequest);
Flux<AdvisedResponse> advisedResponses = chain.nextAroundStream(advisedRequest);
return new MessageAggregator().aggregateAdvisedResponse(advisedResponses, this::observeAfter);
}
}

53
src/main/java/com/wok/supportbot/advisor/ReReadingAdvisor.java

@ -0,0 +1,53 @@
package com.wok.supportbot.advisor;
import org.springframework.ai.chat.client.advisor.api.*;
import reactor.core.publisher.Flux;
import java.util.HashMap;
import java.util.Map;
/**
* 自定义 Re2 Advisor
* 可提高大型语言模型的推理能力
*/
public class ReReadingAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {
/**
* 执行请求前改写 Prompt
* @param advisedRequest
* @return
*/
private AdvisedRequest before(AdvisedRequest advisedRequest) {
Map<String, Object> advisedUserParams = new HashMap<>(advisedRequest.userParams());
advisedUserParams.put("re2_input_query", advisedRequest.userText());
return AdvisedRequest.from(advisedRequest)
.userText("""
{re2_input_query}
Read the question again: {re2_input_query}
""")
.userParams(advisedUserParams)
.build();
}
@Override
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
return chain.nextAroundCall(this.before(advisedRequest));
}
@Override
public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {
return chain.nextAroundStream(this.before(advisedRequest));
}
@Override
public int getOrder() {
return 0;
}
@Override
public String getName() {
return this.getClass().getSimpleName();
}
}

76
src/main/java/com/wok/supportbot/app/AssistantApp.java

@ -0,0 +1,76 @@
package com.wok.supportbot.app;
import com.wok.supportbot.advisor.MyLoggerAdvisor;
import com.wok.supportbot.advisor.ReReadingAdvisor;
import com.wok.supportbot.record.AssistantReport;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.stereotype.Component;
import java.util.List;
import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY;
import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY;
/**
* @Classname AssistantApp
* @Description
* @Version 1.0.0
* @Date 2025/06/27 14:11
* @Author lyx
*/
@Component
@Slf4j
public class AssistantApp {
private final ChatClient chatClient;
private static final String SYSTEM_PROMPT = "你是一名电商平台的智能客服助手,负责解答用户关于商品、订单、支付、物流和售后等问题。" +
"请主动引导用户提供关键信息(如订单号、商品名),并尽量在不转人工的情况下解决问题。保持专业、耐心、礼貌。";
/**
* 初始化 ChatClient
* @param dashscopeChatModel
*/
public AssistantApp(ChatModel dashscopeChatModel) {
// 初始化基于文件的对话记忆
//String fileDir = System.getProperty("user.dir") + "/tmp/chat-memory";
//ChatMemory chatMemory = new FileBasedChatMemory(fileDir);
// 初始化基于内存的对话记忆
ChatMemory chatMemory = new InMemoryChatMemory();
chatClient = ChatClient.builder(dashscopeChatModel)
.defaultSystem(SYSTEM_PROMPT)
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory),
// 自定义日志 Advisor可按需开启
new MyLoggerAdvisor()
// 自定义推理增强 Advisor可按需开启
//,new ReReadingAdvisor()
)
.build();
}
/**
* AI 基础对话支持多轮对话记忆
* @param message
* @param chatId
* @return
*/
public String doChat(String message, String chatId) {
ChatResponse chatResponse = chatClient
.prompt()
.user(message)
.advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)
.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10))
.call()
.chatResponse();
String content = chatResponse.getResult().getOutput().getText();
//log.info("content: {}", content);
return content;
}
}

64
src/main/java/com/wok/supportbot/app/ProductInfoApp.java

@ -0,0 +1,64 @@
package com.wok.supportbot.app;
import com.wok.supportbot.advisor.MyLoggerAdvisor;
import com.wok.supportbot.record.ProductInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.stereotype.Component;
import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY;
import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY;
/**
* @Classname ProductInfoApp
* @Description 电商商品信息抽取助手App
* @Version 1.0.0
* @Date 2025/06/27
* @Author lyx
*/
@Component
@Slf4j
public class ProductInfoApp {
private final ChatClient chatClient;
private static final String SYSTEM_PROMPT = "你是一名电商商品信息抽取助手," +
"请从用户提供的商品网页内容中提取标题(title)、描述(description)、价格(price)、评分(rating)、评论数(reviewCount)、品牌(brand)、分类(category)等字段。" +
"请严格按照JSON格式返回,不要带任何解释和多余内容。";
public ProductInfoApp(ChatModel dashscopeChatModel) {
ChatMemory chatMemory = new InMemoryChatMemory();
chatClient = ChatClient.builder(dashscopeChatModel)
.defaultSystem(SYSTEM_PROMPT)
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory),
// 可以按需添加日志或其它advisor
new MyLoggerAdvisor()
)
.build();
}
/**
* 商品信息结构化抽取
* @param rawContent 爬取的商品网页内容
* @param chatId 对话ID
* @return 结构化的商品信息对象
*/
public ProductInfo extractProductInfo(String rawContent, String chatId) {
ProductInfo productInfo = chatClient
.prompt()
.system(SYSTEM_PROMPT)
.user(rawContent)
.advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)
.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10))
.call()
.entity(ProductInfo.class);
log.info("Extracted product info: {}", productInfo);
return productInfo;
}
}

88
src/main/java/com/wok/supportbot/chatmemory/FileBasedChatMemory.java

@ -0,0 +1,88 @@
package com.wok.supportbot.chatmemory;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import org.objenesis.strategy.StdInstantiatorStrategy;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.Message;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* 基于文件持久化的对话记忆
*/
public class FileBasedChatMemory implements ChatMemory {
private final String BASE_DIR;
private static final Kryo kryo = new Kryo();
static {
kryo.setRegistrationRequired(false);
// 设置实例化策略
kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
}
// 构造对象时指定文件保存目录
public FileBasedChatMemory(String dir) {
this.BASE_DIR = dir;
File baseDir = new File(dir);
if (!baseDir.exists()) {
baseDir.mkdirs();
}
}
@Override
public void add(String conversationId, List<Message> messages) {
List<Message> conversationMessages = getOrCreateConversation(conversationId);
conversationMessages.addAll(messages);
saveConversation(conversationId, conversationMessages);
}
@Override
public List<Message> get(String conversationId, int lastN) {
List<Message> allMessages = getOrCreateConversation(conversationId);
return allMessages.stream()
.skip(Math.max(0, allMessages.size() - lastN))
.toList();
}
@Override
public void clear(String conversationId) {
File file = getConversationFile(conversationId);
if (file.exists()) {
file.delete();
}
}
private List<Message> getOrCreateConversation(String conversationId) {
File file = getConversationFile(conversationId);
List<Message> messages = new ArrayList<>();
if (file.exists()) {
try (Input input = new Input(new FileInputStream(file))) {
messages = kryo.readObject(input, ArrayList.class);
} catch (IOException e) {
e.printStackTrace();
}
}
return messages;
}
private void saveConversation(String conversationId, List<Message> messages) {
File file = getConversationFile(conversationId);
try (Output output = new Output(new FileOutputStream(file))) {
kryo.writeObject(output, messages);
} catch (IOException e) {
e.printStackTrace();
}
}
private File getConversationFile(String conversationId) {
return new File(BASE_DIR, conversationId + ".kryo");
}
}

2
src/main/java/com/wok/supportbot/demo/invoke/SpringAiAiInvoke.java

@ -18,7 +18,7 @@ public class SpringAiAiInvoke implements CommandLineRunner {
@Override @Override
public void run(String... args) throws Exception { public void run(String... args) throws Exception {
AssistantMessage assistantMessage = dashscopeChatModel.call(new Prompt("你好,我在测试我能否用Spring AI来调用某个AI模型"))
AssistantMessage assistantMessage = dashscopeChatModel.call(new Prompt("hello"))
.getResult() .getResult()
.getOutput(); .getOutput();
System.out.println(assistantMessage.getText()); System.out.println(assistantMessage.getText());

14
src/main/java/com/wok/supportbot/record/AssistantReport.java

@ -0,0 +1,14 @@
package com.wok.supportbot.record;
import java.util.List;
/**
* @Classname AssistantReport
* @Description
* @Version 1.0.0
* @Date 2025/06/27 14:21
* @Author lyx
*/
public record AssistantReport(String title, List<String> suggestions) {
}

14
src/main/java/com/wok/supportbot/record/ProductInfo.java

@ -0,0 +1,14 @@
package com.wok.supportbot.record;
import lombok.Data;
@Data
public class ProductInfo {
private String title;
private String description;
private String price;
private String rating;
private Integer reviewCount;
private String brand;
private String category;
}

62
src/test/java/com/wok/supportbot/SupportBotApplicationTests.java

@ -1,13 +1,73 @@
package com.wok.supportbot; package com.wok.supportbot;
import com.wok.supportbot.app.AssistantApp;
import com.wok.supportbot.app.ProductInfoApp;
import com.wok.supportbot.record.AssistantReport;
import com.wok.supportbot.record.ProductInfo;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import java.util.UUID;
@SpringBootTest @SpringBootTest
class SupportBotApplicationTests { class SupportBotApplicationTests {
@Resource
private AssistantApp assistantApp;
@Autowired
private ProductInfoApp productInfoApp;
@Test @Test
void contextLoads() {
void testChat() {
String chatId = UUID.randomUUID().toString();
// 第一轮商品咨询
String message = "你好,我想买一台适合学生用的笔记本电脑,有推荐吗?";
String answer = assistantApp.doChat(message, chatId);
Assertions.assertNotNull(answer);
// 第二轮物流问题
message = "我上周买的那台电脑现在还没到,能查一下物流吗?";
answer = assistantApp.doChat(message, chatId);
Assertions.assertNotNull(answer);
// 第三轮售后问题
message = "电脑到了,但有点问题。你刚刚说的售后流程能再说一遍吗?";
answer = assistantApp.doChat(message, chatId);
Assertions.assertNotNull(answer);
} }
@Test
public void testExtractProductInfo() {
// 模拟爬取的网页内容建议写简洁但包含关键信息
String rawContent = "这是商品标题:智能手表Pro 2025," +
"描述:这款智能手表支持心率监测和GPS," +
"价格:299美元,评分:4.7星,评论数:1567,品牌:TechBrand,分类:电子产品。";
// 生成随机聊天ID模拟独立会话
String chatId = UUID.randomUUID().toString();
// 调用方法
ProductInfo productInfo = productInfoApp.extractProductInfo(rawContent, chatId);
// 断言结果不为空
Assertions.assertNotNull(productInfo);
// 断言关键字段合理你也可以根据实际字段调整
Assertions.assertNotNull(productInfo.getTitle());
Assertions.assertTrue(productInfo.getTitle().contains("智能手表"));
Assertions.assertNotNull(productInfo.getPrice());
Assertions.assertTrue(productInfo.getPrice().contains("299"));
Assertions.assertNotNull(productInfo.getBrand());
Assertions.assertEquals("TechBrand", productInfo.getBrand());
// 你可以打印结果方便调试
System.out.println("提取的商品信息: " + productInfo);
}
} }
Loading…
Cancel
Save