向量数据库不是银弹:从枚举漏检到 ReACT 多轮召回的实践路径
2026/6/6 4:24:01 网站建设 项目流程

向量检索碰到"总述+枚举"结构时容易出现系统性漏检,根源不在模型,在分块策略。本文从可视化实验入手,逐步拆解多路召回和 ReACT Agent 闭环方案,在召回率和 token 成本之间找到较优的平衡点。适合正在做 RAG、知识库或信息抽取的后端和算法同学参考。

一个真实的翻车现场

最近在做一个政务信息抽取的项目,需要从网页里提取"中国人民银行的职责",听起来很简单对吧?把文档切成句子,灌进向量数据库,用关键词检索,取topK个最相似的片段。

结果翻车了。

我写了个小工具,把每个切分后的句段和查询关键词的匹配度可视化出来。提取"中国人民银行的编制"时效果不错,匹配度曲线很漂亮。但换成"中国人民银行的职责",就只有开头那句总述"主要职责是:"匹配度比较高,后面紧跟着的(一)(二)(三)……全漏了。

这不是个例。只要文档里有"总述+枚举"这种结构,逐句切分的向量检索就会系统性漏检。这篇文章讲的就是为什么会漏、怎么缓解、目前公认的最优解是什么。内容偏实战,后面有完整的方案对比和排查清单,建议先收藏,等上线RAG系统时能直接对照着用。

先说结论

向量数据库提供的是一个渐进式的匹配度分布,它本质上是"快速召回"的工具,不是"精准截止"的工具。

问题的根源在于:向量相似度是连续的,不存在一条客观的分割线告诉你"到这里就够了"。而枚举条目,比如职责第一条、第二条,它们同总述句之间的语义距离,往往比想象中远得多。单独拿"制定和执行货币政策"和"中国人民银行的职责"来比较,在embedding空间里的余弦相似度,可能还不如一句完全无关但措辞相近的话。

所以截止判断必须由具备理解力的主体来完成——人或者LLM。向量数据库负责缩小范围,LLM负责判断边界。这是ReACT Agent方案背后的逻辑。

为什么逐句切分必然漏检

打个比方。你去图书馆找一本书,管理员把每本书的每一页都拆开,单独编号放到不同抽屉里。你问"关于货币政策的内容在哪?"管理员只能告诉你哪几页提到了"货币政策"这几个字。但如果某一页写的是"(三)维护金融稳定",它跟"货币政策"这个查询在字面和语义上关联都不强,管理员就不会把它递给你——哪怕它就在你要找的那一章里。

回到技术层面,这个问题叫语义孤立(Semantic Isolation)。文档逐句切分后,每个chunk丢失了它所处的上下文。"(三)维护金融稳定"这句话,脱离了前面的"主要职责是:",就变成一个孤立的陈述句,embedding模型无从判断它属于"职责"列表的一部分。

有人会说,把chunk切大一点不就行了?问题是切多大算合适?切太大,检索精度下降;切太小,上下文丢失。这是个工程上的两难,不存在万能的切分粒度。

缓解手段一:滑动窗口与重叠分块

最直觉的修补方案是:切分时让相邻chunk之间保留一段重叠区域。比如每段设定为400 token,前后各重叠50 token。

这种做法能在一定程度上缓解问题,但治标不治本。原因很简单:一个枚举列表包含10条内容,每条大约30 token,整体跨度达到300 token,50 token的重叠区域根本覆盖不到后面那些条目。重叠区域设置得越大,存储和计算开销越高,索引膨胀也越严重。

适用场景:文档结构简单、枚举跨度短、对召回率要求不极端的场景。比如FAQ问答、短文档检索。

不适用场景:长枚举、嵌套结构、法规条文、技术规范文档。

缓解手段二:父子文档检索

LlamaIndex 和 LangChain 都内置了这套方案,思路如下:

  • 预处理阶段做双层切分:大块(Parent Chunk)是整个段落,包含总述和所有枚举内容;小块(Child Chunk)通常是单句
  • 只对小块做 embedding 入库,每个小块带一个指向父块的 ID
  • 检索命中小块后,不返回小块本身,而是把整个父块拉出来

