RAG
RAG 实战指南:让大语言模型不再"幻觉"
一篇文章讲透 RAG 的核心原理、架构设计、工程实践与常见踩坑点。
1. 为什么需要 RAG?
大语言模型(LLM)很强,但它有三个与生俱来的短板:
| 短板 | 表现 | 后果 |
|---|---|---|
| 知识截止 | 训练数据有截止日期,之后的事不知道 | 问"2026年的新政策"会答不上来 |
| 幻觉 | 不确定时倾向于"编"一个看起来合理的答案 | 误导用户,尤其在专业领域 |
| 不可溯源 | 输出是概率生成的,无法追踪依据 | 合规审计、医疗法律场景不可用 |
RAG(Retrieval-Augmented Generation,检索增强生成)的思路很朴素:先查资料,再回答。把外部知识库当作"开卷考试"的参考书,让模型基于检索到的真实文档来生成答案,而不是纯靠记忆。
一句话总结:
RAG = 检索(找相关文档) + 生成(基于文档回答)
2. RAG 的标准架构
一个完整的 RAG 系统分为两个阶段:
2.1 离线阶段(Indexing / 建库)
原始文档 → 分块(Chunking) → 向量化(Embedding) → 存入向量数据库
分块(Chunking) 是把长文档切成适合检索的小片段。切太大,检索精度低;切太小,上下文丢失。常见策略:
- 固定长度切分:按 token 数切(如 512 tokens),简单粗暴
- 语义切分:按段落、标题、句子边界切,保留语义完整性
- 滑动窗口:块之间有 overlap(如 50 tokens),避免切断关键信息
向量化(Embedding) 是把文本转成高维浮点向量,语义相近的文本在向量空间中距离也近。常用模型:
- OpenAI
text-embedding-3-small/text-embedding-3-large - 开源:
bge-large-zh、m3e-base、GTE - 华为云:盘古嵌入模型
向量数据库 负责存储和高效检索。主流选择:
| 数据库 | 特点 | 适用场景 |
|---|---|---|
| Milvus | 分布式、高性能 | 大规模生产 |
| Qdrant | Rust 编写、轻量 | 中小项目 |
| Chroma | Python 原生、易用 | 原型开发 |
| pgvector | PostgreSQL 扩展 | 已有 PG 基础 |
| FAISS | Facebook 出品、纯索引 | 本地实验 |
2.2 在线阶段(Query / 检索生成)
用户提问 → 向量化 → 向量检索 Top-K → 拼接 Prompt → LLM 生成 → 返回答案
用一段伪代码表示核心流程:
def rag_answer(question: str, vector_db, llm, top_k=5) -> str:
# 1. 把问题转成向量
query_vector = embed(question)
# 2. 在向量库中检索最相关的 K 个文档片段
retrieved_docs = vector_db.search(query_vector, top_k=top_k)
# 3. 拼接成 Prompt
context = "\n\n".join([doc.text for doc in retrieved_docs])
prompt = f"""请根据以下参考资料回答问题。如果资料中没有答案,请说"我不知道"。
参考资料:
{context}
问题:{question}
"""
# 4. 调用 LLM 生成答案
answer = llm.generate(prompt)
# 5. 返回答案 + 引用来源
return {
"answer": answer,
"sources": [doc.metadata for doc in retrieved_docs]
}
3. 进阶:从朴素 RAG 到高级 RAG
朴素 RAG(Naive RAG)能跑通,但效果往往不够好。以下是三个维度的优化方向:
3.1 检索前优化(Pre-Retrieval)
问题:用户的原始提问可能表述不清、太短或太宽泛。
查询改写(Query Rewriting):用 LLM 把口语化的问题改写成更适合检索的形式。
原始问题:"那个报错怎么解决"
改写后 :"Python ConnectionRefusedError 连接被拒绝的解决方法"
查询扩展(Query Expansion):生成多个语义等价的查询,分别检索后合并结果。
HyDE(Hypothetical Document Embeddings):先让 LLM 生成一个"假设性答案",用这个答案的向量去检索——因为答案和文档的语义分布比问题和文档更接近。
3.2 检索优化(Retrieval)
问题:纯向量检索(ANN)有时会召回语义相近但内容无关的片段。
混合检索(Hybrid Search):向量检索(语义) + BM25 关键词检索(精确),加权融合:
def hybrid_search(query, vector_db, keyword_index, alpha=0.5):
vec_results = vector_db.search(embed(query), top_k=10)
bm25_results = keyword_index.search(query, top_k=10)
# Reciprocal Rank Fusion 融合排序
fused = rrf(vec_results, bm25_results)
return fused[:5]
重排序(Re-ranking):召回一批候选(如 20 个),用更精细的模型重排,取 Top-5。常用重排模型:bge-reranker-large、Cohere Rerank。
3.3 检索后优化(Post-Retrieval)
问题:检索到的片段可能信息冗余或矛盾。
上下文压缩:用 LLM 对检索结果做摘要/去重,减少输入 token。
上下文过滤:用一个小模型判断每个片段是否与问题相关,丢弃不相关的。
整体演进路线
Naive RAG → Search RAG → Advanced RAG → Modular RAG
(基础) (混合检索) (全链路优化) (模块化编排)
4. 工程实践:一个可落地的 RAG 项目结构
rag-project/
├── config/
│ └── settings.yaml # 模型、向量库、chunk 参数配置
├── data/
│ └── raw/ # 原始文档(PDF、Markdown、HTML)
├── src/
│ ├── indexing/
│ │ ├── loader.py # 文档加载
│ │ ├── chunker.py # 分块策略
│ │ ├── embedder.py # 向量化
│ │ └── indexer.py # 写入向量库
│ ├── retrieval/
│ │ ├── vector_search.py # 向量检索
│ │ ├── keyword_search.py # 关键词检索
│ │ ├── reranker.py # 重排序
│ │ └── fusion.py # 结果融合
│ ├── generation/
│ │ ├── prompt_builder.py # Prompt 模板
│ │ └── llm_client.py # LLM 调用
│ └── pipeline.py # 主流程编排
├── eval/
│ └── metrics.py # 评估指标
└── requirements.txt
关键配置示例
# config/settings.yaml
embedding:
model: "bge-large-zh-v1.5"
dimension: 1024
batch_size: 32
chunking:
strategy: "semantic" # fixed | semantic | sliding
chunk_size: 512
overlap: 50
retrieval:
top_k: 20 # 召回数量
rerank_top_k: 5 # 重排后保留
hybrid_search: true
vector_weight: 0.7
keyword_weight: 0.3
vector_db:
type: "milvus"
host: "localhost"
port: 19530
collection: "knowledge_base"
llm:
model: "gpt-4o"
temperature: 0.1 # 低温度,减少发散
max_tokens: 2048
5. 评估:怎么知道 RAG 效果好不好?
RAG 的评估分两个维度:
5.1 检索质量
| 指标 | 含义 |
|---|---|
| Hit Rate | Top-K 结果中是否包含正确文档(0 或 1) |
| MRR(Mean Reciprocal Rank) | 正确文档的排名倒数,越接近 1 越好 |
| Recall@K | Top-K 召回的 relevant 文档占全部 relevant 的比例 |
5.2 生成质量
| 指标 | 含义 |
|---|---|
| Faithfulness | 答案是否忠于检索到的文档(不编造) |
| Answer Relevance | 答案是否切题 |
| Context Relevance | 检索到的上下文是否与问题相关 |
推荐使用 RAGAS 框架(开源),它用 LLM-as-Judge 的方式自动评估以上指标:
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_relevancy
result = evaluate(
dataset=test_dataset,
metrics=[faithfulness, answer_relevancy, context_relevancy]
)
print(result)
# {'faithfulness': 0.87, 'answer_relevancy': 0.92, 'context_relevancy': 0.78}
6. 常见踩坑清单
以下是实际项目中高频出现的问题和解决方案:
坑 1:分块切断了关键信息
现象:检索到了文档片段,但答案被切到两半,LLM 无法回答。
解决:
- 使用语义分块而非固定长度
- 设置 overlap(重叠 10%~20%)
- 对结构化文档(如表格、代码)按结构边界切分
坑 2:向量检索召回不相关内容
现象:Top-5 里有 3 个不相关,稀释了有效信息。
解决:
- 加重排序模型(Re-ranker)
- 降低
top_k召回数,宁少勿滥 - 用元数据过滤(如先按文档类型、时间范围筛选,再向量检索)
坑 3:Prompt 过长导致 LLM “迷失在中间”
现象:研究表明,LLM 对 Prompt 中间部分的信息利用率低于首尾。
解决:
- 控制上下文长度(如不超过 4000 tokens)
- 把最相关的片段放在 Prompt 首尾
- 用上下文压缩减少冗余
坑 4:多轮对话中上下文丢失
现象:用户第二轮问"那它的价格呢",但 RAG 只检索了"价格"这个词,丢失了第一轮的主体。
解决:用 LLM 做对话历史压缩,把多轮对话改写成一个独立的检索查询:
def rewrite_with_history(question, chat_history):
prompt = f"""根据对话历史,将用户最新问题改写为一个独立、完整的检索查询。
对话历史:{chat_history}
最新问题:{question}
改写后的查询:"""
return llm.generate(prompt)
坑 5:知识库更新后向量不同步
现象:文档更新了,但向量库里还是旧内容。
解决:
- 建立增量更新机制(文档 hash 变化时重新索引)
- 设置文档的
updated_at元数据,定期扫描变更 - 考虑用 CDC(Change Data Capture)监听文档变更事件
7. RAG vs Fine-tuning:什么时候用哪个?
这是一个高频问题。简而言之:
| 维度 | RAG | Fine-tuning |
|---|---|---|
| 知识更新 | 实时,改文档即可 | 需重新训练 |
| 成本 | 低(无需训练) | 高(需要 GPU + 数据标注) |
| 适合场景 | 事实性问答、知识库查询 | 风格学习、特定任务格式 |
| 可溯源性 | 有(可返回引用来源) | 无 |
| 幻觉控制 | 好(有参考文档约束) | 一般 |
实践中常组合使用:先 Fine-tune 让模型学会特定领域的话术和推理方式,再用 RAG 注入最新知识。
8. 未来趋势
RAG 技术仍在快速演进,几个值得关注的方向:
-
GraphRAG:用知识图谱替代纯向量检索,支持多跳推理和全局摘要。微软已开源 GraphRAG 项目。
-
Agentic RAG:让 Agent 自主决定是否检索、检索几次、如何组合信息。从"单次检索"走向"迭代式检索"。
-
多模态 RAG:不仅检索文本,还检索图片、表格、视频。文档中的图表信息不再丢失。
-
长上下文 vs RAG:随着 LLM 上下文窗口增长到百万 token(如 Gemini),有人质疑 RAG 是否还有必要。但实践表明:长上下文成本高、延迟大,RAG 在效率和成本上仍有优势——把 100 万 token 全塞进去不如精准检索 5000 token。
9. 总结
RAG 的核心价值在于:用检索约束生成,用外部知识弥补模型不足。
搭建一个好的 RAG 系统的关键不在于某个单点技术多先进,而在于整条链路的工程化:
文档质量 → 分块策略 → 嵌入模型 → 检索方法 → 重排序 → Prompt 设计 → LLM 选择 → 评估反馈
每一环差一点,最终效果就会差很多。建议从朴素 RAG 起步,用评估指标驱动迭代优化,逐步引入混合检索、重排序、查询改写等高级技术。
参考资源:
本文写于 2026 年 11 月,RAG 领域发展迅速,部分工具和模型版本可能已有更新。
- 点赞
- 收藏
- 关注作者
评论(0)