第 26 节 · Context:系统里最贵的那块地¶
三个上下文预算,最大和最小差了 20 倍。这一节讲这块地为什么最贵,以及上限必须由人来设。
一、同一件事,三个上下文预算¶
先把三个数字摆在台面上。它们不在同一份文件里,但它们回答的是同一个问题:
模型开口之前,先允许它读进去多少?
| 写在哪儿 | 那个数 | 我当时的想法 |
|---|---|---|
| agent 运行时的一份配置清单 | 200000(用掉 80% 自动压缩) | 给一台常驻的 agent 留足记忆 |
| 一次切换模型的启动脚本 | 1000000 | 干脆拉满 |
| 今天全局生效的那份配置 | 48000 | 修完之后的样子 |
20 万、100 万、4.8 万。最大和最小,差 20 倍。
这不是三个人的意见,是我一个人写的。我写下它们的时候,每一个都觉得挺合理——第一个是"留够",第二个是"给足",第三个才是被现实教过之后的样子。
三个数,每一个单看都对。真正的毛病是:我从没把它们放在同一张纸上对过一次。
这就是"预算"这个词最容易出错的地方。预算的意思本来就是"先在一个地方定好总量,再往下分"。而我这三份预算,是我在三个不同时刻、落在三处载体上分别定的——没有一个地方在统管它们。预算只要不被集中定义一次,它就会在各个角落各自长出一个来。 一个系统里有几个上下文预算,往往不取决于需要几个,取决于有多少个地方可以下手改。
还有一层要交代:"100 万"不是我随口拍的。模型的官方规格表上明明白白印着:原生上下文 262144,官方说明可以扩到 1010000——差不多就是 100 万,代价是要改一组位置编码参数,把它按 4 倍拉长。所以 100 万这个数,是我从规格表上抄下来的。
问题就出在这。规格表上写的是"它最多能读多少",系统里要回答的是"每次该让它读多少"——这不是同一个问题的两种说法,是两个完全不同的问题。 我一开始把前者当成了后者。
这两件事差在哪,得先看第二件事到底要付什么。
图 A-26-1(待出):同一件事的三个答案——200000 / 1000000 / 48000,最大最小差 20 倍
二、读一遍有多贵:prefill 曲线¶
有一件事很多人没意识到:模型不是边读边答。它得先把你给它的全部内容,从头到尾过一遍,才开始吐第一个字。
这一步有个专门的名字,叫 prefill(预填充)。你把上下文给它,它先把整段输入算成一堆内部状态,算完了,才有第一个输出 token。
所以"上下文有多长"这件事,翻译过来就是——开口之前,要先读多久。
我拿一台本地的小模型做了一次哨兵测试,专门量这条曲线。那是个 4B 级、权重只有 2.74GB 的小家伙,同机同权重,只改输入长度:
| 输入长度 | prefill 速度 |
|---|---|
| 3.1 万 token | 953 t/s |
| 7.1 万 token | 608 t/s |
| 14.5 万 token | 125 t/s |
长度涨了不到 5 倍,速度掉了 7.6 倍。
这不是机器坏了,是长上下文的天性:注意力那部分的开销,是随长度按平方涨的。也就是说 你多放进去一倍的内容,不是多读一倍,是里面每一块自己都变得更贵了。 这块地越铺越大,每一寸也越量越慢。
像给一块地做丈量:地小的时候,一个人拿着尺子绕着走一圈就完了;地一大,不只是圈数变多,每一次转角的测量本身都更费劲。长度是线性加的,成本是平方涨的。
按这条曲线的走向往回推,把 100 万 token 一次性读完要多久?
这份测试自己先给过一个答案,后来又亲手把它推翻了。早期我写的是"30 到 60 分钟",后来拿实测曲线重新一算,改成 4 到 9 小时。差了近一个数量级。
我用 4 到 9 小时这个口径,因为它是被实测曲线校正过的那一个。
100 万 token 的上下文,不是"更大的上下文",是一次不可交互的批处理。 你敲一句话,等 4 到 9 小时,才等到第一个字。
图 A-26-2(待出):prefill 速度随上下文长度的衰减——3.1 万→953 / 7.1 万→608 / 14.5 万→125 t/s,外推到 100 万 = 4 到 9 小时
三、撑到 100 万,要先付多少内存¶
时间之外,还有一笔更硬的账:内存。
上下文不是一个"概念",它是模型运行时里一块实实在在占着的内存。现代模型读上下文,主要靠 KV cache——你可以理解成"模型读到一个位置时,为了不用回头重读,把那个位置的状态抄一份存着"。你放进去多少内容,就得存下多少份。
我在同一台机器上,实测了撑到 100 万要付多少:
| 项 | 内存 |
|---|---|
| KV cache(100 万 token,Q8/Q4 量化下) | 12.4GB |
| 模型权重 | 2.74GB |
| 进程常驻合计 | 15.2GB |
看第二行和第一行的关系:模型本身的权重,只有 2.74GB。为了装下这 100 万的上下文,我付的内存是模型本身的 4.5 倍。
模型是租客,上下文才是房东。
不过日常我不会这么跑。日常的妥协是另一套配置:KV 做量化,再把一份上下文切成两路分摊,进程常驻压到 7.0GB。
同一个模型,同一块地,给谁用、允许它长多大,就是 7GB 和 15.2GB 的差别。地价不在地本身,在你想让它装多少东西。
四、翻车现场¶
上面这些都是我事后做的账。真正让我把这块地当回事的,不是账,是一次翻车。
2026 年 8 月 23 日,我在查一件事:这台机器为什么一直发烫。
我原先的猜测是"生成太慢"。查下去发现不对。慢的不是生成,是开口之前那一步。
现场数字只有一行,但是够狠:
- 一次 16012 token 的输入,prefill 花掉 6.6 秒
- 同一次请求的用量回显里,
cached_tokens: 0——一次缓存都没命中
第二行才是关键。
我那台本地推理服务,没有跨请求的缓存。意思是:你上一轮已经把这 5 万 token 读过了,这一轮它不记得,从头再读一遍。上下文对它来说不是一笔可以累积的账,是每次都要重付一次的现金。
于是事情就变成了这样:日常请求动不动就是 5 万到 20 万 token——系统提示、完整对话历史、工具定义、文件内容,一起塞进去。哪怕按最保守的线性粗算(16012 token 要 6.6 秒),每一轮请求的 prefill 也至少要 20 到 80 秒——而按第二节那条平方涨的曲线,只会更慢。一轮接一轮,显卡就没下来过。机器当然烫。
根因是一个数:
上下文预算被写成了 100 万。
100 万意味着什么?意味着那个"上下文快满时自动压缩历史"的机制,几乎永远不会触发。历史只进不出,上下文无限膨胀;而每一轮,又要把越来越长的全部历史再重读一遍。
于是形成一个正反馈:窗口开得越大,每一轮读得越久;读得越久,越没机会压缩;越不压缩,下一轮就更长。
这不是"高估了一个参数"。这是亲手把一只越滚越大的雪球喂大。
还有一个细节值得记下来:那个 1000000,不是我在运行时配置里敲的,是更早一次切换模型的启动脚本,把它写进了全局配置。一个临时脚本里的一个数,改掉的是一台常驻服务每一次开口的成本。 它在那儿躺了很久,直到机器烫起来才被人翻出来。
事后数一数,我其实同时犯了两件错。第一件是值错了:100 万这个数,本身就不该出现在一台要交互的机器上。第二件是位置错了:它被写在一段临时脚本里,而不是一份有人看、有人守着的配置里。
第二件错比第一件贵。因为值写错了,顶多是慢;而值被放在一个没人看的角落,就会一直慢下去,直到有别的症状先跳出来——比如机器烫了——你才有机会回头找到它。
图 A-26-3(待出):翻车链——窗口写成 100 万 → 自动压缩不触发 → 上下文无限膨胀 → 无跨请求缓存 → 每轮 20-80 秒 prefill → GPU 满载
五、修法:一个数改小 20 倍,再加一道硬上限¶
修法非常不体面:把 1000000 改成 48000。
改成的那个数就是 4.8 万。一个数改小 20 倍,机器就不烫了。
为什么是 4.8 万?回到第二节那条曲线。3 万 token 附近,prefill 还在 900 t/s 以上;再往上,每加一块内容,成本翻着涨,而多出来的那部分历史,大半是早就没用的对话残渣。
这块地的最优解不是"最大",是"够用且便宜"。
然后我又在路由那一层补了第二道闸。路由脚本里写着一个输入硬上限常量:
超过这个长度,就从最旧的历史开始丢。
两道闸的分工值得记一句:agent 层那个窗口,管的是"上下文涨到多大才该压缩";路由层这个字符上限,管的是"不管上游怎么说,单次输入绝不超过多少"。一个管趋势,一个管单次——它们防的不是同一个错。
代价也说清楚。从最旧开始丢,有可能把被后续步骤引用的工具结果一起丢掉,打断一条工具调用链。这是用一个已知的副作用,换一个确定的发热上限。安全网从来不是免费的,它是划出来的。
还有一句我必须说清楚,免得把这一节写成"我把 100 万跑通了"。
100 万那一次,我能确证的只有三件事:服务启动成功、内存装得下(15.2GB)、单次生成正常。 至于"在 100 万那么长的上下文里,它到底还记不记得中间那一段"——没验证到底。我做过一次 25 万 token 的回召测试,它跑到 93% 就超时断了,没跑完。
"装得下"和"用得上",是两回事。 一个能吞下 100 万 token 的窗口,不等于它真能从 100 万里捞出你要的那一句。
六、为什么一个 AI System 需要上下文预算¶
上一节是路由,它解决的是"这活派给谁"。这一节紧接着说的是:活派到了模型,模型做的第一件事不是回答,是把给它的东西先读一遍。
这一读,就是这块地。
前面每一个零件,量的都是"一次多大的活"——多少 token、哪个档的模型、走哪条路由。只有上下文这一块,量的不是"一次",是"一直":
- 它会自己长大。没人设上限,历史只进不出。
- 它一长大,每一轮的开口成本就跟着涨。
- 涨到最后,你没多写一个字,机器却越来越慢。
这是整套系统里唯一一块——你不主动设上限,它就自己涨到失控的地。
所以一个 AI System 需要"上下文预算"这个零件。它需要一个明确的数字、一个触发压缩的阈值、一道兜底的硬上限;而且这个数必须由人来定,不能交给模型的规格表。
规格表给的是天花板,预算给的是地板。前者回答"它最多能读多少",后者回答"我打算让它每次读多少"。上限不设,它就是无限的;而无限的上下文,是一笔永远付不完的账。
上下文不是免费的。它最贵的地方不在单价,在它不会自己停。
这一节就走到这里:一块地多大、多贵、怎么划界。下一个问题接着来——这些被反复读进去的东西,一个系统到底打算记住什么、放在哪里。那是下一个零件。
数据与出处¶
- agent 运行时的一份上下文配置(窗口 200000,自动压缩阈值 80%),以及今天全局生效的一份配置(48000)
- 一次切换模型的启动脚本,把全局窗口写成 1000000 的那一行
- 路由脚本里的输入硬上限常量(64000 字符,约 3 万 token)
- 一台本地 4B 级小模型的 100 万上下文实测记录:官方规格(原生 262144,可扩到 1010000)、prefill 速度曲线(3.1 万 / 7.1 万 / 14.5 万 token)、100 万全量 prefill 的修正估算(4 到 9 小时)
- 同一份实测里的内存账:常驻 15.2GB(KV 12.4GB + 权重 2.74GB);日常配置 7.0GB
- 2026-08-23 一次本地推理事故的现场记录:16012 token 输入耗时 6.6 秒、缓存命中为 0、换算到 5 万-20 万 token 的每轮成本
- 一次 25 万 token 的回召测试(跑到 93% 超时中断)
本文所有数字均来自本机实测与配置记录,读者拿同一份模型文件、同一份配置可以复算。