Skip to main content
Glama

evalmine

排行榜上的数字从来无法预测一个模型变更在你实际运行的四十多个任务上会带来什么影响。某个模型在基准测试中登顶,你把它换进来,结果它在你所依赖的工作上悄悄变差了。

evalmine 针对你的任务回答关于模型变更的一个问题:它是有帮助、有损害,还是花了更多钱却得到相同结果?你编写一个 YAML 套件来描述你的任务。它会在两个或更多模型上运行这些任务,对每个答案做 schema 校验、计时,并让 LLM 评判器以两种顺序对答案进行两两比较,这样评判器对先看到内容的偏好就会相互抵消。它用 Cohen's kappa 将该评判器与你的偏好标签进行评分,当无法证明评判器与你意见一致时,它拒绝将胜率作为头条数字。成本来自固定在某个日期的价格表;未知模型会导致运行失败,而不是按 $0 计费。报告按套件哈希进行版本管理;一个三工具 MCP 服务器让 agent 能在任务进行中运行评测。

在示例套件上针对假适配器的结果是:12 个标签上的 kappa 0.25 低于 0.40 的下限,因此 0.463 的胜率以标记形式打印,而不是作为头条。这种拒绝正是该工具发挥作用的方式:

$ evalmine run examples/everyday-eight.yaml \
    --models anthropic/claude-haiku-4-5,google/gemini-2.5-flash --fake

run 20260823T210009Z_c4545e4e_dbc76614  (everyday-eight)
  report: reports/everyday-eight/20260823T210009Z_c4545e4e_dbc76614/report.md
  calibration: below_floor - kappa 0.25 (fair) over 12 labels - headline eligible: false
  google/gemini-2.5-flash vs anthropic/claude-haiku-4-5: win-rate 0.463 (UNCALIBRATED) [0.325-0.613] over schema-passing pairs only, n=20 - flips 3 - excluded 0
  cost: $0.0658 this run (answers $0.0081, judge $0.0578); if uncached $0.0658

假适配器是确定性的,因此在全新检出上这些数字可以精确复现。以上操作没有联系任何提供商,也没有花费一分钱。

evalmine:验证套件,针对假适配器运行,读取其写入报告中的校准和胜率部分

该动画的每一帧都是真实运行。使用 vhs docs/demo.tape 重新录制(vhs,brew install vhs)。

状态。 v0.1.0,预发布版本。核心、三个提供商适配器、执行检查和 MCP 接口均已构建并测试;价格表已在其固定日期对照各提供商的公开定价页面进行验证。尚不存在决策日志条目——参见尚未完成。

规范:docs/spec.md。它是代码所依据的契约,在两者不一致时以它为准。 深入工作原理:docs/learning/how-it-works.md(带样式的 HTML 渲染)。

快速开始

git clone https://github.com/hishamalward/evalmine.git && cd evalmine
python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"          # add ,mcp -> ".[dev,mcp]" for the MCP server

需要 Python 3.10 或更高版本。三个运行时依赖:PyYAML、jsonschema、httpx。

不花一分钱检查套件。 validate 解析文件、应用 JSON Schema、渲染每个提示词(未匹配的 {{placeholder}} 是硬错误),并对照价格表解析每个模型字符串。零网络调用。

evalmine validate examples/everyday-eight.yaml
# ok: examples/everyday-eight.yaml - 8 tasks, 20 cases, 12 labels; every prompt
# rendered; 3 model strings resolved against prices-2026-08-23.yaml

针对假适配器运行。 --fake 将每个模型字符串路由到内置的确定性适配器:无需密钥、无需网络、零花费。下面的两个模型字符串是示例套件中十二个人类标签所引用的,因此此运行端到端地演练了校准路径。

evalmine run examples/everyday-eight.yaml \
  --models anthropic/claude-haiku-4-5,google/gemini-2.5-flash --fake

真实运行。 密钥来自环境变量,且仅来自环境变量。复制 .env.example,在仓库外填写,然后导出所需内容。

export ANTHROPIC_API_KEY=...
export GOOGLE_API_KEY=...

evalmine run examples/everyday-eight.yaml \
  --models anthropic/claude-haiku-4-5,google/gemini-2.5-flash \
  --max-cost 0.50

