在 Dify 中搭建一个工作流后,可以把它导出为 YAML 文件,再导入另一个环境。这看起来像一次普通的配置复制,背后其实涉及一个重要概念:DSL

DSL 是 Domain-Specific Language 的缩写,即“领域特定语言”。它不追求解决所有编程问题,只负责准确描述某个领域中的对象和规则。

Dify 的 DSL 负责描述一个 AI 应用:它有哪些节点、节点如何连接、使用哪些模型和工具、需要哪些变量与功能配置。

一、DSL 不是数据库备份

数据库备份强调完整恢复,往往包含内部主键、运行记录、密钥和状态。DSL 更像一份可移植的应用说明书,只保留重建应用所需的声明式配置。

一份简化后的 DSL 大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
version: 0.1.5
kind: app
app:
name: 文档问答
mode: advanced-chat
description: 根据内部文档回答问题
workflow:
graph:
nodes: []
edges: []
environment_variables: []
conversation_variables: []
dependencies: []

其中:

  • version 表示 DSL 规范版本;
  • kind 表示文件描述的对象类型;
  • app 保存应用基本信息;
  • workflow 保存节点图、功能配置和变量;
  • dependencies 声明模型供应商、工具等插件依赖。

它描述的是“应用应该是什么样”,而不是逐条记录“应用曾经如何运行”。

二、为什么要使用声明式描述

如果只把工作流保存为数据库记录,应用会和当前环境紧密绑定。DSL 把应用结构提取成文本后,会得到几个直接好处:

  • 可以在开发、测试和生产环境之间迁移;
  • 可以进入 Git 做版本管理和代码审查;
  • 可以复制应用模板;
  • 可以比较两次配置变更;
  • 可以在升级前保存可恢复的配置快照。
flowchart LR
    A[开发环境中的应用] --> B[导出 DSL]
    B --> C[代码审查与版本管理]
    C --> D[导入测试环境]
    D --> E[补齐依赖与环境配置]
    E --> F[验证]
    F --> G[导入生产环境]

不过,DSL 只是迁移载体,不会自动解决所有环境差异。

三、导出过程不是简单的序列化

Dify 导出应用时,会根据应用模式组装不同内容:

  • Workflow 和 Chatflow 导出草稿工作流;
  • 普通对话、Agent 和文本生成应用导出模型配置;
  • 扫描节点并生成插件依赖列表;
  • 对环境相关字段进行过滤或转换。

例如工具节点和 Agent 节点可能引用某个工作空间内的凭证 ID。这个 ID 在另一环境中通常没有意义,因此默认导出时应移除,而不是原样复制。

定时触发器、Webhook 和插件订阅也有类似问题:

  • 定时配置可能需要在目标环境重新确认;
  • Webhook 地址属于当前部署环境;
  • 插件订阅 ID 只在原工作空间内有效。

可移植 DSL 的关键,不是“字段越多越好”,而是区分哪些配置具有业务含义,哪些只是当前环境的内部引用。

四、密钥应该如何处理

工作流环境变量可能包含 API Key、数据库密码等敏感信息。导出这些值会让 YAML 文件变成新的泄密入口。

更安全的原则是:

默认导出结构和变量定义,不导出秘密值。

Dify 的工作流序列化提供 include_secret 控制,并在默认导出流程中清理凭证引用。即使系统支持“包含密钥”的特殊导出,也应限制权限,并明确提示风险。

实际使用中建议:

  1. DSL 进入 Git 前检查是否含有秘密;
  2. 目标环境通过环境变量或凭证中心重新注入;
  3. 不在聊天、工单和公共网盘中随意传递含密钥文件;
  4. 导出后若发现泄露,立即轮换凭证,而不只是删除文件。

五、版本字段解决什么问题

应用会持续演进,DSL 格式也会变化。新版本可能增加节点字段、调整变量结构,甚至改变某些配置的含义。

因此导入前要比较:

1
文件中的 DSL 版本 ↔ 当前系统支持的 DSL 版本

Dify 会检查版本兼容性,并返回不同状态:

  • 可以直接导入;
  • 存在较大的版本差异,需要用户确认;
  • 格式错误或缺少必需字段,导入失败。

当版本差异需要确认时,待导入内容会短暂存入 Redis,并生成一个导入 ID。用户确认后,系统再继续创建或覆盖应用。

这比“遇到未知字段照样写入数据库”更稳妥,因为格式不兼容时,静默成功往往比明确失败更危险。

六、兼容策略不只有一种

