工程实现
OpenViking 源码深度解析 · Rust + Python 混合架构
AI Agent 的记忆系统面临一个根本张力:上下文窗口再大,也装不下一个持续运行数月的多会话项目的全部历史。增量写入 vs 全量检索,版本回溯 vs 存储成本,这三个正交问题很难用一套基础设施同时解决好。OpenViking(/tmp/openviking)的解法是把 Git 对象存储直接拿来做记忆底座,源码可查,方案完整。本文从设计决策到 Rust 实现逐层解构。
01
AI 记忆系统的存储需求,本质上可以拆解为三个问题域:写入(记录每次会话的产出)、检索(在历史中捞出相关内容)、回溯(恢复到某个时间点的状态)。这三个问题域对存储引擎的要求并不完全重合。
PostgreSQL 擅长写入和检索,Redis 擅长低延迟读取,但如果要支持「查看这个文件三天前的版本」或「把项目回滚到上周的状态」,数据库方案需要自行实现版本快照逻辑,存储成本高,diff 能力也需额外构建。而 AI 记忆的使用模式天然是追加写 + 可回溯的,这和 Git 的 commit log 高度同构:每次「记忆存档」即一次 commit,检索即 git log / git show,精确回溯即 git restore。
OpenViking 选 Git 的核心逻辑是:用一套基础设施同时解决写入、检索、回溯三个问题,不需要引入额外的版本管理系统。具体来看:
存储方案对比:AI 记忆场景
Git 的代价是:读取性能不如 KV,复杂检索(如语义搜索)仍需在上层补充向量数据库。但对于「精确状态回溯」这个核心需求,Git 的实现质量和运维成本是当前最优解。
02
Rust 层定义了一个极简的 ObjectStore trait(crates/ragfs/src/git/object_store.rs,44 行),这是整个对象存储层的核心抽象。只有三个方法:
crates/ragfs/src/git/object_store.rs
#[async_trait]
pub trait ObjectStore: Send + Sync + 'static {
async fn put(&self, account: &str, oid: &ObjectId, zlib_body: Bytes)
-> Result<(), ObjectStoreError>;
async fn get(&self, account: &str, oid: &ObjectId)
-> Result<Bytes, ObjectStoreError>;
async fn exists(&self, account: &str, oid: &ObjectId)
-> Result<bool, ObjectStoreError>;
}
三个方法的语义设计非常讲究。put 的幂等性是关键设计:同 OID 重复写入是 no-op,不管调用多少次结果都一样。这意味着并发写入天然安全,不需要分布式协调协议。exists 是轻量优化路径,返回 bool 不读内容,让调用方在写之前先做快速检测,避免不必要的 IO。get 返回完整的 uncompressed 字节流,包含 Git header(type + size + \0 + content),这是 git 标准的对象格式。
这个 trait 的设计遵循了接口最小化原则:只暴露必要的能力,不暴露实现细节。调用方只知道「对象可以 put / get / exists」,不需要关心底层是本地文件系统还是 S3。local 和 S3 两种后端都通过同一个 trait 接入,上层 GitService 完全不需要改动。
03
本地实现(crates/ragfs/src/git/backends/local.rs,537 行)的 put 方法完整实现了五个工程决策,这些决策共同保证了并发写入的正确性:
LocalObjectStore put() 的五个工程细节
try_exists(),对象已存在则直接返回,不读写任何字节create_dir_all),避免写文件时找不到目录.tmp.{pid}.{seq} 再 rename,操作系统 rename 是原子的,crash 不留中间状态对象路径遵循 Git 标准 SHA-1 目录布局,使用前两位做一级哈希分区,避免单目录文件数过多导致文件系统性能退化:
crates/ragfs/src/git/backends/local.rs
fn object_path(&self, account: &str, oid: &ObjectId) -> PathBuf {
let hex = oid.to_hex().to_string();
self.base_dir
.join(account)
.join("objects")
.join(&hex[..2]) // 前 2 位做一级目录
.join(&hex[2..]) // 剩余 38 位做文件名
}
一个 SHA-1 是 40 位十六进制字符串,按前 2 后 38 切分后,每个一级目录下最多 16^38 个对象,实际不会达到这个量级,但 2 位 hex 最多 255 个子目录(00-ff),每个子目录下仍可能有大量文件,Git 本身在对象超过几千个时会做 packfile 压缩,这是后续优化方向,当前实现满足单节点轻量使用场景。
04
整个架构分为三层,分别对应三个代码模块:
三层调用栈
以 commit 为例,调用链是这样的:Python VikingFS.commit() 接收一个 viking://account/path/to/file URI,通过 _uri_to_tree_path() 转成 account 相对路径 path/to/file,然后调用 _async_agfs.run("git_commit", ...) 透传到 Rust 侧。Rust GitService.commit() 遍历目录树,对每个文件计算 SHA-1(内容寻址),通过 ObjectStore.put() 写入 blob,计算 tree 对象,再写入 commit 对象,最后更新 ref(分支指针)。
Rust 侧全程异步(基于 tokio runtime),不阻塞 Python 事件循环。Python 和 Rust 之间通过 PyO3 绑定通信,ObjectStore 在 Rust 侧通过 Arc 共享,可以安全跨异步任务使用。
restore 流程是 commit 的逆过程:给定一个 commit OID,GitService 读取该 commit 的 tree,递归展开每个 blob,写入 VFS 层文件。生成的新 commit 以当前 HEAD 为父 commit,而非以 source_commit 为父——这是刻意设计的:restore 是一个「新建状态」的操作,不是「穿越回去」的操作,保留完整历史。
05
local 后端解决了单节点场景的问题。OpenViking 还支持 S3 后端(feature-gated,需要编译时开启 s3 feature),配置段如下:
ragfs.toml
[git]
enabled = true
backend = "s3"
default_branch = "main"
[git.s3]
bucket = "my-ai-memory"
region = "us-east-1"
prefix = "accounts/" // S3 key 前缀,按 account 分区
access_key = "AKIA..." // 从环境变量读取更安全
secret_key = "..."
cas_mode = "native" // 或 "redis_lock"
S3 后端的核心价值是:同一个 Git repo 可以被多个进程、多台机器同时读写,实现了真正的共享记忆网络。多个 AI Agent 实例可以读同一份 commit 历史,写入时通过 S3 的原子性保证 OID 不冲突。这解决了 local 后端的根本局限:单节点文件系统无法跨机器共享。
S3 后端支持两种 CAS(Content-Addressable Storage)一致性模式。native 模式依赖 S3 自身的 PutObject 原子性(etag 一致即写入成功);redis_lock 模式在 S3 写入前通过 Redis 分布式锁对 (account, oid) 加锁,保证并发写入串行化,适合超高并发场景(多个 Agent 同时大量写入记忆)。
06
对象存储解决了 blob/tree/commit 的存储问题,但 Git 的分支语义是通过 ref(引用)实现的。OpenViking 的 LocalRefStore(crates/ragfs/src/git/backends/local.rs)用两个机制保证 ref 的并发安全:
RefStore 的并发安全机制
RefStore 的设计意味着:每个账户可以有多个分支(main / dev / experiment),不同 Agent 可以基于不同分支做实验而不互相干扰。这对 AI 记忆场景很有价值:main 分支是稳定记忆,dev 分支可以尝试新的记忆组织策略,实验完成后 merge 回 main。
07
Git 对象存储的读取路径上有一个潜在性能问题:给定一个 commit OID,要找到「这个账户所有 commit 的时间线」,需要从 HEAD 开始线性遍历所有 commit 对象,每个 commit 对象都需要一次文件系统 read。对于记忆系统常见的「列出最近 30 条记忆」场景,这会触发 30 次独立 IO。
OpenViking 通过 IndexStore(crates/ragfs/src/git/index_store.rs)解决这个瓶颈。IndexStore 维护一个账户维度的 commit 索引,包含 commit OID → (author, timestamp, message, tree_oid) 的映射。默认关闭(tuning.commit_index_enabled = false),需要时在 ragfs.toml 开启:
ragfs.toml
[git.tuning]
commit_index_enabled = true // 开启 commit 索引加速
blob_exists_precheck_enabled = true // 写 blob 前先 exists 检测
开启 IndexStore 后,每次 commit 会同时更新索引文件(也是本地文件),「列出最近 30 条记忆」变成一次顺序读索引文件 + 30 次随机读 commit 对象(可并行),而不是 30 次串行目录遍历。代价是每次 commit 多一次写索引文件 IO,以及索引文件本身的存储成本。适合 commit 频率高且频繁检索的生产环境。
08
一个合理的疑问是:Git 对象存储已有成熟的 C 库 libgit2,为什么 OpenViking 选择自己实现?答案在两个维度。
第一,libgit2 是通用 Git 实现,API 面向版本控制系统设计,commit / checkout / merge / rebase 全部支持。但 AI 记忆系统只需要 blob / tree / commit 的读写能力,不需要 checkout 语义(不需要把文件内容还原到工作目录),也不需要 merge 语义(分支策略由上层应用决定)。自己实现可以从 6057 行 GitService 中删掉大量不需要的代码,只保留记忆系统需要的那部分。
第二,AI 记忆系统对 Git 有特殊需求:多账户隔离、URI 映射(viking:// 协议)、异步 IO(tokio)、S3 后端、IndexStore 加速。这些需求在 libgit2 上做绑定的成本不比自实现低,且自实现可以精确控制每个 IO 的时机和并发策略。Rust 的 async/await + tokio runtime 给这套架构提供了很好的性能基础。
代价是:需要自行维护 Git 对象格式的兼容性,包括 zlib 压缩格式、SHA-1 OID 计算、Git packfile 格式(未来)等。libgit2 在这些边界情况上有多年积累的 bug fix,自实现需要随着用户规模增长逐步补齐。当前阶段自实现足够覆盖 AI 记忆的核心语义。
· · ·
核心洞察 · CORE INSIGHT
AI 记忆系统的正确架构是追加写的版本化存储,而不是原地更新的 KV。Git 对象模型天然满足这个语义,且零额外基础设施成本。幂等性是并发安全的核心,ObjectStore trait 的 put 幂等 + rename 原子性共同保证。
END · 完