长任务最难受的不是慢,是「不知道它在干什么」。stream-json 把每次动作变成一行事件,边跑边吐。 实测两种读法总时长几乎一样(0.775 对 0.770 秒),但流式的第一条 0.527 秒就到了。 它不让你更快,让你更早。
上一节流水线跑起来了,这一节解决「跑的时候看得见」这件事。 这一节有真计时:同一个输出,一种等完再读、一种边跑边读。
一、两种读法
等完再读:进程结束,一次性收全部输出,再解析。简单,但中途是黑的。 边跑边读:每来一行就处理一行,进程还没结束就已经有东西了。 两者的总时长几乎一样——流式不加速,它只是把「到手」的时间提前。
而「提前」在长任务里就是全部:知道它跑到第几步,才知道该等还是该掐。
二、真跑:同一段输出,两种读法
演示里十二步、每步二十毫秒,一共十三行事件(最后一行是 done)。 等完再读:第一条要到快一秒才拿到——那其实就是它跑完的时间。 边跑边读:第一条在半程就到了,剩下的边跑边收。
两个总时长几乎一致。差的那一点,是解析开销,不是速度。
三、流式的真正价值
第一,能设「多久没动静就报错」。没有事件流的任务,卡住和慢长得一模一样。 第二,能把中间结果写进日志。事后查问题,能看到它当时在做第几步。 第三,能提前放弃。看见它在反复改同一个文件,就不用等它跑完。
第三条最省钱:一次跑偏的长任务,早三分钟掐掉,就省三分钟的钱。
四、解析事件流的三个坑
半行:读到一半没有换行就不能解析,必须攒够一行再解。 事件类型:别假设只有一种,一定按类型分流,未知类型直接跳过不报错。 错误事件:失败也是事件,从流里出来,不一定是非零退出码。两处都要看。
第三个坑最容易漏:你在检查退出码,而它已经在流里报过一次错了。
五、什么时候不需要流式
任务三秒就结束,流式只增加解析负担。 结果本来就是一整块(比如生成一份文件),中间过程没有意义。 判据:这个任务会不会超过三十秒?会就上流式,不会就等它。
还有一条更实际的:这行日志有没有人看?没人看的进度条,只是多一处要维护的代码。
六、动手 Demo:流式读和等完再读,差的不是速度是「早」
长任务最难受的地方不是慢,是「不知道它在干什么」。 这里真跑同一段输出,一种等完再读、一种边跑边读,各记一次时间。
for line in p.stdout: # 边跑边读:来一行处理一行
line = line.decode(u"utf-8", u"replace").strip()
if first is None:
first = time.perf_counter() - t0
json.loads(line)
实跑输出:
--------------------------------------------------------------------
流式读和等完再读,差的不是速度是「早」
--------------------------------------------------------------------
长任务最难受的地方不是慢,是「不知道它在干什么」。
stream-json 把每次动作变成一行事件,边跑边吐,日志里就能看见进度。
① 两种读法
--------------------------------------------------------------------
等完再读:进程结束,一次性收全部输出,再解析。
边跑边读:每来一行就处理一行,进程还没结束就已经有东西了。
② 真跑:同一段输出,两种读法
--------------------------------------------------------------------
| 读法 | 第一条到手 | 总共收到 | 中途能看见什么 |
|---|---|:---:|:---:|
| 等完再读 | 0.772 秒 | 13 条 | 结束时才有 |
| 边跑边读 | 0.524 秒 | 13 条 | 第一条就到手 |
总时长几乎一样(0.771 秒对 0.772 秒)——流式不让你更快,它让你更早。
而「更早」在长任务里就是全部:看见它已经跑到第几步,才知道该等还是该掐。
③ 流式的真正价值
--------------------------------------------------------------------
第一,能设「多久没动静就报错」。没有事件流的任务,卡住和慢长得一样。
第二,能把中间结果写进日志。事后查问题,能看到它当时在做第几步。
第三,能提前放弃。看见它在反复改同一个文件,就不用等它跑完。
④ 解析事件流的三个坑
--------------------------------------------------------------------
- 半行。读到一半没有换行就不能解析,必须攒够一行再解。
- 事件类型。别假设只有一种,一定按 type 分流,未知类型直接跳过不报错。
- 错误事件。失败也是事件,从流里出来,不一定是非零退出码。两处都要看。
⑤ 什么时候不需要流式
--------------------------------------------------------------------
任务三秒就结束,或结果本来就是一整块(比如生成一份文件),流式只增加解析负担。
判据:这个任务会不会超过三十秒?会就上流式,不会就等它。
! 事件字段按各家实际格式为准,这里演示的是「一行一个 JSON、按 type 分流」这个形状。
①两种读法:等完再读一次性收全部输出,边跑边读每来一行处理一行。 两者的总时长几乎一样——流式不加速,只是把「到手」的时间提前。 ②十二步、每步二十毫秒,一共十三行事件。 等完再读要等它跑完才拿到第一条,边跑边读第一条在半程就到了。 ③流式的三个价值:能设「多久没动静就报错」、能把中间结果写进日志、能提前放弃。 第三条最省钱——早三分钟掐掉,就省三分钟的钱。 ④三个坑:半行要攒够再解、按类型分流不假设只有一种、错误也是事件(不一定非零退出码)。 第三个最容易漏:你在检查退出码,它已经在流里报过一次错了。
30 秒实验
- 把每步的休眠从二十毫秒改成两百毫秒,重跑一遍。等完再读和边跑边读的差距被拉大了多少? 这说明流式的价值跟什么成正比?
- 在流里插一条错误事件(type 是 error),重跑一遍。你的解析能发现它吗?如果只看退出码呢?
- 把「第一条到手」这个时间点改成「第十条到手」,重跑一遍。 它对「多久没动静就报错」这个策略有什么影响?
常见误区
- 以为流式能让任务更快。它只是让你更早看到进展,总时长不变。
- 不按类型分流,假设流里只有一种事件。加一种类型就崩。
- 只看退出码不看流里的错误事件。失败可能已经在流里报过了。
- 解析半行就 json.loads。会抛异常,而异常位置看起来像「数据坏了」。
- 给三秒的任务上流式。多一层解析,换不来任何东西。
自测三问(点开对答案)
1. 流式和一次性读取的根本差别是什么? —— 不是速度,是「早」。总时长几乎一样,但流式在任务还没结束时就已经有数据了。 2. 流式的三个价值是什么? —— 能设超时告警(多久没动静)、能把中间结果写进日志、能提前放弃跑偏的任务。 3. 解析事件流要防哪三个坑? —— 半行要攒够再解、按类型分流、错误也是事件(不一定体现在退出码上)。看得见了,下一节解决下一个问题:多步任务之间,上下文怎么传。
- ☐ (本篇完,下一篇 7.7 会话管理:跨步骤维持上下文)
相关笔记
- 核心参数体系:参数里的输出格式
- 会话管理:跨步骤维持上下文
- Claude Code 橙皮书 · 索引
- 🐎 Claude Code 实战 · 索引