Skip to content

🎤 面试官:你们的退款系统是怎么设计的?有没有审核流程?

: 有!我们不是“用户一申请就退钱”,而是分了自动退款 + 人工审核两套流程,根据订单金额、用户等级、风险等级来决定走哪条路。

核心目标就三个:

  1. 不能多退、错退(钱退错人,公司兜不住)
  2. 不能拖太久(用户投诉“退了半个月还没到账”)
  3. 能防欺诈(比如黑产买低价商品然后恶意退款)

✅ 一、退款流程整体是怎样的?

text
用户申请退款

系统判断:走自动?还是人工?

┌─────────────┴─────────────┐
↓(小额/低风险)            ↓(大额/高风险)
自动退款审批               人工审核(运营/客服)
       ↓                           ↓
调第三方支付退钱             审核通过 → 调支付退钱
       ↓                           ↓
更新订单状态                 更新状态 + 通知用户

✅ 二、什么情况下走“自动退款”?

我们设了一套自动通过规则,满足就秒退:

条件说明
退款金额 ≤ 50元小额默认信任
用户是“高信用等级”比如老用户、无历史欺诈记录
商品是“虚拟类” or “未发货”比如电子书、充值卡、刚下单还没出库
未使用优惠券 or 可返还避免优惠券白送
非敏感品类比如不是手机、黄金等高价值商品

满足以上条件 → 系统自动审批 → 调用微信/支付宝的 退款接口 → 钱原路返回 → 更新订单状态。

用户感知就是:“点了申请,3秒到账”。


✅ 三、什么情况下要“人工审核”?

以下情况必须人工介入:

场景为什么?
退款金额 > 500元防资损,必须看一眼
用户是新用户 or 有退款欺诈记录黑产常用小号
已发货但用户说“没收到”可能物流丢件 or 恶意白嫖
使用了大额优惠券要判断是否要追回
同一用户近期频繁退款比如7天退了5笔,疑似职业退款党
敏感商品(手机、奢侈品)高价值,怕调包

这时候系统会把退款申请推到运营后台,由客服或风控人员审核。


✅ 四、人工审核流程怎么做?

我们做了个“退款审核工作台”,运营能看到:

信息用途
订单详情买了啥、多少钱、啥时候下单
退款原因用户填的“不想要了”、“发错货”等
用户信息是否新用户、历史订单数、退款率
物流信息是否已签收?签收时间?
商品类型是否高价值、是否易损
历史行为该用户是否频繁退款?
图片证据用户上传的“破损照” or “发错图”

审核员看完后,点“通过” or “拒绝”。

  • 通过 → 系统调支付接口退款;
  • 拒绝 → 给用户发通知,可申诉。

✅ 五、调第三方支付退款是怎么做的?

和支付一样,不能直接退,要调接口:

java
// 调用微信退款接口
WeChatRefundRequest request = new WeChatRefundRequest();
request.setOutTradeNo(order.getOutTradeNo());
request.setOutRefundNo("refund_" + orderId);
request.setTotalFee(order.getAmount());
request.setRefundFee(refundAmount);

WeChatRefundResponse resp = weChatClient.refund(request);

关键点:

  • 必须用原支付单号
  • 退款金额 ≤ 支付金额(支持部分退);
  • 微信/支付宝会异步回调我们 refund_notify_url
  • 我们收到回调后,才把订单退款状态改为“已退款”。

不能只信前端返回!必须等支付平台的回调才算数。


✅ 六、怎么防止“重复退款”?

这是大坑!我们踩过:

有一次网络超时,系统以为退款失败,重试了3次,结果钱退了3遍,资损严重。

现在我们加了三道锁:

🔒 1. 添加数据库唯一索引

sql
ALTER TABLE refund_record 
ADD UNIQUE uk_out_refund_no (out_refund_no); -- 退款单号out_refund_no唯一

🔒 2. Redis 分布式锁

java
String lockKey = "refund:lock:" + orderNo;
Boolean locked = redisTemplate.setIfAbsent(lockKey, "1", 5, TimeUnit.MINUTES);
if (!locked) {
    throw new BizException("正在处理中,请勿重复提交");
}

🔒 3. 幂等处理

每次退款请求都带一个业务幂等ID(如 refund_1001_1), 系统先查是否已处理过,有就直接返回结果,不再调支付。


✅ 七、退款后,优惠券、积分怎么处理?

不能光退钱,关联权益也要还原:

权益处理方式
优惠券如果是“平台券”,退回券池(状态变“未使用”);
如果是“活动券”,可能不退(看规则)
积分扣掉的积分还给用户
成长值会员等级相关的,也要扣回
赠品如果赠品已发,要提示用户退回

这些都在退款成功后,发个 MQ 消息去异步处理。


✅ 八、怎么监控和对账?

为了保证退款业务的完整性,每天凌晨定时跑一个退款对账任务

  1. 查我们数据库里“已退款”的订单;
  2. 调微信/支付宝的 refund.query(退款查询) 接口,查真实退款状态;
  3. 如果发现“我们记了退款,支付平台没成功”,就告警 + 补发;
  4. 如果“支付平台退了,我们没记”,就手动补单(修改订单状态)。

防止出现“钱退了但订单状态还是待退款”。

🎤 面试官:什么是“幂等”?

一句话:同一个操作,做一次和做一百次,结果都一样。

比如:

  • 你给用户退 100 块,退一次是 -100,退十次变成 -1000,这就不幂等
  • 正确做法:退一次成功,再调十次也只退一次,其他都返回“已处理”,这就叫幂等

🌰 举个真实场景:退款接口被重复调用

用户申请退款 → 我们调微信退款接口 → 网络超时 → 我们以为失败了 → 重试 → 结果微信那边其实成功了 → 钱退了两次!

这就是资损,公司要赔钱的!


✅ 幂等的核心思路:每笔业务请求,都带一个“唯一标识”

这个标识叫:幂等ID(idempotent_id)

比如:

json
{
  "orderId": "1001",
  "refundAmount": 9900,
  "idempotentId": "refund_1001_20240601"  // ← 关键!
}

这个 idempotentId 是前端 or 后端生成的,同一个退款请求,ID 必须一样


✅ 幂等处理的 3 种实战做法(从简单到高级)


🔹 方法一:数据库唯一索引(最简单,推荐)

refund_record 表加个字段:

sql
ALTER TABLE refund_record 
ADD COLUMN idempotent_id VARCHAR(64) UNIQUE COMMENT '幂等ID';

插入退款记录时:

sql
INSERT INTO refund_record 
(order_id, amount, idempotent_id, status) 
VALUES (1001, 9900, 'refund_1001_20240601', 'PROCESSING'); -- 退款中

如果这个 idempotentId 已经存在,数据库直接报错: Duplicate entry 'refund_1001_20240601' for key 'idempotent_id'

我们捕获这个异常,返回:

json
{ "code": "SUCCESS", "msg": "请求已处理", "data": { "status": "REFUNDED" } } //已退款

优点:简单、可靠、不依赖外部服务 缺点:得写数据库,高并发时可能锁表


🔹 方法二:Redis 先查后写(高性能)

用 Redis 记录“这个幂等ID是否已处理”:

java
String key = "idempotent:" + idempotentId; // eg: idempotent:refund_1001_20240601

// 1. 先查是否已存在
Boolean exists = redisTemplate.hasKey(key);
if (exists) {
    // 已处理,直接返回结果
    return getRefundResultFromDB(idempotentId);
}

// 2. 加锁,防止并发
String lockKey = "lock:" + key;
Boolean locked = redisTemplate.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (!locked) {
    throw new BizException("处理中,请勿重复提交");
}

try {
    // 3. 再次确认(防锁住后别人已处理)
    if (redisTemplate.hasKey(key)) {
        return getRefundResultFromDB(idempotentId);
    }

    // 4. 走正常退款流程(调微信、写DB)
    WeChatRefundResponse resp = weChatClient.refund(request);
    
    // 5. 记录结果到 Redis,有效期30天
    redisTemplate.set(key, JSON.toJSONString(resp), 30, TimeUnit.DAYS);

    return resp;
} finally {
    redisTemplate.delete(lockKey);
}

优点:快,适合高并发 缺点:Redis 故障可能丢数据,得配合对账


🔹 方法三:状态机 + 条件更新(最安全)

不光看幂等ID,还要看订单当前状态

sql
UPDATE refund_record 
SET status = 'REFUNDED', 
    refund_no = 'wx123456', 
    update_time = NOW()
WHERE order_id = 1001 
  AND status = 'PENDING'        -- 必须是“待退款”状态
  AND idempotent_id = 'refund_1001_20240601';

如果 UPDATE 影响行数为 0,说明:

  • 要么已经退过了(状态不是 PENDING)
  • 要么幂等ID不对

这时就返回“已处理”,不再调支付。

优点:双重校验,最安全 缺点:逻辑复杂点


