一、为什么需要“基于文档的问答”
普通的大语言模型虽然拥有很强的语言理解和生成能力,但是它本身并不知道我们的私有数据。
例如:
公司内部文档
产品说明书
PDF 论文
CSV 商品数据库
项目设计文档
技术手册
自己整理的学习笔记
如果直接问 LLM:
请告诉我公司内部文档中 XXX 模块是怎么设计的?LLM 并没有看到这份文档,因此无法可靠回答。
一种最直接的办法是:
文档内容 + 用户问题 → LLM但实际文档可能有几十页、几百页甚至数 GB,不可能把所有内容一次性塞进 Prompt。
因此需要一种新的思路:
用户问题 ↓ Embedding ↓ 向量相似度搜索 ↓ 找出最相关的几个文档片段 ↓ 相关文档片段 + 用户问题 ↓ LLM ↓ Answer这实际上就是后来非常常见的:
RAG —— Retrieval-Augmented Generation
即:
检索增强生成。
核心思想非常简单:
不是让 LLM 记住所有知识,而是在回答之前先把相关知识检索出来,再交给 LLM。
二、本节整体流程
LangChain 中基于文档问答的基本流程可以概括为:
原始文档 ↓ Document Loader ↓ Documents ↓ Text Splitter ↓ Chunks ↓ Embedding Model ↓ Vectors ↓ Vector Store ↓ Retriever ↓ 相关 Documents ↓ RetrievalQA ↓ LLM ↓ 答案可以进一步分成两个阶段。
阶段一:建立知识库
Document ↓ Load ↓ Split ↓ Embedding ↓ Vector Store阶段二:执行问答
Question ↓ Embedding ↓ Similarity Search ↓ Relevant Documents ↓ Prompt ↓ LLM ↓ Answer所以整个文档问答系统最重要的四个概念是:
Loader Splitter Embedding Retriever最终再由:
RetrievalQA把它们连接起来。
三、加载 CSV 文档
课程使用的是一个户外服装商品数据集:
OutdoorClothingCatalog_1000.csv里面包含商品名称、商品描述等信息。
首先导入 CSVLoader:
from langchain.document_loaders import CSVLoader然后加载文件:
file = "OutdoorClothingCatalog_1000.csv" loader = CSVLoader( file_path=file, encoding="utf-8" )注意:
loader本身只是一个Loader 对象。
真正加载数据需要:
docs = loader.load()此时:
docs通常是:
List[Document]也就是说,它不是普通字符串列表,而是多个 LangChainDocument对象。
四、理解 LangChain 的 Document
LangChain 中非常重要的数据结构就是:
Document一个 Document 通常包含两个核心部分:
Document( page_content="...", metadata={...} )其中:
1. page_content
真正的文本内容。
例如:
doc.page_content可能得到:
name: Women's Campside Oxfords description: This ultracomfortable lace-to-toe Oxford...2. metadata
描述这段文本从哪里来的。
例如:
doc.metadata可能得到:
{ "source": "OutdoorClothingCatalog_1000.csv", "row": 0 }所以可以理解成:
Document ├── page_content │ └── metadata其中 metadata 非常重要。
因为最终系统不仅可以回答:
答案是什么?还可以回答:
答案来自哪一个文件? 哪一页? 哪一行?这也是 RAG 系统能够进行来源追踪的重要基础。
五、Embedding 是什么
这一部分是整个文档问答系统最核心的概念之一。
1. 计算机不能直接理解文本语义
例如:
What clothing can keep me warm?和:
Which jacket is suitable for cold weather?两个句子的单词并不完全相同。
如果使用传统关键词搜索:
warm未必能匹配:
cold weather但实际上二者语义高度相关。
因此需要把文本转换成:
向量即:
Text ↓ Embedding Model ↓ Vector例如:
"I like dogs"可能被转换为:
[ 0.012, -0.034, 0.128, ... ]真实 Embedding 往往有几百甚至几千维。
六、Embedding 的核心意义
Embedding 最大的特点是:
语义相近的文本,在向量空间中的距离通常也比较接近。
例如:
dog puppy animal对应向量:
dog → Vector A puppy → Vector B animal → Vector C其中:
distance(A, B)往往会比较小。
而:
dog airplane对应向量距离通常更大。
因此可以通过:
向量距离实现:
语义搜索而不是简单关键词匹配。
七、问题是怎么找到对应文档的
假设数据库中有三个文档:
Document A: 这是一件防水冲锋衣。 Document B: 这是一双夏季凉鞋。 Document C: 这是一件适合冬季穿着的羽绒服。Embedding 后:
A → Vector A B → Vector B C → Vector C用户问:
What clothes are suitable for cold weather?问题也进行 Embedding:
Question ↓ Embedding ↓ Vector Q然后计算:
similarity(Q, A) similarity(Q, B) similarity(Q, C)假设结果:
A = 0.71 B = 0.15 C = 0.92那么系统就会优先返回:
Document C这就是:
Similarity Search
即:
相似度搜索。
八、Vector Store 是什么
如果只有几个文档,我们直接比较向量即可。
但真实项目可能有:
10 万个文档 100 万个 chunk 1000 万个向量所以需要专门保存和搜索向量的数据结构。
这就是:
Vector Store
也可以理解为:
向量数据库 / 向量索引。
课程示例使用:
DocArrayInMemorySearch导入:
from langchain.vectorstores import DocArrayInMemorySearch它属于一种:
内存向量数据库特点是:
简单 速度快 适合教学 适合小型数据 无需额外数据库服务但不适合真正的大规模生产环境。
实际工程常见的向量数据库还包括:
Chroma FAISS Milvus Pinecone Qdrant Weaviate九、使用 VectorstoreIndexCreator
课程首先演示了一种非常简洁的写法:
from langchain.indexes import VectorstoreIndexCreator然后:
index = VectorstoreIndexCreator( vectorstore_cls=DocArrayInMemorySearch ).from_loaders([loader])这一行虽然很短,实际上内部帮我们完成了很多事情。
可以近似理解为:
Loader ↓ Load Documents ↓ Split Documents ↓ Embedding ↓ Create Vector Store ↓ Store Vectors也就是说:
VectorstoreIndexCreator实际上是一个高度封装的工具。
十、直接进行文档问答
创建好 index 后,可以直接:
query = "Please list all your shirts with sun protection in a table in markdown and summarize each one." response = index.query(query)整个内部流程实际上是:
query ↓ query embedding ↓ vector search ↓ relevant documents ↓ prompt ↓ LLM ↓ answer所以:
index.query()虽然只有一行代码,但背后实际上已经完成了一套简单 RAG。
十一、VectorstoreIndexCreator 到底帮我们干了什么
理解这部分非常重要。
高层 API:
index = VectorstoreIndexCreator(...).from_loaders([loader])实际上近似等价于:
docs = loader.load() splits = text_splitter.split_documents(docs) embeddings = OpenAIEmbeddings() vectorstore = DocArrayInMemorySearch.from_documents( splits, embeddings )因此必须建立一个认知:
VectorstoreIndexCreator不是某种新的 AI 技术。
它只是帮我们把:
Load Split Embed Store封装到了一起。
十二、为什么必须 Split 文档
假设我们有一本:
500 页 PDF如果把整个 PDF 作为一个 Document:
PDF ↓ 一个巨大的 Vector问题就出现了。
假设用户问:
第 378 页介绍的 AXI outstanding 是什么意思?整本 PDF 只有一个 Embedding。
那么这个向量表达的是:
整本 PDF 的综合语义而不是第 378 页具体内容。
因此需要:
Document ↓ Text Splitter ↓ Chunk1 Chunk2 Chunk3 ... ChunkN每个 Chunk 单独生成向量:
Chunk1 → Vector1 Chunk2 → Vector2 Chunk3 → Vector3 ...这样用户提问后,就可以精准找到:
Chunk137 Chunk284 Chunk301十三、Chunk Size 和 Chunk Overlap
文档分割中两个非常重要的参数是:
chunk_size chunk_overlap例如:
RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=150 )含义:
chunk_size = 1000表示每个文本块大约 1000 个字符或 token 单位附近。
而:
chunk_overlap = 150意味着相邻 Chunk 会保留一定重复内容。
例如:
Chunk 1: AAAAAAAA BBBBBBBB CCCCCCCC Chunk 2: CCCCCCCC DDDDDDDD EEEEEEEE其中:
CCCCCCCC就是 overlap。
十四、为什么需要 Chunk Overlap
假设一句关键内容刚好处于两个 Chunk 的边界:
Chunk1: ......AXI允许多个事务同时处于 ------------------分割点------------------ Chunk2: 未完成状态,这种机制叫Outstanding......如果完全没有 overlap:
chunk_overlap = 0那么两个 Chunk 都可能失去完整语义。
加入 overlap 后:
Chunk1: AXI允许多个事务同时处于未完成状态 Chunk2: 多个事务同时处于未完成状态,这种机制叫Outstanding这样 Embedding 的语义完整性会明显更好。
所以:
Overlap 本质上是在牺牲少量存储空间,换取上下文连续性。
十五、手动实现完整流程
相比:
VectorstoreIndexCreator更值得掌握的是底层过程。
核心步骤:
docs = loader.load()然后拆分:
from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=150 ) splits = text_splitter.split_documents(docs)生成 Embedding:
from langchain.embeddings.openai import OpenAIEmbeddings embeddings = OpenAIEmbeddings()创建向量数据库:
db = DocArrayInMemorySearch.from_documents( splits, embeddings )至此:
文档知识库就已经创建完成。
十六、Retriever 是什么
有了 Vector Store 后,并不是直接交给 LLM。
通常先转换成:
Retriever例如:
retriever = db.as_retriever()Retriever 的职责非常明确:
Question ↓ Retriever ↓ Relevant Documents即:
Retriever 负责找资料,LLM 负责读资料并回答。
这是 RAG 系统最重要的职责划分之一。
十七、Retriever 和 Vector Store 的区别
这两个概念很容易混淆。
Vector Store
负责:
保存向量 执行向量搜索例如:
dbRetriever
负责:
根据 Query 获取相关 Document例如:
retriever = db.as_retriever()可以理解为:
Vector Store = 数据库 Retriever = 数据库查询接口因此:
LLM ↑ Retriever ↑ Vector Store十八、Top-K 检索
Retriever 通常会设置:
k例如:
retriever = db.as_retriever( search_kwargs={"k": 4} )表示:
每次返回最相关的 4 个文档块假设:
Question与数据库 Chunk 相似度:
Chunk1 0.94 Chunk2 0.87 Chunk3 0.81 Chunk4 0.79 Chunk5 0.52 Chunk6 0.31如果:
k = 4那么:
Chunk1 Chunk2 Chunk3 Chunk4会送给 LLM。
十九、k 是越大越好吗?
不是。
如果:
k太小:
可能漏掉有用信息如果:
k太大:
大量无关文本进入 Prompt会导致:
Token 消耗增加 噪声增加 LLM注意力被稀释 回答质量下降因此:
k本质上也是一个需要调优的超参数。
典型值可能是:
3 4 5 8具体取决于:
Chunk Size 文档类型 查询复杂度 模型 Context Window二十、RetrievalQA
完成 Retriever 之后,就可以构造真正的:
RetrievalQA
导入:
from langchain.chains import RetrievalQA创建:
qa = RetrievalQA.from_chain_type( llm=llm, chain_type="stuff", retriever=retriever )然后:
result = qa.run(query)完整流程:
Question ↓ Retriever ↓ Top-K Documents ↓ RetrievalQA ↓ Prompt ↓ LLM ↓ Answer二十一、RetrievalQA 的本质
RetrievalQA 其实就是把两个系统连接:
Retriever + LLM所以:
RetrievalQA = Retrieval + Generation也就是:
RAG核心逻辑可以近似写成:
docs = retriever.get_relevant_documents(question) context = "\n".join( doc.page_content for doc in docs ) prompt = f""" 请根据下面资料回答问题。 资料: {context} 问题: {question} """ answer = llm(prompt)LangChain 只是把这些逻辑封装了。
二十二、chain_type 是什么意思
课程中一个非常重要的参数:
chain_type常见方式包括:
stuff map_reduce refine map_rerank1. stuff
最简单的一种:
Doc1 Doc2 Doc3 Doc4 ↓ 全部塞入 Prompt ↓ LLM也就是:
chain_type="stuff"优点:
简单 速度快 只调用一次 LLM缺点:
文档过多容易超过 Context Window适合:
Top-K较少 Chunk较短 问题简单二十三、map_reduce
流程:
Doc1 → LLM → Answer1 Doc2 → LLM → Answer2 Doc3 → LLM → Answer3 ↓ Reduce ↓ Final Answer即:
Map ↓ Reduce优点:
可以处理大量文档缺点:
需要多次调用 LLM 成本更高 速度更慢二十四、refine
流程:
Doc1 ↓ Answer V1 ↓ Doc2 ↓ Answer V2 ↓ Doc3 ↓ Answer V3即:
在已有答案基础上不断加入新资料进行修正。
适合:
需要逐步补充信息 需要综合多个文档缺点:
LLM调用次数多 后面文档会依赖前面的结果二十五、map_rerank
流程类似:
Doc1 → Answer1 + Score1 Doc2 → Answer2 + Score2 Doc3 → Answer3 + Score3然后选择:
Score最高的答案适合:
答案通常存在于单一文档块中二十六、四种 Chain 对比
| Chain | 工作方式 | 优点 | 缺点 |
|---|---|---|---|
| stuff | 所有文档一次输入 | 快、简单 | 容易超 Token |
| map_reduce | 分别处理再汇总 | 可处理大量文本 | LLM 调用多 |
| refine | 不断修正答案 | 综合能力强 | 延迟高 |
| map_rerank | 分别回答并评分 | 易找到最佳答案 | 计算量较大 |
最常用的入门方式通常还是:
chain_type="stuff"二十七、返回 Source Documents
做 RAG 时,非常推荐:
return_source_documents=True例如:
qa = RetrievalQA.from_chain_type( llm=llm, chain_type="stuff", retriever=retriever, return_source_documents=True )查询:
result = qa({"query": query})得到的结果可能类似:
{ "result": "...", "source_documents": [...] }这样就能分别获取:
result["result"]最终答案。
以及:
result["source_documents"]LLM 回答所依据的资料。
二十八、为什么 Source Documents 很重要
普通 ChatGPT 的一个问题是:
你不知道答案从哪里来的。而 RAG 可以做到:
Answer + Evidence例如:
答案: 该产品具有 UPF 50+ 防晒能力。 来源: OutdoorClothingCatalog_1000.csv row = 327这对于:
企业知识库 法律问答 金融问答 论文问答 技术文档问答尤其重要。
因为实际系统通常不仅需要:
Answer还需要:
Evidence二十九、整个 RetrievalQA 的内部结构
完整结构可以理解为:
User │ ▼ Question │ ▼ Query Embedding │ ▼ Vector Store │ Similarity Search │ ▼ Retriever │ Top-K Documents │ ▼ Prompt ┌──────────┴──────────┐ │ │ Documents Question │ │ └──────────┬──────────┘ ▼ LLM │ ▼ Answer这实际上就是最基础的:RAG Pipeline
三十、为什么不能直接让 LLM 搜数据库
这是理解 RAG 架构的关键。
LLM 本身擅长:
理解 推理 总结 生成但它并不擅长直接:
存储百万篇文档 搜索百万篇文档 做大规模向量最近邻搜索所以系统进行了职责划分:
Vector Database 负责: Search LLM 负责: Reason + Generate形成:
Retriever + LLM这也是现代 RAG 系统最基本的架构思想。
三十一、RAG 和模型训练的区别
很多初学者容易认为:
我要让 ChatGPT 学习自己的 PDF,是不是要重新训练模型?
实际上,大多数情况下不用。
Fine-tuning
相当于:
修改模型参数主要用于:
行为调整 输出格式 特定风格 任务能力RAG
相当于:
模型不变 + 动态提供知识例如:
1000页公司文档 ↓ Vector Database ↓ Retriever ↓ LLM所以:
知识库问答通常优先考虑:
RAG而不是 Fine-tuning。
三十二、Embedding 模型和 LLM 不是一个东西
RAG 系统通常实际上包含两个模型。
模型 1:Embedding Model
负责:
Text ↓ Vector主要用于:
检索模型 2:LLM
负责:
Context + Question ↓ Answer主要用于:
理解 推理 生成所以:
Embedding Model ≠ LLM两者作用完全不同。
三十三、RAG 最关键的不是“回答”,而是“检索”
很多人刚开始做 RAG 会把注意力全部集中在:
换更强的 LLM实际上,一个非常重要的工程规律是:
如果检索阶段没有找到正确资料,再强的 LLM 也很难产生可靠答案。
即:
Wrong Retrieval ↓ Wrong Context ↓ LLM ↓ Wrong Answer相反:
Good Retrieval ↓ Correct Context ↓ LLM ↓ Good Answer因此 RAG 项目中的核心问题往往不是:
模型够不够大?而是:
Chunk 怎么切? Embedding 怎么选? Top-K 多少? 怎么做召回? 怎么做 Rerank?三十四、Chunk Size 对 RAG 的影响
例如查询:
AXI4为什么取消写数据交织?如果 Chunk 太小:
Chunk A: AXI4取消了 Chunk B: 写数据交织功能 Chunk C: 主要目的是简化互联逻辑每个 Chunk 都缺少完整语义。
检索效果可能下降。
但如果 Chunk 太大:
Chunk: 包含AXI ID、Burst、Outstanding、Interleaving、 Exclusive、QoS、Cache、Region……那么真正相关内容只占很小比例。
Embedding 语义又会变得模糊。
所以:
Chunk 太小 → 上下文破碎 Chunk 太大 → 语义稀释合理 Chunk Size 是 RAG 的重要调优内容。
三十五、最简单的 RAG 伪代码
如果把 LangChain 所有封装去掉,整个 RAG 可以写成:
# 1. 加载文档 documents = load_documents() # 2. 文档切片 chunks = split_documents(documents) # 3. 每个 chunk 生成 embedding vectors = [] for chunk in chunks: vector = embedding_model.embed(chunk) vectors.append(vector) # 4. 保存向量 vector_db.add(vectors) # ========================= # 用户开始提问 # ========================= question = input() # 5. 问题 embedding query_vector = embedding_model.embed(question) # 6. 相似度搜索 documents = vector_db.search( query_vector, top_k=4 ) # 7. 构造上下文 context = "\n".join(documents) # 8. 构造 Prompt prompt = f""" 请只根据以下资料回答: {context} 问题: {question} """ # 9. LLM回答 answer = llm(prompt)理解这段伪代码之后,再看:
RetrievalQA VectorstoreIndexCreator Retriever VectorStore就会非常容易。
三十六、本节知识结构总结
这一章表面是在学习:
RetrievalQA实际上学习的是一套完整的:
文档问答 / RAG 思维
整个逻辑:
┌─────────────┐ │ Document │ └──────┬──────┘ ↓ Document Loader ↓ Documents ↓ Text Split ↓ Chunks ↓ Embedding ↓ Vectors ↓ Vector Store ↓ ────────────────────────────────── ↑ Question ↓ Embedding ↓ Similarity Search ↓ Retriever ↓ Relevant Chunks ↓ Prompt ↓ LLM ↓ Answer三十七、几个必须记住的概念
Document Loader
负责把外部数据读取成 LangChain Document。Text Splitter
负责把大型 Document 拆成多个 Chunk。Embedding
把文本转换成能够表达语义的向量。Vector Store
保存向量并支持相似度搜索。Retriever
根据用户问题找到最相关的 Documents。RetrievalQA
Retriever + LLM负责完成:
检索 + 回答三十八、一句话理解 RAG
如果只记一句话:
RAG = 先查资料,再让大模型根据查到的资料回答。
如果写成工程流程:
User Question ↓ Retrieval ↓ Relevant Context ↓ Augmented Prompt ↓ Generation因此:
Retrieval + Augmented + Generation = RAG三十九、从本节进一步学习什么
学完这一节后,可以继续深入下面几个方向。
1. Document Loading
解决:
PDF怎么加载? Word怎么加载? 网页怎么加载? Markdown怎么加载?2. Document Splitting
重点研究:
chunk_size chunk_overlap RecursiveCharacterTextSplitter TokenTextSplitter3. Embedding
理解:
文本如何映射到高维语义空间以及:
Cosine Similarity Euclidean Distance Dot Product4. Vector Database
进一步学习:
FAISS Chroma Milvus Qdrant Pinecone5. Retrieval
进一步学习:
Similarity Search MMR Metadata Filter Hybrid Search Multi-Query Retrieval Parent Document Retrieval6. Rerank
基础流程:
Vector Search ↓ Top 20 ↓ Reranker ↓ Top 5 ↓ LLM可以提高最终进入 LLM 的文档质量。
四十、学习心得
真正值得掌握的是它背后的架构:
文档 ↓ Chunk ↓ Embedding ↓ Vector Store ↓ Retriever ↓ Context ↓ LLM以后即使不使用 LangChain,而换成:
LlamaIndex Haystack Milvus FAISS Elasticsearch 自研 RAG核心思想仍然基本相同。
VectorstoreIndexCreator本质封装的是:
Load + Split + Embed + Store而:
RetrievalQA本质封装的是:
Retrieve + Build Context + Prompt + LLM Generation只要把这两个结构彻底理解,后面学习更复杂的 RAG 系统就会容易很多。
四十一、本节最终思维导图
LangChain 文档问答 │ ├── 1. 文档加载 │ └── CSVLoader │ ├── 2. Document │ ├── page_content │ └── metadata │ ├── 3. 文档切分 │ ├── chunk_size │ └── chunk_overlap │ ├── 4. Embedding │ └── Text → Vector │ ├── 5. Vector Store │ └── DocArrayInMemorySearch │ ├── 6. Similarity Search │ ├── 7. Retriever │ └── Top-K Documents │ ├── 8. RetrievalQA │ ├── stuff │ ├── map_reduce │ ├── refine │ └── map_rerank │ └── 9. LLM ↓ Answer