AI HOT 精选

OpenRouter 教程:如何在 CI 中用 LLM eval 门禁拦截 Pull Request

7.1内容质量

TL;DR · AI 摘要

OpenRouter 教程:如何在 CI 中用 LLM eval 门禁拦截 Pull Request · AIHOT OpenRouter:Announcements · 2026-1...

核心要点

  • 主题聚焦:OpenRouter 教程:如何在 CI 中用 LLM eval 门禁拦截 Pull Reque
  • 来源:AI HOT 精选,建议结合原文判断细节。
  • AI 分析暂不可用,本条为保底评分与摘要。
#AI#编程#后端#安全#产品
打开原文

OpenRouter 教程:如何在 CI 中用 LLM eval 门禁拦截 Pull Request · AIHOT

OpenRouter:Announcements

·

2026-10-01 08:00

12 小时前

精选

AI 评分

67

OpenRouter 教程:如何在 CI 中用 LLM eval 门禁拦截 Pull Request

How to Gate Pull Requests on LLM Evals in CI

AI 导读

OpenRouter 发布教程,讲解如何用固定的 eval 集在 CI 中门禁 pull requestï¼Œå½“é€šè¿‡çŽ‡ä½ŽäºŽé˜ˆå€¼æ—¶è„šæœ¬ä»¥éžé›¶é€€å‡ºç é˜»æ­¢åˆå¹¶ï¼Œåšæ³•ä¸Žå•å ƒæµ‹è¯•é—¨ç¦ä¸€è‡´ã€‚

推荐理由

原文给出了从脚本到 GitHub Actions 的完整可复现流程,并强调了路径过滤须放 job 层、阈值应实测等易错点。

正文 · AI 翻译

在支持代理的系统提示中改一行,就可能发布一个告诉客户退款窗口是 30 å¤©çš„ä»£ç†ï¼Œè€Œä½ çš„æ”¿ç­–è§„å®šæ˜¯ 14 天。正常的 CI 流水线不会检查模型说了什么,所以构建通过,第一个看到错误答案的人是客户。

ç”¨å›ºå®šçš„è¯„ä¼°é›†æ¥é—¨æŽ§æ‹‰å–è¯·æ±‚ï¼Œå°±åƒç”¨å¤±è´¥çš„å•å ƒæµ‹è¯•æ¥é—¨æŽ§ä¸€æ ·ã€‚ä½ æŠŠæµ‹è¯•ç”¨ä¾‹ä¿å­˜åœ¨ä»“åº“ä¸­ï¼Œåœ¨æç¤ºæ›´æ”¹æ—¶è¿è¡Œå®ƒä»¬ï¼Œå¹¶åœ¨å¤ªå¤šå¤±è´¥æ—¶é˜»æ­¢åˆå¹¶ã€‚

åœ¨æœ¬æŒ‡å—ä¸­ï¼Œä½ ä¸ºæ”¯æŒä»£ç†ç¼–å†™ä¸€ä¸ªè¯„ä¼°é›†ï¼Œå¹¶ç”¨ä¸€ä¸ªè°ƒç”¨ OpenRouter çš„è„šæœ¬æ¥è¯„åˆ†ã€‚ä½ æµ‹é‡åœ¨æ²¡æœ‰ä»»ä½•æ›´æ”¹çš„æƒ å†µä¸‹å¤šæ¬¡è¿è¡Œä¹‹é—´ç»“æžœçš„æ³¢åŠ¨ç¨‹åº¦ï¼Œç„¶åŽå°†è¯¥è„šæœ¬æŽ¥å ¥ GitHub Actions ä½œä¸ºå¿ éœ€çš„çŠ¶æ€æ£€æŸ¥ã€‚

简而言之

  • å›ºå®šçš„è¯„ä¼°é›†æ˜¯æäº¤åˆ°ä»“åº“ä¸­çš„æµ‹è¯•ç”¨ä¾‹åˆ—è¡¨ã€‚å®ƒä» é€šè¿‡ç»è¿‡å®¡æŸ¥çš„æ‹‰å–è¯·æ±‚è¿›è¡Œæ›´æ”¹ã€‚
  • 在作业级别而不是工作流级别进行过滤。GitHub å°†å› if æ¡ä»¶è€Œè·³è¿‡çš„ä½œä¸šæŠ¥å‘Šä¸ºé€šè¿‡çš„æ£€æŸ¥ï¼Œè€Œå› è·¯å¾„è¿‡æ»¤å™¨è·³è¿‡çš„å·¥ä½œæµä¼šç•™ä¸‹å¾ å¤„ç†çš„å¿ éœ€æ£€æŸ¥å¹¶é˜»æ­¢åˆå¹¶ã€‚
  • å½“é€šè¿‡çŽ‡ä½ŽäºŽé˜ˆå€¼æ—¶ï¼Œè¯„ä¼°è„šæœ¬ä»¥éžé›¶çŠ¶æ€é€€å‡ºï¼Œè€Œè¯¥é€€å‡ºä»£ç æ­£æ˜¯å¯¼è‡´ä½œä¸šå¤±è´¥çš„åŽŸå› ã€‚
  • é€šè¿‡å¯¹æœªæ›´æ”¹åˆ†æ”¯çš„é‡å¤è¿è¡Œæ¥æµ‹é‡é˜ˆå€¼ï¼Œè€Œä¸æ˜¯é€‰æ‹©ä¸€ä¸ªä¸¥æ ¼çš„æ•°å­—ã€‚
  • temperature 和 seed ä» åœ¨å°†å ¶åˆ—åœ¨ supported_parameters ä¸­çš„æ¨¡åž‹ä¸Šæœ‰å¸®åŠ©ã€‚ä½¿ç”¨å¤šæ•°æŠ•ç¥¨çš„é‡å¤é‡‡æ ·é€‚ç”¨äºŽæ‰€æœ‰æ¨¡åž‹ã€‚

CI 中的 LLM 评估意味着什么

è¯„ä¼°æ˜¯ä»£ç†çš„æµ‹è¯•ç”¨ä¾‹ã€‚å®ƒæœ‰ä¸€ä¸ªè¾“å ¥å’Œä¸€ä¸ªç”¨äºŽåˆ¤æ–­æ¨¡åž‹ç­”æ¡ˆæ˜¯å¦å¯æŽ¥å—çš„è§„åˆ™ã€‚å›ºå®šçš„è¯„ä¼°é›†æ˜¯æäº¤åˆ°ä»“åº“ä¸­çš„è¿™äº›ç”¨ä¾‹çš„åˆ—è¡¨ï¼Œå¹¶ä¸”ä» é€šè¿‡ç»è¿‡å®¡æŸ¥çš„æ‹‰å–è¯·æ±‚è¿›è¡Œæ›´æ”¹ã€‚æµæ°´çº¿éƒ¨åˆ†æ˜¯ä½ çš„ CI 设置。它决定用例何时运行,以及当太多用例失败时拉取请求会发生什么。

åˆ¤æ–­æ¨¡åž‹ç­”æ¡ˆæ˜¯å¦è‰¯å¥½æ˜¯ä¸€ä¸ªå•ç‹¬çš„é—®é¢˜ï¼Œä¸€ç§é€‰æ‹©æ˜¯å°†å ¶äº¤ç»™å¦ä¸€ä¸ªå å½“è¯„åˆ†è€ çš„æ¨¡åž‹ã€‚è¿™ç§æŠ€æœ¯ç§°ä¸º LLM-as-a-judge,我们在 我们的 LLM-as-a-judge 指南 中介绍了它。 æˆ‘ä»¬çš„å·¥å ·è°ƒç”¨å¾ªçŽ¯æŒ‡å— æ¶µç›–äº†æž„å»ºä»£ç†æœ¬èº«ã€‚æœ¬æŒ‡å—æ˜¯å ³äºŽå®ƒä»¬ä¹‹é—´çš„æµæ°´çº¿ã€‚

åœ¨ä½ èƒ½é—¨æŽ§ä»»ä½•ä¸œè¥¿ä¹‹å‰éœ€è¦ä»€ä¹ˆ

åœ¨é—¨æŽ§èƒ½å‘Šè¯‰ä½ ä»»ä½•æœ‰ç”¨ä¿¡æ¯ä¹‹å‰ï¼Œéœ€è¦å‡†å¤‡å¥½ä¸‰ä»¶äº‹ã€‚

  • 一个固定的、版本化的评估集。 Anthropic 的代理评估指南建议将 从真实失败中提取的 20 到 50 个简单任务 作为起始集。我们这里使用三个,以便示例保持简短。
  • 一种评分方法和一个阈值。 è¯„åˆ†æ–¹æ³•å°†ä¸€ä¸ªç­”æ¡ˆè½¬åŒ–ä¸ºä½ å¯ä»¥è®¡æ•°çš„é€šè¿‡æˆ–å¤±è´¥ã€‚é˜ˆå€¼é€‚ç”¨äºŽæ•´ä¸ªè¿è¡Œã€‚æœ¬æŒ‡å—ä½¿ç”¨å­—ç¬¦ä¸²æ–­è¨€ã€‚è¯„åˆ†æ ‡å‡†æˆ–è¯„åˆ¤æ¨¡åž‹ä¹Ÿå¯ä»¥ã€‚
  • 运行足够可重复以值得信赖。 é—¨æŽ§åº”åŸºäºŽå›žå½’è€Œä¸æ˜¯å™ªå£°è¿›è¡Œé˜»æ­¢ã€‚ä½¿ç”¨å¤šæ•°æŠ•ç¥¨çš„é‡å¤é‡‡æ ·é€‚ç”¨äºŽæ‰€æœ‰æ¨¡åž‹ã€‚ temperature 和 seed ä» åœ¨åˆ—å‡ºå®ƒä»¬çš„æ¨¡åž‹ä¸Šæœ‰å¸®åŠ©ï¼Œå› æ­¤åœ¨ä¾èµ–å®ƒä»¬ä¹‹å‰è¯·æ£€æŸ¥ 模型的 supported_parameters 。

ä½ è¿˜éœ€è¦ä¸€ä¸ªå¯¼å‡ºä¸º OPENROUTER_API_KEY 的 OpenRouter API 密钥 、Node 20 或更新版本,以及 jq 。

分四步构建门控

å®Œæˆè¿™å››æ­¥åŽï¼Œè§¦åŠä½ çš„æç¤ºçš„æ‹‰å–è¯·æ±‚ä¼šè‡ªåŠ¨è¿è¡Œä½ çš„è¯„ä¼°é›†ï¼Œå¹¶ä¸”å¦‚æžœåˆ†æ•°ä¸‹é™åˆ™æ— æ³•åˆå¹¶ã€‚

  • ä» åœ¨å¯èƒ½ç ´åä½ çš„ä»£ç†çš„æ›´æ”¹ä¸Šè§¦å‘è¯„ä¼°ã€‚
  • å°†è¯„ä¼°é›†æ”¾åœ¨ä»“åº“ä¸­ï¼Œä¸Žå ¶æµ‹è¯•çš„æç¤ºæ”¾åœ¨ä¸€èµ·ã€‚
  • 编写 CI 作业运行的脚本。
  • 测量决定合并是否被阻止的阈值。

第 1 æ­¥ï¼šä» åœ¨ç›¸å ³å˜æ›´æ—¶è§¦å‘

当提示词、agent é€»è¾‘ã€å·¥å · schemaã€è¯„ä¼°é›†ã€è¯„ä¼°è„šæœ¬æˆ–å·¥ä½œæµæœ¬èº«å‘ç”Ÿå˜åŒ–æ—¶è¿è¡Œè¯„ä¼°ã€‚æœ€åŽä¸¤é¡¹å¾ˆå®¹æ˜“è¢«é—æ¼ã€‚å¦‚æžœå®ƒä»¬ä¸åœ¨è¿‡æ»¤æ¡ä»¶ä¸­ï¼Œä¸€ä¸ªç ´åè¯„åˆ†é€»è¾‘æˆ–ä¿®æ”¹é—¨ç¦çš„ pull request å°±ä¼šè·³è¿‡è¯„ä¼°å¹¶åœ¨æœªç»æµ‹è¯•çš„æƒ å†µä¸‹åˆå¹¶ã€‚

ä½ å¯ä»¥ç”¨ä¸¤ä¸ª job 来实现。第一个 job 始终运行。它检查 pull request æ›´æ”¹çš„æ–‡ä»¶æ˜¯å¦åŒ¹é ä¸€ç»„è·¯å¾„ï¼Œå¹¶æ ¹æ®æ˜¯å¦æœ‰åŒ¹é è¾“å‡º true 或 false 。第二个 job è¿è¡Œè¯„ä¼°ï¼Œä» å½“ç¬¬ä¸€ä¸ª job 返回 true 时才会启动。

以工作流头部和第一个 job 开始 .github/workflows/eval-gate.yml 。 agent: ä¸‹åˆ—å‡ºçš„è·¯å¾„æ˜¯ä½ è¦ä¸ºè‡ªå·±çš„ä»“åº“ä¿®æ”¹çš„è·¯å¾„ã€‚

code
name
:
eval-gate
on
:
pull_request
concurrency
:
group
:
eval-gate-${{ github.ref }}
cancel-in-progress
:
true
jobs
:
changes
:
runs-on
:
ubuntu-latest
outputs
:
agent
:
${{ steps.filter.outputs.agent }}
steps
:
-
uses
:
actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
# v7.0.1
-
uses
:
dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d
# v4.0.3
id
:
filter
with
:
filters
:
|
agent:
- '.github/workflows/eval-gate.yml'
- 'prompts/**'
- 'agents/**'
- 'tools/**/schema.json'
- 'eval-sets/**'
- 'scripts/run-evals.mjs'

concurrency é åˆ cancel-in-progress ä¼šåœ¨æœ‰äººå†æ¬¡æŽ¨é€åˆ°åŒä¸€åˆ†æ”¯æ—¶å–æ¶ˆä¸Šä¸€æ¬¡è¿è¡Œï¼Œå› æ­¤ä¸€ä¸ªè¿žç»­å¤šæ¬¡æŽ¨é€çš„ pull request 不会为每次推送都付出一次评估运行的代价。

这里有两件事可能出错。

第一是绿色的对勾背后没有评估。当第一个 job 返回 false 时,GitHub 会跳过评估 job,而 被跳过的 job ä¼šæ»¡è¶³å¿ éœ€çš„çŠ¶æ€æ£€æŸ¥ ã€‚è¿™æ­£æ˜¯æ— å ³çš„ pull request å¾—ä»¥åˆå¹¶çš„åŽŸå› ã€‚è¿™ä¹Ÿæ„å‘³ç€ä¸€ä¸ªæ‹¼é”™çš„ glob ä¼šåœ¨æ²¡æœ‰è¿è¡Œä»»ä½•ç”¨ä¾‹çš„æƒ å†µä¸‹é€šè¿‡æ£€æŸ¥ï¼Œè€Œä¸€ä¸ªç›´æŽ¥å¤±è´¥çš„ changes job ä¹Ÿæœ‰åŒæ ·çš„æ•ˆæžœï¼Œå› ä¸º GitHub ä¼šè·³è¿‡å ¶ needs 依赖失败的 job。第 3 步通过将 changes ä¹Ÿè®¾ä¸ºå¿ éœ€æ£€æŸ¥æ¥å µä½ç¬¬äºŒä¸ªæ¼æ´žã€‚å¯¹äºŽç¬¬ä¸€ä¸ªæ¼æ´žï¼Œåœ¨æç¤ºè¯å˜æ›´é¦–æ¬¡é€šè¿‡æ—¶æ‰“å¼€è¿è¡Œæ—¥å¿—ï¼Œç¡®è®¤è¯„ä¼°ç¡®å®žè¿è¡Œäº†ã€‚

第二是过滤条件放在哪里。不要把它上移到工作流级别作为 on.pull_request.paths 。GitHub å ³äºŽ 跳过工作流运行 çš„æ–‡æ¡£è¯´ï¼Œå½“å·¥ä½œæµå› è·¯å¾„è¿‡æ»¤è€Œè¢«è·³è¿‡æ—¶ï¼Œâ€œä¸Žè¯¥å·¥ä½œæµå ³è”çš„æ£€æŸ¥å°†ä¿æŒâ€˜Pending’状态”,而要求这些检查的 pull request 会被阻止合并。

第 2 æ­¥ï¼šå°†è¯„ä¼°é›†æ”¾å ¥ä»“åº“

将评估集与它所测试的提示词放在同一个仓库中。当有人编辑提示词时,针对它的测试会在同一个 pull request ä¸­å˜æ›´ï¼Œä¸€ä½å®¡æŸ¥è€ èƒ½åŒæ—¶çœ‹åˆ°ä¸¤è€ ã€‚

四个文件并排放置。

code
your-repo/
.github/workflows/eval-gate.yml  -> the workflow from Step 1
prompts/support-agent.md         -> the system prompt
eval-sets/support-agent.json     -> the cases that test it
scripts/run-evals.mjs            -> the script from Step 3

将评估集保存为 eval-sets/support-agent.json ã€‚æ¯ä¸ªç”¨ä¾‹åŒ å«ä¸€ä¸ªè¾“å ¥ã€ç­”æ¡ˆå¿ é¡»åŒ å«çš„å­—ç¬¦ä¸²ï¼Œä»¥åŠç­”æ¡ˆä¸å¾—åŒ å«çš„å­—ç¬¦ä¸²ã€‚ mustMention ä¸­çš„æ¡ç›®ä¹Ÿå¯ä»¥æ˜¯ä¸€ä¸ªåˆ—è¡¨ï¼Œå¦‚ç¬¬ä¸‰ä¸ªç”¨ä¾‹é‚£æ ·ï¼Œæ­¤æ—¶ç­”æ¡ˆåªéœ€åŒ å«å ¶ä¸­çš„ä¸€ä¸ªå­—ç¬¦ä¸²å³å¯ã€‚å½“æ¨¡åž‹åœ¨ä½ æœŸæœ›â€œhuman”的地方写成“a person”或“our teamâ€æ—¶ï¼Œå•ä¸ªå­—é¢å­—ç¬¦ä¸²å°±ä¼šå¤±è´¥ï¼Œå› æ­¤åœ¨æœ‰å¤šç§æŽªè¾žå¯æŽ¥å—çš„åœ°æ–¹è¯·ä½¿ç”¨åˆ—è¡¨ã€‚

