Skip to content

上下文窗口 ​

在哪一组:Context 组 | 上一组出口:能把意图写成四要素提示并当代码管理 | 本页出口:能为一轮请求算清 token 预算(四块配比 + 输出预留)、识别超窗症状、按保配对规则裁剪 前置:提示词工程 | 下一步:上下文工程、结构化输出

1. 概述 ​

结论:提示写清之后质量还差、或多轮之后越聊越贵,第一件要查的是窗口预算。上下文窗口(context window)是单次调用的硬约束:这一轮发给模型的全部内容加起来不能超过模型上限,而且输入与输出共享同一份预算(Anthropic 文档,检索 2026-09-01)。超限只有两种结局:显式报错(400),或静默截断——后者更危险,因为没人知道丢了什么。

token 一句话讲清:tokenizer 把文本切成的最小单元,是窗口计量与计费的单位;同一段文字在不同模型的 tokenizer 下 token 数不同,所以预算必须按目标模型核对。

心智模型:一轮请求的四块预算 ​

text
┌──────────────────── context window(这一轮的硬预算)────────────────────┐
│ system + 工具 schema   稳定前缀:每轮几乎不变                            │
│ 消息历史               随轮次增长,超预算先裁这里(保 tool 配对)          │
│ 检索片段 / 文件         按需注入,不整库预塞                              │
│ 输出预留               模型即将写出的 token 占同一份预算                   │
└─────────────────────────────────────────────────────────────────────────┘
不变量:Σ(四块) ≤ window;超过 80% 就该动手裁;多塞不只是贵——还会更笨(context rot)

「这一轮装什么、按什么优先级装」是策略问题,去 上下文工程;本页只管装得下这一硬约束。

何时使用 / 何时不用 ​

写给谁写多轮对话产品、编码助手工作流、Agent 循环的工程师
何时用任何超过一两轮、或注入外部内容(文件 / 检索 / 工具结果)的场景
何时不用单轮、短提示、无外部数据——预算管理是纯开销
不是本页装什么的来源与优先级 → context-engineering;跨轮历史存哪 → session-memory;KV cache 与 n² 注意力的数学 → Learn LLM 第 15 章 与 架构桥接页

决策表:超预算时动哪一块 ​

动哪块方向控制权状态信任域最低复杂度
裁消息历史丢最旧轮次全在你(组装层)会话内生效丢的是旧信息最低:保配对裁剪(本页)
清深层工具结果成对清除已执行完的 tool 调用全在你一次性低损(结果早已消费)低(本页 R2)
压检索注入降 top-k / 截断片段全在你每轮重组摘要有损中(→ context-engineering)
换更大窗口模型提高上限交给厂商模型代际计费与延迟上升采购决策,且不解决 rot

版本里程碑:未验证(各模型窗口上限持续变化,本页不维护数字清单,以各家官方文档当天页面为准)。

2. 使用 ​

最小实战:token 预算分配器(零 key) ​

15 分钟,Node 22 LTS。演示四块配比 → 溢出告警 → 裁剪策略的完整链路:预算先算后发(I1)、裁剪保 tool 配对(I2)。负例演示裸 slice 如何切断配对。

setup:存为 context-window.ts,运行 npx tsx@4 context-window.ts。

ts
// fixture: token 预算分配器——四块配比、溢出告警、保配对裁剪
// 与厂商 messages 数组同构的最小子集
type Message =
  | { role: 'system'; content: string }
  | { role: 'assistant'; content: string; toolCalls?: { id: string; name: string }[] }
  | { role: 'user'; content: string; toolResultFor?: string }

// 估算启发式:中文约 1-2 字/token、英文约 4 字符/token;对账必须用厂商 token 计数 API
function estimateTokens(text: string): number {
  const cjk = (text.match(/[一-鿿]/g) ?? []).length
  const rest = text.length - cjk
  return Math.ceil(cjk / 1.5 + rest / 4)
}

function messageTokens(m: Message): number {
  let total = estimateTokens(m.content)
  for (const call of m.toolCalls ?? []) total += estimateTokens(`${call.name} ${call.id}`)
  return total
}

const historyTokens = (messages: Message[]): number =>
  messages.reduce((sum, m) => sum + messageTokens(m), 0)

// 四块预算:system 稳定前缀 / history 会话历史 / retrieval 检索注入 / output 输出预留
interface BudgetReport {
  blocks: { system: number; history: number; retrieval: number; output: number }
  used: number
  ratio: number
  fits: boolean
}

function planBudget(
  window: number,
  parts: { system: Message[]; history: Message[]; retrieval: string[]; outputReserve: number }
): BudgetReport {
  const blocks = {
    system: historyTokens(parts.system),
    history: historyTokens(parts.history),
    retrieval: parts.retrieval.reduce((sum, r) => sum + estimateTokens(r), 0),
    output: parts.outputReserve,
  }
  const used = blocks.system + blocks.history + blocks.retrieval + blocks.output
  return { blocks, used, ratio: used / window, fits: used <= window * 0.8 }
}

