RAG 要不要上向量库:720 个 chunk 用 numpy 点积就够了

写给谁看:文档量在几百到几千 chunk 这个区间,正准备装 FAISS 或 Chroma 的人。 上一篇讲切分(96 篇博客切出 720 个 chunk),这篇讲向量化和存储。检索准不准是下一篇的事,这篇只讲索引怎么建。

RAG 教程的默认第三步是「装个向量库」。我这个量级压根不需要。

先把量级算清楚:

ini 复制代码
720 个 chunk × 1024 维 × float32 = 2,949,120 字节 ≈ 2.81 MB

2.81 MB。 一张手机照片的大小。全部塞进内存,一次矩阵乘法就算完了对所有 chunk 的相似度,耗时在毫秒级。

装 FAISS 或 Chroma 能带来什么?多一个依赖、多一层部署、多一套要维护的持久化格式。检索质量一点不变 ------ 因为暴力检索是精确的,近似最近邻(ANN)那些索引结构存在的意义是在百万级向量上牺牲一点精度换速度。我这里没有速度问题,也就没有牺牲精度的理由。

什么时候真的需要向量库

免得这篇被当成「向量库无用论」。分界线大概在这几件事上:

需要向量库 我这个场景
向量数到十万、百万级,全量点积扛不住 720 条
内存装不下,需要磁盘索引 / 分片 2.81 MB
需要元数据过滤 + 向量检索混合查询 用不到
多进程 / 多机共享同一份索引 单机单进程
需要增量插入删除,且不想重建 96 篇博客,重建全量 34 秒

最后一行是关键 :全量重建只要 34 秒,那「增量更新」这个需求就不存在了。向量库很大一部分复杂度是为了解决增量和规模,这两个问题我都没有。

反过来说,等哪天文档到几万条、重建要十几分钟了,再上向量库 ------ 那时候它解决的是真问题。

存的时候先归一化,检索时省一次除法

余弦相似度是 dot(a,b) / (|a|·|b|)。如果入库时就把每个向量归一化成单位长度,检索时余弦相似度就是一次点积

python 复制代码
mat = np.asarray(vectors, dtype=np.float32)
norms = np.linalg.norm(mat, axis=1, keepdims=True)
norms[norms == 0] = 1.0      # 防止零向量除零
mat /= norms
np.save("embeddings.npy", mat)

⚠️ norms[norms == 0] = 1.0 这行别省。理论上 embedding 不会返回零向量,但输入是空字符串或纯符号时不好说,除零会得到一堆 nan,而 nan 会静默地让相似度排序全乱 ------ 它不报错。

检索那一侧就是:

python 复制代码
q = embed(query)              # 也要归一化
scores = mat @ q              # 一次矩阵乘,得到 720 个分数
top = np.argsort(-scores)[:k]

embedding 选型:先复用已有的 key

用的是百炼的 text-embedding-v3,1024 维,OpenAI 兼容接口。

选它的第一个理由不是效果,是它和我另一个项目用的是同一个 key 。多引一套凭证意味着多一处要管的密钥、多一个会过期的额度、多一个出问题时要排查的地方。在还没证明效果有差别之前,减少变量比追求最优更重要。

⏳ 待对照的是本地 bge-small-zh:免费、无网络依赖,但要自己跑模型。这个对比要等评测集跑完才有意义 ------ 现在换过去,我没有任何指标能说明是变好还是变坏。

⚠️ 上一篇漏算的一笔成本:挂标题路径贵了 28.5%

上一篇我说「给每个 chunk 挂标题路径,成本就是拼个字符串」。做到这一步才发现不对。

字数 是什么
204,971 纯正文
263,298 ⭐ 实际送去向量化的文本(正文 + 标题路径)

差 58,327 字,+28.5%。 embedding 按字符收费,这三成是真花出去的。

这个交易仍然划算 ------ 换来的是那些几十字的 chunk 从「检索不到任何东西」变成「能命中」,而总花费也就一毛三。但**「几乎没有代价」这句话,在有数字之前我不该写**。

