Dify 中级实验(07):子工作流——如何把公共逻辑做成可复用积木?
Dify 实验系列 · 中级 07/20 | 实验编号:DIFY-102-08
基于 Dify 1.16.1 实测(2026-08)
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_sentimentrole:systemtext:|你是一个专业情绪分析师。分析以下文本的情感,输出 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:workflowprovider_name:dify102_08_01_情绪分析引擎provider_id:11e9699e-ffa8-448f-8278-e16799e5912a# 发布时的注册 ID,重发会变!tool_name:dify102_08_01tool_description:情绪分析引擎(子工作流)——输入文本,输出结构化情绪字段type:tooltitle:调用情绪分析引擎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:textdefault:示例:太棒了,五星好评# default 决定 UI 面板显示值required:truetype:stringid:tool_sentiment5.3 输出不透传:必须解析展平
工具输出固定三件套text(string)/files(array[file])/json(array[object])——子工作流 end 的自定义字段名不透传,下游直接引用{{#tool.sentiment#}}取不到。必须加代码节点从json数组提取:
defmain(sent_json:list)->dict:importjson data={}ifisinstance(sent_json,list):foriteminsent_json:ifisinstance(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_highconditions:-comparison_operator:isvalue:highvariable_selector:[cd_parse_sent,urgency]varType:stringlogical_operator:and-case_id:case_normalconditions:-comparison_operator:is notvalue:highvariable_selector:[cd_parse_sent,urgency]varType:stringlogical_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
- 源码(可直接导入,先导子工作流再导主工作流):
- dify102_08_01_情绪分析引擎.yml
- dify102_08_02_客服情绪看板.yml
- dify102_08_03_批量反馈分析.yml
文章聚焦核心配置与采坑点;实验的完整分步操作(节点搭建/参数表/调试指引)见实验文档原文。
下一篇:Dify 中级实验(08):代码节点进阶——如何用标准库处理文件与数据?
💬 你在这个实验的场景里踩过什么坑?欢迎评论区分享你的实战经验。