Skip to content

6.7 高级 RAG 技术:多跳检索与自反思

在上一节中,我们学习了 Rerank(重排序)技术——通过交叉编码器对检索结果做精细化的二次排序,把最相关的文档推到列表最前面。Rerank 解决的是"检索质量不够高"的问题,但它仍然假定 RAG 的流程是线性的:检索 → 排序 → 生成,一锤定音。

然而,真实世界的问题远比"查一个文档然后回答"复杂。有些问题需要拆解成多个子问题逐步推理;有些检索结果可能根本不可靠,需要主动纠错;有些知识散落在不同文档之间,需要通过实体关系串联起来。这些场景催生了一系列高级 RAG 技术,它们不再满足于"检索一次就回答",而是引入了多步推理、自我反思和图谱结构,让 RAG 系统更像一个真正的研究助手。

6.7.1 从"图书管理员"到"研究助手"

如果说基础 RAG 像一个图书管理员——你问一个问题,他去书架上找到最相关的几本书递给你,然后让 LLM 照着念——那么高级 RAG 就像一个研究助手

研究助手做深度调研时不会只查一次资料就交差。他会:

  • 拆解问题:面对"Transformer 论文的作者中,谁后来创办了 AI 公司?"这样的问题,他不会指望一本书同时包含论文作者名单和创业信息,而是先查作者名单,再逐个查每个人的后续经历——这就是多跳检索(Multi-hop RAG)
  • 自我审视:查到资料后,他会判断"这些内容真的和问题相关吗?我的回答有没有超出资料支撑的范围?"——这就是自反思 RAG(Self-RAG / CRAG)
  • 梳理关系:面对涉及多方关系的问题,他会画出一张关系图,把人物、机构、事件之间的联系理清楚,而不是零散地翻几个文档——这就是GraphRAG(图检索增强生成)

下面我们逐一展开。

6.7.2 多跳检索(Multi-hop RAG)

核心思想

复杂问题往往无法通过单次检索解决,因为答案取决于多个相互依赖的子问题。多跳检索的核心思路是:将复杂问题分解为一系列子问题,每一步的检索结果为下一步提供线索,就像侦探破案时顺着蛛丝马迹层层追踪。

来看一个具体的例子:

问题: "Transformer 论文的作者中,谁后来创办了 Character.AI?"

Multi-hop 分解:
  第 1 跳: "Transformer 论文的作者有哪些?"
    → 检索得到:[Vaswani, Shazeer, Parmar, Jones, Uszkoreit, 
                  Gomez, Kaiser, Polosukhin]
  
  第 2 跳: "Noam Shazeer 创办了 Character.AI 吗?"
    → 检索得到:"Noam Shazeer 与 Daniel De Freitas 于 2021 年 
                  共同创办了 Character.AI"
  
  最终回答: "Noam Shazeer,他是 Transformer 论文的合著者之一,
            后来与 Daniel De Freitas 共同创办了 Character.AI。"

注意第 1 跳的结果(作者名单)是第 2 跳的前提——没有先知道"Shazeer 是作者",就无法追问"他是否创办了 Character.AI"。这种链式依赖正是多跳检索区别于简单并行检索的关键。

实现代码
python
from langchain_openai import ChatOpenAI
from langchain_chroma import Chroma
from langchain_core.documents import Document
from langchain_openai import OpenAIEmbeddings

# ============================================================
# 多跳检索 RAG 完整实现
# 场景:回答需要跨多个文档推理的复杂问题
# ============================================================

