Read OSS

RAG 流水线:文档索引、向量存储与知识检索

高级

前置知识

  • 第 1-2 篇:架构概览与请求流程
  • 了解文本嵌入和向量相似度搜索的基本概念
  • 熟悉 RAG(检索增强生成)的相关概念

RAG 流水线:文档索引、向量存储与知识检索

检索增强生成(RAG)是构建知识驱动型 LLM 应用的核心支撑。在 Dify 中,RAG 流水线负责处理从 PDF 上传到在对话中返回排序文档片段的全部流程。本文将从两个维度系统梳理这条流水线:将文档转化为可搜索向量的索引路径,以及在查询时找到相关知识的检索路径。在此过程中,我们还会深入探讨 30 余种向量数据库适配器、多数据集路由策略,以及支持细粒度检索的父子分块模型。

文档索引流水线

文档索引由 IndexingRunner 类统一调度,整个流程分为多个阶段:

flowchart TD
    Upload[Document Upload] --> Task[Celery Task<br/>document_indexing_task]
    Task --> Runner[IndexingRunner.run]
    Runner --> Extract[Extract Text<br/>PDF, DOCX, HTML, etc.]
    Extract --> Clean[Clean Text<br/>remove extras, normalize]
    Clean --> Split[Split Text<br/>FixedRecursiveCharacterTextSplitter]
    Split --> Embed[Generate Embeddings<br/>via ModelInstance]
    Embed --> Store[Store in Vector DB<br/>+ PostgreSQL metadata]
    Store --> Done[Update Document Status]

    Runner -->|Error| ErrorHandler[Mark Document as ERROR<br/>with error message]

run() 方法会遍历所有文档,依次经过以下四个阶段进行处理:

  1. 提取(Extract) — 根据文档的数据源类型,委托给对应格式的提取器处理
  2. 转换(Transform) — 应用清洗规则,并按照数据集的处理规则将文本切分成块
  3. 加载(Load) — 生成嵌入向量,并将向量与元数据一并存储

每个文档的状态通过 IndexingStatus 枚举进行跟踪(pending → indexing → completed/error/paused),处理进度在 UI 中实时可见。

tasks/document_indexing_task.py 中定义的索引任务被分发到 dataset Celery 队列。这种队列隔离确保索引工作不会与实时工作流执行争抢 worker 资源。任务使用 TenantIsolatedTaskQueue 实现跨租户的公平调度。

提示: 当索引失败并出现难以理解的错误信息时,可以直接查看数据库中的 Document.error 字段。_handle_indexing_error() 方法会将错误信息存储在此处,UI 也会将其展示给用户。

文本提取与分割策略

Dify 通过 core/rag/extractor/ 目录下的提取器类,支持多种格式的文本提取:

格式 提取器 说明
PDF PdfExtractor 使用 pypdf 或 Unstructured API
DOCX WordExtractor 基于 python-docx
HTML HtmlExtractor BeautifulSoup 解析
Markdown MarkdownExtractor 自定义解析器
CSV/Excel CsvExtractor, ExcelExtractor 支持表格数据
Notion NotionExtractor API 集成
网页 WebsiteExtractor Jina Reader / Firecrawl

提取完成后,由 FixedRecursiveCharacterTextSplitter 负责文本分块。该分割器支持按数据集进行配置,主要参数包括:

  • 块大小(Chunk size) — 每个块的最大字符数
  • 块重叠(Chunk overlap) — 相邻块之间共享的字符数
  • 分隔符(Separator) — 分割的层级结构(段落 → 句子 → 单词)

此外,EnhanceRecursiveCharacterTextSplitter 对代码块和结构化内容提供了更优化的处理方式。数据集的 DatasetProcessRule 模型存储当前生效的分割配置,不同数据集可以使用各自独立的分割策略。

向量数据库适配器模式

Dify 最令人印象深刻的工程成就之一,是通过统一接口支持了 32 种向量数据库VectorType 枚举列出了所有支持的类型:

classDiagram
    class BaseVector {
        <<abstract>>
        +create(texts, embeddings)
        +delete()
        +search_by_vector(query_vector)
        +search_by_full_text(query)
    }

    class QdrantVector
    class WeaviateVector
    class MilvusVector
    class PgVectorVector
    class ElasticsearchVector
    class ChromaVector
    class OceanBaseVector
    class TencentVector

    BaseVector <|-- QdrantVector
    BaseVector <|-- WeaviateVector
    BaseVector <|-- MilvusVector
    BaseVector <|-- PgVectorVector
    BaseVector <|-- ElasticsearchVector
    BaseVector <|-- ChromaVector
    BaseVector <|-- OceanBaseVector
    BaseVector <|-- TencentVector
    note for BaseVector "32 implementations total"

每个适配器位于 core/rag/datasource/vdb/ 目录下,实现了 BaseVector 接口,提供向量的创建、删除和搜索等方法。vector_factory.py 模块根据数据集配置动态解析并加载对应的适配器。

api/configs/middleware/vdb/ 下的配置类为每种数据库提供了类型安全的配置项。正如第 1 篇所介绍的,这些配置通过 Pydantic 多重继承组合进 MiddlewareConfig

