很多人照着示例跑通了 LlamaIndex 的问答 demo,一换成自己的资料就开始出问题:答案答非所问、引用对不上、请求偶尔超时。问题基本不在框架,而在切块粒度、检索参数、Top-K 与拼装顺序这些落地细节。这篇按工程顺序把流程拆一遍,每一步给判断标准和自查动作,你照着改就行。
第一步:把资料变成能检索的块
RAG 效果差,九成问题出在这一步。很多人上来就调 chunk_size、调 top_k、换 embedding 模型,但块本身就是烂的,后面怎么捞都捞不回来。先把文档按类型分堆,再决定怎么切,顺序反了就是白干。
| 文档类型 | 特征 | 处理优先级 |
|---|---|---|
| 结构化表格 | 字段整齐、一行一条记录 | 先思考要不要拆成句子,别整行塞进去 |
| 长正文 | 标题层级清楚、段落完整 | 按标题层级切,最省事也最稳 |
| 扫描件 / 图片 PDF | 文字是像素,复制出来是乱码 | 必须先 OCR 再清洗,否则后面全是噪音 |
| 混排文档 | 表格截图 + 正文 + 页眉页脚 | 先拆页再分类,别一把梭 |
扫描件这里单独说一句:OCR 出来常见的毛病是断行错乱、页眉页脚混进正文、表格变成一列词。这些脏数据进了向量库,检索阶段会把无关内容也召回来,rerank 再强也只是在垃圾里挑相对不垃圾的。所以判断标准很简单——抽几段 OCR 文本读一遍,读不顺就先洗,别往下走。
切块只看两个指标
- 单块能不能独立看懂:把这块单独拿出来,不看上下文,意思是否完整;缺主语、缺前提的,说明切早了或切错了
- 块与块之间有没有截断完整句子:一句话被劈成两半,两半各自都检索不到原意
- 标题层级优先于固定字数:按 H1/H2/H3 切,天然带语义边界;按字数硬切容易在句中间下刀
- 块太长也不好:塞进上下文会挤掉别的召回结果,还容易稀释语义
- 分类按扩展名和内容特征把文档分成表格 / 长正文 / 扫描件三类,分别放进不同目录,后续用不同解析器处理
- 先洗扫描件对扫描件跑 OCR,再去页眉页脚、修断行;抽 5 段人工读一遍,读得通才进下一步
- 按结构切优先用标题层级作为切割点,标题下的正文作为一个块;超长的再按段落二次切
- 抽检回炉随机抽 10 到 20 个块,逐个读;看不懂的块直接回到上一步改切法,不要标记了事
- 记下切法把每类文档用的切割策略写进配置注释,换文档类型时好对照排查
切块是 RAG 里唯一一个靠「人读一遍」就能发现问题的环节,别跳过这次抽检。
抽检这步花不了多少时间,但它决定后面要不要返工。你要是已经切完了,就随手抽一批块读一读——读不懂的比例超过你能接受的程度,就回炉;读得懂,再往下接索引和检索。关于不同切割策略的取舍,可以到 站内指南中心 对照着看当时的整理。
第二步:选索引与向量存储
选索引和向量库这一步,最容易犯的错是「一步到位」:资料还没几篇,就先把分布式向量库、托管服务、各种中间件全搭起来。多一层组件就多一处报错的地方,链路没跑通之前,你连问题出在切分、嵌入还是检索都分不清。所以建议先用框架自带的内存索引把「读文档 → 切块 → 嵌入 → 检索 → 塞进 prompt」这条链路整个跑一遍,能返回结果了再谈扩容。
- 先把内存向量索引跑通:加载几篇资料,直接构建默认索引,问一两个已知答案的问题,看能不能召回正确段落
- 确认检索结果对得上:打印出召回片段的原文,人工核对是不是你要的那段,别只看有没有返回值
- 规模参考:文档量在几十到几百篇、单次检索延迟可接受时,内存方案完全够用;重启后要重建是它的代价
- 等触发下面任一条件再换外部向量库:数据要跨进程或跨重启保留、要按标签/时间/来源过滤、文档量涨到内存索引构建时间明显变长
- 换库时只替换存储后端,索引和检索代码尽量不动,方便回退对比
真要换外部向量库,别先看产品评测,先对齐三件事:要不要持久化、要不要按元数据过滤、团队有没有运维能力。这三条基本能砍掉一大半候选。要不要持久化决定你能不能接受每次启动重建;元数据过滤决定检索时能不能加「只看某个项目/某段时间」这类条件;运维能力决定你该选自托管还是托管服务。具体有哪些产品、各自定位,直接到站内的向量库对照页看,别凭印象选。
| 判断项 | 先问自己的问题 | 倾向选择 |
|---|---|---|
| 持久化 | 重启后数据还要不要? | 要持久化 → 外部向量库;不要 → 内存索引 |
| 元数据过滤 | 检索时要不要按来源/标签/时间筛? | 要 → 选支持过滤的库;不要 → 选择面更宽 |
| 运维能力 | 团队有没有人管部署、备份、扩容? | 有人管 → 可自托管;没人 → 优先托管服务 |
| 规模量级 | 文档是几百还是几十万? | 小规模内存够用;大规模再上外部库 |
嵌入模型和向量库是绑在一起的:向量维度、距离度量方式要对得上,换嵌入模型基本等于所有向量作废、要重建索引。所以动手前先把「用哪个嵌入模型、向量存哪、以后换不换」写下来,中途别来回换。如果只是想试试不同嵌入模型的效果,就建两套独立索引对比,别在原索引上直接覆盖,否则旧数据全废还没法回退。
下一步站内向量库对照页按持久化、过滤、部署方式列了各家的适用场景,选型前先去对一遍,避免选了跟团队运维能力不匹配的方案。→第三步:检索、重排与拼装
很多人把「召回多少条」和「最后塞给模型多少条」当成同一个数,其实应该拆成两段。召回阶段宁可放宽一点,比如先捞一批候选,让重排模型去挑;重排之后再收紧,只留真正相关的少数块进上下文。这一步没做好,最直接的后果就是关键信息被无关块稀释,模型答偏或者干脆漏掉。
| 阶段 | 目标 | 常见失误 |
|---|---|---|
| 召回 | 尽量不漏 | 设太窄,正确块根本没进候选池 |
| 重排 | 把最相关的顶上来 | 只按向量相似度排,忽略关键词和元数据 |
| 收紧 | 控制喂给模型的条数 | 把候选全塞进去,上下文被占满 |
| 拼装 | 按相关性顺序摆放 | 顺序随意,最相关的块被夹在中间 |
拼装顺序是有讲究的,不是随便拼接。把重排后最相关的几块放在上下文的开头或结尾,中间放次相关的,模型对首尾位置的注意力通常更强,给引用时也更稳。同时每一块都要带上来源标识,比如文档名、章节或片段编号,这样模型回答里能指名出处,你事后也能顺着标识回去核对原文。
- 先放宽召回把召回数量调大一些,具体数字到站内模型资料库对照你用的模型上下文上限来定,别写死一个拍脑袋的值。
- 接一个重排用重排模型或关键词加权的混合排序,把候选重新打分;验证方式是看同一批问题下,正确块有没有稳定进前几名。
- 按分数收紧设一个相对分数线或固定条数上限,只放最高分的若干块进上下文,避免上下文被填满。
- 带来源拼装每块前面加来源标识再拼接,最相关的放首尾;回答里出现引用时,回头核对标识对应的原文。
元数据过滤是提精度里性价比最高的一招,而且往往比反复调相似度阈值见效快。思路是先按文档类型、时间范围、部门或标签筛一遍,把明显不相关的语料排除在检索之外,再去算相似度。阈值调来调去是在一堆混着噪音的候选里挑,而元数据过滤是直接从源头把噪音拿掉。什么时候不适用?如果你手上文档几乎没有可靠的结构化字段,或者用户问题本身跨部门跨时间,硬过滤反而会把正确块挡在外面,这时候就退回纯向量检索加重排。
- 按文档类型筛:只检索手册、FAQ 或规范类,排除聊天记录这类碎片
- 按时间筛:需要最新信息的问答,先限定近一段时间的文档
- 按部门或标签筛:企业知识库里按权限和业务线先切一刀
- 过滤字段要能稳定取到,取不到的字段别写进过滤条件
- 过滤完记得留个兜底:命中为空时自动放宽条件再检一次
召回是撒网,重排是挑鱼,拼装和元数据过滤才是决定端上桌那盘菜好不好吃的手艺。
第四步:上线前必须补的工程护栏
前三步跑通不等于能上线。真正到线上,拖垮请求的往往不是模型本身,而是某一次检索突然变慢,或者检索返回空结果时模型开始硬编。这一节讲的就是上线前必须补的三道护栏:超时与重试、空结果兜底、以及能复盘的日志。
超时和重试:分开设,别一刀切
检索和生成是两种完全不同的耗时结构。检索依赖向量库或搜索引擎,波动大;生成依赖模型出 token 的速度,相对稳定。把两者塞进同一个总超时里,结果就是一次慢检索把生成的时间预算也吃掉了。正确做法是给每一环单独设超时,再设一个总超时兜底。
| 环节 | 超时怎么定 | 失败后怎么处理 |
|---|---|---|
| 检索 | 参考站内速度测试量出的基线,在其上留出余量 | 短超时+有限次重试,仍失败就走空结果兜底 |
| 生成 | 按最长可接受响应时间设,通常比检索宽松 | 一般不自动重试,避免重复计费和重复输出 |
| 整条链路 | 大于检索超时+生成超时之和 | 超时直接返回兜底话术,不要让用户干等 |
超时档位不要凭感觉拍。先去站内 /tools/speed-test 跑一次,拿到你这个环境下检索和生成的延迟基线,再在这个基线上加余量。基线是别人机器上的数字,只能作参考,最终要按你自己的部署环境调。
- 重试只加在检索环节,生成环节重试容易造成重复输出和重复计费
- 重试要设上限,并且两次重试之间留间隔,避免打爆下游
- 区分「超时」和「明确报错」:前者可重试,后者要看错误码决定
- 重试次数和间隔写进配置,别散在代码各处,方便统一调
- 每次重试都记一条日志,否则线上根本看不出重试了多少次
检索为空:宁可直说,别让模型硬编
检索返回零结果或相似度全部低于阈值时,最危险的动作是把空上下文直接喂给模型。模型不会说「我不知道」,它会顺着问题编一个看起来合理的答案。上线前必须明确:什么条件下判定为「没检索到」,以及没检索到时返回什么。
- 定阈值给相似度设一个最低阈值,低于它的块不算命中。阈值多少合适,用你手头的真实问题跑一批看命中情况,别照抄别人的数。
- 判空逻辑命中块数量为零,或最高相似度低于阈值,就判定为「没找到」,直接走兜底分支,不调用生成模型。
- 写兜底话术准备一句明确的话术,比如「当前资料里没有找到相关内容,建议换个说法或补充背景」。宁可直说没找到,也不要给一个编出来的答案。
- 留人工出口兜底话术后面给一个联系入口或补充信息的提示,让用户知道下一步能做什么,而不是卡死在这里。
日志:出问题要能定位是哪一环塌的
线上出问题时,最怕的是只有一句「请求失败」。你需要的日志至少要能回答三个问题:命中了哪些块、相似度分别是多少、每一环各花了多久。有了这三段,才能判断是检索没召回、召回质量差,还是生成太慢。
- 命中块 ID
- 记录本次检索返回的块标识,用于回溯内容是否正确
- 相似度分数
- 记录每个命中块的分数,判断是召回问题还是阈值问题
- 分段耗时
- 分别记录检索耗时和生成耗时,定位是哪一环拖慢的
- 超时与重试标记
- 记录本次是否触发超时、重试了几次,便于统计稳定性
这套日志不用做得多复杂,关键是三段信息都要落盘,并且能按请求串起来。日志字段和速度基线的具体取值,到站内 /tools/speed-test 和相关工具页核对当时的实际情况,别把这里的描述当成固定数字。
需要说明的是,上面这套护栏主要针对单轮问答型 RAG。如果你做的是多轮对话或需要跨文档推理的场景,超时和兜底的策略会不一样,重试也可能引入上下文错乱,那类场景要单独设计,不能直接套用这里的默认值。
下一步把你当前的检索和生成延迟跑一次基线,再对照上面的表把超时和重试配置补上,顺便检查日志里有没有落全三段信息。→第五步:怎么判断改完到底变好没
改完切块参数或者检索参数,最怕的就是「感觉好像好了一点」。这种感觉通常是拿两三个自己印象深的例子在脑子里过一遍,样本太少,而且很容易只记住答得漂亮的那次。靠谱的做法是提前攒一组自己业务的固定问题,每次改完都跑同一组,把结果摆在一起比。本节的判断标准不适用于刚起步、连一条能跑通的链路都还没有的阶段,那种情况下先让流程跑起来更重要。
- 建题库从真实业务里挑问题,覆盖三类:答案明确在一份文档里的、需要跨两份文档拼的、答案本来就不存在的(测它会不会硬编)。数量不用多,覆盖住这三类就行,写成一个文件存好。
- 固定跑法每次只改一个变量,其余保持不变,用同一批问题跑一遍。把「召回了哪些块」和「最终回答」都存下来,别只看最终回答,中间召回错了后面再对也是蒙的。
- 记录对比把改动前后的结果并排记下来,按下面两个方向各自打分。不要只记「感觉好了」,要记清楚是漏召变少了,还是无关块变少了。
| 看的方向 | 具体看什么 | 什么算退步 |
|---|---|---|
| 该召回的有没召回 | 标准答案所在的那块,有没有出现在召回结果里 | 原来能召回的问题现在找不到了 |
| 混进多少无关块 | 召回结果里和问题不相关的内容占比 | 无关块明显变多,把有用信息挤掉了 |
| 回答是否被带偏 | 答案里有没有引用到无关块的内容 | 回答开始扯到问题之外的东西 |
这两个方向必须一起看。只盯着召回率,把召回数量开得很大,无关块全涌进来,模型照样被带偏;只盯着无关块,过滤太狠,该找的又找不到了。两者是一对拉扯,任何一次改动都要同时看两边的变化,只看一边很容易做出「这边好了那边塌了」的调参。
- 调参顺序固定成一条线:先动切块,再动过滤,再动重排,最后才碰提示词。
- 为什么这个顺序?前面的环节决定「给模型看什么」,提示词只影响「模型怎么看」。原料不对,提示词写得再花也没用。
- 一次只改一层,改完跑题库,确认这一层确实带来了变化,再进下一层。
- 跳着改的典型后果:切块和提示词一起动,效果变好了也说不清是谁的功劳,下次遇到问题没法复用经验。
- 想快速核对当前各家模型和检索相关能力,可以到 模型资料库(/models) 看当时的整理,别凭记忆判断哪个更适合你的场景。
常见问题
LlamaIndex 的示例能跑通,换成自己的文档就不行,先查哪里?
先查切块。示例文档通常短且规整,你的资料一旦有表格、目录、页眉页脚,切出来的块就是残缺的。抽十几块读一遍,确认每块单独看能懂意思,再去怀疑检索参数和提示词。
检索召回了但答案还是不对,是模型问题吗?
多数不是。先看拼进上下文的内容:相关块有没有真的进去、有没有被无关块挤掉、顺序是不是把关键信息埋中间了。上下文干净之后答案还是偏,再考虑换模型或调提示词。
资料规模不大,有必要上外部向量库吗?
不一定。先用默认索引把从切块到回答的链路跑通,确认效果可接受再考虑迁移。外部向量库解决的是规模、过滤和多人共用的问题,不是效果问题,小体量下反而增加调试成本。