乐于分享
好东西不私藏

同一份文档,两个框架,效果天差地别——LangChain 和 LlamaIndex 在 RAG 切分与检索上的本质差异

同一份文档,两个框架,效果天差地别——LangChain 和 LlamaIndex 在 RAG 切分与检索上的本质差异

引子:一个让团队吵翻天的真实场景

先讲一个我们踩过的坑。

去年下半年,我们团队接了一个企业知识库项目。客户给了一份 50 页的混合型 PDF——有标题层级、有表格、有代码块,还有大段大段的叙述性文字。我们的任务很明确:基于这份文档做问答系统,员工可以问“报销流程中财务审批的时限是几个工作日”“API 的鉴权方式有哪几种”这类问题。

当时我们用的是 LangChain,配了 OpenAI 的 embedding 模型和 GPT-4。Demo 阶段一切顺利,给老板演示的时候,问什么答什么,效果惊艳。但一上真实数据,问题就来了:

  • 问“报销流程中财务审批的时限”,模型答非所问,把“差旅报销”和“对公报销”的条款混在一起;
  • 问“API 鉴权方式”,回答里居然出现了“数据库连接字符串”这种毫不相干的内容;
  • 更离谱的是,有些问题的答案明明就在同一页上,但模型就是“看不见”。

我们一开始怀疑是 embedding 模型不行,换了;怀疑是向量库的问题,也换了;怀疑是 LLM 的 temperature 设置不对,调了。都没用。

后来我们做了一个对比实验:同一份文档、同一个 embedding 模型、同一个 LLM、同一个向量库,只是把框架从 LangChain 换成了 LlamaIndex。结果,检索命中率肉眼可见地提升,回答质量完全不是一个量级。

团队当场就吵起来了。有人说“LlamaIndex 就是比 LangChain 强”,有人说“是我们 LangChain 的用法不对”,还有人说是“玄学”。

后来我们把整个链路拆开,一行一行地对比日志,才找到真正的答案:差异不在模型,不在向量库,而在文档进向量库之前的那一步——切分与索引。

这篇文章,就是把那次排查的全过程,以及背后的原理,完整地讲给你听。

读完这篇文章,你会收获三样东西:

  1. 理解 LangChain 和 LlamaIndex 在文档处理上的本质差异——不是谁好谁坏,而是设计哲学的底层不同;
  2. 掌握一套可落地的切分策略选型方法——什么文档用什么切分器、参数怎么配、为什么这么配;
  3. 一份完整的迁移避坑清单——如果你正打算从 LangChain 迁到 LlamaIndex,或者正在做框架选型,这份清单能帮你省掉至少两周的踩坑时间。

我们开始。

第一节:切分——RAG 链条里最容易被忽视、却决定上限的一环

1.1 RAG 的完整链路:切分到底卡在哪个位置

先快速对齐一下 RAG 的基本流程。一个典型的 RAG 系统,链路是这样的:

文档加载 → 文档切分 → 向量化(Embedding)→ 索引存储 → 检索(Retrieval)→ 重排序(Rerank)→ 送入 LLM 生成

很多人把注意力放在 Embedding 模型选型上,放在向量库的选型上,放在 LLM 的 prompt 设计上。但行业里有一个越来越清晰的共识:检索质量的上限,由文档处理和切分策略决定,而不是由模型决定。

为什么?

因为切分是整条链路的“地基”。你切出来的每一个块(Chunk),就是后续向量化、索引、检索的最小单位。如果块本身切得不好——该拆的没拆开、该连的断掉了、语义边界被拦腰截断——那么后面无论你用多好的 Embedding 模型、多快的向量库、多聪明的 LLM,都只能在“坏块”的基础上做文章。

打个比方:切分就像是做菜之前的“备菜”。食材切得好不好,直接决定这道菜的上限。你请一个米其林三星的大厨来炒一盘土豆丝,但如果土豆丝切得跟薯条一样粗,大厨也救不回来。

1.2 切分 → 索引 → 检索 → 生成的传导链路

