用固定任务集对任意模型跑分并记录diff,避免被demo prompt欺骗;解决免费模型质量不稳定难以评估的问题。方法论实用。
几个月前,我发现自己做了一件尴尬的事:用一个提示词得到的第一段回答好不好看来评判编程模型。我粘贴一条巧妙的提示词,看着一段自信满满的答案流式输出,点点头就过去了。然后,一周后,我真正在工作中使用这个模型时,发现它连我项目里关于 import 风格的三行指令都follow不了。
Demo 提示词很能唬人。它们稳稳地落在训练分布区间内,答案显而易见且唯一正确,而且永远不会回过头来纠缠你。真实任务则丑陋得多:只说了一半、有样式约束、与模型从未见过的代码纠缠在一起。于是我写了一个小小的测试框架,针对任何想要评估的模型运行同一套固定任务,给结果打分,并把所有内容写入日志,方便日后 diff。这篇文章就是这个框架本身,以及围绕它的工作流程。它兼容任何 API 兼容的模型,在决定是否将某个免费层级的模型接入日常工作流程之前,用它来判断某个免费模型是否足以胜任特定工作,非常方便。
核心原则:任务固定,模型变化
大多数随意的模型对比失败,都是因为两个变量同时在动。不同的提示词、不同的模型、不同的心情——你什么都学不到。解决方法平淡无奇却有效:
写下 8–12 个看起来像你实际工作的任务,不是面试题。
把它们和一份冻结的提示词模板一起存入文件。
用相同的任务套件运行每一个候选模型。
在查看任何输出之前,用你事先定好的评分标准打分。
任务套件才是核心资产。模型来了又走;你的任务套件应该比它们更长寿。
一个不是智力问答的最小任务套件
以下是我的套件内容。你的应该有所不同——这才是重点——但注意一下整体结构:
约束遵循:"重构这个函数。不要改变它的签名。不要添加依赖。"(模型喜欢同时违反这两条规则。)
风格匹配:我真实代码库中的一个片段,加上"用与这个文件相同的风格添加错误处理。"
Bug 定位:一个包含一个植入 bug 的小文件,以及一条会失败的测试描述。模型找到的是实际 bug,还是把整个代码重写了?
先解释再操作:"先用两句话说明这段代码做什么,然后修改它。"检查解释是否与修改内容一致。
故意设置的陷阱:一个按描述无法完成的任务(缺少信息)。好的模型会提问或标记;差的模型会信心满满地胡编。
最后一条是我最喜欢的。在模糊情况下产生幻觉是让我损失最多时间的失败模式,Demo 提示词从来不会测试这一点。
这是我运行内容的精简版。纯 Python,一个文件,无框架:
import json, time, pathlib, urllib.request
ENDPOINT = "https://your-provider.example/v1/chat/completions" # any OpenAI-compatible API
MODELS = ["model-a", "model-b"] # candidates under test
SUITE = pathlib.Path("suite") # one .md file per frozen task
def run_task(model, task_text):
body = json.dumps({
"model": model,
"messages": [{"role": "user", "content": task_text}],
"temperature": 0.2,
}).encode()
req = urllib.request.Request(
ENDPOINT, data=body,
headers={"Content-Type": "application/json",
"Authorization": "Bearer YOUR_KEY"})
t0 = time.time()
with urllib.request.urlopen(req, timeout=120) as r:
out = json.loads(r.read())
return out["choices"][0]["message"]["content"], round(time.time() - t0, 1)
results = []
for model in MODELS:
for task in sorted(SUITE.glob("*.md")):
answer, secs = run_task(model, task.read_text())
results.append({"model": model, "task": task.name,
"seconds": secs, "answer": answer})
print(f"{model} x {task.name}: {secs}s")
stamp = time.strftime("%Y%m%d-%H%M")
pathlib.Path(f"results-{stamp}.json").write_text(json.dumps(results, indent=2))
这里没什么巧妙的设计,这是刻意为之。打分发生在我打开评分标准、阅读 JSON 文件的时候:对于每个答案,在"遵循了显式约束"上打 0/1 分,在"我是否在一次修改后愿意合并"上打 0/1 分,并在陷阱任务触发了幻觉时做记录。十个任务、两个模型,大概二十分钟打分。因为文件带有时间戳,我可以三个月后再运行同一套任务,公正地做对比。
几个看起来不起眼但实际很重要的细节:
Temperature 要锁定。否则你比较的就是掷骰子。
延迟要记录。一个模型效果好 15% 但慢了 4 倍,会改变我对在哪里使用它的判断。
任务存在文件中,不在脚本里。编辑任务意味着套件版本更新,这让旧运行记录仍然有意义。
免费模型访问在这个循环中的位置
运行这样的套件最明显的反对意见是成本:在评估上烧付费 token 感觉是浪费。这就是免费层级在我工作流程中发挥作用的地方——我用它们作为筛选阶段。披露:本文是作为 MonkeyCode 产品推广的一部分准备的。具体来说,MonkeyCode 目前宣传可免费访问一系列模型以及一个免费服务器选项,我一直在用上面的框架指向那个端点做筛选阶段:运行套件不花一分钱,任何通过我评分标准的模型会晋级,与我在同一任务上依赖的付费模型正面 PK。
诚实的说法是,免费层级是评估基材和第二意见,而不是承诺。我不假设免费阵容是固定的,不假设配额,也不声称任何我没有在自己的日志中实际测量过的基准。上述脚本刻意与提供商无关,这样如果免费选项消失或变化,我的套件和历史记录仍然完整。
结果实际上改变了我的什么
运行这个循环一段时间后,得出的结论是我光凭感觉永远不会得出的:
感觉相似的模型在约束遵循任务上差异巨大。指令 adherence 最终成为区分我的工作最有意义的维度——比原始正确性更重要。
陷阱任务淘汰了我本来挺喜欢的一个模型。它以完全自信的态度编造了缺失的函数签名,而且编了两次。
对于机械性工作(重命名并传播、测试脚手架搭建),免费层级的模型得分与我付费默认模型相同。我现在把这类任务路由到免费选项,把付费调用留给那些模糊的情况。
最后一点是真正的回报:套件不只是给模型排名,它还告诉你哪些任务是廉价的。
局限性,坦白说
小样本人工打分。一个人给十个任务打分是冒烟测试,不是基准测试。它能发现 dealbreaker,但不会产生排行榜数字。
我的套件反映的是我的工作。它偏向 Python、重构和测试相关任务。前端为主的开发者需要不同的套件。
免费层级会变动。可用性、模型和限制可能会在不通知的情况下变化。你构建的任何运营性工具必须能容忍这一点——这也是框架除了端点外不硬编码任何提供商信息的原因之一。
仅限单轮。这不能衡量 agentic、多步骤行为。我把它当作深度测试前的门槛,而不是替代品。
如果你一个月只用几次编程模型来做新项目的代码片段,设置成本大于收益——直接用你面前的就好。同样,如果你的工作主要是漫长的多文件 agent 会话,单轮套件衡量的是错误的东西;你需要的是轨迹级别的评估。另外,如果公司里已经有人维护评估套件,扩展他们的而不是 fork 一个个人的。
信任是一套测试。免费模型访问——包括 MonkeyCode 的免费模型和免费服务器——使得这套测试的筛选阶段几乎零成本运行,这消除了以 demo 魔法评判模型的最后借口。如果你从这篇文章中学到一样东西,就做那个冻结的任务文件;围绕它们的框架半天就能搭完。如果你最后写了自己的套件,我真的很想知道哪个任务让你意外——陷阱任务是我一直推荐别人去偷的那个。