从原理到落地:基于OpenViking构建多模态RAG方案

Posted by iceyao on Sunday, March 22, 2026

一、为什么要单独看 OpenViking

OpenViking 值得分析,不是因为它最近热,而是因为它踩中了 Agent 系统里最难啃的一块:上下文管理

模型这两年进步很快,但一个真正能连续工作的 Agent,问题常常不出在模型本身,而出在下面这些地方:

  • 用户偏好放哪儿
  • 历史任务怎么留
  • 项目文档和代码怎么组织
  • 当前会话里的临时信息怎么接住
  • 做完任务以后,哪些东西该沉淀成长期记忆

传统 RAG 更像一个"文本碎片检索器":把文档切块、向量化,再从一堆 chunk 里挑若干片段塞进 prompt。这个办法做 FAQ 或单轮问答没什么问题,但场景换成长任务、多步骤执行,问题就会变得很具体:

  1. 目录结构和上下文边界被切碎了。
  2. 检索拿到的内容局部相关,全局却可能跑偏。
  3. 为了保险,系统容易把太多文本塞进模型,token 成本一路涨。
  4. 出了问题不好查,因为很难解释检索链路到底是怎么走偏的。

OpenViking 想处理的就是这一类问题。它没有沿着"做一个更强的向量库"这条路继续卷,而是换了个抽象:把 Agent 的上下文当成文件系统来组织。

官方对它的定义是 open-source context database for AI Agents。这个定义里最值得看的是后半句——它并不把上下文理解成一堆孤立文本,而是理解成一个有目录、有层次、能递归导航的空间。

先看一张总览图:

OpenViking 上下文文件系统总览图

我更愿意把 OpenViking 看成一层上下文操作层。它上面接 Agent 或应用,下面接 embedding、模型服务和本地存储,中间负责三件事:

  • 统一命名空间
  • 分层加工上下文
  • 按路径把正确内容送到模型面前

二、它的核心思路:别再把上下文只当 chunk 池

OpenViking 的关键变化,不是检索接口换了个名字,而是上下文抽象变了。

在传统 RAG 里,一份知识通常会经历这样的路径:

  • 文档
  • chunk 切分
  • embedding
  • 全局相似度检索
  • TopK 拼 prompt

在 OpenViking 里,更接近下面这个过程:

  • 资源进入 viking:// 命名空间
  • 资源被组织成目录和文件
  • 每一层目录都生成摘要和概览
  • 查询先定位目录,再逐层下钻
  • 最后只读取真正要看的细节

这两个思路的区别,看起来只是"多了一层目录",其实差别很大。

很多 Agent 任务,本来就不是"找到最像的三段文本",而是:

  • 先找到正确的项目空间
  • 再找到正确的模块或资料目录
  • 再决定读哪几个文件
  • 最后把少量细节交给模型

人类处理复杂项目资料时,基本也是这么干的。先找位置,再看内容。OpenViking 只是把这个过程做成了系统能力。

三、架构设计:OpenViking 到底由哪些部分组成

从官网描述、README 和仓库结构看,OpenViking 的架构可以拆成四层。

3.1 命名空间层:viking://

第一层是统一寻址。官方示例中,所有上下文都放在 viking:// 协议下,大致长这样:

viking://
├── resources/
│   └── my_project/
├── user/
│   └── memories/
└── agent/
    ├── skills/
    ├── memories/
    └── instructions/

这层设计看起来简单,但非常关键。

它至少解决了三个工程问题:

  • 资源、记忆、技能不再散落在不同后端,地址统一了。
  • 目录关系能保留下来,不会一上来就被切成无父无子的碎片。
  • 后面的递归检索终于有了真正可以走的路径。

3.2 表示层:L0 / L1 / L2

这是 OpenViking 公开资料里出现最频繁的一组概念:

  • L0:一句摘要,官方给出的量级大约是 100 tokens。
  • L1:结构化概览,量级大约是 2k tokens。
  • L2:完整内容,只有真的需要时才加载。

