← 返回资讯

上下文超了怎么办:四条路各自的代价与选型顺序

2026-08-07

线上跑得好好的问答服务,某天开始有一小撮请求固定失败,日志里躺着一行类似「超出最大上下文长度」的错误。这时候几乎所有人的第一反应都一样:换一个窗口更大的模型不就行了。

这条路当然能跑通,但它的代价常被严重低估。原因很直白——长上下文的钱是按 token 实打实付的。你把整份文档塞进去,那份文档里的每一个 token 都要按输入价计费,不管模型实际用到了其中的多少。而在大多数业务场景里,塞进去的内容真正对这次回答有用的只有很小一部分。你花钱买的是”确保它在里面”这份心安,不是”它被用上了”这个结果。

更麻烦的是,换模型这个动作会把问题从”报错”变成”不报错但账单涨了几十倍”。报错至少还会提醒你去看一眼,账单要等到月底才说话。我见过一个团队就是这么走过来的:改了模型名,报错消失,皆大欢喜,两个月后财务把账单甩过来才开始重新算这笔帐。

所以这篇文章想讲的是另一个顺序:先诊断是谁把窗口撑爆的,再按代价从低到高试四条路,把”换大窗口模型”放在最后。

一、先搞清楚是哪一部分把窗口撑爆的

诊断先于治疗。上下文窗口是一个共享的空间,撑爆它的可能是下面任意一部分,而不同的元凶对应完全不同的解法:

组成部分典型特征增长方式
系统提示词固定不变,但可能被历次迭代堆得很长常量,每次请求都付一遍
工具定义(function/tool schema)工具越多越长,JSON Schema 很占地方随工具数量线性增长
对话历史多轮累积,最容易失控随轮次线性甚至加速增长
单次输入的文档 / 检索片段长文档、整份合同、大段日志由单次输入决定,波动大
输出预留看不见,但确实占位置由 max_tokens 和实际生成量决定

这张表里最容易被漏掉的是最后一行:输出也要占窗口

很多人算容量的时候只算输入,把输入刚好压到临界值以下,觉得稳了。结果模型没地方生成了——要么直接报错,要么生成到一半被截断,返回一个语法不完整的 JSON,下游解析器炸掉。这类故障排查起来特别费劲,因为它不是每次都发生,只在输入偏长、回答又偏长的那些请求上出现,看起来像”偶发抽风”。

正确的算法是:输入 + 期望输出 ≤ 可用窗口,而且要给输出留固定余量,不是留一个刚好够的数。具体每个模型的窗口上限是多少,请以你所用模型的官方规格为准,不要凭印象或者按型号名称的大小排序猜——这一点上猜错的代价直接是线上事故。

想把上面这几部分的占比摊开看,可以用 上下文计算器 把系统提示、工具定义、历史、文档分别填进去,一眼就能看出谁是大头。多数团队做完这一步会发现一个反直觉的结论:撑爆窗口的往往不是那份”很长的文档”,而是几十个工具定义,或者一段被历任产品经理层层加码到几千 token 的系统提示。这两种情况的解法极其便宜,跟”换模型”完全不是一个量级的投入。

诊断的另一半是看分布,而不是看均值。把最近一段时间的请求按输入 token 排个序,看 P50、P95、P99 分别是多少。如果 P50 离窗口上限还很远,只有 P99 撞墙,那你要解决的是一小撮长尾请求的降级问题,而不是给全部流量换一个更贵的模型。为 1% 的请求让 100% 的流量涨价,这是我见过最常见的一笔亏本买卖。

二、四条路,按”该先试哪条”排序

路一:裁剪(最先试)

裁剪就是不改架构,只把不该占位置的东西拿掉。可动的地方有几处:

对话历史用滑动窗口或摘要压缩。 保留最近 N 轮原文,更早的部分压成一段摘要。这是投入最小、见效最快的一招,多轮对话类业务基本上一做就能把输入砍下去一大截。具体的实现方式和三种压缩方案的取舍,可以看 上下文压缩与裁剪 那篇,这里不重复。

去掉冗余的少样本示例。 系统提示里的 few-shot 示例是重灾区。上线初期为了稳住输出格式塞了六个示例,后来模型换了、提示词改了,示例其实两个就够,但没人敢删。删之前先跑一轮离线评测,确认删掉后质量没掉,就该删。

精简工具定义。 工具的 description 和参数说明写得越详细,模型调用得越准,但也越占地方。如果你挂了三十个工具而单次对话实际只可能用到其中三五个,考虑按场景做工具分组,按需注入,而不是每次把全套 schema 都发过去。