code
[
{
"id"
:
"refund-window"
,
"input"
:
"How long do I have to request a refund on a digital download?"
,
"mustMention"
: [
"14 days"
],
"mustNotMention"
: [
"30 days"
]
},
{
"id"
:
"refund-exception"
,
"input"
:
"I bought a download 60 days ago. Can I still get a refund?"
,
"mustMention"
: [
"14 days"
],
"mustNotMention"
: [
"yes, you can"
]
},
{
"id"
:
"escalation"
,
"input"
:
"Your product deleted my files and I want a lawyer."
,
"mustMention"
: [[
"human"
,
"person"
,
"our team"
,
"specialist"
]],
"mustNotMention"
: [
"14 days"
]
}
]

这些用例所测试的提示词就在它旁边,位于 prompts/support-agent.md 中。

code
You are a support agent for a digital downloads store.
The refund window is 14 days from purchase. There are no exceptions to it.
If a customer threatens legal action or reports data loss, hand off to a human
and do not quote the refund policy.
Answer in at most three sentences.

从三个用例开始。每当 agent åœ¨ç”Ÿäº§çŽ¯å¢ƒä¸­å‡ºé”™æ—¶ï¼Œå°±æ·»åŠ ä¸€ä¸ªã€‚

将 prompts/ 和 eval-sets/ 都置于 CODEOWNERS 规则之下,并在分支保护规则中开启“Require review from Code Ownersâ€ã€‚å¦åˆ™ï¼Œç»•è¿‡å¤±è´¥é—¨ç¦çš„æœ€å¿«æ–¹æ³•å°±æ˜¯æ”¾å®½é‚£ä¸ªæ•èŽ·äº†å›žå½’çš„æµ‹è¯•ã€‚ä» æœ‰ä¸€ä¸ª CODEOWNERS 文件只会请求审查,并不会阻止合并。

第 3 步:编写 CI job 运行的脚本

该 job è¿è¡Œä¸€ä¸ªè„šæœ¬ï¼Œå½“é€šè¿‡çŽ‡ä½ŽäºŽé˜ˆå€¼æ—¶ä»¥éžé›¶çŠ¶æ€é€€å‡ºã€‚è¿™ä¸ªé€€å‡ºç å°±æ˜¯ CI é˜»æ­¢åˆå¹¶æ‰€éœ€çš„å ¨éƒ¨ã€‚

è¯¥è„šæœ¬åŠ è½½è¯„ä¼°é›†ï¼Œå°†æ¯ä¸ªç”¨ä¾‹å‘é€ç»™æ¨¡åž‹ï¼Œæ£€æŸ¥ç­”æ¡ˆï¼Œå¹¶ä»¥ä¸€ä¸ªå‘ŠçŸ¥ CI å‘ç”Ÿäº†ä»€ä¹ˆçš„é€€å‡ºç é€€å‡ºã€‚å®ƒåªä½¿ç”¨ Node å† ç½®æ¨¡å—ï¼Œå› æ­¤æ— éœ€å®‰è£ ä»»ä½•ä¸œè¥¿ã€‚å°†å ¶ä¿å­˜ä¸º scripts/run-evals.mjs 。

code
import
{ readFileSync }
from
"node:fs"
;
import
{ parseArgs }
from
"node:util"
;
const
{
values
}
=
parseArgs
({
options: {
set: { type:
"string"
, default:
"eval-sets/support-agent.json"
},
prompt: { type:
"string"
, default:
"prompts/support-agent.md"
},
model: { type:
"string"
, default:
"anthropic/claude-sonnet-5"
},
threshold: { type:
"string"
, default:
"0.9"
},
samples: { type:
"string"
, default:
"3"
},
concurrency: { type:
"string"
, default:
"8"
},
},
});
const
systemPrompt
=
readFileSync
(values.prompt,
"utf8"
);
const
cases
=
JSON
.
parse
(
readFileSync
(values.set,
"utf8"
));
const
threshold
=
Number
(values.threshold);
const
samples
=
Number
(values.samples);
const
concurrency
=
Number
(values.concurrency);
// Exit 2 for anything that stops the eval from running, so the job can tell
// "the agent got worse" apart from "the eval could not run".
function
abort
(
message
) {
console.
error
(
`::error::eval could not run: ${
message
}`
);
process.
exit
(
2
);
}
if
(
!
Array.
isArray
(cases)
||
cases.
length
===
0
)
abort
(
`${
values
.
set
} has no cases`
);
if
(
!
(threshold
>
0
&&
threshold
<=
1
))
abort
(
`--threshold must be greater than 0 and at most 1, got "${
values
.
threshold
}"`
);
if
(
!
Number.
isInteger
(samples)
||
samples
<
1
||
samples
%
2
===
0
)
abort
(
`--samples must be a positive odd integer, got ${
values
.
samples
}`
);
if
(
!
Number.
isInteger
(concurrency)
||
concurrency
<
1
)
abort
(
`--concurrency must be a positive integer, got ${
values
.
concurrency
}`
);
class
EvalDidNotRun
extends
Error
{}
async
function
callModel
(
input
) {
const
res
=
await
fetch
(
"https://openrouter.ai/api/v1/chat/completions"
, {
method:
"POST"
,
signal: AbortSignal.
timeout
(
60_000
),
headers: {
Authorization:
`Bearer ${
process
.
env
.
OPENROUTER_API_KEY
}`
,
"Content-Type"
:
"application/json"
,
},
body:
JSON
.
stringify
({
model: values.model,
// Route to one provider only. A different provider serving the same slug
// between runs would look like a prompt regression.
provider: { order: [
"anthropic"
], allow_fallbacks:
false
},
messages: [
{ role:
"system"
, content: systemPrompt },
{ role:
"user"
, content: input },
],
}),
});
if
(
!
res.ok)
throw
new
EvalDidNotRun
(
`${
res
.
status
} ${
await
res
.
text
()
}`
);
const
body
=
await
res.
json
();
return
{ text: body.choices?.[
0
]?.message?.content
??
""
, cost: body.usage?.cost
??
0
};
}
async
function
run
(
input
) {
for
(
let
attempt
=
1
; attempt
<=
3
; attempt
++
) {
try
{
return
await
callModel
(input);
}
catch
(err) {
if
(attempt
===
3
)
throw
new
EvalDidNotRun
(err.message);
await
new
Promise
((
resolve
)
=>
setTimeout
(resolve, attempt
*
2000
));
}
}
}
// A requirement is either a string, or an array meaning "any one of these".
function
matches
(
answer
,
requirement
) {
const
options
=
Array.
isArray
(requirement)
?
requirement
:
[requirement];
return
options.
some
((
option
)
=>
answer.
includes
(option.
toLowerCase
()));
}
function
score
(
text
,
testCase
) {
const
answer
=
text.
toLowerCase
();
const
must
=
testCase.mustMention
??
[];
const
mustNot
=
testCase.mustNotMention
??
[];
return
(
must.
every
((
r
)
=>
matches
(answer, r))
&&
!
mustNot.
some
((
r
)
=>
matches
(answer, r))
);
}
// One unit of work per sample, so the whole matrix runs with a fixed
// concurrency instead of one request at a time.
const
jobs
=
cases.
flatMap
((
testCase
)
=>
Array.
from
({ length: samples }, ()
=>
testCase),
);
const
results
=
new
Map
(cases.
map
((
c
)
=>
[c.id, []]));
let
spend
=
0
;
let
cursor
=
0
;
async
function
worker
() {
while
(cursor
<
jobs.
length
) {
const
testCase
=
jobs[cursor
++
];
const
{
text
,
cost
}
=
await
run
(testCase.input);
spend
+=
cost;
results.
get
(testCase.id).
push
(
score
(text, testCase));
}
}
const
started
=
Date.
now
();
try
{
await
Promise
.
all
(Array.
from
({ length: Math.
min
(concurrency, jobs.
length
) }, worker));
}
catch
(err) {
// A provider timeout is not a quality regression.
abort
(err.message);
}
let
passed
=
0
;
for
(
const
testCase
of
cases) {
const
verdicts
=
results.
get
(testCase.id);
const
majority
=
verdicts.
filter
(Boolean).
length
>
samples
/
2
;
if
(majority) passed
++
;
const
trace
=
verdicts.
map
((
v
)
=>
(v
?
"."
:
"x"
)).
join
(
""
);
console.
log
(
`${
majority
?
"PASS"
:
"FAIL"}  ${
testCase
.
id
}  ${
trace
}`
);
}
const
rate
=
passed
/
cases.
length
;
const
seconds
=
((Date.
now
()
-
started)
/
1000
).
toFixed
(
1
);
console.
log
(
`
\n
pass rate ${
rate
.
toFixed
(
2
)
} against threshold ${
threshold
}`
);
console.
log
(
`${
jobs
.
length
} calls in ${
seconds
}s, cost $${
spend
.
toFixed
(
4
)
} on ${
values
.
model
}`
);
if
(rate
<
threshold) {
console.
error
(
`::error::eval gate failed: ${
passed
}/${
cases
.
length
} cases passed`
);
process.
exit
(
1
);
}

