Skip to main content

Agent 的上下文与记忆管理

·4842 words·10 mins·
Table of Contents

如果文章中有不准确的地方,欢迎留言指正。

1 引言
#

大模型本身是无状态的。

一次请求进来,模型看到的是这次请求里的 system prompt、用户消息、工具返回、历史消息和外部检索结果。

请求结束后,模型不会真的把刚才发生的事存进自己脑子里。下一次请求对它来说仍然是新的。

所以 Agent 的上下文与记忆管理,本质上是怎么在有限上下文里,放入当前任务最需要的信息

2 Context、State、Memory 介绍
#

概念生命周期存储位置注入方式例子
Context Window单次模型调用请求体里注入 promptsystem prompt、最近几轮消息、工具结果、RAG 片段
State当前任务/线程运行时对象、checkpoint、数据库由编排层读写,必要时转成上下文当前步骤、待办列表、工具调用结果、审批状态
Memory跨请求/会话数据库、向量库、图数据库、文件检索或规则命中后按需注入用户偏好、项目约定、历史决策、稳定事实

简单说:

  • Context 是模型这次能看到什么
  • State 是系统这次任务跑到哪里
  • Memory 是系统以后还要不要记住什么

3 Context Window 主要组成部分
#

一次 Agent 调用里,通常包含下面这些:

  • System Prompt:角色、约束、安全规则、输出格式
  • 当前用户输入:这一轮真正的问题
  • 工具 schema:工具名称、参数、描述、返回结构
  • 历史消息:用户和模型的对话记录
  • RAG 结果:从文档、数据库或搜索系统召回的片段
  • 工具返回:文件内容、命令输出、网页内容、JSON 结果
  • 输出预算:还要预留给模型生成结果

这里最容易失控的是工具返回。

用户问一句“帮我看下这个失败原因”,工具可能返回 50KB 的日志。 用户让 Agent 搜索项目,工具可能返回几百个文件路径。 用户让它读一个接口响应,工具可能把整段 JSON 原样塞回来。

如果这些内容不处理,Context Window 很快就会被工具结果占满。

所以实际工程里,工具返回最好先过一层压缩:

  1. 截断:只保留前后若干行,中间用 marker 表示省略
  2. 过滤:只保留和 query、错误关键字、目标文件相关的行
  3. 摘要:把长日志、长网页、长表格压成结构化摘要
  4. 引用外置:大对象不要直接进 prompt,只放 file path、URL、row id、chunk id
  5. 分批读取:先拿目录、标题、schema,再按需读取细节

4 Memory 分层(短期/中期/长期)
#

Memory 通常按生命周期分层,短期、中期、长期,应对不同的使用场景。

层级存什么典型做法适合场景
短期记忆最近 N 轮消息、当前任务状态sliding window、任务 state、临时工作区多轮问答、一次性工具调用
中期记忆当前会话摘要、阶段性结论conversation summary、task summary、checkpoint长任务、代码修改、研究任务
长期记忆跨会话仍然稳定的信息抽取式 memory、用户画像、项目约定、历史决策个人助理、coding agent、客服系统

例如客服多轮对话里,短期记忆通常够用:

  • 用户身份
  • 当前订单号
  • 最近几轮沟通
  • 已经确认过的问题

coding agent 长任务更依赖中期记忆:

  • 已读过哪些文件
  • 当前假设是什么
  • 哪些测试失败过
  • 哪些方案已经排除

个人助理跨天使用时,长期记忆就更重要:

  • 用户喜欢什么表达风格
  • 哪些事情已经安排过
  • 哪些偏好长期稳定
  • 哪些信息已经过期

5 长期 Memory 的难点
#

长期 Memory 看起来像 RAG:把内容存起来,query 来了检索一下。 但真正难的是写入侧,不是读取侧。

5.1 抽取
#

对话原文不能直接当长期记忆。

长期记忆应该先做抽取,把值得保留的信息变成结构化条目。 常见类型可以先分成三类:

  • 事实:用户住在上海、订单服务使用 PostgreSQL、测试环境没有接真实支付网关
  • 偏好:用户喜欢中文回复、代码注释只写必要原因、接口示例要完整
  • 决策:订单状态流转逻辑放在 domain 层,接口层只做参数校验和结果转换

抽取时最好有 schema,而不是让模型自由发挥。

一个 memory schema 可以包含这些字段:

字段含义
owner_type这条 memory 属于谁,例如 usertenantagentproject
owner_id具体 owner 的 ID,用来做隔离,避免把 A 用户的记忆召回给 B 用户
memory_typememory 类型,常见有 factpreferencedecisioninstruction
subject这条 memory 作用在哪个主题上
predicate关系谓词,类似三元组里的关系;prefers 表示“偏好”
object真正要记住的内容
scope检索范围或使用场景,后续可以用它过滤无关 memory
confidence抽取置信度,表示系统有多确定这条 memory 是正确抽出来的
source_message_id来源消息 ID,方便以后回溯和审计
created_at创建时间,用于排序、时间衰减和冲突判断
ttl过期时间或存活时间,null 表示暂时不过期