代价:可能丢信息。裁剪的本质是你替模型决定了什么该记、什么可以忘。这个决定做错了,模型会在某些回合突然”失忆”,用户体验的损伤比多花点钱严重得多。所以裁剪必须配一套判断规则:哪些内容永久保留(用户身份、任务目标、已确认的约束),哪些可以摘要(闲聊、已完成的子任务),哪些可以直接丢(重复确认、寒暄)。规则写不出来,就说明你还没想清楚,别急着上滑动窗口。

路二:检索代替全量塞入(RAG)

不把整份知识库塞进去,而是先检索出与当前问题相关的片段,只把这些片段放进上下文。这是把”上下文超限”从容量问题变成检索质量问题。

它的收益是巨大的,第三节的算例会具体给数。但代价也很实在:

多一套基础设施。 向量库、切块策略、embedding 模型、索引更新流程、检索质量的持续监控,这些都不是一次性投入,是长期要养的东西。文档更新了索引没更新,模型会拿着旧片段一本正经地回答,比装不下更难发现。

检索不准会比全量塞入效果更差。 这一点必须说透。全量塞入的下限是”信息都在里面,模型自己找”,检索的下限是”相关内容根本没被召回,模型基于错误的片段作答”。后者不是回答质量下降,是回答方向错了,而且错得很自信。完整的切块、召回、重排链路怎么抠,见 RAG 怎么做

适用判断可以这么给:

  • 知识库很大、单次问题只涉及其中一小块、内容之间弱耦合 → 检索优势极大,直接上。
  • 知识库不大、但更新频繁 → 也适合,因为检索天然解决了知识时效问题。
  • 内容之间强交叉引用、必须整体理解才能回答 → 检索会切碎逻辑,慎用,见第五节。
  • 只有个位数的文档且总量本来就不大 → 别为了架构好看上 RAG,直接塞是更优解。

路三:分段处理 + 汇总(map-reduce)

长文档拆成若干段,每段单独调一次模型处理,最后把各段结果合并成最终答案。经典的 map-reduce 思路。

它确实能让每次调用都装得下,但有三个代价要说清楚:

调用次数变多。 一次变成 N+1 次(N 个分段加一次汇总)。这直接影响的是延迟和限流压力,不只是钱。串行做会把响应时间拉长到用户不能接受,并行做会撞上并发限额,两头都要处理。

总 token 可能不降反升。 这是最容易被忽略的一点。每一段都要重新带上系统提示词,工具定义也可能要重发,这些固定成本被乘以了分段数。第三节会用具体数字算给你看,分段之后总输入 token 反而更多。

段间上下文丢失。 第 3 段里的一句”如前所述,该条款适用于所有子公司”,模型看不到前面那段,只能猜。跨段的指代、对比、汇总类问题,map-reduce 做出来的结果会明显不如整体输入。缓解办法是段与段之间做重叠(overlap),或者在每段前面拼一小段全局摘要,但这两种缓解手段都会进一步推高总 token。

所以 map-reduce 真正适合的是段间弱相关的任务:批量摘要、逐条抽取实体、每段独立打标签。要做跨段推理,它不是好选择。

路四:换大窗口模型(最后试)

前三条都不行、或者性价比都算不过来的时候,再走这条。它的代价有三层:

单价通常更高。 长窗口的型号一般定价更贵,这个方向上的差异各家都存在,具体倍数以各厂商官方价目表为准。

长输入本身就贵。 这一层比上一层更狠,而且是乘法关系。假设单价高 4 倍、输入量大 14 倍,成本就是 56 倍,不是 18 倍。为什么长上下文的成本会呈现这种加速增长,上下文越长越贵 那篇讲了计费逻辑,可以对照着看。

注意力被稀释的机制问题。 塞进去的内容越多,模型在生成每个 token 时要在越大的范围里分配注意力,真正关键的那几句话在整体中的权重占比就越低。这是注意力机制本身决定的,不是某个模型的缺陷。工程上的表现是:你把一句关键约束埋在一份很长的材料中间,模型有时候会视而不见;同样一句话单独放在提示词末尾,它就照做了。

这里我只讲机制,不给数字——网上流传的各种”长上下文准确率下降百分之多少”的说法,测法、任务、模型各不相同,拿来当决策依据不靠谱。你要真关心这件事对你业务的影响,唯一可信的做法是用你自己的评测集跑一遍对照。

顺带说一句反直觉的推论:长上下文有时候不只是更贵,还更不准。 所以”塞得越全越保险”这个直觉本身就是错的,它既不省钱也不一定更好。

