记忆与上下文:档案馆里那盏灯
人们谈 Agent 时常把"记忆"和"上下文"混为一谈。它们紧密相关,职责却完全不同:记忆保存系统过去知道的内容,回答"有哪些信息可用";上下文是本轮实际发给模型的内容,回答"这次让模型看到哪些"。记忆库可以存几个月的项目历史,一次模型调用却只能塞进其中很小一部分。这一层做不好,模型要么在无关历史里迷路,要么把没验证的推断当成长期事实。
TL;DR
记忆求全、可追溯;上下文求相关、紧凑、预算可控。可靠系统把"证据视图"和"模型视图"分开:完整工具结果、原始会话进证据库,本轮推理只拿到筛选过的投影。展开成五个动作:保存(带来源)、检索(混合+重排)、投影(受预算的模型视图)、预算(调用前裁剪)、整合(离线反思)。
一、记忆不是上下文
先把三条边界钉死,后面所有设计都从它们派生:
- 保存了信息,不代表模型本轮就该看到它。
- 模型本轮看到了信息,不代表系统就该永久保存它。
- 工具返回了完整证据,不代表要把原文整段塞进上下文。
由此分出两种视图,它们的服务对象根本不同:
| 视图 | 面向 | 保存内容 |
|---|---|---|
| 证据视图 | 审计、复现、人工检查 | 完整工具结果、原始会话、版本历史 |
| 模型视图 | 当前这一次模型调用 | 筛选、裁剪、排序后的相关内容 |
一个常见错误是为了省 Token 直接删掉原始证据——后续就再也无法复查"模型当时到底基于什么做的判断"。正确做法是证据完整保存,在每次调用前另外生成一份受预算约束的投影。工具的原始结果应先落进证据存储当作"候选",只有经过验证才被提交进长期记忆;这条"候选 vs 已提交"的边界,是记忆层最重要的纪律。
二、分层记忆:按生命周期切开
即使窗口很大,也不该把所有历史都发给模型:输入越长成本和延迟越高,无关信息会稀释关键约束,旧结论可能已过期,而长任务迟早超过任何固定窗口。更关键的是注意力本身在衰减——token 越多,模型的回忆精度越低,且是渐进式下降而非硬悬崖。所以更大的窗口不能替代记忆筛选,得把上下文当成有限资源来经营。
记忆按生命周期分三层:
| 层级 | 保存内容 | 生命周期 | 特点 |
|---|---|---|---|
| 工作记忆 | 本轮输入、最近消息、当前工具结果、即时状态 | 当前窗口 | 最快,容量有限 |
| 短期记忆 | 会话摘要、最近任务进度、临时决策 | 数小时到数周 | 便于恢复近期工作 |
| 长期记忆 | 用户偏好、项目规则、稳定事实、经验教训 | 跨会话长期 | 容量大,需检索 |
三层不是文件夹分类,而是三种使用承诺:工作记忆承诺"当前可见",短期记忆承诺"近期可恢复",长期记忆承诺"未来相关时可被重新找到"。
要注意:向量索引不是第四种记忆。它只是语义检索入口,指向权威原文。一旦索引和原文冲突,应相信原文并重建索引,而不是反过来。
三、写入治理:什么值得被记住
长期记忆最危险的问题不是遗忘,而是把错误内容稳定地保存下来。Agent 每轮都产出大量信息,多数没有长期价值:临时命令输出能重跑、失败尝试只与当时环境有关、模型未验证的猜测不能当事实。所以写入必须经过决策,至少权衡:用户是否明确要求记住、信息是否被验证、是否影响未来任务、是否已存在冲突条目、是否带来隐私安全风险。
最该坚持的一条,是把三类内容分开标注:
事实 —— 有直接证据(测试结果、配置值、用户明确陈述)
推断 —— 系统据证据做的判断,可能要复核
经验规则 —— 多次任务总结出的做法,适用范围有限
每条记忆都带上来源、置信度、创建时间、最后验证时间和适用范围,未来检索时才能判断它是否还可信。长期记忆最大的事故,就是把"临时推测"固化成"常识"——一旦坏地基被当作事实提交,后面许多步都建在它上面。因此推断应停留在候选态,直到有证据把它升级为事实。
自动写入越积极,污染风险越高。务实的顺序是优先保存已验证结论,而不是模型说过的每句话。
四、检索:找到相关,还要判断可信
只做向量搜索是不够的——语义相近不代表事实相关,更不代表内容仍然正确。不同检索方式各有盲区:
| 方式 | 擅长 | 局限 |
|---|---|---|
| 时间检索 | 最近发生了什么 | 最近 ≠ 相关 |
| 类型/标签 | 用户偏好、项目规则 | 依赖准确分类 |
| 关键词 | 精确的错误码、文件名 | 同义表达会漏 |
| 向量语义 | 以前有没有类似问题 | 召回语义近但事实无关的内容 |
| 结构化查询 | 某项目当前状态/某决策版本 | 需要稳定 Schema |
成熟系统用混合检索,一次实用流程大致是:
当前查询
→ 按用户/项目/权限缩小范围
→ 关键词精确召回 + 向量语义召回(并行)
→ 合并、去重
→ 按相关性 / 重要性 / 近因 / 置信度 / 时效性重排
→ 过滤过期、低置信、冲突条目 → 返回候选 + 来源
记忆检索区别于普通文档检索的地方,正是那条重排——经典做法按近因、重要性、相关性三因子加权:最近、最重要、最相关的记忆才优先进上下文。进入上下文前还要再追问几件事:这条记忆属于当前用户和项目吗?过期了或被新版本取代了吗?它是事实、推断还是经验?原始证据在哪?和别的记忆冲突吗?
最后,检索结果要可解释。当系统用一条长期记忆影响了决策,工程师要能看到它为什么被召回、来自哪、何时验证过。否则模型一次奇怪选择背后,可能藏着一条早就污染的记忆。
五、上下文组装是一份投影
上下文组装不是字符串拼接。一次模型请求里,下面这些内容在争夺同一个窗口:系统指令与安全约束、工具 Schema、当前用户输入、当前任务状态、最近历史、检索到的记忆、最近工具结果、还有为输出预留的空间。组装引擎要决定谁进、进多少、按什么顺序进——追求的不是"放进尽量多的信息",而是"放进尽量对的信息"。
把上下文切成静态和动态两部分很有用:稳定的系统指令、工具定义、用户基础档案可缓存(稳定前缀做提示缓存能把延迟降约 2 倍、成本最高降约 90%);当前任务、最近历史、检索结果每轮都变,需实时选择。
组装本身分三阶段:
①需求分析 判断本轮到底需要哪些信息源(规则/轻量分类器/模型)
②并行检索 从各记忆源并行取候选,进装配前先过权限/时效/置信度
③排序装配 按优先级填充窗口,记录每个来源占多少 token
第三阶段的优先级,一个常见次序是:
安全与系统约束 → 当前用户请求 → 当前任务状态 → 必要项目规则
→ 最近有效行动 → 相关长期记忆 → 参考资料
关键是划出保护区,不让长历史把最重要的约束挤掉:
| 区域 | 典型内容 | 处理 |
|---|---|---|
| 保护区 | 安全指令、用户当前请求、核心目标、未完成的工具调用 | 禁止普通裁剪 |
| 动态区 | 历史、项目记忆、参考资料 | 按相关性与预算填充 |
| 输出预留 | 模型回答与工具调用所需空间 | 调用前必须保留 |
最成熟的做法,是把组装结果本身也记录下来:选了哪些记忆、丢了哪些候选、为什么裁剪、每个来源占多少 token、是否命中缓存。这份 context trace 让上下文问题可被诊断——模型遗漏信息时,你才分得清是检索没找到、组装没选中,还是模型看到了却没用好。它也是后续做轨迹评估的前提。
六、预算、裁剪与摘要
预算检查必须发生在调用模型之前,而不是等 API 报上下文超限。裁剪也不能简单删最旧的消息——那可能拆散工具调用与工具结果的配对,或丢掉原始目标。可靠裁剪要保护:系统与安全约束、当前请求、原始目标与完成标准、未完成工具调用所需信息、最近有效行动与结果。最该先裁的,是冗长的工具输出、重复解释、可重新读取的材料——其中"工具结果清理"通常是最安全的轻量压缩:深层历史里的旧工具结果一般无需再看。
摘要是有损压缩:它可能混淆事实与推断、丢失错误细节、删掉后来才显重要的信息,甚至固化总结模型的误解。所以摘要要保留来源范围、生成时间和版本,允许回到原文核验。一条经验法则是"先最大化召回率,再迭代提精确率"——宁可早期多留一点,也别因过度压缩丢掉后期才显出价值的微妙上下文。
七、记忆整合:离线的反思
持续追加记忆会带来四个问题:同一事实多版本、新旧结论冲突、检索被重复内容占满、成本持续增长。整合就是把分散记录理成更稳定的长期知识,同时保留原始证据。它本质上是离线的反思——定期从记忆流里提炼高级洞察。
一个典型整合管道是"三门触发 + 四阶段":
触发(任一满足即可):
时间门:距上次整合超过一定时间
会话门:累积一定数量的新会话
显式门:用户或系统要求保存进度
│
▼
Orient 确定本次处理的主题与范围
→ Gather 从原始记录提取候选事实(保留来源)
→ Consolidate 与现有长期记忆比对,去重/更新版本/解决冲突
→ Prune 清理过期索引、合并重复、归档低价值(证据移冷存,不直接删)
│
▼
新的长期记忆视图
整合会同时改长期记忆、索引和状态计数,所以它本身也要有事务边界:先生成候选结果,验证后原子提交;失败就退回上一个已知良好版本,而不是留下"记忆改了但索引没改"的脏状态。关键记忆出现语义冲突时,应请求人工审查——合并记忆比合并代码更难,因为两句话表面不冲突、语义上却可能互相否定。
八、两种设计取向
没有唯一正确的记忆设计,常见两种取向,各有代价:
| 维度 | 主动分层整合 | 被动阈值刷写 |
|---|---|---|
| 触发方式 | 时间、会话数、事件、显式 | 上下文压力或固定阈值 |
| 信息组织 | 多类型、结构化 | 文件与日志为主 |
| 实现复杂度 | 较高 | 较低 |
| 长期项目维护 | 更适合 | 需额外约束 |
| 人工可读性 | 取决于存储形式 | 通常较好 |
| 整合成本 | 可后台主动执行 | 接近阈值时集中发生 |
通用 Harness 往往组合两者:热路径用简单可靠的会话摘要和文件记忆,后台再做更细致的分类、去重与索引维护。
九、接入运行时:四个接入点
记忆子系统设计得再好,没接进主循环,Agent 就不会真正"拥有记忆"。运行时至少在四处接入:
①模型调用前 取受预算约束的上下文投影;失败应降级为"无长期记忆"模式,而不是任务起不来
②每个有效行动后 记录工具调用/结果/验证状态/来源;原始结果进证据库(候选),模型可见部分进会话
③会话或任务结束 生成结构化短期摘要(目标/完成/未完成/决策/错误/下一步),与原始会话关联
④后台整合维护 异步更新长期记忆与索引,不阻塞用户主路径,无事务保护时不改关键记忆
"模块通过单元测试"只证明它能独立工作,不证明 Agent 拥有了记忆。只有这四个接入点都落地,记忆子系统才真正生效。
十、常见误区
- 把所有历史都叫记忆。 于是分不清哪些该长期保存、该保留多久。要按用途、生命周期、可信度分类。
- 检索到什么就全塞给模型。 上下文膨胀,相关信息反而被淹没。要经重排、预算、保护区生成模型视图。
- 把模型总结当事实。 一次误解会跨会话传播。摘要和自动提取要标来源与置信度,关键事实要验证。
- 只建向量库,不留权威原文。 无法审计和修正错误记忆。索引应指向权威,而不是取代权威。
- 只压缩,不留原始证据。 摘要漏了就再也追不回。原文可转冷存,但关键任务不该只剩摘要。
- 组件写好了,却没接进运行时。 "模块可用"和"Agent 能记住"之间,隔着运行时的四个接入点。
最小心智模型
一句话:记忆求全,上下文求精。 展开成五个动作——保存(带来源、分事实/推断/经验)、检索(关键词+向量+三因子重排)、投影(受预算的模型视图,留保护区)、预算(调用前裁剪,护住协议完整性)、整合(离线反思,带事务边界)。
衡量这一层好不好,看这几个问题有没有答案:
- 是否区分了记忆库、原始证据和模型可见上下文?
- 每条记忆是否有来源、时间、范围、置信度?是事实还是推断?
- 检索是否先按权限过滤、再混合召回、最后判可信?结果可解释吗?
- 哪些内容属于不可裁剪的保护区?是否记录了 context trace?
- 整合失败能否回滚?原始记录是否仍可追溯?
当这些都有明确答案,系统才能在跨会话的长任务里,表现出稳定的连续性——而不是靠把过去全塞给模型来假装连贯。