多模态上下文裁剪别按图片字符串计费
Ankita 的历史裁剪把图片 base64 长度当作上下文开销,导致截图在模型读取前被错误删除。修复通过读取图片格式头获取尺寸,再估算视觉 token,避免完整解码图片。
AI 深度解读
一张 1.6 MB 的截图,视觉输入可能不到 1,000 tokens,却被聊天程序当成超大文本提前删掉。结果没有报错,模型照常回答,只是根本没看到用户问的那张图。
桌面 AI 伴侣 Ankita 遇到的就是这个问题。作者修复的不只是图片计费,还包括长会话里该保留哪些请求、工具调用如何成组裁剪,以及恢复会话时怎样避免附件丢失。做过多模态聊天或 Agent 历史管理的开发者,值得检查一下自己的实现。
图片有多大,不能看 base64 字符串有多长
问题出在 src/core/history.mjs 的 trimMessages():截图以 base64 data URL 进入历史,旧逻辑直接按字符串长度计算占用。文件越大,就越容易被裁掉。
但按照作者采用的视觉计费方式,图片成本取决于像素,而非 base64 长度。传输体积和模型上下文成本,不能共用一把尺子。
修复后的逻辑只读取图片头,不完整解码:PNG 读取 IHDR,JPEG 查找帧起始标记,GIF 读取 10 字节头部,WebP 解析 VP8 / VP8L / VP8X 数据块。
拿到尺寸后,再估算高细节视觉输入成本:
- 将最长边缩到 2048 px 以内。
- 将短边调整到 768 px。
- 按 512 px 切块,以
85 + 170 × 块数计算 tokens。 - 按每 token 4 字节,换算回裁剪器使用的字节预算。
仓库测试用了一个 1700 × 2200 的 PNG,额外填充 100,000 字节,data URL 超过 100 KB。按上述规则,尺寸先变为约 1583 × 2048,再变为约 768 × 994,共 2 × 2 = 4 块,成本为 85 + 170 × 4 = 765 tokens,即 765 × 4 = 3,060 bytes,低于测试要求的 5,000 字节。
旧逻辑会删,新逻辑会留。这是原文最具体的测试证据。
无法解析尺寸的图片仍退回 URL 长度计费,避免坏数据被算成零成本。作者把这套估算定位为面向 OpenAI-compatible 视觉端点的保守上界,但它不是各家服务的精确计数。
裁剪历史,首先要保住一段完整的对话
图片算对了,不代表历史就裁对了。模型还需要知道当前目标是什么,以及工具为什么返回这些结果。
作者用三条规则约束裁剪。
第一条是保留请求锚点。最新用户消息始终保留,另外最多保留两条较早请求,配置为 USER_REQUEST_ANCHORS = 3。较早请求合计最多占 4,096 字节,目的是防止长工具循环把任务目标和近期纠正挤出去。
第二条是工具交互必须成组。assistant 的工具调用与对应回复组成原子组,TOOL_EXCHANGE_MESSAGES = 2 是最低消息数。只留调用或只留结果,都会破坏对话结构。原文描述旧组按从新到旧的顺序整组移除,并保持请求锚点周围的时间顺序,但没有解释为何选择这个删除方向。
第三条是明确减负顺序和失败底线。预算紧张时,先缩减最大的文本单元;附件文件正文只在最后才允许丢弃;图片只有在视觉 token 预算确实放不下时才删除。
预算也不能全给历史。src/core/agent.mjs 中的 Agent.trimHistory() 会先预留输出 tokens、1,024 字节开销、工具 schemas 和记忆上下文,剩余历史空间低于 512 字节就拒绝运行。
如果连当前请求加核心工具 schemas 都放不下,程序直接报错:
The current request/tool schema exceeds the model context budget.
恢复会话也要做结构修复:丢弃孤立工具消息,把不完整工具调用替换为可见提示 [Incomplete tool calls removed from restored session.],并保留以内容数组存储的用户消息。最后这一项修复了一个隐蔽问题:旧代码只认字符串,连带附件一起删掉,模型随后继续回答过时的上下文。
我的判断
作者最有价值的做法,是把裁剪从「删除长字符串」改成了「维护输入的成本与结构」。图片尺寸估算、请求锚点、工具交互分组,分别解决不同问题,不能互相替代。
但证据边界要分清。原文展示了具体图片测试,没有提供多模型计费对照,也没有裁剪前后的任务成功率。因此,这套视觉上界是否适合所有目标端点,以及固定删除顺序是否更好,目前仍是经验设计。
和按消息数截断相比,这种做法能识别不同内容的成本,也能保护工具协议;代价是解析、预算和恢复逻辑都更复杂。它也没有证明自己优于摘要压缩,两者完全可以配合使用。
最大的缺口是:图片最终被删时,用户不知道。
作者选择优先保住这一轮回答,认为陪伴产品拒绝回答更糟。但如果用户问的是截图里的报错,图片就是关键证据。请求成功,不等于模型收到了完成任务所需的材料。
另外,4,096 字节的旧请求上限只是容量规则,无法判断哪条纠正最重要。重要约束仍可能被挤掉,这个问题没有被解决。
能拿来做什么
- 如果你的前端支持粘贴截图,检查 Node 服务端是否直接用 data URL 长度计算历史预算,并用目标模型的实际规则校准图片估算。
- 如果你在做工具型 Agent,把工具调用和对应结果作为完整单元裁剪,同时为输出、工具 schemas 和记忆预留空间。
- 给会话恢复补上内容数组、孤立工具回复和中断调用的检查,避免存档能读出来,附件却已经丢失。
- 当图片被裁掉时,把状态传到界面;对于明确依赖截图的问题,应让用户知道图片未被发送,再决定继续回答还是重新提交。