🦞 一只用 AI Agent 搭副业产线的程序员


我在公司有一次要给业务团队搭一个"合同条款风险审查"的 AI 应用。技术方案很清楚——RAG + 规则引擎 + LLM。问题是:怎么让不懂代码的法务同事自己调 prompt、换模型、改规则?

答案是一套好用的插件系统和可视化工作流。Dify 在这方面做得最好。

这篇文章拆 Dify 的插件系统——不是怎么用,而是它怎么设计的。


项目简介

Dify(GitHub 60k+ Stars)是一个开源的低代码 AI 应用开发平台。你可以用它拖拽式地搭建 RAG 应用、Agent、工作流。它的核心竞争力在于插件系统和可视化工作流引擎——通过一套统一的抽象,把 LLM、向量库、工具、Embedding 模型都变成可拖拽的"节点"。


架构全景

┌──────────────────────────────────────────────────────────┐
│                    Web 前端(Next.js)                     │
│   可视化工作流编辑器 · 应用管理 · 对话调试 · 数据集管理      │
├──────────────────────────────────────────────────────────┤
│                    API 层(Flask)                         │
│   Controller → Service → Core —— 经典三层架构              │
├──────────────────────────────────────────────────────────┤
│                    核心引擎                                │
│   ┌───────────┐  ┌────────────┐  ┌─────────────────┐    │
│   │ Workflow  │  │  Plugin     │  │  Dataset        │    │
│   │ 工作流引擎 │  │  插件注册表  │  │  知识库引擎      │    │
│   └───────────┘  └────────────┘  └─────────────────┘    │
├──────────────────────────────────────────────────────────┤
│                   插件层(Provider 抽象)                    │
│   LLM  ·  Embedding  ·  VectorStore  ·  Tool  ·  ...    │
└──────────────────────────────────────────────────────────┘

关键设计一:Model Provider 的适配器模式

Dify 要接 100+ 个模型厂商。如果每个厂商写一套集成代码,维护成本会爆炸。

它的解是 Provider + ModelType 两层抽象

# api/core/model_runtime/model_providers/ —— 简化重构
class ModelProvider:
    """模型厂商的抽象"""
    provider_name: str                    # "openai", "anthropic", "deepseek"
    provider_type: str                    # "custom" or "built-in"

    # 支持的模型类型
    supported_model_types: List[ModelType]  # [LLM, EMBEDDING, RERANK, SPEECH2TEXT]

    def validate_credentials(self, credentials: dict) -> bool:
        """验证 API Key 是否有效"""
        ...

    def get_model_instance(self, model_type: ModelType) -> "Model":
        """获取具体模型类型的实例"""
        ...


class LLMModel(Model):
    """LLM 模型的具体实现"""
    model_schemas: List[ModelSchema]  # 参数定义:temperature, top_p, max_tokens...

    def invoke(self, model: str, credentials: dict,
               prompt_messages: List[PromptMessage], parameters: dict):
        """统一调用接口"""
        # 1. 把 Dify 的内部 PromptMessage 转成厂商 API 格式
        # 2. 调厂商 API
        # 3. 把厂商 API 返回转成 Dify 内部 LLMResult
        ...

每接入一个新厂商(比如 Anthropic),只需要:

api/core/model_runtime/model_providers/anthropic/
├── anthropic.yaml              # 厂商元数据
├── llm/
│   ├── claude-3-opus.yaml       # 模型参数定义
│   └── _client.py               # API 调用的薄适配层
├── _common.py                   # 公共工具函数
└── _position.yaml               # 在 UI 中的展示位置

适配的关键逻辑:所有厂商的 API 调用最终都要收敛到 Dify 的内部格式。外部格式多样化,内部格式统一化。

# api/core/model_runtime/entities/llm_entities.py —— 内部统一格式
class PromptMessage:
    """Dify 内部的消息格式——不管外面是 OpenAI 还是 Anthropic"""
    role: PromptMessageRole   # SYSTEM, USER, ASSISTANT, TOOL
    content: str
    name: Optional[str]

class LLMResult:
    """Dify 内部的 LLM 返回格式"""
    model: str
    message: PromptMessage
    usage: LLMUsage  # prompt_tokens, completion_tokens
    finish_reason: str

