解析 ‘Agentic RAG’:为什么让 Agent 自主决定检索次数比一轮强制检索(Naïve RAG)效果好得多?

各位同仁,各位技术爱好者,大家下午好!

今天,我们齐聚一堂,共同探讨一个当前LLM(大型语言模型)领域炙手可热的话题:Agentic RAG。随着LLM能力的飞速发展,我们对它们寄予了厚望,希望它们能成为我们工作和生活中的强大助手。然而,我们很快发现,即便最先进的LLM,也存在知识截止日期、幻觉(hallucination)以及难以处理复杂、多步骤任务的局限性。

为了解决这些问题,RAG(Retrieval Augmented Generation,检索增强生成)应运而生。它旨在将LLM的强大生成能力与外部知识库的准确性和时效性相结合。然而,RAG本身也在不断演进。今天,我们将深入剖析RAG的两种主要形态:Naïve RAG(朴素RAG)Agentic RAG(智能体RAG),并重点阐述为什么让智能体自主决定检索次数,远比一轮强制检索的效果要好得多。

Naïve RAG:基础与局限

首先,让我们从最基础的RAG形态——Naïve RAG开始。理解它的工作原理和固有缺陷,是理解Agentic RAG优越性的基石。

Naïve RAG 的工作原理

Naïve RAG 的核心思想非常直接:当用户提出一个问题时,我们首先从一个预先准备好的外部知识库中检索出与问题最相关的文本片段(或称“块”,chunk),然后将这些检索到的片段与原始问题一起作为上下文,提交给LLM,让LLM基于这些信息生成回答。

其基本流程可以用以下步骤概括:

  1. 知识库准备(Indexing)
    • 将大量的文本数据(如文档、网页、数据库记录等)切分成更小的、语义连贯的块。
    • 为每个文本块生成一个向量嵌入(vector embedding)。
    • 将这些向量嵌入存储在一个向量数据库中(如FAISS, Chroma, Pinecone等),以便快速检索。
  2. 用户查询(User Query)
    • 用户提出一个问题或请求。
  3. 查询嵌入(Query Embedding)
    • 使用与知识库准备阶段相同的嵌入模型,将用户查询也转换成一个向量嵌入。
  4. 相似性检索(Similarity Search)
    • 在向量数据库中,查找与用户查询向量最相似的 k 个文本块。这些文本块被认为是与用户问题最相关的上下文。这里的 k 是一个预设的超参数,例如 k=3k=5
  5. 上下文构建(Context Augmentation)
    • 将检索到的 k 个文本块拼接起来,形成一个完整的上下文。
    • 将这个上下文与原始用户查询一起,构建成一个最终的Prompt。
    • 典型的Prompt结构可能如下:“请基于以下提供的上下文信息,回答我的问题。上下文:[检索到的文本1] [检索到的文本2] … 问题:[用户问题]”。
  6. LLM 生成(LLM Generation)
    • 将构建好的Prompt发送给LLM。
    • LLM利用其强大的语言理解和生成能力,结合提供的上下文,生成最终的答案。

Naïve RAG 的代码示例

为了更好地理解,让我们通过一个简单的Python代码示例来演示Naïve RAG的实现。我们将使用LangChain这个流行的LLM应用开发框架,以及OpenAI作为LLM提供商和嵌入模型。

首先,确保你安装了必要的库:

pip install langchain langchain-openai pypdf faiss-cpu

然后,准备一个简单的文档集合,并构建向量数据库:

import os
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import FAISS
from langchain.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

# 设置你的OpenAI API Key
# os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_API_KEY"

def prepare_vector_store(file_path="example_docs.pdf"):
    """
    加载文档,切分并创建FAISS向量存储。
    在实际应用中,file_path可以是多个文件或一个目录。
    这里为了演示,我们假设有一个PDF文件。
    """
    if not os.path.exists(file_path):
        print(f"创建虚拟PDF文件: {file_path}")
        # 创建一个虚拟的PDF文件用于演示
        from fpdf import FPDF
        pdf = FPDF()
        pdf.add_page()
        pdf.set_font("Arial", size=12)
        pdf.multi_cell(0, 10, "Python asyncio is a library to write concurrent code using the async/await syntax. It's a fundamental part of modern Python for handling I/O-bound and high-level structured network code. It uses an event loop to manage tasks and handle non-blocking operations. When an I/O operation (like reading from a network socket or file) is initiated, asyncio doesn't block the entire program; instead, it switches to another task until the I/O operation is complete. This is achieved through coroutines and the `await` keyword, which yields control back to the event loop. The event loop then schedules other ready tasks. Once the I/O operation finishes, the event loop resumes the waiting coroutine.")
        pdf.add_page()
        pdf.set_font("Arial", size=12)
        pdf.multi_cell(0, 10, "Node.js is a JavaScript runtime built on Chrome's V8 JavaScript engine. It uses an event-driven, non-blocking I/O model that makes it lightweight and efficient. Node.js's event loop is implemented using the `libuv` library, which handles asynchronous I/O operations across different platforms. When an I/O operation is requested in Node.js, it's offloaded to `libuv`'s thread pool, and control immediately returns to the main JavaScript thread. Once the I/O operation completes, a callback function is placed in the event queue, and the event loop processes it in a non-blocking manner. This allows Node.js to handle many concurrent connections with a single thread.")
        pdf.add_page()
        pdf.set_font("Arial", size=12)
        pdf.multi_cell(0, 10, "Comparing Python's asyncio and Node.js's event loop reveals architectural differences. Asyncio's `async/await` syntax provides explicit control over yielding, making it more akin to cooperative multitasking within a single thread. Node.js, while appearing single-threaded to the developer, uses `libuv` internally for thread pooling to manage heavy I/O tasks, abstracting away much of the low-level concurrency. Both are highly effective for I/O-bound applications, but their underlying mechanisms for achieving non-blocking behavior differ. Python's approach is more explicit in its asynchronous flow, while Node.js's is often perceived as more 'fire-and-forget' with callbacks and promises.")
        pdf.output(file_path)

    loader = PyPDFLoader(file_path)
    documents = loader.load()

    text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
    splits = text_splitter.split_documents(documents)

    embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")
    vector_store = FAISS.from_documents(splits, embeddings)
    return vector_store

