模型介绍页经常写着 128K、200K 或 1M 上下文。很多人把这个数字理解成模型的记忆容量,于是把整本书、全部聊天记录和几十个工具定义塞进一次请求。请求可能成功,回答质量却不一定更好。

上下文窗口只描述一次推理的容量。它不能替代长期记忆,也不能保证模型会均匀使用窗口里的每段信息。

上下文窗口是模型在一次请求中能够处理的 token 总量,通常包含输入内容和模型生成的输出。

假设某个模型支持 128K token,你提交了 110K token 的输入,又允许它生成 20K token,输入与输出相加就超过了窗口。API 会拒绝请求、截断输入,或者缩短输出,具体行为取决于服务商。

窗口计算可以写成:

系统提示词 + 历史消息 + 当前问题 + 工具定义 + 检索资料 + 输出上限
≤ 模型上下文窗口

这个不等式解释了很多看似奇怪的问题。你只问了一句话,API 却提示输入过长,原因往往藏在历史消息、工具 schema 或检索内容里。

窗口里装了什么

一次聊天请求通常包含下面几部分:

内容 作用 是否占用 token
System Prompt 规定身份、规则和输出格式
对话历史 保持当前会话连贯
当前问题 用户本轮输入
工具定义 告诉模型有哪些工具、参数怎么传
RAG 片段 提供外部知识
工具返回值 数据库、搜索或命令的执行结果
模型回复 当前请求生成的内容

图片、音频和 PDF 也会占用窗口。多模态服务通常先把这些内容编码成模型能处理的表示,再折算为输入 token 或单独计价。图片分辨率越高、页数越多,占用通常越大。

上下文不是长期记忆

模型不会因为你昨天和它聊过某件事,就在今天自动记住。服务商可以保存聊天记录,但下一次推理时仍要把相关内容重新放进上下文。

两者的职责不同:

  • 上下文窗口负责当前请求需要的信息。
  • 长期记忆系统负责选择、保存和取回跨会话信息。

很多聊天应用会保存用户偏好,然后在下一次对话时把“用户喜欢简短回答”这类信息写进 system prompt。模型看起来记住了偏好,实际是应用替它完成了存取。

Agent 的工作记录也一样。任务运行两小时后,完整日志可能超过窗口。应用需要压缩旧步骤,只保留目标、已完成事项、失败原因和下一步,而不是把全部工具输出重复传给模型。

128K 到底有多大

token 和文字数量不能直接一比一换算。英文常见文本平均每个 token 对应约 4 个字符,中文常见文本可能每个汉字占 1 到 2 个 token。代码、JSON、emoji 和生僻字的比例还会变化。

可以用下面的范围理解 128K token:

内容类型 128K token 大致容量
英文文章 约 8 万到 10 万词
中文文章 约 6 万到 10 万汉字
TypeScript 代码 约 4,000 到 8,000 行
普通聊天记录 数百轮短对话,取决于每轮长度

这些数字只适合估算。真正接入 API 时,应该用对应模型的 tokenizer 计算。不同厂商、不同模型使用的词表并不相同。

长上下文为什么还会漏信息

窗口能装下,不代表模型能准确找到。

研究人员用“针藏在干草堆里”(Needle in a Haystack)测试长上下文:在大段无关文字中插入一条关键信息,再让模型回答。模型对开头和结尾的信息通常识别得更好,对中间部分的识别率会下降。相关研究把这种现象称为 Lost in the Middle。

问题来自注意力分配。模型要在数十万 token 之间计算关联,关键信息只占其中一小段时,无关内容会稀释信号。内容之间存在重复、冲突或相似表述时,模型更容易选错。

长窗口还会带来三个成本:

  • 首 token 延迟增加:模型需要先处理全部输入,输入越长,回复开始得越慢。
  • API 费用增加:服务商按输入 token 计费,重复发送历史内容会反复付费。
  • 调试难度增加:回答错误时,你需要从大量上下文里判断哪一段影响了模型。

一个 token 预算器

下面的例子用 tiktoken 计算请求各部分占用,并在超出预算时阻止发送:

import json
import tiktoken

enc = tiktoken.encoding_for_model("gpt-4o")

