在构建与大语言模型(LLM)交互的应用时,我们经常会遇到一个棘手的问题:模型的知识是静态的,而且容易产生幻觉。如果我问模型关于昨天发生的新闻,或者询问它训练数据中不存在的公司内部文档,它往往会自信地编造错误答案,也就是我们常说的“一本正经地胡说八道”。
为了解决这个问题,我们需要一种方法,能够让 LLM 在生成答案之前,先去查阅相关的、最新的外部资料。这正是我们今天要深入探讨的核心主题——检索增强生成。在本文中,我们将手把手教你如何使用 LangChain 这一强大的框架,结合 Google Gemini 2.5 Flash 模型,从零开始构建一个属于你自己的 RAG 应用程序。你将学习到如何处理文档、如何进行向量化存储,以及如何将检索到的上下文精准地喂给模型。
目录
什么是检索增强生成 (RAG)?
简单来说,RAG 是一种让模型“开卷考试”的模式。传统的 LLM 使用方式更像是“闭卷考试”,完全依赖脑中的记忆(参数)。而 RAG 赋予了模型去图书馆(外部知识库)查阅资料的能力,然后再根据资料回答问题。
RAG 的核心工作流程可以分为三个阶段:
- 索引:我们将数据(PDF、网页、文本)切分成小块,并将其转换为数学向量存储起来。
- 检索:当用户提问时,我们将问题也转换为向量,并在数据库中找到最相似的数据块。
- 生成:我们将问题 + 检索到的数据块一起发送给 LLM,让它基于这些事实生成答案。
为什么要使用 LangChain 构建 RAG?
虽然理论上我们可以自己写代码调用 OpenAI 或 Google 的 API 来实现上述流程,但LangChain 为我们提供了高度模块化的接口,极大地简化了开发复杂度。
- 组件标准化:无论是文档加载器、文本分割器,还是向量存储、检索器,LangChain 都提供了统一的接口。这意味着你可以轻松地从 INLINECODE5c974354 切换到 INLINECODE39870dd2,或者从 INLINECODE89b10ad1 切换到 INLINECODE853eb1cc,而无需重写大量代码。
- 链式编排:LangChain 允许我们将多个组件串联起来,形成一个完整的业务逻辑流。我们可以轻松地实现“先检索,再通过 PromptTemplate 组装,最后生成”的逻辑。
- 集成便利性:它对各种开源模型和商用模型(如 Google GenAI)都有极佳的封装。
准备工作:环境与工具
在开始写代码之前,我们需要搭建好“武器库”。对于本教程,我们将使用以下技术栈:
- LLM 模型:我们将使用 Google Gemini 2.5 Flash。这是一个速度极快且成本效益高的模型,非常适合用于 RAG 的生成阶段。
- 向量存储:使用 FAISS (Facebook AI Similarity Search)。这是目前最流行的本地向量搜索库,非常适合演示和中小规模应用。
- 嵌入模型:使用 SentenceTransformers。这是一个开源的优秀模型,能将文本转化为高质量的向量。
- 开发框架:LangChain。
请打开你的终端或 Google Colab,安装以下必要的库:
# 安装核心 LangChain 库及 Google GenAI 集成
pip install --upgrade langchain langchain-google-genai
# 安装向量数据库 FAISS (CPU 版本)
pip install faiss-cpu
# 安装用于生成文本嵌入的模型
pip install sentence-transformers
# 如果你在 Colab 中运行,确保支持用户数据管理
pip install google-colab
核心实战:一步步构建 RAG 系统
让我们通过一个完整的实战案例来串联这些知识点。假设我们是一家科技公司的开发者,我们希望构建一个能够回答关于“埃隆·马斯克”相关问题的机器人,而我们的知识源是一段简单的文本。
步骤 1:配置 API 密钥
首先,我们需要告诉代码谁有权限访问 Google 的模型。如果你是在本地运行,可以设置环境变量;如果你是在 Colab 中运行,最安全的方式是使用内置的 userdata 功能。
import google.generativeai as genai
from google.colab import userdata
# 从 Colab 的安全存储中获取 API Key
# 请务必在 Colab 左侧的钥匙图标中添加名为 ‘GOOGLE_API_KEY‘ 的 secret
try:
GOOGLE_API_KEY = userdata.get(‘GOOGLE_API_KEY‘)
except userdata.SecretNotFoundError:
raise Exception("请在 Colab 的 Secrets 中添加 GOOGLE_API_KEY")
# 配置 GenAI SDK
genai.configure(api_key=GOOGLE_API_KEY)
实用见解:在生产环境中,永远不要将 API Key 硬编码在代码里。使用 .env 文件或密钥管理服务是最佳实践。
步骤 2:加载与处理文档
在 RAG 中,垃圾进,垃圾出。文档处理至关重要。我们不能把整本书直接扔给模型,因为模型的上下文窗口有限,且太长的文本会干扰检索。我们需要将其“切分”。
from langchain.docstore.document import Document
# 模拟一份原始文档内容
raw_text = """
Elon Musk is a technology entrepreneur and investor. He is the CEO and lead designer of SpaceX,
CEO and product architect of Tesla, Inc., owner of X (formerly Twitter), and founder of the Boring Company.
Musk was born in Pretoria, South Africa. He briefly attended the University of Pretoria
before moving to Canada at age 17, acquiring citizenship through his Canadian-born mother.
Two years later, he matriculated at Queen‘s University and transferred to the University of Pennsylvania,
where he received bachelor‘s degrees in economics and physics.
"""
# 将原始文本封装成 LangChain 的 Document 对象
# 这里我们手动创建,实际项目中可以使用 DirectoryLoader, PyPDFLoader 等
documents = [Document(page_content=raw_text, metadata={"source": "elon_bio.txt"})]
# 检查加载结果
print(f"成功加载 {len(documents)} 个文档片段。")
步骤 3:文本切分
这是 RAG 中最容易被忽视的一步。如果块太大,检索会不够精准(包含无关信息);如果块太小,可能会丢失上下文。
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 定义切分器
# chunk_size: 每个块的最大字符数
# chunk_overlap: 块与块之间的重叠字符数,这有助于保持上下文的连贯性
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50, # 保持一定的重叠可以防止句子被生硬截断
length_function=len
)
# 执行切分
tsplits = text_splitter.split_documents(documents)
print(f"文档被切分为 {len(splits)} 个子块:")
for i, split in enumerate(splits):
print(f"--- 块 {i+1} (长度: {len(split.page_content)}) ---")
print(split.page_content[:100] + "...") # 打印前100个字符预览
步骤 4:创建向量嵌入与向量存储
现在,我们需要将这些文本块变成机器能理解的数学向量。我们将使用 INLINECODE6b284691 本地生成嵌入,并使用 INLINECODE3ec27f1b 进行存储。
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.vectorstores import FAISS
# 1. 初始化嵌入模型
# 我们使用一个轻量且效果好的模型 ‘all-MiniLM-L6-v2‘
# 如果你没有安装模型,这行代码会自动从 HuggingFace 下载
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
# 2. 创建 FAISS 向量存储
# 这一步会将 splits 中的文本通过 embeddings 模型转化为向量,并建立索引
db = FAISS.from_documents(tsplits, embeddings)
print("向量数据库构建完成!")
步骤 5:构建检索器并测试
有了向量库,我们就可以构建检索器了。让我们先测试一下检索功能,看看它能不能在我们提问时找到正确的段落。
# 将向量库转换为检索器
# search_kwargs={"k": 2} 表示每次只返回相似度最高的前2个结果
retriever = db.as_retriever(search_kwargs={"k": 2})
# 提出一个测试问题
query = "Where did Elon Musk go to university?"
# 执行检索
relevant_docs = retriever.get_relevant_documents(query)
print(f"
针对问题: ‘{query}‘
检索到以下相关片段:")
for i, doc in enumerate(relevant_docs):
print(f"
片段 {i+1}:")
print(doc.page_content)
步骤 6:组装 RAG 链与生成回答
最后,我们要把所有组件串联起来。我们将定义一个 Prompt 模板,告诉 LLM:“请仅使用下面的上下文信息来回答问题,如果不知道就说不知道。”
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
# 1. 初始化 LLM
# 这里我们指定使用 Gemini 2.5 Flash (gemini-2.0-flash-exp 或最新可用版本)
# temperature 设置为 0,确保回答更具事实性,减少随机性
llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash-exp", temperature=0)
# 2. 自定义 Prompt 模板
# 这是 RAG 成功的关键,我们需要清晰地指导模型如何利用上下文
prompt_template = """
You are a helpful assistant.
Use the following pieces of context to answer the question at the end.
If you don‘t know the answer based on the context, just say that you don‘t know, don‘t try to make up an answer.
Context: {context}
Question: {question}
Answer:
"""
PROMPT = PromptTemplate(
template=prompt_template, input_variables=["context", "question"]
)
# 3. 创建 QA 链
# chain_type="stuff" 是最简单的类型,它将所有检索到的文档塞入上下文
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=retriever,
return_source_documents=True, # 返回引用的来源文档,方便验证
chain_type_kwargs={"prompt": PROMPT}
# 4. 执行查询
user_query = "What degrees did Elon Musk receive?"
result = qa_chain({"query": user_query})
print(f"
最终回答:
{result[‘result‘]}")
print(f"
引用来源: {result[‘source_documents‘][0].metadata[‘source‘]}")
深入探讨:如何优化你的 RAG 系统
上面的代码只是一个最小可行性产品(MVP)。在实际生产环境中,我们还需要考虑很多优化细节。
1. 处理数据加载的复杂性
现实中的数据往往杂乱无章。你可能需要处理 PDF、Word 文档、甚至是网页。LangChain 提供了大量的 Loader。
from langchain.document_loaders import PyPDFLoader, WebBaseLoader
# 示例:加载 PDF
# pdf_loader = PyPDFLoader("path/to/document.pdf")
# pdf_docs = pdf_loader.load()
# 示例:加载网页
web_loader = WebBaseLoader("https://www.example.com/article")
web_docs = web_loader.load()
# 对于抓取到的网页,通常需要进行更激进的清洗,比如去除 HTML 标签、导航栏文字等
2. 元数据的重要性
向量搜索虽然强大,但有时候我们还需要“过滤”。例如,如果你只想搜索“2023年”的文档,单纯靠语义搜索可能不够精准。
# 在创建 Document 时,可以添加自定义的元数据
doc_with_metadata = Document(
page_content="The company revenue was 5 million dollars.",
metadata={"year": 2023, "category": "finance"}
)
# 在检索时,可以使用自查询检索器 来根据元数据过滤
# 这属于进阶用法,但非常实用。
3. 常见错误与调试技巧
- 回答不完整:如果你发现模型总是回答“不知道”,可能是因为你的
chunk_size太小,导致上下文被截断了。尝试增大块的大小。 - 检索不相关:如果你的文档包含太多专业术语,通用的
all-MiniLM模型可能无法很好地捕捉语义。尝试更换为特定领域的嵌入模型(如用于医疗或法律的专业模型)。 - 速度太慢:每次请求都重新生成嵌入是很慢的。确保你将
FAISS索引保存到本地磁盘,下次直接加载,而不是每次都重新构建。
4. 持久化向量存储
不要每次启动程序都重新计算向量!FAISS 支持保存和加载。
# 保存
vectorstore.save_local("faiss_index_elon")
# 加载
new_vectorstore = FAISS.load_local("faiss_index_elon", embeddings)
总结与后续步骤
在这篇文章中,我们不仅理解了 RAG 的基本原理,更重要的是,我们亲手编写代码实现了一个完整的检索增强生成系统。我们看到了如何将非结构化的文本转化为结构化的向量知识库,并利用 Google Gemini 强大的生成能力,基于特定事实回答问题。
关键要点回顾:
- LangChain 是胶水,它完美连接了文档、向量存储和 LLM。
- 文档切分 是一门艺术,需要根据数据特点调整参数。
- Prompt Engineering 至关重要,明确的指令能显著减少幻觉。
接下来的建议:
- 尝试引入记忆机制,让你的 RAG 机器人能够记住上一轮对话的内容。
- 探索更复杂的链结构,比如 Refine Chain(先总结片段,再逐步完善答案),适用于处理长文档。
- 尝试使用 Streamlit 或 Chainlit 为你的 RAG 系统构建一个可视化的聊天界面。
现在,去尝试构建你自己的知识库吧!无论是公司内部文档、个人笔记还是特定的行业资料,RAG 都能让你以全新的方式与数据对话。