RAG 系统最终检索的不是用户上传的原始文件,而是经过解析、清洗、分块和索引后得到的 Chunk。任何一个环节出错,都会让后续模型建立在错误知识上。

Dify 的 RAG Pipeline 把文档入库过程从固定代码变成可编排工作流。它不仅让用户调整处理步骤,也要求系统解决异步任务、索引状态、配置发布和数据一致性问题。

本文只关注 Dify 的工程实现思路,分块、Embedding 和 Rerank 的基础原理可参考本站其他专题文章。


一、为什么入库需要 Pipeline

1.1 固定流程的局限

最简单的知识库流程是:

1
上传文件 → 提取文本 → 固定长度分块 → Embedding → 写入向量库

真实数据源却差异很大:

  • PDF 需要版面解析和 OCR;
  • Markdown 需要保留标题层级;
  • 表格不能按普通段落切分;
  • 网页需要去除导航、广告和脚注;
  • 在线文档需要 OAuth 和增量同步;
  • 不同业务还要脱敏、分类或提取元数据。

如果所有逻辑都塞进一个 index_document(),新增规则会不断增加条件分支,也很难观察究竟在哪一步失败。

1.2 Pipeline 的核心价值

Pipeline 将每一步变成有输入、输出和状态的节点:

flowchart LR
    Source["数据源"] --> Extract["解析 / OCR"]
    Extract --> Clean["清洗与标准化"]
    Clean --> Split["结构化分块"]
    Split --> Metadata["元数据增强"]
    Metadata --> Embed["Embedding"]
    Embed --> Index["写入索引"]

这样可以:

  • 为不同数据源复用或替换节点;
  • 单独预览某一步输出;
  • 记录阶段状态和耗时;
  • 失败后从明确阶段重试;
  • 将处理配置作为 DSL 导入导出;
  • 发布新版本时保留旧配置。

二、Dify RAG Pipeline 的组成

2.1 Pipeline、Workflow 与 Dataset

三个概念分别承担不同职责:

对象 职责
Pipeline 知识处理应用的身份和发布入口
Workflow 节点图、变量和具体处理配置
Dataset 文档、Chunk、检索配置和索引绑定

Pipeline 负责“怎么处理”,Dataset 负责“处理后的知识存在哪里并如何检索”。二者关联,但不应合成同一个对象。

2.2 草稿与发布版本

用户在界面编辑的是草稿工作流。发布时生成不可变版本,并把知识索引节点中的关键配置同步到 Dataset:

  • 索引方式;
  • Embedding Provider 和模型;
  • Chunk 结构;
  • 检索方式;
  • Rerank 与分数阈值;
  • 摘要索引配置。

运行任务应绑定一个明确版本,不能在执行中读取不断变化的草稿。否则同一批文档可能被不同配置处理。

2.3 数据源插件

Datasource 插件是 Pipeline 的起点,可表示:

  • 本地上传文件;
  • 在线网盘;
  • 企业文档系统;
  • 网站抓取;
  • 其他外部内容源。

数据源节点不应该直接生成向量,它只负责可靠地产生统一文档对象和来源元数据。认证、分页、增量游标和删除同步属于数据源职责。


三、从文档到索引

3.1 Extract、Transform、Load

Dify 的 BaseIndexProcessor 抽象了几个关键阶段:

1
2
3
4
5
6
class BaseIndexProcessor:
def extract(self, extract_setting, *, session): ...
def transform(self, documents, *, session): ...
def load(self, dataset, documents, *, session): ...
def clean(self, dataset, node_ids, *, session): ...
def index(self, dataset, document, chunks, session): ...

可以把它理解为面向 RAG 的 ETL:

  • extract:从文件、网页或存储中得到标准文档;
  • transform:清洗、切分并生成 Chunk;
  • load/index:计算关键词或向量并写入检索存储;
  • clean:文档更新或删除时移除旧索引。

3.2 标准文档模型

解析器输出应包含:

1
正文 + 来源 + 页码/标题层级 + 文件信息 + 自定义元数据

后续节点只依赖统一模型,而不关心原始文件是 PDF 还是在线文档。保留来源定位非常重要,否则检索结果无法引用原文。

3.3 分块结构

Dify 支持普通段落和父子分块等结构:

结构 索引与召回方式 适用场景
普通段落 每个 Chunk 独立索引与返回 短文、FAQ、结构简单内容
父子分块 子块用于精确召回,父块提供完整上下文 长文、章节和复杂说明书

分块结构一旦投入使用,不应随意原地修改。因为数据库中的 Segment、向量库记录和引用关系都依赖该结构。重大变化通常需要重建索引。

3.4 High Quality 与 Economy

可以简化理解为:

  • High Quality 使用 Embedding,支持语义、全文、混合检索和 Rerank;
  • Economy 主要使用关键词索引,成本低但语义能力有限。

选择索引方式会影响存储结构和后续检索能力,不能只作为界面显示选项。


四、异步索引与状态机

4.1 为什么必须异步

解析大文件、调用 Embedding 和批量写向量库可能持续数分钟。如果在上传请求中同步完成:

  • HTTP 请求容易超时;
  • 一个文件会长期占用 Web Worker;
  • 无法显示细粒度进度;
  • 批量上传难以限流;
  • 失败重试会重复整个请求。