# 初始化向量存储
vector_store = prepare_vector_store()
retriever = vector_store.as_retriever(search_kwargs={"k": 3}) # 固定检索 k=3

# 初始化LLM
llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0.0)

# 定义Prompt模板
template = """请根据以下提供的上下文信息,简洁、准确地回答问题。
如果你不知道答案,请说明你不知道。

上下文:
{context}

问题:
{question}
"""
prompt = ChatPromptTemplate.from_template(template)

# 构建Naïve RAG链
naive_rag_chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

# 运行Naïve RAG示例
print("--- Naïve RAG 示例 ---")
query_naive = "Python的asyncio和Node.js的事件循环在处理I/O密集型操作时有什么主要区别?"
response_naive = naive_rag_chain.invoke(query_naive)
print(f"用户问题: {query_naive}n")
print(f"Naïve RAG 回答:n{response_naive}n")

query_naive_simple = "什么是Python asyncio?"
response_naive_simple = naive_rag_chain.invoke(query_naive_simple)
print(f"用户问题: {query_naive_simple}n")
print(f"Naïve RAG 回答:n{response_naive_simple}n")

query_naive_unrelated = "解释一下量子力学的基本原理。"
response_naive_unrelated = naive_rag_chain.invoke(query_naive_unrelated)
print(f"用户问题: {query_naive_unrelated}n")
print(f"Naïve RAG 回答:n{response_naive_unrelated}n")

Naïve RAG 的固有缺陷

尽管Naïve RAG为LLM带来了外部知识,但其固有的“一轮强制检索”机制,使其在面对复杂场景时显得力不从心。主要缺陷包括:

  1. 固定 k 的局限性(Fixed k Limitation)
    • k 是一个预设值。对于简单问题,k 值过大可能引入无关信息,稀释有效上下文;对于复杂问题,k 值过小可能无法获取足够的信息。
    • 在实际应用中,很难找到一个“万能”的 k 值适用于所有查询。
  2. 单一检索轮次(Single-Shot Retrieval)
    • Naïve RAG 假设一次检索就能找到所有所需的信息。这对于多跳(multi-hop)问题(需要从多个文档中提取信息并进行推理的问题)或需要逐步细化信息的问题是无效的。
    • 例如,如果问题是“A公司的产品X在市场上的表现如何?它的主要竞争对手是谁?”。一次检索可能只找到产品X的市场表现,而没有提及竞争对手,或者反之。
  3. 上下文窗口瓶颈(Context Window Bottleneck)
    • LLM的上下文窗口是有限的。如果 k 值设置过大,检索到的文本块可能超过LLM的上下文限制。
    • 即使未超出限制,过多的无关信息也会“稀释”有效信息,增加LLM处理的难度,甚至导致LLM对重要信息的关注度下降(“lost in the middle”现象)。
  4. 查询的模糊性与复杂性(Query Ambiguity and Complexity)
    • 用户原始查询可能模糊不清,或者包含多个子问题。单一的向量相似性搜索很难准确捕捉这些复杂性。
    • 例如,“最近的AI发展”这个查询,可能需要检索关于不同AI子领域(如CV, NLP, RL)的最新进展。一次性搜索很难涵盖所有方面。
  5. 缺乏对检索结果的推理和细化(Lack of Reasoning and Refinement)
    • LLM在Naïve RAG中仅仅是上下文的“消费者”。它无法主动评估检索到的信息是否足够、是否相关,也无法根据检索结果进一步调整搜索策略。
    • 它无法说:“我需要更多关于X的信息”或者“这些信息似乎不够准确,我需要换个关键词再搜一次”。
  6. 召回率与准确率的权衡(Recall vs. Precision Trade-off)
    • 为了提高召回率(找到所有相关信息),我们可能需要增加 k 值,但这往往会牺牲准确率(引入更多不相关信息)。Naïve RAG 很难在两者之间做出智能的动态平衡。

正是这些局限性,促使我们思考更智能、更灵活的RAG范式,从而引出了Agentic RAG。

Agentic RAG:赋予LLM自主决策的能力

