AI Agent 的 Skill 到底是什么?以一个 JMX 生成器为例讲清楚

🔥 难度等级:进阶 | ⏰ 阅读时间:12 分钟 | 💡 实用指数:⭐⭐⭐⭐⭐

之前每次做压测都要手写 JMX 文件,一个接口写半天,改个参数还要翻 XML 文档。后来研究了一下 AI Agent 的 Skill 机制,用几行配置就搞定了从接口文档到 JMX 的自动生成,现在 10 分钟能搞定以前 2 小时的活。今天把这个思路完整分享出来,看完你就知道 Skill 到底是个啥,以及怎么用它解决实际工作里的重复劳动。


一、Skill 是什么?一句话讲清楚

Skill 就是 AI 的外挂技能包

咱们平时跟 ChatGPT、Claude 聊天,靠的是模型自带的"通识能力"——你问啥它都能答一点,但碰到专业场景就经常"好像懂了又好像没懂"。

Skill 解决的就是这个问题:它把特定领域的知识、工具调用方式、处理流程打包成一个可插拔的模块,AI 遇到对应场景就加载这个模块,执行标准化操作。

打个比方:

  • Prompt 工程 = 你每次都要跟新手厨师口述一遍"番茄炒蛋怎么做"
  • Skill = 你直接把菜谱贴在墙上,厨师看到"番茄炒蛋"四个字就知道每一步该干嘛

Skill 的特点就三个:

  1. 可复用——写一次,所有会话都能用
  2. 确定性——不像 Prompt 那样每次结果都飘,Skill 里写的是死逻辑
  3. 可组合——多个 Skill 可以叠加,比如"读接口文档"+“生成 JMX”+“上传到 JMeter”

二、Prompt 工程 vs Skill:为什么后者更靠谱

我知道很多人会说:“我用 Prompt 也能让 AI 生成 JMX 啊,写个 Prompt 不就完了?”

确实能。但用 Prompt 搞过生产环境的人都知道,这里有三个坑:

Prompt 方案 Skill 方案
每次都要重复教 每次新开对话都要贴一遍格式要求 Skill 写好就常驻,自动触发
结果不稳定 这次生成的 XML 格式对了,下次可能就缺个闭合标签 Skill 里写死 XML 模板,输出格式固定
没法调用工具 Prompt 只能让 AI"描述"怎么做,实际执行还得你手动来 Skill 可以直接调本地脚本、写文件、发请求

举个例子,我要把 Swagger 接口文档转成 JMX。用 Prompt 的话,我得每次都说:

“请根据以下 JSON 生成 JMeter 的 JMX 文件,注意 ThreadGroup 要配置 10 个线程,Ramp-Up 5 秒,HTTPSamplerProxy 的 domain 要填 api.example.com…”

用 Skill 的话,AI 看到 Swagger JSON 就直接知道:

  1. 解析 paths 拿到所有接口
  2. 每个接口生成一个 HTTPSamplerProxy
  3. 统一套 ThreadGroup 模板
  4. 输出标准 JMX XML

不需要你教,它已经会了。


三、实战:我的"接口文档生成 JMX" Skill

好了,说那么多不如直接看代码。下面是我自用的 Skill,功能就是:输入 Swagger/YAPI 接口文档的 JSON,输出可直接导入 JMeter 的 .jmx 文件

3.1 Skill 的结构

Skill 本质上就是一个带元数据的 Markdown 文件,长这样:

api-to-jmx/
  SKILL.md          ← 核心逻辑和触发条件
  scripts/
    generate_jmx.py ← 生成 JMX 的实际脚本
  assets/
    jmx_template.xml ← JMX 的 XML 模板

3.2 SKILL.md 内容

---
name: api-to-jmx
description: "Parse Swagger/YAPI API docs and generate JMeter .jmx test plans."
---

# API to JMX

Generate JMeter test plans from API documentation.

## When to use
- User provides Swagger JSON or YAPI export
- User says "生成 JMX" / "转成 JMeter" / "压测脚本"
- User pastes API doc and mentions load testing

## Workflow

1. **Parse input**: Accept Swagger 2.0/OpenAPI 3.0 JSON or YAPI JSON.
2. **Extract endpoints**: For each `path` + `method`, collect:
   - URL (base + path)
   - HTTP method
   - Query/body parameters
   - Headers (Content-Type, auth tokens)
3. **Build JMX**: Use the XML template, insert one `HTTPSamplerProxy` per endpoint.
4. **Configure ThreadGroup**: Default 10 threads, 5s ramp-up, 1 loop (user can override).
5. **Output**: Write `.jmx` file to workspace.

