犀牛鸟 2026 研究笔记 estelledc.github.io

Ch03: RAG 技术演进——从朴素管线到 Agent 自主检索

Part 2: RAG 核心技术 前置章节:Ch02: 阅读路线图与前置知识 后续章节:Ch04: 检索三路军——BM25、向量、知识图谱的协奏


1. 开场:从搜索引擎到 RAG,编排权的变迁

结论先行:RAG 的技术演进本质上是一个问题——”谁在编排检索”。从最初的固定代码编排,到可配置管线,再到 LLM 自主决策,六种模式形成一条清晰的演进光谱。理解这条光谱,就理解了 RAG 领域过去两年所有技术创新的内在逻辑。

从 Google 搜索说起

你用 Google 搜索时发生了什么?你输入关键词,Google 用预设的排序算法返回十条蓝色链接,你自己挑哪条有用。整个过程中,”搜索策略”是 Google 的工程师写死的——你输入什么就搜什么,搜出什么就展示什么。你作为人类承担了”判断相关性”和”决定是否需要换个关键词再搜一次”的工作。

RAG 做的事情是把这个过程自动化:用程序替代你做”搜索”,用 LLM 替代你做”阅读和总结”。但一个关键问题浮出水面——”搜索策略”这件事谁来负责?

在最早的 RAG 系统中,答案很简单:工程师。工程师写一段代码,固定了检索方式(向量搜索)、检索数量(Top-5)、拼接方式(直接拼)。这段代码部署后就不会变——不管用户问的是精确的错误码还是模糊的概念性问题,系统都用同一套策略。这就是 Naive RAG。

但人类搜索不是这样的。一个经验丰富的研究员会根据问题类型调整策略:精确问题用关键词搜索,概念问题用语义搜索,复杂问题先拆解再分别搜索,搜不到就换个角度重新搜。他的”搜索策略”是动态的、自适应的。

RAG 的演进,就是让系统的”搜索策略”越来越接近这个研究员。从固定不变(Naive),到前后加优化(Advanced),到模块可替换(Modular),到图谱增强覆盖面(Graph),到 LLM 自主决策(Agentic),到 LLM 做链式推理(Multi-hop)。每一步都在把更多的”编排权”从工程师手中让渡给系统本身。

核心问题:谁在编排检索

理解 RAG 演进的关键,是区分三种编排方式:

第一种:代码固化(code-fixed)。检索策略在代码中写死,运行时不可变。工程师预先决定好一切——用什么检索方式、取多少条、怎么拼接。优点是可预测、可控、延迟稳定;缺点是无法适应多样化的查询。Naive RAG 和部分 Advanced RAG 属于这一类。

第二种:可配置管线(configurable pipeline)。检索策略通过配置文件或可视化界面调整,但运行时仍按预设路径执行。工程师提前定义好多条路径和分支条件,系统按规则走。灵活性更高,但本质上还是”预设策略”——遇到没预设过的情况就束手无策。Modular RAG 和 Graph RAG 属于这一类。

第三种:LLM 自主决策(LLM-autonomous)。LLM 在运行时实时决定检索策略——检索什么、检索几次、用哪个数据源、结果够不够。没有预设路径,每次查询的执行路径可能不同。灵活性最高,但延迟和成本不可预测,调试困难。Agentic RAG 和 Multi-hop RAG 属于这一类。

这三种方式不是简单的”越新越好”。它们形成一个 trade-off 光谱:编排权越多交给 LLM,灵活性越高,但可控性越低、成本越高。选择哪种,取决于你的场景对”灵活性”和”可控性”哪个需求更强。企业内部知识库查询模式固定,固定管线就够了;开放域研究问答千变万化,可能需要 Agentic 模式。

本章结构

本章按演进顺序逐一讲解六种 RAG 模式。每种模式包含:日常类比(建立直觉)、技术机制(数据流和架构)、Python 伪代码(可执行的理解)、优劣势分析(何时选用)、代表项目(实际落地)。最后用对比矩阵总结全局,帮你在具体场景中做出选择。


2. Naive RAG:最朴素的一次检索

结论先行:Naive RAG 是 RAG 的最简形态——一次向量检索,Top-K 拼接,直接生成。它有三个根本性问题(信号单一、无质量把关、无自救能力),后续所有模式都是在逐一解决这三个问题。

日常类比

想象你让一个朋友去图书馆帮你找资料。你告诉他”帮我找关于分布式系统的书”,他走到书架前,按标题看了一遍,抓起前 5 本标题里带”分布式”的书给你。问题是什么?第一,他只按标题找(单信号),如果一本好书标题里没写”分布式”但内容完全相关,他就错过了。第二,他不判断好坏(无质量把关),5 本书里可能有 3 本是过时的入门读物。第三,如果这一趟什么都没找到,他不会换个策略(无自救),只会空手回来说”没有”。

Naive RAG 就是这个朋友。它能跑通,但质量上限很低。

技术机制

Naive RAG 的数据流极其简单,五步完成:

用户输入 query → embedding 模型将 query 转为向量 → 在向量数据库中做余弦相似度检索取 Top-K 个 chunk → 把 query 和 K 个 chunk 拼成 prompt → LLM 基于 prompt 生成答案。

整个管线没有分支、没有循环、没有条件判断。一条直线从输入到输出。

Python 伪代码

def naive_rag(query: str, vector_db, llm, top_k: int = 5) -> str:
    """Naive RAG: 最简单的一次检索 + 生成"""
    # 步骤 1: 查询向量化
    query_vector = embedding_model.encode(query)

    # 步骤 2: 单路向量检索
    chunks = vector_db.similarity_search(query_vector, k=top_k)

    # 步骤 3: 拼接上下文
    context = "\n---\n".join([chunk.text for chunk in chunks])

    # 步骤 4: LLM 生成答案
    prompt = f"""基于以下参考内容回答用户问题。
如果参考内容中没有相关信息,请说明无法回答。

参考内容:
{context}

用户问题:{query}
"""
    answer = llm.generate(prompt)
    return answer

这段代码就是 Naive RAG 的全部。5 行核心逻辑,任何人都能在 10 分钟内跑通一个 Naive RAG 系统。

三个根本问题

问题一:信号单一。向量检索只捕获”语义相似”这一种信号。但用户的查询意图是多样的——有时需要精确匹配(查错误码 “ECONNREFUSED”),有时需要语义理解(查”如何优雅关闭服务”),有时需要结构推理(查”A 依赖了谁”)。单一信号意味着必然有盲区。当用户查 “errno ECONNREFUSED” 时,向量检索可能返回一堆关于”网络连接”的泛泛而谈的文档,而不是精确包含这个错误码的那一篇。

问题二:无质量把关。检索回来的 Top-K 个 chunk 不经任何筛选直接塞进 prompt。如果 Top-5 中有 3 个是低相关甚至无关的(比如恰好包含同一个关键词但讨论的是完全不同的话题),LLM 就要在混杂的上下文中找答案。结果要么产生幻觉(被无关内容误导),要么给出模糊的折中回答(试图兼顾所有 chunk)。

问题三:无自救能力。当检索结果质量差时——关键词不匹配、向量模型不理解领域术语、文档根本没覆盖这个话题——整个管线没有任何纠偏机制。它不会尝试换一种方式重新搜索,不会告诉你”我没找到相关信息所以无法回答”,只会硬着头皮根据低质量上下文编造一个答案。

理解这三个问题至关重要,因为后续每一种模式都是在针对性解决其中某个:Advanced RAG 解决问题二(Rerank 把关),Modular RAG 让解决方案可组合,Graph RAG 解决问题一(增加结构信号),Agentic RAG 解决问题三(Agent 自纠偏)。

代表项目

AnythingLLM 的默认模式就是典型的 Naive RAG:单路向量检索,无 Rerank,无查询改写。早期的 LangChain RetrievalQA chain 和 LlamaIndex 默认 query_engine 也是这种形态。FastGPT 在不配置工作流节点时的 baseline 模式同样属于 Naive RAG。

何时选用 Naive RAG

文档量小于 1000 篇且查询模式单一时;原型验证阶段需要最快跑通时;延迟要求极严(端到端 < 1 秒)时。大多数场景下,Naive RAG 只适合作为起点,不适合作为终点。


3. Advanced RAG:装饰器模式的优化

结论先行:Advanced RAG 不改变管线主干流程,只在检索前后插入优化模块——检索前改查询(Query Rewrite、HyDE),检索后精筛结果(Rerank、过滤)。这和软件工程的装饰器模式一模一样——核心逻辑不变,在前后各加一层增强。投入产出比是所有升级中最高的。

日常类比

还是那个帮你找资料的朋友,但这次你给了他两个助手。第一个助手在他出发前帮忙:把你模糊的需求翻译成精确的书名列表,或者先写一份”我大概想要这样内容的书”的草稿让他带着去比对。第二个助手在他回来后帮忙:从他带回的一摞书里,精挑细选出真正相关的三本给你。同样的图书馆,同样跑一趟,但前后各加一个助手,结果质量天差地别。

这就是装饰器模式的精髓:不改核心流程(跑图书馆),只在前后加增强(出发前准备、回来后筛选)。升级成本极低,效果提升显著。

检索前优化:让查询更精确

检索前优化的核心思想是:用户的原始 query 往往不是最佳检索词。有几种经典策略:

Query Rewriting(查询改写):用 LLM 把用户的口语化表述改写为更适合检索的形式。比如把”它怎么用”(多轮对话中的指代)改写为”Python match 语句怎么用”(补全上下文);把”对比 A 和 B”拆成两个子查询”A 的特点”和”B 的特点”分别检索。

HyDE(Hypothetical Document Embedding,假设文档嵌入):让 LLM 先生成一个”假设性答案”,用这个假答案的向量去检索。为什么有效?因为假答案的文本风格和长度更接近知识库中的真实文档,比用户的短 query 在向量空间中更容易命中相关文档。类比:你问”怎么炒宫保鸡丁”,用这几个字去搜菜谱效果不如先让大厨写一份”可能的做法”再去比对——因为做法的文本和菜谱的文本形态更接近。HyDE 的代价是多一次 LLM 调用(延迟 +500ms 左右,成本翻倍),但对短查询和抽象查询的召回率提升可达 10-20%。

Step-back Prompting(回退提示):把具体问题抽象化后再检索。”Python 3.12 的 match 语法怎么用”→”Python 模式匹配的发展历史和用法”。从具体退到抽象,提高召回面。

检索后优化:让结果更精准

Rerank(精排)是检索后优化的核心。向量检索是”粗排”——用 bi-encoder 快速算出 Top-K,速度够快但精度不够。Rerank 用 cross-encoder 对每个 (query, chunk) 对做精细打分,只留最相关的 Top-N。

为什么 Rerank 几乎总是值得加?因为它的成本极低(通常 < 50ms),但质量提升显著(15-30%)。这是 RAG 领域公认的”性价比之王”。WeKnora 的复合 Rerank 更进一步,用三路加权(0.6 模型分 + 0.3 基础分 + 0.1 来源分)综合判断,是目前最精细的实现之一。

Python 伪代码

def advanced_rag(query: str, llm, vector_db, reranker, 
                 top_k: int = 10, top_n: int = 3) -> str:
    """Advanced RAG: 检索前改查询 + 检索后精排"""

    # === 检索前优化:Query Rewriting ===
    rewrite_prompt = f"将以下问题改写为更适合检索的形式,输出 2-3 个变体:\n{query}"
    rewritten_queries = llm.generate(rewrite_prompt)
    sub_queries = parse_queries(rewritten_queries)  # ["变体1", "变体2"]
    sub_queries.append(query)  # 保留原始查询

    # === 多变体检索 ===
    all_chunks = []
    for sq in sub_queries:
        vec = embedding_model.encode(sq)
        chunks = vector_db.similarity_search(vec, k=top_k)
        all_chunks.extend(chunks)

    # 去重(同一个 chunk 可能被多个变体命中)
    unique_chunks = deduplicate(all_chunks, key=lambda c: c.id)

    # === 检索后优化:Rerank ===
    # cross-encoder 对 (query, chunk) 精细打分
    scored_chunks = []
    for chunk in unique_chunks:
        score = reranker.score(query, chunk.text)
        scored_chunks.append((chunk, score))
    scored_chunks.sort(key=lambda x: x[1], reverse=True)
    top_chunks = [chunk for chunk, score in scored_chunks[:top_n]]

    # === 生成 ===
    context = "\n---\n".join([c.text for c in top_chunks])
    prompt = f"基于以下内容回答问题:\n{context}\n\n问题:{query}"
    answer = llm.generate(prompt)
    return answer

设计哲学:为什么是装饰器

Advanced RAG 的核心设计哲学是”不改变管线主干,只在节点前后插入优化”。这意味着升级成本极低——你的 Naive RAG 代码不需要重构,只需要在检索前加一个函数调用(查询改写),在检索后加一个函数调用(Rerank),质量就能提升三成以上。

但 Advanced RAG 也有天花板:它仍然是固定管线。检索前优化 → 检索 → 检索后优化 → 生成,这个顺序不会变。当查询需要”查完 A 发现不够,再查 B”这种动态决策时,固定管线就无能为力了。

代表项目

Dify 提供可配置的查询改写节点和 Rerank 节点,在 Workflow 中拖拽即可完成 Advanced RAG 配置。RAGFlow 的 DeepDoc 解析器在文档摄入阶段做优化(属于”检索前”的另一种理解——在索引阶段而非查询阶段优化)。WeKnora 的 RRF 融合 + 复合 Rerank 是 Advanced RAG 在检索后优化方面的标杆实现。LangChain 的 MultiQueryRetriever 实现了查询改写。

90% 的生产 RAG 系统应该至少做到 Advanced 级别。从 Naive 升级到 Advanced 的投入产出比远高于后续任何一步升级。


4. Modular RAG:微服务化的检索管线

结论先行:Modular RAG 把 RAG 管线拆成可插拔的独立模块——检索器、融合器、精排器、生成器各自可替换,通过标准接口连接,像乐高积木一样搭建不同场景的 RAG 流程。它不解决新的检索质量问题,但让解决方案可组合、可替换、可独立演化。

日常类比

Naive RAG 是一体机电脑——整合度高,开箱即用,但想换显卡就得整台换。Advanced RAG 是改装过的一体机——你在机箱里加了散热片和 SSD,性能确实好了,但它还是一体的。Modular RAG 是自己组装的台式机——主板、CPU、显卡、硬盘、电源各自独立,今天用 A 品牌显卡,明天换 B 品牌,只要接口对就行。

延伸这个类比:台式机有标准接口(PCIe、SATA、USB),各组件只要遵守接口规范就能即插即用。Modular RAG 也一样——每个模块有标准的输入输出格式(接口契约),只要遵守这个格式,任何实现都可以替换进来。

类比边界:台式机的模块化是物理层面的——拔掉显卡换一块。RAG 的模块化是软件层面的——通过接口抽象和依赖注入实现组件替换。物理模块化有物理约束(尺寸、功耗),软件模块化的约束是接口契约和数据格式。

核心设计原则

Modular RAG 遵循四个原则。单一职责:每个模块只做一件事——检索器只负责检索,Reranker 只负责排序,生成器只负责生成。接口标准化:模块间通过标准协议通信(定义好输入数据结构和输出数据结构),新模块即插即用。配置驱动:模块选择通过配置而非代码——今天用向量检索器,明天换混合检索器,只改配置文件,代码不动。可观测性:每个模块的输入和输出都可追踪——哪一步出了问题一目了然。

这四个原则和微服务架构的设计理念几乎完全一致。如果你理解微服务,就理解了 Modular RAG:服务 → 模块,API 网关 → 管线编排器,服务发现 → 模块注册表,链路追踪 → 模块 I/O 日志,熔断器 → 质量检查点,灰度发布 → 模块 A/B 测试。