Agentic RAG 的核心思想是:将LLM从一个被动的文本消费者,升级为一个能够主动思考、规划、执行、反思和迭代的“智能体”(Agent)。这个智能体拥有使用各种工具的能力,其中就包括多次、有策略地进行信息检索。

核心概念:LLM成为决策者

在Agentic RAG中,LLM不再仅仅是根据给定上下文生成文本。它变成了整个信息处理流程的指挥官。它会根据当前的任务目标、已有的信息以及对自身能力的评估,自主决定:

  • 何时检索 (When to retrieve):当前信息是否足以回答问题?是否需要更多外部知识?
  • 检索什么 (What to retrieve):基于当前思考和进展,应该搜索哪些关键词?是宽泛的搜索还是聚焦的搜索?
  • 如何检索 (How to retrieve):使用哪个工具进行检索(向量数据库、网络搜索、API等)?
  • 检索多少次 (How many times to retrieve):一次检索是否足够?是否需要多轮迭代来细化或补充信息?

这种自主决策的能力,使得Agentic RAG能够模拟人类进行复杂研究和问题解决的过程:先有个初步想法,然后查阅资料,根据查阅结果调整思路,可能需要查阅更多资料,最终综合信息得出结论。

类比:研究助理 vs. 固定查询

为了更好地理解Naïve RAG和Agentic RAG的区别,我们可以用一个类比:

  • Naïve RAG 就像你走进图书馆,对图书管理员说:“请给我关于‘人工智能’的TOP 5本书!”图书管理员立刻给你五本书,然后你的任务就完成了,你只能基于这五本书来回答问题,无论它们是否完全符合你的深层需求。你没有机会说:“嗯,这几本书里提到了‘深度学习’,我能再找几本专门讲‘深度学习在自然语言处理中应用’的书吗?”

  • Agentic RAG 就像你雇佣了一位研究助理。你告诉他:“帮我研究一下‘人工智能在自然语言处理中的最新进展’。”

    1. 研究助理首先会规划:“好的,我需要先了解人工智能的整体发展,然后聚焦到自然语言处理领域,再查找最新的技术和突破。”
    2. 他会执行第一次检索:“我先去图书馆(向量数据库)查找‘人工智能 最新发展’。”
    3. 他会观察检索结果:“这些书里提到了Transformer模型和Attention机制。”
    4. 他会思考:“这些信息不错,但我需要更具体地了解它们在NLP中的应用。而且,这些书可能有点旧了,我还需要一些更近期的信息。”
    5. 他会执行第二次检索(自主决定):他可能去查阅学术论文数据库(网络搜索工具),搜索“Transformer NLP 最新应用”。
    6. 他会观察第二次检索结果:“我找到了关于BERT、GPT-3等模型的论文。”
    7. 他会思考:“现在我有了关于模型的信息,但我还需要知道它们是如何处理特定任务的,比如情感分析或机器翻译。”
    8. 他会执行第三次检索(自主决定):他可能再次细化搜索,查找“情感分析 Transformer 模型实现”。
    9. 经过多轮检索和思考,他最终会将所有相关信息进行综合、提炼,然后向你汇报一个全面、深入的答案。

这个研究助理就是我们的“智能体”。他拥有自主决策检索次数和内容的能力,这使得他能够处理远比固定一轮检索复杂得多的任务。

Agentic RAG 的架构组件

Agentic RAG的实现,不仅仅是简单地将LLM和检索器连接起来,它通常涉及几个关键的架构组件:

  1. 智能体(The Agent – LLM)

    • 这是核心的决策者和控制器。它是一个强大的LLM,经过特殊设计(通过系统Prompt和Few-shot示例),使其能够理解任务、进行推理、规划行动、选择工具并解析工具的输出。
    • 规划 (Planning):将复杂问题分解为可管理的子任务,确定执行这些子任务的顺序。
    • 工具使用 (Tool Use):能够调用外部工具来获取信息或执行操作。
    • 自我反思/纠正 (Self-Reflection/Correction):评估已执行操作的结果,判断是否需要调整计划或重新尝试,以达到更好的结果。
    • 上下文管理 (Context Management):在多轮交互中维护对话历史和中间结果。
  2. 工具(Tools)

    • 工具是智能体的“手脚”,是它与外部世界交互的接口。智能体能够根据需要,选择并调用合适的工具。
    • 向量存储检索工具 (Vector Store Search Tool):这是RAG的核心,用于在本地知识库中进行相似性搜索。
    • 查询重写/扩展工具 (Query Rewriting/Expansion Tool):一个基于LLM的工具,可以接收用户原始查询或智能体生成的子查询,并将其重写成更适合检索的关键词或短语,或者进行多角度的查询扩展。
    • 摘要工具 (Summarization Tool):当检索到的文本过长或包含大量冗余信息时,智能体可以调用此工具对文本进行摘要,以节省上下文窗口空间并提取关键信息。
    • 网络搜索工具 (Web Search Tool):用于获取外部、实时或更广泛的信息(例如,通过Google Search API, Bing Search API)。
    • API 调用工具 (API Call Tool):与外部系统或数据库进行交互,获取结构化数据或执行特定操作。
    • 代码执行工具 (Code Interpreter Tool):在需要进行数据分析、计算或验证逻辑时,智能体可以生成并执行代码。
  3. 记忆/暂存区(Memory/Scratchpad)

    • 智能体需要一个机制来存储其在多轮思考、规划和执行过程中产生的中间结果、观察和决策。这就像人类的“草稿纸”或“短期记忆”。
    • 它帮助智能体在不同步骤之间保持连贯性,并进行多步推理。
  4. 编排逻辑(Orchestration Logic)

    • 这是智能体执行其决策循环的框架。它定义了智能体如何从接收输入开始,到规划、选择工具、执行工具、观察结果、反思,然后重复这个循环直到任务完成。
    • 常见的编排模式包括ReAct (Reasoning and Acting),它通过明确的“Thought”(思考)和“Action”(行动)步骤,指导LLM进行推理和工具使用。

