
导读:你以为
client.add_resource()只是"存了个文件"?天真了。这行代码背后,是一场涉及 Parser、DAG、LLM、双队列、向量数据库的"大型流水线工程"。今天,我们把 OpenViking 的向量化链路扒个底朝天。
很多开发者第一次用 OpenViking 时,心里都会嘀咕:
"我传了个文件,它到底进没进向量库?"
答案是:进了,而且走了整整两套异步队列。
OpenViking 没有采用传统 RAG "解析→切片→Embedding→入库" 的扁平套路,而是设计了一套文件系统范式 + 三级向量索引 + 层级递归检索的架构。目录有向量,文件有向量,摘要还有向量。检索时不是简单搜一遍,而是像剥洋葱一样层层递归。
这套设计的硬核程度,足以让传统 RAG 框架沉默。
快速开始里的代码看起来人畜无害:
client = ov.OpenViking(path="./data")
client.initialize()
client.add_resource("https://github.com/volcengine/OpenViking")
resources = client.ls("viking://resources/")
results = client.find("context database")
但这五行代码,每一行都在后台掀起波澜。
client.initialize() 不是简单读个配置。它在后台:
换句话说,你调的不是一个客户端,是在本地起了一个完整的上下文服务集群。
你以为 add_resource() 就是"复制文件到某个目录"?太年轻了。
add_resource(path, build_index=True)
└→ ResourceService.add_resource()
└→ ResourceProcessor.process_resource() ← 六幕大戏从这里开始
├→ 第一幕:MediaProcessor.process() —— Parser 解析,写入 viking://temp/
├→ 第二幕:TreeBuilder.finalize_from_temp() —— temp → AGFS 最终目录
├→ 第三幕:生命周期锁 + mv 落盘
└→ 第四幕:Summarizer.summarize() —— 入队 SemanticQueue
└→ SemanticQueue.enqueue(SemanticMsg)
└→ SemanticProcessor.on_dequeue()
├→ SemanticDagExecutor.run() —— DAG 驱动,自底向上遍历
│ ├→ 叶子文件:LLM/VLM 生成摘要
│ └→ 目录节点:生成 .abstract.md + .overview.md
│
├→ _vectorize_directory() —— 目录 L0+L1 入 EmbeddingQueue
└→ _vectorize_single_file() —— 文件内容入 EmbeddingQueue
└→ EmbeddingQueue.enqueue()
└→ TextEmbeddingHandler.on_dequeue()
├→ Embedder 生成 Dense + Sparse 向量
└→ 写入 VectorDB
六幕分解:
幕 | 操作 | 耗时 | 是否阻塞用户 |
|---|---|---|---|
第一幕 | Parser 解析文件结构 | 秒级 | 同步,阻塞 |
第二幕 | TreeBuilder 计算目标路径 | 毫秒 | 同步,阻塞 |
第三幕 | mv 落盘 + 生命周期锁 | 毫秒 | 同步,阻塞 |
第四幕 | Summarizer 构造消息入队 | 毫秒 | 同步,阻塞 |
第五幕 | SemanticQueue → L0/L1 生成 | 秒~分钟 | 异步,不阻塞 |
第六幕 | EmbeddingQueue → 向量入库 | 秒~分钟 | 异步,不阻塞 |
关键设计:前四幕同步完成,保证用户立即看到文件已上传;后两幕异步执行,LLM 摘要和 Embedding 在后台慢慢消化。用户体验和系统吞吐量兼得。
这行最简单,就是 VikingFS.ls() → AGFS 列目录。没有向量参与,纯文件系统操作。
这行代码背后,OpenViking 执行了一套层级递归检索:
用户查询: "context database"
│
▼
Step 1: 查询文本 → Embedder → 查询向量 (Dense + Sparse)
│
▼
Step 2: 全局向量搜索 —— 在整棵目录树的 L0/L1/L2 中找 top-K 候选
│
▼
Step 3: Rerank 精排 —— 用 thinking 模式对候选摘要做二次排序
│
▼
Step 4: 递归目录搜索 —— 像 DFS 一样逐层深入,分数从父目录向子节点传播
│
▼
Step 5: 收敛检测 —— 连续 3 轮 top-K 不变就自动停止,不浪费算力
│
▼
Step 6: 热度分融合 + 返回
传统 RAG 是"搜一遍完事",OpenViking 是"搜到收敛为止"。
OpenViking 最核心的创新之一,是目录也入向量。
传统 RAG 只给文档切片建索引,OpenViking 给每个目录和文件都建了三级索引:
级别 | 文件 | 内容规模 | 作用 |
|---|---|---|---|
L0 | .abstract.md | ~100 tokens | 一句话摘要,用于快速筛选 |
L1 | .overview.md | ~2,000 tokens | 详细概览,用于精排和展示 |
L2 | 原始文件 | 全文 | 最终返回的完整内容 |
一个目录会产生 2 条向量记录(L0 + L1),一个文件产生 1 条。这意味着检索时,系统可以先在 L0 层面快速排除不相关目录,再深入 L1 层面做精排,最后才读取 L2 的完整内容。
这就像搜索引擎的"标题 → 摘要 → 全文"三级过滤,但发生在向量空间。
OpenViking 没有让 LLM 摘要和 Embedding 生成串行执行,而是拆成了两个独立队列:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Summarizer │ ──→ │ SemanticQueue │ ──→ │ SemanticProcessor│
│ (构造消息入队) │ │ (语义处理队列) │ │ (LLM 生成 L0/L1) │
└─────────────────┘ └──────────────────┘ └────────┬────────┘
│
▼
┌─────────────────┐
│ EmbeddingQueue │
│ (向量化队列) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ TextEmbeddingHandler
│ (Embedder → VectorDB)│
└─────────────────┘
为什么拆两个队列?
OpenViking 不只是一个"文件搜索引擎",它还有完整的记忆系统。
当你这样使用时:
session = client.create_session()
client.add_message(session["session_id"], "user", "我喜欢用 Python,偏好简洁风格")
client.commit_session(session["session_id"])
后台发生了一场"记忆提取 + 向量化"的完整闭环:
commit_session()
├→ Phase 1: 归档消息 → 写入 history/archive_xxx/ → 生成 L0/L1 → 向量化
└→ Phase 2: 记忆提取
→ SessionCompressorV2 (V2 版本)
→ ExtractLoop (ReAct 编排器,最多 3 轮 LLM+Tool Use)
→ 输出 MemoryOperations
→ MemoryUpdater (字段级合并策略: patch/sum/immutable)
→ 写入记忆文件 (10 种类型: profile/preferences/entities/...)
→ _flush_semantic_operations()
→ SemanticMsg(context_type="memory")
→ SemanticQueue → L0/L1 生成 → EmbeddingQueue → VectorDB
10 种记忆类型,从用户侧的 profile、preferences、entities,到 Agent 侧的 cases、patterns、skills、soul、identity,全部走向量化。
下次你搜索时,这些记忆会和资源同时被检索,但通过 owner_space 字段自动隔离——你的记忆只有你能看到。
维度 | 传统 RAG | OpenViking |
|---|---|---|
向量化对象 | 仅文档切片 | 目录 L0+L1 + 文件内容,三级索引 |
索引结构 | 扁平列表 | 目录层级树 + 向量索引 |
检索方式 | 单次向量搜索 | 全局搜索 + 递归目录搜索 + Rerank |
分数计算 | 纯向量相似度 | 向量分 × Rerank × 分数传播 × 热度分 |
结果粒度 | 固定大小切片 | L0 摘要 / L1 概览 / L2 全文,按需加载 |
租户隔离 | 通常无 | owner_space 字段自动隔离 |
一致性 | 更新可能残留旧向量 | rm/mv 操作同步更新向量索引 |
收敛控制 | 无 | 连续 3 轮 top-K 不变自动停止 |
传统 RAG 把知识库当成一个"大文档堆",OpenViking 把它当成一个"有结构的文件系统"。这不仅仅是工程实现的不同,是范式级别的差异。
回到开头的问题:你的文件上传后,到底经历了什么?
答案是:它经历了一场从文件系统到向量空间的完整蜕变。
Parser 把它拆解成目录树,LLM 给它生成三级摘要,Embedder 把它变成 Dense + Sparse 双模态向量,HierarchicalRetriever 在检索时像剥洋葱一样层层深入。而你的每一次对话,又通过记忆系统回流到这个向量空间中,形成自迭代的闭环。
OpenViking 的设计哲学很清晰:上下文不是数据,是结构化的、可检索的、可迭代的记忆。
这五行代码的背后,是一套完整的上下文操作系统。