WeKnora 的模块化实践

WeKnora 的 EventManager 事件总线是 Modular RAG 编排器的一个实例。它把检索管线拆成了五个独立模块:Indexer(文档摄入 → 分块 → 向量化 → 入库)、Retriever(BM25 + Vector + Graph 三路并行)、Fusion(RRF k=60 融合三路排名)、Reranker(复合 Rerank 三路加权)、Generator(LLM 生成 + 引用溯源)。每个环节都是独立的 Go package,可以单独替换。例如把 Reranker 从 cross-encoder 换成 LLM-based,只需实现 Reranker 接口即可。

Python 伪代码:插件接口

from abc import ABC, abstractmethod
from typing import List
from dataclasses import dataclass

@dataclass
class Chunk:
    """模块间传递的标准数据结构(数据契约)"""
    id: str
    text: str
    score: float = 0.0
    metadata: dict = None

# === 标准接口定义 ===
class Retriever(ABC):
    @abstractmethod
    def retrieve(self, query: str, top_k: int) -> List[Chunk]:
        """输入查询,输出 chunk 列表"""
        pass

class Reranker(ABC):
    @abstractmethod
    def rerank(self, query: str, chunks: List[Chunk], top_n: int) -> List[Chunk]:
        """输入查询和 chunk 列表,输出精排后的 chunk 列表"""
        pass

class Generator(ABC):
    @abstractmethod
    def generate(self, query: str, chunks: List[Chunk]) -> str:
        """输入查询和上下文 chunks,输出答案"""
        pass

# === 模块化管线编排器 ===
class ModularRAGPipeline:
    def __init__(self, retriever: Retriever, reranker: Reranker, 
                 generator: Generator):
        self.retriever = retriever
        self.reranker = reranker
        self.generator = generator

    def run(self, query: str, top_k: int = 10, top_n: int = 3) -> str:
        # 管线编排:每步可替换实现
        chunks = self.retriever.retrieve(query, top_k)
        reranked = self.reranker.rerank(query, chunks, top_n)
        answer = self.generator.generate(query, reranked)
        return answer

# === 配置驱动:不同场景用不同模块组合 ===
# 生产环境:三路混合检索 + 复合 Rerank + GPT-4
production = ModularRAGPipeline(
    retriever=HybridRetriever(bm25_weight=0.3, vector_weight=0.7),
    reranker=CompositeReranker(model=0.6, base=0.3, source=0.1),
    generator=GPT4Generator(temperature=0.1)
)

# 轻量环境:仅向量检索 + 简单 Rerank + 本地模型
lightweight = ModularRAGPipeline(
    retriever=DenseRetriever(model="bge-base"),
    reranker=CrossEncoderReranker(model="bge-reranker-base"),
    generator=LocalLlamaGenerator(model="llama3-8b")
)

两个反模式

实践中常见两个反模式。过度模块化:把每个小功能都拆成模块,导致模块数量爆炸、接口复杂度飙升。正确做法是只在”需要替换或独立优化”的地方拆模块。如果一个函数永远不会被替换,硬编码反而更清晰。忽视数据契约:如果不显式定义模块间的输入输出格式(比如用 Pydantic model 或 JSON Schema),模块替换时就会因为格式不匹配而崩溃。上面代码中的 Chunk dataclass 就是一个最简的数据契约示例。

代表项目

WeKnora 的 EventManager 架构是 Go 语言中 Modular RAG 的典型实现。Dify 的 Workflow 可视化编排让非开发者也能通过拖拽组合模块。FastGPT 的工作流节点提供类似能力。LangChain 的 LCEL(LangChain Expression Language)用链式语法实现模块组合。

何时选用 Modular RAG

当你需要多场景复用同一套 RAG 基础设施时;当团队多人协作、各负责不同模块时;当需要频繁 A/B 测试不同模块组合时。Modular RAG 是工程化的 Advanced RAG——如果你已经在做 Advanced RAG 且模块数量超过 3 个,拆成 Modular 是自然的演进一步。


5. Graph RAG:知识图谱增强检索

结论先行:Graph RAG 在传统检索基础上叠加知识图谱——不只查文档的”内容相似度”,还通过实体关系链查”结构关联性”,让间接相关的内容也能被找到。它解决的是 Naive RAG 的”信号单一”问题——向量检索的盲区恰好是图谱检索的强项,两者互补。

日常类比

传统检索像在图书馆按主题找书——你要”分布式系统”的资料,就去计算机科学区找书脊上写着”分布式”的书。Graph RAG 多了一步:找到书之后,还翻这本书的”参见也”引用、作者的其他著作、同一主题的相关条目。

用社交网络来理解更直观。假设你想找一个懂 Kubernetes 的人帮忙。传统检索是在人才库里搜”Kubernetes”关键词。Graph RAG 是先找到一个 K8s 专家,然后通过他的社交关系发现”他的同事 B 也懂 K8s”,再通过 B 发现”B 的前同事 C 专门做 K8s 调优”。C 的简历里可能根本没写 “Kubernetes”(他写的是”容器编排”),传统检索永远找不到他,但通过关系网能找到。

类比边界:社交网络中”认识”是双向的关系,但知识图谱中关系是有方向和类型的(”依赖于”、”是…的子类”、”负责”等)。图谱的关系链不是简单的”认识→认识”,而是有语义方向的遍历。

三种图谱构建策略

构建知识图谱有三种主流策略:

LLM 驱动抽取(微软 GraphRAG 采用):对每篇文档调用 LLM,让 LLM 识别实体和关系。关系类型丰富(”发明”、”依赖”、”隶属于”等),质量高。代价是成本极高——每篇文档 2-3 次 LLM 调用。适合文档量 < 10K 篇、预算充足的场景。

统计共现抽取(WeKnora 采用):统计实体在文档中的共现频率,用 PMI(点互信息)衡量关联强度。成本极低(无需 LLM 调用),构建速度快。代价是关系类型只有”共现”一种,无法区分”依赖”和”对立”。适合文档量 > 100K 篇、需要快速构建的场景。

混合抽取:先用统计方法快速构建骨架,再用 LLM 对高频实体对做精细关系分类。兼顾成本和质量,但工程复杂度高。

选择建议:文档量一万篇以下且预算允许,用 LLM 驱动;超过十万篇,先用统计共现跑起来,再按需用 LLM 精化关键区域。

图谱检索与向量检索的互补性

Graph RAG 不是要替代向量检索,而是与之互补。两者覆盖的查询类型几乎不重叠:”这个函数怎么用”向量检索强,”A 依赖了哪些组件”图谱检索强,”A 和 B 的共同上级”只有图谱能回答。正因为互补,WeKnora 选择三路全走——向量 + BM25 + 图谱各覆盖不同查询类型,用 RRF 融合后几乎不存在检索盲区。

Python 伪代码

def graph_rag(query: str, vector_db, bm25_index, graph_db, 
              llm, reranker, top_k: int = 5) -> str:
    """Graph RAG: 传统检索 + 图谱遍历 + RRF 融合"""

    # === 步骤 1: 从 query 中抽取实体 ===
    entity_prompt = f"从以下问题中提取关键实体(人名/组织/技术/产品):\n{query}"
    entities = llm.generate(entity_prompt)  # ["微信支付", "技术架构"]
    entities = parse_entity_list(entities)

    # === 步骤 2: 传统双路检索 ===
    vec_chunks = vector_db.similarity_search(embed(query), k=top_k)
    bm25_chunks = bm25_index.search(query, k=top_k)

    # === 步骤 3: 图谱遍历检索 ===
    graph_chunks = []
    for entity in entities:
        # 获取 1 跳邻居(直接关联的实体)
        neighbors = graph_db.get_neighbors(entity, hop=1)
        for neighbor in neighbors:
            # 用邻居实体名去向量库检索相关 chunk
            related = vector_db.search_by_entity(neighbor.name, k=3)
            graph_chunks.extend(related)

    # === 步骤 4: RRF 融合三路结果 ===
    fused = rrf_fuse([vec_chunks, bm25_chunks, graph_chunks], k=60)

    # === 步骤 5: Rerank + 生成 ===
    reranked = reranker.rerank(query, fused, top_n=5)
    context = "\n---\n".join([c.text for c in reranked])

    # 把实体关系也加入上下文(增强可解释性)
    relation_desc = format_relations(entities, graph_db)
    full_context = f"实体关系:\n{relation_desc}\n\n文档内容:\n{context}"

    answer = llm.generate(f"基于以下信息回答:\n{full_context}\n\n问题:{query}")
    return answer

def rrf_fuse(result_lists: list, k: int = 60) -> list:
    """RRF (Reciprocal Rank Fusion) 融合多路排名"""
    scores = {}
    for results in result_lists:
        for rank, chunk in enumerate(results):
            if chunk.id not in scores:
                scores[chunk.id] = {"chunk": chunk, "score": 0}
            scores[chunk.id]["score"] += 1.0 / (k + rank + 1)
    sorted_items = sorted(scores.values(), key=lambda x: x["score"], reverse=True)
    return [item["chunk"] for item in sorted_items]

核心挑战

图谱构建成本高:LLM 抽取实体/关系,每篇文档至少 2 次 LLM 调用。图谱稀疏:小语料库中实体共现不够,图谱边很少,图谱路退化为空结果。实体消歧困难:”苹果”是水果还是公司?”张伟”是哪个张伟?2 跳邻居噪声爆炸:2 跳后邻居数量指数增长,引入大量无关内容——WeKnora 选择只走 1 跳是明智的工程决策。图谱维护是持续工程:文档更新后图谱需要增量更新,否则图谱和文档不一致。

代表项目

WeKnora 的知识图谱路用 PMI 做关系抽取,成本低但粒度粗。微软 GraphRAG 用 LLM 抽取 + Leiden 算法做社区检测 + 社区摘要,适合全局性摘要类查询。RAGFlow 提供实验性的图谱支持。

何时选用 Graph RAG

实体关系密集的领域(医疗、法律、金融、企业知识库);需要多跳推理的查询(”A → B → C”链条式问题);已有知识图谱或有能力构建;文档量大(> 10K 篇),图谱才有足够密度。如果你的文档实体密度低(每篇文档实体少于 3 个)、查询几乎不涉及关系型问题,Graph RAG 的 ROI 很低,不建议投入。


6. Agentic RAG:LLM 自主编排检索

结论先行:Agentic RAG 把检索的”编排权”从固定代码交给 LLM——Agent 在 ReAct 循环中自主决定何时检索、查什么数据源、结果够不够、要不要换策略再查一次。它解决的是 Naive RAG 的”无自救能力”问题——固定管线在检索结果差时只能认命,Agent 可以自纠偏。

