上下文窗口与 LLM 记忆机制

模型没有真正的“长期记忆”——它只有一次推理时能看到的 token 序列。理解这一点,是设计靠谱对话系统、Agent 和记忆系统的前提。


为什么要读这篇

前几篇文章讲了模型如何“思考”、如何被训练。但当你真正开始做应用时,很快就会遇到一类非常具体、而且非常现实的问题:

  • 用户明明在第 2 轮说过自己的需求,为什么到第 12 轮模型就“忘了”?
  • 为什么一个号称 128K 甚至 1M 上下文的模型,仍然会漏掉长文中间的关键信息?
  • 为什么有时候把整份文档都塞进 prompt,效果反而不如检索几段相关片段?
  • System prompt、Few-shot、对话历史、RAG、摘要、微调——这些到底谁在扮演“记忆”?

这些问题看起来分散,背后其实都指向同一个核心:

LLM 的“记忆”不是一个统一能力,而是一整套分层机制。

有些“记忆”发生在一次请求内部,比如 context window。
有些“记忆”由应用层维护,比如对话历史和摘要。
有些“记忆”来自外部系统,比如 RAG 和向量库。
还有一些能力会被“固化”到模型里,比如微调后形成的风格和领域偏好。

所以这篇文章的目标,不只是解释“上下文窗口有多大”,而是把 LLM 的记忆全景图讲清楚:

  • 模型一次推理到底“看到了什么”
  • 为什么“能塞进去”不等于“能稳定利用”
  • 多轮对话的记忆到底是谁在维护
  • RAG 为什么是“外部记忆”,而不是“模型自己记住了”
  • 生产环境里应该怎么组合这些记忆机制

先说结论:LLM 的“记忆”分成四层

如果你只想先记一个大框架,可以先记住这张图:

System / Few-shot

对话上下文(Conversation Context)

外部记忆(RAG / Database / Vector Store)

模型参数中的“固化习惯”(Fine-tuning)

它们的区别不在于“哪个更高级”,而在于:

  • 生效范围不同
  • 持久度不同
  • 成本不同
  • 可更新性不同

最容易混淆的一点是:

上下文窗口不是长期记忆,它只是一次推理时的可见范围。

模型不会像人一样在会话结束后“自然记住”这段对话。
如果下次请求你没有再把相关信息发给它,它就看不见,也就等于“忘了”。


1. Context Window:不只是“能塞多少”

一句话:上下文窗口是模型一次推理时能“看到”的最大 token 数量。但窗口里的信息并不会被平等利用——位置、注意力分布和任务形式都会影响模型到底看到了什么、用好了什么。


什么是 Context Window

Context Window,或者上下文窗口,本质上就是:

模型在一次前向计算里,最多能处理多少 token。

这些 token 可能来自:

  • System prompt
  • 对话历史
  • Few-shot 示例
  • 当前用户输入
  • 检索到的文档片段
  • 工具调用返回结果

它们都会一起进入当前这次推理。

比如,一个 128K 窗口的模型,并不意味着“用户输入可以有 128K”。
因为你真正能用的空间,通常要减去:

  • system prompt 占用
  • 历史消息占用
  • RAG 检索片段占用
  • 模型回答本身也需要预留空间

所以工程上更准确的理解不是“窗口有多大”,而是:

这次请求的 token 预算怎么分配。


窗口内到底发生了什么

很多人会把上下文窗口理解成一个“文本缓存区”,好像只要信息被塞进去,模型就能稳定记住。
这个理解不够准确。

真正发生的是:

  1. 文本被切成 token
  2. token 被映射成向量
  3. 模型在多层 Transformer 中不断做 attention
  4. 当前生成位置会综合上下文中的相关信息
  5. 再输出下一个 token

所以,上下文窗口更像是:

一次临时工作台,而不是一个永久记事本。

模型并不是把窗口里的每个 token 都“背下来”,而是在当前任务下,动态决定该关注哪些部分。

这就带来一个非常重要的现实:

能放进窗口,不等于能被稳定利用。


窗口内的信息并不平等

理论上,自注意力允许任意两个 token 直接关联。
但实际使用中,窗口里的信息常常并不被平等对待。

因素影响
位置编码决定 token 的相对位置感和距离感
注意力分布模型会更关注某些位置、某些模式、某些关键词
长度窗口越长,信息竞争越激烈,中间内容更容易被忽略
任务形式问答、总结、代码补全、检索问答,对信息使用方式不同

举个直觉例子:

如果你把一段关键信息埋在 8 万 token 文档的正中间,然后在最后问模型一个问题,模型并不是“必然能找到”。
它也许看到了,但没把它当成最关键线索;也可能被前后更显眼的内容干扰。

所以从设计角度看,上下文窗口的关键不是:

“我能不能把所有内容都塞进去?”

而是:

“我有没有把最重要的信息放在最容易被模型利用的位置上?”


RoPE(Rotary Position Embedding):模型如何知道“谁在前谁在后”

Transformer 自注意力本身并不知道 token 的先后顺序。
如果没有额外机制,模型只会看到一堆 token 表示,却不知道哪一个在前、哪一个在后。

位置编码就是为了解决这个问题。

现代很多 LLM,包括 Llama、Qwen 等,都会采用 RoPE(Rotary Position Embedding)。它的核心思想是:把位置信息编码到 attention 的 Query / Key 表示里,让模型不仅知道“有哪些 token”,还知道“它们彼此距离如何、顺序如何”。这类位置建模方法之所以重要,是因为 Transformer 本身只基于 attention 交互,而顺序信息需要额外注入。:contentReference[oaicite:1]{index=1}

直觉上你可以把它理解成:

模型不是给每个位置贴一个静态编号,而是把“位置关系”融合进 token 之间的匹配方式里。

它的好处是:

  • 对相对位置关系建模自然
  • 与自注意力结合紧密
  • 在长上下文扩展中表现较好
  • 比较适合 decoder-only LLM

RoPE 能扩长上下文,但不是“无限记忆”

这里有一个很常见的误解:

既然 RoPE 可以做长度外推,那是不是窗口越长越好?

不是。

RoPE 让模型更容易扩展到比训练时更长的上下文,但这不意味着模型在超长范围里还能同样稳定地利用信息。
很多长上下文技术,包括插值、缩放、YaRN 一类方法,本质上都是在努力缓解“训练长度”和“推理长度”之间的落差,而不是让模型 magically 拥有真正无限且均匀可靠的记忆。相关研究与工程实践都表明,长上下文扩展是可行的,但质量通常会随长度增加而衰减,尤其在信息定位和中间位置利用上更明显。:contentReference[oaicite:2]{index=2}

所以更准确的说法是:

  • 长窗口 = 模型“有机会看到更多内容”
  • 但不等于 = 模型“能同样可靠地用好所有内容”

这就是为什么今天很多模型虽然号称支持超长上下文,但在长文中间找关键信息时,表现仍然可能明显下降。


“Lost in the Middle”:为什么中间最容易丢

长上下文研究里一个非常经典的发现是:

模型往往更擅长利用开头和结尾的信息,而更容易忽略中间的信息。

这类现象通常被称为 Lost in the Middle。相关研究在长上下文问答和 key-value retrieval 任务上发现,许多模型在信息位于输入开头或结尾时表现更好,而当关键信息落在中间位置时,性能会明显下降。:contentReference[oaicite:3]{index=3}

你可以把它理解成一种“位置偏置”:

  • 开头容易被当作全局设定或背景
  • 结尾离当前问题最近,容易被强关注
  • 中间内容最容易被长文本噪声淹没

这和人类看长文有点像:
如果你不做标记、不做提纲,最容易忘的往往也是中间那段。


实践含义:长窗口不是让你“乱塞”

这直接带来几个很重要的工程结论:

1. 不要把关键信息埋在正文深处

特别是当文档很长、问题又高度依赖某一条事实时,更应该:

  • 提前做摘要
  • 把关键结论前置
  • 或直接做检索

2. 长文分析优先考虑“结构化输入”

比起原样塞入全文,更好的方式通常是:

  • 标题 + 摘要
  • 分段切块
  • 章节索引
  • 关键片段前置

3. 超长窗口不是 RAG 的替代品

长窗口解决的是“可见范围”;RAG 解决的是“把最相关信息拿出来”。
两者不是二选一,反而经常是互补关系。


2. 对话记忆(Conversation Memory):模型为什么会“忘事”

一句话:Chat 应用中的对话记忆,通常不是模型自己保留的,而是应用层把历史消息重新拼回了 context。所谓“记住上一轮”,本质上是“上一轮又被发了一遍”。


模型并不会自动记住上一轮

这是做聊天应用时最重要、也最容易被误解的一件事。

很多用户会自然地觉得:
“我刚刚已经和你说过了,所以你应该记得。”

但从模型机制上看,并不是这样。

每一次 API 请求,模型只看得到你这次发给它的内容。
如果你的应用没有把上一轮对话再附带进来,那么对于模型来说,那段历史根本不存在。

所以典型 chat 请求的本质是:

System Prompt
+ 历史消息列表
+ 当前用户输入
= 本轮模型可见上下文

也就是说:

多轮对话的“记忆”,默认是应用层模拟出来的,不是模型天然自带的。


最朴素的做法:全量保留历史

最简单的实现方式是:

  • 第 1 轮:发 system + 用户问题
  • 第 2 轮:发 system + 第 1 轮问答 + 新问题
  • 第 3 轮:继续把全部历史带上

短对话时,这种方式非常有效。
因为模型确实能看到完整上下文,所以会显得“记忆很好”。

但它的问题也很明显:

  • token 成本会持续上涨
  • 上下文越来越长
  • 旧信息会逐渐挤压新问题的预算
  • 长对话里模型对早期信息的利用会越来越不稳定

所以,真实系统很少无限制全量保留。


滑动窗口(Sliding Window)

最常见的折中策略是:只保留最近 N 轮对话

例如:

System Prompt
+ 最近 8 轮历史
+ 当前用户输入

这样做的好处是:

优点说明
简单容易实现,几乎不需要额外逻辑
成本可控token 使用量不会无限增长
对近期问题效果好最近几轮通常最相关

但缺点也非常明确:

缺点说明
早期信息会消失轮数一长,模型就“失忆”
跨主题长链条容易断用户几轮前提过的约束可能被丢掉
不擅长长期协作长会话、项目型对话常常不够用

所以滑动窗口适合:

  • 客服问答
  • 轻量聊天
  • 短周期任务

但不适合独自承担“长期记忆”。


摘要(Summarization):把旧对话压缩成可带走的信息

当对话逐渐变长时,一个很自然的思路是:

既然不能一直保留全文,那就把早期对话压缩成摘要。

典型形式是:

[摘要] 用户是产品经理,正在讨论电商 App 登录流程优化。
[摘要] 已决定采用短信验证码登录,不做邮箱注册。
[最近 6 轮对话]
User: ...
Assistant: ...

这样做的优点很大:

  • 早期关键信息可以继续保留
  • 成本显著低于全量历史
  • 可以把“散乱对话”变成“结构化状态”

但摘要也有明显风险:

1. 摘错了就等于记错了

如果摘要把一个限制条件概括错了,后面整个会话都可能沿着错误方向走。

2. 摘要会丢细节

摘要本质上是压缩,压缩就一定有信息损失。

3. 摘要可能被“摘要的摘要”稀释

如果会话非常长,多次压缩之后,重要细节可能越来越模糊。

所以更好的做法通常不是“只做自由摘要”,而是做结构化摘要


比自由摘要更稳的方法:结构化记忆

很多生产系统会把历史记忆拆成几类,而不是只留一段自然语言总结。

例如:

  • 用户偏好:喜欢简洁回答、用中文
  • 事实信息:用户是产品经理、项目是电商 App
  • 决策记录:确定先做短信登录,不做邮箱注册
  • 待办事项:下次需要输出原型流程图
  • 未解决问题:异常登录状态如何处理

这样做的好处是:

  • 更不容易丢掉关键状态
  • 更利于后续检索和更新
  • 更容易控制哪些内容该长期保留,哪些该过期

换句话说:

真正实用的对话记忆,常常不是“保存原话”,而是“保存状态”。


3. Prompt Memory:System Prompt 与 Few-shot

一句话:System prompt 是每轮都带上的持久设定,Few-shot 是当前任务的工作示范。它们都是最直接、最便宜、最常用的“记忆注入”方式。


System Prompt:最稳定的“短期持久记忆”

在一个 chat 系统里,system prompt 通常用来定义:

  • 角色身份
  • 输出语言
  • 风格要求
  • 行为边界
  • 规则约束
  • 高层背景知识

例如:

  • 你是一个代码助手
  • 默认用中文回答
  • 不要泄露内部实现细节
  • 输出先给结论,再解释
  • 如果不确定,要明确说不确定

它之所以像“持久记忆”,是因为:

每次请求都会把它带上。

所以在当前会话里,它看起来就像模型“始终记得”。

但要注意,这种“记得”是重复注入,不是模型自己长期保存了它。


System Prompt 的强项和局限

强项

  • 成本低
  • 实现简单
  • 适合放角色、规则、格式要求
  • 一致性强

局限

  • 每次都要重复占 token
  • 太长会压缩用户可用空间
  • 不适合放海量知识
  • 如果写得模糊,模型未必稳定遵守

所以一个常见误区是:

把 system prompt 写得极长,希望它同时承担角色、规则、知识库、记忆、流程图、FAQ。

