1. 项目全景与核心误区
1.1 RAG到底解决什么问题,为什么第一版容易烂尾
我做的第一个RAG项目是公司内部知识库问答。当时团队预期拉得很高,觉得只要把文档扔给大模型就能得到一个AI助手。结果第一版上线,回答一塌糊涂:要么答非所问,要么回答的内容跟资料完全不沾边,最要命的是引用[1]指向的往往是一篇无关文档。后来我复盘才总结出一条最值钱的经验——RAG的绝大多数问题不是大模型不行,而是检索链路和数据处理没设计到位。
用一句话解释RAG是什么:它是给大模型配一个可以临时翻阅的资料库,让模型在回答之前先检索相关片段,再结合这些片段组织答案。大模型本身的知识更新成本高、响应时间固化在训练数据里,而企业内部知识库大部分属于私有数据,模型没见过,重训又贵又不现实。RAG等于“开卷考试”,把相关资料塞进上下文里,模型直接根据资料作答。它能解决三个核心痛点:知识更新难、私有数据接入难、回答无法追溯出处。
RAG落地的完整链路通常拆成五段:文档加载、文本切分、向量化、存储与检索、生成答案。很多人以为重点在大模型那头,其实前三段决定上限。我见过不少团队把精力全花在调提示词上,希望靠几句诸如“请你根据资料回答”的口令救回一个已经丢失了正确答案的检索结果。检索如果没有把需要的片段捞回来,后面生成环节再怎么努力都是白搭。
还有一个很容易被忽视的问题:文档切得不对,会导致知识碎片化。一段原本完整的操作流程,被硬生生切成两半,另一半没被召回,答案自然残缺。后面我会单独讲分块策略,这块调整空间非常大。
1.2 落地前需要拆解的项目阶段
现在回头看,我会把一个RAG项目拆成七个阶段,每个阶段都有独立的验收标准:
- 需求盘点:确认问答范围、语料来源、更新频率、评估场景。这一步往往被跳过,但它决定了后面所有选型。
- 数据准备:清洗、格式化、去重、OCR、表格提取。常见文档在PDF转文本时会带出大量噪声。
- 分块与向量化:确定块大小、重叠窗口、批量嵌入方式。
- 存储与检索:选向量库、建索引、设置元数据、确定召回数量和重排方式。
- 生成:设计提示词模板、引用格式、安全边界。
- 评估:离线测试集加线上抽样反馈,这是整个项目的“体检报告”。
- 上线运维:增量更新、日志监控、成本控制。
团队在早期最容易犯的错,是文档一获取就直接embedding、直接扔给LLM,中间省略掉清洗和评估两个关键环节。总想“先跑出一个demo再说”,结果demo跑出来的效果很差,后面再回头补数据工程,工作量反而是先期做好的一倍以上。我的建议很明确:先用二三十条真实问题做最小可行性验证,再铺开全量数据,不要一上来就追求“全量索引、全量知识库”。
2. 选型阶段:嵌入模型、向量库与LLM怎么搭
2.1 嵌入模型选型:不要听别人说,要自己测一套
选嵌入模型,几乎是RAG项目里最容易“迷信玄学”的环节。网上经常有人说某模型“中文效果最好”,但那大概率是他在自己业务语料上测出来的结论,换到你的领域未必成立。我的经验是:自己准备一批业务问题,跑一次最小实验,看检索命中。
我先把自己实际测过的几个模型列出来:
| 嵌入模型 | 向量维度 | 部署方式 | 中文表现 | 备注 |
|---|---|---|---|---|
| openai/text-embedding-3-small | 1536 | API | 均衡,但隐私敏感场景受限 | 需要网络与计费 |
| bge-large-zh-v1.5 | 1024 | 本地/API | 中文场景稳定 | 开源,生态成熟 |
| bge-m3 | 1024 | 本地/API | 支持多语言,检索能力强 | 体积较大 |
| m3e-small/large | 512/768 | 本地 | 轻量,适合原型 | 更新节奏偏慢 |
| 通义text-embedding-v3 | 1024/1536 | API | 中文场景稳,长文档分段友好 | 适合阿里云体系 |
如果你只是搭一个本地个人知识库,考虑到隐私和零成本,本地部署bge系列是主流选择。现在很多工具里已经集成了Ollama拉起bge模型的能力,实现“ollama加简易本地RAG知识库”的效果,确实零基础可复制。但这里有个非常重要的坑:索引时用的嵌入模型和查询时用的嵌入模型必须完全一致。如果不一致,即使维度相同,向量空间也不一样,检索结果会和随机召回差不多。
选嵌入模型不要光看榜单分数,要看三个约束:语种覆盖、最大输入长度、部署条件。中文文档中很多专业术语,比如“焊点”、“良率”、“背板”,常用模型可能处理得都不错,但真正决定效果的是你的特定领域session是否在训练数据里。所以我自己在测试时会准备20-30条业务Query,对每个候选模型跑一遍检索,算一下“正确答案出现在前十条的比例”,这个指标我习惯称作RAG hit rate,后面会展开说。
2.2 向量数据库:从十行代码原型到大规模集群
向量数据库的选择,取决于你的数据规模、团队运维能力和上线时间线。我整理一个横向对照,方便不同规模的项目对号入座:
| 向量库 | 特点 | 适合场景 | 主要代价 |
|---|---|---|---|
| Chroma | 轻量、开箱即用 | 原型验证、小规模知识库 | 数据量变大后性能一般 |
| FAISS | 高性能底层库,无服务化 | 离线索引、单机批量检索 | 需要自己封装服务 |
| Qdrant | Rust实现,自带过滤和集群 | 中型生产、元数据过滤频繁 | 需要了解API和部署 |
| Milvus / Zilliz Cloud | 分布式,生态完整 | 大规模生产、多租户 | 运维成本偏高 |
| pgvector | PostgreSQL扩展 | 已有PG技术栈的团队 | 高并发向量查询略弱 |
我的建议很简单:原型阶段用Chroma或FAISS,正式上线再迁到Qdrant或Milvus,避免在没验证效果之前就背上一个重运维组件。很多企业团队本来就有PostgreSQL在跑,直接装pgvector能省掉一个环节,适合业务量不大、查询并发不高的场景。
顺带回答一个经常被问到的点:RAG知识库能存储图片吗?能。多数向量库本身不解析图片,但你可以用CLIP类模型把图片内容变成向量,和OCR提取的文字向量一起存进去。比如图文混排的FAQ,图片向量和文字向量可以关联存放,检索时把图片作为附加材料返回给用户,实际效果很不错。文件本身放在对象存储或本机路径,向量库里只存标识和向量。
2.3 编排框架:LangChain不是银弹
关于RAG流程的编排框架,LangChain、LlamaIndex、自写管道以及Java生态的LangChain4j,基本覆盖了市面上主流选择。我的态度是:快速搭建用LangChain,正式项目要在关键节点替换成自己的代码。
LangChain的优势是模块化,文档加载、切分、向量化、检索、生成都有现成组件,尤其是“基于LangChain的RAG流程”这类教程满天飞,入门很顺。但问题也出在抽象层:很多环节的默认参数你根本看不到。比如它默认的分块方式、默认的top_k值、默认的prompt模板,可能都会在你不知情的情况下影响效果。我自己遇到过LangChain版本升级后,部分组件接口调整,原来跑通的流水线直接报错。
如果你是Java团队,LangChain4j也能跑easy rag,它的中文生态这几年越来越成熟,适合企业内部系统集成。如果你只是做一个小工具,我建议直接手写一遍流程:读文档、切块、向量化、检索、拼prompt、调LLM。全流程半小时就能串起来,而且每一环你都清楚发生了什么,后面排查问题会非常顺手。框架解决的是工程效率问题,而RAG项目的核心矛盾永远是检索质量和数据质量,框架替代不了这两个。
3. 数据准备与文档切分:工程量最大的环节
3.1 文档清洗:先把脏数据挡在门外
很多RAG项目的“坏结果”,其实在输入文档那一刻就注定了。文档清洗是RAG落地过程中最不性感但最值得投入的一步。
首先是PDF解析。文字型PDF可以直接提取,但扫描件就是图片,必须走OCR,否则提取出来全是乱码。即便文字型PDF,格式复杂的也要注意:多栏排版、表格、脚注,提取出来经常乱序。其次是表格,含合并单元格的复杂表格默认解析器经常拆得七零八落,最好用专门的表格抽取工具,或者干脆把表格截图存成图片、转成向量。第三是页眉页脚干扰,目录、重复标题、页码这些信息混进正文片段后,检索时会产生严重噪声。第四是重复文档,同一份材料被上传了好几个版本,不去重的话,相似片段囤积在索引里,检索结果会被冗余内容淹没。
在这个环节我问过很多团队一个问题:你们有没有本地的文本拆解工具?其实不需要买专门产品,很多底层能力可以自己搭。比如用jieba做中文分词、用spaCy做实体识别和句子切分、用正则表达式按章节标题拆分,这些都是本地完成的,不涉及外部API。关键是清洗逻辑要符合你的文档特征,而不是照搬一套通用流程。
我在实操里会用一份清洗清单:去目录、去页眉页脚、去空行、去乱码字符、统一编码、把标题层级记录下来、把文档来源和更新时间记录进元数据。这块做完,后面的分块和检索会轻松很多。
3.2 分块策略:参数不是魔法,是数学
文本切分,可能是RAG项目里最容易被轻视又最影响效果的一环。切得太小,语义信息不完整;切得太大,块内塞进太多无关信息,向量表示被平均稀释,检索就不准。
常见的分块策略我整理了一张表:
| 策略 | 常见参数 | 适用场景 | 注意事项 |
|---|---|---|---|
| 固定长度切分 | chunk_size=200-500 token | 简单文本 | 容易切断句子,需配合重叠 |
| 递归字符切分 | chunk_size=500,overlap=50 | 大多数字档 | 按段落优先,实现简单 |
| 语义切分 | 按句子embedding相似度合并 | 长文档、主题切换明显 | 计算成本高,但效果扎实 |
| 结构化切分 | 按Markdown/HTML标题层级 | 有结构的文档 | 防止切掉章节主题 |
我自己的经验值是:token数量控制在500左右,重叠窗口设50到100,然后按业务类型微调。如果文档本身结构很强,比如操作手册、规章制度,优先用结构化切分,按标题层级做块,每个章节尽量保持完整。如果是一堆杂散的说明文字,递归字符切分更稳健。
这里有个重要提示:分块大小不是越小越好。你切得小,召回的内容可能只有半个操作步骤,大模型虽然很能“脑补”,但它没法补出你没给它的下半句。反过来,切得太大,一个块里塞了多个主题,检索到的块跟问题只是“沾边”,大模型要从里面找出答案也需要费很大劲。实验时建议至少对比三种size:256、512、1024,用hit rate看差别。
分块之后还有一环容易被忽略:块与块的连接关系。比如一个块属于第3章第2节,我建议在元数据里记录它的章节路径、上一块和下一块的ID,这样后续需要扩展上下文时,可以把相邻块一起拉出来,有效缓解知识割裂问题。
3.3 元数据设计:把检索从“大海捞针”变成“导航”
元数据是RAG知识库中最被低估的设计。所谓元数据,就是贴在每个文档片段上的标签,比如来源文件名、所属部门、章节标题、文档版本、更新时间、作者等。它的价值在于:检索时先做过滤,再做向量相似度排序。
举个例子,企业中一个知识库可能同时包含HR制度、产品手册、研发规范。如果你不做元数据过滤,用户问“年假怎么休”时,向量库给出的Top10很可能混入产品手册里描述产品“年假备份策略”的片段,结果自然不对。但如果存数据时给每个片段打上dept=hr的标签,检索时先限定dept=hr,再算向量相似度,干扰项会少一大半。
元数据设计要在建库之前想好,不要等数据灌进去之后再补。最常见的元数据字段就这几个:文档ID、块序号、章节标题、来源文件、创建时间、更新时间、所属分类、权限级别。如果你的知识库涉及敏感信息,权限过滤必须在检索这层做掉,不能把过滤全部甩给生成环节去“自觉”。
4. 向量化、索引与检索链路:把命中率做上去
4.1 向量索引参数与写入策略
embedding完成之后,第二步就是建索引。大部分向量库默认用HNSW算法,核心参数是M和ef_construction。M指的是每个节点的最大连接数,越大召回越高,但内存和构建时间也增加;ef_construction影响构建时的搜索范围,越大索引质量越高,但构建更慢。我个人常用M=16、ef_construction=100作为起步,如果召回不够再往上调。
以Qdrant为例,插入片段时我会同时带上元数据和文本内容,方便后面回源查看。代码大致是这样:
from qdrant_client import QdrantClient from qdrant_client.models import VectorParams, Distance, PointStruct client = QdrantClient(host="localhost", port=6333) client.create_collection( collection_name="knowledge_base", vectors_config=VectorParams(size=1024, distance=Distance.COSINE), ) client.upsert( collection_name="knowledge_base", points=[ PointStruct( id=i, vector=embedding_vector, # 来自嵌入模型 payload={ "text": chunk_text, "source": "员工手册_v3.pdf", "dept": "hr", "chapter": "休假制度", "updated_at": "2025-06-01" } ) for i, (chunk_text, embedding_vector) in enumerate(chunks_with_vectors) ] )这里有两个容易踩的细节。第一,批量写入时要控制batch size,一次性塞太多会导致内存暴涨,建议每批256到512条。第二,一定要把模型版本或模型名称写进payload,方便后面模型升级时排查、重建索引,不然旧向量和新向量混在一起,检索效果会像一锅粥。
关于“向量库能存几张图”这类问题,其实取决于你的库容量,而不取决于它是不是RAG知识库。向量库本身不关心你存的是文本还是图片向量,它只存数字数组。真正常见瓶颈在向量维度、数据条数和索引类型:比如768维、50万条片段,原始float32向量体量大约是1.5GB,HNSW索引通常还要再占2到3倍空间,部署前先按这个公式估算内存。
4.2 混合检索与重排:单靠向量远远不够
纯向量检索有个天然短板:它擅长找语义相似的内容,却不擅长精确关键词匹配。比如用户问“Windows 10激活失败”,文档里写的是“Win10系统无法激活”,向量检索可能关联得上,但如果用户输入的是精确型号或故障代码,比如“错误码0x800F0922”,向量召回经常不如你用关键词搜文档来得准。
所以生产环境我强烈建议做混合检索:一路走向量相似度,一路走BM25关键词匹配,各路召回TopN之后合并去重,再用重排模型精排,最后取Top5交给大模型生成。
用生活化类比来解释,向量检索像是“意思差不多的同学”,BM25是“字面上完全对得上的同学”。两者各有利弊,混合后才能覆盖更全。重排模型则相当于一个“二次面试官”,它把候选片段和问题同时输入,逐对判断相关性,能从几十条粗召回里把最相关的几条挑出来。bge-reranker或cross-encoder之类的模型都可以做这件事,实测对hit rate提升非常明显。
如果你的场景对成本敏感,不想引入重型重排模型,也可以先用简单的分数加权融合,比如向量相似度分数0.6、BM25分数0.4,做一个加权排序,也能改善不少。但要注意分数分布不同,最好在融合前做一次归一化,否则两个分数量纲不一致,加权没有意义。
4.3 命中率测出来,才谈得上优化
所有检索策略的调整,都应该围绕一个数字来做,那就是RAG hit rate。定义也很简单:在所有测试查询中,正确答案所对应的文档片段出现在召回TopN里的比例。hit@5,就是看前五条召回里有没有正确答案所在的块。
我会建议每个RAG项目都建一份离线评测集。做法是:从真实用户日志里抽取50到100条问题,然后手工标注每条问题对应的答案应该出现在哪个文档片段。这步虽然费力,但它是整个项目的“操纵杆”。评测集建好后,每次改动分块参数、换嵌入模型、加元数据过滤、加混合检索,都跑一遍评测集,看hit率是升是降。
我举个真实的优化轨迹供参考:
| 优化动作 | hit@5 | 备注 |
|---|---|---|
| 初始全库向量检索 | 68% | 看起来还行,但答案参杂许多无关片段 |
| 增加元数据过滤(部门+文档类型) | 82% | 干扰项明显减少 |
| 加入BM25混合召回 | 88% | 精确关键词命中率上升 |
| 加入重排模型后取Top5 | 92% | 答案正确率和引用合理性同时改善 |
评测集还应该配合“答案质量评估”看,比如回答是否忠实于资料、引用是否存在张冠李戴。但注意,不要期望所有业务问题都能靠RAG解决,有些问题需要多轮对话或多跳检索,后面我会单独说。
5. 生成环节:提示词、引用与安全边界
5.1 提示词模板:资料来源优先,严禁编造
生成环节的提示词设计,目标只有一个:让模型严格基于检索到的资料答话。模板可以长这样:
system: 你是企业知识助手。请严格基于给定的“参考资料”回答用户问题。 如果参考资料没有提供相关信息,请直接回复“资料中未找到相关信息”,不要编造。 回答中请用[1]、[2]标注信息来源编号,编号对应你看到的参考资料序号。 user: 参考资料: [1] 员工手册_v3.pdf:休假制度 [2] 员工手册_v3.pdf:考勤管理 (……) 问题:休年假需要提前几天申请?这里有两个要点。第一,把“不知道就说不知道”写死在系统提示词里,比把“答案置信度阈值”调到0.7更管用。第二,要求模型标注引用编号,后面审核答案时能快速定位是哪个片段支撑了这句话。如果引用指向的片段本身是错的,你也能顺着编号去做数据修正。
很多人喜欢在提示词里写“请结合上下文回答”“请用自己的知识做补充”,这种话在安全场景里反而是毒药。我的经验是:宁可让模型说不知道,也不要让它自由发挥。RAG的价值就在于事实可控性,一旦允许自由发挥,RAG就退化成了一个普通聊天机器人。
5.2 知识割裂与多跳检索:一个问题散落在多个文档里怎么办
很多问题不存在于单一文档片段里。比如“新版本上线后出现故障,谁负责排查”,答案可能需要从项目文档里找上线时间,再从运维文档里找负责人。单轮向量检索往往只能命中其中一块,这就是常说的知识碎片化。
最快的解法是“周边扩展检索”:命中某一块之后,把紧挨着它的相邻几块也带出来,一起交给大模型。很多情况下,相关上下文就在周边。但更彻底的办法是查询改写:把原问题拆成多个子查询,比如先查“项目上线时间”,再查“故障负责人”,每次查完把结果拼接,再做最终生成,这是Agentic RAG的雏形。
再往上走,就是GraphRAG、本体RAG这些思路。GraphRAG先把文档实体和关系抽出来建知识图谱,遇到全局性问题时先从图谱定位实体关系,再回到文本检索。本体RAG则借助领域知识模型,把“故障”“负责人”“部门”这些概念的关系预先定义好,跨文档推理能力更强。这些方案的代价是链路更长、构建成本高,适合企业级长期知识库。现阶段如果你的查询大多是一跳问题,先把混合检索和多跳改写做好,不用急着上图谱。
5.3 引用溯源与安全边界
生成环节的最后一道防线,是引用和权限。引用要做到两点:一是答案里的每个关键结论都能对应到具体的文档片段编号;二是用户点击编号后能回看原始文档原文。工程上实现不难,在生成前把检索到的片段编号作为参数传给大模型,生成时强制引导它引用编号即可。
权限安全更要注意。前面提过,过滤权限最好在检索阶段做,不要在生成阶段再指望模型“懂规矩”。比如用户没有权限看薪酬文档,检索时就必须把他的权限维度拼进过滤条件里,让这部分文档根本不进入候选集。这样既能控制越权风险,也能降低上下文噪声,一举两得。
在我实际参与的多个项目里,引用溯源系统一旦上线,用户的信任感反而会提升。因为每句话都有出处,即使答案不够完美,用户也能自己翻原文确认,这正是RAG相比普通聊天机器人最核心的体验优势。
6. 踩坑清单与长期调优
6.1 十个典型坑,每一个都花钱花时间
我把实际项目里最容易踩的坑整理成清单,按出现频率排个序。
- 索引和查询用了不同的embedding模型,影子问题的方向最隐蔽。新建向量库时,如果同事重装环境时顺手换了模型,旧索引没重建,结果整个检索全废。解决方式:每次创建索引都记录model_version,检索时校验一致性。
- 分块大小“一刀切”。同一批文档既有长规章制度又有短Q&A,用统一大小全切,效果自然参差。解决方式:按文档类型配置不同切分策略。
- 没有元数据过滤,全库盲搜。用户问“Python后端岗位JD”,结果召回一堆前端JD,因为向量相似度泛化太严重。加上岗位类型过滤,干扰会大幅减少。
- 上下文塞得太满,产生“中间遗忘”。TopK拉回十条大段落,全塞进提示词,结果大模型对中间位置的内容关注度下降,该用的内容没用上。解决方式:重排后只取Top5,或者把最相关的内容放在上下文开头。
- 增量更新没设计。旧文档修改后,旧向量还留在库里,新老版本内容互相打架。解决方式:按版本号或文档ID做失效处理,更新时先删旧再立新。
- 检索失败被误判为生成失败。用户说“答得不对”,你以为是提示词问题,其实是某个片段压根没被召回。所以要建日志,把每次检索命中的片段记录在案。
- 没有评测集就上线,改动之后没有回归验证。加了一个过滤条件,命中率可能反而降了,你没发现。
- 成本无监控。embedding调用按量计费,文档一多费用直接失控。解决方式:本地批量embedding代替逐条API调用,并加缓存。
- 忽视权限过滤。多租户知识库没做数据隔离,用户A检索到了用户B的文档,这是严重的合规风险。
- 格式清洗不彻底。OCR识别错别字、编码混乱、繁体简体混杂,这些噪声进了索引,相似度计算会被拉偏。
6.2 可观测性建设与成本控制
RAG项目的调试,最怕的就是“黑盒”。我强烈建议上线第一天就加上日志体系。每条请求至少记录:query原文、检索命中了哪些片段、最终答案是什么、答案引用了哪些片段、延迟和成本。
技术选型上,用LangSmith、Langfuse这类工具可以可视化追踪整条链路,捕获各级输入输出。如果不想引入额外平台,也可以直接在代码里落JSON日志:
import json log_entry = { "conversation_id": "abc123", "query": "休年假需要提前几天申请?", "recalled_chunk_ids": [17, 23, 41], "hit": True, "final_answer": "根据员工手册,年假需提前3天申请。", "citations": [17], "latency_ms": 820, "cost_usd": 0.00031, } with open("rag_log.jsonl", "a", encoding="utf-8") as f: f.write(json.dumps(log_entry, ensure_ascii=False) + "\n")这套日志最大的价值在于:当你发现某一类问题总是答错时,能立刻判断是召回环节丢了正确答案,还是生成环节没有利用好答案。定位效率完全不一样。
成本控制方面,思路通常是三块:一是批量embedding,不要在服务端每来一条请求就调用一次embedding API,本地提前算好所有向量。二是加缓存,同一个问题带同样上下文,直接返回缓存结果,能省大量重复的LLM调用费用。三是对历史问题做聚类,先检索历史相似回答,命中就让大模型只做改写而不是重新生成。实践下来,一套3000篇文档的知识库,通过这些方式能把单月成本压到原来的三分之一左右。
6.3 持续优化路径:从标准RAG走向更高级的形态
等项目跑稳了,评测集也有了,再考虑下一步演进。我的建议路线:标准RAG先跑通,再按需引入Agentic RAG、GraphRAG、本体RAG这些进阶方案。
标准RAG适合单跳问答,即“一个问题对应一段资料”。当问题需要跨多个片段联合推理时,先做查询改写和多跳检索。具体实现就是让LLM把“上线后故障谁负责”改写成“上线时间是什么”和“故障负责人是谁”两个子查询,分头检索,合并结果后统一生成,这就是Agentic RAG的雏形。再复杂一些的,可以让Agent自主决定什么时候继续检索、什么时候停止并回答。
如果是“这个文档体系里有哪些常见问题类型”这种全局概览型问题,用GraphRAG效果更好。GraphRAG会先抽取文档中的实体、事件、关系,构建成知识图谱,再基于图谱做社区检测和摘要,回答整体性和关联性问题明显更有底气。不过它的离线构建成本比标准RAG高一个数量级,不是每个项目都值得上。
我的观点是,新技术可以做技术储备和试点,但不要为了“新”而新。每引入一层复杂度,你就得多维护一条链路。先把你当前的hit率和答案质量稳定下来,再去扩展新场景,这才是RAG项目长期存活的关键。
最后再分享一点个人感受:RAG这个方向没有魔法,有的只是数据、检索、生成三个环节里的逐项打磨。每次我怀疑模型能力不行时,回查日志,最后基本都是数据没洗干净、检索没召回、上下文没拼对这三个原因之一。你只要把坑一个个填掉,系统会一步步变好用,这种持续的正反馈,是做RAG项目最大的乐趣所在。