三、成本对比:把差额算出来

下面用一组假设参数做算术演示。这些价格和 token 量都是为了演示而设的假设值,不代表任何厂商的真实定价,你要用请换成自己的实际数据重算。

假设条件:

项目假设值
知识库全文80,000 token
单次问题实际相关的片段4,000 token
系统提示 + 工具定义1,500 token
用户问题200 token
每次输出600 token
日请求量3,000 次(按 30 天 = 90,000 次/月)
大窗口模型单价(假设)输入 0.04 元 / 千 token,输出 0.12 元 / 千 token
小窗口模型单价(假设)输入 0.01 元 / 千 token,输出 0.03 元 / 千 token
query 向量化单价(假设)0.0005 元 / 千 token

方案 A:全量塞进大窗口模型

  • 单次输入 = 80,000 + 1,500 + 200 = 81,700 token
  • 输入成本 = 81.7 × 0.04 = 3.268 元
  • 输出成本 = 0.6 × 0.12 = 0.072 元
  • 单次合计 = 3.340 元
  • 月成本 = 3.340 × 90,000 = 300,600 元

方案 B:先检索,只塞相关片段,仍用大窗口模型

  • 单次输入 = 4,000 + 1,500 + 200 = 5,700 token
  • 输入成本 = 5.7 × 0.04 = 0.228 元
  • 输出成本 = 0.6 × 0.12 = 0.072 元
  • query 向量化 = 0.2 × 0.0005 = 0.0001 元
  • 单次合计 = 0.3001 元
  • 月成本 = 0.3001 × 90,000 = 27,009 元

方案 C:先检索,只塞相关片段,改用小窗口模型

  • 单次输入成本 = 5.7 × 0.01 = 0.057 元
  • 输出成本 = 0.6 × 0.03 = 0.018 元
  • query 向量化 = 0.0001 元
  • 单次合计 = 0.0751 元
  • 月成本 = 0.0751 × 90,000 = 6,759 元

三者对比:

方案单次成本月成本相对方案 A
A 全量 + 大窗口3.3400 元300,600 元基准
B 检索 + 大窗口0.3001 元27,009 元省 273,591 元(约 91%)
C 检索 + 小窗口0.0751 元6,759 元省 293,841 元(约 44 倍差距)

这组数字里最值得盯的是 A 到 B 这一步:模型没换,只是少塞了,月成本就降了约 91%。 换句话说,“换模型”这个动作在整件事里的贡献远小于”少塞点东西”。很多团队把精力全花在选型比价上,却对自己每次往里塞多少 token 没有概念,这个优先级是反的。

当然,方案 B 和 C 要减去基础设施成本。假设向量库托管加运维摊到每月 3,000 元(同样是假设值),方案 C 的总成本是 6,759 + 3,000 = 9,759 元,相比 300,600 元依然不在一个量级。请求量越大,这笔固定成本被摊得越薄;反过来,如果你月请求量只有几百次,那 3,000 元的基础设施就完全不值当,直接塞进去反而是对的。这就是为什么”该不该上 RAG”没有统一答案,它是一道跟你请求量绑定的算术题。

再看路三的 token 账。把 80,000 token 拆成 8 段各 10,000,每段都要重新带 1,500 的系统提示:

  • map 阶段输入 = 8 × (10,000 + 1,500) = 92,000 token
  • 假设每段输出 400 token,reduce 阶段输入 = 8 × 400 + 1,500 + 200 = 4,900 token
  • 总输入 = 92,000 + 4,900 = 96,900 token
  • 对比方案 A 的 81,700 token,多了 15,200 token,涨了约 18.6%
  • 调用次数从 1 次变成 9 次

分段处理解决的是”单次装不下”,不是”总量太大”。它把一个容量问题换成了更高的总 token、更多的调用次数和段间信息损失。选它要有明确理由,比如任务本身天然可分、或者受限于必须使用某个特定模型。

四、工程上怎么防患于未然

上面四条路都是撞墙之后的应对。更省事的做法是不让它撞。

请求前先估 token 并做长度校验。 在发出请求之前本地算一遍输入长度,超了就走降级路径,而不是原样打出去等着被拒。一个超长请求被拒绝的这一趟里,序列化的时间花了、网络传输花了、上游解析花了,换回来一个错误。请求越大浪费得越狠,而恰恰是大请求最容易触发这类失败。本地估算的开销跟这个比完全不值一提。