举例:搜索"中国人民银行的职责"命中了总述句(小块),系统返回包含所有枚举条目的完整段落(父块)。

这个方案有一个前提:预处理阶段要能正确识别哪些句子属于同一个父块。HTML 页面可以靠标签结构判断;纯文本则需要语义分块或人工规则。文档结构混乱时,父子关系本身就难以定义。

成本:预处理复杂度中等,存储空间大约翻倍(大块小块都要存),检索时延几乎不增加。

缓解手段三:BM25 混合召回与上下文注入

向量检索擅长语义匹配,但对关键词不敏感。BM25 擅长关键词精确匹配,但不理解语义。两者结合做多路召回,是目前比较标准的做法。

具体操作:

PYTHON

# 伪代码:多路召回 + 分数融合 vector_results = vector_db.search(query_embedding, top_k=20) bm25_results = bm25_index.search(query_keywords, top_k=20) # RRF (Reciprocal Rank Fusion) 融合排序 final_results = rrf_merge(vector_results, bm25_results, k=60)

另一个有效手段是上下文注入(Contextual Retrieval):chunk 入库前,把所属标题、章节名称、父段落首句拼到 chunk 文本前面。这样,"(三)维护金融稳定"变成"[中国人民银行-主要职责] (三)维护金融稳定",embedding 时自带上下文身份信息。

如果你的团队在做 RAG 系统,可以对照自己的实现:是不是只用了单路向量召回?有没有做上下文注入?很多看起来"偶现"的漏检问题,本质上不是偶现——只是触发条件没完全凑齐。

为什么这些手段都不够

前面提到的三种方案——重叠分块、父子文档、混合召回——本质上都是在"入库前"和"检索时"两个阶段做优化。它们能把召回率从 60% 拉到 85% 左右,但要突破 95%,就很难了。

根本原因是:向量相似度是连续分布的数值,没有天然的截止阈值。取 top10,可能漏掉排第 11 的关键信息;取 top50,又会灌入大量噪声。程序自己判断不了"信息够不够"。

这里有个悖论:如果需要先理解文档内容才能正确切分、判断合理边界,那向量数据库本身就多余了。

但反过来想:向量数据库的价值不在于一次给出完美答案,而在于从海量数据中快速缩小搜索范围。真正的判断工作,该交给有理解能力的主体。在 2024-2025 年的技术栈里,这个主体就是 LLM Agent。

最优解:向量数据库 + ReACT Agent 闭环

这套方案是目前业界公认的最佳实践路径,也是从 Cursor 这类纯 RAG 工具演进到 Claude Code 这类 Agent 工具背后的核心逻辑转变。

核心思路:向量数据库负责快速召回候选集合,LLM Agent 通过多轮"观察-思考-行动"(ReACT)循环,自主判断是否需要扩展检索范围。

一个典型的交互轨迹如下:

YAML

Thought 1: 任务是抽取"中国人民银行的职责"先用混合检索看看相关片段。 Action 1: search_chunks(query="中国人民银行 职责", top_k=20, mode="hybrid") Observation 1: 返回了总述句和前3条枚举,但看起来后面还有更多条目。 Thought 2: 当前结果出现了(一)(二)(三)的模式,可能还有(四)(五)。 基于已命中 chunk 的位置,向后扩展上下文。 Action 2: expand_window(chunk_ids=[101,102,103], window=5) Observation 2: 拿到了完整的(一)到(六)全部条款。 Thought 3: 枚举看起来完整了,再做一次验证性搜索确认没有遗漏。 Action 3: search_chunks(query="中国人民银行 负责 承担", top_k=30, exclude=already_found) Observation 3: 没有发现新的职责条目,只有解释性文字。 Final: 抽取完成,共6条职责,写入结构化数据。

这里的关键点:Agent 不是盲目扩大 top_k,而是根据已有结果中的模式——比如发现了编号序列这样的线索——来决定下一步动作。这就是"理解力"介入的地方。

方案对比与选型建议

把上面所有方案放在一起做个对比:

方案召回率提升实现成本token 消耗适用场景
重叠分块无额外FAQ、短文档
父子文档无额外结构清晰的文档
BM25 混合召回无额外关键词明确的场景
上下文注入中高入库时少量通用场景
ReACT Agent运行时消耗高召回率要求、复杂文档