## Parameters (user can override)
- `threads`: number of threads (default: 10)
- `ramp_up`: ramp-up period in seconds (default: 5)
- `duration`: test duration in seconds (default: 60)
- `base_url`: override the server URL from Swagger

## Output format
Standard JMeter JMX XML (version 5.6+ compatible).

3.3 核心生成脚本

# scripts/generate_jmx.py
import json
import xml.etree.ElementTree as ET
from xml.dom import minidom

def generate_jmx(swagger_json, threads=10, ramp_up=5, duration=60, base_url=None):
    """
    从 Swagger JSON 生成 JMeter JMX 文件
    """
    # 解析 Swagger
    data = json.loads(swagger_json)
    
    # 获取 base URL
    if base_url:
        server_url = base_url
    elif 'servers' in data:  # OpenAPI 3.0
        server_url = data['servers'][0]['url']
    elif 'host' in data:  # Swagger 2.0
        scheme = data.get('schemes', ['https'])[0]
        server_url = f"{scheme}://{data['host']}{data.get('basePath', '')}"
    else:
        server_url = "https://api.example.com"
    
    # 创建 JMX XML 根节点
    jmeter_test_plan = ET.Element("jmeterTestPlan", version="1.2", properties="5.0")
    hash_tree = ET.SubElement(jmeter_test_plan, "hashTree")
    
    # 添加 TestPlan
    test_plan = ET.SubElement(hash_tree, "TestPlan", guiclass="TestPlanGui", 
                              testclass="TestPlan", testname="API Load Test", enabled="true")
    ET.SubElement(test_plan, "stringProp", name="TestPlan.comments").text = "Auto-generated from Swagger"
    ET.SubElement(test_plan, "boolProp", name="TestPlan.functional_mode").text = "false"
    ET.SubElement(test_plan, "boolProp", name="TestPlan.tearDown_on_shutdown").text = "true"
    
    test_plan_hash = ET.SubElement(hash_tree, "hashTree")
    
    # 添加 ThreadGroup
    thread_group = ET.SubElement(test_plan_hash, "ThreadGroup", guiclass="ThreadGroupGui",
                                 testclass="ThreadGroup", testname="Thread Group", enabled="true")
    ET.SubElement(thread_group, "stringProp", name="ThreadGroup.on_sample_error").text = "continue"
    ET.SubElement(thread_group, "elementProp", name="ThreadGroup.main_controller", 
                  elementType="LoopController", guiclass="LoopControlPanel", 
                  testclass="LoopController", testname="Loop Controller", enabled="true")
    ET.SubElement(thread_group, "stringProp", name="ThreadGroup.num_threads").text = str(threads)
    ET.SubElement(thread_group, "stringProp", name="ThreadGroup.ramp_time").text = str(ramp_up)
    ET.SubElement(thread_group, "boolProp", name="ThreadGroup.scheduler").text = "true"
    ET.SubElement(thread_group, "stringProp", name="ThreadGroup.duration").text = str(duration)
    
    thread_hash = ET.SubElement(test_plan_hash, "hashTree")
    
    # 为每个接口生成 HTTPSamplerProxy
    paths = data.get('paths', {})
    for path, methods in paths.items():
        for method, details in methods.items():
            if method in ['get', 'post', 'put', 'delete', 'patch']:
                sampler = create_http_sampler(path, method.upper(), details, server_url)
                thread_hash.append(sampler)
                # 每个 sampler 后面加一个空的 hashTree
                thread_hash.append(ET.Element("hashTree"))
    
    # 美化输出 XML
    rough_string = ET.tostring(jmeter_test_plan, encoding='unicode')
    reparsed = minidom.parseString(rough_string)
    pretty = reparsed.toprettyxml(indent="  ")
    
    # 去掉空行
    lines = [line for line in pretty.split('\n') if line.strip()]
    return '\n'.join(lines)

