这类 RAG 教程最怕的就是一上来就堆砌概念,把简单的事情讲复杂。其实 RAG 的核心就一句话:让大模型能“翻阅”你自己的资料来回答问题,而不是只靠它训练时学过的知识。
如果你手头有私有文档、公司资料或者特定领域的知识,想快速让 AI 帮你查询、总结、回答,RAG 是目前最实用、门槛也相对较低的落地方式。它不要求你重新训练模型,而是通过“检索-增强-生成”三步,把外部知识喂给模型。
下面我会按实际搭建顺序,拆解从环境准备、文档处理、检索增强到生成回答的全流程。重点会放在“哪些参数影响效果”“怎么判断检索质量”“如何避免幻觉回答”这些实操中真正会卡住的地方。
1. 先理清 RAG 到底在解决什么问题,别被华丽名词带偏
很多人一提到 RAG 就想到向量数据库、Embedding、重排序这些技术词。但实际落地时,你最该关心的不是这些组件本身,而是它们怎么配合起来解决“准确回答私有知识”这个问题。
1.1 RAG 的典型使用场景:什么时候该用,什么时候可能过度
RAG 不是万能的,它特别适合以下场景:
- 企业内部知识库问答:比如公司内部的制度文档、产品手册、历史项目资料,新员工或跨部门同事需要查询时,用 RAG 搭建的问答系统可以直接定位到相关段落。
- 技术文档助手:比如你团队在用某个框架或工具,官方文档庞大,每次遇到问题都要手动搜索。用 RAG 把文档灌进去,可以直接问“怎么配置 XX 参数”“错误 YY 怎么解决”。
- 个人知识管理:如果你积累了大量笔记、论文、报告,想快速找到某个主题的相关内容并让 AI 帮你总结,RAG 比单纯的关键词搜索更智能。
但不适合这些场景:
- 需要高度推理或创造性的问题(例如“写一首诗”),RAG 只是增强事实性,并不提升模型的创造力。
- 知识更新极频繁且对时效性要求极高的场景(例如实时新闻),因为 RAG 的知识库需要定期更新,有一定延迟。
- 问题本身非常简短模糊,RAG 检索可能找不到相关片段,反而引入噪声。
我一般会建议团队先明确:我们到底是要做一个“文档检索器”还是“智能问答助手”?前者更看重检索的准确率和召回率,后者还需要模型能理解问题并组织语言。RAG 更偏向后者,但检索质量是基础。
1.2 和微调(Fine-tuning)的区别:什么时候选 RAG,什么时候选微调
这是最容易混淆的点。简单说:
- RAG是给模型“外接硬盘”,模型本身不变,但每次回答时可以临时读取你的知识库。适合知识量大、更新频繁、不想动模型参数的场景。
- 微调是让模型“学习新知识”,通过训练改变模型权重,使模型内化某些知识或风格。适合任务固定、风格迁移、或知识已经稳定且需要模型深度理解的场景。
举个例子:如果你想让模型用你公司的口吻写邮件,微调更合适;如果你想让模型能回答你公司2024年的新产品规格,RAG 更合适,因为产品规格可能会变,你只需要更新文档库就行。
实际项目中,也有两者结合的方案(RAG + 微调),但初期建议先跑通 RAG,再根据效果决定是否要加入微调。
2. 环境准备:选对工具链,别在配置上卡半天
RAG 的整体流程涉及文档加载、文本分割、向量化、检索、重排、生成等多个环节。市面上工具很多,但新手最容易在环境依赖上踩坑。下面是我多次实战后总结的稳妥组合。
2.1 核心组件选型:平衡易用性和灵活性
对于大多数入门和中等规模场景,我推荐这个组合:
- 文档加载与处理:LangChain 或 LlamaIndex。两者都支持 PDF、Word、TXT、HTML 等常见格式。LangChain 更通用,生态丰富;LlamaIndex 对 RAG 场景优化更深,默认参数更友好。如果你是第一次搭,建议从 LlamaIndex 开始,它的 API 更直观。
- 文本嵌入模型(Embedding Model):负责把文本转换成向量。开源推荐
BAAI/bge-small-zh(中文)或BAAI/bge-small-en(英文),体积小、效果不错,适合本地部署。如果资源充足,可以用更大的bge-large。关键:Embedding 模型的选择直接影响检索质量,但初期不必追求最大模型,先跑通流程更重要。 - 向量数据库:负责存储向量并支持相似度检索。轻量级可选 Chroma(无需单独服务),生产级可选 Milvus 或 Qdrant。第一次实验直接用 Chroma,它和 LangChain/LlamaIndex 集成简单,几行代码就能起。
- 大语言模型(LLM):负责最终生成回答。根据你的资源选:
- 本地部署:ChatGLM3-6B、Qwen-7B 等开源模型,需要 GPU 或足够内存。
- 云端 API:OpenAI GPT、文心一言、通义千问等,按量付费,免部署维护。
- 新手建议先用云端 API(比如 GPT-3.5-turbo)把流程跑通,再考虑本地化。
2.2 硬件和依赖环境:显存、内存和网络条件
- CPU/GPU:如果只用 Embedding 和检索(不本地运行 LLM),普通 CPU 即可。如果需要本地运行 7B 左右的模型,至少需要 16GB 内存(纯 CPU 推理)或 8GB 显存(GPU 推理)。更大模型需要更多资源。
- 内存:文档处理和向量检索会占用内存,建议 8GB 以上。
- 磁盘:向量数据库和模型文件需要空间,预留 10-20GB。
- 网络:如果使用云端 API,需要稳定网络。本地部署则无需外网。
具体到安装,用 Python 环境最方便。创建并激活 conda 环境:
conda create -n rag-demo python=3.10 conda activate rag-demo安装核心包(以 LlamaIndex + Chroma 为例):
pip install llama-index-core llama-index-llms-openai llama-index-embeddings-huggingface chromadb如果你用本地 Embedding 模型,还要装transformers和torch。
2.3 关键目录结构规划:提前想好文档、向量、日志放哪里
很多教程不强调这个,但实际跑起来才发现文件散落各处。建议一开始就建好目录:
my-rag-project/ ├── docs/ # 存放原始文档(PDF、Word等) ├── data/ # 处理后的文本片段 ├── vector_db/ # 向量数据库文件 ├── logs/ # 运行日志 └── main.py # 主程序这样后续更新文档、迁移项目都清楚。
3. 从零搭建:文档处理、向量化与检索
环境准备好后,我们一步步实现一个最小可用的 RAG 系统。我会用 LlamaIndex + 本地 Embedding + Chroma 的配置,这样即使断网也能跑。
3.1 文档加载与文本分割:怎么切分效果最好
文档加载不难,但文本分割(chunking)的参数直接影响检索效果。切得太碎,上下文不完整;切得太大,检索会引入无关内容。
LlamaIndex 提供了多种分割器,新手先用SentenceSplitter:
from llama_index.core import SimpleDirectoryReader from llama_index.core.node_parser import SentenceSplitter # 加载 docs/ 目录下的所有支持文档 documents = SimpleDirectoryReader("./docs").load_data() # 创建分割器:块大小 512 字符,重叠 50 字符 parser = SentenceSplitter(chunk_size=512, chunk_overlap=50) nodes = parser.get_nodes_from_documents(documents)关键参数解释:
chunk_size:每个文本块的最大长度。一般 256-1024 之间。太小会丢失上下文,太大会降低检索精度。中文可以按字符数算,英文按 token 数。先从 512 开始试。chunk_overlap:块之间的重叠长度。设置 10%-20% 的重叠可以避免把完整句子切碎,改善边界检索。比如 512 的块大小,重叠 50-100。
分割完后,建议打印几个节点看看内容是否完整:
print(f"总共切分 {len(nodes)} 个节点") print("第一个节点内容:", nodes[0].text[:200]) # 看前200字符如果发现切分不合理(比如半句话、表格乱掉),调整chunk_size和chunk_overlap,或者换用TokenTextSplitter(按 token 数切分,更符合模型习惯)。
3.2 向量化与存储:选对 Embedding 模型和数据库
接下来把文本节点转换成向量,存进向量数据库。
from llama_index.embeddings.huggingface import HuggingFaceEmbedding from llama_index.core import VectorStoreIndex, StorageContext from llama_index.vector_stores.chroma import ChromaVectorStore import chromadb # 1. 初始化本地 Embedding 模型 embed_model = HuggingFaceEmbedding( model_name="BAAI/bge-small-zh" # 用中文小模型 ) # 2. 初始化 Chroma 向量数据库 db = chromadb.PersistentClient(path="./vector_db") chroma_collection = db.get_or_create_collection("rag_demo") vector_store = ChromaVectorStore(chroma_collection=chroma_collection) storage_context = StorageContext.from_defaults(vector_store=vector_store) # 3. 构建索引:将节点向量化并存入数据库 index = VectorStoreIndex( nodes, embed_model=embed_model, storage_context=storage_context )这段代码做了几件事:
- 加载
bge-small-zh模型(首次运行会下载,约 100MB)。 - 连接 Chroma 数据库,数据会持久化在
./vector_db目录。 - 把之前分割的文本节点(nodes)通过 Embedding 模型转换成向量,并建立索引。
完成后,你的向量数据库里就存储了所有文档片段的向量。之后检索就是在这个向量空间里找最相似的片段。
3.3 检索器配置:简单检索 vs 带重排序的检索
最基本的检索是直接取相似度最高的前 k 个片段:
# 创建检索器:取前3个最相似片段 retriever = index.as_retriever(similarity_top_k=3)但这样可能不够准,因为单纯看向量相似度,可能漏掉一些语义相关但表述不同的内容。这时候可以加一个“重排序”步骤,用更小的、专门做相关性判别的模型对检索结果重新排序。
from llama_index.core.postprocessor import SentenceTransformerRerank # 初始化重排序模型(可选,但推荐) rerank_model = SentenceTransformerRerank(model="BAAI/bge-reranker-base", top_n=2) # 现在检索器会先取 top_k=10,然后重排序选出 top_n=2 个最相关的 retriever = index.as_retriever(similarity_top_k=10, node_postprocessors=[rerank_model])重排序模型比 Embedding 模型更擅长判断“问题-段落”之间的相关性,能显著提升准确率,但会增加计算量。初期可以不加,等基础流程跑通后再引入。
4. 问答生成:连接 LLM,控制幻觉与格式
检索到相关片段后,最后一步是把它们和问题一起交给大模型生成答案。这里最需要关注的是提示词设计和如何降低模型“胡说八道”(幻觉)的概率。
4.1 初始化 LLM 并创建查询引擎
如果你用 OpenAI API:
from llama_index.llms.openai import OpenAI import os os.environ["OPENAI_API_KEY"] = "your-api-key" # 替换成你的 key llm = OpenAI(model="gpt-3.5-turbo") # 或者 "gpt-4" query_engine = index.as_query_engine(llm=llm, retriever=retriever)如果你用本地模型(以 ChatGLM3-6B 为例,需额外安装llama-index-llms-chatglm):
from llama_index.llms.chatglm import ChatGLM llm = ChatGLM(model_url="http://localhost:8000/v1") # 假设本地已启动 ChatGLM API 服务 query_engine = index.as_query_engine(llm=llm, retriever=retriever)4.2 提问并分析返回结果
现在可以提问了:
response = query_engine.query("公司年假制度是怎样的?") print(response)但直接打印可能不够,我们更关心模型引用了哪些文档片段(以便验证答案来源):
# 查看检索到的源节点 for node in response.source_nodes: print(f"来源文档: {node.node.metadata.get('file_name', '未知')}") print(f"相似度得分: {node.score:.4f}") print(f"内容片段: {node.node.text[:200]}...\n")这样你就能判断模型是不是真的基于你的资料在回答,而不是凭空编造。
4.3 优化提示词,减少幻觉和无关回答
默认的提示词可能不够强,我们可以自定义,让模型更严格地依据检索内容回答:
from llama_index.core import PromptTemplate qa_prompt_tmpl = ( "请严格依据以下检索到的信息来回答问题。\n" "如果信息不足以回答问题,请直接说“根据现有资料无法回答”。\n" "不要编造信息。\n" "检索到的信息:\n" "{context_str}\n" "问题:{query_str}\n" "答案:" ) qa_prompt = PromptTemplate(qa_prompt_tmpl) query_engine = index.as_query_engine( llm=llm, retriever=retriever, text_qa_template=qa_prompt )这个提示词做了三件事:
- 强调“严格依据检索信息”,降低幻觉。
- 明确要求“信息不足时直接说无法回答”,避免强行编造。
- 结构清晰,把上下文和问题分开。
实测中,好的提示词对答案质量的影响不亚于检索质量。
5. 效果评估与迭代:怎么判断你的 RAG 系统是否可靠
搭建完不是结束,更重要的是评估和优化。很多人只关注“能不能跑起来”,却不知道如何判断“跑得好不好”。
5.1 构建测试集:用真实问题验证
准备 10-20 个典型问题,覆盖简单查询、多步推理、模糊提问等场景。每个问题最好有标准答案或已知答案所在的文档段落。
例如:
- 简单事实:“公司年假有多少天?”
- 多条件:“2024年新员工年假怎么计算?”
- 模糊查询:“关于报销制度有哪些规定?”
手动运行这些问题,记录:
- 检索到的片段是否相关?
- 答案是否准确?
- 模型是否幻觉?
不要只看最终答案,要一步步检查检索质量和生成质量。
5.2 常见问题排查链路:效果不好时按这个顺序查
如果答案不准,别急着调模型,按这个顺序排查:
检索阶段问题:检索到的片段根本不相关。
- 检查文本分割:块大小是否合适?重叠是否足够?切分是否破坏了句子完整性?
- 检查 Embedding 模型:是否适合你的领域?中文/英文模型选对了吗?尝试换模型(如从
bge-small升级到bge-large)。 - 检查检索数量:
similarity_top_k是否太小?尝试从 3 调到 5 或 10。 - 考虑加入重排序。
生成阶段问题:检索片段相关,但模型回答不好。
- 检查提示词:是否明确要求基于上下文?是否限制了幻觉?
- 检查上下文长度:是否因为片段太多导致模型忽略前面内容?尝试减少
top_k或增加上下文窗口。 - 检查 LLM 能力:如果问题复杂,可能需要更强模型(如 GPT-4)。
端到端问题:流程跑通,但批量测试得分低。
- 检查数据质量:原始文档是否清晰?扫描版 PDF 是否提取错误?需要预处理 OCR 或清理格式。
- 考虑进阶优化:如混合检索(关键词+向量)、元数据过滤等。
5.3 进阶优化方向:当基础版满足不了需求时
- 混合检索:结合向量检索和关键词检索(BM25),提升召回率。LlamaIndex 支持
VectorIndexAutoRetriever配置混合检索。 - 元数据过滤:给文档片段加标签(如部门、日期、文档类型),检索时限定范围。例如“只检索财务部2024年的文档”。
- 查询转换:对复杂问题先进行分解或重写,再检索。例如“公司年假和病假制度有什么不同?”拆成“年假制度”和“病假制度”两个查询。
- Agentic RAG:让系统能主动追问、多步检索、自我验证,适合复杂问答场景。可以用 LangGraph 实现多步推理。
但记住:先保证基础流程稳定,再逐步加入进阶功能。一次加太多优化点,出了问题很难定位。
6. 生产化部署:从脚本到可持续服务的注意事项
实验脚本能跑通,和真正投入使用之间还有距离。如果你打算长期用,或者给团队用,需要考虑以下几点。
6.1 文档更新机制:知识库不是一次性的
原始文档会有增删改,你的 RAG 系统需要支持更新,而不是每次全量重建。
- 增量更新:新文档加到
docs/目录后,只需处理新文档并添加到现有向量数据库。LlamaIndex 支持index.insert()方法增量添加。 - 定时重建:如果文档变动大,可以每周/每月全量重建一次。用脚本自动化这个过程,并做好版本备份。
- 版本控制:向量数据库和原始文档最好对应版本号,方便回滚。
6.2 服务化与 API 暴露
把 RAG 系统封装成 API 服务,方便其他应用调用。可以用 FastAPI 快速搭建:
from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class QueryRequest(BaseModel): question: str @app.post("/query") async def query_rag(request: QueryRequest): response = query_engine.query(request.question) return {"answer": str(response), "sources": [node.node.metadata for node in response.source_nodes]}然后通过uvicorn启动服务,前端或其他系统就可以通过 HTTP 调用了。
6.3 监控与日志
生产环境必须要有日志和监控:
- 问题日志:记录每个问题的检索片段、生成答案、耗时。方便后续分析效果。
- 异常监控:API 调用失败、模型超时、数据库连接错误等要有告警。
- 效果指标:定期用测试集跑一遍,记录准确率、召回率、用户满意度。
最简单的开始方式是每问必日志,然后定期人工抽检。
6.4 成本与性能权衡
- 云端 API 成本:如果使用 GPT-4,token 消耗是成本大头。可以通过缓存常见问答、优化提示词减少长度来控制。
- 本地资源成本:如果本地部署模型,考虑 GPU 显存、电费、维护成本。7B 模型在 8GB 显存卡上可以流畅运行,更大模型需要更多资源。
- 响应时间:检索+生成的总耗时影响用户体验。优化方向:检索阶段用更快 Embedding 模型、生成阶段用较小 LLM 或流式输出。
我一般建议初期用云端 API 快速验证需求,等用量稳定后再评估是否本地化。
最后提醒一点:RAG 项目成功的关键往往不是技术选型多先进,而是业务场景是否明确、知识库质量是否高、以及是否有持续迭代的机制。先用一个最小版本解决实际痛点,再逐步优化,比一开始就追求大而全更容易出成果。