10 changed files with 445 additions and 2 deletions
-
12pom.xml
-
62src/main/java/com/wok/supportbot/advisor/MyLoggerAdvisor.java
-
53src/main/java/com/wok/supportbot/advisor/ReReadingAdvisor.java
-
76src/main/java/com/wok/supportbot/app/AssistantApp.java
-
64src/main/java/com/wok/supportbot/app/ProductInfoApp.java
-
88src/main/java/com/wok/supportbot/chatmemory/FileBasedChatMemory.java
-
2src/main/java/com/wok/supportbot/demo/invoke/SpringAiAiInvoke.java
-
14src/main/java/com/wok/supportbot/record/AssistantReport.java
-
14src/main/java/com/wok/supportbot/record/ProductInfo.java
-
62src/test/java/com/wok/supportbot/SupportBotApplicationTests.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); |
|||
} |
|||
|
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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"); |
|||
} |
|||
} |
|||
@ -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) { |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -1,13 +1,73 @@ |
|||
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.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.boot.test.context.SpringBootTest; |
|||
|
|||
import java.util.UUID; |
|||
|
|||
@SpringBootTest |
|||
class SupportBotApplicationTests { |
|||
@Resource |
|||
private AssistantApp assistantApp; |
|||
|
|||
@Autowired |
|||
private ProductInfoApp productInfoApp; |
|||
|
|||
@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); |
|||
} |
|||
|
|||
|
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue