用文档聚类树缩小 Agent 检索范围
先将文档向量递归聚类,并为各节点生成标签和摘要,再让 Agent 自顶向下选择分支,仅在选中的叶节点内检索。文章介绍建树方法、检索循环和失效场景,旨在减少跨主题误召回与上下文消耗。
AI 深度解读
给文档做向量检索,最容易遇到的尴尬是:搜出来的段落用了同样的词,讲的却不是同一件事。原文提出一种检索流程:先把文档整理成带摘要的主题树,再让 Agent 逐层选路,最后到选中的范围里找答案。
它想解决的是大型、多主题语料的检索噪声。思路有吸引力,但成败很依赖目录是否分得合理、摘要能否指对路,以及选错后有没有补救机制。
为什么先选主题,再做检索
普通的扁平向量检索,会让所有文本块与查询计算相关性。在内容跨度很大的知识库里,词汇相近未必代表主题相同。无关结果一旦进入上下文,Agent 就得花 token 阅读和排除。
文档聚类树把这件事拆成两步:先判断问题属于哪些主题,再在这些主题下面检索。Agent 每层只读候选分组的标签和摘要,选择值得展开的分支,不需要看到整个语料库。
作者认为这样有三个好处。首先,上下文更可控:十个同级分组的摘要可能只需几百个 token。其次,限定搜索范围能减少跨主题干扰。最后,系统能留下类似「Sports > Skateboarding」的路径,方便解释结果和排查错误。
但「上下文随树深增长」有前提:每层候选数量、摘要长度和同时保留的路径数都得受控。树分得很宽,或者每次展开很多分支,导航成本照样会上升。
原文引用了 2024 年 1 月的 RAPTOR,即 Recursive Abstractive Processing for Tree-Organized Retrieval。论文摘要报告,RAPTOR 检索结合 GPT-4,在 QuALITY 基准上比此前最佳结果提高了 20 个百分点的绝对准确率。
这项结果属于 RAPTOR,不能直接算成本文 Agent 导航流程的收益。RAPTOR 从树中检索,本文则额外让 Agent 决定打开哪些分支,两者有关联,但不是同一个实验。
树上的摘要,决定 Agent 能不能找到入口
建树分三步。
第一步,为全部文档生成嵌入向量(embedding)。作者要求整个语料使用同一个 embedding 模型,保证各层距离的含义一致。
第二步,递归聚类。先对全集分组,再继续拆分足够大的组,直到组足够小或达到深度限制。大致知道每层需要多少组,可以用 K-means;不确定组数,可以用 HDBSCAN,它允许把离群点标成噪声,不必强行归组。作者还建议先做降维,常见选择是 UMAP,但没有给出不同方案的对照数据。
第三步,从每组抽样文档,让 LLM 生成短标签和一到两句话的摘要。这些文字就是 Agent 选路时看到的入口。
文档存在于树里,不代表 Agent 一定能找到它。
例如,一个父节点同时装着动物和地貌,标签却写成「Species」,有关山脉的问题就可能被挡在门外。因此,作者建议导航时同时展示父节点下可达的叶节点名称,让模型知道里面实际装了什么。
维护成本也从这里开始。新文档可以放到最近的现有分组里,也可能暂时留在树外;内容持续变化,原来的标签会逐渐失真。作者建议按时间、文档数量,或聚类凝聚程度下降、离群点占比上升等信号触发重建,并在每次重建后运行一组已知答案的查询。
这套方法适合有能力维护知识库结构的团队。若文档更新快、主题交叉多,建树之后仍有持续工作。
搜索循环怎么跑,示例代码又省略了什么
作者给 Agent 设计了三个工具:list_children(cluster_id) 返回子节点及标签、摘要,无参数时返回顶层;read_summary(cluster_id) 读取完整摘要;search_within(cluster_id, query) 在指定分组及其后代中做向量或关键词检索。
下面的 Python 示例展示了如何保留多条候选路径,并在证据不足时扩大搜索宽度:
def agentic_search(query, tree, llm_choose, beam=3, max_depth=4, max_hits=20):
frontier = [tree.root] # clusters we are about to open
leaves = []
for _ in range(max_depth):
candidates = []
for node in frontier:
kids = tree.list_children(node)
if not kids:
leaves.append(node) # nothing below: search here
continue
candidates += kids
if not candidates:
break
# The model sees label + summary for each candidate and
# returns the `beam` most promising ones.
frontier = llm_choose(query, candidates, k=beam)
leaves += frontier
hits = []
for leaf in leaves:
hits += tree.search_within(leaf, query)
# Stop when the evidence suffices, otherwise widen the beam and retry.
if enough_evidence(query, hits):
return hits[:max_hits]
if beam >= 9:
return hits[:max_hits]
return agentic_search(query, tree, llm_choose, beam=beam + 2)
重点看最后一行:重试只传了 beam,调用方自定义的 max_depth、max_hits 会恢复默认值。它是流程草图,不能原样当成完整实现。另外,hits[:max_hits] 只是截断,代码没有展示跨分组排序或去重;某些退出路径还可能重复加入叶节点。
默认参数是 beam=3、max_depth=4、max_hits=20。证据不足时,搜索宽度按 3、5、7、9 扩大,到 9 后直接返回已有结果,未必已经回答了问题。llm_choose 和 enough_evidence 的实现都没有展开。
作者还提出,让模型给每个子节点评分,再保留最好的若干条路径;用几何平均聚合路径分数,避免深路径仅因决策次数多而吃亏。早期选错分支时,可以回到候选分数接近的兄弟节点重试。不过,示例没有实现这种回溯,只实现了扩大宽度后重新搜索。
每一层还要支付模型调用成本。作者建议把同层不同路径的候选合并进一次请求,并用小而快的模型做导航。减少无关文本能否抵消这些调用,原文没有给出延迟或费用实测。
我的判断
我认同它对问题的判断:大型混合知识库里,检索失败有时是搜索范围不对,继续往上下文塞段落未必有用。主题路径也提供了比相似度分数更直观的排错线索。
但原文没有提供这套 Agent 循环与扁平检索的直接对照,导航收益、摘要质量和成本方面的建议,仍应当作设计经验。
作者披露自己在 Mixpeek 工作,产品部分需要单独看。按原文转述的文档,Mixpeek 的 cluster_navigation 使用束搜索(beam search),beam_width 默认 3,max_leaf_clusters 控制返回成员的叶节点数量。
关键差别是:它直接返回选中分组的成员文档,不会自动在组内再执行一次向量检索,需要时应串联搜索阶段。结果携带 cluster_path、cluster_path_score,阶段元数据中的 navigation_trace 记录各层候选和概率;文档没有描述回溯能力。
构建侧支持包括 HDBSCAN、K-Means 在内的八种算法、可选 UMAP,以及 hierarchical: true、max_hierarchy_depth。这些配置说明产品能表达这套结构,不能证明它在具体知识库上一定优于平面搜索。
如果现有业务已经有可靠分类,先按分类过滤再检索,同样能缩小范围,也省去自动聚类与摘要维护。是否需要 Agent 选路,应该由实际问题决定。
能拿来做什么
- 给大型技术知识库接入 AI 问答时,先整理一组已知答案的问题,对比全库检索与分组检索,检查选路是否漏掉关键文档,同时记录调用次数和耗时。
- 在 Node 检索服务中保留候选分组、导航路径和命中结果,让错误能追溯到「选错主题」还是「组内没搜到」。
- 采用示例流程时,补齐重试参数传递、结果去重与排序,并为证据不足设置明确返回状态,避免耗尽搜索宽度后仍表现得像已经找到答案。
- 使用 Mixpeek 时,先确认返回分组成员是否满足需求;需要段落级相关性筛选,再串联搜索阶段,并把已知答案查询纳入重建后的检查。