别急着上 Agentic RAG:先用 LangGraph 把最小 RAG 跑明白

学习了 LangGraph 之后,我用《天龙八部》做了一个最小 RAG:EPUB 切片、Embedding、Milvus 向量检索,再通过 LangGraph 串起 Retrieve 和 Generate。本文不堆 API,而是把 RAG 的数据流和 LangGraph 在其中扮演的角色彻底理顺。

前段时间刚把 LangGraph 的基础流程过了一遍。

State、Node、Edge、Conditional Edge......

单独看这些概念其实并不难,但学完之后很容易产生一个问题:

这些东西在真正的 AI 应用里,到底怎么串起来?

这次我拿 RAG 做了一个很小的 Demo。

知识库就一本《天龙八部》,向量数据库用 Milvus,流程用 LangGraph 编排。

最后要实现的事情也很简单:

markdown 复制代码
用户:阿朱的结局是什么?

        ↓

从《天龙八部》中找到相关片段

        ↓

把相关片段交给 LLM

        ↓

基于原文回答

代码跑通之后,我反而觉得这可能是理解 LangGraph 最合适的 Demo 之一。

因为它把几个之前比较抽象的概念一下串起来了。


一、先说清楚:这不是 GraphRAG

这里很容易出现一个术语误区。

我们现在做的是:

复制代码
LangGraph + RAG

并不能直接叫:

复制代码
GraphRAG

真正意义上的 GraphRAG,比如 Microsoft GraphRAG,会从原始文本中抽取实体和关系,构建知识图谱、社区层级和社区摘要,再利用这些结构完成检索。

而我们现在做的事情简单得多:

复制代码
StateGraph
   ↓
retrieve
   ↓
generate

也就是:

使用 LangGraph 编排一个传统 RAG 工作流。

这篇文章讨论的也是这个东西。


二、RAG 到底解决什么问题?

假设我直接问大模型:

复制代码
阿朱的结局是什么?

模型可能本身就知道。

但换个场景:

复制代码
我们公司去年 Q4 的退款规则是什么?

模型大概率不知道。

因为这份规则可能只存在于:

复制代码
公司 PDF
内部 Wiki
数据库
会议文档
私有知识库

LLM 有推理能力,但它没有天然访问这些私有数据的能力。

RAG 做的事情,本质上就是在模型回答之前增加一步:

复制代码
先查资料,再回答。

也就是 Retrieval-Augmented Generation:

diff 复制代码
Retrieval
检索

+

Generation
生成

一个最朴素的 RAG,可以浓缩成:

复制代码
用户问题
   ↓
检索相关文档
   ↓
把文档放进 Prompt
   ↓
LLM 回答

听起来非常简单。

但这里其实少了一半。

因为在"检索"之前,我们得先把知识库准备好。


三、一个完整 RAG,其实有两条流水线

这是我这次写 Demo 后最大的一个认知。

RAG 不应该只看成:

复制代码
问题 → 向量搜索 → LLM

它实际上有两个完全不同的阶段。

第一条:离线索引

复制代码
EPUB
 ↓
解析
 ↓
切分文本
 ↓
Embedding
 ↓
Vector
 ↓
Milvus

作用是:

把书变成可以被搜索的数据。


第二条:在线查询

css 复制代码
用户问题
 ↓
Embedding
 ↓
Milvus 相似度搜索
 ↓
Top K 文档片段
 ↓
Prompt
 ↓
LLM
 ↓
回答

作用是:

根据问题,从已经准备好的知识库中查资料。

把这两条线分开,整个 RAG 一下就清晰了。


四、第一步:先把《天龙八部》变成知识库

我的原始数据就是:

复制代码
天龙八部.epub

首先使用 EPubLoader 解析:

ini 复制代码
const loader = new EPubLoader(EPUB_FILE, {
  splitChapters: true,
});

const documents = await loader.load();

这一步之后,一整本书被拆成若干 Document

大概可以理解成:

css 复制代码
[  {    pageContent: "第一章内容..."  },  {    pageContent: "第二章内容..."  }]

但问题来了。

一整章还是太长。

所以还需要继续切。


五、Chunk:为什么不能直接把整本书存进去?

我这里使用:

ini 复制代码
const textSplitter =
  new RecursiveCharacterTextSplitter({
    chunkSize: 500,
    chunkOverlap: 50,
  });

把一个章节继续拆成:

