惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

爱范儿
爱范儿
Y
Y Combinator Blog
博客园 - Franky
D
Docker
B
Blog RSS Feed
M
MIT News - Artificial intelligence
雷峰网
雷峰网
博客园 - 司徒正美
人人都是产品经理
人人都是产品经理
宝玉的分享
宝玉的分享
S
SegmentFault 最新的问题
GbyAI
GbyAI
Recent Announcements
Recent Announcements
Martin Fowler
Martin Fowler
H
Hackread – Cybersecurity News, Data Breaches, AI and More
MyScale Blog
MyScale Blog
B
Blog
H
Help Net Security
Microsoft Security Blog
Microsoft Security Blog
WordPress大学
WordPress大学
Vercel News
Vercel News
The Cloudflare Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Google DeepMind News
Google DeepMind News

aopstudio 的个人博客

看懂 PyTorch 训练过程:前向计算、损失、反向传播与参数更新 - aopstudio 的个人博客 Python 中容易被忽略的原地操作:从 PyTorch Tensor 的 `-=` 说起 - aopstudio 的个人博客 向量求导为什么有时是行向量,有时是列向量?——分子布局与分母布局 - aopstudio 的个人博客 无网/内网环境 Dify 安装插件:用 vendor 绕过 PyPI 依赖问题 - aopstudio 的个人博客 一场静悄悄的架构革命:Embedding 模型为何从 Encoder 转向 Decoder - aopstudio 的个人博客 同一个 if 判断,Dify 三种节点跑出三种速度 - aopstudio 的个人博客 手动拼了张4像素的图,JPEG颜色错了,PNG却对了——为什么? - aopstudio 的个人博客 最近的生活节奏:慢一点,也挺好 - aopstudio 的个人博客 HuggingFace 为 LLM Agent 做的读论文工具:`hf papers` - aopstudio 的个人博客 Hugging Face 新工具 hf-mount 解析:把 HF Hub 直接变成本地文件系统 - aopstudio 的个人博客 强化学习知识补课:同样的数据集,SFT 和 RL 学到的东西为什么不一样? - aopstudio 的个人博客 AI 编程工具演进梳理:从 IDE 插件到 CLI Agent,终端为何重新成为开发中心 - aopstudio 的个人博客 让OpenClaw做你的个人电脑代理仍不成熟,但它可以是这个…… - aopstudio 的个人博客 阿里 CoPaw 上手体验:电子宠物养着挺好,生产工具差点意思 - aopstudio 的个人博客
踩坑记录:Dify 1.13 升级后,流式接口首包不再是 data: {
aopstudio · 2026-08-04 · via aopstudio 的个人博客

最近业务部门找到我们,说一个对接 Dify chatflow 流式接口的功能"炸了":六月份之前一直能正常解析的返回结果,现在直接解析失败。之前的对接代码是在第一条报文的data里等workflow_started事件来判断"任务已受理"的。现在第一条报文变成了一行光秃秃的event: ping,连data:` 都没有,解析逻辑直接傻眼。

排查:先入为主,把矛头指向了业务部门

接到反馈后,我们一开始根本没打算抓包。原因很简单——想当然:一个 HTTP 接口,怎么可能返回 event: ping 这种离谱的东西?这玩意儿连 JSON 都不是。第一反应是业务部门搞错了。

于是我们翻了网络日志,又翻了 Dify 后台日志。网络日志里 HTTP 状态码清一色 200,请求一切正常;Dify 后台里用户发送的问题、完整的回答结果都清清楚楚躺在那里,说明调用链路和业务处理都没毛病。这更加坚定了我们的判断:就是业务部门在乱打日志,不知道把什么别的东西当成了接口返回。

但业务部门一口咬定:收到的原始报文里,第一条就是一行 event: ping。两边各执一词,僵住了。

没办法,只能真去抓包看原始报文。结果看到的一刻我们傻眼了——它真的在:

event: ping
data: {"event":"workflow_started","workflow_run_id":"...","data":{"id":"...","workflow_id":"...","sequence_number":0}}
data: {"event":"node_started","workflow_run_id":"...","data":{"node_type":"start",...}}

image.png

event: ping 真实存在,是我方接口发出去的。之前怪业务部门乱打日志,是我们错怪了人家。接下来才是各种离谱猜测轮番上阵:

  • 怀疑是不是服务器上的 ping 测试被"透传"回了接口。 我们确实在服务器上执行过 ping 命令做连通性测试——会不会被什么中间层透传进了响应?虽然稍微懂点技术就知道这完全不可能,但当时真的什么想法都冒得出来。
  • 怀疑网关、代理、负载均衡在注入心跳。 健康检查、长连接保活什么的,查了一圈配置,全都对不上。
  • 最后查了Dify的工作流编排。 工作流编排从上线起没动过,也不可能主动输出这种格式。

排查彻底陷入僵局。我们把希望寄托在 Dify 官方文档上,结果文档也没能救我们

文档确实把各种 event 类型列得清清楚楚:messagemessage_endworkflow_startednode_started……在列表最底部,也确实提到了 event: ping——"每 10s 一次的 ping 事件,保持连接存活"。
d8bcd7b99104432e30f31fa9902fb300.jpg

但有两个致命的地方对不上:

  • 文档开篇就说"每个流式块均为 data: 开头",可我们看到的 ping 是 event: 开头、没有 data:,和文档描述直接矛盾;
  • 文档又说 ping 是"每 10s 一次",可我们是第一包就收到了。

文档只罗列了 event 类型,从头到尾没有解释 ping 的特殊性——它是唯一不以 data: 开头的块。而且我们当时压根不知道 SSE 协议这回事,连"event: 开头的行也是一种合法响应格式"这个理论认知前提都不具备。文档翻烂了,也拼不出答案。

问官方:"一直有这个机制"

最后没办法,只能去问 Dify 官方支持人员。直到这时,我们才从他口中了解到 SSE 协议

这个接口的 Content-Type 是 text/event-stream,走的是 Server-Sent Events,不是普通 HTTP 返回多个 JSON 消息。SSE 里每条事件由 event:(事件类型)、data:(载荷)等字段组成,以空行分隔。一条只有 event: 没有 data: 的报文在 SSE 里是完全合法的——它就是一个"带类型的空事件",最常见的用途就是心跳。而我们之前一直把Dify的流式接口当成一个会返回多个JSON消息的HTTP接口用,根本没仔细研究过SSE协议。

所以 event: ping 不是什么异常,它就是 Dify 在流式连接上发的一个心跳包。官方说:一直有 event: ping 这个机制

这就矛盾了:如果一直有,为什么我们之前从来没收到过?为什么现在它变成了第一条报文?

这时我才开始把注意力放回环境本身,核对了对接以来用过的版本:

  • 六月之前:企业版 3.8.0(对应社区版 1.12.1-hotfix.6),流式返回一直解析正常;
  • 现在:企业版 3.9.6(对应社区版 1.13.3),event: ping 出现在首包。

结合官方"一直有这个机制"的说法,我怀疑问题就出在版本变化上。翻了两个版本的源码,果然——Dify 流式通道在 1.13.0 有过一次重构,ping 的发送时机彻底变了。

机制对比:ping 的位置变了

1.13.0 之前(1.12.x):ping 只是"兜底心跳"

旧版走的是内存队列 + 进程内消费的模式。工作流执行时,worker 把 workflow_startednode_started 这些事件写进内存队列,API 进程从队列里逐个取出来转成 SSE 返回。

首包就是 worker 发布的第一个事件——workflow_started。而 ping 只是一个兜底:只有当队列超过 10 秒没有新事件时,才会插入一个 ping 保活,防止连接超时。正常情况下根本轮不到它出场。

所以我们在 3.8.0 上一直没收到 ping,不是巧合,是旧机制里 ping 本来就很少出现。

1.13.0 之后(1.13.3):ping 变成"开场白"

2026 年 2 月,Dify 合入了 Human Input Node(人工输入节点)功能(PR #32060),顺带重构了流式通道。新机制改成了 Redis Pub/Sub 主题订阅:API 进程订阅一个 Redis 主题,worker 把事件广播到主题上,API 订阅到之后转发给客户端。

关键改动在订阅逻辑的开头,源码里是这么写的:

# send a PING event immediately to prevent the connection staying in pending state for a long time.
#
# This simplify the debugging process as the DevTools in Chrome does not
# provide complete curl command for pending connections.
yield StreamEvent.PING.value

一进入函数,就无条件先发一个 ping。注释说得很清楚:让连接立即有响应,不要长时间处于 pending 状态——因为 Chrome DevTools 对 pending 状态的请求不提供完整的 curl 命令,调试起来很痛苦。

于是从 1.13.0 开始,所有走新通道的流式响应,第一个包固定是 event: ping,第二个包才是 workflow_started

这就解释了一切:不是"突然多了个 ping",而是 ping 从"10 秒空闲才会出现的兜底心跳"变成了"每次连接都先发的前置心跳"。我们在 3.8.0 上根本没机会见到它,升级到 3.9.6 之后它成了见面礼。

顺带一提,官方文档里"ping 每 10s 一次"的描述,描述的还是旧机制的兜底心跳;新机制下它是开场白,文档并没有同步更新——这又给我们的排查多添了一层堵。

对接怎么改

对客户端来说,改动其实很小:

1. 忽略没有 data: 的 ping 包

解析 SSE 时,遇到只有 event: ping、没有 data: 字段的报文,直接跳过。它既不是业务事件,也不代表出错,就是个心跳。

2. 判断"任务已受理"改用第一条 data: 事件

之前用"第一条报文"判断任务已受理,现在第一条可能是 ping。改成以第一个带 data: 的事件(即 workflow_started)为准。

3. 其余解析逻辑完全不动

workflow_startednode_startedmessageworkflow_finished 这些事件的结构和顺序都没有变,只多了一条需要跳过的开头心跳。

一些体会

这次排查真正卡住我们的,不是技术问题,而是认知盲区,而且这个盲区有两层。

第一层,我们压根不知道 SSE 协议,一直把流式接口当成"会返回一堆 JSON 的普通 HTTP 接口"在用。而 Dify 官方文档也"功不可没":它开篇就说"每个流式块均为 data: 开头",把 ping 埋在事件列表最底部一句带过,还写着"每 10s 一次"——偏偏新版本里它是首包、还不带 data:。文档没有告诉我们 ping 的特殊性,我们也不知道问什么。最后是从官方支持人员的口中,才补上了协议知识这块拼图。

第二层,也是最打脸的:我们最先排除的假设,恰恰是真相。接到反馈时我们想当然地认为"HTTP 不可能返回这种离谱的东西",连原始报文都没看就把锅扣给了业务部门。而且当时查的日志本身就很有迷惑性——网络日志全是 200,Dify 后台有完整问答记录,但这些只能证明"请求成功了、业务跑通了",根本看不到 SSE 的字节流长什么样。HTTP 200 和完整回答,与 event: ping 的存在完全不矛盾。如果一开始就抓包看原始响应,问题半天就能定位,根本不用绕这么大一圈。先入为主的判断,比技术盲区更致命。

另外,Dify 官方说"一直有这个机制"其实也没说错——ping 一直都在,变的只是它出现的时机。对接第三方平台时,协议里的"可选/兜底"行为,往往比"主流程"更容易在升级中改变,而且这种改变不会写进 changelog 的显著位置。升级依赖前,除了看新特性,还值得把流式、回调这类长连接协议的整体行为重新过一遍,最好用原始报文验证一次首包和尾包,而不是只信"数据结构没变"。


附:关键信息速查

项目说明
现象chatflow 流式接口首包从workflow_started 变为 event: ping
影响版本社区版 1.13.0+(对应企业版 3.9.x);旧版 1.12.x 及以下不受影响
变更来源上游 PR #32060(Human Input Node,2026-02-09)重构流式通道
旧机制内存队列消费,ping 仅作为空闲 10 秒兜底心跳
新机制Redis Pub/Sub 订阅,订阅前无条件先发一个 ping
客户端改法跳过无data: 的 ping 包,以第一条 data: 事件(workflow_started)作为任务受理标志