RAG 流程:切片、检索和拼接提示

一条流水线的全景图

前面三章把地基都铺好了:知识外置的原因、向量化的原理、向量库的选型。这一章把这些零件串成完整的流水线,也就是检索增强生成(RAG)的全流程。先把三个环节的名字记住:切片、检索、拼接提示。所有 RAG 系统都是这三个动作的变体,复杂的产品无非是在每个动作上多挂了点配件。

用做菜打个比方。切片是备料,一头牛不能整锅炖,得切成合适大小的块;检索是配菜,客人点什么你从备好的料里抓对应的几样;拼接提示是装盘,把抓出来的料码好交给厨师(大模型)出菜。厨师的手艺是模型能力决定的,你控制不了太多;但前两步做得好不好,直接决定这盘菜是不是客人想吃的。所以 RAG 项目里八成的调优功夫,都花在这两个可控环节上。

完整链路是这样的:原始文档经过加载、清洗、切成片段,每片调用 embedding 模型算出向量,连同元数据写入向量数据库。用户提问时,问题同样被向量化,到库里按余弦相似度检索出 top 5 片段,把这些片段连同问题一起拼成提示,发给本地运行的 Ollama 模型,模型基于这些片段生成回答。整条链路的每一步都会在下面展开,每一处都有具体参数和踩过的坑等着你。

注意整条链路里有两个完全独立的阶段。灌库是离线的,一份文档处理完躺着不动,只在知识更新时重跑;问答是在线的,用户每问一次就走一遍检索加生成。这个划分决定了工程上怎么排期:离线阶段可以慢慢磨切片质量,在线阶段必须卡住延迟和并发。新人常犯的错是把灌库逻辑写进问答流程里,用户问一次就实时切文档算向量,系统慢到自己都受不了,还纳闷 RAG 怎么这么卡。

切片:把大象切成合适大小的块

所有环节里,切片最容易被轻视,实际上它对最终效果的影响常常盖过 embedding 模型的选择。不好的切片方式会把一句完整的解释一切两半,关键信息被硬生生切断,检索到的半句话模型读着也是懵的。切片的核心矛盾只有一个:每片多大才合适。

直觉是越大越好,信息完整嘛。但片太大一片里混进多个主题,向量的语义指纹就糊了,检索精度直线下降;还浪费上下文,top 5 一塞,提示里动不动就容不下。片太小呢,上下文又断裂,每片孤立看都摸不着头脑。实践中,按字符算每片 500 到 800 字是中文文档一个公认的好起点,英文文档按 token 数一般取 500 到 1000。

参数可以微调,但切片方法要按文档类型选,这张表是我实战后的总结:

文档类型 建议切片方式 每片大小 关键细节
制度规章 按条款编号切 300 到 600 字 保留条款编号作为前缀
技术手册 按标题层级切 600 到 900 字 连同父标题一起组入切片
问答对 一问一答为一片 不限 切片本身已是完整语义
长篇报告 按段落加滑动窗口 500 到 800 字 留 80 到 120 字重叠
代码文档 按函数或段落切 看函数长短 保留函数签名与上下文

两个细节值得单独强调。一是重叠(overlap),相邻两片之间留 80 到 120 字的重复地带,能显著缓解切断带来的信息损失,代价是存储多占一成左右,基本是稳赚不赔的买卖。二是保留标题上下文,一篇长文档切完后,给每片前面拼上它所属的各级标题——这一片的独立性立刻提升,模型回答时也知道这段话的来历。

还有清洗这个前置动作容易被略过。Word 导出的文档带着各种校对标记、PDF 提取出来满是页眉页脚与页码、网页抓下来的文本更是一锅粥。这些噪音被 embedding 一压,整片语义都被稀释。规范做法是切之前先做一遍正则和大模型顺手过滤:去掉不可见字符与连续重复符号、删掉短于某个长度的碎片、把目录和封面这类内容直接扔掉。清洗环节花的每一分钟都会在检索精度上还回来,信不信由你。

落地时这些参数都在代码里,下面是最常见的 LangChain 切片调用,字段含义对照上表就行:

from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(chunk_size=600, chunk_overlap=100,
    separators=["\n\n", "\n", "。", ";", ",", " "])
chunks = splitter.split_text(doc_text)

