Skip to content

OpenViking 本地 Embedding Llama-cpp 设计文档

Date: 2026-04-11 Status: 已批准进入实现

目标

为 OpenViking 增加内置的本地 dense embedding 能力,并满足以下产品行为:

  • 当用户没有显式配置 embedding 时,OpenViking 默认使用本地 embedding backend
  • 默认本地模型为 bge-small-zh-v1.5-f16
  • 本地推理基于 llama-cpp-python 加载 GGUF 模型
  • 本地推理依赖不放入主依赖,而是通过 optional extra 单独分发,降低安装风险

最终效果是:在不改变“默认走本地 embedding”这一产品目标的前提下,让方案具备可实现性和可维护性。

范围

本次设计包含:

  • 新增 embedding.backend = "local" backend
  • 当用户未提供 embedding 配置时,自动生成隐式本地 embedding 配置
  • 基于 llama-cpp-python 的 dense embedder
  • 默认模型 bge-small-zh-v1.5-f16
  • 模型路径解析、下载和缓存目录管理
  • query/document 双路编码语义
  • collection 元数据校验和 rebuild 规则
  • 启动期校验与错误提示
  • 测试方案和 benchmark 预留

本次设计不包含:

  • 本地 sparse embedding
  • 本地 hybrid embedding
  • 本地失败后静默回退到远程 provider
  • 运行时自动安装依赖
  • 替换现有远程 provider

决策摘要

OpenViking 采用以下组合策略:

  1. 产品默认行为:如果用户没有配置 embedding,OpenViking 会隐式选择本地 embedding backend。
  2. 依赖分发策略:llama-cpp-python 不进入主依赖,而是通过 openviking[local-embed] 之类的 optional extra 分发。
  3. 默认本地模型:bge-small-zh-v1.5-f16
  4. 失败策略:如果系统默认选择了本地 embedding,但本地依赖或模型不可用,则直接报错,并给出清晰恢复指引;不会静默回退到远程模型。

这和 QMD 的做法不完全相同。QMD 是 Node CLI 产品,可以把 node-llama-cpp 作为主依赖;而 OpenViking 是 Python SDK 和服务组件,如果让原生依赖阻断主包安装,代价会更高。

为什么这样设计

调研结论和当前代码约束基本指向同一个方向:

  • QMD 证明了“默认本地 embedding”这个产品方向是成立的。
  • OpenClaw / ArkClaw 证明了基于 GGUF 的本地 memory search 有明确用户价值。
  • OpenViking 当前架构决定了 embedding 初始化失败会在启动期暴露,而不是延后到查询时。
  • 在 Python 生态里,原生依赖失败的成本通常比 QMD 所在的 npm/Node 生态更高。

因此,这个设计是在保留产品目标的前提下,尽量缩小原生依赖失败的影响范围。

用户可见行为

默认行为

如果配置中没有 embedding

  • OpenViking 自动生成一份隐式 local dense embedding 配置
  • backend 设置为 local
  • model 设置为 bge-small-zh-v1.5-f16
  • dimension 设置为该模型对应维度

用户应感知到的行为是:“本地 embedding 是默认值”。

显式行为

如果用户显式配置了 embedding,则始终以显式配置为准,包括:

  • 显式 backend: "local"
  • 显式远程 backend,例如 openaivolcenginevikingdb
  • 显式 model_path
  • 显式 cache_dir

不应存在覆盖用户配置的隐式重写。

安装体验

基础安装:

bash
pip install openviking

启用本地 embedding:

bash
pip install "openviking[local-embed]"

如果用户依赖默认本地行为,但没有安装 local extra,系统必须在启动时给出可执行的错误提示,至少包含:

  • 当前默认启用了本地 embedding
  • 缺少 llama-cpp-python
  • 启用本地 embedding 的安装命令
  • 如果用户想改成远程 provider,应如何显式配置

配置设计

EmbeddingModelConfig 中新增 local 作为合法 backend。

本地 dense backend 支持的字段:

  • backend: "local"
  • model: 逻辑模型名,默认 bge-small-zh-v1.5-f16
  • model_path: 可选,显式指定 GGUF 文件路径
  • cache_dir: 可选,缓存根目录,默认 ~/.cache/openviking/models/
  • dimension: 可选,但通常应由内置模型注册表推导
  • batch_size: 预留给后续批量 embedding

建议配置示例:

