← 返回学习中心 · 知识库与科研OiOi Academy
AGENT 理论 · 23

RAG 演进之路:从检索到上下文图

@Ai_Vaidehi 的一句话对比打底,@0xMorlex 用约 150 行代码走完 9 步路线图。

来源 @Ai_Vaidehi / @0xMorlex 形式 X 帖 + X 长文 发布 2026-08-24 / 2026-08-10
💡

开篇:一句话建立直觉

先看 @Ai_Vaidehi(2026-08-24)的极简对比,建立整课的直觉:

RAG Agentic RAG
流程 取相关文档 → 喂给 LLM → 生成答案 重写你的 query → 判断缺什么信息 → 选择最佳数据源(DB / API / web)→ 迭代直到答案真正够好
形态 线性(Linear)、单次(One-shot) 动态(Dynamic)、迭代(Iterative)
驱动 有限思考 目标驱动(Goal-driven)
记住:"RAG retrieves information. Agentic RAG thinks about how to retrieve better information."(RAG 检索信息;Agentic RAG 思考如何更好地检索信息。)

原文结论:对做 AI 产品的人,这个转变不是可选项,而是必然(inevitable)。

原文配图:RAG vs Agentic RAG 对比图
原文配图:RAG vs Agentic RAG 对比图

但"迭代检索"只是演进的一条线。另一条更硬核的线是 @0xMorlex(2026-08-10)给出的:不用框架、约 150 行代码,从 RAG 到可遍历、可引用的上下文图,9 步实现路线图(含完整代码)。本课主体就按这 9 步展开。

🧱

RAG 的结构性局限:多跳查询

以下来自 @0xMorlex。先看 RAG 答不上来的问题:

"如果 Redis 故障,什么会出问题?"

RAG 只能找出含"Redis"词的句子,但答案分散在四份互不关联的文档中——登录流程依赖认证服务,认证服务依赖令牌缓存,令牌缓存后端存于 Redis。相似度搜索无法跨文档跳跃。

避坑
这不是调优问题。嵌入能检索相关文本,但拿不到"事物本身",也拿不到构成答案的关联关系。多跳查询在结构上就不适合 RAG。
高中生类比
就像查字典 vs 侦探破案:RAG 是查字典——给你所有含"Redis"的词条;你要的却是侦探的案情板——"谁依赖谁"的红线关系网。字典再厚也长不出红线。

解决方案:停止获取段落,转而获取图

实体为节点、关系为边、答案为路径。9 步分三级:Tier 1 提取 → Tier 2 连接 → Tier 3 穿越。

原文配图:三级路线图总览
原文配图:三级路线图总览
⛏️

Tier 1:提取——从散文到三元组

01.从 RAG 答不上来的问题入手

失败查询的特征决定图中需要什么实体与关系。单跳查询不需要图——这一步是过滤器,不是仪式。

02.拉取实体与关系三元组,而非嵌入向量

用正则从句子中提取 (主题, 关系, 对象) 三元组,chunk_id 随三元组保留,保证可引用性:

PATTERNS = [
    (re.compile(r"(.+?) depends on (.+)", re.I),   "depends_on"),
    (re.compile(r"(.+?) is backed by (.+)", re.I), "backed_by"),
    (re.compile(r"(.+?) runs on (.+)", re.I),      "runs_on"),
    (re.compile(r"(.+?) is owned by (.+)", re.I),  "owned_by"),
]

def extract(chunk_id: str, text: str) -> list[Triple]:
    out = []
    for sentence in re.split(r"[.\\n]", text):
        s = sentence.strip()
        if not s:
            continue
        for rx, rel in PATTERNS:
            m = rx.match(s)
            if not m:
                continue
            subj = m.group(1)
            for obj in re.split(r"\\band\\b", m.group(2)):
                obj = obj.strip()
                if obj:
                    out.append(Triple(subj.strip(), rel, obj, chunk_id))
            break
    return out

03.规范化 canon:第一道门

转小写、去冠词、合并空白、应用别名映射:

def canon(name: str) -> str:
    n = name.strip().lower()
    n = re.sub(r"^the\\s+", "", n)
    n = re.sub(r"\\s+", " ", n)
    n = re.sub(r"[.]$", "", n)
    return ALIASES.get(n, n)

原文说这是"第一道门",也是实体解析问题的缩影:处理不当,图会碎成互不连通的片段——所有节点都在,但彼此没有关联,自制的 GraphRAG 就什么也返回不了。

原文配图:Tier 1 提取示意
原文配图:Tier 1 提取示意
🔗

Tier 2:连接——边、权重与来源

04.构建双向边列表

图由 fwd / rev 两个邻接映射组成:正向遍历回答"X 依赖什么",反向遍历回答"什么依赖 X"。内存翻倍,但避免单向搜索死角。

05.边权重按来源相互印证计数

_merge:同一条边再次出现则 weight + 1。两个来源都认可的边权重为 2——印证本身就是信号。

06.每条边附来源信息 provenance:第二道门

def cite_path(g: ContextGraph, path: list[str]) -> list[str]:
    lines = []
    for a, b in zip(path, path[1:]):
        for e in g.fwd.get(a, []):
            if e.dst == b:
                src = ",".join(sorted(e.sources))
                lines.append(f"{a} -{e.rel}-> {b}  [{src}]  x{e.weight}")
                break
    return lines