erlang 复制代码
chapter
   ↓
chunk 1
chunk 2
chunk 3
chunk 4
...

每块大约 500 个字符。

同时让前后两个 chunk 重叠 50 个字符。

为什么要重叠?

假设有一句话刚好被切在边界:

diff 复制代码
chunk 1:

......阿朱为了救萧峰,决定假扮成段正淳

-----------------

chunk 2:

最终死在萧峰掌下......

如果完全没有 overlap,两个片段之间的语义就可能被硬生生切断。

所以加入一点重叠:

markdown 复制代码
chunk1
───────────────████

                  ████──────────────
                        chunk2

让上下文不会因为切块完全断掉。

这也是 RAG 里一个很重要的问题:

Chunk 怎么切,会直接影响后面的检索质量。


六、Embedding:把文字变成可以计算的东西

Milvus 不是真的拿两段中文直接比较。

我们需要先把文本转换成向量。

ini 复制代码
const vector =
  await embeddings.embedQuery(chunk);

比如:

erlang 复制代码
"阿朱最后死在萧峰掌下"

        ↓ Embedding

[
  0.123,
  -0.438,
  0.771,
  ...
]

我的 Demo 使用的是:

yaml 复制代码
1024 维向量

这里真正重要的不是 1024。

而是:

语义相近的文本,在向量空间中的距离通常也会比较接近。

于是:

复制代码
阿朱最后怎么样了?

和:

复制代码
阿朱最终死在萧峰掌下

虽然文字并不完全相同,但 Embedding 后可能依然比较接近。

这就是语义检索能够成立的基础。


七、Milvus:真正存进去的不是只有 Vector

最终写入 Milvus 的一条数据,大概是:

yaml 复制代码
{
  id: "1_20_3",

  book_id: "1",

  book_name: "天龙八部",

  chapter_num: 20,

  index: 3,

  content: "阿朱......",

  vector: [...]
}

这里最值得注意的是:

arduino 复制代码
content
+
vector

它们是一一对应的。

arduino 复制代码
原始文本
   │
   ├──────────────→ content
   │
   ↓
Embedding
   │
   └──────────────→ vector

Vector 用来搜索。

Content 用来给 LLM 看。

所以向量数据库不是:

一个只存一堆数字的地方。

我们最终仍然需要通过向量找到对应的原文。


八、到这里,离线阶段结束

现在整个知识库准备过程就结束了:

markdown 复制代码
《天龙八部》
      ↓
EPubLoader
      ↓
章节
      ↓
TextSplitter
      ↓
Chunks
      ↓
Embedding
      ↓
Vectors
      ↓
Milvus

注意:

这时候还没有任何 AI 问答发生。

我们只是把数据处理成了一个"可以被搜索的知识库"。

接下来才是真正的 RAG。


九、LangGraph 开始登场

我的 RAG State 非常简单:

php 复制代码
const GraphState = Annotation.Root({
  question: Annotation,
  k: Annotation,
  documents: Annotation,
  generation: Annotation
});

不用把它想复杂。

运行过程中,本质上就是维护这样一个对象:

yaml 复制代码
{
  question: "阿朱的结局是什么?",

  k: 5,

  documents: [],

  generation: ""
}

四个字段分别代表:

复制代码
question
用户的问题

k
准备检索几个片段

documents
Milvus 找回来的内容

generation
模型最终生成的答案

这就是 LangGraph 的 State。


十、第一个 Node:Retrieve

第一个节点负责:

复制代码
找资料。

核心只有一句:

ini 复制代码
const docsWithScores =
  await vectorStore.similaritySearchWithScore(
    question,
    k
  );

假设:

ini 复制代码
question = "阿朱的结局是什么?";
k = 5;

它会做:

css 复制代码
问题
 ↓
Embedding
 ↓
问题向量
 ↓
Milvus
 ↓
计算向量相似度
 ↓
Top 5

最后拿回:

复制代码
片段 1
片段 2
片段 3
片段 4
片段 5

于是 State 从:

复制代码
{
  question,
  k
}

变成:

复制代码
{
  question,
  k,
  documents
}

这就是:

复制代码
retrieve Node

干的全部事情。


十一、第二个 Node:Generate

现在 State 里已经有资料了。

接下来:

c 复制代码
const context = state.documents
  .map(...)
  .join("\n\n");

把文档整理成:

css 复制代码
[片段 1]

