最近给 QQ Bot 的拟人插件补了一点 GIF / 动态表情理解能力。先说清楚:这不是一个“让 LLM 完美理解动图”的通用方案,也不是什么标准答案,只是我在自己的 QQ Bot 场景里试出来的一种折中办法。
我的目标很小:群里有人发一个动态表情时,Bot 不至于完全看不懂上下文。它可以知道“这个表情大概是在点头、嘲笑、惊讶、举牌子、摆手”,然后再由主模型决定要不要接话、怎么接话。它不需要逐帧还原剧情,也不需要像视频理解那样分析复杂镜头。
问题:GIF 对聊天模型来说经常是空的
QQ 群聊里的动态表情很常见,但到了 Bot 这边,它可能只是一个 image、mface 或 gif 消息段,带一个 URL、文件名和少量 summary。对只吃文本的 LLM 来说,这类消息几乎等于:
[对方发送了一个动态表情]如果直接忽略,Bot 会漏掉很多群聊语气。比如别人发了一个“疯狂点头”的 GIF,下一句 Bot 可能还在认真追问;别人发的是“震惊后退”的动图,Bot 却不知道对方其实是在吐槽。
但反过来,如果把 GIF 当完整视频理解,又会遇到几个现实问题:
- 不同模型和 provider 对 GIF / 视频输入支持不一致;
- 动态表情通常很短,完整视频链路有点重;
- 群聊回复不能等太久,超过几秒体验就明显变差;
- QQ 图片 URL 下载还涉及重定向、大小限制和内网地址拦截;
- 一个群友连续丢好几个动图时,不能让 Bot 每个都慢慢分析。
所以我最后没有追求“完整视频理解”,而是把需求收窄成“给主对话模型提供一段足够用的上下文摘要”。
思路:把动图变成一张时间顺序拼图
核心做法很简单:
- 识别本轮消息里可能是 GIF / 动态表情的片段。
- 安全下载图片字节,限制大小和跳转。
- 用 Pillow 在本地解码 GIF。
- 按时间顺序抽几帧关键帧。
- 把这些帧排成一张 contact sheet,也就是拼图。
- 把这张拼图交给现有视觉模型,让它输出一段短中文摘要。
- 把摘要注入到本轮文本上下文里,交给后面的 Agent / LLM 自己判断。
大致可以理解成这样:
GIF 字节
-> 解码帧
-> 抽样 4-8 帧
-> 渲染成一张 JPEG 拼图
-> 视觉模型摘要
-> [动态表情摘要:角色点头后举起牌子表示赞同。]
-> 主回复模型继续判断是否回复这里比较关键的一点是:主回复模型并不直接看到原始 GIF,它看到的是一句“系统注入的上下文文本”。这样做牺牲了一些细节,但换来了更稳定的模型兼容性。
为什么不是把每一帧都丢给模型
一开始很容易想到:抽出很多帧,然后作为多张图片发给视觉模型。但我没有这么做,主要是因为动态表情不是相册。
多张图片输入时,模型有时会把它们当成几张不相关的图,而不是同一个短动画的连续时间线。把关键帧拼成一张图,再在 prompt 里说明“编号越大表示越靠后的画面,不是多张无关图片”,模型更容易按“短动画”去理解。
在我的实现里,视觉摘要 prompt 会要求它关注:
- 主体是谁或是什么;
- 动作如何变化;
- 表情和情绪;
- 有没有画面文字;
- 作为表情包时可能想表达什么;
- 输出控制在很短的中文摘要里。
最终摘要不会说“这是第几帧”或者“我看到了拼图”,因为这些都是实现细节,不应该污染聊天上下文。
接入到回复链路
在 personification 插件里,这个能力不是单独让 Bot 立刻回复,而是放在普通消息解析阶段。
消息进入后,回复处理器会遍历消息段。遇到 mface、image 或 gif 时,会尝试判断它是不是动态表情。开启 GIF 理解后,如果命中,就走下载和摘要流程;如果没开启,仍保持原来的保守行为。
成功时,消息文本里会多一段类似这样的内容:
[动态表情摘要:角色先挥手,然后露出开心表情。]失败时则退回更朴素的占位符:
[对方发送了一个动态表情]这个设计对我来说很重要:GIF 理解只是“补充上下文”,不是替模型做决定。是否回复、该不该接梗、要不要沉默,仍然交给后面的 LLM 语义链路处理。
一些保护措施
这类功能看起来只是“看图”,但放进真实 Bot 里以后,保护措施比算法本身更重要。
我现在主要做了这些限制:
- 默认关闭,需要明确打开;
- 单个 GIF 默认最多 8MB;
- 单个 GIF 最多解码 180 帧;
- 默认最多抽 8 帧;
- 单轮消息默认只理解 1 个 GIF;
- 下载、抽帧和视觉摘要有总超时;
- 运行时还有 35 秒硬上限;
- 失败或超时都降级成占位符;
- 按 GIF 内容哈希做摘要缓存,重复表情可以复用;
- 下载图片时检查 URL scheme、host、重定向和解析到的 IP,避免明显危险地址。
这些限制并不是为了让效果更好,而是为了让功能在群聊里“不拖垮主流程”。QQ Bot 的体验很依赖及时性,一个动图理解再聪明,如果让普通回复卡住几十秒,也不太值得。
它能解决什么,不能解决什么
这个方案比较适合短的动态表情,尤其是那种“动作和情绪很明确”的 GIF。例如点头、摇头、疑惑、震惊、举牌、拍桌、转圈、擦汗之类。
它不太适合这些情况:
- GIF 很长,前后剧情复杂;
- 关键动作刚好没被抽到;
- 梗依赖某个角色、作品或网络背景;
- 画面文字很小;
- 动图本身压缩严重;
- 群友发的是带强语境的私货表情,单看画面不够判断。
所以我更愿意把它叫做“上下文增强”,而不是“GIF 理解”。它给 LLM 一点线索,让 Bot 不至于完全失明,但不保证每次都懂。
这次我学到的点
做这个功能时,我最大的感受是:在聊天 Bot 里,很多能力不一定要追求端到端的“强理解”。有时候把一个复杂输入压缩成稳定、便宜、可降级的中间表示,反而更适合接入真实对话系统。
GIF 本身是视觉问题,但在 QQ Bot 的回复链路里,它最后会变成语义问题:这段动态表情对当前聊天意味着什么?Bot 要不要接?要怎么接才自然?这些判断还是应该留给主模型。
所以我比较喜欢现在这个分工:
- 本地代码负责下载、抽帧、拼图、限流、缓存、超时和降级;
- 视觉模型负责把短动画压缩成一句语义摘要;
- 主对话模型负责根据上下文决定最终反应。
这只是我在自己的 QQ Bot 里的一个解决方法。如果你的场景里已经有稳定的视频理解模型,或者你不需要那么严格地控制延迟和成本,直接走视频 / 多模态链路可能更合适。对我这个群聊拟人 Bot 来说,关键帧拼图加短摘要,暂时是一个够用、可控、也比较容易回退的方案。