切分的问题,是怎么传导到最终回答质量的?我们用一条链路来解释:

第一步:切分决定了“检索单位”。 用户问“报销流程中财务审批的时限”,如果你的切分把一个包含“财务审批时限”的段落拦腰截断,一半进了 Chunk A,一半进了 Chunk B,那么检索时无论命中哪一个,拿到的都是不完整的上下文。

第二步:检索单位决定了“上下文质量”。 向量检索的本质是语义相似度匹配。一个被拦腰截断的 Chunk,它的语义是残缺的,和用户问题的相似度自然偏低。结果就是:该命中的没命中,不该命中的反而命中了。

第三步:上下文质量决定了“生成质量”。 LLM 是“垃圾进,垃圾出”的典型代表。你喂给它的上下文如果是残缺的、错位的、甚至包含噪声的,那么它的回答质量必然断崖式下跌。

所以你会发现一个很有意思的现象:很多团队在 RAG 上花了大把时间调 prompt、换模型,效果始终不理想。但把切分策略一改,效果立竿见影。这不是玄学,这是链路传导的必然结果。

1.3 为什么很多团队“先 LangChain 后 LlamaIndex”?

在进入正题之前,先回答一个大家都很关心的问题:为什么很多团队一开始用 LangChain,后来都转向了 LlamaIndex?

答案很简单:LangChain 的入门门槛低,但天花板也低。

LangChain 的定位是“应用编排框架”,它的核心优势在于把各种组件(模型、向量库、工具、Agent)以链式的方式组装起来。你不需要理解太多底层原理,照着文档写几行代码就能跑通一个 RAG demo。这是它风靡全球的原因。

但当你进入真实项目,面对复杂的文档结构、多样的检索需求、以及“效果不好但不知道哪里不好”的困境时,LangChain 的“胶水层”定位就暴露了它的短板——文档处理只是它众多能力中的一环,不是它的核心优势。

而 LlamaIndex 的定位是“数据框架”,它从第一天起就是围绕“如何让 LLM 更好地理解你的数据”来设计的。文档加载、切分、索引、检索,这些是它的核心能力,也是它打磨最深的地方。

所以,很多团队的经历是:先用 LangChain 快速搭出 demo,然后发现效果不行,再转向 LlamaIndex 重新设计文档处理流程。这不是说 LangChain 不行,而是它的默认行为和真实需求之间的错位,在文档处理这一层被放大了。

第二节:核心原理——文档抽象与切分机制的底层差异

这一节是全文的干货核心。我们一层一层拆开,看 LangChain 和 LlamaIndex 在文档处理上的本质差异。

2.1 文档抽象的底层差异:Document 与 Document/Node

LangChain 的 Document:一个没有“关系”的文本块

在 LangChain 里,文档的抽象非常朴素。一个 Document 只有两个核心属性:

  • page_content:文本内容
  • metadata:元数据(来源、页码、标题等)

当你对文档做切分时,LangChain 的切分器(TextSplitter)会把一个大的 Document 切成多个小的 Document。注意,切分后的每个块仍然是一个 Document。整个链路中,“文档”的概念没有变化,只是内容被截短了。

这个设计的好处是简单、直观、容易理解。但坏处也很明显:切分后的块之间没有任何关系。它们是一堆孤立的文本片段,彼此之间不知道谁在前、谁在后、谁是谁的父节点。

这意味着什么?意味着如果你想让 LLM 回答一个需要跨多个块才能完整理解的问题(比如“这篇文章的核心论点是什么”),LangChain 的默认行为是做不到的。你得自己想办法把相关的块重新拼起来。

LlamaIndex 的 Node:一个“有身份、有关系”的语义单元

LlamaIndex 的抽象层级则完全不同。它的核心概念是 Node(节点)。

在 LlamaIndex 中,原始文档被称为 Document,切分之后,每个块变成一个 Node。一个 Node 包含:

  • text文本内
  • metadata:元数据
  • node_id:节点唯一标识
  • relationships:节点之间的关系

