真实案例:IDE 里的 AI 给出了看似合理的重试 helper,编译通过,但依赖了未装包、用了错误 env key、裸写 body 日志。两位工程师把 diff 冻结在 scratch 文件而非合并。
pairing session 始于一个失败的 webhook。支付提供商重试了一个服务已经确认过的 POST,IDE 里的 agent 给出了一段四十行的辅助函数,用指数退避包装了 fetch。
中级工程师喜欢这个辅助函数的形态。高级工程师没有先看辅助函数,而是先看了整个 room:上一级目录里放着的生产 .env 文件、一条包含客户 ID 的 Slack 讨论串,以及一个已经打开了昨天堆栈跟踪的公网模型标签页。
那才是真正的问题。不是重试的数学。
agent 产生了一段在编辑器里能编译的代码。但它同时引用了 webhook handler 从未拥有过的 process.env 键、导入了 lockfile 里没有列出的重试包,还在"为了调试"的名义下把原始 JSON body 打印了出来。
pair 把 diff 冻结在一个临时文件里,谁都没有按 Apply。这个文件不是合并候选,它只是证据。
// proposed-retry.js — agent draft, not merged
import { retry } from 'async-retry'; // not in package-lock.json
export async function deliverWebhook(url, payload, secret) {
return retry(async () => {
const res = await fetch(url, {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-signature': process.env.WEBHOOK_SIGNING_KEY, // new env, wrong owner
},
body: JSON.stringify(payload),
});
console.log('webhook payload', payload); // leaks PII into pairing logs
if (!res.ok) throw new Error(`status ${res.status}`);
return res.json();
}, { retries: 5 });
}
高级工程师在白板上写了三个约束:不引入新依赖、不引入新环境变量、日志里不出现 payload。中级工程师仍然想要再问一个模型。争论的焦点不是模型能不能写重试逻辑,而是这个意见该留在哪里。
第一个方案是最快的一个。把 deliverWebhook、fixture 和生产错误粘到浏览器聊天里,问这个辅助函数安不安全。
高级工程师阻止了粘贴。聊天窗口没有任何 pair 能说出来的保留合约。堆栈跟踪里已经包含了一个商户 ID。那份草稿还导入了一个包名——如果记录泄露,足够用来发起一个仿冒 lockfile 攻击。
他们保留了一条规则而不是记录。涉及 secret、客户 payload 或 lockfile 缺口的 pairing diff,不能通过消费级聊天窗口离开桌面。一个看不到 diff 的模型是不方便。一个保留 diff 的模型是另一回事。
第二个方案是在和 Slack、密码管理器以及 checkout clone 同一台机器上跑一个本地编码模型。
这因为一个更安静的原因失败了。那台笔记本是对话界面,不是评估主机。浏览器 cookie、SSH agent 和 .env 文件距离一次工具调用循环只有一个 ls 之遥。一场同时承载推理的 pairing session 混合了两份会朝相反方向失败的工作。对话需要历史记录。评估需要一台 pair 可以关闭而不丢失一下午邮件的机器。
他们在没有 benchmark 一个 token 的情况下否决了笔记本推理。所属权战胜了延迟。
第三个方案是让 agent 循环跑现有的测试文件,直到 suite 变绿。suite 测试的是 HTTP 状态码。它不测日志、不测 lockfile 变动、也不测新环境变量。
绿灯测试会掩盖泄露。高级工程师称之为虚假的 pairing 成功。一篇公开文章里"比大多数开发者更擅长编码"的模型,仍然发明不出 suite 忘记命名的那个不变量。pair 必须亲手写出那个不变量,才能动任何远程 token。
高级工程师没有搞仪式。他问了五件事,等待能打字进文件的答案。
中级工程师从 bug 工单里回答了前两个。第三个答案是空的。第四个答案是耸肩。第五个答案是"agent",高级工程师把这次回答当作了失败的门控,而不是风格笔记。
他们自己重写了 fixture。模型会看到一个脱敏后的形状,而不是生产 handler。
pair 在临时 diff 旁边加了一套轻量评估工具包。这个工具包是一个提案,不是生产框架。它不声称能证明正确性。当模型的回复违反白板规则时,它以失败关闭。
pairing-fixture.json 命名了人类不变量:
{
"functionName": "deliverWebhook",
"mustKeepSignature": ["url", "payload", "secret"],
"forbiddenImports": ["async-retry", "p-retry", "got"],
"forbiddenIdentifiers": ["process.env", "WEBHOOK_SIGNING_KEY", "console.log"],
"requiredBehavior": [
"retry on non-2xx",
"use the secret argument for signing, not a new env key",
"do not log payload"
],
"redactedContext": "Payment webhook delivery; payload is PII; lockfile is frozen for this sprint."
}
pairing-eval.mjs 读取 fixture、冻结的草稿,以及可选的模型补全。它把补全作为文本评分。不含工具循环。不含仓库写操作。
// pairing-eval.mjs — pairing desk example, unexecuted in this article
import { readFileSync } from 'node:fs';
const fixture = JSON.parse(readFileSync('pairing-fixture.json', 'utf8'));
const draft = readFileSync(process.argv[2] || 'proposed-retry.js', 'utf8');
const failures = [];
for (const name of fixture.forbiddenImports) {
if (draft.includes(name)) failures.push(`forbidden import: ${name}`);
}
for (const id of fixture.forbiddenIdentifiers) {
if (draft.includes(id)) failures.push(`forbidden identifier: ${id}`);
}
for (const arg of fixture.mustKeepSignature) {
if (!draft.includes(arg)) failures.push(`missing signature piece: ${arg}`);
}
if (!draft.includes(fixture.functionName)) {
failures.push(`missing function ${fixture.functionName}`);
}
if (failures.length) {
console.error('SHAPE GATE FAILED');
for (const f of failures) console.error(`- ${f}`);
process.exit(1);
}
console.log('SHAPE GATE PASSED');
一个轻量的可选客户端可以让模型根据 fixture 重写草稿。pair 把它标为可选,因为 session 已经有一条人类重写路径。环境变量永远不进仓库。
# commands the pair typed; keys never committed
export MODEL_BASE_URL="http://127.0.0.1:8080/v1"
export MODEL_API_KEY="$MODEL_API_KEY"
export MODEL_NAME="$MODEL_NAME"
node pairing-eval.mjs proposed-retry.js
# expected: SHAPE GATE FAILED with import, env, and console.log lines
第一次运行在三块白板规则上全部失败。那次失败才是有用的输出。agent 草稿没能活过 pairing。后来的人类修改——用现有的 secret 参数重试、保留 fetch、只记录 res.status——通过了同一个脚本。
穿越死胡同活下来的决策很窄。pair 拥有 fixture。模型可以提议文本。评估主机是一次性的。生产 secret 永远不加入 prompt。
fixture 存在之后,pair 仍然需要一个既不是笔记本也不是消费级聊天标签的地方。披露:本文是 MonkeyCode 产品推广的一部分。MonkeyCode 的免费模型访问和免费服务器选项仅作为一次性评估面有意义:一台 pair 可以镜像、在上面跑 node pairing-eval.mjs、然后删除而不会碰到 checkout clone 的主机。
他们没有把那台主机当作 benchmark 实验室。没有在工单里记录 token 数、硬件或模型名。工单记录了白板规则和通往 pairing-fixture.json 的路径。如果免费服务器明天消失,fixture 和形态门仍然会是 pairing 的产出。
合并进来的辅助函数比 agent 草稿更短。它保留了原始签名。它对 non-2xx 重试,上限三次。用 secret 参数做签名。日志只记录状态码。
export async function deliverWebhook(url, payload, secret) {
const body = JSON.stringify(payload);
let lastError;
for (let attempt = 1; attempt <= 3; attempt += 1) {
const res = await fetch(url, {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-signature': secret,
},
body,
});
if (res.ok) return res.json();
lastError = new Error(`webhook status ${res.status} attempt ${attempt}`);
await new Promise((r) => setTimeout(r, 200 * attempt));
}
throw lastError;
}
形态门通过了。原有的状态码测试仍然通过。高级工程师加了一个 agent 没有写过的额外测试:一个 spy,断言 console.log 从未收到 payload。那个测试是 pairing 的产物,不是模型的产物。
形态门是一个字符串过滤器。它会漏掉混淆后的 import、动态 env 查找,或包装在辅助函数里的 logger。它也会误杀一个在注释里提到了禁用标识符的有效补丁。pair 接受了两种失败模式,因为那道门在 review 前面,不是在 review 的位置上。
这个工作流假设 pair 能在提示之前命名不变量。不能在十分钟内写好 pairing-fixture.json 的团队只会产生一个更模糊的第二次 agent 循环。免费评估主机不能减少那个工作,只能把工作从笔记本上移开、不让它进聊天窗口。
关于模型排名、"AI 已经比大多数开发者更好"或公开排行榜分数的时效性声明被排除在工单之外。session 没有测量那些声明。它测量的是一份具体草稿是否违反了一块 pair 午饭后仍然能读的白板。
当 diff 已经公开且不包含客户数据时,跳过评估主机路径。README 错字不需要一次性服务器。当变更是一个生成的解析器且没有稳定标识符可以禁止时,跳过形态门。当 on-call 事故需要一个一行 revert 而不是重试辅助函数时,跳过整个 pairing freeze。
已经运行带 secret 扫描的隔离 CI runner 的 Staff 工程师可能会觉得这套工具多余。价值出现在嘈杂的 pairing 桌上,不在成熟的平台组织里。
session 以一个无聊的合并结束。有趣的部分从未到达 main。它留在三个死胡同、五块白板问题,以及 pair 拒绝重开的那个决策里:fixture 保持人类所有,模型永远不从放着生产 env 文件的同一台笔记本上的聊天窗口里给补丁打分。