老周的工具笔记

老周的工具笔记

已发布 24 篇文章

给技能写测试用例:怎么让改动不怕回归

给技能写测试用例:怎么让改动不怕回归

技能最怕的不是写不出来,而是「改了 A,坏了 B」。你调一句提示词、改一个脚本参数,结果三天前跑得好好的工作流开始吐错。这种事在我带的徒弟身上发生过太多次,每次都是「我就动了一行,怎么全挂了」。

本篇教你给技能套上一层测试,让每次改动都有「回归保护网」。不用担心门槛,技能测试不需要你精通单元测试框架,几行脚本就能搞定,关键在于把「对不对」从人脑里搬到代码里。

一、为什么技能要写测试

先说清楚:技能里能被测的东西,远不止 SKILL.md 那段提示词。一个完整技能通常由三部分组成,我们得分清哪些测得了、哪些测不了:

  • 提示词(prompt):告诉 agent 怎么思考、怎么调用。这部分语义难以断言,基本测不了。
  • 脚本(scripts/):真正执行逻辑、调 API、解析数据的代码。这部分是确定的,可断言。
  • 契约(输入/输出格式):agent 和脚本之间约定好的参数与返回结构。这部分也是确定的,可断言。

所以结论很明确:提示词语义难测,但脚本和契约是确定的、可断言的。把这两个钉死,绝大多数「回归」都能被拦下来。我见过不少同学因为「技能是给 AI 用的,没法测」就完全不写测试,结果每次改完都提心吊胆。其实 AI 那部分你测不了,但脚本和契约你完全测得了。只测能测的,就已经挡掉 80% 的事故。

二、先确定测什么:技能的边界与契约

动手前,先把技能的「契约」写清楚。所谓契约,就是:给什么输入,期望什么输出,出错时返回什么。比如一个「文本翻译」技能,它的契约应该长这样:

# 技能契约(写在 SKILL.md 或 CONTRACT.md 里)
name: translator
inputs:
  - text: 待翻译文本(字符串,必填)
  - target: 目标语言(枚举:en/zh/ja,必填)
outputs:
  - translated: 翻译结果(字符串,非空)
  - detected: 检测到的源语言(字符串)
errors:
  - text 为空 → 返回 code: EMPTY_TEXT
  - target 非法 → 返回 code: BAD_TARGET

契约就是你测试的「判卷标准」。没有它,你连「对不对」都没法定义,更别说自动化了。把契约固化成文件,测试和调用方都读它,避免大家靠「默契」协作。

翻车点:契约只写在脑子里,没落到文件里。半年后你忘了当时约定 target 的默认值是 en 还是 zh,改脚本时顺手换了默认值,调用方全乱。把契约固化成文件,测试也读它,改动才有据可依。

再强调一个容易忽略的点:契约不是写完就不动的。随着技能演进,你迟早要改输入或输出,这时候第一反应应该是「先改契约文件,再改脚本,最后更新 golden 和测试」,而不是闷头改代码。契约先行,能让测试在改动过程中一直给你反馈,而不是等全部改完才发现方向跑偏。很多团队技能腐化的起点,就是契约和代码悄悄分了家。

三、用脚本做冒烟测试(smoke test)

最轻量的测试叫「冒烟测试」:不追求全覆盖,只验证「主路径能跑通、返回结构没坏」。技能目录下加一个 test_smoke.sh 即可:

#!/usr/bin/env bash
# skills/translator/test_smoke.sh
set -e

OUT=$(python scripts/translate.py --text "你好" --target en)
echo "$OUT"

# 断言:返回里必须含 translated 字段且非空
if ! echo "$OUT" | grep -q '"translated"'; then
  echo "FAIL: 缺少 translated 字段"
  exit 1
fi

echo "SMOKE PASS"

跑它:

bash skills/translator/test_smoke.sh

冒烟测试的价值在于:你每次改完技能,先跑这一下,十秒内知道「主功能没塌」。它不能保证结果完全正确,但能拦住「脚本报错、字段消失、依赖缺失」这类硬伤。

翻车点:把断言写在脑子里——「我看一眼输出对不对」。人眼看一次可以,改十次你就不会每次都仔细看,bug 就溜进去了。断言必须写成代码,非 0 退出码才算失败,才能接进自动化。人靠不住,代码靠得住。

四、fixtures 与 golden output(黄金样本)

冒烟只保证「没崩」,要保证「结果对」,需要 fixture(固定输入)和 golden output(已知正确输出)。准备一组小样本:

# skills/translator/fixtures/
#   en_hello.txt         输入
#   en_hello.golden.json 期望输出(人工确认过的正确答案)

测试时把实际输出和 golden 对比,任何偏差都算失败:

#!/usr/bin/env bash
# skills/translator/test_golden.sh
set -e

python scripts/translate.py --text "$(cat fixtures/en_hello.txt)" --target en 
  > /tmp/actual.json

if diff <(jq -S /tmp/actual.json) <(jq -S fixtures/en_hello.golden.json); then
  echo "GOLDEN PASS"
else
  echo "FAIL: 实际输出与黄金样本不符"
  exit 1
fi

golden 样本是回归测试的灵魂:一旦你改坏了翻译逻辑,输出偏离 golden,测试立刻报警。它可以积少成多,每个边界 case 加一份,慢慢就织成一张网。

翻车点:golden 文件是「第一次跑出来的结果」直接存下来的——那等于没测,因为错的也存成对的了。golden 必须由你人工确认过正确,再落盘。之后任何改动如果让输出偏离它,测试才会真正报警。否则测试百分之百通过,却毫无意义。

维护 golden 还有个细节:当你的技能确实需要「改变正确行为」时(比如翻译引擎升级后输出更地道了),golden 也要跟着更新。更新 golden 不是改测试让它过,而是先人工确认「新输出确实更好」,再用新输出覆盖 golden。这条纪律能保证 golden 永远代表「当前公认的正确」,而不是「曾经跑出来的东西」。把它写进团队的改动规范里,避免有人为了过 CI 随手覆盖 golden。

五、把回归测试自动化

单个测试手动跑还行,技能一多就顾不过来。把它们串成一个 run_tests.sh,并接进你已有的 CI(GitHub Actions / GitLab CI 都行),改完 push 自动跑:

#!/usr/bin/env bash
# skills/translator/run_tests.sh
fail=0
bash test_smoke.sh  || fail=1
bash test_golden.sh || fail=1
exit $fail
# .github/workflows/skills.yml(示意)
name: skills-test
on: [push]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pip install -r skills/translator/requirements.txt
      - run: bash skills/translator/run_tests.sh

自动化的好处是:你不用记得去跑测试,CI 替你盯着。哪次提交让技能红了,立刻就能定位到那次改动,而不是等三天后线上报障。

翻车点:CI 里没装技能依赖(requirements.txt / 系统命令),本地能过的测试在云端全红,你以为是代码问题,排查半天发现是环境。CI 的第一条命令就该把依赖装齐,最好和本地用同一份清单,做到环境一致。

关于测试覆盖到什么程度,给个实在的建议:别追求 100%。技能脚本大多不大,优先覆盖「会改变结果的那些分支」——参数校验、错误返回、边界输入,这些才是容易回归的地方。至于那些纯透传、纯格式化的代码,测不测影响不大,把精力留给真正会出事的逻辑。另外,每修一个线上 bug,都应该顺手补一个能复现它的测试,让这个 bug 以后永远不能再偷偷回来。这套「修 bug 必补测试」的习惯,比任何覆盖率指标都管用。

六、翻车点汇总与最小实践

给技能写测试,把下面几条记牢:

  • 测得了的先测:脚本逻辑和输入/输出契约是确定的,优先覆盖;提示词语义难测,放过别纠结。
  • 契约先固化:把输入、输出、错误码写进文件,测试和调用方都读它,避免「默契」翻车。
  • 断言写进代码:用退出码判定成败,别靠人眼;非 0 即失败才能自动化。
  • golden 必须人工确认过:别把第一次跑出来的结果直接当正确答案存,那样测试永远绿却毫无价值。
  • CI 要装齐依赖:环境和本地一致,测试才有意义,否则红得不明不白。

哪怕你只写一个 test_smoke.sh,每次改完技能跑一遍,也能避开绝大多数「改坏旧功能」的尴尬。测试不是负担,是你敢大改技能的底气——有了它,你才敢放手优化提示词、重构脚本,而不用担心哪天把线上的活儿搞瘫。从今天起,给每个你打算长期维护的技能配一个 test_smoke.sh,这一步小投资,会在你第一次放心大改时连本带利还给你。