2 minute read

系列 · 我的评测工具链(持续更新)

  1. 从 trace 到 eval:trace 设计、agent eval 方法论、一个 eval harness 的实现,和它抓到的上游并发 bug
  2. 给 skill 建门禁:一天里的三种沉默失败、一次红线写法实证,和它抓到的上游 bug(又一只)(本文)
  3. 我给 skill-up 报了一个不存在的 bug
  4. CI 红、本地绿:一次「平台差异」误诊,和 merge 干净不等于语义兼容

上个月我写过一篇《从 trace 到 eval》,主线是「测 agent」:trace 是一等公民,eval 要防假通过和假失败,门禁不是判官是反馈回路。文章里的 harness 测的是一个插件系统(dsh)的行为回归。

这个月战场换了一层:测 skill

skill——就是那份 SKILL.md,一段写在仓库里、agent 按 description 自主决定要不要加载的提示词包——本质上是没有编译器的代码。它改一个字,行为就可能漂移;它声明的红线(「不管从零写作」「不许擅自发 PR」),写进去不等于模型会遵守。我维护着九个自用 skill(huohou),朋友 tw93 维护着八个(Waza)。两者都处在「写完跑两下,感觉没问题,发布」的裸奔状态。

然后阿里开源了 skill-up:把软件测试那套方法论——声明式用例、with/without 对照、rule/script/LLM judge 三层评分、结构化报告——完整平移到了 skill 上。我拿它给两套 skill 建了评测,一天之内跑完十七个 skill 的首轮,顺手给两个上游各发了 PR。

这篇文章是按时间顺序的事故报告。上一篇的方法论每一条都在新战场复用了,但每一层都长出了新形态——包括三种我之前没编目的沉默失败。

一、沉默失败第一种:干预根本没注入

上一篇我给「假通过」编过三种形态:agent 兜底答对、工具报错被粉饰、eval 系统自身盲区。今天的第四种,比这三种都靠前——你想测的干预,压根没进到被测系统里

skill-up 的 benchmark 模式会把每个用例跑两遍:with_skill(skill 装进工作区)和 without_skill(不装,作对照)。我给我的 huohou-polish 跑完首轮,6 通过 4 失败,报告漂亮,jitters 都像真的。直到我翻被测 agent 的会话落盘(kimi 的 wire.jsonl),发现一个不对劲的事实:所有 with_skill 的会话里,「huohou-polish」这个词出现的次数是零——不只是没被触发,是连 skill 列表里都没有它。

排查链一路追到 skill-up 的 ListSkillFiles:它用 filepath.Walk 遍历 skill 源目录。而我的九个 skill 全部是符号链接(~/.agents/skills/huohou-polish 指向真实仓库)。Go 的 filepath.Walk 不跟随符号链接根——根节点是链接本身,遍历即结束,选中文件数为零。安装逻辑把这个零当成正常,一个文件没上传,debug 日志还照常打印 skill installed: huohou-polish

也就是说:前面三轮全量评测,with_skill 和 without_skill 跑的都是裸模型。报告里的 PASS 和 FAIL 全是 noise 装扮成的 signal。

这件事最刺痛的地方在于它完全符合上一篇的框架,却又完全出乎我的意料:断言全部通过、工具全部正常、报告全部生成——每一层都在忠实地工作,除了最底层那个「skill 真的装进去了吗」。上一篇我说「断言可能跑在残缺数据上」,今天我补一个更靠前的版本:断言可能跑在一个从未发生的实验上

防御方式也由此成型,我现在把它当作评测基建的第一条纪律:全量跑之前,先验证被测对象真的在场。不是看退出码,是看直接证据——工作区里 SKILL.md 的落盘文件、会话落盘里 skill 被加载的日志行。PASS 只证明流程跑完了,不证明实验发生了。

