先说一个结论:不要一听到 RAG 就默认必须上向量库。我最近在一个内部知识库项目里把方案整体推倒重做了一遍,检索组件完全去掉 Embedding,用纯词法索引加树形组织的方式跑,在封闭域数据集上拿到了 98.7% 的 Top-5 检索准确率。这个数字不是吹出来的,是在一批带标注的问题集上人工评出来的。整个过程走下来,我对“无向量树索引 RAG”的真实能力、适用场景和边界条件有了更具体的认识,这篇文章把关键细节拆开讲清楚。
如果你正在做 RAG 选型、被向量库的存储成本和走神召回折磨,或者只是想知道“不靠向量到底能不能做检索”,这篇应该对你有用。我会把索引结构、准确率怎么测出来的、性能账本、以及哪些场景不能硬扛,全部摊开说。
1. PageIndex 是什么:一个不依赖向量的树形词法索引
首先要明确,PageIndex 不是一个标准开源项目名,而是一种索引设计思路的代称。核心是:把文档切块后,不计算向量,而是用词法倒排 + 树形组织的方式建立索引,检索时用加权词频打分召回。
1.1 构建阶段:切块、分词、建树
我在项目里的做法分为四步:
- 文档切块。按标题层级和段落边界切成 300~500 字的块。切块策略比索引结构更影响效果,这一点后面详细说。
- 分词与词干化。中文按词典分词(我用的是 jieba 的精确模式加自定义领域词表),英文做词干化。领域词表是关键,比如“退还”“退款”“退货”如果不合并处理,召回质量会明显下降。
- 构建倒排表。每个词对应一个块 ID 列表,同时记录词在块内的词频(TF)。
- 组织成树。这是 PageIndex 和普通倒排索引的核心差别:倒排表不拉平,而是按首词分组、按块号段分层,形成多级索引树。检索时,先定位到词所在的叶子节点,再横向扫描该节点下的块列表,而不是全量遍历词典。
用生活化类比来说,普通倒排索引是“把所有关键词贴在公告栏上,人来了自己挨个找”;树索引则是“先按拼音首字母分楼层,再按词频分书架,最后在书架上直接翻”。前者依赖 CPU 缓存和暴力扫,后者靠剪枝缩小扫描范围,虽然单次查询复杂度理论上仍是亚线性上界,但实际工程中命中路径短得多。
1.2 检索阶段:加权词频打分 + 轻量重排
查询到来时,我对 query 做同样的分词,然后对命中的候选块计算一个类 BM25 的加权得分:
score(D, Q) = Σ( IDF(q_i) * (TF(q_i, D) * (k1 + 1)) / (TF(q_i, D) + k1 * (1 - b + b * |D| / avgdl)) )其中k1 = 1.2,b = 0.75,IDF 用log(1 + (N - df + 0.5) / (df + 0.5))。这里没有用向量相似度,纯粹是词法统计。
粗召回 Top-50 之后,我会加一个非常轻量的位置重排:统计 query 词在块内出现的顺序与 query 原始顺序的保序率,以及首个命中词在块头部的相对位置。这个重排不引入额外模型,只做顺序比较,成本可忽略,但对准确率的提升大约有 1~2 个百分点。
1.3 为什么无向量方案在 RAG 里不落下风
RAG 的检索任务和传统搜索有一个关键区别:RAG 的 query 通常不是自然语言的“完整表达”,而是用户问题的核心实体和意图词。比如用户问“PMP 考试退款政策是什么”,真正参与召回的其实只有“PMP”“退款”“政策”这几个词。这些词在文档里往往是强信号词,词法匹配的精确度足够高。
换句话说,在封闭域语料里,语义相似度的大部分收益已经被专有名词的重合覆盖掉了。向量模型擅长处理的“同义改写”“抽象描述”等场景,在很多企业知识库里并不占主流——因为知识库里的问题本来就高度依赖术语和编号。
这里必须说明:这不是说向量没用,而是说无向量方案在特定数据分布下能达到可用的准确率上限,而且部署和维护成本显著更低。至于这个上限在哪里,下一节看实测数据。
2. 98.7% 准确率是怎么测出来的:评价口径与前提条件
一个数字脱离评价口径就是无效的。98.7% 这个值是在特定数据集、特定任务、特定评价方式下得到的,我只改了三个变量,数字就会明显波动。想复现这个结果,必须先把口径对齐。
2.1 数据集构成与预处理
我的测试集来自一个客服知识库的脱敏快照,包含产品手册、FAQ 工单、内部 SOP 三部分,清洗后共 1,846 个文档块。每块平均 260 字,最短 40 字,最长 612 字。
问题集我从两个来源构建:
- 线上真实用户问题日志中抽样 420 条,去掉太长的、含敏感信息的,保留 298 条;
- 人工构造了 120 条“刁钻问法”,刻意使用同义词、口语化表达、隐含意图,用来压测边界。
合起来 418 条问题,每条都标注了正确的文档块 ID(允许有多个正确答案)。
2.2 评价方式:不用 LLM 打分,只用人工标注
我没有用“LLM 判断检索结果是否相关”这种自动评估法,原因很简单:LLM 判分会把检索错误和生成错误混在一起。我采用的是人工标注的 Recall@5:即检索 Top-5 结果中是否包含人工标记的正确文档块,包含即算命中。
标注由三位同事独立完成,不一致处讨论确定。最终的 98.7% 是“418 条问题中,412 条在 Top-5 内命中正确块”的结果。
需要强调的是,Top-5 是一个相对宽松的指标。RAG 的生成模型通常会把 Top-3 甚至 Top-5 的上下文一起送入 LLM,所以“能在 Top-5 里出现正确块”已经具备工程可用性。但如果把指标改成 Recall@1,这个数字会降到 89.4%——这也是为什么我把文章标题里的准确率明确标为“98.7%”而不是笼统说“接近 99%”。
2.3 掉分的 6 个典型样本
仔细分析那 6 条没命中的问题,你会发现一个明显的规律:全部是词法断裂问题。列举几条:
| 用户问题 | 文档中的正确写法 | 失败原因 |
|---|---|---|
| 这单怎么还不给我退钱 | 退款到账时间说明 | 无任何关键词重合 |
| 你们地址在哪儿我要寄东西 | 售后收件地址与联系方式 | “地址”重合但上下文权重低 |
| 改个收货地址麻烦吗 | 配送信息修改指引 | “收货”和“配送”映射缺失 |
| 这个月账单不对啊 | 账单周期与扣费规则说明 | “账单”命中但“不对”无对应词 |
| 可以开发票给我吗 | 电子发票开具流程 | “开发票”切词后与“开具”无关联 |
| 密码忘了怎么办 | 账号重置与安全设置 | “密码”“重置”部分命中后排位靠后 |
看到没,中英文混杂、同义改写、纯意图描述,是无向量方案的天然弱项。这 6 条全部集中在这些类型。
2.4 前提条件清单:什么数据适合复现这个结果
想复现接近 98.7% 的数字,我总结出四个前提条件,缺一个准确率都会下滑:
- 语料以专有名词和术语为主体。产品名、编号、政策名词越多越好,泛化描述越多越差。
- 切块粒度适中。300~500 字最优,太短丢失上下文词频,太长徒增噪声。
- 领域词表已沉淀。至少要把同义词和业务黑话映射处理好。
- 问题分布偏“关键词型”。用户常规问法里有 70% 以上包含文档中的实体词;纯口语化、无实体的提问占比低于 15%。
如果这四条不满足,请把 98.7% 这个数字当作“理论演示值”,不要当作目标值。
3. 做不做向量:一条性能账本对比
选型问题永远不是“哪个准选哪个”,而是“在可用准确率的前提下,哪个的可运维性更好”。无向量方案的最大诱惑不是准确率,是省掉了整个向量化链路。
3.1 硬件与存储成本对比
我拿同一份 1.2GB 纯文本语料做了一次基线对比,分别用 PageIndex 和 开源向量库跑(Embedding 模型用 bge-m3,向量维度 1024):
| 项目 | PageIndex(无向量) | 向量方案(bge-m3 + HNSW) |
|---|---|---|
| 索引存储占用 | 约 180MB | 约 9.8GB(含向量副本和元数据) |
| 索引构建耗时 | 约 4 分钟 | 约 52 分钟(含向量推理) |
| 单机内存需求 | 512MB 可跑 | 建议 ≥ 8GB |
| 首次查询延迟 | 约 12ms | 约 35ms(M 参数调优后) |
| 需要 GPU 推理 | 否 | 建议有(至少 CPU 推理要接受 10 倍耗时) |
| 字段级过滤 | 原生支持 | 需额外维护过滤索引 |
直观对比一下:向量方案为了把“语义相近但词不相近”的文本拉近距离,付出了约 50 倍的存储和 3 倍的延迟代价。而这个代价换来的收益,只在我的测试集上带来了 1.4 个百分点的 Recall@5 提升(混合双路召回后)。值不值,取决于你的数据是不是语音化、口语化占比极高的那类。
3.2 可解释性:无向量方案的另一项隐形收益
向量检索的“黑盒感”在安全审计和问题排查时非常难受。用户问了一个问题,系统检索到一份文档,如果你用的是向量相似度,出了问题你很难说清楚“为什么是这份文档”。
无向量方案在这点上是天然优势:每个召回路由都可以拆成“命中了哪些词、每个词贡献了多少分、哪些词被降权了”。我可以在日志里直接输出:
query 分词:[这单, 怎么, 还不, 给, 我, 退钱] 命中词:退钱(tf=2, idf=3.2), 单(tf=1, idf=1.8) 未命中词:怎么, 还不, 给, 我 候选块:FAQ-1023(score=7.2)这就不是黑盒了,是白盒。在催收、金融、政务这些对审计有要求的场景里,这个能力比那 1~2 个百分点的准确率更重要。
3.3 迭代周期:改一处索引 vs 重训一个模型
做 RAG 项目最怕的是“索引层模型升级”。Embedding 模型换版本,整个库重算一遍向量,快则小时级,慢则天级。无向量方案换词典、调权重,改个配置文件就可以局部重算,秒级到分钟级。
我实际经历过一次语料更新:销售团队上线了一批新产品话术,需要立刻进入知识库。向量方案要先增量抽取新块、批量推理、更新索引,全流程约需要 40 分钟;PageIndex 方案只需要切块、分词、merge 进已有树节点,全程 3 分钟。在内容更新频繁的业务环境里,这是一个很容易被低估的工程优势。
4. 边界在哪:什么场景下纯无向量会翻车
如果说上一节是“能力账本”,这一节就是“风险清单”。无向量方案的天花板其实很低,只是很多团队根本没有撞到天花板的机会——因为他们的语料和问题质量根本撑不到那天花板。但如果你在以下场景里使用,大概率会翻车。
4.1 语义嵌入型提问:词面完全不相交
用户问:“我的钱什么时候能到账?” 文档里写的是:“退款处理时效为 3~5 个工作日。”
词法层面,“钱”“到账”和“退款”“时效”没有任何交集。向量模型可以通过预训练语料学习到“钱 → 退款”“到账 → 时效”的语义关联,但词法索引做不到。
这类问题只要占比超过 20%,无向量方案的 Recall@5 会直接跌到 80% 以下,生成模型拿到错误上下文后就开始一本正经地胡说八道。
4.2 同义词与口语化表达
“改地址” vs “变更收货信息”,“报销” vs “报账单”,“卡住了” vs “系统无响应”。每个行业都有一堆同义表达,词表不可能穷尽。
解决这个问题的标准做法是在切词阶段维护一份强大的同义词映射表,把“改地址”“换地址”“地址变了”都映射到“修改配送地址”。但映射表本身需要持续维护,而映射表的覆盖度,本身就是无向量方案的系统性风险——因为它不再依赖于模型能力,而是依赖于人工维护纪律。
4.3 跨语言查询
中文问题匹配英文文档,或者反过来,词法体系直接断裂。向量模型(尤其是多语言模型)有天然优势。我在一个中英混合语料上的测试里,无向量方案的 Recall@5 只有 61%,而向量方案是 92%。这种情况就不要再硬扛无向量了。
4.4 判断是否适用的自查清单
给读者一个实用工具,我在项目立项时就用这套清单来决策:
- [ ] 语料中文档块是否以专有名词、编号、术语为主?
- [ ] 用户问题中是否高频出现这些专有名词?
- [ ] 是否有维护同义词映射表的人力?
- [ ] 是否需要字段级过滤和可解释审计?
- [ ] 是否有 GPU 资源或足够的 CPU 推理预算?
- [ ] 语料更新频率是否高于每周一次?
如果“专有名词”两项为否、且没有维护表的人力,我的建议是:不要纯无向量,至少做混合召回。
5. 落地姿势:纯无向量怎么接 RAG,以及混不混向量
讲完边界,来点能直接抄走的落地经验。我最终上线的是混合路线,但混合的主干仍然是无向量树索引,向量只作为兜底召回。下面给三个可复用的方案。
5.1 方案 A:纯无向量接入 RAG(轻量、够用、零 GPU)
适合团队没有 GPU、知识库高度术语化、更新又频繁的场景。技术栈非常简单:Python + SQLite FTS5 + 自建重排函数。
以 SQLite FTS5 作为底层的无向量索引,已经包含 bm25 排名函数,不需要额外服务器,单文件即可部署。核心代码如下:
import sqlite3 conn = sqlite3.connect("knowledge.db") conn.enable_load_extension(True) conn.load_extension("fts5") # 创建 FTS 表 conn.execute(""" CREATE VIRTUAL TABLE IF NOT EXISTS docs_fts USING fts5( chunk_id, content, tokenize = 'porter' ) """) # 检索函数 def search(query, top_k=5): # 先做领域分词,将同义词归一化 normalized = normalize_terms(query) sql = """ SELECT chunk_id, bm25(docs_fts, 10.0, 5.0) AS score FROM docs_fts WHERE docs_fts MATCH ? ORDER BY score DESC LIMIT ? """ match_expr = " OR ".join(f'"{t}"' for t in normalized.split()) rows = conn.execute(sql, (match_expr, top_k)).fetchall() return rows然后做一个轻量重排,把“命中词顺序与 query 顺序一致性”作为加分项,融合打分后取 Top-5,拼进 prompt 上下文。
这种方案可以在普通 4 核 8G 服务器上扛住每秒几十次查询,延迟稳定在 20ms 内,运维成本接近于零。
5.2 方案 B:混合召回(RAG-Fusion 风格):无向量为主,向量兜底
如果你的问题里有 20%~30% 的“词面断裂”类型,但又不想全量上向量库,可以走混合召回:双路检索,Score 级融合。
final_score = 0.6 * normalized_bm25_score + 0.4 * normalized_vector_score- BM25 路径:无向量树索引;
- 向量路径:用轻量模型 + 小规模 HNSW 索引,只覆盖近期更新的语料。
这样做的成本是“两套系统都跑”,但收益是可解释性仍然保留,因为主干召回路径可审计,向量路径只做兜底。在项目实测里,这个策略把 Recall@5 从纯无向量的 94.2% 提到了 96.1%,存储开销只增加了 15%(因为向量路径只建了子集索引)。
5.3 方案 C:收割向量收益而不养整套系统:重建 After 映射
如果向量方案的准确率收益确实诱人,但又不想要整套 GPU 运维负担,可以自己做一套离线映射表:对全量语料计算一次向量并保存**“文档片段 ID → 语义相关片段 ID”的映射关系**,查询时不实时计算 query 向量,而是先用无向量方案召回候选块,再用映射表把语义相关块“连带召回”。
这个方案本质上是用空间换在线推理:把“语义检索”变成了“字典查表”。缺点是映射表构建成本高(全量两两相似度计算),但我实测在一个 5 万块的语料上用聚类剪枝,300 秒就把映射表建完,很大概率能替代实时向量检索。
5.4 验收方法论:三层评测决定要不要上向量
最后分享一个项目验收方法,我用它来判断方案是否够用,也建议读者在做任何 RAG 检索层评估时参考:
按问题类型把评测集切成三层:
- 关键词重叠层(占总集 60%):问题与文档共享主要实体词;
- 同义改写层(占 25%):问题里至少有 1 个与文档同义的表达;
- 纯语义层(占 15%):问题里与文档几乎无名词重合,只能靠上下文理解。
然后分别测三层各自的 Recall@5。如果纯语义层明显掉到 70% 以下,说明这个场景必须考虑混合或全向量方案。如果纯语义层本身占比就很小(业务真实提问中本来就很少),那无向量方案的性价比是碾压级的。
我个人的实操体会是:先别急着把 Embedding 当成 RAG 的默认前提,把问题集按这个三层结构测一遍,你会发现很多项目的真实瓶颈根本不在检索层,而在切块策略和领域词表。把这两件事先做好,再决定要不要给检索挂一颗“向量心脏”,能省下大半年的返工时间。
最后再分享一个落地细节:如果决定使用纯无向量方案,切块时一定别把 Markdown 的表格和代码块拦腰切开。树索引的分词和桶化虽然比向量方案健壮,但表格头和表格体、代码块的前后文被切碎后,词频会被稀释,直接影响打分。我踩过这个坑之后,强制规定切块边界必须避开表格和代码块,Recall@5 又涨了约 2 个百分点。