Agent 记忆系统架构总览:Codex、Claude Code、Hermes、Mem0、Letta 与 Graphiti
本文定位为 Agent Memory Engineering 专栏总览与选型地图:用统一生命周期横向比较六种路线;每个系统的源码、状态机、一致性与生产化细节进入独立深挖文章。
| 顺序 | 独立深挖 | 核心问题 |
|---|---|---|
| 1 | Codex Memory:两阶段语义 ETL、Git 巩固与可追溯召回 | 历史任务如何形成未来经验 |
| 2 | Claude Code Memory:CLAUDE.md、Auto Memory 与渐进式上下文披露 | 文件层级如何编译成动态 Instruction Working Set |
| 3 | Hermes Memory:受限自管理、快照隔离与事务化巩固 | Agent 如何在有限预算下安全地管理自己的记忆 |
| 4 | Mem0 Memory Pipeline:ADD-only 提取、混合检索与多存储一致性 | 如何把记忆做成可观测的数据管线 |
| 5 | Letta / MemGPT Memory:虚拟上下文与 Agent-controlled Paging | Agent 如何在 Hot / Cold Context 之间主动调页 |
| 6 | Graphiti Temporal Memory:双时间、实体消歧与事实失效 | 动态世界中的事实何时成立、何时失效 |
0. 阅读说明与研究边界
本文资料与源码基线截至 2026 年 7 月 15 日。
研究对象分成两组:
产品级实践:
- OpenAI Codex
- Anthropic Claude Code
- Nous Research Hermes Agent
开源记忆框架:
- Mem0
- Letta / MemGPT
- Graphiti
本文不会按照“短期记忆、长期记忆、向量记忆”依次做概念科普。真正要研究的是:
- Agent 到底应该记什么?
- 谁决定一条信息值得写入记忆?
- 什么时候写?
- 写入什么结构?
- 记忆属于哪个用户、Agent、任务或业务实体?
- Agent 如何找到过去真正有用的信息?
- 找到记忆后,多少内容应该重新进入 Context?
- 新旧记忆冲突怎么办?
- 历史事实发生变化怎么办?
- 记忆满了以后,谁决定删除什么?
- Memory 和 Context Compaction 是不是同一件事?
- 一次任务执行能否转化成未来可复用的经验?
0.1 引用方法与事实边界
本文只引用公开源码中的极少量关键签名或关键语句。较长流程统一使用 “结构化源码还原”:依据公开源码真实调用顺序整理成更易学习的伪代码,不逐字搬运原仓库。
Claude Code 的核心 Runtime 并未完整公开,因此 Claude Code 一章严格依据 Anthropic 官方公开行为契约做架构还原,不把推测伪装成源码事实。
0.2 源码版本与复现基线
| 系统 | 研究基线 |
|---|---|
| Codex | openai/codex,commit 8604689ec5e3437eb79802d8d72249b7722fbf5b |
| Hermes | NousResearch/hermes-agent,commit 77d5b2d573f82fc514fbef02f0b6303c44149805 |
| Mem0 | mem0ai/mem0,commit ccbe5861a138c7583e01bb3a3aa6168e52526a23 |
| Letta | letta-ai/letta,commit b76da9092518cbaa2d09042e52fdcbde69243e18 |
| Graphiti | getzep/graphiti,commit 526dcad7a300f3c5c506ff96a68bcdc7ca9f97ed |
| Claude Code | Anthropic 官方 Memory 文档行为契约 |
本文插图均为依据上述官方文档与固定版本源码重新绘制的机制图,用于压缩跨文件调用关系与故障语义;它们不是厂商官方架构图。图中若同时出现“当前实现”和“生产化补强”,会用分区和图注明确区分,避免把本文建议误读为框架已经提供的保证。
Part I:问题模型与统一生命周期
1. Agent Memory 真正解决什么问题
1.1 Context Window 不等于 Memory
大模型的 Context Window 解决的是:
当前这一次模型调用能够看到什么。
Memory 解决的是:
系统如何跨时间保存、组织、选择、修正并重新提供过去的信息。
Context Window=当前推理工作集
Memory System=跨时间的信息生命周期系统所以:
Raw History ≠ MemoryVector Database ≠ Memory SystemContext 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 → EndStateful 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 flywayMigrateintegration 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↓MemoryMemory 的第一道价值不是“存”,而是 从经历中提炼未来有用的信息。
1.4 Memory 和 RAG 的根本区别
RAG 的主要输入通常是:
Document CorpusMemory 的主要输入更接近:
Agent Experience Stream包括:
User MessageAgent MessageTool CallTool ResultExecution TraceErrorHuman FeedbackReflectionTask 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
图 1:Memory 不是单一存储组件,而是一条带写策略、检索预算和反馈维护的闭环数据管线。
这就是本文统一采用的 Agent Memory Lifecycle。
2.1 Capture:捕获什么事件
完整 Event Source 可以包括:
User MessageAssistant MessageTool CallTool ResultPlanState TransitionEnvironment ChangeErrorRetryVerifier ResultHuman ApprovalHuman CorrectionFinal Task ResultEvaluation ScoreCoding 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这里有三个不能省略的工程不变量:
- 事件不可变:修正通过追加
correction或supersedes事件完成,不原地改历史。 - 消费幂等:Extractor 以
(pipeline_version, event_id)去重;消息队列采用 at-least-once 投递时,重复事件不能生成重复记忆。 - 来源可追踪:Memory 永远保存
source_event_ids,否则它只是模型生成的无来源文本。
数据库事务与消息队列之间还需要 Transactional Outbox:业务状态与 outbox event 在一个本地事务中提交,再由 relay 发布。否则“任务已完成但事件没有发出”会形成永久的 Memory Recall Ceiling。
2.2 Extract:从事件中抽取什么
好的 Memory Candidate 应该尽量:
Self-containedFuture-usefulLow ambiguityScopedSource-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 / organizationmodality explicit / observed / inferredsource_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 + HumanMemory 架构的核心区别,经常不是“文件还是数据库”,而是:
谁拥有 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 在租约过期后覆盖新 Workerlease_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 EntryMemory BlockSummaryFactPreferenceEpisodeProcedureEntityRelationshipTemporal EdgeSkillTask ExperienceRepresentation 决定系统以后能回答什么问题。
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 CacheCanonical 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 InjectExact LookupMetadata FilterBM25Semantic SearchHybrid SearchGraph TraversalTemporal FilterAgentic File SearchHierarchical Routing一个可靠的 Retrieval Plan 通常是“硬过滤在前、召回在中、效用排序在后”:
Authorization / Namespace / Status / Valid-time Filter ↓BM25 ─┬─ Dense ─┬─ Entity ─┬─ Graph / Temporal └──────── Candidate Union ─────────┘ ↓RRF / Learned Fusion ↓Utility Rerank + DiversityRRF 不要求不同通道的原始分数可比,适合融合 BM25、向量和图检索:
但融合分数仍不是最终效用。生产排序常需要额外乘上或约束 confidence、freshness、scope_match、temporal_validity 与 permission。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 是有约束的组合优化
假设候选记忆 的预计任务效用为 ,Token 成本为 ,总预算为 ,最简单的目标是:
真实系统还要加入冲突对不能同时作为“当前事实”注入、同主题去重、来源多样性、核心记忆保底额度等约束。因此 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 需要:
ADDUPDATEMERGECONSOLIDATEINVALIDATEEXPIREDELETEFORGET如果系统只有 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 Control | Agent 有多少控制权 |
这个分析框架也能被其他主流实现交叉验证。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:后台记忆抽取与任务经验沉淀
3.1 Coding Agent 的 Memory Workload
Coding Agent 长期工作的真实环境包含:
RepositoryBuild SystemTest CommandsArchitecture ConventionsHidden ConstraintsTool QuirksPast Debugging ExperienceUser Workflow Preferences第一次任务发现:
mvn test → 失败检查仓库 → Gradle./gradlew test → 成功下一次再从 Maven 开始,说明历史任务没有转化为未来能力。
Codex Memory 的核心问题是:
过去任务中有哪些工作经验值得未来 Coding Task 复用?
3.2 官方公开的 Memory 边界
OpenAI 当前公开说明中有几个非常重要的行为约束:
- Local Codex 使用独立的本地 Memory Store。
- 从符合条件的历史任务中生成 Memory。
- 活跃或过短的 Session 可以被跳过。
- Memory 后台更新,而不是任务结束立即更新。
- 生成 Memory 字段会做 Secret Redaction。
- 主 Memory 文件位于
~/.codex/memories/。 - 必须遵守的团队规则应该放在
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.rscodex-rs/memories/write/src/phase2.rs能够看到非常明确的两阶段设计:
Historical Rollouts ↓Phase 1Per-Rollout Extraction ↓Raw Memory+Rollout Summary ↓Phase 2Global 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 MemoryvsCompact Routing Summary3.5 Eligibility:Memory Write 之前先决定“这次任务能不能写”
Phase 1 不是遍历全部历史直接总结,而是先 Claim Eligible Rollout Jobs。
当前源码参数包括:
scan_limitmax_claimedmax_age_daysmin_rollout_idle_hoursallowed_sourceslease_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 ARollout B → Raw Memory BRollout C → Raw Memory C在这一阶段可以独立处理。
它非常像数据系统中的 Map Stage。
3.7 Structured Output:Memory Extraction 是 Semantic ETL
Phase 1 使用受约束 JSON Schema,并禁止额外字段。
这不是为了“输出漂亮”。
而是让下游能够:
ValidatePersistIndexRouteConsolidateMeasure所以 Memory Extractor 的模型调用更适合理解为:
Semantic ETL Operator
而不是聊天节点。
3.8 Secret Redaction:Memory Write Path 是 Security Boundary
Coding Agent 可能看到:
API KeyDatabase PasswordPrivate TokenCredentialsInternal URL如果一次 Session 中的 Secret 被写入 Memory:
Session A↓Memory↓Session B↓Session C↓Session D风险被持久化并跨任务传播。
因此:
Memory Write Path 本身就是持久化安全边界。
生产系统至少要考虑:
PII DetectorSecret DetectorCredential RedactionSensitivity ClassificationNamespace ACLRetention PolicyAudit Log3.9 Phase 2:Global Consolidation
Phase 1 得到很多 Raw Memory:
ABC...直接全部塞进未来 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-AAgent 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 WorkspaceGit 天然提供:
BaselineDiffChanged FilesChange DetectionRollback FoundationAuditability因此 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 Runmessages...↓Context Too Long↓Compaction↓Current Run ContinuesMemory:
Run A↓Experience↓Memory
Run B↓Recall Run A Experience因此:
Compaction= Intra-run Context Lifecycle
Memory= Inter-run Experience LifecycleConversation 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 塞进上下文”,而是:
- 从已注入摘要提取关键词。
- 搜索
MEMORY.md注册表。 - 只打开 1~2 个最相关的 skill 或 rollout。
- 只有需要精确命令、错误文本或证据时,继续下钻。
- 没有命中就停止,而不是扩大扫描。
模板还给出理想的 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(...);}其请求/响应里有几个容易忽略的工程细节:
list和search都有 cursor、最大结果数与truncated,因此大目录不会假装一次返回完整结果。read使用 1-basedline_offset、max_lines和max_tokens双重预算,并返回实际起始行号。search支持多 query、上下文行数、大小写与 normalized 模式。- 匹配语义分为
Any、AllOnSameLine、AllWithinLines { line_count },可以表达“多个证据词必须出现在同一局部窗口”,比单字符串 grep 更适合查命令与错误链。
固定版本默认上限是 List 2,000、Search 200、Read 20,000 tokens;注入的 summary 另有 2,500 tokens 上限。这里的重点不是具体数字,而是每一层都有独立预算,工具返回明确声明截断,调用者才能决定是否翻页或下钻。
3.16 Local Backend 的 Path Sandbox
Memory 文件会被模型驱动的工具读取,因此本地路径解析本身就是安全边界。local.rs 与 local/path.rs 的实现拒绝:[S5]
Absolute Path / Windows PrefixParentDir (`..`)Hidden Path ComponentTraversing through a non-directorySymlink at any traversed componentSymlink 拒绝尤其关键。只做字符串级 starts_with(memory_root) 检查无法阻止:
memories/evidence → symlink to ~/.sshCodex 使用 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_idStage 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。

