首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Agent 记忆系统设计:上下文窗口装不下的长期项目,什么该记住、什么该忘掉

Agent 记忆系统设计:上下文窗口装不下的长期项目,什么该记住、什么该忘掉

原创
作者头像
用户11136834
发布于 2026-10-15 19:26:22
发布于 2026-10-15 19:26:22
180
举报

前言

上一篇聊部署形态,结尾留了个钩子:会话跨天之后,记忆怎么设计。这篇把它补上。

先摆一个做 Agent 绕不开的场景:项目持续三周,用户周一说"数据库用 PostgreSQL,别用 ORM",周三的会话里 Agent 又推荐了 Prisma。它不是故意忘的——上下文窗口只保留当前对话,前几天的会话早就被截断了。用户视角这叫"失忆",工程视角这是没有记忆层。

先把两个概念切开:

  • 上下文(context):这次对话的"内存",模型推理时真正能看到的全部内容,会话结束(或截断)就没了;
  • 记忆(memory):跨会话的"磁盘",昨天存的东西今天还在,需要主动决定写什么、读什么、什么时候删。

记忆系统设计的全部问题,就是往这块磁盘写什么、怎么组织、什么时候读、什么时候扔。这篇文章讲我落地的分层方案:从最简的 Markdown 文件,到检索式记忆库,再到最难的部分——写入策略。代码是 TypeScript。

一、先分清三种"记住"

不是所有"记住"都是同一种东西,混着设计必然做砸:

  1. 会话内记忆。当前对话的上下文,模型天然"记得"。要处理的只是装不下怎么办:截断、压缩、摘要——这是上下文管理,不是记忆系统,这篇不展开;
  2. 工作记忆(项目状态)。跨会话但要持续更新的内容:当前进行到哪、做过什么决策、下一步是什么。特征是"每条记忆都有半衰期",上周的状态这周可能就作废;
  3. 长期记忆(稳定事实)。用户的偏好、项目的技术栈约束、踩过的坑。特征是低频写、高频读,写下去基本不改。

三种"记住"对应三种完全不同的存储和淘汰策略。工作记忆适合"一个文件整体覆盖更新",长期记忆适合"逐条积累 + 检索"。最常见的失败是把工作记忆塞进长期记忆库——三个月后检索回来一堆过期进度,比没有记忆更糟。

二、最简可用方案:一个 Markdown 文件,可能就够了

动手上向量库之前,先看看需求的真实规模。单项目、单用户的 Agent,有价值的跨会话记忆通常不超过十几条:技术栈约束、几个关键决策、当前进度、几条教训。这个量级,一个结构化的 Markdown 文件就是最优解:

代码语言:markdown
复制
# 项目记忆

## 技术约束
- 数据库:PostgreSQL 16,禁用 ORM,统一用 Kysely
- 部署:Docker Compose,单机,无 K8s

## 关键决策
- [10-12] 认证选了 session 而不是 JWT:理由是同域部署,不需要跨服务鉴权
- [10-14] 放弃微服务拆分:三人团队,拆了没法维护

## 当前进度
- 支付回调联调中,沙箱环境返回签名偶尔校验失败(待排查)

## 教训
- 直接改 DB 数据不写迁移脚本,下次部署会丢——已经踩过一次

使用方式同样简单:会话开始时整个注入 system prompt,会话结束(或每完成一个里程碑)时让 Agent 输出更新后的版本整体覆盖:

代码语言:typescript
复制
async function loadMemory(): Promise<string> {
  try {
    return await fs.readFile(MEMORY_PATH, "utf8");
  } catch {
    return "(暂无项目记忆)";
  }
}

async function saveMemory(content: string): Promise<void> {
  // 整体覆盖,不 append——过期状态靠这次写入自然消失
  await fs.writeFile(MEMORY_PATH, content, "utf8");
}

整体覆盖而不是追加,是关键设计:过期信息的退出机制靠"重写"实现,不需要任何删除逻辑。Agent 每次更新时自然会把"支付回调联调中"替换成新状态,旧的进度不会堆积。

这个方案的边界也很清楚:记忆条目多到全量注入会挤占上下文(经验值大约超过 4-8KB),或者多项目共享同一个记忆库需要隔离,就到了换方案的临界点。

三、检索式记忆:什么时候该上向量库

当记忆条目超出"全量注入"的规模——比如 Agent 服务多个项目、或者要积累用户在长期使用中的碎片事实——就该按需检索了。套路和 RAG 一样:写入时嵌入,查询时 top-k:

代码语言:typescript
复制
interface MemoryEntry {
  id: string;
  content: string;
  embedding: number[];
  createdAt: number;
  kind: "fact" | "decision" | "lesson" | "preference";
}

