Embedding 不是随便选个模型就行:RAG 向量化模块的工程化设计

6 阅读11分钟

多模型支持、模型推荐、批量处理、智能重试、结果持久化、历史记录与进度反馈。我想解决的不是“调一次 Embedding API”,而是“怎么把分块结果稳定地变成可复用的向量资产”。

前两篇我分别写了文档加载和文档分块。
如果说文档加载解决的是“内容从哪里来”,分块解决的是“内容应该怎么切”,那这一篇要聊的,就是第三个关键问题:

这些块,应该怎么被稳定地转换成向量表示。

很多人聊 RAG 的时候,会把向量化这一步说得很轻:
选个 Embedding 模型,调一下接口,把返回的向量塞进库里,就结束了。

但如果你真的把它放进工程项目里,很快就会发现,向量化其实远远不只是“调一个模型”:

  • 模型到底怎么选
  • 中文、英文、长文本、多模态是不是都适合同一个模型
  • 批量处理失败了怎么办
  • API 限流和超时怎么办
  • 结果怎么落盘复用
  • 进度怎么反馈给用户
  • 怎么避免每次都盲选模型、重复做一样的计算

所以在 RAG Pipeline Hub 里,我把向量化模块做成了一个独立工作台。
这篇文章,我就聊聊这个模块背后的设计思路。

项目地址:

  • GitHub: https://github.com/qingni/rag-pipeline-hub

为什么说向量化不只是调用 Embedding API

从技术定义上看,Embedding 确实就是把文本转换成高维向量。

但在真实 RAG 项目里,这一步承担的责任其实更重:

  • 它决定了文本在向量空间里的语义表达方式
  • 它直接影响后续索引维度、存储成本和搜索效果
  • 它会决定检索是更偏速度,还是更偏语义精度
  • 它会把前面的分块结果变成后面所有模块都依赖的数据资产

所以它不是一个“纯调用层”,而是一个承上启下的基础模块。

我现在越来越认同一个判断:

RAG 里很多“检索效果不好”的问题,根源并不一定在检索逻辑,也可能在向量表示质量。

如果 chunk 切得不错,但嵌入表达不合适,那后面的索引、召回、排序,其实也只是对不够理想的表示做补救。

真实项目里,向量化会遇到哪些问题

在这个模块里,我觉得至少有 5 类问题是绕不过去的。

1. 模型选择本身就不简单

不是所有文档都适合同一个 Embedding 模型。

比如:

  • 中文知识库和英文知识库,侧重点可能不同
  • 普通文本和术语密集文档,需求可能不同
  • 纯文本和图文混合文档,更不该用同一套处理思路
  • 对精度要求高和对成本敏感的场景,也不该一视同仁

如果把模型选择交给用户盲选,通常会很难用。

2. 批量处理很容易出问题

一两个 chunk 做向量化不难,但一份文档、一个分块结果、一个批次的块列表,情况就完全不同了。

你会开始碰到:

  • 限流
  • 超时
  • 部分成功
  • 某几条文本异常
  • 网络抖动

如果没有一套明确的重试和失败处理机制,这一步很容易变成不稳定点。

3. 向量结果不能只存在内存里

如果向量化结束后直接把结果传给索引模块、不做持久化,那你会失去很多关键能力:

  • 无法回看某次向量化到底用了什么模型
  • 无法比较不同模型的结果
  • 无法重复利用已有结果
  • 无法做后续索引推荐和管理

4. 用户需要知道它现在执行到了哪一步

向量化不是一个用户愿意“盲等”的动作。
尤其在批量处理时,如果没有进度、状态和统计,用户体验会很差。

5. 模型接入方式需要可扩展

如果一开始把模型能力写死在某个服务里,后面每加一个模型,整个系统都会越来越难维护。

所以对我来说,向量化模块的核心问题从来不只是“怎么拿到向量”,而是:

怎么把模型选择、批量处理、失败恢复、结果复用和可观察性整合成一个稳定的工程模块。

