文章进阶

记忆与上下文:Agent 的便签本

从上下文窗口讲起,说清 Agent 的短期记忆与长期记忆、什么该放进上下文、什么该检索或丢弃、对话历史与摘要策略,以及记忆带来的成本与隐私问题。框架无关,代码最小化,指向官方文档。

约 6 分钟发布于 2026年9月23日校对 · shelter123更新 AI 辅助初稿 · 人工校对

更新于校对之后,尚未二次校对

本文目录

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 等)评估。

把记忆当成用户数据来治理,而不是当成一个技术缓存——这个心态差别决定了系统是否可长期运营。

拿走即用:最小记忆与上下文组装模板

下面是一个框架无关的最小骨架,用于每轮组装上下文并处理历史溢出;字段名与调用方式请以你所用模型官方文档为准(如 OpenAI Chat Completions、Anthropic Messages API)。它是结构示意,不能直接运行——callModel、format 等都是需要你自行实现的占位符。

// 每轮重新组装 messages:模型无状态,记忆由应用层负责
// 参考:Anthropic《Context windows》、OpenAI《Prompt caching》
async function buildMessages({ system, retrievedFacts, history, userInput, keepRecent = 6 }) {
  const recent = history.slice(-keepRecent);
  const older = history.slice(0, -keepRecent);

  // 历史过长时压缩为摘要(本身是一次模型调用)
  const summary = older.length
    ? await callModel({ messages: [{ role: "user", content: "压缩成要点:\n" + format(older) }] })
    : null;

  return [
    { role: "system", content: system },
    ...(summary ? [{ role: "system", content: "早前摘要:" + summary }] : []),
    ...retrievedFacts, // 外置长期记忆检索结果,按需注入
    ...recent,         // 最近若干轮原文
    { role: "user", content: userInput },
  ];
}
// 工具返回务必在工具层清洗截断,只回传结构化结果

关键点:常驻的只有系统指令、任务目标与最近轮次;稳定事实外置检索、易变状态留在上下文;写入长期记忆前先想清楚成本与隐私边界。

完整可运行版(单文件、零 npm 依赖、默认离线确定性 stub,无需 API key 与联网)见 agent.mjs 与 README.md;示例里的记忆模块实现了窗口化与丢弃轮次的压缩标记,但不含向量检索与长期记忆库。

延伸阅读

完整实现(向量检索、记忆写入策略、多用户隔离)请见各框架与向量数据库的官方文档;本文只给足以启动理解的骨架。

小测验

读完自测

读完选一选,立即看对错与解析;没有 JavaScript 时,在每题下展开「查看答案与解析」同样可读。

第 1 题关于上下文窗口(context window),下列哪种理解最符合文章观点?

查看答案与解析

正确答案:它是单次调用可见的有限工作台,记忆需要应用层每轮重新拼装

上下文窗口同时约束输入与输出,且模型无状态,所谓“记得”是应用层每轮重新拼装的结果。窗口再大也受成本与延迟约束,全量塞入不可持续。

第 2 题面对一段很长的对话历史,文章推荐的主流处理思路是什么?

查看答案与解析

正确答案:保留最近若干轮原文,对更早内容做摘要,并把关键事实外置存储按需检索

文章给出的组合策略是滑动窗口 + 摘要压缩 + 外置检索,兼顾要点保留与成本控制。全量重发或全量注入都会带来成本与噪声问题。

站内搜索

    输入关键词,快速找到文章与课程。