# ---- 第一步:构建知识库 ----
# 准备若干文档,每条文档覆盖一个"知识点"
# 多跳问题的关键:信息分散在不同文档中,单次检索无法覆盖全部
documents = [
    Document(
        # 文档 1:Transformer 论文基本信息——包含完整作者列表
        page_content="Transformer 论文《Attention Is All You Need》发表于 2017 年。"
                     "作者包括:Ashish Vaswani, Noam Shazeer, Niki Parmar, "
                     "Jakob Uszkoreit, Llion Jones, Aidan N. Gomez, "
                     "Lukasz Kaiser, Illia Polosukhin。共 8 位作者。",
        metadata={"id": "transformer_paper"}  # 元数据用于追踪来源
    ),
    Document(
        # 文档 2:Noam Shazeer 的经历——第二跳所需的关键信息
        page_content="Noam Shazeer 是 Transformer 论文的合著者之一。"
                     "他于 2021 年与 Daniel De Freitas 共同创办了 Character.AI。"
                     "Character.AI 是一个 AI 角色扮演平台,允许用户与 AI 角色对话。",
        metadata={"id": "shazeer_bio"}
    ),
    Document(
        # 文档 3:Aidan Gomez 的经历——干扰项,也是有效信息
        page_content="Aidan Gomez 是 Transformer 论文的合著者之一。"
                     "他于 2023 年联合创办了 Cohere,一家专注于企业级 AI 的公司。"
                     "Cohere 提供 Embedding、Rerank 和生成式 AI 服务。",
        metadata={"id": "gomez_bio"}
    ),
    Document(
        # 文档 4:Character.AI 被收购——第三层信息,可做第三跳
        page_content="Character.AI 在 2024 年被 Google 以 25 亿美元收购。"
                     "这次收购使 Google 获得了 Character.AI 的 AI 角色扮演技术。"
                     "Noam Shazeer 回归 Google 继续领导 AI 研究。",
        metadata={"id": "character_ai_acquisition"}
    ),
]

# 创建嵌入模型——将文本转为向量用于相似度搜索
embedding = OpenAIEmbeddings(model="text-embedding-3-small")

# 将文档写入 Chroma 向量数据库,自动完成嵌入和索引
vectorstore = Chroma.from_documents(documents, embedding)

# 创建检索器:每次返回最相关的 3 条文档
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

# ---- 第二步:多跳问答 ----
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)  # temperature=0 保证输出稳定

def decompose_query(query: str) -> list[str]:
    """
    将复杂问题分解为子问题序列。
    
    核心思路:让 LLM 充当"问题拆解器",
    把一个需要多步推理的问题拆成若干可独立检索的子问题。
    子问题之间可能存在依赖关系(后面的问题需要前面的答案)。
    
    参数:
        query: 用户的原始复杂问题
    返回:
        子问题列表,按需要解决的顺序排列
    """
    # 构造提示词:明确要求 LLM 按依赖顺序排列子问题
    prompt = f"""将以下复杂问题分解为 2-4 个子问题,每个子问题一行。
子问题应该按需要解决的顺序排列,后续问题可能依赖前面问题的答案。

问题: {query}

子问题:"""
    # 调用 LLM 进行分解
    response = llm.invoke(prompt)
    # 将 LLM 输出按行拆分,去掉空行和首尾空格
    sub_questions = [q.strip() for q in response.content.strip().split("\n") if q.strip()]
    return sub_questions

def multi_hop_answer(query: str):
    """
    执行完整的多跳检索并回答。
    
    流程:分解问题 → 逐步检索 → 汇总上下文 → 综合回答
    """
    print(f"原始问题: {query}\n")
    
    # 阶段 1:问题分解
    sub_questions = decompose_query(query)
    print("分解为子问题:")
    for i, sq in enumerate(sub_questions):
        print(f"  {i+1}. {sq}")
    
    # 阶段 2:逐步检索(核心循环)
    all_context = []  # 收集每一跳的检索结果
    for i, sub_q in enumerate(sub_questions):
        print(f"\n步骤 {i+1}: {sub_q}")
        # 对每个子问题独立执行向量检索
        docs = retriever.invoke(sub_q)
        # 将检索到的文档拼接为上下文文本
        context = "\n".join([d.page_content for d in docs])
        # 带上步骤标签存入列表,方便后续 LLM 区分各跳信息
        all_context.append(f"[步骤{i+1}检索结果] {context}")
        print(f"  检索到 {len(docs)} 条文档")
        for j, d in enumerate(docs):
            # 打印每条文档的来源 ID 和内容摘要
            print(f"    {j+1}. {d.metadata['id']}: {d.page_content[:60]}...")
    
    # 阶段 3:综合所有检索结果,生成最终答案
    final_prompt = f"""基于以下多步检索结果,回答用户问题。

检索结果:
{chr(10).join(all_context)}

用户问题: {query}

请给出完整、有推理过程的答案:"""
    
    # 将所有跳的上下文一次性喂给 LLM 做最终推理
    answer = llm.invoke(final_prompt)
    print(f"\n最终答案:\n{answer.content}")
    
    return answer

# ---- 第三步:测试 ----
query = "Transformer 论文的作者中,谁后来创办了 AI 公司?这些公司后来怎么样了?"
multi_hop_answer(query)

