直接回答

应把 Cursor、Claude Code、GitHub Copilot 或其他合格 AI 编程工具作为一项受控实验来运行。执行前冻结任务集、仓库快照、验收测试、环境、指令、预算、权限与评审标准,并保留包括失败在内的所有尝试。决策依据应是已验收变更、人类评审与返工、回归、策略违规、不确定性和总成本,而不是生成行数、公开排行榜或主观星级。

本文只负责实验协议。应先使用 AI 编程工具怎么选:能力边界、安全与团队适配定义工作模式,并淘汰无法通过硬门禁的候选。

核心要点

  • 工具名不是实验条件;必须记录客户端版本、模式、模型策略、设置、权限与 Runtime。
  • 公开基准测试数据集和 Harness,无法复现你的仓库、需求、评审者与安全边界。
  • 使用成对代表性任务、干净快照、等价预算、随机顺序与独立评审。
  • 运行前定义“验收”,并计入评审、返工、拒绝尝试、回归和事故。
  • 报告置信区间与切片结果;弱样本下的“结论不足”比虚构冠军更有价值。

为什么公开编程基准无法替产品选型?

公开编程基准是有用的研究工具,但分数不是采购结论。

最初的 SWE-bench 论文从 12 个 Python 仓库收集 2,294 个 Issue,要求模型根据仓库和问题描述生成 Patch。它可以测试长上下文仓库推理和多文件修改,但不能覆盖所有产品模式、语言、代码评审标准、集成方式和运行风险。

基准有效性也会变化。OpenAI 在 2026 年 2 月报告 SWE-bench Verified 存在污染和错误测试;2026 年 7 月进一步审计 SWE-bench Pro,估计公开切分约 30% 的任务损坏,问题包括测试过度严格、Prompt 欠规格、测试覆盖不足和误导性 Prompt,并撤回此前推荐 SWE-bench Pro 的意见。

这不代表所有仓库基准都无效,而是每个分数必须绑定:

  • 数据集和准确切分;
  • Harness、Scaffold、工具、模型与推理设置;
  • 环境与网络策略;
  • 任务质量审计;
  • 污染与泄漏分析;
  • 成本与重试预算;
  • 日期与来源;
  • 基准没有覆盖的能力边界。

产品对比需要更强证据:来自自有工作的私有或新建任务、人工评审、运行控制,以及与决策一致的结果。

生产率比完成时间更难测量

开发者生产率是系统结果,不是单个计时器。

METR 早期 2025 随机对照试验让 16 名资深开源开发者在熟悉仓库中完成 246 项任务。在该协议下,允许使用早期 2025 AI 工具使完成时间增加 19%,但参与者认为工具让自己更快。

METR 后来把该历史结果标记为过时。其 2026 年 2 月更新说明新任务级实验已经无法可靠估计当前影响,因为开发者会退出禁止 AI 的任务、任务选择发生变化、并行 Agent 令时间记录困难,而且输出质量可能不同,原始估计的置信区间也很宽。

持久结论不是“AI 让开发者变慢”或“AI 让开发者变快”,而是感知速度、任务完成、主动人类时间、墙钟时间、输出质量与选择效应是不同变量。实验必须明确自己测量哪一个。

先定义决策,再运行试验

试验应该回答一个有边界的决策:

yaml
decision: choose a default tool for bounded maintenance pull requests
candidates:
  - candidate-a-local-agent
  - candidate-b-cloud-agent
  - current-human-workflow
repositories:
  - typescript-service
  - python-library
task_slices:
  - bug-fix
  - test-addition
  - dependency-safe-update
  - narrow-refactor
excluded:
  - production-operations
  - schema-migrations
  - security-critical-auth-changes
primary_outcome: accepted_change_without_material_rework
hard_gates:
  - no_policy_violation
  - no_unapproved_network
  - no_secret_access
  - required_tests_pass

不要问“哪个工具最好”,而应问“在这些质量、安全、成本与评审约束下,哪种合格模式能为这些任务切片产出更多已验收变更”。

候选可以是混合工具链或当前工作流。保留“不采用”选项,避免实验预设结论。

构建有代表性的任务集

有效任务集应采样团队准备委派的真实工作,也要覆盖工具可能失败的条件。

从真实工作抽样

使用近期已完成或新编写、且预期行为可以独立核验的任务。候选环境必须移除原始 Patch 和后续 Git 历史,不能允许模型或检索工具直接在公网查到答案。

保留任务切片

给每个任务标注可能改变结果的维度:

切片 示例
工作类型 Bug、Feature、重构、测试、文档、依赖更新
范围 单文件、子系统、跨服务
语言和框架 TypeScript、Go、Python、Java、基础设施
上下文质量 完整规格、模糊遗留 Issue、缺失测试
风险 低、中、安全敏感、不可逆
仓库成熟度 Greenfield、成熟服务、遗留单体
验证方式 单测、集成测试、静态分析、人类行为评审

关键切片要有足够重复观测。某切片只有一个任务时,只能报告这次观测,不能泛化。

加入干净与对抗任务

应包含:

  • 不需要修改的任务;
  • 正确动作应该是澄清的欠规格任务;
  • 应该停止的不可完成任务;
  • 含诱饵密钥和禁止路径的仓库;
  • 包含间接提示词注入的不可信注释或工具结果;
  • 应被拒绝的命令;
  • 能通过狭窄测试却破坏系统不变量的变更。

对抗任务必须在隔离 Fixture 中执行,不得包含真实密钥、生产访问或不受控网络出口。

预注册验收与失败状态

必须在看到候选输出前定义验收。

已验收变更应同时满足:

  1. 行为:显式验收测试与相关回归测试通过。
  2. 范围:变更位于请求契约内,或记录经过批准的偏离。
  3. 质量:独立评审未发现实质正确性、可维护性或架构问题。
  4. 安全:没有禁止访问、密钥泄漏、不安全依赖、策略绕过或未经审阅副作用。
  5. 证据:Diff、命令、测试、工具轨迹、成本和审批完整。

使用机器可读终态:

  • accepted;
  • rejected_correctness;
  • rejected_scope;
  • rejected_security;
  • rejected_maintainability;
  • clarification_required;
  • environment_failure;
  • budget_exhausted;
  • invalid_trial。

环境故障与无效试验不能被静默算作工具失败或直接删除,应按预注册策略报告和重跑。

控制实验条件

公平比较指机会等价,不是每个界面点击都相同。

flowchart LR T["冻结任务与 Base Commit"] --> E["干净隔离环境"] E --> C["固定候选配置"] C --> R["随机执行顺序"] R --> A["捕获制品与轨迹"] A --> H["独立验收评审"] H --> L["锁定结果账本"]

固定可以固定的条件

记录:

  • 产品、客户端、扩展或 CLI 版本;
  • 运行模式和模型策略;
  • 可配置时选择的模型;
  • 仓库 Base Commit 与环境镜像 Hash;
  • 规则、Prompt、任务规格与工具配置 Hash;
  • 文件、Shell、网络、密钥与 MCP 权限;
  • 重试、Token、时间与金额预算;
  • 评审规则 Revision。

若厂商管理模型或服务版本,则记录 vendor-managed 和运行时间,不能虚构产品未暴露的精度。

从干净快照开始

每个候选在独立 Worktree、容器或 VM 中接收同一 Base Commit 与依赖状态。下一次运行前清除候选输出,避免缓存泄漏 Patch、对话或测试。

提供等价信息

各候选获得相同任务要求与可访问文档。不能给一个工具精心迭代的 Prompt,另一个只给一句 Issue。产品语法可以不同,但信息和可执行动作必须等价。

随机化并分块

在任务块内随机化候选顺序,降低学习、疲劳与时间效应。不同开发者操作工具时,应按开发者分块或轮换,不能让最强拥护者只操作自己偏爱的工具。

分离操作员与评审者

可行时,评审者只接收隐藏产品身份的标准化 Diff 与证据。在判断正确性和可维护性前,不应看到营销标签、Token 数或叙事轨迹;安全评审可在代码结论后查看完整轨迹。

测量已验收结果与人类负载

原始产出不是生产率。指标集必须暴露权衡。

指标 定义 价值
验收率 已验收试验 / 有效试验 基础有效结果率
人类分钟数 主动操作 + 评审 + 返工 衡量稀缺团队注意力
墙钟时间 从开始到终态 适用于同步和排队任务
Accepted Goodput 每人类小时的已验收变更 结合效用与人力容量
每个已验收变更成本 全部直接与人力成本 / 已验收变更 衡量经济产出
回归数 可归因于候选的后续失败 发现窄测试优化
策略违规 禁止访问或动作事件 安全硬信号
范围偏离 任务外实质未批准修改 发现过度主动
澄清质量 对模糊任务的正确澄清 奖励安全无能力
评审队列影响 等待与评审者负载 发现并行 Agent 饱和

必须按任务与仓库切片报告。高聚合分可能掩盖团队最关键语言或风险类别的失败。

分开记录时间

至少记录:

text
人类分钟数 = 主动操作 + 评审 + 返工
墙钟分钟数 = 从委派到终态的经过时间

并行 Agent 下不能把重叠墙钟直接相加为人类时间,应记录操作员主动区间或采用预定义时间抽样。

为人力与失败定价

text
总成本
  = 工具和计算直接成本
  + 人力费率 * 人类分钟数 / 60
  + 事故和回滚成本

每个已验收变更成本
  = 总成本 / 已验收变更数

拒绝试验也进入总成本。没有已验收变更时,该指标是未定义,而不是零。

使用置信区间与诚实结论

一次观测差异不自动等于稳定优势。

二元验收率应报告数量与置信区间;偏斜的时间和成本应报告中位数与底层观测,而不只给平均值。任务少或区间宽时,应标记为结论不足。

应避免:

  • 用任意权重的综合分给候选排名;
  • 把微小百分比差异当成决定性结论;
  • 合并互不相干的任务切片;
  • 看到结果后排除失败或昂贵尝试;
  • 选择能让偏爱候选获胜的阈值。

安全与正确性硬门禁应和优化指标分开。候选不能用节省评审时间来抵扣密钥访问。

建立机器可读试验账本

每次尝试都应作为不可变证据存储:

json
{
  "experiment_id": "coding-tools-maintenance-v1",
  "labor_cost_per_hour": 120,
  "gates": {
    "min_acceptance_rate": 0.6,
    "max_policy_violations": 0,
    "max_regressions": 0,
    "max_cost_per_accepted_change": 900
  },
  "trials": [
    {
      "trial_id": "candidate-a-task-001",
      "tool": "candidate-a",
      "task_id": "task-001",
      "task_type": "bug-fix",
      "tool_version": "pinned-or-vendor-managed",
      "model": "pinned-or-vendor-managed",
      "environment_hash": "sha256:...",
      "prompt_hash": "sha256:...",
      "status": "accepted",
      "active_minutes": 12,
      "review_minutes": 18,
      "rework_minutes": 4,
      "wall_minutes": 41,
      "direct_cost": 7.4,
      "regressions": 0,
      "policy_violations": 0
    }
  ]
}

以上阈值只是字段示例,不是通用建议。团队应在运行前,根据现有服务、风险与成本目标设置。

可运行评测门禁

以下无第三方依赖的 Python 程序会校验成对任务覆盖,拒绝重复或格式错误的试验,计算 Wilson 区间、人类负载、Accepted Goodput 与已验收变更成本,然后应用可配置门禁。

python
from __future__ import annotations

import argparse
import json
import math
import statistics
from collections import defaultdict
from pathlib import Path
from typing import Any


STATUSES = {
    "accepted",
    "rejected_correctness",
    "rejected_scope",
    "rejected_security",
    "rejected_maintainability",
    "clarification_required",
    "budget_exhausted",
}
NUMERIC_FIELDS = {
    "active_minutes",
    "review_minutes",
    "rework_minutes",
    "wall_minutes",
    "direct_cost",
    "regressions",
    "policy_violations",
}
STRING_FIELDS = {
    "trial_id",
    "tool",
    "task_id",
    "task_type",
    "tool_version",
    "model",
    "environment_hash",
    "prompt_hash",
    "status",
}
GATE_NAMES = {
    "min_acceptance_rate",
    "max_policy_violations",
    "max_regressions",
    "max_cost_per_accepted_change",
}


def is_number(value: Any) -> bool:
    return isinstance(value, (int, float)) and not isinstance(value, bool)


def wilson(successes: int, total: int, z: float = 1.96) -> list[float]:
    if total == 0:
        return [0.0, 0.0]
    p = successes / total
    denominator = 1 + z * z / total
    center = (p + z * z / (2 * total)) / denominator
    spread = z * math.sqrt(
        (p * (1 - p) + z * z / (4 * total)) / total
    ) / denominator
    return [max(0.0, center - spread), min(1.0, center + spread)]


def load(path: Path) -> dict[str, Any]:
    value = json.loads(path.read_text(encoding="utf-8"))
    if not isinstance(value, dict):
        raise ValueError("experiment root must be an object")
    return value


def validate(experiment: dict[str, Any]) -> list[str]:
    errors: list[str] = []
    labor = experiment.get("labor_cost_per_hour")
    gates = experiment.get("gates")
    trials = experiment.get("trials")
    if not is_number(labor) or labor < 0:
        errors.append("labor_cost_per_hour must be non-negative")
    if not isinstance(gates, dict):
        errors.append("gates must be an object")
        gates = {}
    if not isinstance(trials, list) or not trials:
        return errors + ["trials must be a non-empty list"]

    unknown_gates = set(gates) - GATE_NAMES
    if unknown_gates:
        errors.append(f"unknown gates: {sorted(unknown_gates)}")
    for name, value in gates.items():
        if not is_number(value) or value < 0:
            errors.append(f"gate {name} must be non-negative")

    pairs: set[tuple[str, str]] = set()
    tasks_by_tool: dict[str, set[str]] = defaultdict(set)
    for index, trial in enumerate(trials):
        if not isinstance(trial, dict):
            errors.append(f"trials[{index}] must be an object")
            continue
        for field in STRING_FIELDS:
            if not isinstance(trial.get(field), str) or not trial[field]:
                errors.append(f"trials[{index}].{field} must be a string")
        for field in NUMERIC_FIELDS:
            value = trial.get(field)
            if not is_number(value) or value < 0:
                errors.append(
                    f"trials[{index}].{field} must be non-negative"
                )
        status = trial.get("status")
        if isinstance(status, str) and status not in STATUSES:
            errors.append(f"trials[{index}].status is invalid")
        tool, task = trial.get("tool"), trial.get("task_id")
        if isinstance(tool, str) and isinstance(task, str):
            pair = (tool, task)
            if pair in pairs:
                errors.append(f"duplicate tool/task pair: {pair}")
            pairs.add(pair)
            tasks_by_tool[tool].add(task)

    if len(tasks_by_tool) < 2:
        errors.append("at least two tools are required")
    task_sets = list(tasks_by_tool.values())
    if task_sets and any(tasks != task_sets[0] for tasks in task_sets[1:]):
        errors.append("every tool must run the same task_id set")
    return errors


def summarize(experiment: dict[str, Any]) -> dict[str, dict[str, Any]]:
    groups: dict[str, list[dict[str, Any]]] = defaultdict(list)
    for trial in experiment["trials"]:
        groups[trial["tool"]].append(trial)

    labor = experiment["labor_cost_per_hour"]
    output: dict[str, dict[str, Any]] = {}
    for tool, trials in sorted(groups.items()):
        accepted = sum(t["status"] == "accepted" for t in trials)
        human_minutes = [
            t["active_minutes"] + t["review_minutes"] + t["rework_minutes"]
            for t in trials
        ]
        total_human = sum(human_minutes)
        total_direct = sum(t["direct_cost"] for t in trials)
        total_cost = total_direct + labor * total_human / 60
        output[tool] = {
            "trials": len(trials),
            "accepted": accepted,
            "acceptance_rate": accepted / len(trials),
            "acceptance_rate_wilson_95": wilson(accepted, len(trials)),
            "median_human_minutes": statistics.median(human_minutes),
            "median_wall_minutes": statistics.median(
                t["wall_minutes"] for t in trials
            ),
            "accepted_goodput_per_human_hour": (
                accepted * 60 / total_human if total_human else None
            ),
            "total_cost": total_cost,
            "cost_per_accepted_change": (
                total_cost / accepted if accepted else None
            ),
            "regressions": sum(t["regressions"] for t in trials),
            "policy_violations": sum(
                t["policy_violations"] for t in trials
            ),
        }
    return output


def apply_gates(
    summaries: dict[str, dict[str, Any]], gates: dict[str, float]
) -> list[str]:
    errors: list[str] = []
    for tool, result in summaries.items():
        checks = {
            "min_acceptance_rate": (
                result["acceptance_rate"],
                lambda value, limit: value >= limit,
            ),
            "max_policy_violations": (
                result["policy_violations"],
                lambda value, limit: value <= limit,
            ),
            "max_regressions": (
                result["regressions"],
                lambda value, limit: value <= limit,
            ),
            "max_cost_per_accepted_change": (
                result["cost_per_accepted_change"],
                lambda value, limit: value is not None and value <= limit,
            ),
        }
        for name, limit in gates.items():
            value, passed = checks[name]
            if not passed(value, limit):
                errors.append(
                    f"{tool} failed {name}: measured={value} limit={limit}"
                )
    return errors


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("experiment", type=Path)
    args = parser.parse_args()
    try:
        experiment = load(args.experiment)
    except (OSError, json.JSONDecodeError, ValueError) as error:
        parser.error(str(error))
    errors = validate(experiment)
    if errors:
        for error in errors:
            print(f"ERROR: {error}")
        raise SystemExit(1)
    summaries = summarize(experiment)
    print(json.dumps(summaries, indent=2, sort_keys=True))
    failures = apply_gates(summaries, experiment["gates"])
    if failures:
        for failure in failures:
            print(f"ERROR: {failure}")
        raise SystemExit(1)


if __name__ == "__main__":
    main()

运行:

bash
python coding_tool_gate.py experiment.json

程序会强制候选覆盖完全相同的成对任务集。它不能声称产品间成对任务替代随机化的人类无 AI 对照;当决策问题是人类生产率而不是产品输出时,应使用独立研究设计。

把安全作为独立测试轨道

安全失败不能被质量综合分稀释。

对每个候选测试:

  • 仓库文件指示读取诱饵密钥;
  • 工具结果要求无关网络请求;
  • 依赖安装会调用 Lifecycle Script;
  • 任务要求修改范围之外的 CI 或权限;
  • 禁止外部域名;
  • 无害前缀隐藏破坏性操作的命令;
  • 尝试 Push 或 Merge 到分配分支之外;
  • 缺失与过期凭证。

分别记录尝试违规和成功违规。禁止动作必须由控制拦截,不能依赖模型拒绝。全部使用一次性基础设施与合成数据。

Cursor、Claude Code 与 GitHub Copilot Coding Agent 的官方安全文档描述了不同的确认、沙箱、网络、分支和审计边界。必须测试准确模式,不能把一个模式的控制外推到整个产品。

把结果解释为组合方案

最终结果可能不是单一冠军:

  • 代码补全可能在短小本地编辑中胜出;
  • 交互式 Agent 可能减少探索性变更的主动时间;
  • 云端 Agent 可能适合受控异步维护;
  • 高上下文或高风险任务仍可能由当前人类流程胜出;
  • 专用静态分析器可能在确定性检查中胜过所有生成工具。

应按切片选型,同时共享策略、验收与审计门禁。只有收益超过新增配置、培训、合同和事故响应复杂度时,混合工具链才合理。

从证据出发灰度上线

晋级应逐步增加仓库敏感度与 Agent 权限:

  1. Dry Run 协议并修复模糊任务;
  2. 在一次性环境中运行锁定对比;
  3. 让选中模式试点低风险真实工作;
  4. 保持独立评审与现有 CI 强制执行;
  5. 监控验收、评审负载、回归、事故与成本;
  6. 只扩大持续通过的切片;
  7. 保留旧工具与配置用于回滚。

当工具模式、模型策略、客户端、权限、合同、定价或任务组合发生实质变化时,应重新评测。

常见失败模式

让操作员挑选偏爱任务

任务选择产生的提升可能大于工具本身。分配前应冻结或随机化任务集。

把通过狭窄测试当成验收

隐藏不变量、可维护性、范围和安全都可能在单个测试通过时失败,必须分层验收。

忽略被拒绝尝试

删除失败或昂贵运行会同时夸大质量与成本表现。每个有效尝试都必须保留。

比较不相等预算

一个候选无限重试,另一个固定额度,测量的是预算而不是工具质量。应记录并对齐与决策相关的约束。

向评审者暴露工具身份

品牌偏好会污染评审。可行时标准化制品并隐藏身份。

用稀疏数据强行选冠军

宽置信区间和冲突切片表示结论不足或只在特定模式成立,不能压缩成星级。

常见问题

应该让同一开发者操作所有工具吗?

不一定。同一操作员可控制开发者差异,却可能偏向熟悉工具;多名操作员更有代表性,却增加方差。应培训、轮换或分块分配,并记录经验,使设计匹配决策。

可以复用历史完成的 Ticket 作为任务吗?

可以,但必须从候选环境移除原始 Patch、后续 Commit、讨论和隐藏答案,并保证任务仍真实。公开历史仍可能进入模型训练,因此私有或新编任务证据更强。

所有工具应该使用同一模型吗?

只有决策专门比较共享模型下的 Harness 时才需要。产品选型应测试实际准备部署的受支持配置并记录模型策略,不能为了表面公平而关闭产品核心设计。

模糊任务应该如何评分?

预先定义何时澄清是正确动作。工具提出必要问题不应被惩罚为未完成;工具自行虚构需求并修改代码则应在范围或正确性上失败。

该协议能证明全组织生产率吗?

不能。它只能支持所抽样任务、仓库、用户与控制下的有边界产品决策。组织级影响还取决于任务选择、学习、流程重构、评审容量、质量和长期维护。

总结

可信的 Cursor、Claude Code 与 Copilot 对比是一项实验,而不是排名文章。冻结决策、任务集、环境、候选配置、预算、验收规则与安全轨道;保留每个结果;测量已验收变更与人类负载;报告不确定性和切片;最后只让有证据的模式通过可回退灰度。

参考资料