知识库向量化成本怎么算:一次性建库不贵,全量重建才贵
做 RAG 的时候,向量化这笔钱几乎没人认真算。理由听起来也成立:文档就那么多,跑一遍进库,这是一次性投入,摊到后面几个月约等于没有。我自己一开始也这么想。
真正让我改口的是这样一个场景:某个团队的知识库是产品文档,编辑每天会改几篇。他们的更新流程写得非常”干净”——文档目录有任何变动,就触发一次流水线,把整个知识库重新切块、重新向量化、重新灌进向量库。跑一次大概二十几分钟,看起来很稳,也不用操心增量逻辑的边界条件。
问题就在这个”干净”上。一次性投入,被这个流程变成了每天一次的固定开销;而且知识库越大、编辑越勤快,这笔开销涨得越快。等到文档量翻了三倍、编辑从三个人变成八个人,账单曲线才被人注意到,这时候流水线已经跑了大半年。
所以这篇文章的重点不在”建库一次要花多少钱”——那个数其实很容易估,也确实不算贵。重点在你的更新策略会不会把它变成一项持续开销。
一、先把一次性建库的账算明白
建库成本的公式很短:
建库成本 = 送进 embedding 接口的总 token 数 × embedding 单价
麻烦的地方在于左边那个 token 数,很多人是按字数拍出来的。中文尤其容易拍错——一个汉字对应多少 token,不同分词器差别不小,而且标点、数字、代码片段、英文术语、表格里的分隔符各有各的算法。按”一个字一个 token”估出来的数,跟真实用量对不上是常态。
正确做法是拿你真的要用的那个模型的分词器,对你自己的文档抽样实测:从知识库里随机抽几十篇有代表性的(长文、短文、带表格的、带代码的都要有),跑一遍分词器,数出真实 token 数,除以字符数,得到属于你这批语料的换算系数。这个系数只对”你的语料 + 你的模型”有效,换语料或换模型都要重测。这件事的方法论我在中文 token 怎么算里展开写过,不重复。
拿到系数之后,剩下的就是算术。下面这组数字全部是假设参数,只用来演示计算过程,不代表任何厂商的真实定价:
| 项目 | 假设值 |
|---|---|
| 文档篇数 | 8,000 篇 |
| 平均每篇字符数 | 1,200 字 |
| 实测换算系数 | 1 字符 ≈ 0.9 token |
| 目标块长 | 500 token |
| 块间重叠 | 50 token |
| embedding 单价 | P 元 / 百万 token(变量) |
一步步算:
- 总字符数 = 8,000 × 1,200 = 9,600,000 字符
- 正文 token 总量 = 9,600,000 × 0.9 = 8,640,000 token
- 块间有效步进 = 500 − 50 = 450 token
- 块数 ≈ 8,640,000 ÷ 450 = 19,200 块
- 实际送进接口的 token = 19,200 × 500 = 9,600,000 token = 9.6 百万
- 建库成本 = 9.6 × P 元
注意倒数第二步:因为有重叠,真正付费的 token 比正文 token 多出 960,000 个(19,200 块中除首块外每块多带 50 token 的重复内容)。这部分是重叠窗口的直接代价,占正文量的 11.1%。很多人估成本时会漏掉它。
到这一步,结论其实是符合直觉的:一次建库就是九百多万 token 的量级,无论单价是多少,这都是一笔可以接受的一次性支出。建库本身不贵,这个判断是对的。 问题出在下一节。
二、真正贵的是”改一篇就全量重跑”
沿用上面的知识库,再加一个假设:每周有 3% 的文档发生变更,也就是 240 篇。
增量策略下,每周需要重新向量化的量:
- 变更文档正文 token = 240 × 1,200 × 0.9 = 259,200 token
- 计入重叠后 = 259,200 × (500 ÷ 450) = 288,000 token
- 一年 52 周 = 288,000 × 52 = 14,976,000 token ≈ 15 百万
全量重建策略下:
- 每周 9,600,000 token,一年 52 周 = 499,200,000 token ≈ 499 百万
倍数关系是 499.2 ÷ 14.976 = 33.3 倍。这个数其实不依赖单价,也不依赖库的大小——它就是 1 ÷ 3%。你的变更率是多少,全量重建就比增量贵多少倍的倒数。 变更率越低,全量重建越亏;一个每周只改 1% 的知识库,全量重建的浪费是 100 倍。
更要紧的是增长曲线的形状。增量策略的成本只跟”改了多少”走,跟库有多大无关;全量重建的成本跟库的总量成正比。文档量翻三倍,增量成本翻三倍(因为变更绝对数也翻了三倍),但全量重建的成本也翻三倍——两条线看起来都在涨,可它们之间那 33 倍的差距是原样保留的,而且绝对差额在放大。
还有一笔不写在账单上的成本:重建期间的窗口。全量重跑二十分钟,这二十分钟里向量库处于什么状态?如果是原地覆盖,检索结果会在新旧之间抖动;如果是双写切换,你需要两倍的存储。跑到一半失败了怎么办?大概率是整批重来,钱和时间都翻倍。这些工程负担,增量方案几乎都没有。
三、增量更新的四个关键点
3.1 用内容哈希判断”真的变了”
判断依据不能是文件的修改时间,也不能是 Git 的提交记录——重新导出一次、批量改一次格式、跑一次格式化工具,这些都会让全库文件的 mtime 更新,但内容其实一个字没动。
正确的做法是对每个切好的块算内容哈希,把哈希连同向量一起存下来。更新时重新切块、重新算哈希,只有哈希对不上的块才送去向量化。
import hashlib
def block_id(doc_id: str, index: int) -> str:
return f"{doc_id}#{index}"
def content_hash(text: str) -> str:
# 先做规范化,避免空白差异导致假变更
normalized = " ".join(text.split())
return hashlib.sha256(normalized.encode("utf-8")).hexdigest()
def plan_update(doc_id: str, new_blocks: list[str], stored: dict[str, str]):
"""stored: {block_id: content_hash},来自向量库里已存的元数据"""
to_embed, to_delete = [], []
new_ids = set()
for i, text in enumerate(new_blocks):
bid = block_id(doc_id, i)
new_ids.add(bid)
h = content_hash(text)
if stored.get(bid) != h: # 新增或内容变化
to_embed.append((bid, h, text))
for bid in stored: # 文档变短了,尾部的块要删
if bid.startswith(f"{doc_id}#") and bid not in new_ids:
to_delete.append(bid)
return to_embed, to_delete
注意那个规范化步骤。如果不做,把 \r\n 换成 \n、或者在段落间多打了一个空格,都会让哈希变化,于是”增量”退化成”接近全量”。规范化的强度要拿捏:空白折叠通常安全,但别把标点也归一化掉——那可能真的改变语义。
哈希方案还有一个副作用好处:它天然是幂等的。同一批更新重复跑两次,第二次会发现所有哈希都对得上,什么都不做。
3.2 分块策略定下来就别轻易改
这是我最想强调的一条:改分块参数等于全量重建。块长从 500 改成 400,所有块的边界全变了,所有哈希全对不上,19,200 个块一个不剩全要重算。
这件事的隐蔽性在于,“把块长调小一点试试”听起来是个五分钟的小改动,实际代价是一次完整建库。我见过团队在调参阶段来回改了七八次块长,每次都在全量重跑,白白烧掉的量比正式建库多好几倍。
对策也简单:调参阶段用抽样的子集。抽 200 篇有代表性的文档建一个小库,在小库上把块长、重叠、切分规则调到满意,再一次性应用到全量。子集只有全库的 2.5%,试七八次的总成本还不到一次全量重建的零头。分块策略本身怎么选,取决于文档结构和检索目标,跟成本关系不大,这里不展开。
3.3 换 embedding 模型也是全量重建
不同模型产出的向量落在不同的空间里,混着用完全没有意义——你不能拿 A 模型的查询向量去检索 B 模型建的库,算出来的相似度是噪声。所以换模型 = 全库重新向量化 + 向量库重建索引,一次都跑不掉。
正因为代价高,选型要尽量一次到位。选型阶段该测什么,我的清单是这几条:
- 用你自己的语料测检索质量,不要只看公开榜单。准备 50 到 100 条真实用户问过的问题,标出正确答案所在的文档,跑各个候选模型,看命中率和排序位置。榜单反映的是通用语料上的平均表现,跟你的领域词汇未必一致。
- 确认语言与领域适配。中文知识库要专门测中文;如果文档里混着大量英文术语、产品型号、代码片段,这些”非自然语言”内容的表现要单独看。
- 看向量维度对下游的影响。维度直接决定向量库的存储量和检索时的计算量,这部分成本不在 embedding 账单里,但真实存在。具体维度取值以模型官方文档为准。
- 确认单条输入的长度上限。如果模型的输入上限低于你的目标块长,块会被截断,尾部内容静默丢失——这种问题不报错,只是检索质量莫名其妙地差。
- 确认批量接口的行为:一次能提交多少条、返回顺序是否与输入顺序一致、部分失败时怎么标记。这些差异会直接影响你的建库脚本怎么写。具体限制以官方文档和控制台为准。
- 测一测服务的稳定性表现,但别指望第一天就能测准。低并发起步,观察一段时间的错误率和响应分布,再决定要不要加压。
把这些测完再定,比事后换模型划算得多。选型的其他维度可以参考 RAG Embedding 选型指南。
3.4 删除和更新必须同步清理向量库
这条是正确性问题,不只是成本问题,但它经常被增量方案漏掉。
文档被删除了,对应的向量还留在库里,用户一检索就命中一段已经不存在的内容——比如一个已经下线的产品的价格。文档被改短了,原来切出 12 块,现在只有 8 块,后面 4 块的向量还在,检索会命中旧版本的段落。这两种情况生成出来的答案都是言之凿凿的错误,比检索不到还糟。
所以更新逻辑必须是三件事一起做:新增/变更的块写入,消失的块删除,未变的块保持不动。上面那段 plan_update 里返回的 to_delete 就是干这个的。删除操作要和写入放在同一次更新事务里,或者至少保证顺序——先删后写,中间失败了顶多是暂时缺内容;先写后删,中间失败了就是新旧并存,两个版本同时被检索到。
另外,向量库里最好为每个块存上 doc_id、block_index、content_hash、model_version、chunk_config_version 这几个字段。前三个用于增量判断,后两个用于回答一个很实际的问题:这个库到底是用哪个模型、哪套分块参数建的? 没有这两个字段,半年后没人说得清,只能靠全量重建来”确保一致”——又绕回去了。
四、把建库当离线批任务来跑
向量化是典型的离线任务:没有人在等结果,几分钟还是几小时完成都不影响用户。这个性质意味着它天然适合走批量接口和错峰时段,能离线跑的活别在线跑那篇把这类任务的省钱路径讲得比较全,这里只补充建库脚本本身的工程要点。
分片。把 19,200 个块切成固定大小的片,比如每片 500 块,共 39 片。分片的意义不是并发,而是让失败的影响范围可控——一片失败只影响这一片。片的边界要稳定:按块 ID 排序后固定切分,而不是按遍历顺序,这样重跑时片的内容才一致。
断点续跑。建库跑到一半失败非常常见:网络抖动、限速触发、进程被杀、机器重启。脚本必须能从上次的位置接着跑,而不是整批重来。实现上不需要复杂的状态机,一张”已完成块”的记录表就够了——每完成一片就把这片的块 ID 和哈希落盘,重启时先读这张表,跳过已完成的部分。这张表要在写入向量库成功之后才更新,顺序反了会导致漏块。
幂等。同一片被重复处理时,结果必须一致。做法是用 block_id 作为向量库里的主键做 upsert,而不是无脑 insert。否则重跑一次,库里就多一份重复向量,检索时同一段内容占掉多个名额,把别的答案挤出去。
并发要保守起步。你多半不知道自己账号的实际限速档位,那就别猜。从很低的并发开始(比如 2 到 4 个并发),观察一段时间的错误率,没问题再逐步往上加。遇到限速错误时用指数退避重试,而不是原地硬撞——硬撞会让限速窗口一直续期,越撞越慢。批量接口一次提交多少条也同理,先用小批量跑通,确认返回顺序和错误结构,再往上调。
先跑小批量算出自己的单位成本。正式建库前,拿 100 篇文档完整跑一遍,记录真实消耗的 token 数,除以这 100 篇的字符数,得到一个”元/万字”的单位成本。有了这个数,全库要花多少、每周增量要花多少,都是一次乘法的事,比任何估算都准。
五、被忽略的大头:查询侧
前面算的都是写入侧。但 RAG 每一次检索,都要先把用户的查询语句向量化一次——这部分是持续发生的,而且频率跟用户量成正比。
继续用假设参数演示:
| 项目 | 假设值 |
|---|---|
| 日查询次数 | 20,000 次 |
| 平均查询长度 | 30 字符 |
| 换算系数 | 0.9 token/字符 |
- 单次查询 token = 30 × 0.9 = 27 token
- 每日 = 20,000 × 27 = 540,000 token
- 每月(按 30 天)= 540,000 × 30 = 16,200,000 token ≈ 16.2 百万
对比一下:一次建库是 9.6 百万,查询侧只要 9,600,000 ÷ 540,000 ≈ 17.8 天就把整个建库的量用完了。再对比增量更新:每周 288,000 token,折算到每月约 1.25 百万(288,000 × 52 ÷ 12 = 1,248,000),查询侧是它的 13 倍(16.2 ÷ 1.248 ≈ 12.98)。
判断方法可以固化成一条:
查询侧月量 = 月查询次数 × 查询平均 token 数
写入侧月量 = 月变更文档量对应的 token 数(含重叠)
两边一比,就知道优化重点该放哪边。在高频问答场景里,查询侧几乎注定是大头;在内部文档库这种”库很大、用的人不多”的场景里,才是写入侧占主导。别默认答案,算一次。
查询侧最有效的优化是缓存重复查询的向量。用户的问法比想象中集中得多——同一个问题换几个说法反复被问,客服场景里尤其明显。把规范化后的查询文本做哈希,向量缓存到内存或 KV 里,命中就直接复用。假设重复率 25%,上面那个例子每月能省 16.2 × 25% = 4.05 百万 token,比整个月的增量更新量还多三倍。
缓存要注意两点:一是规范化要和建库时保持同一套逻辑(大小写、空白、全半角),否则命中率上不去;二是换 embedding 模型时必须整体清空缓存,缓存 key 里最好带上模型版本号,不然新旧向量混在一起,检索结果会诡异到没法排查。
六、分块大小怎么影响成本
块切得越小,块数越多;重叠若是固定 token 数,块越小重叠占比就越高,总送出 token 也越多。用前面那 8,640,000 正文 token 算几组对比:
| 块长 | 重叠 | 有效步进 | 块数 | 送出 token | 相对基准 |
|---|---|---|---|---|---|
| 200 | 50 | 150 | 57,600 | 11.52 百万 | +20% |
| 300 | 100 | 200 | 43,200 | 12.96 百万 | +35% |
| 500 | 50 | 450 | 19,200 | 9.60 百万 | 基准 |
| 1000 | 50 | 950 | 9,095 | 9.09 百万 | −5% |
(计算方式:块数 = 正文 token ÷ 有效步进,送出 token = 块数 × 块长。相对基准以 500/50 那行的 9.60 百万为 100%。)
这里有个容易被忽略的规律:如果重叠是按块长的固定比例设的(比如统一 10%),那么送出 token 总量跟块长无关,永远是正文量的 1 ÷ 0.9 ≈ 1.111 倍。只有当重叠是固定 token 数时,块长才会影响总量——块越小,那个固定的重叠占比越高。搞清楚自己的切块器是哪种,再决定要不要为了省钱去动块长。
还有一部分成本不在 embedding 账单上:块数决定了向量库里的条数,直接影响存储占用和检索时的计算量。200 token 的块方案有 57,600 条,是 500 方案的三倍;从存储和检索开销看,这个差距比 embedding 那 20% 的差距更值得关心。
但成本从来不该是分块决策的主要依据。块太大,一块里塞了好几个主题,检索命中了也是把一堆无关内容一起塞进上下文,既拉低生成质量又推高对话侧的 token 开销;块太小,语义被切碎,单块信息不足以支撑回答。这是效果与成本的双向权衡,只看一边一定会做错决策。对话侧和检索侧的成本怎么放在一起算,RAG 和长上下文哪个划算里有一套可复算的公式。
七、几件不要做的事
为了省钱把块切得很大。 从上表看,块长从 500 拉到 1000,embedding 侧只省 5%。但检索精度的损失可能很严重,而且命中的大块会塞进对话上下文,那边的单价通常远高于 embedding。省了小头,亏了大头。
跳过质量评估直接上线。 建库跑通了不等于检索有效。上线前至少要有一组标注好的问答对,跑一遍看命中率。没有这个基线,后面任何调整都是盲改——你没法回答”这次改动到底变好了还是变坏了”。
不记录版本信息。 半年后有人问”当前这个库是用哪个模型、哪套分块参数建的”,如果答不上来,唯一稳妥的选择就是全量重建一次。为了省下几个元数据字段,赔进去一次完整建库,很不划算。
把向量库当成唯一的数据源。 原始文档、切块结果、哈希记录都要在别处留一份。只有向量库的话,任何一次重建都必须从原始文档重新走全流程;有了中间产物,很多情况下可以跳过重新切块甚至复用部分向量。
在生产库上做调参实验。 前面说过,抽子集调参。在生产库上改块长,等于一边烧钱一边让线上检索质量抖动。
上线前的自查清单
- 换算系数是实测的:用目标模型的分词器对自己的语料抽样测过,不是按字数拍的。
- 单位成本跑出来了:拿 100 篇左右完整跑过一遍,有一个”元/万字”的真实数字,全库和月度增量都能一次乘法算出来。
- 增量判断基于内容哈希:不是 mtime,不是 Git 提交,并且哈希前做了空白规范化。
- 删除路径是通的:文档删除、文档变短导致的尾部块,都能被清理掉;写入和删除的顺序确定(先删后写)。
- 元数据齐了:每个块存了
doc_id、block_index、content_hash、model_version、chunk_config_version。 - 建库脚本能断点续跑:分片稳定、完成记录在写入成功之后落盘、以
block_id做 upsert 保证幂等。 - 并发是保守起步的:低并发开跑,配了指数退避重试,观察错误率之后再加压,没有靠猜设定限速档位。
- 查询侧算过账并加了缓存:知道查询侧月量与写入侧月量的比值,查询向量做了缓存,缓存 key 带模型版本号。
把这八条过一遍,向量化这笔钱基本就不会失控了。剩下的精力,应该花在检索质量上——那才是 RAG 效果的真正瓶颈。各家 embedding 的计价口径差异,可以再看看 Embedding API 调用成本。