会话记忆自迭代:v3 抽取、用户记忆与 Agent 经验
30 秒导读: 一次对话结束(
session.commit())后,OpenViking 不占用户等待,在后台异步跑一遍"抽取"。抽取分两条线:一条把对话里关于"用户是谁、偏好什么、发生了什么"的长期记忆合并回目录树;另一条由一种叫cases的记忆触发,把"这次 Agent 是怎么把活干成的"蒸馏成可复用经验写回。下次同一个用户来,检索(见 03)就能捞到这些回写,于是 Agent「越用越聪明」。本章只讲这个 会话 → 记忆回写 的闭环;底层怎么落盘看 04,怎么检索看 03。
1. 这是什么(零基础也能懂)
一句话定义: 会话记忆自迭代 = 每次对话结束后,系统自动读一遍这段对话,把值得长期记住的东西抽出来,写回 01 讲的那棵 viking:// 目录树。
解决什么问题: 大模型本身没有跨会话的长期记忆——这次告诉它"我叫张三、常坐靠窗",下次开新会话它就忘了。OpenViking 的做法是把这些事实沉淀成磁盘上的记忆文件;下次检索时再喂回去。
两类要沉淀的东西,必须分清(这是全章主线):
| 线 | 记的是谁的事 | 例子 | 面向 |
|---|---|---|---|
| 用户记忆(user) | 关于用户的长期事实 | "张三,素食,常坐靠窗" | 让 Agent 更懂这个用户 |
| Agent 经验(agent) | 关于Agent 自己怎么把活干成的方法 | "退改签任务的标准工具序列" | 让 Agent 下次干得更好 |
用起来什么样: 用户只调一个 commit(),立刻拿到 task_id 返回,不用等抽取跑完。
result = session.commit()
# {"status": "accepted", "task_id": "...", "archived": True,
# "archive_uri": "viking://user/{uid}/sessions/.../history/archive_001"}
# 抽取在后台异步跑;可用 task_id 轮询进度(依据:docs/en/concepts/08-session.md:66-80)
一句话直觉(这是类比,不是定义): 像人下班后写"复盘日记"——一部分记"今天认识的人是什么样"(用户记忆),一部分记"这活儿我下次该怎么干更顺"(经验)。区别是这里由 LLM 自动写、写进可检索的目录树。
2. 顶层全景(它大概怎么转)
怎么读下面这张图: 从上到下是时间顺序。commit() 只做同步归档就返回;真正的抽取在后台 Phase 2 里跑,产出分两条线回写目录树。
用户: session.commit()
│
┌────┴─────────────────────────────┐
│ Phase 1 同步(立即返回 task_id) │
│ 归档本轮消息 → messages.jsonl │ session.py:1092+ commit()
└────┬─────────────────────────────┘
│ asyncio.create_task(...) session.py:1245
▼
┌──────────────────────────────────────────────┐
│ Phase 2 异步后台 │ _run_memory_extraction
│ │ session.py:1266+
│ extract_long_term_memories() ← 唯一入口 │ compressor_v3.py:254
│ │ │
│ ┌────┴──── 第 1 步 ────┐ ┌── 第 2 步 ──┐ │
│ │ 抽用户记忆(含 cases) │→│ 按 cases 训练 │ │
│ └──────────┬──────────┘ └──────┬──────┘ │
│ ▼ ▼ │
│ [用户记忆线] [Agent 经验线] │
│ profile/preferences trajectory→experience │
│ /events/entities... /skills │
│ │ │ │
│ └────────┬───────────┘ │
│ ▼ 第 3 步 │
│ memory_diff.json 审计日志 │ compressor_v3.py:870
└───────────────────────┬──────────────────────────┘
▼
回写 viking:// 目录树(落盘见 04)
▼
下次检索捞回(见 03)→ 越用越 聪明
主要部件一句话职责:
| 部件 | 干什么 | 在哪 |
|---|---|---|
SessionCompressorV3 | 抽取总编排;三步走完一次会话 | openviking/session/compressor_v3.py:87 |
ExtractLoop | ReAct 抽取循环:让 LLM 读上下文、产出记忆操作 | openviking/session/memory/extract_loop.py:82 |
StreamingMemoryUpdater | 把用户记忆操作缓冲、合流、patch-merge 落盘 | openviking/session/memory/streaming_memory_updater.py:120 |
TrajectoryRolloutAnalyzer | 经验线 Phase 1:把执行抽成 trajectory | openviking/session/train/components/trajectory_analyzer.py:60 |
ExperienceGradientEstimator | 经验线 Phase 2:把 trajectory 蒸馏成 experience 更新信号 | openviking/session/train/components/gradient_estimator.py:41 |
MemoryTypeRegistry | 从 YAML 加载"有哪些记忆类型、写哪、怎么合并" | openviking/session/memory/memory_type_registry.py:28 |
主线走一遍(高层): 会话消息 → extract_long_term_memories → 用户记忆线抽出 profile/events/... 顺带抽出 cases → cases 触发经验线 trajectory→experience → 两条线的改动都汇进 memory_diff.json → 回写目录树。