一、真实场景:一个RAG问答系统,让我加班到凌晨3点
2024年8月,我接手一个内部知识库问答系统。需求很简单:用户上传PDF,系统自动切分、向量化,然后基于大模型回答。我选了当时最火的LangChain,结果上线第一周就翻车了——
- 文档切分后,上下文丢失严重,回答牛头不对马嘴
- 检索召回率只有62%,用户问「报销流程」返回「请假制度」
- 单次请求平均耗时4.2秒,P99直接飙到12秒
- 内存泄漏,运行8小时后OOM
我花了3天重构,换成LlamaIndex,问题解决了。但这不是说LlamaIndex吊打LangChain——各有适用场景。这篇文章把两个框架的实战对比、代码实现、性能数据和坑全摊开,你拿去就能用。
二、问题拆解:RAG系统的三个核心瓶颈
先明确我们要解决什么问题。一个标准的RAG流程:
- 文档加载与解析(PDF、Word、网页)
- 文本切分(Chunking)
- 向量化与索引构建
- 检索(语义搜索)
- 大模型生成回答
三个核心瓶颈:
- 切分策略:固定大小切分 vs 语义切分 vs 递归切分,直接影响召回率
- 检索质量:向量检索的Top-K准确率,以及是否需要混合检索(向量+关键词)
- 端到端延迟:从用户提问到返回答案的总耗时,包括向量化、检索、LLM推理
下面我用LangChain 0.3.0和LlamaIndex 0.12.0分别实现同一个RAG系统,对比效果。
三、方案一:LangChain实现RAG
3.1 环境与依赖
# Python 3.12.4
pip install langchain==0.3.0 langchain-community==0.3.0 langchain-openai==0.2.0
pip install chromadb==0.5.5 pypdf==5.1.0 tiktoken==0.8.0
# 向量模型用 text-embedding-3-small,LLM用 gpt-4o-mini
export OPENAI_API_KEY="sk-your-key"
3.2 文档加载与切分
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 加载PDF
loader = PyPDFLoader("knowledge_base.pdf")
documents = loader.load()
print(f"加载文档数: {len(documents)}") # 输出: 加载文档数: 87
# 递归切分
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
separators=["\n\n", "\n", "。", "!", "?", " ", ""],
length_function=len,
)
chunks = text_splitter.split_documents(documents)
print(f"切分后块数: {len(chunks)}") # 输出: 切分后块数: 342
3.3 向量化与索引
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db"
)
print("索引构建完成")
3.4 检索+生成(RAG Chain)
from langchain.chains import RetrievalQA
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff", # 将检索到的文档直接拼接到prompt
retriever=retriever,
return_source_documents=True,
)
# 测试
query = "公司报销流程是什么?"
result = qa_chain.invoke({"query": query})
print(f"回答: {result['result']}")
print(f"来源文档数: {len(result['source_documents'])}")
四、方案二:LlamaIndex实现RAG
4.1 环境与依赖
# Python 3.12.4
pip install llama-index==0.12.0 llama-index-embeddings-openai==0.3.0
pip install llama-index-llms-openai==0.3.0 pypdf==5.1.0
export OPENAI_API_KEY="sk-your-key"
4.2 文档加载与切分
from llama_index.core import SimpleDirectoryReader
from llama_index.core.node_parser import SentenceSplitter
# 加载PDF(自动处理目录结构)
reader = SimpleDirectoryReader(input_dir="./docs")
documents = reader.load_data()
print(f"加载文档数: {len(documents)}") # 输出: 加载文档数: 87
# 语义切分(默认按句子切分,保留段落结构)
splitter = SentenceSplitter(
chunk_size=1024,
chunk_overlap=200,
paragraph_separator="\n\n",
)
nodes = splitter.get_nodes_from_documents(documents)
print(f"切分后节点数: {len(nodes)}") # 输出: 切分后节点数: 298
4.3 向量化与索引
from llama_index.core import VectorStoreIndex, StorageContext
from llama_index.vector_stores.chroma import ChromaVectorStore
from llama_index.embeddings.openai import OpenAIEmbedding
import chromadb
# 初始化Chroma客户端
db = chromadb.PersistentClient(path="./chroma_llama")
chroma_collection = db.get_or_create_collection("knowledge_base")
# 构建索引
embed_model = OpenAIEmbedding(model="text-embedding-3-small")
vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
storage_context = StorageContext.from_defaults(vector_store=vector_store)
index = VectorStoreIndex.from_documents(
documents,
embed_model=embed_model,
storage_context=storage_context,
show_progress=True,
)
print("索引构建完成")
4.4 检索+生成
from llama_index.core import Settings
from llama_index.llms.openai import OpenAI
Settings.llm = OpenAI(model="gpt-4o-mini", temperature=0)
Settings.embed_model = embed_model
# 创建查询引擎(默认使用top-k=2,可调)
query_engine = index.as_query_engine(similarity_top_k=4)
# 测试
query = "公司报销流程是什么?"
response = query_engine.query(query)
print(f"回答: {response}")
print(f"来源节点数: {len(response.source_nodes)}")
五、效果数据对比
测试环境:MacBook Pro M3 Pro(18GB内存),OpenAI API延迟约200ms。测试数据集:50份内部文档(PDF/Word),共1200页,切分后约4000个chunk。测试问题:100个预定义问题,覆盖常见业务场景。
| 指标 | LangChain 0.3.0 | LlamaIndex 0.12.0 | 差异 |
|---|---|---|---|
| 召回率(Top-5) | 62% | 78% | LlamaIndex高16% |
| 回答准确率(人工评估) | 71% | 84% | LlamaIndex高13% |
| 平均端到端延迟 | 4.2s | 3.1s | LlamaIndex快26% |
| P99延迟 | 12.0s | 6.5s | LlamaIndex低46% |
| 内存占用(峰值) | 1.8GB | 1.2GB | LlamaIndex少33% |
| 索引构建时间 | 45s | 38s | LlamaIndex快16% |
| 代码行数(核心逻辑) | 约80行 | 约60行 | LlamaIndex少25% |
为什么LlamaIndex表现更好?核心原因:
- 切分策略:LangChain的RecursiveCharacterTextSplitter按字符切分,容易切断语义完整的段落。LlamaIndex的SentenceSplitter按句子切分,保留段落结构,召回率更高。
- 检索逻辑:LlamaIndex默认使用Embedding+BM25混合检索(需配置),LangChain默认只做向量检索。混合检索能提升长尾查询的召回。
- 内存管理:LlamaIndex对文档节点(Node)做了轻量级封装,内存占用更低。
六、避坑指南(我踩过的5个坑)
坑1:LangChain的chain_type="stuff"导致token溢出
当检索到4个chunk(每个1000 token)时,加上prompt和问题,总token可能超过模型上下文窗口(gpt-4o-mini是128K,但成本高)。我遇到过一次查询返回了12个chunk,直接报错。解决方案:改用chain_type="map_reduce"或"refine",但会增加延迟。LlamaIndex默认做了token预算检查,自动截断。
坑2:Chroma持久化路径冲突
LangChain和LlamaIndex如果使用同一个Chroma持久化目录,会互相覆盖。我踩过:先跑LangChain建索引,再跑LlamaIndex读同一个目录,结果索引损坏。解决方案:每个项目用独立的持久化目录,或者指定collection名称。
坑3:切分参数不当导致召回率暴跌
chunk_size=500时,召回率从78%降到55%。原因是chunk太小,语义不完整。chunk_size=2000时,召回率升到82%,但延迟增加30%。最佳实践:根据文档类型调整,技术文档用1000-1500,对话记录用500-800。
坑4:OpenAI API限流
并发请求超过10个时,LangChain的RetrievalQA会触发429错误。LlamaIndex默认有重试机制,但重试间隔指数退避,导致P99延迟飙升。解决方案:加一层本地缓存(如Redis),对相同问题直接返回缓存结果。
坑5:版本兼容性问题
LangChain 0.2到0.3的API变化很大,比如from_documents的参数变了。LlamaIndex 0.10到0.12也改了Settings的导入路径。建议锁定版本号,不要用latest。
七、选型建议
没有银弹。根据你的场景选:
- 选LangChain:你需要构建复杂的Agent(多工具调用、多步推理),或者要集成非标准的大模型(如本地部署的LLaMA)。LangChain的生态更丰富,社区更大。
- 选LlamaIndex:你的核心需求是RAG(文档问答、知识库检索),对检索质量和开发效率要求高。LlamaIndex在RAG场景下开箱即用,代码更少,性能更好。
- 混合使用:用LlamaIndex做检索,用LangChain做Agent编排。我现在的生产环境就是这样——LlamaIndex负责文档索引和检索,LangChain负责对话管理和工具调用。
最后,无论选哪个框架,都要做压测。我用Locust模拟50并发,跑30分钟,观察内存和延迟。别信框架文档说的「高性能」,自己测了才放心。