模型与对比指南
在你的任务上公平比较两个 LLM:交换阵营与成本日志
在你自己的工单上对两个模型进行一对一比较:相同设置,每一对都在两种顺序下评判,以及一个给出运行成本的 calls.jsonl 日志。

你让两个模型各跑了三个提示词,第二个赢了。但在你相信这个结果之前,先检查一下哪个模型被列在第二位。公平的比较需要三件事:两个模型使用完全相同的设置,每一对都在两种顺序下评判,以及记录每次调用的 token 数,这样你才知道整个运行花了多少钱。下面是一个小型 Python 脚本,可以在你自己的任务上完成这三件事。
公开锦标赛的做法,你可以借鉴#
Dwarkesh Patel 的 Clocktower Bench 是一个非正式研究项目,让语言模型互相玩社交推理游戏《Blood on the Clocktower》。游戏本身不是重点,重点是测试框架。根据项目的 About 页面(2026 年 11 月 8 日查看),它做了几件任何一对一比较都应该做的事:
- 一个模型扮演所有好人席位,另一个扮演所有邪恶席位,并且每一局都交换阵营打两次,这样幸运的席位不会决定结果;
- 所有模型使用相同设置:相同的推理强度、每回合 8,000 token 上限、发言 150 词限制;
- Storyteller 是脚本化的,所以没有模型来裁判游戏;
- 每次 API 调用都写入
calls.jsonl,记录其 token 和成本。页面说一局游戏的成本大约从 $2 到 $50,取决于所用模型(在 OpenAI 和 Anthropic API 上)。
这个测试框架是为 OpenAI 和 Anthropic 模型构建的,而你的任务很可能不是桌游。但这三个习惯可以迁移:相同设置、交换阵营、成本日志。本指南的其余部分将它们应用到一个普通任务上。