设计洞察:做任何多厂商集成,核心都是"收敛到内部格式"。外部世界越乱,内部抽象越要稳。Dify 的 PromptMessage 就是它的"真理层"——不管你外面是 OpenAI function calling 还是 Anthropic tool use,进来都是同一种消息格式。


关键设计二:Tool Provider——不只是调 API

Dify 的 Tool 抽象和 LangChain 的不同。LangChain 的 Tool 重点在"描述给 LLM";Dify 的 Tool 重点在"在后端执行、在前端配置"。

# api/core/tools/ —— Tool 提供者的抽象(概念性重建)
class ToolProvider:
    """工具提供者:GoogleSearch / DALL-E / CodeInterpreter / ..."""
    identity: ToolIdentity       # name, author, description, icon
    credentials_schema: dict     # 需要用户填的配置(API Key...)

    def get_tools(self, credentials: dict) -> List[Tool]:
        """根据用户配置返回这个 Provider 下所有 Tool"""
        ...


class Tool:
    """单个工具:比如 GoogleSearchProvider 下的 search()"""
    name: str
    description: str             # LLM 版描述

    # 参数定义——运行时动态生成
    def get_parameters(self) -> List[ToolParameter]:
        """返回参数的 JSON Schema"""
        ...

    # 执行——在 Dify Worker 中异步跑
    def invoke(self, user_id: str, parameters: dict) -> ToolInvokeResult:
        """执行工具调用"""
        ...

Dify Tool 和 LangChain Tool 的核心区别

维度 LangChain Tool Dify Tool
谁定义参数 程序员写 Pydantic model 在 UI 上点击配置
谁调用 Python 代码 Dify Workflow 引擎
返回值 字符串 结构化的 dict/文件/列表
凭证管理 代码里硬编码或环境变量 平台统一管理、加密存储

Dify 的 Tool 是为"让非程序员在 UI 上配置 AI 应用"服务的,不是为"让程序员写代码调 API"服务的。目标用户不同,抽象就不同。


关键设计三:Workflow DSL——节点 + 边 + 变量的图模型

Dify 的工作流本质上是一个有向图。每个节点是一个执行单元,每条边是数据依赖。

# api/core/workflow/ —— 工作流引擎的核心数据结构(概念性重建)
from typing import Dict, List, Any
from enum import Enum

class NodeType(str, Enum):
    START = "start"
    LLM = "llm"
    KNOWLEDGE_RETRIEVAL = "knowledge-retrieval"
    CODE = "code"
    IF_ELSE = "if-else"
    HTTP_REQUEST = "http-request"
    TOOL = "tool"
    VARIABLE_AGGREGATOR = "variable-aggregator"
    END = "end"

class WorkflowNode:
    id: str
    node_type: NodeType
    title: str
    position: Dict[str, float]  # 在画布上的位置(x, y)
    data: Dict[str, Any]        # 节点配置(prompt、model、参数...)

class WorkflowEdge:
    source: str        # 源节点 ID
    target: str        # 目标节点 ID
    source_handle: str # 源输出端口(一个节点可能有多个输出)
    target_handle: str # 目标输入端口

class Workflow:
    nodes: List[WorkflowNode]
    edges: List[WorkflowEdge]
    environment_variables: Dict[str, str]  # 环境变量
    conversation_variables: Dict[str, Any] # 对话变量(跨轮次保持)

工作流执行引擎的核心是一个 BFS/拓扑排序:

# 简化的执行逻辑
class WorkflowEngine:
    def run(self, workflow: Workflow, inputs: dict) -> dict:
        # Step 1: 拓扑排序——确定执行顺序
        execution_order = self._topological_sort(workflow)

        # Step 2: 按顺序执行每个节点
        node_outputs: Dict[str, Any] = {}
        for node_id in execution_order:
            node = workflow.nodes[node_id]

            # 收集所有上游节点的输出作为当前节点的输入
            upstream_outputs = {
                edge.source_handle: node_outputs[edge.source]
                for edge in workflow.edges
                if edge.target == node_id
            }

            # 执行节点
            result = self._execute_node(node, upstream_outputs, inputs)
            node_outputs[node_id] = result

            # 如果是条件分支,确定下一跳
            if node.node_type == NodeType.IF_ELSE:
                branch = result["branch"]
                # 只沿匹配的分支继续执行,另一分支的节点标记为跳过
                ...

        return node_outputs[workflow.end_node_id]

