智能客服开发实战:基于扣子平台实现自动化退款功能
电商退款之痛:人工工单为何扛不住高并发
大促零点刚过,客服后台瞬间涌出上千条“仅退款”工单。传统做法靠人工逐条核对:打开订单→复制单号→跳转支付后台→输入金额→短信验证码→提交。平均一单 3 分钟,客服满负荷也只能处理 20 单/小时,高峰时段积压 6 小时起步,买家投诉、平台扣分、店铺权重下滑,恶性循环。
智能客服方案把“人工逐条”变成“规则批量”:买家在聊天窗口输入“我要退 88 元”,系统 1 秒内完成订单校验、风控判断、支付接口调用、结果回传,全流程零人工干预。对比数据:同样 1000 单,人工需要 50 个客服 8 小时,智能客服 2 台 4C8G 容器 10 分钟搞定,且 7×24 在线。
技术方案全景:RPA+规则引擎双轮驱动
扣子平台 RPA 能力与 API 对接设计
扣子把“网页操作”抽象成可编排的积木块:打开页面、点击元素、读取文本、填充表单、等待弹窗。退款场景下,平台预置了淘宝、京东、拼多多、抖音等电商后台组件,开发者只需拖拽即可模拟人工点击。对外接口统一走 OpenAPI 网关,鉴权采用 AK/SK + 限时令牌,回调地址支持 HTTPS 双向校验,避免被抓包重放。
退款业务状态机实现(含幂等性处理)
退款单状态只有 5 个:待校验→校验通过→风控中→已退款→退款失败。任何一步都可重入,利用数据库唯一索引 + 乐观锁保证幂等。状态流转用枚举硬编码,拒绝魔法数,防止 ABA 问题。
class RefundStatus(Enum): PENDING = 0 CHECKED = 1 RISKING = 2 SUCCESS = 3 FAIL = 4 def change_status(order_id, from_status, to_status): # 先校验当前状态是否等于 from_status,再 CAS 更新 row = db.execute( "UPDATE refund SET status=%s,update_time=NOW() " "WHERE order_id=%s AND status=%s", (to_status.value, order_id, from_status.value) ) if row == 0: raise RuntimeError("并发状态冲突或已处理")基于规则引擎的风控模块(伪代码示例)
规则引擎采用开源框架Drools-lite,把“退款金额>订单实付 50%”“近 30 天退货率>30%”“收货地址为黑名单”等条件写成 DSL,热更新无需重启。伪代码如下:
rule "high_amount_refund" when $r: Refund(amount > $order.payAmount*0.5) then $r.block("金额超限"); end核心代码落地:从订单校验到异步回调
订单信息获取与校验(含异常处理)
def get_order_and_validate(order_id): """ 查询订单并做基础校验 返回 dict,异常直接抛出自定义异常,方便上层统一捕获 """ order = rpc_order.get(order_id) if not order: raise OrderNotFound() if order["status"] != "COMPLETED": raise OrderStatusError("只有已完成订单才能退款") if order["refund_status"] != "NO_REFUND": raise DuplicateRefund() return order异步退款任务队列实现(Celery 版)
@celery.task(bind=True, max_retries=3) def async_refund(self, order_id, amount_cents): """ 异步任务:真正调用支付网关退款 失败自动重试,3 次后进入人工队列 """ try: order = get_order_and_validate(order_id) tx_id = pay_gateway.refund( order["pay_tx_id"], amount_cents, reason="用户自助退款" ) # 更新状态 + 写入退款单号 db.execute( "UPDATE refund SET status=3,tx_id=%s WHERE order_id=%s", (tx_id, order_id) ) except Exception as exc: # 记录详细日志,供人工复核 logger.error("Refund fail: %s", exc, extra={"order_id": order_id}) // 延迟重试,指数退避 raise self.retry(exc=exc, countdown=2 ** self.request.retries)结果回调通知机制
支付网关退款成功后,会 POST 回调到/callback/refund。接口内部只做两件事:验签 + 发 MQ。下游系统(CRM、BI、短信)各自订阅,实现最终一致性。
@app.route("/callback/refund", methods=["POST"]) def callback(): sign = request.headers.get("X-Sign") if not verify_sign(request.data, sign): return "fail", 400 payload = request.get_json() # 发布到 RabbitMQ,fanout 模式,多下游并行消费 mq.publish("refund.success", payload) return "ok"性能优化三板斧:索引、漏桶、日志
数据库查询优化(索引设计)
退款单表 2000 万行,核心查询WHERE order_id=?与WHERE status=? AND create_time<?。建立联合索引(order_id)与(status, create_time),把状态枚举放在最左,范围扫描行数从 50 万降到 90 行,RT 从 800 ms 降到 12 ms。
并发控制策略(漏桶算法实现)
支付网关限流 200 TPS,超过直接拒绝。自研漏桶保护:
class LeakyBucket: def __init__(self, rate, capacity): self.rate = rate # 每秒漏出速率 self.capacity = capacity # 桶容量 self.water = 0 self.last = time.time() self.lock = threading.Lock() def acquire(self, drop=1): with self.lock: now = time.time() # 先漏水 self.water = max(0, self.water - (now - self.last) * self.rate) self.last = now if self.water + drop <= self.capacity: self.water += drop return True return False退款任务提交前先acquire(),失败则延迟 1 s 重试,既保护下游,又避免自身线程堆积。
日志监控方案
- 业务日志:JSON 格式,字段含
order_id、status、cost_ms,方便 ELK 聚合 - 性能日志:统计每 30 秒退款成功数、平均耗时、失败率,Grafana 画线
- 异常告警:Prometheus 采集
refund_fail_total,1 分钟增长率>5% 立即@值班
避坑指南:资金安全与灰度发布
资金操作的安全审计要点
- 写操作前先写
refund_log,再真正调用支付接口,保证“日志先行” - 所有金额用“分”存储,64 位整型,避免浮点精度
- 关键字段加入数据库
row_version乐观锁,防止并发提交导致多退
第三方支付接口的容错设计
- 超时重试:读超时 3 s、连接超时 1 s,重试 2 次,间隔 1 s
- 幂等令牌:调用前先在本地生成
idempotency_key=md5(order_id+amount),支付网关支持原路退回 - 回滚补偿:若支付网关返回“处理中”,任务标记
RISKING,定时轮询 5 分钟,最终成功或失败再推进状态
灰度发布策略
- 白名单维度:先让内部员工订单走智能客服,观察 24 小时无异常
- 城市维度:再开放江浙沪,流量占比 10%,收集失败率、客诉率
- 数据回滚:一旦异常,切换开关
REFUND_BOT_ENABLE=False,流量瞬间切回人工,无需重启服务
未来展望:用 LLM 提升退款原因分类准确率
规则引擎虽快,却只能处理“硬规则”。实际聊天里买家一句“东西不喜欢”可能隐含“质量差”“尺寸小”“颜色丑”多种原因。如何用 LLM 在海量非结构化文本中自动打标签,并给出可解释性?模型微调该用 Prompt Engineering 还是 LoRA 微调?欢迎一起探讨。