// I1 预算先算后发:超过窗口 80% 即拒发
function assertFits(report: BudgetReport, window: number, label: string): void {
  const b = report.blocks
  console.log(
    `[${label}] system ${b.system} + history ${b.history} + retrieval ${b.retrieval} + output ${b.output} = ${report.used} / ${window} (${report.ratio.toFixed(2)})`
  )
  if (!report.fits) {
    throw new Error(`context plan ${report.used} tokens exceeds 80% of window ${window} — trim before sending, not after`)
  }
}

// I2 裁剪保配对:tool_use 与 tool_result 是配对结构
function findBrokenToolPairs(messages: Message[]): string[] {
  const problems: string[] = []
  const openToolIds = new Set<string>()
  const answeredToolIds = new Set<string>()
  for (const m of messages) {
    for (const call of m.toolCalls ?? []) openToolIds.add(call.id)
    if (m.role === 'user' && m.toolResultFor) answeredToolIds.add(m.toolResultFor)
  }
  for (const id of openToolIds) {
    if (!answeredToolIds.has(id)) problems.push(`tool_use ${id} has no tool_result`)
  }
  for (const id of answeredToolIds) {
    if (!openToolIds.has(id)) problems.push(`tool_result ${id} has no tool_use`)
  }
  return problems
}

// 裁剪策略:system 永远保留;从队首移除旧消息,但绝不切断 tool 配对
function trimHistory(messages: Message[], keepFrom: number): Message[] {
  const hasSystemHead = messages[0]?.role === 'system'
  const bodyStart = hasSystemHead ? 1 : 0
  const keptHead: Message[] = hasSystemHead ? [messages[0]] : []
  const keptBody = messages.slice(Math.max(keepFrom, bodyStart))
  const first = keptBody[0]
  if (first && first.role === 'user' && first.toolResultFor) {
    const pairIndex = messages.findIndex(
      (m) => m.role === 'assistant' && m.toolCalls?.some((c) => c.id === first.toolResultFor)
    )
    if (pairIndex >= bodyStart && pairIndex < keepFrom) {
      return [...keptHead, ...messages.slice(pairIndex)]
    }
  }
  return [...keptHead, ...keptBody]
}

// 演示数据:一次带两轮工具调用的客服会话
const system: Message = {
  role: 'system',
  content: '你是订单客服助手。回答必须基于工具返回的数据,不得编造订单号。'
}
const history: Message[] = [
  { role: 'user', content: '帮我查一下 Alice 最近的三笔订单。' },
  { role: 'assistant', content: '我来查询订单。', toolCalls: [{ id: 'toolu_01', name: 'search_orders' }] },
  { role: 'user', content: '{"matched":3,"orders":["ORD-101","ORD-102","ORD-109"]}', toolResultFor: 'toolu_01' },
  { role: 'assistant', content: 'Alice 最近三笔订单为 ORD-101、ORD-102、ORD-109。' },
  { role: 'user', content: 'ORD-102 是什么时候发货的?' },
  { role: 'assistant', content: '我查一下发货时间。', toolCalls: [{ id: 'toolu_02', name: 'get_order' }] },
  { role: 'user', content: '{"orderId":"ORD-102","shippedAt":"2026-08-14"}', toolResultFor: 'toolu_02' },
  { role: 'assistant', content: 'ORD-102 于 2026-08-14 发货。' },
  { role: 'user', content: '把它退款到原支付方式。' }
]
const retrieval = ['[policy] 退款政策 v3:原路退回,1-3 个工作日到账。']

const WINDOW = 145 // 故意设小,触发溢出告警

console.log('--- 正例:完整会话预算规划 ---')
console.log('broken pairs:', findBrokenToolPairs([system, ...history]))
const full = planBudget(WINDOW, { system: [system], history, retrieval, outputReserve: 30 })
try {
  assertFits(full, WINDOW, 'full')
} catch (error) {
  console.error(`guard fired: ${(error as Error).message}`)
}

console.log('\n--- 裁剪:保 system + 保配对,预算回到安全线 ---')
const trimmedHistory = trimHistory(history, 4)
const trimmed = planBudget(WINDOW, {
  system: [system],
  history: trimmedHistory,
  retrieval,
  outputReserve: 30,
})
console.log('kept roles:', ['system', ...trimmedHistory.map((m) => m.role)].join(' -> '))
console.log('broken pairs after trim:', findBrokenToolPairs([system, ...trimmedHistory]))
assertFits(trimmed, WINDOW, 'trimmed')

