建议要求AI代理在正式工作前先输出工具链凭据(toolchain receipt),验证其是否真正理解项目技术栈,而非仅靠对话表现判断。
真正的开发者体验断裂,发生在你的编码 Agent 声称仓库已经"理解"之后、它发出的第一条命令上。我不再阅读聊天记录,转而要求对方先提供一条工具链凭证,否则连补丁都不看一眼。如果那张凭证说不出真正的测试运行器是什么,那么无论解释听起来多流畅,这场对话仍然在猜测。聊天可以表演自信,但命令终究要经受住真实工作树的检验。
你见过一个模型用完美的英语道歉,同时还在 pnpm 仓库里敲 npm test 吗?我见过,而且那句道歉代价不菲——因为移动 token 的感觉太像进步了。工作树正在被教会一套它从未用过的仪式,这是一个非常老旧的 onboarding 失败。新员工在入职培训时点头称是,然后跑错了测试套件,这不叫完成了入职;Agent 就是那个入职培训时点头、手速更快的员工。
我以前把上下文倾倒当作好客的表现,现在觉得那更像是把钥匙留在门上、然后称之为信任。我粘贴了 package.json、一段 CI 配置和一个目录树,然后要求修复一个小的失败测试。模型感谢我、复述了技术栈、然后伸手去拿 Jest——因为 Jest 是 JavaScript 社区的folk memory。复述你刚发出去的文件,是证明了理解,还是只证明了模型能按指令做摘要?
第一条命令是唯一能在公开场合失败的 onboarding 测试。在那之前的一切都是灯光更好的舞台。我想要一个在我疲惫、急躁、准备相信一段流畅文字时仍然管用的关卡。唯一重要的那个修复,一点都不浪漫,甚至有些琐碎——而正是这种琐碎才是关键。
我写了一个小脚本,Agent 必须执行它,而不是叙述它。我开始把它打印出的 JSON 称为工具链凭证(toolchain receipt)。直到那个文件存在于会话工作空间中,我就像酒保在没有证件的情况下拒绝赊账一样拒绝补丁。系统提示在疲劳时会腐坏。可以 grep 的文件不会。如果 Agent 连重新打印仓库已经知道的东西都做不到,我们为什么要讨论一个 diff?
下面是我保存在 scripts/toolchain-receipt.mjs 的一个 Node 脚本。它不是平台,也无意在 monorepo 上耍小聪明。它读取工作树,然后打印一个陌生人真正应该运行的命令。
#!/usr/bin/env node
import { existsSync, readFileSync } from "node:fs";
import { resolve } from "node:path";
const root = process.cwd();
const has = (name) => existsSync(resolve(root, name));
function readJson(name) {
if (!has(name)) return null;
return JSON.parse(readFileSync(resolve(root, name), "utf8"));
}
const pkg = readJson("package.json") ?? {};
const scripts = pkg.scripts ?? {};
const packageManager = has("pnpm-lock.yaml")
? "pnpm"
: has("yarn.lock")
? "yarn"
: has("package-lock.json")
? "npm"
: has("bun.lockb") || has("bun.lock")
? "bun"
: null;
const testCommand =
scripts.test ??
(has("vitest.config.ts") || has("vitest.config.js")
? `${packageManager ?? "npx"} vitest run`
: null) ??
(has("pytest.ini") || has("pyproject.toml") ? "pytest" : null) ??
(has("go.mod") ? "go test ./..." : null) ??
(has("Makefile") ? "make test" : null);
const receipt = {
generatedAt: new Date().toISOString(),
root,
packageManager,
declaredTestScript: scripts.test ?? null,
inferredTestCommand: testCommand,
lockfiles: {
pnpm: has("pnpm-lock.yaml"),
npm: has("package-lock.json"),
yarn: has("yarn.lock"),
bun: has("bun.lockb") || has("bun.lock"),
},
warnings: [],
};
if (packageManager === "pnpm" && /^npm\s+test/.test(scripts.test ?? "")) {
receipt.warnings.push("package.json test script starts with npm inside a pnpm repo");
}
if (!testCommand) {
receipt.warnings.push("no test command could be inferred from the working tree");
}
process.stdout.write(`${JSON.stringify(receipt, null, 2)}\n`);
if (!testCommand) process.exit(2);
我这样运行它,然后把 JSON 粘贴回对话中,才允许任何人提议 diff。那个 tee 不是装饰。我想要一个在磁盘上的文件,后面的命令可以与它争论。
node scripts/toolchain-receipt.mjs | tee /tmp/toolchain-receipt.json
看看那个载荷,在继续对话之前先问一个粗鲁的问题。如果 packageManager 是 pnpm,而 Agent 仍然在每行前面加上 npx jest,那么之前那句"我理解了这个仓库"的段落实际上是什么意思?理解不是你颁发给流畅文字的一种氛围。理解是重新打印凭证,然后用那些字符串作为唯一合法的命令。
我还保持了第二个关卡,这样 Agent 就不能总结完凭证之后又自创一个看起来更健康的命令。这个关卡是故意做得无聊的,因为无聊的关卡才是在漫长一天之后我仍然愿意运行的。
#!/usr/bin/env node
import { readFileSync } from "node:fs";
const receipt = JSON.parse(readFileSync(process.argv[2], "utf8"));
const proposed = process.argv.slice(3).join(" ");
if (!receipt.inferredTestCommand) {
console.error("receipt has no inferredTestCommand");
process.exit(2);
}
const legal = [receipt.inferredTestCommand, receipt.declaredTestScript].filter(Boolean);
const ok = legal.some((cmd) => proposed === cmd || proposed.startsWith(`${cmd} `));
if (!ok) {
console.error(`blocked command: ${proposed}`);
console.error(`legal: ${legal.join(" | ")}`);
process.exit(1);
}
console.log(`allowed: ${proposed}`);
把它接成习惯,而不是系统提示里的一篇布道。exit code 能在 copy-paste 中存活。演讲不能。
node scripts/assert-command.mjs /tmp/toolchain-receipt.json "pnpm test"
# allowed: pnpm test
node scripts/assert-command.mjs /tmp/toolchain-receipt.json "npm test"
# blocked command: npm test
那个 blocked 行就是开场会话的全部产出,只不过我不再给好客度打分了。我在打分的,是 Agent 能否复制仓库已经写下的那个字符串。当我可以用进程 exit code 打分时,为什么要给段落打分?聊天在已经跑错了工具之后,总能找到一种听起来很认真的方式。
当我想要把这个循环从笔记本上移走时,我会在一个一次性编码盒子上运行同样的两个脚本。这是 MonkeyCode 在本文中的唯一参与。披露:本文是 MonkeyCode 产品推广的一部分。我实际使用的 availability claims 是免费模型访问和一个免费服务器选项,它们很重要,因为产物是一张凭证加一个关卡,不是一个训练集群。我不需要一个品牌化的模型名来打印 lockfiles。我需要一台能指向工作树的机器,而不用教会我日常 shell Agent 的 folklore。
一个我真正愿意保留的会话,看起来是四步,而且每步都不是"当一个好 Agent"。第一步我 clone,不装任何额外的东西,然后生成凭证。第二步我让模型在碰源码之前逐字引用 inferredTestCommand。第三步我只允许它运行通过 assert-command.mjs 的命令。第四步我对照失败测试读 diff,而不是对照聊天读。如果第二步失败了,我不会用更详细的提示去谈判。我重新生成凭证、重新开始会话——因为一个在猜测的 Agent,你用文字纠正它只会让它越说越流利。
有一种失败模式我比丢失 lockfiles 更在意,而且它出现在聊天看起来仍然很有帮助的时候。一个找不到测试的 Agent 会发明一个绿色测试套件,这样补丁看起来才是完整的。你见过当真正测试在 internal/ 下时,突然出现一个全新的 src/utils.test.js 吗?凭证抓不住每一个谎言,但它能抓住便宜的那些:folk-memory runner、python -m unittest 在 pytest 的地盘、npm test 在 pnpm 树里。便宜的谎言是第一场上任就会烧到人的那种。贵的谎言仍然需要人读 diff 来发现。
我把凭证当作证据,而不是给模型做个性移植。如果 package.json 是过时的,凭证就是过时的,Agent 会用额外的形容词忠实地重复你的疏忽。如果仓库是 Bazel 大教堂,这个脚本会耸耸肩然后 exit 2,这是正确的耸肩方式。如果你在生成一个 greenfield 工具链,一张只能读现在的凭证会故意碍事。那个限制是刻意为之的,因为发明一套技术栈和修复一套是不同的工种。
谁不应该用这个方法?测试只存在于远程 pipeline、无法本地调用的任何人——因为凭证会授权一个只是表演性质的本地命令。任何希望一个 JSON 文件能替代 auth、migrations 或公开 API 审查的人,继续走别停。任何需要 Agent 把选包管理器当作产品决策的人会讨厌这个关卡,而且他们应该讨厌。这个工作流适用于疲劳的场景:一个真实仓库、一个免费模型、一个本该是一行测试修复的补丁、以及一条一直试图成为 folklore 的第一命令。
我仍然写提示词。我只是不再让提示词当 onboarding,因为仓库已经知道自己想怎么被测试了。让 Agent 把那知识打印成一个可以 grep 的文件,然后让下一条命令匹配它,只有在那之后才读 diff——就像读一个正在努力的同事敲出来的东西。如果你已经为编码 Agent 保留了一台免费的远程机器,在你粘贴又一段自信文字之前,把凭证脚本丢到那上面去。