图 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]
这解决两个问题:
- 用户可以知道结论来自哪个长期记忆与历史任务。
- 系统可以统计哪些 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 LayerCodex 代表:
System-Managed Background Experience Memory
最值得学习的九点:
- Memory Extraction 从用户请求主链路移出。
- 任务稳定后再写 Memory。
- 单任务抽取与全局巩固分阶段。
- Memory Maintenance 本身可以交给专用 Agent。
- Policy 必须与 Recall Memory 分离。
- 读取采用 Summary → Registry → Evidence 的渐进披露。
- Memory 工具必须有分页、Token 上限与截断语义。
- 本地文件后端必须实施路径沙箱与 symlink 防护。
- 每条被使用的 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.mdWho writes: HumanPurpose: Instructions / Rules
Auto MemoryWho writes: ClaudePurpose: 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.mdAnthropic 当前公开行为是:
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 Memory4.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↓InjectClaude 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 files4.7 为什么文件系统在 Coding Agent 中可能优于 Vector DB
不是 File Memory 全面优于 Vector Memory,而是 Workload 不同。
项目本身已经是 Filesystem World
Coding Agent 已经有:
readwritegrepglobfindgit diff如果 Memory 也是文件,Agent 不需要新增一套操作抽象。
人类可读、可编辑
MEMORY.mddebugging.mdarchitecture.md可以直接审计。
Vector Store 中则可能是:
vector_idpayloadembeddingmetadata运维和人工编辑成本更高。
天然 Hierarchy
路径本身就是 Metadata:
memory/debugging/database.md表达:
memory→ debugging→ database确定性更强
如果 Index 写明:
数据库迁移经验见 database-migration.md读取路径非常稳定。
Vector Retrieval 则受:
Embedding ModelQuery FormulationTopKThresholdRanking影响。
Git Friendly
文件天然支持:
diffreviewrollbackversion4.8 File Memory 的边界
规模增长到:
1,000 topic files100,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 KBSession 10: 100 KBSession 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 SearchClaude Code 代表:
Memory as File + Progressive Context Disclosure
最值得学习的五点:
- Memory Storage 应贴合 Agent 原生工作环境。
- Hot Memory 必须有 Context Budget。
- Index 和 Detailed Memory 可以分离。
- Recall 可以由 Agent 主动探索,而不是全部自动 Push。
- 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 定义为:
boundedcuratedpersistent官方当前设计有两个文件:[S4]
MEMORY.mdAgent personal notes
USER.mdUser profile默认容量:
MEMORY.md = 2,200 charsUSER.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-agenttools/memory_tool.pyMemoryStore 显式维护:
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 Snapshotremains 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 Promptstill sees V10
Next Session↓Snapshot V12它不是数据库 MVCC 的同一实现,但 Context View 具有类似的 Session Snapshot 语义。
5.6 为什么用字符预算而不是 Token 预算
源码注释指出 Character Count 是 model-independent。
Token Count 会随:
TokenizerModel FamilyLanguage变化。
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 entriesorremove 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 FullAgent 再执行:
replaceremoveadd这就是:
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 InjectionHermes 对 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 V10B read V10A write V11-AB write V11-B导致 Lost Update。
Hermes 使用独立 .lock 文件互斥:
Unix → fcntlWindows → msvcrt这说明:
Memory Store 一旦成为持久化状态,就必须面对数据库系统同类的一致性问题。
Memory 不是 Prompt Feature,而是 State Infrastructure。
5.11 Drift Detection:防止 Tool 覆盖外部修改
Memory 文件可能被:
Manual EditPatch ToolShell AppendSister Session修改。
如果 Parser 不能 round-trip 某段未知内容,而 Replace/Remove 又整体重写文件:
Unknown Content↓Silent Data LossHermes 对 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 → failedreplace → failedadd → 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 nothingSuccess → 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 Tripreplace↓LLM Round TripaddBatch:
remove + replace + add↓one tool call减少:
LLM Round TripsContext ResendPartial Mutation Risk5.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 SessionHermes 代表:
Agent-Managed Bounded Curated Memory
最值得学习的七点:
- User Model 与 Agent Experience 分 Namespace。
- Always-on Memory 必须有限。
- Runtime Snapshot 和 Persistent State 可以分离。
- Memory Mutation 要有并发和数据损坏保护。
- Memory Write 必须防持久化 Prompt Injection。
- Memory Side Effect 不应阻塞主任务。
- 容量压力可以成为 Agent 主动 Consolidation 信号。
6. Codex、Claude Code、Hermes:三种产品路线比较
| 维度 | Codex | Claude Code | Hermes |
|---|---|---|---|
| 核心目标 | 复用任务经验 | 持续化项目上下文 | Agent 持续学习 |
| 控制者 | System | Agent + Filesystem | Agent |
| 写入时机 | Task idle 后后台写 | Session 中积累 | Session 中 Tool Mutation |
| 写入模式 | Extraction + Consolidation | File Notes | Add/Replace/Remove |
| 表示 | Raw Memory + Summary + Workspace | MEMORY.md + Topic Files | Bounded Entries |
| Hot Memory | Consolidated recall context | MEMORY.md 前 200 行/25KB | Frozen Snapshot |
| Consolidation | Phase 2 Agent | Topic organization | Agent replace/remove |
| Security | Secret Redaction | Context 不是 enforcement | Strict scan + sanitized snapshot |
| Policy Source | AGENTS.md / docs | CLAUDE.md / hooks | Prompt / tool policy |
| 最适合 | Coding Task Experience | Repo Context | Persistent 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 初始化包含:
EmbedderVector StoreLLMHistory DBOptional RerankerLazy Entity Store所以:
Memory≠Vector Store Wrapper而更接近:
Extraction+Identity Scope+Storage+History+Retrieval+Ranking+Entity Signal7.2 Scope 是强约束,不是装饰性 Metadata
当前 OSS Memory API 要求至少提供:
user_idagent_idrun_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 0Context Gathering
Phase 1Existing Memory Retrieval
Phase 2Single-pass ADD-only Extraction
Phase 3Batch Embedding
Phase 4 / 5CPU Processing + Hash Dedup
Phase 6Batch Persistence + ADD History
Phase 7Entity Linking
Phase 8Save Messages + ReturnPhase 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 中,这一步的目的不是让模型决定原地 UPDATE 或 DELETE,而是给去重、上下文补全和关联判断提供参照。
已有:
User prefers dark mode.新消息:
Actually, I use light mode now.Extractor 看到新消息后会得到一条新的事实:
ADD light mode最终:
dark modelight 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 映射成:
012...再交给 LLM。
真实 ID:
8df39fe1-...af77b902-...模型可能抄错、漏字符、幻觉一个 ID。
改成:
0: User prefers dark mode1: User works in Java2: 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 MemoriesNew MessagesLast K MessagesCustom 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 FactvsProvider Failure当前 Mem0 源码选择在 Extraction Failure 时抛出 LLMError。
这样上游才能:
RetryFallbackAlert这是数据管道的基本原则:
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_hashesseen_hashes它解决:
Exact Duplicate例如:
User prefers dark modeUser prefers dark mode但:
User prefers dark modeThe user likes dark themesHash 不同。
所以:
Hash Dedup=Cheap Exact Dedup
Semantic Logic=LLM / Retrieval / Entity Signals典型路线:
Cheap Deterministic Filter↓Expensive Semantic Processing7.12 text_lemmatized:Memory Retrieval 不应该押注 Vector Only
Mem0 为 Memory Text 保存 BM25 预处理文本。
例如查询:
ERR_PAYMENT_001Error Code、Identifier、Exact Term 可能更适合 Keyword Search。
所以:
Hybrid Retrieval 往往比单一 Dense Retrieval 更稳。
7.13 Batch Persist + History:事件类型必须按路径解释
Mem0 同时写:
Memory Record+History EventHistory 记录类似:
memory_idold_memorynew_memoryeventcreated_atis_deleted在 V3 自动提取的 Phase 6,所有 History Record 都是:
old_memory = nullnew_memory = extracted textevent = ADDis_deleted = 0UPDATE 和 DELETE 仍会出现在 History 中,但只来自调用方显式执行 update(memory_id, ...) 或 delete(memory_id) 的独立路径,不是 Extraction LLM 的自动决策。History 因而记录的是存储层实际发生的 Mutation,而不是 V3 LLM 输出的多操作 Change Set。
这类 Change History 支持:
AuditDebugEvaluationFailure AnalysisMemory 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 ScoreBM25 ScoreEntity BoostMemory 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这带来三个直接后果:
- 只被关键词精确命中、却没有进入
semantic_results的 Memory,后续 BM25 再高也无法“复活”。 internal_limit = max(limit × 4, 60)通过 Over-fetch 缓解召回上限,但不能消除语义候选池的单路瓶颈。- 如果 Semantic Threshold 在融合前过滤,强 lexical hit 仍可能提前丢失。
所以这一实现属于 semantic-first multi-signal reranking,不是严格意义上的 dense/sparse candidate union。生产系统若要求关键词召回兜底,需要显式合并两路 ID 集合,再做去重、归一化与排序。
而不是:
vector_search(top_k=5)↓done7.16 Expiration:Memory 不是永久事实
当前 OSS 支持 expiration_date。
过期 Memory 默认不会出现在普通 Search / Get All 中,除非显式要求查看。
例如:
User is visiting Tokyo this week.六个月以后如果仍被当成 Current Fact,就是 Stale Memory。
因此部分 Memory 必须拥有:
TTL / Expiration7.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 succeededbut 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_keyCanonical Operation LogOutbox / InboxPer-store apply statusRetryable Repair WorkerRead-time tombstone checkPeriodic Reconciliationclass 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 ExistsRecall 再强也救不回来。
Semantic Conflict
V3 刻意保留新旧事实而不在自动提取阶段覆盖历史,能够减少错误删除,却也会让 Memory 数量持续增长。Flat Memory Records 对时序冲突没有 Temporal Graph 天然;“旧偏好 + 新偏好”能否正确解析,依赖时间表达、候选召回与读时排序。需要控制体量时,应使用 Expiration、显式 Delete 或外部清理策略,而不是假定 Extractor 会自动合并。
Model Dependence
Memory Quality 受:
Extraction PromptLLMConversation Context影响。
Retrieval Tuning
Semantic WeightBM25 WeightEntity BoostThresholdTopKReranker都需要评估。
7.20 Mem0 论文结果如何正确理解
Mem0 论文作者在 LoCoMo 实验中报告:
相对 OpenAI Memory baselineLLM-as-a-Judge 指标相对提升约 26%
相对 full-contextp95 latency 降低约 91%token cost 节省超过 90%这些是 论文作者报告的实验结果,不能直接推导为“任何业务接入 Mem0 都获得同样提升”。
真实效果取决于:
WorkloadMemory DistributionModelEvaluation SetRetrieval Configuration7.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
8.1 Virtual Context Management
MemGPT 的经典思想借鉴操作系统内存层级:
LLM Context Window≈ RAM
External Memory≈ Disk
Agent≈ Memory Manager真正的问题是:
当全部信息无法同时位于 Context 时,谁决定什么留在快层、什么进入慢层、什么时候重新加载?
这是:
Context Allocation Problem8.2 Letta 把 Context Hierarchy 做成显式抽象
Letta 当前官方 Context Hierarchy 包含:[S8]
Memory BlocksFilesArchival MemoryExternal RAG它们是四种不同的 Context Access Model。
| Layer | 是否常驻 Context | Agent 可编辑 | 典型访问 |
|---|---|---|---|
| Memory Blocks | 是 | 是 | memory tools |
| Files | 部分读取 | 通常只读 | open / grep / semantic_search |
| Archival Memory | 否 | 是 | semantic search tools |
| External RAG | 否 | 依实现 | custom tools / MCP |
原则:
Importance ↑Scale ↓→ Put closer to Context
Importance ↓Scale ↑→ Put farther from Context8.3 Memory Block:Hot Memory
Memory Block:
PersistentIn-contextEditable例如:
/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 支持:
createstr_replaceinsertdeleterenameAgent 可以:
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_insertarchival_memory_search适合:
self-contained factsmeeting summariesproject updatespast events可以理解为:
Memory Block=Hot Canonical State
Archival Memory=Cold Searchable Memory8.8 Conversation Search 和 Archival Search 不同
Letta 还提供 Conversation Search。
它搜索:
prior conversation historyArchival 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 支持:
openclosesemantic_searchgrep与 Claude Code 有明显共通点。
区别:
Claude Code=Filesystem is the natural world of Coding Agent
Letta=Files are one tier in a generalized context hierarchy8.10 如何选择 Memory Tier
每轮都必须知道
User nameCritical personaCurrent long-term goalCore 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_RAG8.11 Compaction 与 Memory Block 不同
Long-running Agent:
Message 1Message 2...Message 10000需要:
Older Messages↓Compaction↓Compact History但:
Compacted History≠Memory BlockCompaction 解决当前运行连续性。
Memory Block 保存明确持久化的高价值 Canonical Context。
8.12 Context Distance
Memory Block:
Always visible↓No retrieval miss↓High context costArchival:
Out of context↓Low context cost↓May miss retrieval可以定义一个概念:
Context Distance
越靠近模型:
Access FasterRecall More CertainCost HigherCapacity Smaller越远:
Capacity LargerCost LowerNeed RetrievalRecall UncertainMemory Hierarchy 的本质,是把不同重要度、规模、召回频率的信息放在不同 Context Distance。
8.13 Memory Block 是 Prompt ABI,不只是数据库字段
固定版本的 Block schema/ORM 至少包含 label、description、value、limit、read_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 CorpusLetta 代表:
Memory as an Agent-controlled Context Hierarchy
9. Graphiti:Temporal Context Graph 与动态事实记忆
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 AM2: Alice joins BM3: Alice is CTO of CQuery:
Where does Alice work?Semantic Search 可能返回 M1、M2、M3。
但问题不是:
哪个文本更像 Query?而是:
哪个 Fact 在当前时间有效?
这是 Temporal State Problem。
9.2 Graphiti 构建的是 Temporal World Model
核心对象:
EpisodeEntityEntity Edge / FactCommunity简化:
Episode=发生了什么
Entity=涉及谁 / 什么对象
Fact Edge=对象之间什么关系
Temporal Fields=事实何时有效 / 何时失效例如:
Episode E12025-01-01"Alice joined Company B."
Entity:AliceCompany B
Fact:Alice --WORKS_AT--> Company B
valid_at:2025-01-01后续:
Episode E22026-01-01"Alice left B and joined C."系统不应删除旧事实,而应形成:
Alice --WORKS_AT--> Bvalid_at = 2025-01-01invalid_at = 2026-01-01
Alice --WORKS_AT--> Cvalid_at = 2026-01-01invalid_at = null这就是 Temporal Fact Evolution。
9.3 created_at 与 valid_at
Graphiti Episode 同时维护:
created_atvalid_atcreated_at
系统什么时候摄取 Episode。
Ingestion / Processing Timevalid_at
事件在真实世界的参考时间。
Event Time今天是 2026-07-15,今天导入:
“2024-03-01,Alice 加入 Company A。”则:
created_at = 2026-07-15valid_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 Avalid_at = 2025-01新事实:
Alice works at Bvalid_at = 2026-03旧 Edge 可变成:
valid_at = 2025-01invalid_at = 2026-03含义:
Old Fact不是“永远错误”
而是:曾经为真现在失效对 CRM、订单状态、员工关系、组织结构、资产状态、项目状态非常关键。
9.5 add_episode() 的真实 Write Path
源码入口:
graphiti_core/graphiti.pyGraphiti.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 ReferenceTemporal ResolutionFact Context这说明:
Memory Write 也可能需要先读取旧 Memory。
9.7 Entity Resolution:Identity Canonicalization
新 Episode:
Jupiter fixed the bug.旧 Graph:
Wang Shuai王帅Jupiter如果不做 Entity Resolution:
Node 1: Wang ShuaiNode 2: 王帅Node 3: Jupiter记忆碎片化。
Graph Memory 的质量高度依赖:
Entity Extraction+Entity Resolution这不是简单 NER,而是:
Identity Canonicalization
9.8 Fact Edge 需要 Provenance 与 Time
Graphiti Edge 可包含:
source entitytarget entityrelation typefact textvalid_atinvalid_atepisode attributionreference_time可以抽象成:
Fact { subject predicate object statement valid_time invalid_time source_episode reference_time}Episode Attribution 让系统能回答:
这个事实从哪里来的?企业 Agent 不能只说:
“Memory里有。”而应追溯到具体 Episode / Source。
9.9 Exact Dedup → Hybrid Candidate Search
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 processing9.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 DetectionGraphiti 甚至会去除两种候选集合中的重叠项,避免同一 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 A2019 → 2021
A works at Company B2022 → now虽然 Relation Type 相同,但并不冲突,因为时间区间不重叠。
9.12 expired_at 与 invalid_at 不应混淆
概念上:
invalid_at→ 事实在现实世界何时不再有效
expired_at→ 系统何时将该记录标记为失效/过期状态即:
World TimevsSystem Lifecycle Time业务事实时间和存储生命周期时间是两个维度。
9.13 Saga 双 Watermark:非常高级的时序设计
当前 summarize_saga 维护:
last_summarized_atlast_summarized_episode_valid_atlast_summarized_at
Wall-clock / Ingestion Time。
作用:
下次找“新摄取”的 Episodelast_summarized_episode_valid_at
Event Time。
作用:
摘要覆盖到真实世界哪个时间点考虑 Backfill:
今天 2026-07-15导入一条 2024-01-01 历史事件Episode:
created_at = 2026-07-15valid_at = 2024-01-01如果增量 Summary Filter 使用:
valid_at > last_valid_at2024 的 Backfill 可能被漏掉。
所以增量处理应看 created_at,保证今天新摄取的数据进入下一轮。
但对外表达 Summary 覆盖到哪个事件时间,又应该看 valid_at。
因此:
Processing Watermark+Event-time Watermark这已经是流处理系统中的经典问题。
Temporal Memory 开始接近事件流与时态数据库问题,而不是 Prompt 技巧。

