结论先行
"上下文是有限资源,不是仓库"这句话流传已久,真正有价值的其实是后半句:摘要、检索、丢弃,是三种不同的工程决策,而不是三种"省法"。它们省下的内容相同,丢掉的却完全不同。
作者搭了一座可复现的台架来量化这件事。同一段 28 条历史记录、同一组 10 个问题,只改变"进上下文的那部分是怎么来的"这一个变量,四种策略各跑一遍,结果如下:
| 策略 | 上下文体积 | 答案还在 | 模型答对(3 轮 × 10 题) |
|---|---|---|---|
| 全量 full | 1498 字符 | 10/10 | Qwen 27/30、GLM 28/30 |
| 摘要 summary | 401 字符 | 1/10 | 3/30、6/30 |
| 检索 retrieval | 220 字符 | 8/10 | 24/30、21/30 |
| 丢弃 recent | 375 字符 | 2/10 | 6/30、6/30 |
先不要盯着答对率,看最容易漏掉的那一列:"答案还在"。它衡量的是这份上下文里到底还有没有回答该问题所需的事实,属于机械判定,不看模型脸色。
摘要把 1498 字符压到 401,代价是 10 个事实只剩 1 个。它丢的是细节:变量名、人名、数值、否定句。检索把上下文压到 220 字符,约原体积的七分之一,单点问题 24/24 全对,看似赢家,但它在"跨条"和"换说法"两类题上各归零。丢弃只留最近 8 条,压到 375 字符,近期发生的事一条不少,历史整段消失,10 题里只答得出 2 题。
最关键的一格在后面。把两个模型共 240 次调用,按"答案在不在上下文里"拆开:
- 材料里有答案:126 次调用,答对 118 次,命中率 93.7%;
- 材料里没有答案:114 次调用,答对 0 次。其中 68 次老老实实说"不知道",43 次直接编了一个答案。
不存在"材料里没有但模型推出来了"这回事。缺失的信息不会被推理出来,只会被填出来。
原理:窗口是预算,不是容量
先把归因讲清楚,否则后面全是经验之谈。
第一,"塞满"不是把仓库填满,而是把预算花光。模型读上下文的方式不是读一遍记住,而是每一句话都在和别的句子争夺注意力。多塞一段无关历史,不是"多存了一份",而是让每一句真正有用的话少分到一点权重。"上下文越满越笨"在机制上有两层:一层是超出窗口被硬截断,另一层是没超窗口但被稀释。第二层更隐蔽。
第二,省下来的东西一定会丢,问题只是丢在哪。三种策略没有优劣之分,区别是丢掉哪一类信息:
| 策略 | 怎么省 | 丢什么 | 什么时候翻车 |
|---|---|---|---|
| 摘要 | 按重要性排序,只留"讲了好几遍"的 | 只出现一次的冷门具体值 | 下游要精确值(变量名、数值、人名) |
| 检索 | 按与问句相似度取最相关的几条 | 与问句字面不相干的上下文 | 下游要跨条汇总,或换一种问法 |
| 丢弃 | 只留最近 N 条 | 全部历史 | 下游要问"当初为什么这么定" |
第三,缺失的信息不会自己长出来。114 次"材料里没有"的调用,答对 0 次。里面 68 次模型如实说"不知道",另外 43 次,它编了一个完整、自信、格式完全正确的错误答案。
第四,所以压缩率不是指标,"下游问题集合上的答对率"才是。把上下文压到 15% 本身不是成绩;"压到 15% 之后,下游问题还答得出几成"才是。这句话听着像常识,但绝大多数"上下文优化"的讨论都停在第一个数上。
拆开看:三种省法,各丢一类
摘要丢细节。台架里的摘要不是让模型写一段概述,而是一个可复现的代理:按词频抽句,预算 28%,出现多的句子留下,只出现一次的丢掉。结果很干净——根因那条记录留下了主语,丢掉了变量名 tenant_id;影响面那条留下了"订单",丢掉了最终数字;"没有丢单"这种否定句整条消失。这不是刻意挑选的例子,而是词频抽句的必然结果:越具体的信息,出现的次数越少,越容易被预算淘汰。
检索丢全局。台架里的检索是字面检索,按字对重合度取最相关的 5 条。它表现好得出乎意料:单点问题 8/10,模型答对 24/24。但它有两道题直接归零。跨条题:问题要的答案分散在第 4 条和第 13 条两条记录里,检索取 top 5 只捞到其中一条,另一条与问句不够像,进不来。换说法题:问的是同一个事实,但问句里没有一个词与那条记录重合,检索打 0 分,这条记录根本进不了候选。
丢弃丢历史。只留最近 8 条,近期的事都在,历史全没了。它在模型段只答对 6/30,但有两件值得说的表现:一是那 6 次答对全部落在最近 8 条覆盖的问题上;二是它答错的方式很特别——问"哪条证据排除了最初的怀疑",它答"无资损,无数据丢失"。它不是答错,它答的是另一道题的答案。
一个能跑的对照台架
脚本放在仓库里,离线段纯本地、确定性、几秒跑完,模型段可选:
python3 tools/harness_lab/context_compare.py # 离线段:字数 + 答案还在不在
python3 tools/harness_lab/context_compare.py --selftest # 阳性对照:full 必须 10/10
python3 tools/harness_lab/context_compare.py --tier free \
--model Qwen/Qwen2.5-7B-Instruct --runs 3 # 模型段:答对 + 真实 prompt token台架做的事很简单:四种策略、同一个素材、同一组问题,唯一变量就是"进上下文的是哪些条"。strat_full 全量一条不落;strat_recent 只留最近 8 条;strat_summary 按词频抽句、预算 28%;strat_retrieval 按字面重合取 top 5,且依赖问句。
这里有个值得停下的细节:前三个策略与问题无关,第四个与问题有关。这个不对称是整个讨论的核心。摘要和丢弃是"离线"的,必须在不知道下游会问什么的时候决定丢什么;检索是"在线"的,在回答时才决定取什么。所以检索场景更窄,但更准。
判据是机械的,不请第二个模型打分。present(ctx, qd) 是离线判据,检查这份上下文里还有没有答案,跨条题要求每个答案都在;answered(txt, qd) 是模型判据,检查模型回答里有没有那个关键值。阳性对照是全组数里最不能省的一行:full 覆盖 10/10 题,台架才算可用。全量上下文都答不全的题,说明素材或判据写坏了,不是模型的问题。出现 BAD,先修台架,再谈结论。
实测结果:省下的每一块,都对应丢掉的一类题
先把"答案还在不在"这一列的原始值摆出来,不用模型,随时可复现:
策略 字符 答案还在
full 1498 10/10
summary 401 1/10 丢掉的题:q1 q2 q3 q5 q6 q7 q8 q9 q10
retrieval 220 8/10 丢掉的题:q9(跨条) q10(换说法)
recent 375 2/10 丢掉的题:q1 q2 q3 q5 q6 q7 q9 q10模型段(温度 0、seed 固定、每格 3 轮、两个模型共 240 次调用、0 报错)的完整拆分:
| 策略 | 材料里有答案 | 材料里没答案 |
|---|---|---|
| full | 27/30、28/30 答对 | 全量没有缺失格 |
| summary | 3/3、3/3 答对 | 答对 0、说不知道 15+11、编 12+13 |
| retrieval | 24/24、21/24 答对 | 答对 0、说不知道 3+3、编 3+3 |
| recent | 6/6、6/6 答对 | 答对 0、说不知道 21+15、编 3+9 |
两件事读完就够。
其一,有就是有,没有就是没有,中间没有"推理"这一档。材料里有答案时,两个模型加起来 126 次里答对 118 次;材料里没有时,114 次里答对 0 次。这条界线比模型之间的差异大得多——换一个模型,full 从 27/30 变 28/30,但"材料里没有"那一列,两个模型都是 0。
其二,缺失的代价不是"答错",是"编"。114 次缺信息里,68 次说"不知道",43 次编了。看几条编造的原文:问最初怀疑的方向和最后认定的真凶,摘要丢了最初怀疑条和真凶条,模型答"最初怀疑的方向是网关问题,最后认定的真凶是后端接口并发把 token 缓存覆盖了"。"网关"来自材料里某条的一个反问,"token 缓存覆盖"来自另一条从未被证实的围观猜测。两条被明确排除或从未证实的红鲱鱼,被拼成了一个完整、自信、格式正确的答案。这类答案的问题不是"错",而是它看起来和正确答案一模一样,下游没有任何办法从形状上区分它们。
那 43 次里还有 3 次"答对",是一道是非题("有没有丢单"),模型有先验,蒙对了。这正是不能用是非题证明上下文有效的原因:答对不一定是上下文起了作用。
为什么"没有"比"错了"更贵
把上面表格里 full 那一格单独拎出来看:全量上下文里答案全在,模型还是丢了 3 次(Qwen)和 2 次(GLM)。这 5 次全部落在同一道题上:同一个事实、换了一种问法,模型只答出一半,比如只答出"缓存的 key",缺了那个变量名。
所以"上下文有效"也不是一个布尔值,至少分三档:
- 信息在上下文里,问法也对得上,基本必答对;
- 信息在上下文里,问法对不上,可能答一半、可能答偏;
- 信息不在上下文里,0 次答对,其中约六成说不知道、近四成直接编。
真正危险的是第 3 档里的"编"。它不报错、不抛异常,以完全合法的形状流进下游。旧线里量过同类现象:39 次压缩调用里,有 5/39,约 12.8% 是静默失败,最狠的一次把 2,242 字节压成 3 字节,返回率只有 0.13%,而调用本身是"成功"的。压缩、摘要、检索、丢弃都属于这一类:省成功了,丢的东西没有任何提示。
旧线三组数:压缩、记忆、分叉
这一讲是旧料的重排,不重跑,把已有的三组数摆在一起,它们指向同一个结论。
压缩(约等于摘要):39 次调用,返回率 22.2%,最狠 0.13%,静默失败 12.8%。省得最狠,也最不可逆。
记忆(约等于有硬上限的仓库):每个 target 上限 5,000 字符,写满时是 4,887/5,000、共 21 条,再加一条 132 字直接被拒;而记忆目录只增不减——副本合计 2,102,250 字节(2.0MB),真正活跃的三件套只有 34,479 字节,有效内容占 1.6%。
分叉(约等于换一份上下文):子会话的峰值入 token 只有父会话的 0.30–0.48 倍,看着很便宜;但总入 token 一路从 252,213 递增到 495,294、再到 549,006。贵不在峰值,在累积。
三条合起来就是本讲的判断:上下文管理的核心不是"压到多小",而是"为哪种下游问题留了哪条退路"。压缩最便宜但不可逆,记忆最便宜但有上限、还会污染,换新上下文峰值不高但总额在滚。想省的那部分,一定会以"某类问题答不了"的形式还回来。
三条能直接抄走的写法
其一,先写下游的问题集合,再定策略。别从"能压到多小"出发。拿十条真实会被问到的问题,标上每条的答案在素材的哪几条里,再用这张表去选策略:要精确值就别只做摘要,要跨条汇总就别只做 top-k 检索。这一步花十分钟,能省掉后面所有返工。
其二,检索必须配一条"问句不重合"的兜底。那道换说法题,问的是同一个事实、一个词都不重合,字面检索直接打 0 分。兜底很简单:固定附上一段全局摘要,哪怕是标题列表或关键结论,让字面检索够不着的部分有一个下限覆盖。检索负责准,全局摘要负责不漏。
其三,摘要的预算按"必须保住的字段"定,不按百分比定。"压到 28%"不是一个设计,是一个结果。真正的设计是:先列出这一路上下文里绝对不能丢的东西,如根因、生效时间、责任人、最终数字,先把它们钉进去,剩下的额度再给别的。本讲里被摘要丢掉的 9 道题,丢的全是这类"只出现一次的冷门具体值"。
顺带一条取舍:三种策略不是互斥的。一个常见组合是"最近 N 条全量 + 更早的部分做摘要 + 对问句做一次检索补漏"。它们各自丢的东西不一样,叠起来的盲区反而更小,代价是上下文会重新变大。回到那句判断:省和丢是一笔交易,没人能只拿省的部分。
常见坑
把压缩率当成绩。"上下文被压到了 15%"不是结论,是起点。要补上后半句:"压到 15% 之后,关心的那 10 个问题答得出几个。"本讲里检索压到 220 字符、单点问题满分,看起来是最好的策略,但它有两类题直接归零。只看压缩率,恰好会把这两类题藏起来。
用是非题验证上下文。"有没有丢单"这种题,材料里没有答案时模型也答对了 3 次,它有先验。验证上下文有效性,要用答案不可猜的问题,如具体变量名、随机编号、精确数字,否则量的是模型的常识,不是上下文。
把字面检索当语义检索。本讲的检索是字对重合,它的失分题里有一道是"同一个事实换一种问法",这在字面检索里必然失败,换向量检索可能救回来。但注意另一道失分题(跨条)换成向量检索也未必够,它需要的是两跳,不是更好的相似度。
只看"答得像"不看判据。全量策略里那 5 次失败,答案都是"缓存的 key"这种答了一半的话,人工扫一眼很可能放过。判据必须是"关键值在不在",不是"意思差不差"。
在固定预算里同时要细节和全局。细节和全局此消彼长:摘要留全局丢细节,检索留细节丢全局。如果下游两类问题都会问,就别指望一个策略包圆,要么加预算,要么做组合。
以为"塞多一点总没坏处"。全量那一行是本讲的对照,它确实最高,但它的 prompt token 是 1213/1031,是检索那行的五倍。多塞不亏,但也不白给,它买的是"问法对不上时也能答"的那一档。值不值,取决于要不要那一档。
结语
本讲量化的是"省和丢的交易"。上下文管理的第一性原理,不是把体积压到多小,而是在已知下游问题集合的前提下,为每一类问题保留一条可回答的退路。当一套 Agent 工作流同时接入多个模型后端、RAG 组件与记忆模块时,请求路由与凭据治理会叠加到上下文工程之上。Treerouter 作为 API 网关,可统一这类多后端场景下的流量调度、鉴权与用量记录,降低复杂 Agent 栈的集成负担。
摘要丢细节、检索丢全局、丢弃丢历史,各有各的翻车点,也各有各的适用场景。先写下会被问的问题,再决定留下什么;给检索配一条全局兜底;按必保字段定摘要预算。把这三条落到代码里,比追求一个漂亮的压缩率更接近问题的本质。