有了 schema,后面才能做去重、冲突、检索和审计。 否则 memory store 会变成一堆自然语言碎片,很难维护。

5.2 更新
#

新的 memory 来了,要先和旧 memory 比一遍。

常见处理方式有三种:

  • skip:旧 memory 已经表达了同一件事,新信息没有增量
  • merge:新旧信息互补,可以合并成一条更完整的 memory
  • update:新信息和旧信息冲突,需要让新版本覆盖旧版本,或者把旧版本标记为过期

例如旧 memory 是:

用户不喜欢逐行解释代码的注释。  

新 memory 是:

用户希望注释只写业务规则、边界条件和不直观的取舍。  

这两条不应该并排长期存在。 更好的做法是合并成:

用户写代码注释时偏好解释业务规则、边界条件和设计取舍,避免复述代码本身。  

如果旧 memory 是“用户偏好英文回复”,新 memory 是“用户要求以后都用中文回复”,这就是冲突。 系统不能简单 top-k 召回两条都塞给模型,否则模型会在上下文里看到互相矛盾的指令。

5.3 遗忘
#

Memory 需要遗忘机制。

遗忘不一定是物理删除,也可以是降低权重、标记过期、进入人工 review 队列。

常见做法包括:

  • TTL:临时偏好、活动安排、短期项目事实自动过期
  • access 衰减:长期没被命中的 memory 降低召回权重
  • 冲突淘汰:被新事实覆盖的旧事实不再默认注入
  • 人工 review:高风险或高价值 memory 需要用户确认

没有过期机制的长期记忆,最后会变成“历史聊天垃圾场”。 模型不是记得更多,而是每次都在旧事实里猜哪个还有效。

5.4 检索
#

长期 memory 的检索通常不能只靠 embedding 相似度。

更稳的召回可以组合这些信号:

  • 语义相似度:query 和 memory 内容是否相关
  • metadata filter:owner、tenant、project、scope、memory_type
  • 时间衰减:新的事实通常比旧的事实更可信
  • access 频次:经常被使用的偏好可以适当加权
  • 来源可信度:用户明确说过的,比模型自己总结的更可信

最终注入 prompt 的不是 memory store 里的全部内容,而是当前任务真正需要的几条。

5.5 来源标记
#

每条 memory 都应该知道自己从哪里来。

至少要记录:

  • source_message_id
  • source_session_id
  • created_at
  • extracted_by_model
  • confidence
  • owner_id / tenant_id

这件事平时看起来繁琐,但排查问题时非常重要。

当用户问“你为什么认为我喜欢这种写法”时,系统要能回到原始对话。 当 memory 产生错误时,也要知道是抽取错了、合并错了,还是检索注入错了。

5.6 一个完整链路例子
#

原始对话:

用户:以后给代码加注释时,别把每一行在干什么复述一遍。只在业务规则、边界条件或者不直观的取舍旁边写。  

抽取后的 memory:

{
  "memory_type": "preference",
  "owner_type": "user",
  "owner_id": "u_123",
  "subject": "code_commenting",
  "predicate": "prefers",
  "object": "解释业务规则、边界条件和设计取舍,避免逐行复述代码行为。",
  "scope": "coding_session",
  "confidence": 0.91,
  "source_message_id": "msg_001",
  "created_at": "2026-05-28T10:48:06+08:00",
  "ttl": null
}

下一次用户说:

帮我重构这个订单状态判断函数,必要的话补注释。  

检索命中:

scope=coding_session
memory_type=preference
subject=code_commenting
predicate=prefers
object=解释业务规则、边界条件和设计取舍,避免逐行复述代码行为。

注入给模型的上下文不需要带原始聊天,只需要带压缩后的约束:

代码注释偏好:  
- 不要逐行复述代码做了什么  
- 只在业务规则、边界条件、不直观取舍处补注释  
- 注释应说明为什么这样写,而不是翻译代码  

这就是长期 memory 的价值:不是把历史复读一遍,而是把历史里稳定、有用的部分转成当前任务可用的上下文。

6 Agent 场景中的注意事项
#

6.1 长任务的 State 持久化
#

长任务不是只靠 memory 就能恢复。

例如 coding agent 改一批文件,中途需要用户审批。 这时系统要保存的不只是“用户偏好”,还包括:

  • 当前图执行到哪个节点
  • 已经运行过哪些工具
  • 哪些文件被修改
  • 哪个审批点在等待用户
  • 下一步应该从哪里继续

这里的关键是,State 负责“任务跑到哪里”,Memory 负责“以后应该记住什么”。 不要把 checkpoint 当长期记忆用,也不要指望长期 memory 能恢复一个复杂工作流。

6.2 Multi-Agent 透传规范
#

多 Agent 系统里,一个常见偷懒做法是:父 Agent 把自己所有 messages 传给子 Agent。

这很快会出问题:

  • 子 Agent 看到太多无关内容
  • 父 Agent 的系统约束和子 Agent 的任务边界混在一起
  • 工具结果重复传递,token 成本失控
  • 子 Agent 可能基于父 Agent 的未验证推断继续放大错误

更稳的方式是建立父子 Agent 契约:

父 Agent 传给子 Agent:  
- 任务目标  
- 输入材料引用  
- 必要约束  
- 期望输出格式  
- 不需要传全部聊天历史  

子 Agent 返回:

- 结论  
- 证据或文件引用  
- 不确定点  
- 建议下一步  

上下文传递应该传产物和契约,而不是传聊天记录。

6.3 人工介入反馈
#

人工介入(Human-in-the-loop, HITL)不只是让用户点确认或拒绝。 用户的修改本身也可能是高价值 memory。

例如:

  • 用户把“上线”改成“发布”
  • 用户拒绝了某种测试策略
  • 用户要求以后不要改主题 submodule
  • 用户指出某类摘要太长

这些反馈如果不回流,下次 Agent 还会犯同样的错。 但也不能全部自动写入长期 memory,最好根据类型进入不同层:

  • 一次性修改:留在当前 task state
  • 稳定偏好:写入长期 memory
  • 高风险规则:进入人工确认

6.4 多租户隔离
#

Memory 一旦跨会话保存,就必须认真处理隔离。

最基本的 metadata 不能少:

  • user_id
  • tenant_id
  • agent_id
  • project_id
  • permission scope

检索时先做权限过滤,再做语义召回。 不要先全库向量检索再过滤,否则一旦实现里有漏洞,就可能把别人的 memory 当成候选上下文。

7 Memory 系统评估
#

7.1 召回准确率
#

给定一个 query,系统应该召回哪些 memory?

例如:

query: 帮我重构这个订单状态判断函数,必要的话补注释  
should_recall:  
- 用户不喜欢逐行复述型注释  
- 用户希望注释解释业务规则  
- 用户希望注释覆盖边界条件和设计取舍  

这类评估可以做成业务自己的 golden set。

7.2 冲突解决正确率
#

测试新旧信息矛盾时,系统是否选对。

例如:

旧 memory: 用户偏好英文回复  
新消息: 以后都用中文回复我  
期望: 中文偏好生效,英文偏好降权或过期  

很多 memory 系统在普通召回上表现不错,但一到冲突场景就会同时召回新旧两条。

7.3 端到端任务成功率
#

最终还是要看任务有没有变好。

例如 coding agent:

  • 是否少问重复问题
  • 是否少读重复文件
  • 是否能遵守项目约定
  • 是否减少用户返工

个人助理:

  • 是否记住稳定偏好
  • 是否避免重复安排
  • 是否能正确区分临时信息和长期偏好

7.4 成本对照
#

Memory 不是免费的。

它会带来:

  • 抽取成本
  • embedding 成本
  • 存储成本
  • 检索成本
  • 维护和审计成本

所以要比较的是:

引入 memory 后节省的上下文成本 + 提升的任务成功率  
是否大于 memory 系统自己的成本  

目前没有一个所有业务都认可的 memory benchmark。 公开 benchmark 可以参考,但线上系统通常还是要自己建评测集,尤其是冲突、过期和权限隔离这些场景。

8 主流 Memory 方案
#

方案类型核心思路代表工具适合什么注意点
抽取式 memory从对话抽出结构化事实/偏好,去重后做多信号检索、按需注入Mem0快速给产品接长期记忆,记相对独立的偏好和事实别只接 SDK,owner、scope、过期、冲突还得自己设计
虚拟上下文管理OS 分页类比,区分 main context 和 external memory,Agent 自主换入换出Letta(MemGPT)长对话、长期助理,想让 Agent 自己管理记忆自主性越强,越需要约束和可观测性
时序 / 图记忆把实体、关系、时间变化放进一张随时间演进的图Zep / Graphiti多人多实体、关系复杂、需要多跳推理或时间线图要做实体消歧和关系更新,简单偏好不值得上图
反思式 memory把成功/失败经验蒸馏成可复用的教训,下次少踩坑ReMe编程 Agent、任务型 Agent、反复试错反思质量不稳,最好由测试或反馈信号触发
文件式记忆记忆存成结构化文件,让 Agent 直接读文件,而不是向量召回memU长期常驻 Agent,想省 token、记忆可读可审计文件组织和更新要自己管,规模大时检索要分层

选择时可以按下面的路径判断:

flowchart TD
Q["主要问题是什么?"]
Q --> A["只是单次会话不连贯"]
Q --> B["跨会话要记住偏好和事实"]
Q --> C["实体关系复杂,需要多跳或时间线"]
Q --> D["Agent 需要从失败中学习"]
Q --> E["常驻 Agent,想省 token 且记忆可读"]
Q --> F["长任务要暂停、恢复、审批"]

    A --> A1["短期窗口 + 会话摘要"]
    B --> B1["抽取式 memory,例如 Mem0 或自建 schema"]
    C --> C1["时序 / 图记忆,例如 Zep / Graphiti 或自建图谱"]
    D --> D1["反思式 memory,例如 ReMe + 任务评测反馈"]
    E --> E1["文件式记忆,例如 memU"]
    F --> F1["这是 State 不是 memory,用 checkpoint,例如 LangGraph checkpointer"]

Yu Yantao
Author
Yu Yantao
Software Engineer