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. 整体架构
#mermaid-svg-lkSk8kB0u1bm4ZX2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lkSk8kB0u1bm4ZX2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .error-icon{fill:#552222;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .marker.cross{stroke:#333333;}#mermaid-svg-lkSk8kB0u1bm4ZX2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lkSk8kB0u1bm4ZX2 p{margin:0;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .cluster-label text{fill:#333;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .cluster-label span{color:#333;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .cluster-label span p{background-color:transparent;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .label text,#mermaid-svg-lkSk8kB0u1bm4ZX2 span{fill:#333;color:#333;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .node rect,#mermaid-svg-lkSk8kB0u1bm4ZX2 .node circle,#mermaid-svg-lkSk8kB0u1bm4ZX2 .node ellipse,#mermaid-svg-lkSk8kB0u1bm4ZX2 .node polygon,#mermaid-svg-lkSk8kB0u1bm4ZX2 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .rough-node .label text,#mermaid-svg-lkSk8kB0u1bm4ZX2 .node .label text,#mermaid-svg-lkSk8kB0u1bm4ZX2 .image-shape .label,#mermaid-svg-lkSk8kB0u1bm4ZX2 .icon-shape .label{text-anchor:middle;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .rough-node .label,#mermaid-svg-lkSk8kB0u1bm4ZX2 .node .label,#mermaid-svg-lkSk8kB0u1bm4ZX2 .image-shape .label,#mermaid-svg-lkSk8kB0u1bm4ZX2 .icon-shape .label{text-align:center;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .node.clickable{cursor:pointer;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .arrowheadPath{fill:#333333;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lkSk8kB0u1bm4ZX2 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lkSk8kB0u1bm4ZX2 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-lkSk8kB0u1bm4ZX2 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .cluster text{fill:#333;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .cluster span{color:#333;}#mermaid-svg-lkSk8kB0u1bm4ZX2 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lkSk8kB0u1bm4ZX2 rect.text{fill:none;stroke-width:0;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .icon-shape,#mermaid-svg-lkSk8kB0u1bm4ZX2 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .icon-shape p,#mermaid-svg-lkSk8kB0u1bm4ZX2 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .icon-shape .label rect,#mermaid-svg-lkSk8kB0u1bm4ZX2 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lkSk8kB0u1bm4ZX2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lkSk8kB0u1bm4ZX2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lkSk8kB0u1bm4ZX2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 优化后(dify104_04_02)
命中
未命中
未命中
未命中
开始:query/product_id
读取结果缓存(http → KV)
查询结果缓存(code:md5 键)
缓存是否命中
结束(cache_result,秒回)
情感分析 A(LLM)
意图识别 B(LLM)
库存查询 C(HTTP)
并行结果合并(variable-aggregator)
汇总分析 D(LLM)
组装缓存写入
写入结果缓存(http → KV)
模板输出 E
结束
优化前(dify104_04_01,串行)
开始:query/product_id
情感分析 A(LLM)
意图识别 B(LLM)
库存查询 C(HTTP)
汇总分析 D(LLM)
模板输出 E
结束
链路很清晰:先查缓存 → 未命中走并行分析 → 合并汇总 → 写缓存 → 输出。缓存前置 + 并行分支,是这条链提速的两个支点。
5. 模块设计
5.1 缓存查询(cd_cache)
缓存键 = 参数组合的 md5;命中即返回缓存值,未命中继续往下跑:
python
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 默认口可以多边扇出):
yaml
- 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 实测:并行分支变量不合并,下游拿不到):
yaml
- 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),下次同参数调用直接命中:
python
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 消耗):
jinja2
# 客服处理报告(优化后)
- 用户问题:{{ 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 应用省钱改造怎么做?
💬 你在这个实验的场景里踩过什么坑?欢迎评论区分享你的实战经验。