日常类比

固定管线 RAG 像流水线工人:按 SOP 操作,拿到原材料就加工,不管原材料质量好坏。给他烂番茄他也做番茄酱,不会说”这批原料不行我换一批”。

Agentic RAG 像资深研究助理:你给他一个研究课题,他会先想”这个问题需要查哪些资料”,决定先查内部知识库;查完第一轮发现数据不全,自主决定再查外部数据库;发现两个来源有矛盾,再查第三个来源做验证;确认信息充分且自洽后才写报告。关键是——没人告诉他要查几轮、查哪里,他自己判断。

类比边界:研究助理的”自主判断”不是无限的。他有时间预算(对应 token 限制和 max_iterations),有能力上限(对应 LLM 推理能力),有工具清单(对应可调用的 API/知识库列表)。超出任何一个约束,判断就会出错。而且他可能判断失误——认为信息”够了”但其实遗漏了关键来源,或者一直判断”不够”陷入无限循环。

ReAct 循环:核心机制

Agentic RAG 的技术核心是 ReAct(Reasoning + Acting)循环。每一轮循环包含三步:Thought(LLM 思考下一步该做什么)→ Action(选择并执行一个工具,如检索、搜索、计算)→ Observation(观察工具执行结果)。循环持续直到 LLM 判断”信息足够了”或达到最大迭代次数。

这个循环的关键创新是:LLM 不仅负责”根据上下文生成答案”(这是所有 RAG 模式都做的),还负责”决定需要什么上下文以及如何获取”。后者是之前固定管线模式中由工程师代码完成的工作。

查询分解是 Agentic RAG 最突出的能力。面对复杂问题”对比 React 和 Vue 的性能”,固定管线只会做一次检索”React Vue 性能对比”。Agent 会思考后决定分别检索”React 性能 benchmark”和”Vue 性能 benchmark”,获取两组数据后再对比生成。面对”这个 bug 可能和哪个模块有关”,Agent 会先检索 bug 症状 → 匹配可能的模块 → 检索这些模块的近期变更 → 综合定位。

Python 伪代码:ReAct 循环

def agentic_rag(query: str, llm, tools: dict, max_iterations: int = 5) -> str:
    """Agentic RAG: LLM 自主决定检索策略的 ReAct 循环"""

    system_prompt = """你是一个研究助理。面对用户问题,你需要:
1. 思考需要什么信息来回答这个问题
2. 选择合适的工具获取信息
3. 判断信息是否足够,不够则继续查
4. 信息足够后生成最终答案

可用工具:
- search_internal_kb(query): 搜索内部知识库
- search_product_docs(query): 搜索产品文档  
- web_search(query): 搜索互联网
- finish(answer): 信息足够,输出最终答案
"""
    scratchpad = []  # 记录 Thought/Action/Observation 历史

    for iteration in range(max_iterations):
        # === Thought: LLM 思考下一步 ===
        think_prompt = f"""{system_prompt}

用户问题:{query}
已有信息:{format_scratchpad(scratchpad)}

请思考下一步应该做什么(Thought),然后选择一个工具执行(Action)。
格式:
Thought: <你的思考>
Action: <tool_name>(<input>)
"""
        response = llm.generate(think_prompt)
        thought, action_name, action_input = parse_react_response(response)
        scratchpad.append(f"Thought {iteration+1}: {thought}")

        # === Action: 执行工具 ===
        if action_name == "finish":
            # Agent 认为信息足够,直接返回答案
            return action_input

        tool = tools[action_name]
        observation = tool.execute(action_input)
        scratchpad.append(f"Action {iteration+1}: {action_name}({action_input})")
        scratchpad.append(f"Observation {iteration+1}: {observation[:500]}")  # 截断防溢出

    # === 超过最大迭代次数,强制生成 ===
    force_prompt = f"""基于已收集的所有信息回答问题。如果信息不完整请说明。

用户问题:{query}
已收集信息:{format_scratchpad(scratchpad)}
"""
    return llm.generate(force_prompt)

三种落地形态

Agentic RAG 在实际落地中有三种程度不同的形态:

形态一”检索即工具”(最轻量):Agent 把 RAG 检索器当作一个工具调用,和 web_search、calculator 并列。Agent 决定”要不要调”和”调几次”,但每次调用的检索策略是固定的。代表实现是 LangChain 的 create_retriever_tool、WeKnora 的 MCP Server。适用场景:已有高质量 RAG 引擎,只需让 Agent 决定何时调用。

形态二”检索策略可调”(中等):Agent 不仅决定要不要检索,还能调整检索参数——top_k、检索路选择、相似度阈值。第一次 broad search 发现结果太散,第二次切换为精确关键词检索。代表实现是 Dify Agent 节点 + 参数传递。

形态三”检索管线可编排”(最重):Agent 动态编排检索管线——先查知识库 A,不够查知识库 B,再不够调 web search,最后用 LLM 判断是否足够。本质上 Agent 成为”检索管线生成器”。代表实现是 LangGraph 动态图。生产环境建议从形态一开始。

成本分析

Agentic RAG 的成本通常是固定管线的 3-6 倍。假设一次 LLM 调用 0.01 元、一次检索 0.001 元、一次 Rerank 0.002 元:固定管线一次查询成本约 0.013 元;Agentic 2 轮循环约 0.036 元(2.8 倍);Agentic 5 轮循环约 0.067 元(5.2 倍)。在成本敏感的生产环境中,需要设置严格的 token 预算和最大迭代次数上限。很多团队采用”分层策略”——简单查询走固定管线(便宜快),复杂查询才触发 Agent 模式(贵慢但准)。

