当大多数记忆框架还在给向量库加功能时,OpenViking 问了一个更根本的问题:Agent 的上下文为什么非得是「检索出来」的? 它的答案是把记忆、知识、技能全部装进一个
viking://虚拟文件系统,让 Agent 像开发者浏览代码库一样ls、tree、find自己的上下文——向量搜索只负责「定位到哪个目录」,之后的每一步都是确定性的寻址,且留下可回放的浏览轨迹。这是本站继 MemOS 源码审计之后的第二篇记忆系统源码深读,这次的审计结论比上次干净:它承诺的三件事,代码里都是真的。
本文基于 2026-08-19 的浅克隆(commit dc39985),openviking Python 包 619 个 .py 文件、crates 下 156 个 .rs 文件(亲手 find 统计)。项目由北京火山引擎技术有限公司开源,AGPL-3.0 协议,自述是 “Self-evolving Context Database for AI Agents. Unify Agent Memory, Knowledge RAG and Skills.”
名词速查
| 术语 | 一句话解释 |
|---|---|
viking:// URI | OpenViking 给每条上下文的统一地址,形如 viking://resources/my_project/docs/,和文件路径一个用法 |
| sidecar | 目录里的隐藏伴生文件(.abstract.md / .overview.md),存这个目录的摘要,普通 ls 不显示 |
| L0 / L1 / L2 | 同一份内容的三档分辨率:一句话摘要(默认 256 字符)/ 导航概览(默认 4000 字符)/ 原始全文 |
| AGFS / RAGFS | 项目自己的内容存储层(Aggregated File System),已用 Rust 重写(RAGFS),插件式挂载多种后端 |
| rerank | 向量召回后用专门模型对候选重新打分排序的精排步骤,本站 RAG 主线一篇有展开 |
| rollout | 让 Agent 在一个任务上完整跑一遍所产生的执行记录,强化学习术语 |
| 语义梯度 | 用自然语言表达的「这份经验应该怎么改」信号,此处具体是一对 before/after 文件加修改理由 |
| LoCoMo | 超长多轮对话记忆基准(arXiv:2402.17753),平均每场对话约 300 轮、9K token |
| tau2-bench | 多轮工具调用 Agent 任务基准,含 retail / airline 等客服场景 |
它反对什么:黑盒向量库的两笔账
先交代问题,再看设计。把向量库直接当 Agent 记忆用,有两笔账始终算不平:
第一笔是调试账。 向量检索是「query 进、top-k 出」,中间没有可检查的状态。当检索结果错了,你无法回答「为什么是它」——只能怪 embedding 不够好。这和《别把向量数据库当知识库》里的结论一致:向量库是定位组件,不是知识组织方式。
第二笔是 token 账。 命中的 chunk 要么太碎(丢上下文),要么太大(塞爆窗口)。《遗忘是一种能力》里讲过 context rot:塞得越多,答得越差。检索粒度和加载粒度绑死在一起,是 chunk 范式的结构性缺陷。
OpenViking 对这两笔账的回答,是三次「换表示」。用本站 AI 顶级原理望远镜的话说,这个项目几乎全部押注在「表示决定成败」这一条上。
第一次换表示:一切上下文皆文件
OpenViking 把三类上下文——Memory(用户偏好、事件、经验)、Resource(文档、代码库、网页)、Skill(可执行技能)——统一挂在一棵目录树下:
viking://
├── resources/ # 共享知识库
│ └── my_project/
│ ├── docs/
│ └── src/
└── user/{user_id}/
├── memories/ # 长期记忆(profile、preferences、events、experiences…)
├── resources/ # 私有资源
├── skills/ # 技能
└── peers/ # 交互对象的画像
这个表示换掉的是「记忆、RAG、技能各用一套 API」的现状。三类上下文都是目录和文件,Agent 操作它们用的是同一组动词:ls、tree、read、grep、find、mv。官方 CLI 的体验完全是文件系统的肌肉记忆:
ov add-resource https://github.com/volcengine/OpenViking
ov tree viking://resources/volcengine -L 2
ov find "what is openviking"
「一切皆文件」对 Agent 的意义和它对 Unix 的意义相同:动词集合封闭了,名词空间才能无限扩张。 新增一种上下文类型不需要新增 API,只需要一个新目录。
第二次换表示:内容的三层分辨率
每个目录带两个隐藏 sidecar 文件,构成三档分辨率:
viking://resources/docs/auth/
├── .abstract.md # L0:一句话摘要,默认上限 256 字符
├── .overview.md # L1:结构概览 + 子项导航,默认上限 4000 字符
├── oauth.md # L2:原始全文
└── jwt.md # L2
关键设计:L0/L1 是目录级的,不是文件级的。 文件摘要只作为输入聚合进所在目录的 L1。这意味着 Agent 判断「这个目录值不值得进」只需读 256 字符,判断「进来之后读哪个文件」只需读 4000 字符——把「检索粒度」和「加载粒度」解耦了。这正是对上面第二笔 token 账的回答。
写入流水线:解析与语义分离
三层分辨率不是免费的,代价在写入侧。OpenViking 的写入流水线把「无 LLM 的解析」和「有 LLM 的语义生成」严格分开:
flowchart LR
A[输入文档] --> B[Parser<br>格式转换+智能分割<br>无 LLM]
B --> C[TreeBuilder<br>移入 AGFS]
C --> D[SemanticQueue<br>异步生成 L0/L1<br>LLM 自底向上]
D --> E[EmbeddingQueue<br>向量化]
E --> F[(向量库<br>只存索引)]
C --> G[(AGFS<br>存全部内容)]
Parser 的分割规则是确定性的:文档不超过 1024 token 存单文件;超过则按标题切,小于 512 token 的小节合并,大于 1024 token 的大节升格为子目录。代码文件走 tree-sitter 骨架提取(优先各语言维护的 tags.scm,缺失时回退到 language-pack 通用解析)——和本站读 tree-sitter 那篇讲的 aider 用法同源。
语义生成是异步的、自底向上的:文件摘要 → 叶子目录 L1 → 从 L1 提取 L0 → 逐级向父目录冒泡,直到 namespace 根。每个 sidecar 的 frontmatter 里记录 freshness 元数据(直接子项总数、本轮采样数、已知未反映的变更数);子项超过 32 个时用确定性稳定采样,保证同一棵树重复刷新不产生无意义的 diff。
这里有个值得注意的诚实细节:文档里用 TODO 明确承认当前每次语义任务成功都会向父级冒泡刷新,热点目录存在重复刷新和写放大,节流策略还没做。承诺和现状分得很清,这是好文档的标志。
检索:带地图的下钻,而不是一次近邻查询
读路径分两档。find() 是快路径:单查询直接进层级检索。search() 是慢路径:先由 IntentAnalyzer 用 LLM 把「当前查询 + 会话摘要 + 最近 5 条消息」改写成 0 到 5 个带类型的查询(TypedQuery)——技能查询动词开头(「提取 PDF 表格」)、资源查询名词短语(「API 使用指南」)、记忆查询固定「用户XX」句式。闲聊会产出 0 个查询,直接跳过检索。
真正的核心是 HierarchicalRetriever(openviking/retrieve/hierarchical_retriever.py,644 行)。它不做全库平铺检索,而是先全局向量搜索定位起始目录,再用优先队列逐层下钻:
# 伪代码,对应 hierarchical_retriever.py L452-556 主循环,省略:并行批处理/遥测/level 过滤
dir_queue = [(-score, uri) for uri, score in starting_points] # 最大堆
while dir_queue:
current_uri, parent_score = heappop_best(dir_queue)
children = vector_search(parent_uri=current_uri, query_vector) # 只搜直接子节点
if thinking_mode and rerank_available:
scores = rerank(query, [c.abstract for c in children]) # 精排,失败回退向量分
for child, score in zip(children, scores):
final = alpha * score + (1 - alpha) * parent_score # 分数传播,默认 alpha=1.0
if final < threshold:
continue
collected[child.uri] = max(collected.get(child.uri), final)
if child.is_directory: # L2 文件是终点,目录继续下钻
heappush(dir_queue, (-final, child.uri))
if topk_unchanged_for_3_rounds or pool_stagnant_for_3_rounds: # 收敛检测
break
几个从源码核实的参数:收敛判定 MAX_CONVERGENCE_ROUNDS = 3(L55),每轮最多并行展开 4 个目录(MAX_PARALLEL_CHILD_SEARCHES = 4,L58,注释写明是为了限制对远端向量库的扇出),分数传播系数 score_propagation_alpha 默认 1.0——即默认只信子节点自己的分数,父分数传播是留着的钩子而非当前行为。
这个算法回答了第一笔调试账:检索过程就是一条目录浏览轨迹(进了哪个目录、每层看到什么分数、在哪停下),错了可以逐跳回放。代价也明确:多轮向量查询 + 可选 rerank,延迟高于一次平铺检索。官方自己在 find/search 对比表里承认 search() 延迟「较高」——这是拿延迟换可解释性和结构完整性的灰度选择,不是免费午餐。
第三次换表示:经验目录是策略集,更新是语义梯度
前两次换表示解决存和取,第三次解决「自进化」——这是它区别于一般记忆框架的部分。
会话如何变成记忆
session.commit() 分两阶段:同步阶段只做归档(消息写入 history/archive_NNN/messages.jsonl,立即返回 task_id);异步阶段由 LLM 生成会话摘要、按 schema 提取候选记忆,然后走一条去重流水线:向量预过滤找相似旧记忆 → LLM 做去重决策(跳过 / 新建 / 合并进旧记忆 / 删除冲突旧记忆)→ 写入。
每次提交在归档目录留一份 memory_diff.json,用 before/after 记录本次所有记忆增删改。这个设计值得单独点赞:记忆系统的写操作有了审计日志,这是「数据库范式」对「黑盒范式」的又一次胜利——多 Agent 内存那篇里论文呼吁的「结构化访问控制」,在这里至少有了可回溯的一半。
Agent Evolution:把 RL 的骨架搬到自然语言上
内置记忆类型里有三个特殊成员:cases(任务案例)、trajectories(执行轨迹)、experiences(经验)。启用 experiences 会激活完整的进化链路。docs/design/traj-exp-experience-learning-redesign.md 把设计意图说得毫不掩饰:experiences 目录被视为一个可优化的 Experience Policy Set(经验策略集),训练框架的抽象链路是:
flowchart LR
A[CaseLoader<br>加载任务] --> B[RolloutExecutor<br>跑一遍任务]
B --> C[RolloutAnalyzer<br>抽取轨迹]
C --> D[GradientEstimator<br>估计语义梯度]
D --> E[PolicyOptimizer<br>生成更新计划]
E --> F[PolicyUpdater<br>写回经验文件]
F -.经验参与下次执行.-> B
「梯度」在这里是什么?openviking/session/train/gradients.py 全文只有 46 行,核心是:
@dataclass(slots=True)
class PatchSemanticGradient:
before_file: MemoryFile | None # None 表示建议新建这条经验
after_file: MemoryFile # 建议的目标状态
base_version: int | None
rationale: str # 为什么这么改
links: list[StoredLink] # 经验 -> 来源轨迹的溯源链接
confidence: float
一个「语义梯度」就是一对 before/after 经验文件,加一段修改理由和置信度。这和 TextGrad(arXiv:2406.07496)的思路同源——把「梯度」重新定义为自然语言的修改信号——但 OpenViking 把它落到了更工程的地方:梯度不是自由文本,而是结构化的文件 diff;应用梯度前有 ExperienceSet.lock() 串行化(rollout 和梯度估计可并行,但 reload → plan → apply 必须持锁串行),还有 base-content guard 防止覆盖已经被别人改过的经验。这是把数据库的并发控制纪律,用在了「学习」这件事上。
设计文档还规定了溯源协议:每条经验通过 derived_from 链接指回产生它的轨迹。经验错了可以顺着链接找到「它是从哪次失败里学来的」。
我的判断是:这一层是整个项目最有野心也最未经检验的部分。类 RL 的骨架(case → rollout → gradient → policy update)搬到自然语言上之后,收敛性没有任何理论保证,全靠 LLM 做 GradientEstimator 的质量兜底。官方给出的 tau2-bench 提升(见下节)说明这条链路在客服场景能跑通,但「经验策略集会不会随迭代漂移、退化」这类问题,目前没有公开数据回答。
工程底座:Rust 文件系统 + 只存索引的向量库
存储是双层的,职责切得很干净:
| 层 | 存什么 | 不存什么 |
|---|---|---|
| AGFS(内容层) | L0/L1/L2 全部内容、多媒体、关联关系 | — |
| 向量库(索引层) | URI、向量、元数据 | 文件内容 |
单一数据源原则:所有内容从 AGFS 读,向量库只存引用——索引坏了可以从内容重建,反之不行。AGFS 已用 Rust 重写为 RAGFS(crates/ragfs),是一个插件式虚拟文件系统:MemFS、KVFS、QueueFS 等实现挂载在不同路径下,另有 cache(Redis / Mooncake / 元戎等多种后端)、crypto(静态加密)、git(版本控制)模块。连语义处理队列本身都是文件系统的一个插件(QueueFS)——「一切皆文件」贯彻到了自己的内部实现。
Embedding 输入有白名单纪律:sidecar frontmatter 里只有 directory 字段进向量,source、generated_by、freshness 都不进——保证重建索引不会改变检索语义。这种「哪些字节参与索引」的显式契约,又是数据库工程的味道。
官方数字与它们的边界
以下数字全部来自官方 README 与 benchmark 报告(版本 0.3.22),我未亲手复现,复现脚本在仓库 benchmark/ 目录:
| 基准 | 无 OpenViking | 有 OpenViking | 变化 |
|---|---|---|---|
| LoCoMo(OpenClaw 原生记忆) | 24.20% | 82.08% | +57.88pp |
| LoCoMo(Hermes 原生记忆) | 33.38% | 82.86% | +49.48pp |
| LoCoMo(Claude Code 原生记忆) | 57.21% | 80.32% | +23.11pp |
| tau2-bench retail(无记忆) | 70.94% | 77.81% | +6.87pp |
| tau2-bench airline(无记忆) | 54.38% | 66.25% | +11.87pp |
官方同时报告 LoCoMo 场景下输入 token 下降 34.3%–91.0%、查询延迟下降 58.45%–66.10%。
两点解读边界:其一,LoCoMo 的对照组是各 Agent 的「原生记忆」(把历史直接塞上下文或简单摘要),赢原生记忆是记忆框架的及格线,Mem0、MemOS 等在各自报告里也都大幅赢原生基线,跨框架横比要看第三方评测。其二,tau2-bench 那两行是「经验记忆」的增量收益,+6.87pp 和 +11.87pp 是这个「自进化」故事目前最硬的证据,但只覆盖客服型任务。
三次换表示,一张总表
| 换表示 | 从 | 到 | 换来什么 | 代价 |
|---|---|---|---|---|
| 上下文 → 文件系统 | 记忆/RAG/技能三套 API | 一棵 viking:// 目录树 + 统一动词 | 确定性寻址、可浏览、可审计 | 所有内容都要过一遍导入流水线 |
| 内容 → 三层分辨率 | 扁平 chunk | 目录级 L0/L1 + 原文 L2 | 检索粒度与加载粒度解耦,省 token | 写入侧 LLM 成本 + 父级冒泡写放大(官方 TODO 承认) |
| 经验 → 策略集 | 一次性写入的记忆 | 带锁、带版本、带溯源的可优化经验文件 | 任务成功率可随使用提升(官方 tau2 数据) | 收敛无保证,依赖 LLM 梯度质量 |
用 12 透镜收束:OpenViking = 表示决定成败(文件系统 + 三层分辨率,占八成)+ 搜索与优化(优先队列下钻)+ 反馈闭环(rollout → 语义梯度 → 经验更新)。它赌的是:上下文工程的下一步不是更好的 embedding,而是更好的数据组织——五十年数据库工程的纪律(寻址、事务、审计、并发控制、索引与内容分离)值得在 Agent 上下文上重演一遍。
诚实的提醒
- 本文所有 benchmark 数字来自官方报告,未亲手复现;官方评测用的 rerank 后端是自家的 doubao-seed-rerank,换后端后的检索质量未知。
- 「层级检索优于平铺检索」在本文中是设计论证 + 官方数据支撑的判断,不是我验证过的事实;层级检索的多轮查询延迟代价在深目录树上有多大,同样未测。
- Agent Evolution 链路我只读了设计文档和 domain/gradient 层源码(
session/train/),2171 行的compressor_v3.py提取主流程没有逐行读完,「案例触发轨迹与经验训练」的描述以文档为准。 - 成本最低的亲手验证实验:
pip install openviking --upgrade && openviking-server init(向导支持本地 Ollama,可零 API 成本),然后ov add-resource导入任意一个 GitHub 仓库,用ov tree -L 2看 L0/L1 sidecar 是否如期生成,再ov find "某个只在深层文档里出现的概念"对照返回的目录轨迹——半小时内能验证本文前两次「换表示」的全部核心声明。
参考来源
工程实践
- volcengine/OpenViking — 源码仓库(本文基于 commit
dc39985,2026-08-19) - OpenViking 文档:架构 / 上下文层级 / 检索 / 会话 — 本文架构描述的主要出处
- OpenViking benchmark 报告 — LoCoMo / tau2-bench 数字出处
- 仓库内设计文档
docs/design/traj-exp-experience-learning-redesign.md— Agent Evolution 框架设计
arXiv 论文
- LoCoMo: Evaluating Very Long-Term Conversational Memory of LLM Agents(arXiv:2402.17753)
- TextGrad: Automatic “Differentiation” via Text(arXiv:2406.07496) — 「语义梯度」思路的学术源头
- MemGPT: Towards LLMs as Operating Systems(arXiv:2310.08560) — 分层上下文管理的早期代表,OpenViking 的 L0/L1/L2 可视为其「分层调页」思想在数据组织侧的落地
本站相关
- 别把向量数据库当知识库 — 「向量库是定位组件」论点的展开
- 读 MemOS 源码:记忆的三态 — 另一个记忆系统的源码审计,可对照
- 遗忘是一种能力 — context rot 与上下文预算
- 多 Agent 的内存问题 — 记忆系统的一致性与访问控制视角
- tree-sitter 源码原理 — 代码骨架提取的底层机制