关键就在 relationships 上。LlamaIndex 的 Node 会主动维护和其他节点之间的关系,比如:

  • SOURCE:这个节点的来源文档
  • PREVIOUS/NEXT:前一个节点 / 后一个节点
  • PARENT/CHILD:父子节点关系(用于分层切分)

这个设计看起来只是多了一个字段,但它带来的能力是完全不一样的。Node 是 LlamaIndex 的一等公民,它不仅存文本,还存“上下文关系”。 这为后续的高级检索策略(比如 ParentDocumentRetriever、SentenceWindowRetriever)提供了基础。

这个差异带来的实际影响

我们用一句话总结这个差异:

LangChain 切分后的块是“死”的,LlamaIndex 切分后的节点是“活”的。

“死”的意思是:块与块之间没有关联,你拿到一个块,就只有一个块。“活”的意思是:节点与节点之间存在关系图谱,你拿到一个节点,可以顺着关系找到它的父节点、相邻节点、来源文档。

这个差异在简单问答场景下可能感知不明显。但一旦你面对的是复杂文档、长文档、或者需要多级检索的场景,这个差异就会被急剧放大。

2.2 切分器的实现差异:RecursiveCharacterTextSplitter vs SentenceSplitter

LangChain 的默认切分器:按字符优先级“暴力切分”

LangChain 默认推荐的切分器是 RecursiveCharacterTextSplitter。它的工作原理是:给定一个字符列表 ["\n\n", "\n", " ", ""],从第一个分隔符开始,尝试按它切分;如果切出来的块仍然大于 chunk_size,就换下一个分隔符继续切;直到所有块都小于等于 chunk_size

这个策略的优点是通用、简单、不挑文档类型。但它的本质是“按字符优先级”的暴力切分,完全不感知语义边界

举一个典型的例子。假设你有一段这样的文本:

# 第三章 系统架构本系统采用微服务架构,包含以下核心组件:1. 网关服务:负责请求路由和鉴权2. 用户服务:负责用户管理和权限控制3. 订单服务:负责订单处理和状态流转## 3.1 网关服务网关服务基于 Spring Cloud Gateway 实现...

如果用 RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=20) 来切,它可能会把“3. 订单服务”和“## 3.1 网关服务”切到同一个块里,或者把“1. 网关服务”和“2. 用户服务”拆到两个块里。标题层级、列表结构、语义边界,它统统不关心。

这在早期 demo 阶段问题不大,因为你的测试问题可能恰好避开了这些边界。但一旦文档复杂起来,这种“暴力切分”就会频繁产生语义残缺的块,检索质量自然就崩了。

LlamaIndex 的默认切分器:按句子边界“语义切分”

LlamaIndex 默认推荐的切分器是 SentenceSplitter。它的工作原理是:先把文档按句子边界(句号、问号、感叹号等)切分成一个个句子,然后按 chunk_size 和 chunk_overlap 把相邻的句子聚合成块。

这个策略的本质是“按语义单元”切分。它保证了一个块内部是完整的句子,不会出现“一句话被拦腰截断”的情况。

同样用上面那段文本,SentenceSplitter(chunk_size=100, chunk_overlap=20) 会尽量把完整的句子聚合在一起,保持语义的连贯性。

代码对比:同一个文本,两种切分器的输出差异

我们直接上代码,看两种切分器对同一段文本的输出差异。

# LangChain 方式from langchain.text_splitter import RecursiveCharacterTextSplittertext = """# 第三章 系统架构本系统采用微服务架构,包含以下核心组件:1. 网关服务:负责请求路由和鉴权2. 用户服务:负责用户管理和权限控制3. 订单服务:负责订单处理和状态流转## 3.1 网关服务网关服务基于 Spring Cloud Gateway 实现,支持动态路由和 OAuth2 鉴权。"""splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=20)chunks = splitter.split_text(text)for i, chunk in enumerate(chunks):    print(f"--- Chunk {i+1} ---")    print(chunk)    print()
# LlamaIndex 方式from llama_index.core.node_parser import SentenceSplittertext = """# 第三章 系统架构本系统采用微服务架构,包含以下核心组件:1. 网关服务:负责请求路由和鉴权2. 用户服务:负责用户管理和权限控制3. 订单服务:负责订单处理和状态流转## 3.1 网关服务网关服务基于 Spring Cloud Gateway 实现,支持动态路由和 OAuth2 鉴权。"""splitter = SentenceSplitter(chunk_size=100, chunk_overlap=20)nodes = splitter.get_nodes_from_documents([Document(text=text)])for i, node in enumerate(nodes):    print(f"--- Node {i+1} ---")    print(node.text)    print()