def create_http_sampler(path, method, details, server_url):
    """创建单个 HTTP Sampler"""
    sampler = ET.Element("HTTPSamplerProxy", guiclass="HttpTestSampleGui",
                         testclass="HTTPSamplerProxy", 
                         testname=details.get('summary', details.get('operationId', path)),
                         enabled="true")
    
    # URL
    ET.SubElement(sampler, "stringProp", name="HTTPSampler.domain").text = server_url.replace("https://", "").replace("http://", "").split('/')[0]
    ET.SubElement(sampler, "stringProp", name="HTTPSampler.port").text = ""
    ET.SubElement(sampler, "stringProp", name="HTTPSampler.protocol").text = "https"
    ET.SubElement(sampler, "stringProp", name="HTTPSampler.path").text = path
    ET.SubElement(sampler, "stringProp", name="HTTPSampler.method").text = method
    
    # 如果有参数,添加到 arguments
    params = details.get('parameters', [])
    if params:
        args = ET.SubElement(sampler, "elementProp", name="HTTPsampler.Arguments",
                             elementType="Arguments", guiclass="HTTPArgumentsPanel",
                             testclass="Arguments", testname="User Defined Variables", enabled="true")
        collection = ET.SubElement(args, "collectionProp", name="Arguments.arguments")
        for param in params:
            if param.get('in') in ['query', 'path']:
                arg = ET.SubElement(collection, "elementProp", name=param['name'],
                                   elementType="Argument")
                ET.SubElement(arg, "stringProp", name="Argument.name").text = param['name']
                ET.SubElement(arg, "stringProp", name="Argument.value").text = "${" + param['name'].upper() + "}"
                ET.SubElement(arg, "stringProp", name="Argument.metadata").text = "="
    
    ET.SubElement(sampler, "boolProp", name="HTTPSampler.follow_redirects").text = "true"
    ET.SubElement(sampler, "boolProp", name="HTTPSampler.auto_redirects").text = "false"
    ET.SubElement(sampler, "boolProp", name="HTTPSampler.use_keepalive").text = "true"
    ET.SubElement(sampler, "boolProp", name="HTTPSampler.DO_MULTIPART_POST").text = "false"
    
    return sampler

# 使用示例
if __name__ == "__main__":
    swagger = '''
    {
      "swagger": "2.0",
      "host": "api.example.com",
      "basePath": "/v1",
      "paths": {
        "/users": {
          "get": {
            "summary": "Get user list",
            "parameters": [
              {"name": "page", "in": "query", "type": "integer"}
            ]
          }
        }
      }
    }
    '''
    
    jmx = generate_jmx(swagger, threads=50, ramp_up=10, duration=300)
    with open("api_test.jmx", "w", encoding="utf-8") as f:
        f.write(jmx)
    print("JMX 文件已生成: api_test.jmx")

3.4 使用效果

假设我拿到一个 Swagger JSON,复制粘贴给 AI,说一句"生成 JMX",然后 AI 就会:

  1. 自动触发 api-to-jmx Skill
  2. 解析 JSON 里的所有接口
  3. 生成标准的 JMX XML 文件
  4. 保存到本地

整个过程不需要我手写一行 XML,也不需要我每次都教 AI “JMX 文件长什么样”。


四、我还写了哪些测试相关的 Skill

既然搞清楚了 Skill 的机制,我就顺手搭了一套测试开发常用的 Skill 组合:

Skill 名称 功能 触发词
api-to-jmx 接口文档 → JMeter 脚本 “生成 JMX”
report-pusher 测试报告 → 飞书/钉钉群 “推送报告”
log-analyzer 分析压测日志,自动出瓶颈点 “分析日志”
bug-summarizer 批量 Bug 列表 → 分类汇总 “总结 Bug”
env-checker 检查测试环境各服务状态 “检查环境”

这些 Skill 之间还能联动。比如:

生成 JMX → 执行压测 → 分析日志 → 推送报告

一条指令串下来,以前半天的活现在几分钟搞定。


五、怎么开始写你自己的 Skill

如果你也想试试,步骤很简单:

  1. 找个重复劳动的场景——你每周都要做、每次都要教 AI 的事
  2. 写一个 SKILL.md——描述触发条件、处理流程、输出格式
  3. 配一个脚本(可选)——复杂逻辑用 Python/Shell 写,Skill 里调用
  4. 放到 AI 的 skill 目录——OpenClaw 这类框架会自动加载

一个好的 Skill 判断标准:

  • 你自己用 Prompt 写过 3 次以上
  • 每次写 Prompt 都觉得"怎么又要说一遍"
  • 有明确的输入格式和输出格式
  • 流程固定,不需要 AI “自由发挥”

六、总结

Skill 不是啥高大上的新概念,本质上就是把"反复教 AI 的东西"固化下来。

对于测试开发来说,特别适合做 Skill 的场景:

  • 接口文档转各种格式(JMX、Postman collection、YAML)
  • 测试报告的格式化与推送
  • 环境检查的标准化脚本
  • 常见报错的自动诊断

Prompt 工程解决的是"怎么让 AI 听懂",Skill 解决的是"让 AI 记住并自动执行"。 两者不冲突,但到生产环境里,Skill 的稳定性和效率明显更靠谱。


💡 原创不易,如果这篇文章帮到了你,麻烦点个赞、收藏一下,也欢迎关注我的博客,后续会持续分享测试开发相关的实战干货。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