✅ 幂等ID 谁来生成?怎么生成?

一般由前端 or 网关层生成,规则要唯一且可读:

场景幂等ID 示例
退款refund_{orderId}_{timestamp}
支付pay_{orderId}_{timestamp}
发券coupon_{userId}_{templateId}

也可以用 UUID,但不好查日志。 我们用“业务前缀 + 订单ID + 时间”,方便排查。

🎤 面试官:现在AI这么火,能不能用AI来自动处理退款?

: 能,但不是“AI一键退钱”,而是“用AI辅助决策”。

真实情况是:

  • AI不能直接调支付接口退钱(这太危险了);
  • AI可以帮你判断“这笔退款该不该过”,然后让系统自动执行 or 推给人工

我们现在就在用AI做“智能初审”。


✅ 一、AI在退款里能干啥?(实战场景)

🔹 1. 自动判断“是否合理退款”

比如用户申请退款,理由是:“衣服有色差”。

AI可以:

  • 分析用户上传的照片(是不是真有色差?还是光线问题?);
  • 对比商品详情页的图片;
  • 判断“色差是否在合理范围内”。

输出: ✅ 合理 → 系统自动通过 ❌ 不合理 → 转人工 or 拒绝

这种我们叫“图像识别 + 规则引擎”,本质是AI辅助质检。


🔹 2. 识别“高风险用户” or “职业退款党”

AI模型可以分析用户历史行为:

  • 近30天退款率 > 80%?
  • 退款理由总是“没收到货”但物流显示已签收?
  • 同一设备注册多个账号?

模型打个“欺诈分”:

  • 分数 > 90 → 高风险,必须人工审;
  • 分数 < 30 → 低风险,可走自动退款;
  • 中间值 → AI建议 + 人工复核。

这种我们叫“风控模型”,大厂都在用。


🔹 3. 自动分类退款原因

用户填的退款理由五花八门:

  • “不喜欢”
  • “和图片不一样”
  • “太慢了”
  • “客服态度差”

AI可以用NLP(自然语言处理) 把这些归类:

  • 商品问题
  • 物流问题
  • 服务问题
  • 主观原因

方便运营做数据分析,优化供应链 or 客服流程。


🔹 4. 自动生成拒绝理由 or 客服话术

如果AI判断“不能退”,可以:

  • 自动生成拒绝文案:

    “亲,您上传的图片显示商品无破损,且已签收,不符合退货规则。”

  • 推荐客服标准回复话术,提升效率。


✅ 二、AI不能干啥?(别想太多)

想法现实
AI直接退钱❌ 不可能!资金操作必须有严格流程
AI完全替代人工❌ 复杂纠纷(如调包、真假货)还得人来判
AI保证100%准确❌ 模型会误判,必须有人兜底
AI自己学习规则❌ 得靠人工标注数据 + 持续调优

AI是“助手”,不是“老板”


✅ 三、真实项目里怎么用AI做退款?

我们是这么设计的:

java
用户申请退款

AI模型实时打分(欺诈分、合理性分)

┌─────────────┴─────────────┐
↓(低风险 + 高置信)        ↓(高风险 or 模型不确定)
系统自动通过               推给人工审核
       ↓                           ↓
调支付退款                  运营结合AI建议做最终判断

AI在这里的角色是:第一道过滤器


✅ 四、谁在用AI做退款?

公司做法
淘宝/天猫用AI识别“仅退款”是否合理,减少商家纠纷
京东用模型判断“是否到货拒收”,优化物流成本
拼多多AI自动处理大量小额退款,提升效率
抖音电商用图像识别判断“商品破损”真伪

小公司可能用不起,但大厂基本都在用。


✅ 五、要上AI,得有什么前提?

不是所有公司都能上,得有:

  1. 足够的历史数据(几千条标注好的退款记录);
  2. 基础的风控系统(用户行为埋点、订单、物流数据);
  3. 工程能力(能接AI模型服务,做AB测试);
  4. 人工复核机制(防AI出错);

否则就是“为了AI而AI”,反而增加成本。

🎤 面试官:如果让你来设计一个AI智能退款客服你该怎么设计呢?

我的目标是:做一个“懂规则、会查数据、能调系统”的AI客服

不再是“固定话术机器人”,而是:

用户说:“我买的iPhone碎屏了,能退货吗?” AI 能:

  1. 理解你是“高价值商品破损”;
  2. 查公司退货政策(是否支持碎屏退货);
  3. 查你的订单是否已签收;
  4. 调图像识别API分析照片;
  5. 综合判断后回复:“符合退货条件,已为您安排上门取件”。