separators 列表从上到下依次尝试,优先从段落断点切,实在不行才降级到句子和逗号,这是中文场景比默认英文分隔符好用的关键改动。关于 LangChain 怎么和 Ollama 配合到一起,ol-0305 里有完整的示例工程。

检索:把对的片段从库里捞出来

检索这一步表面上最简单,把问题向量往库里一扔就完事。魔鬼藏在参数里。第一个参数是 top_k:取几条返回。取少了可能漏掉正确答案所在的片段,取多了上下文塞满杂质,模型被干扰答非所问。经验值是 top 5 起步,问答对类的库可以降到 3,综述大文档可以升到 8,但很少超过 10。超过之后正确片段的边际收益几乎被噪音淹没,不值得。

第二个参数是相似度阈值。只收余弦相似度在 0.7 以上的片段,这个门槛帮你挡住不相关内容混进提示。注意阈值过高的副作用:库里根本没有相关内容时,用户问个完全无关的问题,系统宁可啥也不给也别硬塞。什么都没有时模型应该坦白说不知道,而不是胡编一段——这在容错设计上是头等大事。阈值的具体数值依赖你选的 embedding 模型,换模型必须重新标定,这是上上章强调过的知识点。

第三个升级动作是重排(rerank),强烈建议加上。流程是先用向量检索粗排出 top 20,再用一个专门的 reranker 模型精排到前 5。粗排快但糙,精排慢但准,两者结合是当前工业界的标准答案。bge-reranker 之类的小模型在 CPU 上跑一次重排也就几十毫秒,相比换大模型提升检索质量,这是个性价比极高的组件。

顺手提一个高级玩法:先转述再检索。就是让一个小模型先把用户的口语问题改写成更接近文档措辞的形式,再去检索。用户问的是假期怎么请,文档里全篇写的都是休假申请流程,转述一过,命中率立竿见影。这个小模块在 LangChain 里有现成实现,值得在你的流水线里占一格。

另一个常被忽略的手法是元数据过滤的配合。向量负责语义匹配,元数据负责边界约束,两者各管一头。用户在问答界面选了我只看财务部的文档,检索时就把 dept=财务 作为前置过滤,向量库的联合过滤能力在这里派上用场,这也是当初选型时强调 Qdrant 过滤能力强的原因之一。一个只有语义没有边界的系统,在真实权限体系下是跑不长的。

拼接提示:别把好食材糟蹋了

切片和检索辛辛苦苦把对的内容找来了,最后一步如果胡乱拼接,前面全白干。拼接的本质是给模型交代清楚两件事:这些素材是什么、你要拿它们做什么。说穿了就是提示词设计,但 RAG 场景有几个固定套路。

先看一个可以直接抄的模板:

你是公司内部知识助手,只能根据提供的参考资料回答,不要编造。
参考资料:
{retrieved_chunks}
问题:{question}
回答时引用资料编号,如果资料中没有答案,请明确说不知道。

这个模板的每一个零件都有讲究。参考资料放前面、问题放后面,利用的是上下文记忆里靠近问题的内容更容易被遵循这一规律,这一点在 ol-0301 讨论记忆衰减时聊过。明确写只能依据资料,是抑制模型幻觉最有效的一句话,模型宁可尴尬地说不知道,也比编一段强盗逻辑强。要求引用编号,让你的回答可追溯,合规场景直接可用。

也有一个反面教材值得对照。许多人写提示习惯从 你是一个乐于助人的助手 这种空泛开头,然后铺垫三段背景再甩问题。在 RAG 场景这属于自废武功:模型既不知道你给的材料和问题是什么关系,也不知道到底该怎么用。提示不是越客气越好,而是指令越明确越好。把这套提示固化进模型配置、彻底化为一行调用的做法,在后面讲系统模板的篇章里有展开,这里先留个印象。

拼接时还有三个机械操作要注意。一是片段之间用清晰的分隔符隔开,参考资料 1、参考资料 2 之间换行加空行最清楚,别一把连成糊。二是总长度要控制在模型上下文上限的一半以下,给回答留够空间,同时警惕 ol-0402 里细聊的上下文长度与速度的权衡。三是每片前面带上出处信息,部门、日期、文档名,这些以后排查问题全是线索。

