跳转到主要内容

RAG 的噩梦不是检索,而是重跑:把处理流水线拆成可替换节点

13 分钟
RAGDifyWorkflow数据流水线重跑

RAG 最大的麻烦,经常不是问答

很多人第一次做 RAG,会把注意力放在检索效果上:向量库选哪一个,embedding 模型选哪一个,chunk 大小怎么切,reranker 要不要上。

这些当然重要。但做过几轮生产迭代后,会发现真正麻烦的事情往往不是“这次为什么没搜准”,而是另一个问题:

算法、解析器、prompt、打标规则、embedding 模型更新以后,旧数据怎么办?

如果只有几十份文档,答案很简单:删了重来。
如果是几万份、几十万份,答案就不简单了。你不能每次想改一个字段,就把所有 PDF 重新解析、重新抽取、重新切片、重新 embedding、重新上传向量库。成本会很难看,时间也会很难看。更麻烦的是,重跑期间新旧数据混在一起,你还要保证线上查询不要突然变差。

所以第二章不讲“怎么把 RAG 做聪明”,先讲一个更工程的问题:处理流水线必须拆成可替换节点,而且每个节点都要有版本和状态。

这件事听起来很枯燥,但它决定了系统能不能长期迭代。


一个不能拆的流水线,迟早会变成一次性脚本

最容易写出来的 RAG 流水线大概长这样:

拿到 PDF -> 解析 -> LLM 总结/打标 -> 切片 -> embedding -> 写入向量库

写成一个脚本很快,甚至看起来很清楚。问题在于,这里面任何一步都会变。

  • PDF 解析器会变:今天用 minerU,明天可能要换 unstructured,后天又发现表格需要单独处理;
  • 打标 prompt 会变:业务人员发现行业分类不够细,或者想补一个“核心观点”字段;
  • chunk 策略会变:原来按 800 tokens 切,后来发现表格和标题层级需要单独处理;
  • embedding 模型会变:模型升级、价格变化、中文效果变好、供应商策略调整;
  • 向量库 schema 会变:metadata filter 不够用,tenant 字段要补,索引要分集合;
  • 检索策略会变:纯向量不够,要加 BM25、rerank、query rewrite。

如果这些东西都混在一个脚本里,系统就会越来越不敢改。因为每次改动都像在拆一根已经通电的线:理论上知道要改哪里,实际很难确定会不会影响其它地方。

更合理的方式,是把处理链拆成节点。每个节点只关心自己的输入和输出,不直接绑定下游实现。

这张图的重点不是节点名字,而是两个原则。

第一,每个节点都要写状态。不是只写 Done / Fail,而是要写清楚用的是哪个版本:parser_versionprompt_versionchunk_versionembedding_modelindex_version。否则你根本不知道旧数据和新数据差在哪里。

第二,重跑要能从中间开始。解析器没变,只改了打标 prompt,那就不应该重新解析 PDF;chunk 策略没变,只换了 embedding 模型,那就不应该重新跑 LLM 抽取。少重跑一步,就是少一次成本、少一次失败机会。


Dify Workflow 的价值,不只是“拖拽编排”

很多工程师不太喜欢 Dify 这类可视化 workflow。这个反应我理解。看惯了代码、Git、CI/CD,再看一个浏览器里的节点图,很容易觉得不够工程化,也不够可控。

但在 RAG Agent 这种系统里,Dify 有一个很实际的作用:它把一部分处理逻辑交给业务人员参与,而不是所有东西都卡在工程师这里。

尤其是打标、摘要、评分、字段抽取这些节点,本来就不完全是工程问题。一个投研报告应该抽哪些字段?“核心观点”和“推荐理由”怎么区分?行业分类要不要按申万、GICS,还是公司自己的 taxonomy?这些问题让工程师拍脑袋,通常不会太好。

如果全部写在代码里,业务人员每次改一个规则,都要走这样的流程:

业务反馈 -> 产品转述 -> 工程师改 prompt -> 本地调环境 -> 部署 -> 业务再试 -> 再反馈

这条链路看起来正常,实际效率很低。尤其是 prompt 和抽取规则这种东西,本来就需要反复试。工程师每次都要开 terminal、拉环境、处理 key、看日志,业务人员只能等。等多了以后,大家就会倾向于“不改了,先这样用吧”。

Dify Workflow 的独特作用就在这里:它让业务人员可以在一个相对安全的界面里调整部分流程。没有 terminal,没有本地环境,没有 Python 包版本问题,也不需要知道 Lambda 怎么部署。他们可以改 prompt,看中间输出,调字段说明,跑几条样例,先把业务规则打磨到可用。

这不是说 Dify 应该接管整个系统。恰恰相反,我更倾向于把它限制在合适的位置:

适合交给 Dify 的节点不适合直接交给 Dify 的节点
元信息抽取大批量 PDF 解析
摘要 / 评分 / 分类文件下载与权限校验
query rewrite向量库写入和迁移
简单工具编排任务状态机
业务规则试验成本敏感的大规模批处理

