Dify 企业级实验(04):性能优化实战——长流程从 60 秒到秒回有哪些手段?
Dify 实验系列 · 企业级 04/12 | 实验编号:DIFY-104-04
基于 Dify 1.16.1 实测(2026-08)
1. 业务场景
先讲一个我们实际遇到的场景。
客服在电话这头等着答案,电话那头的用户已经不耐烦了。订单分析工作流 8 个节点、串行 3 个 LLM 加 1 次 HTTP 请求,单次跑 40 秒以上——用户等不起,客服更等不起。
我们第一次接这类需求时,第一反应也是「慢就换更强的模型、加机器」。真正动手才发现——慢往往不是某个节点的错,而是结构问题。不测就改,改的往往不是瓶颈。
「工作流跑得慢」是生产环境最高频的问题之一:不是功能不对,是慢到没法用。慢在哪、怎么改、改完有没有用,三个问题一个都答不上来,就只能继续等。
这不是个例。任何「长流程、多 LLM 串行、含外部 HTTP 调用」的应用都是这个模式:报告生成、多步分析、串联校验——环节越多,越慢。
2. 场景痛点
这个流程的痛点,在客服和运维身上体现得最直接:
- 用户流失:40 秒的等待,用户早关掉页面了——功能做得再对,慢到不可用等于没有。
- 串行放大:3 个 LLM 串行,耗时相加,任何一个节点慢一点,整体跟着慢——最慢的节点决定体验。
- 重复计算:相同参数反复跑同样的分析,时间烧了、Token 也烧了,结果一模一样。
- 瓶颈不可见:不知道慢在哪,改了半天改的不是瓶颈,白忙一场。
本质上,慢往往不是某个节点的错,而是结构问题——串行、重复、不可观测,三样占全了。
3. 方案:为什么是先测量再优化
Dify 的节点级耗时分析 + 并行分支 + 结果缓存,正好对应「排查-优化-验证」三个动作。选它的理由:
- 先测量再优化:每个节点都有耗时统计,瓶颈一目了然,不测就改是瞎猜(102-18 调试思想);
- 并行化:无依赖节点并行执行,总耗时从「相加」变成「取最慢分支」;
- 结果缓存:相同参数命中缓存直接秒回,重复计算彻底消除。
这篇文章我们就用它把一条 40 秒+ 的订单分析链,优化到 15 秒内、缓存命中秒回。
4. 整体架构
链路很清晰:先查缓存 → 未命中走并行分析 → 合并汇总 → 写缓存 → 输出。缓存前置 + 并行分支,是这条链提速的两个支点。
5. 模块设计
5.1 缓存查询(cd_cache)
缓存键 = 参数组合的 md5;命中即返回缓存值,未命中继续往下跑:
def main(query: str, product_id: str, cache_body: str) -> dict:
import json, hashlib
key = hashlib.md5((str(query or "") + "|" + str(product_id or ""))
.encode("utf-8")).hexdigest()
hit, cached = "false", ""
try:
cache = json.loads(cache_body or "{}").get("data") or []
for item in cache:
if isinstance(item, dict) and item.get("key") == key:
hit, cached = "true", item.get("value", "")
break
except Exception:
pass
return {"cache_key": key, "hit": hit, "cached": cached}
5.2 缓存命中分流(if5)
命中走 end_cache 直接返回;未命中从默认 false 口扇出三条并行边(1.16 实测合法,if-else 默认口可以多边扇出):
- id: if5
data:
type: if-else
title: 缓存是否命中
cases:
- case_id: hit
logical_operator: or
conditions:
- comparison_operator: is
value: 'true'
variable_selector: [cd_cache, hit]
# 边:if5(hit) → end_cache;if5(false) → lm_senti / lm_intent / http_stock
5.3 并行结果合并(va_merge)
A/B/C 三个无依赖节点并行执行后,用 variable-aggregator 合并各支结果(102-06 实测:并行分支变量不合并,下游拿不到):
- id: va_merge
data:
type: variable-aggregator
title: 并行结果合并
output_type: string
variables:
- variable_selector: [lm_senti, text]
- variable_selector: [lm_intent, text]
- variable_selector: [http_stock, body]
5.4 缓存写入(cd_cache_write + http_cache_set)
汇总完成后组装 KV 写入请求(op: upsert),下次同参数调用直接命中:
def main(cache_key: str, summary: str) -> dict:
import json
return {"payload": json.dumps(
{"op": "upsert", "key": cache_key, "value": summary},
ensure_ascii=False)}
5.5 模板输出(E)
输出层用 template-transform 而非 LLM(验证「模板 vs LLM」选型收益——固定格式输出模板零 Token 消耗):
# 客服处理报告(优化后)
- 用户问题:{{ query }}
- 并行分析结果:{{ merged }}
- 综合答复:{{ summary }}
*结果已缓存,同参数再次查询秒回*
6. 运行验证
| 输入 | 预期 | 结果 |
|---|---|---|
| 同参数首次调用(query + product_id) | 走 A/B/C 并行(最慢分支约 8s,而非串行 21s)+ D 汇总,结果写入缓存 | 通过(文档运行验证) |
| 同参数二次调用 | 命中缓存,end 输出 cache_hit=true,秒回 <1s | 通过(实测) |
| 整体耗时对比 | 优化前 40s+ → 优化后 <15s(P95) | 通过(文档运行验证) |
| 并行改造后 A/B/C 总耗时 | = 最慢分支耗时(8s 而非 21s) | 通过(设计验证) |
7. 实战坑
| 坑 | 现象 | 修复 |
|---|---|---|
| 不测量直接改 | 优化方向错,改了不影响耗时的地方 | 先做节点级耗时分析(elapsed_time)再动手(实测,102-18) |
| 并行分支变量不合并 | 下游拿不到各支结果 | variable-aggregator 合并后再进下游(实测,102-06/07) |
| 缓存用文件/内存模拟 | code 沙箱禁写文件,报 PermissionError: /tmp | 缓存走 KV upsert(key→value)持久化(实测,本实验) |
| 以为 if-else 默认口只能单出口 | 不敢并行扇出 | 实测 1.16 默认 false 口多边扇出合法(实测,本实验) |
| LLM prompt 变量用双花括号 | 不替换,字面传给模型 | 一律 {{#节点.字段#}} 三花括号(实测,102-12) |
| 缓存无过期策略 | 结果永久陈旧 | 按业务加 TTL/版本号(实验文档设计约束) |
8. 实验文档及源码获取
- 实验文档(完整操作步骤):DIFY-104-04:性能优化实战——长流程提速的完整方法.md
- 源码(可直接导入,一个应用一个 DSL):
- 源码一(优化前):dify104_04_01_性能优化-优化前.yml
- 源码二(优化后):dify104_04_02_性能优化-优化后.yml
- 全部源码目录:dify-104/dsl
文章聚焦核心配置与采坑点;实验的完整分步操作(节点搭建/参数表/调试指引)见实验文档原文。
下一篇:Dify 企业级实验(05):Token 成本控制——AI 应用省钱改造怎么做?
💬 你在这个实验的场景里踩过什么坑?欢迎评论区分享你的实战经验。
:性能优化实战——长流程从 60 秒到秒回有哪些手段?&spm=1001.2101.3001.5002&articleId=163572140&d=1&t=3&u=42939db2830740809998214b436661cf)
385

被折叠的 条评论
为什么被折叠?



