1. 测试团队为什么需要一个私有 Skills 库
测试对象变了,这件事比很多人想象得更彻底。以前我们测的是确定性代码,输入 A 必然得到 B,断言写assert result == expected就完事。现在要测的是大模型输出、AI 生成内容、多模态交互结果,同一段提示词跑两次结果可能完全不同,正则和等值断言直接失效。
我所在的团队去年下半年开始接触 AI 类产品测试,最初的做法是人工 review 加零散脚本。一个“判断这段 JSON 返回是否合理”的任务,三个人写出三套逻辑,谁也说不清标准是什么。更麻烦的是数据构造:要造 100 条“深圳地区、月消费 5k-8k、偏好户外运动”的用户画像,每次都得重写一遍随机脚本,字段之间的关联规则还得手动维护。
多模态识别更是重灾区。测一个拍照识物功能,要验证截图里“立即购买”按钮是否可见可点击,传统做法是写图像定位脚本,换个 UI 版本就全废。这三类问题叠加起来,测试用例的维护成本是普通产品的数倍,核心原因就一个:输出不确定,断言写不了。
Skills 库解决的正是这个问题。它把“测试经验”从一次性用例里抽出来,封装成可复用、可组合的能力单元。一个“构造合法邮箱”的 Skill 加一个“生成随机密码”的 Skill,不写新代码就能组合出“构造注册请求数据”的 Skill。资产从线性增长变成指数复用,这是测试团队真正需要的底层能力。
这篇内容面向的是想从零搭建测试专用 Skills 库的团队,覆盖自动断言、数据构造、多模态识别三大能力。我会给出目录结构、Skill 注册配置、断言模板的可复制片段,并演示用 TaoToken 统一 Key 和 API 通道接入多模态识别 Skill 的完整验证步骤,目标是让你跑通一条端到端测试流水线。适合有一定 Python 基础、正在被 AI 测试折磨的测试开发和 QA 工程师。
2. TaoToken 前置准备:统一 Key 与 API 通道
在搭建 Skills 库之前,先把模型调用通道理顺。测试 Skill 会频繁调用大模型,如果每个 Skill 各自管理 Key、各自处理重试和限流,维护成本会失控。我的做法是用 TaoToken 作为统一的 API 通道,一个 Key 覆盖对话、多模态、代码生成等场景,Skill 层只关心业务逻辑。
TaoToken 是一个模型 API 聚合服务,官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 入口是 https://taotoken.net/api 。它的价值在于把不同模型的调用方式统一成 OpenAI 兼容格式,测试 Skill 里不用为每个模型写适配层。对于测试团队来说,这意味着断言 Skill、数据构造 Skill、多模态识别 Skill 可以共用同一套请求封装。
前置准备分三步。第一步是注册账号并创建 API Key,登录后进入控制台,在 API Keys 页面生成一个 Key,建议按环境区分,比如test-skills-dev和test-skills-ci,方便后续做用量归因。控制台地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,API Keys 管理页是 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。
第二步是确认模型 ID。测试 Skill 里会用到两类模型:文本类用于断言和数据构造,多模态类用于图像识别。你可以在模型对话页面先手动验证一下模型是否可用,地址是 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite 。手动对话确认没问题后,再写进 Skill 配置,避免调试时把模型问题和代码问题混在一起。
第三步是准备本地环境。我习惯用 Python 3.10 以上,依赖openai、pydantic、pyyaml、jsonschema这几个库。openai库用来发请求,pydantic做结构化输出校验,pyyaml读 Skill 配置,jsonschema校验数据构造结果。安装命令如下:
pip install openai pydantic pyyaml jsonschema pillow环境变量里配置 Key 和 Base URL,不要硬编码到代码里:
export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"这里有个容易踩的坑:Base URL 末尾不要带/v1,openai库会自动拼接路径。如果你手动加了/v1,请求会变成/v1/v1/chat/completions,直接 404。我试过在 CI 环境里因为这个问题排查了半小时,最后发现是环境变量多写了一截。
对于需要长期跑编码类 Skill、Agent 编排的团队,可以考虑 Coding Plan,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,它在高频调用场景下更划算。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,遇到参数问题先查文档。
前置准备做完后,你的 Skills 库就有了统一的模型出口。接下来所有 Skill 都通过这个通道调用模型,Key 轮换、限流、用量统计都在一处管理,这是从零搭建 Skills 库时最容易被忽略但最重要的一步。
3. 可复制配置:目录结构与 Skill 注册片段
Skills 库的目录结构决定了后续的可维护性。我的建议是按“能力类型”分目录,每个 Skill 一个文件夹,里面放配置、提示词模板、测试样例。这样新增 Skill 时不用改核心代码,只加目录和注册项。下面是我实际在用的结构:
test-skills/ ├── config/ │ ├── settings.yaml # 全局配置:Base URL、默认模型、超时 │ └── registry.yaml # Skill 注册表 ├── skills/ │ ├── assertion/ │ │ ├── json_assert/ │ │ │ ├── skill.yaml # Skill 元信息 │ │ │ ├── prompt.md # 提示词模板 │ │ │ └── samples.jsonl # 回归测试样例 │ │ └── text_tone_assert/ │ ├── data_factory/ │ │ ├── user_profile/ │ │ └── boundary_value/ │ └── multimodal/ │ ├── ui_element/ │ └── object_detect/ ├── core/ │ ├── loader.py # 读取 registry 和 skill.yaml │ ├── executor.py # 统一调用模型、解析结构化输出 │ └── validator.py # 用 pydantic 校验输出 └── run_skill.py # 命令行入口全局配置config/settings.yaml长这样,注意 Base URL 和模型 ID 都从这里读,方便切换环境:
taotoken: base_url: "https://taotoken.net/api" api_key_env: "TAOTOKEN_API_KEY" timeout: 60 max_retries: 3 models: text: "gpt-4o-mini" multimodal: "gpt-4o" defaults: temperature: 0.2 max_tokens: 1024Skill 注册表config/registry.yaml是核心,它把 Skill 名称映射到目录和类型:
skills: - name: json_assert type: assertion path: skills/assertion/json_assert model: text description: "校验 JSON 字段类型与约束,返回结构化断言结果" - name: user_profile type: data_factory path: skills/data_factory/user_profile model: text description: "按规格构造用户画像数据,支持字段关联约束" - name: ui_element type: multimodal path: skills/multimodal/ui_element model: multimodal description: "识别截图中的 UI 元素,返回坐标与可见状态"单个 Skill 的skill.yaml定义输入输出契约,这是保证可组合性的关键:
name: json_assert version: 1.0.0 input_schema: type: object required: [payload, rules] properties: payload: type: string description: "待校验的 JSON 字符串" rules: type: array items: type: object properties: path: { type: string } expect_type: { type: string } not_null: { type: boolean } output_schema: type: object required: [passed, reason, evidence] properties: passed: { type: boolean } reason: { type: string } evidence: { type: string }断言模板prompt.md里最关键的是强制结构化输出,不允许模型写小作文:
你是测试断言执行器。输入是一段 JSON 和一组断言规则。 要求: 1. 逐条检查规则,提取实际值后再比较,禁止凭印象判断。 2. 只输出 JSON,格式为 {"passed": bool, "reason": str, "evidence": str}。 3. reason 用中文简述结论,evidence 给出实际提取到的值。 4. 不允许输出 JSON 以外的任何内容,不允许使用"大概""可能"等模糊词。 输入 JSON: {{payload}} 断言规则: {{rules}}这套配置的好处是:新增一个断言 Skill,只需要复制json_assert目录,改skill.yaml和prompt.md,在registry.yaml加一行,核心代码零改动。数据构造和多模态 Skill 同理,只是model字段指向不同的模型。
如果你用 Claude Code 做 Skill 开发,可以在项目根目录放一个.claude/settings.json,把 Base URL、Key、Model ID 三件套写全,避免每次手动配置:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-你的key", "ANTHROPIC_MODEL": "claude-3-5-sonnet" } }注意这里的 Base URL 同样不带/v1。Claude Code 的接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,配置项以文档为准。三件套缺一不可:Base URL 决定请求发往哪里,Key 决定身份,Model ID 决定用哪个模型,少任何一个都会在启动时报错。
4. 验证请求:跑通多模态识别 Skill 端到端
配置写完后,必须验证一条完整链路。我选多模态识别 Skill 做验证,因为它同时覆盖了模型调用、结构化输出、文件上传三个环节,跑通了说明整条通道没问题。
先写核心执行器core/executor.py,它负责读配置、发请求、解析输出:
import os import json import base64 from openai import OpenAI import yaml def load_settings(path="config/settings.yaml"): with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f) def build_client(settings): return OpenAI( api_key=os.environ[settings["taotoken"]["api_key_env"]], base_url=settings["taotoken"]["base_url"], timeout=settings["taotoken"]["timeout"], ) def encode_image(image_path): with open(image_path, "rb") as f: return base64.b64encode(f.read()).decode("utf-8") def run_multimodal_skill(image_path, target_desc, settings): client = build_client(settings) model = settings["models"]["multimodal"] b64 = encode_image(image_path) resp = client.chat.completions.create( model=model, temperature=settings["defaults"]["temperature"], max_tokens=settings["defaults"]["max_tokens"], messages=[ { "role": "user", "content": [ {"type": "text", "text": f"识别图中目标:{target_desc}。只输出 JSON,格式为 {{\"found\": bool, \"bbox\": [x1,y1,x2,y2], \"visible\": bool, \"reason\": str}}。"}, {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}}, ], } ], ) content = resp.choices[0].message.content.strip() if content.startswith("```"): content = content.strip("`").replace("json", "", 1).strip() return json.loads(content)然后写命令行入口run_skill.py,把参数传进去:
import sys from core.executor import load_settings, run_multimodal_skill if __name__ == "__main__": settings = load_settings() image_path = sys.argv[1] target = sys.argv[2] result = run_multimodal_skill(image_path, target, settings) print(json.dumps(result, ensure_ascii=False, indent=2))执行验证命令:
python run_skill.py ./samples/login_page.png "立即购买按钮"预期返回结果类似:
{ "found": true, "bbox": [320, 680, 520, 740], "visible": true, "reason": "在页面底部检测到'立即购买'按钮,区域完整无遮挡" }看到这个结果,说明从 Key 读取、Base URL 拼接、图片编码、模型调用到结构化解析整条链路是通的。如果返回的bbox坐标明显超出图片尺寸,说明模型在幻觉,需要在 Skill 里加一层坐标范围校验,这是多模态 Skill 的常见加固手段。
验证完多模态,再跑一个文本断言 Skill 确认文本通道也正常:
python run_skill.py --skill json_assert --payload '{"name":"test","age":18}' --rules '[{"path":"$.age","expect_type":"integer","not_null":true}]'返回{"passed": true, ...}就说明断言通道没问题。两条通道都验证通过后,你的 Skills 库就具备了端到端跑测试流水线的基础能力。后续新增 Skill 时,先单独验证,再注册进registry.yaml,最后接入 CI。
5. 本篇常见错排查:401、local proxy failed 与解析异常
搭建过程中最容易卡住的不是业务逻辑,而是通道和解析问题。我把实际遇到过的几类错误整理出来,对照排查能省不少时间。
第一类是 401 认证失败。报错通常是Error code: 401 - {'error': {'message': 'Invalid API key'}}。原因有三个:Key 没设置到环境变量、Key 复制时带了空格、Key 被禁用。排查顺序是先echo $TAOTOKEN_API_KEY确认变量存在且无空格,再去 API Keys 页面确认 Key 状态正常。注意不要在代码里写api_key="sk-xxx "这种带尾随空格的字符串,openai库不会自动 trim。
第二类是local proxy failed或连接超时。这类报错通常出现在 Base URL 写错的情况下,比如写成了https://taotoken.net/api/v1导致路径重复,或者网络环境有额外的转发配置。排查方法是先用 curl 直接打一次接口:
curl -X POST "https://taotoken.net/api/chat/completions" \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"ping"}]}'如果 curl 通而 Python 不通,问题在代码或环境变量;如果 curl 也不通,检查 Base URL 拼写和网络配置。注意 Base URL 就是https://taotoken.net/api,不要自己加后缀。
第三类是reading choices报错,完整信息类似KeyError: 'choices'或list index out of range。这通常是因为返回体不是标准结构,可能是模型名写错导致返回了错误信息,也可能是请求被限流返回了非预期内容。排查方法是把原始响应打出来:
print(resp.model_dump_json(indent=2))看到实际返回结构后再定位。如果是模型名错误,去模型对话页面确认可用模型 ID;如果是限流,加max_retries和退避重试。
第四类是结构化输出解析失败,报json.decoder.JSONDecodeError。原因是模型在 JSON 外面包了 markdown 代码块或解释文字。解决办法是在 Skill 提示词里强化约束,同时在执行器里加清洗逻辑,就是前面executor.py里那段strip("")的处理。更稳妥的做法是用response_format={"type": "json_object"}` 参数,让模型直接返回 JSON。
第五类是 OAuth 或 Claude Code 配置报错。如果你用 Claude Code 接入,报OAuth error或authentication failed,检查.claude/settings.json里的三件套是否完整:ANTHROPIC_BASE_URL、ANTHROPIC_API_KEY、ANTHROPIC_MODEL。Base URL 用https://taotoken.net/api,不要带/v1。如果用了 CC Switch 或 Cline MCP,同样要确认这三项配置一致,任何一项缺失都会导致认证失败。
第六类是数据构造 Skill 输出字段关联错误,比如给 18 岁配了 50 万年薪。这不是通道问题,是提示词约束不够。解决办法是在 Skill 里显式声明字段依赖规则,比如“年龄小于 22 时月收入不超过 15k”,并在输出后用jsonschema做二次校验,不通过就重试。
排查的核心思路是分层定位:先确认通道通不通(curl 测试),再确认 Key 有没有问题(401 排查),最后确认输出解析(结构化校验)。大部分问题集中在通道和解析两层,业务逻辑本身反而很少出错。
6. 从 Skill 库到测试流水线:持续演进
跑通单条链路只是起点,真正让 Skills 库产生价值的是接入 CI 和建立反馈闭环。我的做法是在run_skill.py基础上加一个批量执行模式,读一个 YAML 描述的任务清单,依次调用对应 Skill,把结果汇总成报告。这样一条端到端测试流水线就成型了:数据构造 Skill 生成测试数据,断言 Skill 校验接口返回,多模态 Skill 验证 UI 截图,三个环节串起来就是完整的 AI 产品测试流程。
Skill 是会退化的,模型更新后之前好用的提示词可能变差。所以每个 Skill 目录下的samples.jsonl要维护一组回归样例,每次修改提示词后跑一遍,通过率低于阈值就回滚。同时记录每次执行的输入输出和人工反馈,定期 review 低分记录来优化 Skill 描述。没有反馈闭环的 Skill 库,用不了多久就会变成没人维护的数字垃圾堆。
对于需要长期跑 Agent 编排、高频调用模型的团队,Coding Plan 在成本和稳定性上更有优势,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。接入过程中遇到参数或配置问题,先查接入文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,大部分报错都能在里面找到对应说明。需要新建 Key 或做环境隔离时,去 API Keys 页面操作 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。
最后给一个实用建议:不要一上来就追求 Skill 数量。先把一个高频痛点场景做成 Skill,连续用一周,确认成功率稳定在 80% 以上,再复制这套模式扩展。Skill 库的竞争力不在于封装了多少个,而在于每个 Skill 被复用了多少次。当你的同事开始主动调用你写的 Skill 而不是自己重写时,这个库才算真正活起来了。