这就需要:角色 + RAG + 向量库 + Function Calling + MCP 五件套。

✅ 第一步:定义“角色”(Role)——让AI知道它是谁

在大模型请求中,设置 system 消息,明确角色和职责:

java
// AI 请求的 Message 对象
public class AiMessage {
    private String role;  // system, user, assistant, function
    private String content;
    private String name;  // function name
    private Object functionCall; // 用于输出 function call

    // getter/setter
}
java
// 构建带角色的 prompt
List<AiMessage> messages = new ArrayList<>();
messages.add(new AiMessage()
    .setRole("system")
    .setContent('''
                你是一个电商平台的AI退货客服,名叫‘小退’。  
                  你的任务是:  
                  1. 理解用户退货请求;  
                  2. 查询退货政策;  
                  3. 调用工具检查订单状态、图像;  
                  4. 给出专业、合规的回复;  
                  5. 复杂问题转人工。  
                  你说话要礼貌、清晰、不承诺无法确定的事。
                '''
                ));

作用:让大模型知道自己是“客服”,不是“搜索引擎”。


✅ 第二步:用 RAG + 向量数据库——让AI“懂公司规则”

问题是:大模型不知道你们公司的“碎屏是否支持退货”。

解法:RAG(检索增强生成) + 向量数据库

🔹 1. 引入依赖(pom.xml)

xml
<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>alibaba-cloud-opensearch</artifactId>
    <version>1.0.0</version>
</dependency>
<!-- 或 Milvus -->
<dependency>
    <groupId>io.milvus</groupId>
    <artifactId>milvus-sdk-java</artifactId>
    <version>2.3.0</version>
</dependency>

🔹 2. 把公司规则文档存进去

比如:

  • 《7天无理由退货规则》
  • 《电子商品特殊退货政策》
  • 《破损商品处理流程》

🔹 3. 调用 Qwen Embedding API 生成向量

用 embedding 模型(如 text-embedding-v3)把文档切片并转成向量,存入向量数据库

  • Milvus
  • Pinecone
  • 阿里云 OpenSearch(支持向量检索)
java
@Service
public class EmbeddingService {

    @Value("${qwen.api.key}")
    private String apiKey;