章节:第 20 章

内容:......

----------------

[片段 2]

章节:第 21 章

内容:......

然后把它们塞进 Prompt:

复制代码
你是一个《天龙八部》小说助手。

下面是从小说中检索出的内容:

{context}

用户问题:

阿朱的结局是什么?

请基于提供的小说内容回答。

注意这里真正发生的事情。

LLM 之前只有:

复制代码
Question

现在变成:

复制代码
Context + Question

也就是:

复制代码
原本:

Question
   ↓
LLM


RAG:

Question
   ↓
Retrieve
   ↓
Context
   ↓
Context + Question
   ↓
LLM

这就是所谓:

Retrieval-Augmented Generation。

Generation 被 Retrieval 增强了。


十二、最后用 LangGraph 把两个 Node 串起来

代码反而是最简单的:

sql 复制代码
const graph = new StateGraph(GraphState)
  .addNode("retrieve", retrieveNode)
  .addNode("generate", generateNode)

  .addEdge(START, "retrieve")
  .addEdge("retrieve", "generate")
  .addEdge("generate", END)

  .compile();

得到:

sql 复制代码
START
  ↓
retrieve
  ↓
generate
  ↓
END

然后:

php 复制代码
await graph.invoke({
  question: "阿朱的结局是什么?",
  k: 5,
  documents: [],
  generation: ""
});

整个工作流就开始运行。

所以如果把这份代码压缩到极致:

复制代码
State
  ↓
retrieve
  ↓
更新 documents
  ↓
generate
  ↓
更新 generation

LangGraph 官方对 Graph API 的抽象其实也是类似的三个核心:

复制代码
State
Node
Edge

Node 负责干活,Edge 决定下一步去哪。


十三、等等------这么简单的 RAG,为什么还要 LangGraph?

这是我写完之后觉得最值得问的问题。

因为目前:

复制代码
retrieve
   ↓
generate

完全可以不用 LangGraph。

普通 JavaScript:

ini 复制代码
const documents = await retrieve(question);

const answer =
  await generate(question, documents);

一样能跑。

所以:

如果只是为了实现一个两步 RAG,LangGraph 并不是必须的。

那为什么还要把它画成 Graph?

因为这只是第一版。

现在我们的流程是:

sql 复制代码
START
  ↓
retrieve
  ↓
generate
  ↓
END

但真实业务很快就会变成:

markdown 复制代码
                    ┌→ 直接回答
                    │
用户问题 → 判断问题 ┤
                    │
                    └→ RAG
                         ↓
                      retrieve
                         ↓
                      评估结果
                      ↙      ↘
                  足够         不够
                   ↓            ↓
               generate      重写问题
                                ↓
                            再次检索

这时候普通的一条 Chain 就会开始越来越别扭。

而 Graph 的价值开始出现了。


十四、Naive RAG 最大的问题:太听话了

目前这套 RAG 有一个非常明显的特点:

复制代码
问题进来
 ↓
一定检索
 ↓
一定生成

它不会思考。

比如用户问:

复制代码
你好

它还是:

复制代码
Embedding
↓
Milvus
↓
搜索《天龙八部》

显然没必要。

再比如:

复制代码
段誉和乔峰两个人的人生经历有什么共同点?

可能一次 TopK 检索并不足以得到完整答案。

还有一种情况:

复制代码
用户问题
 ↓
Milvus
 ↓
检索到了 5 个片段

系统现在默认:

"搜出来了,那肯定就是对的。"

但实际上可能 5 个全是垃圾。

当前系统没有任何节点去判断:

复制代码
这次检索质量到底行不行?

这也是 Naive RAG 最大的问题之一。

它是一条非常固定的 Pipeline:

复制代码
Retrieve
   ↓
Generate

能工作。

但不会判断。

不会纠错。

不会重新规划。


十五、这时候 Agentic RAG 就开始变得合理了

假设我们继续往图里加节点。

markdown 复制代码
START
  ↓
route
  ↓
retrieve
  ↓
grade
  ↓
 ┌───────────────────┐
 │                   │
相关                不相关
 │                   │
 ↓                   ↓
generate          rewrite
                     ↓
                  retrieve

这里突然出现了几个以前没有的能力:

arduino 复制代码
route
判断到底需不需要知识库

grade
判断检索结果质量

rewrite
搜索不好就重写 Query

loop
重新检索

于是 RAG 从:

复制代码
一条固定流水线

开始变成:

复制代码
一个会根据结果选择下一步的系统。

这也是我现在对 Agentic RAG 最直观的理解:

不是简单地给 RAG 加一个 Agent 名字,而是让检索流程拥有"决策权"。


十六、重新看 LangGraph,就完全不一样了

刚学 LangGraph 时:

复制代码
State
Node
Edge
Conditional Edge

很容易变成几个需要记忆的 API。

但把它放到 RAG 里面:

ini 复制代码
State
= 当前问题、检索结果、生成答案

Node
= Retrieve、Grade、Rewrite、Generate

Edge
= 下一步执行谁

Conditional Edge
= 根据检索质量决定下一步去哪

它们一下都有了实际意义。

所以我现在反而不太想把 LangGraph 理解成:

"一个画流程图的库。"

更准确一点应该是:

把一个复杂 AI 应用里的状态、步骤和决策显式化。


十七、最后用一张图总结整个 Demo

这次实现可以完整拆成:

scss 复制代码
                     RAG
                      │
        ┌─────────────┴─────────────┐
        │                           │
   Offline Indexing            Online Query
        │                           │
      EPUB                      Question
        ↓                           ↓
     Loader                     Embedding
        ↓                           ↓
    Documents                    Milvus
        ↓                           ↓
   Text Splitter                Top K
        ↓                           ↓
      Chunk                    Documents
        ↓                           ↓
    Embedding                  Context
        ↓                           ↓
      Vector                    Prompt
        ↓                           ↓
      Milvus                      LLM
                                    ↓
                                 Answer

而在线部分又被 LangGraph 表达成:

markdown 复制代码
                GraphState
                    │
                    │ question
                    ↓
              retrieveNode
                    │
                    │ documents
                    ↓
              generateNode
                    │
                    │ generation
                    ↓
                   END

到这里,一个最基础的 RAG 就彻底跑通了。


写在最后

以前看 RAG 时,很容易记住:

sql 复制代码
Embedding
Vector Database
Chunk
Similarity Search
Prompt

但这些词单独记下来没有太大意义。

真正重要的是把数据流串起来:

复制代码
文档
 ↓
Chunk
 ↓
Embedding
 ↓
Vector DB


问题
 ↓
Embedding
 ↓
Retrieve
 ↓
Context
 ↓
LLM

而这次用 LangGraph 再把在线流程表达成:

复制代码
retrieve → generate

之后,我对两件事情都清楚了很多。

RAG 解决的是"模型回答前去哪里找知识"。

LangGraph 解决的是"整个 AI 系统下一步应该执行什么"。

现在的:

sql 复制代码
START → retrieve → generate → END

还很简单。

真正有意思的部分,是下一步:

复制代码
问题是否需要 RAG?
检索结果是否可信?
搜不到怎么办?
是否需要改写 Query?
能不能重新检索?
什么时候搜索网络?
什么时候直接回答?

当这些判断开始进入流程:

Naive RAG,也就开始向 Agentic RAG 演进了。

而这,才是 LangGraph 真正开始发挥价值的地方。

相关推荐
LaughingZhu1 小时前
Product Hunt 每日热榜 | 2026-09-12
人工智能·深度学习·神经网络·搜索引擎·百度
天真小巫1 小时前
2026.9.13总结(工作量日益繁重的当下,AI如何提效)
人工智能
Zguigo2 小时前
【CUDA1】GPUvsCPU,CUDA Kernel
人工智能·pytorch·深度学习
thesky1234562 小时前
用 ONNX Runtime 把 PyTorch 模型变成跨平台极速推理引擎:导出、优化、量化完整实
人工智能·深度学习·模型部署
米小虾2 小时前
把 KV Cache 从 3514 字节压到 890 字节:DeepSeek V4.1-Flash 动了什么,又没动什么
人工智能
锋行天下2 小时前
LangGraph 进阶:Command + Send 动态控制流、并行 Map-Reduce 实战与踩坑
人工智能
米小虾2 小时前
AI 观察:CEO 们集体喊"慢一点",钱却在加速进场
人工智能
微三云生态系统架构师-彭丹2 小时前
远方好物S2B2C系统架构:一级分销与保证金托管的合规技术实现
人工智能·算法
冬奇Lab2 小时前
DeepSeek Harness 系列(06):System Prompt 组装——动态提示词的工程实现
人工智能·deepseek