先给结论:RAG 是目前搭建知识库问答最稳的基线方案。RAG 的全称是 Retrieval-Augmented Generation,检索增强生成。它解决的问题很直接:大模型不知道你的私有资料、内部文档、产品手册和业务数据,那就先把这些资料切块、向量化、存进向量库;用户提问时,先从库里检索最相关的文本片段,再把片段和问题一起交给大模型,让模型基于给定材料回答。这样既能回答训练数据之外的内容,又能给出引用出处,方便人工核对。这篇教程是一条完整的 RAG 入门到实战路线,覆盖文档加载、文本切分、向量化、向量库检索、召回优化、重排序、问答链路搭建、接口服务和批量任务。看完你可以照着跑通一套最小可用的知识库问答系统,并且知道检索结果不准时应该往哪个方向调。适合刚开始接触 RAG、想从零搭一套本地知识库问答的新手,也适合已经跑通流程但召回质量不理想、想系统做优化的开发同学。没有 GPU 能不能学 RAG?可以。如果你用 API 方式调用大模型和 Embedding 模型,普通笔记本就能跑通全链路;如果你只想在本地跑,8GB 内存的机器也能完成文档入库和向量检索,只是在本地加载模型做生成时会慢一些。下面开始。1. RAG 核心能力速览能力项说明技术架构检索增强生成(Retrieval-Augmented Generation)核心链路文档加载 -> 文本切分 -> 向量化 -> 向量库检索 -> 重排序 -> LLM 生成主流实现框架LangChain、LlamaIndex、Haystack、RagFlow、FastGPT、Dify 等常用向量库Chroma、FAISS、Milvus、Qdrant、pgvector、Elasticsearch硬件门槛纯 API 调用时普通笔记本即可;本地 Embedding 建议 8GB 以上内存;本地 LLM 推理按模型规格另行评估接口能力框架普遍支持 REST API,可接入业务系统批量任务支持批量文档入库、批量问答,建议配合队列和日志适合场景企业知识库问答、产品文档助手、私有资料检索、客服辅助、合规审核等这张表里没有给固定显存数字,原因是 RAG 的资源占用高度依赖你选用的模型:纯向量检索阶段内存占用不高;本地跑 Embedding 模型会占内存;本地跑 7B、13B 参数的大语言模型才会明显吃显存。更稳妥的判断是:先跑通 API 版本,再决定要不要上本地模型。2. RAG 原理拆解:知识库问答完整链路RAG 可以拆成三个大阶段:索引阶段、检索阶段、生成阶段。理解这三个阶段,后面调参时就不会乱。2.1 索引阶段:把资料变成可检索的文本块索引阶段的目标是把一份 PDF、Word、Markdown 或纯文本文档,变成一组带向量表示的、可被快速检索的文本块。流程是:文档加载:读取原始文件。PDF 要抽文本,Word 要解析段落,网页要抓正文,这一步处理不好后面全是噪音。文本切分:按章节、段落、句子或固定长度把文档切成小块,同时保留一定重叠。切分直接影响召回质量。向量化:用 Embedding 模型把每个文本块编码成一个稠密向量。语义相近的文本,向量距离更近。向量库写入:把文本块内容和向量一起存进向量数据库,同时记录来源、页码、章节等元数据。切分为什么要重叠?因为一个完整语义可能横跨两个文本块边界。不重叠,检索时容易漏掉半边信息;重叠一部分,主题信息在两块里都保留一份,召回更稳。2.2 检索阶段:把问题变成向量,取回最相关内容用户输入问题后,系统做三件事:问题向量化:用和文档入库时同一个 Embedding 模型,把用户问题编码成向量。相似度检索:在向量库里用余弦相似度或点积等方式,取出最相似的 TopK 个文本块。可选重排序:TopK 结果用 Cross-Encoder 风格的 Reranker 再精排一次,把真正相关的内容顶到前面。检索阶段是最值得调优的地方。TopK、切分大小、检索策略、Reranker 都会影响最后问答质量。后面单独用一章讲。2.3 生成阶段:把检索结果拼进提示词生成阶段做的事情是把检索到的文本块按顺序拼进系统提示词,加上用户问题,一起交给大模型生成回答。典型的提示词结构是:系统提示词:你是一个知识库问答助手。请只根据下面提供的资料回答问题,不要编造资料中没有的内容。 资料 1:<检索到的文本块> 资料 2:<检索到的文本块> 资料 3:<检索到的文本块> 用户问题:<用户输入>要求模型"只根据资料回答",是抑制幻觉的关键。还可以让模型在回答末尾列出引用的资料编号,实现可追溯。3. 适用场景与使用边界3.1 适合解决什么问题RAG 最核心的价值是解决"大模型不知道私有知识"的问题。企业知识库问答:内部制度、产品手册、售后文档,员工问一句就能定位答案。产品文档助手:把官网文档切片入库,用户遇到问题直接问,答案还带出处。私有资料检索:合同、报告、论文、工程文档的语义搜索。客服辅助:在聊天窗口里帮客服快速找到标准答案和原文位置。合规审核辅助:把规章制度向量化,审核时提问并核对原文。RAG 和微调的区别值得记住:微调是改变模型本身的参数,适合改变语气、固化少量专业知识;RAG 不改模型参数,每次回答都动态查最新资料,适合知识频繁更新、需要可追溯、需要权限隔离的场景。对大多数知识库问答需求,先上 RAG,解决不了再看微调。3.2 不适合什么场景不是所有问题都适合 RAG。高频实时数据:股价、库存、订单状态,这类数据应该直接查数据库或调用业务接口,而不是走文档检索。全局统计类问题:"这个季度销售额是多少",如果知识库里只有非结构化的日报文本,RAG 查不准。强多跳推理:问题需要把十几个文档的信息串起来推理,单轮检索很难覆盖,需要引入 Agent 和规划能力。需要绝对精确计算的场景:RAG 的答案是生成式的,适合"找到并概括原文",不适合当计算器。3.3 使用边界与合规提醒RAG 系统处理的一定是真实业务数据,部署和使用时要注意:确认知识库内容有合法来源和授权,不要上传未经许可的文档、合同、个人信息和隐私数据。涉及人脸、声音、身份信息的数据要先脱敏。接口服务要限制访问范围,不要裸奔在公网,建议加鉴权。对外发布或商用之前,要对检索结果和回答内容做人工抽样复核。4. 环境准备与前置条件4.1 硬件与系统从低到高有几种跑法:跑法建议配置说明纯 API 跑法4GB 内存即可,无 GPU 也能跑Embedding 和 LLM 全部走远程 API,本机只做切分、向量库和检索本地 Embedding + API LLM8GB 以上内存需要在本地加载一个 Embedding 模型,建议有 CPU 即可,但首次向量化会偏慢全本地跑法16GB 以上内存,最好有一张 8GB 以上显存的 GPU本地跑 Embedding 加本地大模型,要求最高,适合数据不能出内网的场景更稳妥的启动路径是:先跑纯 API 版本,确认全链路能通,再逐步替换成本地组件。这样能把"链路问题"和"资源问题"分开排查。4.2 Python 环境RAG 生态以 Python 为主,建议准备:Python 3.9 及以上版本。独立的虚拟环境,避免和已有项目冲突。pip 依赖管理。推荐的目录结构:rag_project/ ├── data/ # 原始文档 │ └── product_manual.pdf ├── chunks/ # 切分后的中间结果,方便排查 ├── vector_db/ # 向量库持久化目录 ├── scripts/ # 入库、检索、服务脚本 ├── logs/ # 日志 └── .env # API Key 等环境变量前端展示不是必须的,先做服务端链路。日志目录和中间结果目录一定要保留,RAG 排错全靠它们。5. 最小可用系统搭建:LangChain + Chroma 部署 RAG 服务这一节我们用 LangChain 配合 Chroma 搭最小可用系统。LangChain 是当前使用最广的编排框架,Chroma 是本地嵌入式向量库,适合学习和原型验证,不用单独启动服务端。5.1 安装依赖# 创建虚拟环境 python -m venv rag_env # Windows 激活 rag_env\Scripts\activate # macOS / Linux 激活 source rag_env/bin/activate # 安装依赖,版本按实际环境选择 pip install langchain langchain-community langchain-openai chromadb faiss-cpu pypdf sentence-transformers fastapi uvicorn python-dotenv如果网络受限,可以只装核心依赖,逐个补。这里不用固定版本号,因为 LangChain 迭代较快,接口名在不同版本之间会有差异,安装时以官方文档为准。5.2 环境变量# .env 文件 OPENAI_API_KEY=sk-your-key-here如果你不用 OpenAI,而是用其他兼容接口的模型服务,把 base URL 和模型名替换成对应服务的地址即可。Key 不要写进代码,用环境变量管理。5.3 启动前检查# 确认 Python 版本 python --version # 确认依赖已经安装 pip show langchain chromadb首次运行建议用一个 10 页以内的小文档测试,不要上来就跑 500 页的大目录。小文档能快速暴露切分、向量化、检索链路里的问题。6. 文档入库实践:加载、切分、向量化6.1 加载文档先用 PyPDFLoader 加载 PDF,用 TextLoader 加载 TXT 和 Markdown。import os from dotenv import load_dotenv load_dotenv() from langchain_community.document_loaders import PyPDFLoader, TextLoader, DirectoryLoader # 加载单个 PDF pdf_loader = PyPDFLoader("data/product_manual.pdf") documents = pdf_loader.load() print(f"PDF 加载完成,共 {len(documents)} 页文档对象") # 按目录加载文本文件 text_loader = DirectoryLoader( "data", glob="**/*.txt", loader_cls=TextLoader, loader_kwargs={"encoding": "utf-8"} ) text_docs = text_loader.load()加载后建议先打印前几页文本,看看有没有解析乱码、表格丢行、图片文字缺失。PDF 解析是最容易出问题的一环,乱码文本喂进去,后面的检索和生成都受影响。6.2 文本切分切分参数有两个要重点调:chunk_size控制每个文本块的长度,chunk_overlap控制相邻块之间的重叠长度。from langchain_text_splitters import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=80, separators=["\n\n", "\n", "。", "!", "?", ".", "!", "?", ";", ";"], length_function=len, ) chunks = text_splitter.split_documents(documents) print(f"切分完成,共 {len(chunks)} 个文本块") # 把切分结果落盘,方便之后排查召回 import json with open("chunks/chunks_preview.json", "w", encoding="utf-8") as f: json.dump( [{"content": chunk.page_content, "metadata": chunk.metadata} for chunk in chunks[:20]], f, ensure_ascii=False, indent=2 )separators里加入中文句号和感叹号非常重要。很多默认切分器只识别英文标点,遇到长中文文档会把整段话切碎或切得语义混乱。切完先人工看一眼预览文件,确认每个块语义基本完整。6.3 向量化与向量库写入向量化可以选远程 API 模型,也可以选本地开源中文模型。这里给出两种方式。远程 Embedding 方式:from langchain_openai import OpenAIEmbeddings embeddings = OpenAIEmbeddings(model="text-embedding-3-small")本地中文模型方式:from langchain_community.embeddings import HuggingFaceEmbeddings # BGE 系列中文向量模型,适合中文知识库 embeddings = HuggingFaceEmbeddings( model_name="BAAI/bge-large-zh-v1.5", encode_kwargs={"normalize_embeddings": True}, )写入 Chroma:from langchain_community.vectorstores import Chroma vectorstore = Chroma.from_documents( documents=chunks, embedding=embeddings, persist_directory="./vector_db" ) print("文档向量化完成,数据已写入 vector_db 目录")首次向量化时注意耗时。如果文档数量大,可以分批写入,不要一次性把几千个块塞进内存。6.4 检查入库结果# 统计向量数量 collection = vectorstore._collection print(f"向量数量:{collection.count()}") # 做一次相似度检索验证 test_query = "产品的主要功能有哪些?" docs = vectorstore.similarity_search(test_query, k=3) print("检索到 3 个相关文本块:") for doc in docs: print("-" * 40) print(doc.page_content[:200]) print(f"来源:{doc.metadata.get('source', 'unknown')}")如果检索结果和问题明显无关,先回切分参数和文档解析质量,不要急着加 Reranker。7. 检索召回优化:查询改写、混合检索与重排序很多 RAG 项目跑通后,效果不理想,问题大多出在召回阶段,比如:用户问题故意绕弯子,和知识库里原文表达不一致。知识库关键词和用户问题关键词对不上。向量检索召回的前几个块,看着相关但都不是关键段落。7.1 先确认问题出在召回还是生成一个实用的办法:直接打印召回来的源文档,不看生成结果。test_query = "退货需要什么条件?" retrieved = vectorstore.similarity_search(test_query, k=5) for i, doc in enumerate(retrieved): print(f"第 {i+1} 个召回块:{doc.page_content[:150]}")如果这 5 个块里没有正确答案,那就是召回问题,调检索策略;如果召回块里有正确答案,但回答仍不对,那就是生成或提示词组装问题。7.2 查询改写用户问题往往很口语化,直接拿原始问题去检索效果不好。可以先让大模型把问题改写成更适合检索的形式,或者在多轮对话时把指代问题补全。from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate rewrite_prompt = ChatPromptTemplate.from_messages([ ("system", "你是检索查询改写助手。请把用户问题改写成适合向量检索的简洁关键词句子,保留关键术语。"), ("human", "{query}") ]) llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) rewrite_chain = rewrite_prompt | llm rewritten = rewrite_chain.invoke({"query": "我想了解一下产品坏了能不能修,该找谁"}) print(rewritten.content)输出可能是"产品售后维修流程 联系人",而不是用户原话。这样检索命中率会明显提升。7.3 混合检索向量检索擅长语义匹配,关键词检索擅长精确匹配。工程实践里常用混合检索:向量召回和 BM25 关键词召回各取一部分,合并去重后再给 Reranker。Chroma 不带内置 BM25,可以用bm25s或rank_bm25这类轻量库做一层关键词召回,然后用 RRF(Reciprocal Rank Fusion)合并两路结果。# 概念示例:RRF 合并 # 向量检索结果和关键词检索结果都带一个排名 # rrf_score = sum(1 / (k + rank_i)),k 一般取 60 # 合并后按 rrf_score 从高到低取 TopN具体实现不复杂,重点是思路:不要把全部希望压在向量检索一条路。对于术语明确的领域,比如"合同编号""退款时效 7 天",关键词召回往往比向量召回更准。7.4 重排序 Reranker向量检索是双塔结构,问题和文档分别编码,速度快但精度有限。Reranker 用 Cross-Encoder 把问题和文档拼成一个输入,逐条打分,速度慢但更准。from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import CrossEncoderReranker from langchain_community.cross_encoders import HuggingFaceCrossEncoder # 基础召回器:先取 8 个候选 base_retriever = vectorstore.as_retriever( search_type="similarity", search_kwargs={"k": 8} ) # 重排序模型,用 BGE 的中文 Reranker reranker = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-base") compression_retriever = ContextualCompressionRetriever( base_compressor=CrossEncoderReranker(model=reranker, top_n=3), base_retriever=base_retriever ) # 验证重排序后的结果 top_docs = compression_retriever.invoke("退货需要什么条件?") print(f"重排序后保留 {len(top_docs)} 个块")Reranker 的典型用法是"先召回多、再精排少":向量检索召回 8 到 10 个候选,Reranker 精排后只保留 3 个。这样可以兼顾效率和准确率。7.5 元数据过滤与切分参数调整如果知识库里有明确的类型、部门、年份、产品线等元数据,可以在检索时加过滤条件,缩小范围。# 示例:只检索指定产品线的文档 retriever = vectorstore.as_retriever( search_kwargs={ "k": 5, "filter": {"product": "企业版"} } )如果过滤之后效果仍不好,回到切分层调整。常见思路:召回块太碎:调大chunk_size到 800 或 1000。召回块太杂:调小chunk_size到 300 到 400,让边界更清晰。语义跨块丢失:增加chunk_overlap。表格类文档:考虑按表格结构单独切分,不要让大表格被硬切成多块。8. 功能测试与效果验证RAG 系统上线前要有一套可重复的测试用例,不能只看一两个问答的效果。建议准备 20 到 50 条真实业务问题,标注每道题的期望答案来自哪份文档的哪一节。测试项测试输入预期结果判断标准文档入库10 页测试 PDF向量数量大于文本页数,文本无乱码打印前 3 个块的全文单轮问答"退款需要什么条件?"答案包含退款时效和申请入口答案内容能对应到召回块原文多轮指代"那要是货没收到呢?"系统把"那"指代到退货场景召回块里包含退货相关内容术语匹配"Z100 报错怎么解决"找到 Z100 相关章节召回的前 3 个块包含 Z100 关键词长文本处理5000 字长文档单章提问从正确章节召回,而不是截到无关段落召回块来源正确批量问答10 条问题批量跑全部有返回,超时的重试成功日志记录时间与失败次数功能测试建议写成一个 Python 脚本,输入是 JSON 文件里的问题列表,输出是一个评测报告。# test_rag.py 简化示例 import json import time questions = [ {"query": "退款需要什么条件?", "expected_source": "product_manual.pdf"}, {"query": "Z100 报错怎么解决?", "expected_source": "faq.pdf"}, ] for item in questions: start = time.time() result = qa_chain.invoke({"query": item["query"]}) cost = round(time.time() - start, 2) print(f"问题:{item['query']}") print(f"回答:{result['result'][:200]}") print(f"耗时:{cost}s") print("-" * 40)判断标准不是"回答看起来像那么回事",而是:回答内容是否有召回块支撑,来源是否和人工预期一致,同一问题多次回答是否稳定。这三个标准能过滤掉大部分不达标的 RAG 系统。9. 接口 API 与批量任务设计9.1 用 FastAPI 暴露问答接口系统跑通后,下一步是给前端或业务系统提供接口。用 FastAPI 写一层很轻。# api.py import os from dotenv import load_dotenv load_dotenv() from fastapi import FastAPI, HTTPException from pydantic import BaseModel from langchain_openai import ChatOpenAI from langchain_community.vectorstores import Chroma from langchain_community.embeddings import HuggingFaceEmbeddings from langchain.chains import RetrievalQA app = FastAPI(title="RAG 知识库问答服务") # Embedding 必须与入库时保持一致 embeddings = HuggingFaceEmbeddings( model_name="BAAI/bge-large-zh-v1.5", encode_kwargs={"normalize_embeddings": True}, ) vectorstore = Chroma( persist_directory="./vector_db", embedding_function=embeddings, ) retriever = vectorstore.as_retriever(search_kwargs={"k": 5}) llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.2) qa_chain = RetrievalQA.from_chain_type( llm=llm, chain_type="stuff", retriever=retriever, return_source_documents=True, ) class QueryRequest(BaseModel): query: str top_k: int = 5 @app.post("/api/rag/query") async def query_rag(request: QueryRequest): if request.top_k < 1 or request.top_k > 20: raise HTTPException(status_code=400, detail="top_k 范围是 1 到 20") result = qa_chain.invoke({"query": request.query}) return { "answer": result["result"], "sources": [ { "content": doc.page_content, "metadata": doc.metadata, } for doc in result["source_documents"] ], }启动服务:uvicorn api:app --host 127.0.0.1 --port 8000注意端口被占用时换一个:uvicorn api:app --host 127.0.0.1 --port 80019.2 用 curl 和 Python 验证接口curl -X POST http://127.0.0.1:8000/api/rag/query \ -H "Content-Type: application/json" \ -d '{"query": "退货需要什么条件?", "top_k": 5}'import requests url = "http://127.0.0.1:8000/api/rag/query" payload = {"query": "退货需要什么条件?", "top_k": 5} response = requests.post(url, json=payload, timeout=120) if response.status_code == 200: data = response.json() print(data["answer"]) print("引用来源:") for src in data["sources"]: print("-", src["metadata"].get("source")) else: print("请求失败:", response.status_code, response.text)接口层要控制的三个问题:超时时间、来源字段是否完整、返回结果是否稳定。接口返回的sources非常重要,前端可以展示"回答依据来自哪个文档",这是 RAG 相对纯大模型回答的核心优势。9.3 批量任务与失败重试批量问答不要在单线程里裸跑。建议把问题列表先落盘,逐条调用接口,记录状态,失败重试,最后生成汇总报告。# batch_qa.py import json import time import requests questions = [ "退款需要什么条件?", "Z100 报错怎么解决?", "如何联系客服?", ] results = [] for i, q in enumerate(questions, start=1): for attempt in range(3): try: resp = requests.post( "http://127.0.0.1:8000/api/rag/query", json={"query": q}, timeout=60, ) resp.raise_for_status() data = resp.json() results.append({ "query": q, "answer": data["answer"], "sources": [s["metadata"].get("source") for s in data["sources"]], "status": "success", }) print(f"[{i}/{len(questions)}] 完成,共 {len(data['sources'])} 个引用") break except Exception as e: print(f"[{i}/{len(questions)}] 第 {attempt+1} 次失败:{e}") time.sleep(2 ** attempt) else: results.append({"query": q, "status": "failed"}) with open("logs/batch_result.json", "w", encoding="utf-8") as f: json.dump(results, f, ensure_ascii=False, indent=2) print(f"批量完成,成功 {sum(1 for r in results if r['status'] == 'success')} 条")批量任务的核心原则:过程要有日志,失败要有重试,结果要有落盘。不要用 print 代替日志,建议写成结构化 JSON 或 CSV 文件。10. 资源占用与性能观察RAG 的资源观察要分阶段看。10.1 向量化阶段向量化阶段主要消费 CPU 和内存。用本地 BGE 模型做向量化时,几百页文档也要跑一段时间。观察方法:# 监控内存占用 htop # 或者看 Python 进程占用 ps aux | grep python降低向量化耗时的方法:用 GPU 版 sentence-transformers,模型自动跑 GPU。分批向量化,避免一次性加载全部文本。设置batch_size,例如 32 或 64。首次向量化后把向量库持久化,后续增量入库。10.2 检索阶段向量检索在内存中计算相似度,纯检索阶段内存占用较低。如果向量数量很大,建议升级到 FAISS 或 Milvus,Chroma 在本地原型阶段够用,但数据量到几十万条以上时,性能和持久化都要重新评估。10.3 生成阶段生成阶段的资源消耗取决于 LLM 在哪里跑:API 方式:本机几乎无压力,瓶颈在接口响应时间和限流。本地 GPU 方式:显存占用取决于模型大小和上下文长度。显存占用会随检索块数量、文本块长度和 max_tokens 增加而上升。实际占用要以本机测试为准,不要直接照搬网上数值。观察显存可以用:nvidia-smi -l 110.4 影响性能的主要参数参数影响调优方向chunk_size越大单块信息越全,越小检索越精准但越碎300 到 1000 之间按文档类型调chunk_overlap越大上下文越连贯,但向量库体积变大通常取 chunk_size 的 10% 到 20%TopK越大上下文越长,生成越慢,噪音越多先取 5 到 8,配合 Reranker 后降到 3是否 Reranker增加耗时,但准确率提升明显对结果要求高的场景建议启用批量数batch_size 越大向量化越快,但内存占用越高从 32 开始试,内存不够就降模型选择大模型参数越大,推理越慢够用就行,不要盲目上大模型最稳妥的做法是建立一套小规模评测集,每次只改一个参数,对比召回质量和回答准确率,不要凭感觉同时改多个参数。11. 常见问题与排查方法问题现象可能原因排查方式解决方案页面或接口一直打不开启动失败、依赖缺失、端口被占用看终端报错,检查uvicorn进程修复依赖,换端口,重启服务向量化很慢文档过大、模型加载慢、CPU 推理统计文档页数和字符数,观察进程占用分批入库,加大 batch_size,用 GPU检索结果和问题无关切分不合理、Embedding 模型不适配、文档有乱码打印 TopK 召回块原文调整 chunk_size/overlap,换中文 Embedding,清理文档召回块里有答案但回答错误提示词约束不够、LLM 忽略上下文、TopK 太小打印召回块和完整提示词提示词强调只依据资料,调大 TopK,换温度更低模型回答引用来源不准元数据丢失、切分时来源信息被覆盖检查 chunk metadata入库前确保每个 chunk 带上 source 和页码多轮对话上下文丢失没有做指代消解打印当前轮检索结果加查询改写链,把历史对话压缩进当前问题API 请求超时检索慢、模型生成慢、候选块太多看服务端日志和耗时统计降低 TopK,加请求超时,Reranker 及时截断批量任务中途卡住无重试、无日志、内存堆积检查日志和任务状态文件每轮落盘,加失败重试和幂等键显存不足本地模型过大、上下文太长用nvidia-smi观察换小模型、缩短文本块、减小 max_tokens依赖安装失败网络问题、版本冲突、Python 版本不符查看报错信息换镜像源,指定兼容版本,重开虚拟环境依赖版本冲突是最常见的新手坑。强烈建议新建虚拟环境,不用系统 Python;遇到报错先看最后十几行,很多问题都是缺某个包或包版本不对。12. 最佳实践与工程化建议12.1 先把链路跑通,再谈优化第一次搭建 RAG 时不要追求完美,先用一个小文档、一套最小代码把"导入文档 -> 切分 -> 向量化 -> 检索 -> 生成 -> 接口返回"整条链路走通。链路通了,后面所有优化都有可验证的基线。建议每一步都留中间产物:原始文本、切分块、向量数量、召回块、生成结果。排错时看中间产物,比猜问题快得多。12.2 建立评测集是性价比最高的投入找业务方要 20 到 50 条真实问题,每道题标注出期望参考文档或标准答案,然后用评测集跑系统。每次调参后跑一遍评测集,记录正确率和召回命中率。不要靠人工肉眼测试两三个问题就判断系统好不好。12.3 目录、日志、鉴权一个都不能少数据、切分结果、向量库、输出结果分目录管理。批量任务务必落盘日志和结果。接口服务如果部署到服务器,必须加鉴权,配置访问白名单,把服务限制在内网。12.4 合规红线要前置RAG 处理的是真实业务数据,上传文档前要确认版权和授权;涉及用户隐私、人脸、声音、合同敏感信息的,必须脱敏和权限管控;对外提供问答服务前,要对答案质量做人工抽样复核,避免错误信息传播。总结与下一步RAG 最值得尝试的点是它的可落地性:不需要训练模型,只需要一份文档、一个向量库、一个 LLM 调用,就能搭出带出处、可更新的知识库问答系统。最先应该验证的功能是端到端链路里的"文档入库 + 单轮问答 + 来源回显"三项。最容易踩的坑有三个:PDF 解析质量差、中文切分不按标点、召回结果不对就直接调模型。前两个在数据预处理阶段就能发现,第三个要先看召回块再决定是改切分还是加 Reranker。后续可以继续扩展的方向包括:把 Chroma 换成 Milvus 或 pgvector 应对更大数据量;引入多路召回加 RRF 融合提升召回率;用 Agent 实现多轮规划和多库联查;加上在线评测集做持续回归。对入门来说,先把这一套跑通,你就已经掌握了 RAG 的核心骨架,后面的优化都是在骨架上加器官。建议把这篇教程的运行步骤保存下来,按章节逐步实现,遇到问题回看第 11 章的排查表,大部分场景都能找到对应解法。
先给结论:RAG 是目前搭建知识库问答最稳的基线方案。RAG 的全称是 Retrieval-Augmented Generation,检索增强生成。它解决的问题很直接:大模型不知道你的私有资料、内部文档、产品手册和业务数据,那就先把这些资料切块、向量化、存进向量库;用户提问时,先从库里检索最相关的文本片段,再把片段和问题一起交给大模型,让模型基于给定材料回答。这样既能回答训练数据之外的内容,又能给出引用出处,方便人工核对。这篇教程是一条完整的 RAG 入门到实战路线,覆盖文档加载、文本切分、向量化、向量库检索、召回优化、重排序、问答链路搭建、接口服务和批量任务。看完你可以照着跑通一套最小可用的知识库问答系统,并且知道检索结果不准时应该往哪个方向调。适合刚开始接触 RAG、想从零搭一套本地知识库问答的新手,也适合已经跑通流程但召回质量不理想、想系统做优化的开发同学。没有 GPU 能不能学 RAG?可以。如果你用 API 方式调用大模型和 Embedding 模型,普通笔记本就能跑通全链路;如果你只想在本地跑,8GB 内存的机器也能完成文档入库和向量检索,只是在本地加载模型做生成时会慢一些。下面开始。1. RAG 核心能力速览能力项说明技术架构检索增强生成(Retrieval-Augmented Generation)核心链路文档加载 -> 文本切分 -> 向量化 -> 向量库检索 -> 重排序 -> LLM 生成主流实现框架LangChain、LlamaIndex、Haystack、RagFlow、FastGPT、Dify 等常用向量库Chroma、FAISS、Milvus、Qdrant、pgvector、Elasticsearch硬件门槛纯 API 调用时普通笔记本即可;本地 Embedding 建议 8GB 以上内存;本地 LLM 推理按模型规格另行评估接口能力框架普遍支持 REST API,可接入业务系统批量任务支持批量文档入库、批量问答,建议配合队列和日志适合场景企业知识库问答、产品文档助手、私有资料检索、客服辅助、合规审核等这张表里没有给固定显存数字,原因是 RAG 的资源占用高度依赖你选用的模型:纯向量检索阶段内存占用不高;本地跑 Embedding 模型会占内存;本地跑 7B、13B 参数的大语言模型才会明显吃显存。更稳妥的判断是:先跑通 API 版本,再决定要不要上本地模型。2. RAG 原理拆解:知识库问答完整链路RAG 可以拆成三个大阶段:索引阶段、检索阶段、生成阶段。理解这三个阶段,后面调参时就不会乱。2.1 索引阶段:把资料变成可检索的文本块索引阶段的目标是把一份 PDF、Word、Markdown 或纯文本文档,变成一组带向量表示的、可被快速检索的文本块。流程是:文档加载:读取原始文件。PDF 要抽文本,Word 要解析段落,网页要抓正文,这一步处理不好后面全是噪音。文本切分:按章节、段落、句子或固定长度把文档切成小块,同时保留一定重叠。切分直接影响召回质量。向量化:用 Embedding 模型把每个文本块编码成一个稠密向量。语义相近的文本,向量距离更近。向量库写入:把文本块内容和向量一起存进向量数据库,同时记录来源、页码、章节等元数据。切分为什么要重叠?因为一个完整语义可能横跨两个文本块边界。不重叠,检索时容易漏掉半边信息;重叠一部分,主题信息在两块里都保留一份,召回更稳。2.2 检索阶段:把问题变成向量,取回最相关内容用户输入问题后,系统做三件事:问题向量化:用和文档入库时同一个 Embedding 模型,把用户问题编码成向量。相似度检索:在向量库里用余弦相似度或点积等方式,取出最相似的 TopK 个文本块。可选重排序:TopK 结果用 Cross-Encoder 风格的 Reranker 再精排一次,把真正相关的内容顶到前面。检索阶段是最值得调优的地方。TopK、切分大小、检索策略、Reranker 都会影响最后问答质量。后面单独用一章讲。2.3 生成阶段:把检索结果拼进提示词生成阶段做的事情是把检索到的文本块按顺序拼进系统提示词,加上用户问题,一起交给大模型生成回答。典型的提示词结构是:系统提示词:你是一个知识库问答助手。请只根据下面提供的资料回答问题,不要编造资料中没有的内容。 资料 1:<检索到的文本块> 资料 2:<检索到的文本块> 资料 3:<检索到的文本块> 用户问题:<用户输入>要求模型"只根据资料回答",是抑制幻觉的关键。还可以让模型在回答末尾列出引用的资料编号,实现可追溯。3. 适用场景与使用边界3.1 适合解决什么问题RAG 最核心的价值是解决"大模型不知道私有知识"的问题。企业知识库问答:内部制度、产品手册、售后文档,员工问一句就能定位答案。产品文档助手:把官网文档切片入库,用户遇到问题直接问,答案还带出处。私有资料检索:合同、报告、论文、工程文档的语义搜索。客服辅助:在聊天窗口里帮客服快速找到标准答案和原文位置。合规审核辅助:把规章制度向量化,审核时提问并核对原文。RAG 和微调的区别值得记住:微调是改变模型本身的参数,适合改变语气、固化少量专业知识;RAG 不改模型参数,每次回答都动态查最新资料,适合知识频繁更新、需要可追溯、需要权限隔离的场景。对大多数知识库问答需求,先上 RAG,解决不了再看微调。3.2 不适合什么场景不是所有问题都适合 RAG。高频实时数据:股价、库存、订单状态,这类数据应该直接查数据库或调用业务接口,而不是走文档检索。全局统计类问题:"这个季度销售额是多少",如果知识库里只有非结构化的日报文本,RAG 查不准。强多跳推理:问题需要把十几个文档的信息串起来推理,单轮检索很难覆盖,需要引入 Agent 和规划能力。需要绝对精确计算的场景:RAG 的答案是生成式的,适合"找到并概括原文",不适合当计算器。3.3 使用边界与合规提醒RAG 系统处理的一定是真实业务数据,部署和使用时要注意:确认知识库内容有合法来源和授权,不要上传未经许可的文档、合同、个人信息和隐私数据。涉及人脸、声音、身份信息的数据要先脱敏。接口服务要限制访问范围,不要裸奔在公网,建议加鉴权。对外发布或商用之前,要对检索结果和回答内容做人工抽样复核。4. 环境准备与前置条件4.1 硬件与系统从低到高有几种跑法:跑法建议配置说明纯 API 跑法4GB 内存即可,无 GPU 也能跑Embedding 和 LLM 全部走远程 API,本机只做切分、向量库和检索本地 Embedding + API LLM8GB 以上内存需要在本地加载一个 Embedding 模型,建议有 CPU 即可,但首次向量化会偏慢全本地跑法16GB 以上内存,最好有一张 8GB 以上显存的 GPU本地跑 Embedding 加本地大模型,要求最高,适合数据不能出内网的场景更稳妥的启动路径是:先跑纯 API 版本,确认全链路能通,再逐步替换成本地组件。这样能把"链路问题"和"资源问题"分开排查。4.2 Python 环境RAG 生态以 Python 为主,建议准备:Python 3.9 及以上版本。独立的虚拟环境,避免和已有项目冲突。pip 依赖管理。推荐的目录结构:rag_project/ ├── data/ # 原始文档 │ └── product_manual.pdf ├── chunks/ # 切分后的中间结果,方便排查 ├── vector_db/ # 向量库持久化目录 ├── scripts/ # 入库、检索、服务脚本 ├── logs/ # 日志 └── .env # API Key 等环境变量前端展示不是必须的,先做服务端链路。日志目录和中间结果目录一定要保留,RAG 排错全靠它们。5. 最小可用系统搭建:LangChain + Chroma 部署 RAG 服务这一节我们用 LangChain 配合 Chroma 搭最小可用系统。LangChain 是当前使用最广的编排框架,Chroma 是本地嵌入式向量库,适合学习和原型验证,不用单独启动服务端。5.1 安装依赖# 创建虚拟环境 python -m venv rag_env # Windows 激活 rag_env\Scripts\activate # macOS / Linux 激活 source rag_env/bin/activate # 安装依赖,版本按实际环境选择 pip install langchain langchain-community langchain-openai chromadb faiss-cpu pypdf sentence-transformers fastapi uvicorn python-dotenv如果网络受限,可以只装核心依赖,逐个补。这里不用固定版本号,因为 LangChain 迭代较快,接口名在不同版本之间会有差异,安装时以官方文档为准。5.2 环境变量# .env 文件 OPENAI_API_KEY=sk-your-key-here如果你不用 OpenAI,而是用其他兼容接口的模型服务,把 base URL 和模型名替换成对应服务的地址即可。Key 不要写进代码,用环境变量管理。5.3 启动前检查# 确认 Python 版本 python --version # 确认依赖已经安装 pip show langchain chromadb首次运行建议用一个 10 页以内的小文档测试,不要上来就跑 500 页的大目录。小文档能快速暴露切分、向量化、检索链路里的问题。6. 文档入库实践:加载、切分、向量化6.1 加载文档先用 PyPDFLoader 加载 PDF,用 TextLoader 加载 TXT 和 Markdown。import os from dotenv import load_dotenv load_dotenv() from langchain_community.document_loaders import PyPDFLoader, TextLoader, DirectoryLoader # 加载单个 PDF pdf_loader = PyPDFLoader("data/product_manual.pdf") documents = pdf_loader.load() print(f"PDF 加载完成,共 {len(documents)} 页文档对象") # 按目录加载文本文件 text_loader = DirectoryLoader( "data", glob="**/*.txt", loader_cls=TextLoader, loader_kwargs={"encoding": "utf-8"} ) text_docs = text_loader.load()加载后建议先打印前几页文本,看看有没有解析乱码、表格丢行、图片文字缺失。PDF 解析是最容易出问题的一环,乱码文本喂进去,后面的检索和生成都受影响。6.2 文本切分切分参数有两个要重点调:chunk_size控制每个文本块的长度,chunk_overlap控制相邻块之间的重叠长度。from langchain_text_splitters import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=80, separators=["\n\n", "\n", "。", "!", "?", ".", "!", "?", ";", ";"], length_function=len, ) chunks = text_splitter.split_documents(documents) print(f"切分完成,共 {len(chunks)} 个文本块") # 把切分结果落盘,方便之后排查召回 import json with open("chunks/chunks_preview.json", "w", encoding="utf-8") as f: json.dump( [{"content": chunk.page_content, "metadata": chunk.metadata} for chunk in chunks[:20]], f, ensure_ascii=False, indent=2 )separators里加入中文句号和感叹号非常重要。很多默认切分器只识别英文标点,遇到长中文文档会把整段话切碎或切得语义混乱。切完先人工看一眼预览文件,确认每个块语义基本完整。6.3 向量化与向量库写入向量化可以选远程 API 模型,也可以选本地开源中文模型。这里给出两种方式。远程 Embedding 方式:from langchain_openai import OpenAIEmbeddings embeddings = OpenAIEmbeddings(model="text-embedding-3-small")本地中文模型方式:from langchain_community.embeddings import HuggingFaceEmbeddings # BGE 系列中文向量模型,适合中文知识库 embeddings = HuggingFaceEmbeddings( model_name="BAAI/bge-large-zh-v1.5", encode_kwargs={"normalize_embeddings": True}, )写入 Chroma:from langchain_community.vectorstores import Chroma vectorstore = Chroma.from_documents( documents=chunks, embedding=embeddings, persist_directory="./vector_db" ) print("文档向量化完成,数据已写入 vector_db 目录")首次向量化时注意耗时。如果文档数量大,可以分批写入,不要一次性把几千个块塞进内存。6.4 检查入库结果# 统计向量数量 collection = vectorstore._collection print(f"向量数量:{collection.count()}") # 做一次相似度检索验证 test_query = "产品的主要功能有哪些?" docs = vectorstore.similarity_search(test_query, k=3) print("检索到 3 个相关文本块:") for doc in docs: print("-" * 40) print(doc.page_content[:200]) print(f"来源:{doc.metadata.get('source', 'unknown')}")如果检索结果和问题明显无关,先回切分参数和文档解析质量,不要急着加 Reranker。7. 检索召回优化:查询改写、混合检索与重排序很多 RAG 项目跑通后,效果不理想,问题大多出在召回阶段,比如:用户问题故意绕弯子,和知识库里原文表达不一致。知识库关键词和用户问题关键词对不上。向量检索召回的前几个块,看着相关但都不是关键段落。7.1 先确认问题出在召回还是生成一个实用的办法:直接打印召回来的源文档,不看生成结果。test_query = "退货需要什么条件?" retrieved = vectorstore.similarity_search(test_query, k=5) for i, doc in enumerate(retrieved): print(f"第 {i+1} 个召回块:{doc.page_content[:150]}")如果这 5 个块里没有正确答案,那就是召回问题,调检索策略;如果召回块里有正确答案,但回答仍不对,那就是生成或提示词组装问题。7.2 查询改写用户问题往往很口语化,直接拿原始问题去检索效果不好。可以先让大模型把问题改写成更适合检索的形式,或者在多轮对话时把指代问题补全。from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate rewrite_prompt = ChatPromptTemplate.from_messages([ ("system", "你是检索查询改写助手。请把用户问题改写成适合向量检索的简洁关键词句子,保留关键术语。"), ("human", "{query}") ]) llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) rewrite_chain = rewrite_prompt | llm rewritten = rewrite_chain.invoke({"query": "我想了解一下产品坏了能不能修,该找谁"}) print(rewritten.content)输出可能是"产品售后维修流程 联系人",而不是用户原话。这样检索命中率会明显提升。7.3 混合检索向量检索擅长语义匹配,关键词检索擅长精确匹配。工程实践里常用混合检索:向量召回和 BM25 关键词召回各取一部分,合并去重后再给 Reranker。Chroma 不带内置 BM25,可以用bm25s或rank_bm25这类轻量库做一层关键词召回,然后用 RRF(Reciprocal Rank Fusion)合并两路结果。# 概念示例:RRF 合并 # 向量检索结果和关键词检索结果都带一个排名 # rrf_score = sum(1 / (k + rank_i)),k 一般取 60 # 合并后按 rrf_score 从高到低取 TopN具体实现不复杂,重点是思路:不要把全部希望压在向量检索一条路。对于术语明确的领域,比如"合同编号""退款时效 7 天",关键词召回往往比向量召回更准。7.4 重排序 Reranker向量检索是双塔结构,问题和文档分别编码,速度快但精度有限。Reranker 用 Cross-Encoder 把问题和文档拼成一个输入,逐条打分,速度慢但更准。from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import CrossEncoderReranker from langchain_community.cross_encoders import HuggingFaceCrossEncoder # 基础召回器:先取 8 个候选 base_retriever = vectorstore.as_retriever( search_type="similarity", search_kwargs={"k": 8} ) # 重排序模型,用 BGE 的中文 Reranker reranker = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-base") compression_retriever = ContextualCompressionRetriever( base_compressor=CrossEncoderReranker(model=reranker, top_n=3), base_retriever=base_retriever ) # 验证重排序后的结果 top_docs = compression_retriever.invoke("退货需要什么条件?") print(f"重排序后保留 {len(top_docs)} 个块")Reranker 的典型用法是"先召回多、再精排少":向量检索召回 8 到 10 个候选,Reranker 精排后只保留 3 个。这样可以兼顾效率和准确率。7.5 元数据过滤与切分参数调整如果知识库里有明确的类型、部门、年份、产品线等元数据,可以在检索时加过滤条件,缩小范围。# 示例:只检索指定产品线的文档 retriever = vectorstore.as_retriever( search_kwargs={ "k": 5, "filter": {"product": "企业版"} } )如果过滤之后效果仍不好,回到切分层调整。常见思路:召回块太碎:调大chunk_size到 800 或 1000。召回块太杂:调小chunk_size到 300 到 400,让边界更清晰。语义跨块丢失:增加chunk_overlap。表格类文档:考虑按表格结构单独切分,不要让大表格被硬切成多块。8. 功能测试与效果验证RAG 系统上线前要有一套可重复的测试用例,不能只看一两个问答的效果。建议准备 20 到 50 条真实业务问题,标注每道题的期望答案来自哪份文档的哪一节。测试项测试输入预期结果判断标准文档入库10 页测试 PDF向量数量大于文本页数,文本无乱码打印前 3 个块的全文单轮问答"退款需要什么条件?"答案包含退款时效和申请入口答案内容能对应到召回块原文多轮指代"那要是货没收到呢?"系统把"那"指代到退货场景召回块里包含退货相关内容术语匹配"Z100 报错怎么解决"找到 Z100 相关章节召回的前 3 个块包含 Z100 关键词长文本处理5000 字长文档单章提问从正确章节召回,而不是截到无关段落召回块来源正确批量问答10 条问题批量跑全部有返回,超时的重试成功日志记录时间与失败次数功能测试建议写成一个 Python 脚本,输入是 JSON 文件里的问题列表,输出是一个评测报告。# test_rag.py 简化示例 import json import time questions = [ {"query": "退款需要什么条件?", "expected_source": "product_manual.pdf"}, {"query": "Z100 报错怎么解决?", "expected_source": "faq.pdf"}, ] for item in questions: start = time.time() result = qa_chain.invoke({"query": item["query"]}) cost = round(time.time() - start, 2) print(f"问题:{item['query']}") print(f"回答:{result['result'][:200]}") print(f"耗时:{cost}s") print("-" * 40)判断标准不是"回答看起来像那么回事",而是:回答内容是否有召回块支撑,来源是否和人工预期一致,同一问题多次回答是否稳定。这三个标准能过滤掉大部分不达标的 RAG 系统。9. 接口 API 与批量任务设计9.1 用 FastAPI 暴露问答接口系统跑通后,下一步是给前端或业务系统提供接口。用 FastAPI 写一层很轻。# api.py import os from dotenv import load_dotenv load_dotenv() from fastapi import FastAPI, HTTPException from pydantic import BaseModel from langchain_openai import ChatOpenAI from langchain_community.vectorstores import Chroma from langchain_community.embeddings import HuggingFaceEmbeddings from langchain.chains import RetrievalQA app = FastAPI(title="RAG 知识库问答服务") # Embedding 必须与入库时保持一致 embeddings = HuggingFaceEmbeddings( model_name="BAAI/bge-large-zh-v1.5", encode_kwargs={"normalize_embeddings": True}, ) vectorstore = Chroma( persist_directory="./vector_db", embedding_function=embeddings, ) retriever = vectorstore.as_retriever(search_kwargs={"k": 5}) llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.2) qa_chain = RetrievalQA.from_chain_type( llm=llm, chain_type="stuff", retriever=retriever, return_source_documents=True, ) class QueryRequest(BaseModel): query: str top_k: int = 5 @app.post("/api/rag/query") async def query_rag(request: QueryRequest): if request.top_k < 1 or request.top_k > 20: raise HTTPException(status_code=400, detail="top_k 范围是 1 到 20") result = qa_chain.invoke({"query": request.query}) return { "answer": result["result"], "sources": [ { "content": doc.page_content, "metadata": doc.metadata, } for doc in result["source_documents"] ], }启动服务:uvicorn api:app --host 127.0.0.1 --port 8000注意端口被占用时换一个:uvicorn api:app --host 127.0.0.1 --port 80019.2 用 curl 和 Python 验证接口curl -X POST http://127.0.0.1:8000/api/rag/query \ -H "Content-Type: application/json" \ -d '{"query": "退货需要什么条件?", "top_k": 5}'import requests url = "http://127.0.0.1:8000/api/rag/query" payload = {"query": "退货需要什么条件?", "top_k": 5} response = requests.post(url, json=payload, timeout=120) if response.status_code == 200: data = response.json() print(data["answer"]) print("引用来源:") for src in data["sources"]: print("-", src["metadata"].get("source")) else: print("请求失败:", response.status_code, response.text)接口层要控制的三个问题:超时时间、来源字段是否完整、返回结果是否稳定。接口返回的sources非常重要,前端可以展示"回答依据来自哪个文档",这是 RAG 相对纯大模型回答的核心优势。9.3 批量任务与失败重试批量问答不要在单线程里裸跑。建议把问题列表先落盘,逐条调用接口,记录状态,失败重试,最后生成汇总报告。# batch_qa.py import json import time import requests questions = [ "退款需要什么条件?", "Z100 报错怎么解决?", "如何联系客服?", ] results = [] for i, q in enumerate(questions, start=1): for attempt in range(3): try: resp = requests.post( "http://127.0.0.1:8000/api/rag/query", json={"query": q}, timeout=60, ) resp.raise_for_status() data = resp.json() results.append({ "query": q, "answer": data["answer"], "sources": [s["metadata"].get("source") for s in data["sources"]], "status": "success", }) print(f"[{i}/{len(questions)}] 完成,共 {len(data['sources'])} 个引用") break except Exception as e: print(f"[{i}/{len(questions)}] 第 {attempt+1} 次失败:{e}") time.sleep(2 ** attempt) else: results.append({"query": q, "status": "failed"}) with open("logs/batch_result.json", "w", encoding="utf-8") as f: json.dump(results, f, ensure_ascii=False, indent=2) print(f"批量完成,成功 {sum(1 for r in results if r['status'] == 'success')} 条")批量任务的核心原则:过程要有日志,失败要有重试,结果要有落盘。不要用 print 代替日志,建议写成结构化 JSON 或 CSV 文件。10. 资源占用与性能观察RAG 的资源观察要分阶段看。10.1 向量化阶段向量化阶段主要消费 CPU 和内存。用本地 BGE 模型做向量化时,几百页文档也要跑一段时间。观察方法:# 监控内存占用 htop # 或者看 Python 进程占用 ps aux | grep python降低向量化耗时的方法:用 GPU 版 sentence-transformers,模型自动跑 GPU。分批向量化,避免一次性加载全部文本。设置batch_size,例如 32 或 64。首次向量化后把向量库持久化,后续增量入库。10.2 检索阶段向量检索在内存中计算相似度,纯检索阶段内存占用较低。如果向量数量很大,建议升级到 FAISS 或 Milvus,Chroma 在本地原型阶段够用,但数据量到几十万条以上时,性能和持久化都要重新评估。10.3 生成阶段生成阶段的资源消耗取决于 LLM 在哪里跑:API 方式:本机几乎无压力,瓶颈在接口响应时间和限流。本地 GPU 方式:显存占用取决于模型大小和上下文长度。显存占用会随检索块数量、文本块长度和 max_tokens 增加而上升。实际占用要以本机测试为准,不要直接照搬网上数值。观察显存可以用:nvidia-smi -l 110.4 影响性能的主要参数参数影响调优方向chunk_size越大单块信息越全,越小检索越精准但越碎300 到 1000 之间按文档类型调chunk_overlap越大上下文越连贯,但向量库体积变大通常取 chunk_size 的 10% 到 20%TopK越大上下文越长,生成越慢,噪音越多先取 5 到 8,配合 Reranker 后降到 3是否 Reranker增加耗时,但准确率提升明显对结果要求高的场景建议启用批量数batch_size 越大向量化越快,但内存占用越高从 32 开始试,内存不够就降模型选择大模型参数越大,推理越慢够用就行,不要盲目上大模型最稳妥的做法是建立一套小规模评测集,每次只改一个参数,对比召回质量和回答准确率,不要凭感觉同时改多个参数。11. 常见问题与排查方法问题现象可能原因排查方式解决方案页面或接口一直打不开启动失败、依赖缺失、端口被占用看终端报错,检查uvicorn进程修复依赖,换端口,重启服务向量化很慢文档过大、模型加载慢、CPU 推理统计文档页数和字符数,观察进程占用分批入库,加大 batch_size,用 GPU检索结果和问题无关切分不合理、Embedding 模型不适配、文档有乱码打印 TopK 召回块原文调整 chunk_size/overlap,换中文 Embedding,清理文档召回块里有答案但回答错误提示词约束不够、LLM 忽略上下文、TopK 太小打印召回块和完整提示词提示词强调只依据资料,调大 TopK,换温度更低模型回答引用来源不准元数据丢失、切分时来源信息被覆盖检查 chunk metadata入库前确保每个 chunk 带上 source 和页码多轮对话上下文丢失没有做指代消解打印当前轮检索结果加查询改写链,把历史对话压缩进当前问题API 请求超时检索慢、模型生成慢、候选块太多看服务端日志和耗时统计降低 TopK,加请求超时,Reranker 及时截断批量任务中途卡住无重试、无日志、内存堆积检查日志和任务状态文件每轮落盘,加失败重试和幂等键显存不足本地模型过大、上下文太长用nvidia-smi观察换小模型、缩短文本块、减小 max_tokens依赖安装失败网络问题、版本冲突、Python 版本不符查看报错信息换镜像源,指定兼容版本,重开虚拟环境依赖版本冲突是最常见的新手坑。强烈建议新建虚拟环境,不用系统 Python;遇到报错先看最后十几行,很多问题都是缺某个包或包版本不对。12. 最佳实践与工程化建议12.1 先把链路跑通,再谈优化第一次搭建 RAG 时不要追求完美,先用一个小文档、一套最小代码把"导入文档 -> 切分 -> 向量化 -> 检索 -> 生成 -> 接口返回"整条链路走通。链路通了,后面所有优化都有可验证的基线。建议每一步都留中间产物:原始文本、切分块、向量数量、召回块、生成结果。排错时看中间产物,比猜问题快得多。12.2 建立评测集是性价比最高的投入找业务方要 20 到 50 条真实问题,每道题标注出期望参考文档或标准答案,然后用评测集跑系统。每次调参后跑一遍评测集,记录正确率和召回命中率。不要靠人工肉眼测试两三个问题就判断系统好不好。12.3 目录、日志、鉴权一个都不能少数据、切分结果、向量库、输出结果分目录管理。批量任务务必落盘日志和结果。接口服务如果部署到服务器,必须加鉴权,配置访问白名单,把服务限制在内网。12.4 合规红线要前置RAG 处理的是真实业务数据,上传文档前要确认版权和授权;涉及用户隐私、人脸、声音、合同敏感信息的,必须脱敏和权限管控;对外提供问答服务前,要对答案质量做人工抽样复核,避免错误信息传播。总结与下一步RAG 最值得尝试的点是它的可落地性:不需要训练模型,只需要一份文档、一个向量库、一个 LLM 调用,就能搭出带出处、可更新的知识库问答系统。最先应该验证的功能是端到端链路里的"文档入库 + 单轮问答 + 来源回显"三项。最容易踩的坑有三个:PDF 解析质量差、中文切分不按标点、召回结果不对就直接调模型。前两个在数据预处理阶段就能发现,第三个要先看召回块再决定是改切分还是加 Reranker。后续可以继续扩展的方向包括:把 Chroma 换成 Milvus 或 pgvector 应对更大数据量;引入多路召回加 RRF 融合提升召回率;用 Agent 实现多轮规划和多库联查;加上在线评测集做持续回归。对入门来说,先把这一套跑通,你就已经掌握了 RAG 的核心骨架,后面的优化都是在骨架上加器官。建议把这篇教程的运行步骤保存下来,按章节逐步实现,遇到问题回看第 11 章的排查表,大部分场景都能找到对应解法。