拆解 Dify:低代码 AI 应用平台的插件系统设计
一只用 AI Agent 搭副业产线的程序员我在公司有一次要给业务团队搭一个合同条款风险审查的 AI 应用。技术方案很清楚——RAG 规则引擎 LLM。问题是怎么让不懂代码的法务同事自己调 prompt、换模型、改规则答案是一套好用的插件系统和可视化工作流。Dify 在这方面做得最好。这篇文章拆 Dify 的插件系统——不是怎么用而是它怎么设计的。项目简介DifyGitHub 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/ —— 简化重构classModelProvider:模型厂商的抽象provider_name:str# openai, anthropic, deepseekprovider_type:str# custom or built-in# 支持的模型类型supported_model_types:List[ModelType]# [LLM, EMBEDDING, RERANK, SPEECH2TEXT]defvalidate_credentials(self,credentials:dict)-bool:验证 API Key 是否有效...defget_model_instance(self,model_type:ModelType)-Model:获取具体模型类型的实例...classLLMModel(Model):LLM 模型的具体实现model_schemas:List[ModelSchema]# 参数定义temperature, top_p, max_tokens...definvoke(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 —— 内部统一格式classPromptMessage:Dify 内部的消息格式——不管外面是 OpenAI 还是 Anthropicrole:PromptMessageRole# SYSTEM, USER, ASSISTANT, TOOLcontent:strname:Optional[str]classLLMResult:Dify 内部的 LLM 返回格式model:strmessage:PromptMessage usage:LLMUsage# prompt_tokens, completion_tokensfinish_reason:str设计洞察做任何多厂商集成核心都是收敛到内部格式。外部世界越乱内部抽象越要稳。Dify 的PromptMessage就是它的真理层——不管你外面是 OpenAI function calling 还是 Anthropic tool use进来都是同一种消息格式。关键设计二Tool Provider——不只是调 APIDify 的 Tool 抽象和 LangChain 的不同。LangChain 的 Tool 重点在描述给 LLMDify 的 Tool 重点在在后端执行、在前端配置。# api/core/tools/ —— Tool 提供者的抽象概念性重建classToolProvider:工具提供者GoogleSearch / DALL-E / CodeInterpreter / ...identity:ToolIdentity# name, author, description, iconcredentials_schema:dict# 需要用户填的配置API Key...defget_tools(self,credentials:dict)-List[Tool]:根据用户配置返回这个 Provider 下所有 Tool...classTool:单个工具比如 GoogleSearchProvider 下的 search()name:strdescription:str# LLM 版描述# 参数定义——运行时动态生成defget_parameters(self)-List[ToolParameter]:返回参数的 JSON Schema...# 执行——在 Dify Worker 中异步跑definvoke(self,user_id:str,parameters:dict)-ToolInvokeResult:执行工具调用...Dify Tool 和 LangChain Tool 的核心区别维度LangChain ToolDify Tool谁定义参数程序员写 Pydantic model在 UI 上点击配置谁调用Python 代码Dify Workflow 引擎返回值字符串结构化的 dict/文件/列表凭证管理代码里硬编码或环境变量平台统一管理、加密存储Dify 的 Tool 是为让非程序员在 UI 上配置 AI 应用服务的不是为让程序员写代码调 API服务的。目标用户不同抽象就不同。关键设计三Workflow DSL——节点 边 变量的图模型Dify 的工作流本质上是一个有向图。每个节点是一个执行单元每条边是数据依赖。# api/core/workflow/ —— 工作流引擎的核心数据结构概念性重建fromtypingimportDict,List,AnyfromenumimportEnumclassNodeType(str,Enum):STARTstartLLMllmKNOWLEDGE_RETRIEVALknowledge-retrievalCODEcodeIF_ELSEif-elseHTTP_REQUESThttp-requestTOOLtoolVARIABLE_AGGREGATORvariable-aggregatorENDendclassWorkflowNode:id:strnode_type:NodeType title:strposition:Dict[str,float]# 在画布上的位置x, ydata:Dict[str,Any]# 节点配置prompt、model、参数...classWorkflowEdge:source:str# 源节点 IDtarget:str# 目标节点 IDsource_handle:str# 源输出端口一个节点可能有多个输出target_handle:str# 目标输入端口classWorkflow:nodes:List[WorkflowNode]edges:List[WorkflowEdge]environment_variables:Dict[str,str]# 环境变量conversation_variables:Dict[str,Any]# 对话变量跨轮次保持工作流执行引擎的核心是一个 BFS/拓扑排序# 简化的执行逻辑classWorkflowEngine:defrun(self,workflow:Workflow,inputs:dict)-dict:# Step 1: 拓扑排序——确定执行顺序execution_orderself._topological_sort(workflow)# Step 2: 按顺序执行每个节点node_outputs:Dict[str,Any]{}fornode_idinexecution_order:nodeworkflow.nodes[node_id]# 收集所有上游节点的输出作为当前节点的输入upstream_outputs{edge.source_handle:node_outputs[edge.source]foredgeinworkflow.edgesifedge.targetnode_id}# 执行节点resultself._execute_node(node,upstream_outputs,inputs)node_outputs[node_id]result# 如果是条件分支确定下一跳ifnode.node_typeNodeType.IF_ELSE:branchresult[branch]# 只沿匹配的分支继续执行另一分支的节点标记为跳过...returnnode_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/ —— 插件注册表概念性重建classPluginRegistry:全局插件注册表_providers:Dict[str,Type[ModelProvider]]{}classmethoddefregister(cls,provider_class:Type[ModelProvider]):注册一个 Provider在模块导入时自动调用providerprovider_class()cls._providers[provider.provider_name]provider_classclassmethoddefget_provider(cls,provider_name:str)-ModelProvider:懒加载第一次调用时才创建实例ifprovider_namenotincls._providers:raiseProviderNotFoundError(provider_name)returncls._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