RAG入门指南:用LangChain构建文档问答系统(完整代码)
标签:#ai #langchain #python #教程
大语言模型并不了解你的私人文档。ChatGPT可以为你写一篇关于量子计算的文章,但它对你公司的内部维基、客户报告或上周二保存的PDF文件一无所知——除非你每次都手动粘贴进去。
RAG(检索增强生成)解决了这个问题。本文将介绍相关概念,并提供一份完整的可运行代码示例。
架构
你的文档 → 分割成块 → 转换为嵌入向量 → 存储在向量数据库中
用户问题 → 转换为嵌入向量 → 查找最匹配的块 → 将问题+相关块发送给大语言模型 → 生成答案
实际上只有两个步骤:检索(找到数据中的相关部分)和生成(让大语言模型利用这些内容来回答,而不是仅凭训练数据猜测)。
环境配置
pip install langchain langchain-community langchain-openai faiss-cpu
你还需要设置 OPENAI_API_KEY 环境变量:
export OPENAI_API_KEY="your-key-here"
完整可运行示例——文本文档
from langchain_community.document_loaders import TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain.chains import RetrievalQA
1. 加载文档
loader = TextLoader("client_seo_report.txt")
documents = loader.load()
2. 分割成块——大语言模型处理聚焦的小块比处理大段文本效果更好
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50 # 重叠部分防止关键句被截断
)
chunks = splitter.split_documents(documents)
3. 将块转换为嵌入向量并存储在本地向量索引中
embeddings = OpenAIEmbeddings()
vector_store = FAISS.from_documents(chunks, embeddings)
4. 连接检索与生成
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
retriever=vector_store.as_retriever(search_kwargs={"k": 4}) # 返回最匹配的4个块
)
5. 针对你的文档提问
answer = qa_chain.invoke("What was the biggest traffic change this month?")
print(answer["result"])
这就是一个完整可运行的RAG管道,大约20行代码。
替换为PDF而非纯文本
大多数实际文档都不是.txt文件。只需更改加载器即可:
from langchain_community.document_loaders import PyPDFLoader
loader = PyPDFLoader("client_seo_report.pdf")
documents = loader.load()
管道中的其他部分完全保持不变
LangChain提供了大多数常见格式的加载器——Word文档用Docx2txtLoader,电子表格用CSVLoader,直接抓取URL用WebBaseLoader。下游管道每次都相同。
为什么chunk_size和chunk_overlap比看起来更重要
这几乎是每个人第一次都会遇到的问题:
- 块大小过大 → 检索会拉回大量不相关的周围文本,大语言模型的回答会变得模糊。
- 块大小过小 → 你会丢失上下文;一个块可能没有足够的周围信息来独立发挥作用。
- 没有重叠 → 你可能会在重要信息所在的位置恰好截断句子的含义。
没有通用的"正确"数值——对于报告类文本,500个字符加50个重叠是一个合理的起点,但请根据你的实际文档进行实验。
常见错误(除了分块之外)
- 期望第一次就获得完美的检索结果。有时,通过嵌入相似度找到的"最匹配"块实际上并不是问题最相关的块。这很正常——调整检索质量是大部分实际工程工作所在,远不止这个示例这么简单。
- 没有按元数据过滤。如果你正在索引来自多个客户或时间段的文档,请用元数据(client_name, date)标记你的块,这样你就可以过滤搜索,而不是每次都搜索所有内容:
vector_store.as_retriever(
search_kwargs={"k": 4, "filter": {"client_name": "acme_corp"}}
)
- 使用了错误的k值。检索的块太少(k=1或2),大语言模型可能没有足够的上下文。太多(k=10以上),你就在为模型不需要的token付费,并且稀释了相关信号。k=3到k=5是一个合理的起始范围。
后续方向
一旦这个基本模式运行起来,自然的下一步包括:
- 当你需要扩展或跨会话持久化时,尝试使用生产级向量存储(Pinecone、Chroma或Weaviate)替代本地FAISS
- 当你索引多个来源时,按上述方法添加元数据过滤
- 尝试混合搜索(将关键词搜索与嵌入相似度结合)——纯嵌入搜索有时会漏掉精确匹配的术语,如产品名称或代码
我接下来会介绍智能体——让大语言模型能够实际执行操作,而不仅仅是回答问题。RAG是许多后续功能的基础,所以如果你理解了本文,你已经走完了大部分路程。
如果你在构建过程中遇到问题,请在评论区留言——我很乐意帮助调试。
我是Piyush Kumar Soni,一名AI开发者和LLM集成专家,拥有15年以上数字代理运营经验。我撰写关于SEO和数字营销的实用AI工具文章。