在首次实时调用之前会运行预检估算。如果超过 --max-cost,运行将被拒绝(退出码 4),且不会花费任何费用。如果任何地方都没有设置上限,CLI 默认值为 $2.00。每次调用都按内容哈希缓存在磁盘上,因此重新运行是免费的,报告也是可复现的;--no-cache 强制进行全新调用,但仍会写入缓存。

其他命令:evalmine prices [--for suite.yaml]、evalmine last suite.yaml、evalmine report <run-id>、evalmine compare <report_a> <report_b>。

Related MCP server: AgentOps EvalBench MCP

套件文件

一个 YAML 文件包含你的任务、评判器配置和你的标签。随附的示例是 examples/everyday-eight.yaml:八个虚构任务(重写、提取、分类、解释、小代码改动),共二十个用例,其中三个带有输出 schema,并有十二个偏好标签。完整 schema 见规范 §5;其形态为:

suite: everyday-eight
version: 1

defaults: { temperature: 0, max_tokens: 700, timeout_s: 60 }
limits:   { max_cost_usd: 1.50 }

judge:
  model: anthropic/claude-sonnet-4-6
  rubric: |
    Prefer the answer that a competent colleague would ship without editing.
    ...
  calibration: { min_kappa: 0.40, min_labels: 10, on_below_floor: flag }

tasks:
  - id: ticket-triage
    kind: classify                # a free label, used only to group report rows
    prompt: |
      Classify this support ticket. Return JSON only.

      Ticket:
      {{ticket}}
    schema: { type: object, required: [category, severity], ... }
    rubric: |                     # appended to the suite rubric for this task
      In addition to the suite rubric: ...
    cases:
      - id: charged-twice
        vars: { ticket: "I was charged twice this month..." }

labels:
  - { task: ticket-triage, case: charged-twice,
      baseline: anthropic/claude-haiku-4-5,
      candidate: google/gemini-2.5-flash,
      prefer: candidate, note: "team-wide lockout is high, not medium" }

关于此文件有三点是有意为之:

  • 模板不是 Jinja。 恰好是 {{name}},只替换一次,没有表达式也没有过滤器。没有匹配变量的占位符在加载时是硬错误,因为静默的空变量是让评测悄悄失去意义的最简单方式。

  • 未知键是错误, 在每一层都是如此。一个被忽略的拼写错误的 rubrik: 会产生一份看起来正常但毫无意义的报告。

  • labels 是该工具可信度的来源。 它们是你事先记录的看法,在你看到胜率之前就已记录,评判器会以它们为基准进行评分。没有标签的套件仍然可以运行;只是无法产生头条数字。

用你自己的任务替换示例。这正是该工具的全部意义所在。

代码任务的执行检查

散文是判断代码能否运行的很差代理。一个用例可以声明 check:一段 bash 片段,它获取答案的代码($ANSWER 是一个文件,$ANSWER_TEXT 是文本),如果代码可用则退出 0。它在全新的临时目录中运行,有超时限制,环境变量中已剥离密钥,且永远不会被缓存。答案中的每个围栏代码块都会按顺序运行,每个都在自己的 fixture 上;最后一个块是判定,前面的块记录在它旁边,因此一个撤回错误块并写出第二个块的答案会按第二个块评分,并显示撤回。

- id: jq-remote
  vars: { task: "Write a jq filter ... the JSON is in postings.json" }
  check:
    setup: 'printf "[{\"t\":\"a\",\"remote\":true}]" > postings.json'
    run: 'jq -r "$(cat "$ANSWER")" postings.json | grep -q a'

结果——通过/失败、退出码、输出——与答案一起放在 answers.jsonl、记分卡和 HTML 配对视图中,评判器会看到它,并遵循一条固定规则:检查失败的答案不能击败通过的答案。规范 §6.6。

如何阅读报告

reports/<suite>/<run-id>/report.md 与 report.json、report.html、answers.jsonl 和 pairs.jsonl 放在一起。按以下顺序阅读。

