揭示「保留最近 12 轮」背后是 window−系统提示−工具 schema−检索文档−输出预留−推理预留的完整方程,迁移时容易被忽略。
你的聊天服务保留最近十二轮对话。这个"十二"曾经是经过计算的——来自一个你已不再使用的模型窗口。迁移目标窗口大数倍,所以直觉做法是保留更多轮次——但正是这个直觉做法,会在你当初为截断而优化的那些对话上,让答案质量下降。
"最近十二轮"是一个编译产物。曾经有人测量过一轮对话的平均长度,从窗口大小中减去固定开销,再做除法。这个除法的所有输入都特定于旧模型,但没有任何地方把这个常数旁边的输入记录下来。完整的预算公式是:
window
− system prompt
− tool schemas
− retrieved documents
− reserved output (max_tokens)
− reserved reasoning (if the model thinks before answering)
= tokens available for conversation history
其中两项容易被忘记,而且在迁移时都会咬你一口。reserved output 不是可选项:大多数 API 上,补全内容与输入共享同一窗口,所以一个慷慨的 max_tokens 直接从历史配额中扣除空间——这里的区别涉及 context window 与 max tokens 的概念。reserved reasoning 更新近,而且让人措手不及:在先思考再回答的模型上,思考 token 与可见回复一起计入 max_tokens,所以为非思考模型计算的历史预算完全没有留出这部分空间,导致回答被截断在句子中间。
用标注了变量的公式来计算,代入你自己的数字。假设新模型有 1M token 窗口,你的 system prompt 和 tool schemas 在新分词器下共 6,000 token,你附加最多 20,000 token 的检索文档,并预留 16,000 token 用于输出(包括思考)。
available = 1,000,000 − 6,000 − 20,000 − 16,000 = 958,000 tokens
median turn (measured on your own logs, new tokenizer) = 420 tokens
958,000 / 420 ≈ 2,280 turns
Compare against the old model:
200,000 − 5,000 − 20,000 − 4,000 = 171,000
171,000 / 350 ≈ 488 turns — and you kept 12.
两个数字都很荒谬,而做算术的意义正在于此:旧的常数"十二"从来不是容量限制。它是一个穿着容量限制外衣的质量与成本决策。有人发现超过十几轮后答案变差或者账单变得离谱,于是将其编码为截断规则。所以迁移提出的问题不是"现在能容纳多少轮",而是"质量与成本的权衡点是否还在同一个位置"。
在除法之前,先在目标模型自己的分词器上重新测量中位数轮长度。Token 数量是模型特定的,用错误的分词器计算出的历史预算,会因为两种词表的不同而产生偏差——看看迁移会对压缩比产生什么影响。
这是容量计算会遗漏的部分。在长上下文中,检索精度在位置上并不均匀。Liu et al. 在 Lost in the Middle: How Language Models Use Long Contexts(arXiv:2307.03172,发表于 TACL)中指出,当相关信息位于输入开头或结尾时性能最高,而当位于中间时性能下降——且这种下降随上下文长度增加而加剧。后来的模型对长上下文的处理比那篇研究中的模型更好,厂商也发布了自己的长上下文评估,但这种效果的形状是注意力在长序列上的行为特性,而非已被修复的 bug。
这对于截断策略的影响是直接的。当你保留十二轮时,你保留的一切都在 prompt 末端附近,位于模型最可靠注意的区域。保留两百轮时,第九十轮在中间——技术上存在,实际上几乎不可见。你没有让那条信息更容易获取,而是让它出现在了更差的位置,而且你在每个请求上都为这个特权付出了代价。
第二个效应加剧了这一点。更长的历史包含更多矛盾:用户说过又收回的偏好、被推翻的决定、被纠正过的名字。在十二轮时,过期版本通常已经滑出窗口。在两百轮时,两个版本都存在,模型需要裁决。用户将此体验为助手"记住"了他们曾收回的东西。
有一个交叉点。低于它,保留原始轮次更简单、更便宜、更好。高于它,你应该从历史中选择而不是包含它。交叉点不是一个能从规格表上读出的 token 数量,但它受三个可观测事物的约束。
每轮成本。每个请求都重新发送整个历史。长度为 n 的历史在 n 轮对话中是按总 token 计费的二次方,而 prompt 缓存只能扁平化跨请求字节完全相同的前缀部分。在决定之前,先以新的历史长度计算你第 95 百分位对话的成本。
延迟。预填充大致与输入长度线性相关。一个交互式助手有首 token 响应时间预算,而超出这个预算的历史已经违背了产品需求,无论答案质量如何。
位置。最重要的检查,也是没人运行的检查:取那些答案依赖于早期说过的话的对话,测量模型在新的历史长度下是否仍然能正确回答。这正是一个多轮上下文丢失测试的形状,这是要首先写的测试。
一个在窗口变化中存活的策略有三个层次,而不是一个截断点,并且它把重要内容保留在 prompt 两端——那里的位置效应是有利的。
固定的标题。关于对话的持久事实——用户声明的约束、所做决定、实体名称——作为一个紧凑块紧跟在 system prompt 之后。通过明确的摘要步骤更新而不是累积。这个块很小,位于前端,而且它是你最想让模型注意的东西。
逐字保留的尾部。完整的最近 k 轮,其中 k 的选择是为了对话连贯性而不是容量——代词解析和后续问题很少回溯超过几轮。这位于末端,另一个有利位置。
检索的中间部分。其他一切都按索引,在每个请求中根据与当前消息的相关性来选择,而不是按近因包含。两三个检索片段优于两百个原始轮次,成本只是零头,而且把相关内容放在尾部旁边而不是埋没在中间。
把三个都用 token 预算来表示,而不是轮次计数,并从页面顶部的公式推导预算。这样下一次窗口变化就是一个配置编辑加上重新计算的数字,而"我们应该保留多少轮"这个问题永远不需要从记忆中重新争论。