RAG 系统从搭建到优化:我踩过的 5 个坑,每一个都让我重新写代码

发布时间:2026/7/6 3:00:06
RAG 系统从搭建到优化:我踩过的 5 个坑,每一个都让我重新写代码 TL;DR搭建一个「能跑」的 RAG 系统只需要 50 行代码。但让它「好用」——检索精准、回答稳定、不编造——我花了 3 个月踩了 5 个大坑。这篇文章是踩坑实录也是避坑指南。背景我为什么要搭 RAG 系统公司要做企业知识库问答系统。需求很简单把内部文档PDF、Word、Wiki喂进去员工问问题系统回答。第一版我只用了一周用 LangChain 加载文档用 OpenAI 的 text-embedding-3-small 生成向量用 FAISS 存向量用户提问时检索 Top-3拼进 Prompt让 GPT-4 回答上线第一天就翻车了。老板问了一个很简单的问题「公司的报销流程是什么」系统回答「请提交纸质申请表给财务部门。」但实际上公司已经全面线上化了根本不需要纸质表。文档里写得清清楚楚但系统就是答错了。排查后发现不是模型的问题是检索的问题。检索根本没找到那段文字。这是第一个坑。后面还有 4 个。坑 1向量数据库选型错误坑点描述第一版用 FAISS本地跑得飞快。但文档量从 1 万篇涨到 50 万篇后检索延迟从 200ms 飙到 3 秒。而且 FAISS 不支持增量更新每次新增文档都要重建整个索引。我当时的选择逻辑很简单FAISS 是 Facebook 开源的应该很成熟本地部署不花钱LangChain 官方文档里有示例代码照抄就行但我忽略了一个关键问题FAISS 是为「静态数据集」设计的。它的索引构建是一次性的构建后不支持动态插入。每次新增文档你都得重新构建整个索引。对于企业知识库这种「每天都在新增文档」的场景FAISS 完全不适用。解决方案换成Milvus / Qdrant / Pinecone支持增量更新的向量数据库。我最终选了 Qdrant开源、轻量、支持 Docker 部署docker-compose.ymlversion: 3 services: qdrant: image: qdrant/qdrant:latest ports: - 6333:6333 volumes: - ./qdrant_storage:/qdrant/storage增量插入的代码Python - 增量插入向量from qdrant_client import QdrantClient from qdrant_client.models import Distance, VectorParams, PointStruct client QdrantClient(urlhttp://localhost:6333) # 创建 collection只需执行一次 client.create_collection( collection_namecompany_docs, vectors_configVectorParams(size1536, distanceDistance.COSINE) ) # 增量插入新文档 points [ PointStruct( iddoc_001, vectorembedding_vector, payload{text: 文档内容, source: wiki} ) ] client.upsert(collection_namecompany_docs, pointspoints)向量数据库适用场景增量更新部署复杂度FAISS静态数据集、研究实验❌ 不支持低Qdrant动态数据、中小规模✅ 支持中Milvus大规模生产环境✅ 支持高Pinecone托管服务、不想运维✅ 支持零SaaS坑 2文档切片策略不对坑点描述一开始我按固定字数切分每段 500 字重叠 50 字。结果把很多完整的语义单元切断了。比如一个「报销流程」的步骤被切成两段检索时只找到后半段回答就缺了关键信息。举个真实例子原文原始文档片段报销流程 1. 登录 OA 系统进入「财务审批」模块 2. 填写报销单上传发票扫描件必须是 PDF 格式 3. 提交给直属领导审批 4. 审批通过后财务会在 3 个工作日内打款到工资卡按固定 500 字切分后这段话被切成了两段片段 1包含步骤 1-2片段 2包含步骤 3-4用户问「发票格式要求是什么」检索到了片段 1但片段 1 里只说了「上传发票扫描件」没说格式。答案是「PDF 格式」——在片段 2 里。这就是固定字数切分的致命问题它不考虑语义边界。解决方案按语义单元切分——段落、章节、或者用模型判断切分点。LangChain 提供了几种切分策略Python - 语义切分from langchain.text_splitter import RecursiveCharacterTextSplitter # 按「段落 → 句子 → 字数」的优先级切分 splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, separators[\n\n, \n, 。, , ] ) chunks splitter.split_text(document)更好的方案是用Semantic Chunker基于句子相似度判断切分点但计算成本更高。❌ 错误做法按固定字数切分不考虑语义边界✅ 正确做法按段落/章节切分或用 RecursiveCharacterTextSplitter 按分隔符优先级切分坑 3检索 Top-K 设太小坑点描述一开始我只检索 Top-3觉得「最相关的 3 条信息应该够了吧」。实测发现完全不够。用户问复杂问题时答案可能分散在 5-10 个文档片段里Top-3 只能覆盖一部分。举个例子用户问「新员工入职需要办哪些手续」答案涉及人事合同签署在人事制度文档工牌办理在行政流程文档电脑领用在 IT 资产管理文档账号开通在信息安全文档这 4 个信息分布在 4 个不同的文档片段里。如果只检索 Top-3至少漏掉 1 个。我做过测试Top-K检索召回率Token 消耗回答完整性362%低经常缺信息578%中大部分完整1091%高基本完整2096%很高完整但噪声多解决方案Top-K 设 10但要做重排序Rerank把真正相关的片段提到前面。Rerank 的原理先用向量检索召回 Top-20快但不够精准用 Cross-Encoder 模型对 20 个候选片段重新打分慢但精准取重排序后的 Top-10 喂给 LLMPython - Rerank 示例from sentence_transformers import CrossEncoder # 加载 Rerank 模型 reranker CrossEncoder(cross-encoder/ms-marco-MiniLM-L-6-v2) # 候选片段从向量检索得到的 Top-20 candidates [片段1, 片段2, ..., 片段20] # 用 Cross-Encoder 重新打分 scores reranker.predict([(query, doc) for doc in candidates]) # 按分数排序取 Top-10 ranked sorted(zip(candidates, scores), keylambda x: x[1], reverseTrue)[:10]坑 4没有做重排序Rerank这个坑和坑 3 直接相关。增大 Top-K 可以提高召回率但会引入噪声。坑点描述向量检索只看「语义相似度」不看「问题相关性」。比如用户问「如何报销」检索结果里可能有「报销流程」相关和「报销被拒的案例」不相关但语义相似。如果直接把这些喂给 LLM它会混淆。Rerank 的作用过滤噪声把真正相关的片段提到前面。实际测试数据方法准确率延迟仅向量检索 Top-1068%150ms向量检索 Rerank89%280ms延迟增加了 130ms但准确率提升了 21 个百分点。值。⚠️ 注意Rerank 模型本身有计算成本。如果候选片段太多比如 Top-50Rerank 会很慢。推荐做法向量检索 Top-20 → Rerank 取 Top-10。坑 5缺少回答验证机制坑点描述LLM 会编造答案。即使检索到的文档里没有相关信息它也可能「一本正经地胡说八道」。我遇到过用户问「公司的竞争对手是谁」系统回答了 3 家公司但实际上文档里只提到了 1 家另外 2 家是模型编的。这是 RAG 系统最致命的问题检索不到时模型不会说「不知道」而是编答案。解决方案三种方法叠加使用方法 1Prompt 约束System Prompt你是一个企业知识库助手。 规则 1. 只根据提供的文档内容回答问题 2. 如果文档中没有相关信息回答「抱歉知识库中没有相关信息」 3. 不要编造或推断答案 4. 回答时标注信息来源文档名称方法 2置信度阈值让模型输出置信度分数低于阈值就拒绝回答Python - 置信度检查import openai response openai.chat.completions.create( modelgpt-4, messages[ {role: system, content: system_prompt}, {role: user, content: user_query} ], temperature0, logprobsTrue # 返回 token 的概率 ) # 计算平均置信度 avg_logprob sum(response.choices[0].logprobs.content) / len(response.choices[0].logprobs.content) confidence math.exp(avg_logprob) if confidence 0.7: return 抱歉我对这个问题的回答不够确信建议咨询人工客服。方法 3引用来源要求模型在回答中标注引用的文档片段 ID回答示例根据公司报销制度文档ID: doc_001 报销流程如下 1. 登录 OA 系统进入「财务审批」模块 2. 填写报销单上传发票扫描件PDF 格式 3. 提交给直属领导审批 4. 审批通过后财务会在 3 个工作日内打款 来源[doc_001, doc_003]用户看到来源标注至少能判断答案是否可信。总结RAG 优化的 5 个关键点坑点问题解决方案向量数据库选型错误FAISS 不支持增量更新换 Qdrant / Milvus / Pinecone文档切片策略不对固定字数切断语义按段落/章节切分或用 RecursiveCharacterTextSplitter检索 Top-K 太小复杂问题答案分散Top-K 设 10-20配合 Rerank没有做重排序向量检索不精准用 Cross-Encoder Rerank缺少回答验证模型编造答案Prompt 约束 置信度阈值 引用来源搭建 RAG 系统的门槛很低LangChain OpenAI 50 行代码就能跑起来。但让它「好用」——检索精准、回答稳定、不编造——需要在这些细节上反复打磨。如果只记住一条向量检索只是第一步Rerank 和回答验证才是区分「能跑」和「好用」的关键。如果对你有帮助欢迎在评论区聊聊你踩过的 RAG 坑。