Agentic RAG 如何解决 Naïve RAG 的问题(重点:迭代检索)

现在,让我们深入探讨Agentic RAG如何通过其核心的迭代检索和自主决策能力,克服Naïve RAG的局限性。

1. 动态与上下文感知的检索(Dynamic and Context-Aware Retrieval)

  • Naïve RAG 的痛点:固定 k 值无法适应不同查询的需求。
  • Agentic RAG 的解决方案:智能体不再盲目检索 k 个文档。它会根据当前的问题复杂性、已有的信息量以及对所需信息粒度的判断,动态地决定检索的量。
    • 如果初步搜索结果已经非常明确且足以回答问题,智能体可以决定停止检索,直接生成答案。
    • 如果初步结果不足或存在歧义,智能体可以决定增加检索数量 (k 值),或者改变检索策略(例如,从宽泛搜索变为精确搜索)。
    • 智能体甚至可以对检索到的文档进行初步评估,只选择最相关的部分,而不是机械地将所有 k 个文档都扔给LLM。

2. 迭代细化与多跳推理(Iterative Refinement and Multi-Hop Reasoning)

这是Agentic RAG最显著的优势之一。

  • Naïve RAG 的痛点:单次检索无法处理需要多步信息提取和逻辑推理的问题。
  • Agentic RAG 的解决方案:智能体能够执行以下循环:
    1. 初始搜索(Initial Search):根据用户原始问题进行一次初步、可能相对宽泛的检索。
    2. 评估与思考(Evaluation and Thought):智能体读取并理解初始检索结果。它会评估这些信息是否足够、是否完整、是否直接回答了问题。
    3. 子问题生成(Sub-Question Generation):如果初始结果不足,智能体不会放弃。它会根据已有的信息和未解决的部分,自主地生成新的、更具体或更聚焦的子问题。例如,如果原始问题是“产品X的发布日期和市场反馈”,智能体可能会先搜索“产品X 发布日期”,然后基于这个结果,再生成“产品X 市场反馈”的子问题。
    4. 查询重写/扩展(Query Rewriting/Expansion):智能体可以利用其语言理解能力,将子问题转化为更有效的搜索查询关键词或短语,甚至可以对原始查询进行多角度的扩展,确保覆盖全面。
    5. 重新检索(Re-retrieval):使用新的查询执行第二次、第三次甚至更多次的检索。
    6. 信息综合(Information Synthesis):在每次检索后,智能体会将新获取的信息与之前的信息进行整合和分析,逐步构建一个更全面、更准确的答案。
    7. 循环与终止(Loop and Termination):这个过程会一直持续,直到智能体认为它已经收集到足够的信息来全面且准确地回答用户问题,或者在多次尝试后仍然无法找到相关信息时,它会决定停止并告知用户。

示例场景:
假设用户问题是:“解释一下2008年金融危机对欧洲银行业监管的影响,特别是巴塞尔协议III是如何因此演进的?”

  • Naïve RAG:尝试一次性搜索“2008年金融危机 欧洲银行业监管 巴塞尔协议III”。它可能检索到一些关于金融危机和巴塞尔协议III的文档,但很难将两者之间的因果关系和演进路径清晰地串联起来。
  • Agentic RAG
    1. 思考:这是一个复杂的多跳问题,需要理解金融危机、欧洲银行监管以及巴塞尔协议III三者之间的联系。
    2. 行动:使用检索工具搜索“2008年金融危机 欧洲银行业影响”。
    3. 观察:检索到了一些关于金融危机对欧洲银行冲击和初期监管反应的文档。
    4. 思考:现在我知道了危机的影响,但还没有明确提到巴塞尔协议III的演进。我需要聚焦于危机如何导致了新的监管框架。
    5. 行动:使用检索工具搜索“2008年金融危机 巴塞尔协议III 关联”。
    6. 观察:检索到了巴塞尔协议II的不足以及巴塞尔协议III作为应对方案的背景信息。
    7. 思考:我有了关联,但还需要更具体地了解巴塞尔协议III的具体内容及其对欧洲银行业监管的具体改变。
    8. 行动:使用检索工具搜索“巴塞尔协议III 主要内容 欧洲银行业监管变化”。
    9. 观察:检索到了资本要求、流动性覆盖率等具体条款以及欧洲如何实施这些条款的信息。
    10. 思考:我现在有了足够的信息来回答整个问题,可以开始综合整理了。

3. 克服上下文窗口限制(Overcoming Context Window Limitations)

  • Naïve RAG 的痛点:不加区分地填充检索到的上下文,容易导致上下文窗口溢出或信息稀释。
  • Agentic RAG 的解决方案:智能体能够智能地管理上下文
    • 选择性检索:只检索当前子任务最需要的信息,而不是一次性检索所有可能相关的信息。
    • 摘要与提炼:对于检索到的长文本,智能体可以调用摘要工具,提取关键信息,避免将整个原始文本都送入LLM,从而节省上下文空间。
    • 逐步构建答案:智能体可以在多轮迭代中逐步构建答案,只将当前步骤所需的相关信息和已形成的答案草稿保留在上下文中,避免不必要的冗余。

4. 处理查询的模糊性和不确定性(Handling Ambiguity and Uncertainty)

  • Naïve RAG 的痛点:对模糊或复杂查询的检索效果不佳。
  • Agentic RAG 的解决方案
    • 查询重写与扩展:智能体可以根据对用户查询的初步理解,主动重写或扩展查询,尝试从多个角度进行检索,以提高检索的准确性和覆盖面。
    • 内部澄清:智能体可以在其内部思考过程中,识别查询中的模糊点,并尝试通过检索不同关键词来“澄清”这些点,而无需直接向用户提问。

5. 对不佳初始查询的鲁棒性(Robustness to Poor Initial Queries)

  • Naïve RAG 的痛点:如果用户初始查询质量不高,检索结果也会很差。
  • Agentic RAG 的解决方案
    • 智能体可以利用其推理能力,对用户输入的查询进行预处理和意图理解。如果它判断原始查询可能导致不良检索结果,它会自主地将其重写或分解为更有效的搜索子查询。
    • 例如,用户输入“告诉我关于AI的一些事”。Naïve RAG会直接搜索“AI”。Agentic RAG可能会思考:“‘AI的一些事’太泛了,我需要具体化。我应该先找‘AI定义’,然后是‘AI发展历史’,最后是‘AI在不同领域的应用’。”然后分步进行检索。

代码实践:实现一个简单的 Agentic RAG

现在,让我们通过代码来感受Agentic RAG的魅力。我们将继续使用LangChain,利用其Agent和Tooling能力。

我们将实现一个简单的Agent,它能够:

  1. 使用我们之前构建的向量存储进行检索。
  2. 能够进行多轮思考和行动,自主决定是否需要更多检索。
import os
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import FAISS
from langchain.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

# 导入Agent相关的组件
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain import hub
from langchain_core.tools import tool

# 设置你的OpenAI API Key (确保已设置)
# os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_API_KEY"

# 重新准备向量存储 (或使用之前准备好的)
def prepare_vector_store(file_path="example_docs.pdf"):
    """
    加载文档,切分并创建FAISS向量存储。
    此函数与Naïve RAG示例中的相同。
    """
    if not os.path.exists(file_path):
        print(f"创建虚拟PDF文件: {file_path}")
        from fpdf import FPDF
        pdf = FPDF()
        pdf.add_page()
        pdf.set_font("Arial", size=12)
        pdf.multi_cell(0, 10, "Python asyncio is a library to write concurrent code using the async/await syntax. It's a fundamental part of modern Python for handling I/O-bound and high-level structured network code. It uses an event loop to manage tasks and handle non-blocking operations. When an I/O operation (like reading from a network socket or file) is initiated, asyncio doesn't block the entire program; instead, it switches to another task until the I/O operation is complete. This is achieved through coroutines and the `await` keyword, which yields control back to the event loop. The event loop then schedules other ready tasks. Once the I/O operation finishes, the event loop resumes the waiting coroutine.")
        pdf.add_page()
        pdf.set_font("Arial", size=12)
        pdf.multi_cell(0, 10, "Node.js is a JavaScript runtime built on Chrome's V8 JavaScript engine. It uses an event-driven, non-blocking I/O model that makes it lightweight and efficient. Node.js's event loop is implemented using the `libuv` library, which handles asynchronous I/O operations across different platforms. When an I/O operation is requested in Node.js, it's offloaded to `libuv`'s thread pool, and control immediately returns to the main JavaScript thread. Once the I/O operation completes, a callback function is placed in the event queue, and the event loop processes it in a non-blocking manner. This allows Node.js to handle many concurrent connections with a single thread.")
        pdf.add_page()
        pdf.set_font("Arial", size=12)
        pdf.multi_cell(0, 10, "Comparing Python's asyncio and Node.js's event loop reveals architectural differences. Asyncio's `async/await` syntax provides explicit control over yielding, making it more akin to cooperative multitasking within a single thread. Node.js, while appearing single-threaded to the developer, uses `libuv` internally for thread pooling to manage heavy I/O tasks, abstracting away much of the low-level concurrency. Both are highly effective for I/O-bound applications, but their underlying mechanisms for achieving non-blocking behavior differ. Python's approach is more explicit in its asynchronous flow, while Node.js's is often perceived as more 'fire-and-forget' with callbacks and promises.")
        pdf.output(file_path)

    loader = PyPDFLoader(file_path)
    documents = loader.load()

    text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
    splits = text_splitter.split_documents(documents)

    embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")
    vector_store = FAISS.from_documents(splits, embeddings)
    return vector_store