这不是普通的"摘要 + 原文"两层结构,而更像一个分辨率切换机制。

  • 系统刚开始判断相关性时,看 L0 就够了。
  • 进入规划或决策阶段时,再看 L1。
  • 需要实现细节、代码或完整上下文时,才读 L2。

真正有意思的是,README 里给出的例子不只给文件生成 .abstract.overview,目录本身也会有自己的摘要和概览。这样一来,目录不是静态容器,而成了可检索节点。

3.3 检索层:递归式检索引擎

OpenViking 最像它自己的地方,就在这儿。

它不是一上来就在全局语料里捞 TopK,而是先找高分目录,再进目录里继续找。如果还有更细的子目录,就继续往下走。

这条链路可以概括成下面几步:

  1. 分析查询意图,扩展检索条件。
  2. 在 L0 / L1 层定位高相关目录。
  3. 进入目录后继续检索子目录和文件。
  4. 在更小的候选集合里继续筛选。
  5. 只在最后阶段读取 L2 详情。

对应的思路,可以用一段很短的伪代码说明:

def recursive_retrieve(query, node):
    intents = expand_query_intent(query)
    candidates = semantic_search(node.children_summaries(), intents)

    hits = []
    for child in top_k(candidates):
        if child.is_directory:
            hits.extend(recursive_retrieve(query, child))
        else:
            hits.append(child)

    return rerank_and_load_details_on_demand(hits)

这不是源码,只是把 README 里的公开思路压成了一个更好懂的形状。重点不在代码,而在背后的策略:先收缩搜索空间,再读细节。

3.4 会话与记忆层

很多检索系统擅长"查",但不太擅长"用完以后怎么办"。Agent 系统不一样,它必须处理回写问题:

  • 用户偏好要不要沉淀成长期记忆
  • 新生成的文档要不要进入项目资源
  • 这次任务学到的执行经验要不要被 Agent 复用

OpenViking 的公开说明里提到自动会话管理、记忆压缩和长期记忆抽取。换句话说,它不只是 retrieval store,还试图负责记忆的回流。

3.5 工程形态:不是单语言项目

从仓库结构能看到,OpenViking 并不是把所有东西都塞在一个 Python 包里:

  • openviking:核心 Python 包
  • crates/ov_cliopenviking_cli:Rust CLI
  • AGFS:README 提到需要 Go 1.22+ 构建
  • src 和 native extension:承接性能敏感部分
  • bot:面向 Agent / Bot 的接入层

这套拆法挺务实。Python 负责生态集成和开发效率,Rust 适合做 CLI,Go 负责部分系统能力,底层扩展则用来处理性能热点。

四、工作流程:一次查询在 OpenViking 里会怎么走

下面这张图可以先把检索路径看清楚:

OpenViking 递归检索流程图

如果按一次真实查询来拆,流程大致是这样的。

4.1 资源接入

第一步通常是把外部资料收进 viking://resources/。官方 CLI 示例里,直接把一个 GitHub 仓库加进来就行:

openviking-server
ov add-resource https://github.com/volcengine/OpenViking --wait
ov tree viking://resources/volcengine -L 2

这一步背后通常会发生几件事:

  • 拉取或解析外部资源
  • 把内容映射为目录和文件
  • 为目录和文件生成 L0 / L1 表示
  • 为后续检索建立向量表示

4.2 查询进入系统

当 Agent 抛出一个问题,比如"这个项目的上下文加载机制怎么做",OpenViking 不会立刻把一大段正文推给模型。它先会在摘要层做粗定位,先判断:

  • 相关信息更可能在 resources/user/ 还是 agent/
  • 哪个目录最值得先进去看
  • 哪些目录可以直接排除

4.3 目录内继续细化

一旦命中了某个目录,检索空间就从全局语料缩到局部子树。接下来的步骤不是"重复全局搜索",而是在目录内部继续看:

  • 子目录摘要
  • 文件摘要
  • 局部候选的相关性

如果信息还不够,就继续往下钻。

