夜雨聆风学习资料网

ARTICLE · 1234258

Agent框架Letta 源码解析(五):记忆写入机制

Agent框架Letta 源码解析(五):记忆写入机制

主 agent 不是记忆的唯一写者。Letta 的记忆写入机制由三条路径共同维护:主 agent 在回合里直接写、memory worker 在后台的私有 git worktree 里写、写完合并回来、reflection 在复盘时写(整理记忆、生成 skill)。三条路径同时改一个 git 仓库,并发安全全部落在 git 分支模型上——这也是 MemGPT 论文里 sleep-time compute(空闲时整理记忆)在产品里的实现形态。

写入机制总览

~/.letta/agents//memory(主 checkout,git 仓库)                              ▲        ┌─────────────────────┼─────────────────────┐        │                     │                     │   手一:主 agent        手二:memory worker     手三:reflection   回合内直接写          后台任务,私有 worktree   复盘近期对话                       写完合并回主分支          改记忆 + 生成 skill   用户在场              主 agent 不等待           定时/事件/手动触发                              │                    git worktree 分支 → merge

为什么需要多条路径写入:回合内写记忆要占用对话轮次,用户在等;而「整理」类工作——去重、纠错、把散落的信息归并——不该打断任何对话。写入挪到后台,主对话零延迟,代价是要解决「后台改的内容怎么安全合并回来」——这个代价用 git 分支付。

memory worker,私有 worktree 加合并

worker 的行为契约写在 runMemoryWorker 的文档注释里(src/agent/subagents/memory-worker.ts:71,源码原文):

An update worker edits a private worktree of the memory repository, so theprimary agent's own edits to the checkout are never touched: a cancelledworker is discarded outright, and a finished worker's commits are mergedback before the lease is released. A merge that conflicts with theprimary's changes keeps the worker's branch and reports it instead ofguessing. A repair worker must work on the checkout itself.Called inside the existing background task, never awaited by the primary.

五条契约逐条对应代码:worker 在私有 worktree(git 的多工作树机制,一个仓库多个工作目录)里改,主 checkout 不受影响;取消即丢弃(worktree 和分支直接清掉);完成即合并——合并发生在租约释放前,整个操作包在 withMemoryOperation 的 checkout 租约里(记忆的 git 操作串行化);冲突不猜——与主 agent 的改动冲突时,保留 worker 的分支、报告出来,由后续决定;主 agent从不等待worker。

合并的具体实现在 integrateMemoryWorkerWorktree(src/agent/memory-worktree.ts:685,节选):