部分适配器支持混合搜索(将向量相似度与关键词/全文搜索结合使用):

  • MilvusMILVUS_ENABLE_HYBRID_SEARCH
  • 腾讯向量数据库TENCENT_VECTOR_DB_ENABLE_HYBRID_SEARCH
  • Elasticsearch — 原生支持 BM25 + 向量的混合搜索
  • PgVector — 结合 pg_bigm 实现全文搜索

提示: 在选择向量数据库时,务必考虑是否需要混合搜索。纯语义搜索对精确关键词(如错误码或产品 SKU)的匹配效果较差。选用原生支持混合搜索的数据库,可以省去单独构建关键词索引的成本。

多数据集检索与查询路由

当工作流中的知识检索节点执行时,可能需要跨多个数据集进行搜索。DatasetRetrieval 类提供了两种路由策略来协调这一过程:

flowchart TD
    Query[User Query] --> Strategy{Routing Strategy?}
    Strategy -->|single_dataset| Direct[Search single dataset]
    Strategy -->|function_call| FCRouter[FunctionCallMultiDatasetRouter<br/>LLM selects relevant datasets]
    Strategy -->|react| ReactRouter[ReactMultiDatasetRouter<br/>ReAct reasoning loop]

    FCRouter --> Selected[Selected datasets]
    ReactRouter --> Selected
    Selected --> Parallel[Parallel retrieval<br/>per-dataset strategy]

    Parallel --> Semantic[Semantic Search<br/>vector similarity]
    Parallel --> Keyword[Keyword Search<br/>BM25/full-text]
    Parallel --> Hybrid[Hybrid Search<br/>combined score]

    Semantic --> Merge[Merge results]
    Keyword --> Merge
    Hybrid --> Merge
    Merge --> Rerank[Reranking Pipeline]
    Rerank --> Return[Return top-K results]

FunctionCallMultiDatasetRouter 的设计思路十分简洁:它将每个数据集封装成一个"工具"提供给 LLM,由 LLM 判断哪些数据集与当前查询相关。若只有一个数据集,则直接跳过 LLM 调用。ReactMultiDatasetRouter 则采用 ReAct 推理循环,适用于更复杂的路由决策场景。

选定数据集后,系统会按照各数据集自身配置的检索方式(语义、关键词或混合)进行查询,并支持 top-K、分数阈值、重排序等数据集级别的独立参数配置。

默认检索模型的配置如下:

default_retrieval_model = {
    "search_method": RetrievalMethod.SEMANTIC_SEARCH,
    "reranking_enable": False,
    "top_k": 4,
    "score_threshold_enabled": False,
}

重排序、后处理与父子分块

初步检索完成后,结果会进入 data_post_processor.py 中的后处理流水线。DataPostProcessor 依次执行以下操作:

  1. 重排序(Reranking) — 使用重排序模型(如 Cohere Rerank 或 BGE Reranker)对结果按相关性重新评分,支持基于模型的重排序、加权评分和交叉编码器重排序等多种模式。
  2. 分数阈值过滤 — 移除相似度低于可配置阈值的结果。
  3. 重新排列(Reordering)reorder.py 模块可应用基于多样性的重排策略,避免返回语义重复的文本块。

父子分块模型是 Dify 中较为精妙的 RAG 特性之一。ChildChunk 模型实现了两级分块策略:

erDiagram
    Dataset ||--o{ Document : contains
    Document ||--o{ DocumentSegment : "split into"
    DocumentSegment ||--o{ ChildChunk : "further split into"

    DocumentSegment {
        string id PK
        string content
        string index_node_id
        int word_count
        int position
    }

    ChildChunk {
        string id PK
        string segment_id FK
        string content
        int position
        string index_node_id
    }

父段落(parent segment)提供上下文窗口,子块(child chunk)则支持细粒度匹配。检索时,系统针对子块进行匹配,但最终返回其对应的父段落作为上下文。这一设计有效解决了 RAG 中的经典权衡难题:小块提高检索精度,大块为 LLM 生成提供更好的上下文。

异步索引与进度追踪

索引流水线通过 Celery 异步执行,并提供细粒度的进度追踪。document_indexing_task 是整个流程的入口:

@shared_task(queue="dataset")
def document_indexing_task(dataset_id: str, document_ids: list):
    """Async process document"""
    _document_indexing(dataset_id, document_ids)

dataset 队列隔离确保索引任务不会与实时工作流任务争抢资源。_document_indexing() 函数负责创建 session、获取文档,并将处理委托给 IndexingRunner.run()

进度追踪覆盖多个粒度层级:

  • 文档级别IndexingStatus 枚举(pending/indexing/completed/error/paused)
  • 段落级别SegmentStatus 追踪单个块的处理状态
  • 时间追踪indexing_latencycompleted_atstopped_at 等时间戳字段

系统还支持增量重新索引 — 文档更新时,只需对变更的段落重新生成嵌入并存储,无需处理全部内容。

提示: 对于大批量文档集合,建议关注 Celery dataset 队列的积压深度。若索引速度较慢,可以考虑增大 CELERY_WORKER_AMOUNT 或开启自动扩缩容(CELERY_AUTO_SCALE=true)。租户隔离的任务队列机制可以确保某个租户的批量上传不会阻塞其他租户的任务。

下一步

我们已经完整追踪了文档从上传到向量存储、再到检索返回的全链路。第 5 篇将深入探讨 Dify 如何通过统一接口抽象 100 余种 LLM provider、agent 和工作流中可用的五种工具类型,以及安全执行不受信任代码的插件守护进程架构。