23128 字
116 分钟

Agent 记忆系统架构总览:Codex、Claude Code、Hermes、Mem0、Letta 与 Graphiti

本文定位为 Agent Memory Engineering 专栏总览与选型地图:用统一生命周期横向比较六种路线;每个系统的源码、状态机、一致性与生产化细节进入独立深挖文章。

顺序独立深挖核心问题
1Codex Memory:两阶段语义 ETL、Git 巩固与可追溯召回历史任务如何形成未来经验
2Claude Code Memory:CLAUDE.md、Auto Memory 与渐进式上下文披露文件层级如何编译成动态 Instruction Working Set
3Hermes Memory:受限自管理、快照隔离与事务化巩固Agent 如何在有限预算下安全地管理自己的记忆
4Mem0 Memory Pipeline:ADD-only 提取、混合检索与多存储一致性如何把记忆做成可观测的数据管线
5Letta / MemGPT Memory:虚拟上下文与 Agent-controlled PagingAgent 如何在 Hot / Cold Context 之间主动调页
6Graphiti Temporal Memory:双时间、实体消歧与事实失效动态世界中的事实何时成立、何时失效

0. 阅读说明与研究边界#

本文资料与源码基线截至 2026 年 7 月 15 日

研究对象分成两组:

产品级实践:

  • OpenAI Codex
  • Anthropic Claude Code
  • Nous Research Hermes Agent

开源记忆框架:

  • Mem0
  • Letta / MemGPT
  • Graphiti

本文不会按照“短期记忆、长期记忆、向量记忆”依次做概念科普。真正要研究的是:

  1. Agent 到底应该记什么?
  2. 谁决定一条信息值得写入记忆?
  3. 什么时候写?
  4. 写入什么结构?
  5. 记忆属于哪个用户、Agent、任务或业务实体?
  6. Agent 如何找到过去真正有用的信息?
  7. 找到记忆后,多少内容应该重新进入 Context?
  8. 新旧记忆冲突怎么办?
  9. 历史事实发生变化怎么办?
  10. 记忆满了以后,谁决定删除什么?
  11. Memory 和 Context Compaction 是不是同一件事?
  12. 一次任务执行能否转化成未来可复用的经验?

0.1 引用方法与事实边界#

本文只引用公开源码中的极少量关键签名或关键语句。较长流程统一使用 “结构化源码还原”:依据公开源码真实调用顺序整理成更易学习的伪代码,不逐字搬运原仓库。

Claude Code 的核心 Runtime 并未完整公开,因此 Claude Code 一章严格依据 Anthropic 官方公开行为契约做架构还原,不把推测伪装成源码事实。

0.2 源码版本与复现基线#

系统研究基线
Codexopenai/codex,commit 8604689ec5e3437eb79802d8d72249b7722fbf5b
HermesNousResearch/hermes-agent,commit 77d5b2d573f82fc514fbef02f0b6303c44149805
Mem0mem0ai/mem0,commit ccbe5861a138c7583e01bb3a3aa6168e52526a23
Lettaletta-ai/letta,commit b76da9092518cbaa2d09042e52fdcbde69243e18
Graphitigetzep/graphiti,commit 526dcad7a300f3c5c506ff96a68bcdc7ca9f97ed
Claude CodeAnthropic 官方 Memory 文档行为契约

本文插图均为依据上述官方文档与固定版本源码重新绘制的机制图,用于压缩跨文件调用关系与故障语义;它们不是厂商官方架构图。图中若同时出现“当前实现”和“生产化补强”,会用分区和图注明确区分,避免把本文建议误读为框架已经提供的保证。


Part I:问题模型与统一生命周期#

1. Agent Memory 真正解决什么问题#

1.1 Context Window 不等于 Memory#

大模型的 Context Window 解决的是:

当前这一次模型调用能够看到什么。

Memory 解决的是:

系统如何跨时间保存、组织、选择、修正并重新提供过去的信息。

Context Window
=
当前推理工作集
Memory System
=
跨时间的信息生命周期系统

所以:

Raw History ≠ Memory
Vector Database ≠ Memory System
Context Window ≠ Long-Term Memory

把所有历史消息放进数据库,只完成了 Persistence

把历史消息做 Embedding 放进 Vector DB,只完成了 Semantic Retrieval Primitive

真正的 Memory 至少还要回答:

哪些事件值得进入记忆?
从事件中抽取什么?
属于谁?
是事实、偏好、经验还是状态?
与旧记忆是否重复或冲突?
如何持久化?
未来什么时候召回?
召回以后放多少进 Context?
事实变化以后如何修正?
什么时候过期、压缩或遗忘?

因此,一个工业级 Agent Memory 更接近 Memory Lifecycle Engine,而不是 vector_store.add(text)

1.2 从 Stateless LLM 到 Stateful Agent#

普通 LLM:

Input → Model → Output → End

Stateful Agent:

Current Task
Working State
Context Builder
Historical Memory Recall
Agent Decision
Tool / Environment Action
Observation
Task Result
Memory Update
Future Task

真正的变化发生在:

Task Result
Memory Update
Future Task

历史执行开始改变未来决策。

但“历史影响未来”不等于把全部历史塞给模型。Memory Engineering 追求的是:

用最小、最准确、最相关的历史信息改变当前决策。

1.3 为什么 Full History 是糟糕的默认方案#

假设 Agent 执行 1,000 个任务,每个任务产生 20,000 tokens:

1,000 × 20,000
=
20,000,000 tokens

问题不只在成本。

Context Pollution#

真正相关的信息可能只有:

项目使用 Gradle
数据库迁移命令是 ./gradlew flywayMigrate
integration test 必须启动 Docker

但模型看到数百轮 Debug 历史,Relevant Signal 被噪声淹没。

Historical Conflict#
2025-01: 用户住北京
2025-09: 用户搬到上海
2026-05: 用户搬到深圳

“能召回三条”不代表“知道当前哪条有效”。

History 不是 Knowledge#

原始过程:

Agent: 可能是 Redis。
User: 不是。
Agent: 可能是 MySQL。
User: 也不是。
Agent: 找到了,是时区序列化。

如果完整保存,三个假设都存在。真正应该形成的 Memory 是:

该项目订单时间解析异常的根因是时区序列化配置,
不是 Redis/MySQL 连接。

所以:

History
Extraction
Distillation
Memory

Memory 的第一道价值不是“存”,而是 从经历中提炼未来有用的信息

1.4 Memory 和 RAG 的根本区别#

RAG 的主要输入通常是:

Document Corpus

Memory 的主要输入更接近:

Agent Experience Stream

包括:

User Message
Agent Message
Tool Call
Tool Result
Execution Trace
Error
Human Feedback
Reflection
Task Outcome

因此:

RAG
→ 外部知识如何被检索
Memory
→ 运行历史如何形成未来可复用状态与经验

二者可以共享 Embedding、BM25、Reranker、Graph,但数据语义不同。不要因为都能用向量数据库,就把它们做成同一个逻辑模块。


2. 用统一生命周期理解 Agent Memory#

Experience / Event
Capture
Extract
Evaluate
Write
Organize
Retrieve / Route
Inject
Use
Update / Consolidate / Invalidate / Forget

Agent Memory 从事件捕获、抽取、评估、写入到召回、注入与维护的完整生命周期

图 1:Memory 不是单一存储组件,而是一条带写策略、检索预算和反馈维护的闭环数据管线。

这就是本文统一采用的 Agent Memory Lifecycle

2.1 Capture:捕获什么事件#

完整 Event Source 可以包括:

User Message
Assistant Message
Tool Call
Tool Result
Plan
State Transition
Environment Change
Error
Retry
Verifier Result
Human Approval
Human Correction
Final Task Result
Evaluation Score

Coding Agent 第一次进入仓库:

mvn test → 失败
检查仓库 → 发现 Gradle
./gradlew test → 成功

可以抽取两种不同记忆:

Semantic Fact:
Project build system = Gradle
Procedural Experience:
When build command is uncertain, inspect build descriptors before selecting Maven/Gradle.
Capture 不是“把聊天存下来”,而是建立可重放的事实日志#

如果 Capture 层只保存 role + content,后面很难回答“这条结论来自哪个工具结果”“重试后哪个结果才是最终结果”“同一个事件是否被消费过”。更稳妥的事件信封至少需要:

class MemorySourceEvent(BaseModel):
event_id: str # 全局幂等键
trace_id: str # 一次 Agent Run
span_id: str # 某次模型或工具调用
sequence: int # 同一 trace 内的稳定顺序
schema_version: int
occurred_at: datetime # 事件实际发生时间
ingested_at: datetime # Memory 系统观察到它的时间
producer: str # user / model / tool / verifier / human
trust_domain: str # first_party / third_party / untrusted
payload_ref: str | None # 大对象放对象存储,不复制进消息队列
payload_hash: str # 内容完整性与去重
sensitivity: str

这里有三个不能省略的工程不变量:

  1. 事件不可变:修正通过追加 correctionsupersedes 事件完成,不原地改历史。
  2. 消费幂等:Extractor 以 (pipeline_version, event_id) 去重;消息队列采用 at-least-once 投递时,重复事件不能生成重复记忆。
  3. 来源可追踪:Memory 永远保存 source_event_ids,否则它只是模型生成的无来源文本。

数据库事务与消息队列之间还需要 Transactional Outbox:业务状态与 outbox event 在一个本地事务中提交,再由 relay 发布。否则“任务已完成但事件没有发出”会形成永久的 Memory Recall Ceiling。

2.2 Extract:从事件中抽取什么#

好的 Memory Candidate 应该尽量:

Self-contained
Future-useful
Low ambiguity
Scoped
Source-linked

例如用户说:

以后 Java 项目我一般优先 Maven,除非仓库明确用了 Gradle。

长期 Memory 不应该机械保存整句聊天,而可以抽成:

User prefers Maven for new Java projects unless the existing repository already uses Gradle.
Candidate 不是 Canonical Fact#

Extractor 的输出应该进入候选区,而不是直接成为“系统相信的事实”。一个候选至少要区分:

claim 模型抽出的陈述
evidence 支持它的原始事件与片段
subject 这是谁或什么对象的事实
scope user / agent / project / organization
modality explicit / observed / inferred
source_trust 来源可信度
extractor_score 抽取模型置信度
valid_time 事实在哪个时间区间成立

extractor_score 不能替代 source_trust。模型可以非常确信地复述一个恶意网页中的指令,但来源仍然是不可信外部数据。生产系统应保留 PROPOSED → ACCEPTED / REJECTED / QUARANTINED 状态,使评测、人工审核和安全策略能够在持久化之前介入。

2.3 Evaluate:不是所有信息都值得记#

可以使用一个工程启发式:

Memory Utility
Importance
× Reusability
× Confidence
× Expected Future Usage
-
Context Cost
-
Risk Penalty

这不是标准学术公式,而是工程建模思路。

  • Importance:信息重要吗?
  • Reusability:未来任务会复用吗?
  • Confidence:是用户明确事实还是 Agent 猜测?
  • Expected Future Usage:未来多久可能再次使用?
  • Context Cost:常驻 Context 的成本多大?
  • Risk Penalty:敏感性、错误传播风险多大?

2.4 Write:Memory Control Plane 在哪里#

四种典型控制模型:

Harness 自动写
→ Codex / Mem0
Agent 主动写
→ Hermes / Letta
Human 写
→ CLAUDE.md / AGENTS.md
Hybrid
→ System + Agent + Human

Memory 架构的核心区别,经常不是“文件还是数据库”,而是:

谁拥有 Memory Control Plane。

Write Path 需要显式状态机#

后台生成不是一个布尔任务,而是一组可恢复的状态转换:

PENDING
└─ claim(lease, ownership_token, input_watermark)
RUNNING
├─ commit(output, expected_version) → SUCCEEDED
├─ commit_no_output() → SUCCEEDED_NO_OUTPUT
├─ retryable_error → RETRY_WAIT
└─ permanent_error / retry=0 → DEAD_LETTER

关键字段不是 status 一个枚举,而是:

ownership_token 防止旧 Worker 在租约过期后覆盖新 Worker
lease_expires_at 允许崩溃恢复
input_watermark 这次处理到哪个源版本
retry_remaining 有界重试
next_retry_at 指数退避与抖动
last_error 可观测性与人工修复

其中 SUCCEEDED_NO_OUTPUT 必须是一等状态:一次任务没有值得记的内容,是有效业务结果,不应该被无限重试。Codex 固定版本源码就显式实现了 mark_stage1_job_succeeded_no_output,并区分“本来就没有旧输出”和“本次需要删除旧输出后触发全局巩固”两种情况。[S5]

2.5 Organize:Memory 不只是 Text Chunk#

Memory Representation 可以是:

Markdown Entry
Memory Block
Summary
Fact
Preference
Episode
Procedure
Entity
Relationship
Temporal Edge
Skill
Task Experience

Representation 决定系统以后能回答什么问题。

Text Chunk 很适合:

“用户现在住深圳”

但如果要回答:

用户 2025 年住在哪里?
什么时候从上海搬走?
过去住过哪些城市?

就开始需要 Temporal Representation。

Canonical Record 与派生索引必须分层#

向量库、BM25 索引和知识图谱不应该同时被当成独立真相源。更稳妥的模型是:

Canonical Memory Record
├── Dense Vector Index
├── Lexical / BM25 Index
├── Entity Link Index
├── Temporal Graph Projection
└── Prompt-ready Summary Cache

Canonical Record 保存版本、来源、权限、有效期和删除状态;其他结构都是可重建的 Materialized View。这样才能定义一致性:

class CanonicalMemory(BaseModel):
memory_id: str
version: int
content: str
kind: str
namespace: tuple[str, ...]
source_event_ids: list[str]
valid_at: datetime | None
invalid_at: datetime | None
expires_at: datetime | None
trust_level: str
sensitivity: str
status: Literal["active", "invalid", "deleted"]

写路径先以 Optimistic Concurrency Control 更新 canonical row,再通过 outbox 异步刷新各索引。检索结果必须带 memory_id + version,Context Builder 在注入前回查当前版本,避免旧向量索引把已经删除或失效的内容重新送进 Prompt。

2.6 Retrieve:召回不是只有 Vector TopK#

可用策略包括:

Always Inject
Exact Lookup
Metadata Filter
BM25
Semantic Search
Hybrid Search
Graph Traversal
Temporal Filter
Agentic File Search
Hierarchical Routing

一个可靠的 Retrieval Plan 通常是“硬过滤在前、召回在中、效用排序在后”:

Authorization / Namespace / Status / Valid-time Filter
BM25 ─┬─ Dense ─┬─ Entity ─┬─ Graph / Temporal
└──────── Candidate Union ─────────┘
RRF / Learned Fusion
Utility Rerank + Diversity

RRF 不要求不同通道的原始分数可比,适合融合 BM25、向量和图检索:

RRF(d)=rR1k+rankr(d)\operatorname{RRF}(d)=\sum_{r\in R}\frac{1}{k+\operatorname{rank}_r(d)}

但融合分数仍不是最终效用。生产排序常需要额外乘上或约束 confidencefreshnessscope_matchtemporal_validitypermission。Graphiti 的固定版本源码把 Edge、Node、Episode、Community 的 Search Config 和 Reranker 分开,正说明不同检索单元不应共享一个粗糙 TopK。[S9]

2.7 Inject:检索到不等于全部塞进 Prompt#

Retriever 回 50 条 Memory 后,Context Builder 还要决定:

哪些进入 Context?
顺序是什么?
放在哪一层?
是否需要摘要?
是否存在冲突?
是否过期?
Token Budget 是多少?

所以:

Retriever
→ 找到什么相关
Context Builder
→ 模型现在应该看到什么