json
{
  "embedding": {
    "dense": {
      "backend": "local",
      "model": "bge-small-zh-v1.5-f16",
      "cache_dir": "~/.cache/openviking/models"
    }
  }
}

显式模型路径示例:

json
{
  "embedding": {
    "dense": {
      "backend": "local",
      "model": "bge-small-zh-v1.5-f16",
      "model_path": "/data/models/bge-small-zh-v1.5-f16.gguf"
    }
  }
}

架构设计

新增组件

新增一个本地 dense embedder 实现,例如:

  • openviking/models/embedder/local_embedders.py
  • LocalDenseEmbedder

其职责包括:

  • 校验 llama-cpp-python 是否可用
  • 将逻辑模型名解析为 GGUF 模型规格
  • 解析或下载模型文件
  • 初始化 llama embedding context
  • 提供 query/document 双路 embedding 方法
  • 返回模型维度
  • close() 时释放本地资源

Factory 与配置改造

需要修改:

  • EmbeddingModelConfig.validate_config() 以接受 backend == "local"
  • EmbeddingConfig._create_embedder() 以支持 ("local", "dense")
  • 默认配置生成逻辑,使“缺失 embedding 配置”自动变成 local dense

模型注册表

新增一个内置本地模型注册表。第一版可以先做成简单映射,按逻辑模型名索引:

  • 逻辑模型名
  • GGUF 下载 URL 或 HuggingFace 定位信息
  • 预期维度
  • 推荐 prompt 规则
  • 可选的目标文件名

首个内置模型为:

  • bge-small-zh-v1.5-f16

Query / Document 双路编码

这部分不是可选优化,而是本方案必须处理的设计点。

BGE/E5 一类模型是检索导向模型,通常需要区分:

  • query:用户输入的搜索词或问题
  • document:被存储和检索的文本块

OpenViking 当前只有 embed(text),这不足以表达这种语义差异。

设计上新增显式接口:

  • embed_query(text: str) -> EmbedResult
  • embed_document(text: str) -> EmbedResult

为了兼容现有代码,embed(text) 可以保留为一个薄封装,但内部必须带角色语义。新的检索代码应调用 embed_query(),新的入库代码应调用 embed_document()

query/document 的格式规则必须封装在本地 embedder 内部,而不是散落在业务层拼装。

模型解析与下载流程

解析顺序

  1. 如果配置了 model_path,直接使用该路径。
  2. 否则通过内置本地模型注册表解析 model
  3. 如果目标文件不存在,则下载到 cache_dir
  4. 用解析后的 GGUF 文件初始化 llama-cpp-python

缓存目录

默认目录:

  • ~/.cache/openviking/models/

行为要求:

  • 目录不存在时自动创建
  • 下载后的 GGUF 文件保存在这里
  • 如果目标文件已存在,则不重复下载

下载策略

第一版需要支持:

  • 可读性好的错误输出
  • 稳定可预测的文件命名
  • 失败后可手动重试

第一版暂不要求:

  • 断点续传
  • 多镜像源自动切换
  • 后台异步下载器

启动时机与失败行为

OpenViking 当前在 client 启动时就初始化 embedder,本地方案保持这一行为。

因此,下面这些问题都会在启动期直接暴露:

  • 没有安装 local extra
  • llama-cpp-python import 失败
  • 模型文件缺失且下载失败
  • GGUF 文件存在但加载失败
  • 当前 collection 元数据与配置模型不一致

错误处理规则

缺少本地依赖:

  • 直接抛出明确的配置/运行时错误
  • 错误信息中必须包含:缺失包名、安装命令、切换远程 provider 的方法

模型下载失败:

  • 抛出包含逻辑模型名、解析 URL、缓存目录和原始异常的错误

模型加载失败:

  • 抛出 GGUF 不兼容、文件损坏或当前运行环境不支持的错误

元数据不一致:

  • 抛出“当前 embedding 设置与已有索引不兼容,需要 rebuild”的错误

不允许静默回退:

  • 本地初始化失败时,不得悄悄切换到 openaivolcenginevikingdb

Collection 元数据与重建规则

当前系统只在写入时校验向量维度,这在本地模型成为默认值之后是不够的。

需要至少持久化以下元数据:

  • embedding_backend
  • embedding_model
  • embedding_dimension
  • embedding_model_identity

其中 embedding_model_identity 用于区分“看起来模型名相同,但实际模型文件不同”的情况,可以采用:

  • 解析后的模型路径
  • 模型路径哈希
  • 文件哈希(如果成本可接受)