代表项目

Dify 的 Agent 节点模式支持在 Workflow 中配置 Agent 逻辑。LangChain Agent + Retriever Tool 是 Python 生态中最常用的 Agentic RAG 实现。LangGraph 的 ReAct 模板提供更精细的图结构控制。WeKnora 本身是固定管线,但通过 MCP Server 可以被外部 Agent 框架调用——定位是”高质量检索引擎”而非”Agent 平台”。


7. Multi-hop RAG:链式推理的深度检索

结论先行:Multi-hop RAG 是 Agentic RAG 的进阶——Agent 不只做多轮独立检索,而是每轮检索的结果成为下一轮检索的输入线索,形成推理链。它能回答”需要跨越多个知识点才能推导出答案”的复杂问题,但误差会沿推理链级联放大。

日常类比

单跳 Agentic RAG 像研究员查一轮资料就写报告——每轮检索之间相互独立。Multi-hop 像研究员做深度调查:第一轮查到”这个项目的负责人是张三”,第二轮用”张三”作为线索查”张三还负责哪些项目”,第三轮查”那些项目的技术栈”。三轮之后才能回答”这个项目可能和哪些技术栈有关”。

关键区别在于:后一轮的检索词来自前一轮的检索结果,而不是来自原始问题。这意味着每一跳都在远离原始查询的语义空间——第三跳的检索词可能和用户原始问题在文本上毫无相似之处,但逻辑上紧密关联。传统的一次检索完全无法覆盖这种情况。

查询分解示例

用户问题:”修改 UserService 的认证逻辑会影响哪些下游服务?”

这个问题不可能一步检索回答。推理链如下:第一跳,检索”UserService 认证逻辑”→ 了解该服务被哪些模块调用(假设找到 OrderService、PaymentService、NotificationService)。第二跳,提取线索(三个服务名)→ 逐个检索这些服务的认证依赖 → 了解它们如何调用 UserService 的认证接口。第三跳,检索这些服务的外部调用方 → 评估影响范围 → 生成影响分析报告。

每一跳的输出(服务名列表)成为下一跳的输入。如果第一跳遗漏了某个调用方(比如通过反射调用的隐式依赖),后续所有跳都基于不完整的信息,结论就有盲区。

Python 伪代码

def multi_hop_rag(query: str, llm, retriever, max_hops: int = 3) -> str:
    """Multi-hop RAG: 链式推理,每跳结果是下一跳的输入"""

    all_evidence = []       # 累积所有跳的证据
    current_queries = [query]  # 当前跳的检索词(初始 = 原始问题)
    hop_trace = []          # 推理链追踪(可解释性)

    for hop in range(max_hops):
        hop_results = []

        # === 当前跳:执行检索 ===
        for q in current_queries:
            chunks = retriever.retrieve(q, top_k=5)
            hop_results.extend(chunks)

        all_evidence.extend(hop_results)
        hop_trace.append({
            "hop": hop + 1,
            "queries": current_queries,
            "results_count": len(hop_results)
        })

        # === 关键步骤:从结果中提取下一跳线索 ===
        extract_prompt = f"""基于以下检索结果,判断是否需要进一步检索。

原始问题:{query}
当前是第 {hop+1} 跳。
本跳检索结果:
{format_chunks(hop_results)}

如果信息已足够回答原始问题,输出 "SUFFICIENT"。
如果还需要进一步查找,输出需要检索的具体问题(每行一个)。
"""
        next_step = llm.generate(extract_prompt)

        if "SUFFICIENT" in next_step:
            break  # LLM 认为信息已足够

        # 解析出下一跳的检索词
        current_queries = parse_next_queries(next_step)
        if not current_queries:
            break

    # === 综合所有 hop 的证据生成答案 ===
    all_context = "\n---\n".join([e.text for e in all_evidence])
    synthesis_prompt = f"""基于以下所有检索证据回答问题。
请在答案中标注每条信息来自哪一跳检索。

推理链:{format_trace(hop_trace)}

所有证据:
{all_context}

问题:{query}
"""
    answer = llm.generate(synthesis_prompt)
    return answer

误差级联:核心工程挑战

Multi-hop 最核心的问题是误差级联。如果每一跳的准确率是 90%,那么 2 跳的端到端准确率是 81%,3 跳是 73%,4 跳是 66%,5 跳只剩 59%。跳数越多,误差累积越大。

缓解策略有四种。每跳验证:每跳检索后用 LLM 判断结果是否相关,不相关则重试或换方向。多路径并行:同一跳拆成多个子查询并行检索,多数投票取结果,降低单路出错概率。回溯机制:后续跳发现矛盾时,回退到前面跳重新检索。人类在环:关键跳点暂停请人确认,在准确率要求极高的场景(合规、医疗)中使用。

实践建议:如果你的基础检索准确率(单跳)低于 95%,不建议做超过 2 跳的 Multi-hop——误差级联会让结果比不做 Multi-hop 更差。先把单跳质量做到极致,再考虑多跳。

Multi-hop 与 Graph RAG 的关系

