管道的约定很朴素:标准输出接标准输入,一行一条记录。 把 agent 当普通命令用,它就自动能进任何脚本。 但有一个坑必须知道:管道的退出码只等于最后一段。前面失败了,整条还是绿色。
上一节把参数拆开了,这一节把它塞进命令行。 这一节真跑了一条两段管道,并让第一段故意失败,看退出码怎么被吃掉。
一、管道的形状
一句命令:第一段输出,直接进第二段的输入。没有临时文件、没有接口、没有协议。 这是它最值钱的地方:任何能读写标准输入输出的程序,天生就能接进来。 agent 只要遵守「输出一行一条记录」,它就和 grep、jq 是同一种东西。
所以「能不能进流水线」这个问题,答案往往不在功能,在输出格式。
二、真跑:两段接起来
演示里第一段吐三条 JSON,第二段过滤出通过的,再包一层。 三条全部穿过来了,格式也没变——因为两段都遵守「一行一条」的约定。 这里没有解析上的麻烦:一行就是一个完整对象,不需要攒、不需要猜边界。
这也是为什么 NDJSON 这种「一行一个 JSON」的格式在流水线里这么常见。
三、退出码被吃掉了
演示里第一段故意退出 3(假装最后一条有问题),第二段正常退出 0。 整条管道的退出码是 0——它只等于最后一段的退出码。 所以一个「数据生成失败、下游照常输出空结果」的流水线会一直显示绿色。
修法两个:bash 里打开 set -o pipefail,让它取第一个非零;或者逐段等、逐段看退出码。 第二条在脚本里更明确:把每一段的返回码收集起来,取最大值。
四、管道里该传什么
一行一条记录,不要一段多行。多行就要猜边界,猜就要出错。 每条自带类型字段。下游靠字段分流,不靠位置——位置一变全错。 出错也走记录。把「这条失败了」当成一条正常记录发出去,比抛异常好接。
每一步都能单独跑。管道里的一环,单独喂个文件也能跑,调试时才不用整条重来。 最后这条最实用:能单独跑,就能在本地复现;不能单独跑,就只能一遍遍推 CI。
五、三个阶段的分界
生成、过滤、格式化——这是管道里最稳的三段分法。 生成端不懂格式,过滤端不懂业务,格式化端不懂数据来源。三段各自可以替换。 反过来说,如果一段既要调模型、又要过滤、又要排版,那它就没法单独测。
拆段的判据不是「代码长短」,是「换掉其中一段,其他段要不要改」。
六、动手 Demo:三行命令串成管道,退出码去哪了
Unix 管道的约定很朴素:标准输出接标准输入,一行一条记录。 这里真跑一条两段管道,并让第一段故意失败。
p1 = subprocess.Popen([PY, u"gen.py"], stdout=subprocess.PIPE)
p2 = subprocess.Popen([PY, u"filt.py"], stdin=p1.stdout, stdout=subprocess.PIPE)
p1.stdout.close()
o, _e = p2.communicate() # 只看 p2 的退出码,p1 的就被吃掉了
实跑输出:
--------------------------------------------------------------------
三行命令串成管道,退出码去哪了
--------------------------------------------------------------------
Unix 管道的约定很朴素:标准输出接标准输入,一行一条记录。
把 agent 当一个普通命令用,它就自动能进任何脚本——这是 headless 最值钱的地方。
① 管道长什么样
--------------------------------------------------------------------
python gen.py | python filt.py
第一段输出,直接进第二段的输入。没有临时文件、没有接口、没有协议。
② 真跑:三段接起来
--------------------------------------------------------------------
{"n": 0, "tag": "通过"}
{"n": 1, "tag": "通过"}
{"n": 2, "tag": "通过"}
第一段退出码 3,第二段退出码 0。
③ 退出码被吃掉了
--------------------------------------------------------------------
| 这一段 | 退出码 | 说明 | 结论 |
|---|:---:|---|---|
| gen.py | 3 | 它才是真失败的 | — |
| filt.py | 0 | 它成功了 | — |
| 整条管道 | 0 | 只反映最后一段 | 失败被吞 |
管道的退出码只等于最后一段的退出码。前面任何一段失败,都不会传出来。
所以 CI 里两件事必须做:打开管道的失败传递(bash 的 set -o pipefail),或者逐段等、
逐段看退出码。
没有这一条,一个「数据生成失败但下游照常输出空结果」的流水线会一直显示绿色。
④ 管道里该传什么
--------------------------------------------------------------------
- 一行一条记录,不要一段多行。多行就要猜边界,猜就要出错。
- 每条自带类型字段。下游靠字段分流,不靠位置。
- 出错也走记录。把「这条失败了」当成一条正常记录发出去,比抛异常好接。
- 每一步都能单独跑。管道里的一环单独喂文件也能跑,调试时才不用整条重来。
⑤ 三个阶段的分界
--------------------------------------------------------------------
生成、过滤、格式化——这是管道里最稳的三段分法。
生成端不懂格式,过滤端不懂业务,格式化端不懂数据来源。三段各自可以替换。
! 这里的两段脚本是替身,替的是「模型输出 JSON、下游用 jq 取字段」这个形状。
①管道最值钱的地方:任何能读写标准输入输出的程序,天生就能接进来。 agent 只要遵守「一行一条记录」,它就和 grep、jq 是同一种东西。 ②第一段故意退出 3,第二段正常退出 0。整条管道的退出码是 0——它只等于最后一段的退出码。 所以「数据生成失败、下游照常输出空结果」的流水线会一直显示绿色。 修法:打开管道的失败传递,或者逐段等、逐段看退出码。 ③管道里传什么:一行一条记录、每条自带类型字段、出错也走记录、每一步都能单独跑。 最后这条最实用:能单独跑就能在本地复现,不能单独跑就只能一遍遍推 CI。
30 秒实验
- 把第一段的退出码改成 0,重跑一遍。整条管道的退出码变了吗?这说明「逐段检查」查的是什么?
- 让第一段故意输出一行不完整的 JSON(少半个括号),重跑一遍。第二段会报什么?真实的坑在哪儿?
- 把两段合成一段,重跑一遍。功能一样,但调试时会失去什么?
常见误区
- 只看最后一段的退出码。前面失败会被吞掉,流水线一路绿到底。
- 阶段之间传多行文本。下游要猜边界,一猜就错。
- 出错就抛异常。管道里抛异常,下游收到的是空输入,看起来像「没有数据」。
- 把生成、过滤、排版写在一个脚本里。单段没法测,也没法替换。
- 以为管道是 shell 的事,和 agent 无关。输出格式一改,它就接不进去了。
自测三问(点开对答案)
1. 管道的退出码反映的是哪一段? —— 只反映最后一段。前面任何一段失败都不会传出来,所以要开 pipefail 或者逐段检查。 2. 管道里为什么要求「一行一条记录」? —— 因为一行就能解一条,不用猜边界。多行文本要靠分隔符或者长度,一猜就错。 3. 「每一步都能单独跑」为什么重要? —— 能单独跑就能在本地复现问题;不能单独跑,就只能把整条流水线推到 CI 上试。管道会接了,下一节进真正的 CI:让它在每次提 PR 时自动审查。
- ☐ (本篇完,下一篇 7.4 实战一:GitHub Actions 自动代码审查)
相关笔记
- 核心参数体系:输出格式怎么选
- 实战一:接进 GitHub Actions
- Claude Code 橙皮书 · 索引
- 🐎 Claude Code 实战 · 索引