好的,各位同学,大家好!我是你们的AI应用开发讲师。今天我们要聊一个非常“硬核”但又非常实用的主题——RAG检索增强生成实战。
很多同学可能已经玩过ChatGPT或者各种大模型,但会发现它们在回答一些需要“查资料”或“看文档”的问题时,容易“一本正经地胡说八道”。RAG技术就是为了解决这个问题而生的。它就像是给大模型配了一个“实时图书馆”,让它能先查资料再回答。
好,让我们正式开始这45分钟的进阶之旅。
---
# RAG检索增强生成实战
想象一下,你正在为公司开发一个智能客服机器人。如果直接用大模型,它可能不知道你们公司最新的产品参数、内部规章制度,甚至可能把竞品的信息当成你们的来回答。这显然不行。
RAG(Retrieval-Augmented Generation)的出现,完美解决了大模型“知识滞后”和“幻觉”问题。它通过先检索、后生成的方式,让AI模型能够:
1. 连接私有知识库:对接公司内部文档、产品手册、科研论文等非公开数据。 2. 提供可追溯的回答:回答问题时,能明确指出信息来源于哪份文档的第几页,增强可信度。 3. 降低模型幻觉:强迫模型基于检索到的真实文本进行回答,而不是凭空编造。 4. 实现知识的实时更新:只需更新知识库,无需重新训练或微调大模型。
实际场景:智能客服、企业知识库问答、AI辅助写作、在线教育答疑、法律/医疗文书辅助等。
学完这45分钟,你将能够:
1. 独立搭建一个基础的RAG问答系统:从文档加载到最终问答,跑通全流程。 2. 掌握文档切分与嵌入的核心技巧:知道如何把PDF、Word等文档变成计算机能理解的向量。 3. 熟练使用LangChain/LlamaIndex框架:用最流行的工具快速开发,而不是从零造轮子。 4. 了解多模态RAG的原理:知道如何让AI“看图说话”并检索相关图片。 5. 学会评估和优化RAG系统:能诊断出系统“回答不好”的原因,并给出改进方案。
我们将围绕RAG的四个核心模块展开:文档加载与切分、向量化与存储、检索与增强、生成与集成。
原理讲解: 大模型有“上下文窗口”限制(比如4K、8K Token)。你不能把一本500页的书直接塞给它。我们需要把文档切成一个个小的“文本块”(Chunk)。切分策略直接影响检索效果。切得太碎,丢失上下文;切得太大,容易包含噪音。
关键技术对比:
| 切分策略 | 原理 | 优点 | 缺点 | 适用场景 | | :--- | :--- | :--- | :--- | :--- | | 固定大小切分 | 按字符数或Token数硬切 | 简单、速度快 | 可能切断句子或段落,语义不完整 | 对语义要求不高的场景 | | 递归字符文本切分 | 按段落、句子、单词的优先级递归切分 | 保持语义单元完整,效果好 | 速度稍慢 | 最推荐,通用场景 | | 语义切分 | 利用嵌入模型判断语义边界进行切分 | 语义最连贯,检索精度高 | 计算成本高 | 对检索精度要求极高的场景 |
代码示例 (使用LangChain的RecursiveCharacterTextSplitter):
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 假设我们有一段很长的文本
long_text = "..." # 你的文档内容
# 初始化切分器,设置块大小为1000字符,重叠200字符
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
length_function=len,
separators=["\n\n", "\n", " ", ""] # 按段落、换行、空格、字符的优先级切分
)
# 执行切分
chunks = text_splitter.split_text(long_text)
print(f"文档被切成了 {len(chunks)} 个块")
print(f"第一个块的内容是: {chunks[0]}")
模块2:向量化与向量数据库
原理讲解:
计算机不认识文字,只认识数字。我们需要把上一步切好的文本块,通过“嵌入模型”(Embedding Model)转换成一组浮点数向量。语义相近的文本,其向量在空间中的距离也相近。然后,我们将这些向量存入一个专门的数据库——向量数据库。
关键技术对比:
| 嵌入模型 | 提供商 | 维度 | 特点 |
| :--- | :--- | :--- | :--- |
| text-embedding-ada-002 | OpenAI | 1536 | 效果好,但需API Key,有费用 |
| bge-large-zh-v1.5 | BAAI (智源) | 1024 | 中文效果极佳,可本地部署,免费 |
| all-MiniLM-L6-v2 | SentenceTransformers | 384 | 轻量级,速度快,适合小规模应用 |
代码示例 (使用LangChain + Chroma向量数据库):
from langchain.embeddings import HuggingFaceBgeEmbeddings
from langchain.vectorstores import Chroma
# 1. 初始化嵌入模型(使用开源的BGE中文模型)
model_name = "BAAI/bge-large-zh-v1.5"
model_kwargs = {'device': 'cpu'} # 或 'cuda'
encode_kwargs = {'normalize_embeddings': True}
hf_embeddings = HuggingFaceBgeEmbeddings(
model_name=model_name,
model_kwargs=model_kwargs,
encode_kwargs=encode_kwargs
)
# 2. 创建向量数据库并存入文档块
# 假设 chunks 是上一步切分好的文本列表
vectorstore = Chroma.from_texts(
texts=chunks,
embedding=hf_embeddings,
persist_directory="./chroma_db" # 持久化存储路径
)
print("向量数据库创建完成并已持久化!")
模块3:检索与增强(核心)
原理讲解:
当用户提出一个问题时,RAG系统会做两件事:
1. 检索:用同样的嵌入模型将用户问题转换成向量,然后在向量数据库中搜索最相似的k个文本块。
2. 增强:将这k个文本块作为“背景信息”,和用户的原始问题一起,组装成一个新的、更详细的Prompt,发送给大模型。
关键技术对比:
| 检索策略 | 原理 | 优点 | 缺点 |
| :--- | :--- | :--- | :--- |
| 相似度检索 | 直接找向量距离最近的Top-K | 简单、快 | 可能忽略关键词 |
| MMR(最大边际相关性) | 在相关性和多样性之间做平衡 | 结果更丰富,减少重复 | 计算稍复杂 |
| HyDE(假设文档嵌入) | 先让LLM生成一个假设回答,再用它去检索 | 效果惊人,能检索到隐含信息 | 需要两次调用LLM,成本高 |
代码示例 (构建检索链):
from langchain.chains import RetrievalQA
from langchain.llms import OpenAI # 这里以OpenAI为例,你也可以用本地模型
# 1. 初始化大模型
llm = OpenAI(temperature=0) # 设置温度为0,让回答更确定
# 2. 创建检索器
retriever = vectorstore.as_retriever(search_kwargs={"k": 3}) # 检索最相似的3个块
# 3. 创建RAG问答链
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff", # 最简单的方式,将所有检索结果塞进Prompt
retriever=retriever,
return_source_documents=True # 返回检索到的源文档,方便溯源
)
# 4. 提问
query = "请告诉我,我们公司最新的产品定价策略是什么?"
result = qa_chain({"query": query})
print("AI回答:", result['result'])
print("\n参考来源:")
for doc in result['source_documents']:
print(doc.page_content[:200] + "...")
模块4:多模态RAG入门
原理讲解:
多模态RAG不再局限于文本。它可以同时检索和生成包含图片、表格、音频等信息。例如,你问“我们公司去年的营收趋势图是怎样的?”,系统不仅能找到相关文字描述,还能检索出那张具体的图表图片。
实现思路:
1. 混合嵌入:使用支持多模态的嵌入模型(如OpenAI的clip-vit-base-patch32),将图片和文本都映射到同一个向量空间。
2. 多模态存储:向量数据库中同时存储文本和图片的向量。
3. 多模态生成:检索到的图片和文本一起作为上下文,交给支持多模态输入的大模型(如GPT-4V, Gemini)生成最终答案。
代码示例 (概念性):
# 注意:此代码为概念展示,实际实现需要处理图片加载等细节
from langchain.embeddings import OpenAIEmbeddings # 假设使用多模态嵌入
# 1. 将图片描述和图片本身都向量化
# image_embedding = multi_modal_embed_model.embed_image("chart.png")
# text_embedding = multi_modal_embed_model.embed_text("2023年营收增长20%")
# 2. 存入向量数据库
# vectorstore.add_embeddings([image_embedding, text_embedding], metadatas=[{"type": "image"}, {"type": "text"}])
# 3. 检索时,同时检索文本和图片
# results = vectorstore.similarity_search("营收趋势", k=2)
# 4. 将检索到的图片URL和文本一起发给GPT-4V
# response = gpt4v_model.generate("根据以下信息回答:...", images=[results[0].url])
三、实操步骤
让我们动手搭建一个完整的RAG问答系统,使用一个简单的本地PDF文档。
Step 1: 环境准备
# 创建虚拟环境(推荐)
python -m venv rag_env
source rag_env/bin/activate # Windows: rag_env\Scripts\activate
# 安装核心依赖
pip install langchain langchain-community chromadb sentence-transformers pypdf
Step 2: 加载PDF文档
from langchain.document_loaders import PyPDFLoader
# 加载PDF文件
loader = PyPDFLoader("your_document.pdf") # 替换成你的PDF路径
documents = loader.load()
print(f"成功加载 {len(documents)} 页文档")
Step 3: 切分文档并创建向量库
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings import HuggingFaceBgeEmbeddings
from langchain.vectorstores import Chroma
# 切分
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = text_splitter.split_documents(documents)
# 嵌入和向量化
embeddings = HuggingFaceBgeEmbeddings(model_name="BAAI/bge-small-zh-v1.5") # 使用小模型,速度更快
vectorstore = Chroma.from_documents(chunks, embeddings, persist_directory="./my_rag_db")
print("知识库构建完成!")
Step 4: 构建问答链并提问
from langchain.chains import RetrievalQA
from langchain.llms import HuggingFacePipeline
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
import torch
# 使用本地小模型(例如:Qwen1.5-1.8B-Chat,需先下载)
model_id = "Qwen/Qwen1.5-1.8B-Chat"
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(model_id, trust_remote_code=True, torch_dtype=torch.float16)
pipe = pipeline("text-generation", model=model, tokenizer=tokenizer, max_new_tokens=512)
llm = HuggingFacePipeline(pipeline=pipe)
# 构建检索器
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
# 构建问答链
qa_chain = RetrievalQA.from_chain_type(llm=llm, chain_type="stuff", retriever=retriever)
# 提问
query = "文档中提到的核心观点是什么?"
response = qa_chain.run(query)
print(f"AI回答:{response}")
预期效果:
你会看到AI模型不再胡言乱语,而是根据你提供的PDF文档内容,给出一个基于事实的回答。如果文档里有相关描述,回答就会很精准;如果文档里没有,模型会回答“根据提供的文档,我无法找到相关信息”。
四、常见问题与故障排查
问题1:AI回答得驴唇不对马嘴,或者回答“我不知道”
- **判断思路**:大概率是检索环节出了问题,没找到相关文本。
- **排查流程**:
1. 检查检索结果:打印出retriever.get_relevant_documents(query),看看返回了哪些文本块。
2. 检查切分策略:chunk_size是否太小?导致关键信息被截断;chunk_overlap是否为0?导致上下文断裂。
3. 检查嵌入模型:对于中文文档,是否使用了中文优化的嵌入模型(如BGE系列)?英文模型处理中文效果会很差。
问题2:程序运行非常慢
- **判断思路**:可能是嵌入模型或大模型太大,或者向量数据库数据量过大。
- **排查流程**:
1. 更换小模型:将嵌入模型换成bge-small-zh-v1.5,将大模型换成Qwen1.5-1.8B或更小的模型。
2. 使用GPU加速:确保model_kwargs = {'device': 'cuda'}生效。
3. 减少检索数量:降低search_kwargs={"k": 1},减少每次检索的文本块数量。
问题3:向量数据库文件越来越大,启动慢
- **判断思路**:每次运行脚本都重新创建了向量库,没有使用持久化。
- **排查流程**:
1. 检查Chroma初始化时是否指定了persist_directory。
2. 确保在第一次创建后,后续运行时使用Chroma(persist_directory="./my_rag_db", embedding_function=embeddings)来加载已有的数据库,而不是重新from_documents。
问题4:AI回答总是在重复相同的句子
- **判断思路**:大模型的`temperature`参数设置过低,或者检索到的多个文本块内容高度重复。
- **排查流程**:
1. 调高temperature,例如从0调到0.3或0.5,增加回答的多样性。
2. 在检索时使用MMR策略(retriever = vectorstore.as_retriever(search_type="mmr")),让检索结果更多样化。
五、总结与扩展
重点知识回顾
1. RAG核心思想:先检索,后生成,为大模型提供实时知识库。
2. 关键三要素:文档切分(Chunking)、向量化(Embedding)、向量检索(Retrieval)。
3. 框架选择:LangChain 功能全面,生态丰富;LlamaIndex 更专注于数据索引和检索,性能常更优。
4. 效果瓶颈:RAG系统的天花板往往不在于大模型,而在于检索质量。检索不到,神仙也答不对。
5. 评估维度:不仅要看回答的流畅度,更要看答案相关性和引用准确性。
实战中的最佳实践建议
- **从小处着手**:先用一个简单的PDF跑通流程,再逐步增加复杂度。
- **重视元数据**:在存入向量库时,为每个文本块添加来源文件名、页码等元数据,方便溯源。
- **Prompt工程不可少**:精心设计给大模型的Prompt模板,例如“请基于以下上下文回答问题,如果找不到答案,就说不知道”。
- **记录日志**:记录每次查询的检索结果和最终回答,方便复盘和优化。
推荐进一步学习的方向和资源
- **框架深入学习**:
- **LangChain官方文档**:[https://python.langchain.com/](https://python.langchain.com/)
- **LlamaIndex官方文档**:[https://docs.llamaindex.ai/](https://docs.llamaindex.ai/)
- **进阶主题**:
- **高级检索**:学习`Self-Query Retriever`、`Multi-Query Retriever`等高级检索策略。
- **Agent