我给这个模块定的目标

围绕上面这些问题,我给向量化模块定了几个明确目标:

  1. 支持多种 Embedding 模型
  2. 支持直接从分块结果进行向量化
  3. 支持批量处理和部分成功
  4. 支持限流、超时、网络错误的智能重试
  5. 支持结果持久化与历史查看
  6. 支持模型推荐,而不是完全靠用户盲选
  7. 支持后续索引模块直接复用结果

如果只用一句话概括,就是:

把“向量化”做成一个可复用、可观测、可扩展的向量资产生产模块。

当前模块在整条链路里的位置

这个模块位于文档分块之后、向量索引之前。

它处理的对象不再是原始文档,而是已经被切好的块级内容。

从流程上看,可以简单理解成:

选择分块结果
  -> 选择模型
  -> 配置批量处理参数
  -> 执行向量化
  -> 保存结果到数据库与 JSON
  -> 供索引模块继续消费

也就是说,向量化模块的输出并不是“某次调用返回了一堆数组”,而是一份完整的向量化结果记录。

这份记录至少应该包含:

  • 使用的模型
  • 向量维度
  • 成功数 / 失败数
  • 向量结果内容
  • 批量执行统计
  • 对应的分块任务来源

这也是为什么我更愿意把它叫做“向量资产”而不是“向量结果”。

当前支持了哪些模型

这个项目里目前支持的核心模型包括:

  • bge-m3
  • qwen3-embedding-8b
  • qwen3-vl-embedding-8b

它们不是简单的“几个选项”,而是各自适合不同场景。

bge-m3

它更像一个稳健、通用、偏多语言的基础型选择。

适合:

  • 多语言文档
  • 通用检索场景
  • 希望维度和成本比较平衡

qwen3-embedding-8b

它更偏高精度、高维度、长文本支持强。

适合:

  • 中文场景
  • 语义要求更高的检索
  • 更看重表达能力而不是极致成本

qwen3-vl-embedding-8b

这是我觉得很有意思的一类能力,因为它把向量化从“纯文本模块”扩展到了多模态。

适合:

  • 图文混合文档
  • 图片较多的知识库
  • 想为后续多模态检索留空间的场景

所以在这个项目里,我不想让模型选择变成“拍脑袋看名字”,而是尽量把不同模型的能力边界和适用场景明确下来。

为什么我要做模型推荐

如果系统里只有一个 Embedding 模型,那推荐当然没必要。

但一旦你支持多模型,问题就来了:

用户怎么知道该选哪个?

很多系统在这个地方会把责任完全推给用户。
表面看是“灵活”,实际上经常意味着:

  • 用户不知道差异
  • 用户不知道成本
  • 用户不知道维度变化会影响什么
  • 用户只会反复选自己最熟悉的那个

所以我在这个项目里加入了模型推荐能力。

推荐逻辑会考虑几类信号:

  • 文档语言
  • 文档领域特征
  • 是否存在多模态内容
  • 对精度和成本的权衡

最终给出的是:

  • 推荐模型
  • 推荐理由
  • 可能的替代选项

这件事的价值不是“更智能”这么简单,而是:

让模型选择从经验主义,变成有依据的默认决策。

尤其是对第一次上手项目的人来说,这能显著降低试错门槛。

为什么批量处理、重试和部分成功是必须的

如果你只向量化一段文本,大多数接口看起来都很稳。

但在真实场景里,你经常面对的是:

  • 一个文档对应几十个 chunk
  • 一个任务里上百个 chunk
  • 一次要处理很多历史结果

这时候问题就会快速出现:

  • 某一批请求超时
  • 某几条文本不合法
  • 某次调用被限流
  • 网络临时抖动

所以这个项目在向量化模块里专门做了几件事:

1. 批量处理

把大量 chunk 按批次送进模型,而不是一条条串行处理。

2. 智能重试

