背景痛点:人工客服的“三座大山”
做电商的朋友都懂,抖店客服一旦爆单,消息就像雪片一样飞过来。我们团队去年双11高峰期,平均响应时间飙到 3 分钟,差评率直接翻倍。总结下来,痛点就三句话:
- 咨询洪峰:晚 8~10 点并发 300+,人工根本接不住。
- 重复问题:80% 问“发什么快递”“什么时候发货”,客服像复读机。
- 状态不同步:用户后台已发货,客服还跑去 ERP 查,体验断层。
老板一句“降本增效”,我们就开始琢磨把客服先换掉。
技术选型:为什么不是 LangChain、Coze,而是 Dify?
我们把需求拆成 5 个维度打分(1~5 分),结论先看表:
| 维度 | LangChain | Coze | Dify |
|---|---|---|---|
| 电商插件生态 | 2 | 4 | 4 |
| 低代码调试 | 2 | 5 | 5 |
| 私有部署 | 5 | 1 | 5 |
| 中文 NLU | 3 | 4 | 5 |
| API 扩展成本 | 3 | 3 | 4 |
LangChain 自由度高,但电商插件全要自己写;Coze 国内插件多,可惜只能公域部署,订单数据传出去合规部过不了。Dify 开源、插件市场带“订单查询”“优惠券派发”模板,基本拖拖拽拽就能跑,私有化一条命令docker-compose up -d搞定,于是拍板。
核心实现:从意图到回复的全链路
1. Dify 客服 Agent 搭建 3 步走
- 新建 Agent → 选“ChatGPT-3.5 16k”当大脑,温度 0.2,保证回复稳定。
- 知识库上传“商品问答.xlsx”“售后政策.pdf”,自动拆段落,向量维数 1536。
- 在“Tools”里装官方“OrderQuery”插件,把 shop_id、app_key 配进去,调试窗口输入“我的快递单号”能返回物流信息即 OK。
意图识别不用写正则,Dify 内置的 NLU 会把“想退货”“怎么退款”都归到return_goods意图,置信度低于 0.75 再走兜底“转人工”流程,误判率压到 5% 以下。
2. 抖店开放 API 对接(OAuth2.0 示例)
抖店接口分“消息事件”和“订单操作”两套,先拿授权码:
# auth.py import requests, os APP_ID = os.getenv("DOUYIN_APP_ID") APP_SECRET = os.getenv("DOUYIN_APP_SECRET") def get_shop_token(auth_code: str) -> str: """ 用授权码换 shop_token,有效期 7 天,建议 6 小时轮询刷新 时间复杂度:O(1),网络请求常数级 """ url = "https://openapi-fxg.jinritemai.com/oauth2/access_token" payload = { "app_id": APP_ID, "app_secret": APP_SECRET, "auth_code": auth_code, "grant_type": "authorization_code" 来回都是标准 OAuth2,没啥魔法 } rsp = requests.post(url, json=payload, timeout=5) rsp.raise_for_status() return rsp.json()["data"]["shop_token"] # 存 Redis,key=shop_id3. 消息异步处理架构
我们采用“事件驱动 + 队列”削峰,先看图:
- 抖店推送 webhook → 网关统一验签 → 丢 Kafka。
- 消费侧起 20 个 asyncio 协程,拉一条处理一条,平均耗时 350 ms。
- 需要调 Dify 的对话接口,把 user_msg 塞进去,返回 assistant_msg 后,再调“商家发送消息”接口回到抖店。
整个链路无共享状态,水平扩容只加容器。
代码示例:生产级关键模块
1. Webhook 验签 & 事件订阅
# webhook.py import hmac, hashlib, json from flask import Flask, request, abort app = Flask(__name__) SECRET = os.getenv("DOUYIN_WEBHOOK_SECRET") @app.route("/douyin_msg", methods=["POST"]) def douyin_msg(): sign = request.headers.get("X-Sign") body = request.get_data() # 验签,防篡改 mac = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest() if not hmac.compare_digest(mac, sign): abort(403) evt = json.loads(body) # 只处理用户文本消息,其他事件直接 200 if evt["type"] != "text": return "ok" # 写 Kafka,异步化 kafka_producer.send("douyin_msg", value=evt) return "ok"2. 对话状态管理(幂等性)
# state.py import redis, json, uuid r = redis.Redis(host="redis", decode_responses=True) def get_or_create_session(user_id: str) -> str: """ 以 user_id 为维度维护 session,幂等键 = user_id 返回 Dify 需要的 conversation_id """ key = f"conv:{user_id}" conv_id = r.get(key) if conv_id: return conv_id # 首次对话,Dify 会返回新的 conversation_id conv_id = str(uuid.uuid4()) r.setex(key, 3600*6, conv_id) # 6 小时过期 return conv_id幂等关键点:同一 user_id 重复消息不会创建多轮对话,避免“你好+1”刷屏。
3. 敏感信息过滤
# filter.py import re PHONE_RE = re.compile(r"1[3-9]\d{9}") IDCARD_RE = re.compile(r"\d{15}|\d{18}|\d{17}X") def mask_sensitive(text: str) -> str: """ 把手机号、身份证打码,O(n) 扫描一遍 """ text = PHONE_RE.sub("📞", text) text = IDCARD_RE.sub("🆔", text) return text生产级优化:让 300 并发也稳如狗
- 限流:网关层用令牌桶,每秒 150 个请求,抖店单店铺官方上限 200,留 25% buffer。
- 缓存:对话上下文只存 Redis,TTL 6 h,命中率 92%,DB 零访问。
- 熔断:Dify 接口 RT>1 s 且连续 5 次,自动降级到“静态 FAQ 模板”,保证用户不至于沉默。
避坑指南:3 个血泪教训
- 消息字段差异:抖店推送的
msg_id是字符串,而自家早期用整型存,导致重复回复。统一msg_id为 string 做主键。 - Dify 冷启动:私有版首次加载 LLM 会耗时 8~10 s,可提前预热:容器启动后脚本自动发一句“嗨”。
- 订单接口分页:/order/list 默认只给 20 条,高峰期查历史订单记得带
page_size=100,否则循环请求把配额秒光。
效果与数据
上线两周,核心指标对比:
- 平均响应:180 s → 25 s,提升约 300%。
- 人工会话量:日 4.2 k → 1.1 k,释放 3 名客服同学去做私域运营。
- 差评率:2.7% → 1.1%,用户满意度提高最明显。
还没完——开放讨论
目前意图识别只靠向量召回,对“我要那款红色 128 G 有货吗”这类带多重属性的句子容易掉链子。如果把商品知识图谱(SKU/属性/库存三元组)融进来,让 Agent 先精准定位 SKU 再走库存查询,效果会不会再上一个台阶?大家有没有现成的图谱+LLM 联合召回经验,欢迎评论区一起头脑风暴!