该脚本除了调用模型之外还做三件事。

å½“è¯„ä¼°é›†ä¸ºç©ºæˆ–æŸä¸ªé€‰é¡¹æ ¼å¼é”™è¯¯æ—¶ï¼Œå®ƒä¼šæ‹’ç»è¿è¡Œï¼Œå¹¶åœ¨å‘é€è¯·æ±‚å‰ä»¥é€€å‡ºç  2 退出。如果没有这些检查,一个被意外替换为 [] 的评估集会产生 NaN 的通过率,而 NaN < threshold ä¸ºå‡ï¼Œå› æ­¤æ²¡æœ‰ä»»ä½•è¯„ä¼°èƒ½é€šè¿‡é—¨ç¦ã€‚ --threshold 为 90% 或负的 --samples ä¹Ÿä¼šä»¥åŒæ ·çš„æ–¹å¼é€šè¿‡ï¼Œè€Œç©ºçš„ --threshold 会变成 0 ,没有任何通过率能低于它,所以该检查也会拒绝 0 。

å®ƒå¯¹æ¯ä¸ªç”¨ä¾‹è¿è¡Œå¤šæ¬¡ï¼Œæ¯ä¸€æ¬¡è¿è¡Œéƒ½æ˜¯ä¸€ä¸ªæ ·æœ¬ã€‚å®ƒå–æ‰€æœ‰æ ·æœ¬ä¸­çš„å¤šæ•°è£å†³ï¼Œå› ä¸ºç›¸åŒçš„æç¤ºå¹¶ä¸æ€»æ˜¯äº§ç”Ÿç›¸åŒçš„ç­”æ¡ˆã€‚

它还会将每个请求路由到同一个提供商。像 anthropic/claude-sonnet-5 è¿™æ ·çš„æ¨¡åž‹ slug 会通过 OpenRouter ç”±å¤šä¸ªæä¾›å•†æä¾›æœåŠ¡ã€‚åœ¨æ’°å†™æœ¬æ–‡æ—¶ï¼Œå ¶ç«¯ç‚¹åŒ æ‹¬ Anthropic、Amazon Bedrock、Azure 和 Google。如果没有路由偏好,同一评估的两次运行可能会到达两个不同的提供商,而它们答案之间的差异看起来就像是提示回归。 provider.order 是提供商 slug çš„ä¼˜å ˆçº§åˆ—è¡¨ï¼Œ allow_fallbacks: false å‘Šè¯‰æˆ‘ä»¬è¦å°è¯•è¯¥åˆ—è¡¨ä¹‹å¤–çš„ä»»ä½•æä¾›å•†ã€‚å¦‚æžœåˆ—å‡ºçš„æä¾›å•†å¤±è´¥ï¼Œè¯·æ±‚å°±ä¼šå¤±è´¥ï¼Œè€Œä¸æ˜¯è½¬ç§»åˆ°å¤‡ç”¨æä¾›å•†ï¼Œè„šæœ¬ä¼šä»¥é€€å‡ºç  2 退出。 提供商选择文档 æ¶µç›–äº†è¿™ä¸¤ä¸ªå­—æ®µã€‚è¯¥å›ºå®šç»‘å®šåˆ°ä½ æŒ‡å®šçš„æ¨¡åž‹ã€‚å¦‚æžœä½ å°† --model æ›´æ”¹ä¸ºå ¶ä»–ä¾›åº”å•†çš„æ¨¡åž‹ï¼Œä¹Ÿè¦æ›´æ”¹ order çš„å€¼ï¼Œå¦åˆ™æ²¡æœ‰æä¾›å•†ä¼šåŒ¹é ï¼Œæ¯ä¸ªè¯·æ±‚éƒ½ä¼šå¤±è´¥ã€‚

æœ€åŽä¸€è¡Œæ‰“å°çš„æˆæœ¬æ¥è‡ªæˆ‘ä»¬åœ¨æ¯ä¸ªéžæµå¼å“åº”ä¸­åŒ å«çš„ usage 对象。 ç”¨é‡æ ¸ç®—æ–‡æ¡£ 描述了这些字段。

å ˆåœ¨æœ¬åœ°è¿è¡Œå®ƒã€‚

code
export
OPENROUTER_API_KEY
=
"sk-or-..."
node
scripts/run-evals.mjs
--samples
3

æ¯ä¸ªç‚¹æ˜¯ä¸€ä¸ªé€šè¿‡çš„æ ·æœ¬ï¼Œæ¯ä¸ª x æ˜¯ä¸€ä¸ªå¤±è´¥çš„æ ·æœ¬ï¼Œå› æ­¤ä½ å¯ä»¥çœ‹åˆ°å“ªä¸ªç”¨ä¾‹é—´æ­‡æ€§å¤±è´¥ï¼Œè€Œä¸ä» ä» æ˜¯æœ€ç»ˆçš„é€šè¿‡çŽ‡ã€‚

code
PASS  refund-window  ...
PASS  refund-exception  ...
PASS  escalation  ...
pass rate 1.00 against threshold 0.9
9 calls in <seconds>s, cost $<cost> on anthropic/claude-sonnet-5

å½“æŸä¸ªç”¨ä¾‹çš„å¤§å¤šæ•°æ ·æœ¬é€šè¿‡æ—¶ï¼Œè¯¥ç”¨ä¾‹å°±ç®—ä½œé€šè¿‡ï¼Œæ‰€ä»¥ ..x 仍然是 PASS 。

çŽ°åœ¨å°†ç¬¬äºŒä¸ªä½œä¸šæ·»åŠ åˆ° .github/workflows/eval-gate.yml 中,位于 changes 作业下方。

code
eval-gate
:
needs
:
changes
if
:
needs.changes.outputs.agent == 'true'
runs-on
:
ubuntu-latest
timeout-minutes
:
15
steps
:
-
uses
:
actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
# v7.0.1
-
uses
:
actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
# v7.0.0
with
:
node-version
:
'24'
-
name
:
Run fixed eval set
env
:
OPENROUTER_API_KEY
:
${{ secrets.OPENROUTER_API_KEY }}
run
:
node scripts/run-evals.mjs --samples 3 --threshold 0.9

if: 行读取 changes ä½œä¸šçš„è¾“å‡ºï¼Œæ­£æ˜¯å®ƒé˜»æ­¢äº†è¯„ä¼°åœ¨ä¸å¯èƒ½ç ´åä»£ç†çš„æ‹‰å–è¯·æ±‚ä¸Šè¿è¡Œã€‚ timeout-minutes: 15 é˜»æ­¢æŒ‚èµ·çš„æä¾›å•†å ç”¨è¿è¡Œå™¨è¾¾ é»˜è®¤å ­å°æ—¶ 。每个操作都固定到完整的提交 SHAï¼Œå¹¶åœ¨æœ«å°¾æ³¨é‡Šä¸­æ³¨æ˜Žå ¶å¯¹åº”çš„å‘å¸ƒç‰ˆæœ¬ã€‚åƒ v4 è¿™æ ·çš„æ ‡ç­¾å¯ä»¥è¢«ç§»åŠ¨ä»¥æŒ‡å‘æ–°ä»£ç ï¼Œè€Œè¿™ä¸ªä½œä¸šæŒæœ‰ä½ çš„ API å¯†é’¥ï¼Œå› æ­¤å›ºå®šåˆ° SHA æ„å‘³ç€è¿è¡Œçš„ä»£ç ä¸ä¼šåœ¨æ²¡æœ‰ä½ ä»“åº“ä¸­å˜æ›´çš„æƒ å†µä¸‹æ”¹å˜ã€‚ GitHub çš„å®‰å ¨åŠ å›ºæŒ‡å— ä¹ŸæŽ¨èè¿™æ ·åšã€‚

在门禁能够阻止任何东西之前,做三件事。

  • 在 Settings > Secrets and variables > Actions ä¸‹å°†ä½ çš„å¯†é’¥æ·»åŠ ä¸ºä»“åº“æœºå¯†ï¼Œå‘½åä¸º OPENROUTER_API_KEY 。
  • 打开一个触及 prompts/ 的拉取请求,以便工作流运行一次。
  • åœ¨ä½ çš„åˆ†æ”¯ä¿æŠ¤è§„åˆ™ä¸­å°† changes 和 eval-gate éƒ½æ·»åŠ ä¸º å¿ éœ€çŠ¶æ€æ£€æŸ¥ 。

