首页 / 文章

智能体驱动的RAG:让大语言模型决定何时以及从何处检索信息。

为什么静态RAG在多跳查询和时效性要求高的问题上会失效,以及LangGraph智能体如何在Qdrant与网络搜索之间进行路由,并附相关限制与注意事项。

发布者:REACTAPP.TOP阅读约 9 分钟2167 词

来源: Your RAG Pipeline Is Lying to You. The Problem Is Not the Embeddings — Agentic RAG

“RAG基本上已经是一个解决的问题了。”在团队评审中常会听到这样的说法,而诚实的回答是:这取决于你对检索的理解。任何在实际环境中使用RAG流程处理医疗记录摘要、自然语言转SQL的工作流或多智能体研究系统的人,往往都会遇到同样的瓶颈。

这个瓶颈并非出在嵌入模型上,而在于那种认为检索只需在某个固定步骤中针对单一数据源进行一次的假设。

静态RAG(将文档嵌入,获取前k个结果,再将它们传递给大语言模型)擅长处理基于稳定内部知识的简单、独立的问题。一旦问题变得需要多步推理、对时间敏感,或既需要内部文档又需要实时数据,这种静态流程就会开始产生错误的答案。这样的流程甚至可能根据过时的内部指南推荐临床操作步骤,而实际上只需花半分钟在网络上搜索就能了解到相关变更。

更好的嵌入技术也无法解决这个问题。真正有效的办法是让大语言模型自行决定何时以及从何处获取信息。

“一次检索,永久回答”的弊端

想象一下,有一位图书管理员,他只能在你说完话之前就走到同一个书架前进行回答,而且每次都是同一个书架。

只要询问“二甲双胍的副作用有哪些?”,系统就能立即给出答案。但现在试问一个更难的问题:FDA最近针对二甲双胍与新型GLP-1类药物指出了哪些药物相互作用,现有的患者治疗方案是否涵盖了这些情况?这需要查阅内部文件和实时监管数据,还需分析两者之间的差异。静态RAG仅从单一来源获取信息,而大语言模型则会对收到的内容进行简单处理。

这并非假设情况。在某项针对医疗流程的内部评估中[1],对于简单问题,静态RAG的答案相关性可达89%,但当问题需要将内部临床文档与外部治疗指南相互对照时,这一比例降至61%。初步评估往往因为答案看似合理就予以通过,但实际上这些答案早已过时。

这个解决方案说起来简单,但实现起来却很复杂:信息检索不再是一个固定步骤,而变成了一种决策过程。

代理式RAG的含义

在代理式RAG中,大语言模型不会在预设时间点自动进行信息检索。它将检索视为一种工具,类似于计算器或代码执行器,只有在判断现有证据不足时才会使用它。由此会产生三种静态流程无法实现的行为。

  • 条件性检索。如果上下文已能回答问题,代理就会直接跳过检索步骤。
  • 多轮检索。代理会先获取某些信息并阅读,发现还有缺失的部分后便再次去获取,每轮检索都会受到前一轮结果的制约。这正是人们进行研究的方式。
  • 源路由机制。智能体会根据问题类型在向量存储、网络搜索或结构化数据库之间进行选择。例如,“我们的政策是怎么规定的?”会去向量存储查询,而“FDA上周宣布了什么?”则会通过网络搜索获取信息。
  • 这些理念其实并不新鲜。Yao及其同事在2022年发表的ReAct论文提出了将推理步骤与工具调用相互交织的模式,而智能体RAG则是将ReAct应用到信息检索领域[2]。

    近期的变化在于相关工具的改进。LangGraph终于使得带有分支的状态循环成为可能。Tavily则提供了一种专为智能体设计的网页搜索API,能够返回可直接供大语言模型使用的内容,据称其每月可处理超过1亿次请求,50%请求的搜索延迟为180毫秒[3]。详细信息可在Tavily 101文章中找到。

    三个核心组成部分

    不妨把侦探而非图书管理员放在心上。图书管理员是按指令取书,而侦探则会阅读手头资料,判断缺少什么,再去获取,再次阅读并持续下去,直到能得出可成立的结论。这个流程包含三个组成部分。

    1. 路由器负责确定查询需要何种检索方式。大语言模型会分析问题,判断现有上下文是否足够、是否需要文档、是否需要网络信息,或是三者皆需。
    2. 检索工具就是证据来源。最少需要两种工具:一种用于搜索内部文档的向量存储,另一种用于搜索网络上的实时信息。这两种都是普通函数,大语言模型通过查询字符串来调用它们。
  • 合理性检查旨在判断现有证据是否足够。在给出最终答案之前,智能体会评估所获取的信息是否充分且相关,如果不满足条件,则会再次进行检索。静态处理流程无法实现这种循环机制。
  • 可运行的实现方案

    这是 LangGraph 中的一个混合式智能体,能够在 Qdrant 向量存储与 Tavily 网络搜索之间切换。它会自行选择工具,可以按顺序调用这两种工具,若首个搜索结果无法为答案提供依据,则会再次检索。Qdrant 部分遵循 LangChain 的集成指南。

    from typing import Annotated, Literal
    from langgraph.graph import StateGraph, START, END
    from langgraph.prebuilt import ToolNode
    from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
    from langchain_core.tools import tool
    from langchain_openai import ChatOpenAI
    from langchain_qdrant import QdrantVectorStore
    from langchain_openai import OpenAIEmbeddings
    from tavily import TavilyClient
    from qdrant_client import QdrantClient
    import operator
    from typing import TypedDict, Sequence
    
    # --- State definition ---
    class AgentState(TypedDict):
        messages: Annotated[Sequence, operator.add]
        retrieval_count: int  # guard against infinite loops
    
    # --- Tool 1: Vector store retrieval (internal docs) ---
    qdrant_client = QdrantClient(url="http://localhost:6333")
    embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
    vector_store = QdrantVectorStore(
        client=qdrant_client,
        collection_name="internal_docs",
        embedding=embeddings,
    )
    @tool
    def retrieve_from_docs(query: str) -> str:
        """
        Search internal document knowledge base.
        Use for questions about internal policies, protocols, or proprietary knowledge.
        """
        results = vector_store.similarity_search(query, k=4)
        if not results:
            return "No relevant internal documents found for this query."
        return "\n\n---\n\n".join([
            f"[Source: {doc.metadata.get('source', 'internal')}]\n{doc.page_content}"
            for doc in results
        ])
    
    # --- Tool 2: Web retrieval (live data) ---
    tavily = TavilyClient(api_key="YOUR_TAVILY_API_KEY")
    @tool
    def retrieve_from_web(query: str) -> str:
        """
        Search the live web for current information.
        Use for recent news, regulatory updates, or anything that may have changed
        since the internal knowledge base was last updated.
        """
        results = tavily.search(
            query=query,
            max_results=5,
            search_depth="advanced",  # deep retrieval for better accuracy
        )
        if not results.get("results"):
            return "No relevant web results found."
        return "\n\n---\n\n".join([
            f"[Source: {r['url']}]\n{r['content']}"
            for r in results["results"]
        ])
    
    # --- Agent node ---
    tools = [retrieve_from_docs, retrieve_from_web]
    tool_node = ToolNode(tools)
    llm = ChatOpenAI(model="gpt-4o", temperature=0).bind_tools(tools)
    SYSTEM_PROMPT = """You are a research assistant with access to two retrieval tools:
    - retrieve_from_docs: internal document knowledge base (policies, protocols, proprietary data)
    - retrieve_from_web: live web search (recent news, regulatory updates, real-time information)
    For any question:
    1. Decide whether you need to retrieve information or can answer from context.
    2. If retrieval is needed, choose the right source - or call both if the question spans internal and live knowledge.
    3. After retrieving, check if the evidence is sufficient. If not, retrieve again with a refined query.
    4. Always cite your sources in the final answer.
    Do not hallucinate. If you cannot find sufficient evidence, say so explicitly."""
    def call_agent(state: AgentState) -> AgentState:
        messages = [SystemMessage(content=SYSTEM_PROMPT)] + list(state["messages"])
        response = llm.invoke(messages)
        return {
            "messages": [response],
            "retrieval_count": state.get("retrieval_count", 0)
        }
    def update_retrieval_count(state: AgentState) -> AgentState:
        return {
            "messages": [],
            "retrieval_count": state.get("retrieval_count", 0) + 1
        }
    
    # --- Routing logic ---
    MAX_RETRIEVALS = 3  # prevent infinite loops
    def should_continue(state: AgentState) -> Literal["tools", "end"]:
        last_message = state["messages"][-1]
        # If the LLM called a tool and we haven't hit the loop guard, continue
        if hasattr(last_message, "tool_calls") and last_message.tool_calls:
            if state.get("retrieval_count", 0) < MAX_RETRIEVALS:
                return "tools"
        return "end"
    
    # --- Build the graph ---
    graph_builder = StateGraph(AgentState)
    graph_builder.add_node("agent", call_agent)
    graph_builder.add_node("tools", tool_node)
    graph_builder.add_node("count_retrieval", update_retrieval_count)
    graph_builder.add_edge(START, "agent")
    graph_builder.add_conditional_edges(
        "agent",
        should_continue,
        {"tools": "tools", "end": END}
    )
    graph_builder.add_edge("tools", "count_retrieval")
    graph_builder.add_edge("count_retrieval", "agent")
    agent = graph_builder.compile()
    
    # --- Run it ---
    if __name__ == "__main__":
        result = agent.invoke({
            "messages": [
                HumanMessage(content=(
                    "What does our current patient protocol say about GLP-1 usage, "
                    "and are there any FDA safety updates issued in 2025 we should be aware of?"
                ))
            ],
            "retrieval_count": 0,
        })
        print(result["messages"][-1].content)
    

    有些细节值得注意。

    路由逻辑中的 MAX_RETRIEVALS 限制并非装饰性设置。如果没有它,智能体在面对一个模棱两可的问题时可能会进行多达15次检索,从而浪费资源与时间。通常三次检索就足够了;如果不够,说明问题范围过大或工具返回的结果质量不佳。

    状态中的 retrieval_count 字段使得循环能够被中断。LangGraph的 StateGraph 会在每次节点转换时都保留该值,因此它会在整个对话过程中持续存在,而不仅仅局限于单个循环。

    系统提示同样非常重要。明确告知模型何时使用何种工具,而不仅仅是说明工具的存在,这样才能区分出那些会合理选择工具的智能体与那些出于懒惰而总是调用 retrieve_from_web 的智能体。

    当智能体RAG反而让情况更糟时

    它并非取代静态RAG,而是针对特定问题场景替代后者,但在四种情况下会适得其反。

    • 延迟是主要限制因素。每次循环都会多一次模型调用以及一次工具调用。对于那些要求p95延迟必须控制在1秒以内的基础问答助手而言,可能需要循环两到三次的智能体方案根本不可行。经过良好调优的静态检索器速度更快且已足够使用。
    • 知识库应当稳定且自包含。如果用户提出的所有问题都能从向量存储中准确获取答案,那么引入这种智能体方案只会徒增复杂性。首先使用RAGAS(相关论文)测试静态处理流程,若上下文召回率高于85%且答案准确性高于90%,则使用智能体封装层只会带来额外开销。
  • 无法依赖模型选择的工具。根据相同的内部测试数据,通过精心设计的系统提示词,GPT-4o和Claude Sonnet在约88%的情况下能够正确地在文档检索和网页检索之间切换。剩下的12%则会引发来源错误,这类错误比单纯的幻觉更难调试,因为答案虽然有依据,但出现在了错误的位置。对于性能较弱的模型,则应在部署前就对工具选择进行优化。
  • 实时数据不可靠。网页检索会带来实时攻击面。Tavily在其API中设置了提示词注入过滤机制,但没有任何过滤方法是完美的,因此基于网页内容执行的任何操作都应将该内容视为潜在的威胁。
  • 当知识来源于多个渠道且其中部分内容会发生变化时,应选择代理型RAG。可以先从静态RAG开始,一旦RAGAS评分显示出该设计存在的特定缺陷,再考虑升级。

    从哪里开始

    最小的步骤,耗时不到五分钟:使用 from langchain_community.tools.tavily_search import TavilySearchResults 将一个 Tavily 工具添加到现有的 LangChain 智能体中。此时还不需要 LangGraph。先暴露该工具,编写说明何时调用它的系统提示词,运行五个测试查询,观察它会选择什么结果。

    更高阶的步骤,大约需要一个下午的时间:将上面的 LangGraph 示例指向你自己的向量存储,并保留这两个工具。最初先不添加检索计数器,看看智能体会多频繁地循环,之后再添加它。这种对比能带来很多启示。

    高级模式:在每个工具处理结果之后、智能体下次响应之前插入一个评分步骤。首先通过一个规模更小、成本更低的LLM来判断所获取的上下文是否足够,只有得到肯定答复后,主模型才会生成回复。这一步骤将上下文调用的标准从“测试中足够好”提升到“生产环境中可靠”。

    占比高出12%的工具路由错误是导致生产环境调试时间消耗最多的原因。应尽早对这类错误进行监控,让评分节点在用户发现之前就将其捕获。

    参考资料

    [1] 使用RAGAS 0.1.x对200个精心挑选的临床查询在医疗领域RAG流程上进行的内部评估,不属于已发表的研究。

    [2] Yao等人(2022),《ReAct:语言模型中的推理与行动协同》,arXiv:2210.03629。

    [3] Tavily博客,《Tavily 101》,数据为2026年5月产品页面上标注的延迟与处理量数值。

    相关阅读

  • 在智能体内存测试中grep与向量搜索的对比,以及框架的重要性 —— 对116道LongMemEval题目的研究显示grep的效果优于向量检索,但结果呈现方式对准确率的影响与检索器本身相当。由此可得出哪些结论。
  • 专家级RAG:HyDE、压缩技术、自查询与智能体工作流 —— Haystack平台上用于生产环境RAG的四种高级检索技术:HyDE、上下文压缩、自查询和智能体工作流,以及每种技术的适用场景。
  • 逐层构建:为AI智能体设计真正的RAG检索管道 — 通过SourceDocument和DocumentChunk接口、分块处理、嵌入模型以及ChromaDB来规划RAG管道,所有这些功能都由RetrievalService统一管理,从而保障智能体的稳定性。
  • RAG项目不应是庞大的单个Python文件:可扩展的文件夹结构 — 将演示用的RAG应用拆分为数据导入、分块处理、嵌入生成、向量存储、检索功能、提示词处理、LLM客户端、API接口、测试模块以及日志记录等部分,使其能够超越初期测试阶段并持续发展。
  • 实用笔记:为何您的 RAG 流水线需要重新排序器(以及实现方法 — 详细讲解实用笔记:为何您的 RAG 流水线需要重新排序器(以及实现方法,包括相关契约、检查项以及供采用该模式的团队直接使用的代码片段。