配置上下文超限的专门回退。 如果你前面挂了网关,把上下文超限单独配一条回退链,而不是让它跟其他失败混在同一条通用回退里。LiteLLM 的 context_window_fallbacks 就是干这个的,LiteLLM 回退与熔断配置 里有完整写法。这里想强调的是一个判断:在所有失败类型里,上下文超限是唯一一种”换个更大的模型就一定能成功”的失败。 它是纯粹的容量问题,没有随机性、没有服务商差异。也正因如此,它值得单独配一条链——如果让它去走通用回退,链上那些窗口不比主模型大的备选会一个接一个地失败,你付出 N 次失败的延迟,换回一个照样失败的结果。

给输出预留固定余量。 别把输入塞到刚好卡在上限。设一个明确的输出预留值,输入的校验阈值按”窗口上限减去输出预留再减去安全余量”来定。这个余量宁可给宽一点,被截断的 JSON 造成的下游故障,排查成本远高于多留那点空间的成本。

给上下文长度加监控。 把每次请求的输入 token 数打点上报,看 P95、P99 的走势。上下文膨胀几乎都是缓慢发生的——多加了一个工具、系统提示多写了两段、历史保留轮次从 5 调到 10——单看每一次改动都无害,累积起来就撞墙了。有这条曲线,你能在撞墙前两周就看见它在涨。

给单次输入做硬上限并明确降级行为。 用户上传了一份异常大的文档,你的服务应该给出”文档过长,已只处理前 N 页”这样明确的提示,而不是抛一个原始 API 错误给用户看。降级路径要提前设计,别等线上出问题临时想。

五、什么时候该老老实实用大窗口

前面一直在说别急着换大窗口,但确实有些场景就该用,而且用得理直气壮。

判断标准只有一条:内容确实全都相关,且不可拆。

典型例子是整份合同的交叉引用审查。合同第 3 条定义的术语,第 27 条在用;违约责任条款要跟前面所有的义务条款对照着看;一处金额改了,散落在各处的相关条款都要跟着核。这种任务里,任何切分都可能把一对需要放在一起看的条款分到两次调用里,而模型无从知道自己漏了什么。类似的还有:长篇代码库的跨文件重构评审、一整份财报的前后勾稽核对、需要通篇一致性的长文档翻译。

这些场景的共同点是,拆错的代价远高于省下的那点钱。审漏一条冲突条款的后果,跟省下的 token 费用完全不在一个量纲上。这时候硬拆是典型的捡了芝麻丢了西瓜。

但即使在这些场景里,也还有便宜的优化空间:系统提示和工具定义该精简还是要精简;同一份长文档如果要反复问多个问题,看看你所用的服务是否提供上下文缓存类能力,能否把重复输入的部分复用(具体是否支持、怎么计费以官方文档为准);输出预留照样要留。“该用大窗口”不等于”其他优化都不用做了”。

还有一种情况值得单独提:内容全都相关,但问题是可分的。比如一份长文档,你要抽取其中所有的日期。这种时候虽然文档不可拆解理解,但任务本身对段间关系不敏感,map-reduce 是完全成立的选择。所以判断的落点不在”文档能不能拆”,而在”这个任务需不需要跨段推理”。

六、自查清单

撞上上下文超限时,按这个顺序过一遍:

  1. 先定位元凶。 把系统提示、工具定义、对话历史、单次文档、输出预留分别量一遍,看谁占大头,别一上来就换模型。用 上下文计算器 拆开看最快。
  2. 确认输出也算进去了。 校验阈值必须是「窗口上限 − 输出预留 − 安全余量」,输入卡在刚好够用的位置迟早出事。
  3. 看分布不看均值。 P99 撞墙就只治 P99,别为 1% 的请求给 100% 的流量涨价。
  4. 按顺序试:裁剪 → 检索 → 分段 → 换模型。 前面每一条的代价都低于后面一条,跳步骤等于多花钱。
  5. 算清楚再决定上不上 RAG。 把请求量、单次省下的 token、基础设施月摊销三个数放一起算,请求量太小的时候直接塞才是对的。
  6. 别指望分段能省钱。 分段解决的是单次装不下,总 token 通常反而更高,调用次数还翻几倍。
  7. 上线前配好三件事: 发送前长度预检、上下文超限的专门回退链、输入 token 的 P95/P99 监控。
  8. 确认自己不属于”就该用大窗口”那一类。 全文强交叉引用、必须整体理解的任务,硬拆的代价比省下的钱大得多。

各模型的上下文窗口上限、价格和是否支持上下文缓存,请以你所用模型的官方规格与价目表为准,别按型号名称的大小猜。

要把自己这套请求的各部分占比摊开算一遍,用 上下文计算器

相关阅读