上下文窗口:模型的短期记忆

0 阅读8分钟

你跟 ChatGPT 说"我叫小明",聊了一百轮之后再问"我叫什么",它答不上来。 不是它变笨了,是"小明"这两个字早就被挤出了它的视线。

本文收录于微信公众号:愤怒的代码 AngryCode

0x00、一句话定义

上下文窗口(Context Window)是模型在同一个时刻能"读进去并记住"的最大文本量,用 token 来衡量,而不是用字数。

它是模型的短期记忆。

你可以把它理解成一张固定大小的桌面:模型每回答一次,都要把你给的资料摊在桌面上读一遍,桌面之外的东西它一概看不见。桌面就这么大,东西太多,就得往下撤。

0x01、模型本身没有记忆

先解决一个前提问题:模型到底有没有记忆?

没有。

模型每次收到你的消息,都是从零开始,只读你这一次塞给它的文本。它不会在脑子里存"哦,这个用户叫小明"。

那它是怎么记住的?

靠重发。你发第二条消息时,整个对话——你之前说过的话、它之前的回复、加上你这条新消息——会被打包成一个整体,重新喂给模型。模型从头读一遍,看到"我叫小明",于是答"你叫小明"。

你发消息 2
    ↓
+----------------------------+
|  上下文窗口                 |
|  "我叫小明。"              |
|  "我叫什么?"              |
+----------------------------+
    ↓
+----------+
|   模型    |
+----------+
    ↓
"你叫小明。"

模型的参数一个字都没变,变的只是每次塞进窗口里的内容。它没在记你,只是把旧对话重新抄一遍再发回来。

0x02、token 不是"字"

窗口的大小用 token 算,那 token 是什么?

一小段文本。它可以是一个完整的词、半个词,甚至一个字符。

模型不是直接读整句,而是先把文本切成一小块一小块的 token,再去读这些块。切分这一步叫 tokenization(分词)。

看一个英文的例子:

"I like machine learning"
→ ["I", " like", " machine", " learn", "ing"]

四个词被切成了五个 token,"learning" 被拆成了 "learn" + "ing"。这就是为什么说 token 不等于"词"。

中文不太一样:早期的分词器大概一个字算一个 token,现在主流模型里一个汉字大致是 1 到 2 个 token。

经验数字记两个就行:英文里 1 token ≈ 4 个字符,100 token ≈ 75 个词。所以一个 8000 token 的窗口,大约能装 6000 个英文单词。中文单个字的信息密度更高,同样的 token 数量通常能装下更多内容。

还有一个前提别忽略:这个空间是输入和回复共享的。你贴进去的文档、模型写出来的答案,占的是同一块地方。

0x03、窗口里有哪些信息

下面这些全都挤在同一个固定大小的空间里:

部分内容
系统指令你给模型定的规则,比如"你是一位耐心的老师"
历史消息你之前说的、模型之前回的全部内容
当前问题你刚发的新消息
粘贴的文档你塞进去当参考资料的任何文件或文字
模型正在写的回复答案一个字一个字生成,边生成边占地方

它们互相抢位置。文档贴得越多,留给对话和答案的空间就越少。

0x04、模型怎么读这个窗口

窗口里塞满 token,模型怎么读?

一次性全读。它把窗口里所有 token 同时扫一遍,看每个 token 跟它前面的 token 是什么关系——这个"看关系"的动作就是注意力机制(attention)。靠这一步,它才知道整段话在讲什么。

读完,预测出下一个 token。然后把这个新 token 加回窗口,重新扫一遍,再预测下一个。循环到答案写完为止。

代价在这里:回复是一个一个 token 蹦出来的,每蹦一个,模型都要把整个窗口重新读一遍。窗口越大,每写一个字要扫的内容就越多,算得越慢、越贵。

这也解释了窗口为什么重要:信息在窗口里,模型就看得见、用得上;信息在窗口外,模型完全看不见,再重要也没用。

0x05、窗口满了怎么办

现在主流模型的窗口有多大?GPT-4o、DeepSeek、Llama 3.1 这一档是 128K;Claude 是 200K;Gemini 做到了 1M。128K 什么概念?按英文算大约 9.6 万个词,中文大约能装几万到十几万个字,一本中篇小说丢进去,它能从头读到尾。

