2024年8月,我接手一个内部知识库问答系统。需求很简单:用户上传PDF,系统自动切分、向量化,然后基于大模型回答。我选了当时最火的LangChain,结果上线第一周就翻车了——
我花了3天重构,换成LlamaIndex,问题解决了。但这不是说LlamaIndex吊打LangChain——各有适用场景。这篇文章把两个框架的实战对比、代码实现、性能数据和坑全摊开,你拿去就能用。
先明确我们要解决什么问题。一个标准的RAG流程:
三个核心瓶颈:
下面我用LangChain 0.3.0和LlamaIndex 0.12.0分别实现同一个RAG系统,对比效果。
# 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"
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
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("索引构建完成")
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'])}")
# 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"
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
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("索引构建完成")
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表现更好?核心原因:
当检索到4个chunk(每个1000 token)时,加上prompt和问题,总token可能超过模型上下文窗口(gpt-4o-mini是128K,但成本高)。我遇到过一次查询返回了12个chunk,直接报错。解决方案:改用chain_type="map_reduce"或"refine",但会增加延迟。LlamaIndex默认做了token预算检查,自动截断。
LangChain和LlamaIndex如果使用同一个Chroma持久化目录,会互相覆盖。我踩过:先跑LangChain建索引,再跑LlamaIndex读同一个目录,结果索引损坏。解决方案:每个项目用独立的持久化目录,或者指定collection名称。
chunk_size=500时,召回率从78%降到55%。原因是chunk太小,语义不完整。chunk_size=2000时,召回率升到82%,但延迟增加30%。最佳实践:根据文档类型调整,技术文档用1000-1500,对话记录用500-800。
并发请求超过10个时,LangChain的RetrievalQA会触发429错误。LlamaIndex默认有重试机制,但重试间隔指数退避,导致P99延迟飙升。解决方案:加一层本地缓存(如Redis),对相同问题直接返回缓存结果。
LangChain 0.2到0.3的API变化很大,比如from_documents的参数变了。LlamaIndex 0.10到0.12也改了Settings的导入路径。建议锁定版本号,不要用latest。
没有银弹。根据你的场景选:
最后,无论选哪个框架,都要做压测。我用Locust模拟50并发,跑30分钟,观察内存和延迟。别信框架文档说的「高性能」,自己测了才放心。
专注技术分享与实战