运行后,LLM 会先拆出"Transformer 论文作者有哪些?"和"这些作者创办了什么公司?"等子问题,然后逐跳检索,最终将所有信息汇总给出完整答案——这正是研究助手做调研的方式。

多跳检索的适用场景

多跳检索并非所有场景都适用。它的代价是多次调用 LLM 和检索器,延迟和成本都会增加。因此,需要判断何时使用:

场景是否需要多跳理由
"Transformer 是什么?"单次检索即可覆盖
"GPT-4 的上下文窗口多大?"单一事实,一次检索足够
"Transformer 论文作者中谁创办了 AI 公司?"需要先查作者列表,再逐个查创业经历
"对比 Chroma 和 Milvus 在百万级向量上的性能?"需要分别检索两个数据库的性能数据,再综合比较

经验法则:如果一个问题需要在脑海中"先查 A,再根据 A 查 B",就应该用多跳检索

6.7.3 自反思 RAG(Self-RAG 与 CRAG)

如果说多跳检索解决的是"问题太复杂"的问题,那么自反思 RAG 解决的是"检索结果不可靠"的问题。它又回到我们的研究助手类比——好的研究助手在查到资料后不会全盘照搬,他会审视:这些资料可信吗?和问题相关吗?我的结论有没有超出资料支撑的范围?

Self-RAG:三道反思关卡

Self-RAG 的核心创新是在生成过程中插入三个反思决策点,让模型自我判断:

传统 RAG:总是检索 → 总是基于检索结果生成
Self-RAG:按需检索 → 评估检索结果 → 选择性使用 → 自我反思

决策 1: 需要检索吗?     → 不需要就直接回答(省时省力)
决策 2: 检索结果相关吗?   → 不相关就忽略检索结果
决策 3: 生成内容有支撑吗? → 没支撑就修正或重新生成

这三个决策点分别对应 Self-RAG 论文中的三个特殊 token:[Retrieve][ISREL][ISSUP]

用户提问


┌─────────────────────────┐
│ 决策 1: 需要检索吗?      │  ← 判断是否需要外部知识
│ (Retrieve Token)         │
└──────────┬──────────────┘

    ┌──────┴──────┐
    │ YES         │ NO
    ▼             ▼
┌──────────────┐  ┌──────────────┐
│ 执行检索      │  │ 直接生成      │
└──────┬───────┘  └──────────────┘


┌─────────────────────────┐
│ 决策 2: 检索结果相关吗?   │  ← 逐条判断检索质量
│ (ISREL Token)            │
└──────────┬──────────────┘

    ┌──────┴──────┐
    │ RELEVANT    │ IRRELEVANT
    ▼             ▼
┌──────────────┐  ┌──────────────┐
│ 基于检索生成   │  │ 忽略检索结果   │
└──────┬───────┘  └──────────────┘


┌─────────────────────────┐
│ 决策 3: 生成内容有支撑吗?  │  ← 判断是否存在幻觉
│ (ISSUP Token)            │
└──────────┬──────────────┘

    ┌──────┴──────┐
    │ FULLY SUP   │ PARTIALLY / NO
    ▼             ▼
┌──────────────┐  ┌──────────────┐
│ 输出答案       │  │ 修正或重新生成  │
└──────────────┘  └──────────────┘

Self-RAG 的三道关卡各自有明确的职责:

  1. 按需检索(决策 1):不是每个问题都需要外部知识。"1+1 等于几?"这种问题直接回答即可,强行检索反而浪费时间和金钱。
  2. 检索质量评估(决策 2):检索器返回了文档不代表文档有用。如果检索结果和问题毫不相关,Self-RAG 会选择忽略它们,避免被噪声误导。
  3. 事实性检查(决策 3):即使基于相关文档生成,LLM 仍可能"添油加醋"。Self-RAG 会检查生成的每个声明是否有文档支撑,减少幻觉。
python
# ============================================================
# Self-RAG 自反思检索增强生成 —— 伪代码示例
# 展示三道反思关卡的核心逻辑
# ============================================================