1. 校准,首先。 它被有意打印在胜率之上。你需要评判器判定与你的标签之间的 Cohen's kappa,附上其 Landis-Koch 区间名称,以及其下方的 3x3 混淆矩阵。用 kappa 而不是简单一致率,因为一旦某个类别占主导地位,一致率就会被夸大——而它确实会:评判器会学会平局是安全的。矩阵告诉你评判器如何出错,这很重要——一个在你判平时从不说"平局"的评判器,与一个系统性地偏好新内容的评判器是不同的问题。在它下方,按任务分解告诉你在哪里出错:一个 kappa 可能掩盖一个在你的重写任务上表现出色但在你的分诊任务上毫无用处的评判器,而平均值正是你会错过的发现。

2. 一个你不应该信任的胜率。 三个条件,任何一个都足以触发:

  • headline_eligible: false — kappa 低于下限、标签太少,或者因为两个评分者全程只使用一个类别而导致 kappa 未定义。报告禁止该数字成为头条,用剑号标记每个数字,JSON 和每个 MCP 响应都携带相同的标记,因此读取摘要的 agent 不能在不附带警告的情况下引用该数字。

  • 翻转率高于 0.30。 翻转是指当两个答案交换位置时评判器改变其答案的配对。超过约三分之一时,胜率衡量的是呈现顺序,而不是质量。报告在同一张表中说明了这一点。

  • n 很小,或正在缩小。 胜率仅基于通过 schema 的配对计算:任一侧无法解析或未通过其 schema 的配对会被排除,而不是计为失败,这样不擅长输出 JSON 的模型不会因格式失败而在质量比较中落败。代价是 n 会缩小,这就是为什么该部分标题为"仅基于通过 schema 的配对,n=…",以及为什么 n 从不在同一屏幕上不附带 schema 通过率就打印。

在发布数字之前,将 min_kappa 提高到 0.60。 随附的默认值是 0.40——公平到中等一致性的传统下限,低到足以让一个只有十几个标签的初始套件有可能通过。这是自己使用数字的下限,配合你自己对标注过程的记忆。0.60——"substantial"(显著)——是告诉别人一个数字的下限,因为那种记忆不会随数字传递。该工具以宽松的默认值发布,这样初始套件值得运行两次;这条建议的存在是为了让初始套件不会最终出现在博客文章中。

3. 然后是记分卡,将成本与质量一起阅读,绝不在其后阅读。 Schema 通过率(标记为 native 或 prompted,因为为你强制 schema 的提供商与仅仅被礼貌请求的提供商不是同一种度量)、执行通过率及其 n(在任务声明了执行检查的情况下)、p50 和 p95 延迟及其 n、本次运行的成本以及未缓存时的成本。一个以三倍价格赢得 0.55 的候选者与一个以一半价格赢得 0.55 的候选者是不同的决策。

4. 按任务表格,最差在前排序。 一个头条胜率没有变化,而三个任务朝相反方向移动了 0.4,这正是你可能会错过的发现。evalmine compare A B 恰好打印两次运行之间的这些变动项。

5. report.html 和标注流程。 每次运行还会写入一个自包含页面——无需服务器、无依赖,可通过 file:// 路径打开。相同的部分,外加每个被评判的配对并排显示,模型名称被隐藏,评判器的判定被折叠起来,这样你就能像评判器阅读它们时那样阅读答案。每个配对下方有 Prefer A · Tie · Prefer B,然后 copy labels YAML 为你提供 labels: 条目,可直接粘贴回你的套件:十分钟的点击代替半小时的手工编辑,这正是校准集能够增长与不能增长之间的区别。

报告中不包含任何形容词,也不做任何推荐。判断进入 DECISIONS.md,措辞来自你的判定——报告在每次运行的底部为你预填了模板。

MCP

evalmine-mcp 是一个 stdio MCP 服务器,恰好暴露三个工具,它们调用与 CLI 相同的 core.py 函数:

工具

功能

花费

run_suite(suite_path, models, max_cost, baseline, no_cache)

运行套件,返回摘要和报告路径

最高至上限

compare(report_a, report_b)

两份报告之间的差异

无

last_report(suite_path)

套件的最新报告

无

通过将 .mcp.json.example 复制为 .mcp.json 来注册。先安装额外依赖:pip install -e ".[mcp]"。