图 4:依据 Graphiti 固定版本的时间字段与 Saga 水位线 [S9] 绘制。旧事实的 invalid_at 表示业务有效区间结束,迟到 Episode 则说明 Processing Time 与 Event Time 必须分别推进。
9.14 Search:Graph Memory 不是 Traversal Only
Graphiti Search 支持多个 Signal:
BM25 / Full-textCosine SimilarityBFSReranker 可包括:
RRFMMRCross EncoderEpisode Mentions源码根据 Search Config 动态构造 Search Tasks,并并发执行。一个关键并发语义可以从短句看出:
search_results = list(await semaphore_gather(*search_tasks))源码:graphiti_core/search/search.py
固定版本的 SearchConfig 进一步把检索配置拆到四种结果类型:[S9]
| Retrieval Unit | Candidate Method | 可选 Reranker |
|---|---|---|
| Edge | BM25、Cosine、BFS | RRF、MMR、Cross Encoder、Node Distance、Episode Mentions |
| Node | BM25、Cosine、BFS | RRF、MMR、Cross Encoder、Node Distance、Episode Mentions |
| Episode | BM25 | RRF、Cross Encoder |
| Community | BM25、Cosine | RRF、MMR、Cross Encoder |
每种 config 还有自己的 sim_min_score、mmr_lambda、bfs_max_depth,顶层再用 limit 和 reranker_min_score 控制最终输出。官方 recipe 甚至分别提供 combined RRF、combined MMR 和 combined cross-encoder 方案,而不是宣称存在唯一最佳检索器。
Graphiti 的 RRF 源码实现为:
它只依赖各通道排名,不要求 BM25、cosine 和 BFS 的分数处于同一量纲。MMR 则在相关性与候选间冗余之间权衡:
这里的工程含义是: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 / RerankingGraph 只是结构和检索信号之一。
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_ATEMPLOYED_BYMEMBER_OF可能表示相近关系。
Infrastructure Cost
需要:
Graph DBLLM ExtractionEmbeddingHybrid SearchReranker因此,不要因为 Graphiti 技术先进就给所有聊天机器人上 Temporal Graph。
它最适合:
事实持续变化关系重要历史需要保留多跳关系查询Temporal Reasoning9.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 StateGraphiti 代表:
Memory as an Evolving Temporal World Model
10. Mem0、Letta、Graphiti:三种框架路线比较
| 维度 | Mem0 | Letta / MemGPT | Graphiti |
|---|---|---|---|
| 核心问题 | 如何抽取和召回 Memory | Memory 放在哪一层 | 动态事实如何演化 |
| 核心抽象 | Memory Record | Context Hierarchy | Temporal Context Graph |
| Write | Pipeline Extraction | Agent Memory Action | Episode Ingestion |
| Agent Control | 中 | 高 | 通常较低 |
| Hot Memory | 依 Context Builder | Memory Blocks | 查询后注入 |
| Cold Memory | Vector Store | Archival / Files / RAG | Graph |
| Retrieval | Semantic + BM25 + Entity + Rerank | Always-on + File + Semantic | BM25 + Vector + BFS + Rerank |
| Conflict | ADD-only 保留历史,读时排序;显式 CRUD 独立 | Block Rewrite | Temporal Invalidation |
| Temporal | Expiration | 依内容设计 | 原生 valid/invalid time |
| Provenance | History / Metadata | 依 Store | Episode Attribution |
| 最强项 | Production Pipeline | Agent-controlled Hierarchy | Dynamic World State |
| 主要代价 | Extraction/Retrieval 调优 | Context Routing Complexity | Graph/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
图 5:六个案例分别强调后台经验巩固、文件层级、受限自管理、工业化抽取、上下文分层与时态世界模型。横轴不是优劣排序,而是控制权与表示能力的变化。
Part IV:工业级架构设计、工程难题与场景选型
11. 六个系统背后的五种 Memory 范式
11.1 System-Managed Memory
代表:
CodexMem0控制流:
Event↓System Pipeline↓Extraction↓Evaluation↓Persistence优势:
统一治理可观测容易执行安全策略Agent不需要主动记问题:
Extractor可能漏记系统未必准确判断当前Agent主观上的Future Utility适合:
Enterprise AgentHigh-throughput AgentCentralized Memory Platform11.2 Memory as File
代表:
Claude CodeHermesLetta Files核心:
Memory=Human/Agent-readable artifacts优势:
ReadableEditableAuditableVersionableTool-nativeHierarchical边界:
Large-scale Fuzzy RecallSemantic ConflictIndex Maintenance适合:
Coding AgentResearch AgentLocal-first Agent11.3 Agent-Controlled Memory
代表:
HermesLettaMemory 操作进入 Agent Action Space:
memory.addmemory.replacememory.removememory.rethinkarchival_memory_insertarchival_memory_search优势:
Agent知道当前任务上下文Agent能判断主观Future Utility可主动整合风险:
Agent乱写过度记忆Memory Tool LoopMemory PoisoningWrong Forgetting因此必须配:
BudgetGuardrailTool LimitValidationAudit11.4 Hierarchical Memory
代表:
Claude CodeLetta核心:
不是所有Memory离Model一样近可以抽象成:
L0Always-on Core Memory
L1Indexed / Topic Memory
L2Semantic Archive
L3External 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
图 6:Canonical Memory 是权限、版本与来源的真相源;Vector、BM25、Graph 和 Summary 都是可修复的派生投影,删除由 Tombstone 向所有读路径传播。
12.1 Memory Capture
职责:
统一接收 Agent 运行事件建议 Event Schema:
from datetime import datetimefrom 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 OutboxBroker: at-least-once deliveryConsumer: idempotent applyStore: unique(pipeline_version, event_id)pipeline_version 必须进入幂等键,因为升级 Extractor 后可能需要对同一个源事件重新抽取。重放任务还要记录 from_watermark、to_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 datetimefrom 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: strExtractor 不应该直接写 Store。
推荐:
Extractor↓Candidate
Evaluator / Policy↓Write Decision否则:
LLM一输出↓永久持久化风险过高。
12.3 Memory Evaluator
职责:
Should Write?至少考虑:
ConfidenceImportanceReusabilityFreshnessSensitivityDuplicate ProbabilityConflict 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 onlynot canonical semantic state12.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 Store12.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:
FileMemoryStoreVectorMemoryStoreBlockMemoryStoreTemporalGraphMemoryStoreStore Contract 必须表达版本和部分失败
上面的协议还缺两个生产关键字段:expected_version 与 operation_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 transaction2. check expected_version / authorization3. write canonical row or tombstone4. append change_log + projection_outbox5. COMMIT6. async apply vector/BM25/graph/summary projections7. acknowledge projection offsetsRead Path 不能盲信派生索引。检索命中 (memory_id=m1, version=7) 后,注入前批量回查 canonical store;如果当前是 version 9、invalid 或 deleted,就丢弃旧命中或重新取内容。这是对 eventual consistency 的显式闭环。
还需要 Reconciliation Job 定期比较:
canonical active IDsvs vector IDsvs lexical IDsvs 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 会让调参变成猜测。
一个可审计的启发式排序可以写成:
其中 是检索相关性, 是 freshness, 是 confidence, 是 scope match, 是 task utility, 是风险或冲突惩罚。权重必须通过业务 Eval Set 与消融实验选择,而不是凭经验长期固定。
12.7 Context Builder
Retriever 返回 20 条 Memory,不代表全部进入 Prompt。
Context Builder 应做:
DedupConflict MarkingFreshness CheckTemporal ValiditySensitivity CheckToken BudgetDiversityOrderingCompression建议输出结构化 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_42predicate=works_at
M8: Acme, valid=[2024-01, 2025-03), source=userM9: Beta, valid=[2025-03, +∞), source=userM10: 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,而不是把所有已注入内容都算作“使用过”。

