如果文章中有不准确的地方,欢迎留言指正。
1 引言#
大模型本身是无状态的。
一次请求进来,模型看到的是这次请求里的 system prompt、用户消息、工具返回、历史消息和外部检索结果。
请求结束后,模型不会真的把刚才发生的事存进自己脑子里。下一次请求对它来说仍然是新的。
所以 Agent 的上下文与记忆管理,本质上是怎么在有限上下文里,放入当前任务最需要的信息。
2 Context、State、Memory 介绍#
| 概念 | 生命周期 | 存储位置 | 注入方式 | 例子 |
|---|---|---|---|---|
| Context Window | 单次模型调用 | 请求体里 | 注入 prompt | system prompt、最近几轮消息、工具结果、RAG 片段 |
| State | 当前任务/线程 | 运行时对象、checkpoint、数据库 | 由编排层读写,必要时转成上下文 | 当前步骤、待办列表、工具调用结果、审批状态 |
| Memory | 跨请求/会话 | 数据库、向量库、图数据库、文件 | 检索或规则命中后按需注入 | 用户偏好、项目约定、历史决策、稳定事实 |
简单说:
- Context 是模型这次能看到什么
- State 是系统这次任务跑到哪里
- Memory 是系统以后还要不要记住什么
3 Context Window 主要组成部分#
一次 Agent 调用里,通常包含下面这些:
- System Prompt:角色、约束、安全规则、输出格式
- 当前用户输入:这一轮真正的问题
- 工具 schema:工具名称、参数、描述、返回结构
- 历史消息:用户和模型的对话记录
- RAG 结果:从文档、数据库或搜索系统召回的片段
- 工具返回:文件内容、命令输出、网页内容、JSON 结果
- 输出预算:还要预留给模型生成结果
这里最容易失控的是工具返回。
用户问一句“帮我看下这个失败原因”,工具可能返回 50KB 的日志。 用户让 Agent 搜索项目,工具可能返回几百个文件路径。 用户让它读一个接口响应,工具可能把整段 JSON 原样塞回来。
如果这些内容不处理,Context Window 很快就会被工具结果占满。
所以实际工程里,工具返回最好先过一层压缩:
- 截断:只保留前后若干行,中间用 marker 表示省略
- 过滤:只保留和 query、错误关键字、目标文件相关的行
- 摘要:把长日志、长网页、长表格压成结构化摘要
- 引用外置:大对象不要直接进 prompt,只放 file path、URL、row id、chunk id
- 分批读取:先拿目录、标题、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 属于谁,例如 user、tenant、agent、project |
owner_id | 具体 owner 的 ID,用来做隔离,避免把 A 用户的记忆召回给 B 用户 |
memory_type | memory 类型,常见有 fact、preference、decision、instruction |
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"]