(这个 bug 的后续:报了 issue #253,有 contributor 秒认领,我作为报告者用现成的复现环境把修复写了——EvalSymlinks 解析 + 零文件安装直接报错,附三个回归测试,PR #257。后一个改动是我坚持要加的:symlink 只是零文件的一种成因,「选中了零个文件却照常评估」这个沉默本身才是该被判死的东西。)

二、对照组的纯度:两种污染

修好注入问题之后,with/without 对照还有第二层的坑:对照组不干净

第一种污染来自 skill 自动发现。kimi CLI 默认会加载用户级 skills 目录,于是 without_skill 变体照样把九个 huohou skill 全装进了上下文——「无 skill 对照组」名存实亡。修法是 wrapper 里把 --skills-dir 钉死在用例工作区内的目录:with_skill 指向装好的那份,without_skill 指向一个空目录。对照组的「无」必须是被强制出来的,不能是默认值的恩赐。

第二种污染更隐蔽,来自我自己。我的 kimi 配了一个 UserPromptSubmit hook,每轮自动往上下文里注一段「回复末尾附当前时间戳」的指令。评测跑到 agent_judge 时开始报解析失败:JSON code fence is not closedinvalid character 'â'——那个 â 是 ⏱ 的 UTF-8 字节被误读后的样子。我的 hook 指令不仅污染了被测 agent 的输入,它产生的输出尾巴还直接打碎了 judge 的 JSON 契约。

修法是环境隔离:wrapper 给被测 kimi 进程指一个隔离的 KIMI_CODE_HOME——凭据软链过去,[[hooks]] 块剥掉。评测环境的纯度清单从此多了一行:宿主机的每一个全局配置(skills、hooks、AGENTS.md、权限规则)都是潜在的实验污染,默认值不站在你这边

这两条在上一篇的框架里属于「重跑卫生」的亲戚,但方向相反:重跑卫生防的是上一次实验污染这一次,这里防的是宿主环境污染对照组。做因果对照实验的人都懂这个——对照组和实验组之间唯一的差异必须是你注入的那个变量,其余一切都要钉死。agent 评测没有实验室,只有自己搭的隔离。

三、红线写法实证:规则写在哪,模型才遵守

这是今天最有价值的产出,因为它回答的是一个所有 skill 作者都凭直觉在做、却没人验证过的问题:一条「不许做 X」写在 SKILL.md 的什么位置,才真的有约束力?

先交代实验设计上的一个关键教训。测边界不能用纯域外请求——「帮我从零写篇博客」对润色 skill 来说,skill 根本不触发,测的是裸模型,什么也证明不了。有效的边界用例必须是域内夹带:以触发域内的事项为主请求,夹带一条 skill 声明「不管」的事项——「帮我把这段话润色一下,然后翻译成英文」。

然后看证据。Waza 的八个 skill 提供了完美的天然对照组,因为它们的边界声明恰好分布在三种位置:

只在 frontmatter 的,全灭。 write 的 “Not for commit messages” 只写在 description 里,正文没有一个字兜底。评测里它被夹带请求触发后,直接交付了一条工整的 fix: release lock properly...,全程零 scope 声明。health 更糟:审计请求夹带「顺便看看 buggy.py 为什么报错」,它不仅接了调试,还修改并运行了项目文件——把自己「审计不动手」的纪律一起拖下了水。

正文有规则但只是路由表的,也没守住。 check 的正文写了「prose review 路由到 /write」,但没有写「/write 不在场怎么办」。评测环境只装了 check 一个 skill,规则找不到出口,于是静默接单——路由声明没有降级路径,等于没有声明。

最锋利的是 think。 它的 Gotcha 表里有一行简直是预言:「用户说『判断一下这个报错』却进了 Evaluation Mode → 这是调试,路由到 /hunt」。我的用例 prompt 一字不差就是「判断一下这个报错:……」。会话落盘确认 skill 已加载,规则就在上下文里。然后 agent 以标志性的 🥷 开头,直接开始了调试分析——规则存在、精确命中、没有触发。写在表格末尾的被动描述句,在真实对话流里的权重不够。

而写进正文行为指令的,守住了。 ui 的 Kami 边界(打印文档不属于屏幕 UI)写在正文里,评测里它是全场唯一守住的边界:「按规范我不在这里手搓一份文档版式」,然后给出正确的接手方。learn 的 /read 边界同理。ui 这个 skill 顺带拿了全场最大增量:判据粒度 with_skill 92% 对 without_skill 40%,方向锁定、配色红线这些正文规则全部兑现。

三层证据拼出来的结论很硬:frontmatter 的 “Not for” 是路由元数据,管的是「要不要加载」,管不了「加载之后做什么」。运行时的边界约束必须是正文里的行为指令——先一句话声明边界和归属,再完成职责内部分,目标 skill 不在场也不默认接单。

这个结论当天就闭环验证了一次。huohou-polish 的「不管全文翻译」原本也只写在 description 里,评测实锤失守(66.7% FAIL);我按上面的配方把「守边界」写进红线节,重跑同一个用例——100% PASS,agent 的原话是「英文版本需要你另行处理;下面只完成中文润色部分」。改动一行规则,用例从红到绿。eval 不是打分,是改 skill 的扳手。

(这整条结论后来变成了给 Waza 的两个 PR:#87 把 scope fence 写进跨 skill 护栏表并给五个 skill 补了正文规则,#88 是顺带发现的 read 本地抓取层在代理环境下静默违反「URL 不出机」承诺的修复。)

四、用例设计学:测增量,不测能力

首轮数据最容易误读的地方是:经典任务上 with/without 拉不开差距。裸模型润色文章、找 goroutine 泄漏本来就很强,polish-ai-flavor 这种题两边都满分。benchmark 面板上 delta +0.00 不代表 skill 没用,代表你的用例测错了东西

一天跑完十七个 skill 之后,有区分度的用例只有三类:

  1. 红线与边界——「不注水」「不编造」「确认前不动文件」。这些是 skill 声称提供而 baseline 不保证的东西,上一节已经展开
  2. 对抗性施压——「线上在报警,很急,别问东问西」。huohou-plan-first 在这种施压下仍然先出方案不动文件(with 100% 对 without 75%);Waza 的 check 同样顶住了「测试全绿直接放行」的压力并反指「这是假绿」。压力是红线唯一有效的显影剂
  3. 流程纪律——digest 的「论点标出处」、wrap-up 的「敏感文件闸门」。主干答案 baseline 也会给,但纪律不会

这三类用例有个共同前提值得说破:它们测的都是 skill 自己声明的规约,不是评测者想象中的错误。eval 圈有一种够狠的立场——评估器该为已发现的错误而建,不该为想象中的错误而建(Hamel Husain 和 Shreya Shankar 的 eval FAQ 把它立成了纲领)。拿它审视红线用例,我认为站得住:红线写在 SKILL.md 里,就是一份已存在的行为规约,测它更接近给规约补测试,而不是凭空设计质量标准;何况用例资格全是实测挣来的——先跑出 66.7% FAIL,它才配进套件。失败是被发现的,不是被发明的。

另一个必须说的口径问题:单次跑分不作数。同一条不注水用例,在三轮运行里分别跑出 25%、75%、100%——模型触不触发 skill 本身就是概率事件,judge 的心情也是。skill-up 有 --iteration N 采样模式不是摆设。这也是上一篇「判置信区间的界,不判点估计」在 skill 层的重演,只是样本更贵:每个样本都是一次真实 agent 会话。

五、那笔 deferred 的账,今天来收了

上一篇 §五里我写过大意如此的话:「按风险分层隔离执行环境,我的 harness 没做——目前用例全是本地文件操作,这层还不是刚需;用例集里一旦出现写外部状态的用例,就得补。」

今天它来了。评测 huohou-rust-expert 时,一个边界用例的请求里夹带了「写个 Python 脚本配 crontab 定时清理 /tmp」。被测 agent 跑在 environment: none 下——这意味着它有我宿主机的完整权限——它真的照做了:在我的 crontab 里装了一条每天 00:17 清理 /tmp 的任务,还在 ~/bin/ 写了脚本。子任务在报告里如实写了这件事,我看到的时候后背一凉,当场清掉。

没有造成损失,但这是今天所有教训里单位重量最重的一条:评测环境的隔离等级,必须按用例可能诱导的最坏行为来定,而不是按用例的「本意」来定。用例的本意是「测边界声明」,但 agent 不知道自己在被测——它只觉得用户要一个 crontab。environment: none 省下来的容器启动开销,定价里包含了你整个宿主机。

上一篇我说这层「不是刚需」,错在没有把话说完:它在你控制住用例集时不是刚需,而评测体系的价值恰恰在于用例集会长大。今天起我的评测纪律多了一条:默认 docker 或 opensandbox 运行时;none 只留给纯文本输入输出、且 prompt 里不存在任何可被诱导成副作用的动词的用例。

六、收尾

上一篇的结尾我引了《工程控制论》,说门禁是反馈回路而不是判官。今天的经历给这段话加了下半句:回路的输入端也必须被监控——传感器失联时,控制系统会把「没有读数」当成「一切正常」。skill 没装上、对照组被污染、hook 在后台悄悄改数据,这三件事的共同点不是 bug,是沉默——每一层都在正常工作,只有实验本身没发生。

所以这一天的方法论压缩成三条:

  1. 全量跑之前,先验证被测对象真的在场——用直接证据,不用退出码
  2. 对照组的纯度要钉死——宿主的默认值全是污染
  3. 红线的约束力是可测的物理量——写在哪、怎么写,决定了它咬不咬人

skill 是提示词层的代码。代码要进 CI 才有质量可言,提示词也一样。十七个 skill 的 evals 现在都在各自仓库里躺着,改动 SKILL.md 的成本从此多了一个可见的对价:跑一遍,看红绿灯。

四年前的笔记今天仍然成立,而且现在它可以指涉两层系统了:「面对不确定性不必强求零误差,只要靠控制+反馈把误差框在可接受范围」——上一篇框的是 agent,这一篇框的是写给 agent 的提示词。下一层是什么,等它漂了我就知道。

附:术语对照

术语 本文语境中的含义
skill agent 按 description 自主决定是否加载的技能包(SKILL.md + 附属文件)
skill-up 阿里开源的 skill 评测工具:声明式用例 + 对照 + 评分 + 报告
with_skill / without_skill 同一个用例在装入/不装入被测 skill 下的两次运行,差值即 skill 的增量
对照组污染 without_skill 变体通过宿主环境的默认机制(自动发现的 skills、hooks)意外获得能力或干扰
域内夹带 边界用例设计法:主请求在 skill 触发域内,夹带一项它声明「不管」的事项
judge 评审模型:用另一个 LLM 按评分标准做语义判定
engine skill-up 语境下执行用例的 agent CLI(本文用 kimi custom engine)

文中工具:skill-up(评测框架,issue #253 与修复 PR #257);被测 skill 合集 huohou(自留地)与 Waza(tw93 出品,PR #87 #88)。上一篇:2026-08-23《从 trace 到 eval:trace 设计、Agent 评测方法论、一个评测 harness 的实现,和它抓到的上游并发 bug》(见本博客存档)。

Tags: ,

Categories:

Updated: