10-大模型智能体开发工程师:RAG检索增强生成
系列文章导航: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-small Step 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", "。", " "]依次分割 直到块大小合适 优点: 兼顾语义和大小# LangChain递归分块示例fromlangchain.text_splitterimportRecursiveCharacterTextSplitter 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 检索策略
基础: 向量相似度检索
# 余弦相似度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
fromopenaiimportOpenAIimportchromadbfromchromadb.utilsimportembedding_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构建向量索引
- 实现查询功能,返回答案+来源
参考答案:
# save as: rag_demo.pyimportchromadbfromchromadb.utilsimportembedding_functionsfromopenaiimportOpenAI# 初始化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"]fordindocuments],ids=[d["id"]fordindocuments],metadatas=[{"source":d["source"]}fordindocuments])defask(question:str)->str:# 检索results=collection.query(query_texts=[question],n_results=2)context_parts=[]fordoc,metainzip(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)returnresponse.choices[0].message.content# 测试print(ask("Docker是谁创建的?"))print("---")print(ask("K8s和Docker有什么关系?"))🎉Part 2完成!你已经掌握了Prompt、Context、结构化输出、RAG这些应用开发核心技能。
下一篇文章见:AI系列文章导航目录-持续更新中