最后允许你犯的一个常见错误是:把检索结果全部塞进去就不管了。多花一晚时间实现按重要度排序——先放相似度最高的一片,模型注意力的峰值位置它当之无愧。细节是魔鬼,这里是真魔鬼住的地方。

评测:怎么知道这套流水线及格

没有评测的调优都是闭眼开车。RAG 的评测可以非常简单,准备二十到五十条真实问答对,每条标注出正确答案所来自的文档片段,然后跑下面三个指标:召回率,正确片段是否出现在检索结果里;命中率,回答里有没有给出正确信息;引用准确率,标注的出处对不对。

三个指标分工明确。召回率低就去调切片和检索,问题出在前半段;召回率高但命中率低,问题出在拼接提示或生成模型;引用准确率低就是模板要求没写死,属于最好修的那类问题。这套方法看着土,比直接看回答主观打分可靠得多,而且二十条问答测一下天就能跑完一轮,改动任何一个参数都值得重跑一遍用数据说话。

再补一个容易被略过的经验:评测集要跟着业务一起长。每月往里添几条用户真实提过的新问题,删掉已经太容易的旧问题,保持集子始终有难度梯度。这种滚动评测的好处是能发现知识库的盲区——哪些主题的问题反复召回失败,往往说明那部分的文档根本没收集或者写得太空。评测集于是同时具备了验收工具和需求调研工具两重身份,一鱼两吃。

严格一点的团队会再引入自动化评测,让另一个强模型当裁判给回答打分。前提是你得投入额外算力,私有化环境下这条路成本不低。我的建议是先搞手工评测,等积累了几百条问答且调参进入瓶颈后再上自动评测工具,顺序别颠倒。

常见问题

检索回来的内容不相关,从哪里查起

按三段论排查。先把检索结果打印出来,看相似度分数分布——如果所有分数都低(比如不到零点五),是问题本身库里没有对应内容,考虑扩充文档或降低阈值;如果分数挺高但仍不相关,是 embedding 模型或切片的问题,换模型或者调整每片大小与重叠;如果粗排结果里有正确的但没进 top 5,说明排序能力不足,加 reranker 立竿见影。定位顺序永远是先看分数分布再动手,别凭感觉乱改。

切片大小调了又调,效果还是不行怎么办

先别在 chunk_size 上死磕,回头检查两件事。第一,你切的依据是什么,如果还在按固定字符数硬切,换按段落或标题层级再看。第二,切片前清洗过吗,页眉页脚、自动生成的目录、网页抓取残留的导航文字都会污染语义。清洗切片属于隐性大工程,一次做好,后面的调参全跟着顺。清理得好,600 字的标准切片往往就够用了,根本不用调来调去。

模型回答里引用了资料里没有的数字

这是典型的幻觉,说明提示压制得不够。三个动作选一个:加强提示,把如果资料中没有请明确说不知道这句话放到末尾离问题更近的位置;提高检索质量,确保正确片段真的进了 top 5;换一个更强或者更本分的生成模型。另外留意问答对形式的语料别和说明书混在一个集合里,模型会把上面一段的答案串到下面的问题上。RAG 效果不如意时问题大概率在检索,可以回 ol-0302 复习下向量与相似度的原理,再对照本节对症下药。

top_k 设为多少最合适

没有标准答案,但有判断方法。看你的文档平均每片多大、正确答案通常分布在几片。说明书类文档一个答案集中在一片,top 3 到 5 足够;综述报告的内容分散在多处,top 8 到 10 才扎得全。上探的代价是提示变长、速度变慢、噪音增多。花半小时用不同 top_k 跑一遍评测集,画一条命中率曲线看拐点在哪儿,比抄任何博客的推荐值都靠谱。

整个流水线慢得像蜗牛,哪一步最可疑

分两类。首次提问慢、后续提问快,通常是模型首次加载的冷启动在背锅,权重从硬盘挪进显存只需要等一次。每次都慢,那就逐段掐表:embedding 调用、向量检索、拼接提示、模型生成,四段时间里生成占了大头是常态。优化顺序是先压提示长度(缩短上下文立竿见影),再看能否流式输出让用户早点看到字,最后才考虑换更小的生成模型。流式输出的具体接法在 API 集成那篇讲过,对首屏体验的改善非常明显。