五种RAG架构及其在实际应用中的适用场景。
通过LlamaIndex和LangGraph,对朴素的、混合型的、基于图的、高级的以及代理型的RAG技术进行实用讲解,涵盖其故障模式、成本以及如何组合使用这些技术。
来源: The 5 RAG Architectures and Exactly When to Use Each One in Production
“RAG”这一术语如今涵盖了多种截然不同的架构,每种架构都是为不同类型的检索问题而设计的。选择错误的架构不仅仅是性能问题,它还可能决定系统是能够正常运行,还是会在大规模场景下向用户提供看似可靠但实际上错误的答案。
这是整个架构体系的概览图。其目标是能够针对任何检索问题判断需要哪种架构,并说明如何利用LangGraph进行调度、LlamaIndex处理数据层来构建该架构。这五种架构从简单到复杂依次为:朴素型、混合型、图型、高级型和智能代理型。它们并非相互竞争的关系,而是随着问题复杂度的提升而逐步添加的层次,大多数实际应用系统都会结合使用至少两种架构。
共同面临的挑战
从根本上说,每种RAG架构都在解决同一个问题:语言模型需要其在训练过程中从未见过的事实,这些事实需要在模型回答问题时立即提供,并且要以便于其进行推理的形式呈现。
模型在某个截止日期后就会停止学习。它们从未阅读过公司的内部维基、规格说明书,或是训练结束后发布的任何内容。针对这类内容进行微调既耗时又昂贵,而且一旦文档发生变化,经过微调的模型也会立刻过时。
RAG则避免了所有这些问题。它不是向模型传授新事实,而是在查询时检索相关知识并将其作为上下文放入提示词中。模型无需记忆任何内容,只需在需要时阅读即可。以下的五种架构分别是针对如何高效检索问题的五种解决方案,每种都适用于不同的场景。
架构1:基础RAG
Naive RAG是起点,许多实际应用场景根本不需要更复杂的功能:内部政策问答机器人、常见问题解答助手、文档检索、新人引导工具等。尽管名称如此,这其实是一种已经成熟且可大规模应用的架构模式。你只需加载文档,构建一个LlamaIndex的VectorStoreIndex,将其查询引擎作为@tool接口暴露出来,再将该工具提供给LangGraph智能体即可。
其五个连续步骤与LlamaIndex默认提供的功能直接对应。前四步在构建索引时完成,第五步则会在处理每个问题时执行。
Stages 1–4: Indexing time (run once)
Stage 5: Query time (run on every user question)
RAG领域最重要的区别在于索引时间与查询时间。嵌入成本只需一次性预先支付。在查询时,系统仅需支付一次相似度搜索费用以及一次大语言模型调用费用。
其工作原理
用户的查询会被嵌入到与文档片段相同的向量空间中,那些向量在几何上与查询向量最接近的文档片段会被返回。其基本假设是向量空间中的接近程度反映了语义上的接近程度。对于结构清晰、内容真实的语料库而言,这一假设的适用性出奇地好。
代码实现
以下是一个完整的模板,涵盖了导入与配置、状态管理、持久化知识库、相关工具、节点处理、路由机制以及图结构构建等内容:
# ============================================================
# NAIVE RAG — COMPLETE TEMPLATE
# ============================================================
# ── MODULE 1: IMPORTS & CONFIGURATION ───────────────────────
import os
from typing import Literal
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_core.tools import tool
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode
from langgraph.checkpoint.memory import MemorySaver
from llama_index.core import (
Settings, SimpleDirectoryReader, VectorStoreIndex,
StorageContext, load_index_from_storage
)
from llama_index.llms.openai import OpenAI as LlamaOpenAI
from llama_index.embeddings.openai import OpenAIEmbedding
# LangGraph's reasoning model
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# LlamaIndex's internal model - separate from the above
# These two model configs do NOT share state or conversation history
Settings.llm = LlamaOpenAI(model="gpt-4o-mini", temperature=0.1)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
Settings.chunk_size = 512
Settings.chunk_overlap = 50
# ── MODULE 2: STATE ──────────────────────────────────────────
class State(MessagesState):
pass # messages list inherited; extend if you need more
# ── MODULE 3: KNOWLEDGE BASE (Build once, persist, reload) ───
PERSIST_DIR = "./storage/naive"
if os.path.exists(PERSIST_DIR):
storage_context = StorageContext.from_defaults(persist_dir=PERSIST_DIR)
index = load_index_from_storage(storage_context)
else:
documents = SimpleDirectoryReader("./data").load_data()
index = VectorStoreIndex.from_documents(documents, show_progress=True)
index.storage_context.persist(persist_dir=PERSIST_DIR)
query_engine = index.as_query_engine(similarity_top_k=3)
# ── MODULE 3 (cont.): TOOL ───────────────────────────────────
@tool
def search_knowledge_base(query: str) -> str:
"""Search internal company documents for policies, product specs,
and procedures. Use this for any question requiring domain-specific
knowledge rather than general world knowledge."""
response = query_engine.query(query)
return str(response)
tools = [search_knowledge_base]
llm_with_tools = llm.bind_tools(tools)
tool_node = ToolNode(tools)
# ── MODULE 4: NODES ──────────────────────────────────────────
def agent_node(state: State) -> dict:
system_prompt = SystemMessage(content=(
"You are a helpful assistant with access to an internal knowledge base. "
"Use search_knowledge_base for company-specific questions. "
"Answer general questions directly."
))
response = llm_with_tools.invoke([system_prompt] + state["messages"])
return {"messages": [response]}
# ── MODULE 5: ROUTING ────────────────────────────────────────
def should_continue(state: State) -> Literal["tools", "__end__"]:
last = state["messages"][-1]
if hasattr(last, "tool_calls") and last.tool_calls:
return "tools"
return "__end__"
# ── MODULE 6: GRAPH ASSEMBLY ─────────────────────────────────
builder = StateGraph(State)
builder.add_node("agent", agent_node)
builder.add_node("tools", tool_node)
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", should_continue,
{"tools": "tools", "__end__": END})
builder.add_edge("tools", "agent")
graph = builder.compile(checkpointer=MemorySaver())
其缺陷所在
简单的RAG模型基于一个假设,而这个假设在两种常见情况下会失效。
第一种是词汇不匹配。用户询问适用于一级客户的SLA标准,但文档中承诺的是为黄金级客户提供99.9%的正常运行时间。SLA、一级客户和黄金级客户虽然相关,但并不完全对应,因此相似度评分可能会将相关内容排在较低位置,导致正确答案被遗漏。
第二种是涉及关联关系的问题。如果要了解某个供应商宕机后哪些产品会受到影响,就需要在多份文档中追踪各种关联关系。没有哪一段内容能单独包含答案,而简单的RAG模型只能返回来自不同文档的零散信息,且无法将这些信息关联起来。
接下来的两种架构正是为了解决这两种缺陷而设计的。
架构2:混合式RAG
混合式RAG体现了实践者在实际应用中总结出的经验:语义相似度与精确关键词匹配都是判断相关性的重要依据,但单独使用其中任何一种都不够。
密集检索,即向量搜索,擅长处理同义表达——即便查询的是服务等级协议相关内容,它仍能找到关于黄金级服务可用性的相关段落。但对于专有名词、产品代码、医学术语、法律引用以及其他那些含义完全取决于精确字符串的表达,它的表现就不如人意了。
稀疏检索(如BM25或关键词搜索)则恰恰相反。它在精确匹配术语方面表现极佳,搜索“SKU-4829”时总能找到对应的条目,但由于没有语义等价的概念,因此无法将“SLA”与“服务可用性保证”关联起来。
混合RAG同时并行执行两次搜索,然后通过重排器将合并后的结果转换为有序列表。重排器是其中的关键组件。嵌入模型会分别独立比较查询与文本片段,而交叉编码器重排器则会同时读取查询与每个候选项,并直接为二者之间的关系生成得分。虽然这种方式比相似度搜索慢,但由于仅处理已筛选过的候选集,因此延迟仍可控制在合理范围。
代码实现
LlamaIndex版本结合了密集检索器、BM25检索器以及互反排名融合技术:
# ============================================================
# HYBRID RAG — LLAMAINDEX IMPLEMENTATION
# pip install llama-index-retrievers-bm25 rank-bm25
# ============================================================
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex
from llama_index.core.retrievers import VectorIndexRetriever
from llama_index.retrievers.bm25 import BM25Retriever
from llama_index.core.retrievers import QueryFusionRetriever
from llama_index.core.query_engine import RetrieverQueryEngine
from langchain_core.tools import tool
# Build document store and index as before
documents = SimpleDirectoryReader("./data").load_data()
index = VectorStoreIndex.from_documents(documents)
# ── DENSE RETRIEVER (vector similarity) ──────────────────────
vector_retriever = VectorIndexRetriever(
index=index,
similarity_top_k=5, # Fetch more candidates pre-rerank
)
# ── SPARSE RETRIEVER (BM25 keyword match) ────────────────────
# BM25Retriever builds directly from the index's nodes
bm25_retriever = BM25Retriever.from_defaults(
index=index,
similarity_top_k=5,
)
# ── FUSION: Merge both retrievers with Reciprocal Rank Fusion ─
# QueryFusionRetriever is LlamaIndex's built-in hybrid combiner
# mode="reciprocal_rerank" implements the RRF algorithm:
# it combines ranked lists without needing score calibration
hybrid_retriever = QueryFusionRetriever(
retrievers=[vector_retriever, bm25_retriever],
similarity_top_k=3, # Final top-k after fusion
num_queries=1, # No query expansion in this mode
mode="reciprocal_rerank", # The fusion algorithm
use_async=True, # Run both retrievers in parallel
)
# Build a QueryEngine on top of the hybrid retriever
hybrid_query_engine = RetrieverQueryEngine.from_args(
retriever=hybrid_retriever,
)
# ── LANGGRAPH TOOL ────────────────────────────────────────────
@tool
def search_hybrid(query: str) -> str:
"""Search internal knowledge base using both semantic similarity
and keyword matching. More precise than pure vector search,
especially for technical terms, product codes, and exact names."""
response = hybrid_query_engine.query(query)
return str(response)
# The LangGraph graph structure is identical to Naive RAG.
# You are only swapping the tool. Everything from Module 4 onward
# stays exactly the same.
混合检索下的分块大小
在混合搜索中,通常会为比纯向量搜索更小的分块建立索引。与长分块相比,BM25能从短分块中重复出现的术语那里获得更强烈的信号。当BM25被纳入处理流程后,256个标记且重叠30个标记的设置是一个合理的初始选择。
何时适合使用混合检索
只要文档中同时包含自由形式的文字和结构化的术语,就应使用混合检索,这几乎涵盖了所有重要的企业应用场景。例如法律审查中需要完全匹配的引用格式,充满精确药物名称、剂量代码和ICD代码的医疗记录,包含股票代码和条款标识符的财务分析,以及包含错误代码、API方法名称和版本号的技术文档。
架构3:图式RAG
Graph RAG重新定义了知识的单位。在传统的RAG和混合式RAG中,文档仅仅是文本,检索系统会挑选与查询内容最为相似的段落。而在Graph RAG中,文档变成了由关系相连的实体,检索系统则沿着该网络中的路径进行查找。
试想一下,如果旧版认证模块被废弃,哪些企业客户会受到影响。一个简单的系统会查找同时出现“企业客户”和“认证模块”字样的内容,或许能找到一些相关条目。但实际上需要的是一条完整的链条:从该模块到依赖它的产品,再从这些产品到使用它们的客户。没有任何一条单独的内容能给出答案,这个答案需要从知识图谱的结构中才能得出。Graph RAG正是为这种情况设计的,无论是需要跨越多个链接、追踪某种关系,还是回答需要从语料库中相距甚远的部分获取事实的问题,它都能应对。
索引的构建方式
与将文档分割并嵌入片段的不同,图RAG首先对每份文档进行实体提取。它识别出命名实体(产品、人物、组织、概念)以及它们之间的关系,如“依赖于”“是……的客户”“由……创作”或“被……取代”。这些实体和关系便构成了知识图谱的节点与边。随后,图聚类算法会将关系密切的实体归类到同一组中,再由大语言模型为每组生成摘要。在查询时,系统会搜索这些组汇总的摘要,并通过图结构找到相关的实体。
代码实现
LlamaIndex的属性图索引负责处理实体提取流程:
# ============================================================
# GRAPH RAG — LLAMAINDEX PROPERTY GRAPH INDEX
# pip install llama-index-core
# ============================================================
from llama_index.core import SimpleDirectoryReader, PropertyGraphIndex
from llama_index.core.indices.property_graph import (
ImplicitPathExtractor,
SimpleLLMPathExtractor,
)
from langchain_core.tools import tool
# Load documents
documents = SimpleDirectoryReader("./data").load_data()
# ── BUILD THE PROPERTY GRAPH INDEX ───────────────────────────
# LlamaIndex's PropertyGraphIndex handles the full extraction pipeline.
# SimpleLLMPathExtractor uses an LLM to extract (subject, relation, object)
# triples from each chunk - these become graph edges.
# ImplicitPathExtractor uses fast heuristics (cheaper, less precise).
index = PropertyGraphIndex.from_documents(
documents,
kg_extractors=[
# LLM-based extraction: higher quality, higher cost
SimpleLLMPathExtractor(
llm=Settings.llm,
max_paths_per_chunk=10,
),
# Heuristic extraction: fast fallback
ImplicitPathExtractor(),
],
show_progress=True,
)
# ── CREATE A GRAPH-AWARE RETRIEVER ────────────────────────────
# This retriever traverses the graph rather than searching flat vectors.
kg_retriever = index.as_retriever(
include_text=True, # Include surrounding text with each entity
retriever_mode="hybrid", # Combines keyword + embedding on the graph
similarity_top_k=3,
)
from llama_index.core.query_engine import RetrieverQueryEngine
graph_query_engine = RetrieverQueryEngine.from_args(retriever=kg_retriever)
# ── LANGGRAPH TOOL ────────────────────────────────────────────
@tool
def search_knowledge_graph(query: str) -> str:
"""Search the knowledge graph for questions involving relationships
between entities - dependencies, organizational hierarchies, impact
analysis, and multi-hop reasoning across connected information.
Use this when the answer requires tracing a chain of relationships
rather than finding a single relevant document."""
response = graph_query_engine.query(query)
return str(response)
# ── COMBINING WITH NAIVE RAG: DUAL-TOOL AGENT ─────────────────
# In practice, Graph RAG and Naive RAG are often combined.
# The agent's LLM decides which tool fits the query.
@tool
def search_knowledge_base(query: str) -> str:
"""Search for factual information in company documents.
Best for direct questions with answers in a single document."""
response = query_engine.query(query) # The naive query engine
return str(response)
# Give the LangGraph agent BOTH tools.
# It will route to the right one based on the question type.
tools = [search_knowledge_base, search_knowledge_graph]
llm_with_tools = llm.bind_tools(tools)
tool_node = ToolNode(tools)
# Graph assembly remains identical - only the tool list changes.
关于成本的警告
实体提取会针对每个数据块调用大型语言模型,因此大量的文档意味着在索引时需要数千次调用。按设计而言,这种方式成本高昂且速度缓慢:其代价是一次性获得更丰富的数据结构。图式RAG索引绝不应在每次启动时都重新构建,而应像传统版本那样保存图结构后再重新加载。
何时选择它
当问题需要追踪各种关系链时,可选择图式RAG。典型应用场景包括合规性与风险分析(某项法规会影响哪些流程)、供应链分析(哪些产品依赖于某个供应商)、组织结构认知(谁拥有什么以及这些所有权关系如何关联),以及软件依赖关系映射(移除某个模块后哪些功能会出问题)。
架构4:高级RAG
高级RAG并非一种全新的技术,而是一系列结构化的改进措施,这些措施叠加在现有的基础检索系统之上。普通RAG依赖第一次检索结果,而高级RAG则会对其提出质疑、进行优化与验证。这些改进措施可分为三类,分别应用于检索之前、查询处理过程中以及检索之后。
检索之前:查询重写与HyDE
用户输入的内容往往并非最佳的搜索查询语句。对于企业客户的退货政策这类问题,人类自然会直接提出,但这样的查询得到的结果往往不如“企业客户退货及退款政策流程”这样的精确短语。查询重写技术能让大语言模型在查询进入索引之前将其转换为更合适的搜索语句。
HyDE是Hypothetical Document Embedding的缩写,采用另一种方法。它不直接用问题进行搜索,而是要求大语言模型撰写一份能够回答该问题的假设性文档,然后再对这份文档进行嵌入以用于搜索。其核心思路在于:具有答案形态的文本在向量空间中与其他答案形态的文本距离更近,而与问题形态的文本距离较远。
查询分解
多步骤问题会让简单的RAG方法无法应对,因为这类问题需要多次检索。先进的RAG方法会先对这些问题进行分解。例如,要求比较企业客户和零售客户的退款政策并总结主要差异,实际上包含三个任务:检索企业客户的政策、检索零售客户的政策、然后对两者进行比较。通过分解,这类问题会被拆分成多个子查询,分别进行检索,再将结果合并后再进行综合处理。
检索之后:重排序与CRAG
重排序已在混合搜索中提及,此时再次适用:交叉编码器会对已获取的片段重新评分,即便检索过程完全是密集型的。
CRAG(校正型RAG)是最复杂的检索后处理技术。在获取片段后,它会快速检查这些片段的相关性;如果发现其质量不足,就会转向其他数据源,比如网络搜索或更广泛的索引,从而避免让大语言模型在薄弱的信息背景下作答。
代码实现
有一种实现方式将多种此类技术整合在一起:
# ============================================================
# ADVANCED RAG — MULTI-TECHNIQUE IMPLEMENTATION
# ============================================================
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex
from llama_index.core.query_engine import (
SubQuestionQueryEngine, # Handles query decomposition
RetrieverQueryEngine,
)
from llama_index.core.tools import QueryEngineTool, ToolMetadata
from llama_index.core.postprocessor import SentenceTransformerRerank
from langchain_core.tools import tool
documents = SimpleDirectoryReader("./data").load_data()
index = VectorStoreIndex.from_documents(documents)
# ── RERANKER: Post-retrieval cross-encoder scoring ────────────
# Fetch 8 candidates, rerank down to 3
# This is the most impactful single improvement in Advanced RAG
reranker = SentenceTransformerRerank(
model="cross-encoder/ms-marco-MiniLM-L-2-v2",
top_n=3,
)
# ── RETRIEVER WITH RERANKING ──────────────────────────────────
base_retriever = index.as_retriever(similarity_top_k=8)
reranked_engine = RetrieverQueryEngine.from_args(
retriever=base_retriever,
node_postprocessors=[reranker], # Applied after retrieval
)
# ── QUERY DECOMPOSITION with SubQuestionQueryEngine ───────────
# Wrap the base engine as a "tool" that the decomposer can call
engine_tools = [
QueryEngineTool(
query_engine=reranked_engine,
metadata=ToolMetadata(
name="company_knowledge_base",
description=(
"Searches company documents for policies, procedures, "
"and product information."
),
),
)
]
# SubQuestionQueryEngine decomposes complex questions into
# sub-questions, runs each against the available tools,
# and synthesizes a final answer from all sub-answers
decomposed_engine = SubQuestionQueryEngine.from_defaults(
query_engine_tools=engine_tools,
use_async=True, # Sub-queries run in parallel when possible
)
# ── HYDE: Hypothetical Document Embedding ─────────────────────
from llama_index.core.indices.query.query_transform.base import (
HyDEQueryTransform,
)
from llama_index.core.query_engine import TransformQueryEngine
hyde_transform = HyDEQueryTransform(include_original=True)
hyde_engine = TransformQueryEngine(
query_engine=reranked_engine,
query_transform=hyde_transform,
)
# ── LANGGRAPH TOOLS: Different engines for different patterns ──
@tool
def search_with_decomposition(query: str) -> str:
"""Search for answers to complex questions that may require
combining information from multiple documents. Automatically
breaks the question into sub-questions and merges the results.
Best for comparison questions, multi-part questions, and anything
requiring synthesis across different topics."""
response = decomposed_engine.query(query)
return str(response)
@tool
def search_with_hyde(query: str) -> str:
"""Search the knowledge base using hypothetical document embedding.
More effective than standard search for abstract or exploratory
questions where the exact terminology in the answer differs from
the terminology in the question."""
response = hyde_engine.query(query)
return str(response)
# The LangGraph agent now has specialized retrieval tools
# and its LLM decides which retrieval strategy each question needs.
tools = [search_with_decomposition, search_with_hyde]
需遵循的规则
避免将所有方法都尝试一遍。应选择最有可能解决你所遇到的问题的那一种方法。通常首先应进行重新排序,之后再考虑分解方法;对于抽象或概念性集合,HyDE则是不错的第三选择。逐步构建模型,并在每一步之后检查其表现。
架构5:智能代理RAG
智能代理RAG不仅提升了检索效果,还将检索决策权交给了其他实体。前四种架构都属于固定流程,在每次运行时行为一致。而在这种架构中,检索变成了一个循环:LLM代理负责决定要搜索什么、评估搜索结果、判断是否需要再次搜索,一旦获得足够信息或认定问题无解便会停止搜索。
这正是LangGraph的设计目的。智能体节点、工具节点以及条件边已经构成了一个智能体RAG框架。变化在于工具集的丰富度以及围绕其构建的路由逻辑的复杂程度。
同时使用多种检索工具
LangGraph中智能体RAG的真正优势在于,可以同时为智能体提供上述所有检索策略,由其LLM为每个子问题选择合适的工具。该框架让同一个智能体能够使用全部五种架构:
# ============================================================
# AGENTIC RAG — COMPLETE MULTI-TOOL TEMPLATE
# This is the full production scaffold: all five architectures
# available to a single agent, which selects dynamically.
# ============================================================
# ── MODULE 1: IMPORTS & CONFIGURATION ───────────────────────
import os
from typing import Literal, Annotated
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage, BaseMessage
from langchain_core.tools import tool
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode
from langgraph.checkpoint.memory import MemorySaver
from llama_index.core import (
Settings, SimpleDirectoryReader, VectorStoreIndex,
StorageContext, load_index_from_storage, PropertyGraphIndex
)
from llama_index.core.indices.property_graph import SimpleLLMPathExtractor
from llama_index.core.postprocessor import SentenceTransformerRerank
from llama_index.core.query_engine import RetrieverQueryEngine, SubQuestionQueryEngine
from llama_index.core.tools import QueryEngineTool, ToolMetadata
from llama_index.llms.openai import OpenAI as LlamaOpenAI
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.retrievers.bm25 import BM25Retriever
from llama_index.core.retrievers import QueryFusionRetriever, VectorIndexRetriever
# The agent's main reasoning model (LangGraph)
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# LlamaIndex internal configuration
Settings.llm = LlamaOpenAI(model="gpt-4o-mini", temperature=0.1)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
Settings.chunk_size = 512
Settings.chunk_overlap = 50
# ── MODULE 2: STATE ──────────────────────────────────────────
class AgentState(MessagesState):
# Extend with retrieval metadata if you need observability
retrieval_count: int = 0 # Track how many retrieval calls were made
# ── MODULE 3: KNOWLEDGE BASES (all built at startup) ──────────
VECTOR_DIR = "./storage/vector"
GRAPH_DIR = "./storage/graph"
documents = SimpleDirectoryReader("./data").load_data()
# Vector index (for Naive, Hybrid, Advanced)
if os.path.exists(VECTOR_DIR):
ctx = StorageContext.from_defaults(persist_dir=VECTOR_DIR)
vector_index = load_index_from_storage(ctx)
else:
vector_index = VectorStoreIndex.from_documents(documents, show_progress=True)
vector_index.storage_context.persist(persist_dir=VECTOR_DIR)
# Property Graph index (for Graph RAG)
if os.path.exists(GRAPH_DIR):
ctx = StorageContext.from_defaults(persist_dir=GRAPH_DIR)
graph_index = load_index_from_storage(ctx)
else:
graph_index = PropertyGraphIndex.from_documents(
documents,
kg_extractors=[SimpleLLMPathExtractor(llm=Settings.llm)],
show_progress=True,
)
graph_index.storage_context.persist(persist_dir=GRAPH_DIR)
# ── Set up individual engines ─────────────────────────────────
reranker = SentenceTransformerRerank(
model="cross-encoder/ms-marco-MiniLM-L-2-v2", top_n=3
)
# Tool 1: Naive / vector search
vector_engine = vector_index.as_query_engine(
similarity_top_k=3,
node_postprocessors=[reranker],
)
# Tool 2: Hybrid (vector + BM25)
hybrid_retriever = QueryFusionRetriever(
retrievers=[
VectorIndexRetriever(index=vector_index, similarity_top_k=5),
BM25Retriever.from_defaults(index=vector_index, similarity_top_k=5),
],
similarity_top_k=3,
mode="reciprocal_rerank",
use_async=True,
)
hybrid_engine = RetrieverQueryEngine.from_args(
retriever=hybrid_retriever,
node_postprocessors=[reranker],
)
# Tool 3: Graph traversal
graph_engine = graph_index.as_query_engine(
include_text=True,
retriever_mode="hybrid",
similarity_top_k=3,
)
# Tool 4: Decomposed (for complex multi-part questions)
decomposed_engine = SubQuestionQueryEngine.from_defaults(
query_engine_tools=[
QueryEngineTool(
query_engine=vector_engine,
metadata=ToolMetadata(
name="docs",
description="Company documents and policies."
)
)
],
use_async=True,
)
# ── MODULE 3 (cont.): ALL TOOLS ───────────────────────────────
@tool
def search_documents(query: str) -> str:
"""Search company documents by semantic meaning. Best for
conceptual questions where the exact wording in the answer
may differ from the question."""
return str(vector_engine.query(query))
@tool
def search_exact_terms(query: str) -> str:
"""Search using both keyword and semantic matching. Best when
the query contains specific terminology, product codes, names,
or exact phrases that must appear in the result."""
return str(hybrid_engine.query(query))
@tool
def search_relationships(query: str) -> str:
"""Search the knowledge graph for questions about how things
connect: dependencies, impact chains, organizational links,
and multi-hop reasoning. Use when the answer requires tracing
a relationship across multiple entities."""
return str(graph_engine.query(query))
@tool
def search_complex_question(query: str) -> str:
"""For multi-part questions requiring synthesis across several
topics. Automatically decomposes the question into sub-queries,
retrieves each independently, and combines the results."""
return str(decomposed_engine.query(query))
tools = [
search_documents,
search_exact_terms,
search_relationships,
search_complex_question,
]
llm_with_tools = llm.bind_tools(tools)
tool_node = ToolNode(tools)
# ── MODULE 4: NODES ──────────────────────────────────────────
def agent_node(state: AgentState) -> dict:
system_prompt = SystemMessage(content=(
"You are a precise research assistant with access to four retrieval tools:\n\n"
"1. search_documents - semantic search over company documents\n"
"2. search_exact_terms - hybrid semantic + keyword search\n"
"3. search_relationships - graph traversal for relationship questions\n"
"4. search_complex_question - decomposed retrieval for multi-part questions\n\n"
"Think step by step. Use the tool that best fits the question type. "
"You may call multiple tools if a question has multiple parts. "
"Only answer when you have retrieved sufficient evidence."
))
response = llm_with_tools.invoke([system_prompt] + state["messages"])
return {
"messages": [response],
"retrieval_count": state.get("retrieval_count", 0),
}
# ── MODULE 5: ROUTING ────────────────────────────────────────
def should_continue(state: AgentState) -> Literal["tools", "__end__"]:
last = state["messages"][-1]
if hasattr(last, "tool_calls") and last.tool_calls:
return "tools"
return "__end__"
# ── MODULE 6: GRAPH ASSEMBLY ─────────────────────────────────
builder = StateGraph(AgentState)
builder.add_node("agent", agent_node)
builder.add_node("tools", tool_node)
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", should_continue,
{"tools": "tools", "__end__": END})
builder.add_edge("tools", "agent")
graph = builder.compile(checkpointer=MemorySaver())
# ── MODULE 7: ENTRYPOINT ──────────────────────────────────────
if __name__ == "__main__":
config = {"configurable": {"thread_id": "agentic-session-001"}}
print("Agentic RAG ready. Ask anything.\n")
while True:
user_text = input("You: ").strip()
if not user_text or user_text.lower() in ("exit", "quit"):
break
response = graph.invoke(
{"messages": [HumanMessage(content=user_text)]},
config=config,
)
print(f"\nAgent: {response['messages'][-1].content}\n")
管道的局限性
它与其他系统的不同之处在于具备自我修正能力。管道无法察觉自己获取了错误的资料,而智能体可以。当初始结果不够充分时,它会通过新的查询词重新构建查询语句;当问题存在依赖关系时,智能体会在处理到一半时发现这一问题并进一步获取所需信息;而对于模糊的问题,它则会在搜索前先向用户询问。
在合规、法律、金融或医疗等领域,错误答案可能带来严重后果,此时应采用这种架构,因为验证步骤、置信度阈值以及人工干预检查点都可以直接融入智能体架构中。
延迟代价
经济放缓的事实已无法掩盖。单工具流程的响应时间大约在200到500毫秒之间;而需要先进行三次检索的智能体则可能需要8到12秒,这样的速度对于许多实时接口的主流程来说实在过慢。有两种方法可以缓解这一问题:一是实时展示中间处理步骤,让用户能够看到进展而非默默等待;二是在后台运行智能体检索,预先获取可能的后续所需信息。
选择与组合
对于每一个检索问题,这五种方法中都有合适的解决方案。大致的决策思路如下:对于简单、基于事实的文档问答,可从基础的RAG开始;当涉及专业术语或精确标识符时,再采用混合式方法;如果答案取决于文档间的关联关系,则应使用图结构RAG;当需要提升检索质量时,可加入更高级的技术;而对于那些开放性强、风险较高或需要自我修正的任务,则应采用智能体方案。
这些架构也是可组合的,因此无需选择其中一个而舍弃其余的。最常见的生产模式是堆叠结构。在LangGraph中,这种堆叠结构直接对应工具列表。一个拥有基础工具、混合工具、图工具以及分解查询工具的智能体,正是由这五种架构构成的堆叠结构,其中智能体作为最顶层,在每一步中从其他工具中选择并协调它们的使用。
关键词参考卡
这张卡片列出了每一层的主要类别和设置:
NAIVE RAG
SimpleDirectoryReader Load files into Document objects
VectorStoreIndex.from_documents Build the embed-and-store index
index.as_query_engine() Full retrieve-and-answer pipeline
index.as_retriever() Retrieve only (no answer generation)
Settings.chunk_size Token size per Node
Settings.chunk_overlap Token overlap between adjacent Nodes
HYBRID RAG
BM25Retriever Keyword-based sparse retriever
VectorIndexRetriever Dense embedding retriever
QueryFusionRetriever Merges multiple retrievers (RRF algorithm)
SentenceTransformerRerank Cross-encoder reranker for post-retrieval
GRAPH RAG
PropertyGraphIndex Builds a knowledge graph from documents
SimpleLLMPathExtractor LLM-based entity and relation extraction
ImplicitPathExtractor Heuristic-based entity extraction (fast)
ADVANCED RAG
SubQuestionQueryEngine Decomposes complex queries into sub-queries
HyDEQueryTransform Hypothetical Document Embedding transform
TransformQueryEngine Wraps any engine with a query transform
node_postprocessors Where rerankers and filters attach
AGENTIC RAG (LANGGRAPH LAYER)
@tool The bridge - every LlamaIndex engine becomes
a LangGraph tool through this decorator
ToolNode Executes whatever tool the agent selects
bind_tools() Gives the agent LLM its tool registry
MemorySaver / SqliteSaver Thread-level memory across turns (Part 2)
interrupt() Human approval checkpoint before retrieval (Part 3)
检索作为基础设施
这五种方法并非实现同一功能的不同途径,而是针对五种不同检索问题的解决方案,按从简单到复杂的顺序依次呈现。
- 朴素RAG:快速、低成本,非常适合大多数普通文档的问答场景。
- 混合RAG:当词汇表具有专业性时,这是合理的默认选择。
- 图结构RAG:当事实之间的关联比单个文档更重要时,应选用此方法。
- 高级RAG:用于在检索质量存在缺陷时提升准确率。
- 智能代理RAG:适用于需要系统自主推理的开放式、高风险任务。
将LlamaIndex用于数据层,LangGraph用于任务协调,这些方案几乎能满足生产环境中的所有检索应用需求。