跳转到内容

对话历史

一段长时间的对话会撞上一堵硬墙:模型的上下文窗口。本页的一切内容都因这堵墙而存在,而真正有意思的部分在于:哪些东西会被丢弃、哪些会被保留,以及哪些还能再找回来。

与其说它们是几个备选项,不如说它们是一架梯子,从代价最低的开始。

机制 代价 保留 丢失
省略 无 叙述中的每一个字 平淡无奇的旧工具结果
图像剥离 无 它们周围的文本 旧截图
快照 无 以像素形式保留的每一个字 无视觉模型的可读性
摘要 一次模型调用 一段叙述 它没有挑选的细节

省略和剥离先执行,因为它们是免费的,而且在真正重要的意义上无损:它们丢掉的都是没人会再读的冗余内容。

摘要是这四种方式中唯一要花一次模型调用的,也是唯一会替你决定什么才算重要的。两者都是实实在在的代价。而更有意思的是那次调用,因为它出现的时机。

压缩通常在撞墙之前按阈值触发。但它也会被动触发:在提供方自己以请求过大为由拒绝之后——启发式估算低估了提供方实际测到的数量。在这种情况下,采用摘要的压缩器必须再发一次请求来缩减上下文,而它发出的这次请求,恰恰就是刚刚溢出的那一次。

快照压缩把被丢弃的历史渲染成布满像素字形的密集页面,再以图像的形式交还。

其前提源于一个计费上的怪癖:视觉模型对图像按固定费率或按面积计费,与图像里有多少字符毫无关系。一万二千个字符被丢弃的对话,要花掉相当于一万二千个字符的文本 token。同样的字符渲染到一页上,只花一张图像的钱。

它之所以值得拥有,而不只是显得聪明,是因为它不需要模型调用、不需要密钥,也不需要网络。它是溢出恢复期间唯一始终可用的策略。

重建后的形态是:原文照录的头部、转为图像的中间部分、原文照录的尾部。头部是任务以及下游一切都要回指的那些约束;尾部是当前工作所在的位置。中间部分被重读得最少,代价却最高。

快照在每一轮都从保留的源文本重新渲染,绝不基于上一轮的像素。对图像再成像,字形每压缩一次就损失一代,直到没有任何东西能读懂它们。

过去,每次运行框架运行都从空历史开始,靠提示词里的 <context> 块重建连续性——那是对话的摘要,而不是对话本身。由此带来三个后果:

  • 恢复等于重新讲述。 模型从未看到自己先前的推理,也没看到工具实际返回的字节,只看到对它们的描述。
  • 回退随进程一同消亡。 checkpoint 是指向实时消息列表的一个索引,因此重启会丢失模型设过的每一个标签。
  • 重试无法区分“没有可重试的内容”和“什么都没加载”。 无论哪种情况,重启后失败的那一轮都已从实时状态中消失。

这三个问题都通过持久化对话记录得到解决。它在每个回合边界保存,绝不在回合中途保存:在工具调用与其结果之间捕获的历史,没有任何提供方会接受,因此在这一瞬间崩溃,恢复的是上一个完整回合,而不是一个无法重放的半截回合。

恢复后的运行会裁剪到最新的消息,剪切点是不留下孤立工具结果的那种边界——一次结果如果它的调用被裁掉,这种请求会被提供方直接拒绝,而一份发不出去的对话记录比一份更短的记录更糟。

每条消息都记录自己所承接的上一条消息,每个对话都指明自己当前的分支末端。这一个决定就是整个特性的全部。

建立分支不写入任何内容。 它只是移动指针。你离开的那条路径仍然在那里,所以切回去只是再移动一次指针,而不是做一次恢复——而且为了重跑而修改提示词,也不再会让你失去那个正在用来对比的答案。

没有它,任何形式的后退都必须隐藏或删除消息,而隐藏是一个后来的读者看不穿的谎言。

分支 分叉
发生的事 指针移动 路径被复制
结果去向 同一个对话 一个新对话
代价 无 分支的一份副本

分叉会用全新的 id 复制,而不是共享数据行。两个共享数据行的对话会互相看到对方后来的消息,而这恰恰是分叉绝不该做的事。

运行期间的 checkpoint / rewind 组合会标记一个干净的回合边界,并在一次探索之后回到那里,同时保留一份摘要。由于检查点与对话记录一起持久化,模型在重启前设下的标签仍然会落在它当初被设置的位置。目标没有在裁剪中存活的检查点会被丢弃,而不是被就近钳制——“回到该标签”却落在别处,恰恰是标签存在的意义所在:防止这种失败。