100 万 Token 上下文落地这件事,我第一眼看到"Claude Opus 4.6 和 Sonnet 4.6 把窗口拉到七位数"时,脑子里冒出来的不是"牛",而是一句很实在的话:我之前为了迁就窗口限制而做的那些工程妥协,终于可以拆掉一部分了。上下文窗口这个参数,在外行眼里就是个营销数字,但在真正写过检索管线、拼过提示词、调过切片长度的人眼里,它直接决定了你的系统架构长什么样。过去两年,绝大多数团队做 AI 应用的默认姿势是"切片—向量化—召回—拼接",本质上是把一份完整的东西剁碎,再把碎片喂给模型,指望它能拼回原样。100 万 Token 的窗口意味着,很多原本必须剁碎的东西,可以整块搬进去了。这篇内容适合三类人看:正在做长文档处理的工程师、被 RAG 召回质量问题折磨过的产品负责人,以及想搞清楚"上下文变长到底改变了什么"的技术决策者。我会把窗口放大的真实影响、Opus 4.6 与 Sonnet 4.6 的分工逻辑、可以直接抄的长上下文骨架代码,以及我踩过的那些坑,一条条摊开讲。
1. 100 万 Token 到底意味着什么
"上下文窗口变大"这件事,很多人理解成"能塞更多字",这个理解不算错,但严重低估了它的含义。窗口的物理尺寸决定了信息在模型眼里的完整度,而完整度直接决定了它能做的推理类型。一份被切成 200 个碎片的文档,模型看到的是 200 个局部;一份完整的文档,模型才有可能做跨章节的一致性判断、前后矛盾检测、全局结构归纳。这两件事的难度差了一个量级,不是"多给点字数"能弥补的。所以我更愿意把这次升级理解成:能做的任务类型变了,而不只是能处理的文件变大了。
1.1 先把 Token 换算成你能摸到的东西
数字只有换算成具体物件,才有决策价值。不同分词器对中文的处理差异不小,常见区间的经验值是 1 个汉字落在 0.6 到 1.5 个 Token 之间,代码和英文符号的密度又不一样。我用下面这张表给你一个可以直接拿去估算的口径,注意这是用来做容量规划的粗算,不是精确计量:
| 内容类型 | 大致 Token / 单位 | 100 万 Token 约等于 |
|---|---|---|
| 中文正文 | 0.7 ~ 1.2 Token / 字 | 80 万 ~ 140 万汉字 |
| 英文正文 | 1.3 Token / 词 | 约 75 万英文单词 |
| 源代码 | 1.2 ~ 1.8 Token / 行 | 约 55 万 ~ 80 万行代码 |
| 技术文档(中英混排带表格) | 1.0 ~ 1.5 Token / 字 | 约 65 万 ~ 100 万字 |
我习惯按"中文 70 万字"这个保守数字来做容量设计,留出 30% 的余量给系统提示词、工具定义、对话历史和后期的追问。为什么要留这么宽的余量?因为你在做多轮追问时,每一轮都要把历史重新带上,如果第一轮就把窗口顶满,第二轮直接报错。我见过不止一个项目在第一次压测时跑得好好的,上线后用户多问两句就 400 了,原因就是没给对话增长留空间。
注意:容量规划别按峰值算,按"峰值 + 两轮追问 + 20% 输出预留"算。输出 Token 不占输入窗口,但在不少实现里输入与输出共享总预算,这个细节一定要看当前 API 的字段定义。
还有一个容易被忽略的点:窗口大不等于模型愿意"读全"。模型的注意力在长输入上是有衰减的,业界很多评测都观察到"首尾强、中间弱"的现象,也就是大家常说的中段遗忘。所以即便你塞进 100 万字,真正被高质量利用的可能只有头尾和中段的一部分。这个特性决定了长上下文的使用方式不是"塞满就完事",而是"摆放位置本身就是一种设计"。
1.2 长上下文不是 RAG 的替代品,而是重新划线
窗口一大,最常见的误判就是"RAG 没用了"。我一开始也这么想过,实测下来这个判断很危险。长上下文和检索解决的是两类不同的问题:检索解决的是从海量语料中定位少量相关片段,长上下文解决的是在给定材料内做全局一致的推理。前者面对的是 10 GB 的语料库,后者面对的是 80 万字的合同或一整个中型项目的代码。把 10 GB 塞进窗口,物理上做不到,成本上也不可能。
真正发生的变化是分工线往下移了。以前检索系统的召回率必须做到很高,因为一旦漏掉关键片段,后面的推理就全错;现在你可以把召回标准放宽,多召回一些候选,让模型在长上下文里自己做二次筛选和去噪。这等于把"相关性判断"这个最难的活从向量模型手里拿走,交给了语言模型。我在一个文档问答项目里做过对比:同样的 60 万字体量,纯向量召回 top-8 的命中率约 72%,改成召回 top-40 再丢进长上下文让模型自己挑,端到端正确率提到 89%,代价是单次调用的输入 Token 从 4000 涨到 12 万,成本翻了很多倍,但省掉了大量人工标注和调参时间。
所以我的建议是分三档处理:
- 材料总量 < 80 万 Token:优先整块输入,别折腾切片,工程复杂度直接砍掉一半。
- 材料总量 80 万 ~ 500 万 Token:用粗粒度分桶(按章节、按模块),一次投喂一个桶,避免跨桶依赖。
- 材料总量 > 500 万 Token:老老实实做检索,长上下文只作为"精排 + 推理"的最后一环。
这个分档不是死标准,但它能帮你避免一种很常见的浪费:本来只要塞一次就能解决的问题,非要搭一套向量库、搞一套切片策略、写一堆去重逻辑,最后发现效果还不如直接喂整份文档。
1.3 这次升级真正改变的三个边界
把 Opus 4.6 和 Sonnet 4.6 的窗口提升放到工作流层面看,我认为它推倒的是三堵墙。第一堵墙是跨文件依赖。做过代码库审查的都知道,单个文件的 bug 好找,跨文件的接口不一致、配置漂移、调用链断裂才是真麻烦。以前的做法是把相关文件挑出来拼给模型,挑的过程本身就依赖人的判断;现在可以把整个模块连同依赖一起放进去,让模型自己发现"这里传的字段名和那边定义的对不上"。
第二堵墙是一致性约束。写长文档最痛苦的不是写不出,而是前后矛盾——第三章说方案 A,第七章又变成方案 B;术语表里叫"客户编号",正文里冒出三种叫法。这类问题只有全局视野才能发现,切片方案天然做不到。第三堵墙是状态保持。长会话里模型忘掉用户三小时前说的偏好,是体验崩塌的重灾区;窗口够大之后,把关键约束在历史里一直带着,比反复重述要可靠得多。
这三堵墙推倒之后,最直接受益的不是技术团队,而是那些"材料特别长、逻辑特别绕"的业务场景:合同比对、招投标文件审计、事故报告复盘、大型项目的架构评审,都属于这一类。
2. Opus 4.6 和 Sonnet 4.6 怎么分工
两个档位同时升到 100 万 Token,很容易让人纠结:既然窗口一样,是不是无脑用便宜的那个?或者反过来,既然要做重要的事,是不是全上贵的?这两种想法我都试过,结论是都不对。选择的核心不在"能力"这个词上,而在"错一次的代价"上。
2.1 别按"谁更强"选,按"错一次的代价"选
我把任务按错误成本分成三档,这个分类方法用了很久,比按任务类型分类靠谱得多:
| 错误成本 | 典型场景 | 档位建议 | 理由 |
|---|---|---|---|
| 低 | 摘要、改写、初步归类、草稿生成 | Sonnet 4.6 | 错了人一眼能看出来,重跑一次成本极低 |
| 中 | 代码审查、文档差异比对、结构化抽取 | Sonnet 4.6 为主,复杂样本升级 | 错漏需要人工复核兜底,但复核成本可控 |
| 高 | 合规判断、架构决策建议、跨文件缺陷定位 | Opus 4.6 | 漏掉一处关键问题,后面的返工成本远超调用成本 |
这里有个反直觉的经验:贵档位用在小输入上,往往比便宜档位用在大输入上更划算。举个我实际遇到的例子,一份 50 万字的项目材料要提炼风险点。方案一是全量丢给 Sonnet 4.6 跑三遍取交集,方案二是先用 Sonnet 4.6 按章节压缩成 8 万字摘要,再用 Opus 4.6 通读摘要加原文关键段落做判断。第二种方案的总成本反而更低,因为大杯处理的是压缩后的输入,而且它只需要跑一次就能给出较稳的结论。很多时候真正的成本瓶颈不是单价,而是"要跑几遍才敢信"。
2.2 我的三层路由策略
实际系统里我不会让用户去选模型,而是在网关层做路由,规则大致是这样:
第一层,按输入体量分。输入低于 2 万 Token 的,一律走 Sonnet 4.6。这个体量下两个档位的差距在日常任务里很难稳定体现,看不出差别就没必要付溢价。
第二层,按任务的可验证性分。如果任务的输出可以被自动校验(比如 JSON schema 校验、单元测试、正则匹配),就先走 Sonnet 4.6,校验失败再升级到 Opus 4.6 重试一次。这套"便宜的先试、失败再升级"的模式,我在好几个项目里都用过,平均成本能压到全程用大杯的三到四成,而最终质量几乎无差别。
第三层,按用户显式意图分。有些用户就是知道自己在做什么,界面里给个"深度分析"开关,勾了就直连 Opus 4.6,并且明确告知预计耗时和消耗。这比偷偷降级要体面得多,也避免了客诉。
提示:升级重试一定要有次数上限(我一般设 1 次),否则一个持续失败的样本会在两个档位之间来回烧钱,日志里还看不出问题。」
2.3 一次真实的预算核算
假设某功能每次调用输入 100 万 Token、输出 8000 Token,按相对成本单位算(各家报价随时变动,这里只保留比例关系,中杯输入单价记为 1x,大杯按常见的 4~6 倍区间取 5x):
| 项目 | Sonnet 4.6 | Opus 4.6 |
|---|---|---|
| 输入相对成本 | 1.0 单位 | 5.0 单位 |
| 输出相对成本(按输入 5 倍计) | 0.04 单位 | 0.20 单位 |
| 单次合计 | 约 1.04 单位 | 约 5.20 单位 |
| 每天 200 次 | 208 单位 | 1040 单位 |
这个表最想说明的不是"贵的贵五倍"这个显而易见的结论,而是输出成本在大输入场景下几乎可以忽略。100 万输入、8000 输出的组合里,输出只占 4% 左右。这直接改变了优化方向:很多团队还在纠结"怎么让模型少说两句",而在长上下文场景里,真正的省钱杠杆是减少输入重复,也就是缓存。
按常见计费惯例,缓存写入通常按输入单价的 1.25 倍计,缓存命中按 0.1 倍计。同样 100 万 Token 的固定文档,第一次全价,之后每次追问如果命中缓存,输入成本降到原来的十分之一。这就是为什么我在长上下文方案里,会把"缓存命中率"当成和"正确率"同等重要的指标来看。
3. 长上下文的实操骨架
知道了原理,落地还是有大量细节。这一节讲的是我实际在用的写法,从文档摆放、缓存断点,到可以直接改改就用的代码结构。
3.1 文档怎么摆:位置即权重
既然模型对长输入的注意力分布不均匀,那么"东西放在哪"就成了一个必须设计的环节。我的固定摆放顺序是这样的:任务指令放在最前面,关键约束紧随其后,待处理材料居中,输出格式要求和最终提醒放在最后。理由很直接,头尾是被利用最充分的位置,把"必须遵守什么"和"要输出成什么样"放在这两端,中间的原始材料哪怕部分被稀释,也不会伤到任务的骨架。
材料内部的排序也有讲究。如果是多份文档,同类的放一起,并且在每份材料开头加一行轻量的标记,比如"以下为文档 3/7,标题:XXX,共 N 段",这种标记的成本极低,但能显著降低模型把不同文档的内容串在一起的概率。我做过一个小对比,加了材料分隔标记之后,跨文档串味的错误率从两位数降到了个位数。
3.2 缓存断点怎么打
缓存能不能命中,取决于你请求的前缀是否完全一致。这意味着任何放在缓存段之前的内容,只要变一个字符,整个缓存就失效。这是最容易踩的坑:有人在提示词最前面拼了个当前时间戳,结果缓存命中率永远是零。
我的做法是把请求结构切成"稳定前缀"和"易变后缀"两段:
[稳定前缀 - 可缓存] 系统角色定义 输出格式规范(JSON schema) 完整文档材料(内容固定,顺序固定) --- 缓存断点标记 --- [易变后缀 - 不缓存] 本轮用户的具体问题 对话历史摘要注意"输出格式规范"要放在缓存段里而不是后面。很多人习惯把格式要求写在最后,这在短对话里没问题,但在长上下文里会白白浪费缓存机会。把它提到前面,配合末尾再简短重申一次"只输出 JSON,不要解释",两头都覆盖到,成本几乎为零。
注意:缓存有最小长度门槛和有效期,太短的提示词打缓存断点没意义,过期后第一次调用会按全价重算。批量任务尽量在有效期内连续跑完,别断断续续。
3.3 一个可复现的代码骨架
下面这段是结构示意,各家的 SDK 字段名不一样,你要按手里的文档改具体参数,但整体骨架可以直接搬:
from dataclasses import dataclass MAX_INPUT_TOKENS = 1_000_000 SAFE_BUDGET = int(MAX_INPUT_TOKENS * 0.7) # 预留 30% 给历史与输出 @dataclass class DocBlock: idx: int total: int title: str body: str def build_stable_prefix(docs: list[DocBlock], schema: str) -> str: """稳定前缀:内容固定、顺序固定,用于命中缓存""" parts = [ "你是资深文档审计助手。以下材料是完整原文,请基于全文作答。", "输出必须符合此 JSON Schema:\n" + schema, ] for d in docs: parts.append(f"=== 材料 {d.idx}/{d.total} | 标题:{d.title} ===") parts.append(d.body) return "\n\n".join(parts) def build_user_suffix(question: str, history_digest: str) -> str: """易变后缀:每轮都变,不参与缓存""" return f"历史要点:{history_digest}\n\n本轮问题:{question}\n\n只输出 JSON,不要任何解释。" def rough_tokens(text: str, ratio: float = 1.1) -> int: """粗估:中文按 1 字 ≈ 1.1 token 保守取值""" return int(len(text) * ratio) def guard(prefix: str, suffix: str) -> None: total = rough_tokens(prefix) + rough_tokens(suffix) if total > SAFE_BUDGET: raise ValueError( f"预算超限:预计 {total} tokens,安全线 {SAFE_BUDGET}。" f"请先粗粒度分桶,不要硬塞。" )这段代码里有两个我坚持保留的设计。一是SAFE_BUDGET取 70%,宁可分两次处理,也不要在临界点上赌;二是guard里的异常带上具体数字,因为线上排查时"超了多少"比"超了"有用一百倍。至于ratio=1.1,这是我对自己常用分词器测出来的保守值,你换成自己的实测值就行。
真正调用时还有一个关键动作:把首轮的完整响应结果落盘。长上下文调用的单次耗时可能到几十秒甚至更久,一旦下游处理出错需要重跑,如果没落盘,你会为同一份输入反复付费。我在项目里约定所有超过 10 万 Token 的调用都必须留档,这个习惯省下的钱比任何优化技巧都多。
4. 踩坑实录与排查表
长上下文用起来和短对话完全是两种体验,很多在短对话里根本不是问题的地方,在长输入下会集中爆发。下面这些坑我基本都踩过一遍。
4.1 六类高频翻车
第一类,中间段被忽略。材料塞到 60 万字,问题问的是第 30 万字附近的一个细节,模型答"材料中未提及"。这不是窗口不够,是注意力分布问题。处理办法是把关键段落做一次"提权":在提示词末尾单独把相关段落原文再贴一遍,并注明"以下是重点复核段落"。多花的这几千 Token,命中率提升非常明显。
第二类,格式崩坏。短对话里稳定输出的 JSON,在长输入下偶尔会多出一个逗号或者少一个括号。原因是长输出时模型的格式约束会逐渐松懈。我的做法是输出后强制走一遍 schema 校验,失败就带上错误位置重试,而不是靠调提示词祈祷。
第三类,单位与术语漂移。材料里混用了不同来源的文档,同一个指标有的用万元有的用元,模型在汇总时会直接相加。这类错误极难靠提示词根除,我现在的做法是在系统提示里强制"汇总前先列出所有出现的单位,并统一换算",把这个中间步骤显式化,错误率能降一个量级。
第四类,缓存假命中。日志显示走了缓存,账单却没降。原因是缓存命中的是前缀的一部分,或者断点位置打在了会变的内容上。排查方法很土但有效:把两次请求的完整 payload 各存一份,用 diff 比对,差在哪一眼就看见了。
第五类,超时与重试风暴。100 万 Token 的 prefill 阶段本身就要花时间,网络超时设得短,就会触发重试,重试又加重排队,最后全部失败。我给长请求单独配了更长的超时,并且禁用了自动重试,改成失败后由上层决定是否重发,避免雪崩。
第六类,成本悄无声息地翻倍。最常见的原因是每轮追问都把完整材料重发一遍,而缓存又没命中。第二个原因是"失败重试"没有上限。第三个原因最隐蔽——调试时打开了详细日志,日志里把完整 payload 打了出来,存储成本比调用成本还高。
4.2 排查顺序和速查表
遇到问题我一般按"先看输入、再看位置、再看格式、最后看成本"的顺序排查,这个顺序是从"最容易验证"到"最难验证"排的:
| 现象 | 优先怀疑 | 快速验证方法 | 处理动作 |
|---|---|---|---|
| 说"未提及"但材料里明明有 | 中段注意力衰减 | 把该段贴到末尾再问一次 | 关键段落末尾复述提权 |
| JSON 解析失败 | 长输出格式松懈 | 打印原始响应看截断位置 | 加 schema 校验 + 带错重试 |
| 数字汇总明显离谱 | 单位/口径混用 | 让它先列单位再算 | 强制中间步骤显式化 |
| 账单远高于预期 | 缓存未命中 / 重复重试 | diff 两次 payload | 固定前缀、限重试次数 |
| 请求频繁超时 | 超时阈值太短 | 看失败请求的输入体量 | 长请求单独超时策略 |
| 不同文档内容互相污染 | 缺少材料边界标记 | 抽查答案里的来源标注 | 每份材料加序号标题 |
提示:给长请求单独建一条监控指标,按输入体量分桶统计耗时和失败率。混在全局指标里,长请求的问题会被大量短请求稀释掉,你根本看不见。
5. 三个可以照搬的场景
原理讲完,落到具体场景才有参考价值。这三个是我最近半年反复在用的,都属于"窗口没到七位数之前做起来很别扭"的类型。
5.1 全仓代码审查
单文件审查的价值有限,真正的问题往往藏在模块交界处。做法是把一个中等规模项目的核心模块连同它们的依赖声明、配置文件、接口定义一起放进去,任务描述写清楚"找出接口签名与实际调用不一致的地方、找出配置项在代码中存在但配置文件中缺失的情况"。这类任务的输出必须是结构化的,每条都要带文件路径和行号,方便直接跳到 IDE 里核对。
这里有个细节:不要把整个仓库无差别塞进去。我一般先按目录树做一次粗筛,把测试文件、生成的产物、依赖锁定文件排除掉,通常能砍掉一半体积。剩下的部分按"入口模块 + 被调用模块"成组投喂,比按字母顺序平铺效果好得多,因为相关的代码放在一起,模型更容易建立调用关系的直觉。
5.2 长文档比对与审计
这类场景的典型需求是"两份长文档找差异"或者"一份长文档找内部矛盾"。切片的做法在这里几乎是失效的,因为差异可能跨越几十页。整块投喂之后,我要求的输出格式是三条线索并列:位置、原文摘要、问题说明。位置必须定位到章节标题而不是页码,因为不同版本的页码会变。
一个很实用的技巧是让模型先做一次"结构目录提取",把两份文档的章节标题各列一遍,再基于目录做对齐。这个前置步骤的成本很低,但能显著减少"拿 A 文档第三章和 B 文档第五章对比"这种错位比较。
5.3 长会话与知识沉淀
客服、咨询、项目陪伴这类长期会话,最大的痛点是模型健忘。窗口够大之后,可以把整段对话历史一直带着,但代价是成本线性增长。我的折中方案是"双层记忆":原始的完整历史保留在缓存前缀里,同时维护一份两三百字的结构化摘要(用户身份、已确认事项、待办、禁忌),放在每轮请求的最前面。摘要参与每轮重算,成本可忽略;完整历史靠缓存命中,成本压到十分之一。
这套结构跑下来,用户反馈"它记得我说过什么"的比例明显上升,而单次成本只比短会话方案高出三成左右。关键在于摘要要结构化而不是自然语言段落,字段固定的摘要比一段散文更容易被模型稳定利用。
6. 工程化收尾:成本、延迟、稳定性
窗口大了之后,工程重心会从"怎么塞进去"转移到"怎么塞得起、塞得稳"。这部分没什么炫技空间,全是实打实的开关和阈值。
6.1 成本控制的四个开关
开关一,缓存断点位置。所有不变的内容往前放,变的内容往后放,断点打在分界线上。这一条做好,成本直接砍到十分之一量级。
开关二,档位路由。可自动校验的任务先走 Sonnet 4.6,失败再升 Opus 4.6,重试上限设 1 次。
开关三,输入体量上限。我在网关层硬性限制单次输入不超过安全线的 70%,超了就拒,逼着调用方去做分桶。这个限制一开始被团队抱怨,后来所有人都感谢它,因为它把"悄悄烧钱"变成了"当场报错"。
开关四,结果落盘复用。同一份材料的所有分析结果统一存起来,二次查询先查本地,命中就不调模型。文档类场景里,重复提问的比例高得惊人,这一层的命中率往往能到 40% 以上。
6.2 延迟与超时的处理
长上下文的延迟主要在 prefill 阶段,输入越大越明显。我的处理原则是把等待显性化。同步接口超过一个阈值就改成异步任务,前端给进度反馈,而不是让用户对着转圈猜。实测下来,同样等 40 秒,有进度提示的投诉率比没有的低一个数量级,这跟技术无关,纯粹是体验设计。
超时策略要分层:短请求用短超时快速失败,长请求单独配长超时并且禁用自动重试。另外记得在网关层给长请求做并发限流,因为它们占用的资源是短请求的几十倍,一个用户同时发起五个百万级请求,整个服务就会被拖垮。
最后分享一个我最近在用的判断标准:如果一个任务在 10 万 Token 下能做好,就不要上 100 万 Token。窗口是上限,不是目标。我见过太多团队因为"反正塞得下"而把整份材料无差别投喂,结果既贵又慢,效果还不如精心挑选的十万字。窗口变大真正的价值,是让你在确实需要全局视野的那部分任务上,不用再妥协;而不是让你在所有任务上都不再思考。这个边界感,比任何参数都重要。