输出长这样:登录流程 -依赖于-> 认证服务 [c1] x1 ——三次跳跃,三次引用,其中一次被两个来源印证(x2)。

记住:"A context graph with no citation is RAG with extra steps."(没有引用的上下文图,只是多几步的 RAG。)来源是"第二道门":一个无法引用其边的图,不过是一堆包装更精美的断言。
原文配图:Tier 2 连接示意
原文配图:Tier 2 连接示意
🧭

Tier 3:穿越——检索路径而非片段

07.k 跳邻域检索

neighborhood(g, start, hops):上下文 = 实体 k 步内的子图,而不是 top-k 片段。从"找最像的几段话"变成"圈出一片关系网"。

08.路径排序而非通道排序:第三道门

多跳问题用反向可达性解:Redis 故障 → 沿反向边找出所有会一同故障的节点。impacted_by 做 BFS,返回的不是文档排序列表,而是因果链路径的排序列表——每条路径都可查看、可验证。

原文的运行结果:3 样东西会损坏(登录流程 → 认证服务 → 令牌缓存 → Redis 的完整链条),4 样能存活(计费系统、平台团队、PostgreSQL、用户数据库)。

记住:"存活列表"是片段检索器永远给不出的答案:语料库里根本没写"计费系统没坏",这个信息只能从结构中看出来。图能同时给出"损坏"与"存活"两组,RAG 给不出后者。
高中生类比
就像停电查线路:RAG 告诉你"3 号楼提过停电";上下文图给你整张电网拓扑——哪条线断了影响哪几栋、哪几栋还在电网上,一目了然。

09.组装子图交模型

最后一步:把答案路径上的节点 + 连接它们的边 + 每条边的块标识一起装进上下文窗口。模型的任务从"从这堆信息中找到答案"变为"阅读这段内容并表达出来"。

原文说这才是真正的收获:最难的部分——把零散事实联系起来——已经从 prompt 里抽出来,放进了一个可查看的结构。模型不再需要猜测联系,直接读取就行。

原文配图:Tier 3 穿越示意
原文配图:Tier 3 穿越示意

三道门的位置(原文总结)

  • 规范化终结 Tier 1(散文 → 有序的实体与关系);
  • 来源终结 Tier 2(实体关系 → 带权重、可引用的图);
  • 排序路径终结 Tier 3(图 → 可阅读、可验证的答案路径)。

作者用 8 个单事实片段建图,同一问题跑片段检索 vs 图遍历:相同语料库,一个检索工具找到了相关词汇,另一个找到了答案。结论:RAG 检索出与答案相关的内容,图直接获取答案。

🛑

何时停在 RAG:诚实结论

@0xMorlex 的结论非常诚实,值得全文引用:

  • 多数检索是单跳——单跳用 RAG 更简单、更便宜、更正确;
  • 测试是第 1 步:先判断你最难的问题是 lookup(查)还是 chain(链);
  • 如果通过测试,再小步构建:一种关系类型、一张规范表、一次遍历;
  • 顺序重要:提取 → 连接 → 穿越,别跳步。
记住:决策流程:问题是单跳 → RAG;是多跳链条 → 先做步骤 01 的测试(失败查询长什么样)→ 通过则按 9 步小步构建。不要为"图"而图。

回到 @Ai_Vaidehi 的判断:对做 AI 产品的人,从 RAG 到 Agentic RAG / 上下文图的转变不是可选项而是必然——因为真实世界的答案本来就住在关系里,不住在段落里。

词

术语翻卡:点卡片看解释

Agentic RAG
Agentic RAG
会思考"如何更好地检索"的 RAG:重写 query、选数据源、迭代到答案达标。
上下文图
context graph
实体为节点、关系为边、答案为路径的知识结构,可遍历、可引用。
三元组
triple
(主题, 关系, 对象),如(认证服务, 依赖, 令牌缓存),图的最小单元。
规范化
canonicalization
把"认证服务 / The Authentication Service"合并成同一节点;第一道门。
来源
provenance
每条边附带的出处块标识;没有引用的图只是多几步的 RAG。
k 跳邻域
k-hop neighborhood
实体 k 步内的子图;Tier 3 用子图而非 top-k 片段做上下文。
多跳查询
multi-hop query
答案分散在多份文档、需跨文档跳跃的问题,如"Redis 故障影响谁"。
测

自测一下:点问题展开参考答案

"RAG retrieves information. Agentic RAG thinks about how to retrieve better information." RAG 是线性单次检索;Agentic RAG 会重写 query、判断缺什么信息、选择最佳数据源、迭代直到答案达标,是目标驱动的。

嵌入能检索相关文本,但拿不到"事物本身"和构成答案的关联关系。如"Redis 故障影响谁",答案分散在四份互不关联的文档中,相似度搜索无法跨文档跳跃——这不是调优能解决的。

规范化终结 Tier 1、来源终结 Tier 2、排序路径终结 Tier 3。因为 cite_path 让每条边都可引用、可验证;没有引用的图只是包装更精美的断言,无法区分"相关词汇"和"答案"。

"存活列表":3 样会损坏、4 样能存活(计费系统、平台团队、PostgreSQL、用户数据库)。语料库里没写"谁没坏",这个信息只能从图结构中看出来。