用固定任务集+确定性断言+重复运行构建prompt回归测试,输出pass/fail表替代主观感受。可对接任意OpenAI兼容API,免费层也能跑。
本周 DEV 社区所有人都在讨论 Agent、编排和多 Agent 流水线。但所有这些之前有一个无聊的问题:你选的这个模型真的能完成你的任务吗?我们大多数人的答案是把提示词粘贴到聊天界面,眯着眼睛看输出,然后说"看起来没问题"。这不是评估——如果你在对着付费 API 迭代,那在犹豫的过程中也在慢慢烧钱。
我常用的一个更便宜的方案是:一个小型固定任务集,配合确定性检查,指向任意 OpenAI 兼容端点运行。半天就能搭建好,如果指向免费层则分文不花,而且给你的是一张通过/失败表,而不是一种感觉。
提示词回归测试框架有三部分:
固定任务 —— 一组小型编码提示词,代表你实际让模型做的事情。
确定性验证 —— 每个生成的答案都会被提取出来,用真实的断言语句执行。没有人工眯眼判断。
可重复性 —— 在 temperature: 0 下将每个任务运行 N 次,并报告通过率。一次幸运的生成不应该算数。
因为这个框架使用 OpenAI chat-completions 格式,你可以指向几乎任何端点:付费 API、本地服务器(如 llama.cpp 或 vLLM),或托管的免费层。
纯标准库 Python。保存为 model_harness.py:
#!/usr/bin/env python3
"""Score a coding model against a small fixed task set.
Point it at any OpenAI-compatible endpoint:
export LLM_BASE_URL="https://your-endpoint/v1"
export LLM_API_KEY="..." # or a placeholder for local servers
export LLM_MODEL="model-name"
python model_harness.py
"""
import json, os, re, subprocess, sys, tempfile, time, urllib.request
BASE_URL = os.environ.get("LLM_BASE_URL", "http://localhost:8000/v1").rstrip("/")
API_KEY = os.environ.get("LLM_API_KEY", "none")
MODEL = os.environ.get("LLM_MODEL", "change-me")
RUNS = int(os.environ.get("RUNS", "3"))
TASKS = [
{
"name": "dedupe_keep_order",
"prompt": (
"Write a Python function `dedupe(items)` that returns the list "
"with duplicates removed, preserving first-occurrence order. "
"Reply with only a single python code block."
),
"test": "from solution import dedupe\n"
"assert dedupe([1, 2, 1, 3, 2]) == [1, 2, 3]\n"
"assert dedupe([]) == []\n"
"assert dedupe(['a', 'a', 'b']) == ['a', 'b']\nprint('ok')\n",
},
{
"name": "fizzbuzz",
"prompt": (
"Write a Python function `fizzbuzz(n)` returning the classic "
"FizzBuzz list from 1 to n. Reply with only a python code block."
),
"test": "from solution import fizzbuzz\n"
"assert fizzbuzz(5) == [1, 2, 'Fizz', 4, 'Buzz']\n"
"assert fizzbuzz(15)[14] == 'FizzBuzz'\nprint('ok')\n",
},
{
"name": "parse_keyvals",
"prompt": (
"Write a Python function `parse(s)` that parses a string like "
"'a=1, b = 2 ,c=3' into {'a': '1', 'b': '2', 'c': '3'}. "
"Reply with only a python code block."
),
"test": "from solution import parse\n"
"assert parse('a=1, b = 2 ,c=3') == {'a': '1', 'b': '2', 'c': '3'}\n"
"assert parse('x=y') == {'x': 'y'}\nprint('ok')\n",
},
]
def chat(prompt):
body = json.dumps({
"model": MODEL,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0,
}).encode()
req = urllib.request.Request(
f"{BASE_URL}/chat/completions",
data=body,
headers={"Content-Type": "application/json",
"Authorization": f"Bearer {API_KEY}"},
)
with urllib.request.urlopen(req, timeout=120) as r:
return json.load(r)["choices"][0]["message"]["content"]
def extract_code(text):
blocks = re.findall(r"```(?:python)?\s*\n(.*?)\n```", text, re.S)
return blocks[0] if blocks else text
def run_once(task):
code = extract_code(chat(task["prompt"]))
with tempfile.TemporaryDirectory() as d:
with open(os.path.join(d, "solution.py"), "w") as f:
f.write(code)
p = subprocess.run([sys.executable, "-c", task["test"]],
cwd=d, capture_output=True, text=True, timeout=30)
return p.returncode == 0
if __name__ == "__main__":
total_pass = total_runs = 0
for task in TASKS:
passed = 0
start = time.time()
for _ in range(RUNS):
try:
passed += run_once(task)
except Exception as e:
print(f" error on {task['name']}: {e}", file=sys.stderr)
total_pass += passed
total_runs += RUNS
print(f"{task['name']:20s} {passed}/{RUNS} passed "
f"({time.time() - start:.1f}s)")
print(f"\nOverall: {total_pass}/{total_runs} "
f"({100 * total_pass / max(total_runs, 1):.0f}%)")
用本地服务器跑一遍,再指向托管端点跑一遍,然后对比两张表。任务故意选得很简单——这是起点,不是基准。把它们换成你过去一个月实际使用中抽取的提示词,分数就开始有意义了。
安全提醒:这会执行模型生成的代码。对于玩具任务以外的内容,要把子进程跑在容器或一次性 VM 里,且不能有网络和凭证。
披露:本文是 MonkeyCode 产品推广的一部分。
对于上述工作流,一个实用的端点选择是 MonkeyCode,它目前提供免费模型访问和免费服务器选项。这个组合对于上面描述的阶段确实有用:你正在迭代提示词和任务定义,会烧掉大量调用,而且你还不知道哪个模型值得你花钱。将框架指向零成本的端点意味着你的实验预算只是时间,而不是金钱。按照其文档设置 LLM_BASE_URL、LLM_API_KEY 和 LLM_MODEL,上述脚本即可无缝运行。
两个真诚的注意事项:免费层可能变更配额、模型阵容或可用性,因此请将当前条款作为事实来源,而不是本文中的任何内容——并且不要在未经检查数据政策的情况下,通过任何第三方端点(免费或付费)传输专有代码或密钥。
小样本会说谎。三个任务乘以三次运行,从统计学上讲几乎说明不了什么问题。把早期分数当作方向性参考,从你观察到的实际失败中扩展任务集。
通过测试 ≠ 代码好。模型可以通过断言但输出难以阅读、无法维护的内容。对于任何要发布的内容,加上人工审查。
污染是真实存在的。FizzBuzz 这样的经典任务几乎肯定在训练数据里,这会虚增分数相对于你的新颖、私有任务的分数。
Temperature 0 不是确定性。提供商确实会在不同运行和版本间返回不同输出。定期重新运行,并用模型名和日期记录结果。
免费层可用性可能变化。不要把免费端点硬编码到你明天它消失就会想念的东西里。
如果你已经有评估框架(如正经的基准测试套件或 LLM 评判流水线),这只是降级。如果你的决策纯粹关于生产延迟或合规性,玩具通过率回答不了这些问题。而且如果你永远只问模型一个问题——直接用聊天界面就好了。
对于"哪个模型值得花钱"这个阶段的其他所有人:偷走这个脚本,换上你自己的任务,让表格来做决定,而不是靠直觉。如果你没有闲置的 GPU 容量或 API 预算,MonkeyCode 的免费模型访问和免费服务器选项是获得实验端点的一种零成本方式。