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_version、prompt_version、chunk_version、embedding_model、index_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。最理想的情况,是每次变更都能回答两个问题:
- 哪些数据受影响?
- 从哪个节点开始重跑成本最低?
答不上来,就会回到那句熟悉的话:要不全量再跑一遍?
这句话在 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 把这些节点串起来。那会更接近代码,也更接近真正能跑的系统。