console.log('\n--- 负例:裸 slice(-3) 切断 tool 配对 ---')
const naive = history.slice(-3)
console.log('kept roles:', ['system', ...naive.map((m) => m.role)].join(' -> '))
console.log('broken pairs after naive slice:', findBrokenToolPairs([system, ...naive]))

正常输出(超限被守卫拦下,裁剪后回到安全线):

text
--- 正例:完整会话预算规划 ---
broken pairs: []
[full] system 20 + history 94 + retrieval 15 + output 30 = 159 / 145 (1.10)
guard fired: context plan 159 tokens exceeds 80% of window 145 — trim before sending, not after

--- 裁剪:保 system + 保配对,预算回到安全线 ---
kept roles: system -> user -> assistant -> user -> assistant -> user
broken pairs after trim: []
[trimmed] system 20 + history 46 + retrieval 15 + output 30 = 111 / 145 (0.77)

负例输出(裸 slice 切断配对,下一轮请求会直接 400):

text
--- 负例:裸 slice(-3) 切断 tool 配对 ---
kept roles: system -> user -> assistant -> user
broken pairs after naive slice: [ 'tool_result toolu_02 has no tool_use' ]

验收命令:

bash
npx tsx@4 context-window.ts && echo BUDGET-OK

清理:删除临时文件。

场景表 ​

场景输入动作输出适用不适用
基础:发前预算规划本轮四块内容planBudget + assertFits通过 / 拒发所有多轮产品单轮短请求
常见:超窗裁剪超预算会话trimHistory(保 system + 保配对)合法且过审的消息序列长会话需要完整历史审计的场景(存日志,不进窗口)
组合:预算护栏进回归本页 fixture断言 ratio 与配对检查进 CI漂移即红换模型 / 改提示后一次性脚本

3. 原理 ​

为什么窗口有限:注意力、KV cache 与训练分布(决策影响层) ​

Anthropic 工程文章(检索 2026-09-01)给出三个事实:Transformer 里 n 个 token 产生 n² 个成对注意力关系,上下文越长每个关系被摊得越薄;训练数据里短序列远多于长序列,模型对长程依赖经验不足;推理时的 KV cache 显存随 token 数增长,窗口越大服务成本越高。工程结论只有一句:把窗口当预算做规划,而不是当仓库做堆放。

n² 复杂度、位置编码插值、KV cache 的数学推导 → 本仓停止,去 Learn LLM(第 15 章)与 模型架构桥接页。

为什么更大的窗口不是解法:context rot ​

needle-in-a-haystack 类基准揭示 context rot(上下文腐烂):窗口内 token 越多,准确回忆能力越差,且这一特征跨模型存在(Chroma 研究给了系统实证,检索 2026-09-01)。所以「换 200k 窗口模型」解决的是 400 报错,解决不了「塞进去但模型看不见」。信息密度比窗口上限重要。

超窗的两类表现(症状表) ​

表现你看到什么根因第一动作
显式拒绝400,错误信息指向 context length输入侧超上限把 assertFits 挪到组装层(I1)
静默截断回答突然「失忆」、不守规则;或输出中途断掉(stop_reason: max_tokens)框架/厂商静默裁剪;输出预留不足查预算报告四块占比;查 stop_reason

token 计量的三层 ​

层工具用途
启发式估算字符近似(本页 estimateTokens)发前守卫、本地断言
厂商计数 APIOpenAI / Anthropic 的 count tokens 接口计费对账、容量规划
tokenizer 本地复算厂商 tokenizer 库机制理解(→ Learn LLM)

规范要求 vs 本地实测 ​

官方/规范断言本仓 fixture / 实践
输入与输出共享窗口预算fixture 的 output 块即输出预留 30 token,计入 used
超限应先算后发planBudget + 80% 阈值拒发,负例可见告警
裁剪不得切断 tool 配对findBrokenToolPairs 检出裸 slice 的断裂
中文 1-2 字 / 英文 4 字符 ≈ 1 token启发式仅做守卫;计费对账用厂商 token API

关键不变量 ​

  1. I1 预算先算后发:组 messages 时检查,不发到厂商那里才发现 400。
  2. I2 裁剪保配对:tool_use 与 tool_result 是配对结构;切断后轻则下一轮 400,重则模型重复执行副作用工具。
  3. 输出预留计入预算:max_tokens 不是免费额度,它占的是同一份窗口。
  4. 信息密度比上限重要:「窗口 200k」不等于「可以把整个仓贴进去」。

4. 开发 ​