class SelfRAG:
    """自反思 RAG:在生成过程中插入三道决策关卡"""
    
    def __init__(self, llm, retriever):
        self.llm = llm            # 大语言模型,用于生成和判断
        self.retriever = retriever  # 检索器,用于获取外部知识
    
    def should_retrieve(self, query: str) -> bool:
        """
        决策 1:这个问题需要检索外部知识吗?
        
        策略:让 LLM 判断问题是否涉及它不确定的事实性内容。
        简单的常识问题(如数学计算)不需要检索。
        """
        prompt = f"以下问题是否需要查阅外部资料才能准确回答?只回答 YES 或 NO。\n问题: {query}"
        response = self.llm.invoke(prompt)
        return "YES" in response.content.upper()
    
    def is_relevant(self, query: str, doc: str) -> bool:
        """
        决策 2:这条检索结果和问题相关吗?
        
        策略:让 LLM 逐条评估检索文档与查询的相关性。
        过滤掉不相关的检索结果,避免噪声干扰生成。
        """
        prompt = f"以下文档与问题相关吗?只回答 YES 或 NO。\n问题: {query}\n文档: {doc}"
        response = self.llm.invoke(prompt)
        return "YES" in response.content.upper()
    
    def is_supported(self, answer: str, docs: list[str]) -> bool:
        """
        决策 3:生成的答案有文档支撑吗?
        
        策略:将答案拆分为原子声明,逐一检查是否有文档支撑。
        这是防止幻觉的最后一道防线。
        """
        prompt = f"""以下答案是否完全由给定文档支撑?只回答 YES 或 NO。
文档: {docs}
答案: {answer}"""
        response = self.llm.invoke(prompt)
        return "YES" in response.content.upper()
    
    def generate(self, query: str) -> str:
        """
        完整的 Self-RAG 生成流程:三道关卡串联执行。
        """
        # ---- 决策 1:是否需要检索 ----
        if self.should_retrieve(query):
            # 需要检索:执行向量检索
            docs = self.retriever.invoke(query)
            
            # ---- 决策 2:过滤掉不相关的检索结果 ----
            relevant_docs = [
                d.page_content for d in docs
                if self.is_relevant(query, d.page_content)
            ]
            
            if relevant_docs:
                # 有相关文档:基于它们生成答案
                answer = self.llm.invoke(
                    f"基于以下信息回答问题。\n信息: {relevant_docs}\n问题: {query}"
                ).content
                
                # ---- 决策 3:检查答案是否有文档支撑 ----
                if self.is_supported(answer, relevant_docs):
                    return answer  # 三道关卡全部通过
                else:
                    # 答案缺乏支撑:加约束重新生成
                    return self.llm.invoke(
                        f"仅使用以下信息回答,不要添加额外内容。\n"
                        f"信息: {relevant_docs}\n问题: {query}"
                    ).content
            else:
                # 检索结果全部不相关:放弃检索,直接回答
                return self.llm.invoke(query).content
        else:
            # 不需要检索:直接用 LLM 内部知识回答
            return self.llm.invoke(query).content

注意:原始 Self-RAG 论文通过微调让模型学会输出 [Retrieve][ISREL][ISSUP] 等特殊 token。上面的代码用 LLM prompt 实现了等价逻辑,更易理解,但效率不如微调模型。

Corrective RAG(CRAG):检索质量三档纠错

Self-RAG 的反思是"有或无"的二元判断——要么相关要么不相关,要么有支撑要么没支撑。CRAG(Corrective RAG)则更进一步,引入了置信度评分三档纠错策略,就像研究助手对资料可信度做分档处理:

  • 高置信:检索结果质量很好,直接用——就像找到了权威的一手资料。
  • 中置信:检索结果部分有用,需要提炼——就像资料里夹杂了一些无关内容,需要筛选。
  • 低置信:检索结果基本没用,需要补充外部来源——就像图书馆找不到答案,转而去网上搜索。
CRAG 流程:

检索结果


┌──────────────────────────┐
│ 检索质量评估               │
│ (Confidence Score)        │
└──────────┬───────────────┘

    ┌──────┼──────┐
    ▼      ▼      ▼
  高置信   中置信   低置信
    │      │      │
    ▼      ▼      ▼
  直接   知识精炼   网页搜索
  使用   (提炼有用   (补充外部
        信息)      知识)
    │      │      │
    └──────┴──────┘


       LLM 生成
python
# ============================================================
# Corrective RAG (CRAG) —— 检索质量评估与自动纠错
# 核心创新:三档置信度评分 + 差异化纠错策略
# ============================================================