4.4 按需读取 L2

只有当系统判断"这份文件真的值得读"时,才会把完整内容拿出来。这个延迟加载很重要,因为它直接决定了 token 成本。

传统 RAG 的典型问题是过早读太多内容。OpenViking 的做法刚好相反:能不碰 L2 就先别碰,直到最后一刻再说。

4.5 任务结果回写

任务结束后,链路还没完。系统还得决定哪些东西应该留下:

  • 稳定的用户偏好写入 user/memories
  • Agent 的经验写入 agent/memories
  • 新产出的文档、代码或说明写回 resources/

这也是 OpenViking 和很多"只负责查资料"的系统不太一样的地方。它更像一个持续演化的工作空间。

五、技术实现细节:它为什么能省 token,还能把完成率拉上去

5.1 省 token,不只是因为有摘要

OpenViking 的公开结果里,最容易被注意到的是成本下降。但如果把原因简单归结成"它有摘要,所以更省",其实说浅了。

更准确一点,它是在四个地方一起省:

1)目录摘要先挡掉无关区域

不是每次都要在全局语料里做一轮大规模召回。很多无关内容在目录层就被刷掉了,根本轮不到 L2 出场。

2)L0 / L1 让平均读取粒度变小

很多问题到 L1 就已经足够判断下一步,不需要完整原文。系统平均读取的文本量自然会降下来。

3)递归下钻会一路缩小候选集合

每深入一层,候选空间都更小。后面的 rerank 更容易做准,误召回也更少。

4)记忆回写减少重复检索

一些任务结果一旦沉淀成用户记忆或 Agent 记忆,后面就不用每次都从原始资源里重新翻一遍。

5.2 公开 benchmark 说明了什么

README 里给出了一组基于 LoCoMo10 和 OpenClaw Context Plugin 的公开结果。先看图:

OpenViking 公开 benchmark 对比图

对应数据如下:

实验组 任务完成率 输入 Token 总成本
OpenClaw(memory-core) 35.65% 24,611,530
OpenClaw + LanceDB(关闭原生 memory) 44.55% 51,574,530
OpenClaw + OpenViking Plugin(关闭原生 memory) 52.08% 4,264,396
OpenClaw + OpenViking Plugin(启用原生 memory) 51.23% 2,099,622

如果只看结论:

  • 相比原始 OpenClaw,任务完成率最高提升 43%。
  • 输入 token 成本最高下降 91%。
  • 相比接了 LanceDB 的对照组,完成率更高,成本却低得多。

这组数字背后的意思很明确:OpenViking 不是靠"多喂一点上下文"把效果硬堆上去,反而是在更少输入的情况下,把任务链路做得更稳。

5.3 为什么完成率会更高

我觉得最关键的原因,不是它比别家"更会做向量检索",而是它更擅长处理结构。

传统向量检索拿到的是"语义接近的片段"。这当然有用,但在长链路 Agent 任务里,真正关键的往往是:先找到对的位置。

位置一旦错了,后面再精细的 rerank 都是在错误范围内优化。OpenViking 的目录递归检索,本质上是在先解决"搜索空间对不对",再解决"片段像不像"。

5.4 可观测性不只是调试功能

公开资料里还提到可视化检索轨迹。这个点我挺认同,因为它不是锦上添花,而是工程上必须有的一层。

复杂 Agent 系统最难调的,经常不是模型输出,而是中间过程:

  • 是目录设计有问题
  • 还是摘要写得太空
  • 是递归深度不够
  • 还是某个 provider 的 embedding 本身就不适合当前数据

没有轨迹时,优化基本靠猜。有了轨迹,你至少能知道链路是在哪一步开始偏的。

六、如果放到生产环境,哪些地方最值得调

OpenViking 的设计思路很清楚,但落地效果仍然高度依赖你怎么组织内容。我更建议先调这几件事。

6.1 先调目录,不要上来就卷模型

因为它的检索链路本来就是"先目录、后细节",所以目录树本身就是性能的一部分。