图 7:这是本文综合六个系统得到的生产检索路径,不对应某一个框架。Candidate Miss、Ranking Miss 与 Packing Miss 必须分别记录,否则“正确 Memory 没进入答案”只能得到一个无法行动的总失败率。
12.8 Memory Consolidator
它对应:
Codex Phase 2Hermes agent-driven consolidationLetta rethink_memory输入:
Existing Memory+New Candidates+Usage Statistics+Task Outcomes输出:
MergeRewritePromoteDemoteInvalidateDelete建议决策 Schema:
class ConsolidationDecision(BaseModel): action: Literal[ "keep", "merge", "rewrite", "promote", "demote", "invalidate", "delete", ]
source_memory_ids: list[str] target_memory: MemoryCandidate | None = None reason: strConsolidation 应拥有 Change Log:
M12 + M18 + M33↓MERGE↓M57
Reason:three entries describe same Gradle test workflow否则 Memory Debugging 会非常痛苦。
12.9 Memory Governance
至少需要:
Namespace IsolationAccess ControlEncryptionPII DetectionSecret RedactionPrompt Injection ScanRetentionExpirationAuditUser DeleteData ExportVersioningMemory 比普通日志更危险。
因为:
日志→ 通常供人或系统查看
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 UtilityProcedural ApplicabilityFreshnessConfidence13.3 Context Problem:召回多少才够
TopK 太小:
Missing EvidenceTopK 太大:
Context Pollution最终目标不是 Retrieval Recall 最大,而是:
在有限 Context Budget 内最大化 Downstream Task Utility。
13.4 Conflict Problem:新旧记忆冲突
冲突类型:
Preference Changedark → light
World State Changeworks_at A → works_at B
CorrectionAgent guessed X → User says wrong
Source DisagreementTool A says X → Tool B says Y不同冲突需要:
ReplaceInvalidateKeep both with provenanceLower confidenceRequest verification不能统一使用:
newest wins13.5 Temporal Problem:哪个事实现在有效
Temporal Memory 要回答:
What was true?When?Until when?What is true now?When did system learn it?至少考虑:
Event TimeIngestion TimeValidity IntervalSystem Expiration一旦 Agent 面向真实业务状态,时间语义会迅速成为核心问题。
13.6 Consolidation Problem:100 条经验如何变成 5 条规律
Agent 有:
30 次 Gradle 错误经验20 次 Docker 测试经验10 次 Migration 经验直接存 60 条,Memory 不断膨胀。
理想结果:
Project Build ProcedureIntegration Test ProcedureMigration Troubleshooting Procedure即:
Episodes↓Pattern Discovery↓Procedure真正强的 Memory 系统最终需要 Experience Distillation。
13.7 Forgetting Problem:Agent 必须学会遗忘
应该遗忘:
Expired Temporary StateLow-value NoiseSuperseded PreferenceDuplicated ExperienceIncorrect MemorySecurity-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 BoundarySource ClassificationContent ScanInstruction-like Text DetectionWrite PolicySanitized InjectionHuman AuditHermes 的 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 injection | Memory 内容命令模型调用危险工具 | data/instruction 隔离、工具授权不继承 |
| Deletion evasion | 原始记录删了,summary/embedding 仍存在 | lineage DAG、tombstone、purge verification |
一个关键不变量是:
derived_trust <= max(source_trusts)更保守的系统甚至采用最小来源信任级别。Consolidator 可以提高表达质量和复用度,但不能凭一次 LLM 总结把不可信网页内容“洗白”为系统规则。
防御应分四道门:
Capture: classify source + preserve raw evidenceWrite: policy + schema + protected fields + quarantineRead: authorization + freshness + sanitized serializationUse: tool permissions and approvals remain independent of memory最后一道尤其重要:即使 Memory 写着“允许部署生产”,执行工具仍必须根据当前 principal 和环境重新授权。Memory 只能提供上下文,不能签发 Capability。
运行层面还需要不可变 mutation log、周期快照、内容 hash、异常写入速率告警和一键 rollback。Hash 只能发现变化,不能判断变化是否恶意;因此它必须与来源、策略和行为 Eval 组合,而不是单独充当“防投毒”。