class CorrectiveRAG:
    """修正型 RAG:评估检索质量,按档位触发不同纠错策略"""
    
    def __init__(self, llm, retriever, web_search_fn):
        self.llm = llm                       # LLM 用于生成和质量评估
        self.retriever = retriever            # 本地向量检索器
        self.web_search_fn = web_search_fn    # 网页搜索函数(低置信时兜底)
    
    def retrieve_and_correct(self, query: str) -> dict:
        """
        完整的 CRAG 流程:检索 → 评估 → 纠错 → 生成。
        
        返回包含答案、执行动作、置信度等信息的结果字典。
        """
        # ---- 第 1 步:执行初始检索 ----
        docs = self.retriever.invoke(query)
        
        # ---- 第 2 步:评估检索结果的质量(核心步骤)----
        confidence = self.evaluate_quality(query, docs)
        
        # ---- 第 3 步:根据置信度选择纠错策略 ----
        if confidence > 0.8:
            # 高置信档:检索结果质量很好,直接使用
            context = [d.page_content for d in docs]
            action = "直接使用"  # 记录执行的动作,便于调试
            
        elif confidence > 0.4:
            # 中置信档:检索结果部分有用,需要知识精炼
            # 知识精炼 = 从检索结果中提取与问题最相关的片段,丢弃噪声
            context = self.knowledge_refinement(query, docs)
            action = "知识精炼"
            
        else:
            # 低置信档:检索结果基本不可用,触发网页搜索补充外部知识
            web_docs = self.web_search_fn(query)
            context = [d.page_content for d in docs] + web_docs
            action = "网页搜索补充"
        
        # ---- 第 4 步:基于最终上下文生成答案 ----
        answer = self.llm.invoke(
            f"基于以下信息回答问题。\n信息: {context}\n问题: {query}"
        ).content
        
        return {
            "answer": answer,          # 最终生成的答案
            "action": action,          # 实际执行的纠错策略
            "confidence": confidence,  # 检索质量置信度
            "context": context         # 最终使用的上下文
        }
    
    def evaluate_quality(self, query: str, docs) -> float:
        """
        评估检索结果的质量,返回 0-1 的置信度分数。
        
        策略:让 LLM 综合判断检索结果与查询的整体相关性。
        分数越高表示检索结果越可信。
        """
        # 拼接所有检索文档的内容
        docs_text = "\n".join([d.page_content for d in docs])
        
        prompt = f"""评估以下检索结果与查询的相关性(0-1分):
查询: {query}
检索结果: {docs_text}
仅返回一个 0 到 1 之间的数字:"""
        
        # 调用 LLM 打分,并做异常处理
        score_str = self.llm.invoke(prompt).content.strip()
        try:
            return float(score_str)
        except ValueError:
            return 0.5  # 解析失败时给中等置信度,触发知识精炼
    
    def knowledge_refinement(self, query: str, docs) -> list[str]:
        """
        知识精炼:从检索结果中提取与查询最相关的信息片段。
        
        当检索结果部分相关时,不做全盘否定,而是提取有用部分。
        这比简单的"保留或丢弃"更精细。
        """
        docs_text = "\n".join([d.page_content for d in docs])
        
        prompt = f"""从以下文档中提取与"{query}"最相关的信息:
文档: {docs_text}
仅提取相关内容,不要添加额外信息:"""
        
        refined = self.llm.invoke(prompt).content
        return [refined]  # 返回提炼后的上下文列表

CRAG 和 Self-RAG 的关键区别在于纠错的粒度:Self-RAG 是"通过/不通过"的二元判断,而 CRAG 引入了中间档位——即使检索结果不够好,也不立即放弃,而是先尝试提炼有用部分。这更接近研究助手真实的工作方式:资料不完美时不会直接扔掉,而是仔细筛选可用的信息。

6.7.4 GraphRAG(图检索增强生成)

核心思想

传统 RAG 把文档视为孤立的文本块,检索时只看文本相似度。但现实世界中,信息之间是有关联的——人物和公司之间有"就职于"的关系,事件之间有"导致"的关系。GraphRAG 的核心思路是:将文档中的实体和关系构建成知识图谱,通过图结构进行检索,捕获文档之间的隐含关联

回到研究助手的类比:普通 RAG 像是在翻一堆零散的资料卡片,而 GraphRAG 则像是研究助手在白板上画了一张关系图——把人物、机构、事件之间的联系全部标出来,一目了然。

