高级 12 分钟高级

本地 Agentic RAG:何时值得 peine

Agentic RAG 将检索的控制权交给模型:由它决定检索什么,阅读检索到的内容,并在缺少信息时再次发起查询。这个循环是有代价的,尤其是在本地机器上,每一轮都要付出生成时间。本指南将说明它与经典 RAG 相比有哪些变化、哪些情况下值得承担这笔额外开销,并给出一个使用 Ollama 提供模型服务并结合 LangGraph 的完整示例。

作者: Samir K.·更新于 2026-11-02·已在 Windows、macOS 和 Linux 上测试

#Agentic RAG 概览

传统 RAG 遵循固定路径:问题触发一次检索,检索到的段落进入提示词,然后由模型作答。Agentic RAG 则用循环取代了这条路径。模型拥有检索工具,并自行决定是否调用、使用什么查询以及调用多少次,直到它判断自己可以作答或应当放弃为止。

微软的 AI Agents for Beginners 课程将其描述为这样一种运行方式:模型规划后续步骤,同时从外部来源获取信息,并采用“maker-checker”式循环:生成、检查、修正。该课程同时指出了这种自主性的边界:它只在你提供的任务和工具范围内发挥作用。

实际结论可以用一句话概括。如果你的 RAG 失败是因为单次检索无法汇集完整答案,循环可能会有帮助。如果失败是因为数据摄取或分块质量差,循环只会以更慢的速度重复同样的问题。

i
本指南未作出的断言
由于没有基于您的数据进行测量,本文不宣称任何准确率提升。本指南未进行任何基准测试:文中的数量级均为示意性计算,并已作相应标注。

#Agentic RAG 与单次检索 RAG:两者的区别

打造专属本地 RAG 的完整套件。

借助我们的经验和引导式路径,搭建你的本地 RAG,用你自己的文档工作。

  • 在线空间,终身可用
  • PDF + 文件
  • 30 天内退款

基础组件保持不变:embedding、向量数据库、生成模型。如果您还不熟悉这些组件,请先阅读本地 RAG 入门介绍。变化在于由谁决定执行流程。在经典 RAG 中,决定者是您的代码;在智能体式 RAG 中,决定者是模型,但要受代码为其设定的限制约束。

查询
经典 RAG:使用用户的问题,有时会改写一次。智能体式:模型自行编写查询,并可将一个问题分解为多个查询。
检索次数
经典 RAG:一次,事先已知。智能体式:因问题而异,受您设定的上限约束。
来源
传统 RAG:通常只有一个索引。智能体式:可使用多种工具(向量索引、关键词检索、SQL 数据库),由模型在其中进行选择。
结果把控
经典 RAG:没有,模型根据收到的内容撰写回答。智能体式 RAG:模型可以判断段落不足并重新检索,但这并不是保证:这一判断本身也可能出错。
对模型的要求
经典 RAG:任何能根据摘录内容撰写回答的模型。智能体式 RAG:能正确处理工具调用的模型。
可预测性
经典 RAG:响应时间相当稳定。智能体式 RAG:耗时不固定,执行路径需要通过运行轨迹来检查。

这个循环由三种机制组成。规划:将问题分解为子问题。工具使用:将每个子问题转化为一次调用。重新发起:读取结果,并决定是改写查询还是得出结论。关于智能体的整体架构(记忆、安全护栏、监控),请参阅专门的指南:这里我们只关注检索循环。

还有一种常被忽略的中间方案:固定步骤流水线。查询改写、混合检索和 reranker 始终按相同顺序执行,无需模型做出决策。这不属于智能体式方案,而且通常是应当首先尝试的做法。

#何时值得使用 Agentic RAG

正确的判断标准在于您的问题形式,而不是期望达到的复杂程度。请查看现有 RAG 的实际失败案例,然后对其进行分类。

多跳问题
第一次检索的结果决定第二次检索:先找到某份合同的供应商,再找到适用于该供应商的条款。
跨文档比较
“比较合同 A 和合同 B 的通知期”需要两次独立的检索。仅一次查询可能会返回同一份合同中的五个段落。
多个不同性质的数据源
一个数字存放在 SQL 数据库中,而相关说明存放在 PDF 里。模型根据问题选择工具,正如 Microsoft 课程中针对复杂数据库交互所描述的那样。
对等待的容忍度
文献检索、资料准备、后台处理:只要结果会经过复核,多花几十秒不会对任何人造成困扰。
速度方面的优先修正
当不完整的回答代价高昂时,增加一个验证步骤是合理的,但前提是对具有约束力的决策保留人工复核。

