文档信息:本文由 AI 生成初稿;生成模型 = deepseek-chat;知识截止 = 2026-09-11; 文档抓取时间 = 2026-09-10T16:40:15.983Z;建议复核周期 = 季度。
TL;DR
- 上下文窗口(context window)是模型单次调用能“看见”的全部文本,它是有上限、按 token 计费的工作台,不是数据库。
- 短期记忆 = 当前这次任务里塞进上下文的对话与工具结果;长期记忆 = 存在外部存储、需要时再检索回来的信息。
- 判断标准只有一句:这条信息下一轮推理用得上吗?用得上就放,用不上就丢弃或外置到检索。
- 对话历史放不下时,主流做法是“保留最近若干轮 + 对更早内容做摘要 + 关键事实外置存储”,而不是硬塞。
- 记忆有成本(token 费用、延迟)也有隐私风险(写入即持久化),两者都要在架构层面显式处理。
前置知识
- 你已经知道什么是 LLM 调用:给一段文本(prompt),拿回一段文本(completion)。
- 你写过至少一次带对话历史的循环调用(把上一条回复拼回下一次请求)。
- 不需要你熟悉任何具体框架:本文不依赖 LangChain / LlamaIndex / 各家 SDK 的特定 API。
- 不需要向量数据库经验:检索部分只讲清“为什么检索”,不展开索引实现。
上一课我们拆过 Agent 的四个基本构件,这一课把其中一个最容易失控的构件——记忆——单独拎出来讲透。
上下文窗口:Agent 的工作台,不是仓库
上下文窗口(context window)指模型在一次调用中能接收和生成的 token 总量上限。它同时约束输入(prompt、历史、工具返回)和输出(回复、工具调用参数)。超出上限,请求会被拒绝或被迫截断。
理解它有三个要点:
它是有限的。 不同模型的窗口大小差异很大,从几万到上百万 token 不等。具体数值以官方文档为准,且会随版本变化——截至 2026-09-11,请以你所用模型当前文档页的说明为准。
它是按 token 计费的。 输入和输出通常分开计价,输入越长,单次调用越贵。这就是为什么“把全部历史都塞进去”在工程上不可持续:成本随轮数线性增长,而延迟也随之上升。
它是无状态的。 模型不记得上一次调用。所谓“Agent 记得刚才说过什么”,本质是你在每次请求里,把需要它知道的内容重新拼进去。记忆是应用层的责任,不是模型自带的能力。
一个最小的调用骨架长这样,注意 messages 就是你的工作台内容:
// 每轮都重新构造 messages,模型本身不保存任何状态
const messages = [
{ role: "system", content: "你是一个助手。" },
...retrievedFacts, // 外置记忆检索回来的事实
...recentTurns, // 最近的对话轮次
{ role: "user", content: userInput },
];
const res = await callModel({ model: "your-model", messages });
真正的难点不在调用,而在 retrievedFacts 和 recentTurns 怎么算出来。
短期记忆 vs 长期记忆
这两个词借自认知科学,在工程上对应两种完全不同的存储介质。
短期记忆(short-term memory) 活在上下文窗口里:当前任务的对话历史、刚拿到的工具返回、临时的中间推理。它的生命周期就是这次任务,任务结束即消失。它的优点是模型直接可见、无需检索;缺点是容量有限、每轮都要重发、成本随长度增长。
长期记忆(long-term memory) 活在上下文窗口之外:数据库、文件、向量库、键值存储。它容量几乎无上限、跨会话持久,但模型看不见它——必须先把相关内容检索出来、拼进上下文,模型才能用。
把两者对比着记:
| 维度 | 短期记忆 | 长期记忆 |
|---|---|---|
| 存储位置 | 上下文窗口内 | 外部存储 |
| 容量 | 受窗口上限约束 | 基本不受限 |
| 生命周期 | 单次任务 | 跨会话持久 |
| 模型可见性 | 直接可见 | 需检索后注入 |
| 主要成本 | token 费用、延迟 | 存储、检索质量、隐私 |
关键认知:长期记忆不是“更大的上下文”,而是“按需取用的外部事实源”。把长期记忆想象成 Agent 的便签本——平时放在抽屉里,需要哪张就抽哪张贴到工作台上,用完可以撕掉。
什么该放进上下文,什么该丢弃或检索
这是本课最实用的一节。每次构造 prompt 前,对每条候选信息问三个问题:
第一,下一轮推理真的用得上吗? 用不上就别放。用户十分钟前问的天气、已经执行完的工具的原始返回、调试用的日志,多数情况下对当前这一步没有价值。
第二,它是稳定事实还是易变状态? 稳定事实(用户偏好、项目约定、领域知识)适合外置长期存储、按需检索;易变状态(当前任务进度、本轮工具结果)适合留在上下文里。
第三,它有多长、值不值? 一段 5000 token 的文档,如果只有一句话相关,就应该只放那一句。检索的意义正是把“大块原料”压缩成“小片高价值信息”。
一个可操作的分类:
- 必须常驻上下文:系统指令、当前任务目标、最近几轮对话、正在进行的工具调用状态。
- 应当检索后注入:历史偏好、过往结论、领域文档片段、相似案例。
- 应当丢弃:已完成子任务的中间过程、重复信息、与当前目标无关的工具输出原文。
- 应当外置不注入:全量日志、原始文件、可事后查询但当前用不到的数据。
工具返回特别值得警惕:一次网页抓取可能返回几万 token 的 HTML,其中 99% 是噪声。正确做法是在工具层就做清洗和截断,只把结构化结果交回上下文,而不是把原始响应直接塞进去。
对话历史与摘要策略
对话轮数一多,历史必然撑爆窗口。三种主流策略,通常组合使用:
滑动窗口(sliding window)。 只保留最近 N 轮,更早的直接丢弃。实现最简单,缺点是会丢掉早期达成的共识,用户可能发现 Agent“忘了”之前说过的事。
摘要压缩(summarization)。 把超出窗口的早期对话交给模型压缩成一段摘要,用摘要替换原文。这样保留了要点,代价是细节丢失,且摘要本身也要花一次模型调用。实践中常见做法是:保留最近 K 轮原文 + 更早内容的滚动摘要。
外置 + 检索(externalize & retrieve)。 把整段历史写入外部存储,每轮根据当前输入检索相关片段注入。这是长期记忆的典型形态,适合跨会话场景。
一个摘要压缩的骨架,注意它本身也是一次模型调用:
// 当历史超过阈值时,把最老的一批轮次压成摘要
async function compact(history, keepRecent = 6) {
if (history.length <= keepRecent) return history;
const old = history.slice(0, -keepRecent);
const recent = history.slice(-keepRecent);
const summary = await callModel({
messages: [{ role: "user", content: "把以下对话压缩成要点:\n" + format(old) }],
});
return [{ role: "system", content: "早前对话摘要:" + summary }, ...recent];
}
选择策略时问自己:这个 Agent 的任务是短程一次性(如单次问答)还是长程多轮(如持续协作的编码助手)?前者滑动窗口就够,后者必须上摘要 + 外置检索。
记忆的成本与隐私
记忆不是免费的,它有两个隐性账单。
成本账单。 每一次把记忆注入上下文,都在为 token 付费;每一次检索,都在增加延迟;每一次摘要,都是一次额外的模型调用。记忆越多,单轮越贵越慢。所以“记得更多”不等于“更好”——过度记忆会拖垮响应速度和预算。工程上的目标是在够用的前提下记最少。
隐私账单。 这是更容易被忽视的一面。用户随口说的一句话,一旦写入长期记忆,就可能被持久化、被后续会话检索出来、被日志记录、被第三方存储服务持有。几个必须显式处理的点:
- 写入前告知:用户应知道哪些信息会被长期保存。
- 提供删除路径:记忆必须可查、可删,否则无法满足数据主体的删除请求。
- 最小化收集:不因为“以后可能有用”就全量存储。
- 隔离与加密:多用户场景下,记忆必须按用户隔离,避免跨用户泄漏。
- 注意合规:涉及个人数据的记忆处理,需对照当地隐私法规(如 GDPR 等)评估。
把记忆当成用户数据来治理,而不是当成一个技术缓存——这个心态差别决定了系统是否可长期运营。
延伸阅读
- Anthropic:Context windows:讲清上下文窗口的边界、长上下文的行为特征,以及超出窗口时该怎么处理。
- OpenAI:Prompt caching:解释为什么“重复发送相同前缀”可以被缓存降价,直接影响你把什么放进上下文的成本决策。
- Anthropic:Building effective agents:从工程视角讲 Agent 的整体设计取舍,记忆与上下文管理是其中一环。
- OWASP Top 10 for LLM Applications:列出 LLM 应用的典型风险,其中包含敏感信息泄漏与数据治理相关条目。
完整实现(向量检索、记忆写入策略、多用户隔离)请见各框架与向量数据库的官方文档;本文只给足以启动理解的骨架。