上下文窗口与 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 预算怎么分配。
窗口内到底发生了什么
很多人会把上下文窗口理解成一个“文本缓存区”,好像只要信息被塞进去,模型就能稳定记住。
这个理解不够准确。
真正发生的是:
- 文本被切成 token
- token 被映射成向量
- 模型在多层 Transformer 中不断做 attention
- 当前生成位置会综合上下文中的相关信息
- 再输出下一个 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 应用中的记忆,不是一种机制,而是一组从临时到持久、从便宜到昂贵的分层方案。
四种主要记忆方式
| 层级 | 手段 | 持久度 | 成本 | 适用场景 |
|---|---|---|---|---|
| Prompt | System + Few-shot | 单次请求 | 低 | 角色、规则、格式 |
| Context | 对话历史 + 摘要 | 会话级 | 中 | 多轮对话、协作任务 |
| External | RAG / 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、结构化提示、约束提示分别适合什么场景