LangChain 与 LlamaIndex 实现 RAG:代码知识点总结
一、两个代码共同实现的核心流程
无论是 LangChain 还是 LlamaIndex,本质上实现的都是标准 RAG(Retrieval-Augmented Generation,检索增强生成)流程。核心思想不是让大模型直接凭记忆回答,而是先从知识库中检索相关资料,再把检索结果作为上下文交给大模型生成答案。
文档读取 → 文档切分 → Embedding 向量化 → 向量数据库/向量索引 → 问题检索 Top-K 文档块 → 拼接 context → 大模型生成答案
|
步骤 |
作用 |
关键对象 |
|
文档读取 |
把 txt、PDF、网页等资料读入程序 |
TextLoader / SimpleDirectoryReader |
|
文档切分 |
把长文档切成可检索的小块 |
RecursiveCharacterTextSplitter / SentenceSplitter |
|
向量化 |
把文本转换为语义向量 |
HuggingFaceEmbeddings / OpenAIEmbedding |
|
向量存储 |
保存文本块向量并支持相似度检索 |
Chroma / FAISS / VectorStoreIndex |
|
检索增强 |
根据问题召回相关片段作为 context |
similarity_search / Retriever |
|
答案生成 |
让 LLM 基于 context 生成答案 |
ChatOpenAI / QueryEngine |
二、LangChain 代码知识点
1. 文档读取:TextLoader
loader = TextLoader("./藜麦.txt", encoding="utf-8")
documents = loader.load()
TextLoader 用于读取本地 txt 文件。Windows 中文系统默认编码可能是 GBK,而很多文本文件实际是 UTF-8,因此必须显式设置 encoding="utf-8",否则容易出现 UnicodeDecodeError。
|
对象/参数 |
含义 |
|
loader |
文档加载器,负责读取本地文本文件 |
|
documents |
Document 对象列表,虽然只有一个文件,也会返回列表 |
|
page_content |
Document 中的正文内容 |
|
metadata |
Document 中的元数据,例如 source 文件路径 |
|
encoding="utf-8" |
指定文本编码,避免 Windows 默认 GBK 解码失败 |
2. 文档切分:RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=256,
chunk_overlap=50,
separators=["\n\n", "\n", "。", "!", "?", ";", ",", " "]
)
RAG 不能简单地把整篇文档作为一个整体检索。切分的目的是把长文档变成多个语义较完整的小块,使检索更精准。
|
参数 |
作用 |
|
chunk_size=256 |
每个文本块大约 256 个字符,适合短中文文档 |
|
chunk_overlap=50 |
相邻文本块保留 50 个字符重叠,避免重要句子被截断 |
|
separators |
指定中文分隔符,优先按段落、句号、问号、分号、逗号等切分 |
3. 生成切分后的文本块
texts = text_splitter.create_documents(
[documents[0].page_content],
metadatas=[documents[0].metadata]
)
texts 是切分后的 Document 块列表。后续构建向量库时应使用 texts,而不是原始 documents。因为 RAG 检索的基本单位是 chunk,而不是整篇文章。
4. Embedding 模型:HuggingFaceEmbeddings
embedding = HuggingFaceEmbeddings(
model_name="moka-ai/m3e-small",
model_kwargs={"device": "cpu"},
encode_kwargs={"normalize_embeddings": True}
)
Embedding 的作用是把文本转成高维向量,使计算机能够基于语义相似度进行检索。例如“藜一般几月播种”和“藜麦什么时候种植”文字不同,但语义相近,向量距离也应较近。
|
参数 |
含义 |
|
model_name="moka-ai/m3e-small" |
中文语义向量模型,适合中文检索 |
|
device="cpu" |
使用 CPU 推理,小模型可以正常运行 |
|
normalize_embeddings=True |
对向量归一化,便于相似度检索 |
5. 向量数据库:Chroma
db = Chroma.from_documents(
documents=texts,
embedding=embedding
)
Chroma 用于保存“文本块 + 向量 + 元数据”。当用户输入问题时,系统会先把问题转为向量,再在 Chroma 中寻找语义最相似的文本块。
注意:这里 documents=texts 是正确写法。如果用 documents=documents,则相当于把整篇文章作为一个大块存入向量库,检索粒度会变粗。
6. 相似度检索:similarity_search
search_result = db.similarity_search(question, k=4)
|
参数 |
作用 |
|
question |
用户输入的问题 |
|
k=4 |
返回最相似的 4 个文本块 |
RAG 答错时,首先应检查 similarity_search 返回的片段是否相关。如果检索结果不相关,大模型后续再强也难以给出正确答案。
7. 大模型配置:ChatOpenAI
llm = ChatOpenAI(
model="deepseek-v3",
temperature=0,
max_retries=2,
api_key="...",
base_url="https://api.apiyi.com/v1"
)
ChatOpenAI 可以调用 OpenAI 兼容接口,不一定只调用 OpenAI 官方模型。这里通过 APIyi 的 base_url 调用 deepseek-v3。RAG 场景中 temperature 通常设为 0,使回答更稳定、更少发散。
安全建议:不要把真实 API Key 写死在代码中,更推荐使用环境变量,例如 api_key=os.getenv("APIYI_API_KEY")。
8. RAG Prompt 模板
prompt = PromptTemplate.from_template("""
你是一个严谨的知识库问答助手。
请只根据给定资料回答问题。
如果资料中没有相关信息,请回答“根据现有资料无法确定”。
资料:
{context}
问题:
{question}
答案:
""")
Prompt 的作用是约束大模型:只能根据检索到的 context 回答,如果资料中没有答案,就明确回答无法确定。这是减少幻觉的关键。
9. LCEL 链式调用
chain = prompt | llm | StrOutputParser()
这是 LangChain 新版 LCEL 写法,表示 PromptTemplate → LLM → 字符串输出解析器。相比旧版 LLMChain,这种写法更推荐,也更符合新版 LangChain 的设计。
10. RAG 问答函数
def rag_answer(question: str, top_k: int = 4):
docs = db.similarity_search(question, k=top_k)
context = "\n\n".join([f"资料{i + 1}:{doc.page_content}" for i, doc in enumerate(docs)])
answer = chain.invoke({"context": context, "question": question})
return answer, docs
该函数完成完整 RAG:输入问题 → 检索相关文本块 → 拼接 context → 调用大模型 → 返回答案和参考片段。返回 source_docs 可以帮助检查答案依据。
三、LlamaIndex 代码知识点
1. LlamaIndex 的定位
LlamaIndex 更偏向“数据接入 + 索引构建 + 查询引擎”的 RAG 框架。相比 LangChain 手写流程,LlamaIndex 封装程度更高,尤其适合文档问答、PDF 解析、索引管理、查询引擎和 Agent 工具调用。
|
模块 |
作用 |
|
Data Connectors |
读取本地文件、PDF、网页、数据库等数据源 |
|
Data Indexes |
对文档进行切分、向量化和索引构建 |
|
Query Engines |
基于索引回答用户问题 |
|
Chat Engines |
支持带上下文和记忆的聊天 |
|
Data Agents |
让大模型根据任务选择工具并调用 |
2. Settings 全局配置
Settings.llm = OpenAI(...)
Settings.embed_model = OpenAIEmbedding(...)
Settings 是 LlamaIndex 的全局配置,用来指定默认 LLM 和 Embedding 模型。后续创建索引和查询时,如果没有单独指定,就会默认使用 Settings 中的模型配置。
3. 文档读取:SimpleDirectoryReader / PyMuPDFReader / LlamaParse
LlamaIndex 支持多种数据读取方式。SimpleDirectoryReader 适合读取目录下的 txt、pdf 等文件;PyMuPDFReader 适合读取普通 PDF;LlamaParse 适合复杂 PDF、表格、论文和版式复杂文档。
|
Reader |
适用场景 |
|
SimpleDirectoryReader |
读取本地目录中的多种文件 |
|
PyMuPDFReader |
读取普通 PDF 文本 |
|
BeautifulSoupWebReader |
读取网页内容 |
|
LlamaParse |
解析复杂 PDF、表格、论文、报告,输出 Markdown |
4. 文档切分:SentenceSplitter
splitter = SentenceSplitter(
chunk_size=1024,
chunk_overlap=100,
paragraph_separator="\n\n"
)
nodes = splitter.get_nodes_from_documents(documents)
LlamaIndex 中切分后的文本块叫 nodes,对应 LangChain 中的 texts。nodes 是后续构建 VectorStoreIndex、SummaryIndex 和 QueryEngine 的基础。
5. 向量索引:VectorStoreIndex
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine()
response = query_engine.query("问题")
VectorStoreIndex 会自动完成文档切分、向量化、索引构建等流程。相比 LangChain 中手动构建 Chroma,LlamaIndex 的写法更简洁。
6. FAISS 与 StorageContext
faiss_index = faiss.IndexFlatL2(embed_dim)
vector_store = FaissVectorStore(faiss_index=faiss_index)
storage_context = StorageContext.from_defaults(vector_store=vector_store)
FAISS 是高性能向量检索库,适合大规模向量检索。StorageContext 是 LlamaIndex 中统一管理存储后端的对象,用于指定向量存储、文档存储、索引存储等。
7. Retriever 与 QueryEngine
vector_retriever = VectorIndexRetriever(index=vector_index, similarity_top_k=5)
query_engine = RetrieverQueryEngine.from_args(retriever=vector_retriever)
Retriever 负责检索相关 nodes,QueryEngine 负责把检索结果组织成上下文并调用 LLM 生成答案。这相当于 LangChain 中自己写的 rag_answer 函数。
8. SummaryIndex 与全文总结
summary_index = SummaryIndex(nodes)
summary_query_engine = summary_index.as_query_engine(
response_mode="tree_summarize",
use_async=True
)
SummaryIndex 用于全文总结。tree_summarize 会先对多个文本块分别总结,再递归合并成最终总结,适合长文档概括。use_async=True 可以提高长文档总结效率。
9. Agent 工具调用:vector_tool 与 summary_tool
LlamaIndex 可以把查询函数或查询引擎包装成工具。例如 vector_tool 负责具体问题检索,summary_tool 负责全文总结。Agent 可以根据用户问题自动判断调用哪个工具。
|
工具 |
适合问题 |
|
vector_tool |
查询具体事实,例如“第几页讲了什么”“某个指标是多少” |
|
summary_tool |
总结全文,例如“概括这篇文章”“总结主要内容” |
四、LangChain 与 LlamaIndex 对比
|
对比项 |
LangChain |
LlamaIndex |
|
文档读取 |
TextLoader 等 Loader |
SimpleDirectoryReader、PyMuPDFReader、LlamaParse |
|
切分对象 |
texts |
nodes |
|
切分器 |
RecursiveCharacterTextSplitter |
SentenceSplitter |
|
Embedding |
HuggingFaceEmbeddings |
OpenAIEmbedding / 自定义 Embedding |
|
向量库 |
Chroma |
FAISS / VectorStoreIndex / 其他 VectorStore |
|
检索方式 |
db.similarity_search() |
VectorIndexRetriever.retrieve() |
|
问答方式 |
手写 rag_answer + chain.invoke |
query_engine.query() |
|
Prompt 控制 |
PromptTemplate |
system_prompt / query engine 参数 |
|
工具调用 |
可用 Tool/Runnable 自定义 |
FunctionTool / QueryEngineTool 更方便 |
|
特点 |
灵活、流程清楚、适合学习底层 |
封装高、适合复杂文档问答系统 |
五、学习 RAG 时必须掌握的关键点
1. RAG 的关键不是“让模型更聪明”,而是“让模型基于外部知识回答”。
私有文档、课程资料、企业知识库、论文报告等通常不在大模型训练数据中,因此需要通过检索把相关资料喂给模型。
2. Chunk 质量直接影响检索质量
chunk 太小会导致语义不完整,chunk 太大会导致检索不精准。中文短文档可以使用 256-500 字符,长 PDF 或报告可以使用 800-1200 字符,并配合适当 overlap。
3. Embedding 决定语义匹配能力
如果中文资料使用不适合中文的 Embedding,检索结果可能偏差较大。常见中文模型包括 m3e-small、bge-small-zh、bge-base-zh 等。
4. 检索结果要可检查
调试 RAG 时应打印检索到的参考片段。只有确认检索片段相关,才能继续判断模型回答是否正确。
5. Prompt 必须限制模型不要胡编
例如“请只根据给定资料回答;如果资料中没有相关信息,请回答无法确定”。这是知识库问答中非常重要的安全约束。
6. API Key 不应写死在代码里
真实项目中应使用环境变量或配置文件管理 API Key,避免泄露。
六、推荐排错顺序
|
顺序 |
检查内容 |
常见问题 |
|
1 |
文件是否读取成功 |
路径错误、编码错误、文件为空 |
|
2 |
文本是否切分成功 |
chunk 太大或太小、中文分隔符不合适 |
|
3 |
Embedding 是否加载成功 |
模型下载失败、网络问题、设备配置错误 |
|
4 |
向量库是否构建成功 |
Chroma/FAISS 安装或版本问题 |
|
5 |
检索片段是否相关 |
Embedding 不合适、top_k 设置不合理、chunk 策略不佳 |
|
6 |
Prompt 是否合理 |
没有限制模型只基于资料回答 |
|
7 |
LLM 接口是否成功 |
API Key、base_url、model 名称或权限问题 |
七、最终结论
LangChain 代码适合学习 RAG 的底层流程,能清楚看到读取、切分、向量化、检索、拼接 context 和调用大模型的每一步。LlamaIndex 代码更适合快速搭建文档问答系统,尤其在复杂 PDF 解析、索引构建、查询引擎、总结工具和 Agent 调用方面更方便。
两者的核心思想完全一致:先检索,再生成。真正决定 RAG 效果的关键不是单一大模型,而是文档切分质量、Embedding 检索质量、向量库召回效果、Prompt 约束和结果可追溯性。
更多推荐


所有评论(0)