这个设计的三个精妙之处

  1. 变量作用域:每个节点的输出变量只在后续节点中可见——词法作用域。节点 A 输出的 result.text,节点 C 可以直接引用 {{A.result.text}}。前端用花括号模板语法展示。

  2. 条件分支IF_ELSE 节点不只是一个分支判断,它还决定了后续节点的"执行/跳过"状态。这比 LangChain 的 RunnableBranch 更适合可视化编辑——每个分支在画布上就是一条岔路。

  3. 节点 → JSON DSL:工作流在前端是拖拽出的可视化图,在后端是一个 JSON 结构。同一个工作流可以在开发环境编辑、生产环境执行,JSON 是它们的交换格式。


核心代码拆解:插件注册的懒加载机制

Dify 启动时不会加载所有插件——只加载元数据,真正的 Provider 实例在第一次使用时才创建:

# api/core/model_runtime/ —— 插件注册表(概念性重建)
class PluginRegistry:
    """全局插件注册表"""
    _providers: Dict[str, Type[ModelProvider]] = {}

    @classmethod
    def register(cls, provider_class: Type[ModelProvider]):
        """注册一个 Provider(在模块导入时自动调用)"""
        provider = provider_class()
        cls._providers[provider.provider_name] = provider_class

    @classmethod
    def get_provider(cls, provider_name: str) -> ModelProvider:
        """懒加载:第一次调用时才创建实例"""
        if provider_name not in cls._providers:
            raise ProviderNotFoundError(provider_name)
        return cls._providers[provider_name]()


# 每个 Provider 文件底部:
# PluginRegistry.register(OpenAIProvider)
# PluginRegistry.register(AnthropicProvider)
# PluginRegistry.register(DeepSeekProvider)

为什么用注册表 + 懒加载而不是 import 时自动发现?

  1. 启动速度:100+ 个 Provider 如果全实例化,启动要十几秒。只注册类引用,毫秒级。
  2. 显式优于隐式:每个 Provider 必须显式调用 register(),不存在"写了个 .py 文件放目录下就自动加载了"的魔法。这是 Go 语言社区的价值观,用到 Python 里同样成立。
  3. 依赖隔离:某个 Provider 导入失败不会影响其他 Provider。因为每个 Provider 在独立的 try/except 块里注册。

你可以抄的作业

1. 两层抽象做多厂商适配

第一层是"厂商/Provider"(管凭证、管种类),第二层是"具体类型"(LLM / Embedding / Rerank)。不要在一个类里既管认证又管调用——分开解耦,测试和扩展都容易。

2. 可视化工作流 = 图 + JSON DSL

不要从头造可视化编程语言。用"节点 + 边"的有向图模型,把配置存成 JSON,执行引擎对所有节点类型通用。你的用户可能不懂代码,但他们看得懂"输入 → 处理 → 输出"的流程图。

3. 内部格式是护城河

所有外部 API 的多样性收敛到一套内部格式。PromptMessageLLMResult 就是 Dify 的"铁打营盘",外面厂商 API 是"流水的兵"。这层抽象越稳,你的系统越不容易被依赖变更击穿。

4. 懒加载不只省内存

插件注册表的懒加载省的不只是启动时间,更是降低模块间的耦合程度。A 插件的加载失败不影响 B 插件——因为根本没加载。


最后

Dify 的插件系统没有发明什么新算法,但它把经典的设计模式(Adapter、Registry、Lazy Loading)用到了对的地方。这不是学术贡献,是工程品味的体现——知道什么时候该用成熟方案,知道怎么把成熟方案组合成新的产品形态。

做平台类产品,技术难点往往不在"单个功能怎么实现",而在"100 个功能怎么不互相踩脚"。

下一讲拆 FastGPT。跟 Dify 相比,它把知识库、工作流、对话三者合一的设计有什么不同?


本文拆解的 Dify 版本:v0.15.x。源码地址:github.com/langgenius/dify


🦞 一只用 AI Agent 搭副业产线的程序员

全平台同名:虾哥不加班 | 源码:GitHub - lobster-bujiaban
需要定制 AI 工具?来聊聊 → lob_ai

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