这通常不是最优方案。
因为越长的 system prompt,越可能:

  • 成本高
  • 规则冲突
  • 注意力稀释
  • 把真正需要动态变化的信息埋掉

更好的原则是:

System prompt 适合放“稳定、不常变、必须始终生效”的东西。


Few-shot:不是知识库,而是“工作示范”

Few-shot 示例的作用,通常不是给模型补知识,而是告诉模型:

  • 当前任务该怎么做
  • 输出长什么样
  • 应该学什么风格
  • 哪些边界要遵守

比如:

用户: 把“你好”翻译成英文
助手: Hello

用户: 把“谢谢”翻译成英文
助手: Thank you

这类示例会强烈暗示模型:

  • 这是个翻译任务
  • 输出应简洁
  • 不要多余解释

所以 Few-shot 更像一种工作记忆
它告诉模型:“你现在应该按照这几条示范来办事。”


Few-shot 的正确用法

Few-shot 最适合用于:

  • 固定输出格式
  • 风格模仿
  • 信息抽取
  • 分类标签
  • 结构化输出
  • 工具调用模板

但不适合承担:

  • 海量知识注入
  • 长期对话记忆
  • 超长规则文档
  • 大规模事实库存储

一句话:

Few-shot 教的是做法,不是存储。


4. 外部记忆(External Memory):RAG 为什么重要

一句话:RAG 不是让模型“学会了新知识”,而是把外部知识在回答时动态取回来,再注入当前上下文。


为什么光靠模型参数不够

哪怕模型已经很强,它也仍然有天然限制:

模型局限表现
训练截止日期不知道训练之后发生的事
私有知识缺失不知道你的企业文档、产品规则、内部流程
事实稳定性有限会幻觉、会混淆相似概念
长文不易直接利用全文塞入上下文成本高且效果不稳

这时,RAG 的价值就出来了。


RAG 的本质:按需取回,再喂给模型

RAG(Retrieval-Augmented Generation)的核心思路不是“让模型记住更多”,而是:

在回答问题之前,先去外部知识库里找相关材料,再把这些材料作为当前上下文的一部分送进模型。

典型流程:

1. 用户提问
2. 将问题转为检索查询(通常是 embedding,也可能是关键词 / hybrid)
3. 在知识库中找最相关的若干片段
4. 将这些片段拼进 prompt
5. 模型基于“检索内容 + 用户问题”生成回答

所以 RAG 更像:

  • 给模型配了一个外接资料柜
  • 而不是给模型做了永久脑内升级

RAG 为什么不是“万灵药”

很多人第一次接触 RAG,会有一种过高期待:

只要接了向量库,幻觉就没了。

其实不是。

RAG 可以显著改善知识新鲜度和依据性,但它仍然会失败,原因包括:

1. 没检到

相关片段根本没被召回。

2. 检错了

召回的是表面相似但实际无关的内容。

3. 检到了,但上下文污染

Top-K 太大,无关内容把真正关键内容淹没了。

4. 模型读到了,但没正确使用

尤其在长上下文、长答案或复杂约束场景里,这很常见。

所以更准确的理解是:

RAG 提高了“有据可依”的概率,但不自动保证“回答一定正确”。


RAG 最适合解决什么问题

RAG 特别适合以下场景:

1. 私有知识

如企业知识库、产品文档、内部流程、客服 FAQ。

2. 实时更新知识

如最新政策、最新商品信息、最新公告。

3. 长文档问答

不是把整本手册全塞进去,而是只提取最相关的片段。

4. 可引用回答

让模型更容易基于文档给出“带出处”的回答。


向量库不是重点,检索策略才是重点

很多初学者会花大量时间比较 Pinecone、Milvus、Weaviate、FAISS、pgvector。
这些当然重要,但真正决定效果的通常不是“库的名字”,而是:

  • 文档怎么切 chunk
  • chunk 多长
  • 是否做 overlap
  • 检索用向量、关键词还是混合
  • Top-K 取多少
  • rerank 要不要做
  • 拼接顺序怎么安排
  • 是否做 query rewrite

也就是说:

RAG 的难点通常不在“存”,而在“取对”和“喂对”。


5. 记忆层级总览:从“本轮可见”到“长期固化”

一句话:LLM 应用中的记忆,不是一种机制,而是一组从临时到持久、从便宜到昂贵的分层方案。


四种主要记忆方式

层级手段持久度成本适用场景
PromptSystem + Few-shot单次请求角色、规则、格式
Context对话历史 + 摘要会话级多轮对话、协作任务
ExternalRAG / DB / Vector Store跨会话可持续私有知识、可更新知识
Fine-tuning微调模型参数持久风格固化、领域适配、高频行为

