老周的工具笔记

老周的工具笔记

已发布 24 篇文章

技能之间的数据传递:别再用复制粘贴

技能之间的数据传递:别再用复制粘贴

技能之间要协作,第一道坎就是数据怎么从 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 足够;上了规模、要可追溯、要多写者,再上数据库。过早引入重存储是另一种过度设计。

技能协作的成熟度,看的就是数据怎么流动。复制粘贴是石器时代;约定目录 + 结构化文件 + 版本校验,才是能长期跑、能多人维护的现代做法。把这一层打通,你离「组合技」(把多个技能串成自动工作流)就只差一步编排了——而那一步,恰恰建立在你今天写下的这份契约之上。