你运行这段代码,会看到明显的输出差异。LangChain 的切分可能把“1. 网关服务”和“2. 用户服务”拆到两个块里,或者把“## 3.1 网关服务”和下面的正文拆开;而 LlamaIndex 的切分则会尽量保持句子的完整性。

一个最容易踩的坑:chunk_size 的含义不同

这里必须强调一个迁移时最容易踩的坑:chunk_size 在 LangChain 中表示“字符数”,在 LlamaIndex 中表示“token 数”(默认)。

同一个参数名,含义完全不同。这意味着什么?意味着如果你在 LangChain 里用 chunk_size=512 调好了效果,直接搬到 LlamaIndex 里用 chunk_size=512,实际切出来的块会大得多——因为 512 个 token 大约相当于 700-800 个英文字符,或者 400-500 个中文字符。

迁移到 LlamaIndex 后,你必须重新调参,不能直接沿用 LangChain 的参数。

2.3 索引结构的默认行为差异

LangChain:平铺的“文档块列表 + 向量”

LangChain 的向量存储索引,本质上是“文档块列表 + 向量”的平铺结构。每个切分后的 Document 被向量化后存入向量库,检索时按向量相似度取 TopK。

这个结构简单直接,但它的局限性在于:检索到的块之间没有任何关系。你拿到 TopK 个块,就是 TopK 个孤立的文本片段。如果你需要跨块理解(比如一个问题的答案分散在三个块里),你得自己做后处理。

LlamaIndex:带关系图谱的索引结构

LlamaIndex 的 VectorStoreIndex 在构建时,会额外维护 Node 之间的关系。这意味着你可以从检索到的叶子节点,回溯到它的父节点或相邻节点。

更重要的是,LlamaIndex 内置了多种索引类型,每种索引适合不同的文档结构和检索需求:

  • SummaryIndex:适合“摘要型”问答,把整个文档压缩成一个摘要
  • TreeIndex:适合层级结构的文档,按树形结构组织节点
  • KeywordTableIndex:适合“关键词匹配”场景,按关键词建立索引
  • PropertyGraphIndex:适合知识图谱场景,提取实体和关系

而 LangChain 主要依赖向量检索 + 外部编排。你要实现类似的能力,需要自己组装多个组件。不是说 LangChain 做不到,而是它没有把这些能力内置成默认行为。

第三节:落地做法——不同场景下的切分策略选型与参数配置

理解了原理,我们来看落地。这一节给出可操作的切分策略选型和参数配置指南。

3.1 按文档类型选择切分策略

结构化文档(Markdown、HTML):用标题感知的切分器

如果你的文档是 Markdown 或 HTML 格式,强烈建议使用标题感知的切分器,保留层级结构。

LangChain 提供了 MarkdownHeaderTextSplitter,可以按标题层级切分:

from langchain.text_splitter import MarkdownHeaderTextSplitterheaders_to_split_on = [    ("#""H1"),    ("##""H2"),    ("###""H3"),]splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)chunks = splitter.split_text(markdown_document)

LlamaIndex 提供了 MarkdownNodeParser

from llama_index.core.node_parser import MarkdownNodeParserparser = MarkdownNodeParser()nodes = parser.get_nodes_from_documents([document])

决策点: 如果你的文档有清晰的标题层级,务必使用标题感知的切分器。这能保证每个块都带有“上下文锚点”(比如“第三章 > 3.1 网关服务”),检索时更容易匹配到语义相关的块。