这两个模块应该分离。

Context Packing 是有约束的组合优化#

假设候选记忆 mim_i 的预计任务效用为 uiu_i,Token 成本为 cic_i,总预算为 BB,最简单的目标是:

maxiuixis.t.icixiB,  xi{0,1}\max \sum_i u_i x_i \quad \text{s.t.}\quad \sum_i c_i x_i\le B,\;x_i\in\{0,1\}

真实系统还要加入冲突对不能同时作为“当前事实”注入、同主题去重、来源多样性、核心记忆保底额度等约束。因此 top_k=20 后直接拼接不是 Context Builder,只是把检索噪声转移给模型。

注入格式也应把内容与指令隔离:

<memory_item id="m_123" trust="observed" valid_at="2026-07-01">
<content>Integration tests require Docker Compose.</content>
<provenance>trace_8/tool_result_4</provenance>
</memory_item>

Memory 是不完全可信的数据,不应因为被放进 system prompt 就自动升级为高优先级指令。

2.8 Maintenance:Memory 必须会变化#

完整 Memory Lifecycle 需要:

ADD
UPDATE
MERGE
CONSOLIDATE
INVALIDATE
EXPIRE
DELETE
FORGET

如果系统只有 add()search(),更像 Retrievable Event Store,而不是完整 Memory System。

删除也不是 DELETE FROM memories 一条语句。它必须沿派生关系传播:

Canonical Tombstone
├── Vector delete / deny-list
├── BM25 document delete
├── Graph node/edge invalidation
├── Summary regeneration
├── Prompt cache purge
├── Audit event
└── Backup retention workflow

在所有派生索引确认删除前,读路径应优先检查 canonical tombstone。否则“向量库删除成功、摘要缓存仍保留”会让用户删除请求在行为层面失效。

2.9 统一分析维度#

维度核心问题
Memory Source记忆从哪里产生
Write Policy谁决定写
Write Timing什么时候写
Representation记忆如何表示
Storage存在哪里
Retrieval如何召回
Context Injection如何进入 Context
Compaction历史如何压缩
Conflict新旧信息冲突怎么办
Forgetting如何删除、过期或遗忘
Agent ControlAgent 有多少控制权

这个分析框架也能被其他主流实现交叉验证。LangGraph 官方文档把 thread-scoped short-term state 交给 checkpointer 持久化,把 cross-thread long-term memory 放入自定义 namespace,并明确区分 hot-path write 与 background write;同时讨论 Profile 与 Memory Collection 在更新复杂度、召回率和检索成本上的权衡。[S13] 这说明 Scope、Write Timing 和 Representation 不是某一个产品的偶然实现细节,而是跨框架反复出现的架构变量。


Part II:产品级 Agent 的记忆实践#

3. Codex:后台记忆抽取与任务经验沉淀#

独立源码深挖:Codex Memory:两阶段语义 ETL、SQLite Job 状态机、Git 巩固与渐进式召回

3.1 Coding Agent 的 Memory Workload#

Coding Agent 长期工作的真实环境包含:

Repository
Build System
Test Commands
Architecture Conventions
Hidden Constraints
Tool Quirks
Past Debugging Experience
User Workflow Preferences

第一次任务发现:

mvn test → 失败
检查仓库 → Gradle
./gradlew test → 成功

下一次再从 Maven 开始,说明历史任务没有转化为未来能力。

Codex Memory 的核心问题是:

过去任务中有哪些工作经验值得未来 Coding Task 复用?

3.2 官方公开的 Memory 边界#

OpenAI 当前公开说明中有几个非常重要的行为约束:

  1. Local Codex 使用独立的本地 Memory Store。
  2. 从符合条件的历史任务中生成 Memory。
  3. 活跃或过短的 Session 可以被跳过。
  4. Memory 后台更新,而不是任务结束立即更新。
  5. 生成 Memory 字段会做 Secret Redaction。
  6. 主 Memory 文件位于 ~/.codex/memories/
  7. 必须遵守的团队规则应该放在 AGENTS.md 或受版本控制的文档中,而不是只依赖 Memory。[S1]

因此:

Required Policy
→ AGENTS.md / checked-in rules / enforcement
Learned Experience
→ Memory

即:

Policy ≠ Memory。

3.3 两阶段后台流水线#

公开源码:

codex-rs/memories/write/src/phase1.rs
codex-rs/memories/write/src/phase2.rs

能够看到非常明确的两阶段设计:

Historical Rollouts
Phase 1
Per-Rollout Extraction
Raw Memory
+
Rollout Summary
Phase 2
Global Consolidation
Durable Memory Workspace

这是一套典型的:

Extract → Consolidate

架构。

3.4 Phase 1:Rollout Extraction#

Phase 1 模型输出被限制为三个字段。公开源码中的关键结构很短:

struct StageOneOutput {
raw_memory: String,
rollout_summary: String,
rollout_slug: Option<String>,
}

源码:openai/codex/codex-rs/memories/write/src/phase1.rs

三者职责不同:

raw_memory
→ 单次 Rollout 中提炼的详细记忆
rollout_summary
→ 紧凑摘要,适合 Routing / Indexing
rollout_slug
→ 可选 artifact naming

这说明 Codex 至少区分:

Detailed Memory
vs
Compact Routing Summary

3.5 Eligibility:Memory Write 之前先决定“这次任务能不能写”#

Phase 1 不是遍历全部历史直接总结,而是先 Claim Eligible Rollout Jobs。

当前源码参数包括:

scan_limit
max_claimed
max_age_days
min_rollout_idle_hours
allowed_sources
lease_seconds

结构化源码还原:

# 非 Codex 原始源码
candidates = state_db.claim_memory_jobs(
scan_limit=THREAD_SCAN_LIMIT,
max_claimed=config.max_rollouts_per_startup,
max_age_days=config.max_rollout_age_days,
min_idle_hours=config.min_rollout_idle_hours,
allowed_sources=interactive_session_sources,
lease_seconds=JOB_LEASE_SECONDS,
)

这背后有五层含义。

Memory Generation 是 Background Job

不是用户请求主链路中的同步副作用。

Idle Threshold 防止 Premature Commit

Task 暂时停止
Task 完成

如果用户十分钟后继续 Debug,过早写 Memory 可能固化半成品结论。

Age Limit 控制处理窗口

过旧 Rollout 不一定值得重新处理。

Source Filter 控制来源

不是所有 Session Source 都应进入记忆流水线。

Lease 防重复处理

后台 Worker 需要 Ownership / Lease,避免重复消费同一任务。

所以 Codex Phase 1 本质是:

带资格筛选、租约、并发和生命周期管理的 Memory ETL Job System。

3.6 为什么 Phase 1 可以并行#

源码通过并发流执行 Rollout Extraction。

结构化还原:

async def phase1():
candidates = claim_eligible_rollouts()
outcomes = await parallel_map(
candidates,
extract_one_rollout,
concurrency_limit=CONCURRENCY_LIMIT,
)
emit_metrics(aggregate(outcomes))

因为:

Rollout A → Raw Memory A
Rollout B → Raw Memory B
Rollout C → Raw Memory C

在这一阶段可以独立处理。

它非常像数据系统中的 Map Stage。

3.7 Structured Output:Memory Extraction 是 Semantic ETL#

Phase 1 使用受约束 JSON Schema,并禁止额外字段。

这不是为了“输出漂亮”。

而是让下游能够:

Validate
Persist
Index
Route
Consolidate
Measure

所以 Memory Extractor 的模型调用更适合理解为:

Semantic ETL Operator

而不是聊天节点。

3.8 Secret Redaction:Memory Write Path 是 Security Boundary#

Coding Agent 可能看到:

API Key
Database Password
Private Token
Credentials
Internal URL

如果一次 Session 中的 Secret 被写入 Memory:

Session A
Memory
Session B
Session C
Session D

风险被持久化并跨任务传播。

因此:

Memory Write Path 本身就是持久化安全边界。

生产系统至少要考虑:

PII Detector
Secret Detector
Credential Redaction
Sensitivity Classification
Namespace ACL
Retention Policy
Audit Log

3.9 Phase 2:Global Consolidation#

Phase 1 得到很多 Raw Memory:

A
B
C
...

直接全部塞进未来 Context 会产生:

重复
冲突
碎片
过期
噪声

所以需要 Phase 2。

真实控制流可重构为:

# 非 Codex 原始源码
async def phase2():
claim = claim_global_consolidation_lock()
prepare_git_backed_memory_workspace()
agent_config = build_locked_down_consolidation_config()
raw_memories = db.select_phase2_inputs(
max_raw_memories=config.max_raw_memories,
max_unused_days=config.max_unused_days,
)
sync_inputs_to_memory_workspace(raw_memories)
diff = calculate_memory_workspace_diff()
if no_change(diff) and artifacts_are_valid():
mark_success()
return
write_diff_for_agent(diff)
agent = spawn_consolidation_agent(
config=agent_config,
prompt=build_consolidation_prompt(),
)
handoff_completion_and_heartbeat(agent)

3.10 为什么 Phase 2 需要 Global Lock#

Phase 1 可独立抽取。

Phase 2 修改的是全局 Memory Workspace。

两个 Consolidator 同时工作:

Agent 1 读取 V10 → V11-A
Agent 2 读取 V10 → V11-B

容易产生 Lost Update。

因此 Phase 2 使用 Global Job Claim、Ownership Token 和 Watermark。

这已经非常接近 Stateful Stream Processing。

3.11 Git-backed Memory Workspace#

Phase 2 做了:

Prepare Workspace
Ensure Git Baseline
Sync Inputs
Calculate Workspace Diff

核心源码中有一句:

let workspace_diff = memory_workspace_diff(&root).await?;

源码:codex-rs/memories/write/src/phase2.rs

为什么 Git 适合这里?

Old Memory Workspace
New Inputs
Agent Editing
New Memory Workspace

Git 天然提供:

Baseline
Diff
Changed Files
Change Detection
Rollback Foundation
Auditability

因此 Codex 的 Consolidator 更像:

受限的 Memory Workspace Editing Agent

而不是一次 summarize(all_memory)

3.12 为什么 Spawn 独立 Consolidation Agent#

Consolidation 的任务是:

理解已有长期记忆
检查新增 Raw Memory
发现重复
识别新增知识
整合冲突
编辑持久记忆

这本身就是复杂 Agent Task。

所以 Codex 直接 Spawn 专门的 Consolidation Agent。

这代表一种重要架构:

Main Agent
→ 完成用户任务
Memory Agent
→ 维护长期记忆

Memory Sub-Agent

3.13 Memory 和 Compaction 必须分开#

OpenAI 官方 Cookbook 给出的工程边界非常清楚:[S2]

Compaction
→ 当前 Long-running Run 如何继续
Memory
→ 未来 Run 如何复用过去经验
Current Run
messages...
Context Too Long
Compaction
Current Run Continues

Memory:

Run A
Experience
Memory
Run B
Recall Run A Experience

因此:

Compaction
= Intra-run Context Lifecycle
Memory
= Inter-run Experience Lifecycle

Conversation Summary 不能自动等同于 Long-Term Memory。

3.14 Read Path:Summary → Registry → Evidence / Skill#

前面的分析主要解释 Codex 如何写 Memory,但固定 Commit 的源码还公开了一条同样重要的读取路径。read_path.md 把本地目录设计成 Progressive Disclosure 层级:[S5]