vector_store = prepare_vector_store()

# 将向量存储的检索器封装成一个工具
# LangChain的@tool装饰器使得将一个函数变成LLM可调用的工具变得非常简单。
# LLM会根据函数的docstring和参数签名来理解工具的用途。

@tool
def vector_store_retriever(query: str) -> str:
    """
    用于从本地向量存储中检索相关文档片段的工具。
    输入是一个字符串查询,输出是检索到的文档内容。
    智能体应该使用此工具来获取关于特定主题或问题的背景信息。
    """
    docs = vector_store.similarity_search(query, k=3) # 智能体可以决定k值,但这里我们先固定
    return "nn".join([doc.page_content for doc in docs])

# 定义一个简单的摘要工具,用于处理检索到的长文本
@tool
def summarize_text(text: str, query: str) -> str:
    """
    用于对长文本进行摘要的工具。
    输入是要摘要的文本和与该文本相关的原始查询,输出是文本的简洁摘要。
    智能体应该在检索到大量文本时使用此工具,以提取关键信息并节省上下文空间。
    """
    summarization_llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0.0)
    summary_prompt = ChatPromptTemplate.from_template(
        "请根据以下文本,针对问题'{query}',生成一个简洁的摘要:nn{text}"
    )
    chain = summary_prompt | summarization_llm | StrOutputParser()
    return chain.invoke({"text": text, "query": query})

# 初始化LLM
llm = ChatOpenAI(model_name="gpt-4-turbo-preview", temperature=00) # 使用更强大的LLM作为Agent

# 定义Agent可以使用的工具列表
tools = [vector_store_retriever, summarize_text]

# 获取Agent的Prompt模板
# LangChain Hub提供了一系列预设的Prompt模板,这里我们使用一个为OpenAI函数调用设计的Agent模板。
# 这个模板已经包含了ReAct模式的指令。
prompt = hub.pull("hwchase17/openai-tools-agent")

# 创建Agent
# create_tool_calling_agent 函数会自动将LLM、工具和Prompt组合成一个Agent。
agent = create_tool_calling_agent(llm, tools, prompt)

# 创建Agent执行器
# AgentExecutor 是Agent的运行环境,它负责驱动Agent的思考-行动循环。
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True, handle_parsing_errors=True)

print("n--- Agentic RAG 示例:处理复杂问题 ---")
complex_query = "Python的asyncio和Node.js的事件循环在处理I/O密集型操作时有什么主要区别?它们在内部实现上有什么不同,以及它们各自的优势和劣势是什么?"
print(f"用户问题: {complex_query}n")

response_agentic = agent_executor.invoke({"input": complex_query})
print(f"nAgentic RAG 回答:n{response_agentic['output']}n")

print("n--- Agentic RAG 示例:处理需要初步探索的问题 ---")
explore_query = "告诉我关于Python asyncio的一些基础知识和它的用途。"
print(f"用户问题: {explore_query}n")

response_agentic_explore = agent_executor.invoke({"input": explore_query})
print(f"nAgentic RAG 回答:n{response_agentic_explore['output']}n")

print("n--- Agentic RAG 示例:处理文档中可能没有直接答案的问题 ---")
unrelated_query_agentic = "2024年全球经济增长预测是多少?"
print(f"用户问题: {unrelated_query_agentic}n")

response_agentic_unrelated = agent_executor.invoke({"input": unrelated_query_agentic})
print(f"nAgentic RAG 回答:n{response_agentic_unrelated['output']}n")

代码解释:

  1. prepare_vector_store: 保持不变,用于构建我们的知识库。
  2. @tool 装饰器: 这是LangChain中将普通Python函数转换为LLM可调用工具的关键。
    • vector_store_retriever: 封装了我们的向量搜索逻辑。注意,在实际Agent中,k 值也可以是LLM动态决定的参数,但为了简化,这里仍设为3。LLM会根据其docstring来决定何时调用此工具。
    • summarize_text: 新增的摘要工具。Agent在检索到过长的文本时,可以决定先对其进行摘要,再进行分析。这体现了Agent对上下文窗口的智能管理。
  3. LLM 初始化: 我们使用 gpt-4-turbo-preview 作为Agent的底层模型,因为它通常在推理和工具使用方面表现更出色。
  4. prompt = hub.pull("hwchase17/openai-tools-agent"): 从LangChain Hub拉取一个预设的Agent Prompt。这个Prompt包含了指导LLM进行思考(Thought)、选择工具(Action)、观察结果(Observation)的指令,这正是ReAct模式的核心。
  5. create_tool_calling_agent: 这是LangChain提供的一个便捷函数,用于创建一个能够使用OpenAI函数调用API的Agent。LLM会根据工具的定义和Prompt的指导,自动选择合适的工具和参数。
  6. AgentExecutor: 这是Agent的运行器。verbose=True 会打印Agent的思考过程(Thought, Action, Observation),这对于理解和调试Agent的行为至关重要。handle_parsing_errors=True 允许Agent在工具调用或输出解析失败时尝试恢复。

