前言

上一篇文章里,我拆解了一个基于 LangChain 的 RAG Demo 项目(传送门:从零拆解一个 RAG 系统:基于 LangChain + Chroma + DeepSeek 的学习笔记)。那个版本用 LCEL 链式调用实现了完整的 RAG 流程:文档切分 → 向量入库 → 混合检索 → LLM 生成。

跑通之后我开始想:LangChain 的链式管道是线性的,一路到底没有回头路。如果检索到的文档不相关怎么办?如果 LLM 的回答是编的怎么办?

这就引出了 LangGraph——它把 RAG 从"管道"升级为"状态图",支持条件分叉、循环重试和兜底路径。

但这篇文章不是 LangGraph 的入门教程。我想记录的是:在用 LangGraph 改造入库流程时,我经历的三次设计迭代,以及每次迭代背后的思考和踩坑。

🔗 项目地址:GitHub 搜索 daixueyun3377/RAG-demo 分支dev-LangGraph


先看全局:LangChain vs LangGraph 的 RAG 查询流程

在讲入库之前,先快速对比一下两个版本的查询流程,理解 LangGraph 的核心优势。

LangChain 版(rag_engine.py)——线性管道:

query → [rewrite/hyde] → retrieve → [rerank] → generate → return
(一路到底,没有回头路)

LangGraph 版(rag_graph.py)——状态图,带分叉和循环:

query → transform_query → retrieve → grade_documents
                                          ↓
                                有相关文档?──→ 无 → fallback → END
                                  ↓ 有
                                rerank → generate → check_hallucination
                                                        ↓
                                                  有依据?──→ 无且<2次 → generate(重试)
                                                    ↓ 是              ↓ 无且≥2次
                                                   END              fallback → END

LangGraph 版多了两个关键节点:

  • grade_documents:LLM 逐篇判断检索到的文档是否与问题相关,不相关的直接过滤掉
  • check_hallucination:LLM 验证生成的回答是否有文档依据,发现幻觉就重新生成(最多重试 2 次)

这两个节点 + 条件路由,让 RAG 从"检索到什么就用什么"变成了一个能自我验证、自我纠错的闭环系统。

理解了这个思路之后,我开始想:查询流程可以这样做,入库流程是不是也可以?


设想一:用 LLM 动态选择切分策略

问题背景

上一篇文章里,入库流程是这样的:

def ingest_file(file_path, strategy="recursive", chunk_size=512):
    docs = load_file(file_path)
    chunks = split_documents(docs, strategy=strategy, chunk_size=chunk_size)
    ingest_documents(chunks)

策略是调用方硬编码的。上传一篇技术文档用 recursive,上传一段日志也用 recursive,上传一篇论文还是 recursive

但不同类型的文档,最佳切分策略是不一样的:

  • 有标题、列表、代码块的技术文档 → recursive 按层次切分效果好
  • 结构松散的纯文本日志 → fixed 固定长度切就行
  • 主题切换频繁的长文(论文、对话记录)→ semantic 基于语义边界切分更合理

既然 LangGraph 有状态图的能力,能不能在入库时加一个"分析文档特征 → 动态选策略"的节点?

设计方案

我在 rag_graph.py 里新建了一个入库图(Ingest Graph),核心是加了一个 analyze_document 节点:

START → load_document → analyze_document → split_document → store_document → END
                          (LLM 分析特征,
                           动态选策略+参数)

analyze_document 的逻辑:

  1. 取文档前 2000 字作为样本 + 文档总长度,喂给 LLM
  2. LLM 从三种策略中选一种,同时决定 chunk_size(256/512/1024)
  3. 输出结构化 JSON,解析失败时回退到 recursive + 512

同时在 main.py 里加了 /smart-upload 端点,返回结果包含 LLM 的分析理由和完整执行轨迹(graph_steps)。

这个图目前是线性的,但后续可以扩展成带条件分叉的版本——比如分析后发现文档混合了代码和自然语言,分叉成两条路径分别切分再合并入库。

写完之后我觉得挺好的,直到我问了自己一个问题:凭什么 recursive + 512 就能当安全兜底?


设想二:fallback 不靠谱,加质量验证闭环

为什么 recursive + 512 不是"安全"的

仔细想想,recursive + 512 只是"最不容易崩",不是"安全":

  • recursive 容错性确实最高——它按 ["\n\n", "\n", "。", ".", " ", ""] 逐级降级分割,不管文档结构如何总能切出东西来。而 fixed 会在词中间硬切,semantic 依赖 embedding API,API 挂了就直接报错。
  • 512 是社区里用得最多的中间值,上一篇文章里我也测过,对那个示例文档效果不错。但一个文档的测试结果不能代表所有文档。

更关键的是,当前的 fallback 只处理了一种情况:LLM 输出解析失败。但没处理更致命的情况:

  1. LLM 选了 semantic,但 embedding API 挂了 → 整个入库直接报错
  2. LLM 选了 fixed + 256,但文档是长篇论文 → 切出几百个碎片,检索质量很差
  3. 切出来的块平均 3000 字 → 根本没切开,等于没切

改进方案:加 validate_chunks 节点

这才是 LangGraph 该干的事——切完之后验证质量,不合格就换策略重试:

START → load → analyze(LLM选策略) → split → validate_chunks
                                                  ↓
                                        ┌── 质量合格?──┐
                                        ↓ yes           ↓ no
                                      store        fallback_strategy
                                        ↓               ↓
                                       END         split (换策略重试)
                                                        ↓
                                                  validate_chunks ...

validate_chunks 用的是硬指标,不是 LLM 判断:

指标 阈值 拦截什么问题
最少块数 ≥ 1 切分执行崩溃(如 API 超时)
空块占比 ≤ 10% 切出一堆垃圾空块
平均长度下限 ≥ 50 字符 切得太碎
平均长度上限 ≤ 3000 字符 根本没切开

不合格就按降级链 recursive → fixed → semantic 依次尝试。降级时不再信任 LLM 选的 chunk_size,直接用 config 里的默认值。所有策略都试完还不行,就用当前最好的结果硬入库。

这样 recursive 不再是一个拍脑袋的默认值,而是降级链里容错性最高的第一选择,且每次选择都有可量化的质量指标兜底。

写完之后又觉得挺好的,直到我开始算账。


设想三:LLM 选策略的 ROI 太低,换成规则

算一笔账

时间成本:

最好情况(LLM 一次选对):

原版 ingest_file:  load + split + store ≈ 几百ms
智能版:            load + LLM分析(1-3s) + split + validate + store

analyze_document 那一次 LLM 调用就多了 1-3 秒。

最坏情况(三种策略都试一遍,semantic 的 embedding API 超时):

LLM分析(1-3s) + split_semantic失败(embedding超时30s) + fallback
             + split_recursive + validate + fallback
             + split_fixed + validate + store

三轮下来可能 40 秒以上。原版写死 recursive 几百毫秒就完事了。

金钱成本:

每次上传文件都要调一次 LLM。批量入库 100 个文件就是 100 次额外调用。而且大部分技术文档用 recursive 就够了,LLM 分析完大概率还是选 recursive——花钱买了个寂寞。(还是因为我太穷😭)

准确性问题:

这是最根本的:

  • analyze_document 只看了前 2000 字的样本,一个文档前面是结构化目录、后面是松散正文,前 2000 字不能代表全文
  • LLM 对"什么文档适合什么切分策略"的判断没有经过系统验证,本质上是在猜
  • temperature=0.3 不是 0,同一个文档问两次可能给出不同答案

花了 1-3 秒等 LLM 做了一个可能不比随机好多少的决策。

质量验证的局限:

四个硬指标只能拦住"明显切崩了"的情况,但真正影响检索质量的问题检测不到:

  • 语义完整性:一个完整的概念被切成了两半
  • 信息密度:块长度合适但全是重复内容(overlap 设太大)
  • 上下文丢失:代码块被从它的说明文字中切开了

验证通过不代表切得好,只代表切得不算太烂。😮‍💨

降级链的盲区:

recursive → fixed → semantic 是按容错性排的,但不一定对所有场景合理。比如纯对话记录,真正该用的就是 semantic,但它被放在降级链最后。如果前两个因为指标太粗"质量通过"了,semantic 永远不会被尝试。

最终方案:规则启发式替代 LLM

想清楚之后,我把 analyze_document 从 LLM 调用换成了纯规则的启发式判断:

特征提取(6 个维度):

特征 提取方式
标题数 正则匹配 ^#{1,6}\s+
列表项数 正则匹配 ^[-*]\s+^\d+\.\s+
代码块数 统计 ` ````对数
表格行数 正则匹配 ^|.+|
段落数/均长 按双换行分割统计
换行密度 换行数 / 千字符

核心指标:结构化标记密度 = (标题 + 列表 + 代码块 + 表格行) / (总长度 / 1000)

决策路径:

结构密度 > 3  → recursive(结构化文档)
                 └─ 代码块 ≥ 3 → chunk_size=1024(保代码完整性)
                 └─ 短文档 < 2000字 → chunk_size=256
                 └─ 其他 → chunk_size=512

结构密度 < 1 且段落均长 > 500 → semantic(长段落少标记)

结构密度 < 0.5 且换行密度 < 5 → fixed(纯文本流)

其他 → recursive(通用兜底)

对比 LLM 版本的优势:

维度 LLM 版 规则版
延迟 1-3 秒 ≈ 0(纯字符串统计)
成本 每次调用计费
确定性 同文档可能不同结果 同文档永远相同结果
分析范围 前 2000 字样本 全文
可调试性 LLM 黑盒 每个阈值都可调

后面的质量验证 + 降级重试机制保持不变,作为第二道防线。


什么时候值得用智能入库,什么时候不值得

经过三轮迭代,我的结论是:

值得用的场景:

  • 知识库文档类型差异很大(日志、论文、代码、手册混在一起)
  • 入库是低频操作(一天传几个文件,不在乎多等几毫秒)
  • 你需要入库过程的可观测性(graph_steps 记录了完整决策链路)

不值得用的场景:

  • 文档类型比较统一(比如全是 Markdown 技术文档),recursive + 512 就够了
  • 批量入库(100+ 文件),即使是规则版,validate + fallback 的循环也有开销
  • 对入库延迟敏感的实时场景

总结

这三次迭代的过程,其实就是一个不断追问"为什么"的过程:

  1. 为什么不能动态选策略? → 可以,用 LLM 分析文档特征
  2. 为什么 fallback 用 recursive + 512? → 因为它容错性最高……但这不够,应该加质量验证
  3. 为什么要用 LLM 来选? → 其实不需要,规则更快更稳更便宜

LangGraph 的价值不在于"能用 LLM 做更多事",而在于它提供了一个框架,让你能把"判断 → 执行 → 验证 → 重试"这种闭环逻辑自然地表达出来。至于判断的部分用 LLM 还是用规则,取决于 ROI。

具体的代码实现会在下一篇 LangGraph 版 Demo 解读里详细展开。如果有其他思路或者奇思妙想,欢迎评论区交流!🤩


📌 本文为个人学习笔记,记录的是思考过程而非最佳实践,如有错误欢迎指正。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