关键在于,agent 可以在任务进行中运行你的评测——"在替换此文件中的模型之前,运行套件并告诉我胜率"——而不是事后由人来阅读报告。

选择三个工具而不是整个 CLI,是因为面向 agent 的接口应该是支持决策的最小动词集合,而每多一个工具就是多一种花掉无人授权的钱的方式。

上限,以及为什么 agent 的默认值低于你的。 上限是 core.run_suite() 的一个参数,而不是 MCP 重新实现的 CLI 标志:只有一个地方可以花钱,并且在那里设置了上限。如果 agent 提供了 max_cost,则使用它,但超过 EVALMINE_MCP_MAX_COST_CEILING(默认 $5.00)的请求会被直接拒绝,而不是被截断后运行。如果 agent 省略了它,上限为 min(suite.limits.max_cost_usd, EVALMINE_MCP_MAX_COST),默认 $1.00——是 CLI 的 $2.00 的一半,因为在 CLI 前的是输入了数字的人,而 agent 没有。超上限的运行返回结构化拒绝,不花费任何费用,也永远不会被静默截断以适应上限;截断的运行会产生一个看起来像完整数字的较小数字。

run_suite 返回摘要和路径,绝不返回原始 provider 响应。那些内容保留在磁盘上的 answers.jsonl 中。一个把每条回答都流式传回 agent 上下文的工具,会让调用方付出比 eval 本身更高的代价,并把 eval 框架变成从你的提示词中窃取数据的通道。suite_path 还必须解析到 EVALMINE_MCP_SUITE_ROOT 之内(默认值:服务器的工作目录)。

同类工具

promptfoo 和 Braintrust 是这里显而易见的工具,而且两者都比本工具更强大。

promptfoo 拥有多得多的断言类型、一个 Web 查看器、红队测试,以及远不止三种的 provider 覆盖。Braintrust 是一个托管平台:追踪、从生产日志构建的数据集、真正的 UI、协作,以及作为某家公司的产品所具备的运维成熟度。如果你想要广度,或者想要一个团队看到同样的数字,请使用其中一种。

evalmine 的存在有三个更狭窄的理由。

  • 评判器是针对你校准的,否则它的数字不会打印出来。 上述两者都可以用 LLM 评判器打分。但两者都没有把针对你的标签进行校准作为胜率是否可引用的门槛。这种反转——默认拒绝——才是整个核心论点,而且这不是你能硬塞给一个无论如何都会输出数字的工具的功能。

  • 决策日志是一等公民产物。 eval 的输出不是一个数字,而是一个你必须在六个月后为之辩护的决策。DECISIONS.md 由报告预填、由人类撰写,它存在于你的仓库中,紧挨着该决策所涉及的代码。

  • 表面足够小,可以一口气读完。 大约 6,000 行,包括四个适配器、报告和执行检查。没有 LLM 框架,没有 provider SDK——只有三个手写的 POST 请求,指向有文档的 JSON 端点。这个代价是真实的,值得说明:当 provider 更改其 API 时,我们是通过崩溃而不是升级来发现的。

如果这三条对你都不重要,那么诚实的推荐是 promptfoo。

尚未实现

v0.1.0 的范围之外,README 明确说明了这一点,而不是让你自己去发现:RAG 或检索 eval;agent 或多轮轨迹;微调任何东西;Web UI;任何托管服务;超过三种 provider;rubric 自动生成;除上述三个之外的 MCP 工具。

本 README 中的每个数字都来自虚构示例套件上的假适配器。在真实套件上,还没有一次带标签的运行产生过校准数字或 DECISIONS.md 条目;这要发生在任何 v0.1.0 标签之前。

开发

pip install -e ".[dev,mcp]"
python -m pytest -q          # 310 tests, none of which make a network call
python -m ruff check src tests

CI 运行 {ubuntu, macos, windows} x {3.10, 3.13},每个分支运行所有测试,另外对工作树和完整 git 历史进行秘密扫描。本仓库中不允许出现任何 API 密钥,如果套件文件包含与已知密钥前缀匹配的字符串,evalmine run 会拒绝启动。

参见 CONTRIBUTING.md。变更从 docs/spec.md 开始。

许可证

MIT。参见 LICENSE。

Related MCP Connectors

Related MCP Servers