反之,在以下情况下请继续使用简单 RAG:

FAQ 和单一来源问题
答案就在一个段落中:一次检索就够了,循环只会增加等待时间。
小型硬件上的对话助手
用户期望快速得到回答,而模型的运行已接近其极限。
未能命中正确段落的检索
请先修正切分、embedding 和混合检索。重新运行一次未调好的检索,只会得到另一批糟糕的结果。
对结构化数据进行精确计算
写好并测试过的 SQL 查询,比智能体临时拼凑的查询更可靠。
整个语料库中实体之间的多重关联
如果需求是大规模遍历关系,基于图的方法解决的是另一类问题:请参阅 GraphRAG 指南。

#延迟与模型方面的代价

在云端,每一轮循环都会体现在账单上;在本地,则体现在耗时和内存占用上。有三项开销值得关注。

#延迟成倍增加

进行 N 次检索时,模型需要生成 N + 1 次:每次检索前生成一次,最后再为回答生成一次。单次 RAG 只生成一次。由于提示词每轮都会变长,每次生成都要重新读取更多文本,但运行时的上下文缓存可复用的部分除外。总耗时取决于模型、量化方式和机器;请在自己的环境中实测,不要依赖笼统的数字。

#上下文很快被占满

示例计算:按每个 token 3 到 4 个字符计算,4 个 1 200 字符的段落约相当于 1 400 个 token。因此,五次检索会增加约 7 000 个 token,还不包括问题、指令和模型的思考内容。当 num_ctx 为 8192 时,余量几乎耗尽。此时 Ollama 会截断上下文,最早的结果乃至指令都可能被移出窗口,而应用程序不会明确提示这一点。

增大 num_ctx 会消耗更多内存,并拖慢后续轮次。通常更好的做法是少返回一些段落,或者先摘要其内容再重新注入。

#模型必须能撑得住循环

模型必须生成格式正确的工具调用,然后判断已读取的内容是否足够。请使用 ollama show 检查是否支持:Capabilities 部分应列出 tools。这项检查只能说明该格式受支持,并不能说明模型会做出正确决策。

RTX 3060 12 GB 或 RTX 4070 12 GB
Q4_K_M 量化的 7 到 9B 模型(本站参考值:7B ≈ 5 GB)可为上下文和 embedding 留出空间。这正是下文示例的起点,使用的是 qwen3.5:9b。
RTX 4080 16 GB
Q4_K_M 量化的 14B 模型(≈ 9 GB)可以运行,并为扩大上下文留出更充裕的余量。
RTX 4090 24 GB 或 M4 Pro 24 至 48 GB
可以考虑更大的模型,例如 qwen3.8:27b(约 18 GB)。在 24 GB 的配置上,请留意扩大上下文后还剩余多少内存。
!
两个模型之间的来回切换
循环会交替执行 embedding 和生成。如果两个模型无法同时载入内存,Ollama 可能会在每一轮卸载其中一个以加载另一个,这部分时间会叠加到其他所有耗时之上。请在运行期间用 ollama ps 进行检查,并在需要时调整 OLLAMA_MAX_LOADED_MODELS。

#选择编排器

编排器负责执行循环:转发工具调用、将结果返回给模型并应用各项限制。共有三种途径,全都兼容本地模型。

手写循环
围绕 Ollama 的聊天 API 及其 tools 参数,大约二十行代码即可实现。适合用来理解其机制或处理非常简单的场景。限制和运行轨迹需要你自己编写。
LangGraph
循环是一张由节点和边构成的显式图,具备状态、递归上限和步骤流式输出。如果你想控制每一次状态转移,这是不错的选择。我们的示例采用的正是它。
LlamaIndex
如果您的数据摄取和索引已经基于它构建,它就值得考虑:其智能体会将您的查询引擎复用为工具。

不要根据流行程度来选择。如果你的流水线已经在某个框架上运行,就继续用它。无论如何,都要锁定版本:这些库迭代很快,其预构建函数也会变化。因此,我们的示例显式编写了图,这也让循环更易读懂。

