不用向量的本地 RAG:用 SQLite FTS5 和 BM25 做代码搜索
为什么稠密向量索引会拖累开发者的机器,以及 SQLite FTS5 配合 BM25 如何只用一小部分内存,就带来更快、更精确的代码检索。
这几年,本地代码搜索的套路几乎一成不变:把代码仓库切成块,把每一块变成 1536 维的嵌入向量,再让一个 HNSW 索引在后台进程里一直运行。这看起来很现代,仿佛笔记本电脑多了一个能读懂代码含义的小型搜索引擎。可是在一台本就繁忙的工作站上,这套做法有一笔很少写进 README 的成本。
本地向量搜索的宣传数字很亮眼:MTEB 检索得分很高,几分钟就能为数百万个代码 token 生成嵌入,在消费级硬件上实现"即时语义智能"。但如果去读内存方面的说明而不是营销文案,看到的就是另一幅景象。稠密向量索引的召回率,是用内存带宽、RAM 或 VRAM 以及可预测性换来的。
当“全都用向量”撞上内存墙
把向量数据库和本地 LLM、IDE 以及几个 Docker 容器放在一起跑,它并不会闲着。它要把庞大的图结构常驻在 RAM 或 VRAM 里,和其他工具争抢带宽,还会带来语义漂移:你搜索精确字符串 ERR_AUTH_EXPIRED,拿回来的却是只和身份验证沾点边的代码。
正因如此,不少团队正在悄悄回归用 C 实现的原生算法,也就是带 BM25 的 SQLite FTS5。本地查询不再做向量运算,而是用基于词频的确定性排序,在 5 毫秒以内找出精确的标识符。
概念匹配与可预测的速度
这不只是一次小调整,它改变了开发者工具的构建方式。用 FTS5 取代本地向量的开销,就是放弃理论上的概念匹配,换来精简而快速的执行。
- 效率:去掉稠密索引可以腾出好几 GB 内存。原本要花约 200 毫秒计算嵌入的工作,以原生方式在 5 毫秒内就能完成。
- 精度:查找精确的关键字和函数名,例如
getUserById(),结果完全确定,索引不会返回毫不相干的函数体。
两种流程的对比如下:
[Dense Vector RAG Flow]
Code Query -> Generate Vector Embeddings -> HNSW Index Search -> Semantic Drift Risk -> LLM Context
[Deterministic BM25 Flow]
Code Query -> Native SQLite C-Index -> Exact Keyword Match (5ms) -> Minimal LLM Context
你可能已经遇到过的症状
只要跑过本地智能体或代码搜索,大概都感受过这些摩擦:
- 资源争用。风扇狂转,电池只能撑两小时,本地推理慢得像爬,因为后台有个引擎在抢内存。
- 精确匹配的代价。搜索某个具体的错误字符串或变量,返回的是五个在含义上接近身份验证的文件,偏偏没有真正包含那个标志的那一个文件。
并排对比,数字为粗略估计:
| 维度 | 本地向量数据库(Chroma、Qdrant) | SQLite FTS5(BM25) |
|---|---|---|
| 内存占用 | 约 1.5 到 4 GB 的 RAM 或 VRAM | 约 15 到 30 MB 的 RAM |
| 搜索延迟 | 120 到 400 毫秒,主要花在嵌入计算上 | 低于 5 毫秒,原生 C |
| 标识符精确匹配 | 较弱,容易漂移 | 很强,确定性的关键字匹配 |
| 系统开销 | 后台守护进程、C++ 绑定 | 空闲时没有任何进程在跑 |
| 最适合 | 在文本中做宽泛的主题搜索 | 在代码、日志和本地 RAG 中做精确查找 |
这些只是大致数字,实际结果取决于语料规模、模型选择和硬件。
这对技术负责人意味着什么
遇到本地问题就挂一个向量数据库的条件反射正在消退。更有用的习惯是把握分寸。资深架构师的本事,不在于把越来越重的 AI 基础设施塞进开发者的笔记本,而在于看清什么时候十行原生代码或一个 30 MB 的 SQLite 文件,比动辄数 GB 的神经网络流水线更好用。
随着工具越来越快、工作流越来越精简,胜出的不会是跑着最大模型的技术栈,而是那些交付干净、没有冗余代码的技术栈。这并不意味着嵌入毫无用处:对于文本里模糊的、概念层面的问题,它们依然有用武之地。但要在代码库里找一个精确的标识符,先用便宜又确定的工具,只在它失手的地方再加入向量。