ç¬¬ä¸‰æ­¥æ‰æ˜¯å°†è¯„ä¼°å˜æˆé—¨ç¦çš„å ³é”®ã€‚æ²¡æœ‰å®ƒï¼Œå¤±è´¥çš„è¯„ä¼°ä¼šåœ¨æ‹‰å–è¯·æ±‚ä¸Šæ˜¾ç¤ºä¸€ä¸ªçº¢ X,但拉取请求仍然可以合并。同时要求 changes å¯ä»¥è¦†ç›–è¿‡æ»¤ä½œä¸šæœ¬èº«å¤±è´¥çš„æƒ å†µï¼Œä¾‹å¦‚æ£€å‡ºé”™è¯¯ã€‚ç„¶åŽ GitHub 会跳过 eval-gate ,而被跳过的作业算作通过,所以如果只要求 eval-gate ï¼Œæç¤ºæ›´æ”¹å¯èƒ½ä¼šåœ¨æœªè¢«è¯„ä¼°çš„æƒ å†µä¸‹åˆå¹¶ã€‚å¦‚æžœä¹Ÿè¦æ±‚ changes ï¼Œå¤±è´¥çš„è¿‡æ»¤ä½œä¸šä¼šé˜»æ­¢åˆå¹¶ã€‚ä¸ç›¸å ³çš„æ‹‰å–è¯·æ±‚ä»ç„¶å¯ä»¥åˆå¹¶ï¼Œå› ä¸º changes 通过而 eval-gate 被跳过。

除 GITHUB_TOKEN 外, GitHub ä¸ä¼šå°†å¯†é’¥ä¼ é€’ç»™ç”± fork 仓库触发的工作流 ï¼Œå› æ­¤æ¥è‡ª fork çš„æ‹‰å–è¯·æ±‚ä¼šå› ç¼ºå°‘å¯†é’¥è€Œå¤±è´¥ã€‚å¦‚æžœä½ çš„ä»“åº“æŽ¥å— fork 贡献,也要在推送到 main æ—¶è¿è¡Œè¯„ä¼°ï¼Œè¿™æ ·é€šè¿‡ fork æäº¤çš„æ›´æ”¹ä»ä¼šè¢«æ£€æŸ¥ã€‚å¦‚æžœä½ éœ€è¦è®°å½•æŸæ¬¡é˜»æ–­åˆå¹¶çš„è¿è¡Œæ£€æŸ¥äº†ä»€ä¹ˆï¼Œè¯·å°†è¿è¡Œè¾“å‡ºä½œä¸ºå·¥ä½œæµäº§ç‰©ä¸Šä¼ ã€‚

çŽ°åœ¨è¿™ä¸ªé—¨ç¦ä¼šåœ¨æ¯ä¸ªå¯èƒ½ç ´å agent çš„æ‹‰å–è¯·æ±‚ä¸Šè¿è¡Œã€‚å®ƒä»ç„¶ä½¿ç”¨ä¸€ä¸ªæ²¡äººèƒ½è¯æ˜Žå ¶åˆç†æ€§çš„é˜ˆå€¼ï¼Œæ‰€ä»¥ç¬¬ 4 步要测量出一个。

第 4 步:测量阈值

针对未改动的 main åˆ†æ”¯è¿è¡Œè¯„ä¼°é›†å ­æ¬¡ã€‚å„æ¬¡è¿è¡Œä¹‹é—´ä¸è¦åšä»»ä½•æ›´æ”¹ã€‚è®°ä¸‹ä½ çœ‹åˆ°çš„æœ€ä½Žé€šè¿‡çŽ‡ã€‚ä½ è¦çš„æ˜¯æœ€å·®çš„ä¸€æ¬¡è¿è¡Œï¼Œè€Œä¸æ˜¯å ¸åž‹çš„é‚£æ¬¡ï¼Œæ‰€ä»¥å¦‚æžœæ¡ä»¶å è®¸å°±å¤šè¿è¡Œå‡ æ¬¡ã€‚é‚£ä¸ªæœ€ä½Žé€šè¿‡çŽ‡å°±æ˜¯ä½ çš„ä¸‹é™ï¼Œä½ æŠŠé˜ˆå€¼è®¾åœ¨è¯¥å€¼æˆ–ä»¥ä¸‹ã€‚

åœ¨ä¸‰ä¸ªç”¨ä¾‹çš„æƒ å†µä¸‹ï¼Œå ¶ä¸­ä¸€ä¸ªç”¨ä¾‹åœ¨è¿™å ­æ¬¡è¿è¡Œä¸­çš„æŸä¸€æ¬¡å¤±è´¥ï¼Œä¸‹é™å°±æ˜¯ 0.67。相对于 0.9 çš„é˜ˆå€¼ï¼Œé‚£æ¬¡è¿è¡Œå°±æ˜¯ä¸€ä¸ªè¢«é˜»æ–­çš„æ‹‰å–è¯·æ±‚ï¼Œè€Œå ¶èƒŒåŽå¹¶æ²¡æœ‰å›žå½’ã€‚

å½“ä½ çš„ä¸‹é™å¾ˆä½Žæ—¶ï¼ŒæŒ‰é¡ºåºæŽ’æŸ¥ä»¥ä¸‹ä¸‰é¡¹ã€‚

é¦–å ˆï¼Œæ£€æŸ¥ä½ è‡ªå·±çš„æ–­è¨€ã€‚ä¸Šé¢çš„ escalation 用例接受 human 、 person 、 our team 或 specialist 中的任意一个。如果某个版本要求字面字符串 human ,那么每当模型写成“a personâ€æ—¶å®ƒéƒ½ä¼šå¤±è´¥ï¼Œè€Œæç¤ºè¯å¹¶ä¸èƒ½é˜»æ­¢æ¨¡åž‹è¿™æ ·åšã€‚æ¯æ¬¡éƒ½è¦å ˆæ£€æŸ¥ä½ çš„æ–­è¨€ï¼Œå†æ£€æŸ¥å ¶ä»–ä»»ä½•ä¸œè¥¿ã€‚

å ¶æ¬¡ï¼Œæ£€æŸ¥ä½ çš„ç¡®å®šæ€§è®¾ç½®æ˜¯å¦èµ·äº†ä½œç”¨ã€‚å°† temperature 设为零并固定一个 seed ï¼Œåªæœ‰åœ¨æ¨¡åž‹æ”¯æŒè¿™äº›å‚æ•°æ—¶æ‰æœ‰å¸®åŠ©ã€‚ä¸‹é¢çš„å‘½ä»¤ä¼šæ‰“å°å‡ºæ¨¡åž‹æ”¯æŒçš„å‚æ•°ï¼Œè¿™æ ·ä½ å°±èƒ½çœ‹åˆ° temperature 和 seed 是否在列表中。

code
curl
-s
"https://openrouter.ai/api/v1/models"
\
|
jq
-r
'.data[] | select(.id=="anthropic/claude-sonnet-5") | .supported_parameters'

Claude Sonnet 5 ä¸¤è€ éƒ½æ²¡æœ‰åˆ—å‡ºï¼Œè¿™å°±æ˜¯ä¸Šé¢çš„è„šæœ¬ä¸å‘é€å®ƒä»¬çš„åŽŸå› ã€‚æˆ‘ä»¬çš„ Claude Sonnet 5 迁移指南 指出,对于该模型, temperature 、 top_p 和 top_k 会被静默忽略。2026 å¹´ 9 月 18 日,目录中列出了 445 ä¸ªæ¨¡åž‹ï¼Œå ¶ä¸­ 267 个同时列出了 seed 和 temperature ã€‚å ¶ä½™ 178 ä¸ªï¼Œçº¦å äº”åˆ†ä¹‹äºŒï¼Œåªåˆ—å‡ºäº†å ¶ä¸­ä¹‹ä¸€æˆ–ä¸¤è€ éƒ½æ²¡æœ‰ã€‚è¿™æ¡å‘½ä»¤ä¼šç»Ÿè®¡åŒæ—¶åˆ—å‡ºä¸¤è€ çš„æ¨¡åž‹æ•°é‡ï¼Œä½ å¯ä»¥é‡æ–°è¿è¡Œå®ƒä»¥èŽ·å¾—å½“å‰æ•°å­—ã€‚

code
curl
-s
"https://openrouter.ai/api/v1/models"
|
jq
'
[.data[] | select(.supported_parameters | index("seed") and index("temperature"))] | length'

å¦‚æžœä½ çš„æ¨¡åž‹ç¡®å®žåˆ—å‡ºäº†å®ƒä»¬ï¼Œå°±æŠŠ temperature: 0 和 seed: 42 åŠ å ¥è¯·æ±‚ä½“ï¼Œå¹¶åœ¨ order 旁边设置 provider.require_parameters: true 。使用默认的 require_parameters: false 时,即使某个提供商不支持请求中的每个参数,它仍可能收到请求并忽略自己不认识的参数。使用 require_parameters: true 时,请求只会被路由到支持所有参数的提供商。

ç¬¬ä¸‰ï¼Œç»è¿‡å‰ä¸¤é¡¹æ£€æŸ¥åŽä»ç„¶å­˜åœ¨çš„æ³¢åŠ¨å°±æ˜¯çœŸå®žçš„ï¼Œä½ é€šè¿‡é‡‡æ ·æ¥å¸æ”¶å®ƒã€‚æé«˜ --samples 直到下限不再变化。3 是一个合理的默认值,而且它已经是单次运行成本的三倍,所以在提高到 5 ä¹‹å‰å ˆæµ‹é‡ä¸€ä¸‹ã€‚

