返回博客

并行 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 里的错误会被 N 个 Agent 等比例放大

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"](...) 手动补两处)。

正解:派发前 Read 源码验 cheatsheet,派发后一次 tsc 找共同根因
  1. 写 cheatsheet 时,每条条目从源码核实,不凭记忆;
  2. 所有 Agent 完工后统一跑 tsc --noEmit,同类重复错误 = 单一根因;
  3. 批量调用形态修复用括号深度扫描,不用 sed/正则。

一句话外卖

Cheatsheet 是你给每个 Agent 的输入合同——合同里有一处错误,所有 Agent 都会按合同出错;验证一次比修复 N 次便宜得多。