比较稳妥的做法是:

  • resources/ 按项目、域、模块分层
  • 避免一个目录塞太多异构资料
  • 目录命名尽量直接反映语义
  • 会一起被使用的内容尽量放在同一子树

目录结构乱,再好的 embedding 也只能补一部分。

6.2 摘要质量会直接影响上层命中率

L0 / L1 本质上是路标。路标写得空,系统前两步就容易跑偏。

我会更看重三点:

  • 有没有区分度
  • 有没有任务导向
  • 有没有清楚边界

换句话说,摘要不是写给人"欣赏"的,而是写给后续检索流程用的。

6.3 长期记忆和项目资源最好分开治理

user/agent/resources/ 这三个空间,最好别混着用。

原因很简单:

  • 检索时边界清楚
  • 治理时责任清楚
  • 更新频率可以不同

长期偏好、策略说明、项目事实,本来就不该在同一个桶里维护。

6.4 递归深度要按任务类型定

递归不是越深越好。下钻太深,同样会引入额外延迟和无效探索。

一般来说:

  • 简单问答用浅递归就够了
  • 文档库、代码库分析可以适当加深
  • 长任务场景更适合动态决定要不要继续下钻

README 里也提到了 embedding / VLM 的并发配置。真到高吞吐场景,除了递归深度,还要一起看:

  • embedding 批量化
  • provider 选择
  • 并发上限
  • 缓存命中

6.5 把它放在正确的位置上

OpenViking 很适合作为 Agent 系统里的上下文层,但它不等于完整的 Agent Runtime。

比较合理的分工通常是:

  • 上层 Agent 框架负责任务规划和工具调用
  • OpenViking 负责上下文组织、检索和记忆沉淀
  • 模型服务负责生成
  • 外部工具负责真正执行动作

把它当 Context Layer 来用,通常会比试图让它包办一切更顺。

七、基于 OpenViking 搭一套完整的多模态 RAG 方案

前面六节都是在讲 OpenViking 本身。但如果你真想把它用到生产环境里做知识库问答,光有上下文数据库是不够的——上游还差一段:多模态文件预处理

现实中的知识库,很少只有干净的 Markdown 或纯文本。更常见的情况是一堆 PDF(扫描件、电子版混着来)、Word/PPT、截图、手写白板照片,甚至还有音视频会议录音。这些东西如果不先做结构化处理,直接喂给 OpenViking,目录树和 L0/L1 摘要的质量都没法保证。

下面这张图是我整理的一条完整链路,从文件进来到回答出去,四个阶段:

基于 OpenViking 的多模态 RAG 全链路

7.1 Stage 1:多模态文件预处理——把乱七八糟的文件变成干净的 Markdown

这一步的目标很明确:不管进来的是什么格式,出去的都是结构化的 Markdown + 元数据 JSON。

不同文件类型要走不同的处理路径

文件类型 处理策略 推荐工具
电子版 PDF(原生文本) 直接提取文本和表格,保留标题层级 PyMuPDF、pdfplumber
扫描版 PDF / 影印件 先 OCR 再做版面分析 MinerU、olmOCR
图片(PNG/JPG/TIFF) OCR + 图像描述(VLM) MinerU、olmOCR、Qwen-VL
Word / PPT / Excel 解析富文本,提取表格和嵌入图片 python-docx、python-pptx、openpyxl
代码仓库 按目录结构保留,可附加 AST 摘要 tree-sitter、OpenViking 原生支持
音视频 ASR 转文字后按时间戳分段 Whisper、FunASR

OCR 不只是"把图变成字"

很多人一提 OCR 就想到 Tesseract,但现在做 RAG 预处理,OCR 只是第一步,后面至少还有三件事要做:

  1. 版面分析(Layout Analysis):识别页面中的标题、正文、表格、图注、页眉页脚,不然 OCR 出来的文本是一团浆糊。
  2. 表格重建(Table Reconstruction):把 OCR 识别到的单元格重新拼成结构化表格,输出 HTML 或 Markdown table。
  3. 图像描述(Image Captioning):对文档中嵌入的图表、流程图、截图,用 VLM(如 Qwen-VL、InternVL)生成文字描述,让后续检索也能覆盖到视觉信息。