传统 RAG:独立文档 → 独立检索 → 拼接上下文
GraphRAG:实体+关系 → 图检索 → 结构化上下文
GraphRAG 架构

GraphRAG 的处理流程分为三个阶段:

文档集合


┌────────────────┐
│ 实体识别与关系   │  提取:人物、地点、概念、事件 + 关系
│ 抽取            │
└───────┬────────┘


┌────────────────┐
│ 知识图谱构建     │  节点:实体(如 Sam Altman、OpenAI)
│                │  边:关系(如 CEO_OF、FOUNDED)
└───────┬────────┘


┌────────────────┐
│ 图检索           │
│                │
│ 查询 → 定位实体  │  先在图中找到相关实体
│   → 遍历邻居    │  沿关系边扩展到相邻实体
│   → 收集子图    │  提取包含完整关系的子图
│   → 图→文本     │  将子图转成自然语言描述
└───────┬────────┘


┌────────────────┐
│ LLM 生成        │  基于结构化上下文生成答案
└────────────────┘
GraphRAG 的优势

用一个具体场景说明传统 RAG 和 GraphRAG 的差异:

场景:查询 "OpenAI 的 CEO 是谁?他之前创办过什么公司?"

传统 RAG:
  检索到文档 A: "Sam Altman 是 OpenAI 的 CEO"
  检索到文档 B: "Sam Altman 之前创办了 Loopt"
  → 两段独立文本,LLM 自行拼接
  → 如果文档 B 没被检索到,就无法回答后半问

GraphRAG:
  实体节点: [Sam Altman] ──CEO_OF──-> [OpenAI]

             ├──FOUNDED──-> [Loopt]

             └──FORMER_PRESIDENT_OF──-> [Y Combinator]
  
  → 结构化图谱,关系清晰
  → 沿图遍历可以发现:Sam Altman 还曾任 Y Combinator 总裁
  → 即使没有直接描述这些关系的文档,也能通过图谱推导出来

传统 RAG 的短板在于:如果"Sam Altman 创办了 Loopt"这句话恰好没被检索到(可能因为文档 B 的嵌入向量和查询不够相似),答案就会缺失。GraphRAG 通过实体间的图结构,只要找到了"Sam Altman"这个节点,就能顺藤摸瓜发现所有关联信息。

GraphRAG 实现代码
python
# ============================================================
# GraphRAG 概念实现(使用 LangChain + Neo4j 图数据库)
# 实际部署需安装 graphrag 包并配置 Neo4j 实例
# ============================================================

from langchain_openai import ChatOpenAI

# ---- 第 1 步:实体与关系抽取 ----
# 使用 LLM 从非结构化文本中提取结构化的实体和关系

llm = ChatOpenAI(model="gpt-4o")  # 用较强模型保证抽取质量

# 定义抽取提示词:要求 LLM 输出结构化 JSON
extraction_prompt = """
从以下文本中提取实体和关系,以 JSON 格式输出:
{
  "entities": [
    {"name": "Sam Altman", "type": "Person"},
    {"name": "OpenAI", "type": "Company"}
  ],
  "relations": [
    {"source": "Sam Altman", "target": "OpenAI", "relation": "CEO_OF"},
    {"source": "Sam Altman", "target": "Loopt", "relation": "FOUNDED"}
  ]
}

文本: {text}
"""

# ---- 第 2 步:构建知识图谱 ----
# 将提取的实体和关系写入图数据库(如 Neo4j)
# 节点 = 实体,边 = 关系

# Cypher 查询示例(Neo4j 的查询语言):
# 创建实体节点
#   CREATE (p:Person {name: 'Sam Altman'})
#   CREATE (c:Company {name: 'OpenAI'})
# 创建关系边
#   CREATE (p)-[:CEO_OF]->(c)

# ---- 第 3 步:图检索 ----
# 面对用户查询,先定位图中的相关实体,再沿关系边遍历收集子图

# Cypher 查询示例:查找 OpenAI 的 CEO 及其创办的公司
#   MATCH (p:Person)-[:CEO_OF]->(c:Company)       -- 找到 CEO
#   WHERE c.name = 'OpenAI'
#   OPTIONAL MATCH (p)-[:FOUNDED]->(s:Company)      -- 找到他创办的公司
#   RETURN p, c, collect(s) as founded_companies