非结构化长文(报告、论文):用句子级别的切分器

对于没有明显结构的长文,建议用句子级别的切分器。chunk_size 建议 300-500 token,chunk_overlap 建议 50-100 token。

决策点:chunk_size 不是越大越好,也不是越小越好,取决于你的检索单位与回答粒度的匹配。

  • 如果你的问答是“摘要型”(需要全局理解),用较大的 chunk(500-800 token)或 ParentDocument 策略;
  • 如果是“事实型”问答(精确查找某个事实),用较小的 chunk(200-300 token),提高检索精度。

混合型文档(含表格、代码块):需要自定义切分规则

混合型文档是最难处理的。表格和代码块有自己特殊的结构,用普通的文本切分器很容易切坏。

推荐做法有两种:

  1. 先按类型拆分,再分别切分: 用文档解析工具把表格、代码块、正文分别提取出来,各自用合适的切分器处理;
  2. 用分层切分: 使用 LlamaIndex 的 HierarchicalNodeParser做多层级切分,把文档切成“父节点 → 子节点”的树形结构。

3.2 实战参数配置指南

两个框架在不同场景下的推荐参数表

场景

LangChain chunk_size

LangChain chunk_overlap

LlamaIndex chunk_size

LlamaIndex chunk_overlap

事实型问答

300-500(字符)

30-50

200-300(token)

20-30

摘要型问答

800-1200(字符)

100-200

500-800(token)

50-100 

代码文档

200-400(字符)

20-40

150-300(token)

15-30

混合型文档

自定义切分

自定义

分层切分(2048/512/128)

20

注意: 这张表是经验值,不是标准答案。每个项目的文档类型、问题类型、embedding 模型都不同,你需要在自己的数据上做实验调参。

一个容易被忽视的点:chunk_size 与回答粒度的匹配

我们反复强调“chunk_size 不是越大越好,也不是越小越好”,用案例来说明。

假设你有一个 100 页的企业制度文档,用户问“年假天数是怎么规定的”。这个问题的答案可能只在一个段落里,用 200 token 的小 chunk 就能精确命中。但如果你用 2000 token 的大 chunk,检索命中的块可能包含大量无关信息,LLM 容易被噪声干扰。

反过来,如果用户问“公司的人力资源制度整体框架是什么”,这个问题需要全局理解。用 200 token 的小 chunk,你可能需要检索很多个块才能拼出全貌,而且拼出来的内容可能是碎片化的。这时候用大 chunk 或 ParentDocument 策略更合适。

一句话总结:让 chunk 的大小匹配你的回答粒度。

用 LlamaIndex 的 HierarchicalNodeParser 实现父子节点切分

LlamaIndex 的 HierarchicalNodeParser 是一个非常有用的工具,它可以把文档切成多层级结构:

from llama_index.core.node_parser import HierarchicalNodeParsernode_parser = HierarchicalNodeParser.from_defaults(    chunk_sizes=[2048, 512, 128],  # 三层:父-中-子    chunk_overlap=20)nodes = node_parser.get_nodes_from_documents([document])

这个切分策略的价值在于:检索时先命中小的子节点(精度高),然后通过关系回溯到父节点(上下文全)。 这就是所谓的“ParentDocument 策略”。

LangChain 中对应的方案是 ParentDocumentRetriever

from langchain.retrievers import ParentDocumentRetrieverfrom langchain.storage import InMemoryStorefrom langchain.vectorstores import Chromaparent_splitter = RecursiveCharacterTextSplitter(chunk_size=2000)child_splitter = RecursiveCharacterTextSplitter(chunk_size=400)retriever = ParentDocumentRetriever(    vectorstore=vectorstore,    docstore=store,    child_splitter=child_splitter,    parent_splitter=parent_splitter,)

对比总结: LlamaIndex 把父子节点关系内建在 Node 里,开箱即用;LangChain 需要你手动组装多个组件。两者都能实现类似效果,但 LlamaIndex 的默认行为更合理,LangChain 需要你理解原理后自己搭。

3.3 检索增强:切分之后还能做什么

切分只是第一步,检索质量还取决于检索策略。这一节对比两个框架在“检索后处理”上的能力差异。

LlamaIndex 内置的检索策略

  • SentenceWindowRetriever: 检索句子级节点后,自动扩展到整个窗口上下文。适合“事实型”问答。
  • AutoMergingRetriever: 检索叶子节点后,自动合并到父节点。适合“摘要型”问答。
from llama_index.core.indices.query.query_transform import HyDEQueryTransformfrom llama_index.core.query_engine import TransformQueryEngine# 使用 SentenceWindowRetrieverfrom llama_index.core.indices.postprocessor import SentenceTransformerRerankfrom llama_index.core.retrievers import AutoMergingRetriever

LangChain 的检索策略

LangChain 需要借助外部组件实现类似能力:

  • ParentDocumentRetriever:类似 AutoMergingRetriever,但需要手动组装;
  • EnsembleRetriever:组合多个检索器,适合多路召回;
  • ContextualCompressionRetriever:检索后做上下文压缩,去除无关信息。

对比总结: LlamaIndex 把高级检索策略内置为默认能力,LangChain 需要你理解原理后自行组装。如果你的核心诉求是“文档问答”,LlamaIndex 的默认行为更合理。

第四节:对比总结与踩坑清单——选型建议和迁移指南

4.1 框架定位的本质差异

我们用一句话总结两个框架的本质差异:

LangChain 是“应用编排框架”,文档处理只是其中一环,核心优势在 Chain/Agent 的灵活组装。
LlamaIndex 是“数据框架”,文档处理和索引是核心能力,核心优势在数据接入、索引类型和检索策略的丰富度。

这个定位差异决定了它们的默认行为。LangChain 的默认行为是“通用”,适合各种应用场景,但每个场景都不是最优的;LlamaIndex 的默认行为是“数据优先”,适合文档问答,但在其他场景(比如多轮对话、Agent)需要额外组装。

选型建议:

  • 如果你的 RAG 是“文档问答”为主,选 LlamaIndex,它的默认行为更合理;
  • 如果你的 RAG 是“对话流程”中的一环,选 LangChain,它的编排能力更契合;
  • 如果你的项目需要知识图谱、复杂索引,选 LlamaIndex;
  • 如果你的项目需要 Agent、工具调用、多轮对话,选 LangChain。

4.2 迁移踩坑清单

如果你正打算从 LangChain 迁到 LlamaIndex,这份清单请收好。

坑 1:chunk_size 的语义不同(字符 vs token)

这是最大的坑。LangChain 的 chunk_size 是字符数,LlamaIndex 的是 token 数。迁移时必须重新调参,不能直接沿用。

坑 2:LangChain 切分后丢失了文档结构信息

LangChain 切分后的 Document 是“死”的,没有关系图谱。迁移到 LlamaIndex 后,你需要重新设计 Node 解析策略,充分利用 Node 的关系能力。

坑 3:metadata 处理方式不同

两个框架的 metadata 处理方式不同,影响过滤和检索。LangChain 的 metadata 是 Document 的属性,LlamaIndex 的 metadata 是 Node 的属性。迁移时需要重新设计 metadata 的保留和传递策略。

坑 4:向量库的索引结构不兼容

两个框架的向量库索引结构不兼容,迁移需要重新 embedding 和入库。这不是改几行代码就能解决的。

坑 5:切分器的默认行为不同

即使你用了相同的 chunk_size 和 chunk_overlap,两个框架的切分结果也可能完全不同。因为它们的切分逻辑不同(字符优先级 vs 句子边界)。迁移后必须重新评估切分质量。

4.3 结论:没有“哪个框架更好”,只有“哪个框架的默认行为更匹配你的场景”

最后,我们给出一个决策表,按项目类型推荐框架:

项目类型

推荐框架

理由

纯文档问答

LlamaIndex

默认行为更合理,Node 关系图谱和高级检索策略开箱即用

多轮对话 + RAG

LangChain

编排能力更强,容易集成到对话流程中

知识图谱

LlamaIndex

内置 PropertyGraphIndex,支持知识图谱构建

Agent 应用

LangChain

Agent 生态更成熟,工具调用更灵活

混合型(对话 + 文档问答)

两者结合

用 LlamaIndex 处理文档和索引,用 LangChain 做编排

最终建议:不要把框架当信仰,把文档处理当黑盒。理解切分和索引的原理,比选哪个框架更重要。

实例:用 LlamaIndex 构建一个文档问答系统(含分层切分)

这一节,我们做一个可落地的小项目:用 LlamaIndex 构建一个基于分层切分的文档问答系统,并用 LangChain 的 ParentDocumentRetriever 做对比,让你直观感受两个框架在切分和检索上的差异。

项目目标

  • 输入:一份 Markdown 格式的技术文档(含标题层级、列表、代码块)
  • 输出:一个命令行问答工具,支持“事实型”和“摘要型”两类问题

步骤 1:准备环境

pip install llama-index langchain chromadb openai

步骤 2:准备测试文档

我们用一个简化的技术文档作为测试数据,保存为 test_doc.md

系统架构文档## 1. 系统概述本系统采用微服务架构,包含网关服务、用户服务、订单服务三个核心组件。## 2. 网关服务网关服务基于 Spring Cloud Gateway 实现,支持动态路由和 OAuth2 鉴权。### 2.1 动态路由动态路由支持基于路径和请求头的路由规则。### 2.2 鉴权方式支持 OAuth2 和 JWT 两种鉴权方式。## 3. 用户服务用户服务负责用户管理和权限控制,支持 RBAC 权限模型。## 4. 订单服务订单服务负责订单处理和状态流转,支持分布式事务。## 5. 部署说明推荐使用 Docker Compose 部署,最低配置为 2 核 4G 内存。

步骤 3:用 LlamaIndex 构建分层切分 + 问答系统

from llama_index.core import Document, VectorStoreIndexfrom llama_index.core.node_parser import HierarchicalNodeParser, get_leaf_nodesfrom llama_index.core.retrievers import AutoMergingRetrieverfrom llama_index.core.query_engine import RetrieverQueryEnginefrom llama_index.core.storage.docstore import SimpleDocumentStorefrom llama_index.core.storage import StorageContext# 1. 加载文档with open("test_doc.md""r"as f:    text = f.read()document = Document(text=text)# 2. 分层切分:三层结构(父-中-子)node_parser = HierarchicalNodeParser.from_defaults(    chunk_sizes=[2048512128],    chunk_overlap=20)nodes = node_parser.get_nodes_from_documents([document])# 3. 构建索引(使用叶子节点)leaf_nodes = get_leaf_nodes(nodes)storage_context = StorageContext.from_defaults()storage_context.docstore.add_documents(nodes)  # 存储所有节点,包括父子关系index = VectorStoreIndex(    leaf_nodes,    storage_context=storage_context)# 4. 使用 AutoMergingRetriever(自动合并到父节点)retriever = AutoMergingRetriever(    index.as_retriever(similarity_top_k=3),    storage_context=storage_context)# 5. 构建查询引擎query_engine = RetrieverQueryEngine.from_args(retriever=retriever)# 6. 测试queries = [    "网关服务支持哪些鉴权方式?",  # 事实型    "系统的核心组件有哪些?",       # 摘要型]for q in queries:    response = query_engine.query(q)    print(f"Q: {q}")    print(f"A: {response}")    print("---")

关键输出:

  • 事实型问题“网关服务支持哪些鉴权方式”能精确命中“### 2.2 鉴权方式”节点;
  • 摘要型问题“系统的核心组件有哪些”能通过 AutoMergingRetriever 自动合并到父节点,拿到完整上下文。

步骤 4:用 LangChain 实现对比

from langchain.text_splitter import RecursiveCharacterTextSplitterfrom langchain.vectorstores import Chromafrom langchain.embeddings import OpenAIEmbeddingsfrom langchain.retrievers import ParentDocumentRetrieverfrom langchain.storage import InMemoryStore# 1. 加载文档with open("test_doc.md""r"as f:    text = f.read()# 2. 父子切分parent_splitter = RecursiveCharacterTextSplitter(chunk_size=2000, chunk_overlap=100)child_splitter = RecursiveCharacterTextSplitter(chunk_size=400, chunk_overlap=50)# 3. 构建向量库和文档存储vectorstore = Chroma(    collection_name="test_docs",    embedding_function=OpenAIEmbeddings())store = InMemoryStore()# 4. 构建 ParentDocumentRetrieverretriever = ParentDocumentRetriever(    vectorstore=vectorstore,    docstore=store,    child_splitter=child_splitter,    parent_splitter=parent_splitter,)# 5. 切分并入库retriever.add_documents([Document(page_content=text)])# 6. 测试from langchain.chains import RetrievalQAfrom langchain.llms import OpenAIqa = RetrievalQA.from_chain_type(    llm=OpenAI(),    retriever=retriever,    return_source_documents=True)result = qa.invoke("网关服务支持哪些鉴权方式?")print(result["result"])

对比结论:

  • 对于这份简单的测试文档,两个框架的表现差异不大;
  • 但当你把文档复杂度提升(增加更多层级、表格、代码块),LlamaIndex 的 Node 关系图谱和 AutoMergingRetriever 的优势就会显现;
  • LangChain 需要你手动组装多个组件,理解原理后也能实现类似效果,但默认行为不如 LlamaIndex 合理。

步骤 5:调试与调参

决策点 1: 如果事实型问题的检索精度不够,尝试减小 chunk_sizes 中的叶子节点大小(比如从 128 降到 64)。

决策点 2: 如果摘要型问题的上下文不够完整,尝试增大父节点的大小(比如从 2048 提到 4096),并检查 AutoMergingRetriever 是否正确合并。

决策点 3: 如果两个框架的效果差异不明显,检查你的文档是否足够复杂。简单文档上,两个框架的差异会被掩盖。

小结与延伸阅读

写到这里,我们把整篇文章的核心观点做一个小结:

  1. RAG 的检索质量上限由文档处理和切分策略决定,而不是由模型决定。
  2. LangChain 的 Document 抽象是“平铺”的,LlamaIndex 的 Node 抽象是“带关系图谱”的。
  3. LangChain 的默认切分器按字符优先级暴力切分,LlamaIndex 的默认切分器按句子边界语义切分。
  4. chunk_size 在 LangChain 中表示字符数,在 LlamaIndex 中表示 token 数——迁移时必须重新调参。
  5. 没有“哪个框架更好”,只有“哪个框架的默认行为更匹配你的场景”。

如果你想继续深入,推荐阅读以下方向:

  • GraphRAG: 在文档切分的基础上,进一步提取实体和关系,构建知识图谱;
  • RAPTOR: 递归式地压缩和抽象文本,实现多层级检索;
  • Self-RAG: 让 LLM 自己判断是否需要检索、检索什么内容。

写在最后

回到文章开头那个让团队吵翻天的场景。后来我们做了什么?

我们并没有放弃 LangChain,也没有全面转向 LlamaIndex。我们把文档处理这一层单独拆出来,用 LlamaIndex 做切分和索引,然后通过 LangChain 的 LCEL(LangChain Expression Language)把检索结果接入到对话流程中。两个框架各取所长,效果反而最好。

所以,我的最终建议是:不要把框架当信仰,把文档处理当黑盒。理解切分和索引的原理,比选哪个框架更重要。

你在做 RAG 项目时,有没有遇到过“文档切分导致检索效果差”的坑?你是用什么方法解决的?欢迎在评论区分享你的经验,或者说说你在 LangChain 和 LlamaIndex 之间做选型时的纠结——我们一起聊聊。

如果这篇文章对你有帮助,欢迎点赞、在看、转发给团队里正在做 RAG 的同事。你的转发,可能帮他们省掉两周的踩坑时间。