如果你的 PDF 里有跨页表格,推荐试一下 MinerU,它在复杂版面下的表现比较稳。

一段典型的预处理伪代码

from pathlib import Path
from preprocessor import detect_file_type, extract_pdf_native, ocr_and_layout, parse_office, describe_images

def preprocess(input_dir: Path, output_dir: Path):
    for file in input_dir.rglob("*"):
        ftype = detect_file_type(file)

        if ftype == "pdf_native":
            md, meta = extract_pdf_native(file)
        elif ftype in ("pdf_scanned", "image"):
            md, meta = ocr_and_layout(file)        # OCR + 版面分析 + 表格重建
        elif ftype in ("docx", "pptx", "xlsx"):
            md, meta = parse_office(file)
        else:
            md, meta = file.read_text(), {}

        # 对文档中的图片单独跑一遍 VLM 描述
        md = describe_images(md, meta.get("images", []))

        # 输出标准化 Markdown,保留原始目录层级
        out_path = output_dir / file.relative_to(input_dir).with_suffix(".md")
        out_path.parent.mkdir(parents=True, exist_ok=True)
        out_path.write_text(md)

这段代码的重点不是具体实现,而是思路:按文件类型分流 → OCR + 版面分析 → 图像描述 → 统一输出 Markdown,同时保留目录层级。 保留目录层级这一点非常重要,因为后面 OpenViking 要靠它来建 viking:// 命名空间。

7.2 Stage 2:入库——让 OpenViking 把 Markdown 变成可检索的上下文

预处理完成后,就可以把输出目录整个推给 OpenViking:

# 启动 OpenViking 服务
openviking-server

# 把预处理后的 Markdown 目录加进来
ov add-resource ./preprocessed_docs --wait

# 检查目录树是否符合预期
ov tree viking://resources/preprocessed_docs -L 3

这一步 OpenViking 会自动做几件事:

  1. 把 Markdown 文件映射到 viking://resources/ 下的目录树。
  2. 为每个目录和文件生成 L0(~100 tokens 摘要)和 L1(~2k tokens 概览)。
  3. 对 L0/L1/L2 分别做 embedding,建立多粒度向量索引。

这里有个容易踩的坑:如果预处理阶段输出的目录结构是扁平的(所有文件丢在同一个文件夹),OpenViking 的目录递归检索就废了一半。 所以上一步保留原始目录层级不是可选项,而是必须的。

7.3 Stage 3:递归检索——先找对地方,再读对内容

这一层就是前面第四节讲的 OpenViking 的核心能力。在完整 RAG 方案里,它的角色是:

  • 接收用户 query
  • 在 L0 层快速排除无关子树
  • 在 L1 层逐层下钻找到目标目录和文件
  • 只在最后一步加载 L2 全文

相比传统 RAG 方案里"全局 embedding → TopK → 拼 prompt"的路径,这条路的好处前面已经说过了:搜索空间收缩更快,误召回更少,token 成本更低。

但在多模态场景下有一个额外的收益:因为预处理阶段已经把图像描述也写进了 Markdown,所以图表、流程图里的信息也能被检索到,不再是黑洞。

7.4 Stage 4:生成与记忆回写——闭环才是关键

检索结果组装成 prompt 交给 LLM 生成回答,这一步和大多数 RAG 系统没什么区别。真正值得注意的是回写:

  • 如果用户在问答中表达了偏好(“我只关心 Python 实现”),这个信息可以写入 viking://user/memories/
  • 如果 Agent 在执行过程中总结出了某个模块的经验(“这个项目的配置文件在 config/ 下”),可以写入 viking://agent/memories/
  • 如果问答过程中产生了新的文档或代码,可以写回 viking://resources/