# ---- 第 4 步:图转文本 + 生成 ----
# 将子图结构转换为自然语言描述,作为 LLM 的上下文

def graph_to_text(subgraph: dict) -> str:
    """
    将图查询结果(子图)转换为自然语言描述。
    
    参数:
        subgraph: 包含节点和边的字典
    返回:
        描述子图内容的自然语言文本
    """
    descriptions = []  # 收集每条关系的文字描述
    
    for rel in subgraph.get("relations", []):
        # 将每条三元组(source, relation, target)转为自然语言
        # 例如:("Sam Altman", "CEO_OF", "OpenAI") → "Sam Altman 是 OpenAI 的 CEO"
        desc = f"{rel['source']} {rel['relation']} {rel['target']}"
        descriptions.append(desc)
    
    return "\n".join(descriptions)

# 最终生成:将图转文本的结果作为上下文喂给 LLM
# answer = llm.invoke(f"基于以下信息回答问题。\n信息: {graph_to_text(subgraph)}\n问题: {query}")

实践提示:微软开源的 GraphRAG 框架已经实现了完整的图谱构建和检索流程,支持从原始文档自动构建知识图谱并进行图检索。对于不想从零搭建的团队,可以直接使用该框架。

6.7.5 技术选型:何时用什么

面对这么多高级 RAG 技术,实际项目中该如何选择?下表给出了基于问题复杂度和知识结构特征的选型建议:

场景特征推荐技术理由
简单事实问答基础 RAG + Rerank一次检索足够,无需增加复杂度
需要多步推理的复杂问题Multi-hop RAG问题需要拆解,逐步检索才能回答
检索结果质量不稳定Self-RAG / CRAG需要自动评估检索质量并纠错
知识涉及大量实体关系GraphRAG图结构能捕获文档间的隐含关联
需要灵活调用多种工具Agentic RAGAgent 自主决定检索策略和工具组合

选型原则可以概括为一句话:先用最简单的方案跑通,根据实际问题再逐步引入高级技术。不要一上来就上 GraphRAG + Self-RAG + Multi-hop 的"全家桶"——复杂度会吞噬你。

6.7.6 Agentic RAG:从 RAG 走向 Agent

细心的读者可能已经注意到:前面讨论的所有高级 RAG 技术都在回答"如何更好地检索和生成",但它们仍然是预定义的流水线——Multi-hop 固定做分解-检索-汇总,Self-RAG 固定过三道关卡。如果场景需要更灵活的决策呢?

Agentic RAG 给出了一个方向:把 RAG 系统嵌入 Agent 框架,让 Agent 自主决定何时检索、用什么工具检索、如何组合信息。这就像从"按流程办事的职员"升级为"有自主判断力的研究员"。

Agentic RAG 架构示例:

用户: "对比 GPT-4 和 Claude 3.5 在数学推理上的表现"

Agent 思考过程:
  1. 需要检索 GPT-4 的数学推理能力 → 调用向量检索工具
  2. 需要检索 Claude 3.5 的数学推理能力 → 调用向量检索工具
  3. 需要最新的 Benchmark 数据 → 调用网页搜索工具
  4. 需要计算两者的差距 → 调用计算器工具
  5. 综合所有信息,生成对比报告 → 调用 LLM 生成
python
# ============================================================
# Agentic RAG 概念示例
# Agent 自主决策使用哪些工具来完成 RAG 任务
# ============================================================

from langchain.agents import create_openai_functions_agent
from langchain.tools import Tool

# ---- 定义 Agent 可用的工具集 ----
# 每个工具都是一个可调用的函数 + 描述信息
# Agent 根据 LLM 的判断决定调用哪个工具

tools = [
    Tool(
        name="vector_search",          # 向量检索工具
        func=lambda q: retriever.invoke(q),  # 调用本地向量数据库
        description="搜索内部知识库中的文档"    # 告诉 Agent 这个工具能做什么
    ),
    Tool(
        name="web_search",             # 网页搜索工具
        func=lambda q: web_search(q),  # 调用搜索引擎 API
        description="搜索互联网上的最新信息"   # 当知识库中没有时使用
    ),
    Tool(
        name="calculator",             # 计算器工具
        func=lambda expr: eval(expr),  # 执行数学计算
        description="执行数学计算"        # 需要精确计算时使用
    ),
]

# ---- 创建并运行 Agent ----
# Agent 会根据用户输入自主决定调用哪些工具、按什么顺序调用
agent = create_openai_functions_agent(llm, tools, prompt)

