LLM Cookbook学习——使用 LangChain 开发应用程序>基于文档的问答 Question Answering over Documents
2026/9/2 7:58:47 网站建设 项目流程

一、为什么需要“基于文档的问答”

普通的大语言模型虽然拥有很强的语言理解和生成能力,但是它本身并不知道我们的私有数据

例如:

  • 公司内部文档

  • 产品说明书

  • 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

负责:

保存向量 执行向量搜索

例如:

db

Retriever

负责:

根据 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_rerank

1. 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 TokenTextSplitter

3. Embedding

理解:

文本如何映射到高维语义空间

以及:

Cosine Similarity Euclidean Distance Dot Product

4. Vector Database

进一步学习:

FAISS Chroma Milvus Qdrant Pinecone

5. Retrieval

进一步学习:

Similarity Search MMR Metadata Filter Hybrid Search Multi-Query Retrieval Parent Document Retrieval

6. 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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询