关于 llm-eval-harness
用于测量模型端点可用性、请求保真度、速度、并发稳定性、协议兼容性和用例质量的评估技能,适合接入新模型或排查网关表现。通过环境变量名传递 API 密钥,避免把密钥写入命令参数或报告。
主要功能
- 01测量模型 ID 可用性
- 02检查提示词、工具与多轮请求传递
- 03评估首 token 延迟和持续速度
- 04测试并发稳定性与协议兼容性
- 05运行用例回归并读取结构化结果
适用场景
- 01接入新模型或网关前验证端点
- 02排查系统提示词或工具调用未生效
- 03比较模型速度并测试并发上限
- 04检查 Anthropic thinking 与历史重放兼容性
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ daymade/ claude-code-skills --skill llm-eval-harness安装后在 Claude Code 中输入/llm-eval-harness确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
assets/example_usecases.jsonreferences/evaluation_disciplines.mdmachine-results.mdproduction_testing_patterns.mdquality_blind_judge.mdvendor_evidence_protocol.mdscripts/availability_probe.pyconcurrency_probe.pyeval_result.pyfidelity_probe.pyprotocol_probe.pyspeed_probe.pyusecase_runner.pytests/test_results.py详细介绍
LLM 评估框架
概述
为此技能提供一个端点(base_url + model + 通过环境变量提供的 API 密钥),它就会测量该端点是否确实可用,以及模型是否足够快速、稳定、符合协议且满足要求——而不是盲目相信厂商醒目的宣传数字。六个维度通常散落在各种临时脚本中,而这些脚本每次都会被重写(也会重复出现相同的 bug):
| 维度 | 脚本 | 可回答的问题 |
|---|---|---|
| 可用性 | scripts/availability_probe.py | 哪些模型 ID 在这里可用,并对错误进行三态分类 |
| 请求保真度 | scripts/fidelity_probe.py | system 提示词 / 工具 / 历史记录是否确实传达给了模型? |
| 速度 | scripts/speed_probe.py | TTFT + 持续解码 tok/s,感知思考过程 |
| 并发 / 稳定性 | scripts/concurrency_probe.py | 成功率、p50/p90 延迟,以及在哪些情况下会出问题 |
| 协议合规性 | scripts/protocol_probe.py | 请求时 Anthropic thinking 块是否确实触发,以及端点是否接受后续轮次历史记录中已有的 thinking 块(N≥10,两项检查彼此独立——见维度 3)? |
| 质量 / 用例回归 | scripts/usecase_runner.py + 盲测评审 | 是否通过了你积累的用例? |
应运行哪些维度
根据用户实际要做的事选择:
- “接入新模型/新网关先测一下” / 接入服务商 → 可用性 → 保真度, 如果看起来可能采用,再测速度/并发
- “这个 ID 能不能用 / 写个支持列表” → 可用性(并在判定“不可用”之前阅读 纪律说明 §9)
- “模型不听 system prompt / agent 行为怪但没报错” → 保真度(system canary)
- “快不快 / tok/s 是真的吗” → 速度;“扛不扛得住” → 并发
- “这个兼容端点是真兼容吗” → 协议 + 保真度中的工具/身份验证检查
- “这个模型/渠道用着用着就 400 了 / session 断了 / continue 不下去了”(扩展思考客户端、
多轮 session)→ 协议的
--check history-replay,具体来说——这种症状意味着生成检查通过了,但历史记录重放却悄无声息地失败;不要只检查生成 - “换模型质量会不会掉” → 质量回归
- “要给厂商报 bug” → 使用
--output运行相关探测,然后参阅 references/vendor_evidence_protocol.md - “部署闸门 / 常驻监控 / 告警老误报” → references/production_testing_patterns.md
不要例行执行全部六项——只选择能回答用户问题的项目。
密钥处理(不可妥协): 每个脚本都通过环境变量名称接收 API 密钥
(--key-env MY_KEY),绝不在命令行中传入密钥值——这样密钥就不会出现在 ps、
shell 历史记录或任何已保存的报告中。绝不要将密钥硬编码到用例文件或包装脚本中。阅读
references/evaluation_disciplines.md
,了解这一要求及其他纪律背后的完整理由。
你的私有数据应保存在此资源包之外。 用例库、模型列表和密钥应放在 ~/.llm-eval/
(或你存放机密信息的位置),而不是此技能目录中——此技能通用且公开;你的测试套件属于你自己。参阅下文的“用例库”。
机器启动器与中断的运行: 使用配套的 --result-json / --run-id
接口,以及捆绑的 scripts/eval_result.py 读取器,以区分已完成、不合规、部分完成、未测量、失败和已取消的运行。集成启动器前请阅读
机器结果;该文档说明了回执时效性、类型化证据、退出代码、身份验证转发以及离线回归命令。保留旧版 --output 和质量答案产物。
快速开始
检测你拥有的内容,然后运行适用的维度。对于兼容 OpenAI 的模型:
export MY_KEY=sk-... # the key never appears in a command below
# Speed: real-task throughput + sustained decode ceiling
uv run --with openai python scripts/speed_probe.py \
--base-url https://api.example.com/v1 --model some-model --key-env MY_KEY --mode both
# Concurrency: ramp until it breaks
uv run --with aiohttp python scripts/concurrency_probe.py \
--url https://api.example.com/v1/chat/completions --model some-model --key-env MY_KEY \
--format openai --concurrency 10 20 40 60
如果端点采用 Anthropic-Messages 格式(/v1/messages),还要运行协议探测(见下文)。
根据用户实际提出的问题选择维度——如果用户只问“速度快吗?”,就不要运行全部六项。
当 Anthropic 协议/并发探测所用的路由要求使用 Authorization 而非 x-api-key 时,传入 --auth bearer。默认仍为 x-api-key,并同时适用于协议生成和历史记录重放。OpenAI 并发探测仍使用 Bearer 身份验证。
维度 0 — 可用性(哪些模型 ID 实际可用)
uv run --with aiohttp python scripts/availability_probe.py \
--base-url <base> --key-env <ENV> --format both \
--models model-a vendor/model-b @models.txt --output /tmp/avail.json
- 对每种故障,结论分为 3 种状态,而非通过/失败:
no-channel(没有此 ID 对应的路由)、upstream-error(路由存在,但上游故障——稍后重试)、empty-content(通常是推理模型的 max_tokens 问题,并不代表模型已损坏)。探测默认将--max-tokens设为 8192,正是为了避免思考模型被误判为不可用——不要将其调低来“优化”。 - 在编写任何“不可用”列表之前:确认每个故障 ID 都存在于官方文档中。当前一代模型名称可能晚于你的训练数据——请通过 WebSearch 查找供应商/模型开发者的模型页面,而不是凭猜测列举;后缀(
-preview、日期变体、层级名称)决定路由是否可用。完整陷阱见 disciplines §9。 - 切勿将客户端标记当作真实模型 ID 来探测。 带方括号后缀的字符串(例如
some-model[1m])通常是 CLI 客户端自己的本地约定(Claude Code 会在构建请求前解析并移除[1m]——完整机制见 claude-switch-models-setup 的“配置上下文窗口大小”一节),它们永远不会出现在网络请求中。探测发现此类字符串时会发出警告,但应由你自行遵守这一原则:探测供应商文档中记录的原始模型 ID,而不是从 Claude Code 的ANTHROPIC_MODEL环境变量中复制的字符串。 - 网关的
/v1/models列表只是一个输入,绝非最终结论——真实网关可能会路由列表中未列出的模型。
维度 0.5 — 请求保真度(你的负载是否到达模型?)
uv run --with aiohttp python scripts/fidelity_probe.py \
--base-url <base> --model <model> --key-env <ENV> \
--format anthropic --check all --repeat 10 --output /tmp/fidelity.json
- 四项检查:
system(在系统提示词中植入金丝雀代码——模型能否复述?)、tools(完成完整往返,包括返回工具结果并确认最终回答使用了该结果)、multiturn(在多个轮次中植入事实,并确认模型能否全部回忆)、auth(在两个协议端点上分别测试 x-api-key 与 Bearer——网关通常会在一个路径上同时接受两者,而在另一个路径上只接受其中一种;错误的请求头导致的 401 与密钥失效的表现完全相同)。 - 这项检查能发现最棘手的网关故障:所有请求都返回 200,聊天也流畅,但系统提示词从未到达模型——因此,任何规则写在系统提示词中的工具都会悄无声息地出错,且不会出现任何错误。在一个真实网关上,某个模型别名在 100 多个样本中,无论通过哪种合法方式发送,都从未传递系统提示词;而它的其他同类别名则间歇性传递,且传递率每小时都在变化。
- 经由路由网关传递具有概率性且随时间变化的特点——因此需要使用
--repeat(默认值为 10)和三态结论(已传递/间歇性传递 k-of-N/未传递)。在一个时间窗口内得到的干净结果也只代表该时间窗口:发布数值前,应在另一个时间段重新采样(见 disciplines §12)。 - 探测会报告用于佐证的 token 计量数据,但绝不会单独信任这些数据——以行为证据为准;请参见 disciplines §10(校准计量器)和 §11(为什么金丝雀是中性的外部事实,而非身份或服从性测试)。
维度 1 — 速度(考虑思考过程)
uv run --with openai python scripts/speed_probe.py \
--base-url <…/v1> --model <model> --key-env <ENV> --mode both --output /tmp/speed.json
mixed运行有代表性的任务(反映真实使用体验);decode强制生成一段长输出,以找出持续速度上限(用于对比供应商宣传数据的数值);both则两者都运行。- **此脚本旨在避免的陷阱:**推理模型会在单独的
reasoning_content字段中流式输出思考过程,但completion_tokens会将其计入。若只收集content,却用completion_tokens作除数,得出的数字会被大幅夸大——某个实际速度约为 750 tok/s 的模型,曾以这种方式测出 4700 tok/s。脚本会同时捕获两者,将 TTFT 设为任一类型的首个 token,并报告completion_tokens / (total − TTFT)。 - **正确解读输出:**真实任务的吞吐量会低于解码速度上限,因为短输出无法达到稳定状态——这是预期现象,不是故障。请报告两个数值,并注明模型是否会输出思考过程(其端到端延迟包含推理时间,而不只是打字时间)。
维度 2 — 并发 / 稳定性
uv run --with aiohttp python scripts/concurrency_probe.py \
--url <full endpoint URL> --model <model> --key-env <ENV> \
--format openai|anthropic --concurrency 10 20 40 60 --output /tmp/conc.json
- 传入多个
--concurrency值逐步提升并发量,找出上限——即成功率下降或延迟激增的并发等级。单线程速度很快的模型,在中等并发量下仍可能崩溃(真实案例:一家供应商在 50 个并发请求下保持 0.4s,另一家却在仅 5 个并发请求时就开始丢弃请求)。 - 脚本会与任何环境代理隔离(
trust_env=False),并禁用 keep-alive 连接池(force_close),否则测到的会是代理的限制,或某个固定的上游副本,而不是模型。脚本会输出“并发证明”(重叠的请求对),供你确认请求确实并行运行。 - 根据输出区分故障类型:HTTP 429(正常限流,可重试)、导致请求一直挂起直到超时的 TCP 断连(对用户体验的影响严重得多),以及 5xx。它们所需的修复方式截然不同。
维度 3 — 协议合规性(Anthropic thinking 块:生成和历史重放)
uv run python scripts/protocol_probe.py \
--url <…/v1/messages> --model <model> --key-env <ENV> --check all --repeat 10 --output /tmp/proto.json
- 仅适用于声称兼容 Anthropic
/v1/messages的端点。--check all(默认值)会运行两个子检查,它们测试不同的代码路径;服务提供商可能通过其中一项,却在另一项上彻底失败:- generation:请求时设置
thinking: {type: enabled},是否真的会生成thinking_delta/signature_deltaSSE 事件?(原始检查) - history-replay:在后续轮次重放历史记录时,端点是否接受已存在于先前助手轮次中的
type: "thinking"块——这正是 Claude Code 和所有其他 Agent 客户端在每次继续对话时都会执行的操作? (运行--check history-replay可单独执行此项检查)
- generation:请求时设置
- 促使我们加入历史重放检查的真实事件(2026-07-21):通过中国转售商接入的 Kimi/Moonshot 模型顺利通过了生成检查(按要求正确输出 thinking),却彻底拒绝历史重放——一旦先前的 thinking 块作为输入再次传回,就报错 400「invalid part type: thinking」,导致会话在后续每一轮都中断。 生成检查通过对此毫无说明;这是彼此正交的故障模式(响应生成与请求验证)。
- 不要仅凭一次跨维度比较就断定“服务提供商/转售商 X 有问题”——也不要因为某次单维度比较证实了服务提供商文档中记录的参数,就此打住。
我们花了三轮才弄清楚这一点,并将其作为典型警示记录在纪律条款第 17–19 节:第一轮同时比较了不同模型和不同转售商,因而(错误地)归咎于转售商。第二轮修正了这一点——转售商相同,仅改变模型——结果与一个文档中明确命名的服务提供商参数(Moonshot 自己的
preserve_thinking)吻合,看起来像是得到了证实。但第二轮仍然是错的:探测始终只经过那一家转售商,因此看不到服务提供商自己的直连/原生端点其实可以正常处理那个“被拒绝”的模型——真实生产流量证实了这一点。将任何经单一转售商验证的结果视为定论之前,请先阅读第 19 节;在将“X 不支持 thinking”之类的结论写入任何内容之前,请检查服务提供商自己的端点或相关渠道的真实流量。 - 这两项检查的合规性往往都是概率性的,而非二元的。 某个真实服务提供商仅在约 13% 的生成请求中保留了 thinking 块(而两家竞争对手的比例为 100%)。因此
--repeat默认值为 10;生成检查的结论有三种状态(fully-implemented、intermittent (k/N)、not-implemented),历史重放检查则有三种以上状态(accepts-thinking-in-history、rejects-thinking-in-history、inconsistent,或者当错误看起来与 thinking 完全无关时为inconclusive)。切勿根据单个样本下结论。 - 每个请求都会强制设置
Connection: close,以防负载均衡器将所有样本固定分配给同一个副本,从而掩盖真实分布(一次真实探测在同一端点上使用 keep-alive 时为 0/10,使用 close 时为 17/90)。
维度 4 — 质量/用例回归(盲评)
这里特意分为两个阶段:收集,然后独立评判。
第 1 步 — 收集模型对你的用例库的回答:
uv run --with openai python scripts/usecase_runner.py \
--base-url <…/v1> --model <model> --key-env <ENV> \
--usecases ~/.llm-eval/usecases.json --output-dir ~/.llm-eval/runs/<model>
第 2 步 — 由独立盲评评审进行评判(直接编排——不要让模型给自己打分)。 对运行目录中的每个回答,启动 3 个独立的 Task Agent(快速检查时可以更少)。每位评审只能获得:提示词、回答和该案例的 rubric,并须明确告知其独立评判,不得了解其他评审的评分或任何先前评估(这可以避免锚定效应)。然后汇总:
- 只有在评审多数意见一致时,该案例才算通过。
- 按类别计算精确率(使用每个案例的
tags):如果评审对某个类别与评分标准系统性地意见不一,这就是一个真实弱点——在一次实际评估中,某个类别的精确率仅为 12.5%,暴露了单个评分者可能漏掉的系统性误分类。 - 只统计明确的判断。 未给出结论的评审不算通过——沉默 ≠ 同意。这可以防止自动化偏差。
关于评分标准的评分机制(LLM-as-judge 阈值、llm-rubric),你也可以结合使用 promptfoo-evaluation 技能——将其 providers 指向同一端点。此工具包的盲评方法与 promptfoo 的评分标准断言相辅相成:使用 promptfoo 快速进行逐案例的通过/失败判定,使用盲评评审检查你怀疑较弱的类别的精确率。完整方法:references/quality_blind_judge.md。
用例库
将它保存在此工具包之外(例如 ~/.llm-eval/usecases.json),这样它就能在技能更新后保留,也不会进入公共仓库。它是一个普通的 JSON 列表——将其置于私有仓库中进行版本管理,逐步积累回归测试集:
[
{"id": "refund-window", "prompt": "A customer asks for a refund 20 days after purchase. Reply as support.",
"rubric": "1.0 if it correctly cites the 30-day refund window; 0.0 if it refuses or invents a different window.",
"tags": ["support", "policy"]},
{"id": "lru-cache", "prompt": "Implement an LRU cache in Python with O(1) get/put.",
"rubric": "1.0 if get and put are both O(1) via dict + doubly linked list and the self-test passes.",
"tags": ["code"]}
]
assets/example_usecases.json 是一个可供复制的入门示例。只需 id 和 prompt;添加 rubric、expected 和 tags 可以让评判更加准确。
运行完整评估
当用户说“评估 / 基准测试这个模型”时,典型流程如下:
- 确定接口类型——是 OpenAI 兼容的(
/v1/chat/completions),还是 Anthropic-Messages (/v1/messages)?调用GET /v1/models或查看供应商文档;不要想当然。这决定了 适用哪些探测(协议探测仅适用于 Anthropic)。请记住,无论哪种情况,模型列表都不是完整的 证据(规范 §9)。 - 对于新端点,先检查可用性和保真度——如果模型始终收不到系统提示词,那么它的速度 数据回答的就是错误的问题。
- 运行用户关心的维度——如果用户想知道“是否快速 / 稳定”,就测试速度和并发;如果是
Anthropic 供应商,则增加协议测试;如果用户有用例套件,则增加质量测试。将每个探测的
--outputJSON 写入运行目录。 - 如实报告,并区分实测结果与推断。 开头先说明用户询问的重点(例如,“持续解码上限 超过供应商声称的 tok/s,但实际任务的吞吐量较低”)。 如果某个数字看起来不可能(例如,吞吐量远高于供应商声称的水平,或仅凭单个样本就给出 协议结论),应将其视为有待调查的测量伪差,而不是结果——这正是这套测试工具的核心 用途。路由网关上的速率还会随时间变化:在将任何数字写入文档之前,请换个时间段重新 采样(规范 §12)。
- 要比较两个模型? 使用相同的标志,对每个模型运行完全相同的探测,并将两个 JSON 输出并列比较。保持测试条件一致(并发级别、用例都相同),否则比较就毫无意义。
- 发现值得报告的供应商缺陷? 不要将原始探测输出粘贴到供应商的支持渠道——请按照 references/vendor_evidence_protocol.md 准备证据材料包(先进行自审、使用描述观察结果的措辞、附上请求 ID 表、预先登记阈值、 进行对抗性复核)。
如需针对在线系统反复运行的测试——部署门禁、常驻金丝雀、故障注入模拟,以及那些会撒谎的 监控统计数据,请参阅 references/production_testing_patterns.md。
下一步
运行后,提供顺理成章的后续选项:
Evaluation complete for <model>.
Options:
A) Render an HTML dashboard of the results — compose with a visualization skill (Recommended if sharing)
B) Compare against another model — same probes, side-by-side
C) Add the failing cases to ~/.llm-eval/usecases.json as a permanent regression guard
D) Done — the numbers answer the question