两者都能处理”需要间接关联信息”的查询,但机制不同。Graph RAG 是”提前修好高速公路”——关系在图谱构建阶段已经建好,查询时直接遍历,速度快、准确率高。Multi-hop 是”没有高速公路,每步靠导航探索”——实时检索和推理,速度慢但灵活。有高速公路当然更好,但修路(构建图谱)成本高,而且只能到修过路的地方;没修路的地方只有 Multi-hop 能到达。

在实践中两者可以组合:用 Graph RAG 快速覆盖已知的关系链,用 Multi-hop 探索图谱未覆盖的新关联。

何时选用 Multi-hop RAG

查询本质上是多跳推理(链式追溯、影响分析、对比分析);对延迟容忍度高(离线分析、深度研究、代码审计);有完善的每跳验证机制;单跳准确率 > 95%。大多数生产场景不需要 Multi-hop。它在深度研究、代码审计、合规溯源、竞品分析等特定领域有价值。


8. 六种模式对比矩阵

下表从七个维度对比六种 RAG 模式,帮助你在具体场景中做出选择:

模式 编排者 检索次数 典型延迟 工程复杂度 检索质量上限 成本倍率 代表项目
Naive RAG 固定代码 1 < 1s 1x AnythingLLM, FastGPT baseline
Advanced RAG 固定代码 + 优化模块 1-2 1-2s 1.5-2x Dify, RAGFlow, WeKnora baseline
Modular RAG 可组合管线 1-N 1-3s 2x WeKnora EventManager, Dify Workflow
Graph RAG 固定代码 + 图谱引擎 1-2 2-5s 很高 2-3x WeKnora PMI, 微软 GraphRAG
Agentic RAG LLM 自主 1-N(不确定) 3-15s 很高 3-6x Dify Agent, LangChain Agent
Multi-hop RAG LLM 自主 + 推理链 N(逐跳递增) 10-30s 很高 极高(理论) 5-10x LangGraph, IRCoT

选择决策树

面对一个具体场景,按以下顺序判断:

第一步,查询模式是否确定且单一?是 → Naive 或 Advanced 足够。否 → 继续判断。

第二步,是否需要模块独立演化或 A/B 测试?是 → Modular。否 → 继续。

第三步,领域是否实体关系密集?是 → 加 Graph 路。否 → 跳过。

第四步,查询是否高度多样、无法预设策略?是 → Agentic。否 → 停留 Modular。

第五步,查询是否需要链式推理?是且单跳准确率 > 95% → Multi-hop。否 → 停留 Agentic。

演进不是替代

需要强调的是:这六种模式不是互相替代的关系,而是层层叠加的关系。一个成熟的 RAG 系统可以同时包含多种模式——用 Modular 架构组织代码,内部包含 Advanced 级别的 Rerank,检索路中包含 Graph,对外通过 MCP 暴露给 Agentic 框架调用。WeKnora 的当前状态就是 Advanced + Modular + Graph 的组合,同时通过 MCP Server 支持被 Agent 调用。

渐进升级路径

从 Naive 升级到高级模式不是一步到位。推荐的渐进路径是:先加 Rerank(+15-30% 质量,1 天工作量)→ 再加 Query Rewriting(+10-20% 长尾查询)→ 再加 BM25 双路(+20-40% 精确查询)→ 拆成 Modular 架构(工程可维护性)→ 按需加 Graph 路或 Agentic 编排。

每一步都有明确的 ROI,不需要一次性跳到最复杂的模式。90% 的场景在 Advanced + Modular 级别就能满足需求。


9. 本章小结

回到开头的核心问题:”谁在编排检索?”

六种 RAG 模式的演进,本质上是编排权从工程师逐步让渡给 LLM 的过程。Naive RAG 中工程师在代码中写死一切;Advanced RAG 中工程师在前后加优化但主干不变;Modular RAG 中工程师把组件拆开但编排逻辑仍固定;Graph RAG 中工程师预先构建知识网络供检索使用;Agentic RAG 中 LLM 开始接管”查什么、查几次”的决策;Multi-hop RAG 中 LLM 进一步接管”推理链方向和深度”的决策。

越往后,LLM 的决策权越大,系统越灵活,但可控性越低、成本越高。选择哪种模式不是”越新越好”,而是”越匹配场景越好”。企业知识库查询模式固定——Advanced + Graph 就是最优解。开放域研究问答——需要 Agentic。大多数生产场景落在中间,取”半 Agentic”策略:简单查询走固定管线,复杂查询触发 Agent。

另一个重要结论是:检索质量的基础在于单跳质量。无论用哪种高级模式,如果底层的向量检索质量差、Rerank 不到位、文档分块不合理,上层的 Agent 编排再精妙也救不回来。所以正确的投入顺序是——先把单跳质量做到极致(Advanced RAG 的 Rerank + Query Rewriting),再考虑架构升级(Modular → Graph → Agentic)。

下一章我们深入检索层本身——BM25 关键词检索、向量语义检索、知识图谱结构检索,三种检索信号各自的原理、优势和互补关系。这是所有 RAG 模式共享的基础层,无论管线多复杂,最终都要落到”一次高质量的检索”上。


参考来源:本章内容基于 Agentic RAG 模式研究笔记 整理,结合 WeKnora、Dify、RAGFlow、LangChain 等项目的实际实现。


导读目录:README.md 上一章:Ch02: 阅读路线图与前置知识 下一章:Ch04: 检索三路军——BM25、向量、知识图谱的协奏