#先决条件

Ollama
已安装,并在 http://localhost:11434 上监听。
Python
一个较新的虚拟环境。请在安装时确认各软件包要求的版本。
文档
在 mes_documents 文件夹中放几个 Markdown 或文本文件。如果是 PDF,请沿用 ChromaDB 教程中的摄取流程。
内存
9B 模型及其上下文约需 6 GB 可用内存;如果增大 num_ctx,则需要更多。

#使用 LangGraph 和 Ollama 的 Agentic RAG 示例

该示例构建了一个智能体,它只有一个检索工具,并基于本地 ChromaDB 索引运行。示例有意保持最简,以便清楚地展示循环过程。

!
本指南未实际运行此代码
这段代码遵循这些库已公开的 API 文档,但在撰写时并未实际运行,此处也未展示任何输出。请先在小型语料上进行测试,并查阅您所安装版本的文档。

#第 1 步:安装模型和软件包

终端
ollama pull qwen3.5:9b
ollama pull nomic-embed-text
ollama show qwen3.5:9b

python -m venv .venv
source .venv/bin/activate
pip install langgraph langchain-ollama chromadb ollama
pip freeze > requirements.txt

#第二步:为文档建立索引

按字符切分只是为了让示例保持简短。在真实语料上,请使用尊重文档结构的切分方式,以及已在您的法语文本上验证过的 embedding 模型。

index.py
from pathlib import Path

import chromadb
import ollama

EMBED_MODEL = "nomic-embed-text"

client = chromadb.PersistentClient(path="./chroma_db")
collection = client.get_or_create_collection(
    "docs", metadata={"hnsw:space": "cosine"}
)


def decouper(texte, taille=1200, recouvrement=150):
    pas = taille - recouvrement
    return [texte[i:i + taille] for i in range(0, len(texte), pas)]


for fichier in Path("mes_documents").glob("*.md"):
    morceaux = decouper(fichier.read_text(encoding="utf-8"))
    if not morceaux:
        continue
    vecteurs = ollama.embed(model=EMBED_MODEL, input=morceaux)["embeddings"]
    collection.upsert(
        ids=[f"{fichier.name}-{i}" for i in range(len(morceaux))],
        documents=morceaux,
        embeddings=vecteurs,
        metadatas=[{"source": fichier.name} for _ in morceaux],
    )

print(collection.count(), "passages indexés")

#第 3 步:编写工具和循环

工具描述很重要:模型正是通过阅读它来决定是否调用工具。系统指令要求先检索再作答、拆分子问题,并在缺少信息时如实承认。

agent.py
import chromadb
import ollama
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_core.tools import tool
from langchain_ollama import ChatOllama
from langgraph.graph import START, MessagesState, StateGraph
from langgraph.prebuilt import ToolNode, tools_condition

EMBED_MODEL = "nomic-embed-text"
collection = chromadb.PersistentClient(path="./chroma_db").get_collection("docs")


@tool
def chercher_documents(requete: str) -> str:
    """Cherche dans les documents internes. Utiliser une requête courte
    portant sur un seul sujet. Renvoie des passages précédés de leur source."""
    vecteur = ollama.embed(model=EMBED_MODEL, input=requete)["embeddings"][0]
    res = collection.query(query_embeddings=[vecteur], n_results=4)
    passages = zip(res["documents"][0], res["metadatas"][0])
    return "\n\n".join(f"[{m['source']}] {d}" for d, m in passages) or "Aucun passage."


outils = [chercher_documents]
llm = ChatOllama(model="qwen3.5:9b", temperature=0, num_ctx=8192).bind_tools(outils)

CONSIGNE = SystemMessage(content=(
    "Tu réponds uniquement à partir des résultats de chercher_documents. "
    "Si la question porte sur plusieurs sujets, fais une recherche par sujet. "
    "Si les passages ne suffisent pas, reformule une fois puis dis clairement "
    "ce que tu n'as pas trouvé. Cite la source entre crochets."
))


def agent(state: MessagesState):
    return {"messages": [llm.invoke([CONSIGNE] + state["messages"])]}


graphe = StateGraph(MessagesState)
graphe.add_node("agent", agent)
graphe.add_node("tools", ToolNode(outils))
graphe.add_edge(START, "agent")
graphe.add_conditional_edges("agent", tools_condition)
graphe.add_edge("tools", "agent")
app = graphe.compile()