Dify 最适合做的是“业务可调的 LLM 节点”。它不应该替代任务表,也不应该替代底层队列,更不应该把所有长任务都包进去。它应该像流水线里的一个可拆模块:输入明确,输出明确,版本明确,失败可回放。

这样用,Dify 的价值就很明显了。工程师负责边界、状态、部署和成本控制;业务人员负责规则、字段、分类和样例质量。两边都不用假装自己会做对方的工作。


RAG 的噩梦:算法更新之后怎么重跑

RAG 系统最怕的不是第一次跑不起来,而是第二次、第三次、第四次改进时不知道怎么处理旧数据。

一个很常见的场景:

第一版上线时,你用 embedding-model-a,chunk 策略是 800 tokens,metadata 里只有标题、日期、来源。跑了一段时间后,发现召回不稳定,于是你做了三件事:

  • 换成新的 embedding 模型;
  • chunk 从固定长度改成按标题层级切;
  • metadata 里补上公司实体、行业、报告类型。

这时候你会发现,线上库里已经有一批旧数据。它们不是错的,但它们和新数据不一样。新旧数据混在一起,会带来几个问题:

  • 检索结果不可比:同一个 query,旧 chunk 和新 chunk 的粒度不同;
  • metadata filter 不完整:旧数据没有新字段,过滤时会被排除或误伤;
  • rerank 结果不稳定:新旧文本块结构差异太大;
  • 成本不可控:如果全量重跑,解析、LLM、embedding、上传都会再付一次钱;
  • 线上风险变大:重跑过程中如果覆盖错误,用户会直接看到结果波动。

这就是为什么我说 RAG 的噩梦是重跑。

解决它不能靠“以后注意”。需要从第一版开始就设计几个字段。

字段为什么需要
source_file_id追溯回原始文件
parse_version知道解析产物来自哪个解析器 / 参数
meta_version知道打标 prompt 和字段规则
chunk_version知道 chunk 策略
embedding_model知道向量来自哪个模型
index_version支持新旧索引并行
task_status支持失败重试和人工检查
last_error不要让失败变成沉默

有了这些字段,重跑才有操作空间。

比如只是改了打标 prompt,那么从 meta_version 开始重跑;如果改了 chunk 策略,那就从 chunk 节点开始;如果只换 embedding 模型,可以直接用已经生成好的 chunk 重新 embedding。最理想的情况,是每次变更都能回答两个问题:

  1. 哪些数据受影响?
  2. 从哪个节点开始重跑成本最低?

答不上来,就会回到那句熟悉的话:要不全量再跑一遍?

这句话在 demo 阶段没什么,在一天一千份 PDF 的系统里就很贵。


重跑不是异常流程,而是正常流程

很多系统把重跑当成异常处理:出了问题才手动点一下,或者临时写脚本补一次。

我现在更倾向于把重跑当成正常流程的一部分。因为 RAG 系统一定会重跑。解析器会升级,prompt 会升级,embedding 会升级,向量库 schema 会升级。只要系统还在变好,重跑就不会消失。

所以任务表里最好不要只有一个 status。可以更细一点:

raw_ready
parse_pending / parsing / parse_done / parse_failed
meta_pending / meta_running / meta_done / meta_failed
chunk_pending / chunk_done / chunk_failed
embedding_pending / embedding_done / embedding_failed
index_pending / index_done / index_failed

不一定要完全照这个名字来,但状态要能表达“卡在哪一步”。否则失败只会变成一个笼统的 Fail,排查起来很累。

另外,重跑最好不要覆盖旧结果。更稳妥的方式是新旧版本并行一段时间:

  • 新解析产物写到新的 parse_version
  • 新向量写到新的 collection 或新的 index_version
  • 线上查询先灰度到一部分用户或一部分租户;
  • 评估通过后再切换默认版本;
  • 老版本保留一段时间,确认无误再清理。

这看起来麻烦,但比线上结果突然变差要好。RAG 系统的很多问题不是“完全不能用”,而是“看起来能用,但关键时候答错”。这种问题最难发现,也最容易损害信任。


这一章真正想说的

可拆卸的处理节点不是为了架构好看,而是为了以后还敢改。

Dify Workflow 也不是为了让工程师少写代码,而是让业务人员能参与那些本来就属于业务判断的节点。没有 terminal,没有环境问题,没有“你等我部署一下”。这会显著缩短打标、摘要、分类这些规则的迭代周期。

而重跑,是 RAG 从 demo 走向生产之后一定会面对的问题。只要算法、模型、解析器、prompt 还会更新,就必须提前设计版本、状态和重跑路径。

下一章可以继续往下写:一个最小任务表应该怎么设计,以及如何用 EventBridge / Lambda / Dify Workflow 把这些节点串起来。那会更接近代码,也更接近真正能跑的系统。