本期拆解 L1:L0 的流水账如何变成一张张"记忆卡片",以及系统如何用两次 LLM 调用完成抽取与冲突消解。
一、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)。采用三级降级策略:
- 向量召回(最佳):
embeddingService.embed+searchL1Vectorcosine 相似度; - FTS5 BM25(降级):无 embedding 能力时用关键词召回;
- 跳过:两者都不可用时直接全部
store——宁可多存,也不做 O(N) 全文件扫描。
关键约束:候选召回带
team/user/agent隔离过滤(IsolationFilter),去重判定永不跨租户。
阶段 B:单次 LLM 批量判定
把「所有新记忆 + 各自的候选池」放进一次 LLM 调用,对每条新记忆给出四种动作之一:
| 动作 | 含义 |
|---|---|
store | 无冲突,作为新记忆写入 |
update | 与某条旧记忆冲突,更新旧记录(保留 record_id) |
merge | 与多条旧记忆重复,合并为一条 |
drop | 信息冗余 / 被覆盖,丢弃 |
如果批量判定本身失败(如 LLM 超时),降级为全部 store——保证对话经验不丢失,代价是可能短暂重复,由下一次去重收敛。
五、写入与存储
l1-writer.ts 的 writeMemory 按决策写入:
- 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 原子记忆:抽取与批量去重(在新窗口打开完整图示)
源码索引
| 内容 | 文件 |
|---|---|
| L1 抽取主流程 | MemoryCore/src/core/record/l1-extractor.ts |
| 批量冲突检测 | MemoryCore/src/core/record/l1-dedup.ts |
| 写入与决策执行 | MemoryCore/src/core/record/l1-writer.ts |
| 抽取 / 去重 Prompt | MemoryCore/src/core/prompts/l1-extraction.ts、l1-dedup.ts |
| 表结构与索引 | MemoryCore/src/core/store/sqlite.ts(l1_records / l1_vec DDL) |
| 生成溯源 | MemoryCore/src/core/memory-generation-log/ |