if (commitCount === 0) {  await cleanup(true);  return { status: ”no_changes” };              // 没改动:清掉 worktree}// 先移除 worktree,分支保留到合并成功const merged = await tryRunGit(worktree.parentMemoryDir, [  ”merge”, ”--no-edit”, ”-m”, `merge(memory): ${worktree.branchName}`,  worktree.branchName,]);if (!merged) {  await tryRunGit(worktree.parentMemoryDir, [”merge”, ”--abort”]);  return { status: ”merge_conflict”, commitCount, branchName };}                                              // 冲突:回滚合并、保留分支await tryRunGit(worktree.parentMemoryDir, [”branch”, ”-d”, ...]);return { status: ”merged”, commitCount, head };

四种结局:no_changes(没写东西,清场)、merged(合并成功,删分支,主 checkout 多一条 merge(memory): ... 提交)、merge_conflict(回滚这次合并、分支保留,冲突作为状态报告出去)、discarded(取消)。git log 里看到 merge(memory): 开头的提交,就是 worker 的产出。

worker 本身是一个 subagent,跑在三道闸的沙箱里(confinement 注释原文写的正是 "unattended memory subagents")。给它的指令(src/agent/subagents/builtin/memory-v2.md)几条硬约束:静默运行(无通知、无结果展示,不干扰主对话);禁止提问、禁止给主 agent 或用户发消息、禁止再派生 agent;父对话只作参考材料,不是待继续的任务;用户的纠正和例外要逐字引用保存,不许概括改写;遇到进行中的 merge 先读两边再解决,禁止 abort/reset/stash/amend,意图不明就保持未解决状态并在报告里说明阻塞点。最后这条和 harness 的「冲突不猜」是同一个原则的两层:代码层不猜合并,模型层不猜语义。

手三:reflection,复盘近期对话

reflection 也是一个后台 subagent(builtin/reflection-v2.md),干两件事:改记忆(把值得留的事实、偏好、纠正写进文件)和生成/维护 skill——但指令里有一句克制判断(原文):"Skills are not the default. A one-off task, a fact, or a preference belongs in memory, not a skill."(skill 不是默认产出:一次性的任务、事实、偏好进记忆;只有可复用的多步流程才配生成 skill)。它复盘的是已经发生的对话(system/assistant/user 三种角色的消息),明确声明自己不是主 agent、不能提问、结束时交一份报告。

什么时候触发,代码里是三个枚举加手动命令(src/reflection-settings.ts:1):

export type ReflectionTrigger = ”off” | ”step-count” | ”compaction-event”;export type ReflectionMergeMode = ”auto” | ”explicit”;
  • off
    :关闭自动反思
  • step-count
    :对话步数达标触发(论文 sleep-time 的「定时整理」形态)
  • compaction-event
    :上下文压缩事件触发——被压缩驱逐的内容先经反思写进记忆再丢(第 6 篇展开这条联动)
  • 手动:/dream、/reflect、/reflection 三个命令等价(src/websocket/listener/commands.ts:181),随时可以要求 agent「做个梦」

reflection 的改动同样走 worktree + 合并那套(memory-worktree.ts 的 createReflectionMemoryScope),合并模式可选 auto(自动并入)或 explicit(报告待并入)。

一条完整的时间线

一天的使用时间线(内容为示意,机制与结局状态来自上述源码):

10:00  用户:「我下周开始周三在家办公」       手一:主 agent 回合内 Write preferences/work.md + 更新索引       (04 的写路径:落盘 → 回合终态 commit → 三查 → push)10:02  harness 在后台派一个 memory worker(手二):       「把上周说的办公安排和这条新信息归并」       worker 在私有 worktree 分支上改 → 主对话继续,不等它10:05  worker 完成 → integrate:       ├─ 无冲突 → merge 提交进主分支,worktree 清理       └─ 与 10:00 主 agent 的提交冲突 → merge_conflict,          worker 分支保留,报告交回22:00  step-count 达标,reflection(手三)启动:       复盘今天的对话 → 修正记忆里过期的办公安排 →       无 skill 可生成(没有可复用流程)→ worktree 合并次日   用户问「我哪天在家办公」→ 手一的读路径直接命中更新后的记忆

10:02 到 10:05 是冲突现场:主 agent 在 10:00 写的内容和 worker 的归并目标如果是同一个文件,冲突就发生在 10:05——冲突的结局不是数据丢失,是一个保留待处理的工作分支加一份报告,谁都没猜。

设计亮点

并发模型整个建在 git 上。不自建锁协议、不搞操作日志,worker 的隔离(worktree)、结果的汇合(merge)、冲突的处理(保留分支)全是 git 原语。对照 LoopX 第五篇的写范围互斥(会改东西的排队、不许并发),这是另一种并发哲学:LoopX 从源头禁止冲突,Letta 允许并发、用分支合并处理冲突。判断标准在数据形态:互斥适合写代码(冲突解决成本高),分支合并适合写记忆(改动小、语义可读、有版本历史兜底)。

冲突不猜,保留现场。合并冲突时代码层 merge --abort 保留分支、模型层被指令禁止单方面取舍——两层都不猜,把决定权留给之后的处理。对照「冲突自动解决」的实现:自动猜错的代价是静默丢失一方的记忆,而且发现晚。宁可留下一个待处理分支,也不产出一个可疑的合并。

skill 生成的克制门槛。reflection 能生成 skill,但指令把默认设为「不生成」——一次性的进记忆,可复用流程才配 skill。这个门槛防的是 skill 泛滥(每个小事都生成一个技能文件,检索和维护成本爆炸)。可迁移的判断:让 agent 自主产出持久资产时,给产出设一个「默认不产出」的高门槛。

代价与局限

merge_conflict 需要后续处理。冲突分支保留是安全设计,但谁来解?主 agent 收到报告后可以在后续回合处理,用户也可以手动解——不管哪种,冲突积压时记忆更新会滞后,而且「保留待合并的工作分支」对不熟悉 git 的用户是个心智负担。

后台任务与回合仍有竞态窗口。checkout 租约把 git 操作串行化了,但「worker 基于旧状态做决定」的窗口存在:worker 读文件的瞬间主 agent 可能正在改同一个事实。租约保证的是 git 状态一致,不保证语义一致——语义冲突最后还是走到 merge_conflict 那条路。

reflection 的质量取决于模型。复盘什么、留什么、删什么,全是模型的判断;三道闸(预算/格式/隔离)照样拦不住「把错误的归纳写进记忆」——第 4 篇说过格式闸不挡内容,reflection 是内容的主要来源之一,它的错误会写进记忆长期留存。

相关学习资料