集成要点 ​

  1. 预算检查进组装层:在「组 messages 的函数」里调用 assertFits,而不是在 HTTP 客户端里事后补救。
  2. max_tokens 与输出预留对齐:输出预留不足 → stop_reason: max_tokens 截断;预算规划时给输出块留足空间。
  3. token 对账用厂商 API:估算只做守卫;计费与容量规划用 OpenAI / Anthropic 的 token counting 接口。
  4. 窗口数字随模型走:换模型即重核对窗口上限与 tokenizer 差异,模型本身要 pin(见 模型 API 契约)。

调试 runbook ​

R1 长会话越聊越差、成本线性涨 ​

症状:同一会话第 N 轮后回答质量下降、延迟与费用持续上升。 证据:逐轮 token 计数曲线(planBudget 的 used);确认输入 token 随轮次单调增长。 处理:上保配对裁剪或清深层工具结果;检索内容从「每轮全量」改为按需注入(→ context-engineering)。 完成标准:token 曲线回到锯齿稳态;预算守卫连续 N 轮不再触发。

R2 裁剪后下一轮 400 ​

症状:加历史裁剪后,偶发 400,错误信息指向消息结构。 证据:dump 出错请求的完整 messages;对 tool_use / tool_result 做配对检查(findBrokenToolPairs)。 处理:换成保配对裁剪;或最轻手段先成对清除深层工具结果(两侧一起删,不是单边)。 完成标准:连续无 400;配对检查入 CI fixture。

R3 输出总在中途断掉 ​

症状:回答说到一半停住,JSON 不完整无法 parse。 证据:响应的 stop_reason / finish_reason 为 max_tokens;预算报告里 output 块为 0 或过小。 处理:给输出预留加预算并同步调大 max_tokens;输出过长则拆任务(一次要一件事)。 完成标准:stop_reason 回到正常结束;schema 验证不再因截断失败。

R4 守卫误报:估算与厂商计数差太多 ​

症状:本地算着没超窗,厂商却报 400;或反过来守卫频繁误拦。 证据:对比 usage.prompt_tokens 与 estimateTokens 的偏差率;中英混排与代码片段偏差最大。 处理:用厂商 count tokens 接口校准启发式系数;对账场景直接换厂商计数。 完成标准:偏差进入可接受区间(如 ±10%);守卫阈值不再频繁误触发。

反模式清单 ​

  • 整本塞:把整份设计文档 / 整库代码贴进每一轮;检索 top-k 带来源即可。
  • 裸 messages.slice(-N):切断 tool 配对,下一轮 400。
  • 静默丢 system:超窗时丢最前面的系统提示——规则全没了,且没人知道。
  • 输出不留预留:max_tokens 拉满输入,输出必然截断。
  • 拿估算 token 对账计费:启发式只配做守卫。
  • 「窗口大就不管预算」:rot 不因窗口变大而消失,成本曲线也不会。

5. 资料库 ​

四级阅读路线 ​

级读什么为什么是这个顺序
BeginnerOpenAI 指南 context window 节 | Anthropic Context windows 文档先建立「预算」与 token 的直觉
Builder本页 fixture 跑通 | Anthropic:Effective context engineering手上有守卫,再读官方心智模型
Operator两家 token counting 文档 | Anthropic Context windows(计费口径)I1 的对账工具与预算口径
ResearcherChroma:Context Rot 研究 | Learn LLM 第 15 章 A5衰减实证与 KV cache / 注意力数学

资源表 ​

名称层级canonical URL用途支持的断言下一步
OpenAI prompt engineering(context window 节)L1https://developers.openai.com/api/docs/guides/prompt-engineering官方预算视角窗口以 token 计、RAG 定义读其 Structured Outputs
Anthropic Context windowsL1https://platform.claude.com/docs/en/build-with-claude/context-windows窗口与计费口径输入输出共享预算配 token counting 用
Anthropic context engineeringL1https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents心智模型总纲注意力预算 / context rot组装策略 → context-engineering
Chroma Context RotL4https://research.trychroma.com/context-rot衰减实证长 ctx 检索退化的实验证据设计长文任务前读
Learn LLM 第 15 章L2https://llm.zenheart.site/chapters/15-prompt-memory机制层KV cache / 注意力数学 / 截断实现要「为什么」时读
本页 fixtureEcontext-window.ts(正文内联)零 key 验证四块配比 / 配对裁剪行为换成厂商计数 API

(retrievedAt: 2026-09-01。)

主动证伪与未决问题 ​

  • 估算启发式(中文 1.5 字 / token)未经厂商 tokenizer 逐字核对,只用于守卫阈值,差 20% 不影响「先算后发」的结论。
  • 未决:context rot 的量化临界点(多少 token 开始显著衰减)无通用公式,逐模型实测是唯一可靠来源。
  • 各模型窗口上限与 tokenizer 持续变化,本页不维护数字清单;依赖前查官方当天文档。

learn-ai 到此为止 / 继续去哪 ​

为前端工程师打造 · 基于 VitePress 构建