Agent Memory 越做越乱,本质上是 Runtime 问题
很多 Agent 项目做到第二个月,会开始加 memory。
一开始理由很正当:
用户不想每次重复偏好。
长任务需要知道前面做过什么。
coding agent 应该记得项目结构。
客服 agent 应该记得用户历史。
然后大家很自然地做了一件事:把对话、文档、工具结果、用户偏好、项目摘要丢进向量库。
前几天效果不错。模型'记得'更多东西了,回答也更贴心。
再过一阵子,味道开始不对:
- 用户新要求已经变了,Agent 还按旧偏好行动。
- 一个项目里的事实被检索到另一个项目里。
- 旧的错误总结反复影响新任务。
- 用户说'忘掉这件事',系统只从 UI 隐藏,检索层还拿得到。
- 历史摘要写得很像事实,但找不到原始证据。
- Memory 里混进了模型自己的猜测,后面又被当作用户偏好。
这时候再调 embedding、调 topK、调 reranker,只能缓一缓。
根子不在那里。
Agent Memory 不是一个向量库功能。它是 Runtime 能力。
Memory 到底在改变什么
Memory 不是'多给模型一点背景'这么简单。
它会长期改变模型看到的上下文,也会改变模型的默认行为。一次错误写入可能影响后面很多 run。
比如 coding agent 记住:
这个项目不允许修改数据库 schema。
这是有用记忆。
但如果它又记住:
用户上次要求优先使用 Prisma migration。
下一次用户明确说'不要改 schema',旧记忆就可能和新指令冲突。模型不一定会自然分清'长期偏好''上一轮任务策略''当前明确约束'的优先级。
Runtime 必须帮它分清。
我会把 Memory 分成几类:
Session summary:当前会话压缩摘要。
User preference:用户长期偏好。
Project memory:某个 workspace 或 repo 的事实。
Run note:某次任务的结果和未完成事项。
Skill memory:可复用操作流程或团队约定。
External memory:来自 CRM、工单、知识库等系统的上下文。
这些东西不能混在一个桶里。
Session memory 和 Long-term memory 不是一回事
OpenAI Agents SDK 里有 sessions,用来自动维护 conversation history。LangGraph 文档里也会把短期记忆放在 thread state,把长期记忆放在跨 thread 的 store,通常还会用 namespace 区分范围。
这条边界很关键。
短期记忆解决的是'这次任务进行到哪里了'。它应该跟 run、turn、step、checkpoint 绑定。
长期记忆解决的是'以后还能不能用'。它应该有来源、作用域、TTL、审批、删除和审计。
短期记忆可以比较详细,因为它只服务当前任务。长期记忆必须克制,因为它会跨任务复用。
如果你把两者混在一起,系统迟早会出现这种问题:
用户上周临时要求'为了调试先跳过测试'
↓
Agent 记成长期偏好
↓
下周真实上线任务里继续跳过测试
这不是模型笨。是 memory 写入策略太随便。
写入比检索更危险
很多团队把精力放在检索:embedding 模型、topK、rerank、hybrid search。
这些当然重要。但我更关心写入。
谁有资格把一条信息写进 memory?
用户明确说'以后都这样'时,可以候选写入。模型从一次任务里推断出的偏好,不应该直接写。工具返回的事实可以写,但要带来源。用户审批记录应该进 audit log,不要靠自然语言摘要保存。
一个 memory entry 至少要长这样:
type MemoryEntry = {
id: string;
kind: "preference" | "project_fact" | "run_note" | "skill" | "external";
scope: {
userId?: string;
workspaceId?: string;
projectId?: string;
sessionId?: string;
};
content: string;
source: {
type: "user" | "tool" | "trace" | "manual" | "external";
ref: string;
};
status: "pending" | "approved" | "active" | "rejected" | "expired";
confidence: "high" | "medium" | "low";
createdAt: string;
expiresAt?: string;
};
source.ref 很重要。
没有来源的记忆,后面就没法解释,没法删除,没法纠错。它会变成系统里的小纸条:看起来像事实,其实没人知道是谁写的。
摘要不是事实
长会话一定要压缩。否则 token 会炸。
但滚动摘要有一个危险:它会丢细节,也可能引入偏差。模型写摘要时,常常把'我们尝试过 A,但失败了'压成'使用 A 方案'。后面再把这个摘要喂给模型,Agent 就会沿着错误方向继续走。
所以摘要要写得像工程记录,而不是散文。
Summary range: run_12 step_1-step_8
User constraints:
- Do not modify database schema.
- Preserve existing CLI flags.
Completed:
- Located runtime loop in src/runtime/loop.ts.
- Added schema validation for read_file.
Open issues:
- run_command timeout still lacks AbortSignal cleanup.
Decisions:
- Keep ToolResult.content short; move full output to artifact.
Evidence:
- trace:run_12/step_6/tool_read_file
这段不漂亮,但很好用。
Memory 里的摘要应该能回答:覆盖范围是什么,哪些是用户约束,哪些是完成事项,哪些是未完成事项,证据在哪里。
摘要不能替代原始 trace。它只能是证据索引。
检索污染比你想得更常见
Memory 检索最怕'看起来相关'。
用户问:
帮我修一下 auth middleware 的测试。
检索出来:
上次用户为了临时排查,要求跳过 auth middleware 测试。
这条记忆文本上非常相关,语义上也相关。但对当前任务可能是毒药。模型可能以为跳过测试是用户偏好。
所以 Memory 检索不能只看相似度。
还要看:
- 是否同一个用户?
- 是否同一个 workspace?
- 是否同一个项目?
- 是否仍在有效期?
- 是否和当前用户明确约束冲突?
- 是否有来源证据?
- 是否是模型推断,还是用户明确表达?
Runtime 应该在注入 memory 前做过滤和排序,而不是把 topK 直接塞进上下文。
type MemoryCandidate = {
entry: MemoryEntry;
relevance: number;
scopeMatch: number;
freshness: number;
sourceTrust: number;
conflict?: string;
};
如果发现冲突,应该优先保留当前用户指令,并把冲突写进 context report。
Memory 也要有权限和删除
OpenAI 的 ChatGPT memory 文档里,会强调用户可以查看、管理和删除已保存记忆。这个产品细节背后,其实是 Runtime 要求:长期影响模型的内容,用户应该能看见和控制。
企业 Agent 更是这样。
Memory 可能包含客户信息、内部项目、代码路径、错误日志、操作习惯、商业策略。你不能把它当普通缓存。
最小治理要有:
查看:用户能看到系统记住了什么。
来源:每条 memory 能追到来源。
删除:用户删除后,检索层也不能再返回。
作用域:不同用户、团队、workspace 隔离。
TTL:临时策略自动过期。
审批:模型建议写入时需要用户或 policy 批准。
审计:谁写入、谁修改、谁删除都有记录。
如果做不到这些,最好先不要做长期 memory。先做 session summary 和 run notes,风险小很多。
Memory 注入需要 fencing
Hermes 这类产品级 agent 项目里,memory manager 不是简单读写内容。它会处理 turn 前 prefetch、turn 后 sync、外部 provider tool schema 注入、context fencing、streaming scrubber,避免 memory context 标签或内部 system note 泄漏到用户可见输出。
这个点很实际。
Memory 注入给模型时,应该有清楚边界:
下面是可参考的长期记忆。
这些内容可能过期,不能覆盖当前用户明确要求。
如需基于其中事实行动,必须优先查证当前 workspace 或外部系统。
模型最终回答时,不应该把内部 memory 标签原样吐给用户。Trace 可以记录 memory id,用户界面可以显示'使用了哪些记忆',但内部提示不应泄漏。
这就是 context fencing。
我建议的 Memory Runtime 流程
一个稳妥的流程是:
Turn started
↓
Load session summary
↓
Retrieve scoped memory candidates
↓
Filter by user / workspace / TTL / source trust
↓
Detect conflicts with current user request
↓
Inject selected memories with fencing
↓
Run agent loop
↓
Generate memory write candidates
↓
Require approval or policy decision
↓
Write active memory with source ref
↓
Trace all read/write decisions
这看起来比'向量库 topK'麻烦。
但生产环境要的就是这层麻烦。它能防止模型把一次性任务策略写成长期偏好,也能防止跨用户、跨项目、跨 workspace 的信息串线。
最小实现可以很朴素
第一版不需要上复杂系统。
你可以先用 SQLite 或 JSONL:
type MemoryStore = {
search(query: MemoryQuery): Promise<MemoryCandidate[]>;
propose(entry: MemoryDraft): Promise<MemoryEntry>;
approve(id: string, actor: string): Promise<void>;
delete(id: string, actor: string): Promise<void>;
list(scope: MemoryScope): Promise<MemoryEntry[]>;
};
先支持:
session_summaryrun_noteuser_preferenceproject_fact
每条都带 source、scope、status、expiresAt。
检索先别追求智能。先保证不会串用户、不会串项目、不会拿 expired memory、不会把 pending memory 注入模型。
这比一个'效果很惊艳但谁也管不住'的 memory 系统更适合生产。
最后
Memory 做乱,通常不是向量库做错了。
是 Runtime 没有回答几个问题:
谁写入?
写到哪里?
作用域是什么?
有效期多久?
来源在哪?
谁批准?
如何删除?
如何进入上下文?
和当前用户要求冲突时谁优先?
出了问题怎么追?
这些问题不性感,也不适合做 demo 视频。但它们决定了 Agent Memory 会成为能力,还是成为污染源。
参考资料
- OpenAI Help: Memory FAQ
- OpenAI Agents SDK Docs: Sessions
- LangGraph Docs: Memory overview
- LangGraph Docs: Add and manage memory
- LangChain Blog: Memory for agents