system_prompt = "根据给定资料回答问题。资料没有答案时说明不知道。"
history = [
    {"role": "user", "content": "什么是 MCP?"},
    {"role": "assistant", "content": "MCP 是连接 AI 应用与外部工具的开放协议。"},
]
question = "它和 Function Calling 有什么区别?"
retrieved_chunks = ["MCP 规定客户端与服务端之间的通信方式。"]

prompt_parts = [
    system_prompt,
    json.dumps(history, ensure_ascii=False),
    question,
    *retrieved_chunks,
]

input_tokens = sum(len(enc.encode(part)) for part in prompt_parts) 
output_budget = 2_000

if input_tokens + output_budget > 128_000: 
    raise ValueError("请求超过上下文窗口")

实际 API 还会给消息结构、角色名称和工具调用添加少量 token,因此 SDK 提供官方计数方法时应优先使用。上面的代码适合在请求进入模型前做粗略拦截。

对话越聊越贵

聊天应用通常在每一轮请求里重新发送历史消息。

假设每轮用户和模型合计生成 1,000 token:

轮数 本轮需要重发的历史 累计输入规模
第 1 轮 0 约 1,000 token
第 5 轮 前 4 轮 约 5,000 token
第 20 轮 前 19 轮 约 20,000 token

如果每轮都完整重发,20 轮对话的累计计费量远大于 20,000 token,因为第 1 轮的内容会在后续请求中重复出现。

应用可以在对话变长后做摘要:

保留:
- 用户的目标与限制
- 已确认的事实
- 已完成的步骤
- 当前错误和下一步

删除:
- 重复解释
- 已经失效的方案
- 冗长工具日志
- 与当前任务无关的闲聊

摘要会损失细节,因此不能一开始就压缩。应用应保留最近几轮原文,把更早的历史整理成结构化摘要。

长上下文、RAG 和记忆怎么选

这三种方案经常同时出现,但解决的问题不同。

方案 适合的问题 主要限制
长上下文 当前任务需要通读一份完整材料 贵、慢,中间信息可能被忽略
RAG 从大量文档中找与问题相关的片段 检索可能漏掉关键内容
长期记忆 跨会话保存用户偏好和任务状态 错误记忆会持续影响后续对话

一份 50 页的合同需要整体分析时,可以直接放入长上下文。一个拥有十万份合同的系统应该先用 RAG 检索。用户希望系统下次记住常用合同模板时,再加入长期记忆。

工具定义也会撑满窗口

Agent 应用经常一次注册几十个工具。每个工具都包含名称、描述、参数类型、枚举值和字段说明,这些 JSON Schema 会在每轮请求中占用 token。

工具越多,模型还会遇到选择问题。search_docssearch_websearch_database 的描述如果含糊,模型容易误选。

常见处理方式有两种:

  1. 按任务类型只加载相关工具,例如写代码时加载文件和终端工具,不加载邮件工具。
  2. 先让一个路由器判断工具类别,再把该类别的工具交给主模型。

MCP 解决工具接入协议,但不会自动消除工具 schema 的上下文成本。客户端仍要控制模型本轮能看到哪些工具。

怎样使用上下文窗口

  • 先算 token,再发送请求。不要等 API 报错才处理超长输入。
  • 给输出留空间。128K 窗口塞入 127K 输入后,模型没有足够空间生成完整回答。
  • 删掉重复内容。系统提示词、RAG 片段和用户消息可能重复描述同一规则。
  • 把关键要求放在清晰位置。任务目标和输出格式放在 system prompt 或当前问题里,不要埋进中间材料。
  • 长文档保留结构。标题、章节名和页码能帮助模型定位,也方便回答时引用来源。
  • 保存原始资料。摘要只用于控制上下文,不应该覆盖原文。需要核实时再取回对应片段。
  • 用评测确认效果。窗口从 32K 提升到 128K 后,应该用固定问题集测试准确率、延迟和成本,而不是只看请求能否成功。

写在最后

上下文窗口提供容量,应用负责选择内容。把所有资料塞进去省掉了检索设计,也把成本、延迟和错误定位一起推高。

处理长任务时,先明确模型完成当前一步需要什么。保留目标、约束、相关资料和最近状态,其余内容交给 RAG、摘要或长期存储。窗口里装得少一些,模型往往更容易找到答案。