编码 Agent 最尴尬的体验,是它每次都像第一天上班:你昨天才解释过这个仓库的构建脚本为什么要临时翻转 skipTests,今天它又踩同一个坑。根因在于大模型本身是无状态的——它只看得到当前请求里的上下文窗口。所谓"记忆",不是模型长出了海马体,而是工程上为它搭了一套外部状态层,在每轮请求时把相关历史重新喂回去。本文讨论如何为编码 Agent 设计这套记忆系统。

为什么上下文窗口不等于记忆

很多人以为把窗口拉到 1M token 就解决了记忆问题,其实不然。原因有三:

  • 会话边界:窗口只在单次会话内有效,进程退出、/clear 或跨天重开,历史就没了。
  • 成本与延迟:每轮请求都重发全部历史,token 线性增长,长会话后期既慢又贵。
  • 信噪比:窗口里塞满几十轮琐碎对话,真正关键的约束反而被稀释,模型的注意力会被噪声拉偏。

所以记忆的本质是有选择地持久化、有选择地召回:把值得记的东西落盘,在需要时只取相关的一小部分塞回窗口。

记忆的三个层次

按生命周期和粒度,编码 Agent 的记忆大致可分三层:

  1. 工作记忆(Working):当前会话的对话历史,活在上下文窗口里。它最完整也最易失。
  2. 情景记忆(Episodic):一次具体任务的过程与结果——“上次重构鉴权模块时,发现 os.path.basename 是清洗下载文件名的关键”。通常按时间或任务 ID 归档。
  3. 语义记忆(Semantic):从多次情景里沉淀出的稳定结论——项目约定、用户偏好、架构事实。这是跨会话最值钱的部分,例如"master 是工作分支,但新功能走 feature/* 分支"。

工程上不必严格三分,但要意识到:情景需要遗忘,语义需要长存。把两者混在一个文件里,要么无限膨胀,要么把重要约定淹没。

写入:什么时候记、记什么

记忆系统最难的不是存储,而是决定写什么。两种主流策略:

  • 显式写入:模型在对话中主动调用一个 save_memory 工具,把它认为重要的事实落盘。优点是模型自己判断相关性;缺点是需要在系统提示里引导,且容易记冗余。
  • 后处理提炼:会话结束(或被压缩)时,用一次单独的"反思"调用把整段历史蒸馏成几条结构化记忆。这样写入时机集中、可控,也能去重。

提示词在这里很关键。按编码 Agent 的调参经验,不要写成过激的 “CRITICAL!你必须在每轮都保存记忆”——新一代模型对这类措辞会过度触发工具,结果是满屏噪声记忆。改成温和的条件式更稳:

1
2
3
4
当出现以下情况时,调用 save_memory:
- 用户陈述了一个会跨任务复用的偏好或约定
- 你发现了一个不显然、且未来可能再次踩到的坑
否则不要保存。

记忆条目本身建议结构化,便于后续检索与淘汰:

1
2
3
4
5
6
7
8
{
"id": "mem_0xa1",
"kind": "convention", // convention | gotcha | preference | fact
"scope": "repo:spe-arm-ai-svc",
"text": "根/子 pom 都硬编码 skipTests=true,跑单测需临时翻转",
"created_at": "2026-06-18",
"last_used_at": "2026-06-18"
}

召回:把对的那几条塞回窗口

下一轮会话开始时,不能把全部记忆一股脑倒进去——那又退化成"窗口即记忆"了。常见召回方式:

  • 索引/目录式:维护一个轻量级 MEMORY.md 索引(一行摘要 + 指向详情文件的链接),开场只注入索引,模型按需再读详情。适合条目不多、人也要看的场景。
  • 检索式(RAG):对记忆向量化,用当前任务描述做相似度检索,取 top-k 注入。适合记忆量大、需要语义匹配的场景。
  • 元数据过滤:按 scopekind 等字段精确筛选,比如"只取当前仓库的 convention"。常与上面两种叠加用。

一个实用的 manual loop 召回骨架:

1
2
3
4
5
6
def build_system_context(task: str, repo: str) -> str:
index = read("MEMORY.md") # 轻量索引,总是注入
hits = retrieve(task, scope=f"repo:{repo}", k=5) # 按需检索详情
for h in hits:
h.last_used_at = today() # 命中即续命,供淘汰参考
return render(index, hits)

遗忘:记忆系统的下半场

只写不删的记忆系统会慢慢腐烂——过期的临时结论、已被推翻的假设、重复条目,都会污染召回。需要主动维护:

  • TTL / 陈旧淘汰:情景类记忆设过期时间;last_used_at 长期不更新的条目降权或归档。
  • 去碎片(defrag):定期把膨胀的文件拆分、把重复条目合并、把已完成任务的记忆移到归档目录。这件事可以由一个专门的维护流程周期性触发。
  • 失效标记:当某条约定被新决策推翻,不是悄悄删除,而是标记状态迁移(active → archived),保留可追溯性。

安全:记忆是被持久化的、也可被注入读取

记忆层天然是持久化存储,这带来一条硬性安全约束:密钥、令牌等敏感凭据绝不能进记忆,正如它们不该进系统提示或消息历史。任何写进记忆的东西,都可能在未来某轮被重新注入窗口,从而被一次 prompt injection 诱导吐出。需要鉴权的第三方调用,应把凭据留在 orchestrator 侧(用客户端自定义工具承载),记忆和沙箱里只出现占位符。同理,写入前最好对记忆内容做一道清洗,避免把用户粘贴的密钥顺手存了下来。

小结

Agent 记忆不是给模型加内存条,而是工程上搭一套"写入—召回—遗忘"的外部状态闭环:选择性地落盘语义结论、按相关性召回一小部分、并主动淘汰陈旧内容,同时严守凭据不入库的底线——做好这三件事,编码 Agent 才真正从"每天第一天上班"变成"记得住项目脾气的老同事"。