运行Agentic RAG示例时,你会在控制台看到Agent的详细思考过程(Thought/Action/Observation):

--- Agentic RAG 示例:处理复杂问题 ---
用户问题: Python的asyncio和Node.js的事件循环在处理I/O密集型操作时有什么主要区别?它们在内部实现上有什么不同,以及它们各自的优势和劣势是什么?

> Entering new AgentExecutor chain...
Thought: The user is asking a complex question about the differences between Python's asyncio and Node.js's event loop, including their internal implementations, advantages, and disadvantages for I/O-bound operations. This will likely require multiple searches and synthesis of information.

First, I need to understand Python's asyncio in the context of I/O operations.
Action: vector_store_retriever
Action Input: Python asyncio I/O handling
Observation: Python asyncio is a library to write concurrent code using the async/await syntax. It's a fundamental part of modern Python for handling I/O-bound and high-level structured network code. It uses an event loop to manage tasks and handle non-blocking operations. When an I/O operation (like reading from a network socket or file) is initiated, asyncio doesn't block the entire program; instead, it switches to another task until the I/O operation is complete. This is achieved through coroutines and the `await` keyword, which yields control back to the event loop. The event loop then schedules other ready tasks. Once the I/O operation finishes, the event loop resumes the waiting coroutine.

Thought: I have a good understanding of Python's asyncio. Now I need to get information about Node.js's event loop and its I/O handling.
Action: vector_store_retriever
Action Input: Node.js event loop I/O handling
Observation: Node.js is a JavaScript runtime built on Chrome's V8 JavaScript engine. It uses an event-driven, non-blocking I/O model that makes it lightweight and efficient. Node.js's event loop is implemented using the `libuv` library, which handles asynchronous I/O operations across different platforms. When an I/O operation is requested in Node.js, it's offloaded to `libuv`'s thread pool, and control immediately returns to the main JavaScript thread. Once the I/O operation completes, a callback function is placed in the event queue, and the event loop processes it in a non-blocking manner. This allows Node.js to handle many concurrent connections with a single thread.

Thought: I have information on both asyncio and Node.js event loops. Now I need to find information that directly compares them, focusing on their architectural differences, internal implementations, and then deduce their advantages and disadvantages for I/O-bound operations based on the retrieved information.
Action: vector_store_retriever
Action Input: Python asyncio vs Node.js event loop comparison I/O implementation
Observation: Comparing Python's asyncio and Node.js's event loop reveals architectural differences. Asyncio's `async/await` syntax provides explicit control over yielding, making it more akin to cooperative multitasking within a single thread. Node.js, while appearing single-threaded to the developer, uses `libuv` internally for thread pooling to manage heavy I/O tasks, abstracting away much of the low-level concurrency. Both are highly effective for I/O-bound applications, but their underlying mechanisms for achieving non-blocking behavior differ. Python's approach is more explicit in its asynchronous flow, while Node.js's is often perceived as more 'fire-and-forget' with callbacks and promises.

Thought: I have retrieved information covering the core aspects of both asyncio and Node.js event loops, including their I/O handling, internal implementations (libuv for Node.js), and a direct comparison of their architectural differences. Based on this, I can synthesize the advantages and disadvantages. I have enough information to answer the user's question comprehensively.
Final Answer: Python的asyncio和Node.js的事件循环在处理I/O密集型操作时有显著区别。

**Python asyncio:**
*   **工作原理:** 使用`async/await`语法实现并发代码,通过事件循环管理任务和非阻塞操作。当I/O操作启动时,asyncio会将控制权交还给事件循环,从而不阻塞整个程序,直到I/O操作完成,再恢复等待的协程。这是一种在单线程内通过显式控制(`await`)实现协作式多任务的方式。
*   **内部实现:** 基于Python的事件循环(`asyncio`模块自身提供),通过协程(coroutines)和任务(tasks)进行调度。
*   **优势:**
    *   提供了对异步流程的显式控制,代码可读性强,易于理解数据流。
    *   适合I/O密集型和高层级网络代码。
*   **劣势:**
    *   在CPU密集型任务上表现不佳,因为GIL(全局解释器锁)限制了真正的并行计算。

**Node.js 事件循环:**
*   **工作原理:** 基于事件驱动、非阻塞I/O模型,使其轻量高效。当请求I/O操作时,它会将其卸载到`libuv`库的线程池中,并立即将控制权返回给主JavaScript线程。I/O操作完成后,回调函数被放入事件队列,事件循环以非阻塞方式处理它。
*   **内部实现:** 核心是`libuv`库,它负责跨平台处理异步I/O操作,并利用线程池管理底层繁重的I/O任务,从而在表面上保持JavaScript单线程的特性。
*   **优势:**
    *   能够以单线程处理大量并发连接,非常适合构建高性能、可伸缩的网络应用。
    *   “即发即弃”的模式(通过回调和Promise)在某些场景下开发效率高。
*   **劣势:**
    *   回调地狱(callback hell)问题(尽管Promise和async/await有所缓解)。
    *   CPU密集型任务会阻塞事件循环,影响性能。

