✅实战:仿PDD自动帮买家申请退款
目标:针对用户反馈的商品质量问题,自动帮买家发起退款。如果是其他问题,则作为客服安抚用户。
需要用到技术:
1、提示词工程+提示词模板
2、Function Call
3、Spring AI
4、对话记忆
5、流式输出
6、结构化输出
提示词工程+提示词模板
角色定义:你是一名专业的电商平台客户体验专家,你的核心职责是高效、准确地处理用户关于商品的反馈。你的首要任务是敏锐识别用户对商品质量的严重不满,并在确认后立即主动为用户申请退款,以最大化客户满意度和信任度。
FewShot:例如:“根本没法用”、“是坏的”、“有瑕疵”、“质量太差了”
COT:我们规定了LLM的第一步、第二步、第三步做什么。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| # Role你是一名专业的电商平台客户体验专家,你的核心职责是高效、准确地处理用户关于商品的反馈。你的首要任务是敏锐识别用户对商品质量的严重不满,并在确认后立即主动为用户申请退款,以最大化客户满意度和信任度。 # Task请严格遵循以下步骤与用户进行对话: 第一步:主动识别与确认 倾听与分析: 仔细阅读用户输入,寻找表明对商品质量严重不满的关键词和情绪,例如: “根本没法用”、“是坏的”、“有瑕疵”、“质量太差了” “和描述完全不符”、“严重色差”、“尺寸根本不对” “一用就坏了”、“有安全隐患” “我要投诉”、“这简直是欺诈” 共情与确认: 一旦识别出潜在问题,首先表达共情和理解。然后,必须用封闭式问题确认问题的具体性质,以判断是否符合“质量问题退款”标准。 正确示范: “非常抱歉给您带来了不好的体验。您是说刚收到的这件衣服袖口已经完全开线了,对吗?” 避免使用: “您有什么问题?”(过于开放) 第二步:判断与执行退款 触发条件:当用户确认了你上一步中提到的具体质量问题(例如,用户回答“对的,就是开线了”或“是的,完全用不了”)时,即视为满足“严重质量问题”标准。 立即行动:无需用户主动提出,你应直接、明确地告知用户你将为其申请退款。 标准话术:“我完全理解,这确实属于严重的质量问题。为了节约您的时间,我将立即为您发起退款申请。款项将按原路径在1-7个工作日内退回,请您注意查收。” 第三步:后续安抚与闭环 表达歉意: 再次为不佳的购物体验向用户致歉。 提供确定性: 告知用户下一步会发生什么,以及他们无需再做任何事。 标准话术: “再次为这次不愉快的购物向您表示诚挚的歉意。退款流程已经启动,您无需再进行其他操作。感谢您的反馈,这帮助我们改进了商品品质。” 3. Limit仅处理质量问题: 仅对明确的“商品质量”问题执行此流程。对于“不喜欢”、“尺寸不合适(非描述不符)”、“物流慢”等问题,请按常规客诉流程处理(如换货、补偿优惠券等),不要直接退款。 不索要额外信息: 在此流程中,默认系统已有用户的订单信息,不要向用户索要订单号、手机号等隐私信息,确保流程顺畅。
|
对话记忆
因为这是个对话功能,肯定需要记忆的,要不然用户前面问了某个订单,后面模型不知道具体哪个订单了,这就尴尬了。
所以,我们需要让我们的chatClient具备对话功能,给他注册一个对话记忆的Advisor:
1
| this.chatClient = ChatClient.builder(chatModel)
|
这里的chatmemory是直接Autowire进来的。
结构化输出
我们在最开始,提供一个newChat的方法,用户第一次和客服开始对话时,就调用这个接口,初始化一些用户和订单的基本信息,并且做一个结构化输出,确保输出固定的内容,这样前端拿到内容之后才能解析出关键信息。
整体的交互流程如下:

先定义出第一轮对话后需要返回给前端的信息,主要是对话id和对话状态,这两个需要后端生成一个新的,然后返回给前端。
1 2 3 4 5 6
| package cn.hollis.llm.llmentor.model; import com.fasterxml.jackson.annotation.JsonPropertyDescription; public record OrderChat( @JsonPropertyDescription("订单号") String orderId , @JsonPropertyDescription("用户Id") String userId , @JsonPropertyDescription("对话Id") String chatId , @JsonPropertyDescription("对话状态") ChatStatus status) { }
|
然后看一下newChat接口的主要实现:
1 2 3 4 5 6
| @GetMapping("/newChat") public OrderChat newChat(String userId, String orderId, HttpServletResponse httpServletResponse) { httpServletResponse.setCharacterEncoding("UTF-8"); return chatClient .prompt() .user(String.format("我要咨询订单相关的售后问题,我的用户id是%s,我的订单号是: %s ,本地的对话Id是 %s,当前状态是 %s", userId, orderId, chatId, ChatStatus.CHAT_START.name())) .advisors(spec -> spec.param(CONVERSATION_ID, chatId) .param("chat_memory_retrieve_size", 100)) .call().entity(OrderChat.class); }
|
这里主要是先生成一个对话Id,把他用作记忆的CONVERSATION_ID,并且把订单号、用户id这些信息告知大模型,让他存下来这些记忆,方便后续对话知道具体的订单号和用户信心。
接着用户开始发起对话。
Function Call
定义一个工具,用于关闭订单。
假设原来应用中已经有一个退款服务了:
1 2 3 4 5 6 7 8 9
| @Service public class OrderManageService { public String getOrderById(String orderId) { return "订单号:" + orderId; } public String refund(String orderId, String reason) { System.out.println("退款成功"); return UUID.randomUUID().to String(); } }
|
那么我们可以定义一个tool,然后让他调用这个关单接口,如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package cn.hollis.llm.llmentor.tools; import cn.hollis.llm.llmentor.service.OrderManageService; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component;
@Component public class OrderTools {
@Autowired private OrderManageService orderManageService;
@Tool(name = "apply_refund", description = "根据用户传入的订单信息发起退款") public String refund( @ToolParam(description = "订单编号,为数字类型") String orderId, @ToolParam(description = "商品名称") String name, @ToolParam(description = "退款原因") String reason) { System.out.println("已为商品:" + name + ",订单号:" + orderId + "申请退款 , 退款原因: " + reason); orderManageService.refund(orderId, reason); return "已为商品:" + name + ",订单号:" + orderId + "申请退款 , 退款原因: " + reason; } }
|
然后在对话中,让模型可以使用这个工具:
1 2 3 4 5 6
| @GetMapping("/ask") public Flux<String> ask(String question, String chatId, HttpServletResponse httpServletResponse) { httpServletResponse.setCharacterEncoding("UTF-8"); return chatClient .prompt() .user(question).tools(orderTools) .advisors(spec -> spec.param(CONVERSATION_ID, chatId) .param("chat_memory_retrieve_size", 100)) .stream().content(); }
|
使用同一个chatId作为对话的CONVERSATION_ID,并且通过tools让模型知道我们有一个关单工具。
效果:
第一轮对话:

第二轮对话:

第三轮对话:

第四轮对话:

通过控制台查看,也可以看到,最后一轮对话的时候,调用了工具进行申请退款,。并且把记忆中的商品、订单、退款原因都带上了。

常见问题
在工具调用的时候,最开始我用的是openai的chatmodel,发现调用工具的时候总是失败,因为他会给我生成两次工具调用,一次是有工具名但是没参数,一次是有参数但是没有工具名,会导致调用都失败,无法成功。换成DashScopeChatModel就解决了。
问题出在大模型流式输出的时候,他返回的工具function这个字段是一次性返回的,还是分段返回的。
这个和不同模型的特点、工具参数的复杂度也有关系。当function call流式输出的时候,判定本次需要调用工具,大模型可能会一次性返回function call,也可能每次只返回一段chunk,这种方式正确的逻辑应当是:检测到tool_call,然后汇聚arguments,进行拼接,直到没有tool_call了,最后调用,但是springai对于这块的处理是有问题的,具体源码如图所示。

他是检测到tool_call就执行,并没有拼接arguments的操作,这就有可能导致他执行工具的时候,要么工具名称chunk还没到,为空,要么工具参数还没到,为空,具体的解决方式可以参考:
✅ReAct Agent 流式输出后续问题
tool_call出现时机 我在后续切换使用其他模型的时候,发现不同模型他的返回的行为方式是有一些差异的。 比如我一开始使用我们公司自己微调量化出来的qwen32b模型,他在进行流式输出的时候,如果需要调用工具,那么第一块chunk确实是直
LLMentor
也就是自己接管流式输出的调用时机,关闭internalToolExecutionEnabled自动调用工具。