从"流水线"到"工具调用"的范式转移
第 1–10 章里我们学的是RAG 流水线:分块 → Embedding → 检索 → Rerank → 生成。这是预定义的、固定的步骤。
Agentic RAG 和 Context Engineering 共享一个更激进的想法:让 LLM 自己决定何时检索、检索什么、是否还要再检索一次——把"检索"从流水线节点降级为一种 tool_call。
传统 RAG 思维
"用户提问 → 我必须先去向量库找 5 个 chunk → 拼进 prompt → 让 LLM 回答"。流程是固定的。
Agentic 思维
"用户提问 → 让 LLM 看一眼 → 它觉得需要查就调 search 工具,觉得需要看具体文件就调 read_file,觉得查到的不够就再查一次。"流程是涌现的。
方向 ②:Agentic RAG
核心模式:自反思检索循环
Agentic RAG 最常见的实现是 Self-RAG / CRAG(Corrective RAG) 风格的循环:检索 → LLM 评估检索质量 → 不够好则重写查询再检索 → 直到满意才生成。
┌──────────────────────────────────────────────────┐
│ Agentic RAG 控制流 │
│ │
│ user_question │
│ │ │
│ ▼ │
│ [Plan Agent] 拆分子问题 │
│ │ │
│ ├──→ [Retrieve Agent] 向量检索 │
│ │ │ │
│ │ ▼ │
│ ├──→ [Grade Agent] 这些 chunk 真的有用吗? │
│ │ │ │
│ │ ┌───┴───┐ │
│ │ 够用 不够用 │
│ │ │ │ │
│ │ │ └──→ [Rewrite] 改写查询 │
│ │ │ ↑ │ │
│ │ │ └─────┘ 循环 │
│ │ ▼ │
│ └──→ [Synthesize Agent] 综合多源信息 │
│ │ │
│ ▼ │
│ 最终答案 │
└──────────────────────────────────────────────────┘
用 LangGraph 实现最小骨架
from langgraph.graph import StateGraph, END
from typing import TypedDict, List
from langchain_anthropic import ChatAnthropic
llm = ChatAnthropic(model="claude-sonnet-4-6")
class RAGState(TypedDict):
question: str
documents: List[str]
grade: str # "useful" | "not_useful"
rewrites: int # 重写次数防止死循环
def retrieve(state: RAGState) -> RAGState:
docs = vector_store.search(state["question"], top_k=5)
return {"documents": docs}
def grade_documents(state: RAGState) -> RAGState:
"""LLM 自评估检索质量"""
prompt = f"""问题:{state['question']}
检索到的文档:{state['documents']}
这些文档能回答问题吗?只回答 useful 或 not_useful。"""
grade = llm.invoke(prompt).content.strip().lower()
return {"grade": grade}
def rewrite_query(state: RAGState) -> RAGState:
"""检索失败时重写查询"""
prompt = f"""原问题:{state['question']}
检索失败。请改写问题,使其更容易匹配文档(例如换关键词、加领域术语)。"""
new_q = llm.invoke(prompt).content
return {"question": new_q, "rewrites": state["rewrites"] + 1}
def generate(state: RAGState) -> RAGState:
answer = llm.invoke(f"基于以下文档回答:\n{state['documents']}\n问题:{state['question']}")
return {"answer": answer.content}
def decide_next(state: RAGState) -> str:
if state["grade"] == "useful" or state["rewrites"] >= 3:
return "generate"
return "rewrite"
# 编排图
g = StateGraph(RAGState)
g.add_node("retrieve", retrieve)
g.add_node("grade", grade_documents)
g.add_node("rewrite", rewrite_query)
g.add_node("generate", generate)
g.set_entry_point("retrieve")
g.add_edge("retrieve", "grade")
g.add_conditional_edges("grade", decide_next, {"generate": "generate", "rewrite": "rewrite"})
g.add_edge("rewrite", "retrieve") # 闭环
g.add_edge("generate", END)
agentic_rag = g.compile()
result = agentic_rag.invoke({"question": "...", "rewrites": 0})
不是"它每次检索都更准"——单次检索质量并没有变。价值在于对失败的恢复力:朴素 RAG 召回 0 个相关文档时直接 GG,Agentic RAG 会自动重写查询、换关键词、再来一次。生产环境里召回失败是最大杀手,能在线挽回是真金白银的提升。
完整 LangGraph 教程见 LangGraph 教程,多 agent 编排见 AI Agent 教程。
Agentic RAG 的成本警告
每多一轮 grade/rewrite/retry 就多一次 LLM 调用。一个朴素 RAG 单次查询 1 次 LLM 调用,Agentic RAG 在最差情况下可能调用 5–8 次。成本上升 5–8 倍,延迟从 500ms 拉到 10s+。
实践原则:先用第 9 章的 RAGAS 评估你的朴素 RAG。如果 Faithfulness > 0.85、Context Precision > 0.7,多半不需要 Agentic。Agentic 是给"召回率本身就 < 50%"的硬场景准备的。
方向 ③:Context Engineering
它和 RAG 的根本区别
Context Engineering 不是一个具体技术,是 2024 下半年 Anthropic / Cursor / Cognition AI 提出的思维方式。一句话概括:
"LLM 上下文窗口里放什么、什么时候放、怎么组织,比训练数据和模型选择更重要。"
对应到工程:与其预先把所有文档切片、Embedding、放进向量库,不如给 Agent 一组工具,让它自己边读边决定。
Coding Agent 是 Context Engineering 的标杆
看一下 Claude Code、Cursor、Aider 这类 Coding Agent 怎么处理"代码库问答"——它们几乎不用向量库。
| 步骤 | 传统 RAG 的做法 | Coding Agent 的做法 |
|---|---|---|
| 预处理 | 对所有文件分块、Embedding、入库(耗时几十分钟) | 无预处理,直接读文件系统 |
| 定位文件 | 向量相似度找"最相关"的 5 个 chunk | 用 grep / find 搜索关键词、符号、文件名 |
| 读上下文 | 送 5 × 200 token 的 chunk 拼装 | 调用 read_file 工具读完整文件(或指定行号区间) |
| 跨文件追踪 | 跨 chunk 关联非常脆弱 | 调用 LSP / 代码符号工具找定义和引用 |
| 知识更新 | 文件改了要重新 Embedding | 每次都读最新文件,永远是新的 |
Cursor 的 @codebase 算混合方案——保留向量索引但同时给了 Agent 直接的文件读写工具。详见 Cursor 教程第 3 章 @codebase 向量检索原理。
Context Engineering 的最小骨架
from anthropic import Anthropic
import subprocess, json
client = Anthropic()
tools = [
{
"name": "grep",
"description": "在文件系统中搜索文本(精确匹配)",
"input_schema": {"type": "object",
"properties": {"pattern": {"type": "string"},
"path": {"type": "string"}},
"required": ["pattern"]}
},
{
"name": "read_file",
"description": "读取文件内容,可指定行号区间",
"input_schema": {"type": "object",
"properties": {"path": {"type": "string"},
"start": {"type": "integer"},
"end": {"type": "integer"}},
"required": ["path"]}
},
]
def execute_tool(name: str, args: dict) -> str:
if name == "grep":
out = subprocess.run(["grep", "-rn", args["pattern"], args.get("path", ".")],
capture_output=True, text=True)
return out.stdout[:3000]
if name == "read_file":
with open(args["path"]) as f:
lines = f.readlines()
s, e = args.get("start", 1) - 1, args.get("end", len(lines))
return "".join(lines[s:e])
def context_engineering_query(question: str) -> str:
"""不预建索引,让模型自己探索代码库"""
messages = [{"role": "user", "content": question}]
while True:
resp = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
tools=tools,
messages=messages,
)
if resp.stop_reason == "end_turn":
return resp.content[0].text
# 模型决定调用工具
tool_use = [b for b in resp.content if b.type == "tool_use"][0]
result = execute_tool(tool_use.name, tool_use.input)
messages.append({"role": "assistant", "content": resp.content})
messages.append({"role": "user", "content": [
{"type": "tool_result",
"tool_use_id": tool_use.id,
"content": result}
]})
能用:代码库(grep + read_file 自然适配)、可枚举的文件系统(个人 Obsidian 笔记、项目文档)、API 可调用的结构化数据源(Jira、Linear、Notion)。
不能用:千万级文档的客服知识库(grep 在 10TB PDF 上不是答案)、无法直接访问的私域语料、需要语义搜索的"模糊问题"("找跟产品定价策略相关的文档" — grep 啥关键词?)。
方向 ④:GraphRAG 与多模态检索
GraphRAG:给检索加上"关系"
纯向量检索回答不了"X 引用的论文里,哪些被 Y 又引用过"这种多跳关系查询。GraphRAG(微软 2024 年开源)把文档先抽成知识图谱,再做检索:
┌─────────────────────────────────────────────────┐
│ GraphRAG 索引阶段(一次性) │
│ │
│ 原始文档 ─→ LLM 抽取 ─→ (实体, 关系, 实体) │
│ (论文A) -[引用]-> (论文B) │
│ (作者X) -[发表]-> (论文A) │
│ │
│ ─→ 构建图(Neo4j) │
│ ─→ 社区检测(Leiden 算法)找出主题集群 │
│ ─→ LLM 给每个社区生成摘要 │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ GraphRAG 查询阶段 │
│ │
│ 用户问题 │
│ │ │
│ ├─→ Local Search:实体邻居遍历 │
│ │ 适合"X 的具体属性"类问题 │
│ │ │
│ └─→ Global Search:基于社区摘要 │
│ 适合"整个语料的主题趋势"类问题 │
└─────────────────────────────────────────────────┘
第 10 章 混合存储:向量 + 图数据库 给了 Neo4j 的最小代码。完整 GraphRAG 实现成本不低(实体抽取要消耗大量 LLM 调用),只在确实需要关系查询时上。
ColPali:视觉文档检索
朴素 RAG 处理 PDF 的最大痛点:分块切片把表格、公式、架构图的图注全切碎了。ColPali(2024 年 Faysse 等提出)走了另一条路——不做 OCR,不做分块,直接用视觉模型把 PDF 每一页编码成多向量,检索时用 late interaction 计算页面与查询的相似度。
朴素 RAG 处理 PDF
PDF → OCR/解析 → 分块 → 文本 Embedding → 检索文本 chunk
问题:表格被切碎、公式渲染丢失、图表完全丢失
ColPali 处理 PDF
PDF → 每页转图片 → ColPali 视觉编码 → 多向量索引 → 检索整页图像
优势:保留版式、表格、图表;输入 LLM 时直接送图片(多模态 LLM)
对于 PDF 密集的领域(医疗、法律、金融研报),这是当前最优解。完整教程见 ColPali 视觉文档检索教程,10 章覆盖架构原理、byaldi 上手、多向量索引、领域微调、ViDoRe 评估。
四个方向的组合:一个真实生产蓝图
用户问题: "对比2023和2024年我们和竞品在自动驾驶领域的专利布局"
│
▼
[Plan Agent] 拆分子问题
│
├─→ 子问题1: 我们公司的自动驾驶专利?
│ └─→ [GraphRAG Local Search](专利-公司图)
│
├─→ 子问题2: 竞品 A/B/C 的自动驾驶专利?
│ └─→ [GraphRAG Local Search]
│
├─→ 子问题3: 专利全文里的技术细节?
│ └─→ [ColPali 视觉检索](保留专利图、流程图)
│
├─→ 子问题4: 行业研报背景?
│ └─→ [Prompt Caching CAG](5 篇核心研报常驻缓存)
│
▼
[Synthesize Agent] 综合 → 输出对比报告
这就是 第 11 章 反复强调的"真实生产系统是混合体"。
三章总览:你现在拥有的工具箱
- 第 11 章:地图与决策树——什么时候改、改成哪条路
- 第 12 章 CAG / 长上下文:知识库小且稳定时跳过检索
- 第 13 章 Agentic RAG:召回率不够时让 Agent 自反思重检索
- 第 13 章 Context Engineering:可枚举的文件系统/代码库直接给工具
- 第 13 章 GraphRAG:关系密集的查询用图遍历
- 第 13 章 ColPali:PDF 版式密集的领域用视觉检索
本三章是导览。每个方向都有专门的教程深入:
📦 Anthropic API 第 6 章 Prompt Caching — CAG 工程化的完整 API
🤖 LangGraph 教程 — Agentic RAG 的标准编排框架
🧠 AI Agent 教程 — 多 agent 协作的整体方法论
👁 ColPali 视觉文档检索 — 10 章完整覆盖
🔍 Cursor @codebase 原理 — Context Engineering 的代表实现
🎯 DSPy 教程 — 用声明式编程优化检索流水线
📊 AI Evals 教程 — 任何方向都需要评估闭环
读完这三章不要立刻"全部重写成 Agentic"。先用第 9 章的 RAGAS 评估你现有 RAG,找出最大失败模式,再对症下药——
• Faithfulness 低(答案不忠实于文档)→ 先改 Prompt,可能根本不需要换架构
• Context Precision 低(检索一堆无关 chunk)→ 加 Reranker(第 6 章)或换 Agentic RAG
• Context Recall 低(关键文档没召回)→ 改进分块、混合检索,或换 ColPali / GraphRAG
• 多轮对话 token 烧太快 → 上 CAG / Prompt Caching
没有银弹,只有用对工具。