- Accueil
- Plugins
- Développement et outils de plugins
- dsh-eval-harness
dsh-eval-harness
biboyang/dsh-eval-harness
Harness d'évaluation pour les plugins DSH : des cas au format YAML pilotent de vraies exécutions d'agent en mode headless, avec des assertions sur les appels d'outils, les arguments, les résultats et l'utilisation des tokens, ainsi qu'une barrière de référence pour la non-régression en CI.
Installer
dsh plugin --profile web add github:biboyang/dsh-eval-harnessREADME
dsh-eval-harness
DSH 插件/skill 作者的回归评测门禁:写 yaml 用例 → headless 驱动真实 agent 跑 → 解析 session trace 断言 → 对比 baseline 出 PASS/WARN/FAIL 报告与 CI 退出码。
简介
给 DSH 插件/skill 的回归评测流程提供一个可进 CI 的门禁工具:
- 用 yaml 写评测用例(prompt + 期望行为断言);
eval_run逐条 forkdsh --profile headless --patch <overlay> <prompt>子进程跑真实 agent(overlay 把会话落盘切到隔离目录,每条用例独立 workspace),解析落盘的session.jsonl/session.jsonl.zstdtrace(多帧 zstd 直读),执行断言,写report.json+report.md;eval_gate把本次报告与 baseline 报告对比,输出OVERALL=PASS|WARN|FAIL|N/A与退出码,供 CI 拦截回归。
安装
已发布到 npm(dsh-eval-harness):
dsh plugin --profile headless add dsh-eval-harness
# 或从 GitHub 源码安装:
# dsh plugin --profile headless add github:boyang/dsh-eval-harness
# 验证挂载
dsh --profile headless --dump-config | grep dsh-eval-harness
能力面
Tools
| 工具 | 说明 |
|---|---|
eval_run | 跑 cases_dir 下全部用例:headless 驱动真实 agent → 采集 session trace → 断言 → 写 report.json/report.md |
eval_gate | 对比 baseline 与本次报告,输出门禁判定(OVERALL/EXIT_CODE),strict 模式收紧 WARN 退出码 |
Skills
| Skill | 作用 |
|---|---|
eval | 教模型帮用户编写评测用例(用例格式、断言编写要点、解析子集约束) |
用例格式(cases/*.yml)
一个文件一条用例:
name: 用例名 # 唯一,gate 按 name 对比 baseline
prompt: "发给 agent 的内容" # 多行可用块标量 `|`
require_plugins: [some-plugin] # 可选,元信息
tags: [fast] # 可选,标签;eval_run 的 tags 筛选按任一命中匹配
retries: 1 # 可选,失败重跑次数(非负整数,缺省用 eval_run 的全局 retries)
assert:
turn_end: completed # turn/end 事件的 reason.kind
tools_called: [tool_a] # tool/call 名称序列须按序包含(保序子序列)
output_contains: ["关键词"] # 最终 assistant 文本须包含全部
max_steps: 8 # 可选,step/end 数上限
max_tokens: 50000 # 可选,token 上限(input+output+reasoning;cacheRead/cacheWrite 不计入,防多步膨胀)
no_tool_errors: true # 可选,任何 tool/result 硬错误(data.error / isError)即 fail
tools_exact: [tool_a] # 可选,工具调用名称序列须完全一致(长度+顺序+内容)
tools_not_called: [tool_b] # 可选,列出的工具一次都不能被调用
output_not_contains: ["抱歉"] # 可选,最终 assistant 文本不得包含任一子串
output_matches: ["^okay"] # 可选,最终 assistant 文本须匹配全部正则(解析期预编译校验)
tool_args_contains: # 可选,指定工具至少一次调用的参数 JSON 串包含子串
- name: tool_a
contains: '"path"'
tool_result_contains: # 可选,指定工具至少一次结果的文本包含子串
- name: tool_a
contains: total
output_judge: # 可选,LLM 语义评审(结构断言全过后才调,判 FAIL 记 fail)
rubric: "回答应解释原因而非只给结论"
LLM-as-judge(output_judge):表达「解释原因而非只给结论」这类写不出正则的语义
期望。定位是结构断言优先、judge 兜语义——一个 attempt 只有结构性断言全过后才会调
judge(结构已失败不白烧 judge token);judge 判 FAIL 时理由进该用例 failures
(形如 output_judge: <理由>),判 PASS 不留任何痕迹。judge 走 OpenAI 兼容 chat
completions 接口(零依赖,Node 内置 fetch),配置全靠环境变量:EVAL_JUDGE_API_KEY
(缺省回落 DEEPSEEK_API_KEY,两者都无时报错)、EVAL_JUDGE_BASE_URL(默认
https://api.deepseek.com)、EVAL_JUDGE_MODEL(默认 deepseek-chat)。judge 调用
本身失败(HTTP 错误/超时/回复解析失败/无 key)按 error 处理而非 fail——infra 抖动
不是断言失败,可被 retries 覆盖。
报告里的 token 是分字段聚合:total (in X+out Y+reas Z; cacheR A+cacheW B)——prompt cache
命中时 inputTokens 只剩零头、真实输入在 cacheReadTokens,分字段展示让 cache
命中情况一眼可见。max_tokens 对 total(input+output+reasoning)生效:cacheRead 是
多步会话里同一段缓存的重复读回,计入会让上限随步数膨胀,故只展示、不计入。
示例见 cases/example.case.yml。
cases/real/ 收录了 11 条针对真实插件(bash/fs/search/todo/web_search/subagent/workflow 等)的实测用例,全部在真实 agent 回合中验证过;
其中 08-read-image.yml 演示 no_tool_errors 如何拦下「工具报错但 agent 兜底答对」的假通过(在无视觉能力的模型上该用例预期 fail,属正常)。
解析约束:harness 内置零依赖 YAML 子集解析器(块级 map、- 标量/map 序列、
flow 序列、引号、数字/布尔/null、|/> 块标量、注释)。不支持锚点、多文档;
解析失败报带行号的 eval_run: 前缀错误。
工具参数
eval_run
| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
cases_dir | string | 是 | - | 用例目录(.yml/.yaml) |
output_dir | string | 是 | - | report.json / report.md 输出目录 |
session_root | string | 否 | <output_dir>/.sessions | 隔离的 session 落盘根 |
profile | string | 否 | headless | dsh profile |
timeout_ms | integer | 否 | 600000 | 单条用例子进程超时 |
dsh_bin | string | 否 | $DSH_BIN 或 dsh | dsh 可执行命令,按空白拆分;本机无全局 dsh 时用 npx -y @deepseek-ai/dsh |
concurrency | integer | 否 | 1 | 并行跑用例的并发数;每条用例独占 session 根与 workspace,并行互不干扰 |
retries | integer | 否 | 0 | 失败重跑的全局默认次数;单条用例最多跑 retries+1 次,任一 attempt 全过即停(fail 和 error 含超时都触发重跑);用例 yaml 的 retries 优先于此值 |
tags | string | 否 | - | 逗号分隔标签筛选:只跑 yaml tags 命中任一的用例 |
only | string | 否 | - | 逗号分隔用例名(精确匹配);与 tags 同给时取交集;筛选后无命中会直接报错(防 CI 笔误空跑假绿) |
输出:JSON 文本(summary + 报告路径 + 各用例状态)。错误一律 throw
eval_run: 前缀消息(找不到 dsh 可执行文件、用例解析失败等)。
eval_gate
| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
before | string | 否 | - | baseline report.json 路径;缺省或文件不存在 → N/A |
after | string | 是 | - | 本次 report.json 路径 |
strict | boolean | 否 | false | strict 模式下 WARN 退出码为 2 |
gate_json | boolean | 否 | false | true 时输出单条 JSON(供 CI 解析),否则 key=value 文本 |
max_token_increase_pct | integer | 否 | 50 | token total(与 max_tokens 同口径)涨幅阈值百分比:状态不变的用例超阈值记 token 回归(WARN);0 关闭 |
gate 协议
判定规则(优先级从高到低):
| 条件 | 判定 | 退出码 |
|---|---|---|
| 有用例 PASS → FAIL/error,或新增用例即 FAIL/error | FAIL | 1 |
| 有用例 FAIL/error → PASS,或用例数量变化(新增通过/移除) | WARN | 0(strict 为 2) |
状态不变但 token total 涨幅超阈值(默认 +50%,max_token_increase_pct 可调,0 关闭) | WARN | 0(strict 为 2) |
| 全部与 baseline 一致 | PASS | 0 |
| 无 baseline | N/A | 2 |
文本输出(key=value 行 + 明细行):
OVERALL=FAIL
EXIT_CODE=1
STRICT=false
REGRESSIONS=1
NEW_FAILURES=0
IMPROVEMENTS=0
ADDED=0
REMOVED=0
TOKEN_REGRESSIONS=0
REASON regression: echo-hello pass -> fail
REGRESSION echo-hello: pass -> fail
gate_json=true 时输出单条 JSON(含 verdict/exitCode/reasons/regressions 等字段)。
CI 集成
真实 workflow 见 .github/workflows/eval.yml:pnpm build && pnpm test
后直调 lib/runner.js 的 runEval 跑 cases/real/ 全量(真实 LLM,需仓库 secret
DEEPSEEK_API_KEY),再用 lib/gate.js 的 computeGate 对比 baseline/report.json,
按 EXIT_CODE 拦截;report 作为 artifact 留存。用例或 harness 代码变更会触发重跑,
另有每日定时跑(近 24h 无新 commit 则跳过)。
baseline 更新走 .github/workflows/update-baseline.yml:
Actions 页手动触发 → 全量重跑 → 覆盖 baseline/report.json 并开 PR(附报告摘要),
人工复核后合并,不自动合入。
baseline/report.json 已入库(首轮全量评测人工复核:read-image 在无视觉能力模型上
预期 fail,见上)。用例/断言口径变更时须重跑全量、人工复核后更新 baseline,否则 gate
会把口径变化判成 WARN/FAIL。
session trace 说明
评测依赖 DSH 落盘的会话 trace(默认 $DSH_HOME/sessions/<cwd编码>/<session-id>/session.jsonl[.zstd],
每行一帧信封 { type, seq, time, data })。eval_run 不污染环境变量,而是为每条用例生成一个 --patch overlay
(<output_dir>/eval-overlay-<序号>-<用例名>.patch.yml),按 row id 整体替换 base bundle 的
session-persistence-jsonl 配置:把 root 切到该用例的隔离目录
(<session_root>/<序号>-<用例名>,session_root 默认 <output_dir>/.sessions;序号是加载序,
因为 slug 化不是唯一键,如 read image 与 read-image 同 slug);每条用例再以
独立 workspace 作 cwd。per-case session 根 + workspace 让用例可以并行跑(concurrency),
互不干扰;subagent/workflow 的子会话也落在同一用例的根下。用例名重名会直接报错
(gate 按 name 对比 baseline)。
子进程命令形如 dsh --profile headless --patch <overlay> <prompt>(launcher flags 在前,
prompt 是 app 位置参数放最后)。
collector 按文件头魔数自动识别编码:默认的多帧 zstd(session.jsonl.zstd)走
decodeZstdLog 直读(结构扫描帧边界 + 逐帧解压,零外部依赖,仅 Node 内置 node:zlib),
纯文本 session.jsonl 走 UTF-8。两种编码都能读,eval_run 不再依赖 overlay 强制
compression: none。真实落盘帧的契约快照见 tests/fixtures/ 与 tests/zstd.spec.ts。
会话发现(findSessionFile):subagent/workflow 用例会在同一 root 额外落下
delegationDepth > 0 的子会话日志;多候选时按 header 行的 delegationDepth 分档,
父会话(0)优先于不可解析、再优先于子会话(>0),同档取最新 mtime。
超时兜底:用例子进程超时(SIGKILL)时不再只记 error,而是尽力采集已落盘的部分
trace(残缺尾帧由 decodeZstdLog 恢复)写进 report,供排查超时原因;采集失败
不掩盖超时本身。
开发命令
pnpm install # 安装 devDependencies(typescript / vitest / biome / @types/node)
pnpm build # tsc → lib/(含类型声明 lib/types/)
pnpm test # vitest run tests
pnpm lint # biome check(仅 lint,format 未启用)
插件管理
已装插件用 plugin-registry 的薄控制台管理(浏览器面板):管理 profile
插件安装态(bundle 层栈 + insert 行 + 启停),无需手改配置。安装:
dsh plugin --profile web add <plugin-registry>/packages/plugin/console
Plugins associés
mirage (dsh)
strukto-ai/mirage
dsh-market
dsh-market/dsh-market
oh-dsh
hust-open-atom-club/oh-dsh
DSH-Plugins-Marketplace
bradegithub/dsh-plugins-marketplace