**主要区别总结:**
*   **控制权交接:** Asyncio通过`await`显式让出控制权;Node.js通过将I/O操作卸载到`libuv`的线程池来隐式地非阻塞主线程。
*   **底层并发:** Asyncio主要在Python解释器层面实现协作式多任务;Node.js则通过`libuv`在C/C++层面利用线程池实现真正的底层并发,但上层JavaScript仍然是单线程。
*   **编程范式:** Python的`async/await`更倾向于结构化、显式的异步流;Node.js的事件循环结合`libuv`,使得异步操作对开发者而言更像是一种“后台任务”。

两者都非常适合I/O密集型应用,但实现非阻塞行为的底层机制和开发者感知到的异步流程有所不同。

> Finished chain.

可以看到,Agent进行了三次vector_store_retriever调用,每次调用都基于前一次的观察和思考,逐步聚焦和补充信息。这正是迭代检索的强大之处。对于无法回答的问题,Agent也会根据其思考给出“我不知道”或“我无法回答”的答案,避免了Naïve RAG可能出现的幻觉。

高级 Agentic RAG 概念

Agentic RAG 的发展远不止于此,以下是一些更高级的概念:

  • 分层智能体(Hierarchical Agents):一个主智能体负责高层级规划,并可以将复杂任务委托给一个或多个子智能体来执行。每个子智能体专注于特定领域或任务,拥有自己的工具集。
  • 自我修正与反思(Self-Correction and Reflection):智能体不仅执行任务,还能在执行过程中或任务完成后,主动评估自己的表现。例如,它可以检查生成答案的准确性、完整性,如果发现不足,会反思并重新规划执行步骤。
  • 记忆管理(Memory Management):除了短期暂存区,智能体还可以拥有长期记忆,存储过去学习到的知识、用户偏好或常用策略,以提高未来任务的效率和质量。
  • 工具编排(Tool Orchestration):更智能的工具选择机制,例如,根据问题的类型动态选择最合适的工具组合,甚至可以生成新的工具。
  • 人机协作(Human-in-the-Loop):在关键决策点,智能体可以向人类寻求帮助或确认,从而结合人类的判断力与AI的自动化能力。

性能考量与挑战

尽管Agentic RAG前景广阔,但它并非没有挑战:

  • 延迟(Latency):多次LLM调用和工具执行会显著增加响应时间,尤其是在需要多轮迭代的复杂任务中。
  • 成本(Cost):更多的LLM调用意味着更高的API费用。对于生产环境,需要仔细权衡性能和成本。
  • 复杂性(Complexity):Agent的调试和维护比Naïve RAG复杂得多。理解Agent的思考链、确保工具的正确使用以及处理各种边缘情况都需要更多的工程投入。
  • 鲁棒性(Robustness):Agent的推理能力仍然依赖于LLM。LLM在规划或工具使用过程中仍可能出现“幻觉”,导致Agent进入错误循环或生成不准确的结果。
  • Prompt 工程(Prompt Engineering for Agents):设计有效的Agent Prompt,引导LLM进行正确的思考、规划和工具使用,是一门复杂的艺术。
  • 状态管理(State Management):在多轮交互中维护Agent的内部状态(包括记忆、已检索信息、当前目标等)是一个挑战。

Naïve RAG 与 Agentic RAG:比较一览

特性/方面 Naïve RAG Agentic RAG
检索策略 单轮检索,固定 k 迭代式、动态 k 值,多轮次检索,自主决定检索次数
LLM 角色 被动的上下文消费者 主动的编排者、决策者、推理者
查询处理 直接对原始查询进行搜索 分解复杂查询,重写,细化子查询,多角度探索
上下文窗口管理 容易因无关数据而溢出或稀释 策略性上下文选择,检索摘要,避免不必要的冗余数据
推理深度 限于单次信息综合 多跳推理,迭代细化,自我修正
处理模糊性 难以处理模糊/复杂查询 能重构、探索查询的不同方面,内部澄清
工具使用 通常仅限于向量存储检索 向量存储、网络搜索、API 调用、摘要、代码解释器等多样化工具
实现复杂度
延迟/成本 较高(因多轮LLM调用和工具使用)
复杂任务表现 欠佳,易受限 显著提升,更鲁棒、更全面

智能体驱动的未来

综上所述,Agentic RAG 代表了RAG乃至整个LLM应用开发领域的一次重大飞跃。它将LLM从一个单纯的“语言模型”提升为能够自主思考、规划和行动的“智能体”。核心在于,智能体不再被动地接收一次性检索到的信息,而是能够根据任务需求,自主决定何时、何地、以何种方式以及多少次进行信息检索。这种能力的赋予,使其能够:

  • 克服固定检索轮次和上下文窗口的限制。
  • 进行多跳推理和迭代细化,处理复杂、多步骤的问题。
  • 更智能地管理信息,避免信息过载。

尽管Agentic RAG在延迟、成本和复杂度方面带来了新的挑战,但其在处理复杂、真实世界任务上的显著优势,预示着它将是构建更智能、更可靠AI应用的必由之路。我们正处于一个激动人心的时代,LLM的未来,无疑是智能体驱动的未来。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注