图 8:A 区依据 OWASP Agent Memory Guard [S11] 的检测与策略路径绘制;B 区对应 Mem0 [S7] 的顺序 best-effort 删除;C 区是本文提出的生产化补强。只有 Projection ACK、Reconciliation 与 DELETED_VERIFIED 完成后,才能证明派生索引和缓存不再可读。
13.9 Privacy Problem:能记不等于应该记
个人 Agent 很容易接触:
HealthFinancialCredentialsPrivate RelationshipPrecise LocationInternal Secrets原则:
Can remember≠Should rememberMemory Write Policy 需要:
Data MinimizationPurpose LimitationRetentionDeletionUser ControlPrivacy 的难点是派生数据,而不只是原文
一条敏感事实可能同时存在于:
Raw EventExtracted FactEmbeddingEntity / EdgeRollout SummaryConsolidated ProfilePrompt CacheEvaluation TraceBackup因此每个 Memory Record 需要 data_subject_ids、purpose、legal_basis/consent_ref、retention_class、source_event_ids 和 derived_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 representationEmbedding 不是天然匿名化。它仍可能泄漏语义、被成员推断,且无法像结构化字段那样精确局部删除。对高敏内容,优先存受控引用或最小化 projection;删除时执行 canonical tombstone、索引 purge、缓存失效与密钥销毁,并对备份记录可验证的到期策略。
NIST AI RMF Generative AI Profile 强调全生命周期风险管理、数据治理、隐私、安全与可测量性。[S12] 对 Memory 系统而言,落地形式不是一段隐私声明,而是可查询的数据地图、保留规则、访问日志、删除 SLA 与定期恢复演练。
13.10 Evaluation Problem:怎么证明 Memory 真有用
不要单独用:
Memory CountMemory Hit Rate评价系统。
最关键的是:
Downstream Task Improvement推荐指标:
| 层级 | 指标 |
|---|---|
| Write | Memory Write Precision |
| Write | Memory Write Recall |
| Write | Duplicate Write Rate |
| Write | Sensitive Memory Write Rate |
| Retrieval | Recall@K |
| Retrieval | Context Precision |
| Retrieval | Relevant Memory MRR |
| Conflict | Conflict Resolution Accuracy |
| Temporal | Temporal Accuracy |
| Freshness | Stale Memory Rate |
| Security | Memory Poisoning Success Rate |
| Context | Avg Memory Context Tokens |
| System | Memory Add Latency |
| System | Recall Latency |
| E2E | Task Success Rate |
| E2E | Repeated Task Improvement |
| E2E | Token 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 interventionLayer 1 衡量抽取与写策略,Layer 2 衡量索引和排名,Layer 3 衡量 Context Builder,Layer 4 才衡量系统是否获得能力。每层都保存中间 artifact,才能把 E2E 失败归因到 not written、not retrieved、not packed 或 retrieved 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 MemoryRecent Window OnlyFull History(可放入时)BM25 OnlyDense OnlyHybrid RetrievalHybrid + RerankerFull Memory SystemOracle EvidenceOracle 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 improvementP50/P95 first-action and total latencyInput tokens and retrieval costWrong personalization / stale assertion rateSensitive write ratePoisoning attack success rateUser 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 SuccessTool CallsRetriesLatencyToken CostHuman Intervention如果 Memory 存了很多,但第二次任务没有更快、更准、少犯错,那么 Memory 没有产生能力价值。
14. 不同 Agent 场景如何选择 Memory 架构
14.1 Coding Agent
推荐:
File Memory+Procedural Experience+Background Consolidation参考:
Claude CodeCodexHermes架构:
Core:project conventionsbuild commands
Topic Files:debuggingarchitecturetool quirks
Background:task rollout extraction
Procedural:verified workflows14.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核心对象:
CustomerCompanyContactOpportunityContractTicketProduct关系:
works_atownsinterested_innegotiatingsignedcancelledescalated这些关系具有明显时间变化。
14.4 Long-running Autonomous Agent
推荐:
Letta-like Hierarchy+Compaction+Procedural Memory需要:
Hot BlocksCold ArchiveCompacted HistoryExternal Knowledge14.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 PathSources ReadHypothesesRejected HypothesesInterim FindingsCitation Evidence不能只存最终 Summary。
14.7 Self-Evolving Agent
推荐:
Experience Memory↓Outcome Evaluation↓Pattern Mining↓Procedural Memory↓Skill Lifecycle可以借鉴:
Codex Phase 1/2Hermes Learning LoopLetta Agent-controlled Memory关键原则:
失败轨迹不能直接变成 Procedure。
必须:
Experience↓Outcome / Verifier↓High-quality Experience↓Consolidation↓Procedure否则 Agent 会把错误经验固化。
14.8 Multi-Agent System
建议分:
Private MemoryShared MemoryRole MemoryTask Memory例如:
CoordinatorPrivate:routing experience
Policy AgentPrivate:policy interpretation procedure
Shared:case stateverified evidencehuman decisionsShared Memory 必须拥有:
OwnershipSchemaWrite PermissionConflict 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 映射
| System | Capture | Extract | Evaluate | Write | Retrieve | Consolidate | Forget/Invalidate |
|---|---|---|---|---|---|---|---|
| Codex | Rollout | Phase 1 Agent | Eligibility | Background Job | Local Recall | Phase 2 Agent | Retention / prune |
| Claude Code | Session learning | Claude | Agent判断 | Markdown | Index + file tools | File organization | Edit/delete |
| Hermes | Session observation | Agent | Agent + capacity | Memory Tool | Frozen Snapshot | Replace / Batch | Remove |
| Mem0 | Messages | LLM | Extraction Pipeline | ADD-only Batch Store | Hybrid Search | 无写时合并,读时排序 | Expiration / explicit delete |
| Letta | Agent interaction | Agent | Agent | Block/Archive Tool | In-context + search | rethink_memory | Rewrite/delete |
| Graphiti | Episode | Node/Edge extraction | Resolution | Temporal Graph | Hybrid Graph Search | Saga summary | Fact invalidation |
附录 B:工程选型速查表
| 核心问题 | 优先研究 |
|---|---|
| 历史任务如何形成未来经验 | Codex |
| Coding Agent 项目上下文如何组织 | Claude Code |
| Agent 如何自己管理有限 Memory | Hermes |
| 如何快速搭生产 Memory Pipeline | Mem0 |
| Long-running Agent 如何管理多层 Context | Letta |
| 动态事实、关系、时间状态怎么管理 | Graphiti |
| 需要长期事实 + 用户偏好 | Mem0 |
| 需要当前状态与历史状态并存 | Graphiti |
| 需要 Always-on 核心记忆 | Letta / Hermes |
| 需要透明可审计的本地 Memory | Claude Code / Hermes |
| 需要任务经验后台沉淀 | Codex |
| 需要 Experience → Procedure | Codex + 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
- Building Reliable Agents with Memory and Compaction
- 核对内容:Compaction 服务当前长 Run 的连续执行;Memory 服务未来 Run 对历史 Workflow Lessons 的复用。
[S3] Anthropic Claude Code Memory
- How Claude remembers your project
- 核对内容:
CLAUDE.md、Auto Memory、MEMORY.md、Topic Files、On-demand File Read、层级发现与导入规则。
[S4] Nous Research Hermes Agent Persistent Memory
- Persistent Memory
- 核对内容:bounded curated memory、
MEMORY.md、USER.md、2,200 / 1,375 字符预算、Frozen Snapshot、add/replace/remove 与容量满时的显式错误。
固定版本源码
[S5] OpenAI Codex Source
- Repository:openai/codex,Commit
8604689ec5e3437eb79802d8d72249b7722fbf5b。 - Write Path:phase1.rs、phase2.rs、runtime/memories.rs。
- Read Path:read_path.md、backend.rs、local.rs、search.rs、citations.rs。
[S6] Hermes Agent Source
- Repository:NousResearch/hermes-agent,Commit
77d5b2d573f82fc514fbef02f0b6303c44149805。 - Key file:tools/memory_tool.py。
框架、论文与官方实现说明
[S7] Mem0
- 论文:Mem0: Building Production-Ready AI Agents with Scalable Long-Term Memory,Prateek Chhikara et al., 2025。
- 官方资料:Memory Evaluation、Migrating to the New Memory Algorithm、迁移文档 Markdown 原文。迁移文档明确区分旧算法的
ADD / UPDATE / DELETE与 V3 的 Single-pass ADD-only。 - Repository:mem0ai/mem0,Commit
ccbe5861a138c7583e01bb3a3aa6168e52526a23。 - Key paths:V3
_add_to_vector_store、显式 Update/Delete、ADDITIVE_EXTRACTION_PROMPT、storage.py。旧DEFAULT_UPDATE_MEMORY_PROMPT仍在同一文件中,但固定版本 V3 主路径没有调用它。
[S8] Letta / MemGPT
- 论文:MemGPT: Towards LLMs as Operating Systems,Charles Packer et al., 2023。
- 官方资料:Context hierarchy。
- Repository:letta-ai/letta,Commit
b76da9092518cbaa2d09042e52fdcbde69243e18。 - Key files:base.py、block.py、memory.py、block_manager.py。
[S9] Zep / Graphiti
- 论文:Zep: A Temporal Knowledge Graph Architecture for Agent Memory,Preston Rasmussen et al., 2025。
- 官方资料:Adding Episodes。
- Repository:getzep/graphiti,Commit
526dcad7a300f3c5c506ff96a68bcdc7ca9f97ed。 - Key files:graphiti.py、edge_operations.py、search_config.py、search_utils.py、search_filters.py。
评测、安全与交叉验证
[S10] LongMemEval
[S11] OWASP Agent Memory Security
- OWASP Agent Memory Guard。
- AI Agent Security Cheat Sheet。
- 核对内容:Memory Poisoning、持久化 Prompt Injection、完整性基线、策略校验、快照、审计与回滚。
[S12] NIST AI RMF Generative AI Profile
- Artificial Intelligence Risk Management Framework: Generative Artificial Intelligence Profile,NIST AI 600-1,DOI
10.6028/NIST.AI.600-1。
[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。