返回博客

模型与对比指南

在你的任务上公平比较两个 LLM:交换阵营与成本日志

在你自己的工单上对两个模型进行一对一比较:相同设置,每一对都在两种顺序下评判,以及一个给出运行成本的 calls.jsonl 日志。

作者:CheapTokens Team发布于 7 分钟阅读

封面:公平比较两个模型,三步流程:相同提示词、交换阵营、记录每次调用
本页内容
  1. 公开锦标赛的做法,你可以借鉴
  2. 任务:为 30 张支持工单起草回复
  3. 交换阵营:每一对评判两次
  4. 记录每次调用,然后计算运行成本
  5. 在启动完整运行之前
  6. 阅读结果而不自欺

你让两个模型各跑了三个提示词,第二个赢了。但在你相信这个结果之前,先检查一下哪个模型被列在第二位。公平的比较需要三件事:两个模型使用完全相同的设置,每一对都在两种顺序下评判,以及记录每次调用的 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://api.cheaptokens.dev/v1 上使用。使用文档中的完整模型 id:deepseek-ai/DeepSeek-V4-Flash-0731 和 zai-org/GLM-5.3-Flash。两个模型还会使用第三个模型作为评判者,MiniMaxAI/MiniMax-M2.7,这样两个参赛者都不会给自己的作品打分。

从让日志成为可能的部分开始:一个所有调用都经过的函数。

python
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 的技巧正好适用:让每一对在两个角色中都出现。只有当评判者在两种顺序下都选中某个回复时,该回复才赢得这张工单。如果裁决跟随的是位置而不是回复,这张工单就是平局。

python
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:

python
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。把三者相加,你就在发送任何请求之前得到了整个比较的价格。同样的起草工作量在三个模型上的对比,输入和输出合并计算:

模型 费用 OpenRouter
GLM-5.3-Flash $0.00048 $0.00092
DeepSeek-V4-Flash $0.00089 $0.0017
MiniMax-M2.7 $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 模型(检测与欺骗)的发现是他针对自己游戏的结果;你的结果将来自你的工单。