在你的用户之前,发现 AI 性能的下降。
修改一个 prompt,更换一个模型,升级一个依赖。有什么东西崩坏了吗?你通常
是从用户那里得知的。
EvalCore 记录你的 AI 的行为方式,并根据该记录检查每一次变更。它是一个二进制文件,由一个 YAML 文件驱动,并且可以离线运行且成本为 $0,
因此 eval suite 可以成为每个 pull request 上的阻塞检查,而不是某个
人记得去运行的每周任务。
你永远不需要为了使用它而编写 Rust。Target 通过 HTTP 或 shell 通信,自定义 scorer 通过 stdin 和 stdout 使用 JSON 通信,而 judge 可以是任何兼容 OpenAI 的 endpoint。
**[文档](https://eval-core.github.io/evalcore/)** ·
[快速开始](https://eval-core.github.io/evalcore/getting-started/quickstart/) ·
[crates.io](https://crates.io/crates/evalcore) ·
[发布版本](https://github.com/eval-core/evalcore/releases) ·
[更新日志](CHANGELOG.md)
## 目录
- [安装](#install)
- [快速开始](#quickstart)
- [阅读输出](#reading-the-output)
- [工作原理](#how-it-works)
- [你可以用它做什么](#what-you-can-do-with-it)
- [Target 和 scorer](#targets-and-scorers)
- [在 CI 中运行](#running-in-ci)
- [设计原则](#design-principles)
- [维护者](#maintainers)
- [贡献](#contributing)
## 安装
```
cargo install evalcore
```
或者从[发布页面](https://github.com/eval-core/evalcore/releases)下载适用于 Linux x64 或 macOS(x64 / arm64)的
预编译二进制文件。更多选项请查看
[安装指南](https://eval-core.github.io/evalcore/getting-started/installation/)。
## 快速开始
本仓库提供了一个可运行的示例:一个小型银行支持机器人,其评分
标准是每个回答是否引用了它所依赖的策略。它不需要 API key,也不会发起
任何网络调用。
```
git clone https://github.com/eval-core/evalcore
cd evalcore
cargo run -p evalcore -- run examples/quickstart/evals.yaml
```
```
PASS late-refund (8ms)
PASS fee-dispute (8ms)
PASS card-lost (8ms)
PASS wire-eta (8ms)
4 passed, 0 failed, 4 total
GATE PASS pass_rate >= 0.95 (actual 1.00)
PASSED
```
在交互式终端中,状态词会带有颜色(绿色表示通过,红色表示失败),
在运行过程中,一个 spinner 会在 stderr 上统计用例数量,而最后的 `PASSED`
/ `FAILED` 会加粗显示——但这些文字始终存在,因此不依赖
颜色。在管道、重定向或 `NO_COLOR` 环境下,输出与上面的纯文本
完全相同:确定性的、可用 grep 搜索的,并且可以安全地粘贴到 pull request 中。
`--color auto|always|never`、`--progress auto|never` 和 `-q/--quiet`(仅显示失败)
可以对其进行调整;机器报告器和 `--output` 文件永远不会带有颜色。
一个 suite 由两个文件组成。YAML 说明要运行什么以及如何评分:
```
# evals.yaml
targets:
support-bot:
type: shell # your real app goes here
cmd: "sh examples/quickstart/bot.sh"
datasets:
- file: cases.jsonl
scorers:
- type: contains # grounding: cite a policy
value: "policy"
case_sensitive: false
- type: regex # specificity: cite a numbered rule
pattern: "policy [0-9.]+"
run:
gates:
- type: pass_rate # a floor over the whole run
min: 0.95
```
JSONL 包含各个用例。`context` 是 RAG suite 进行评分时所依据的
检索到的证据。Scorer 可以看到它,但 target 永远看不到:
```
{"id": "late-refund", "input": "It has been three weeks and my refund still has not shown up.", "context": ["Policy 4.2: Approved refunds are processed within 30 business days."]}
{"id": "wire-eta", "input": "How long will an international wire transfer take?", "context": ["Policy 5.3: International wire transfers settle within 3 to 5 business days."]}
```
## 阅读输出
终端报告中的每一个字符都有特定的含义。这是一次带有
一个失败用例的运行,并附有注释。
**每个用例的行**,每个用例一行,始终按数据集顺序排列:
```
PASS late-refund (8ms)
──┬─ ─────┬───── ──┬─
│ │ └─ latency, measured by the target itself
│ └────────── case id, straight from your cases.jsonl
└────────────────── every scorer passed
FAIL fee-dispute
exact: expected "60 days", got "we will look into it"
──┬── ─────────────────────┬──────────────────────
│ └─ that scorer's reason, verbatim
└─ which scorer objected (one line per failing scorer)
```
一个从未产生输出的用例会报告原因而不是分数,因为
target 错误是一个带有原因的失败用例,而不是崩溃:
```
FAIL wire-eta
target error: connection refused
```
**摘要行**,接着是每个 gate 一行:
```
2 passed, 1 failed, 3 total · 210 tokens · $0.0020 · 1 flaky
─────────┬────────────────── ────┬───── ───┬─── ───┬──
│ │ │ └─ cases whose trials
│ │ │ disagreed with each other
│ │ └─ your declared rates × those tokens
│ └─ reported by the provider (or read from a trace)
└─ a case passes when every scorer passes
GATE FAIL pass_rate >= 0.95 (actual 0.67)
──┬─ ──┬─ ────────┬─────── ────┬─────
│ │ │ └─ what the run actually scored
│ │ └─ the floor you declared under run.gates
│ └─ this gate's verdict
└─ gate lines appear only when you configure gates
```
摘要行的最后三个片段仅在适用时出现:当运行报告了 token 使用量和成本时,当某个用例运行了多次 trial 时出现 `flaky`。
一个不包含这些信息的运行只会打印 `4 passed, 0 failed, 4 total`。
**判定结果**,始终在最后,一个词:
```
FAILED · 2 regressed, 1 new
──┬─── ────────┬─────────
│ └─ a clause only when a baseline explains the failure
└─ PASSED / FAILED, matching the exit code exactly
```
它反映了*整个*契约 —— 用例、gate 以及任何 baseline —— 因此它
永远不会与退出代码不一致。当判定结果为
`PASSED` 时,`evalcore run` 退出代码为 **0**,否则为 **1**。这就是整个 CI 契约。
其他输出格式:用于完整结果树的 `--reporter json`,
用于 CI 测试面板的 `--reporter junit`,以及用于
你可以附加到 pull request 的独立页面的 `--html report.html`。请参阅
[CLI 参考](https://eval-core.github.io/evalcore/reference/cli/)。
## 工作原理
```
cases.jsonl evals.yaml
│ │
│ id, input, │ targets, scorers,
│ expected, context │ gates, trials
└───────────┬────────────┘
▼
┌───────────────┐ ┌──────────────────────────┐
│ TARGET │◄──────►│ record / replay cache │
│ │ │ .evalcore/cache.db │
│ shell · http │ │ │
│ openai · trace│ │ keyed on a hash of the │
└───────┬───────┘ │ canonical request │
│ └──────────────────────────┘
│ output text, tokens, latency, trajectory
▼
┌───────────────┐
│ SCORERS │ run in order, all of them, every case
│ │
│ contains · exact · regex · json-schema · similarity
│ judge · subprocess · trajectory
└───────┬───────┘
│ a case passes when every scorer passes
▼
┌───────────────┐
│ GATES │ floors over the whole run
│ │ pass_rate · mean_score · accuracy · macro_f1
└───────┬───────┘
│
▼
exit 0 or 1 ──► CI
```
三个属性在任何地方都成立,它们是这个工具是有用
的、而不仅仅是方便的原因:
**确定性。** 相同的输入产生相同的输出。结果保持
数据集顺序,报告器是纯函数,除了延迟之外,没有任何用户可见的东西会读取
时钟。这正是使缓存值得信赖的原因。
**记录一次,永远重放。** 每次对可缓存 target 的调用都会被记录到
本地的 SQLite 文件中,以规范化请求的哈希值作为键。提交这个文件,
CI 就会重放它,因此该任务不会发起网络调用,不需要 API key,并且
成本为零。
```
evalcore run evals.yaml # auto (default): replay hits, record misses
evalcore run evals.yaml --cache replay # CI: cache only, a miss fails the case
evalcore run evals.yaml --cache live # re-record everything
evalcore run evals.yaml --cache off # bypass
```
更改模型、URL 或用例的输入都会更改键,因此过时的
记录永远无法悄悄地为你未发出的请求提供答案。Shell target
永远不会被缓存,因为它们运行的是本地代码,这些代码可以在不更改
配置的情况下发生改变。
**失败即数据。** Target 错误变成带有原因的失败用例,
scorer 错误变成带有原因的失败分数。运行永远不会 panic,并且一个
糟糕的用例永远不会中止整个 suite。
## 你可以用它做什么
| 你想要 | 使用 | 指南 |
|---|---|---|
| 阻止回归而不是要求完美 | `--baseline` | [Gate 和 baseline](https://eval-core.github.io/evalcore/guides/gates-and-baselines/) |
| 为整个运行设定质量底线 | `run.gates` | [Gate 和 baseline](https://eval-core.github.io/evalcore/guides/gates-and-baselines/) |
| 停止信任单个幸运样本 | `run.trials` | [Trial 和统计数据](https://eval-core.github.io/evalcore/guides/trials-and-statistics/) |
| 决定更便宜的模型是否足够好 | `--matrix a,b` | [比较模型](https://eval-core.github.io/evalcore/guides/comparing-models/) |
| 评估一个 agent *做了*什么,而不仅仅是它说了什么 | `trace` + `trajectory` | [Agent 和 trace](https://eval-core.github.io/evalcore/guides/agents-and-traces/) |
| 评估你已部署的 REST API | `type: http` | [评估 REST API](https://eval-core.github.io/evalcore/guides/evaluating-rest-apis/) |
| 根据检索到的 context 进行评分 | 用例 `context` | [RAG 评估](https://eval-core.github.io/evalcore/guides/rag-evaluation/) |
| 使用任何断言都无法表达的 rubric 进行评分 | `type: judge` | [LLM-as-judge](https://eval-core.github.io/evalcore/guides/llm-as-judge/) |
| 跟踪支出并设置上限 | `cost` + `budget_usd` | [成本和预算](https://eval-core.github.io/evalcore/guides/cost-and-budgets/) |
| 使用 Python 或任何语言进行评分 | `type: subprocess` | [自定义 scorer](https://eval-core.github.io/evalcore/guides/custom-scorers/) |
| 浏览过去的运行并比较任意两个 | `evalcore serve` | [运行历史](https://eval-core.github.io/evalcore/guides/run-history-and-serve/) |
其中有三个值得看看它们的真实输出。
**Baseline 基于性能恶化进行拦截。** 保存一个可接受的状态,然后进行比较。已经
失败的用例保持容忍;只有真正的回归才会使运行失败:
```
baseline "main": 2/3 passed -> current: 0/3 passed
REGRESSED refund-window
exact: expected "30 days", got "I do not know"
REGRESSED wire-eta
exact: expected "3 to 5 days", got "I do not know"
baseline gate: FAIL (2 regressed, 0 new failing)
```
这里所有三个用例都失败了,但只有两个被报告。第三个在
保存 baseline 时就已经失败了。
**Trial 是测量而不是采样。** 随机模型的一次运行就是一个
样本。`run.trials` 将每个用例运行 N 次,并使用
`all`、`majority` 或 `any` 汇总结果。同一个不稳定的用例,在两种策略下:
```
run: trials: { count: 3, require: majority }
PASS fee-dispute (10ms) [2/3 trials]
3 passed, 0 failed, 3 total · 1 flaky exit 0
run: trials: { count: 3, require: all }
FAIL fee-dispute [2/3 trials]
exact: trial 2: expected "60 days", got "unsure"
2 passed, 1 failed, 3 total · 1 flaky exit 1
```
无论哪种方式,你都会发现这个用例是不稳定的。`require` 只决定
这是否会使你的构建失败。
**矩阵在单次调用中并排比较 target**:两个模型、两个
prompt 或两个已部署的 endpoint。
```
== comparison
case baseline-bot improved-bot
refund-window PASS PASS tie
fee-dispute FAIL PASS improved-bot
wire-eta PASS PASS tie
wins: baseline-bot 0 · improved-bot 1 · ties 2
```
## Target 和 scorer
**Target** 是被评估的事物。
| 类型 | 评估内容 | 是否缓存 |
|---|---|---|
| `shell` | 任何命令。用例输入到达 stdin,stdout 是输出。 | 否 |
| `openai-compatible` | 任何 OpenAI 格式的 chat endpoint:OpenAI、vLLM、Ollama、gateway。 | 是 |
| `http` | 任何 HTTP/JSON API,通常是你自己部署的应用。 | 是 |
| `trace` | 记录的 agent 运行,采用 EvalCore 的原生格式或 OTel / OpenInference 导出。不调用任何东西。 | 否 |
**Scorer** 决定输出是否可接受。每个 scorer 都会对每个
用例运行,并且只有当所有 scorer 都通过时,用例才算通过。
| 类型 | 通过条件 | 分数 |
|---|---|---|
| `contains` | 输出包含某个子字符串。 | 0 或 1 |
| `exact` | 输出等于 `value`,或用例的 `expected`。 | 0 或 1 |
| `regex` | 正则表达式匹配输出中的任意位置。 | 0 或 1 |
| `json-schema` | 输出解析为 JSON 并通过 draft 2020-12 schema 验证。 | 0 或 1 |
| `similarity` | 输出与 `expected` 之间的余弦相似度超过阈值。 | 余弦值 |
| `judge` | 根据你的 rubric 进行评分的 LLM 分数达到或超过阈值。 | judge 的分数 |
| `subprocess` | 你自己的命令打印出通过判定。任何语言。 | 你的分数 |
| `trajectory` | Agent 的工具调用满足每条规则(`must_call`、`must_not_call`、`max_steps`)。 | 0 或 1 |
Judge 和 similarity 调用通过与 target 相同的记录/重放缓存,因此
LLM 评分的 suite 可以确定性地且免费地重放。每种类型的每个字段都记录在
[配置参考](https://eval-core.github.io/evalcore/reference/configuration/)中。
## 在 CI 中运行
一个步骤运行一个 suite 并拦截该任务。终端报告显示在步骤
摘要中,而独立的 HTML 报告作为 artifact 上传,审查者
可以直接从 pull request 中打开它:
```
- uses: eval-core/evalcore@v0.7.5
with:
config: evals/evals.yaml
args: --cache replay --baseline main
html-artifact: evalcore-report # default; set to "" to disable
```
`--cache replay` 意味着该任务不需要 API key 且零花费。
`--baseline main` 意味着它因回归而失败,而不是因为不完美。即使
suite 失败,HTML 报告也会上传,而这正是最有用的时候。
该 action 是为了方便,而不是必需的。二进制文件的退出代码就是
整个契约,因此任何 CI 系统都可以工作。请参阅
[在 CI 中运行](https://eval-core.github.io/evalcore/guides/running-in-ci/)以了解
GitLab、Jenkins 和 bare-shell 设置。
## 设计原则
**协议优先于 SDK。** 每个扩展点都是语言无关的。Target
通过 HTTP 或 shell 通信,自定义 scorer 通过 stdin 和 stdout 使用 JSON 通信,judge 是
任何兼容 OpenAI 的 endpoint,agent trace 以 OTel 或 OpenInference
JSON 到达。Rust 是引擎,永远不是接口。
**配置优先。** 功能从 YAML 开始。如果某件事值得做,就值得
作为数据来描述,让审查者可以在 diff 中阅读。
**构造上确定。** 相同的输入,相同的字节输出。缓存、
baseline 和 CI 拦截都是基于此构建的。
**本地优先。** 你的 suite、你的记录和你的运行历史位于你
仓库旁边的 SQLite 文件中。没有服务器要运行,也没有账户要
创建,并且该工具不会向任何地方发送任何内容。
## 维护者
EvalCore 由 [Abhishek Manyam](https://github.com/abhishekmanyam) 和
[Kuladeep Mantri](https://github.com/kuladeepmantri) 构建。
## 贡献
欢迎提交 Bug 报告、功能请求和 pull request。从
[CONTRIBUTING.md](CONTRIBUTING.md) 开始,了解工作区布局、四条
架构规则以及 CI 运行的检查。安全问题请通过
[SECURITY.md](SECURITY.md) 处理,而不是公共 tracker。
```
cargo build
cargo nextest run --workspace # or: cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all
```
## 许可证
Apache-2.0。请参阅 [LICENSE](LICENSE)。