DSL 演进常见三种策略:

策略 做法 适用场景
向后兼容 新系统仍能读取旧字段 小幅增加字段、默认值明确
迁移转换 导入时把旧结构转换成新结构 字段改名或结构调整
明确拒绝 提示版本不兼容 语义变化大,无法安全推断

成熟的导入器通常宽容读取旧格式,但输出时统一使用当前格式。这样可以逐步淘汰历史结构,避免 DSL 长期出现多套写法。

七、插件依赖也属于应用的一部分

工作流图中可能使用:

  • 某个模型供应商;
  • 某个工具插件;
  • 重排序模型;
  • 知识检索所需的嵌入模型。

只迁移节点配置而不迁移依赖信息,目标环境就会出现“图能打开,但节点不能运行”的情况。

Dify 会扫描工作流节点或模型配置,生成 dependencies。导入后再检查当前工作空间是否缺少这些插件。

旧版 DSL 没有完整依赖列表时,系统还可以从工作流图反向分析依赖。这是一种很实用的兼容做法:文件格式没有提前声明的信息,由导入器根据节点内容补推出来。

八、知识库 ID 为什么需要转换

知识检索节点会引用知识库 ID,但知识库属于具体工作空间,目标环境通常不存在同一个内部 ID。

Dify 在导出时可对知识库 ID 做转换,导入时再尝试还原;无法在当前租户下识别的引用会被过滤。它解决的是内部 ID 暴露和误引用问题,但不能凭空迁移知识库内容。

因此跨环境迁移后,仍应:

  • 确认目标环境已有对应知识库;
  • 重新选择知识检索节点的数据源;
  • 检查嵌入模型与重排序模型;
  • 用真实问题验证召回结果。

不要把“成功导入 DSL”等同于“应用已经可以直接上线”。

九、新建与覆盖是两种不同风险

导入通常包含两种模式:

  • 新建:根据 DSL 创建一个新应用;
  • 覆盖:把配置写入已有应用。

新建相对安全,失败时可以放弃新对象。覆盖则可能影响现有用户,因此至少应检查:

  • 被覆盖应用属于当前租户;
  • 应用模式是否允许覆盖;
  • 导入内容是否合法;
  • 数据库操作是否处于清晰的事务边界;
  • 失败后能否回滚到原配置。

Dify 的 DSL 服务由调用方持有数据库会话,服务内部在需要生成 ID 时执行 flush,最终由调用方决定提交或回滚。这个边界可以避免导入到一半便留下部分数据。

十、完整的导入流程

flowchart TD
    A[读取 YAML] --> B{格式和 kind 正确?}
    B -- 否 --> X[返回失败]
    B -- 是 --> C[检查 DSL 版本]
    C --> D{需要确认?}
    D -- 是 --> E[暂存导入信息]
    E --> F[用户确认]
    D -- 否 --> G[解析应用配置]
    F --> G
    G --> H[检查租户与覆盖目标]
    H --> I[重建图、变量和模型配置]
    I --> J[记录并检查插件依赖]
    J --> K[提交事务]
    K --> L[补齐凭证、知识库和环境配置]

可以把这条链分成三层:

  1. 语法层:YAML 能否解析,结构是否完整;
  2. 兼容层:版本和旧字段能否安全转换;
  3. 运行层:插件、模型、凭证和知识库是否齐备。

只有三层都通过,迁移后的应用才算真正可用。

十一、如何把 DSL 用进发布流程

如果希望把 AI 应用纳入工程化管理,可以采用下面的简单流程:

  1. 在开发环境修改工作流;
  2. 导出不含密钥的 DSL;
  3. 在 Git 中查看差异并评审;
  4. 导入测试环境,补齐外部依赖;
  5. 执行固定测试用例;
  6. 通过后再迁移到生产环境;
  7. 在生产环境单独配置凭证和数据源。

DSL 让可视化编排具备了类似代码的可追踪性,但它仍需要测试、权限和发布纪律配合。

十二、总结

Dify DSL 的价值不只是“导出一个 YAML 文件”,而是为 AI 应用建立一份可读、可比较、可迁移的声明式定义。

一套可靠的 DSL 机制需要同时处理:

1
2
3
4
5
6
结构描述
+ 版本兼容
+ 依赖声明
+ 敏感信息过滤
+ 环境引用转换
+ 事务化导入

理解这些边界后,就能更准确地判断:哪些内容可以随 DSL 迁移,哪些必须在目标环境重新配置,以及为什么“导入成功”只是部署验证的开始。