RAG 知识库问答系统,在 AI 应用开发里已经算是一个“入门必做”的项目了。但最近在帮一个团队排查他们的内部知识库问答系统时,我遇到了一个很典型的翻车现场:几十份产品文档已经做了向量化,接上大模型后,回答看起来逻辑通顺,信息却是错的。用户问“退款周期是几个工作日”,模型一本正经地给出了老版本手册里的数字,而新规则就在知识库里。团队一开始以为是模型不够聪明,换了更大的模型,问题依然存在。最后定位下来,问题出在检索链路——排在最前面的片段根本不是用户问的那一条。
这件事让我觉得,RAG 项目的复杂度往往不是“接入大模型”,而是“让系统知道该用哪段知识”。RAG 的上限,基本由检索质量决定,大模型只是最后一步的阅读器。下面我会把从 0 到 1 做 RAG 应用时真正需要关注的环节拆开来讲,重点放在原理和高级检索实战两个方向。
1. 先理解 RAG 解决什么:不是“更聪明的模型”,而是“可控的知识来源”
1.1 大模型的两个老问题:幻觉和知识边界
大模型本身有知识截止日期,也有幻觉倾向。对公开常识、通用技术问题,它往往能给出不错答案;可一旦涉及企业内部文档、产品手册、私有知识库、未公开的运营规则,模型根本没有见过这些内容,硬答就很容易编造。
RAG 的思路不是让模型记住更多知识,而是把知识放在外部,在生成前先检索出相关片段,再让模型基于这些片段回答。这样做有两层直接好处:
- 答案是“有引用来源”的,至少在系统设计上可以追溯到知识库里的具体文档。
- 知识更新不需要重新训练模型,替换或新增文档后,索引更新即可生效。
对比微调,RAG 更适合知识频繁变化、合规要求高、查询范围需要受控的场景。微调更适合让模型学习某种稳定的语言风格、输出格式或专业术语,但它不该用来硬塞事实性知识,因为事实会变化,重新训练成本也太高。
1.2 RAG 三阶段:索引、检索、生成
RAG 全链路可以拆成三个阶段:
- 索引阶段:把文档加载进来,完成解析、清洗、切块、向量化、建索引。
- 检索阶段:接收用户问题,生成查询,召回候选片段,做重排。
- 生成阶段:把问题和候选片段拼进提示词,调用大模型生成答案。
很多教程会把重点放在“生成”和提示词设计上,但实际项目里,问题更多出在前两个阶段。比如解析不完整导致答案分叉,切块不当导致上下文断裂,检索召回不准确导致模型根本没看到正确答案。这些问题都不是靠换模型能解决的。
还需要注意:RAG 不是万能的。如果问题不依赖外部知识,比如“Python 的列表推导式怎么写”,模型本身已经会了,强行套 RAG 反而增加延迟和故障点。做技术选型时,先判断业务问题到底需不需要外部知识支撑,再决定要不要上 RAG。
判断标准很简单:如果这个问题的答案会随时间变化、会因组织而异、或者必须来自某个私有文档集,才值得用 RAG。
2. 索引阶段:切块和解析才是召回质量的上限
2.1 从文档到文本:解析这一步最容易被跳过去
文档解析在演示项目里往往被忽略,因为加载一个 Markdown 文件太简单了。但真实业务里的文件类型五花八门:PDF、Word、Markdown、PPT、扫描件、HTML、日志导出。每一种解析器都有自己的脾气:
- PDF 多栏布局如果不做版面分析,文本顺序会被搅乱。
- 表格转成纯文本后,表头和单元格的关系容易丢。
- Word 里的批注、修订记录如果不剔除,会被当成正文。
- Markdown 里的代码块、流程图、嵌套列表,如果按普通文本切分,语义会断。
更麻烦的是扫描件。扫描版 PDF 本身只是图片,不做 OCR 就根本没有文本层。如果输入材料没有提供现成的解析方案,落地前最好先拿一小批真实文档试跑,把解析结果打印出来,逐行检查有没有乱序、缺字、多余噪音。
这一步投入的时间,决定了后续所有环节的上限。
2.2 切块策略:块大小、重叠窗口和结构信息
切块是决定召回质量的关键开关。块切得太小,单个片段缺乏上下文,模型很难理解;块切得太大,向量表示会被平均化,相关性被稀释,噪声也变多。
常见的切块方式有几种:
- 固定长度切块:按字符或 token 数硬切,实现简单,但容易切断句子和代码块。
- 按分隔符切块:按换行、句号、空行切,比硬切自然一些。
- 按文档结构切块:按标题、段落、列表项切,能保留语义边界,是目前比较推荐的做法。
- 递归切块:先按大结构切,再对超长块二次切分,兼顾结构和长度。
不管用哪种方式,一般都要设置重叠窗口。比如块大小 500、重叠 50,意思是相邻两个块之间保留 50 字符的重复内容。这样做是为了避免一句话恰好被边界截断。
不同文档类型需要的策略不一样。FAQ 适合一问一答独立成块;产品手册适合按章节切;表格类内容最好让每行保留表头信息;代码类内容要保留代码块的完整性。所谓“最佳参数”,应该由你的文档形态和真实问题共同决定。
下面是一个很简单的按分隔符切块示意,真实项目里可以在这个基础上扩展:
def split_text_simple(text, chunk_size=500, overlap=50): paragraphs = [p.strip() for p in text.split("\n") if p.strip()] chunks = [] current = "" for para in paragraphs: if len(current) + len(para) > chunk_size and current: chunks.append(current) current = current[-overlap:] + para else: current += para + "\n" if current: chunks.append(current) return chunks这段代码只是为了展示思维模型。实际工程中建议先跑 20 条真实问题,分别用不同 chunk_size 测试,看召回内容是否足够回答问题。不要一开始就追求最复杂的切分方案,先把简单版本跑通,再根据失败样例做迭代。
2.3 向量化与存储:别丢掉关键词和元数据
切好块之后,要选择嵌入模型做向量化。如果文档是中文,需要验证模型对中文语义的理解能力。不同嵌入模型对中文的支持差异比较大。使用 API 时还要注意向量维度、每千条 token 的成本、并发限制;使用开源模型时则要考虑显存和推理速度。
向量数据库的选择范围也很广。轻量验证时可以用本地文件或轻量方案,生产环境再评估 Qdrant、Milvus、pgvector、Elasticsearch 等。这里没有绝对最优,关键看团队熟悉程度、部署形态和运维成本。
有一个容易被忽略的细节:不要把全部精力压在向量检索上。很多 RAG 项目最终都要做混合检索,也就是向量召回加关键词召回。关键词召回依赖的倒排索引,需要在索引阶段同步建好。如果你在索引阶段就把原始文本扔了,后面想补关键词召回会很痛苦。
元数据也一样。来源文档、章节标题、更新时间、业务标签、权限范围,这些字段要一起写入索引。比如前面提到的退款版本问题,完全可以在检索阶段先按“版本日期”过滤一遍,从根上避免旧版本抢占前排。
记住:向量不是万能的,关键词和元数据是补丁,更是安全网。
3. 高级检索实战:从“向量召回”升级到“多路召回 + 重排”
3.1 为什么纯向量检索经常让人失望
纯向量检索的逻辑是:把问题转成向量,在向量空间里找最接近的片段。听起来很优雅,实际使用中却有几类非常典型的失效场景:
- 精确匹配弱:产品型号、ID、数字、法律条款,向量召回可能因为语义相近而召回“看起来像但不完全对”的内容。
- 用户问法和文档表述差异大:用户说“你们多久发货”,文档里写的是“48 小时内出库”。向量可能觉得相关,但不够稳。
- 相似度阈值难调:阈值设太高,召回为空;设太低,噪声一堆。完全依赖 top_k 也不可靠,因为相关片段可能排在十名之外。
所以,高级检索的第一步,是承认“单路向量检索”不够用。
3.2 查询改写:让问题更接近文档的表述
用户问题往往不适合直接用于检索。太短、口语化、带指代词,比如“它的退款政策呢”这种问题,直接拿去向量检索效果很差。
一条实用路径是:让大模型先做查询改写,把指代还原、补充上下文、拆解复合问题。下面是一个很常见的提示词框架:
你是一个检索查询改写助手。 用户的原始问题是:{question} 请生成 3 个适合检索的改写版本。要求: 1. 保持原意,不要编造事实。 2. 适当补全缺失的指代信息。 3. 如果原始问题包含多个子问题,可以拆开。并不是每个问题都需要改写。简单明确的问题直接检索反而更快。比较稳妥的做法是:先直接检索,如果第一轮召回效果不满意,或者已经判断出问题比较复杂,再触发查询改写。这个“触发策略”本身也是一种路由。
3.3 混合检索:向量召回和关键词召回互补
混合检索的思路很朴素:同时用向量召回和关键词召回,把两者的结果合并起来。关键词保证精确数字、型号、专有名词;向量保证语义泛化。
常见做法是:
- 向量召回 top N,比如 30。
- 关键词召回 top N,比如 30。
- 合并去重,得到候选池。
- 再通过重排模型或分数融合,得到最终 top K。
这里有一个工程判断:合并之后,不要简单地把两路分数相加。不同召回方式的分数量纲不同,直接加没有意义。要么用重排模型重新打分,要么先做分数归一化,再做加权融合。
3.4 重排:把最相关的片段选出来
召回阶段的目标是“宁可多,不可漏”,生成阶段的目标却是“只给最相关的”。所以中间通常需要一层重排。
重排模型一般比向量检索更精确,因为它会把问题和候选片段拼接在一起做深度语义建模。但代价是速度更慢,所以不能对全库做重排,只能对召回后的少量候选做精排。
工程流程通常是:
- 混合召回后得到 50 条候选。
- 重排模型对每条候选计算相关性分数。
- 取分数最高的 5 到 8 条作为生成上下文。
如果不想额外部署重排模型,也可以让大模型对候选做选择,但延迟更高、成本更高。下面这张表可以帮你理解三个阶段的分工:
| 阶段 | 作用 | 典型问题 | 代价 |
|---|---|---|---|
| 向量召回 | 用语义找到“像”候选 | 精确匹配弱 | 快,成本低 |
| 关键词召回 | 用字面匹配找到精确候选 | 语义理解弱 | 快,成本低 |
| 重排 | 精细判断候选与问题的相关性 | 需要额外模型或额外 LLM 调用 | 慢,成本偏高 |
3.5 Agentic RAG:让模型决定什么时候去查
传统 RAG 的流程是固定的:提问,检索,生成。但真实问题并不总是“查一次就够”。有些问题需要拆成多个子问题,分别检索,再把结果汇总;有些问题像“根据最近三个月的客户反馈,总结主要投诉点”,需要多次检索、临时筛选、再综合。
Agentic RAG 的核心变化是:把“是否要检索”“检索什么”“检索几轮”的决策权交给模型。
这样做的好处是能处理复杂问题,代价也很明显:
- 延迟增加明显,因为可能涉及多轮模型调用。
- token 成本上升。
- 状态管理复杂度上来了,需要处理多轮工具调用的中间结果。
- 更容易失控,模型可能反复检索、偏离主题。
所以我的建议是:刚入门不要直接上复杂 Agent。先把固定的“检索→生成”流程跑稳,再加上“是否需要触发检索”的路由,最后再考虑多轮检索和工具调用。一步一步演进,而不是一开始就搭一个“什么都让模型决定”的系统。
4. 工程化落地:评估、缓存、批处理与本地部署
4.1 先建评估集,再谈优化
RAG 优化最大的坑,是没有标准答案就乱调参数。今天觉得这个切块好,明天又换成另一个参数,根本不知道是变好了还是变差了。
正确的做法是先建一个评估集。不用大,20 到 50 条真实问题即可。标注清楚:标准答案应该来自哪份文档、哪个段落。然后定义几个核心指标:
| 评估维度 | 怎么测 | 为什么重要 |
|---|---|---|
| 检索命中率 | 问题的正确答案是否出现在召回 top K 中 | 检索是地基 |
| 生成正确率 | 答案是否与标准答案一致 | 最终业务价值 |
| 答案忠实度 | 答案是否基于提供的上下文,没有自由发挥 | 降低幻觉风险 |
| 引用准确度 | 答案引用的文档是否真的包含答案 | 可追溯性 |
每次改动切块策略、检索参数、重排逻辑后,跑一遍评估集,记录分数。没有评估,你所有的修改都是“盲调”。评估集本身也会随着业务问题变化而更新,它应该是一个持续维护的资产。
4.2 缓存、异步索引和批量任务
工程化还要处理几个性能问题:
- 重复查询缓存:相同或高度相似的问题直接走缓存,可以显著降低成本。缓存键要设计好,比如对改写后的查询做哈希。文档更新后,需要失效对应 chunk 的缓存,而不是整个库一起失效。
- 索引任务异步化:文档解析、切块、向量化都比较耗时,不能在请求链路里同步执行。一般做法是文档上传后进入队列,异步处理,完成后标记索引状态。
- 批量任务限流:大批量文档索引时要控制并发,避免把模型 API 打爆。必须设置超时、失败重试和死信处理。
- 权限隔离:企业知识库通常不是所有人可见,检索前要按用户权限过滤。这一步最好在索引阶段完成,写入文档权限标签,检索时作为前置过滤条件。
有一个容易被忽视的问题:文档更新后,旧向量还在库里,如果索引覆盖不完整,用户会搜到过期内容。需要设计“文档与分片”的映射,更新时只替换受影响的分片。
4.3 本地部署场景:小模型一样能做 RAG
很多企业数据不能出内网,RAG 系统就需要本地部署。常见组合是:本地推理后端加载量化模型,FastAPI 包一层检索与生成服务,向量库也用本地方案。
比如有人会基于 llama.cpp 这类推理后端加载 Qwen 系列量化模型,再用 FastAPI 做一个 HTTP 服务。这样做的好处是文档不出内网,数据主权可控。但代价也明显:量化模型的质量有损失,并发能力受限于硬件,部署和运维成本更高。
在开始本地部署前,建议先确认几个问题:
- 模型量化到多少位,回答质量能否被你接受?
- 单卡能跑多少并发,是否存在排队?
- 向量库和模型服务是否需要拆开部署?
- 有没有监控和日志?模型推理失败时,用户会看到什么?
本地部署不是把模型放到内网就结束了,它同样需要一个可观测、可兜底的服务架构。
5. 问题排查链路:先从检索看起,再回头查模型
5.1 一个可以复用的排查顺序
RAG 项目出问题时,最容易犯的错误是直接怀疑模型:“是不是该换更大的模型?” 但更合理的排查顺序,是从检索链路向生成链路逐层检查。
我的固定排查顺序是:
- 看现象:先确认是答非所问、空回复、超时,还是成本飙升。
- 看输入:用户问题是否清晰?改写后的 query 是什么样?改写有没有引入错误?
- 看索引:目标文档有没有被正确解析?切块后的内容是否完整?
- 看召回:打印检索出来的 top K 片段,看正确答案是否在里面。
- 看上下文:如果答案在 top K 里,但模型没用上,看提示词结构、排序和上下文长度。
- 看工程:依赖版本、权限、资源占用、API 超时、并发限制。
很多“模型回答错误”的问题,在第四步就会发现,召回结果里根本没有正确答案。这时候换模型没有任何意义,要回头改索引或检索策略。
5.2 新手最容易踩的四个坑
第一个坑是不看切块结果。一次性全量导入所有文档,然后发现回答质量差,却说不出是哪一类文档出了问题。正确做法是先拿几份真实文档,看切出来的块长什么样,再决定调整方向。
第二个坑是只用向量检索,忽略关键词和元数据。对代码、型号、日期这类内容,关键词召回的稳定性比向量好得多。
第三个坑是上下文无脑塞满。把召回的所有片段全部拼进提示词,结果模型被无关信息带偏。需要做好重排,控制最终上下文的数量。
第四个坑是忽略缓存和失效策略。业务第二天更新了文档,但系统还在用旧索引,用户搜出来的内容已经过期。这需要一套可控的增量更新机制。
排查时,先把“检索返回了什么”打出来看。看到真实召回结果,问题原因往往就清楚了一半。
6. RAG 的适用边界和从 0 到 1 的路线建议
6.1 适合场景与不适合场景
RAG 相对适合的场景:
- 私有知识问答,比如企业内部制度、产品手册、客服话术。
- 低频更新的知识库,比如合同模板、技术规范、FAQ。
- 需要有来源引用的场景,比如咨询答复、审计支持。
- 内容检索辅助,比如代码库搜索、研究资料整理。
RAG 相对不适合的场景:
- 实时性极高且知识频繁变化,比如每秒都在变的行情数据。RAG 需要先把文档切块、向量化、建索引,这个过程天然有延迟。
- 复杂多步推理。RAG 擅长“找答案”,不擅长“算答案”。如果问题需要大量计算和推理,应该考虑 Agent + 工具调用。
- 对事实准确率要求极高、且无法接受模型自由发挥。RAG 只能降低幻觉概率,不能完全消除。关键决策场景,必须加人工复核或来源校验。
- 低延迟极简场景,比如直接写一个通用聊天机器人,不涉及私有知识,硬套 RAG 反而更慢。
如果团队已经在用 Dify、Spring AI、LangChain 这类框架,RAG 的上层流程可能已经封装好了,但这不代表数据质量就自动解决了。框架可以帮你把链路串起来,不会替你决定切块策略,也不会替你评估召回效果。真正的调优工作,仍然落在解析、切块、检索和评估这些底层环节上。
6.2 从最小可用流程开始,逐步加高级检索
不要一上来就搭一个大而全的 Agentic RAG。更稳妥的路线是:
- 先搭最小可用流程:一份文档 → 解析 → 切块 → 向量化 → 检索 → 生成。
- 手动写 10 条真实问题,逐个看召回结果和最终答案。
- 把这些问题固化成评估集。
- 评估集跑通后,再加查询改写,解决口语化问题。
- 加混合检索,解决精确匹配和语义匹配不平衡的问题。
- 加重排,保证上下文质量。
- 最后再考虑 Agentic RAG,智能判断是否要多轮检索。
每一步都要有评估数据和失败样例支撑。没有失败样例,你根本不知道改动应该往哪个方向走。
回到开头那个翻车案例。最后定位到的原因很简单:新版规则和旧版本被切进相近的两个块,向量召回时把旧版本排到了前面。修正方式不是换大模型,而是给文档加上“版本日期”元数据,并在检索时按版本过滤。这类问题靠提示词治不了。
RAG 工程的本质,是把检索质量管好,而不是把希望全押在大模型身上。下一步你可以先做一件事:挑出 10 条真实问题,把检索结果打印出来看看。如果召回的片段里根本没有正确答案,那问题大概率不在生成,而在检索。