Appearance
什么值得长期记住:Long-Term Memory 的写入与检索
前置:运行中的信息放哪里:Conversation、Checkpoint 与 Short-Term Memory
核心问题:哪些信息值得跨任务长期保存,什么时候应该写入、何时应该过期或拒绝写入,避免 Memory 变成永久垃圾场?
用户多次纠正系统“这个仓库使用 uv,不用 pip”。是否应该把这条经验写成长期记忆?写入后多久有效?另一个仓库是否适用?下一次运行何时检索?这些都不是一个 save() 调用能回答的。
Mental Model
text
Run / Observation
↓
Memory Candidate Extraction
↓
Write Policy
├─ usefulness
├─ stability
├─ scope
├─ sensitivity
└─ duplication
↓
Durable Memory Record + Provenance
↓ future run
Memory Query → scope filter → rank/freshness → selected memories
↓
ContextManager → token budget → Model ContextLong-Term Memory 不是“更大的聊天记录”,而是一条受策略控制的长期知识管线。
关键边界是 Write、Store、Retrieve、Inject 四步分离。模型可以帮助提取 candidate 或判断语义相关性,但真正写入哪个 namespace、是否允许持久化、是否过期以及最终注入多少 token,都应由程序策略约束。
Long-Term Memory 的定义与 Scope
Long-Term Memory 指跨当前 thread/session 仍有复用价值的信息。典型是关于用户、项目或环境的相对稳定事实,例如:
text
project uses Python 3.12
repository test command is `uv run pytest`
user prefers concise changelogsMemory Write Policy
Tool 返回 1000 条日志,这 1000 条不是 1000 条 Memory。
写入前至少评估:
text
Future usefulness:以后还会用吗?
Stability:这个事实容易变化吗?
Confidence:来源可靠程度?
Scope:属于 user / project / workspace / agent / global?
Sensitivity:是否包含敏感信息?
Duplication:已有相同/更好的记录吗?🔍 源码对照:Memory 写入是 opportunistic maintenance,不是 Turn commit
精确定位
- 文件:
source/host/runner/turn-memory.ts - Symbol:
runTurnMemory()→runMemoryExtraction() - 深入索引:Code Atlas:07-memory-maintenance
ts
async function runTurnMemory(store, progress, session, ctx, ts, exchange) {
await runMemoryExtraction(store, session, ctx, exchange)
if (!progress) return
try {
progress.recordEpisodeTurn({ ts, ...exchange })
if (progress.pending().length < episodeInterval()) return
await summarizeAndPersistEpisode(progress, store, session, ctx)
} catch {
// maintenance must not fail the user-visible turn
}
}对照理解
- Memory extraction / episode summarization 属于 maintenance path,不是 Turn settlement 的强一致步骤。
- 源码故意吞掉这条维护路径的异常:记忆失败不能把已经完成的用户 Turn 反向判成失败。
- 因此 Memory、Checkpoint、Conversation State 必须分开:Checkpoint 决定能否恢复,Memory 只增强未来决策。
源码说明
turn-memory.ts 直接展示了两类维护动作:memory extraction 与 episode summary;更重要的是,这条路径显式捕获异常,注释要求 memory maintenance 不能让 user-visible turn 失败。这个边界很适合说明 Long-Term Memory 是辅助知识系统,而不是 Run correctness 的唯一事实源。
工程边界
“best effort”适用于这里的 memory maintenance,不意味着所有 memory 系统都可随意丢失;如果产品把某类用户承诺/偏好当强持久化数据,就需要更强 durability contract。
Shadow Harness 对照
- Memory write 有独立 policy:candidate → dedup/conflict/provenance → commit。
- Episode summary 与 raw evidence 分离,摘要可重建/更新,证据保留 provenance。
验证
让 memory extraction 模型调用失败,用户 Turn 仍成功;随后重跑 maintenance,验证不会重复写入相同 memory。
Memory Record 设计
一条可治理的 MemoryRecord 不能只有 text + embedding,至少还需要 scope、provenance、有效期和 supersede 关系:
python
@dataclass
class MemoryRecord:
memory_id: str
namespace: tuple[str, ...]
kind: str
content: str
provenance: MemoryProvenance
confidence: float
valid_from: datetime
valid_until: datetime | None
supersedes: str | None
tags: set[str]
embedding_ref: str | NoneMemory Retrieval
即使检索命中,也不代表必须放入 Context。
它同样需要相关性排序,但长期记忆还必须受 scope 和可用性策略约束;freshness、conflict 与 isolation 的完整规则放到下一篇专门处理。
text
Memory Store
↓ retrieve candidate
Candidate Set
↓ filter/rank
Relevant Memories
↓ context budget / trust policy
Injected Context最小实现 / 数据结构
python
class MemoryStore(Protocol):
async def put(self, record: MemoryRecord) -> None: ...
async def search(self, query: MemoryQuery) -> list[MemoryHit]: ...
async def supersede(self, old_id: str, new_record: MemoryRecord) -> None: ...
async def delete(self, memory_id: str) -> None: ...
class MemoryWritePolicy(Protocol):
async def evaluate(self, candidate: MemoryCandidate, ctx: RunContext) -> WriteDecision: ...
class MemoryRetrievalPolicy(Protocol):
async def select(self, hits: list[MemoryHit], ctx: RunContext, token_budget: int) -> list[MemoryHit]: ...在 Shadow Harness 中怎么落地
Shadow Harness 建议把 Memory 拆成 MemoryCandidateExtractor、MemoryWritePolicy、MemoryStore、MemoryRetrievalPolicy 四个接口。不要让 ContextManager 直接拥有 Memory 写权限;它只消费经过 retrieval policy 选出的记录。
Failure Modes
自动保存每一句用户话
症状: 所有 message/observation 都直接写入长期 Memory。
为什么危险: 长期存储会被一次性信息、噪声和敏感数据污染,未来 retrieval 更容易返回错误内容。
处理: 引入 MemoryCandidate + WritePolicy,按 usefulness、stability、scope、sensitivity、duplication 决定 commit。
只靠 Vector Similarity
症状: 检索只按 embedding similarity top-k,不考虑 namespace、freshness、provenance 或 structured key。
为什么危险: 语义相似不等于属于当前用户/项目,也不等于事实仍有效。
处理: 先强制 namespace/filter,再组合 exact/keyword/semantic/hybrid,并加入 freshness/source trust。
永不更新旧 Memory
症状: 同一 canonical fact 的多个版本长期同时 active。
为什么危险: 模型每次要重新解释冲突,行为会随检索排序波动。
处理: 支持 upsert/supersede/merge/delete,读取默认过滤 inactive/superseded。
检索结果全部注入
症状: Memory search 命中多少条就向 Context 注入多少条。
为什么危险: Store 容量与模型 working set 是两个概念;无限注入会制造 context pollution。
处理: retrieve 与 inject 分开,用 RetrievalPolicy 做 filter/rank/token budget/progressive disclosure。
如何验证
| Case | 断言 |
|---|---|
| 自动保存每一句用户话 | 用标注 observation set 计算 write precision/recall,敏感/越 scope 候选不得 commit。 |
| 只靠 Vector Similarity | 写入跨 project 的高相似 memory,断言未授权 scope 永远不进入候选。 |
| 永不更新旧 Memory | 写入同 key 新版本后,默认检索只返回 active 记录,同时历史链可审计。 |
| 检索结果全部注入 | 让 Store 返回大量候选,断言最终注入受 budget/threshold 控制。 |
关联源码
自测
- 用户一次临时偏好是否应该写入长期 Memory?哪些条件能让一条信息值得跨 Session 保存?
- 长期 Memory 检索到了旧事实。为什么“检索命中”不等于“当前仍可直接使用”?