选型时几条原则:

  1. 文档结构规整(HTML、Markdown这类有明确层级的格式),父子文档加BM25混合召回基本够用,不需要再引入Agent
  2. 文档是非结构化纯文本,业务上对漏检零容忍,适合用ReACT Agent
  3. 只做一次性检索、不需要反复查询的场景,直接把全文丢给LLM处理可能更划算
  4. token预算紧张,先把上下文注入和混合召回做好,Agent留作兜底

第3点值得单独说一下:不是所有场景都需要向量数据库。文档总量小、查询次数少的情况下,向量数据库反而多了一个中间层,白白增加运维负担。

实战落地的关键细节

如果你决定用 ReACT Agent 方案,有几个实操中容易忽略的要点:

  1. meta 信息要带位置:每个 chunk 入库时,除了文本内容,还要把它在原文中的绝对位置存进去(比如第几段、第几句)。Agent 靠这个位置信息判断该向前扩展还是向后扩展。
  2. expand_window 工具单独实现:不要让 Agent 每次重新检索,而是提供一个"根据已知 chunk 的位置,向前后扩展 N 句"的工具函数。比重新检索快,也省 token。
  3. 设最大轮次上限:ReACT 循环不能无限跑。通常 3-5 轮够用,超过说明查询本身有问题或数据质量太差。
  4. 用示例向量做枚举召回:如果已经找到了"(一)制定货币政策",拿这条的 embedding 当新查询向量,去检索结构相似的"(二)(三)……"。比直接用原始查询词召回效果好。
  5. DeepSeek 的缓存价格优势:多轮 Agent 交互中,前几轮的 context 会被缓存,后续轮次只为新增 token 付费。实测 5 轮 ReACT 总成本约为单次全文输入的 1.5 到 2 倍,但召回率能从 70% 提到 95% 以上。

什么时候别用向量数据库

前面讲了这么多优化手段,最后聊聊边界问题。以下场景中,向量数据库未必是好选择:

  • 文档总量不到 100 篇,每篇不超过 5000 字:直接把全文丢给 LLM,省掉预处理和检索环节
  • 查询模式是精确匹配(法规编号、文件字号之类):传统数据库的全文索引或正则匹配更靠谱
  • 信息只需要提取一次,后续不会反复查询:向量化的前期投入摊不回来
  • 文档更新极其频繁(实时新闻流这种):向量索引重建的成本可能比收益还高

向量数据库解决的问题是"数据量大的时候,用语义快速缩小检索范围"。如果数据量不大、查询不频繁、或者匹配模式本身就是确定性的,传统方案更简单也更可靠。

技术选型最怕的不是选错,而是不知道自己为什么选。把数据规模、查询频率、召回率要求、token 预算这几个因素摆出来,答案基本就明确了。

带走这张清单

下面是一份可以直接贴到项目文档里的决策清单:

入库前检查:

  • [ ] chunk 是否携带上下文 meta(标题、章节、位置)
  • [ ] 是否做了双层切分(父块 + 子块)
  • [ ] 是否同时建了 BM25 索引
  • [ ] chunk 粒度是否经过可视化验证(建议写一个匹配度可视化工具辅助判断)

检索时检查:

  • [ ] 是否用了多路召回(向量 + BM25)
  • [ ] 是否有融合排序策略(RRF 或加权)
  • [ ] top_k 是否根据文档类型动态调整

Agent 层检查:

  • [ ] 是否提供了 expand_window 工具
  • [ ] 是否设置了最大轮次上限
  • [ ] 是否用已召回结果作为示例向量做二次检索
  • [ ] 是否有最终的完整性验证步骤

成本检查:

  • [ ] 单次查询平均 token 消耗是否在预算内
  • [ ] 是否用了 LLM 缓存机制降低多轮交互成本
  • [ ] 是否评估过直接全文输入的性价比

如果你团队里有人在做 RAG 或知识库项目,上面的清单和方案对比表能省不少踩坑时间。实际项目中遇到过更棘手的召回问题,评论区聊聊你的场景和解法。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询