RAG架构与实现
一、RAG基础
Q1: 什么是RAG?为什么需要RAG?
RAG(Retrieval-Augmented Generation,检索增强生成) = 信息检索 + 大语言模型生成。让LLM在生成回答前先检索相关文档,基于文档内容生成答案。
为什么需要RAG?
LLM的三大痛点 RAG的解决方案
1. 知识陈旧(训练数据有时效) → 接入实时知识库,动态更新
2. 幻觉问题(编造不存在事实) → 答案基于真实文档,可溯源
3. 私有知识(企业内部文档) → 连接企业文档,无需微调模型Q2: RAG vs 微调(Fine-tuning)对比
| 维度 | RAG | 微调 |
|---|---|---|
| 核心思想 | 外接知识库 → 让模型"查资料" | 注入知识 → 让模型"记住" |
| 知识更新 | 新增文档即可,无需重新训练 | 需要重新训练/微调 |
| 事实准确性 | 高(有文档来源支持) | 中(易产生幻觉) |
| 可解释性 | 强(可以展示引用来源) | 弱(无法解释知识来源) |
| 部署成本 | 低(仅需向量数据库) | 中高(需要训练GPU资源) |
| 推理延迟 | 中(多了检索步骤) | 低(标准推理) |
| 风格/语气适配 | 弱 | 强 |
结论: 对于基于文档的问答、知识检索等场景,RAG是首选;对于需要特定风格/语气的生成任务(如客服话术),微调更合适。实际项目中常两者结合使用。
Q3: RAG系统的核心流程
┌──────────────────────────────────────────────────────────────┐
│ RAG 完整流程 │
├──────────────────────────────────────────────────────────────┤
│ │
│ ═══════════ 离线数据处理 ═══════════ │
│ │
│ 原始文档(PDF/Word/网页) │
│ ↓ ① 文档解析 │
│ 文本+元数据 │
│ ↓ ② 文本切分(Chunking) │
│ Chunk 1 / Chunk 2 / Chunk 3 ... │
│ ↓ ③ 向量化(Embedding) │
│ 向量1 / 向量2 / 向量3 ... │
│ ↓ ④ 存储 │
│ 向量数据库(Milvus / Chroma / Pinecone) │
│ │
│ ═══════════ 在线推理阶段 ═══════════ │
│ │
│ 用户问题 │
│ ↓ ⑤ 问题向量化 │
│ 查询向量 │
│ ↓ ⑥ 相似度搜索(ANN索引) │
│ Top-K 相似文档 │
│ ↓ ⑦ 构建增强Prompt │
│ [上下文] + [用户问题] │
│ ↓ ⑧ LLM生成 │
│ 答案 + 引用来源 │
│ │
└──────────────────────────────────────────────────────────────┘二、文档处理与切分
Q4: 文档切分(Chunking)的常用策略
| 策略 | 说明 | 优点 | 缺点 |
|---|---|---|---|
| 固定长度切分 | 按字符数/token数切分 | 简单高效 | 可能切断语义完整的句子 |
| 按段落切分 | 以换行符/段落分隔切分 | 语义完整 | 段落长短差异大 |
| 递归切分 | 先按大分隔符切分,不够再按小分隔符 | 兼顾结构与长度 | 需要调参 |
| 语义切分 | 基于Embedding相似度,在语义转折处切分 | 语义最完整 | 计算开销大 |
| Agent切分 | 用LLM理解文档结构智能切分 | 最智能 | 成本高、速度慢 |
推荐实践: LangChain的 RecursiveCharacterTextSplitter
python
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
separators=["\n\n", "\n", "。", "!", "?", ";", ",", " "],
chunk_size=500, # 目标chunk大小(字符/token)
chunk_overlap=50, # 重叠部分,保证上下文连续
length_function=len,
)
chunks = text_splitter.split_text(long_text)Q5: Chunk Size大小的选择
| Chunk Size | 适用场景 | 优缺点 |
|---|---|---|
| 小(100-200 tokens) | 关键词检索、事实问答 | 检索精准但上下文不足 |
| 中(300-500 tokens) | 通用问答 | 平衡精度与上下文 |
| 大(800-1000 tokens) | 需要完整上下文理解的任务 | 上下文完整但噪声多 |
经验建议:
- 先从500 token起步(约300-400中文字符)
- 同时设置 50-100 token 的重叠
- 根据实际效果微调:召回不够就增大chunk,噪声太多就减小chunk
Q6: 如何处理PDF/Word/HTML等格式文档?
文档格式处理策略:
1. PDF文档
├── PyPDF2 / pdfplumber: 提取文本
├── pdf2image + OCR: 扫描版PDF
└── 保留表格: Camelot / Tabula-py
2. Word文档 (.docx)
├── python-docx: 提取段落、表格
└── 保留格式信息(标题层级、加粗等)
3. HTML/网页
├── BeautifulSoup: 解析HTML
├── newspaper3k / Goose: 提取文章主体
└── 去噪: 删除导航栏、广告、页脚
4. Markdown
├── 原生支持(已是结构化文本)
└── 按标题层级切分效果最好三、向量数据库与相似度搜索
Q7: 主流向量数据库对比
| 数据库 | 类型 | 特点 | 适用场景 |
|---|---|---|---|
| Milvus | 自建服务 | 功能全面、性能强、支持分布式 | 生产环境、大规模 |
| Chroma | 本地/服务 | 轻量级、LangChain友好、易部署 | 原型开发、中小规模 |
| Pinecone | SaaS云服务 | 零运维、托管服务、弹性扩展 | 快速上线、不想运维 |
| Weaviate | 自建服务 | 向量+传统字段混合查询 | 需要丰富元数据 |
| FAISS | 库(非DB) | Facebook开源,仅向量索引 | 本地研究、集成到自建系统 |
Q8: 相似度计算方法
1. 余弦相似度(Cosine Similarity) ⭐ 最常用
cos(θ) = A · B / (|A| × |B|)
范围: [-1, 1],越大越相似
优点: 不随向量长度变化,对文本任务效果好
2. 欧式距离(L2 Distance)
d = √(Σ(Ai - Bi)²)
范围: [0, ∞),越小越相似
适用: 图像等需要空间距离的场景
3. 点积(Inner Product / Dot Product)
A · B = Σ(Ai × Bi)
范围: 取决于向量长度
优点: 计算快(如向量已归一化,等效于余弦)实际选择:
- 文本Embedding → 余弦相似度(或归一化后的点积)
- 图像/特征向量 → L2距离
- Milvus等支持自定义metric_type
Q9: 为什么需要ANN(近似最近邻)索引?
暴力搜索(Flat Index)计算量 = N × d(样本数 × 维度) 当N=1亿时,每次搜索要做1亿次向量乘法,太慢!
ANN索引方案对比:
| 索引 | 原理 | 召回率 | 内存 | 构建时间 |
|---|---|---|---|---|
| HNSW | 多层小世界图 | 高 | 中 | 中 |
| IVF | 倒排聚类+残差 | 中 | 低 | 快 |
| IVF-PQ | IVF+乘积量化 | 中 | 极低 | 慢 |
| DiskANN | 磁盘友好索引 | 中高 | 极低 | 慢 |
生产推荐:HNSW(召回率高、搜索速度快,Milvus/FAISS都支持)
四、检索优化(RAG的关键瓶颈)
Q10: 基础检索为什么不够好?
核心问题:
- 语义漂移: "苹果"(公司) vs "苹果"(水果)
- 长尾词: 专业术语、缩写、代号
- 长文档: 信息分散在多个chunk
- 查询与文档不一致: 查询是"为什么",文档是"是什么"
Q11: 常用检索优化技术
技术1: 混合检索(Hybrid Search)——关键词+向量
语义检索(向量) 关键词检索(BM25)
↓ ↓
"如何配置Nginx负载均衡" "Nginx 负载均衡 配置"
↓ ↓
返回语义相关的Top-K 返回关键词匹配的Top-K
↓ ↓
┌───────────融合(RRF/加权)───────────┐
│ 最终排序 = α × 向量分数 + (1-α) × BM25分数 │
└─────────────────────────────────────────┘技术2: HyDE(Hypothetical Document Embedding)
先用LLM生成一个"假设性答案",再用这个假设答案去检索,而不是直接用用户问题。
用户问题: "公司年假制度是怎样的?"
↓
LLM生成假设答案(不需要真实准确,只要风格对):
"公司年假一般根据入职年限计算,新员工有5天,满1年后...
公司制度规定,年假需要提前申请..."
↓
用这个"假设答案"去检索(比问题更接近文档风格)
↓
返回真实的年假制度文档技术3: 重排序(Re-ranking / Cross-Encoder)
第一阶段(粗召回): 向量搜索 → 返回Top-100候选(快但不精)
↓
第二阶段(精排): Cross-Encoder模型 → 对每个[查询, 文档]对打分
↓
最终返回: Top-5相关文档(慢但精准)
常用模型:
- BGE-Reranker (中文效果好)
- Cross-Encoder/MS-Marco技术4: 查询改写(Query Rewriting)
用户原始问题: "XX产品的退货流程?"
↓
LLM改写(加入同义词、补充领域术语):
"XX产品退货政策 退款流程 售后服务 7天无理由..."
↓
用改写后的查询做检索(召回率更高)Q12: RAG优化技术栈一览
┌────────────────────────────────────────────────────────────┐
│ RAG 优化技术层级 │
├────────────────────────────────────────────────────────────┤
│ │
│ Level 1 - 基础RAG │
│ 纯向量搜索 + Top-K │
│ │
│ Level 2 - 高级检索 │
│ 混合检索(向量+关键词) │
│ 查询改写 + HyDE │
│ 元数据过滤 │
│ │
│ Level 3 - 精排 │
│ Cross-Encoder重排序 │
│ RRF/加权融合 │
│ LLM-as-a-Judge 打分 │
│ │
│ Level 4 - 高级上下文管理 │
│ 上下文压缩(Contextual Compression) │
│ 句子级精确定位 │
│ 多跳检索(先查文档A,再根据A查文档B) │
│ │
│ Level 5 - 集成Agent │
│ 检索-推理-再检索的循环 │
│ 文档理解与推理结合 │
│ │
└────────────────────────────────────────────────────────────┘五、Prompt与上下文管理
Q13: RAG中的Prompt设计要点
┌────────────────────────────────────────────────────────┐
│ RAG 增强 Prompt 模板 │
├────────────────────────────────────────────────────────┤
│ │
│ # 角色 │
│ 你是基于知识库的问答助手 │
│ │
│ # 核心规则(⚠️ 最重要) │
│ - 仅基于参考资料回答,不要使用自身知识 │
│ - 如果参考资料中没有答案,请明确说明"参考资料未包含该信息"│
│ - 回答中的关键信息必须引用来源编号 │
│ - 如果多个来源有冲突,指出差异 │
│ │
│ # 参考资料 │
│ [1] [文档标题A] ...[文档内容]... │
│ [2] [文档标题B] ...[文档内容]... │
│ [3] [文档标题C] ...[文档内容]... │
│ │
│ # 用户问题 │
│ {question} │
│ │
│ # 输出要求 │
│ - 答案控制在200字以内 │
│ - 关键信息标注来源编号,如"根据[1][2]..." │
│ - 用自己的话复述,不要直接复制粘贴 │
│ │
└────────────────────────────────────────────────────────┘Q14: 上下文过长怎么办?(Context Window问题)
策略1: 上下文压缩(Contextual Compression)
Top-K文档(可能很长)
↓
LLM/Extractor → 提取每个文档中与问题最相关的句子
↓
压缩后的上下文(只保留最相关片段,大幅减少token)策略2: Map-Reduce模式
Step 1 (Map): 每个文档单独问LLM "这份文档是否回答了问题?如果有,请提取答案"
↓
Step 2 (Reduce): 收集所有文档的答案,让LLM合并为最终答案策略3: 分块总结(Refine)
第一个文档 → 生成初始答案
↓
第二个文档 → 让LLM "根据新文档,是否需要更新答案?"
↓
第三个文档 → 继续更新
↓
...最终答案Q15: 如何处理"检索到的文档都不相关"的情况?
防御性设计:
python
# 1. 设置相似度阈值
results = vector_store.similarity_search_with_score(query, k=5)
mean_score = sum(score for _, score in results) / len(results)
if mean_score < THRESHOLD: # 例如:相似度 < 0.7
return "抱歉,知识库中暂无相关信息,请换个说法或联系客服"
# 2. LLM判断
prompt = """
以下是检索到的文档,请问这些文档是否能回答用户的问题?
如果能,请输出答案;如果不能,请输出"无相关信息"
问题:{query}
参考文档:
{context}
"""六、评估方法
Q16: 如何评估RAG系统的质量?
┌────────────────────────────────────────────────────────────┐
│ RAG 评估指标体系 │
├────────────────────────────────────────────────────────────┤
│ │
│ 🔍 检索质量(Retrieval Quality) │
│ · Precision@K: Top-K中有多少是真的相关的 │
│ · Recall@K: 相关文档有多少被检索到了 │
│ · NDCG@K: 考虑排序位置的加权指标 │
│ │
│ ✍️ 回答质量(Generation Quality) │
│ · Faithfulness(忠实度): 回答是否都在上下文里 │
│ · Answer Relevance: 回答是否针对问题 │
│ · 人工评测(Human Evaluation) │
│ · LLM-as-a-Judge: 用更强的LLM评测答案质量 │
│ │
│ ⚡ 性能指标 │
│ · 端到端延迟(检索+生成) │
│ · Token消耗/成本 │
│ · 吞吐量(QPS) │
│ │
└────────────────────────────────────────────────────────────┘Q17: 构建RAG评测数据集
三步构建法:
Step 1: 从文档中人工设计问题
文档A第3段 → 问题:"XX产品的保修期是多久?"
答案:"1年"
(保证答案确实在文档中)
Step 2: 补充边缘情况
· 多文档交叉引用的问题
· 文档中没有答案的问题(测试"不知道"能力)
· 需要综合多篇文档的问题
Step 3: 标注黄金答案
由人工/专家写出高质量参考答案七、生产环境部署要点
Q18: RAG系统的常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 答案不准确/幻觉 | 检索到的文档不相关,或模型忽视上下文 | 增加检索条数、提高Prompt约束、加入忠实度检查 |
| 检索太慢 | 向量库数据量大,索引不当 | 合理设置索引参数、设置分区、增加缓存层 |
| 上下文太长 | 检索到的内容太多 | 上下文压缩、Chunk大小调整 |
| 元数据缺失 | 切分时信息丢失 | 每个Chunk保留标题、来源、页码等元数据 |
| 中英文混合 | Embedding对中文效果差 | 使用BGE/Zhina-Embedding等中文优化模型 |
| 表格数据检索差 | 表格内容切分后结构丢失 | 单独处理表格:转成结构化文本/JSON |
Q19: RAG系统监控要点
┌────────────────────────────────────────────────────────┐
│ RAG 生产环境监控项 │
├────────────────────────────────────────────────────────┤
│ │
│ 🔌 基础设施 │
│ · 向量数据库QPS / 延迟 / 内存 │
│ · Embedding服务可用性 │
│ · LLM API调用成功率 │
│ │
│ 📈 业务指标 │
│ · 用户满意度评分 │
│ · "无相关信息"的比例(监控检索质量) │
│ · 用户追问率(是否需要追问才能得到答案) │
│ │
│ 📝 日志与可观测 │
│ · Query → 检索到的文档 → 最终答案 完整链路日志 │
│ · 负面反馈收集(用户点击"不准确"时记录) │
│ · A/B测试框架(测试不同检索/Prompt策略) │
│ │
│ 💾 成本监控 │
│ · 每次查询的Embedding+LLM token总成本 │
│ · 向量数据库存储成本 │
│ │
└────────────────────────────────────────────────────────┘RAG高频问题速查
| 问题 | 一句话解决方案 |
|---|---|
| 检索不准 | 换更强的Embedding模型、增加混合检索、重排序 |
| 答案不来自文档 | 加强Prompt约束、加入"不知道"出口、用LLM审查答案 |
| 无法回答多跳问题 | 引入Agent模式,让LLM自主检索-推理-再检索 |
| 上下文太长 | 上下文压缩、Map-Reduce模式、Refine模式 |
| PDF表格处理差 | 用专门的表格解析工具、转成结构化格式 |
| 代码检索效果差 | 代码用AST-based切分、保留完整函数结构 |
| 成本高 | 用本地部署的小模型做Embedding、设置缓存 |