RAG 原理不难,难的是”检索结果不准”——我踩过的那些坑
★半年前我接了一个内部知识库的需求要求很简单「把我们的文档喂给 AI让它能回答用户的问题」。我当时觉得这不就是 RAG 嘛两天搞定。结果我花了整整三周才让它勉强能用。这篇文章记录的就是这三周里我被折磨出来的经验。一、先把原理讲透再谈为什么会出问题很多教程上来就贴代码但如果你不理解每一步在做什么代码跑通了你也不知道结果为什么不对更不知道从哪里下手优化。RAG 的本质是三件事找到相关内容、把内容给 AI、让 AI 基于内容回答。用更具体的语言描述① 建库阶段离线 把文档切成小段Chunk → 每段文字转成一串数字向量 / Embedding → 存进向量数据库② 检索阶段在线 用户提问 → 问题也转成向量 → 在数据库里找数字最接近的那些段落 → 取出 Top-K 段原文③ 生成阶段在线 把检索到的段落 用户问题拼成 Prompt → 送给 LLM 生成最终回答听起来很直观对吧问题就藏在每一个箭头里。二、坑一Chunk 切得像乱刀斩乱麻最开始怎么写的刚开始图省事直接按固定字数切500 字一刀干净利落def naive_chunk(text, chunk_size500): return [text[i:ichunk_size] for i in range(0, len(text), chunk_size)]发生了什么有个文档写的是★“……综上所述该方案存在三点主要风险。第一资金链断裂风险第二供应商集中风险第三……”切完之后第一段最后是「第三」第二段开头是「合规风险具体表现为……」用户问「有哪些风险」检索到第一段AI 告诉用户「只有两点风险」。为什么会这样固定字数切割完全不看内容语义只认字数。一个完整的知识点很可能被切成两半检索时只能拿到半截LLM 当然给出残缺的答案。怎么解决改成递归语义切割优先按段落、句子等自然边界切同时加上重叠窗口让相邻 Chunk 之间共享一段内容防止上下文在边界处断裂from langchain.text_splitter import RecursiveCharacterTextSplittersplitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap80, # 相邻 Chunk 重叠 80 字保住边界上下文 separators[ \n\n, # 优先按空行段落切 \n, # 其次按换行切 。, , , # 再按句子结尾切 , # 最后才按字切 ])chunks splitter.split_text(text)经验值chunk_overlap设成chunk_size的 15%~20% 比较稳。重叠太小上下文断裂重叠太大引入冗余干扰检索排名。三、坑二Embedding 模型选错中文像在说火星话最开始怎么写的从一篇英文教程直接复制过来的代码用的是text-embedding-ada-002from openai import OpenAIclient OpenAI()def embed(text: str) - list[float]: return client.embeddings.create( inputtext, modeltext-embedding-ada-002 ).data[0].embedding发生了什么用户问「如何申请年假」文档里明明有一整节「年假申请流程」检索结果里没有它反而出来一堆不相关的段落。代码没有任何报错跑得很顺但结果就是不对。这是最隐蔽的一类坑。为什么会这样ada-002以英文语料为主训练对中文语义理解相当有限。它不太能理解「申请年假」和「年假申请流程」在语义上高度相关。怎么解决中文文档必须换中文友好的 Embedding 模型模型特点推荐场景BAAI/bge-m3开源、中文效果最强优先考虑本地或云端均可text-embedding-3-largeOpenAI 新版多语言大幅提升不想本地部署、预算充足moka-ai/m3e-base轻量中文够用内存资源紧张时换成bge-m3之后「年假」那个例子直接从检索不到变成排名第一from sentence_transformers import SentenceTransformer# 第一次运行会自动下载模型约 2GBmodel SentenceTransformer(BAAI/bge-m3)def embed(texts: list[str]) - list[list[float]]: # normalize_embeddingsTrue 后可直接用点积计算余弦相似度 return model.encode(texts, normalize_embeddingsTrue).tolist()Embedding 模型是 RAG 效果的地基地基没打好后面再怎么优化都是在沙滩上盖楼。换模型这件事越早做越好。四、坑三向量检索不认数字和专有名词发生了什么财务同事问「2024 年第三季度的净利润是多少」文档里这个数字明明白白写着但检索出来的是一堆「公司财务状况概述」和「利润分配原则」就是没有那个具体的季度数据。为什么会这样向量检索的原理是语义相似度它擅长理解「意思差不多」的表达但不擅长精确匹配。「2024 年第三季度净利润」这种查询语义层面和很多财务类文本都沾边但「2024」「第三季度」这些精确信息反而被语义的海洋淹没了。这个问题有个名字叫词汇鸿沟Lexical Gap。怎么解决混合检索Hybrid Search向量检索负责语义BM25 关键词检索负责精确匹配两个结果加权融合from rank_bm25 import BM25Okapiimport numpy as npclass HybridRetriever: def __init__(self, chunks: list[str], embedder): self.chunks chunks self.embedder embedder self.bm25 BM25Okapi([c.split() for c in chunks]) self.vectors np.array(embedder(chunks)) def retrieve(self, query: str, top_k: int 8, alpha: float 0.5): alpha: 向量检索权重(1 - alpha) 为 BM25 权重 alpha 越大 → 越偏语义alpha 越小 → 越偏精确匹配 q_vec np.array(self.embedder([query])[0]) vec_scores self.vectors q_vec bm25_scores np.array(self.bm25.get_scores(query.split())) def normalize(arr): mn, mx arr.min(), arr.max() return (arr - mn) / (mx - mn 1e-9) combined alpha * normalize(vec_scores) (1 - alpha) * normalize(bm25_scores) top_idx combined.argsort()[::-1][:top_k] return [self.chunks[i] for i in top_idx]调参建议对话问答类场景alpha0.6偏语义财务报告、技术文档等精确查询多的场景alpha0.3让 BM25 更强势不确定时从0.5起步跑几条测试用例再调五、坑四Top-K 设太大LLM 被噪音淹没发生了什么为了「保险」我把top_k设成 10把 10 段内容全塞进 Prompt。然后 LLM 开始把不相关的内容混进答案把某段「行业背景介绍」当成依据给出了完全错误的回答还给得理直气壮。为什么会这样LLM 不是搜索引擎它不会「忽略」不相关的内容而是尝试用所有给它的内容来生成答案。这个现象有个研究名字叫Lost in the MiddleLLM 倾向于重点关注 Prompt 开头和结尾的内容中间的内容容易被混淆。塞的内容越多相关信号被稀释越严重。怎么解决在检索和生成之间加一层Rerank重排序先粗检索一批候选再用专门的重排序模型精准打分只保留真正相关的 Top-Nfrom sentence_transformers import CrossEncoderreranker CrossEncoder(BAAI/bge-reranker-v2-m3)def rerank(query: str, candidates: list[str], top_n: int 3) - list[str]: # CrossEncoder 对每个 (query, candidate) 对单独打分 # 比向量相似度更精准但速度更慢所以只用于精排阶段 scores reranker.predict([(query, c) for c in candidates]) ranked sorted(zip(scores, candidates), keylambda x: x[0], reverseTrue) return [doc for _, doc in ranked[:top_n]]# 完整流程粗检索 → 精排raw_candidates retriever.retrieve(query, top_k10) # 粗检索 10 个final_context rerank(query, raw_candidates, top_n3) # 精排保留 3 个这是性价比最高的单点优化接入成本低效果立竿见影。粗检索 8~12、精排保留 3~5这个区间大多数场景下都稳。六、坑五用户提问太模糊检索完全跑偏发生了什么测试阶段大家都在问很完整的问题上线之后真实用户是这样问的「上次说的那个报销流程怎么弄」「之前那个问题解决了吗」「还有呢」这种问题里根本没有检索锚点向量数据库不知道「上次」「之前」指的是什么只能返回莫名其妙的结果。为什么会这样RAG 的检索是无状态的每次检索只看当前这条 Query完全不知道之前聊了什么。怎么解决Query 改写检索之前先用 LLM 把用户的模糊问题结合对话历史改写成一个完整、独立的检索 Queryfrom openai import OpenAIclient OpenAI()def rewrite_query(history: list[dict], user_query: str) - str: # 只取最近 4 轮太长引入干扰 recent history[-4:] history_text \n.join(f{m[role]}: {m[content]}for m in recent) prompt f根据以下对话历史将用户的最新问题改写为一个完整、独立的搜索查询。只输出改写后的查询不超过 30 字不要任何解释。对话历史{history_text}用户最新问题{user_query}改写后的查询 resp client.chat.completions.create( modelgpt-4o-mini, # 改写任务用 mini 就够省钱 messages[{role: user, content: prompt}], temperature0 # 改写不需要创造力temperature0 保证稳定输出 ) return resp.choices[0].message.content.strip()# 实际效果示例# 用户说上次说的那个报销流程怎么弄# 改写后差旅费报销流程及所需提交材料 ← 这才能检索到东西这步成本极低每次改写消耗 Token 不超过 200换来多轮对话场景下检索质量的大幅提升强烈建议无脑加上。七、坑六文档更新了知识库还活在过去发生了什么某个功能的操作流程改了新文档上传了但向量数据库里还是旧版本。用户问新功能怎么用AI 给出旧答案还给得理直气壮。怎么解决给每个 Chunk 打上元数据用内容哈希做变更检测设计增量更新机制import hashlibdef upsert_document(doc_id: str, text: str, meta dict, vector_store): # 计算内容哈希内容没变就跳过节省 Embedding 费用 content_hash hashlib.md5(text.encode()).hexdigest() existing vector_store.get_metadata(doc_id) if existing and existing.get(content_hash) content_hash: print(f[跳过] {doc_id} 内容未变化) return # 删除该文档的所有旧 Chunk vector_store.delete(filter{doc_id: doc_id}) # 重新切分、向量化、插入 chunks splitter.split_text(text) vectors embed(chunks) records [ { id: f{doc_id}_chunk_{i}, vector: vectors[i], text: chunk, metadata: { **metadata, doc_id: doc_id, chunk_index: i, content_hash: content_hash } } for i, chunk in enumerate(chunks) ] vector_store.upsert(records) print(f[完成] {doc_id} 更新共 {len(chunks)} 个 Chunk)建议在 CI/CD 流程里挂一个自动同步脚本文档仓库有变更就触发更新彻底解决知识库过期的问题。八、把这些坑串起来看完整的流程经历了上面这些之后整个 RAG 流程大概长这样【建库阶段】 文档 → 递归语义切分chunk_size500, overlap80 → bge-m3 向量化 → 存入向量数据库带 doc_id、内容哈希等元数据 → 文档变更时自动增量更新【查询阶段】 用户输入 → Query 改写结合近 4 轮对话历史gpt-4o-mini → 混合检索向量 BM25top_k10 → Rerank 精排bge-reranker-v2-m3保留 top_3 → 拼入 Prompt → LLM 生成回答不需要一次性全上先从最痛的那个坑开始解决每解决一个效果就会有一次明显跃升。九、最后说几句真心话RAG 这个方向入门门槛很低。LangChain、LlamaIndex 封装得很完善三十行代码就能跑起来一个 demo然后你会觉得「这也太简单了」。但这正是最大的陷阱所在。**Demo 跑通不等于生产可用。**真正的难点有三个**一是评估体系。**你怎么知道 RAG 效果是在变好还是变坏不能只靠「感觉」得有指标检索召回率、答案忠实度、RAGAS 评分得有 benchmark 数据集得能量化比较每次优化前后的差异。这件事很多人懒得做然后调参全靠玄学。**二是数据质量。**垃圾进垃圾出。文档里大量重复内容、排版混乱的 PDF、表格被提取成乱码——再好的检索策略也救不了一份烂文档预处理阶段该花的时间一点都省不了。**三是对业务的理解。**用户最常问什么他们的问法有什么规律哪类问题答错了代价最大这些问题的答案直接影响你 Chunk 策略、检索权重、兜底逻辑的设计。技术是工具业务才是靶心。我现在对 RAG 的判断是**这是一个工程细节决定成败的方向不是一个算法壁垒很高的方向。**把本文这几个坑都填完再搭好评估体系80 分以上的 RAG 系统大多数团队都能做到。如果你也在做 RAG欢迎评论区聊聊你踩过的坑——尤其是那种「代码没报错、结果就是不对」的情况说不定比我的更离谱。学AI大模型的正确顺序千万不要搞错了2026年AI风口已来各行各业的AI渗透肉眼可见超多公司要么转型做AI相关产品要么高薪挖AI技术人才机遇直接摆在眼前有往AI方向发展或者本身有后端编程基础的朋友直接冲AI大模型应用开发转岗超合适就算暂时不打算转岗了解大模型、RAG、Prompt、Agent这些热门概念能上手做简单项目也绝对是求职加分王给大家整理了超全最新的AI大模型应用开发学习清单和资料手把手帮你快速入门学习路线:✅大模型基础认知—大模型核心原理、发展历程、主流模型GPT、文心一言等特点解析✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑✅开发基础能力—Python进阶、API接口调用、大模型开发框架LangChain等实操✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经以上6大模块看似清晰好上手实则每个部分都有扎实的核心内容需要吃透我把大模型的学习全流程已经整理好了抓住AI时代风口轻松解锁职业新可能希望大家都能把握机遇实现薪资/职业跃迁这份完整版的大模型 AI 学习资料已经上传CSDN朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】