if __name__ == "__main__":
    question = "Compare la durée de préavis du contrat A et du contrat B."
    entree = {"messages": [HumanMessage(content=question)]}
    for etape in app.stream(entree, {"recursion_limit": 12}, stream_mode="values"):
        etape["messages"][-1].pretty_print()

#第 4 步:查看跟踪记录

stream_mode="values" 的好处在于能显示每条消息。在看最终回答之前,请先阅读运行轨迹,并参考下面这张检查表:

  1. 01
    模型是否调用了工具?
    如果它直接作答,就说明没有进行检索:请强化指令或更换模型。
  2. 02
    查询语句是否有意义?
    对于比较类问题,理想情况下每份合同对应一条查询。如果只有一条查询且照搬整个问题,则说明没有完成问题分解。
  3. 03
    这些段落能回答问题吗?
    如果片段与问题无关,问题出在索引上,而不是智能体。
  4. 04
    回答是否基于检索到的片段?
    请在对应段落中核对每一处方括号引用。

#第 5 步:为循环设定上限

每次检索会消耗图中的两个步骤(先是智能体,然后是工具)。因此,上限为 12 时可以进行五次检索,然后生成最终回答。超出上限后,LangGraph 会抛出 GraphRecursionError 错误(langgraph.errors 模块):请在应用中捕获该错误并返回明确的提示信息,不要让用户直接面对错误堆栈。

#陷阱与故障排除

模型从不调用工具
检查 ollama show 中的 tools,改写工具描述,并尝试其他模型。不要靠写越来越长的提示词来弥补。
它重复执行相同的检索
模型在原地打转。请降低限制,并让工具在查询已执行过时返回明确的消息。
回答遗漏了最初的检索结果
这很可能是上下文被截断的迹象。请减小 n_results、切分得更细,或在内存允许的情况下增大 num_ctx。
回答变得非常缓慢
查看 ollama ps:可能有一部分模型运行在 CPU 上,或者多个模型在内存中轮流加载。同时也要统计运行轨迹中的轮次数量。
它什么都没找到,却仍在编造内容
循环并不能阻止模型捏造内容。请通过程序校验引用:每个被引用的来源都必须出现在对话的工具结果中。
过于强大的工具
读取文档的智能体也会接收文档内容,而这些内容可能带有恶意。请只给它提供只读工具,任何会修改内容的操作都要经过人工确认。
→
先试试不用智能体
在加入循环之前,请先测试固定分解:一次生成将问题拆分为若干子问题,每个子问题进行一次检索,然后给出回答。如果这样能解决您的比较类问题,您就能保持可预期的响应时间和更简单的代码。

#根据您自己的问题做出决定

其他地方公布的数据基于不同的语料库、模型和参数设置。只有用你自己的数据做对比才能得出结论,而且这并不需要太多硬件。

  1. 01
    建立一组问题集
    选取您已知答案和来源段落的真实问题,并混合简单问题与多跳问题。请记录每个问题的类型。
  2. 02
    确定基准
    将它们输入您现有的 RAG,不使用智能体。记录是否检索到了正确的段落、回答是否正确,以及总耗时。
  3. 03
    只改动一项
    在同一台机器上,使用相同的索引和模型,让智能体运行同一组测试。
  4. 04
    按问题类型阅读
    请分别查看简单问题和复杂问题的结果,并记录每个问题的检索次数。整体平均值可能会掩盖这样的情况:一类问题有所改善,另一类问题却白白耗费时间。
  5. 05
    保留确有必要的部分
    如果只有部分问题能从中受益,做一层路由可能就够了:默认使用直接 RAG,被判定为复杂的问题再走循环。

如需自动完成这项对比中的质量评估部分,可参阅 Ragas 专题指南,其中介绍了适用的指标。另外,请记录软件包版本、模型、量化方式和 num_ctx:否则下个月你的结果将无法比较。


#深入了解

Agentic RAG 建立在已经正常运行的 RAG 之上。这些指南涵盖需要首先稳定的基础、相邻方案以及智能体架构。

这份指南对您有帮助吗?

有反馈、发现了错误,或想补充说明?请告诉我们,让这份指南对每个人都更有帮助。