Agent 记忆

L1 原子记忆:单次 LLM 的结构化抽取与去重

本期拆解 L1:L0 的流水账如何变成一张张"记忆卡片",以及系统如何用两次 LLM 调用完成抽取与冲突消解。

L1 原子记忆:抽取与批量去重 全屏查看 ↗

一、L1 的定位

官方定义:

L1 Atom:从对话提取的事实、偏好、约束与事件 —— 用于精确召回可执行信息

如果说 L0 是流水账,L1 就是会议纪要:系统在后台把对话通读一遍,先按情境分段,再从每段里抽出结构化事实。每张卡片只记一件事,带类型和优先级。

核心实现文件是 l1-extractor.ts,官方架构文档的描述是:

L1:core/record/l1-extractor.ts —— 单次 LLM 调用(JSON 结构化输出):场景分段 + 记忆抽取一次完成;随后 l1-dedup.ts 批量冲突检测(以向量召回旧记录为候选,LLM 判定 add/update/drop)。

二、整体流程

L0 过滤后的消息
   │
   ▼
① 质量门 shouldExtractL1 ── 过滤过短/纯符号/注入负载
   ▼
② 消息切分 ── new(最近 10 条)+ background(之前最多 5 条)
   ▼
③ 单次 LLM 调用(JSON 结构化输出)
   └── 场景分段 SceneSegment[] + 每条记忆卡片
   ▼
④ 批量冲突检测 batchDedup
   ├── 候选召回:向量 cosine(最佳)→ FTS5 BM25(降级)→ 跳过(无能力)
   └── 单次 LLM 判定:store / update / merge / drop
   ▼
⑤ 写入 ── l1_records 表 + l1_vec 向量表 + FTS5 + JSONL 备份

三、抽取:一次 LLM 调用完成"分段 + 提取"

消息切分

抽取不是把整段对话塞给 LLM,而是控制预算:

  • maxMessagesPerExtraction 默认 10 条(new 消息,最近);
  • maxBackgroundMessages 默认 5 条(background 消息,做上下文);
  • maxMemoriesPerSession 默认 10 条(单会话记忆上限,超限截断)。

LLM 输出协议

LLM 返回一个 JSON 数组,每个元素是一个"场景段":

[
  {
    "scene_name": "鉴权模块重构",
    "message_ids": ["msg_1785…", "msg_1786…"],
    "memories": [
      {
        "content": "别重构旧鉴权模块,移动端还在用",
        "type": "instruction",
        "priority": 80,
        "source_message_ids": ["msg_1785…"],
        "metadata": {"activity_start_time": "2026-08-01T00:00:00Z"}
      }
    ]
  }
]

注意 source_message_ids —— 每张卡片都带着来源消息 ID,这就是第 1 期说的"证据链":L1 → L0 可溯源

记忆类型

normalizeType 支持的 7 种类型(源码 VALID_TYPES):

类型含义
persona用户画像 / 偏好(旧名 preference 并入此类)
episodic事件(旧名 episode)
instruction规则 / 指令(旧名 instruct)
work_fact工作事实
work_task工作任务
work_method工作方法
work_artifact工作产物

JSON 容错

弱模型偶尔会输出不规范的 JSON(如 "priority": sheet 这种裸标识符)。工程上做了两层防护:

  • sanitizeJsonForParse:修复字符串内的控制字符;
  • repairExtractionJson:检测到 priority 字段不是数字时回填默认值 50,并清理尾随逗号——一条坏标量不拖垮整批抽取

四、去重:批量冲突检测(l1-dedup.ts)

抽取出的记忆不能直接入库——同一件事可能被反复提及。官方 v4 版本的实现是两阶段

阶段 A:候选召回(无 LLM,快)

对每条新记忆,从存量记忆里召回最相似的候选(默认 topK=5)。采用三级降级策略:

  1. 向量召回(最佳):embeddingService.embed + searchL1Vector cosine 相似度;
  2. FTS5 BM25(降级):无 embedding 能力时用关键词召回;
  3. 跳过:两者都不可用时直接全部 store——宁可多存,也不做 O(N) 全文件扫描

关键约束:候选召回带 team/user/agent 隔离过滤(IsolationFilter),去重判定永不跨租户

阶段 B:单次 LLM 批量判定

把「所有新记忆 + 各自的候选池」放进一次 LLM 调用,对每条新记忆给出四种动作之一:

动作含义
store无冲突,作为新记忆写入
update与某条旧记忆冲突,更新旧记录(保留 record_id)
merge与多条旧记忆重复,合并为一条
drop信息冗余 / 被覆盖,丢弃

如果批量判定本身失败(如 LLM 超时),降级为全部 store——保证对话经验不丢失,代价是可能短暂重复,由下一次去重收敛。

五、写入与存储

l1-writer.tswriteMemory 按决策写入:

  • l1_records 表:元数据(content / type / priority / scene_name / session / team / user / agent / version / 时间字段 / metadata_json);
  • l1_vec 虚拟表(vec0,cosine):向量,供召回;
  • FTS5:全文索引,供 BM25 关键词召回;
  • JSONL 备份records/YYYY-MM-DD.jsonl,服务模式下是恢复的源(源码注释提到 CR-2 根因:无 storage 时 JSONL 是唯一恢复通道)。

l1_records 表 DDL 关键字段(src/core/store/sqlite.ts):

CREATE TABLE IF NOT EXISTS l1_records (
  record_id TEXT PRIMARY KEY,
  content TEXT NOT NULL,
  type TEXT DEFAULT '',
  priority INTEGER DEFAULT 50,
  scene_name TEXT DEFAULT '',
  session_key TEXT DEFAULT '',
  session_id TEXT DEFAULT 'default',
  team_id TEXT DEFAULT 'default',
  task_id TEXT DEFAULT '',
  user_id TEXT NOT NULL DEFAULT 'default',
  agent_id TEXT NOT NULL DEFAULT 'default',
  version INTEGER NOT NULL DEFAULT 0,
  timestamp_str TEXT DEFAULT '',
  timestamp_start TEXT DEFAULT '',
  timestamp_end TEXT DEFAULT '',
  created_time TEXT DEFAULT '',
  updated_time TEXT DEFAULT '',
  metadata_json TEXT DEFAULT '{}'
);

时间语义:timestamp 是"点时间"(何时发生),timestamp_start / timestamp_end 是"段时间"(活动区间),召回格式化时优先展示活动区间。

六、生成溯源(v2.0 新增能力)

官方在 MemoryCore README 中强调:

生成溯源:记录 L1/L2/L3 实际使用的 Prompt ID、版本、来源、内容哈希以及输入输出引用,支持按 Memory ID 精确定位生成日志;不保存 Prompt 正文快照。

L1 抽取完成后会写 memory_generation_refs(generation_ref_id = l1:<memory_id>),记录:

  • 使用的 Prompt ID / 版本 / 来源(Agent > Team > Instance > 系统内置 解析结果);
  • input_refs:本次输入引用的 L0 消息 ID;
  • output_refs:本次产出的 L1 记忆 ID;
  • 模型、耗时、状态。

这意味着:任何一条 L1 记忆,都能回答"它是用哪个 Prompt、基于哪些对话、由哪个模型生成的" —— 这正是"记忆是资产"的审计底座。

七、本期小结

  • 抽取:单次 LLM 调用完成场景分段 + 记忆提取,JSON 结构化输出,7 种记忆类型。
  • 去重:候选召回(向量 → FTS → 跳过)无 LLM 快速召回,再单次 LLM 批量判定 store / update / merge / drop。
  • 写入:元数据表 + 向量表 + FTS5 + JSONL 四通道,三租户隔离。
  • 溯源:每条记忆可回溯到 Prompt、来源对话与模型,支持按 memory_id 查生成日志。
  • 容错哲学:解析失败修一次、判定失败全 store——宁可暂时重复,不可丢失经验

下一期看记忆卡片的"归档":L2 场景记忆——LLM 沙箱 Agent 如何把卡片组织成场景档案。


图示索引

源码索引

内容文件
L1 抽取主流程MemoryCore/src/core/record/l1-extractor.ts
批量冲突检测MemoryCore/src/core/record/l1-dedup.ts
写入与决策执行MemoryCore/src/core/record/l1-writer.ts
抽取 / 去重 PromptMemoryCore/src/core/prompts/l1-extraction.tsl1-dedup.ts
表结构与索引MemoryCore/src/core/store/sqlite.ts(l1_records / l1_vec DDL)
生成溯源MemoryCore/src/core/memory-generation-log/

L1原子记忆LLM 抽取冲突消解

← 返回AI 笔记