RAG-6-高级RAG实战案例:自适应路由 + 自评估重写 + 网络回退

一、RAG 存在的问题

1. RAG 流程

RAG(Retrieval-Augmented Generation)将检索与生成结合,基本流程分为两个阶段:

索引阶段(Index Process)
  • 加载文件:读取原始文档(PDF、Word、HTML、Markdown、TXT、CSV 等)

  • 读取文本:提取文档中的文字内容,处理表格、图片等非结构化数据

  • 文本分割:将长文本切分成更小的"块"(chunks)

  • 文本向量化:使用嵌入模型将每个文本块转换为向量表示

  • 存入数据库:将向量与原始文本关联存储到向量数据库(如 FAISS、Chroma、Pinecone)

查询阶段(Query Process)
  • 用户提问:输入自然语言问题

  • 问句向量化:用同一嵌入模型将问题转换为向量

  • 相似度检索:在向量库中匹配最相似的 top k 个文本块

  • 构建 prompt:将检索到的文本块作为上下文,与问题一起填入预定义的 prompt 模板

  • LLM 生成:提交给大语言模型,生成最终答案

2. RAG 痛点分析

索引阶段的故障点
  • 内容缺失(Missing Content)

    知识库中根本没有回答问题所需的信息。此时 RAG 容易"编造"答案,产生幻觉。
    例子:知识库只包含产品说明书,用户问"售后服务电话是多少",库中无此信息,模型可能随便编一个号码。

  • 文档加载准确性和效率

    PDF、图片型扫描件、复杂表格等格式解析困难。
    例子:PDF 中的文字可能是图片的一部分,或表格结构在提取后变成纯文本,丢失行列关系。

  • 文档切分的粒度(Chunking Granularity)

    块太大:包含无关信息,干扰答案,且浪费 token。

    块太小:关键信息可能被切散,答案不完整。
    例子:一个问题的答案需要跨越两个相邻段落,如果切分将它们分开,检索可能只拿到其中一段。

查询阶段的故障点

  • 错过排名靠前的文档(Missed Top Ranked)

    正确答案在知识库中,但其向量相似度排名不高(比如排在第 6 位,而只取 top 5),导致未被召回。

  • 上下文与答案无关(Not in Context)

    检索到的文本块虽然相似,但实际不包含答案,这是前两个问题的直接后果。

  • 格式错误(Wrong Format)

    LLM 没有按要求的格式输出。
    例子:要求返回 JSON {"answer": "..."},模型却返回普通字符串"答案是 42"。

  • 答案不完整(Incomplete)

    答案只回答了问题的一部分,遗漏其他要点。
    例子:问"A 和 B 的区别是什么",只回答了 A 的特点,未提 B。

  • 未提取到答案(Not Extracted)

    检索到的上下文中明明有正确答案,但 LLM 未能从中提取出来。
    原因:上下文过长、信息分散、模型注意力不足等。

  • 答案粒度不当(Incorrect Specificity)

    答案过于具体或过于笼统。
    例子:问"如何修复网络连接问题",答"检查网线"(太具体)或"尝试一些方法"(太笼统)

二、RAG 优化方法

1. 内容缺失

  • 扩充知识库:识别缺失的知识领域,主动添加相关文档或数据源。

  • 数据清洗与增强:去除噪声、统一格式、消除矛盾。例如,使用正则表达式清理特殊符号,或用大模型对冲突信息进行消歧。

  • Prompt 引导拒答:在 prompt 中加入指令:"如果你无法从给定的上下文中找到确切答案,请回答'根据现有知识库,无法回答该问题'。" 这能有效减少胡乱编造。

2. 文档加载准确性和效率

  • 专用文档解析器

    • PDF:使用 pdfplumberPyMuPDF 提取文字,对扫描件配合 OCR(如 Tesseract)。

    • HTML:使用 BeautifulSoup 或 LangChain 的 WebBaseLoader 去除标签、保留正文。

    • 表格:使用 pandas 保留结构,或将每行转为句子描述。

  • 数据预处理流水线:建立统一的清洗流程(去空白、规范化、去除非内容标记等)。

3. 文档切分的粒度

选择合适的策略比固定一种分割方式更重要:

  • 固定长度分块:最简单,直接按 token 数或字符数切分,适合通用场景。

  • 内容重叠分块:相邻块保留重叠部分(如重叠 10%~20%),避免信息被切散到边界。

  • 基于结构的分块 :利用 Markdown 标题、HTML 章节、段落标记,保持逻辑单元完整。例如按 ## 分割。

  • 递归分块 :LangChain 的 RecursiveCharacterTextSplitter 先尝试按段落 \n\n 切分,若块过大则按 \n 再切,再大则按空格、字符依次细分。

  • 分块大小选择因素

    • 嵌入模型偏好:OpenAI text-embedding-ada-002 在 512 tokens 左右效果较好;开源模型可能更短。

    • 文档类型:长文档(书籍、论文)用较大块(800-1200 tokens);短消息、FAQ 用小块(200-300 tokens)。

    • 查询特征:短查询配合小块;长查询、复杂问题配合大块或使用多级检索。

4. 错过排名靠前的文档

  • 过召回 + 重排序(Reranking)

    1. 设置较大的初始召回数量(如 topK=20)。

    2. 使用更精细的重排模型(如 Cohere Rerank、BGE-reranker)对 20 个结果重新计算相关性分数。

    3. 取重排后的前 3~5 个作为最终上下文。
      优点:兼顾效率和精度,避免简单增大 K 带来的噪声。

  • 多路检索:同时使用关键词检索(BM25)和向量检索,合并结果后再重排。

5. 提取上下文与答案无关

该问题通常是"内容缺失"或"错过排名靠前文档"的直接体现,因此优化应回归到:

  • 确保知识库覆盖足够广且质量高。

  • 使用重排序提高召回的准确率。

  • 可增加 查询重写 步骤:将原始问题改写为更精准的多个子查询,分别检索后合并。

6. 格式错误

  • Prompt 工程 :明确要求格式并给出示例(Few-shot)。
    示例:

    text

    复制代码
    请严格以 JSON 格式返回,例如:{"answer": "云计算的英文是 Cloud Computing"}
  • 结构化输出解析器 :使用 LangChain 的 PydanticOutputParser

    • 定义 Pydantic 数据模型(如 class Answer(BaseModel): answer: str)。

    • 解析器自动验证 LLM 输出,若不匹配则尝试修复或重试。

  • 输出后处理:对常见错误(如忘记闭合括号、多了额外文本)用正则或简单逻辑修复。

7. 答案不完整

  • 问题分解

    • 引导用户将复杂问题拆解为多个简单问题。

    • 或自动使用 LLM 将原问题分解成若干子问题,依次检索并收集答案,最后再汇总生成最终回答。

  • 多跳检索(Multi-hop Retrieval):对于需要多个事实才能回答的问题,先检索第一个证据,再用它生成下一个查询,迭代获取完整信息。

8. 未提取到答案

  • 提示压缩技术 :当检索到的上下文很长时,使用 上下文压缩 方法:

    • 先用一个轻量级模型(或 LLM 自身)提取与问题最相关的句子。

    • 或者使用 LLMChainExtractor(LangChain 提供),它会自动识别并保留只有相关内容的压缩文档。

    • 压缩后的 prompt 更短,模型更容易定位答案,同时节省 token。

  • 优化指令:在 prompt 中强调"请基于下面上下文中的原文来回答,不要自己编造"。

9. 答案太具体或太笼统(与幻觉高度相关)

  • 暂无彻底解决方案,这是大模型固有的"幻觉"问题的一种表现形式。

  • 可尝试的缓解措施

    • 使用更大的模型(如 GPT-4 比 GPT-3.5 更稳定)。

    • 增加检索上下文的多样性和冗余度(多个来源支持同一答案)。

    • 加入置信度评分:要求模型同时输出对答案的确信程度。

    • 使用反馈循环:让另一个模型实例评估答案的粒度是否合适,并修正。

三、Advanced RAG

在基础 RAG 基础上,Advanced RAG 通过预检索策略后检索策略提升检索质量,从而更好地解决内容缺失、排名靠后、答案不完整等痛点。

一、预检索策略

1. 优化索引(提高被检索内容的质量)

  • 增强数据颗粒度:合理切分文档,保留完整语义单元。

  • 优化索引结构:如使用层级索引、摘要索引等。

  • 添加元数据:为每个文本块附加时间、来源、标题等信息,便于过滤。

  • 对齐优化:使文本块与常见查询类型更匹配。

  • 混合检索:结合向量检索与关键词检索(如 BM25),兼顾语义与精确匹配。

2. 查询优化(让原始问题更适合检索)

查询重写(Query Rewriting)
  • 目标:不改变意图,仅优化表述清晰度与完整性。

  • 常用技术 :使用 LLM 重写查询。
    示例 Prompt
    "请将以下用户查询改写成更清晰、更完整的句子,同时保持原意不变。用户查询:{query}"

查询转换(Query Transformation)
  • 目标:改变查询形式,使其更易检索。

  • 常用技术

    • 关键词提取:只保留名词、动词等核心词。

    • 步退提示(Step-back Prompting):将复杂问题抽象成更高级的背景问题,先检索背景知识,再结合原问题检索。

    • 子问题分解:将复杂问题拆解为多个简单子问题,分别检索后合并答案(通常需多步检索)。

查询扩展(Query Expansion)
  • 目标:增加相关术语或上下文,丰富查询信息。

  • 常用技术

    • LLM 扩展:让模型生成与原始查询相关的扩展词或短语。

    • 多查询扩展:生成多个不同表述的查询变体,分别检索后合并结果。

二、后检索策略(优化检索结果的集成方式)

1. 重排序(Reranking)

  • 目标:对检索到的文档列表重新排序,将更相关、更准确的文档排在前面。

  • 实现方式:使用专用重排模型(如 Cohere Rerank、BGE-reranker)计算查询与文档之间的相关性得分,按得分排序。

  • 典型流程:先过召回(如 top 20),再重排取前几名(如 top 3)。

2. 摘要(Summary)

  • 目标:生成检索结果的简短摘要,帮助用户快速把握主要内容。

  • 两种方法

    • 抽取式摘要:直接选取原文中的关键句子或短语。

    • 生成式摘要:使用 LLM 重新生成简明的摘要文本。

3. 融合(Fusion)------ 以 RAG-Fusion 为代表

  • 目标:从原始查询生成多个相似但不完全相同的查询变体,覆盖用户意图的多个方面,提升检索结果的全面性与相关性。

  • 典型步骤

    1. 基于原始查询生成多个查询变体。

    2. 对每个变体分别检索。

    3. 融合所有检索结果(如使用倒数排名融合 RRF 或重排合并)。

四、RAG实战

1.背景与痛点

RAG(检索增强生成)是目前大语言模型落地问答系统的标配架构。但朴素 RAG 常常面临一些"翻车"场景:

  • 用户问了一个知识库里没有的问题,模型却自信地胡编乱造;

  • 检索到的文档压根不相关,模型仍然强行生成答案;

  • 一些通用问题(如"今天天气如何")本应联网搜索,却走内部向量库,得到"未找到答案"的尴尬回复。

为此设计了一个具备自适应路由 + 自我纠错 + 网络回退的增强型 RAG 系统。它能像一位经验丰富的客服经理一样:先判断问题归属,再从合适的来源找资料,还能自我评估检索质量,必要时重写查询甚至更换搜索渠道。

基于 LangChain + LangGraph + Chroma + 智谱大模型,完整实现这一系统。

2. 总体架构

系统整体流程图

  • 顶层路由器:使用 LLM 判断问题属于"人事管理"(内部知识库)还是"通用问题"(网络搜索)。

  • 内部检索闭环:检索 → 评估 → 不合格则重写查询 → 重试,最多 2 次。

  • 网络回退:内部检索失败或通用问题直接调用智谱搜索引擎,确保高覆盖率。

  • 自我纠错:评估节点用 LLM 判定文档相关性,避免"垃圾进垃圾出"。

3.完整代码

3.1定义状态和常量

复制代码
from typing import TypedDict, List
from langchain.agents import create_agent
from langchain_community.document_loaders import WebBaseLoader, UnstructuredWordDocumentLoader
from langchain_community.embeddings import ZhipuAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_core.documents import Document
from langchain_core.messages import HumanMessage
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.tools import tool
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langgraph.constants import END
from langgraph.graph import StateGraph
from agent.my_llm import llm, zhipuai_client

#最大重试次数
MAX_RETRY = 2

#自定义状态类
class GraphState(TypedDict):
    question: str  #问题
    generation: str #最总答案
    documents: List[Document] #搜索到的文档
    retry_count: int #重试次数
    next_step: str #下一步节点

3.2构建知识库

  • Word 文档人事管理流程.docx,包含请假、考勤、薪酬等内容

    #加载文档

    web_loader = WebBaseLoader(

    web_paths=('https://news.pku.edu.cn/mtbdnew/12ade13c2ed542fda4ab268ca3bc4524.htm',),

    )

    web_documents = web_loader.load()

    #加载文档
    doc_loader = UnstructuredWordDocumentLoader("./人事管理流程.docx")
    doc_documents = doc_loader.load()

    #切分文档
    text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
    docs = text_splitter.split_documents(doc_documents)

3.3 向量存储

采用 Chroma(轻量级本地向量库),配合智谱的 embedding-2 模型。

复制代码
#embedding 模型
# 智谱 Embedding 配置
embeddings = ZhipuAIEmbeddings(
    api_key="XXX",
    model="embedding-2",   # 智谱的 embedding 模型名
)
vector_store = Chroma.from_documents(
    documents=docs,  # 这里使用您加载并分割后的文档列表
    embedding=embeddings,  # 使用您的智谱嵌入模型
    persist_directory="./chroma_db",  # 本地持久化目录
    collection_name="demo_rag"
)

3.4核心节点

3.4.1检索节点
复制代码
#检索节点
def retrieve(state: GraphState):
    print("----------进入检索节点----------")
    question = state['question']
    documents = vector_store.similarity_search(question, k=3)
    print(f"----------检索到{len(documents)}个文档----------")
    return {"documents": documents, "retry_count": state.get("retry_count", 0) + 1}
3.4.2评估节点
复制代码
grade_prompt = """
您是一个评估检索到的文档与用户问题相关性的评分员。
这个测试不需要非常严格,目标是过滤掉明显错误的检索结果。
如果文档包含与用户问题相关的关键词或语义信息,就判定为相关。
相关则回答'yes' 否则回答'no'
严格输出'yes'或者'no'不要输出其它内容
"""
prompt_template = ChatPromptTemplate.from_messages(
    [
        ("system", grade_prompt),
        ("human", "检索到的文档: \n\n {document} \n\n 用户问题: {question}")
    ]
)
chain = prompt_template | llm | StrOutputParser()
#评估节点
def grade_documents(state: GraphState) -> dict:
    print("----------进入评估节点----------")
    question = state["question"]
    documents = state["documents"]
    filtered_docs = []

    for doc in documents:
        score = chain.invoke(
            {"question": question, "document": doc.page_content}
        )
        if score == 'yes':
            filtered_docs.append(doc)
    print(f"----------评估合格文档数:{len(filtered_docs)}----------")
    return {"documents": filtered_docs}
3.4.3生成节点
复制代码
#生成节点
def generate(state: GraphState) -> dict:
    print("----------进入生成节点----------")
    """节点:基于评估合格的文档生成最终答案"""
    print("---生成节点---")
    question = state["question"]
    documents = state["documents"]

    def format_docs(docs):
        return "\n\n".join(doc.page_content for doc in docs)

    prompt = ChatPromptTemplate.from_messages(
        [
            ("system", "你是一个乐于助人的助手。请严格基于提供的上下文来回答问题。如果无法从上下文中找到答案,请如实告知。"),
            ("human", "上下文: {context}\n\n问题: {question}"),
        ]
    )
    rag_chain = prompt | llm | StrOutputParser()
    generation = rag_chain.invoke({"question": question, "context": format_docs(documents)})
    return {"generation": generation}
3.4.4重写节点
复制代码
#重写节点
def rewrite_query(state: GraphState):
    print("----------进入重写节点----------")
    question = state["question"]

    rewrite_prompt = ChatPromptTemplate.from_messages(
        [
            ("system", "你是一个查询重写助手。你的任务是将用户的问题改写成更适合进行语义检索的版本。"),
            ("human", "原始问题: {question}")
        ]
    )
    rewrite_chain = rewrite_prompt | llm | StrOutputParser()
    better_question = rewrite_chain.invoke({"question": question})
    return {"question": better_question}
3.4.5网络搜索节点
复制代码
@tool
def search_tool(query:str) -> str:
    """网络搜索工具"""
    try:
        response = zhipuai_client.web_search.web_search(search_engine="search_pro", search_query=query)
        if response.search_result:
            return "\n\n".join([result.content for result in response.search_result])
        return "没有搜素到任何内容"
    except Exception as e:
        print(e)
        return "搜索出现错了"

#网络搜索节点
def web_search(state: GraphState) -> dict:
    question = state['question']
    agent = create_agent(
        llm,
        tools=[search_tool],
        system_prompt="你是一个智能助手可以的调用工具回答用户的问题"
    )
    resp = agent.invoke({"messages": [HumanMessage(content=question)]})
    return {"generation": resp['messages'][-1].content}
3.4.6路由分类节点
复制代码
def route_classifier(state: GraphState) -> dict:
    question = state['question']
    classification_prompt = ChatPromptTemplate.from_messages([
        ("system", """你是一个分类器。请判断用户问题是否与"人事管理流程"相关。
    人事管理流程包括:请假、考勤、招聘、入职、离职、培训、绩效考核、薪资福利等人力资源管理相关事务。
    如果问题涉及上述内容,请回答 'hr';否则回答 'general'。
    只输出 'hr' 或 'general',不要输出其他内容。"""),
        ("human", "问题:{question}")
    ])
    classifier_chain = classification_prompt | llm | StrOutputParser()
    decision = classifier_chain.invoke({"question": question}).strip().lower()

    print(f"---路由决策:问题 '{question}' 被分类为 '{decision}'---")
    if decision == "hr":
        return {"next_step": "retrieve"} # 走内部向量检索流程
    else:
        return {"next_step": "web_search"} # 走网络检索节点

3.5工作流组装

复制代码
#条件边
def decide_to_generate(state: GraphState):
    filtered_documents = state["documents"]
    retry_count = state.get("retry_count", 0)

    if not filtered_documents:
        # 没有相关文档
        if retry_count >= MAX_RETRY:
            print(f"---已达到最大重试次数 ({MAX_RETRY}),无法找到答案,转向网路搜素---")
            return "web_search"  # 转向特殊处理节点
        else:
            print(f"---无相关文档,当前重试次数 {retry_count},将进行查询重写---")
            return "rewrite_query"
    else:
        return "generate"

# 4. 构建工作流
workflow = StateGraph(GraphState)

# 添加节点
workflow.add_node("retrieve", retrieve)
workflow.add_node("grade_documents", grade_documents)
workflow.add_node("rewrite_query", rewrite_query)
workflow.add_node("generate", generate)
workflow.add_node("web_search", web_search)
workflow.add_node("route_classifier", route_classifier)
# 设置入口点
workflow.set_entry_point("route_classifier")
# 分类器的条件边
workflow.add_conditional_edges(
    "route_classifier",
    lambda state: state.get("next_step", "retrieve"),  # 实际从分类器返回值决定(我们需要用一个临时变量)
    {
        "retrieve": "retrieve",
        "web_search": "web_search",
    }
)

# 添加边
workflow.add_edge("retrieve", "grade_documents")
workflow.add_conditional_edges(
    "grade_documents",
    decide_to_generate,
    {
        "rewrite_query": "rewrite_query",
        "generate": "generate",
        "web_search": "web_search",  # 新增分支
    },
)
workflow.add_edge("rewrite_query", "retrieve")  # 重写后需重新检索
workflow.add_edge("generate", END)
workflow.add_edge("web_search", END)
# 编译应用
app = workflow.compile()

3.6测试

复制代码
# 假设你的提问是:
final_state = app.invoke({"question": "薪酬组成结构?", "retry_count": 0})
# 观察终端输出,看看系统是否会在第一次检索失败后,重写查询词并再次尝试。
print("最终答案:", final_state["generation"])

# 假设你的提问是:
final_state = app.invoke({"question": "如何请假?", "retry_count": 0})
# 观察终端输出,看看系统是否会在第一次检索失败后,重写查询词并再次尝试。
print("最终答案:", final_state["generation"])
相关推荐
chaofan9802 小时前
AI 时代的云财务管理革命:如何利用聚合平台实现 GPT-5.5 成本的大幅优化与 Token 审计
人工智能·gpt·自动化·api
AI服务老曹2 小时前
架构实战 | 深度解析支持 X86/ARM 异构计算的 AI 视频管理平台:从 GB28181 接入到边缘推流与源码交付
arm开发·人工智能·架构
乐迪信息2 小时前
乐迪信息:精准识别每一艘船:船舶AI类型分类算法技术解析
大数据·人工智能·物联网·安全·目标跟踪·分类·数据挖掘
财经资讯数据_灵砚智能2 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(日间)2026年4月27日
人工智能·python·信息可视化·自然语言处理·ai编程
qq_432703662 小时前
c++怎么在不使用STL的情况下利用Win32 API进行低级文件IO【底层】
jvm·数据库·python
Hody912 小时前
【XR技术介绍】AI快速扫描3D场景技术全景解析:水平、路径与技术选型
人工智能·3d·xr
努力学习_小白2 小时前
DenseNet——Pytorch学习记录
人工智能·pytorch·机器学习·densenet
qq_372906933 小时前
mysql用户无法访问存储过程权限提示_MySQL EXECUTE赋权方案
jvm·数据库·python
biubiubiu07063 小时前
python解释器安装
python