并行 Agent 派发前未验证共享 API Cheatsheet,一行错误炸出 53 个编译错误
用多个并行 Agent 构建页面是提速的利器:5 个 Agent 同时开工,各自负责独立的页面模块,互不干扰。为了防止 Agent 凭空臆造 props,我们会在每个 Agent 的 prompt 里注入一份统一的"API cheatsheet"——组件 props 速查表、函数签名、调用形态。这个做法方向没错,但有一个隐蔽的爆炸点。
现象
向 5 个 Agent 分发 12 个页面任务,所有 Agent 共享同一份 cheatsheet,里面写着:
// cheatsheet 里的写法
toast.success("操作成功");
toast.warning("请注意");所有 Agent 完成后,跑一次 tsc --noEmit:
error TS2339: Property 'success' does not exist on type '(options: ToastOptions) => void'
error TS2339: Property 'warning' does not exist on type '(options: ToastOptions) => void'
...(共 53 个)
53 个错误,分布在 12 个文件里。每个 Agent 都在忠实执行我给它的 cheatsheet——只是那份 cheatsheet 本身是错的。
根因
组件库的 toast 实际导出是对象式调用,没有 .success / .warning / .info / .danger 这些方法链:
// 实际 API
toast({ title: "操作成功", tone: "info" });
toast({ title: "请注意", tone: "danger" });
// tone 只有 "info" | "danger" | "neutral",没有 "success" / "warning"我在写 cheatsheet 时没有 Read 源码,凭印象从另一个风格类似的库(Radix + sonner 组合)迁移了调用方式,写进去的是错的。错误代价的不对称性极其悬殊:
- 核实一个 API 的成本:1 次
Read,30 秒 - 猜错一个 API 的成本:N 个 Agent × 每个 Agent 调用该 API 的次数 = 53 个错误 + 全量修复一遍
Cheatsheet 不是随手写的参考,它是你给每个 Agent 的合同。一行错误 × N 个 Agent = N 倍错误。尤其危险的几类:
- 命令式 vs 链式:
toast({ ... })还是toast.success(...)?modal.confirm({...})还是<Modal />? - render prop vs asChild:Base UI 系组件库用
<Trigger render={<Button />} />,不是 Radix 的asChild模式 - 枚举值集合:
tone可能是info | danger | neutral,并不包含你以为存在的success | warning - 数字 prop 的语义:
total是总页数还是总条数?
正解
1. 派发前,逐条从源码核实 Cheatsheet
每一条 cheatsheet 条目都要 Read 对应组件的 .d.ts / 类型声明或实际调用示例,不要凭记忆转写。
# 快速定位类型声明
grep -rn "export.*toast" src/components/ui/toast.tsx不确定的条目,与其猜测,不如在 prompt 里告诉 Agent:「本条不确定,请自行 Read src/components/ui/toast.tsx 核实签名后使用。」—— 猜测的 API 比没有 API 更危险。
2. 派发后,一次权威 typecheck
不要信任 Agent 的自我汇报("已完成,类型通过")。等所有 Agent 完工后,跑一次集中的 tsc --noEmit:
npx tsc --noEmit同一个错误反复出现在多个文件 = cheatsheet 中毒,不是 N 个 Agent 各自犯错。 找到那一条源头错误,修一次,不要逐文件追踪症状。
3. 系统性调用形态错误,用括号深度扫描器批量修
当修复是机械性的(toast.success(ARG) → toast({ title: ARG, tone: "info" })),朴素的 sed 正则会在模板字符串和嵌套括号处截断。用带括号深度跟踪的 Python 脚本一次处理全部:
import re, pathlib
MAP = {"success": "info", "info": "info", "warning": "danger", "danger": "danger"}
pat = re.compile(r'toast\.(success|info|warning|danger)\(')
for f in pathlib.Path('.').rglob('*.ts*'):
s = f.read_text(); out = []; i = 0
while (m := pat.search(s, i)):
out.append(s[i:m.start()])
depth = 0; j = m.end() - 1; instr = None
while j < len(s):
c = s[j]
if instr:
if c == '\\': j += 2; continue
if c == instr: instr = None
elif c in '"\'`': instr = c
elif c == '(': depth += 1
elif c == ')':
depth -= 1
if depth == 0: break
j += 1
arg = s[m.end():j]
out.append(f'toast({{ title: {arg}, tone: "{MAP[m.group(1)]}" }})')
i = j + 1
out.append(s[i:])
f.write_text(''.join(out))脚本跑完,重新 tsc --noEmit:53 个错误全部消失(动态索引形式 toast[cond ? "success" : "info"](...) 手动补两处)。
- 写 cheatsheet 时,每条条目从源码核实,不凭记忆;
- 所有 Agent 完工后统一跑
tsc --noEmit,同类重复错误 = 单一根因; - 批量调用形态修复用括号深度扫描,不用
sed/正则。
一句话外卖
Cheatsheet 是你给每个 Agent 的输入合同——合同里有一处错误,所有 Agent 都会按合同出错;验证一次比修复 N 次便宜得多。