背景与痛点
把 AI 客服塞进微信小程序,听起来像“调个接口”那么简单,真动手才发现到处是坑:
- 微信要求域名 HTTPS 备案,Dify 默认本地端口 5001,直接调不通
- 小程序 request 并发 10 条封顶,高峰秒回 50+ 提问就 502
- 冷启动 3~4 s,用户以为卡死,狂点屏幕触发重试,结果雪崩
- 返回 Markdown 富文本,小程序原生 text 组件直接罢工
- 审核小哥一句“涉及用户生成内容”就打回,补充“敏感词过滤”材料跑到秃头
一句话:接口通了 ≠ 能用,能用 ≠ 好用,好用 ≠ 能上线。
技术选型对比
| 维度 | 微信云开发·原生智能接口 | 自建后台 + Dify |
|---|---|---|
| 模型灵活度 | 固定微信提供,不可换 | 任意 LLM、Embedding 即插即用 |
| 费用 | 按调用量阶梯计费,不可控 | 自建服务器 + Dify 开源,成本线性 |
| 富文本输出 | 仅支持文本 | 支持 Markdown、图片、卡片 |
| 私有知识库 | 无 | 直接上传文档、自动分段 |
| 审核风险 | 微信侧已过滤 | 需自己对接“内容安全”接口 |
结论:需要“私有知识库 + 可换模型 + 富文本”,Dify 赢;剩下就是怎么让它在微信生态里跑得稳。
核心实现细节
1. 整体架构
小程序 ←→ 微信←→ 业务网关(Nginx + Node)←→ Dify /api/v1/chat-messages
网关负责三件事:
- 反向代理加域名备案
- 做缓存、限流、熔断
- 把 Dify 的 SSE 流式回答转成长连接,减少小程序重复建连开销
2. 鉴权链路
Dify 使用“应用级 API Key”——把 key 放网关 Header,小程序侧完全感知不到,避免前端泄露。
微信侧用户身份用wx.login拿code→ 后台换session_key→ 生成自研 JWT,与 Dify 无关,却保证后续“谁问了什么”可追踪。
3. 请求封装(小程序端)
// utils/dify.js const BASE = 'https://api.yourdomain.com/dify'; // 已备案 const request = (data) => { return new Promise((resolve, reject) => { wx.request({ url: `${BASE}/chat-messages`, method: 'POST', header: { 'Content-Type': 'application/json', 'X-Client': 'weapp' }, responseType: 'text', // 关键:流式返回 enableChunked: true, // 微信基础库 2.25+ data, success: resolve, fail: reject }); }); };4. 流式渲染
小程序enableChunked每次收到一块就触发onChunkReceived,把增量文本 append 到页面,3 s 内完成首字展示,体感延迟降到 600 ms 以内。
5. 缓存 & 幂等
- 问题分类层(售前/售后/物流)固定,问题→答案 1 对 1 场景占 60%,直接 Redis 缓存 key=hash(question),TTL 1 h,命中后 30 ms 返回
- 对同一用户 3 s 内重复点击,用防抖 + 请求 ID 去重,保证幂等,避免 Dify 重复扣费
6. 高并发兜底
网关层令牌桶 200 r/s,超量返回 HTTP 429,小程序端收到后弹“客服忙,请稍候”并禁用按钮 5 s;同时把溢流写进任务队列,Worker 异步重试,保证不丢单。
代码示例:单轮问答完整组件
// pages/chat/index.js Page({ data: { list: [], // 对话数组 inputTxt: '', loading: false }, // 输入框变化 onInput(e) { this.setData({ inputTxt: e.detail.value }); }, // 点击发送 async send() { const { inputTxt, list } = this.data; if (!inputTxt.trim()) { return; } this.setData({ loading: true }); // 1. 本地追加用户问题 const userItem = { role: 'user', content: inputTxt }; this.setData({ list: [...list, userItem], inputTxt: '' }); // 2. 构造 Dify 格式 const body = { inputs: {}, query: inputTxt, response_mode: 'streaming', conversation_id: this.data.convid || null // 首次为空 }; // 3. 流式接收 let aiText = ''; await request(body, { onChunkReceived: (res) => { const lines = res.data.split('\n').filter(l => l.startsWith('data:')); lines.forEach(line => { try { const chunk = JSON.parse(line.slice(5)); if (chunk.answer) { aiText += chunk.answer; // 更新最后一条 AI 消息 const aiItem = { role: 'assistant', content: aiText }; this.setData({ list: [...this.data.list.slice(0, -1), aiItem], convid: chunk.conversation_id }); } } catch (e) { /* ignore */ } }); 一句话:接口通了 ≠ 能用,能用 ≠ 好用,好用 ≠ 能上线。 [](https://t.csdnimg.cn/l0Z1) ---