# Agent 会自动编排工具调用:
#   先 vector_search("GPT-4 数学推理")
#   再 vector_search("Claude 3.5 数学推理")
#   再 web_search("GPT-4 Claude 3.5 math benchmark 2024")
#   再 calculator("计算差距")
#   最后综合生成报告
agent.invoke({"input": "对比 GPT-4 和 Claude 3.5 的数学推理能力"})

Agentic RAG 是 RAG 技术演进的终点站——它已经不再是一个单纯的"检索+生成"流水线,而是一个有自主决策能力的智能体。Agent 能根据问题自主选择检索策略、调用多种工具、动态组合信息源,这正是下一章"工具调用"要深入探讨的主题。

6.7.7 常见误区

在实践高级 RAG 技术时,以下误区需要特别注意:

误区一:认为高级 RAG 一定比基础 RAG 好

"既然 Self-RAG 能自我反思,那我所有场景都上 Self-RAG 不就好了?"

并非如此。Self-RAG 的三道反思关卡意味着每个问题要额外调用 3-6 次 LLM,延迟和成本翻倍。如果你的知识库质量高、问题又简单,基础 RAG + Rerank 就够了。高级技术是为了解决特定问题而引入的,不是越多越好

误区二:多跳检索的子问题分解一定准确

"让 LLM 分解问题,它总能给出正确的子问题序列。"

实际上 LLM 经常会分错。比如问题"对比 A 和 B 的性能",LLM 可能只分出"A 的性能是什么"一个子问题,遗漏了 B。实践中需要结合人工验证或增加分解提示的约束。更稳健的做法是让 LLM 同时输出子问题和每个子问题的理由,便于调试。

误区三:GraphRAG 可以替代传统 RAG

"知识图谱这么强,为什么还要传统 RAG?直接全用 GraphRAG。"

GraphRAG 的构建成本很高:需要抽取实体、识别关系、构建图数据库,每次新增文档都要更新图谱。对于以长文本问答为主的场景(如法律文书全文检索),传统 RAG 反而更实用。GraphRAG 适合实体关系密集的场景(如组织架构查询、产品关联分析)。两者通常配合使用:用 GraphRAG 做关系推理,用传统 RAG 做全文检索。

误区四:CRAG 的置信度阈值可以随便设

"高置信 > 0.8,中置信 > 0.4,这俩阈值应该对所有项目通用吧?"

不通用。不同知识库和不同查询类型,LLM 打出的分数分布差异很大。需要用实际数据校准阈值。建议先用一批标注数据统计 LLM 评分的分布,再确定分档线。

6.7.8 本节小结

本节从基础 RAG 的局限性出发,介绍了三种高级 RAG 技术,它们分别从不同维度增强了 RAG 系统的能力:

技术核心思想解决的问题类比
Multi-hop RAG将复杂问题分解为子问题,逐步检索推理问题太复杂,单次检索无法覆盖研究助手逐层追踪线索
Self-RAG三道反思关卡:按需检索、质量评估、事实检查检索结果可能不相关或生成可能有幻觉研究助手审视资料可信度
CRAG三档置信度评分 + 差异化纠错(直接用/精炼/网页搜索)检索结果质量不稳定,需要自适应纠错研究助手对资料分档处理
GraphRAG构建知识图谱,通过图结构检索实体关系信息分散在文档间,传统检索无法关联研究助手画关系图理清脉络
Agentic RAGAgent 自主决策检索策略和工具组合需要灵活调用多种工具和数据源研究助手自主规划调研方案

这些技术的共同趋势是:RAG 正在从"固定流水线"走向"智能体"。多跳检索让 RAG 有了"多步推理"的能力,自反思让 RAG 有了"自我纠错"的能力,Agentic RAG 则让 RAG 有了"自主决策"的能力。当 RAG 系统不仅能检索知识、还能主动调用工具来完成复杂任务时,它就已经跨入了 Agent 的领域。

下一章,我们将正式进入工具调用(Tool Use)——探讨如何让 AI Agent 拥有使用外部工具的能力,从搜索、计算到代码执行,让 LLM 从"能说会道"进化为"能干实事"。RAG 中检索器的调用本身就是一种简单的"工具调用",而 Agentic RAG 中 Agent 自主编排多种工具的行为,正是下一章要深入展开的内容。