memory_summary.md
已注入的全局摘要,先判断是否可能相关
MEMORY.md
可搜索的 Registry,定位主题和证据路径
skills/<skill-name>/SKILL.md
可执行的流程知识
rollout_summaries/*.jsonl
原始任务回顾、命令、错误和工具证据

读取策略不是“把整个 ~/.codex/memories 塞进上下文”,而是:

  1. 从已注入摘要提取关键词。
  2. 搜索 MEMORY.md 注册表。
  3. 只打开 1~2 个最相关的 skill 或 rollout。
  4. 只有需要精确命令、错误文本或证据时,继续下钻。
  5. 没有命中就停止,而不是扩大扫描。

模板还给出理想的 4~6 次搜索步数预算。这体现了一个关键架构原则:Memory Recall 自身也有延迟、Token 和注意力预算,Agentic Retrieval 必须有停止条件。

3.15 MemoriesBackend:把存储能力与 Agent 工具协议分离#

codex-rs/ext/memories/src/backend.rs 定义的 MemoriesBackend 不是一个只有 search(text) 的接口,而是四组能力:[S5]

trait MemoriesBackend {
add_ad_hoc_note(...);
list(...);
read(...);
search(...);
}

其请求/响应里有几个容易忽略的工程细节:

  • listsearch 都有 cursor、最大结果数与 truncated,因此大目录不会假装一次返回完整结果。
  • read 使用 1-based line_offsetmax_linesmax_tokens 双重预算,并返回实际起始行号。
  • search 支持多 query、上下文行数、大小写与 normalized 模式。
  • 匹配语义分为 AnyAllOnSameLineAllWithinLines { line_count },可以表达“多个证据词必须出现在同一局部窗口”,比单字符串 grep 更适合查命令与错误链。

固定版本默认上限是 List 2,000、Search 200、Read 20,000 tokens;注入的 summary 另有 2,500 tokens 上限。这里的重点不是具体数字,而是每一层都有独立预算,工具返回明确声明截断,调用者才能决定是否翻页或下钻。

3.16 Local Backend 的 Path Sandbox#

Memory 文件会被模型驱动的工具读取,因此本地路径解析本身就是安全边界。local.rslocal/path.rs 的实现拒绝:[S5]

Absolute Path / Windows Prefix
ParentDir (`..`)
Hidden Path Component
Traversing through a non-directory
Symlink at any traversed component

Symlink 拒绝尤其关键。只做字符串级 starts_with(memory_root) 检查无法阻止:

memories/evidence
→ symlink to ~/.ssh

Codex 使用 symlink_metadata 逐级检查路径组件,使 list/read/search 都被限制在 Memory Root 内。MemoriesBackend 接口同时要求实现返回相对路径,并允许未来的远程后端复用同一能力契约。换句话说,文件型 Memory 不是“让模型自由读磁盘”,而是一个有资源边界、分页和访问规则的虚拟文件系统。

3.17 Job State Machine:Watermark、租约与“无输出成功”#

codex-rs/state/src/runtime/memories.rs 展示了后台 Memory Job 的完整控制语义:[S5]

Stage 1 job key = thread_id
Stage 2 job key = "global"
claim:
ownership_token + lease_expires_at + input_watermark
success:
last_success_watermark = input_watermark
clear lease
upsert or delete stage1 output
enqueue global consolidation when workspace may change
failure:
clear lease
persist last_error
decrement retry_remaining
schedule backoff

源码默认重试预算为 3;当同一线程出现更新的输入 watermark 时,即使旧输入已经耗尽重试,也可以恢复预算。这避免“一次坏输入永久封死整个线程的后续 Memory”。

Phase 2 使用全局单例租约,并在成功后设置 6 小时 cooldown。更微妙的是,数据库 input_watermark 主要用于调度与所有权,不作为“是否有实际工作”的唯一 dirty check;真正是否需要提交由 Git-backed Memory Workspace 的 diff 判断。这样把任务新鲜度物化输出是否变化分开了。

线程还可以进入 polluted memory mode,Phase 2 输入选择会跳过这些线程;系统也会按 retention 清理旧 Stage 1 输出。这些设计共同说明,成熟的 Memory Pipeline 需要:来源隔离、可恢复租约、单调 watermark、有限重试、无输出成功、污染隔离与保留策略,而不只是一个后台 cron。

Codex 两阶段后台 Memory Job 的租约、水位线、重试、隔离与 Git Workspace 状态机

图 2:依据 Codex 固定版本源码 [S5] 简化绘制。Stage 1 以 thread 为并行单元,Stage 2 以 global lease 串行巩固;SUCCEEDED_NO_OUTPUT、新 Watermark 恢复重试和 polluted quarantine 都是控制语义,不是普通 cron 的附属细节。

3.18 Provenance、Drift 与受控更新#

读取模板要求在使用 Memory 后生成机器可解析的 <oai-mem-citation>,其中同时记录文件行号和 rollout ID;codex-rs/memories/read/src/citations.rs 再把它解析成结构化引用。[S5]

这解决两个问题:

  1. 用户可以知道结论来自哪个长期记忆与历史任务。
  2. 系统可以统计哪些 rollout 真正被复用,而不是只统计“检索过多少条”。

模板还明确区分“低漂移事实”和“高漂移事实”:高漂移且验证便宜时应实时验证;验证昂贵时可以使用 Memory,但必须说明它可能过时。这相当于把 freshness risk × verification cost 纳入 Recall Policy。

最后,生成的核心 Memory 文件不允许被 Agent 直接原地修改。只有用户明确要求更新时,才在 extensions/ad_hoc/notes/ 写一个小型变更说明,交给后续流水线处理。它类似 append-only command log,避免模型绕过 Consolidator 破坏生成状态。

3.19 Codex 的工程范式#

Eligible Task Selection
Delayed Background Write
Per-Rollout Extraction
Structured Raw Memory
Global Consolidation
Git-backed Workspace Diff
Dedicated Memory Agent
Durable Recall Layer

Codex 代表:

System-Managed Background Experience Memory

最值得学习的九点:

  1. Memory Extraction 从用户请求主链路移出。
  2. 任务稳定后再写 Memory。
  3. 单任务抽取与全局巩固分阶段。
  4. Memory Maintenance 本身可以交给专用 Agent。
  5. Policy 必须与 Recall Memory 分离。
  6. 读取采用 Summary → Registry → Evidence 的渐进披露。
  7. Memory 工具必须有分页、Token 上限与截断语义。
  8. 本地文件后端必须实施路径沙箱与 symlink 防护。
  9. 每条被使用的 Memory 都应该可追溯到文件证据与历史 Run。

4. Claude Code:Memory as File 与 Progressive Disclosure#

独立行为契约深挖:Claude Code Memory:层级加载器、Auto Memory、Path Rules、Compaction 恢复与安全治理

4.1 为什么先进 Coding Agent 仍大量使用 Markdown#

很多人第一次看到 Claude Code Memory 会问:

为什么不直接上 Vector Database?

Anthropic 官方当前公开两套跨 Session 机制:[S3]

CLAUDE.md
+
Auto Memory

二者职责不同:

CLAUDE.md
Who writes: Human
Purpose: Instructions / Rules
Auto Memory
Who writes: Claude
Purpose: Learnings / Patterns

这是第一层边界:

Human-authored Persistent Context vs Agent-authored Learned Context

4.2 CLAUDE.md:Hierarchical Persistent Instruction#

CLAUDE.md 可以存在于不同 Scope:

Organization
User
Project
Local

项目可能是:

repo/
├── CLAUDE.md
├── backend/
│ └── CLAUDE.md
└── frontend/
└── CLAUDE.md

Anthropic 当前公开行为是:

Working Directory 上层的 CLAUDE.md
Launch 时加载
Subdirectory CLAUDE.md
Claude读取相关目录文件时按需加载

这是一种:

Hierarchical Context Scoping

它避免:

frontend rule
永久污染 backend task context

传统 Memory Retrieval 常由 Query Similarity 决定,而这里:

Filesystem Scope

本身就是一种 Context Router。

4.3 Auto Memory:MEMORY.md 不是“大型记忆数据库”#

官方描述的 Auto Memory 目录采用:

memory/
├── MEMORY.md
├── debugging.md
├── api-conventions.md
└── project-notes.md

最关键的是:

MEMORY.md 更接近 Memory Index / Hot Working Set。

当前官方规则:

Session Start
Load first 200 lines or 25 KB of MEMORY.md

超过阈值的内容不会自动全部加载。

详细信息进入 Topic Files:

MEMORY.md
=
Hot Memory Index
Topic Files
=
Warm / Cold Detailed Memory

4.4 这其实是一种 Context Cache Hierarchy#

概念类比:

Fast / Small
MEMORY.md Core Index
Topic Files
Repository Files
External Resources
Large / Slow

这里不是说 Claude Code 真按 CPU Cache 实现,而是其工程思想类似:

高频、短、关键的信息常驻;详细内容按需读取。

也就是:

Progressive Disclosure of Memory

4.5 Recall Path:Agentic Retrieval#

传统 Vector Memory:

Query
Embedding
TopK
Inject

Claude Code 的文件路线更像:

Session Start
Read MEMORY.md index
Current Task
Agent发现需要 Debug 历史
Read debugging.md
信息仍不足
Grep / Search
定位更具体内容

Recall 不完全由 Middleware 自动决定。

而是:

Agent
发现 Context Gap
主动探索 Memory Space

这是:

Agentic Memory Retrieval

4.6 如何严谨地“还原” Claude Code 的 Memory 实现#

Claude Code Core Runtime 未完整开源,所以不能声称内部一定存在某个 MemoryManager 类。

但依据官方行为契约,可以做接口级还原:

# 行为级架构还原,不是 Claude Code 原始源码
def build_session_context(cwd):
instruction_files = resolve_claude_md_hierarchy(cwd)
auto_memory_index = load_memory_md(
max_lines=200,
max_bytes=25 * 1024,
)
return compose_context(
instructions=instruction_files,
auto_memory=auto_memory_index,
)
def recall_topic_memory(topic):
path = choose_topic_file(topic)
return file_tool.read(path)

重要的不是类名,而是两个公开行为:

Always-load small index
+
On-demand detailed files

4.7 为什么文件系统在 Coding Agent 中可能优于 Vector DB#

不是 File Memory 全面优于 Vector Memory,而是 Workload 不同。

项目本身已经是 Filesystem World#

Coding Agent 已经有:

read
write
grep
glob
find
git diff

如果 Memory 也是文件,Agent 不需要新增一套操作抽象。

人类可读、可编辑#
MEMORY.md
debugging.md
architecture.md

可以直接审计。

Vector Store 中则可能是:

vector_id
payload
embedding
metadata

运维和人工编辑成本更高。

天然 Hierarchy#

路径本身就是 Metadata:

memory/debugging/database.md

表达:

memory
→ debugging
→ database
确定性更强#

如果 Index 写明:

数据库迁移经验见 database-migration.md

读取路径非常稳定。

Vector Retrieval 则受:

Embedding Model
Query Formulation
TopK
Threshold
Ranking

影响。

Git Friendly#

文件天然支持:

diff
review
rollback
version

4.8 File Memory 的边界#

规模增长到:

1,000 topic files
100,000 entries

问题出现:

Agent如何知道读哪个文件?

如果 Index 没有覆盖相关 Topic:

Recall Miss

对模糊问题:

“以前那个跟时间解析差不多的问题是什么?”

Semantic Retrieval 可能更合适。

所以 Claude 路线并不是“Vector DB 没用”,而是:

Coding Agent 的 Memory 应优先利用 Agent 原生掌握的 Filesystem + Search 能力。

4.9 200 行 / 25 KB:Always-on Memory 必须有预算#

如果 MEMORY.md 无限增长:

Session 1: 5 KB
Session 10: 100 KB
Session 100: 2 MB

最终每个任务都先支付巨大的:

Memory Context Tax

所以:

Hot Memory
必须 bounded
Detailed Memory
下沉到 Topic Files

这是 Memory Tiering。

4.10 Claude Code 的范式#

Human Persistent Instructions
+
Agent Learned Memory
Filesystem Hierarchy
Small Core Index
Topic Files
Lazy Read
Agentic Search

Claude Code 代表:

Memory as File + Progressive Context Disclosure

最值得学习的五点:

  1. Memory Storage 应贴合 Agent 原生工作环境。
  2. Hot Memory 必须有 Context Budget。
  3. Index 和 Detailed Memory 可以分离。
  4. Recall 可以由 Agent 主动探索,而不是全部自动 Push。
  5. Hierarchical Scope 本身就是 Context Routing。

5. Hermes:Agent 自主管理 Memory 与 Learning Loop#

独立源码深挖:Hermes Memory:Frozen Snapshot、Live State、File Lock、Drift Guard、Batch 事务与 Write Approval

5.1 Hermes 为什么特别值得研究#

Hermes Agent 把 Memory 定义为:

bounded
curated
persistent

官方当前设计有两个文件:[S4]

MEMORY.md
Agent personal notes
USER.md
User profile

默认容量:

MEMORY.md = 2,200 chars
USER.md = 1,375 chars

很多系统追求“存更多”。

Hermes 反而主动限制:

只能记很少

因为它关注的是:

Curated Working Memory

而不是完整事件仓库。

5.2 USER.md 与 MEMORY.md:User Model 和 Agent Experience 分离#

用户喜欢简洁回答

属于:

USER.md

因为这是 User Model。

本机安装 Docker
项目使用 Axum + SQLx
某工具在当前环境有特殊行为

属于:

MEMORY.md

因为这是 Agent Learned Environment / Experience。

所以:

USER.md
=
Who is the user?
MEMORY.md
=
What has the agent learned?

企业 Agent 不应该把:

用户偏好
业务事实
Agent经验
工具经验

全部扔进一张 memory 表。

5.3 MemoryStore 的双状态模型#

源码:

NousResearch/hermes-agent
tools/memory_tool.py

MemoryStore 显式维护:

Frozen Snapshot
+
Live State

一个核心成员是:

self._system_prompt_snapshot: Dict[str, str] = {"memory": "", "user": ""}

架构可以表示成:

Disk
Session Start
Load MEMORY.md / USER.md
Sanitize
Frozen Snapshot
System Prompt
--------------------------------
Mid-session memory.add()
Live State Update
Persist Disk
Tool Response Reflects New State
BUT
System Prompt Snapshot
remains unchanged

这是:

Dual Memory State

5.4 为什么 Frozen Snapshot 不立即更新 Context#

直觉上:

Agent刚写Memory
为什么不立即更新System Prompt?

Hermes 给出的工程理由是:

Preserve Prefix Cache

如果每次 Memory Mutation 都改变 System Prompt:

Prompt Prefix Changes
Prefix Cache Miss
重复计算
Latency / Cost Impact

因此:

Persistent State
立即更新
Runtime Prompt Snapshot
当前 Session 不变

下一 Session 才重新 Load。

这是:

Persistence Freshness 与 Runtime Cache Stability 的 Trade-off

5.5 Session-level Snapshot Isolation#

可以做语义类比:

Session Start
Read Memory Snapshot V10
During Session
Write V11
Write V12
Current Prompt
still sees V10
Next Session
Snapshot V12

它不是数据库 MVCC 的同一实现,但 Context View 具有类似的 Session Snapshot 语义。

5.6 为什么用字符预算而不是 Token 预算#

源码注释指出 Character Count 是 model-independent。

Token Count 会随:

Tokenizer
Model Family
Language

变化。

Character Budget:

简单
稳定
可预测

代价是:

不能精确反映真实 Context Token Cost

这是 Implementation Simplicity 与 Token Precision 的取舍。

5.7 Bounded Memory:容量满了故意不自动压缩#

Hermes add 路径大体是:

new_total > limit
Reject Write
Return Current Entries
Tell Agent:
replace overlapping entries
or
remove stale entries
Retry

结构化源码还原:

# 非 Hermes 原始源码
def add_memory(content):
validate(content)
scan_security(content)
with file_lock():
state = reload_latest_memory()
if duplicate(content):
return success_noop()
if current_size + size(content) > limit:
return {
"success": False,
"current_entries": state,
"instruction": "Consolidate or remove stale entries, then retry.",
}
append(content)
persist()

系统知道 Memory 满了,但不自动决定删什么。

容量错误变成 Agent 的 Observation:

Memory Full

Agent 再执行:

replace
remove
add

这就是:

Agent-controlled Memory Management

5.8 Capacity Pressure 是一种认知压力#

自动 Compaction:

System Summarizer
决定什么重要

Hermes:

Agent
看到当前Task和Memory
判断哪些经验仍有用
主动整合

例如:

Entry 1: User prefers concise replies.
Entry 2: User dislikes long explanations.
Entry 3: User prefers direct technical answers.

可以 Consolidate 成:

User prefers concise, direct technical answers and dislikes unnecessary long explanations.

这不是字符压缩,而是 Semantic Consolidation。

5.9 Memory Poisoning 防御:Sanitized Snapshot#

Memory Poisoning 的危险是:

Malicious Tool Result
Memory
Future Session
Persistent Prompt Injection

Hermes 对 Memory Content 使用 Strict Threat Pattern Scanning。

Snapshot Load 可结构化还原为:

# 非 Hermes 原始源码
for entry in disk_entries:
findings = strict_scan(entry)
if findings:
snapshot_entry = "[BLOCKED: threat pattern detected]"
else:
snapshot_entry = entry

但 Live State 仍保留原始条目。

为什么?

如果静默删除:

攻击痕迹被隐藏

Hermes 选择:

System Prompt Snapshot
→ 不注入危险内容
Live / Disk State
→ 保留供用户审计和删除

这是:

Execution Safety 与 Forensic Visibility 分离

5.10 File Lock:Memory 同样有并发一致性问题#

两个 Session 同时修改:

MEMORY.md

可能出现:

A read V10
B read V10
A write V11-A
B write V11-B

导致 Lost Update。

Hermes 使用独立 .lock 文件互斥:

Unix → fcntl
Windows → msvcrt

这说明:

Memory Store 一旦成为持久化状态,就必须面对数据库系统同类的一致性问题。

Memory 不是 Prompt Feature,而是 State Infrastructure。

5.11 Drift Detection:防止 Tool 覆盖外部修改#

Memory 文件可能被:

Manual Edit
Patch Tool
Shell Append
Sister Session

修改。

如果 Parser 不能 round-trip 某段未知内容,而 Replace/Remove 又整体重写文件:

Unknown Content
Silent Data Loss

Hermes 对 Replace / Remove 做 External Drift Detection。

发现 Drift:

Create Backup
Refuse Mutation
Tell Operator Resolve Drift

这是:

Fail Closed on Potential Data Loss

5.12 为什么 Add 可以弱化 Drift Guard#

Add

语义是 Append。

已有未知内容不会被整体重写。

Replace / Remove

需要:

Read
Parse
Modify
Rewrite

如果 Parser 没正确理解全部磁盘内容,Rewrite 可能丢数据。

所以保护强度应该根据 Mutation Semantics 设计,而不是所有操作一刀切。

5.13 Memory Side Effect 不能阻塞用户主任务#

源码中:

_MAX_CONSOLIDATION_FAILURES_PER_TURN = 3

如果 Agent 因 Memory 满不断:

add → failed
replace → failed
add → failed
...

可能出现:

Memory Side Effect Loop
耗尽 Turn Budget
用户拿不到回答

超过失败上限后,Hermes 返回 Terminal Result,要求停止 Memory Retry,继续回答用户。

这是一个非常重要的 Runtime 原则:

Auxiliary Cognitive Side Effect must not starve the primary user task.

5.14 apply_batch:Memory Mutation 需要事务语义#

Hermes 支持 Batch Memory Operations:

Remove old
+
Replace duplicated
+
Add new

一次 Tool Call 完成。

其语义:

Validate all
Apply to Working Copy
Check FINAL budget
Any failure → write nothing
Success → commit final state

即 All-or-Nothing。

结构化还原:

def apply_batch(operations):
scan_all_new_content()
with file_lock():
working = copy(load_latest())
for op in operations:
apply_to_working_copy(working, op)
if size(working) > limit:
abort()
persist(working)

旧流程:

remove
LLM Round Trip
replace
LLM Round Trip
add

Batch:

remove + replace + add
one tool call

减少:

LLM Round Trips
Context Resend
Partial Mutation Risk

5.15 Memory 与 Learning Loop#

Hermes 的产品思路强调 Agent during-use learning。

可以形成:

Execute Task
Observe Result
Persist Useful Experience
Repeated Pattern
Procedure
Skill
Future Task Reuse

需要区分:

Memory System
Skill Curator

但二者可以连接:

Episodic Experience
Repeated Pattern
Procedural Insight
Stable Skill

也就是:

Experience → Procedure → Capability

5.16 Hermes 的范式#

MEMORY.md
+
USER.md
Bounded Curated State
Frozen Session Snapshot
Agent Memory Tool
Add / Replace / Remove
Capacity Pressure
Agent Consolidation
Future Session

Hermes 代表:

Agent-Managed Bounded Curated Memory

最值得学习的七点:

  1. User Model 与 Agent Experience 分 Namespace。
  2. Always-on Memory 必须有限。
  3. Runtime Snapshot 和 Persistent State 可以分离。
  4. Memory Mutation 要有并发和数据损坏保护。
  5. Memory Write 必须防持久化 Prompt Injection。
  6. Memory Side Effect 不应阻塞主任务。
  7. 容量压力可以成为 Agent 主动 Consolidation 信号。

6. Codex、Claude Code、Hermes:三种产品路线比较#

维度CodexClaude CodeHermes
核心目标复用任务经验持续化项目上下文Agent 持续学习
控制者SystemAgent + FilesystemAgent
写入时机Task idle 后后台写Session 中积累Session 中 Tool Mutation
写入模式Extraction + ConsolidationFile NotesAdd/Replace/Remove
表示Raw Memory + Summary + WorkspaceMEMORY.md + Topic FilesBounded Entries
Hot MemoryConsolidated recall contextMEMORY.md 前 200 行/25KBFrozen Snapshot
ConsolidationPhase 2 AgentTopic organizationAgent replace/remove
SecuritySecret RedactionContext 不是 enforcementStrict scan + sanitized snapshot
Policy SourceAGENTS.md / docsCLAUDE.md / hooksPrompt / tool policy
最适合Coding Task ExperienceRepo ContextPersistent Personal Agent

三个短句概括:

Codex
=
System-managed Background Memory
Claude Code
=
Filesystem-oriented Progressive Memory
Hermes
=
Agent-managed Bounded Memory

真正的分水岭是:

Memory Control Plane 在哪里。

Part III:开源 Agent Memory 框架的实现机制#

7. Mem0:把 Memory 做成工业级 Pipeline#

独立源码深挖:Mem0 Memory Pipeline:V3 ADD-only、Batch Persist、Entity Link、Hybrid Ranking 与多存储故障窗口

7.1 最值得学习的是“Memory Pipeline 化”#

很多 Memory Demo:

vector_store.add(text)
vector_store.search(query)

Mem0 当前源码明显超出这个阶段。

mem0/memory/main.py 中,Memory 初始化包含:

Embedder
Vector Store
LLM
History DB
Optional Reranker
Lazy Entity Store

所以:

Memory
Vector Store Wrapper

而更接近:

Extraction
+
Identity Scope
+
Storage
+
History
+
Retrieval
+
Ranking
+
Entity Signal

7.2 Scope 是强约束,不是装饰性 Metadata#

当前 OSS Memory API 要求至少提供:

user_id
agent_id
run_id

之一。

即:

Memory
必须属于某个 Identity Scope

这是:

Memory Isolation Boundary

例如:

User A:
我对花生过敏
User B:
我喜欢花生酱

如果检索没有 User Scope:

Query:
推荐早餐
错误召回 User A Memory

这不是普通 Retrieval Accuracy 问题,而是 Cross-tenant Memory Leakage。

因此:

Memory Namespace 同时是正确性与安全边界。

7.3 infer=False:Raw Persistence Path#

Mem0 支持:

infer=False

此时不让 LLM 抽取。

结构化源码还原:

# 非 Mem0 原始源码
for message in messages:
if message.role == "system":
continue
embedding = embed(message.content)
create_memory(
text=message.content,
embedding=embedding,
metadata={
"role": message.role,
...
},
)

它适合:

上游已经构造好的 Memory
需要保留原始内容
日志型数据

即 Raw Memory Ingestion。

7.4 infer=True:V3 Phased Batch Pipeline#

当前源码直接标记:

# === V3 PHASED BATCH PIPELINE ===

源码:mem0/memory/main.py

其真实流程是:

Phase 0
Context Gathering
Phase 1
Existing Memory Retrieval
Phase 2
Single-pass ADD-only Extraction
Phase 3
Batch Embedding
Phase 4 / 5
CPU Processing + Hash Dedup
Phase 6
Batch Persistence + ADD History
Phase 7
Entity Linking
Phase 8
Save Messages + Return

Phase 8 不是无关紧要的收尾:session messages 只有在前面的向量、History 与 Entity 路径处理之后才保存。它属于一次 add() 的可见副作用,但并没有与前三类存储共享原子事务;进程在阶段边界崩溃时,后续请求看到的会话上下文可能与已经写入的 Memory 投影不一致。

这就是 Mem0 的核心:把语义抽取拆成显式阶段,同时也暴露出跨阶段恢复语义必须由调用方认真处理。

7.5 Phase 0:Extraction 本身也需要 Context#

源码会:

build session scope
load last 10 messages
parse current messages

例如当前消息:

User:
改成深圳吧。

单看当前消息无法知道改什么。

最近历史:

User:
我的常住城市是上海。
Assistant:
好的。
User:
改成深圳吧。

Extractor 才能得到:

Current city = Shenzhen

因此:

Memory Extraction 本身也是 Context Engineering。

7.6 Phase 1:Read-before-Write#

流程:

New Messages
Embed
Search Existing Memories
Top Candidates
Give to Extraction LLM

为什么先查旧 Memory?在 V3 中,这一步的目的不是让模型决定原地 UPDATEDELETE,而是给去重、上下文补全和关联判断提供参照。

已有:

User prefers dark mode.

新消息:

Actually, I use light mode now.

Extractor 看到新消息后会得到一条新的事实:

ADD light mode

最终:

dark mode
light mode

共存。

看 Existing Memory 后,模型可以把它理解为一次偏好变化,并在 Prompt 输出中尝试关联旧记录:

ADD "User now prefers light mode"
linked_memory_ids = [old preference]

但必须注意,V3 自动提取仍然只执行 ADD:旧的 dark-mode Memory 不会在这个阶段被覆盖或删除,新旧记录会同时保留,当前偏好由检索排序与时间语义在读时解析。官方迁移文档将这一变化明确表述为“Single-pass ADD-only”;显式 update() / delete() API 是另一条管理路径。[S7]

所以这里的 Read-before-Write 更准确地说是:

Read-before-Append:用旧记忆帮助去重与理解变化,但不在自动提取中改写旧记忆。

7.7 UUID → Integer Mapping:设计意图与固定版本落地缺口#

源码把 Existing Memory 的 UUID 映射成:

0
1
2
...

再交给 LLM。

真实 ID:

8df39fe1-...
af77b902-...

模型可能抄错、漏字符、幻觉一个 ID。

改成:

0: User prefers dark mode
1: User works in Java
2: User lives in Shanghai

设计意图是让模型只需输出:

linked_memory_ids = ["0"]

再由系统映射回 UUID。这是合理的 Anti-hallucination Interface:模型侧使用 Short Alias,系统侧保存真实 ID。

但固定 Commit 还暴露了一个需要源码审计才能发现的落地缺口:同步 V3 路径虽然构造了 uuid_mapping[str(idx)] = mem.id,后续却没有消费这个映射;Phase 4 构造新 Memory Payload 时也没有保存 Extractor 返回的 linked_memory_ids。Phase 7 的 Entity Linking 是从新 Memory 文本重新抽取实体,不能等价为把 Prompt 里的旧 Memory Link 持久化。

因此,对这个固定版本只能准确地说:

Short Alias Map 已构造并送入 Prompt
Prompt Link 已可靠映射并持久化

这也是一个通用审计原则:

不要只检查 Prompt 是否声明某个契约,还要沿 Parse → Mapping → Payload → Store 验证它是否真正落地。

7.8 Phase 2:Single-pass ADD-only Semantic Capture#

Extraction Prompt 输入:

Existing Memories
New Messages
Last K Messages
Custom Instructions

输出新的 Memory Candidates,每条候选包含自包含文本、attributed_to,并可带关联提示。V3 的 ADDITIVE_EXTRACTION_PROMPT 明确规定唯一操作是 ADD。[S7]

这不是普通总结。

它更像把会话增量转成可检索的语义事实:

Conversation Delta
New Semantic Facts
ADD-only Append

因此可以把它类比为 Semantic Capture,但不能把它画成会输出 ADD / UPDATE / DELETE / NONE 的 CDC Change Set。prompts.py 中虽然仍存在旧的 DEFAULT_UPDATE_MEMORY_PROMPT,固定版本 _add_to_vector_store(..., infer=True) 实际调用的是 ADDITIVE_EXTRACTION_PROMPT;不能因为旧常量仍在仓库中,就把旧算法行为归到 V3 主路径。

自动推断路径只追加;显式 CRUD 路径才会改写或删除指定 Memory。

7.9 Provider Failure 和“No Memory Extracted”必须分开#

两种状态:

正常空结果:本轮没有可写 Memory#
LLM正常
没有值得记忆的信息
[]
异常空结果:Extractor 没有完成工作#
LLM timeout / 429 / 5xx
Extraction failed
[]

如果都静默返回空列表,上游无法区分:

No Fact
vs
Provider Failure

当前 Mem0 源码选择在 Extraction Failure 时抛出 LLMError

这样上游才能:

Retry
Fallback
Alert

这是数据管道的基本原则:

Empty Result ≠ Processing Failure

7.10 Phase 3:Batch Embedding + Graceful Fallback#

Memory Candidates:

mem_texts
embed_batch()

Batch 失败:

fallback
embed individually

即:

Fast Path
=
Batch
Degraded Path
=
Per-item

长期基础设施不能因为一个 Batch Failure 就让整批 Memory 全灭。

7.11 Hash Dedup 不替代 Semantic Dedup#

源码计算文本 MD5,并检查:

existing_hashes
seen_hashes

它解决:

Exact Duplicate

例如:

User prefers dark mode
User prefers dark mode

但:

User prefers dark mode
The user likes dark themes

Hash 不同。

所以:

Hash Dedup
=
Cheap Exact Dedup
Semantic Logic
=
LLM / Retrieval / Entity Signals

典型路线:

Cheap Deterministic Filter
Expensive Semantic Processing

7.12 text_lemmatized:Memory Retrieval 不应该押注 Vector Only#

Mem0 为 Memory Text 保存 BM25 预处理文本。

例如查询:

ERR_PAYMENT_001

Error Code、Identifier、Exact Term 可能更适合 Keyword Search。

所以:

Hybrid Retrieval 往往比单一 Dense Retrieval 更稳。

7.13 Batch Persist + History:事件类型必须按路径解释#

Mem0 同时写:

Memory Record
+
History Event

History 记录类似:

memory_id
old_memory
new_memory
event
created_at
is_deleted

在 V3 自动提取的 Phase 6,所有 History Record 都是:

old_memory = null
new_memory = extracted text
event = ADD
is_deleted = 0

UPDATEDELETE 仍会出现在 History 中,但只来自调用方显式执行 update(memory_id, ...)delete(memory_id) 的独立路径,不是 Extraction LLM 的自动决策。History 因而记录的是存储层实际发生的 Mutation,而不是 V3 LLM 输出的多操作 Change Set。

这类 Change History 支持:

Audit
Debug
Evaluation
Failure Analysis

Memory Store 也应该拥有 Change History。

7.14 Entity Linking:从 Vector Similarity 到 Multi-signal Ranking#

当前源码会:

Extract Entities
Dedup Entity
Link Memory IDs

例如:

Entity:
PostgreSQL
linked_memory_ids:
[M1, M8, M27]

查询:

数据库最近遇到过什么问题?

Entity Signal 可以 Boost PostgreSQL 相关 Memory。

于是 Ranking 可以组合:

Semantic Score
BM25 Score
Entity Boost

Memory Search 已经从 Vector Similarity 升级成 Multi-signal Ranking。

7.15 Search Path:Semantic Candidate Pool → Signal Enrichment → Rank#

结构化源码还原:

# 非 Mem0 原始源码
query_keywords = lemmatize_for_bm25(query)
query_entities = extract_entities(query)
query_vector = embed(query)
internal_limit = max(top_k * 4, 60)
semantic_results = vector_search(
query_vector,
top_k=internal_limit,
)
keyword_results = keyword_search(
query_keywords,
top_k=internal_limit,
)
# 固定版本源码以 semantic_results 作为候选全集;
# keyword_results 只按 memory_id 提供归一化 BM25 分数。
results = score_and_rank(
semantic_candidates=filter_expired(semantic_results),
bm25_scores=normalize_by_id(keyword_results),
entity_boosts=match_entities(query_entities),
)
if reranker_enabled:
results = rerank(query, results)
return results[:top_k]

这里必须做一个容易被“Hybrid Search”术语掩盖的源码级区分:该固定版本并没有把 keyword-only 命中与 vector 命中做候选集合并集。score_and_rank() 遍历的是 semantic_results;BM25 与 Entity 只为这些语义候选补充分数。因此更准确的结构是:

Semantic Candidate Generation
BM25 / Entity Signal Enrichment
Normalized Multi-signal Ranking
Optional Reranking

这带来三个直接后果:

  1. 只被关键词精确命中、却没有进入 semantic_results 的 Memory,后续 BM25 再高也无法“复活”。
  2. internal_limit = max(limit × 4, 60) 通过 Over-fetch 缓解召回上限,但不能消除语义候选池的单路瓶颈。
  3. 如果 Semantic Threshold 在融合前过滤,强 lexical hit 仍可能提前丢失。

所以这一实现属于 semantic-first multi-signal reranking,不是严格意义上的 dense/sparse candidate union。生产系统若要求关键词召回兜底,需要显式合并两路 ID 集合,再做去重、归一化与排序。

而不是:

vector_search(top_k=5)
done

7.16 Expiration:Memory 不是永久事实#

当前 OSS 支持 expiration_date

过期 Memory 默认不会出现在普通 Search / Get All 中,除非显式要求查看。

例如:

User is visiting Tokyo this week.

六个月以后如果仍被当成 Current Fact,就是 Stale Memory。

因此部分 Memory 必须拥有:

TTL / Expiration

7.17 Procedural Memory:记住“如何做”#

Mem0 当前源码对 procedural_memory 有显式处理。

对比:

Fact:
Repository uses Gradle.
Procedure:
When tests fail in this repository,
run ./gradlew test --stacktrace first,
then inspect module reports.

后者对 Agent Capability 提升更直接。

7.18 多存储写入:吞吐优化不等于事务一致性#

固定 Commit 的 V3 路径先批量写 Vector Store,再批量写 SQLite History,最后更新 Entity Store 并保存 session messages。[S7]

Vector Store
History SQLite
Entity Store
Session Messages

源码对吞吐和可用性做了大量降级处理:批量 embedding 失败后逐条 embedding;批量 vector insert 失败后逐条 insert;批量 history 失败后逐条记录;entity linking 失败只记 warning,不让主 add 失败。这种策略能让部分结果继续可用,但它不是跨存储事务。

例如,逐条 vector insert 中某条失败后,当前实现仍会基于原始 records 构造全部 History,返回值也来自全部 records。于是可能出现:

History says ADD succeeded
but Vector Store has no corresponding record

反方向也可能发生:Vector Store 已写入,而进程在写 History 前崩溃。显式 Update/Delete 路径同样是先修改 Vector Store,再追加 UPDATE / DELETE History,最后清理或重建 Entity Link。这是调用方按 ID 发起的管理操作,不能与 infer=True 的 ADD-only 自动提取混为一谈。源码事实说明 Mem0 OSS 在这里偏向 best-effort availability;如果业务要求严格审计或 exactly-once,需要在外层补强,而不能把 history() 误当成数据库 WAL。

一个生产级封装可以增加:

operation_id + idempotency_key
Canonical Operation Log
Outbox / Inbox
Per-store apply status
Retryable Repair Worker
Read-time tombstone check
Periodic Reconciliation
class MemoryMutation(BaseModel):
operation_id: str
idempotency_key: str
memory_id: str
expected_version: int | None
action: Literal["add", "update", "delete"]
vector_applied: bool = False
history_applied: bool = False
entity_applied: bool = False

这里最重要的区分是:

Batch API
= 性能语义
Transaction / Idempotency / Reconciliation
= 正确性语义

两者不能互相替代。

flowchart LR
A["Messages + recent context"] --> B["Read existing memories<br/>dedup / context / relation hint"]
B --> C["Single-pass extraction<br/>ADD only"]
C --> D["Batch embed + hash dedup"]
D --> V["Vector Store"]
V --> H["History: ADD"]
H --> E["Entity Store"]
E --> S["Session Messages"]
V -. crash .-> X1["Vector exists<br/>History missing"]
H -. partial item failure .-> X2["History may say ADD<br/>Vector item missing"]
E -. warning only .-> X3["Entity projection incomplete"]

图 3:依据 Mem0 固定版本源码 [S7] 还原 V3 ADD-only 自动提取及顺序写入。Vector、History、Entity 与 Session 之间存在 Crash Window;operation_id、幂等、逐存储状态与 Reconciliation 是本文建议的生产化补强,不代表 Mem0 OSS 已提供跨存储事务。

7.19 Mem0 的边界#

Extraction Loss

Raw Event
Extractor Miss
Memory Never Exists

Recall 再强也救不回来。

Semantic Conflict

V3 刻意保留新旧事实而不在自动提取阶段覆盖历史,能够减少错误删除,却也会让 Memory 数量持续增长。Flat Memory Records 对时序冲突没有 Temporal Graph 天然;“旧偏好 + 新偏好”能否正确解析,依赖时间表达、候选召回与读时排序。需要控制体量时,应使用 Expiration、显式 Delete 或外部清理策略,而不是假定 Extractor 会自动合并。

Model Dependence

Memory Quality 受:

Extraction Prompt
LLM
Conversation Context

影响。

Retrieval Tuning

Semantic Weight
BM25 Weight
Entity Boost
Threshold
TopK
Reranker

都需要评估。

7.20 Mem0 论文结果如何正确理解#

Mem0 论文作者在 LoCoMo 实验中报告:

相对 OpenAI Memory baseline
LLM-as-a-Judge 指标相对提升约 26%
相对 full-context
p95 latency 降低约 91%
token cost 节省超过 90%

这些是 论文作者报告的实验结果,不能直接推导为“任何业务接入 Mem0 都获得同样提升”。

真实效果取决于:

Workload
Memory Distribution
Model
Evaluation Set
Retrieval Configuration

7.21 Mem0 的范式#

Messages
Context Gathering
Existing Memory Retrieval
LLM Extraction
(Single-pass ADD-only)
Batch Embedding
Exact Dedup
ADD Persistence + ADD History
Entity Linking
Hybrid Recall
Rerank

显式 update() / delete() 位于这条自动流水线之外,是按 memory_id 触发的管理控制面。

Mem0 代表:

Memory as a Production Pipeline


8. Letta / MemGPT:Agent-Controlled Memory Hierarchy#

独立源码深挖:Letta / MemGPT Memory:Memory Block Prompt ABI、Shared State、Archival / Conversation Search 与 Context Paging

8.1 Virtual Context Management#

MemGPT 的经典思想借鉴操作系统内存层级:

LLM Context Window
≈ RAM
External Memory
≈ Disk
Agent
≈ Memory Manager

真正的问题是:

当全部信息无法同时位于 Context 时,谁决定什么留在快层、什么进入慢层、什么时候重新加载?

这是:

Context Allocation Problem

8.2 Letta 把 Context Hierarchy 做成显式抽象#

Letta 当前官方 Context Hierarchy 包含:[S8]

Memory Blocks
Files
Archival Memory
External RAG

它们是四种不同的 Context Access Model。

Layer是否常驻 ContextAgent 可编辑典型访问
Memory Blocksmemory tools
Files部分读取通常只读open / grep / semantic_search
Archival Memorysemantic search tools
External RAG依实现custom tools / MCP

原则:

Importance ↑
Scale ↓
→ Put closer to Context
Importance ↓
Scale ↑
→ Put farther from Context

8.3 Memory Block:Hot Memory#

Memory Block:

Persistent
In-context
Editable

例如:

/persona
/user_profile
/project_context
/current_strategy

优点:

Recall Miss = 0

因为不需要搜索。

缺点:

Context Tax every turn

所以:

Always-visible Memory 用 Context Cost 换 Retrieval Certainty。

8.4 Memory 正式进入 Agent Action Space#

Letta 公开源码:

letta/functions/function_sets/base.py

统一 Memory Management Tool 支持:

create
str_replace
insert
delete
rename

Agent 可以:

Reason
发现 Preference 变化
memory.str_replace(...)

因此:

Memory Management is an Agent Action.

8.5 rethink_memory:Whole-block Consolidation Primitive#

Letta 有 rethink_memory

核心落点是:

agent_state.memory.update_block_value(
label=target_block_label,
value=new_memory,
)

源码:letta/functions/function_sets/base.py

函数语义要求:

保留所有仍有效信息
移除 outdated / inconsistent 内容
整合新信息
形成 organized / readable / comprehensive block

这就是 Memory Consolidation。

Consolidation Unit 是:

One Memory Block

所以 Letta 的维护逻辑可以理解为:

Block-local Semantic Rewrite

8.6 为什么 Whole-block Rewrite 很重要#

旧 Block:

User is a Java developer.
User prefers Spring Boot.
User lives in Beijing.
User is learning Python.

新事件:

User moved to Shenzhen.
User now primarily works on Agent engineering.

简单 Append:

User lives in Beijing.
User moved to Shenzhen.

冲突。

Whole-block Rewrite:

Read whole block
Integrate new facts
Remove outdated / inconsistent info
Rewrite canonical state

得到:

User primarily focuses on Agent engineering.
User has strong Java/Spring Boot experience and is learning Python.
User currently lives in Shenzhen.

这是 Canonical Memory State,而不是 Event Log。

8.7 Archival Memory:Cold Semantic Memory#

Archival Memory 不常驻 Context。

Agent 使用:

archival_memory_insert
archival_memory_search

适合:

self-contained facts
meeting summaries
project updates
past events

可以理解为:

Memory Block
=
Hot Canonical State
Archival Memory
=
Cold Searchable Memory

8.8 Conversation Search 和 Archival Search 不同#

Letta 还提供 Conversation Search。

它搜索:

prior conversation history

Archival Search 搜索:

agent explicitly stored long-term memory

所以:

Conversation History
=
What happened
Archival Memory
=
What agent chose to preserve

不要把“过去说过的话”自动视为“长期知识”。

8.9 Files:与 Claude Code 的交汇#

Letta Files 支持:

open
close
semantic_search
grep

与 Claude Code 有明显共通点。

区别:

Claude Code
=
Filesystem is the natural world of Coding Agent
Letta
=
Files are one tier in a generalized context hierarchy

8.10 如何选择 Memory Tier#

每轮都必须知道

User name
Critical persona
Current long-term goal
Core project constraint

→ Memory Block

偶尔召回

三个月前会议
历史讨论
某次任务总结

→ Archival Memory

大型资料

几百页文档
几十个文件

→ Files

百万级 Corpus

→ External RAG

可以抽象:

def route_memory(item):
if must_always_be_visible(item):
return MEMORY_BLOCK
if is_large_document(item):
return FILE
if is_recallable_experience(item):
return ARCHIVAL_MEMORY
return EXTERNAL_RAG

8.11 Compaction 与 Memory Block 不同#

Long-running Agent:

Message 1
Message 2
...
Message 10000

需要:

Older Messages
Compaction
Compact History

但:

Compacted History
Memory Block

Compaction 解决当前运行连续性。

Memory Block 保存明确持久化的高价值 Canonical Context。

8.12 Context Distance#

Memory Block:

Always visible
No retrieval miss
High context cost

Archival:

Out of context
Low context cost
May miss retrieval

可以定义一个概念:

Context Distance

越靠近模型:

Access Faster
Recall More Certain
Cost Higher
Capacity Smaller

越远:

Capacity Larger
Cost Lower
Need Retrieval
Recall Uncertain

Memory Hierarchy 的本质,是把不同重要度、规模、召回频率的信息放在不同 Context Distance。

8.13 Memory Block 是 Prompt ABI,不只是数据库字段#

固定版本的 Block schema/ORM 至少包含 labeldescriptionvaluelimitread_only、template 信息和 metadata。[S8] 这些字段并非都只是管理后台展示属性。letta/schemas/memory.py 编译 Context 时会把 Block 渲染为:

<memory_blocks>
<human>
<description>...</description>
<metadata>
- read_only=true
- chars_current=820
- chars_limit=2000
</metadata>
<value>...</value>
</human>
</memory_blocks>

这意味着 Block Schema 实际上构成 Agent 可见的 Prompt ABI:

  • label 决定语义分区和工具寻址。
  • description 告诉模型该区应该保存什么。
  • limit 既是写入约束,也把容量压力显式暴露给模型。
  • read_only 是 Agent Action Space 的权限声明。
  • value 才是具体记忆内容。

BlockManager 还显式维护 PROMPT_AFFECTING_BLOCK_FIELDS = {description, label, limit, read_only, value}。更新时先检测 no-op;只要这些字段改变,就为所有连接该 Block 的 Agent 重建 system prompt。这揭示了一个常被忽略的一致性问题:

Database block updated
Every active agent immediately observes the new prompt

共享 Block 会把一次写入扩散到多个 Agent,因此实现必须处理 Prompt Cache Invalidation、正在执行 Run 的 snapshot 边界,以及只读 Block 的授权校验。Letta 的编译与重建路径把这种扩散显式化了,而不是假设数据库更新自然等于模型 Context 更新。

检索工具也体现了分层契约。conversation_search 支持 role 与时间范围,并使用 text + semantic hybrid search;archival_memory_search 支持 tags、any/all 匹配、时间窗口和 TopK。两者看起来都是“搜过去”,但前者的检索单元是消息与会话顺序,后者的检索单元是 Agent 主动沉淀的长期对象。把它们合并成一个无类型向量集合,会丢失 role、时间和写入意图。

8.14 Letta / MemGPT 的范式#

Agent Context
┌───────────────┐
│ Memory Blocks │
│ Always-on │
└───────┬───────┘
Files
Partial Read / Search
Archival Memory
Semantic Recall
External RAG
Massive Corpus

Letta 代表:

Memory as an Agent-controlled Context Hierarchy


9. Graphiti:Temporal Context Graph 与动态事实记忆#

独立源码深挖:Graphiti Temporal Memory:Episode、Entity Resolution、Fact Invalidation、Saga 双 Watermark 与 Hybrid Graph Search

9.1 Vector Memory 的真正弱点:世界会变化#

2024:
Alice works at Company A.
2025:
Alice joins Company B.
2026:
Alice becomes CTO of Company C.

Vector DB 保存:

M1: Alice works at A
M2: Alice joins B
M3: Alice is CTO of C

Query:

Where does Alice work?

Semantic Search 可能返回 M1、M2、M3。

但问题不是:

哪个文本更像 Query?

而是:

哪个 Fact 在当前时间有效?

这是 Temporal State Problem。

9.2 Graphiti 构建的是 Temporal World Model#

核心对象:

Episode
Entity
Entity Edge / Fact
Community

简化:

Episode
=
发生了什么
Entity
=
涉及谁 / 什么对象
Fact Edge
=
对象之间什么关系
Temporal Fields
=
事实何时有效 / 何时失效

例如:

Episode E1
2025-01-01
"Alice joined Company B."
Entity:
Alice
Company B
Fact:
Alice --WORKS_AT--> Company B
valid_at:
2025-01-01

后续:

Episode E2
2026-01-01
"Alice left B and joined C."

系统不应删除旧事实,而应形成:

Alice --WORKS_AT--> B
valid_at = 2025-01-01
invalid_at = 2026-01-01
Alice --WORKS_AT--> C
valid_at = 2026-01-01
invalid_at = null

这就是 Temporal Fact Evolution。

9.3 created_atvalid_at#

Graphiti Episode 同时维护:

created_at
valid_at
created_at#

系统什么时候摄取 Episode。

Ingestion / Processing Time
valid_at#

事件在真实世界的参考时间。

Event Time

今天是 2026-07-15,今天导入:

“2024-03-01,Alice 加入 Company A。”

则:

created_at = 2026-07-15
valid_at = 2024-03-01

如果只有 created_at,系统可能误认为 Alice 2026 年才加入 A。

所以:

When system learned it 与 When the fact was true 必须分开。

9.4 invalid_at:失效不等于删除#

Graphiti 冲突处理源码有一个关键赋值:

edge.invalid_at = resolved_edge.valid_at

源码:graphiti_core/utils/maintenance/edge_operations.py

旧事实:

Alice works at A
valid_at = 2025-01

新事实:

Alice works at B
valid_at = 2026-03

旧 Edge 可变成:

valid_at = 2025-01
invalid_at = 2026-03

含义:

Old Fact
不是“永远错误”
而是:
曾经为真
现在失效

对 CRM、订单状态、员工关系、组织结构、资产状态、项目状态非常关键。

9.5 add_episode() 的真实 Write Path#

源码入口:

graphiti_core/graphiti.py
Graphiti.add_episode(...)

流程:

Episode Input
Retrieve Previous Episodes
Create EpisodicNode
Extract Nodes
Resolve Nodes / Entity Resolution
Extract Edges / Facts
Resolve Edges
├── Duplicate
├── New
└── Contradictory / Invalidated
Extract Entity Attributes
Build Episode Provenance Edges
Persist Nodes + Edges + Episode
Optional Community Update

这是完整的 Temporal Knowledge Ingestion Pipeline。

9.6 为什么 Write Path 先 Recall Previous Episodes#

当前 Episode:

She left the company last Friday.

没有历史:

She = ?
the company = ?
last Friday relative to what?

Previous Episodes 提供:

Entity Reference
Temporal Resolution
Fact Context

这说明:

Memory Write 也可能需要先读取旧 Memory。

9.7 Entity Resolution:Identity Canonicalization#

新 Episode:

Jupiter fixed the bug.

旧 Graph:

Wang Shuai
王帅
Jupiter

如果不做 Entity Resolution:

Node 1: Wang Shuai
Node 2: 王帅
Node 3: Jupiter

记忆碎片化。

Graph Memory 的质量高度依赖:

Entity Extraction
+
Entity Resolution

这不是简单 NER,而是:

Identity Canonicalization

9.8 Fact Edge 需要 Provenance 与 Time#

Graphiti Edge 可包含:

source entity
target entity
relation type
fact text
valid_at
invalid_at
episode attribution
reference_time

可以抽象成:

Fact {
subject
predicate
object
statement
valid_time
invalid_time
source_episode
reference_time
}

Episode Attribution 让系统能回答:

这个事实从哪里来的?

企业 Agent 不能只说:

“Memory里有。”

而应追溯到具体 Episode / Source。

Graphiti 先对:

(source UUID, target UUID, normalized fact)

做 Exact Dedup。

然后:

Create Embeddings
Get Existing Edges Between Nodes
Hybrid Search Related Edges
Hybrid Search Invalidation Candidates

又是:

Cheap deterministic processing
Expensive semantic processing

9.10 Duplicate Candidate 与 Invalidation Candidate 分离#

新 Fact:

Alice works at Company B.

已有:

Alice is employed by Company B.

可能是 Duplicate。

另一个:

Alice works at Company A.

可能是 Contradiction / Temporal Replacement。

因此:

Related Edges
→ Duplicate Resolution
Broader Existing Edges
→ Invalidation Detection

Graphiti 甚至会去除两种候选集合中的重叠项,避免同一 Edge 进入两个决策路径。

9.11 冲突处理核心是时间窗口#

结构化逻辑:

For each old edge:
如果 Old invalid_at <= New valid_at
时间不重叠
skip
如果 New invalid_at <= Old valid_at
时间不重叠
skip
如果 Old valid_at < New valid_at
Old invalid_at = New valid_at
mark invalidated

因此:

A works at Company A
2019 → 2021
A works at Company B
2022 → now

虽然 Relation Type 相同,但并不冲突,因为时间区间不重叠。

9.12 expired_atinvalid_at 不应混淆#

概念上:

invalid_at
→ 事实在现实世界何时不再有效
expired_at
→ 系统何时将该记录标记为失效/过期状态

即:

World Time
vs
System Lifecycle Time

业务事实时间和存储生命周期时间是两个维度。

9.13 Saga 双 Watermark:非常高级的时序设计#

当前 summarize_saga 维护:

last_summarized_at
last_summarized_episode_valid_at
last_summarized_at#

Wall-clock / Ingestion Time。

作用:

下次找“新摄取”的 Episode
last_summarized_episode_valid_at#

Event Time。

作用:

摘要覆盖到真实世界哪个时间点

考虑 Backfill:

今天 2026-07-15
导入一条 2024-01-01 历史事件

Episode:

created_at = 2026-07-15
valid_at = 2024-01-01

如果增量 Summary Filter 使用:

valid_at > last_valid_at

2024 的 Backfill 可能被漏掉。

所以增量处理应看 created_at,保证今天新摄取的数据进入下一轮。

但对外表达 Summary 覆盖到哪个事件时间,又应该看 valid_at

因此:

Processing Watermark
+
Event-time Watermark

这已经是流处理系统中的经典问题。

Temporal Memory 开始接近事件流与时态数据库问题,而不是 Prompt 技巧。

Graphiti 中 Valid Time、Ingestion Time、迟到 Backfill 与双 Watermark 的关系

图 4:依据 Graphiti 固定版本的时间字段与 Saga 水位线 [S9] 绘制。旧事实的 invalid_at 表示业务有效区间结束,迟到 Episode 则说明 Processing Time 与 Event Time 必须分别推进。

9.14 Search:Graph Memory 不是 Traversal Only#

Graphiti Search 支持多个 Signal:

BM25 / Full-text
Cosine Similarity
BFS

Reranker 可包括:

RRF
MMR
Cross Encoder
Episode Mentions

源码根据 Search Config 动态构造 Search Tasks,并并发执行。一个关键并发语义可以从短句看出:

search_results = list(await semaphore_gather(*search_tasks))

源码:graphiti_core/search/search.py

固定版本的 SearchConfig 进一步把检索配置拆到四种结果类型:[S9]

Retrieval UnitCandidate Method可选 Reranker
EdgeBM25、Cosine、BFSRRF、MMR、Cross Encoder、Node Distance、Episode Mentions
NodeBM25、Cosine、BFSRRF、MMR、Cross Encoder、Node Distance、Episode Mentions
EpisodeBM25RRF、Cross Encoder
CommunityBM25、CosineRRF、MMR、Cross Encoder

每种 config 还有自己的 sim_min_scoremmr_lambdabfs_max_depth,顶层再用 limitreranker_min_score 控制最终输出。官方 recipe 甚至分别提供 combined RRF、combined MMR 和 combined cross-encoder 方案,而不是宣称存在唯一最佳检索器。

Graphiti 的 RRF 源码实现为:

scoreRRF(u)=j1rankj(u)+c\operatorname{score}_{\mathrm{RRF}}(u)=\sum_j\frac{1}{\operatorname{rank}_j(u)+c}

它只依赖各通道排名,不要求 BM25、cosine 和 BFS 的分数处于同一量纲。MMR 则在相关性与候选间冗余之间权衡:

MMR(d)=λsim(q,d)(1λ)maxsSsim(d,s)\operatorname{MMR}(d)=\lambda\operatorname{sim}(q,d)-(1-\lambda)\max_{s\in S}\operatorname{sim}(d,s)

这里的工程含义是:RRF 解决异构信号融合,MMR 解决有限 Context 中的结果多样性,Cross Encoder 解决高成本精排,Node Distance/Episode Mentions 则利用图结构与来源频次。它们解决的不是同一个问题,不能只用离线 Recall@K 选一个“总冠军”。

SearchFilters 同时支持 node labels、edge types、edge UUID、property filters,以及 valid_at / invalid_at / created_at / expired_at 的组合比较。时间过滤应尽量下推到 Candidate Generation,而不是先召回过期事实再让 LLM 猜哪条有效;后者既浪费 TopK,也容易让冲突内容同时进入 Context。

因此:

Multi-channel Candidate Generation
Fusion / Reranking

Graph 只是结构和检索信号之一。

9.15 为什么同时 Search Edge、Node、Episode、Community#

不同 Query 的最佳 Retrieval Unit 不同。

“Alice现在在哪工作?”
→ Edge
“Alice是谁?”
→ Node
“上次项目事故发生了什么?”
→ Episode
“数据库迁移讨论的主要主题?”
→ Community

所以:

Memory Retrieval Unit 应与 Query Intent 对齐。

这比把所有信息统一切成 Chunk 更精细。

9.16 Graphiti 的成本和风险#

Entity Resolution Error

错误合并两个实体会污染整张图。

Entity Fragmentation

同一实体被拆成多个 Node,Recall 不完整。

Temporal Extraction Error

相对时间被解析错,会破坏状态演化。

Relation Ontology Drift

WORKS_AT
EMPLOYED_BY
MEMBER_OF

可能表示相近关系。

Infrastructure Cost

需要:

Graph DB
LLM Extraction
Embedding
Hybrid Search
Reranker

因此,不要因为 Graphiti 技术先进就给所有聊天机器人上 Temporal Graph。

它最适合:

事实持续变化
关系重要
历史需要保留
多跳关系查询
Temporal Reasoning

9.17 Zep / Graphiti 论文结果如何理解#

Zep 论文作者报告:

DMR:
94.8%
vs MemGPT 93.4%
LongMemEval:
部分评估准确率提升最高约 18.5%
相对基线降低约 90% 响应延迟

这是论文作者报告的实验结果。

它说明 Temporal Knowledge Graph 路线在动态企业 Memory 中有潜力,但不能脱离数据分布泛化到所有 Agent。

9.18 Graphiti 的范式#

Event / Episode
Entity Extraction
Entity Resolution
Fact Extraction
Temporal Annotation
Duplicate Resolution
Conflict / Invalidation
Graph Persistence
Hybrid Retrieval
Temporal World State

Graphiti 代表:

Memory as an Evolving Temporal World Model


10. Mem0、Letta、Graphiti:三种框架路线比较#

维度Mem0Letta / MemGPTGraphiti
核心问题如何抽取和召回 MemoryMemory 放在哪一层动态事实如何演化
核心抽象Memory RecordContext HierarchyTemporal Context Graph
WritePipeline ExtractionAgent Memory ActionEpisode Ingestion
Agent Control通常较低
Hot Memory依 Context BuilderMemory Blocks查询后注入
Cold MemoryVector StoreArchival / Files / RAGGraph
RetrievalSemantic + BM25 + Entity + RerankAlways-on + File + SemanticBM25 + Vector + BFS + Rerank
ConflictADD-only 保留历史,读时排序;显式 CRUD 独立Block RewriteTemporal Invalidation
TemporalExpiration依内容设计原生 valid/invalid time
ProvenanceHistory / Metadata依 StoreEpisode Attribution
最强项Production PipelineAgent-controlled HierarchyDynamic World State
主要代价Extraction/Retrieval 调优Context Routing ComplexityGraph/Entity/Temporal Complexity

一句话:

Mem0
→ 什么值得抽成 Memory,如何高效找回?
Letta
→ Memory 应该离 Context 多远,由谁移动?
Graphiti
→ 世界变化后,如何保留历史又知道现在?

三者可以组合:

Mem0-like Extraction Pipeline
Memory Router
├── Hot Important State → Letta-style Block
├── Episodic Memory → Vector / Archival
└── Temporal Business Facts → Graphiti

Codex、Claude Code、Hermes、Mem0、Letta 与 Graphiti 六种记忆架构的机制对比

图 5:六个案例分别强调后台经验巩固、文件层级、受限自管理、工业化抽取、上下文分层与时态世界模型。横轴不是优劣排序,而是控制权与表示能力的变化。

Part IV:工业级架构设计、工程难题与场景选型#

11. 六个系统背后的五种 Memory 范式#

11.1 System-Managed Memory#

代表:

Codex
Mem0

控制流:

Event
System Pipeline
Extraction
Evaluation
Persistence

优势:

统一治理
可观测
容易执行安全策略
Agent不需要主动记

问题:

Extractor可能漏记
系统未必准确判断当前Agent主观上的Future Utility

适合:

Enterprise Agent
High-throughput Agent
Centralized Memory Platform

11.2 Memory as File#

代表:

Claude Code
Hermes
Letta Files

核心:

Memory
=
Human/Agent-readable artifacts

优势:

Readable
Editable
Auditable
Versionable
Tool-native
Hierarchical

边界:

Large-scale Fuzzy Recall
Semantic Conflict
Index Maintenance

适合:

Coding Agent
Research Agent
Local-first Agent

11.3 Agent-Controlled Memory#

代表:

Hermes
Letta

Memory 操作进入 Agent Action Space:

memory.add
memory.replace
memory.remove
memory.rethink
archival_memory_insert
archival_memory_search

优势:

Agent知道当前任务上下文
Agent能判断主观Future Utility
可主动整合

风险:

Agent乱写
过度记忆
Memory Tool Loop
Memory Poisoning
Wrong Forgetting

因此必须配:

Budget
Guardrail
Tool Limit
Validation
Audit

11.4 Hierarchical Memory#

代表:

Claude Code
Letta

核心:

不是所有Memory离Model一样近

可以抽象成:

L0
Always-on Core Memory
L1
Indexed / Topic Memory
L2
Semantic Archive
L3
External Massive Knowledge

需要两个 Router。

Memory Write Router

Information
Which Level?

Memory Retrieval Router

Query
Which Level to Search?

11.5 Temporal World Model#

代表:

Graphiti

核心:

Memory is not a bag of sentences.
Memory is an evolving state of entities and relations.

适合回答:

Who?
What?
Relationship?
When?
Changed From?
Changed To?
What is valid now?

它把 Memory 从 Retrieval Problem 升级成 State Evolution Problem。

11.6 真实生产系统通常是组合#

Agent Runtime
Memory Write Router
┌───────────────────┼───────────────────┐
│ │ │
User Profile Experience Business State
│ │ │
Mem0-like Episodic / Graphiti-like
Pipeline Procedural Temporal Graph
│ │ │
└───────────────────┼───────────────────┘
Context Builder

所以不要问:

Mem0、Letta、Graphiti 哪个绝对最好?

正确问题是:

我的 Workload 中存在什么类型的 Memory Problem?


12. 一个完整工业级 Agent Memory Harness 应该长什么样#

综合六个系统,可以抽象出八个核心模块。

Event Sources
Memory Capture
Memory Extractor
Memory Evaluator
Memory Router
Memory Stores
Memory Retriever
Context Builder
Agent Runtime
Execution / Feedback
Memory Consolidator
Memory Governance

生产级 Agent Memory 控制面中的 Canonical Memory、派生索引、策略门、审计、墓碑与修复任务

图 6:Canonical Memory 是权限、版本与来源的真相源;Vector、BM25、Graph 和 Summary 都是可修复的派生投影,删除由 Tombstone 向所有读路径传播。

12.1 Memory Capture#

职责:

统一接收 Agent 运行事件

建议 Event Schema:

from datetime import datetime
from typing import Any, Literal
from pydantic import BaseModel, Field
class AgentEvent(BaseModel):
event_id: str
trace_id: str
session_id: str
actor_id: str | None = None
event_type: Literal[
"user_message",
"assistant_message",
"tool_call",
"tool_result",
"state_transition",
"error",
"human_feedback",
"task_result",
"evaluation",
]
payload: dict[str, Any]
occurred_at: datetime
sensitivity: Literal[
"public",
"internal",
"confidential",
"restricted",
] = "internal"
metadata: dict[str, Any] = Field(default_factory=dict)

建议进一步增加:

ingested_at

使:

occurred_at
=
Event Time
ingested_at
=
Processing Time

这是从 Graphiti 双时间语义得到的直接工程启发。

交付语义:接受重复,不接受不可检测的丢失#

Memory Capture 通常跨越业务数据库、消息队列和后台 Worker。追求端到端 exactly-once 往往成本很高,更现实的契约是:

Producer: Transactional Outbox
Broker: at-least-once delivery
Consumer: idempotent apply
Store: unique(pipeline_version, event_id)

pipeline_version 必须进入幂等键,因为升级 Extractor 后可能需要对同一个源事件重新抽取。重放任务还要记录 from_watermarkto_watermark、配置版本与模型版本,使“线上写入”和“离线回填”不会互相覆盖。

建议补充三张控制表:

memory_capture_offset(consumer, partition, watermark)
memory_job(job_id, state, lease_owner, lease_until, retry_count)
memory_dead_letter(job_id, event_id, error_class, payload_ref)

监控不应只看 queue lag,还要看 oldest_unprocessed_event_age、重复消费率、dead-letter 增长和 watermark 停滞时间。队列长度为零不等于完整,有可能事件根本没有从业务事务发布出来。

12.2 Memory Extractor#

职责:

Event Stream
Memory Candidate

建议输出:

from datetime import datetime
from typing import Literal
from pydantic import BaseModel, Field
class MemoryCandidate(BaseModel):
content: str
kind: Literal[
"profile",
"semantic",
"episodic",
"procedural",
"temporal_fact",
]
subject_ids: list[str] = Field(default_factory=list)
source_event_ids: list[str]
confidence: float
importance: float
reusability: float
valid_at: datetime | None = None
invalid_at: datetime | None = None
expires_at: datetime | None = None
sensitivity: str

Extractor 不应该直接写 Store。

推荐:

Extractor
Candidate
Evaluator / Policy
Write Decision

否则:

LLM一输出
永久持久化

风险过高。

12.3 Memory Evaluator#

职责:

Should Write?

至少考虑:

Confidence
Importance
Reusability
Freshness
Sensitivity
Duplicate Probability
Conflict Probability

一个工程启发式示例:

def memory_utility(candidate: MemoryCandidate) -> float:
benefit = (
candidate.importance
* candidate.reusability
* candidate.confidence
)
risk_penalty = {
"public": 0.0,
"internal": 0.05,
"confidential": 0.30,
"restricted": 1.00,
}.get(candidate.sensitivity, 0.20)
return benefit - risk_penalty

再次强调:这是工程示例,不是学术标准公式。

高风险系统中:

Restricted Memory
Never auto-write
Confidential Memory
Policy / Encryption / Retention
Low-confidence Memory
Episodic only
not canonical semantic state

12.4 Memory Router#

职责:

Candidate
Which Store / Tier?

示例:

from enum import Enum
class MemoryTarget(str, Enum):
CORE = "core"
EPISODIC = "episodic"
PROCEDURAL = "procedural"
TEMPORAL_GRAPH = "temporal_graph"
DROP = "drop"
def route_memory(candidate: MemoryCandidate) -> MemoryTarget:
if candidate.confidence < 0.5:
return MemoryTarget.DROP
if candidate.kind == "temporal_fact":
return MemoryTarget.TEMPORAL_GRAPH
if candidate.kind == "procedural":
return MemoryTarget.PROCEDURAL
if (
candidate.importance > 0.9
and candidate.reusability > 0.8
):
return MemoryTarget.CORE
return MemoryTarget.EPISODIC

这里直接融合:

Claude / Letta Hierarchy
+
Mem0 Pipeline
+
Graphiti Temporal Store

12.5 Memory Store#

不要只定义:

save(text)
search(query)

更合理的统一协议:

from typing import Protocol
class MemoryStore(Protocol):
async def add(self, candidate: MemoryCandidate):
...
async def update(
self,
memory_id: str,
candidate: MemoryCandidate,
):
...
async def invalidate(
self,
memory_id: str,
*,
invalid_at,
reason: str,
):
...
async def search(self, query):
...
async def history(self, memory_id: str):
...

不同 Store:

FileMemoryStore
VectorMemoryStore
BlockMemoryStore
TemporalGraphMemoryStore
Store Contract 必须表达版本和部分失败#

上面的协议还缺两个生产关键字段:expected_versionoperation_id。没有前者,两个会话同时更新用户 Profile 时会 last-write-wins;没有后者,超时重试无法判断第一次写是否已经成功。

class MutationResult(BaseModel):
operation_id: str
memory_id: str
previous_version: int | None
committed_version: int
canonical_applied: bool
pending_projections: list[str]
class VersionConflict(Exception):
current_version: int

推荐写序:

1. BEGIN canonical transaction
2. check expected_version / authorization
3. write canonical row or tombstone
4. append change_log + projection_outbox
5. COMMIT
6. async apply vector/BM25/graph/summary projections
7. acknowledge projection offsets

Read Path 不能盲信派生索引。检索命中 (memory_id=m1, version=7) 后,注入前批量回查 canonical store;如果当前是 version 9、invalid 或 deleted,就丢弃旧命中或重新取内容。这是对 eventual consistency 的显式闭环。

还需要 Reconciliation Job 定期比较:

canonical active IDs
vs vector IDs
vs lexical IDs
vs graph provenance IDs

它输出 missing projection、orphan projection、version drift 和 tombstone leak。Mem0 固定版本的 best-effort 多存储写入,正好说明这一层不能被 SDK 的成功返回值替代。[S7]

12.6 Memory Retriever#

Retriever 不应该只有:

semantic_search()

推荐:

Query
Intent Analysis
Retrieval Plan
├── Core Memory
├── BM25
├── Dense Vector
├── Entity Lookup
├── Temporal Graph
└── File Search
Candidate Fusion
Rerank
Context Selection

示例 Query Schema:

class MemoryQuery(BaseModel):
text: str
user_id: str | None = None
agent_id: str | None = None
run_id: str | None = None
needs_temporal_reasoning: bool = False
needs_procedural_memory: bool = False
max_context_tokens: int = 2000

路由:

async def retrieve_memory(query: MemoryQuery):
candidates = []
candidates.extend(
await core_store.get_always_visible(query)
)
if query.needs_procedural_memory:
candidates.extend(
await procedural_store.search(query.text)
)
if query.needs_temporal_reasoning:
candidates.extend(
await temporal_graph.search(query.text)
)
candidates.extend(
await episodic_store.hybrid_search(query.text)
)
return rerank_and_budget(
query=query,
candidates=candidates,
)
Retrieval 必须能解释“为什么这条被选中”#

每个候选建议保留分通道特征,而不是只返回一个无法解释的 score

class RetrievalCandidate(BaseModel):
memory_id: str
version: int
lexical_rank: int | None
dense_rank: int | None
graph_distance: int | None
entity_overlap: float
freshness: float
confidence: float
scope_match: float
reranker_score: float | None
selected: bool
rejection_reason: str | None

这样才能区分三类问题:

Candidate miss 正确记忆没有进入候选集
Ranking miss 进入了候选集但排序太低
Packing miss 排名足够高但被 Context Budget 丢弃

三类问题的修复方向完全不同。只记录最终 TopK 会让调参变成猜测。

一个可审计的启发式排序可以写成:

S(m,q)=wrR+wfF+wcC+wsS+wtTwpPS(m,q)=w_rR+w_fF+w_cC+w_sS+w_tT-w_pP

其中 RR 是检索相关性,FF 是 freshness,CC 是 confidence,SS 是 scope match,TT 是 task utility,PP 是风险或冲突惩罚。权重必须通过业务 Eval Set 与消融实验选择,而不是凭经验长期固定。

12.7 Context Builder#

Retriever 返回 20 条 Memory,不代表全部进入 Prompt。

Context Builder 应做:

Dedup
Conflict Marking
Freshness Check
Temporal Validity
Sensitivity Check
Token Budget
Diversity
Ordering
Compression

建议输出结构化 Context:

<user_profile>
...
</user_profile>
<current_project_memory>
...
</current_project_memory>
<relevant_experience>
...
</relevant_experience>
<temporal_facts as_of="2026-07-15">
...
</temporal_facts>

而不是:

Here are some memories:
- ...
- ...
- ...

Memory Context 需要 Semantic Segmentation。

Conflict Set 应作为一个整体进入 Builder#

如果两条 Memory 互相冲突,分别排序会把它们当成独立候选。更合理的是先形成 Conflict Set:

subject=user_42
predicate=works_at
M8: Acme, valid=[2024-01, 2025-03), source=user
M9: Beta, valid=[2025-03, +∞), source=user
M10: Acme, valid=unknown, source=old_web_page

对“现在在哪工作”只注入 M9,并保留“由 M8 演化而来”的 provenance;对“2024 年在哪工作”选择 M8;对“历史变化”同时注入 M8/M9,但标注有效区间。M10 不是简单按相似度竞争,而应因来源旧、时间未知被降权或隔离。

最终 Prompt 中每段 Memory 都应带稳定 ID、版本、来源类型、有效期和 trust label。模型答案再回传 used_memory_ids,才能计算真正的 Memory Attribution,而不是把所有已注入内容都算作“使用过”。

从并行检索、候选融合、重排、冲突集、权限过滤到 Token Packing 的 Memory 诊断漏斗

图 7:这是本文综合六个系统得到的生产检索路径,不对应某一个框架。Candidate Miss、Ranking Miss 与 Packing Miss 必须分别记录,否则“正确 Memory 没进入答案”只能得到一个无法行动的总失败率。

12.8 Memory Consolidator#

它对应:

Codex Phase 2
Hermes agent-driven consolidation
Letta rethink_memory

输入:

Existing Memory
+
New Candidates
+
Usage Statistics
+
Task Outcomes

输出:

Merge
Rewrite
Promote
Demote
Invalidate
Delete

建议决策 Schema:

class ConsolidationDecision(BaseModel):
action: Literal[
"keep",
"merge",
"rewrite",
"promote",
"demote",
"invalidate",
"delete",
]
source_memory_ids: list[str]
target_memory: MemoryCandidate | None = None
reason: str

Consolidation 应拥有 Change Log:

M12 + M18 + M33
MERGE
M57
Reason:
three entries describe same Gradle test workflow

否则 Memory Debugging 会非常痛苦。

12.9 Memory Governance#

至少需要:

Namespace Isolation
Access Control
Encryption
PII Detection
Secret Redaction
Prompt Injection Scan
Retention
Expiration
Audit
User Delete
Data Export
Versioning

Memory 比普通日志更危险。

因为:

日志
→ 通常供人或系统查看
Memory
→ 会主动重新进入模型 Context
→ 改变未来行为

因此被污染的 Memory 具有:

Behavioral Persistence

这就是 Memory Poisoning 比普通脏数据更危险的原因。

Governance 是 Policy Decision Point,不是后台清理脚本#

每次 Write、Read、Export、Consolidate、Delete 都应经过相同的 Policy Decision Point:

principal + purpose + namespace + action + sensitivity + source_trust
allow / deny / review

用户删除流程需要生成 deletion job,而不是同步返回后即宣称完成:

REQUESTED → CANONICAL_TOMBSTONED → PROJECTIONS_PURGED
→ CACHES_PURGED → BACKUP_POLICY_RECORDED → VERIFIED

每一步记录证据和截止时间;读路径从 CANONICAL_TOMBSTONED 开始就必须 deny。对于由多条源记忆生成的 summary/skill,还需要 provenance DAG:删除一个源时,系统才能判断派生物应完全删除、重新生成还是仅移除对应陈述。


13. Agent Memory 真正困难的十个工程问题#

13.1 Write Problem:到底什么值得记#

Memory 最难的问题可能不是 Search,而是 Write Precision。

写太多:

Memory Pollution

写太少:

Memory Recall Ceiling

因为没写入的信息永远检索不到。

所以:

Memory System 的质量上限经常从 Write Path 开始决定。

13.2 Retrieval Problem:相关不等于有用#

Query:

如何修复支付模块测试?

Memory A:

支付模块去年重构。

语义相关。

Memory B:

支付模块集成测试必须先启动 Docker Compose。

真正有用。

所以 Ranking 不能只有 Semantic Relevance。

还应该考虑:

Task Utility
Procedural Applicability
Freshness
Confidence

13.3 Context Problem:召回多少才够#

TopK 太小:

Missing Evidence

TopK 太大:

Context Pollution

最终目标不是 Retrieval Recall 最大,而是:

在有限 Context Budget 内最大化 Downstream Task Utility。

13.4 Conflict Problem:新旧记忆冲突#

冲突类型:

Preference Change
dark → light
World State Change
works_at A → works_at B
Correction
Agent guessed X → User says wrong
Source Disagreement
Tool A says X → Tool B says Y

不同冲突需要:

Replace
Invalidate
Keep both with provenance
Lower confidence
Request verification

不能统一使用:

newest wins

13.5 Temporal Problem:哪个事实现在有效#

Temporal Memory 要回答:

What was true?
When?
Until when?
What is true now?
When did system learn it?

至少考虑:

Event Time
Ingestion Time
Validity Interval
System Expiration

一旦 Agent 面向真实业务状态,时间语义会迅速成为核心问题。

13.6 Consolidation Problem:100 条经验如何变成 5 条规律#

Agent 有:

30 次 Gradle 错误经验
20 次 Docker 测试经验
10 次 Migration 经验

直接存 60 条,Memory 不断膨胀。

理想结果:

Project Build Procedure
Integration Test Procedure
Migration Troubleshooting Procedure

即:

Episodes
Pattern Discovery
Procedure

真正强的 Memory 系统最终需要 Experience Distillation

13.7 Forgetting Problem:Agent 必须学会遗忘#

应该遗忘:

Expired Temporary State
Low-value Noise
Superseded Preference
Duplicated Experience
Incorrect Memory
Security-sensitive Data

不会遗忘:

Memory Volume ↑
Context Noise ↑
Conflict ↑
Retrieval Cost ↑

所以:

Forgetting is a feature.

13.8 Poisoning Problem:攻击可以跨 Session 持久化#

Malicious Tool Result
Memory Extractor
Persistent Memory
Future Session
System Context Injection

攻击从 Single-turn Injection 升级为 Persistent Behavioral Injection。

防御需要:

Trust Boundary
Source Classification
Content Scan
Instruction-like Text Detection
Write Policy
Sanitized Injection
Human Audit

Hermes 的 Sanitized Snapshot 是很好的工程参考。

OWASP 已把 Memory Poisoning 明确为 Agentic Application 风险:持久 Memory 可被直接/间接 Prompt Injection、身份混淆和跨 Session 复用污染,其影响不在当前回答结束,而会持续改变未来行为。[S11]

完整威胁模型至少覆盖:

攻击面示例主要控制
Direct write用户要求“以后忽略审批”写策略、protected predicates、人工确认
Indirect write网页/邮件把恶意指令伪装成事实source trust、指令检测、隔离区
Scope escalation项目事实被写成组织级规则namespace authorization、scope ceiling
Cross-tenant collision相同 entity name 合并两个租户tenant-bound entity key、硬过滤
Consolidation poisoning多条低可信 Episode 被总结成高可信规则trust 不得因 merge 自动提升
Retrieval injectionMemory 内容命令模型调用危险工具data/instruction 隔离、工具授权不继承
Deletion evasion原始记录删了,summary/embedding 仍存在lineage DAG、tombstone、purge verification

一个关键不变量是:

derived_trust <= max(source_trusts)

更保守的系统甚至采用最小来源信任级别。Consolidator 可以提高表达质量和复用度,但不能凭一次 LLM 总结把不可信网页内容“洗白”为系统规则。

防御应分四道门:

Capture: classify source + preserve raw evidence
Write: policy + schema + protected fields + quarantine
Read: authorization + freshness + sanitized serialization
Use: tool permissions and approvals remain independent of memory

最后一道尤其重要:即使 Memory 写着“允许部署生产”,执行工具仍必须根据当前 principal 和环境重新授权。Memory 只能提供上下文,不能签发 Capability。

运行层面还需要不可变 mutation log、周期快照、内容 hash、异常写入速率告警和一键 rollback。Hash 只能发现变化,不能判断变化是否恶意;因此它必须与来源、策略和行为 Eval 组合,而不是单独充当“防投毒”。

Agent Memory 写入投毒防线、顺序删除的部分失败窗口与可验证删除状态机

图 8:A 区依据 OWASP Agent Memory Guard [S11] 的检测与策略路径绘制;B 区对应 Mem0 [S7] 的顺序 best-effort 删除;C 区是本文提出的生产化补强。只有 Projection ACK、Reconciliation 与 DELETED_VERIFIED 完成后,才能证明派生索引和缓存不再可读。

13.9 Privacy Problem:能记不等于应该记#

个人 Agent 很容易接触:

Health
Financial
Credentials
Private Relationship
Precise Location
Internal Secrets

原则:

Can remember
Should remember

Memory Write Policy 需要:

Data Minimization
Purpose Limitation
Retention
Deletion
User Control
Privacy 的难点是派生数据,而不只是原文#

一条敏感事实可能同时存在于:

Raw Event
Extracted Fact
Embedding
Entity / Edge
Rollout Summary
Consolidated Profile
Prompt Cache
Evaluation Trace
Backup

因此每个 Memory Record 需要 data_subject_idspurposelegal_basis/consent_refretention_classsource_event_idsderived_from。没有 lineage,系统无法回答“删除 Alice 的资料会影响哪些 summary 和 graph edge”。

推荐把敏感字段分成三层:

Restricted raw value
→ encrypted field / tokenized reference
Prompt-safe projection
→ only the minimum fact needed for the task
Search feature
→ non-reversible or separately protected representation

Embedding 不是天然匿名化。它仍可能泄漏语义、被成员推断,且无法像结构化字段那样精确局部删除。对高敏内容,优先存受控引用或最小化 projection;删除时执行 canonical tombstone、索引 purge、缓存失效与密钥销毁,并对备份记录可验证的到期策略。

NIST AI RMF Generative AI Profile 强调全生命周期风险管理、数据治理、隐私、安全与可测量性。[S12] 对 Memory 系统而言,落地形式不是一段隐私声明,而是可查询的数据地图、保留规则、访问日志、删除 SLA 与定期恢复演练。

13.10 Evaluation Problem:怎么证明 Memory 真有用#

不要单独用:

Memory Count
Memory Hit Rate

评价系统。

最关键的是:

Downstream Task Improvement

推荐指标:

层级指标
WriteMemory Write Precision
WriteMemory Write Recall
WriteDuplicate Write Rate
WriteSensitive Memory Write Rate
RetrievalRecall@K
RetrievalContext Precision
RetrievalRelevant Memory MRR
ConflictConflict Resolution Accuracy
TemporalTemporal Accuracy
FreshnessStale Memory Rate
SecurityMemory Poisoning Success Rate
ContextAvg Memory Context Tokens
SystemMemory Add Latency
SystemRecall Latency
E2ETask Success Rate
E2ERepeated Task Improvement
E2EToken Saving vs Full Context
评测必须拆开 Write、Retrieve、Read 与 E2E#

只看最终回答无法定位失败。推荐建立四层数据集:

Layer 1 - Write Set
events → should_write / should_not_write / canonical target / scope
Layer 2 - Retrieval Set
query + as_of_time → required memory IDs + forbidden stale IDs
Layer 3 - Context Set
candidate pool + budget → expected evidence set / conflict handling
Layer 4 - Agent Task Set
history + task → success, cost, latency, tool errors, human intervention

Layer 1 衡量抽取与写策略,Layer 2 衡量索引和排名,Layer 3 衡量 Context Builder,Layer 4 才衡量系统是否获得能力。每层都保存中间 artifact,才能把 E2E 失败归因到 not writtennot retrievednot packedretrieved but ignored

LongMemEval 将长期对话记忆拆成信息抽取、多 Session 推理、时序推理、知识更新和 abstention;2026 年的 LongMemEval-V2 又把 Web Agent 经验扩展为静态状态、动态状态、workflow、environment gotchas 与 premise awareness,并使用最长 500 条 trajectory、约 1.15 亿 Token 的历史。[S10] 这说明“能回答用户生日”不足以代表 Agent Experience Memory;还要评测操作流程、环境规律、失败条件和前提变化。

基线、消融与时间切分#

至少比较:

No Memory
Recent Window Only
Full History(可放入时)
BM25 Only
Dense Only
Hybrid Retrieval
Hybrid + Reranker
Full Memory System
Oracle Evidence

Oracle Evidence 给出该任务所需的正确 Memory,用来估计 Reader/Agent 上限;如果 Oracle 也失败,继续调 Retriever 没有意义。消融应逐项关闭 entity linking、temporal filter、consolidation、freshness、conflict resolution,测量每个模块的增益和成本。

数据集必须按时间切分:先让系统消费过去的 Run,再评测未来任务。随机把同一用户或同一仓库的近重复记录分到训练/测试,会产生严重泄漏。涉及 LLM Judge 时还要固定 Judge 版本、盲化系统名称、抽样人工复核,并报告多次运行的均值与置信区间。

在线实验要同时看收益与伤害#

Memory A/B 的 Guardrail 至少包括:

Task success / repeated-task improvement
P50/P95 first-action and total latency
Input tokens and retrieval cost
Wrong personalization / stale assertion rate
Sensitive write rate
Poisoning attack success rate
User correction and memory deletion rate

平均成功率上涨可能掩盖少量高影响错误。例如个人偏好召回提升 3%,但跨租户泄漏一次,系统仍不可上线。安全、隐私和越权指标应是 release gate,而不是与准确率加权后被抵消的普通分数。

Memory Write Precision

写入的 Memory 中
真正值得长期保留的比例

Memory Write Recall

应该被保留的信息中
实际写入多少

Context Precision

进入当前 Prompt 的 Memory 中
真正帮助当前任务的比例

Stale Memory Rate

被召回但已经失效的 Memory 比例

Repeated Task Improvement

这是最关键的 Agent 指标之一:

第一次执行任务
vs
拥有历史经验后再次执行类似任务

比较:

Task Success
Tool Calls
Retries
Latency
Token Cost
Human Intervention

如果 Memory 存了很多,但第二次任务没有更快、更准、少犯错,那么 Memory 没有产生能力价值。


14. 不同 Agent 场景如何选择 Memory 架构#

14.1 Coding Agent#

推荐:

File Memory
+
Procedural Experience
+
Background Consolidation

参考:

Claude Code
Codex
Hermes

架构:

Core:
project conventions
build commands
Topic Files:
debugging
architecture
tool quirks
Background:
task rollout extraction
Procedural:
verified workflows

14.2 Personal Assistant#

推荐:

User Profile
+
Semantic Memory
+
Temporal Facts

可组合:

Mem0-like pipeline
+
Graphiti-like temporal layer

原因是 Preference、Goal、Relationship、Location、Job 等都会变化。

不能只做 Vector TopK。

14.3 CRM / Customer Intelligence Agent#

推荐:

Temporal Graph

核心对象:

Customer
Company
Contact
Opportunity
Contract
Ticket
Product

关系:

works_at
owns
interested_in
negotiating
signed
cancelled
escalated

这些关系具有明显时间变化。

14.4 Long-running Autonomous Agent#

推荐:

Letta-like Hierarchy
+
Compaction
+
Procedural Memory

需要:

Hot Blocks
Cold Archive
Compacted History
External Knowledge

14.5 Customer Support Agent#

推荐:

Mem0-like User Memory Pipeline
+
Business State from Source of Truth
+
Optional Temporal Graph

特别强调:

订单状态、退款状态等业务事实不能只依赖 Agent Memory。

应该:

Authoritative Business Data
→ Tool / DB
Memory
→ User preference / interaction history / reusable context

即:

Source of Truth
Memory

这与 Codex 的:

Policy
Memory

是同一个工程思想。

14.6 Research Agent#

推荐:

File Memory
+
Episodic Memory
+
Provenance

需要记录:

Search Path
Sources Read
Hypotheses
Rejected Hypotheses
Interim Findings
Citation Evidence

不能只存最终 Summary。

14.7 Self-Evolving Agent#

推荐:

Experience Memory
Outcome Evaluation
Pattern Mining
Procedural Memory
Skill Lifecycle

可以借鉴:

Codex Phase 1/2
Hermes Learning Loop
Letta Agent-controlled Memory

关键原则:

失败轨迹不能直接变成 Procedure。

必须:

Experience
Outcome / Verifier
High-quality Experience
Consolidation
Procedure

否则 Agent 会把错误经验固化。

14.8 Multi-Agent System#

建议分:

Private Memory
Shared Memory
Role Memory
Task Memory

例如:

Coordinator
Private:
routing experience
Policy Agent
Private:
policy interpretation procedure
Shared:
case state
verified evidence
human decisions

Shared Memory 必须拥有:

Ownership
Schema
Write Permission
Conflict Policy

不能让所有 Agent 对同一个 Vector DB 随意 add()


Part V:结论、工程速查与延伸阅读#

15. 结论:Memory 正从 Storage 走向 Cognitive Infrastructure#

回看六个系统:

Codex
→ Experience Extraction + Global Consolidation
Claude Code
→ Filesystem Memory + Progressive Disclosure
Hermes
→ Bounded Agent-controlled Curated Memory
Mem0
→ Production Memory Pipeline
Letta
→ Agent-controlled Context Hierarchy
Graphiti
→ Temporal World Model

它们共同指向一个趋势:

Memory
不再只是
“把过去存起来”
而是
“管理过去如何影响未来决策”

Agent Memory 的演进可以表示为:

Chat History
Conversation Summary
Vector Memory
Memory Extraction Pipeline
Hierarchical Memory
Agent-controlled Memory
Temporal Memory
Experience Consolidation
Procedural Memory
Cognitive Infrastructure

未来真正优秀的 Memory System,需要同时回答五个问题。

What to Remember

什么值得记?

Where to Place It

Hot Context、File、Archive 还是 Temporal Graph?

When to Recall

什么时候主动 Push,什么时候让 Agent Pull?

How to Evolve

新事实如何更新、旧事实如何失效、经验如何巩固?

When to Forget

什么时候必须删除、过期或降级?

所以,Memory 的终点不是:

无限存储

而是:

把有限 Context 变成一个可持续更新的认知工作集。

真正强的 Agent,也不是“记得所有事情”。

而是:

知道什么值得记住,什么时候应该回忆,什么时候应该修正过去,什么时候应该遗忘,以及如何把一次执行转化成下一次任务的能力。

把全文进一步压缩成一个工程判断:

Memory Engineering
= Data Pipeline
+ Context Engineering
+ State Management
+ Information Retrieval
+ Temporal Modeling
+ Security
+ Agent Policy
+ Evaluation

当 Agent 从一次性 Demo 进入长期运行系统,Memory 就不再是“可选增强”或 Storage Engineering 的附属功能,而是:

Agent Cognitive Infrastructure。


附录 A:六个系统的统一 Memory Lifecycle 映射#

SystemCaptureExtractEvaluateWriteRetrieveConsolidateForget/Invalidate
CodexRolloutPhase 1 AgentEligibilityBackground JobLocal RecallPhase 2 AgentRetention / prune
Claude CodeSession learningClaudeAgent判断MarkdownIndex + file toolsFile organizationEdit/delete
HermesSession observationAgentAgent + capacityMemory ToolFrozen SnapshotReplace / BatchRemove
Mem0MessagesLLMExtraction PipelineADD-only Batch StoreHybrid Search无写时合并,读时排序Expiration / explicit delete
LettaAgent interactionAgentAgentBlock/Archive ToolIn-context + searchrethink_memoryRewrite/delete
GraphitiEpisodeNode/Edge extractionResolutionTemporal GraphHybrid Graph SearchSaga summaryFact invalidation

附录 B:工程选型速查表#

核心问题优先研究
历史任务如何形成未来经验Codex
Coding Agent 项目上下文如何组织Claude Code
Agent 如何自己管理有限 MemoryHermes
如何快速搭生产 Memory PipelineMem0
Long-running Agent 如何管理多层 ContextLetta
动态事实、关系、时间状态怎么管理Graphiti
需要长期事实 + 用户偏好Mem0
需要当前状态与历史状态并存Graphiti
需要 Always-on 核心记忆Letta / Hermes
需要透明可审计的本地 MemoryClaude Code / Hermes
需要任务经验后台沉淀Codex
需要 Experience → ProcedureCodex + Hermes + Letta 思想组合

附录 C:建议的源码阅读路线#

Codex#

codex-rs/memories/write/src/phase1.rs
codex-rs/memories/write/templates/memories/stage_one_system.md
codex-rs/memories/write/src/phase2.rs
codex-rs/memories/write/templates/memories/consolidation.md
codex-rs/state/src/runtime/memories.rs

阅读问题:

1. Rollout Eligibility 怎么判断?
2. 为什么需要 Lease?
3. Phase 1 为什么可以并行?
4. Raw Memory 与 Rollout Summary 分工是什么?
5. Phase 2 为什么全局串行?
6. Watermark 如何推进?
7. Git Workspace Diff 为什么适合 Memory Consolidation?
8. Consolidation Agent 权限如何限制?

Hermes#

tools/memory_tool.py
tools/threat_patterns.py
Prompt Assembly
Skills / Curator

阅读问题:

1. Frozen Snapshot 和 Live State 如何分离?
2. 为什么 Sanitization 不直接删除磁盘原始内容?
3. Add 为什么可以弱化 Drift Guard?
4. Replace/Remove 为什么需要 Round-trip Check?
5. File Lock 如何保证并发安全?
6. Capacity Failure 如何反馈给 Agent?
7. 为什么限制每 Turn Consolidation Retry?
8. apply_batch 如何实现最终状态事务?

Mem0#

mem0/memory/main.py
configs/prompts
memory/storage
utils/entity_extraction
utils/scoring
vector_stores

阅读问题:

1. infer=True/False 两条 Write Path 有什么区别?
2. Session Scope 为什么是必需约束?
3. Existing Memory 为什么在 ADD-only Extraction 前检索?
4. UUID → Integer 映射为什么必须沿 Payload 持久化路径验证?
5. LLM Failure 和 Empty Extraction 如何区分?
6. Batch Embed/Insert 如何降级?
7. Hash、BM25、Entity 各解决什么问题?
8. Candidate Pool 为什么 Over-fetch?

Letta#

letta/functions/function_sets/base.py
letta/services/tool_executor/core_tool_executor.py
letta/schemas/memory.py
Agent State
Prompt Assembly / Compaction

阅读问题:

1. Memory Block 为什么常驻 Context?
2. memory tool 如何进入 Tool Executor?
3. rethink_memory 与普通 replace 有什么区别?
4. Conversation Search 和 Archival Search 的数据语义有何不同?
5. Memory Hierarchy 如何影响 Context Cost?
6. Agent 何时应该主动移动信息层级?

Graphiti#

graphiti_core/graphiti.py
utils/maintenance/node_operations.py
utils/maintenance/edge_operations.py
edges.py / nodes.py
search/search.py
search/search_config_recipes.py

阅读问题:

1. Episode、Entity、Fact 的边界是什么?
2. created_at 与 valid_at 为什么必须分离?
3. Entity Resolution 如何避免碎片?
4. Duplicate Candidate 与 Invalidation Candidate 为什么分开?
5. Temporal Contradiction 如何判断?
6. invalid_at 与 expired_at 的语义区别?
7. Saga 为什么需要双 Watermark?
8. BM25、Vector、BFS 如何并行融合?

参考资料与源码#

以下资料均在 2026-07-15 联网核对。源码链接固定到本文开头记录的 Commit,避免 main 分支后续变化导致分析无法复现。

官方产品文档#

[S1] OpenAI Codex Memories#
  • Memories
  • 核对内容:本地 Codex Memory 与 ChatGPT Memory 的边界、后台生成、idle eligibility、secret redaction、~/.codex/memories/、任务级控制、Memory 与 AGENTS.md 的职责边界。
[S2] OpenAI Cookbook:Building Reliable Agents with Memory and Compaction#
[S3] Anthropic Claude Code Memory#
[S4] Nous Research Hermes Agent Persistent Memory#
  • Persistent Memory
  • 核对内容:bounded curated memory、MEMORY.mdUSER.md、2,200 / 1,375 字符预算、Frozen Snapshot、add/replace/remove 与容量满时的显式错误。

固定版本源码#

[S5] OpenAI Codex Source#
[S6] Hermes Agent Source#

框架、论文与官方实现说明#

[S7] Mem0#
[S8] Letta / MemGPT#
[S9] Zep / Graphiti#

评测、安全与交叉验证#

[S10] LongMemEval#
[S11] OWASP Agent Memory Security#
[S12] NIST AI RMF Generative AI Profile#
[S13] LangGraph Memory#
  • Memory overview
  • Add and manage memory
  • Persistence
  • 核对内容:thread-scoped state/checkpointer、cross-thread namespace/store、semantic/episodic/procedural memory、Profile vs Collection、hot-path vs background write。
Agent 记忆系统架构总览:Codex、Claude Code、Hermes、Mem0、Letta 与 Graphiti
https://jupiter-ws.cn/posts/ai-coding/agent-memory-system-deep-dive/
作者
Jupiter
发布于
2026-07-15
许可协议
CC BY-NC-SA 4.0