Dify 中级实验(07):子工作流——如何把公共逻辑做成可复用积木?
1. 业务场景
先讲一个我们实际遇到的场景。
一家同时做客服、产品调研、舆情监控的公司:客服要实时看客户情绪,产品团队要分析用户反馈,舆情部门要盯社区言论。三拨人各自开发了一套「情绪分析」——三份重复代码、三套维护成本、三个不一致的口径:客服判「负面」的,舆情可能判「中性」。
我们第一次接这类需求时,第一反应也是「把情绪分析的代码复制三份,各改各的参数」。真正动手才发现——需求一样、代码三份,改一处要改三处,漏改一套口径就漂移。后来翻 Dify 的节点列表才发现:模块化设计有原生手段——子工作流(Sub-workflow):把「情绪分析」这类公共逻辑抽成独立工作流,一处定义、多处调用(1.16 的真实做法是发布为工具调用)。
这不是个例。任何「多个业务线共用同一能力」的业务场景都是这个模式:情绪分析、文本清洗、格式化输出……公共逻辑不做模块化,就是在重复造轮子,而且每个轮子还不太一样。
2. 场景痛点
这个流程的痛点,在研发/业务团队身上体现得最直接:
- 同一逻辑重复开发:三条线各写一份情绪分析,需求一样、代码三份,开发资源被重复消耗——重复的代码不是资产,是负债,每次升级都要连本带利还。
- 改一处要改三处:分析逻辑升级(比如紧急度规则调整),三套代码要同步改,漏改一套口径就漂移——线上行为从此不一致。
- 调用方被实现绑架:每个调用方都要关心「怎么分析」——传什么参数、用什么模型、怎么解析输出,而不是只关心「分析结果是什么」。
- 输出格式不统一:三条线产出的字段名、格式各异,下游汇总统计对不上,数据越攒越乱。
本质上,公共逻辑每多一个调用方,重复成本就翻一倍——模块化的价值不在「少写代码」,而在「一处定义、多处调用、接口即契约」。
3. 方案:为什么是子工作流
选子工作流的理由,我们实际对比过:
- 一处定义、多处调用:情绪分析做成独立工作流,两个主工作流消费它——客服看板逐条分析,批量反馈迭代内逐条调用;
- 接口即契约:输入
text/language、输出 6 个结构化字段(sentiment/score/confidence/keywords/brief/urgency),边界清晰,调用方只看接口不看实现; - 1.16 的真实实现:Dify 1.16 没有 sub-workflow 节点类型,真实做法是发布子工作流为工具(Workflow as Tool),用 tool 节点调用——本实验完整演示这条路。
这篇文章我们就用它搭「情绪分析引擎」子工作流 + 两个消费它的主工作流(客服情绪看板 / 批量反馈分析)。
4. 整体架构
链路很清晰:子工作流定义能力,主工作流消费能力——情绪分析引擎只做「分析并输出结构化字段」,两个主工作流各自编排自己的业务逻辑(看板按紧急度分流、批量分析走迭代)。
5. 模块设计
5.1 子工作流:枚举字段必须给显式规则
LLM 输出的 JSON 里 urgency 是枚举字段——只写
low/medium/high 不给规则,LLM 会随意输出(实测负面投诉返回
low,下游分支全走错):
prompt_template:
- id: p_sentiment
role: system
text: |
你是一个专业情绪分析师。分析以下文本的情感,输出 JSON 格式(不要 Markdown):
文本:{{#start.text#}}
{
"sentiment": "positive/negative/neutral/mixed",
"score": 0.0 到 1.0 之间的浮点数,
"confidence": 0.0 到 1.0,
"keywords": ["关键词1", "关键词2", ...],
"brief": "一句话情感总结",
"urgency": "low/medium/high"
}
紧急度规则:负面情绪/投诉/损坏/退款/愤怒类内容 → "high";中性咨询类 → "medium";正面/普通内容 → "low"
reasoning_format: separated参数提取器 6 个参数(sentiment string / score number / confidence
number / keywords array[string] / brief string / urgency
string),reasoning_mode: function_call。
5.2 主工作流:发布子工作流为工具(核心)
Dify 1.16.1 没有 sub-workflow 节点类型(运行时报
No class mapping found for node type: sub-workflow),必须「发布为工具」后用
tool 节点调用:
- data:
provider_type: workflow
provider_name: dify102_08_01_情绪分析引擎
provider_id: 11e9699e-ffa8-448f-8278-e16799e5912a # 发布时的注册 ID,重发会变!
tool_name: dify102_08_01
tool_description: 情绪分析引擎(子工作流)——输入文本,输出结构化情绪字段
type: tool
title: 调用情绪分析引擎
tool_configurations: # ⚠️ 与 tool_parameters 双写同一份值(UI 权威格式)
text: {type: mixed, value: '{{#start.customer_message#}}'}
language: {type: mixed, value: 中文}
tool_parameters:
text: {type: mixed, value: '{{#start.customer_message#}}'}
language: {type: mixed, value: 中文}
paramSchemas:
- name: text
default: 示例:太棒了,五星好评 # default 决定 UI 面板显示值
required: true
type: string
id: tool_sentiment5.3 输出不透传:必须解析展平
工具输出固定三件套
text(string)/files(array[file])/json(array[object])——子工作流
end 的自定义字段名不透传,下游直接引用
{{#tool.sentiment#}} 取不到。必须加代码节点从
json 数组提取:
def main(sent_json: list) -> dict:
import json
data = {}
if isinstance(sent_json, list):
for item in sent_json:
if isinstance(item, dict):
data.update(item)
return {
"sentiment": str(data.get("sentiment", "未知")),
"brief": str(data.get("brief", "无摘要")),
"urgency": str(data.get("urgency", "low")),
"score": str(data.get("score", "")),
"sentiment_json": json.dumps(data, ensure_ascii=False),
}紧急度分支用字符串比较(is / is not):
cases:
- case_id: case_high
conditions:
- comparison_operator: is
value: high
variable_selector: [cd_parse_sent, urgency]
varType: string
logical_operator: and
- case_id: case_normal
conditions:
- comparison_operator: is not
value: high
variable_selector: [cd_parse_sent, urgency]
varType: string
logical_operator: and6. 运行验证
| 输入 | 预期 | 实测 |
|---|---|---|
| 正面:太棒了!客服很贴心,五星好评! | sentiment=positive,urgency=low → 常规回复 | 与预期一致 |
| 负面:东西收到就坏了,联系客服三天没人理,太失望了! | sentiment=negative,urgency=high → 安抚回复 | 与预期一致 |
| 中性:周二下午三点可以安排配送吗? | sentiment=neutral,urgency=medium → 常规回复 | 与预期一致 |
| 批量:3 条反馈 JSON | 迭代逐条分析,汇总情绪分布 | 与预期一致 |
7. 实战坑
| 坑 | 现象 | 修复 |
|---|---|---|
| 用 sub-workflow 节点类型 | 运行报
No class mapping found for node type: sub-workflow |
1.16 必须「发布为工具」:tool 节点 +
provider_type: workflow |
| 子工作流重导后 provider_id 失效 | 主工作流调用报 workflow provider not found | 重发后从 tool-providers 动态查最新 provider_id 同步(实测 2a6187e8 → 11e9699e) |
| 下游直接引用工具的自定义字段 | {{#tool.sentiment#}}
取不到值,分支全走默认 |
工具输出只有 text/files/json,必须 code 解析 json 展平(见 5.3) |
| tool 参数只写 tool_parameters | UI 配置面板显示参数为空,手动调试报「要分析的文本不能为空」 | tool_parameters 与
tool_configurations
双写同一份值;paramSchemas[].default 填占位值让 UI
不空 |
| 枚举字段不给规则 | 负面投诉消息返回 urgency=low,下游分支全走错 | prompt 给显式映射规则(负面/投诉/退款→high,咨询→medium,正面→low) |
| 迭代内 item 是字符串却传 item.content | 工具收到空文本,全部判默认值 | 字符串 item 直接传
{{#iter.item#}} |
💡 什么时候该抽子工作流:同一逻辑出现在 ≥2 个流程、接口稳定、输入输出边界清晰。接口即契约——改子工作流输出结构,所有调用方都要跟着改,这是模块化的真实成本。
8. 实验文档及源码获取
- 实验文档(完整操作步骤):DIFY-08:子工作流——搭积木式模块化设计.md
- 源码(可直接导入,先导子工作流再导主工作流):
文章聚焦核心配置与采坑点;实验的完整分步操作(节点搭建/参数表/调试指引)见实验文档原文。
- Dify 中级实验(01):参数提取器实战——如何从自然语言中提取结构化数据?
- Dify 中级实验(02):问题分类器——智能路由引擎如何四路分发?
- Dify 中级实验(03):模板转换实战——如何用零 Token 完成文本加工?
- Dify 中级实验(04):迭代进阶——如何批量处理数据并守住性能边界?
- Dify 中级实验(05):并行执行——如何让多路任务同时跑?
- Dify 中级实验(06):变量聚合——如何确定性合并多路分支结果?
- Dify 中级实验(07):子工作流——如何把公共逻辑做成可复用积木?
- Dify 中级实验(08):代码节点进阶——如何用标准库处理文件与数据?
- Dify 中级实验(09):HTTP 节点进阶——如何搞定认证、分页与错误重试?
- Dify 中级实验(10):知识库深度调优——如何科学评估检索质量?
- Dify 中级实验(11):高级 RAG 流水线——如何搭建多路检索与精排?
- Dify 中级实验(12):Agent 深度配置——如何让智能体自主调用工具?
- Dify 中级实验(13):多 Agent 协作——如何编排多个智能体分工干活?
- Dify 中级实验(14):对话变量与状态管理——如何让工作流记住多轮对话的状态?
- Dify 中级实验(15):条件分支高阶策略——多条件路由如何避免分支爆炸?
- Dify 中级实验(16):错误处理与降级——工作流如何有尊严地失败?
- Dify 中级实验(17):调试监控与性能优化——响应慢和 Token 超支如何定位?
- Dify 中级实验(18):插件开发入门——如何把工作流变成 Agent 可调用的工具?
- Dify 中级实验(19):综合实战——如何把 19 个实验串成一条生产级流水线?
- Dify 中级实验(20):综合实战——自动化报告生成流水线如何从数据到周报一步到位?