要说清楚 Router、Sub-index 和 多文档 Agent 这三个进阶方向,绕不开一个背景:当 RAG 从“Demo”走向“可用”,检索的形态就不能再是一条流水线打到底。你和团队做知识库问答,早期只有一个全局索引加一个 Top-K 召回,看起来什么都能答,等文档规模上来、业务细分之后,才发现召回质量拉胯、关键词污染严重、跨文档冲突频发,甚至用户问一句“对比一下两版合同差异在哪”,单索引的 RAG 就直接哑火。这篇文章就是针对这些场景的解法拆解。
如果你正在搭企业级知识库、做本地 RAG 工具选型,或者刚读完 LangChain4j、LlamaIndex 的教程但不确定怎么组织多文档检索,这篇文章可以把“路由 + 子索引 + 文档级 Agent”这套进阶编排思路完整讲透,并且带你跑通一个可复现的实战示例。
1. 为什么单个 RAG 很快会撞墙——Router 路由思想的由来
1.1 单一大索引的三大痛点
很多团队的第一版 RAG 都长一个样:把所有文档切片、embedding、写进一个向量数据库,查询来了就全局检索 top-k。这个架构在文档少、主题单一的时候没问题,可一旦扩展到几十个业务域、不同格式的文档、不同时效性的资料,问题会集中爆发。
第一个痛点是**“语义偏移”**。全局索引里如果市场报告占了一半的切片,你问“今年团队扩张计划”这种偏 HR 的问题,召回回来的片段很可能被市场内容淹没,因为向量空间里“增长”“计划”“资源”这类词在两边都有高相似度。你以为在找组织规划,模型却看到一堆市场分析,答案自然偏。
第二个痛点是上下文污染。多文档混合检索时,跟问题语义相近但属于不同主体、不同时间线的片段会同时进上下文窗口,语言模型很难区分“哪条事实属于哪个来源”,轻则答得含糊,重则把 A 合同的条款嫁接到 B 合同上。
第三个痛点更隐蔽:检索策略没法按场景切换。有的问题适合关键词精确匹配,有的适合向量语义召回,有的根本不需要检索外部知识,光靠模型记忆就能答。单一索引只能用一个固定策略服务所有 query,从根上限制了系统的天花板。
1.2 Router 在 RAG 里的角色定位
Router 不是一个搜索引擎,而是一个决策分发层。它的职责是看清楚用户到底需要走哪条检索路径,然后把查询送到最合适的“子索引”或“工具”中。
打个比方,一个企业客服中心如果只有一个总机,所有电话都往一个部门转,效率很糟。Router 的作用就是智能总机,听到“退款”转售后,听到“产品参数”转技术,听到“你们放假吗”转公告查询。RAG 里的 Router 就是这样一个总机,它做的事包括:
- 判断 query 属于哪个业务域,分发到对应子索引
- 判断当前 query 需要向量召回还是关键词匹配
- 判断是否要走多工具编排,比如“先查当前库存,再查物流规则”
- 判断是否需要多文档联合问答,还是单文档精确回答
Router 的实现从轻到重有几种:基于 LLM 的 Function Calling 路由、基于小模型的分类路由、基于规则的硬路由。多数生产系统用“硬路由打底 + LLM 路由兜底”的组合,避免每轮查询都额外消耗大模型推理,也不用承担纯规则无法覆盖的语义变化。
注意:Router 解决的不是“把召回做准”的问题,而是“别用一把尺子量所有问题”的问题。先分流,再谈每个分片内部的精度优化。
2. Sub-index:把知识库切清楚,比建得大更重要
2.1 Sub-index 的划分策略
Sub-index 就是将一个大的知识库按业务维度拆成多个独立的索引单元。每个子索引有独立的 embedding 空间、独立文档集合和独立元数据。用户查询时,Router 先把 query 分到某个或某几个子索引,再在子索引内部做检索。
划分策略要结合知识库的真实业务边界。常见维度有:
按业务域划分,HR、法务、财务、产品文档各一个索引,这是最自然的边界,Router 分类准确率也高。
按文档时序划分,历史归档和当前版本分开,避免旧文档中的过时条款影响当前决策。这个策略适合合同管理、规章制度库。
按内容形态划分,结构化表格、长文本、FAQ 短文本分开索引,因为它们的切片大小和检索方式完全不同。
按安全等级划分,公开资料和权限资料做物理隔离,这不仅是检索效果问题,更是权限控制问题。
划分子索引不是越细越好。划分太碎,Router 误判的成本升高;划分太粗,又回到单索引的问题。我建议一个经验法则:一个子索引的文档数量控制在 2000 到 5000 个切片之间,既保证检索效率,也方便人工维护。
2.2 元数据与映射方案
子索引划分之外,元数据设计同样重要。Sub-index 解决了“去哪找”的问题,元数据解决“找到之后怎么用”的问题。我的习惯是每个子索引强制给每一个 chunk 打上三级标签:
- 来源域(哪个部门/哪类文档)
- 文档标识(哪一份文件的 ID 和标题)
- 时间戳与版本号
有了这些标签,检索返回的每个命中结果都能附带上“我的答案出自哪份文档、哪个章节”,这对需要给出来源引用场景(比如合规问答)是救命的。更进一步,Sub-index 之间还可以建立映射关系。比如多文档 Agent 回答问题“对比方案 A 和方案 B 的预算差异”,就需要先根据主题映射到财务索引和项目索引,再把两个子索引的召回结果送给语言模型合并。
映射的实现有两种常见方式:一种是基于共享元数据字段——两个子索引都带 project_id 字段,Agent 先筛选这个字段;另一种是维护一个轻量的“外部映射表”,记录跨文档关联关系,比如版本链、替代关系。第一种通用性强,第二种灵活度高,实际项目我两种都在用。
3. 多文档 Agent 的实战设计——从单库检索到编排调度
3.1 Agent 怎么在多个索引间决策
多文档 Agent 的核心能力不是“多读了几份文档”,而是能自主决定接下来读什么、怎么读、读到什么程度。它不是一次性把所有子索引问一遍,而是像一个研究员:先看总纲,判断这个问题可能涉及哪些文件,再决定先翻哪份、后翻哪份、哪些只需要扫摘要、哪些要细读全文。
实战中最常用的设计模式是React Agent + 工具注册表。给 Agent 注册一堆检索工具:search_finance_docs()、search_hr_docs()、search_legal_docs()、get_doc_summary(doc_id)。Agent 拿到 query 后,先推理出需要调用哪些工具,然后循环执行“观察结果-再推理-再调用”,最终汇总答案。
这个模式的精妙之处在于:它把 Router 的内置决策升级成了动态多轮决策。Router 通常只做一次分流,而 Agent 可以在第一个工具返回之后就判断“资料还不够,我再去查一下对应版本”,灵活性高得多。
不过,多文档 Agent 也有代价。每轮工具调用都有一次大模型推理开销,回答延迟可能从单次 RAG 的 2 秒变成 10 秒甚至更久。实际应用时一定要加缓存层,同一个 query 的检索结果在短时间内可以直接复用,不要每次重复跑全套 Agent 流程。
3.2 路由 + 子索引 + 多文档 Agent 融合架构
把前两个概念串起来,真正的生产级架构长这样:
用户 query 先经过 Lightweight Router,快速判断问题类型。如果是单域简单问题,直接走对应子索引的 RAG 流程,几秒钟出答案;如果是复杂跨域问题,才交给多文档 Agent,让 Agent 在多个子索引之间动态调度。
这个设计的巧妙之处在于分层成本控制。大多数用户提问其实是单域简单问题,走轻量路径可以省掉大量推理消耗;只有少数需要对比、综合分析的问题才调用重武器。整体系统吞吐和成本都能得到保障。
我做的实际项目里,融合架构的召回准确率比单索引高出明显幅度——一个 3 万份文档的企业知识库,单索引时语义干扰严重,改造成“Router + 8 个子索引 + 多文档 Agent”之后,常见问答准确率提升到可接受水平,复杂的跨文档对比类问题从完全答不了变成能给出有依据的结构化答案。
提示:融合架构不等于“所有问题都先过一遍 Agent”。多文档 Agent 是兜底路径,而不是默认路径。这个设计原则能帮你省下 70% 的不必要模型调用开销。
4. 实操过程与核心环节实现
4.1 环境准备与数据组织
下面用一个最小可行示例演示上述架构。技术栈选择 LlamaIndex + Chroma + OpenAI Embedding,你也可以替换成 LangChain4j 或者 Ollama 本地模型,原理完全一致。
先准备四份虚构业务文档:财务制度、员工手册、产品说明、市场分析。在data/目录下分成四个子文件夹,每个文件夹对应一个子索引。
data/ ├── finance/ ├── hr/ ├── product/ └── marketing/数据准备阶段最容易踩的坑是:子文档混合放内存再统一切分,这样切出来的嵌套结构会导致后续路由元数据丢失。正确做法是按文件夹逐个加载、逐个建索引、逐个持久化,让每个子索引保有独立的内聚结构。
接着写一个基础的本地嵌入脚本,用 SentenceTransformer 跑本地 embedding,避免依赖外部 API:
from llama_index.core import Settings from llama_index.embeddings.huggingface import HuggingFaceEmbedding Settings.embed_model = HuggingFaceEmbedding( model_name="BAAI/bge-large-zh-v1.5" )4.2 构建子索引与元数据绑定
每个子目录独立加载、独立分块、绑定来源元数据。以 HR 文档为例:
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex, StorageContext from llama_index.vector_stores.chroma import ChromaVectorStore import chromadb chroma_client = chromadb.PersistentClient(path="./storage_hr") collection = chroma_client.get_or_create_collection("hr_docs") vector_store = ChromaVectorStore(chroma_collection=collection) storage_context = StorageContext.from_defaults(vector_store=vector_store) documents = SimpleDirectoryReader("data/hr").load_data() # 给每个文档绑定元数据:来源域和文档标识 for doc in documents: doc.metadata["domain"] = "hr" doc.metadata["doc_id"] = doc.metadata["file_name"] hr_index = VectorStoreIndex.from_documents( documents, storage_context=storage_context ) hr_index.storage_context.persist(persist_dir="./storage_hr")其他三个子索引照葫芦画瓢,改一下路径、集合名和 metadata 即可。注意集合名的规范性,后面 Router 要依靠它做映射,命名乱了自己都分不清。
4.3 Router 查询引擎接入
LlamaIndex 提供了内置的RouterQueryEngine,可以注册多个 query engines 并按 query 自动路由:
from llama_index.core.query_engine import RouterQueryEngine from llama_index.core.selectors import LLMSingleSelector finance_engine = finance_index.as_query_engine(similarity_top_k=5) hr_engine = hr_index.as_query_engine(similarity_top_k=5) product_engine = product_index.as_query_engine(similarity_top_k=5) marketing_engine = marketing_index.as_query_engine(similarity_top_k=5) router = RouterQueryEngine( selector=LLMSingleSelector.from_defaults(), query_engine_tools=[ finance_engine.as_tool("finance", "财务制度、报销、预算相关"), hr_engine.as_tool("hr", "员工手册、请假、考勤相关"), product_engine.as_tool("product", "产品参数、功能说明相关"), marketing_engine.as_tool("marketing", "市场分析、营销活动相关"), ], ) response = router.query("员工年假有几天?")这个实现更适合新手建立整体感知,它内部就是“分类器 + 对应引擎”,背后的 Router 机制和你手写 Function Calling 路由没有本质区别,只是封装得更省事。
4.4 多文档 Agent 编排
多文档 Agent 部分用 LlamaIndex 的SubQuestionAnswerEngine做跨文档联合问答。它会把复杂 query 拆解成多个子问题,分配到不同的 query engine,然后汇总:
from llama_index.core.query_engine import SubQuestionAnswerEngine from llama_index.core.tools import QueryEngineTool tools = [ QueryEngineTool.from_defaults(engine=finance_engine, description="财务制度相关"), QueryEngineTool.from_defaults(engine=hr_engine, description="员工手册相关"), QueryEngineTool.from_defaults(engine=product_engine, description="产品说明相关"), QueryEngineTool.from_defaults(engine=marketing_engine, description="市场分析相关"), ] sub_engine = SubQuestionAnswerEngine.from_defaults( query_engine_tools=tools ) response = sub_engine.query("对比一下产品功能和市场宣传点之间的差距")这个查询实际执行时会被拆成“产品功能有哪些”“市场宣传说了什么”“两者差异点在哪”三个子问题,分别检索后再让模型综合。输出会带上引用来源,方便人工核验。
4.5 关键参数设置参考
参数调优是 RAG 实战中花时间最多的一环,我给出常用的预置值做参考:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| chunk_size | 256~512 | 中文场景不建议超过512,超过后语义纯度下降 |
| chunk_overlap | 32~64 | 太小丢失上下文衔接,太大造成冗余 |
| similarity_top_k | 4~8 | 子索引内部召回,不需要一开始就拉几十条 |
| 温度 | 0.1~0.3 | 知识问答追求稳定,温度必须设低 |
| Agent 最大迭代轮数 | 4~6 | 防止 Agent 在多个工具间死循环 |
这些参数不是拍脑袋定的,我有一次做保险合同问答,chunk 从 768 调到 256,精确匹配率提高明显,原因是合同里密密麻麻的条款在长 chunk 里互相干扰了 embedding 表达。建议你按文档类型分别调参,不要全局统一一个值。
5. 常见问题与排查技巧实录
5.1 路由误判怎么办
Router 最大的头疼问题就是误判:财务问题被分到产品索引,或者一句话直接落入兜底分支。我排查时通常先看 selector 的输出日志,确认 LLM 当时到底是怎么理解 query 的。
误判高发场景是跨域查询,比如“产品送修的差旅费怎么报销”,既涉及产品知识又涉及财务规则。单路由只能二选一,必错。解决办法是在 Router 和 Agent 之间再加一层“联合检索规则”:如果 query 包含两个以上子索引的领域关键词,就不走单路分发,直接升级到多文档 Agent。实现上可以写个小规则函数,优先于 LLM 路由执行。
5.2 子索引检索不到内容但文件明明在
大概率是 embedding 模型和分块策略的问题。我遇到过一个场景:PDF 里的表格内容被大类 chunk 吞掉,转成向量后丢失了结构信息,导致“这个产品支持哪些协议”这种表格型问答老是漏召回。
解决办法有两个:一是对表格类文档用更小的分块粒度单独建索引,不要和正文混在一起;二是如果是图片型 PDF 或扫描件,文本抽取层就要做 OCR,很多本地 RAG 工具在这里翻车。检索不到内容,先怀疑文本抽取,别急着调向量参数。
5.3 RAG 知识库能存图片吗
这个问题的标准答案是:传统文本嵌入 RAG 不能直接存图片。你没法绕过图片视觉内容做 embedding,图片本身也不适合进文本索引。但实用场景中有两种扩展方式:
- 图片先 OCR 或走多模态模型生成文本描述,再把描述文本嵌入索引,检索命中后回伸到原图
- 用多模态 embedding 模型(如 CLIP 类)把图片映射到向量空间,单独建多模态索引
效果上第一种对含文字截图类图片非常实用,比如系统报错截图、发票、制度扫描件。我在实际项目中给一个客服知识库做了截图问答,准确率提升明显,代价是多了一个离线 batch 任务,每天定时把新图片转成描述文本再进索引。
5.4 Agent 多轮调度死循环
Agent 在工具之间反复横跳,会拖垮响应延迟和成本。我的调试经验是给每个工具描述里写清楚“适用条件 + 不适用条件”,别只写一个正向描述,例如:
融资租赁相关问题请检索 legal,但报销金额计算不要走 legal 而走 finance。
“负面描述”能大幅减少模型在无法决策时反复调用无关工具的情况。另外,工具数量控制在 5 个以内,超过 5 个,LLM 的调用准确率会明显下滑。架构上学会“先合并同类工具,再考虑细分索引”。
6. 踩坑心得与后续扩展方向
6.1 轻量路由优先,复杂路由兜底
整套架构里,我最想重点说的一点:先写死规则,再让模型接管。早期做 Router 时我直接用 LLM selector,觉得“大模型分类总比规则准”,结果每次查询都多出几百毫秒延迟和额外 token 消耗,而且简单 query 偶尔也会被模型“想太多”而走错分支。后来调整为:能正则匹配的走正则,能关键词命中走关键词,只有语义不明确时才调 LLM 分类。
同样的原则适用于 Agent 的工具选择。把“确定性决策”放在代码层,把“不确定性决策”留给模型,整体效果、成本、可解释性三个维度的收益都会提升。
6.2 元数据驱动是后期迭代的生命线
Sub-index 建好之后,知识库一定会持续增长。没有完善的元数据体系,后期的子索引拆分、合并、淘汰都无从下手。我现在每个文档入库时强制检查三样东西:来源、时间、业务域。漏掉任一个,该文档就不进库。
这个“宁缺毋滥”的原则帮我躲过好几次翻车事故——有一次用户问“去年年终奖政策”,由于旧政策文档没有时间标签,被当作最新政策召回产生误导。给每个 chunk 打上版本号之后,类似问题直接从根上消除了。
6.3 本地化工具链参考
文章里用的 LlamaIndex 示例可以用 Ollama 模型完全本地化运行:embedding 用 bge-large-zh-v1.5,生成用 qwen2.5 或 llama3,向量库换 Chroma 或 Qdrant。这套组合不需要外部网络连接,也不涉及云端 API 调用,特别适合对数据隔离要求高的场景。
我有一次在完全离线环境搭这套,最大的坑是中文文本抽取——有些 PDF 字体编码不标准,通用解析器抽出来是乱码,必须配合 PaddleOCR 这类中文优化的抽取模块。如果你看到召回结果里全是无意义字符串,先查抽取层而不是模型。
6.4 评估体系比模型选型更重要
最后我强烈建议:从第一天起就建立评测集。不要只靠几个样板问题自我感觉良好。准备 100 条覆盖各子索引的典型 query,标注答案和参考文档出处,每次调整 embedding 模型、分块参数或 Router 路由规则后跑一遍,分别记录路由准确率、召回准确率、最终答案准确率三个指标。
我之前在改 chunk_size 的时候没有跑评测集,上线一周后才发现某类长文本问答准确率倒退,回滚后才恢复。建立评测集之后,每次改动都能看到数字变化,做技术决策就不再靠感觉了。
RAG 做到进阶阶段,最有价值的不是某个库的某个 API,而是你对“信息如何被组织、如何被调度、如何被验证”的整体设计能力。Router、Sub-index、多文档 Agent 就是这套设计的三个抓手:先分流再检索,先隔离再融合,先规划再执行。把这三件事做扎实,你的 RAG 系统才真正算得上从能跑进化到能用。