RAG技术详解:让大模型拥有"外挂知识库"
理解检索增强生成的原理与实践,让大模型访问最新、最准确的信息。
前言
大语言模型虽然强大,但存在一个致命缺陷:知识截止。GPT-4的知识只到2023年,无法回答关于最新事件的问题。而且,模型可能对某些专业知识了解不足。
**RAG(Retrieval-Augmented Generation,检索增强生成)**就是为了解决这个问题而生------让大模型能够"查阅资料"后再回答问题。
一、RAG的核心概念
为什么需要RAG?
markdown
大模型的局限性:
1. 知识截止
用户:2024年奥运会金牌榜是怎样的?
模型:我的知识截止于2023年... ❌
2. 幻觉问题
用户:介绍一下《时间简史》这本书
模型:《时间简史》是霍金写的...作者是张三 ❌(错误信息)
3. 私有数据
用户:根据公司内部文档回答这个问题
模型:我无法访问您的私有数据 ❌
4. 专业领域
用户:这个医学影像显示什么问题?
模型:我不是医学专家... ❌
RAG的解决思路
markdown
RAG工作流程:
用户提问
↓
┌─────────────────┐
│ 检索阶段 │ 在知识库中搜索相关内容
└────────┬────────┘
↓
┌─────────────────┐
│ 增强阶段 │ 将检索结果与问题组合
└────────┬────────┘
↓
┌─────────────────┐
│ 生成阶段 │ 大模型基于上下文生成回答
└────────┬────────┘
↓
最终回答
RAG vs 微调
| 维度 | RAG | 微调 |
|---|---|---|
| 知识更新 | 实时更新知识库 | 需要重新训练 |
| 成本 | 较低 | 较高 |
| 可解释性 | 可追溯知识来源 | 黑盒 |
| 适用场景 | 知识密集型任务 | 特定能力学习 |
| 私有数据 | 天然支持 | 需要训练数据 |
二、RAG架构详解
基础架构
┌─────────────────────────────────────────────────────────────┐
│ RAG系统架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 离线阶段 │ │
│ │ 文档 → 分块 → 向量化 → 存入向量数据库 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 在线阶段 │ │
│ │ │ │
│ │ 用户问题 ─→ 向量化 ─→ 向量检索 ─→ 获取相关文档 │ │
│ │ ↓ │ │
│ │ 问题 + 相关文档 ─→ Prompt构建 ─→ LLM生成回答 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
代码实现
python
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.llms import OpenAI
from langchain.chains import RetrievalQA
class RAGSystem:
def __init__(self, documents, embedding_model="text-embedding-ada-002"):
self.embeddings = OpenAIEmbeddings(model=embedding_model)
self.llm = OpenAI(temperature=0)
self.vectorstore = None
self.documents = documents
def build_index(self, chunk_size=1000, chunk_overlap=200):
"""构建向量索引"""
# 文档分块
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap
)
chunks = text_splitter.split_documents(self.documents)
# 创建向量存储
self.vectorstore = Chroma.from_documents(
documents=chunks,
embedding=self.embeddings
)
print(f"已索引 {len(chunks)} 个文档块")
return self.vectorstore
def query(self, question, k=4):
"""查询并生成回答"""
# 检索相关文档
docs = self.vectorstore.similarity_search(question, k=k)
# 构建Prompt
context = "\n\n".join([doc.page_content for doc in docs])
prompt = f"""根据以下参考资料回答问题。如果资料中没有相关信息,请说"根据现有资料无法回答"。
参考资料:
{context}
问题:{question}
回答:"""
# 生成回答
response = self.llm(prompt)
return {
"answer": response,
"sources": docs
}
# 使用示例
from langchain.document_loaders import TextLoader
# 加载文档
loader = TextLoader("knowledge_base.txt")
documents = loader.load()
# 创建RAG系统
rag = RAGSystem(documents)
rag.build_index()
# 查询
result = rag.query("什么是机器学习?")
print(f"回答:{result['answer']}")
print(f"来源:{len(result['sources'])} 个文档块")
三、文档处理与分块
分块策略
python
from langchain.text_splitter import (
RecursiveCharacterTextSplitter,
CharacterTextSplitter,
TokenTextSplitter,
MarkdownHeaderTextSplitter
)
# 策略1:递归字符分块(推荐)
recursive_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
separators=["\n\n", "\n", "。", "!", "?", " ", ""]
)
# 策略2:按Token分块
token_splitter = TokenTextSplitter(
chunk_size=500,
chunk_overlap=50
)
# 策略3:Markdown按标题分块
markdown_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=[
("#", "header1"),
("##", "header2"),
("###", "header3")
]
)
# 策略4:语义分块(按句子边界)
def semantic_chunk(text, min_chunk_size=100, max_chunk_size=500):
"""按语义边界分块"""
import re
# 按句子分割
sentences = re.split(r'[。!?\n]', text)
chunks = []
current_chunk = ""
for sentence in sentences:
if len(current_chunk) + len(sentence) < max_chunk_size:
current_chunk += sentence
else:
if len(current_chunk) >= min_chunk_size:
chunks.append(current_chunk)
current_chunk = sentence
if current_chunk:
chunks.append(current_chunk)
return chunks
分块最佳实践
markdown
分块考虑因素:
1. 块大小
├── 太小:语义不完整
└── 太大:检索精度下降
推荐:500-1500字符
2. 块重叠
├── 避免关键信息被截断
└── 推荐重叠:10-20%
3. 分块边界
├── 自然语言:按段落/句子
├── 代码:按函数/类
└── Markdown:按标题层级
4. 元数据
├── 来源文件名
├── 页码/位置
└── 创建时间
四、向量数据库
主流向量数据库对比
| 数据库 | 特点 | 适用场景 |
|---|---|---|
| Pinecone | 全托管,易用 | 生产环境 |
| Milvus | 开源,高性能 | 大规模部署 |
| Chroma | 轻量,本地运行 | 开发测试 |
| Weaviate | 支持混合检索 | 复杂查询 |
| FAISS | Meta开源,纯向量检索 | 研究原型 |
| Qdrant | Rust实现,高性能 | 生产环境 |
向量检索原理
python
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
def vector_search(query_embedding, document_embeddings, top_k=5):
"""
向量相似度检索
query_embedding: 查询向量 (d,)
document_embeddings: 文档向量矩阵 (n, d)
"""
# 计算余弦相似度
similarities = cosine_similarity(
[query_embedding],
document_embeddings
)[0]
# 获取top-k索引
top_indices = np.argsort(similarities)[-top_k:][::-1]
return top_indices, similarities[top_indices]
# 示例
query = np.array([0.1, 0.2, 0.3])
docs = np.array([
[0.1, 0.2, 0.35], # 相似度最高
[0.5, 0.6, 0.7], # 相似度较低
[0.15, 0.25, 0.32] # 相似度较高
])
indices, scores = vector_search(query, docs, top_k=2)
print(f"最相似的文档索引: {indices}")
print(f"相似度分数: {scores}")
混合检索
python
def hybrid_search(query, vectorstore, keyword_index, alpha=0.5, k=10):
"""
混合检索:向量检索 + 关键词检索
alpha: 向量检索权重 (0-1)
"""
# 向量检索
vector_results = vectorstore.similarity_search(query, k=k*2)
vector_scores = {r.id: r.score for r in vector_results}
# 关键词检索 (BM25)
keyword_results = keyword_index.search(query, k=k*2)
keyword_scores = {r.id: r.score for r in keyword_results}
# 分数归一化
vector_scores = normalize_scores(vector_scores)
keyword_scores = normalize_scores(keyword_scores)
# 加权融合
all_ids = set(vector_scores.keys()) | set(keyword_scores.keys())
final_scores = {}
for doc_id in all_ids:
v_score = vector_scores.get(doc_id, 0)
k_score = keyword_scores.get(doc_id, 0)
final_scores[doc_id] = alpha * v_score + (1 - alpha) * k_score
# 排序返回
sorted_ids = sorted(final_scores.keys(),
key=lambda x: final_scores[x],
reverse=True)
return sorted_ids[:k]
def normalize_scores(scores):
"""分数归一化到0-1"""
if not scores:
return scores
max_score = max(scores.values())
min_score = min(scores.values())
if max_score == min_score:
return {k: 1.0 for k in scores}
return {k: (v - min_score) / (max_score - min_score)
for k, v in scores.items()}
五、高级RAG技术
1. 多查询检索
python
def multi_query_retrieval(query, llm, vectorstore, n_queries=3):
"""
多查询检索:生成多个相关查询,扩展检索范围
"""
# 生成多个查询变体
prompt = f"""请生成{ n_queries}个与以下问题语义相似但表述不同的问题:
原始问题:{query}
要求:
1. 保持核心语义不变
2. 使用不同的表述方式
3. 每行一个问题
问题列表:"""
response = llm(prompt)
queries = [query] + response.strip().split('\n')
# 对每个查询进行检索
all_docs = []
for q in queries:
docs = vectorstore.similarity_search(q, k=3)
all_docs.extend(docs)
# 去重
unique_docs = list({doc.id: doc for doc in all_docs}.values())
return unique_docs
2. 重排序
python
from sentence_transformers import CrossEncoder
def rerank_results(query, documents, top_k=5):
"""
使用Cross-Encoder重排序检索结果
"""
# 加载重排序模型
reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
# 构建查询-文档对
pairs = [(query, doc.page_content) for doc in documents]
# 计算相关性分数
scores = reranker.predict(pairs)
# 按分数排序
scored_docs = list(zip(documents, scores))
scored_docs.sort(key=lambda x: x[1], reverse=True)
return [doc for doc, score in scored_docs[:top_k]]
3. 自查询检索
python
def self_query_retrieval(query, llm, vectorstore):
"""
自查询:从问题中提取过滤条件
"""
prompt = f"""分析以下问题,提取检索相关信息:
问题:{query}
请输出JSON格式:
{{
"search_query": "核心检索内容",
"filters": {{
"author": "作者名(如果有)",
"date_range": {{"start": "开始日期", "end": "结束日期"}},
"category": "分类(如果有)"
}}
}}"""
response = llm(prompt)
parsed = parse_json(response)
# 使用过滤条件检索
results = vectorstore.similarity_search(
parsed["search_query"],
filter=parsed.get("filters", {})
)
return results
4. 知识图谱增强RAG
python
def graph_enhanced_rag(query, vectorstore, knowledge_graph, llm):
"""
结合知识图谱的RAG
"""
# 1. 向量检索
vector_docs = vectorstore.similarity_search(query, k=5)
# 2. 从知识图谱中检索相关实体和关系
entities = extract_entities(query)
graph_info = []
for entity in entities:
# 获取实体的邻居节点和关系
neighbors = knowledge_graph.get_neighbors(entity)
relations = knowledge_graph.get_relations(entity)
graph_info.append({
"entity": entity,
"neighbors": neighbors,
"relations": relations
})
# 3. 构建增强Prompt
context = format_context(vector_docs, graph_info)
prompt = f"""基于以下信息回答问题:
向量检索结果:
{format_docs(vector_docs)}
知识图谱信息:
{format_graph_info(graph_info)}
问题:{query}
回答:"""
return llm(prompt)
六、RAG评估
评估指标
markdown
RAG评估维度:
1. 检索质量
├── 召回率 (Recall)
├── 精确率 (Precision)
└── MRR (Mean Reciprocal Rank)
2. 生成质量
├── 准确性:回答是否正确
├── 相关性:回答是否切题
├── 完整性:信息是否完整
└── 忠实性:是否基于检索内容
3. 端到端指标
├── 响应时间
└── 用户满意度
评估代码
python
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_recall,
context_precision
)
def evaluate_rag(test_data):
"""
使用RAGAS框架评估RAG系统
test_data: DataFrame包含
- question: 问题
- answer: 生成的回答
- contexts: 检索的文档
- ground_truth: 标准答案
"""
results = evaluate(
test_data,
metrics=[
faithfulness, # 忠实性
answer_relevancy, # 回答相关性
context_recall, # 上下文召回率
context_precision # 上下文精确率
]
)
return results
七、RAG最佳实践
实践建议
markdown
1. 文档处理
├── 选择合适的分块策略
├── 保留文档元数据
└── 定期更新知识库
2. 检索优化
├── 使用混合检索
├── 添加重排序步骤
└── 调整chunk数量
3. 提示工程
├── 明确指示基于检索内容回答
├── 处理"不知道"的情况
└── 添加引用来源
4. 系统设计
├── 缓存常见查询
├── 异步处理长文档
└── 监控检索和生成质量
Prompt模板
python
rag_prompt_template = """
你是一个专业的问答助手。请根据提供的参考资料回答用户问题。
要求:
1. 仅基于参考资料回答,不要使用外部知识
2. 如果参考资料中没有相关信息,请明确说明
3. 引用具体的参考来源(如[文档1])
4. 回答要简洁、准确、完整
参考资料:
{context}
用户问题:{question}
回答:
"""
小结
| 组件 | 功能 | 关键技术 |
|---|---|---|
| 文档处理 | 知识库构建 | 分块、清洗、元数据 |
| 向量化 | 文本→向量 | Embedding模型 |
| 向量存储 | 向量索引与检索 | Pinecone、Milvus等 |
| 检索 | 找到相关文档 | 向量检索、混合检索 |
| 生成 | 生成最终回答 | LLM + Prompt |
思考与练习
-
思考题:
- RAG和微调各适合什么场景?
- 如何选择合适的分块大小?
-
动手练习:
- 使用LangChain构建一个简单的RAG系统
- 尝试不同的分块策略,比较检索效果
-
延伸阅读:
下期预告
下一篇文章,我们将深入探讨:Agent智能体:大模型的"手"和"脚"
会解答这些问题:
- 什么是AI Agent?
- Agent如何规划和执行任务?
- 有哪些主流的Agent框架?
关注专栏,不错过后续更新!
作者:ECH00O00 本文首发于掘金专栏《AI科普实验室》 欢迎评论区交流讨论,点赞收藏就是最大的鼓励 ❤️