技能之间要协作,第一道坎就是数据怎么从 A 流到 B。新手最容易犯的错:让 A 把结果打印出来,人肉复制,再贴进 B 的对话。这法子短期能跑,长期必崩——格式一变就断、量大就溢、多人就乱、换个人接手就傻眼。
正解是:把产物落盘成约定格式,下游技能按约定读取。A 写文件,B 读文件,中间不靠人肉。下面拆开讲文件、结构化数据各怎么传,以及最常见的断链怎么修。这是把多个技能串成稳定工作流的地基——地基不稳,上层建筑(组合技)一跑就塌。
一、先想清楚:传的是「产物」还是「数据」
两类东西,传法不同:
- 产物(artifact):一个已经成型的东西——报告 PDF、生成的代码、爬下来的网页。本质是「文件」,传路径即可。
- 数据(data):还需要被继续处理的结构化信息——提取的字段、统计结果、配置参数。本质是「键值」,传 JSON 最稳。
经验:能落盘成文件的就别塞进对话;能结构化的就别用散文。人读散文爽,机器读 JSON 稳。这条原则贯穿整个技能协作设计。
二、文件怎么传
2.1 约定一个共享目录
不要各写各的临时目录。约定一个稳定的交换区,比如技能根目录下的 tmp/,或基于 {baseDir} 派生。A 把产物写进去,B 从同一处读。这个目录就是技能间的「快递柜」。
# A 技能:把报告写到约定交换区
OUT="{baseDir}/tmp/report-$(date +%Y%m%d).md"
generate_report > "$OUT"
echo "REPORT_PATH=$OUT" # 把路径回传给调用方
# B 技能:从同一交换区读取
REPORT="{baseDir}/tmp/report-$(date +%Y%m%d).md"
[ -f "$REPORT" ] && publish "$REPORT" || echo "MISSING"
2.2 文件名要可预测
下游靠名字找文件,名字就不能随机。用日期、任务 ID、固定前缀这类确定性命名,别用 tmp-abc123 这种只有 A 自己知道的随机串。随机串的唯一合法用途是防并发冲突(见下文 4.3),不是用来当「主键」的。
2.3 大文件永远走路径,不走文本
把 5MB 的日志整段贴回对话,上下文立刻爆。正确做法:A 落盘,只回传一行路径;B 读文件处理。这点和第 01 篇「上下文超长」、第 04 篇「子代理回收」是一脉相承的——大东西落盘,对话里只流小东西。
三、结构化数据怎么传
当 B 需要拿 A 的具体字段继续算,用 JSON 文件最干净:
{
"task_id": "t-2026-001",
"stats": { "total": 1280, "failed": 3 },
"items": [
{ "url": "https://x.com/a", "status": "ok" },
{ "url": "https://x.com/b", "status": "fail" }
]
}
A 负责写,B 负责读并解析:
# B 读取并提取字段
DATA="{baseDir}/tmp/result.json"
TOTAL=$(jq '.stats.total' "$DATA")
FAILED=$(jq '.stats.failed' "$DATA")
echo "成功率: $(( (TOTAL - FAILED) * 100 / TOTAL ))%"
用
jq这类成熟工具解析,别用grep硬抠。JSON 格式一变,grep 规则全废,jq 还能稳。grep 适合临时看一眼,不适合写进生产链路。
3.1 环境变量也是一种传法
如果数据量小、且是同一次执行链路内的相邻步骤,可以通过环境变量传递。但环境变量不适合传大块或嵌套数据,只适合传标量开关/短令牌。比如 MODE=strict、API_KEY=xxx 这种,放环境变量合理;把一整份报表塞环境变量,是自找麻烦。
3.2 标准输出回传
相邻技能若在同一执行流里串行调用,上游的标准输出可被下游作为入参接收。但这条链路脆弱(输出格式一变就断),仅适合简单标量;复杂结构一律落盘成文件。把它当成「便签」,别当成「数据库」。
3.3 更重的传递通道(思路)
当技能协作规模变大,单纯的「文件 + 路径」可能不够——你需要查询、需要历史、需要多写者协调。这时候可以考虑外部通道,比如消息队列(解耦生产消费)、共享数据库(持久化结构化数据)。这些是否原生支持、怎么配,以官方文档当前版本为准。但核心思想不变:解耦生产端与消费端,让数据有稳定的归宿。
四、常见断链与修法
4.1 路径不一致(最高频)
A 写进 /abs/path/a,B 去读 ./a 相对路径,结果找不到。修法:双方都用同一个变量(如 {baseDir})推导绝对路径,谁都别写死、别用相对。这是技能协作里第一号 bug 来源。
4.2 编码/换行不一致
Windows 上生成的文件带 CRLF,Linux 工具按 LF 解析,字段对不上。修法:落盘时统一用 UTF-8 无 BOM、统一换行;读取端用 dos2unix 兜底。跨平台协作尤其要盯这一条。
4.3 并发写同一文件
两个技能同时写 result.json,内容互相覆盖,谁读谁错。修法:用带唯一标识的文件名(任务 ID / 进程 PID / 时间戳 + 随机串),写完再「原子改名」到最终名,避免读到半截文件。
# 先写临时名,再原子移动,下游只认最终名
TMP="{baseDir}/tmp/result.$$.json"
echo '{...}' > "$TMP" && mv "$TMP" "{baseDir}/tmp/result.json"
4.4 格式漂移
A 升级后多塞了个字段,B 还按旧结构解析,静默拿到 null。修法:给数据文件加 schema_version 字段,B 读取时先校验版本,不匹配就报错而不是硬撑。
{ "schema_version": 2, "stats": { "total": 1280 } }
VER=$(jq '.schema_version' "$DATA")
[ "$VER" = "2" ] || { echo "版本不兼容: $VER"; exit 1; }
4.5 权限/沙箱拿不到
A 写在沙箱里的文件,B 跑在另一个受限环境读不到。修法:把交换区放在双方都可达的共享位置,并确认沙箱策略允许该路径的读写(具体策略以官方文档当前版本为准)。
4.6 没人知道「该传什么」
这是协作崩坏最隐蔽的原因:A 以为 B 会读 result.json,B 以为 A 会写 report.md,两边对不上。修法:把交换区的文件名约定、字段约定、版本约定写进一份共享的「接口说明」,放在技能仓库显眼处。技能间的契约,值得像 API 文档一样被认真对待。
五、监控断链
断链发生时如果没人知道,危害更大。两个轻量做法:
- 存在性检查:B 启动时先确认输入文件存在、关键字段齐全,缺了就明确报错而不是默默用空值。
- 校验和/版本戳:A 写完顺带写一个
.meta记录版本与生成时间,B 读前核对,过期或版本不符立刻报警。
六、一个协作走查
任务:「爬取模块 A 抓数据 → 清洗模块 B 处理 → 报告模块 C 出图。」
- A 把原始数据写成
{baseDir}/tmp/raw.json,并写raw.meta标schema_version=1。 - B 读
raw.json,校验版本,清洗后写clean.json(版本升到 2)。 - C 读
clean.json,生成图表。某天 A 改了字段没通知,B 校验schema_version发现不匹配,直接报错,而不是产出一份基于旧字段的空报告。
这一道版本校验,拦下了一次本会导致下游全盘错误的静默故障。
七、一张选型表
| 你要在技能间传什么 | 用什么传 | 为什么 |
|---|---|---|
| 成型的报告/代码/网页 | 文件 + 路径 | 大、已成型,走路径不撑上下文 |
| 字段/统计/配置 | JSON 文件 | 机器可解析、可校验版本 |
| 短开关/令牌 | 环境变量 | 标量、同链路简单 |
| 简单标量、紧邻步骤 | stdout 回传 | 轻量,但脆弱 |
| 大规模/需历史查询 | 外部队列/库 | 解耦、持久化 |
| 断链现象 | 根因 | 修法 |
|---|---|---|
| 文件找不到 | 路径不一致 | 统一变量推导绝对路径 |
| 字段对不上 | 编码/换行 | 统一 UTF-8、LF |
| 内容被覆盖 | 并发写 | 唯一名 + 原子改名 |
| 静默拿 null | 格式漂移 | 加 schema_version 校验 |
| 沙箱读不到 | 隔离策略 | 放共享可达路径 |
| 两边对不上 | 无契约 | 写共享接口说明 |
八、把契约写成文档:接口说明长什么样
前面说「交换区要有共享接口说明」,给个最小模板,直接抄:
# 技能间数据契约:raw -> clean
生产者:爬取模块 A,输出 {baseDir}/tmp/raw.json
消费者:清洗模块 B,读取 raw.json,输出 clean.json
字段(schema_version=1):
- items[].url 字符串,必填
- items[].status 枚举 ok|fail,必填
变更规则:任何字段增删改,schema_version +1,并通知下游
这份文档不用长,但必须有人维护。它回答了一个最关键的问题:「上游改了,下游知不知道」。没有它,技能协作迟早因为「某次悄悄的改动」断链。
这份接口说明最好也进版本管理,和技能代码一起改、一起评审。很多人把它随手写在聊天记录或某个脑图里,结果换人接手就丢了,新人对着一份对不上的契约继续开发,断链从那一刻起就已经埋下。契约文档和代码同等重要,它定义了技能之间的「API」,理应享受和代码一样的维护待遇——改字段先改文档,再改代码,最后通知下游,这个顺序别乱。
九、幂等、重试与跨环境传递
- 幂等:同一个技能被定时任务重复触发,或失败后重试,写出的文件不应互相打架。用「先写临时名再原子改名」保证下游要么拿到完整的旧版、要么拿到完整的新版,绝不拿到半截。
- 重试:下游读不到上游产物时,别立刻判死,给一两次短暂重试(上游可能只是晚到了几秒)。但重试要带上限,避免雪崩。
- 跨机器/跨环境:如果技能跑在不同机器或不同沙箱,本地交换目录就不够了,需要共享存储(网络盘或对象存储)。具体怎么挂,以官方文档当前版本为准,但原则是一样的:找一个双方都可达、权限都明确的地方放交换文件。
十、最后一道防线:端到端冒烟
所有契约、校验都写好之后,别只看单点。定期跑一次「A→B→C 全链路冒烟」:故意用一个已知样本走完整条链,确认文件传到位、字段对得上、版本校验生效。这一遍全链路,能在用户之前发现 90% 的断链。把它也接进定时任务,每周自动跑一次,等于雇了个不睡觉的质检员。
这里的「端到端冒烟」不必复杂:准备一个写死的样例输入,让 A 产出、B 消费、C 出图,全程不依赖任何真实外部数据,十秒就能跑完。它测的不是业务逻辑,而是「接缝还在不在」——文件能不能传、字段对不对、版本校验生不生效。接缝一旦悄悄断掉,业务写得再对也跑不出结果,而冒烟就是那个最早发现接缝断裂的警报。
十一、一个常被问的问题:能不能直接走数据库而不是文件
能,而且当协作规模变大时往往更合适。文件交换胜在简单、零依赖,但缺点也明显:不方便查询历史、不方便多写者协调、不方便跨机器。数据库(或对象存储)可以把「交换」升级成「共享状态」——上游写一条记录,下游按条件查,天然支持并发与历史。但具体怎么连、原生支持哪些存储,以官方文档当前版本为准。我的建议是:两三个技能小打小闹,文件 + JSON 足够;上了规模、要可追溯、要多写者,再上数据库。过早引入重存储是另一种过度设计。
技能协作的成熟度,看的就是数据怎么流动。复制粘贴是石器时代;约定目录 + 结构化文件 + 版本校验,才是能长期跑、能多人维护的现代做法。把这一层打通,你离「组合技」(把多个技能串成自动工作流)就只差一步编排了——而那一步,恰恰建立在你今天写下的这份契约之上。