重建触发条件

只要以下任一项发生变化:

  • backend
  • model
  • dimension
  • model identity

都应判定现有向量不可兼容。系统需要:

  • 在启动时直接报错并提示 rebuild,或
  • 在用户显式触发时执行 rebuild 流程

第一版建议采用显式 rebuild,而不是隐式迁移。

数据流改造

入库流程

当前流程:

  • 语义处理得到文本
  • 队列消费者调用 embed()

改造后流程:

  • 队列消费者调用 embed_document()
  • 本地 embedder 自动套用 document 侧规则
  • 向量写入时附带与当前模型一致的 collection 元数据

检索流程

当前流程:

  • retriever 调用 embed()

改造后流程:

  • retriever 调用 embed_query()
  • 本地 embedder 自动套用 query 侧规则
  • 检索时使用与 document 同体系生成的向量

批量 Embedding 策略

第一版可以先保证单条处理正确,但设计上必须明确批量优化路径。

阶段一:

  • LocalDenseEmbedder 中实现 embed_batch()
  • 队列层暂时仍允许继续按单条处理

阶段二:

  • 在队列侧做消息聚合,一次编码多条待 embedding 文本

如果没有批量能力,本地 CPU 索引构建吞吐大概率会明显低于模型 benchmark。

开发顺序

  1. 增加 local backend 的配置校验和 factory 注册。
  2. 增加“缺失 embedding 配置时默认生成 local dense 配置”的逻辑。
  3. 实现基于 llama-cpp-pythonLocalDenseEmbedder
  4. 增加内置本地模型注册表,并接入 bge-small-zh-v1.5-f16
  5. 增加模型路径解析、缓存目录和自动下载逻辑。
  6. 增加 embed_query() / embed_document() 双路接口。
  7. 持久化 collection embedding 元数据,并补一致性检查。
  8. 增加 rebuild-required 错误流。
  9. 增加 embed_batch() 和 benchmark 基础设施。
  10. 更新用户文档、示例配置和安装说明。

测试计划

配置测试

  • 缺失 embedding 时自动生成隐式 local dense 配置
  • 显式远程配置时不触发默认本地逻辑
  • model_path 能覆盖逻辑模型解析
  • cache_dir 覆盖生效

依赖与初始化测试

  • 缺少 llama-cpp-python 时,启动错误信息正确
  • 显式 local backend 且依赖已安装时可成功初始化
  • 非法 GGUF 路径会触发模型加载失败
  • 下载失败时错误信息完整可读

Embedding 行为测试

  • embed_query()embed_document() 走不同路径
  • 返回维度与模型维度一致
  • embed_batch() 的结果顺序和数量正确

元数据与重建测试

  • 首次启动时能生成和当前模型一致的元数据
  • 改变 model identity 时会触发需要重建
  • 改变 dimension 时会触发需要重建

检索回归测试

  • 中文 query 能正确召回中文文档
  • query/document 双路编码不会破坏现有检索链路
  • 现有远程 provider 行为保持不变

打包测试

  • pip install openviking 可以在不安装本地依赖的情况下成功
  • pip install "openviking[local-embed]" 可以启用本地 import 路径
  • 缺少 extra 且触发默认本地行为时,报错应明确,而不是模糊 import failure

基准测试

至少记录以下指标:

  • 依赖已安装且模型已缓存时的启动耗时
  • 首次下载模型时的启动耗时
  • 单条 embedding 延迟
  • 批量 embedding 延迟
  • 在代表性中文语料上的索引构建吞吐

在 benchmark 出来之前,不应假设“默认本地 embedding”在所有环境里都同样合适。

运维说明

推荐安装命令:

bash
pip install "openviking[local-embed]"

如果用户想使用远程 embedding:

  • 显式配置 embedding.dense.backend
  • 提供相应 provider 的凭证

风险

  • 原生依赖安装失败
  • 预编译 wheel 覆盖不足
  • GGUF 与运行时版本不兼容
  • 用户预期“零配置”但实际缺少 local extra
  • 模型切换后索引不兼容
  • 未做批量聚合时索引吞吐偏低

交付物

  • 本地 dense embedder 实现
  • local backend 配置与 factory 集成
  • 内置模型注册表
  • 启动期错误提示与安装指引
  • collection 元数据校验
  • rebuild-required 机制
  • 测试和 benchmark 脚手架
  • 用户文档更新

Released under the Apache-2.0 License.