    public float[] getEmbedding(String text) {
        String url = "https://dashscope.aliyuncs.com/api/v1/services/embeddings/text-embedding-v3/text-embedding";

        OkHttpClient client = new OkHttpClient();
        MediaType JSON = MediaType.get("application/json");

        // 请求体
        JsonObject requestJson = new JsonObject();
        requestJson.addProperty("input", text);
        requestJson.addProperty("model", "text-embedding-v3");

        RequestBody body = RequestBody.create(JSON, requestJson.toString());
        Request request = new Request.Builder()
            .url(url)
            .addHeader("Authorization", "Bearer " + apiKey)
            .post(body)
            .build();

        try (Response response = client.newCall(request).execute()) {
            if (response.isSuccessful()) {
                String responseBody = response.body().string();
                // 解析 JSON 获取 embedding
                JsonElement embeddingElement = JsonParser.parseString(responseBody)
                    .getAsJsonObject()
                    .get("output")
                    .getAsJsonObject()
                    .get("embeddings")
                    .getAsJsonArray()
                    .get(0)
                    .getAsJsonObject()
                    .get("embedding");

                JsonArray embArray = embeddingElement.getAsJsonArray();
                float[] vector = new float[embArray.size()];
                for (int i = 0; i < embArray.size(); i++) {
                    vector[i] = embArray.get(i).getAsFloat();
                }
                return vector;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

🔹 3. 用户提问时,先检索相关规则

java
@Service
public class VectorSearchService {

    @Autowired
    private EmbeddingService embeddingService;

    public List<RuleDocument> searchRules(String query, int topK) {
        float[] queryVector = embeddingService.getEmbedding(query);

        // 伪代码:调用 OpenSearch 向量检索
        SearchRequest searchRequest = SearchRequest.newBuilder()
            .index("return_policy_index")
            .size(topK)
            .vectorQuery("vector_field", queryVector, 0.8f) // 相似度阈值
            .build();

        SearchResponse response = openSearchClient.search(searchRequest);

        List<RuleDocument> rules = new ArrayList<>();
        for (Hit hit : response.getHits()) {
            rules.add(new RuleDocument(
                hit.getSource().get("text").toString(),
                hit.getScore()
            ));
        }
        return rules;
    }
}

返回最相关的规则,拼到 prompt 里:

text
【用户问题】
我买的iPhone碎屏了,能退货吗?

【检索到的规则】
手机碎屏属于严重破损,签收后48小时内可申请退货。

作用:让AI基于最新、准确的内部规则回答,而不是靠“训练数据猜”。


✅ 第三步:Function Calling——让AI“能调系统”

AI 知道规则还不够,还得查“这个用户是不是真买了iPhone?是不是已签收?”

解法:Function Calling(函数调用)

🔹 1. 定义可调用的函数

java
public class FunctionDefinition {
    private String name;
    private String description;
    private Map<String, Object> parameters;

    // 构造函数、getter/setter
}
java
// 构建函数列表
List<FunctionDefinition> functions = new ArrayList<>();

FunctionDefinition checkOrder = new FunctionDefinition();
checkOrder.setName("check_order_status");
checkOrder.setDescription("查询订单状态");
checkOrder.setParameters(Map.of(
    "type", "object",
    "properties", Map.of(
        "order_id", Map.of("type", "string")
    ),
    "required", List.of("order_id")
));
functions.add(checkOrder);

FunctionDefinition analyzeImage = new FunctionDefinition();
analyzeImage.setName("analyze_image");
analyzeImage.setDescription("分析商品图片是否破损");
analyzeImage.setParameters(Map.of(
    "type", "object",
    "properties", Map.of(
        "image_url", Map.of("type", "string")
    ),
    "required", "image_url")
));
functions.add(analyzeImage);

🔹 2. 调用大模型(Qwen + Function Calling)

java
@Service
public class AiService {

    @Value("${qwen.api.key}")
    private String apiKey;

    public Object callWithFunctions(List<AiMessage> messages, List<FunctionDefinition> functions) {
        String url = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation";

        OkHttpClient client = new OkHttpClient();
        MediaType JSON = MediaType.get("application/json");

        JsonObject requestJson = new JsonObject();
        requestJson.addProperty("model", "qwen-max");

        // 消息
        JsonArray messagesJson = new JsonArray();
        for (AiMessage msg : messages) {
            JsonObject m = new JsonObject();
            m.addProperty("role", msg.getRole());
            m.addProperty("content", msg.getContent());
            if ("function".equals(msg.getRole())) {
                m.addProperty("name", msg.getName());
            }
            messagesJson.add(m);
        }
        requestJson.add("input", messagesJson);

        // 函数
        JsonArray functionsJson = new JsonArray();
        for (FunctionDefinition f : functions) {
            JsonObject func = new JsonObject();
            func.addProperty("name", f.getName());
            func.addProperty("description", f.getDescription());
            func.add("parameters", JsonParser.parseString(new Gson().toJson(f.getParameters())));
            functionsJson.add(func);
        }
        requestJson.add("functions", functionsJson);

        RequestBody body = RequestBody.create(JSON, requestJson.toString());
        Request request = new Request.Builder()
            .url(url)
            .addHeader("Authorization", "Bearer " + apiKey)
            .addHeader("Content-Type", "application/json")
            .post(body)
            .build();

        try (Response response = client.newCall(request).execute()) {
            if (response.isSuccessful()) {
                String responseBody = response.body().string();
                // 解析是否返回 function_call
                JsonObject result = JsonParser.parseString(responseBody).getAsJsonObject();
                JsonObject choice = result.getAsJsonArray("output").get(0).getAsJsonObject();
                if (choice.has("function_call")) {
                    return choice.get("function_call");
                } else {
                    return choice.get("text");
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

作用:AI 从“只会说”变成“能做事”。


✅ 第四步:MCP(Model Context Protocol)——让AI“记住上下文”

问题:用户聊了5轮,AI 忘了前面说的订单号、图片链接。

解法:MCP 思想(不是协议,是一种设计模式)

java
@Component
public class SessionContextManager {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final String SESSION_PREFIX = "ai:session:";

    public void saveContext(String sessionId, Map<String, Object> context) {
        String key = SESSION_PREFIX + sessionId;
        String json = new Gson().toJson(context);
        redisTemplate.opsForValue().set(key, json, Duration.ofHours(2));
    }

    public Map<String, Object> getContext(String sessionId) {
        String key = SESSION_PREFIX + sessionId;
        String json = redisTemplate.opsForValue().get(key);
        if (json != null) {
            Type type = new TypeToken<Map<String, Object>>(){}.getType();
            return new Gson().fromJson(json, type);
        }
        return new HashMap<>();
    }
}

🔹 1. 维护一个“上下文状态机”

{
  "session_id": "sess_123",
  "user_id": "u1001",
  "current_intent": "apply_return",
  "order_id": "1001",
  "image_urls": ["https://xxx.jpg"],
  "function_results": {
    "check_order_status": { "sign_time": "2024-06-01 14:30" },
    "analyze_image": { "label": "broken", "confidence": 0.96 }
  }
}

🔹 2. 每次请求,把上下文拼进 prompt

【用户历史】
- 提供了订单号:1001
- 上传了碎屏照片
- 已调用 check_order_status → 已签收
- 已调用 analyze_image → 确认为破损

【当前问题】
现在能退货吗?

作用:AI 不再是“失忆患者”,能做复杂决策。


✅ 第五步:整合流程——完整工作流

java
@RestController
@RequestMapping("/api/ai")
public class AiRefundController {

    @Autowired
    private AiService aiService;
    @Autowired
    private VectorSearchService vectorSearchService;
    @Autowired
    private SessionContextManager contextManager;
    @Autowired
    private OrderService orderService;
    @Autowired
    private ImageAnalysisService imageAnalysisService;

    @PostMapping("/chat")
    public String chat(@RequestBody ChatRequest request) {
        String sessionId = request.getSessionId();
        String userInput = request.getMessage();

        // 1. 获取上下文
        Map<String, Object> context = contextManager.getContext(sessionId);
        List<AiMessage> messages = (List<AiMessage>) context.get("messages");
        if (messages == null) messages = new ArrayList<>();

        // 加入用户输入
        messages.add(new AiMessage().setRole("user").setContent(userInput));

        // 2. RAG:检索规则
        List<RuleDocument> rules = vectorSearchService.searchRules(userInput, 3);
        String ragContext = rules.stream()
            .map(r -> "【规则】" + r.getText())
            .collect(Collectors.joining("\n"));

        // 加入规则到 prompt
        messages.add(new AiMessage()
            .setRole("system")
            .setContent(ragContext));

        // 3. 调大模型 + Function Calling
        List<FunctionDefinition> functions = buildFunctions(); // 上面定义的
        Object result = aiService.callWithFunctions(messages, functions);

        if (result instanceof JsonObject functionCall) {
            // 4. 执行函数
            String funcName = functionCall.get("name").getAsString();
            JsonObject args = functionCall.getAsJsonObject("arguments");

            if ("check_order_status".equals(funcName)) {
                OrderStatus status = orderService.getOrderStatus(args.get("order_id").getAsString());
                String funcResult = new Gson().toJson(status);

                // 5. 记录函数结果(MCP)
                messages.add(new AiMessage()
                    .setRole("function")
                    .setName("check_order_status")
                    .setContent(funcResult));
            }

            if ("analyze_image".equals(funcName)) {
                ImageAnalysisResult analysis = imageAnalysisService.analyze(args.get("image_url").getAsString());
                messages.add(new AiMessage()
                    .setRole("function")
                    .setName("analyze_image")
                    .setContent(new Gson().toJson(analysis)));
            }

            // 再次调用大模型,让它基于函数结果回复
            Object finalResponse = aiService.callWithFunctions(messages, functions);
            messages.add(new AiMessage().setRole("assistant").setContent(finalResponse.toString()));
            contextManager.saveContext(sessionId, Map.of("messages", messages));
            return finalResponse.toString();
        }

        // 纯文本回复
        messages.add(new AiMessage().setRole("assistant").setContent(result.toString()));
        contextManager.saveContext(sessionId, Map.of("messages", messages));
        return result.toString();
    }
}

结果如下:

text
用户:iPhone碎屏了,能退吗?[图片]


1. Role:AI 明白自己是“退货客服”


2. RAG:检索“电子商品碎屏退货政策” → 找到“48小时内可退”


3. Function Calling:
   → 调 check_order_status(order_id=1001)
   → 调 analyze_image(image_url=xxx.jpg)


4. MCP:把订单状态、图片结果存入上下文


5. 大模型综合判断:
   - 规则:碎屏48小时内可退
   - 签收时间:2024-06-01 14:30
   - 当前时间:2024-06-02 10:00 → 在48小时内
   - 图片:确认破损


AI回复:
“您好,检测到商品有破损,且在签收48小时内,符合退货条件。  
已为您安排上门取件,请保持电话畅通。”