这个回写机制让整套 RAG 方案不再是一次性的"查了就完",而是每次交互都在让知识库变得更好。

7.5 落地时的几个实操建议

1)预处理管线要做好兜底

OCR 不是万能的。扫描质量差、手写体、水印遮挡都可能导致识别率下降。实际部署时建议:

  • 对 OCR 置信度低于阈值的页面标记为"待人工复核"
  • 对同一文件跑多个 OCR 引擎,取结果交集或做投票
  • 图像描述走 VLM 时,控制 prompt 长度,避免描述过于冗长

2)目录设计比选模型更重要

这一点怎么强调都不过分。不管你的 embedding 模型多好,如果预处理输出的目录结构不合理,OpenViking 的目录递归检索就会跑偏。

几个原则:

  • 同一主题的文档放在同一目录下
  • 目录深度控制在 3-5 层
  • 目录名用人类能理解的语义,不要用纯 ID
  • 大文件拆成多个子文件,每个子文件一个清晰主题

3)预处理和入库最好做成 pipeline

手动跑一次可以,但生产环境需要自动化。一个实用的做法是:

# 监听文件变更 → 预处理 → 入库
watch_dir ./raw_docs | preprocess --output ./preprocessed | ov add-resource --incremental

增量更新是关键。每次有新文件进来,不需要全量重建,只处理变化的部分。

4)评估不能只看回答质量

完整的评估应该覆盖四个维度:

维度 指标 说明
预处理质量 OCR 准确率、表格还原率 垃圾进垃圾出,前面不行后面全白搭
检索质量 召回率、目录命中率 OpenViking 的目录定位是否准确
生成质量 答案准确率、引用命中率 LLM 输出是否可靠
成本效率 平均 Token 消耗、端到端延迟 能不能用得起

八、它的边界也要看清楚

OpenViking 不是所有场景都合适。

8.1 对扁平问答场景,优势没那么明显

如果你的数据本来就没有清晰层级,任务也只是小规模 FAQ 或单轮问答,那它的目录抽象未必能带来明显收益,反而可能增加建模成本。

8.2 它很依赖内容组织质量

OpenViking 的优势建立在"结构存在且结构靠谱"这个前提上。所以:

  • 目录设计差,效果会掉
  • 摘要质量差,效果会掉
  • 命名混乱,效果也会掉

这更像一套上下文工程方法,而不是装上就自动最优的黑盒。

8.3 公开 benchmark 仍然要自己复验

README 里的结果很有参考价值,但毕竟是在特定数据集和插件场景下得到的。真正上线前,还是要拿自己的 workload 去看:

  • 语料规模和层级深度
  • 模型和 embedding 组合
  • 是问答任务,还是规划和执行任务
  • 多租户与并发压力下的表现

九、总结

如果把 OpenViking 只看成"另一个向量数据库",其实会看偏。

它更像是在做一件朴素但很重要的事:把 Agent 的上下文重新组织起来,让系统别再一上来就面对一大池碎片,而是先找到地方,再读内容,再把结果写回去。

它真正有价值的地方,我觉得有四个:

  • viking:// 把资源、记忆和技能放进统一命名空间
  • 用 L0 / L1 / L2 控制上下文粒度
  • 用递归检索缩小搜索空间
  • 用记忆回写把一次任务变成后续任务的上下文资产

而如果你想在此基础上搭一套真正能用的多模态 RAG 方案,前面还要加一段预处理管线:用 OCR、版面分析、表格重建和图像描述把异构文件变成干净的 Markdown,再保留目录层级喂给 OpenViking。整条链路走通以后,你得到的不是一个一次性的问答系统,而是一个会随着使用不断变好的知识工作空间。

这条路线能不能成为 Agent 基础设施的主流,现在还不好说。但至少在今天,它提供了一个很值得认真验证的方向:上下文系统不一定非得从 chunk 开始,也可以从结构开始。

参考资料

「真诚赞赏,手留余香」

爱折腾的工程师

真诚赞赏,手留余香

使用微信扫描二维码完成支付