2.从 LangChain 到 LangGraph:我在 RAG 入库流程上的三次设计迭代(纯个人思考,轻喷
前言
上一篇文章里,我拆解了一个基于 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 的逻辑:
- 取文档前 2000 字作为样本 + 文档总长度,喂给 LLM
- LLM 从三种策略中选一种,同时决定
chunk_size(256/512/1024) - 输出结构化 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 输出解析失败。但没处理更致命的情况:
- LLM 选了
semantic,但 embedding API 挂了 → 整个入库直接报错 - LLM 选了
fixed + 256,但文档是长篇论文 → 切出几百个碎片,检索质量很差 - 切出来的块平均 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 的循环也有开销
- 对入库延迟敏感的实时场景
总结
这三次迭代的过程,其实就是一个不断追问"为什么"的过程:
- 为什么不能动态选策略? → 可以,用 LLM 分析文档特征
- 为什么 fallback 用 recursive + 512? → 因为它容错性最高……但这不够,应该加质量验证
- 为什么要用 LLM 来选? → 其实不需要,规则更快更稳更便宜
LangGraph 的价值不在于"能用 LLM 做更多事",而在于它提供了一个框架,让你能把"判断 → 执行 → 验证 → 重试"这种闭环逻辑自然地表达出来。至于判断的部分用 LLM 还是用规则,取决于 ROI。
具体的代码实现会在下一篇 LangGraph 版 Demo 解读里详细展开。如果有其他思路或者奇思妙想,欢迎评论区交流!🤩
📌 本文为个人学习笔记,记录的是思考过程而非最佳实践,如有错误欢迎指正。
更多推荐


所有评论(0)