因此 API 只创建 Document 和任务,Celery Worker 在后台执行索引。

4.2 文档状态

Dify 会记录多个阶段时间:

1
2
3
4
5
6
WAITING
→ PARSING
→ CLEANING
→ SPLITTING
→ INDEXING
→ COMPLETED

还需要 PAUSEDERRORSTOPPED 等分支,以及:

  • processing_started_at
  • parsing_completed_at
  • cleaning_completed_at
  • splitting_completed_at
  • completed_at
  • error
  • 已完成与总 Segment 数。
stateDiagram-v2
    [*] --> Waiting
    Waiting --> Parsing
    Parsing --> Cleaning
    Cleaning --> Splitting
    Splitting --> Indexing
    Indexing --> Completed
    Parsing --> Error
    Cleaning --> Error
    Splitting --> Error
    Indexing --> Error
    Error --> Parsing: Retry
    Waiting --> Stopped: Cancel
    Parsing --> Paused: Pause
    Paused --> Parsing: Resume

状态不仅用于界面进度,也是重试和清理旧数据的依据。

4.3 数据库事务边界

长时间调用解析器、模型和向量库时,不应一直占用数据库事务:

  1. 短事务读取任务与配置;
  2. 写入当前阶段并提交;
  3. 释放数据库连接;
  4. 执行外部耗时操作;
  5. 新事务保存结果和下一状态。

否则 Worker 并发增加后,连接池会先于 CPU 和模型配额耗尽。

4.4 幂等与重试

任务重试不能简单地再次插入所有 Chunk。推荐做法:

  • 为一次索引生成批次 ID;
  • Segment 使用稳定业务标识;
  • 写索引使用 upsert;
  • 重建前明确清理旧向量节点;
  • 状态转换使用条件更新,防止两个 Worker 同时处理;
  • 失败只重试可恢复阶段;
  • 完成后再切换可见版本。

五、配置变更与索引一致性

5.1 哪些变更需要重建

配置变更 是否通常需要重建
文档显示名称
检索 Top K、分数阈值
Rerank 模型 通常否
Embedding 模型
分块大小、重叠和分隔符
普通分块切换父子分块
清洗规则
摘要索引策略 取决于是否影响已有摘要向量

Embedding 模型变化后,新旧向量不能直接混用。即使维度相同,向量空间也不一定兼容。

5.2 安全发布

生产级重建可以采用双版本:

1
2
3
4
5
ACTIVE 索引继续服务
↓ 后台按新配置构建
STAGING 索引校验完整性和质量
↓ 原子切换
新 ACTIVE 上线,旧索引延迟回收

这比边删除边重建更可靠,避免长时间检索不到知识。

5.3 删除与更新

删除文档需要同时处理:

  • Document 记录;
  • Segment 与子 Chunk;
  • 向量索引节点;
  • 关键词倒排数据;
  • 关联文件和附件;
  • 缓存与检索引用。

应先标记不可检索,再异步清理物理数据。这样即使向量库暂时故障,也不会继续返回已删除内容。


六、从索引到检索闭环

入库配置最终要服务检索:

1
Query → 路由知识库 → 召回 → 元数据过滤 → Rerank → 阈值过滤 → 上下文组装

Dify 根据 Dataset 的索引方式选择关键词、语义、全文或混合检索,再应用 Top K、Score Threshold 和 Rerank。

检索质量问题不能只在查询侧修补:

  • 召回不到,可能是解析或分块丢失内容;
  • 召回太散,可能是 Chunk 太小或缺少标题;
  • 相似度异常,可能是查询和文档使用了不同 Embedding;
  • 引用错误,可能是入库时没有保留来源定位。

因此需要把“入库版本—索引版本—检索日志—最终回答”串联起来,才能形成可评测闭环。


七、排障与工程清单

文档长期停留在 Queuing 时,按顺序检查:

  1. API 是否成功投递 Celery 任务;
  2. Worker 是否监听对应 Dataset/Pipeline 队列;
  3. Redis 是否存在积压;
  4. Document 状态和错误字段是否更新;
  5. 解析器、对象存储和远程文件是否可访问;
  6. Embedding 凭证、限流和超时是否正常;
  7. 向量库是否可写,集合维度是否匹配;
  8. 是否有重复任务锁或陈旧缓存;
  9. 重试前是否正确清理半成品索引。

生产环境还应监控:每阶段耗时、队列等待时间、Chunk 数、Embedding Token、失败率和索引写入延迟。


八、总结

Dify RAG Pipeline 的工程价值可以概括为:

  1. 用 Workflow 描述可变的知识处理步骤;
  2. 用 Dataset 承载文档、索引和检索配置;
  3. 数据源插件只负责可靠地产生统一文档;
  4. IndexProcessor 抽象 Extract、Transform、Load 和 Clean;
  5. Celery 将长时间索引移出请求线程;
  6. 细粒度状态支持进度、重试和排障;
  7. Embedding、分块等重大变更触发索引重建;
  8. 发布版本与索引版本关联,避免草稿污染生产;
  9. 入库指标和检索效果共同构成质量闭环。

RAG Pipeline 不是把几个处理函数画成流程图,而是让数据加工变得可版本化、可恢复、可观测和可评测。