å°è§„æ¨¡è¯„ä¼°é›†æœ‰ä¸€ä¸ªå€¼å¾—äº†è§£çš„ç‰¹æ€§ã€‚åœ¨ä¸‰ä¸ªç”¨ä¾‹çš„æƒ å†µä¸‹ï¼Œé€šè¿‡çŽ‡åªèƒ½æ˜¯ 0、0.33、0.67 或 1.00,所以 0.9 çš„é˜ˆå€¼æ„å‘³ç€ä¸‰ä¸ªç”¨ä¾‹å¿ é¡»å ¨éƒ¨é€šè¿‡ã€‚è¿™æ˜¯æŠŠè¯„ä¼°é›†æ‰©å 到 20 个或更多用例的另一个理由。

å½“æŸä¸ªæ‹‰å–è¯·æ±‚ä» å› ä¸€ä¸ªç”¨ä¾‹è€Œæœªé€šè¿‡é—¨ç¦æ—¶ï¼Œå¯¹ main è¿è¡ŒåŒæ ·çš„è¯„ä¼°ã€‚å¦‚æžœ main ä¹Ÿå¤±è´¥ï¼Œé‚£è¿™æ¬¡å¤±è´¥å°±æ˜¯å™ªå£°ï¼Œä½ éœ€è¦æ›´å¤šæ ·æœ¬æˆ–æ›´ä½Žçš„é˜ˆå€¼ã€‚å¦‚æžœ main 通过,就把这次失败当作该拉取请求中的回归来处理。

è¿™å°±æ˜¯å®Œæ•´çš„é—¨ç¦ã€‚æœ¬æŒ‡å—çš„å ¶ä½™éƒ¨åˆ†ä»‹ç»å½“å­—ç¬¦ä¸²æ£€æŸ¥ä¸å†è¶³å¤Ÿæ—¶è¯¥æ€Žä¹ˆåšï¼Œä»¥åŠä½•æ—¶å€¼å¾—ç”¨ä¸€ä¸ªèƒ½å­˜å‚¨è¿è¡ŒåŽ†å²çš„å¹³å°æ¥æ›¿æ¢æ™®é€šè„šæœ¬ã€‚

用 Ori Eval æµ‹è¯•è°ƒç”¨å·¥å ·çš„ agent

ä¸Šé¢çš„è„šæœ¬å‘é€ä¸€æ¡æ¶ˆæ¯å¹¶è¯»å–ä¸€æ¡å›žå¤ã€‚å¦‚æžœä½ çš„ agent è°ƒç”¨å·¥å ·ï¼Œè¿™è¿˜ä¸å¤Ÿã€‚å­—ç¬¦ä¸²æ£€æŸ¥æ— æ³•å‘Šè¯‰ä½ agent æ˜¯å¦è°ƒç”¨äº†æ­£ç¡®çš„å·¥å ·ï¼Œæˆ–è€ æ˜¯å¦è°ƒç”¨äº†ä¸€ä¸ªæœ¬åº”é¿å çš„æ˜‚è´µå·¥å ·ã€‚

Ori Eval 是我们用于 agent 的 eval 测试框架。测试框架是运行 eval 的程序。它运行 agent,记录 agent åšäº†ä»€ä¹ˆï¼Œå¹¶æ ¹æ®ä½ çš„æ–­è¨€æ£€æŸ¥ç»“æžœã€‚Ori eval 是 .eval.ts 文件,断言针对的是 agent 做了什么。

code
import
{ test }
from
'bun:test'
;
import
{ assertModelIsLive, setupAgent, setupJudge }
from
'ori/eval'
;
const
MODEL
=
'anthropic/claude-sonnet-5'
;
await
assertModelIsLive
(
MODEL
);
const
agent
=
setupAgent
({ model:
MODEL
});
// Grade with a different model family than the one under test.
const
judge
=
setupJudge
({
agent:
setupAgent
({ model:
'openai/gpt-5-mini'
}),
minScore:
0.8
,
});
test
(
'looks up the order before quoting the refund policy'
,
async
()
=>
{
const
run
=
await
agent.
run
(
'Can I refund order #1234? I bought it 60 days ago.'
);
run.
tool
(
'lookup_order'
).
toBeCalled
();
run.
tool
(
'issue_refund'
).
toNotBeCalled
();
run.
toComplete
();
await
judge.
autoEvals
({
criteria:
'Cites the 14-day window and does not invent exceptions.'
,
run,
});
});

run.tool(...) æ˜¯æ™®é€šè„šæœ¬æ— æ³•åšåˆ°çš„éƒ¨åˆ†ã€‚å¦‚æžœ slug 离开了目录, assertModelIsLive ä¼šä»¥æ¸ æ™°çš„æ¶ˆæ¯ä½¿è¿è¡Œå¤±è´¥ï¼Œå› æ­¤æ–‡ä»¶ä¼šå¤§å£°å¤±è´¥ï¼Œè€Œä¸æ˜¯æµ‹è¯•ä¸€ä¸ªå·²ä¸å­˜åœ¨çš„æ¨¡åž‹ã€‚åœ¨ä½ è®©è¯„åˆ¤å™¨ä½¿æž„å»ºå¤±è´¥ä¹‹å‰ï¼Œè‡ªå·±ç»™åŒä¸€æ‰¹è¿è¡Œä¸­çš„æ ·æœ¬æ‰“åˆ†ï¼Œå¹¶å°†ä½ çš„åˆ¤å®šä¸Žè¯„åˆ¤å™¨çš„åˆ¤å®šè¿›è¡Œæ¯”è¾ƒã€‚Anthropic çš„æŒ‡å—å‡ºäºŽåŒæ ·çš„åŽŸå› å»ºè®® ç”¨äººç±»ä¸“å®¶æ ¡å‡† LLM 评判器 。

Ori 使用 Bun 运行 eval 文件,当 CI 为 true æ—¶ï¼Œå®ƒä¸ä¼šä¸ºä½ å®‰è£ Bunã€‚åœ¨å·¥ä½œæµä¸­ï¼Œä½ å ˆå®‰è£ Bun,然后下载一个固定版本的 Ori å‘å¸ƒåŒ å¹¶åœ¨è¿è¡Œå‰éªŒè¯å ¶æ ¡éªŒå’Œï¼Œå› ä¸ºè¯¥ä»»åŠ¡æŒæœ‰ä½ çš„ API key。设置 OPENROUTER_API_KEY 后,Ori 在 CI 中不需要 ori login 。

code
-
uses
:
oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
# v2.2.0
-
name
:
Install Ori
env
:
ORI_RELEASE
:
cli-0.15.0-531912d
ORI_SHA256
:
d2545db7a686f29ebae5bbf7e134d89a409cd00c760c1f24a5f8a88692c5947d
run
:
|
base="https://github.com/OpenRouterLabs/ori-releases/releases/download/$ORI_RELEASE"
curl -fsSL --proto '=https' -o ori "$base/ori-linux-x64"
echo "$ORI_SHA256  ori" | sha256sum -c -
mkdir -p "$HOME/.local/bin"
install -m 0755 ori "$HOME/.local/bin/ori"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
-
name
:
Run pinned agent eval
env
:
OPENROUTER_API_KEY
:
${{ secrets.OPENROUTER_API_KEY }}
run
:
ori eval --report eval-report.md

ORI_RELEASE 和 ORI_SHA256 æŒ‡å®šäº†æ’°å†™æ—¶çš„ç¨³å®šç‰ˆæœ¬åŠå ¶ ori-linux-x64 æ‘˜è¦ã€‚å½“ä½ è¿ç§»åˆ°æ›´æ–°çš„ç‰ˆæœ¬æ—¶ï¼Œè¯·åŒæ—¶æ›´æ–°ä¸¤è€ ã€‚ä»ŽåŒä¸€ä¸ªå‘å¸ƒé¡µé¢ä¸‹è½½æ ¡éªŒå’Œæ–‡ä»¶ä¸ä¼šå¢žåŠ ä»»ä½•ä¸œè¥¿ï¼Œå› ä¸ºä»»ä½•èƒ½æ›¿æ¢äºŒè¿›åˆ¶æ–‡ä»¶çš„äººéƒ½èƒ½æ›¿æ¢å®ƒæ—è¾¹çš„æ ¡éªŒå’Œã€‚å°†æ‘˜è¦ä¿ç•™åœ¨å·¥ä½œæµä¸­æ„å‘³ç€äºŒè¿›åˆ¶æ–‡ä»¶ä¸€æ—¦æ”¹å˜ï¼Œ sha256sum 检查就会失败。

ori eval 查找当前目录下的每个 *.eval.ts 文件并将它们交给 bun test ã€‚å®ƒçš„é€€å‡ºä»£ç å°±æ˜¯ bun test çš„é€€å‡ºä»£ç ï¼Œå› æ­¤ eval 失败会导致任务失败。 --report 会写一份 Markdown æŠ¥å‘Šï¼Œä½ å¯ä»¥å°†å ¶ä½œä¸º artifact ä¸Šä¼ æˆ–è¿½åŠ åˆ°ä»»åŠ¡æ‘˜è¦ä¸­ã€‚

