问题背景:为什么“有图”却“只回字”?
第一次把扣子智能客服接入公司小程序时,我信心满满地给它配了图文素材:商品图、步骤图、甚至表情包都准备好了。结果用户一问“怎么退货”,客服噼里啪啦甩回三段文字,一张图都没冒出来。老板在群里发了个“?”,我当场社死。
翻日志才发现,扣子默认把“图片”当成富媒体,只在特定通道(如微信客服、飞书)才自动合并;在大多数 WebHook 场景里,如果开发者没显式声明msg_type = 'mixed',平台就干脆把图片字段丢掉,只留文字。换句话说,不是没图,而是图被“静默过滤”了。
痛点总结:
- 用户侧:文字太长,跳出率 38%→61%,人工会话量翻倍。
- 运营侧:准备好的长图、流程图全吃灰,重复答疑。
- 开发侧:日志里 200 OK,看似成功,其实“缺胳膊少腿”。
技术方案:三条路线谁更适合?
我先后试了三种玩法,优缺点直接摆表格:
| 方案 | 实现思路 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| ① 纯文字+外链 | 文字里插 Markdown 图片链接 | 零接入成本 | 微信等环境会自动屏蔽外链,图裂 | 内部系统 |
| ② 多消息分开发 | for 循环,先发文字再发图片 | 逻辑简单 | 两次请求,延迟翻倍;容易乱序 | 低频客服 |
| ③ 单条 mixed 消息 | 构造msg_type=mixed,文字+媒体数组一次发 | 平台原生支持,顺序固定,延迟最低 | 需要 Base64 或临时素材上传,代码量 +30% | 正式生产 |
结论:正式环境直接上③,一次往返就能把“步骤文字+步骤图”绑在一起,用户体验最顺滑。
核心实现:30 行代码搞定图文混合
下面以 Node.js 为例,展示“用户问退货流程→客服返回文字+三张图”的最小闭环。其他语言思路完全一致:先上传临时素材→拿到media_id→组装mixed消息→一次性 POST。
/** * 入口:扣子 WebHook 回调 */ app.post('/coze/webhook', async (req, res) => { const { user_id, content } = req.body; // 1. 根据意图关键词“退货”捞取本地素材 const text = await getText('return_goods'); // 返回 Markdown 字符串 const imgPaths = ['./static/return_1.png','./static/return_2.png','./static/return_3.png']; // 2. 把图片上传到扣子临时素材库(有效期 3 天) const mediaIds = await Promise.all( imgPaths.map(path => uploadMedia(path)) // 返回 media_id 数组 ); // 3. 构造 mixed 消息体 const mixedMsg = { msg_type: 'mixed', items: [ { type: 'text', content: text }, ...mediaIds.map(id => ({ type: 'image', media_id: id })) ] }; // 4. 一次性回复 await replyToUser(user_id, mixedMsg); res.status(200).end('ok'); }); /** * 上传素材并返回 media_id */ async function uploadMedia(filePath) { const form = new FormData(); form.append('file', createReadStream(filePath)); const { data } = await axios.post('https://api.coze.com/v1/media/upload', form, { headers: { ...form.getHeaders(), 'Authorization': `Bearer ${process.env.COZE_TOKEN}` } }); return data.media_id; // 平台返回的临时素材 id }要点拆解:
- 先把图片当文件流上传,拿到
media_id;不要试图把 2MB 的图直接 Base64 塞进 JSON,会 413。 msg_type必须写mixed,否则平台仍按纯文本处理。items数组顺序即用户端展示顺序,想先图后文就调换位置。- 如果图>5 张,建议拼长图或转 PDF,再传 1 个
file类型,避免消息体过长被截断。
性能考量:让 200 ms 再飞一会儿
图文混合最大的开销是“上传素材”这一步,实测 500 KB 图平均 180 ms,2 MB 图 650 ms。上线第一天高峰期 QPS 120,P99 延迟直接飙到 1.8 s,被老板点名。
优化三板斧:
- 预上传 + 缓存
把“退货流程图”等常用素材提前上传,把media_id存在 Redis 并设置 TTL=2 天,用户提问时直接复用,省掉一次 POST。 - 连接池
axios默认不 Keep-Alive,开httpsAgent: new Agent({ keepAlive: true }),TLS 握手复用,延迟降 30%。 - 并行改串行
如果一次要发 6 张图,先并发上传 3 张,返回后再继续,避免瞬间打满带宽导致超时。
压测结果:同样 120 QPS,P99 从 1.8 s 降到 420 ms,服务器 CPU 只涨了 8%,可接受。
避坑指南:那些线上才冒出的雷
- 图裂“白屏”
微信环境会把https://tmp.coze.com/xxx当成“临时域名”,若用户 24 小时后再翻历史,图就 404。解法:把关键图转存到 COS/OSS,拿到永久 URL 再塞进mixed消息。 - media_id 跨环境失效
测试库的media_id在生产库不可用,上线前记得把预上传脚本在正式环境跑一遍。 - 大小写陷阱
文档写的是media_id,实际传mediaId会 400,字段必须一字不差。 - 回调风暴
如果用户长按消息“再次提问”,平台会重复回调,把上传动作再做一次,流量爆炸。加分布式锁(Redis SETNX)过滤重复user_id+msg_id。 - 图床合规
客服图里不小心出现“二维码”或“外部小程序码”,微信会直接拦截整条消息,用户看到“红色感叹号”。审核流程要走完再上线。
下一步:让回复再聪明一点
图文混合只是“能看”,离“好看”还差两步:
- 动态图
把“安装步骤”做成 5 秒 GIF,体积 <1 MB,用户不用点就能秒懂。 - 个性化排序
根据用户画像(新客/老客、安卓/iOS)决定先推图还是先推字,老客往往只看图,新客需要文字安全感。 - 智能压缩
监测用户网络(平台在回调头里带network_type: 3G),自动把 PNG 换成 60% 质量 JPEG,省 60% 流量,降低 30% 失败率。
如果你已经跑通混合消息,不妨把以上三点做成 A/B 实验,用“图文阅读完成率”和“人工转接率”双指标评估,再迭代。客服机器人最怕“一上线就没人管”,持续喂数据,它才会越来越像人。
踩完这些坑,我的客服会话时长从平均 4 min 降到 1.2 min,人工介入率降了 42%。一张图真的能顶三百字,只要平台接口别掉链子。祝你也能让“有图有真相”成为标配,而不是彩蛋。