上下文管理的分水岭不在"塞不塞得进去",而在"塞进去的还在不在起作用"。塞满历史的窗口与只留结论的摘要,token 成本可能差三倍,质量差距却未必朝低成本的方向走。决定这件事的是三点:多少内容还在承担推理作用、摘要丢了哪些不可再生的信息、续接时状态能否被正确恢复。设计上下文策略,本质是设计一份信息遗嘱——什么必须原样保留,什么可以压缩,什么必须主动丢弃。
1. 窗口的三类浪费
| 浪费类型 | 典型来源 | 成本影响 | 质量影响 |
|---|---|---|---|
| 无关历史 | 已结束话题、跑题问答 | 线性推高输入 token | 轻微,但稀释注意力 |
| 重复信息 | 同一文件多次全文粘贴 | 单条可占掉大半窗口 | 中,模型可能引用旧版本 |
| 格式冗余 | 缩进、空行、包装层、表格边框 | 可观,代码块尤甚 | 通常为零,纯损耗 |
三类性质不同:格式冗余是纯损耗,压掉只有好处;重复信息制造版本歧义;无关历史最微妙——既占 token 又参与注意力分配,删对提质量,删错丢约束。
判据:删掉某段历史,行为不变是冗余,变差是约束,变好是干扰。
2. 四种策略的成本与信息损失
| 策略 | 成本曲线 | 信息损失 | 适用 | 主要失效模式 |
|---|---|---|---|---|
| 全量追加 | 累积平方级 | 无 | 短对话、需回溯 | 溢出、成本失控 |
| 滑动窗口 | 恒定上限 | 丢最早部分 | 闲聊、无状态问答 | 丢系统约束与初始目标 |
| 摘要压缩 | 分段线性 | 有损 | 长任务、多轮迭代 | 摘要漂移 |
| 结构化记忆 | 近恒定 | 只留字段 | 有明确状态的协作 | 字段设计不当,记住垃圾 |
四种不互斥,生产上多是混合:
[system prompt 固定] ← 永不压缩
[结构化记忆槽位] ← 按字段覆盖写
[更早历史摘要] ← 分段滚动摘要
[最近 N 轮原文] ← 滑动窗口
[当前输入]
3. 摘要压缩的三种做法与失真
| 做法 | 机制 | 保真度 | 成本 | 风险 |
|---|---|---|---|---|
| 模型摘要 | 让模型写前情提要 | 中,依赖提示 | 每次压缩一次调用 | 把猜测写成事实 |
| 规则提取 | 正则/解析抽字段 | 高但覆盖窄 | 极低 | 抽不到隐含信息 |
| 混合 | 规则抽硬事实,模型写叙述 | 较高 | 中 | 拼接处可能矛盾 |
失真是最大的隐性代价,且不会立刻暴露。三次滚动摘要后,"用户要求 A"会变成"用户倾向于 A",再变成"用户同意 A"。不是模型坏,是每次压缩都在做有损映射,误差累积。
防守三条:摘要不覆盖硬约束(金额、版本、ID、禁止项走规则进槽位);摘要挂原始轮次区间可溯源;区分"用户说的"与"模型推断的"。
SUMMARY_PROMPT = """\
把以下对话压缩为 200 字以内。规则:
1. 只写明确出现的事实,不得推断或补全。
2. 用户提出的约束、数字、专有名词逐字保留。
3. 未完成事项列为 TODO,不得写成已完成。
4. 不确定内容标注 [?]。
{chunk}
"""
第 3 条最常被省掉,而它正是续接失败的主因:摘要把"待办"写成"已完成",恢复后就不再执行。
4. 结构化记忆:该存与不该存
| 类别 | 该存 | 不该存 | 理由 |
|---|---|---|---|
| 事实 | 项目名、技术栈、版本、环境 | 推导出的结论 | 结论易过期 |
| 偏好 | 输出语言、格式、禁止项 | 一次性要求 | 一次性要求不复用 |
| 结论 | 已定方案与理由 | 讨论中间态 | 中间态污染判断 |
| 进度 | 当前步骤、待办、阻塞点 | 完整执行日志 | 日志可查,不必进上下文 |
| 易变状态 | —(一律实时读) | 缓存的时间戳、计数 | 存下必与真实偏离 |
字段少而硬:字段越多,写入越易夹带噪声,读取时越难判断哪条仍有效。
{
"facts": {"stack": "Astro + Spring Boot 3.3", "env": "prod"},
"prefs": {"lang": "zh-CN", "no_disclaimer": true},
"decisions": [{"id": "D3", "what": "走蓝绿而非灰度", "why": "回滚更快"}],
"progress": {"step": "压测", "todo": ["补 canary 脚本"], "blocked_by": null}
}
decisions 带 id 与 why:没有理由的结论会在下次讨论里被重新推翻,有理由才能判断前提是否仍成立。
5. 检索式记忆与顺序式拼接
| 维度 | 顺序式 | 检索式 |
|---|---|---|
| 选取依据 | 时间远近 | 与当前问题相关度 |
| 成本 | 随轮次增长 | 随命中数,可控 |
| 丢失风险 | 早期关键信息被截断 | 语义不相似者检索不到 |
| 一致性 | 天然保序,因果完整 | 片段化,可能自相矛盾 |
| 适用 | 线性任务 | 跨会话复用 |
失效模式互补:顺序式怕"太远",检索式怕"不像"。所以长期系统两者并用:当前任务线性保序,跨任务知识走检索。
坑在于把"当时说的话"直接当知识。临时判断、被否决的方案、玩笑提议一旦入库,几个月后会被当正式结论召回。入库必须有确认门,只让显式采纳的内容进去。
6. 多轮任务的续接
续接不是"把历史再发一遍",而是恢复三类状态:目标、进度、约束。
| 状态 | 载体 | 恢复失败的后果 |
|---|---|---|
| 目标 | 任务描述,不可压缩 | 模型跑去做别的事 |
| 进度 | 结构化 checkpoint | 重复已完成的步骤 |
| 约束 | 规则槽位 | 违反已确认的边界 |
checkpoint:
task_id: T-2026-11-09-01
goal: "b8 目录 4 篇补写到 8–11KB"
done: [exam-module-time-allocation.md]
doing: conversation-context-management.md
next: [inference-cost-governance.md]
constraints: ["简体中文", "每篇≥4表5码块"]
artifacts: {dir: /root/博客运营官/work/gap-fill/b8/}
checkpoint 要可机读。自由文本的"做到第三篇"无法被程序判断,恢复时模型再理解一遍,每理解一次多一次误差。
7. 系统提示被稀释的表现
上下文变长时,系统提示不是消失,而是权重下降,表现为可观察的行为退化:
| 表现 | 现象 | 与"模型变笨"的区别 |
|---|---|---|
| 格式漂移 | 后几轮开始加解释性开场 | 短上下文同样输入不出现 |
| 约束遗忘 | 禁止项在 20 轮后被违反 | 重贴一遍约束即恢复 |
| 风格回退 | 回到模型默认口吻 | 与话题难度无关 |
| 优先级错乱 | 把后文用户话当更高优先级 | 缩短上下文后消失 |
判断方法:同样的输入在短上下文跑一次。短上下文正常、长上下文异常,问题在上下文管理,不在提示词。
缓解不是把系统提示写更长,而是周期性重述关键约束——每 N 轮或每次任务切换时插入 3–5 条硬约束,成本极低。
8. 成本结构:增长曲线与缓存
# 全量追加的累积输入(示意)
total = sum(n * avg_turn for n in range(1, N + 1)) # O(N^2)
# N=50 时总输入约为单轮的 1275 倍;滑动窗口压成线性,摘要再降斜率
缓存是独立杠杆:前缀稳定时重复部分按缓存价计费,命中率取决于"系统提示 + 历史前缀"是否逐字节一致。任何在前缀里插入时间戳、随机 ID 的做法都会让命中率归零,因此易变内容放尾部、稳定内容放头部,顺序错了缓存全废。
9. 质量评估:长对话该测什么
| 指标 | 定义 | 采集 | 恶化信号 |
|---|---|---|---|
| 一致性 | 同一事实各轮表述是否冲突 | 抽查关键事实 | 出现两个版本 |
| 遗忘率 | 早期约束后期被遵守的比例 | 埋入探针约束 | 探针被违反 |
| 矛盾率 | 结论被自己推翻的次数 | 抽样判定 | 上升 |
| 恢复成功率 | 从 checkpoint 续接后是否继续 | 端到端跑 | 重复已完成步骤 |
最有效的是探针法:早期埋 3–5 条明确但无用的约束(如"全部输出用二级标题"),后期检查是否仍被遵守。比人工读全文便宜。
10. 工程落地
| 决定 | 选项 | 判据 |
|---|---|---|
| 存储位置 | Redis / 数据库 / 文件 | 多实例选集中存储 |
| 压缩时机 | 按轮次 / 按 token 水位 / 按阶段 | 水位最稳,阶段最准 |
| 回滚 | 保留最近 K 个快照 | 摘要出错能退回 |
| 触发者 | 前端 / 网关 / 应用层 | 网关统一,避免各写一套 |
压缩时机选水位的理由:轮次与 token 不成正比,一轮贴个大文件可能顶二十轮闲聊,只有水位对得上真实约束。
11. 排错:长对话退化的六种情况
| 现象 | 根因 | 处理 |
|---|---|---|
| 20 轮后违反禁止项 | 系统提示被稀释 | 周期性重述硬约束 |
| 摘要出现没说过的话 | 提示未禁推断 | 硬事实改走规则提取 |
| 续接后重复已完成步骤 | checkpoint 缺进度 | 补 done/doing/next |
| 成本突然翻倍 | 前缀被污染,缓存失效 | 稳定前置、易变后置 |
| 召回旧方案当结论 | 无入库确认门 | 只让显式采纳者入库 |
| 贴大文件后回答变差 | 单条挤占窗口 | 先摘要或分片 |
12. 边界:什么时候不需要这套东西
| 情境 | 为什么不适用 | 替代 |
|---|---|---|
| < 10 轮短对话 | 开发成本高于收益 | 全量追加 |
| 需精确回溯原文 | 摘要必然有损 | 原文留存 + 检索 |
| 合规要求原文留存 | 压缩即篡改 | 原文归档,另建索引 |
| 一次性批处理 | 无跨轮状态 | 单请求自包含 |
| 强一致多步事务 | 摘要漂移造成状态错乱 | 状态外置,上下文只放引用 |
能外置的状态一律外置:上下文是缓存,不是数据库。把进度存在对话里,等于唯一副本放在会被压缩和改写的介质上。
13. 自测
- 判断一段历史是"冗余""约束"还是"干扰",用什么方法?
- 三次滚动摘要后约束被弱化,是哪一步丢了信息?
- 结构化记忆为什么不该存"易变状态"?
- 顺序式与检索式的失效模式分别是什么?
- 上下文变长后缓存命中率归零,最可能的设计错误是什么?
- 哪些场景下"不做摘要、保留原文"才是正确选择?