📌 顺带一条通用的:凡是「顺手加一个字段」的优化,都要想一遍它在下游按量计费的环节会放大成多少。

实测数字

chunk 数 720
维度 1024
送去向量化的字数 263,298
耗时 34.3 秒
API 调用次数 72(批量 10)
平均每次 0.48 秒
矩阵大小 2.81 MB
成本 约 ¥0.13

(定价按写作时 ¥0.0005/1k token 估,中文粗算 1 字 1 token,以官方页面为准。)

一毛三。 这个数字值得单独说一句:很多人对「调 embedding 要花钱」的心理预期远高于实际,于是先去折腾本地模型,把简单的事做复杂了。先花一毛三跑通,再决定要不要省这一毛三。

两个工程细节

批量大小取 10,偏保守。 各家 embedding 接口对单次输入条数和总 token 都有上限,而超限的报错通常很含糊。批量从小开始,跑通了再往上调 ------ 这比一开始塞 100 条然后猜哪个限制被撞了要快。

指数退避重试。 批量接口最常见的失败是限流,退避重试三次基本能过。

python 复制代码
for attempt in range(retry):
    try:
        return client.embeddings.create(model=MODEL, input=texts,
                                        encoding_format="float")
    except Exception as e:
        if attempt == retry - 1:
            raise
        time.sleep(2 ** attempt)      # 1s, 2s, 4s

⭐ 另外给脚本留了个 --limit N 参数,先跑 20 条确认接口通了再全量。这个习惯在按量计费的接口上很值 ------ 全量跑到一半才发现参数写错,钱和时间都白花。

下一步:先有评测集,再谈调参

索引建好了,但我现在不能说检索效果好 ------ 没有评测集,任何「感觉挺准的」都不算数。

评测集已经建了 25 条,形态是 问题 → 期望命中的文章,量 top-k 召回率。两条纪律:

  • 问题用读者的措辞写,不能照抄文章标题。 照抄会让检索虚高,测不出真实效果。
  • ⭐ 其中 8 条刻意设计成「问法和原文用词几乎不重叠」 ,单独算这一组的召回率 ------ 那才是语义检索真正被考验的地方。比如问「外层 div 的高度被里面浮动的元素撑不开」,去命中一篇标题叫 BFC 的文章:两句话没有一个共同的词。

召回率数字下一篇给。

最值得记的三条

  1. 几百到几千 chunk 不需要向量库。 720 × 1024 维只有 2.81 MB,全量点积毫秒级;而全量重建只要 34 秒,连「增量更新」这个需求都不存在。向量库的复杂度主要用来解决规模和增量,这两个问题都没有的时候,它只是多一个依赖。
  2. 入库时归一化,检索就是一次点积norms[norms == 0] = 1.0 别省,零向量除零会得到静默的 nan,让排序全乱且不报错。
  3. 一毛三。 720 个 chunk 全量向量化的实际花费。先花这一毛三跑通,再决定要不要为省它去折腾本地模型。
相关推荐
虞七月1 小时前
2026企业AI办公工具选型完全指南
人工智能·ai编程
Lambert2811 小时前
AgentScope Java 从零(07):官方有权限引擎,我却在工具里写了个 if
java·后端·ai编程
canber1 小时前
AI Agent 指令分层工程:System Prompt、总地图与条件加载
ai编程
Ticnix1 小时前
多租户 RAG:让每个用户只检索到自己的知识库
后端·python·agent
ID34610744201 小时前
【课程设计】基于Spring Boot+Vue的游戏账号租赁系统的设计与实现-计算机毕设 附源码50345
javascript·vue.js·spring boot·python·node.js·php·课程设计
青梅煮酒论英雄1 小时前
我们是怎么让 AI 找到那个 Bug 的
agent·ai编程·harness
赵赵4301 小时前
AI 写前端,优化的是演示,不是交付
ai编程
ClouGence1 小时前
从 Prompt 到可复用作业辅导助手:我搭了一个 AI 作业批改工作流
人工智能·aigc·ai编程
Ticnix1 小时前
我删了 200 行 if-else,把决策逻辑全写进了 System Prompt
python·llm·agent