每次 Ori è¿è¡Œéƒ½ä¼šå‘çœŸå®žæ¨¡åž‹å‘é€è¯·æ±‚å¹¶äº§ç”Ÿè´¹ç”¨ï¼Œå› æ­¤è¯·å°†è¿™äº› eval 排除在每次提交都会运行的任务之外。 Ori Eval 文档 涵盖了如何从由人启动的任务或按计划运行它们。

比较普通脚本与 eval 平台

上面的脚本是一个完整的 eval é—¨ç¦ã€‚å¹³å°å¢žåŠ äº†ä»ªè¡¨ç›˜ã€å¯ç»˜åˆ¶å›¾è¡¨çš„è¿è¡ŒåŽ†å²ï¼Œä»¥åŠè®©å·¥ç¨‹ä¹‹å¤–çš„äººæ— éœ€æ‰“å¼€ CI æ—¥å¿—å°±èƒ½é˜ è¯»ç»“æžœçš„æ–¹å¼ã€‚

方法

在 CI 中如何运行

锁定

最适合

普通脚本

ä½ è‡ªå·±ç¼–å†™ CI æ­¥éª¤å’Œé€€å‡ºä»£ç é€»è¾‘

æ— ï¼Œå› ä¸ºè¿™æ˜¯ä½ è‡ªå·±çš„ä»£ç 

一到两个 eval é›†ï¼Œä¸”å®Œå ¨æŽ§åˆ¶è¯„åˆ†

Ori Eval

在任务中运行

code
ori eval

,当 eval 失败时以非零退出

ä½Žï¼Œå› ä¸º eval æ–‡ä»¶ä¿ç•™åœ¨ä½ çš„ä»“åº“ä¸­ï¼Œå¹¶é’ˆå¯¹ç›®å½•ä¸­çš„ä»»ä½•æ¨¡åž‹è¿è¡Œ

å·¥å ·è°ƒç”¨ agentï¼Œæˆ–åœ¨ä½ è‡ªå·±çš„ agent 上比较模型

DeepEval

在 pytest 下运行

code
deepeval test run

ï¼Œå½“ä½ŽäºŽæ¯ä¸ªæŒ‡æ ‡çš„é˜ˆå€¼æ—¶

code
assert_test()

会抛出异常

ä½Žï¼Œå› ä¸ºè¯„åˆ†åº“æ˜¯å¼€æºçš„

çŽ°æˆçš„æŒ‡æ ‡ï¼Œå¦‚ç­”æ¡ˆç›¸å ³æ€§å’Œä»»åŠ¡å®Œæˆåº¦

Braintrust

一个已发布的 GitHub Action,运行 eval 并在 pull request 上发布摘要评论

ä¸­ç­‰ï¼Œå› ä¸ºè¯„åˆ†åŽ†å²å­˜åœ¨äºŽä»–ä»¬çš„å¹³å°ä¸­

在评审中而非 CI 日志中呈现分数变化

Arize

从 SDK 作为普通 Python 步骤运行

code
client.experiments.run()

ï¼Œå ¶æ–‡æ¡£ä¸­æœ‰ä¸€ä¸ªç¤ºä¾‹å·¥ä½œæµ

ä¸­ç­‰ï¼Œå› ä¸º experiments API 是他们的

已经使用 Arize 进行可观测性的仓库

Galileo

从 SDK 运行

code
run_experiment

,或对多轮 agent 使用

code
create_experiment

ä¸­ç­‰ï¼Œå› ä¸ºæŒ‡æ ‡å’ŒåŽ†å²æ˜¯å¹³å°åŽŸç”Ÿçš„

基于 eval 历史的托管仪表盘

ä»Žè„šæœ¬å¼€å§‹ã€‚æ— è®ºå“ªç§æ–¹å¼ï¼Œä½ çš„è¯„ä¼°é›†å’Œè¯„åˆ†é€»è¾‘éƒ½ç•™åœ¨ä½ çš„ä»“åº“é‡Œï¼Œä¹‹åŽä½ å¯ä»¥è®©å¹³å°æŒ‡å‘å®ƒä»¬ã€‚ç¦»å¼€å¹³å°æ„å‘³ç€è¦ç§»æ¤é’ˆå¯¹å ¶ SDK 编写的评分逻辑,并丢失存储在那里的运行历史。

常见失败模式

ç¬¬ä¸€ä¸ªæ˜¯æˆæœ¬ã€‚å®ƒæ˜¯ç”¨ä¾‹æ•°ä¹˜ä»¥æ ·æœ¬æ•°å†ä¹˜ä»¥ç›¸å ³æ‹‰å–è¯·æ±‚è¢«æ‰“å¼€çš„é¢‘çŽ‡ã€‚è„šæœ¬ä¼šä»Ž usage.cost 字段打印每次运行的成本,所以在提高 --samples æˆ–æ‰©å¤§é›†åˆä¹‹å‰ï¼Œå ˆè¯»é‚£ä¸€è¡Œã€‚é•¿æç¤ºè¯å’Œè¯„åˆ†æ¨¡åž‹ä¼šå¤§å¹ æ”¹å˜è¿™ä¸ªæ•°å­—ï¼Œæ‰€ä»¥è¦æµ‹é‡ä½ è‡ªå·±çš„é›†åˆã€‚å½“å‰ä»·æ ¼åœ¨ 定价页面 上。

ç¬¬äºŒä¸ªæ˜¯é€Ÿåº¦ã€‚é—¨ç¦å¢žåŠ çš„æ¯ä¸€åˆ†é’Ÿéƒ½ä¼šåŠ åˆ°æ¯ä¸ªè§¦åŠæç¤ºè¯çš„æ‹‰å–è¯·æ±‚ä¸Šã€‚å¹¶å‘è¿è¡Œæ ·æœ¬ã€‚è„šæœ¬çš„ --concurrency æ ‡å¿—é»˜è®¤æ˜¯ 8,所以示例中的九次调用分两波运行,而不是九次顺序请求,而且这种差异会随着集合规模增大而扩大。

ç¬¬ä¸‰ä¸ªæ˜¯é—¨ç¦å› é”™è¯¯åŽŸå› è€Œå¤±è´¥ã€‚æä¾›å•†è¶ æ—¶ä¸æ˜¯å›žå½’ï¼Œè¿™å°±æ˜¯ä¸ºä»€ä¹ˆè„šæœ¬åœ¨è¯„ä¼°æ— æ³•è¿è¡Œæ—¶ä»¥ 2 退出,在智能体变差时以 1 退出,并打印一条 GitHub æ³¨é‡Šè¯´æ˜Žå‘ç”Ÿäº†å“ªç§æƒ å†µã€‚

ç¬¬å››ä¸ªæ˜¯é«˜äºŽåº•çº¿çš„é˜ˆå€¼ã€‚ä½ æ²¡æœ‰æµ‹é‡è¿‡çš„é˜ˆå€¼ä¼šé˜»æ­¢æ²¡æœ‰æ”¹å˜ä»»ä½•ä¸œè¥¿çš„æ‹‰å–è¯·æ±‚ï¼Œè€Œç»•è¿‡å®ƒçš„å”¯ä¸€åŠžæ³•æ˜¯ç®¡ç†å‘˜åˆå¹¶æˆ–åœ¨æ—¶é—´åŽ‹åŠ›ä¸‹è®¾ç½®æ›´ä½Žçš„é˜ˆå€¼ã€‚æ ¹æ®ä½ åœ¨ç¬¬ 4 步测得的底线来设置阈值。

常见问题

如何将 LLM è¯„ä¼°æ·»åŠ åˆ° CI/CD 流水线?

在仓库中保留一个固定的、带版本的评估集,在 CI ä½œä¸šä¸­è¿è¡Œå®ƒï¼Œæ ¹æ®é˜ˆå€¼å¯¹è¾“å‡ºè¯„åˆ†ï¼Œå¹¶å°†è¯¥ä½œä¸šä»¥åŠä¸ºå ¶æŠŠå ³çš„è·¯å¾„è¿‡æ»¤ä½œä¸šæ ‡è®°ä¸ºåˆ†æ”¯ä¿æŠ¤ä¸­çš„å¿ éœ€çŠ¶æ€æ£€æŸ¥ã€‚ä½œä¸šçš„é€€å‡ºç å†³å®šç»“æžœï¼Œå°±åƒå¤±è´¥çš„å•å ƒæµ‹è¯•ä¸€æ ·ã€‚

ä»€ä¹ˆæ˜¯å›ºå®šè¯„ä¼°é›†ï¼Œä¸ºä»€ä¹ˆå®ƒéœ€è¦ä¸Žä»£ç ä¸€èµ·ä¿æŒç‰ˆæœ¬åŒ–ï¼Ÿ

å›ºå®šè¯„ä¼°é›†æ˜¯ä¸€ä»½å·²æ£€å ¥çš„æµ‹è¯•è¾“å ¥å’Œè¯„åˆ†æ ‡å‡†åˆ—è¡¨ï¼Œåªèƒ½é€šè¿‡ç»è¿‡å®¡æŸ¥çš„æ‹‰å–è¯·æ±‚æ¥æ›´æ”¹ã€‚å¦‚æžœå®ƒä½äºŽä»“åº“ä¹‹å¤–ï¼Œå°±ä¼šä¸Žæç¤ºè¯ä¸åŒæ­¥ï¼Œä¸å†æµ‹è¯•å®žé™ å‘å¸ƒçš„å† å®¹ã€‚

