Chapter 13

Agentic RAG 与 Context Engineering

检索从"预处理流水线"变成了"运行时工具调用"。本章把 Agentic RAG、Context Engineering、GraphRAG、视觉检索四条新路线放在一起讲清楚,每条都给可运行的最小骨架。

从"流水线"到"工具调用"的范式转移

第 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 的成本警告

不要默认上 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 提出的思维方式。一句话概括:

Context Engineering 的核心断言

"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}
        ]})
Context Engineering 的适用边界

能用:代码库(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 章 反复强调的"真实生产系统是混合体"。

三章总览:你现在拥有的工具箱

站内深入路径

本三章是导览。每个方向都有专门的教程深入:

📦 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

没有银弹,只有用对工具