做文档问答最常卡的不是模型不行,而是整条链路糊成一团:切分、召回、拼上下文、生成、引用、评测全塞在一段代码里,答错了也不知道改哪。这篇从进料到回答把 RAG 拆成分层架构,逐层给职责、输入输出和自检点,让你能定位问题、能换零件、能跟同事对齐分工。
做文档问答最常见的困境不是模型笨,而是整条链路糊成一团:切分、召回、拼上下文、生成、引用、评测全塞在一个脚本里。答错了,你根本不知道是文档解析把表格拆断了,还是检索没召回,还是提示词让模型自由发挥。结果每次调参都像买彩票。
这篇把 RAG 从进料到回答拆成六层,每层说清三件事:它负责什么、输入输出契约是什么、出问题时该往哪查。目标是让你能换零件、能定位故障、能跟同事对齐分工。先说清判断标准,再给可照做的分层清单。
为什么 RAG 必须分层,而不是一条流水线写到底
单段脚本写到底,短期爽,长期三个后果:出问题无法定位、换模型要重写全流程、多人协作互相覆盖。分层不是追求架构美感,是把不确定性关进各自的盒子,让每层的问题只影响自己。判断是否该分层的标准,就是「你能不能在不读另一层代码的情况下,说清这层的输入输出」。
- 按职责分层,不按调用顺序分层:进料、索引、检索、重排与组装、生成、评测与观测,每层只关心自己的输入输出契约
- 每层留一个稳定的接口描述,比如进料层输出「带元数据的片段数组」,下游不关心它来自 PDF 还是网页
- 层与层之间传结构化数据而不是裸字符串,方便打日志、做单测、互相替换
- 不适用的情况:一次性小脚本、语料不到几百页、只跑给自己看的问答,分层反而增加维护成本,先跑通再拆
进料层:文档进来第一步先把脏东西挡在外面
这层只做三件事:解析出文本、保留结构与元数据、按规则切分。它不做检索也不做生成。很多人把清洗逻辑散落在检索和生成里,导致改一次切分要动三个文件。进料层的输出契约应该是:一段文本 + 来源标识 + 位置信息 + 权限标签。
- 先分离解析与切分解析负责从原始文件拿到纯文本和结构;切分只吃纯文本加结构信息。两件事分开写,换文档格式时只动解析,不动切分规则。
- 切分带重叠并保留标题路径每段片段前面附上它所属的章节标题路径,跨片段提问时模型才知道上下文边界。重叠是为了避免答案正好落在切缝上。
- 元数据至少留四样来源标识、更新时间、权限标签、文档类型。后面的引用展示、权限过滤、时效排序全靠这几样。
- 抽查切分结果随机抽二十段,看有没有半句话、表格被拆断、页眉页脚混进正文。扫描件和 PDF 的清洗单独走一条路,不要和纯文本共用一套规则。
索引层:向量库只是其中一半
完整的索引有三样东西:向量索引、关键词或倒排索引、元数据过滤字段。只做向量,在专有名词、编号、型号上特别容易翻车——用户搜一个精确的编号,向量检索可能给你返回语义相近但完全不对的文档。混合检索不是高级玩法,是基本配置。
分层时把 embedding 调用单独包一层。因为换 embedding 模型往往意味着整库重建,所以版本标识要写进索引元数据,方便判断新旧数据是否混用。索引层不负责打分排序,它只负责「给定查询条件,返回候选集合」。
| 索引组成 | 负责什么 | 什么时候可以省 |
|---|---|---|
| 向量索引 | 语义相近的模糊匹配 | 语料量小、查询多为精确匹配时 |
| 关键词或倒排 | 精确词、编号、专有名词命中 | 几乎不建议省,成本低收益高 |
| 元数据字段 | 按来源、时间、权限做过滤 | 没有多租户或时效要求时可以简化 |
检索与重排:召回多、留下少,这层决定答案上限
这层拆成两段:召回负责不漏,重排负责不滥。召回阶段放宽数量、走混合检索,宁可多拿;重排阶段用相关性打分后截断,把真正有用的留下。两段合在一起写,就很难判断到底是召回没拿到,还是重排把对的挤掉了。
- 重排可以是模型打分,也可以是规则加关键词加权;没有重排时,至少用元数据过滤和来源加权兜一下
- 把召回条数和最终送入组装的条数分开配置,两个数字不要写死在同一处
- 排查顺序:答不上来时先看召回集合里有没有正确片段;有,说明是重排或截断问题;没有,往进料或索引层查
- 每次只调召回或只调重排其中一个,两个一起动,指标变化无法归因
- 到站内工具目录看看有没有现成的检索评测工具,别自己重复造
上下文组装:把碎片拼成模型能读的一段话
组装层要处理三件事:排序、去重、预算。排序上,最相关的片段放头尾,模型对首尾更敏感;去重上,同一段被多个查询路径召回要合并;预算上,按 token 上限决定留几条,超了要按分数砍而不是硬截——硬截经常把后面某个关键片段切一半。
每条片段都要带来源标签,生成层才能要求模型引用。不带标签,就别指望输出里有出处。这层是上下文工程的主战场,预算控制、缓存命中、长文档裁剪都在这里落地,别把逻辑散到检索和生成两边。
生成层:提示词、引用与拒答
系统提示里写清三件事:只根据给定片段回答、必须带来源、找不到就说找不到。不给拒答出口,模型一定会编。拒答不是失败,是正确行为。判断标准很简单:拿一个语料里根本没有的问题去问,看它是老实说不知道,还是编一段听起来合理的答案。
结构化输出更适合接下游:答案、引用列表、置信度分字段返回,前端渲染和评测都好处理。参数层面要区分两类任务——摘要归纳可以放低随机性,多轮追问要保留上下文,两者混用一套参数最容易出现前后不一致。具体模型的价格和参数资料,到站内的模型资料库核对当时的数字。
| 任务类型 | 关注点 | 容易踩的坑 |
|---|---|---|
| 单轮事实问答 | 引用是否命中、是否拒答 | 提示词太宽松导致自由发挥 |
| 多轮追问 | 上下文是否保留、指代是否消解 | 把上一轮答案当成事实再喂回去 |
| 摘要归纳 | 覆盖度、是否引入外部信息 | 模型补充了片段里没有的结论 |
评测与观测:哪一层坏了要靠日志说话
每层记自己的字段:进料记文档数和切分数,检索记召回条数和分数分布,组装记 token 用量和片段数,生成记延迟和引用命中。没有这些字段,出问题只能靠猜。评测的要点是攒一组固定问题加标准答案,改任何一层前后都跑一遍,看的是哪层指标掉了,而不是整体感觉变好。
- 固定问题集不用大,几十条覆盖典型场景就够,关键是要长期稳定不随便改
- 每次只改一层,跑完对比该层指标和端到端指标,两个都看
- 观测看趋势不看单次:同一批问题隔几天跑一次,比一次调参后的手感可靠
- 把每层的输入输出落盘留存,排查时可以回放,不用重新跑整条链路
- 到站内评测与指南中心看看有没有可参考的评测框架和指标口径
拆分顺序:从一条链路到分层,先动哪刀
既然已经跑通了单链路,接下来不是大重构,而是按顺序切三刀。第一步把进料和检索切开,让语料处理可以独立重跑;第二步在检索里单独加一层重排,不动其他任何地方;第三步才把组装和生成分离。每次只动一层,动完跑同一组评测。
- 切开进料与检索把文档解析和切分抽成独立入口,产物落盘。验证方式:换一批文档重跑进料,检索端不用改代码就能用新数据。
- 在检索内加一层重排召回保持原样,重排作为新增环节插在召回和组装之间。验证方式:关掉重排时指标回到改动前,说明改动没有副作用。
- 分离组装与生成组装层输出带来源标签的片段列表,生成层只吃这个列表。验证方式:把生成层换成另一个模型,组装层代码不动就能跑。
- 补观测字段每层加日志字段,落盘留存。验证方式:随便挑一次答错的案例,能从日志定位到是哪一层的问题。
不适用的情况也说清楚:如果现在连稳定跑通都做不到,先按单链路把数据流打磨清楚,分层是优化不是起点。分层带来的收益是定位能力和可替换性,代价是更多的接口和日志维护成本,语料规模和协作人数都很小的时候,这笔账不一定划算。
下一步分层清单有了,下一步是确认每层用的模型和检索方案值不值得。到站内中转站导航和价格追踪页面,对比当时的实际数字,再决定每一层用哪家的能力,比拍脑袋选型靠谱。→常见问题
RAG 分层是不是过度设计,小项目也要分吗?
不一定。语料少、只服务自己或小团队,单链路跑通更省事。判断标准是:答错时你能不能三分钟内定位到是切分、召回还是生成的问题。定位不了、或者要换模型重写全流程,就该拆了。
向量库和关键词检索必须都上吗?
不必须。查询多是专有名词、编号、精确短语时,关键词加元数据过滤往往更准且好排查;查询偏口语、语义相近时向量召回更稳。可以先单跑一种,用评测集看漏召回比例,再决定要不要加第二种做混合。
模型答错,怎么知道是检索的锅还是生成的锅?
做一次离线对照:把人工认为正确的片段直接塞进上下文让模型回答。如果这样它能答对,问题在检索或重排;如果还是答错,才轮到提示词和模型选型。这一步能把排查范围砍掉一半。
切分粒度到底多大合适?
没有通用数字,受文档结构和问题类型影响。经验做法是切分带重叠、保留标题路径,然后拿真实问题跑召回,看正确片段是否落在前几条。用小粒度提高命中、多召回几条再重排,通常比一次切很大更灵活。
分层之后,改一层的效果怎么量化?
给每层定一个可记录的指标,比如召回命中率、组装后的平均 token 数、引用命中率,然后用同一组固定问题在改动前后各跑一遍。只看整体回答质量容易被主观印象带偏,分指标看才知道是哪层真的变好了。