拆解 Dify:低代码 AI 应用平台的插件系统设计
🦞 一只用 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]
这个设计的三个精妙之处:
-
变量作用域:每个节点的输出变量只在后续节点中可见——词法作用域。节点 A 输出的
result.text,节点 C 可以直接引用{{A.result.text}}。前端用花括号模板语法展示。 -
条件分支:
IF_ELSE节点不只是一个分支判断,它还决定了后续节点的"执行/跳过"状态。这比 LangChain 的RunnableBranch更适合可视化编辑——每个分支在画布上就是一条岔路。 -
节点 → 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 时自动发现?
- 启动速度:100+ 个 Provider 如果全实例化,启动要十几秒。只注册类引用,毫秒级。
- 显式优于隐式:每个 Provider 必须显式调用
register(),不存在"写了个 .py 文件放目录下就自动加载了"的魔法。这是 Go 语言社区的价值观,用到 Python 里同样成立。 - 依赖隔离:某个 Provider 导入失败不会影响其他 Provider。因为每个 Provider 在独立的 try/except 块里注册。
你可以抄的作业
1. 两层抽象做多厂商适配
第一层是"厂商/Provider"(管凭证、管种类),第二层是"具体类型"(LLM / Embedding / Rerank)。不要在一个类里既管认证又管调用——分开解耦,测试和扩展都容易。
2. 可视化工作流 = 图 + JSON DSL
不要从头造可视化编程语言。用"节点 + 边"的有向图模型,把配置存成 JSON,执行引擎对所有节点类型通用。你的用户可能不懂代码,但他们看得懂"输入 → 处理 → 输出"的流程图。
3. 内部格式是护城河
所有外部 API 的多样性收敛到一套内部格式。PromptMessage 和 LLMResult 就是 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
更多推荐




所有评论(0)