存储层:VikingFS 门面、RAGFS 虚拟文件系统与向量库
30 秒导读: 上层看到的是
viking://user/alice/notes/a.md这样的虚拟路径(范式见 01)。这一章讲它到底落到哪、怎么落:VikingFS是 Python 侧的门面,把 URI 翻成按账号隔离的物理路径、守住"哪些命名空间能写能删"、给加密写加锁;再通过一份 Python↔Rust 契约把活交给RAGFS——一个用 Rust 重写的 AGFS 虚拟文件系统,负责多后端挂载、缓存、版本化与真正的字节读写。与此并行,同一个文件动作还会被镜像进向量索引后端:一份文件语义 = 向量集合里的若干条 level 0/1/2 记录 + KV 里的标量行。
1. 这是什么(零基础也能懂)
一句话定义: 存储层 = "把虚拟文件语义变成真实字节 + 可检索向量"的那一层。它夹在上层的 URI 语义和最底层的 ANN/KV 引擎(见 06)之间。
它同时干两件互相独立又必须同步的事:
| 落到哪 | 存什么 | 谁负责 |
|---|---|---|
| 文件系统(字节) | 文件原文、目录树、.abstract.md/.overview.md | VikingFS → RAGFS → 后端插件 |
| 向量库(可检索) | 每份内容的稠密向量 + 标量字段(uri/level/tags…) | VikingVectorIndexBackend → collection + KV |
为什么要两套? 文件系统回答"给我 a.md 的内容";向量库回答"和'退款政策'语义最近的是哪几份内容"。检索靠后者(算法见 03),但真相永远以文件系统为准——所以删一个文件时,两边都得动,且不能删歪账号。
一句话直觉: 把 VikingFS 当收发室——它不亲自搬箱子,只做三件事:查你有没有权限往这个格子放东西、给箱子贴上带账号的物理货位号、然后喊 RAGFS 这个仓库机器人去搬。搬完顺手更新一张语义索引卡(向量库),方便以后按"意思"找货。
本节不出现底层代码;只要记住:一层门面(VikingFS)、一份契约(pyagfs)、一个仓库(RAGFS)、一套语义索引(向量库)。
2. 顶层全景(它大概怎么转)
怎么读这张图: 从上到下是一次写入的下沉路径;左边是字节主线,右边是它触发的向量镜像。左右在 VikingFS 里被同一个方法编排。
上层调用 viking://user/alice/notes/a.md , data
│
┌─────────────────────▼──────────────────────┐
│ VikingFS(Python 门面 storage/viking_fs.py)│
│ ① 校验:能不能写这个命名空间 │
│ ② _uri_to_path:URI → /local/{account}/… │
│ ③ 加密写加锁(配了 encryptor 时) │
└───────┬───────────────────────────┬─────────┘
│ 字节主线 │ 向量镜像(rm/mv 时)
▼ ▼
┌───────────────────────┐ ┌──────────────────────────────┐
│ pyagfs 契约 │ │ VikingVectorIndexBackend │
│ AGFSSyncClientProtocol │ │ (per-account 后端门面) │
│ ls/read/write/mv/grep… │ │ upsert / delete_uris / │
│ + ctx={account_id} │ │ update_uri_mapping │
└──────────┬─────────────┘ └───────────────┬──────────────┘
│ PyO3 绑定 │
▼ ▼
┌───────────────────────────┐ ┌───────────────────────────┐
│ RAGFS(Rust · crates/ragfs)│ │ collection + KV │
│ Stats→Mountable→per-mount: │ │ 稠密向量→ANN 索引 │
│ Cache / Encryption 包装 │ │ 标量字段→KV(RocksDB) 行 │
│ multibackend/git/shape │ │ 一份文件 = level 0/1/2 记录 │
└──────────┬────────────────┘ └───────────────────────────┘
▼ (ANN/KV 引擎细节见 06)
localfs / memfs / s3fs / kvfs …
部件一句话职责:
| 部件 | 干什么 | 在哪 |
|---|---|---|
VikingFS | URI↔路径映射、命名空间守卫、加密写锁、rm/mv 时同步向量库 | openviking/storage/viking_fs.py:253 |
AGFSSyncClientProtocol | Python 侧对"AGFS 客户端"的最小契约(同步接口) | openviking/pyagfs/protocols.py:12 |
AsyncAGFSClient | 把同步客户端丢到线程池跑,并按路径注入 account_id | openviking/pyagfs/async_client.py:57 |
RAGFSBindingClient | PyO3 原生类,Rust 侧实现上述契约 | crates/ragfs-python/src/lib.rs:849 |
FileSystem(trait) | RAGFS 所有后端必须实现的统一文件接口 | crates/ragfs/src/core/filesystem.rs:97 |
MountableFS | 按挂载点路由,逐后端套 Cache/Encryption | crates/ragfs/src/core/mountable.rs:62 |
VikingVectorIndexBackend | per-account 向量后端门面:写/删/改 URI 映射 | openviking/storage/viking_vector_index_backend.py:644 |
3. 核心原理(逐个机制,由浅入深)
3.1 VikingFS 门面:三道关口
VikingFS 对上暴露 read/write/mkdir/rm/mv/grep/ls/stat 等(storage/viking_fs.py:476 起),但它自己不碰字节。它的价值全在"转交之前"和"转交之后"做的守卫与编排。
关口一:URI → account 隔离的物理路径。 映射是纯前缀替换——viking://{余下} → /local/{account_id}/{余下},account_id 来自请求上下文:
# 示意,非源码;对应 _uri_to_path
def _uri_to_path(uri, ctx):
account_id = ctx.account_id # 租户身份
_, parts = normalized_uri_parts(uri) # 拆 viking:// 之后的段
return f"/local/{account_id}/" + "/".join(parts)
真实实现在 _uri_to_path(storage/viking_fs.py:2367)。多租户隔离的物理基础就这一行:不同账号即便同名 URI,落盘目录也不同。规范化前还会拒绝 ./..、\、C: 这类穿越/平台特例段(_normalized_uri_parts,storage/viking_fs.py:317),从源头堵路径穿越。
关口二:命名空间写/删校验。 不是所有 URI 都能写或删。_ensure_supported_write_namespace(storage/viking_fs.py:449)与 _ensure_supported_delete_namespace(storage/viking_fs.py:425)在动手前拦截几类"会伤到共享根"的目标:
| 目标 | 写 | 删 | 原因 |
|---|---|---|---|
viking://user(裸根) | 拒 | 拒 | 会跨用户误伤,必须给到具体用户命名空间 |
viking://agent(裸根) | 拒 | 拒 | 递归会抹掉每个账号的 skills/endpoints/tools/payments |
viking://agent/{id}(旧格式) | 拒(写) | — | 已废弃,改用 viking://user/.../peers/{id} |
viking://session/... | 拒 | — | session 只读,改用 user 命名空间 |
viking://temp(非 root) | 拒 | 拒 | temp 根对非 root 只读 |
关口三:加密写锁。 当配置了 encryptor,写入不是"原地覆盖",而是"写临时文件再原子替换"。为避免并发写撞车,_run_with_encrypted_write_lock(storage/viking_fs.py:369)对最终路径 + 临时路径两条路径同时上 "exact" 锁:
# 示意,非源码
async def _run_with_encrypted_write_lock(path, op):
if encryptor is None:
return await op() # 明文栈:不加这层锁
lock_paths = [path, _encrypted_temp_path(path)] # 两条路径一起锁
async with LockContext(mgr, lock_paths, lock_mode="exact"):
return await op()
临时路径是确定性推导的(_encrypted_temp_path,storage/viking_fs.py:352):按最终路径的 mount 相对路径做 sha256,落到 .../temp/.encrypt_stage/{digest}.encrypt。确定性是为了让锁"同一个最终文件 → 同一个临时文件"能真正互斥。(锁管理器与租约见 storage/transaction/lock_manager.py:35。)
注意:加密本身发生在 Rust 层(按
account_id派生密钥),Python 侧只负责加这道跨双路径的互斥锁;write()把明文交给下层即可(storage/viking_fs.py:514)。