async function recall(query: string, topK = 5): Promise<MemoryEntry[]> {
  const [qv] = await embed([query]);
  const all = await store.all();
  return all
    .map(e => ({ e, score: cosine(qv, e.embedding) }))
    .sort((a, b) => b.score - a.score)
    .slice(0, topK)
    .map(x => x.e);
}

但检索式记忆有三个 RAG 场景没有的坑:

  • 最相似的往往不是最该用的。用户问"数据库怎么连接",检索召回三条连接相关的旧记忆,但上周刚做过"连接池从 10 调到 50"的决策可能排在第五。所以排序要叠加时间权重,同分近邻里新的优先,甚至"最近 N 天内的事实直接保送";
  • 碎片事实会互相矛盾。记忆库里同时存在"用户在用 Vite"(三个月前)和"用户迁到 webpack 了"(上周),两条都高度相似。不处理冲突,检索等于随机翻旧账;
  • 每轮都检索是浪费。很多问题根本不涉及历史记忆。先用轻量判断(这轮对话是否涉及用户偏好/历史决策)决定要不要查,而不是无脑每轮 top-5 注入——那是在用上下文预算给记忆库交税。

四、写入策略:比读取难一个量级

读取顶多是排序问题,写入是"什么值得记住"的判断问题,这才是记忆系统的成败点。

按价值分三档:

  • 必记:用户显式要求记住的("记住我用 pnpm");纠正性反馈("不要用 var,项目统一 const")——这类不记,下个会话必然重犯;
  • 值得记:稳定的约束和事实(技术选型及理由、部署环境);踩坑教训(成因和规避方法都成立的);
  • 别记:过程性内容(尝试了什么失败了什么)、一次性任务细节、可以被代码和文档直接查到的信息。记忆库里最没价值的就是"我们讨论过 X"这种会议纪要式条目。

两个落地规则:

其一,冲突用覆盖不用追加。 新事实和旧事实矛盾时,旧的那条必须删除或标记作废,而不是两条并存。实现上给写入加一步"相似检索 + 冲突消解":新条目写入前先查 top-3,语义冲突(同一主体、相反断言)就替换。用户搬家了,记忆库里只能有一个地址。

其二,遗忘要有机制,不能靠自觉。 最简单的两种:createdAt 超过 TTL 且未被检索命中的条目定期清理;每过一段时间(或条目超阈值时)跑一次"蒸馏"——让模型把几十条碎片合并成几条结构化事实,比如二十条关于部署的零散记录合并成"部署环境"一节。蒸馏和第二节的整体覆盖是同一件事在检索式存储上的翻版:给记忆一个收敛机制,否则它只会单调膨胀。

五、务实的分层:三种存储各管一段

把上面的方案叠成分层结构,按记忆的性质路由:

层

存储

内容

读法

写法

L1

会话上下文

当前对话

自动

自��

L2

项目记忆文件

约束/决策/进度/教训

每次会话全量注入

里程碑时整体覆盖

L3

向量记忆库

碎片事实/偏好/长尾细节

相关时 top-k 检索

逐条写入+冲突消解

路由规则一句话:结构化的、每次都该看的知识进 L2;零散的、按需才相关的进 L3。团队共享的项目约束写成文档和 L2 注入,用户个人的使用偏好进 L3 检索——混在一起,两边都做不好。

一个容易忽略的点:给用户看记忆、让用户改记忆。记忆错了(过期的偏好、错误的教训)比没有记忆更伤,因为它会固执地影响每次会话。一个 /memory 命令让用户查看和编辑当前记忆,是信任问题,也是调试问题——记忆系统出问题时,你总得有个地方看它到底记了什么。

写在最后

清单版总结:

  • 先分清会话内/工作记忆/长期记忆,三者存储和淘汰策略完全不同;
  • 单项目先上 Markdown 文件整体覆盖,超出 4-8KB 再考虑向量库——不要一上来就 RAG;
  • 检索式记忆叠加时间权重,同分近邻新的优先;每轮检索前先判断"这轮需不需要记忆";
  • 写入按三档过滤:显式指令和纠正反馈必记,过程性内容别记;冲突覆盖不追加;遗忘靠 TTL + 定期蒸馏;
  • L2 进度状态、L3 碎片偏好,分层路由;给用户一个查看和编辑记忆的入口。

记忆系统做好之后,你会发现下一个瓶颈是上下文本身:长会话里早期内容被截断,Agent 行为就开始"变形"——下一篇文章可能聊聊会话内的上下文管理:压缩时机、摘要策略,以及什么内容永远不该被压缩掉。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 前言
  • 一、先分清三种"记住"
  • 二、最简可用方案:一个 Markdown 文件,可能就够了
  • 三、检索式记忆:什么时候该上向量库
  • 四、写入策略:比读取难一个量级
  • 五、务实的分层:三种存储各管一段
  • 写在最后
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档