首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >别急着给 AI 编程助手打分:先搭一套能重跑的评测任务集

别急着给 AI 编程助手打分:先搭一套能重跑的评测任务集

原创
作者头像
老王说技术
发布于 2026-11-15 08:03:52
发布于 2026-11-15 08:03:52
320
举报

说明:本文不含任何具体产品的评分、排名、耗时或准确率数字。这类数字只有在你自己的仓库、网络、账号下跑出来才有意义。文中的目录结构、YAML 和 Python 脚本可以直接照着搭,示例里的数值只是格式占位,不代表任何产品表现。

一、"试了一下挺好用"为什么不够

最近在给团队选辅助编码工具,发现最容易得出的结论是"我用着挺顺"。这个结论通常经不起复现,原因有三:

  • 任务太友好。随手挑的都是单文件、上下文自足、有现成答案的小改动,恰好落在所有工具都擅长的区间。
  • 上下文被悄悄喂饱。IDE 插件会自动带上打开的文件、全仓索引、最近编辑历史;换成命令行或另一个插件,同一个模型拿到的输入完全不同。
  • 只统计首次生成,不统计把它改到能跑通的代价。一段看起来漂亮的代码,如果要手工改三处、回滚两次,实际成本比一次生成但平淡的结果更高。

所以要建的不是排行榜,而是一套别人拿去也能跑出同样结果的考卷。

二、先把"评测对象"拆开

说"评一个 AI 产品"其实含糊。能操作编辑器的工具至少是四层叠加:

  • 模型层:底座模型、版本、温度、上下文窗口。
  • 上下文组装层:检索哪些文件、截断策略、是否带 git 历史。
  • 集成层:IDE 插件 / CLI / Web,权限模型、工具调用能力。
  • 运行环境层:网络、缓存、账号配额、机器负载。

层与层不隔离,结论换台机器就翻转。实践做法是先固定后面三层只比较一层;或者干脆绕开插件,统一走同一个 API 入口、自己拼上下文,这样"模型层"的对比才干净。下面这套脚本走的是后一条路。

三、任务集怎么设计

不要按"难度"分类,按"能力边界"分类,每类至少三个任务,每个任务必须带可自动判定的断言:

  • 单文件缺陷修复:给一个跑不过的最小仓库,断言是单测全绿且没改测试文件。
  • 跨文件小重构:断言是行为不变(同一组测试)且指定符号已迁移。
  • 从报错定位:只给错误日志和目录,断言是最终补丁落在预期文件。
  • 遵循仓库约定:断言是新增代码通过 lint 且复用了仓库里已有的工具函数,而不是另写一套。

任务定义用一个 YAML:

代码语言:yaml
复制
id: fix-001
category: 单文件缺陷修复
fixture: fixtures/repo-a
allow_paths:
  - src/rate_limit.py
prompt: |
  src/rate_limit.py 的 allow() 在窗口边界会把计数算错。
  请只修改这个文件,让 test_rate_limit.py 全部通过,不要改测试文件。
assertions:
  - name: 单测通过
    type: exit0
    cmd: python -m pytest -q test_rate_limit.py
  - name: 未改测试文件
    type: exit0
    cmd: test -z "$(git diff --name-only | grep test_)"

关键约束:prompt 里不能出现期望代码,断言里不能出现答案,任务集目录不要放在被测仓库内部,否则会被插件自动索引读进上下文,等于泄露考卷。

四、脚本:从干净工作区到 JSONL

依赖只有 pyyaml。被测工具用环境变量注入,脚本本身不绑定任何产品:

代码语言:python
复制
#!/usr/bin/env python3
import hashlib, json, os, shutil, subprocess, time
from pathlib import Path
import yaml

ROOT = Path(__file__).resolve().parent
TASKS, FIXTURES, OUT = ROOT / "tasks", ROOT / "fixtures", ROOT / "runs"

def tree_sha(path):
    h = hashlib.sha256()
    for p in sorted(path.rglob("*")):
        if p.is_file():
            h.update(str(p.relative_to(path)).encode())
            h.update(p.read_bytes())
    return h.hexdigest()[:16]

def prepare(task, run_dir):
    work = run_dir / task["id"]
    shutil.copytree(FIXTURES / task["fixture"], work)
    for cmd in (["git", "init", "-q"],
                ["git", "add", "."],
                ["git", "-c", "user.email=e@local", "-c",
                 "user.name=eval", "commit", "-qm", "baseline"]):
        subprocess.run(cmd, cwd=work, check=True, capture_output=True)
    return work

def invoke(work, prompt_file):
    cmd = os.environ["AGENT_CLI"].format(prompt=str(prompt_file))
    t0 = time.monotonic()
    p = subprocess.run(cmd, shell=True, cwd=work, capture_output=True,
                       text=True, timeout=int(os.environ.get("AGENT_TIMEOUT", "300")))
    return {"elapsed_s": round(time.monotonic() - t0, 2),
            "exit_code": p.returncode,
            "stderr_tail": p.stderr[-1000:]}

def diff_stat(work):
    r = subprocess.run(["git", "diff", "--numstat"], cwd=work,
                       capture_output=True, text=True, check=True)
    files = added = deleted = 0
    for line in r.stdout.splitlines():
        parts = line.split("\t")
        a, d = parts[0], parts[1]
        files += 1
        added += 0 if a == "-" else int(a)
        deleted += 0 if d == "-" else int(d)
    return {"files": files, "added": added, "deleted": deleted}

def run_assertions(work, assertions):
    out = []
    for a in assertions:
        p = subprocess.run(a["cmd"], shell=True, cwd=work,
                           capture_output=True, text=True)
        ok = p.returncode == 0 if a["type"] == "exit0" else a["expect"] in p.stdout
        out.append({"name": a["name"], "pass": ok,
                    "detail": (p.stdout + p.stderr)[-300:]})
    return out

def main():
    run_id = time.strftime("%Y%m%d-%H%M%S")
    run_dir = OUT / run_id
    run_dir.mkdir(parents=True, exist_ok=True)
    with (run_dir / "results.jsonl").open("w") as f:
        for path in sorted(TASKS.glob("*.yaml")):
            task = yaml.safe_load(path.read_text())
            work = prepare(task, run_dir)
            prompt = work / ".eval-prompt.md"
            prompt.write_text(task["prompt"])
            inv = invoke(work, prompt)
            asserts = run_assertions(work, task["assertions"]) if inv["exit_code"] == 0 else []
            rec = {
                "run_id": run_id,
                "task_id": task["id"],
                "category": task["category"],
                "fixture_sha": tree_sha(FIXTURES / task["fixture"]),
                "agent_build": os.environ.get("AGENT_BUILD", "unset"),
                "elapsed_s": inv["elapsed_s"],
                "diff": diff_stat(work),
                "assertions": asserts,
                "pass_rate": sum(1 for a in asserts if a["pass"]) / max(len(asserts), 1),
            }
            f.write(json.dumps(rec, ensure_ascii=False) + "\n")
            print(task["id"], rec["pass_rate"])
    print("results:", run_dir / "results.jsonl")

if __name__ == "__main__":
    main()

跑法:

代码语言:bash
复制
export AGENT_CLI='your-agent-cli --non-interactive --prompt-file {prompt}'
export AGENT_BUILD='记录模型版本与客户端版本,自行填写'
python3 eval.py

有个顺序细节值得说明:git add . 在 baseline 提交之前执行,此时 .eval-prompt.md 还没写入,所以提示词文件是未跟踪文件,不会出现在 git diff --numstat 里,也就不会污染改动面积的统计。

五、除了通过率,还要记这几个字段

  • 改动面积(files / added / deleted):同样通过断言,改 3 行和改 300 行不是一回事。
  • 越界改动:拿 git diff --name-only 和 YAML 里的 allow_paths 比对,越界直接标失败。
  • 往返轮数:一次性通过还是追问三轮才通过,脚本外单独记一列。
  • 墙钟耗时:只在同一台机器、同一时间段、同一负载下横向比较,不能跨环境比。
  • 失败模式:超时、权限拒绝、工具调用失败要分开统计,混在"未通过"里会误导选型。

这些字段都进 JSONL,之后用任何表格工具都能做透视,不需要专门搭平台。

六、最容易踩的五个坑

  • 只看首次生成通过率,不看回滚与人工修补成本。
  • 把示例答案或期望补丁写进 prompt,等于开卷考。
  • 任务集和被测仓库放同一个目录,被插件索引读进上下文。
  • 同一台机器上先跑 A 再跑 B,缓存预热让后者占便宜;应随机化顺序并交替重复。
  • 用单次结果下结论。至少跑三轮看分布,波动大的任务先修任务本身,别急着下判断。

七、落地节奏

第一周只做四到六个任务、把脚本跑通,目标是 fixture 可复现而不是分数好看;第二周补齐四类任务和越界检查,加入第三个工具做对比;第三周把任务集纳入版本管理,每次客户端或模型版本变更就跑一次回归,把 JSONL 存档。

到这里,"哪个工具更好"会变成一个可以指着某次 run_id 讨论的问题,而不是会议室里的感觉之争。评测结果请自行在自己的环境里产生,本文不提供也不暗示任何产品的分数。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、"试了一下挺好用"为什么不够
  • 二、先把"评测对象"拆开
  • 三、任务集怎么设计
  • 四、脚本:从干净工作区到 JSONL
  • 五、除了通过率,还要记这几个字段
  • 六、最容易踩的五个坑
  • 七、落地节奏
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档