ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Agent Skills测试实战:从静态检查到CI回归的完整套路

Agent Skills测试实战:从静态检查到CI回归的完整套路 聊一个我最近反复踩坑的话题skills的测试。现在各种Agent平台都在推Skills官方市场和第三方仓库里的技能包越来越多前端开发skills、superpower skills、自动化测试skills遍地都是但真正愿意把“测试”这件事认真讲清楚的资料很少。很多人下载一个技能包能跑出结果就万事大吉结果换个场景就翻车。这篇文章不是给你背概念我用一个真实的技能包从设计、写测试到跑CI的全过程聊聊我踩过的坑和现在固定下来的套路。1. Skills到底怎么测先从技能包结构说起1.1 先聊清楚一个Skills包由什么组成Skills这个名字在不同平台叫法不太一样Claude Agent Skills、Codex Skills、GitHub Skills底层逻辑都差不多给智能体一个“外挂模块”告诉它在什么场景下、用什么姿势调用一段能力。这个模块通常不是单一文件而是一个目录。我见过太多种目录组织方式但最合理、也最容易测试的结构大致是这几个部分目录/文件作用测试关注点SKILL.md技能包的主入口包含名称、描述、指令正文描述是否清晰、触发条件是否准确、格式是否合法scripts/放可执行脚本Python、Shell、Node.js都可以脚本逻辑、入参出参、异常处理、退出码assets/模板、图片、参考文档等静态资源路径引用是否有效、文件是否齐全tests/技能包自己的测试用例覆盖率、断言是否可信CHANGELOG.md/README.md变更记录和使用说明版本号、兼容性说明是否同步有些人会把所有指令都塞进SKILL.md脚本负责干活模型负责“阅读理解”。这种设计本身没问题但会给测试带来一个麻烦SKILL.md 是给人看也给模型看的文本它的“正确性”没法用传统断言完全覆盖。所以测试一个Skills包不能只跑脚本要连文档结构、触发描述、行为一致性一起测。1.2 为什么“跑通脚本”不等于“技能可用”我见过不少技能包脚本单独执行完全正常但放到Agent里就是触发不了。核心原因在于Agent调用Skills是一个带不确定性的过程模型要先把用户请求和技能描述做语义匹配再决定是否需要调用工具最后才把脚本的执行结果组织成回复。这跟普通代码测试有本质区别。普通代码测试是确定性的输入11断言输出2。但Skills测试是概率性的同一句话模型可能这次触发、下次不触发同一个技能包在一个平台表现很好换一个平台就失灵。所以我把Skills测试拆成三层来看第一层静态检查。验证SKILL.md格式、目录结构、依赖声明、文件引用。这一层是最确定的用pytest就能拦下80%的低级错误。第二层单元测试。验证技能包里的脚本逻辑。脚本是确定性的可以写严格的断言比如时间解析、参数校验、返回值格式。第三层行为走查。验证模型在真实对话里会不会正确触发技能、会不会按预期执行。这一层没法用assert解决要靠一套固定的人工用例清单定期回归。这三层缺一不可。只做第一层和第二层你会得到一个“格式规范、脚本正确但Agent根本不鸟你”的技能包只做第三层你会陷入手工点来点去的泥潭改一行脚本就要重新测半天。2. 设计一个可测试的Skill比测试本身更重要2.1 把“触发条件”写清楚description是第一道测试线很多人写技能包把精力都放在正文指令上description随便写一句就完事。但实测下来description才是决定技能能否被正确触发的最关键字段。模型判断“当前场景要不要调用这个技能”主要就是读description。我在自己的技能包里会把description写成这种风格在什么场景下使用、面向什么任务、不适用于什么情况。甚至可以加几句“如果你接到的是XX类型的请求才使用本技能如果用户只是闲聊不要调用本技能”。这些否定条件特别重要因为在真实对话中模型经常会把无关请求误触发到技能上。为什么要在这里强调测试因为description等于你给模型画了一个“触发边界”。测试的第一步就是确认边界画对了。我一般会准备10到20条用户输入一半是应该触发的一半是不应该触发的然后在Agent环境里跑一遍看触发率有多少。2.2 一个能落地的最简技能包示例番茄钟助手光讲抽象概念没意思我拿一个自己写的“番茄钟助手”技能包来演示。这个技能包功能不复杂用户告诉它需要多长时间它计算结束时间并生成一个番茄钟工作周期的待办清单适合用来演示SKILL.md和脚本怎么配合。目录结构是这样的skills/ pomodoro-helper/ SKILL.md scripts/ pomodoro_calc.py assets/ pomodoro_template.md tests/ test_static_skill.py test_pomodoro_calc.py manual_cases.mdSKILL.md的核心内容大致如下--- name: pomodoro-helper description: 当用户需要执行番茄工作法、设置专注时间、安排休息周期时使用。 如果用户只是简单查看时间或闲聊不要调用本技能。 version: 1.0.0 --- # Pomodoro Helper ## 使用场景 - 用户说“帮我安排一个25分钟的专注周期” - 用户需要计算番茄钟的结束时间、休息时间 ## 操作步骤 1. 调用 pomodoro_calc.py参数为用户输入的专注分钟数。 2. 脚本会输出一个 JSON包含 start_time、end_time、break_time。 3. 将 JSON 内容转成阅读友好的文本展示给用户。 ## 注意 - 专注时长必须大于0小于180。 - 如果脚本返回错误码直接向用户反馈参数问题不要继续解释。脚本pomodoro_calc.py并不复杂核心就一个函数#!/usr/bin/env python3 import argparse import json import sys from datetime import datetime, timedelta def calc_pomodoro(duration_minutes: int) - dict: if duration_minutes 0 or duration_minutes 180: raise ValueError(duration must be between 1 and 180) start datetime.now() end start timedelta(minutesduration_minutes) break_minutes max(5, min(duration_minutes // 4, 15)) return { start_time: start.strftime(%Y-%m-%d %H:%M:%S), end_time: end.strftime(%Y-%m-%d %H:%M:%S), break_time: break_minutes, duration_minutes: duration_minutes, } if __name__ __main__: parser argparse.ArgumentParser() parser.add_argument(--minutes, typeint, requiredTrue) args parser.parse_args() try: print(json.dumps(calc_pomodoro(args.minutes), ensure_asciiFalse)) except ValueError as e: print(json.dumps({error: str(e)}), filesys.stderr) sys.exit(1)注意这里我故意做了几件事参数范围校验、错误输出到stderr、退出码非零。这些都是给测试留的“抓手”没有它们自动化测试很难判断失败原因。2.3 为测试设计的三个钩子dry-run、verbose、exit code真正成熟一点的技能包我会建议在脚本里预留三个东西dry-run、verbose、有意义的退出码。dry-run的意思是让脚本只做“预演”不产生真实副作用。这个在技能包里很重要因为Agent场景下技能脚本经常会操作文件、改配置、发请求。如果测试环境里不能安全执行你就不敢做自动化回归。有了dry-run测试脚本可以先跑一遍看到底会做什么操作再决定要不要放行。verbose就是详细日志。技能出错的时候模型只能拿到stdout和stderr里的内容如果没有足够的日志模型连“报什么错”都看不出来。我会在技能脚本里默认往stderr打关键步骤比如“读取到配置”“开始计算”“输出结果”这样Agent在回复用户时就能把查明的问题一并反馈。退出码是自动化断言的基础。成功返回0参数错误返回2环境不满足返回3内部异常返回1。测试代码不需要解析日志内容光看退出码就能判断脚本有没有正常完成。有人觉得这是过度设计但我在实际项目里真被坑过之前有个技能脚本出错时同样返回0只是把错误信息写到输出里。结果Agent拿到输出误以为自己操作成功了把错误信息直接展示给用户。从那以后我要求所有技能脚本必须用退出码区分成功与失败。2.4 确定测试边界哪些要断言哪些只做人工复核写测试之前先想清楚一件事什么能用机器断言什么不能。SKILL.md的格式、脚本的参数校验、输出JSON结构、退出码这些是确定性行为可以自动化断言。但“模型在对话中是否会触发技能”“生成的总结是否符合用户预期”这类行为本质上是非确定的。你写再多assert也没用它本来就是一个需要人工观察的指标。我给技能包划分测试边界时会用一张表测试内容自动化断言人工复核SKILL.md frontmatter 字段完整是否脚本单元测试是否输出格式是否边界参数与异常输入是否模型触发准确率否是结果可读性否是与其它技能的组合行为否是把范围划清楚之后自动化回归才不会变成“假绿”。我之前见过一个团队CI全绿结果人工走查一测技能几乎不能用。原因就是他们把大量非确定行为硬塞进自动化断言里指标好看但没有真正覆盖用户场景。3. 基于pytest的Skills回归测试实战3.1 目录与fixture设计自动化回归我选的是pytest理由很简单生态成熟、参数化方便、CI支持好。很多自动化测试框架pytest相关技能包也会依赖这套逻辑。我的技能包测试目录里会有一个公共的conftest.py用来管理技能包路径和相关fixtureimport json import subprocess from pathlib import Path import pytest SKILLS_ROOT Path(__file__).resolve().parents[2] POMODORO_DIR SKILLS_ROOT / pomodoro-helper SCRIPT_PATH POMODORO_DIR / scripts / pomodoro_calc.py pytest.fixture def skill_dir() - Path: return POMODORO_DIR pytest.fixture def run_script(): def _run_script(*args: str) - subprocess.CompletedProcess: return subprocess.run( [sys.executable, str(SCRIPT_PATH), *args], capture_outputTrue, textTrue, timeout10, ) return _run_script这里有个细节测试脚本通过subprocess调用技能脚本而不是直接用import。原因是技能脚本将来可能在别的运行时里执行比如Node.js或者打包后的二进制用subprocess能模拟更真实的执行环境。当然纯Python函数用import也没错just my习惯把执行和解析分开测试更接近生产。3.2 SKILL.md静态检查先把格式问题拦在门外SKILL.md看起来是纯文本但Agent平台解析技能包时对frontmatter和结构是有要求的。比如frontmatter里必须有name和description正文里不能有失控的图片引用脚本路径要真实存在。这部分我写成一个静态检查测试文件test_static_skill.pyimport re from pathlib import Path import pytest def test_frontmatter_exists(skill_dir): skill_md skill_dir / SKILL.md content skill_md.read_text(encodingutf-8) assert content.startswith(---), SKILL.md必须包含frontmatter assert name: in content.split(---)[1] assert description: in content.split(---)[1] def test_description_has_trigger_and_negative_rules(skill_dir): skill_md skill_dir / SKILL.md content skill_md.read_text(encodingutf-8) description content.split(---)[1] assert len(description.strip()) 40, description太短影响触发准确率 assert 不要 in description or 不要调用 in description, 建议补充负向触发规则 pytest.mark.parametrize(referenced_path, [ scripts/pomodoro_calc.py, assets/pomodoro_template.md, ]) def test_referenced_files_exist(skill_dir, referenced_path): assert (skill_dir / referenced_path).exists(), f引用的文件不存在: {referenced_path} def test_script_has_execute_permission(skill_dir): script_path skill_dir / scripts / pomodoro_calc.py assert script_path.exists() assert script_path.stat().st_mode 0o111, 脚本缺少执行权限这些断言看起来很简单但能拦住大量低级失误。我见过有人改了一行脚本路径SKILL.md里的引用没同步结果模型每次调用都得到一个file not found排查了半天。这些静态检查跑在CI里改坏了立刻有红点提示。3.3 技能脚本的单元测试给确定性逻辑上保险脚本逻辑的测试就可以写严格断言了。以番茄钟助手为例参数范围、时间计算、输出JSON格式都要覆盖到。import json import pytest def test_valid_input_returns_json(run_script): result run_script(--minutes, 25) assert result.returncode 0 payload json.loads(result.stdout) assert payload[duration_minutes] 25 assert payload[break_time] 5 def test_out_of_range_input_fails(run_script): result run_script(--minutes, 200) assert result.returncode ! 0 assert error in result.stderr def test_zero_input_fails(run_script): result run_script(--minutes, 0) assert result.returncode ! 0 assert duration in result.stderr pytest.mark.parametrize(minutes,expected_break, [ (5, 5), (25, 6), (45, 11), (60, 15), ]) def test_break_time_rule(run_script, minutes, expected_break): result run_script(--minutes, str(minutes)) payload json.loads(result.stdout) assert payload[break_time] expected_break注意我在测试里对break_time的计算规则做了参数化断言。这个规则是我在设计技能脚本时自己定的休息时间取专注时长的四分之一但最少5分钟、最多15分钟。如果不把规则固化成测试以后有人为了“优化”随手改了公式可能导致所有已有对话的记忆错乱。回归测试在这里的作用就是把隐含规则明确下来。3.4 Agent行为回归使用“人工用例清单”做半自动走查脚本层测试只覆盖了确定性部分还得解决 Agent 行为验证。我的做法是在技能包里维护一个manual_cases.md把该触发和不该触发的输入列出来# Pomodoro Helper 人工走查清单 ## 应该触发的场景 - [ ] 用户说帮我开一个25分钟的番茄钟 - [ ] 用户说我要专注45分钟休息多久合适 - [ ] 用户说今天第四轮番茄钟提醒我 ## 不应该触发的场景 - [ ] 用户说现在几点 - [ ] 用户说讲个笑话 - [ ] 用户说帮我总结这份文档 ## 边界场景 - [ ] 用户说设置0分钟番茄钟应报错 - [ ] 用户说设置200分钟番茄钟应报错然后在pytest里用一个标记把需要人工复核的用例收集出来import pytest pytestmark pytest.mark.manual pytest.mark.parametrize(case_id, [ TC-001-should-trigger, TC-002-should-trigger, TC-003-should-not-trigger, ]) def test_manual_case(case_id): pytest.skip( f需要人工在Agent环境执行此用例并记录结果: {case_id} )这样跑测试时automated部分和manual部分分开。pytest -m not manual走CI全自动pytest -m manual生成一个人工走查清单供每次发布前过一遍。很多人嫌人工走查麻烦。但说实话模型行为这种动态指标本来就适合用“固定剧本定期抽查”的方式来盯而不是靠一次性调试。你只要形成习惯每次改动技能包后花十几分钟过一遍清单就能避免很多线上翻车。3.5 在CI里让测试自动跑起来自动化这部分完全可以塞进GitHub Actions。我的技能包仓库里用一个workflow来跑pytest配置大致如下name: skills-test on: push: paths: - skills/** pull_request: paths: - skills/** jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-pythonv5 with: python-version: 3.12 - run: pip install pytest - run: pytest -m not manual --tbshort测试只会在skills/目录有改动时触发避免无关改动浪费CI时间。--tbshort让失败信息更紧凑方便快速定位。如果团队用别的方式管理代码核心思路是一样的凡是涉及SKILL.md或脚本的改动必须跑一遍确定性测试。4. 我踩过的坑Skills测试常见问题排查4.1 技能不触发或触发错乱先查description而不是脚本有段时间我写了一个“URL缩短技能”脚本跑得很好但Agent总是该触发时不触发。我把脚本翻来覆去查了一遍没发现问题。后来一复盘问题出在description写得太泛只写了“处理链接”模型根本判断不了什么时候算“处理链接”。排查这类问题我现在的固定套路是先看description是否明确包含触发词和否定条件再看SKILL.md正文里的指令是不是够具体最后才查脚本。顺序百分之八十能定位问题。4.2 输出格式漂移JSON少个逗号Markdown缩进变了模型输出天然不稳定。同一个技能包这次生成的JSON格式规整下次可能多一段解释文字再下次直接给个Markdown代码块。如果你的下游系统用严格JSON解析这里就是最容易崩的地方。我的对策有两层。第一层技能脚本内部尽量自己生成结构化数据不让模型自由发挥格式第二层在SKILL.md里给出明确的输出模板如果模型按照模板输出格式基本可控。测试时我会专门准备几个“输出格式漂移”用例比如在模型回复前插入一句无关文本看解析逻辑会不会依然稳健。4.3 工具权限过大与参数逃逸技能包需要调用系统命令时一定要小心参数逃逸。我之前写过一个技能把用户输入的文本拼进Shell命令表面测试都通过直到来了一个包含分号和管道符的输入命令直接执行了危险操作。后来我全部改成subprocess列表参数传参绝不拼shell字符串。测试脚本里要加这类恶意输入用例pytest.mark.parametrize(bad_input, [ 25; rm -rf /tmp/test, 25 | echo hacked, $(whoami), ]) def test_shell_injection_safety(run_script, bad_input): result run_script(--minutes, bad_input) assert result.returncode ! 0这里不是要写漏洞利用脚本而是验证你用了安全的调用方式。参数传错了可以重来权限失控是大事。4.4 测试数据互相污染技能状态残留有些技能包会写临时文件、环境变量或者状态缓存。如果不做隔离测试A产生的状态可能影响测试B的结果。比如某个技能会往用户目录下写配置我第一版测试跑完第二次再跑就出现了脏数据。解决方案是每个fixture里都清理状态。pytest的tmp_path就是一个不错的选择但技能脚本不一定知道你传进去的是临时目录。更稳妥的做法是在技能脚本里提供--workdir参数让测试指定workspaceCI跑完直接丢弃整个临时目录。4.5 一张表速查常见故障我把经常遇到的问题整理成了排查表直接抄作业用现象优先检查常见根因技能完全不触发description与调用条件描述缺乏触发词、被其它技能抢占偶尔触发偶尔不触发description表达模糊缺少否定条件、描述过长脚本执行了但结果错误脚本参数与SKILL.md指令模型传错了参数类型输出JSON解析失败脚本是否直接输出结构化数据模型在JSON外又添加了文本权限错误脚本运行用户测试环境缺少执行权限状态被污染技能脚本对全局环境的影响缺少workdir隔离中文乱码编码声明stdout未设置utf-85. 不同场景下的Skills测试速查5.1 知识问答型前端开发skills、Linux面试题测试现在市面上有大量知识型技能包比如“前端开发skills”和“linux面试题测试”。这类技能的脚本部分往往很薄核心价值在提示词模板和知识库组织方式上。测试这类技能重点不在脚本而在“信息的准确性和答案的稳定性”。我会准备一组基准问题跑多次看答案是否一致有没有出现一本正经胡说八道。比如设置一个“已知正确答案的问题集”每次更新技能包都去跑一遍如果某道题的答案漂移了大概率是知识库或提示词被改坏了。5.2 代码生成/自动执行型pytest、Appium、设备老化测试脚本代码生成类技能是重灾区。比如一个“自动化测试框架pytest生成技能”用户让它写pytest用例它就写一堆看似合理的代码但根本没法执行。测试这类技能光靠模型回复判断不够我通常会把它生成的代码自动拉到沙箱里跑一遍。Appium自动化测试技能也一样它可能会生成移动端测试脚本。要验证这类技能就得准备一台测试机或模拟器跑通冒烟用例。设备老化测试全自动执行脚本这种技能更直接不仅生成代码还要全自动执行老化流程。我会把这类技能拆成两段验证第一段是生成代码的静态检查第二段是真实环境的小规模老化演练先跑10分钟确认无崩溃再正式跑完整流程。5.3 硬件与音视频测试T-Box、RTSP测试流硬件测试场景下Skills的测试会更加依赖环境。比如汽车电子测试里常见的T-Box测试技能、HIL和PIL测试技能它们的脚本往往需要连接硬件设备、读取总线信号、控制测试台架。这类技能的自动化测试我建议多做“连接检查”和“参数校验”。脚本执行前先检查设备是否在线、端口是否可访问、测试流地址是否有效。像RTSP测试流、RTMP测试地址这类资源技能包里一般会内置一堆可供快速验证的地址测试时就必须确认这些地址是否还活着不然技能会拿一个失效的地址去“测试”最后报一个莫名其妙的失败。我记得有个技能包专门做交流法电阻内阻测试里面甚至内置了电路原理图。测试它的关键就是确保原理图里的接线定义和脚本里的配置一致否则生成的报告再漂亮硬件参数也是错的。5.4 安全测试与敏感信息检查安全测试类技能我用之前总会格外谨慎尤其那些用来检查APP登录密码是否明文存储、检测接口安全漏洞的技能包。它们往往要扫描代码、抓请求、检查密钥文件。这些操作在生产环境里风险极高。我的建议是安全测试技能必须默认开启dry-run模式所有扫描动作先展示不做确认无风险后再真正执行。测试用例里要覆盖目标文件不存在、目标文件过大、目标包含二进制数据等异常场景。这类技能的自动化回归尽量放在隔离环境里不要直接指向生产系统。5.5 创作与互动型分镜skills、网格射击测试网页版、鹈鹕测试提示词创作类技能比如“分镜skills”它的输出是创意内容没有绝对的对错但依然有“可用性”问题。我的测试方法是指定同一段输入连续跑五遍看生成的分镜结构是否清晰、有没有遗漏关键角色设定、镜头顺序是否合理。这种测试不适合用assert更适合打个1到5分的可读性评分定期对比。互动类技能比如网格射击测试网页版就偏向传统前端测试。它本质是一个带UI交互的网页应用只不过入口被包装成了技能包。测试时要用Playwright这类工具模拟用户点击和键盘操作验证射击命中判定、计分逻辑和页面响应。这类技能有个特点脚本逻辑、UI渲染和Agent提示词三层耦合改任何一层都可能破坏整体所以回归测试特别重要。至于“鹈鹕测试提示词”这类的技能包本质上就是把一套结构化测试模板封装成可复用的提示词。测试起来反而简单核心是验证模板在不同输入下的稳定性换一批测试数据输出的框架结构不能乱。6. 把Skills测试做成可持续的工程习惯6.1 用GitHub Skills做版本管理GitHub Skills本身是GitHub官方用来教学新用户使用Git和Actions的交互式课程它跟Agent技能包是两个概念但都被大家叫做Skills。不管哪个生态版本管理都是必须的。我给每个技能包单独建仓库或者在一个大仓库里用子目录管理版本号写在SKILL.md的frontmatter里。每次改动必须更新版本号和CHANGELOG。这样做的直接好处是当Agent平台或下游系统缓存了旧版本你能快速定位是版本没刷新还是技能包本身有bug。6.2 建立金标准测试集测试做久了你会慢慢沉淀出一批“金标准用例”。这些用例不是一次性写出来的而是从线上事故、用户反馈、模型表现变化里一点点积累的。我维护技能包时会开一个golden_cases.md里面只放那些“历史上触发过bug、以后改动绝不能再犯”的场景。比如某个技能曾经因为特殊字符崩溃我就会把那类输入加进金标准测试集。技能包迭代越勤金标准测试集越厚CI的守护能力也越强。6.3 跨平台一致性Codex Skills、Reasonix安装新Skills同一套技能包在不同平台上的表现是有差异的。Codex Skills、Reasonix安装新Skills、以及其它Agent工具平台的解析规则不完全一致。有的平台对frontmatter字段大小写敏感有的平台要求描述不能超过多少字有的平台支持直接执行Markdown里的代码块有的必须调用外部脚本。跨平台测试没有捷径只能搭一个“多平台走查矩阵”每个平台跑同一套金标准用例记录触发率和输出质量。我见过有人直接写脚本把技能包在多个平台上自动安装并跑基本用例这次测试通过下次改动前再跑一遍。这个矩阵一旦建立往后所有技能包的迭代都会安全很多。6.4 我对Skills测试的最终心得把Skills测试常态化之后我的体会是不要试图一次把所有东西都测到完美。技能包的价值在于快速迭代测试的目的是让你敢改、改完不怕坏。静态检查守住格式单元测试守住脚本逻辑人工走查守住模型行为三层搭起来一个技能包就能从“能跑”进化成“能稳定交付”。如果你刚准备开始给自己的技能包搭测试我建议从最小闭环入手先写一个test_static_skill.py把SKILL.md的格式校验跑起来再给脚本写几个参数用例。就这两步已经能拦住我踩过的大部分坑。剩下的人工走查清单可以后面慢慢补养成习惯远比一次性做到位重要。
返回列表