对限流、超时、网络错误采用指数退避重试,而不是立刻整批失败。

3. 部分成功

某些条目失败,不代表整个任务失败。
我希望系统能保留成功部分,并把失败项明确标出来,方便后续重试。

我很看重这一点,因为很多工程系统真正难做的,不是 happy path,而是:

当外部服务不稳定时,系统还能不能优雅地继续工作。

为什么结果一定要持久化

向量化还有一个经常被简化的问题:
很多项目做完 embedding 之后,直接进入索引,不保存中间结果。

这样虽然流程短,但长期来看会带来很多问题:

  • 没办法复用已有向量化结果
  • 没办法对比不同模型效果
  • 没办法查看某次任务的具体统计
  • 没办法给索引模块做清晰的输入管理

所以这个项目会把向量化结果同时保存到:

  • 数据库
  • JSON 文件

这么做的意义很大:

  1. 后续索引模块可以直接选择已有向量化结果
  2. 用户可以查看历史任务
  3. 可以对比模型之间的差异
  4. 可以为失败重试和结果审计提供依据

也就是说,向量化模块的输出不是“临时数据”,而是系统里的长期资产。

为什么进度反馈和历史记录不能省

我一直觉得,工程化系统和 demo 的差别,很多时候不在核心算法,而在这些“看起来不那么炫”的能力上。

比如向量化模块,如果没有:

  • 执行面板
  • 成功 / 失败统计
  • 向量维度展示
  • 历史列表
  • 结果详情

用户会非常难用。

因为他根本不知道:

  • 当前到底在执行什么
  • 这次用了什么模型
  • 成功了多少、失败了多少
  • 结果是否可以继续拿去建索引
  • 之前有没有做过类似任务

所以我在前端里单独做了:

  • 文档选择器
  • 模型选择器
  • 参数配置面板
  • 执行面板
  • 结果展示
  • 历史记录列表

这件事本质上不是“把 UI 做完整”,而是:

让向量化模块从一个后台调用,变成一个用户可理解、可回看、可管理的流程。

这一层和普通 Embedding demo 最大的区别是什么

如果只看最小实现,Embedding 模块可以非常简单:

输入文本 -> 调 API -> 返回向量

但在这个项目里,我更看重的是下面这些差异:

  • 不只是支持一个模型,而是支持多模型和多模态
  • 不只是人工选择,而是支持模型推荐
  • 不只是单条调用,而是支持批量处理
  • 不只是失败就报错,而是支持重试和部分成功
  • 不只是内存返回,而是支持结果持久化
  • 不只是一次执行,而是支持历史查看和复用

这也是我对“工程化向量化模块”的理解:

不是把模型接上,而是把向量资产的生产、管理和复用流程搭起来。

我对这个模块的一个核心判断

如果只让我总结一句话,我会说:

Embedding 不是把文本交给模型这么简单,而是在决定一套知识库最终会以什么语义形态进入检索系统。

这一步做得好,后面的索引和检索才有高质量基础。
这一步做不好,后面的优化就会越来越像“带着偏差继续优化偏差”。

所以在 RAG Pipeline Hub 里,我才会愿意把向量化单独做成一整套模块,而不是一个工具函数。

下一篇写什么

向量化解决的是“怎么把 chunk 变成向量”。
下一步的关键问题就是:

这些向量,应该怎么组织、存储、索引,才能真正支撑后续高效检索?

所以下一篇我会继续写:

《向量写进库里只是开始:RAG 向量索引与 Collection 管理实践》

项目地址:

  • GitHub: https://github.com/qingni/rag-pipeline-hub

如果这篇文章对你有帮助,欢迎:

  • 点个 star
  • 提个 issue
  • 留言说说你最常用的 Embedding 模型

如果你也做过多模型选型、批量向量化、缓存复用或者多模态 Embedding,欢迎交流你踩过的坑。
我越来越觉得,很多系统在索引和检索之前,其实就已经在“向量表示”这一步拉开差距了。