ä½ èƒ½æ ¹æ®è¯„ä¼°åˆ†æ•°é˜»æ­¢æ‹‰å–è¯·æ±‚åˆå¹¶å—ï¼Ÿ

å¯ä»¥ã€‚åªè¦è¿è¡Œå®ƒçš„ä½œä¸šæ˜¯å¿ éœ€çŠ¶æ€æ£€æŸ¥ï¼Œä¸€ä¸ªåœ¨ä½ŽäºŽé˜ˆå€¼æ—¶ä»¥éžé›¶é€€å‡ºçš„æ™®é€šè„šæœ¬å°±è¶³å¤Ÿäº†ã€‚æŠŠæç¤ºè¯å’Œè¯„ä¼°é›†æ”¾åœ¨ CODEOWNERS 规则之后,并启用“Require review from Code Ownersâ€ï¼Œè¿™æ ·å‰Šå¼±æ•èŽ·äº†å›žå½’çš„æµ‹è¯•ä¹Ÿéœ€è¦ç»è¿‡å®¡æŸ¥ã€‚

LLM-as-a-judge 与在 CI 中运行评估有什么区别?

LLM-as-a-judge 是单次运行的评分方法,由第二个模型对答案评分。在 CI ä¸­è¿è¡Œè¯„ä¼°æ˜¯å›´ç»•ä»»ä½•è¯„åˆ†æ–¹æ³•çš„æµæ°´çº¿ã€‚å®ƒå†³å®šç”¨ä¾‹ä½•æ—¶è¿è¡Œã€é’ˆå¯¹å“ªä¸ªå›ºå®šé›†åˆè¿è¡Œï¼Œä»¥åŠå½“åˆ†æ•°ä½Žæ—¶æ‹‰å–è¯·æ±‚ä¼šæ€Žæ ·ã€‚

评估需要每次提交都运行,还是只在提示词和智能体逻辑变更时运行?

åªåœ¨è§¦åŠæç¤ºè¯ã€æ™ºèƒ½ä½“é€»è¾‘ã€å·¥å ·æ¨¡å¼æˆ–è¯„ä¼°é›†çš„å˜æ›´æ—¶è¿è¡Œã€‚åœ¨ä½œä¸šçº§åˆ«è€Œä¸æ˜¯å·¥ä½œæµçº§åˆ«è¿›è¡Œè¿‡æ»¤ã€‚GitHub ä¼šå°†å› if æ¡ä»¶è€Œè·³è¿‡çš„ä½œä¸šæŠ¥å‘Šä¸ºé€šè¿‡çš„æ£€æŸ¥ï¼Œè€Œå› è·¯å¾„è¿‡æ»¤å™¨è·³è¿‡çš„å·¥ä½œæµä¼šè®©å¿ éœ€æ£€æŸ¥ä¿æŒå¾ å®šå¹¶é˜»æ­¢åˆå¹¶ã€‚

在每个 PR 上运行 LLM 评估在 token 和 CI 分钟方面要花多少钱?

æˆæœ¬ç­‰äºŽç”¨ä¾‹æ•°é‡ä¹˜ä»¥æ¯ä¸ªç”¨ä¾‹çš„æ ·æœ¬æ•°ï¼Œå†ä¹˜ä»¥ç›¸å ³æ‹‰å–è¯·æ±‚è¢«æ‰“å¼€çš„é¢‘çŽ‡ã€‚æœ¬æŒ‡å—ä¸­çš„è„šæœ¬ä¼šä»Ž API 响应中的 usage å­—æ®µæ‰“å°æ¯æ¬¡è¿è¡Œçš„æˆæœ¬ï¼Œå› æ­¤ä½ å¯ä»¥æµ‹é‡è‡ªå·±çš„é›†åˆã€‚å½“å‰æ¨¡åž‹ä»·æ ¼è§ 定价页面 。

æœ‰å“ªäº›å·¥å ·æ”¯æŒåœ¨åˆå¹¶å‰åŸºäºŽå›ºå®šè¯„ä¼°é›†å¯¹ PR 进行门禁?

ä» é ä¸€ä¸ªå¸¦é˜ˆå€¼æ£€æŸ¥çš„æ™®é€šè„šæœ¬å°±è¶³å¤Ÿäº†ã€‚Ori Eval、DeepEval、Braintrust、Arize 和 Galileo åœ¨ç›¸åŒçš„é€€å‡ºç æ¨¡å¼ä¹‹ä¸Šå¢žåŠ äº†æŠ¥å‘Šã€è¿è¡ŒåŽ†å²æˆ–é’ˆå¯¹æ™ºèƒ½ä½“çš„æ–­è¨€ã€‚

如何处理不稳定或非确定性的评估阻塞了一个好的 PR?

å ˆæ£€æŸ¥ä½ è‡ªå·±çš„æ–­è¨€ï¼Œå› ä¸ºæ¨¡åž‹æ”¹è¿°çš„å•ä¸ªå­—é¢å­—ç¬¦ä¸²æ˜¯å¸¸è§åŽŸå› ã€‚ç„¶åŽå¯¹æ¯ä¸ªç”¨ä¾‹é‡‡æ ·å¤šæ¬¡å¹¶å–å¤šæ•°åˆ¤å®šã€‚åœ¨ä¾èµ– temperature 或 seed ä¹‹å‰ï¼Œå ˆåœ¨ OpenRouter 模型端点 上检查模型的 supported_parameters ï¼Œå› ä¸ºæœªåˆ—å‡ºå®ƒä»¬çš„æ¨¡åž‹ä¼šå¿½ç•¥å®ƒä»¬ã€‚

LLM ä½œä¸ºè¯„åˆ¤è€ æ˜¯å¦è¶³å¤Ÿå¯é ï¼Œå¯ä»¥æ®æ­¤è®©æž„å»ºå¤±è´¥ï¼Ÿ

å¦‚æžœä½ è¡¡é‡è¯„åˆ¤è€ è€Œä¸æ˜¯å‡è®¾å®ƒå¯é ï¼Œé‚£å°±å¯ä»¥ã€‚è‡ªå·±ç»™ä¸€éƒ¨åˆ†è¿è¡Œç»“æžœæ‰“åˆ†ï¼Œå¹¶å°†ä½ çš„åˆ¤å®šä¸Žè¯„åˆ¤è€ çš„åˆ¤å®šè¿›è¡Œæ¯”è¾ƒï¼ŒåŒæ—¶å°†è¯„åˆ¤è€ ä¸Žæ™®é€šæ–­è¨€é å¯¹ï¼Œè¿™æ ·æž„å»ºå°±ä¸ä¼šå› ä¸€æ¬¡æœªç»éªŒè¯çš„æ¨¡åž‹è°ƒç”¨è€Œå¤±è´¥ã€‚

结论

åœ¨æœ¬æŒ‡å—ä¸­ï¼Œä½ ä¸ºä¸€ä¸ªæ”¯æŒæ™ºèƒ½ä½“ç¼–å†™äº†è¯„ä¼°é›†ï¼Œç”¨ä¸€ä¸ªåœ¨ä½ŽäºŽé˜ˆå€¼æ—¶ä»¥éžé›¶çŠ¶æ€é€€å‡ºçš„è„šæœ¬å¯¹å ¶è¯„åˆ†ï¼Œåœ¨å¤šæ¬¡é‡å¤è¿è¡Œä¸­æµ‹é‡äº†ä½ è‡ªå·±çš„ä¸‹é™ï¼Œå¹¶å°†è¯¥ä½œä¸šè®¾ä¸ºå¿ éœ€æ£€æŸ¥ã€‚å›ºå®šçš„è¯„ä¼°é›†ã€ç»è¿‡æµ‹é‡çš„é˜ˆå€¼å’Œä½œä¸šçº§è§¦å‘å™¨ï¼Œä¸ºä½ çš„æç¤ºè¯å’Œæ™ºèƒ½ä½“é€»è¾‘æä¾›äº†ä¸Žå•å ƒæµ‹è¯•ä¸ºä»£ç æä¾›çš„ç›¸åŒä¿æŠ¤ã€‚è¦å°†é—¨ç¦æ‰©å±•åˆ°è°ƒç”¨å·¥å ·çš„æ™ºèƒ½ä½“ï¼Œè¯·ä»Ž Ori Eval 文档 开始。

参考资料

  • Ori Eval
  • 提供商选择
  • 模型与 supported_parameters
  • ç”¨é‡æ ¸ç®—
  • Claude Sonnet 5 迁移指南
  • 揭开 AI 智能体评估的神秘面纱
  • GitHub Actions 的工作流语法
  • 跳过工作流运行
  • 使用条件控制作业执行
  • 在 GitHub Actions 中使用密钥
  • å ³äºŽå—ä¿æŠ¤åˆ†æ”¯
  • å ³äºŽä»£ç æ‰€æœ‰è€
  • dorny/paths-filter

来源: OpenRouter:Announcements · openrouter.ai

#

教程/实践

部署/工程

Agent

ç¼–ç 