系列文章导航:AI系列文章导航目录-持续更新中
第10课:RAG检索增强生成
📝 本文摘要:本文详解RAG技术------解决LLM知识局限(训练数据截止、私有数据未知、专业领域不精)的核心方案,梳理RAG流程(离线索引:文档→分块→嵌入→向量数据库;在线查询:嵌入→向量检索→Top-K→拼接到Prompt→LLM生成),详解关键技术(分块策略、嵌入模型选择、向量数据库选型、混合检索、重排序),提供最简RAG和生产级RAG架构代码,以及常见问题优化。
RAG是大模型应用最核心的技术之一。它解决了一个根本问题:模型不知道你公司的数据。RAG让模型"查资料"后再回答,大幅减少幻觉。
一、为什么需要RAG
1.1 LLM的知识局限
一句话理解:LLM只知道它训练时看过的内容,不知道你公司的数据、今天的新闻、你的私有文档。RAG让模型先"查资料"再回答,就像你开卷考试一样。
问题1: 训练数据截止
模型的知识停留在训练数据的时间点
"今天的新闻" → 不知道
类比: 就像一个2024年毕业的学生,不知道2025年发生了什么
问题2: 私有数据
模型不知道你公司的内部文档、产品手册、客户数据
"我们的退货政策是什么" → 不知道
类比: 就像一个新员工,还没看过公司内部文档
问题3: 专业领域
模型在特定领域的知识不够精确
"这个医疗器械的型号X的使用规范" → 可能编造(幻觉)
类比: 就像一个通才,什么都知道一点但不够精
RAG的解法: 让模型先"查资料",再基于资料回答
类比: 开卷考试 ------ 不需要模型记住所有知识,只需要能找到并理解相关资料
1.2 RAG vs 微调
| 维度 | RAG | 微调 |
|---|---|---|
| 知识更新 | 实时更新检索库 | 需要重新训练 |
| 成本 | 低(只改检索) | 高(需要GPU训练) |
| 可解释性 | 高(可以展示来源) | 低(知识融入权重) |
| 适用场景 | 事实性问答、文档查询 | 风格适配、领域适配 |
| 数据量 | 无限制 | 受训练预算限制 |
结论:90%的"让模型了解更多知识"的需求,用RAG而非微调。
二、RAG的核心流程
┌──────────────────────────────────┐
│ 离线索引阶段 │
│ │
│ 文档 → 分块 → 嵌入 → 向量数据库 │
└──────────────────────────────────┘
┌──────────────────────────────────┐
│ 在线查询阶段 │
│ │
用户问题 → 嵌入 → 向量检索 → Top-K文档 → 拼接到Prompt → LLM → 回答
2.1 详细步骤
类比理解:RAG就像图书馆的工作流程:
-
离线阶段 = 图书馆建设(买书、编目录、上架)
-
在线阶段 = 读者查书(提问→查目录→找到书→阅读→回答)
=== 离线阶段(只做一次,或文档更新时重做) ===
Step 1: 文档加载
PDF/Word/HTML/Markdown → 纯文本
类比: 把各种格式的书籍都拆封取出内容Step 2: 文本分块 (Chunking)
长文本 → 多个小块(每块约200-500字)
类比: 把一本书切成一页一页的卡片
为什么要分块: 因为检索时我们只需要找到最相关的那几块,而不是整篇文档Step 3: 嵌入 (Embedding)
每个文本块 → 向量 (如1024维的数字数组)
类比: 给每张卡片贴上一个"语义指纹",意思相近的卡片指纹也相近
模型: BGE-M3, GTE, text-embedding-3-smallStep 4: 存储
向量 + 原文 → 向量数据库
类比: 把卡片和指纹一起存入图书馆系统=== 在线阶段(每次用户提问时执行) ===
Step 5: 查询嵌入
用户问题 → 向量(用同一个嵌入模型)
类比: 把用户的问题也转成"指纹"Step 6: 向量检索
查询向量 vs 数据库所有向量 → 余弦相似度 → Top-K最相关
类比: 用问题的指纹去图书馆找指纹最像的卡片Step 7: 上下文构建
Top-K文档块 + 用户问题 → 完整Prompt
类比: 把找到的卡片和问题一起交给"回答专家"Step 8: LLM生成
基于检索到的上下文回答问题
类比: 专家阅读卡片后回答你的问题
三、关键技术详解
3.1 文本分块(Chunking)
方法1: 固定大小分块
chunk_size=500, overlap=50
优点: 简单
缺点: 可能切断语义完整性
方法2: 按段落/章节分块
按Markdown标题、段落边界分
优点: 语义完整
缺点: 块大小不均匀
方法3: 语义分块(Semantic Chunking,基于语义的文本分块)
用嵌入计算相邻句子的相似度
相似度骤降处 → 分块边界
优点: 语义最完整
缺点: 计算成本高
方法4: 递归分块(LangChain默认)
尝试按["\n\n", "\n", "。", " "]依次分割
直到块大小合适
优点: 兼顾语义和大小
python
# LangChain递归分块示例
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", "。", "!", "?", ".", " ", ""]
)
chunks = splitter.split_text(long_text)
3.2 嵌入模型(Embedding)
嵌入模型把文本映射为向量,语义相似的文本 → 相似的向量
"机器学习" → [0.12, -0.34, 0.56, ...] (1024维)
"深度学习" → [0.11, -0.32, 0.58, ...] ← 很接近!
"红烧肉" → [-0.45, 0.23, -0.12, ...] ← 很远
常用模型:
┌─────────────────────┬──────────┬──────────┐
│ 模型 │ 维度 │ 特点 │
├─────────────────────┼──────────┼──────────┤
│ BGE-M3 │ 1024 │ 中文最强 │
│ GTE-Qwen2 │ 768/1024 │ 阿里出品 │
│ text-embedding-3 │ 1536 │ OpenAI │
│ Cohere embed v3 │ 1024 │ 多语言 │
└─────────────────────┴──────────┴──────────┘
3.3 向量数据库
┌──────────────────┬──────────────┬─────────────────────┐
│ 数据库 │ 类型 │ 特点 │
├──────────────────┼──────────────┼─────────────────────┤
│ Chroma │ 嵌入式 │ 最简单,开发测试用 │
│ FAISS │ 库 │ Meta出品,纯内存 │
│ Milvus │ 分布式 │ 生产级,云原生 │
│ Qdrant │ 分布式 │ Rust写,性能好 │
│ pgvector │ PG扩展 │ 已有PG的直接用 │
│ Weaviate │ 分布式 │ 支持混合搜索 │
└──────────────────┴──────────────┴─────────────────────┘
3.4 检索策略
基础: 向量相似度检索
python
# 余弦相似度
similarity = cos(query_vector, doc_vector)
# 范围: [-1, 1],越大越相似
进阶: 混合检索(Hybrid Search)
向量检索: 找语义相似的
关键词检索(BM25, Best Matching 25,经典信息检索算法): 找精确匹配的
混合 = 向量检索结果 ∪ 关键词检索结果 → 重排序 → Top-K
为什么需要混合:
"找RFC 2616文档" → 关键词检索更准(精确匹配"RFC 2616")
"HTTP协议的设计理念" → 向量检索更准(语义匹配)
进阶: 重排序(Reranking)
初始检索: Top-20 (宽松,宁可多找,K=20即返回前20个最相似结果)
↓
Reranker模型: 对20个结果精细打分排序
↓
最终结果: Top-5 (精准,K=5即只取前5个最相关结果)
常用Reranker: BGE-Reranker, Cohere Rerank, Cross-Encoder
四、RAG实战代码
4.1 最简RAG
python
from openai import OpenAI
import chromadb
from chromadb.utils import embedding_functions
# 1. 初始化
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
chroma_client = chromadb.Client()
# 使用Ollama的嵌入模型
embed_fn = embedding_functions.OllamaEmbeddingFunction(
url="http://localhost:11434",
model_name="bge-m3"
)
# 2. 创建集合
collection = chroma_client.get_or_create_collection(
name="docs",
embedding_function=embed_fn
)
# 3. 添加文档
docs = [
"Python是一种高级编程语言,由Guido van Rossum于1991年创建。",
"Java是由James Gosling在1995年发布的面向对象编程语言。",
"Rust是一种系统编程语言,注重安全性和性能,由Mozilla研发。"
]
collection.add(
documents=docs,
ids=["doc1", "doc2", "doc3"]
)
# 4. 查询
query = "谁创建了Python?"
results = collection.query(query_texts=[query], n_results=2)
# 5. 构建Prompt并生成回答
context = "\n".join(results["documents"][0])
prompt = f"""基于以下参考资料回答问题。如果资料中没有答案,请说"我不知道"。
参考资料:
{context}
问题:{query}"""
response = client.chat.completions.create(
model="qwen2.5:7b",
messages=[{"role": "user", "content": prompt}],
temperature=0.0
)
print(response.choices[0].message.content)
# 预期: "Python由Guido van Rossum于1991年创建。"
4.2 生产级RAG架构
用户查询
↓
Query改写/扩展
↓
┌──────────┐
│ 向量检索 │ ← 嵌入模型 + 向量数据库
│ BM25检索 │ ← 全文检索引擎
└────┬─────┘
↓ 结果合并
Reranker重排序
↓
Top-K文档
↓
上下文压缩/过滤
↓
Prompt构建
↓
LLM生成
↓
答案 + 来源引用
五、RAG的常见问题与优化
5.1 检索不到相关文档
原因:
- 分块太大,关键信息被稀释
- 嵌入模型不适合你的领域
- 用户问题表述与文档差异大
优化:
- 查询扩展: 把用户问题改写为多个查询
- 假设性文档嵌入(HyDE, Hypothetical Document Embeddings): 先让LLM生成"假设答案",用假设答案去检索
- 调整分块大小: 256-512 tokens通常效果最好
5.2 检索到但没用好
原因:
- 塞入太多无关文档
- 文档排序不对
- Prompt没引导模型关注检索结果
优化:
- 用Reranker精排
- Prompt中强调"基于参考资料回答"
- 要求模型引用来源
5.3 幻觉仍然存在
原因:
- 模型忽略检索结果,用自身知识回答
- 检索结果不完整
优化:
- Prompt约束: "只能基于参考资料回答,不要使用外部知识"
- 引用机制: 要求每个论断标注出自哪个文档
- 置信度评估: 让模型评估自己的答案可靠性
📝 作业
作业1:构建一个简单的文档问答系统
- 准备3-5段技术文档(可以复制自网络)
- 用ChromaDB构建向量索引
- 实现查询功能,返回答案+来源
参考答案:
python
# save as: rag_demo.py
import chromadb
from chromadb.utils import embedding_functions
from openai import OpenAI
# 初始化
llm_client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
chroma = chromadb.Client()
# 用Ollama嵌入,如果Ollama没有bge-m3,用默认的
try:
embed_fn = embedding_functions.OllamaEmbeddingFunction(
url="http://localhost:11434",
model_name="nomic-embed-text" # 轻量嵌入模型,先ollama pull nomic-embed-text
)
except:
embed_fn = embedding_functions.DefaultEmbeddingFunction()
# 知识库文档
documents = [
{
"id": "k8s_1",
"text": "Kubernetes(K8s)是Google开源的容器编排系统。它于2014年首次发布,"
"基于Google内部运行了15年的Borg系统设计。K8s的核心组件包括API Server、"
"etcd、Scheduler、Controller Manager和Kubelet。",
"source": "K8s入门文档"
},
{
"id": "docker_1",
"text": "Docker是一个开源的容器化平台,由Solomon Hykes于2013年创建。"
"Docker使用Linux容器的技术来实现应用隔离,核心概念包括镜像(Image)、"
"容器(Container)和仓库(Registry)。",
"source": "Docker入门文档"
},
{
"id": "go_1",
"text": "Go语言(Golang)由Google的Robert Griesemer、Rob Pike和Ken Thompson"
"于2007年开始设计,2012年发布1.0版本。Go语言的设计目标是简单、高效、"
"并发友好,内置goroutine和channel支持。Docker和Kubernetes都是用Go语言编写的。",
"source": "Go语言教程"
},
{
"id": "prometheus_1",
"text": "Prometheus是SoundCloud开源的监控和告警系统,于2016年加入CNCF。"
"它采用拉取式数据采集、多维数据模型和PromQL查询语言。"
"Prometheus与Grafana是云原生监控的标准组合。",
"source": "Prometheus文档"
}
]
# 创建集合并添加文档
collection = chroma.get_or_create_collection("tech_docs", embedding_function=embed_fn)
collection.add(
documents=[d["text"] for d in documents],
ids=[d["id"] for d in documents],
metadatas=[{"source": d["source"]} for d in documents]
)
def ask(question: str) -> str:
# 检索
results = collection.query(query_texts=[question], n_results=2)
context_parts = []
for doc, meta in zip(results["documents"][0], results["metadatas"][0]):
context_parts.append(f"[来源: {meta['source']}]\n{doc}")
context = "\n\n".join(context_parts)
# 生成
prompt = f"""基于以下参考资料回答问题。请标注信息来源。如果资料中没有答案,请说"根据现有资料无法回答"。
参考资料:
{context}
问题:{question}"""
response = llm_client.chat.completions.create(
model="qwen2.5:7b",
messages=[{"role": "user", "content": prompt}],
temperature=0.0
)
return response.choices[0].message.content
# 测试
print(ask("Docker是谁创建的?"))
print("---")
print(ask("K8s和Docker有什么关系?"))
🎉 Part 2完成! 你已经掌握了Prompt、Context、结构化输出、RAG这些应用开发核心技能。
下一篇文章见:AI系列文章导航目录-持续更新中