但窗口再大也有硬上限,塞不进去就是塞不进去。

假设窗口是 100 token,对话已经长到 130 token,多出来的 30 token 放不下,就必须丢东西。多数聊天应用的做法是:把最旧的消息挤出窗口。

对话 = 130 token
+---------------------+
| "我叫小明。"         |
| 较早的回复           |
| 最近的几条消息        |
| 最新的问题           |
+---------------------+
        ↓ 窗口只有 100 token,最旧的被挤出去
+---------------------+
| 最近的几条消息        |
| 最新的问题           |
+---------------------+

看出问题了吗?"我叫小明"在最开头,一旦被挤出去,模型就再也看不到它。这时候你再问"我叫什么",它只能答不知道。

长对话聊到后面模型"失忆",不是 bug,就是这段信息不在窗口里了。

有的工具不直接丢,而是把旧对话压成一小段摘要放回窗口,用几十个 token 保住关键事实。

所以窗口管理就一件事:让重要信息待在窗口里。

第一,重要信息放在问题旁边。 窗口空间有限,别用来装跟当前问题无关的东西。

第二,长对话要总结旧内容。 把一大段旧聊天压成一句摘要,关键事实还在,却省出大量空间:

旧聊天(约 400 token):
"我叫小明,来自深圳,正在学习 AI 和机器学习课程……(后面还有一大堆)"

摘要(约 30 token):
"用户小明,深圳人,正在学习 AI 和机器学习课程。"

400 token 换成 30 token,省下来的空间全留给新消息和答案。

第三,大文档只贴相关部分。 50 页的文件你只用到其中一页,就贴那一页。贴满整份文件,窗口大半都浪费了。

第四,给回复留空间。 答案本身也占 token。输入把窗口塞得差不多满,模型就没地方把答案写完整。

上面四条是你能手动做的。前沿实验室在更底层也做了几件事,思路一样。

自动压缩。 Claude Code、OpenAI Codex 这类编程助手内置了这招:对话快撞到窗口上限时,自动把历史总结成摘要,替换掉原始消息,接着聊。这就是上面“总结旧内容”的自动化版,不用你手动写摘要。

长期记忆外置。 ChatGPT 的 Memory 把“我叫小明、我在做 AI”这类个人信息单独存起来,跨会话复用,不占当前窗口。等于把每次都要重复的私货搬出窗口。

架构上把窗口做大、做省。 DeepSeek 用 MLA(多头潜在注意力,Multi-head Latent Attention)压缩注意力计算里的缓存,长上下文更省显存;Gemini 走另一条路,直接把窗口做到 1M、2M,用大窗口硬扛。

0x06、三个高频错误

错误一:贴一份超大文档,指望模型一字不落全记住。 窗口是大了,但文档越长,中间的信息越容易被漏掉——模型“读完”不等于“全用上”;真超过窗口,还是会被截断。

错误二:一个对话从头聊到尾,话题换了好几个。 窗口里堆满旧的、不相干的文字,模型容易串味。现在产品会帮你自动总结旧内容,但换话题最好还是开个新对话。

错误三:把真正的问题写在最上面,后面又贴好几页内容。 模型对长文本中间和靠前位置的注意力会打折,关键问题容易被埋住——这就是 Lost in the Middle(迷失在中间)。问题要清楚,而且要挨着相关上下文放。

0x07、总结

回到开头那个问题:为什么模型聊着聊着就"失忆"了?

因为它没有记忆,只有一扇固定大小的窗口。你每次说话,它都只能透过这扇窗看世界;窗里的东西它能用,窗外的它看不见。窗口没满时,"小明"还在窗里,它答得上来;窗口满了,"小明"被挤出去,它就只能说不知道。

看懂这扇窗,就明白两件事:别指望模型“记住”没放进窗口的东西;往窗口里放什么、不放什么,直接决定了答案的质量。这是理解一切长上下文问题——包括后面会遇到的 RAG(检索增强生成)、摘要策略、Lost in the Middle——的入口。


本文收录于微信公众号:愤怒的代码 AngryCode