任务:为 30 张支持工单起草回复#
假设你运营一个小型支持台,想知道 DeepSeek-V4-Flash 还是 GLM-5.3-Flash 写的第一版草稿更好。你有一个文件 tickets.jsonl,包含 30 张真实工单,每行一个 JSON 对象:{"id": 1, "text": ".。
API 兼容 OpenAI,所以官方 openai 包可以通过 CheapTokens 在 https:/ 上使用。使用文档中的完整模型 id:deepseek- 和 zai-。两个模型还会使用第三个模型作为评判者,MiniMaxAI/,这样两个参赛者都不会给自己的作品打分。
从让日志成为可能的部分开始:一个所有调用都经过的函数。
import json, os, re
from openai import OpenAI
client = OpenAI(base_url="https://api.cheaptokens.dev/v1",
api_key=os.environ["CHEAPTOKENS_API_KEY"])
CONTESTANTS = {"deepseek": "deepseek-ai/DeepSeek-V4-Flash-0731",
"glm": "zai-org/GLM-5.3-Flash"}
JUDGE = "MiniMaxAI/MiniMax-M2.7"
SETTINGS = {"temperature": 0.2, "max_tokens": 800} # same for both
def call(model, messages, role, ticket_id, **settings):
r = client.chat.completions.create(model=model, messages=messages, **settings)
with open("calls.jsonl", "a", encoding="utf-8") as f:
f.write(json.dumps({
"role": role, "ticket": ticket_id, "model": model,
"input_tokens": r.usage.prompt_tokens,
"output_tokens": r.usage.completion_tokens,
"finish": r.choices[0].finish_reason}) + "\n")
return r.choices[0].message.content or ""
def draft(model, ticket):
msgs = [{"role": "system", "content": "You answer support tickets. "
"Reply in under 80 words, politely, with no promises about refunds."},
{"role": "user", "content": ticket["text"]}]
return call(model, msgs, "draft", ticket["id"], **SETTINGS)calls.jsonl 中的字段是这个脚本选择的;用量数字来自 API 响应的 usage 对象。两个模型获得相同的系统提示词、相同的 temperature 和相同的 max_tokens。如果只为其中一个模型改动其中一项,你比较的就是设置,而不是模型。
交换阵营:每一对评判两次#
现在说评判者。评判模型可能偏向某个特定位置的答案,所以单次裁决会把质量和位置混在一起。Clocktower 的技巧正好适用:让每一对在两个角色中都出现。只有当评判者在两种顺序下都选中某个回复时,该回复才赢得这张工单。如果裁决跟随的是位置而不是回复,这张工单就是平局。
def judge(ticket, first, second):
prompt = (f"Ticket:\n{ticket['text']}\n\nReply A:\n{first}\n\nReply B:\n{second}\n\n"
"Which reply is better for the customer? Finish with a last line: Verdict: A or Verdict: B.")
out = call(JUDGE, [{"role": "user", "content": prompt}], "judge",
ticket["id"], temperature=0, max_tokens=1000)
letters = re.findall(r"Verdict:\s*([AB])\b", out)
return letters[-1] if letters else None
tickets = [json.loads(l) for l in open("tickets.jsonl", encoding="utf-8")]
wins = {"deepseek": 0, "glm": 0, "tie": 0}
for t in tickets:
a = draft(CONTESTANTS["deepseek"], t)
b = draft(CONTESTANTS["glm"], t)
v1 = judge(t, a, b) # DeepSeek's reply shown as A
v2 = judge(t, b, a) # GLM's reply shown as A
pick1 = {"A": "deepseek", "B": "glm"}.get(v1)
pick2 = {"A": "glm", "B": "deepseek"}.get(v2)
wins[pick1 if pick1 and pick1 == pick2 else "tie"] += 1
print(wins)有两个细节容易忽略。评判者以 temperature=0 运行,以保持裁决更稳定;并非每个模型都遵守该设置,所以在信任它们之前,先重跑几对并比较裁决。裁决只从最后的 Verdict: 行读取,并且上限很宽松(草稿 800,评判者 1000),这样在回答前进行推理的模型不会在思考中途被截断;下面的估算使用典型大小,而不是上限。无法解析的答案会得到 None,这会落入平局,而不是让运行崩溃。
自己试试试一试:评判者在两种顺序下都对工单 7 说了 "A"。谁赢得了这张工单?显示答案
没有人。在第一种顺序中,A 是 DeepSeek 的回复;在第二种顺序中,A 是 GLM 的回复,所以评判者偏好的是位置,而不是某个模型。代码将其计为平局。大量这样的平局本身就是一种结果:两个模型在你的任务上很接近,或者你的评判提示词太模糊。
顺序只是让测试产生偏差的一种方式。设置、提示词和日志记录也可能造成偏差,所以要检查整个设置,而不仅仅是谁先出场。下面是同一实验在改进前后的对比。
比较
当你停止随意比较时,会发生什么变化
随意
每个模型一个提示词,保留默认值,这一对只以一种顺序展示,月底从仪表盘读取账单。赢家可能是恰好被列在前面的那个模型。
公平
一个共享的系统提示词,相同的 temperature 和 max_tokens,每一对都在两种顺序下评判,每次调用都在 calls.jsonl 中写一行。赢家是从两个席位都能获胜的那个。
记录每次调用,然后计算运行成本#
日志把比较变成了预算。运行结束后,按模型汇总 token:
from collections import defaultdict
totals = defaultdict(lambda: [0, 0, 0])
for line in open("calls.jsonl", encoding="utf-8"):
c = json.loads(line)
s = totals[c["model"]]
s[0] += c["input_tokens"]; s[1] += c["output_tokens"]; s[2] += 1
for model, (tin, tout, n) in totals.items():
print(model, n, "calls", tin, "input", tout, "output")你会得到每个模型一行,包含调用次数、输入 token 和输出 token。乘以模型的两个价格(输入和输出分别计费),或者更简单,把总数粘贴到实时成本标记中。
你也可以在运行前估算。假设每张工单加上系统提示词约为 300 个输入 token,一份草稿约为 250 个输出 token,评判者看到大约 800 个输入 token(工单加上两个回复再加上问题),并以约 200 个 token 回答(裁决行加上一点思考空间):
- 草稿:30 张工单 × 2 个模型 = 60 次调用,每个参赛者 30 × 300 = 9000 输入 token 和 30 × 250 = 7500 输出 token;
- 裁决:30 张工单 × 2 种顺序 = 60 次调用,评判者上 60 × 800 = 48000 输入 token 和 60 × 200 = 12000 输出 token。
按当前价格,一个参赛者的 30 份草稿在 DeepSeek-V4-Flash 上花费 $0.00089,在 GLM-5.3-Flash 上花费 $0.00048。MiniMax-M2.7 上的 60 次裁决花费 $0.0042。把三者相加,你就在发送任何请求之前得到了整个比较的价格。同样的起草工作量在三个模型上的对比,输入和输出合并计算:
| 模型 | 输入 $/1M | 输出 $/1M | 费用 | OpenRouter |
|---|---|---|---|---|
| GLM-5.3-Flash | $0.02 | $0.04 | $0.00048 | $0.00092 |
| DeepSeek-V4-Flash | $0.015 | $0.10 | $0.00089 | $0.0017 |
| MiniMax-M2.7 | $0.05 | $0.15 | $0.0016 | $0.0189 |
总计 9,000 个输入 token 和 7,500 个输出 token。每 100 万 token 的价格为当前价格,会随我们的资费一同变动;OpenRouter 列显示相同工作负载的参考价格。
表中还显示了 OpenRouter 参考价,这样你可以看到总数如何比较。token 假设由你更改:如果你的工单很长,评判者输入增长最快,因为它要两次携带两个回复。如果你的评判者在回答前大声思考,它的输出数字也会增长,你的日志会显示出来。关于在请求前计算 token 的习惯,请参阅在发送请求前计算 token。
Clocktower 使用的同一个上限值得借鉴。它设置每回合 8,000 token,而你已经有了 max_tokens。把它放进 SETTINGS,失控的回答就无法悄悄放大你的估算。
在启动完整运行之前#
清单
公平比较的预检
最后一项很重要。跑三张工单,打开 calls.jsonl 并阅读它。检查 finish 显示的是 stop 而不是 length;如果某份草稿被 max_tokens 截断,某个模型可能只是因为它的回答被截断而输。评判者的调用也是如此:那里的 length 会变成静默平局,所以也要统计 None 裁决。如果你的任务只有一个正确答案,你可能根本不需要评判者:用精确匹配评分在 20 个案例上测试你的提示词。
阅读结果而不自欺#
看看 wins 中的三个数字。三十张工单是小样本:像 12 比 9 加 9 平这样的分布告诉你两个模型很接近,而不是其中一个更好。日志在这里有帮助。如果一个模型略胜但使用多得多的输出,真正的问题是任务的价格,而不是胜场数;每 1M token 价格与每任务成本 详细讨论了这种比较。
如果结果接近,就为任务选择更便宜的模型,或者增加更多工单并重跑。两者尝试起来都很便宜,因为整个机制已经是一个循环。当你想在另一个模型上重复这个练习时,上面三个模型都在一个 API 和一个密钥之后:每个模型的设置文档都相同,定价页面列出了每个模型当前的价格。Dwarkesh Patel 自己关于 OpenAI 与 Anthropic 模型(检测与欺骗)的发现是他针对自己游戏的结果;你的结果将来自你的工单。