这几种方式的核心区别可以一句话概括:

  • Prompt:告诉模型“这轮该怎么做”
  • Context:告诉模型“我们刚刚在聊什么”
  • External Memory:告诉模型“你需要去哪里查”
  • Fine-tuning:把某种行为或风格变成模型更稳定的习惯

一个非常重要的边界:微调不是知识库替代品

很多团队会问:

我是不是把产品知识微调进模型,就不需要 RAG 了?

通常不建议这么做。

因为微调更适合固化:

  • 说话风格
  • 输出格式
  • 行业术语
  • 特定任务偏好

而不适合承担:

  • 高频更新知识
  • 大量可变事实
  • 需要可追溯引用的资料

所以一个很实用的原则是:

知识放外部,行为放模型里。


6. 生产环境里的记忆管理策略

一句话:真正好用的 LLM 应用,通常不是靠一种记忆机制,而是靠多层记忆组合起来。


常见场景怎么配

场景推荐方案
客服机器人System prompt(角色+边界)+ 滑动窗口(最近对话)+ RAG(知识库)
代码助手System prompt(规范)+ 当前文件 / 相关文件 context + 必要时检索仓库文档
个人助理用户偏好记忆 + 摘要式历史 + 日程 / 文档外部检索
文档问答RAG 为主 + 少量 Few-shot 控制回答格式
长会话协作结构化摘要 + 最近窗口 + 关键决策状态存储
Agent / 工作流系统System 规则 + 工具结果缓存 + 状态数据库

一个实用的记忆组合模板

很多应用其实可以用下面这个组合起步:

System Prompt(角色 / 风格 / 边界)
+ Structured Memory(用户偏好 / 已知事实 / 已做决策)
+ Recent Conversation(最近几轮)
+ Retrieved Knowledge(当前问题相关资料)
+ Current User Input

它的好处是:

  • 角色和规则稳定
  • 旧信息被压缩成状态
  • 最近对话保留局部连贯性
  • 外部知识按需取用
  • 不会把所有责任都压给一个超长窗口

这通常比“把所有历史和所有文档一起塞进去”更稳。


最常见的坑

1. 窗口塞满

历史太长、system 太长、检索太多,导致新问题被挤到边缘,模型反而抓不住当前任务重点。

2. 检索噪声过大

Top-K 设太大,召回片段太杂,模型被无关内容干扰。

3. 摘要写得像废话

如果摘要只有“用户在讨论一个项目”,这种泛泛总结几乎没有价值。

4. 把 RAG 当成知识正确性的保证

检索只是提供证据候选,不代表模型一定会正确理解和使用。

5. 把长上下文当长期记忆

1M context 不是“永久记住”,只是“本轮可见范围更大”。

6. 不做状态分层

把用户偏好、历史原话、外部知识、当前任务全混在一起,后期很难扩展和维护。


一个最好记住的原则

如果你只想带走一句工程原则,那就是:

能用 prompt 解决的,不要先上微调;能用检索解决的,不要硬塞超长上下文;能用结构化状态保存的,不要只靠原始聊天记录。

换句话说:

  • Prompt 负责规则
  • Context 负责连续性
  • RAG 负责知识
  • Memory State 负责长期状态
  • Fine-tuning 负责固化行为

把这几层分清楚,你设计出来的系统才会稳定。


核心概念速查

概念一句话
Context Window一次推理时模型能看到的 token 上限
Token Budget本轮请求里 system、历史、检索、用户输入共同竞争的总预算
RoPE将位置信息融入 attention 的位置编码方法,适合长上下文扩展
Lost in the Middle长上下文里,中间位置的信息往往更难被模型稳定利用
Sliding Window只保留最近 N 轮历史的对话记忆策略
Summarization把旧对话压缩成摘要或结构化状态以节省 token
System Prompt每轮都带上的角色、规则和高层约束
Few-shot用示例告诉模型当前任务该怎么做
RAG检索外部知识并注入当前上下文的生成方案
External Memory存在模型外部、按需取回的知识或状态
Fine-tuning通过训练改变模型参数,使某些行为更稳定

下一篇

理解了“记忆”以后,下一步就是:如何主动利用这些记忆,让模型更稳定地按你的方式工作。

这就进入 Prompt 工程的世界:

  • 角色设定为什么有效
  • 指令为什么有时会失效
  • Few-shot 为什么能改变输出格式
  • 为什么同一句需求,换个写法效果差很多
  • Chain-of-Thought、结构化提示、约束提示分别适合什么场景