【AI测试系统】第4篇:告别硬编码!基于 Markdown + Python 的 Skill 引擎设计:让 AI 测试系统拥有无限扩展的“灵魂”

一、为什么需要 Skill 引擎?

1.1 传统测试系统的痛点

在传统的测试系统中,新增一个测试能力(比如代码检查、浏览器自动化、性能压测)通常需要:

  1. 修改代码:在核心逻辑中硬编码新的测试类型
  2. 重新部署:修改代码后需要重新构建和部署
  3. 高度耦合:测试逻辑与系统核心代码紧密耦合,难以维护
  4. 缺乏规范:不同开发者实现的测试能力风格迥异,难以统一
python 复制代码
# ❌ 反模式:硬编码测试类型
def run_test(test_type, config):
    if test_type <span class="wx-em-red"> "api":
        return run_api_test(config)
    elif test_type </span> "ui":
        return run_ui_test(config)
    elif test_type <span class="wx-em-red"> "performance":
        return run_performance_test(config)
    # 每新增一个测试类型,就要修改这里
    elif test_type </span> "security":
        return run_security_test(config)

1.2 Skill 引擎的设计理念

Skill 引擎的核心思想是:声明式配置 + 插件式执行

复制代码
┌─────────────────────────────────────────────────────────┐
│                    用户编写 MD 配置                      │
│              (声明式:输入/输出/参数/依赖)               │
├─────────────────────────────────────────────────────────┤
│                    SkillEngine 解析                      │
│          YAML Frontmatter → 配置字典 → 缓存              │
├─────────────────────────────────────────────────────────┤
│                  SkillWrapper 桥接                       │
│     懒加载执行器 + 输入验证 + 执行日志 + 错误处理         │
├─────────────────────────────────────────────────────────┤
│              Python 执行器(插件式)                      │
│   TestCaseGenExecutor / HealerExecutor / GenericExecutor │
└─────────────────────────────────────────────────────────┘

核心优势:

  • 声明式:用 Markdown + YAML 定义技能,无需修改代码
  • 插件式:新增技能只需添加 MD 文件 + 执行器类
  • 可验证:输入参数自动验证,类型/长度/必填检查
  • 可观测:执行日志、耗时统计、错误追踪
  • 可降级:专用执行器不存在时,自动使用通用执行器

二、Skill 引擎架构解析

2.1 核心类图

python 复制代码
SkillEngine(引擎)
    ├── loaded_skills: Dict[str, dict]     # 已加载的技能配置缓存
    ├── executors: Dict[str, object]       # 执行器实例缓存
    ├── load_skill(name) -> SkillWrapper   # 加载单个技能
    ├── list_skills() -> List[dict]        # 列出所有技能
    └── reload_all()                       # 重新加载所有技能

SkillWrapper(包装器)
    ├── engine: SkillEngine                # 引擎引用
    ├── name: str                          # 技能名称
    ├── config: dict                       # YAML 配置
    ├── executor: SkillExecutor            # 懒加载执行器
    ├── execute(**kwargs) -> Any           # 执行技能
    ├── _validate_inputs(inputs)           # 验证输入参数
    └── _log(level, message, data)         # 记录日志

SkillExecutor(执行器基类)
    ├── config: dict                       # 配置
    └── execute(**kwargs) -> Any           # 执行(子类实现)

GenericExecutor(通用执行器)
    └── execute(**kwargs) -> Any           # 通用执行逻辑
        └── _execute_python(code, context) # 执行 Python 代码片段

2.2 文件结构

bash 复制代码
backend/app/skills/
├── __init__.py              # 导出便捷函数
├── engine.py                # SkillEngine + SkillWrapper + 驼峰转蛇形
├── test_case_gen.py         # 测试用例生成 Skill(第 6 篇已讲)
├── healer.py                # 自愈修复 Skill
├── explorer.py              # 探索性测试 Skill
├── StaticCheck.md           # 静态代码检查技能配置
├── BrowserUse.md            # 浏览器自动化技能配置
└── executors/
    ├── __init__.py
    ├── base.py              # SkillExecutor 基类 + 安全沙箱 GenericExecutor
    ├── test_case_gen_executor.py    # 真实 LLM + 三级降级
    ├── healer_executor.py           # 自愈修复执行器
    ├── explorer_executor.py         # 探索性测试执行器
    ├── static_check_executor.py     # 静态代码检查执行器
    └── browser_use_executor.py      # 浏览器自动化执行器

三、YAML Frontmatter:声明式技能定义

3.1 什么是 YAML Frontmatter?

YAML Frontmatter 是 Markdown 文件中位于 --- 之间的 YAML 配置块,位于文件开头。它起源于 Jekyll、Hugo 等静态网站生成器,用于定义页面的元数据。

yaml 复制代码
---
# YAML 配置从这里开始
name: StaticCheck
version: 1.0.0
description: 静态代码检查
inputs:
  - name: code_path
    type: string
    required: true
---
<!-- Markdown 内容从这里开始 -->

# 使用说明
...

3.2 Skill 的 YAML 结构规范

一个完整的 Skill 配置文件包含以下部分:

yaml 复制代码
---
# ===<span class="wx-em-red"> 1. 元数据 </span>===
name: StaticCheck                    # 技能名称(必填,唯一标识)
version: 1.0.0                       # 版本号
author: AI 测试系统                   # 作者
description: 静态代码检查              # 描述
category: analyzer                   # 分类(analyzer/executor/generator)
tags: [代码检查,Ruff,Semgrep]        # 标签

# ===<span class="wx-em-red"> 2. 输入定义 </span>===
inputs:
  - name: code_path                  # 参数名
    type: string                     # 类型:string/integer/object/array/boolean
    required: true                   # 是否必填
    description: 代码路径              # 描述
    min_length: 1                    # 最小长度(string 类型)
    max_length: 500                  # 最大长度(string 类型)
  
  - name: language
    type: string
    required: false
    default: "python"                # 默认值
    enum: [python, javascript, all]  # 枚举值

# ===<span class="wx-em-red"> 3. 输出定义 </span>===
outputs:
  - name: success
    type: boolean
    description: 是否成功

# ===<span class="wx-em-red"> 4. 执行配置 </span>===
execution:
  prompt: |                          # Prompt 模板(LLM 类技能)
    请分析以下代码:{{requirement}}
  quality_check:
    enabled: true
    thresholds:
      completeness: 0.3
      clarity: 0.3
      automation: 0.2

# ===<span class="wx-em-red"> 5. 工具配置(可选) </span>===
tools:
  ruff:
    enabled: true
    command: "ruff check {code_path}"
    timeout: 300

# ===<span class="wx-em-red"> 6. 依赖配置 </span>===
dependencies:
  - name: ruff
    version: ">=0.1"
    install_command: "pip install ruff"

# ===<span class="wx-em-red"> 7. 日志配置 </span>===
logging:
  level: info                        # 日志级别:debug/info/warning/error
  log_input: true
  log_output: true
  log_latency: true
---

3.3 实际案例:StaticCheck 技能

StaticCheck.md 为例,这是一个真实的静态代码检查技能:

yaml 复制代码
---
name: StaticCheck
version: 1.0.0
description: 静态代码检查技能 - 自动扫描代码中的安全漏洞、代码规范问题
category: quality
inputs:
  - name: code
    type: string
    required: true
    description: 待检查的代码内容
  - name: language
    type: string
    required: false
    default: python
    description: 编程语言
outputs:
  - name: issues
    type: array
    description: 发现的问题列表
tools:
  - name: flake8
    description: Python 代码规范检查
  - name: semgrep
    description: 安全漏洞扫描
  - name: bandit
    description: Python 安全扫描
dependencies:
  - name: flake8
    version: ">=6.0.0"
  - name: semgrep
    version: ">=1.0.0"
filter:
  min_severity: medium
logging:
  level: info
execution:
  timeout: 60
  retry: 2
---

设计亮点: 设计亮点:

  • 自描述:输入/输出/依赖/工具全部在配置中声明
  • 可验证:类型/长度/枚举值自动检查
  • 可扩展 :新增工具只需在 tools 中添加配置
  • 可过滤:支持按严重程度/规则/目录过滤
  • 可观测:日志级别/输入输出/耗时全部可配置

四、SkillEngine:引擎核心实现

4.1 初始化与目录发现

python 复制代码
# backend/app/skills/engine.py

class SkillEngine:
    """
    Skill 执行引擎
    
    架构:
    1. 用户编写 md 配置(声明式)
    2. 引擎解析 md 配置
    3. 调用 Python 执行器执行实际逻辑
    """
    
    def __init__(self, skills_dir: str = None):
        self.skills_dir = skills_dir or self._default_skills_dir()
        self.loaded_skills: Dict[str, dict] = {}  # 技能配置缓存
        self.executors: Dict[str, object] = {}    # 执行器缓存
    
    def _default_skills_dir(self) -> str:
        """默认 Skills 目录"""
        return os.path.join(
            os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
            'skills'
        )

关键设计:

  • skills_dir 可配置,默认指向 backend/app/skills/
  • loaded_skills 缓存已解析的配置,避免重复读取文件
  • executors 缓存执行器实例,避免重复创建

4.2 YAML Frontmatter 解析

python 复制代码
def _parse_markdown(self, path: str) -> dict:
    """
    解析 Markdown 文件(提取 YAML Frontmatter)
    
    Args:
        path: md 文件路径
    
    Returns:
        配置字典
    """
    with open(path, 'r', encoding='utf-8') as f:
        content = f.read()
    
    # 提取 YAML Frontmatter(--- 之间的内容)
    match = re.match(r'^---\s*\n(.*?)\n---\s*\n', content, re.DOTALL)
    
    if not match:
        raise ValueError(f"无效的 Skill 格式,缺少 YAML Frontmatter:{path}")
    
    yaml_content = match.group(1)
    
    # 解析 YAML
    config = yaml.safe_load(yaml_content)
    
    # 存储原始内容(提示词等)
    config['_raw_content'] = content
    config['_path'] = path
    
    return config

正则表达式解析逻辑:

ruby 复制代码
^---\s*\n    # 文件开头必须是 ---
(.*?)        # 捕获 YAML 内容(非贪婪)
\n---\s*\n   # 结束标记 ---

安全考虑:

  • 使用 yaml.safe_load() 而非 yaml.load(),防止任意代码执行
  • 保留 _raw_content_path 供后续使用(如 Prompt 模板)

4.3 技能加载流程

python 复制代码
def load_skill(self, skill_name: str) -> 'SkillWrapper':
    """
    加载 Skill
    
    Args:
        skill_name: Skill 名称(如 TestCaseGen)
    
    Returns:
        Skill 包装器
    """
    # 1. 查找 md 文件
    md_path = os.path.join(self.skills_dir, f"{skill_name}.md")
    
    if not os.path.exists(md_path):
        raise FileNotFoundError(f"Skill 未找到:{skill_name},路径:{md_path}")
    
    # 2. 解析 md 文件
    config = self._parse_markdown(md_path)
    
    # 3. 缓存配置
    self.loaded_skills[skill_name] = config
    
    # 4. 创建包装器
    wrapper = SkillWrapper(self, skill_name, config)
    
    return wrapper

调用示例:

ini 复制代码
from app.skills.engine import SkillEngine

engine = SkillEngine()

# 加载技能
skill = engine.load_skill('StaticCheck')

# 执行技能
result = skill.execute(
    code_path="/home/liu/ai-test-system/backend",
    language="python",
    check_types=["lint", "security"],
    severity="warning"
)

4.4 技能列表与热重载

python 复制代码
def list_skills(self) -> List[dict]:
    """列出所有可用 Skills"""
    skills = []
    
    if not os.path.exists(self.skills_dir):
        return skills
    
    for file in os.listdir(self.skills_dir):
        if file.endswith('.md'):
            skill_name = file[:-3]  # 去掉 .md
            try:
                wrapper = self.load_skill(skill_name)
                skills.append({
                    'name': skill_name,
                    'version': wrapper.config.get('version', '1.0.0'),
                    'description': wrapper.config.get('description', ''),
                    'category': wrapper.config.get('category', 'general'),
                })
            except Exception as e:
                skills.append({
                    'name': skill_name,
                    'error': str(e),
                })
    
    return skills

def reload_all(self):
    """重新加载所有 Skills"""
    self.loaded_skills.clear()
    for file in os.listdir(self.skills_dir):
        if file.endswith('.md'):
            skill_name = file[:-3]
            self.load_skill(skill_name)

实际运行效果:

shell 复制代码
skills = engine.list_skills()
# 输出:
# [
#   {'name': 'StaticCheck', 'version': '1.0.0', 'description': '静态代码检查...', 'category': 'analyzer'},
#   {'name': 'BrowserUse', 'version': '1.0.0', 'description': 'AI 驱动的浏览器自动化...', 'category': 'executor'},
#   {'name': 'TestCaseGen', 'version': '1.0.0', 'description': '测试用例生成...', 'category': 'generator'},
# ]

五、SkillWrapper:桥接层实现

5.1 懒加载执行器

python 复制代码
class SkillWrapper:
    """
    Skill 包装器
    
    将 md 配置与 Python 执行器桥接
    """
    
    def __init__(self, engine: SkillEngine, name: str, config: dict):
        self.engine = engine
        self.name = name
        self.config = config  # YAML 配置(dict)
        self._executor = None
    
    @property
    def executor(self):
        """懒加载执行器"""
        if self._executor is None:
            self._executor = self._create_executor()
        return self._executor
    
    def _create_executor(self):
        """创建对应的 Python 执行器"""
        # 动态导入执行器
        try:
            # 驼峰转蛇形命名:StaticCheck → static_check
            def camel_to_snake(name):
                s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
                return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
            module_name = f"app.skills.executors.{camel_to_snake(self.name)}_executor"
            module = __import__(module_name, fromlist=[''])
            executor_class = getattr(module, f"{self.name}Executor")
            return executor_class(self.config)
        except ImportError:
            # 如果执行器不存在,使用通用执行器
            print(f"⚠️ 未找到专用执行器,使用通用执行器:{self.name}")
            from .executors.base import GenericExecutor
            return GenericExecutor(self.config)

动态导入逻辑(驼峰转蛇形):

arduino 复制代码
Skill 名称:StaticCheck
→ camel_to_snake("StaticCheck") → "static_check"
→ 模块名:app.skills.executors.static_check_executor
→ 类名:StaticCheckExecutor
→ 如果不存在,降级到 GenericExecutor

驼峰转蛇形函数:

python 复制代码
def camel_to_snake(name):
    s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
    return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()

# 转换示例:
# StaticCheck  → static_check
# BrowserUse   → browser_use
# TestCaseGen  → test_case_gen
# Healer       → healer

降级机制的价值:

  • 新增 Skill 时,可以先只写 MD 配置
  • 系统自动使用 GenericExecutor 兜底
  • 后续再实现专用执行器,无需修改引擎代码

5.2 输入参数验证

python 复制代码
def _validate_inputs(self, inputs: dict):
    """验证输入参数"""
    input_defs = self.config.get('inputs', [])
    
    for input_def in input_defs:
        name = input_def['name']
        required = input_def.get('required', False)
        
        # 检查必填
        if required and name not in inputs:
            raise ValueError(f"缺少必填参数:{name}")
        
        # 检查类型
        if name in inputs:
            value = inputs[name]
            expected_type = input_def.get('type')
            
            if expected_type <span class="wx-em-red"> 'string' and not isinstance(value, str):
                raise TypeError(f"参数 {name} 应为 string 类型")
            
            if expected_type </span> 'integer' and not isinstance(value, int):
                raise TypeError(f"参数 {name} 应为 integer 类型")
            
            if expected_type <span class="wx-em-red"> 'object' and not isinstance(value, dict):
                raise TypeError(f"参数 {name} 应为 object 类型")
            
            if expected_type </span> 'array' and not isinstance(value, list):
                raise TypeError(f"参数 {name} 应为 array 类型")
            
            # 检查长度限制
            if expected_type <span class="wx-em-red"> 'string':
                min_len = input_def.get('min_length')
                max_len = input_def.get('max_length')
                
                if min_len and len(value) < min_len:
                    raise ValueError(f"参数 {name} 长度不能小于 {min_len}")
                
                if max_len and len(value) > max_len:
                    raise ValueError(f"参数 {name} 长度不能大于 {max_len}")

验证规则:

规则 说明 示例
required 是否必填 required: true → 缺少则抛异常
type 类型检查 type: string → 必须是字符串
min_length 最小长度 min_length: 5 → 至少 5 个字符
max_length 最大长度 max_length: 500 → 最多 500 个字符
enum 枚举值 enum: [python, javascript] → 只能选这些

验证示例:

ini 复制代码
skill = engine.load_skill('StaticCheck')

# ✅ 正确调用
skill.execute(code_path="/path/to/code", language="python")

# ❌ 缺少必填参数
skill.execute(language="python")
# ValueError: 缺少必填参数:code_path

# ❌ 类型错误
skill.execute(code_path=123, language="python")
# TypeError: 参数 code_path 应为 string 类型

# ❌ 长度超限
skill.execute(code_path="a" * 600, language="python")
# ValueError: 参数 code_path 长度不能大于 500

5.3 执行与日志记录

python 复制代码
def execute(self, **kwargs) -> Any:
    """
    执行 Skill
    
    Args:
        **kwargs: 输入参数
    
    Returns:
        执行结果
    """
    # 1. 验证输入
    self._validate_inputs(kwargs)
    
    # 2. 记录开始时间
    start_time = datetime.utcnow()
    
    try:
        # 3. 执行
        result = self.executor.execute(**kwargs)
        
        # 4. 记录日志
        self._log('info', f"执行完成:{self.name}", {
            'duration_ms': (datetime.utcnow() - start_time).total_seconds() * 1000,
            'inputs': kwargs,
            'result': result,
        })
        
        return result
        
    except Exception as e:
        # 5. 错误处理
        self._log('error', f"执行失败:{self.name}", {
            'error': str(e),
            'inputs': kwargs,
        })
        raise

def _log(self, level: str, message: str, data: dict = None):
    """记录日志"""
    log_config = self.config.get('logging', {})
    log_level = log_config.get('level', 'info')
    
    levels = {'debug': 0, 'info': 1, 'warning': 2, 'error': 3}
    
    if levels.get(level, 1) >= levels.get(log_level, 1):
        print(f"[{level.upper()}] {self.name}: {message}")
        if data:
            print(f"  Data: {json.dumps(data, ensure_ascii=False, indent=2)[:200]}...")

执行流程:

markdown 复制代码
输入参数 → 验证 → 执行 → 记录日志 → 返回结果
                    ↓ 失败
              记录错误日志 → 抛出异常

日志输出示例:

csharp 复制代码
[INFO] StaticCheck: 执行完成:StaticCheck
  Data: {
    "duration_ms": 12345.67,
    "inputs": {
      "code_path": "/home/liu/ai-test-system/backend",
      "language": "python"
    },
    "result": {
      "success": true,
      "total_issues": 15
    }
  }...

六、执行器体系:从基类到专用执行器

6.1 SkillExecutor 基类

python 复制代码
# backend/app/skills/executors/base.py

class SkillExecutor:
    """Skill 执行器基类"""
    
    def __init__(self, config: dict):
        self.config = config
    
    def execute(self, **kwargs) -> Any:
        """
        执行 Skill
        
        Args:
            **kwargs: 输入参数
        
        Returns:
            执行结果
        """
        raise NotImplementedError("子类必须实现 execute 方法")
    
    def validate_inputs(self, inputs: dict) -> bool:
        """验证输入参数"""
        return True
    
    def format_output(self, result: Any, format: str = 'json') -> Any:
        """格式化输出"""
        return result

设计原则:

  • execute() 是抽象方法,子类必须实现
  • validate_inputs()format_output() 提供默认实现,子类可覆盖

6.2 GenericExecutor 通用执行器

python 复制代码
class GenericExecutor(SkillExecutor):
    """
    通用执行器
    
    当没有专用执行器时使用
    """
    
    def execute(self, **kwargs) -> Any:
        """通用执行逻辑"""
        # 获取执行配置
        execution_config = self.config.get('execution', {})
        
        # 如果有 Python 代码,执行代码
        python_code = execution_config.get('python_code')
        if python_code:
            return self._execute_python(python_code, kwargs)
        
        # 否则返回配置信息
        return {
            'skill': self.config.get('name'),
            'status': 'configured',
            'message': '未找到专用执行器,请实现对应的 Python 执行器',
        }
    
    def _execute_python(self, code: str, context: dict) -> Any:
        """执行 Python 代码片段(受限沙箱)"""
        # 受限安全沙箱:只提供安全内置函数
        safe_builtins = {
            'len': len, 'str': str, 'int': int, 'float': float,
            'list': list, 'dict': dict, 'tuple': tuple, 'set': set,
            'range': range, 'enumerate': enumerate, 'zip': zip,
            'min': min, 'max': max, 'sum': sum, 'abs': abs,
            'sorted': sorted, 'reversed': reversed,
            'isinstance': isinstance, 'type': type,
            'True': True, 'False': False, 'None': None,
        }
        safe_globals = {'__builtins__': safe_builtins}
        local_vars = {'inputs': context, 'result': None}
        exec(code, safe_globals, local_vars)
        return local_vars.get('result')

安全沙箱设计:

  • ❌ 旧版:exec(code, {}, local_vars) --- 空 globals 导致连 len/str 等内置函数都不可用
  • ✅ 新版:exec(code, {'__builtins__': safe_builtins}, local_vars) --- 提供安全的内置函数子集
  • 🔒 不可用:__import__openevalexecossys 等危险函数

使用场景:

  • 简单技能:只需在 MD 中写 Python 代码片段
  • 过渡期:专用执行器开发完成前的临时方案
  • 原型验证:快速验证技能设计是否合理

示例:

yaml 复制代码
---
name: Hello
execution:
  python_code: |
    result = f"Hello, {inputs.get('name', 'World')}!"
---
lua 复制代码
skill = engine.load_skill('Hello')
result = skill.execute(name="AI 测试系统")
# 输出:{'skill': 'Hello', 'status': 'configured', 'message': '...'}
# 如果配置了 python_code,则执行并返回结果

6.3 TestCaseGenExecutor 专用执行器

以测试用例生成执行器为例,展示专用执行器的完整实现:

python 复制代码
# backend/app/skills/executors/test_case_gen_executor.py

class TestCaseGenExecutor(SkillExecutor):
    """TestCaseGen 执行器"""
    
    def __init__(self, config: dict):
        super().__init__(config)
        # LLM 客户端通过 DashScope API 调用,无需预初始化
    
    def execute(self, requirement: str, context: dict = None, **kwargs) -> Dict:
        """
        执行测试用例生成
        
        Args:
            requirement: 需求描述
            context: 上下文信息
            **kwargs: 其他参数
        
        Returns:
            生成的测试用例
        """
        # 1. 输入验证
        self._validate_requirement(requirement)
        
        # 2. 构建提示词
        prompt = self._build_prompt(requirement, context)
        
        # 3. 调用 LLM(真实 API + 三级降级)
        llm_response = self._call_llm(prompt, requirement)
        
        # 4. 解析响应
        test_cases = self._parse_response(llm_response)
        
        # 5. 质量评分
        if self.config.get('execution', {}).get('quality_check', {}).get('enabled', True):
            for case in test_cases:
                case['quality_score'] = self._calculate_quality_score(case)
        
        return {
            'test_cases': test_cases,
            'count': len(test_cases),
            'average_quality': sum(c.get('quality_score', 0) for c in test_cases) / len(test_cases) if test_cases else 0,
        }
    
    def _build_prompt(self, requirement: str, context: dict = None) -> str:
        """构建提示词"""
        prompt_template = self.config.get('execution', {}).get('prompt', '')
        
        # 替换变量
        prompt = prompt_template.replace('{{requirement}}', requirement)
        
        if context:
            context_str = json.dumps(context, ensure_ascii=False, indent=2)
            prompt = prompt.replace('{{context}}', context_str)
        else:
            prompt = prompt.replace('{{context}}', '无')
        
        return prompt
    
    def _call_llm(self, prompt: str, requirement: str = "") -> str:
        """调用 LLM - 使用 DashScope API,三级降级策略"""
        import httpx
        import os
        
        api_key = os.getenv("DASHSCOPE_API_KEY", "")
        
        if not api_key:
            # 第一级降级:规则引擎
            return self._get_rule_engine_response(requirement)
        
        try:
            response = httpx.post(
                "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation",
                headers={
                    "Authorization": f"Bearer {api_key}",
                    "Content-Type": "application/json"
                },
                json={
                    "model": "qwen3.5-plus",
                    "input": {
                        "messages": [
                            {"role": "system", "content": "你是一个专业的测试工程师。"},
                            {"role": "user", "content": prompt}
                        ]
                    },
                    "parameters": {
                        "temperature": 0.7,
                        "max_tokens": 2000
                    }
                },
                timeout=30.0
            )
            
            if response.status_code </span> 200:
                result = response.json()
                return result.get("output", {}).get("text", "")
            else:
                print(f"LLM API 调用失败:{response.status_code},降级到规则引擎")
                return self._get_rule_engine_response(requirement)
                
        except Exception as e:
            print(f"LLM API 调用异常:{e},降级到规则引擎")
            return self._get_rule_engine_response(requirement)
    
    def _get_rule_engine_response(self, requirement: str) -> str:
        """第二级降级:规则引擎生成用例"""
        try:
            from ..services.enhanced_rule_engine import EnhancedRuleEngine
            engine = EnhancedRuleEngine()
            if requirement:
                cases = engine.generate_cases_from_requirement(requirement)
                if cases:
                    return json.dumps({"test_cases": cases}, ensure_ascii=False)
        except Exception as e:
            print(f"规则引擎降级失败:{e},使用 Mock 兜底")
        
        return self._get_mock_response()
    
    def _get_mock_response(self) -> str:
        """第三级降级:Mock 兜底"""
        mock_response = {
            "test_cases": [
                {
                    "title": "正常登录流程",
                    "description": "用户使用正确的用户名和密码登录",
                    "priority": "P0",
                    "type": "functional",
                    "preconditions": ["用户已注册", "网络连接正常"],
                    "steps": [
                        {"step": 1, "action": "打开登录页面", "expected": "页面加载成功"},
                        {"step": 2, "action": "输入正确的用户名", "expected": "输入框显示用户名"},
                        {"step": 3, "action": "输入正确的密码", "expected": "输入框显示为密文"},
                        {"step": 4, "action": "点击登录按钮", "expected": "登录成功,跳转到首页"}
                    ],
                    "test_data": "用户名:<你的邮箱>, 密码:<你的密码>",
                    "automation": True
                },
                {
                    "title": "密码错误登录",
                    "description": "用户使用错误的密码登录",
                    "priority": "P1",
                    "type": "functional",
                    "preconditions": ["用户已注册"],
                    "steps": [
                        {"step": 1, "action": "打开登录页面", "expected": "页面加载成功"},
                        {"step": 2, "action": "输入正确的用户名", "expected": "输入框显示用户名"},
                        {"step": 3, "action": "输入错误的密码", "expected": "提示密码错误"},
                        {"step": 4, "action": "点击登录按钮", "expected": "登录失败"}
                    ],
                    "test_data": "用户名:<你的邮箱>, 密码:WrongPassword",
                    "automation": True
                }
            ]
        }
        return json.dumps(mock_response, ensure_ascii=False)
    
    def _parse_response(self, response: str) -> List[Dict]:
        """解析 LLM 响应"""
        try:
            data = json.loads(response)
            return data.get("test_cases", [])
        except json.JSONDecodeError:
            # 尝试从文本中提取 JSON
            json_match = re.search(r'{.*}', response, re.DOTALL)
            if json_match:
                try:
                    data = json.loads(json_match.group())
                    return data.get("test_cases", [])
                except:
                    pass
            return []
    
    def _calculate_quality_score(self, test_case: Dict) -> float:
        """计算用例质量分数"""
        score = 0.0
        thresholds = self.config.get('execution', {}).get('quality_check', {}).get('thresholds', {})
        
        # 完整性(步骤数 >= 3)
        steps = test_case.get("steps", [])
        if len(steps) >= 3:
            score += thresholds.get('completeness', 0.3)
        
        # 清晰度(标题 > 5 字符,描述 > 10 字符)
        title = test_case.get("title", "")
        description = test_case.get("description", "")
        if len(title) > 5 and len(description) > 10:
            score += thresholds.get('clarity', 0.3)
        
        # 优先级(P0=0.2, P1=0.15, P2=0.1, P3=0.05)
        priority = test_case.get("priority", "P3")
        priority_scores = {"P0": 0.2, "P1": 0.15, "P2": 0.1, "P3": 0.05}
        score += priority_scores.get(priority, 0.05)
        
        # 可自动化
        if test_case.get("automation", False):
            score += thresholds.get('automation', 0.2)
        
        return min(score, 1.0)

执行流程:

复制代码
需求描述 → 构建 Prompt → 调用 LLM → 解析响应 → 质量评分 → 返回结果

质量评分算法:

维度 权重 评分标准
完整性 30% 步骤数 >= 3
清晰度 30% 标题 > 5 字符,描述 > 10 字符
优先级 20% P0=0.2, P1=0.15, P2=0.1, P3=0.05
可自动化 20% automation=True

6.3.1 三级降级策略

_call_llm 实现了完整的三级降级策略,确保系统在任何情况下都能返回测试用例:

vbnet 复制代码
LLM 调用(qwen3.5-plus, 30s 超时)
  ↓ 失败(无 API Key / 网络错误 / 超时)
规则引擎(EnhancedRuleEngine, ~10ms, 免费)
  ↓ 失败(ImportError / 生成失败)
Mock 兜底(2 条硬编码登录用例)

降级策略对比:

级别 方案 耗时 成本 用例数 适用场景
第一级 LLM API 5-30s ~0.05-0.1 元 10-20 条 正常情况,生成创意用例
第二级 规则引擎 ~10ms 0 元 15-25 条 API 不可用时,生成针对性用例
第三级 Mock 兜底 <1ms 0 元 2 条 最后兜底,保证系统可用

设计原则:

  • ✅ LLM 是主力:生成创意、边界、异常场景用例
  • ✅ 规则引擎是保障:免费、快速、生成针对性用例
  • ✅ Mock 是底线:确保系统永远不会返回空结果

6.4 HealerExecutor 自愈执行器

python 复制代码
# backend/app/skills/executors/healer_executor.py

class HealerExecutor(SkillExecutor):
    """Healer 执行器"""
    
    def __init__(self, config: dict):
        super().__init__(config)
        self.error_patterns = self._load_error_patterns()
        self.repair_strategies = self.config.get('repair_strategies', {})
    
    def _load_error_patterns(self) -> Dict[str, List[str]]:
        """加载错误模式"""
        return self.config.get('error_classification', {}).get('patterns', {})
    
    def execute(self, error_message: str, context: dict = None, **kwargs) -> Dict:
        """执行自愈流程"""
        # 1. 诊断错误
        diagnosis = self.diagnose(error_message, context)
        
        # 2. 判断是否可自动修复
        auto_repair_config = self.config.get('auto_repair', {})
        if auto_repair_config.get('enabled', True) and diagnosis.get('confidence', 0) >= auto_repair_config.get('min_confidence', 0.7):
            repair_result = self.execute_repair(diagnosis, context)
            diagnosis['repair_result'] = repair_result
        
        return diagnosis
    
    def diagnose(self, error_message: str, context: dict = None) -> Dict:
        """诊断错误"""
        error_type = self._classify_error(error_message)
        
        # 计算置信度
        confidence = self._calculate_confidence(error_type, error_message, context)
        
        # 分析根本原因
        root_cause = self._analyze_root_cause(error_type, error_message, context)
        
        return {
            'error_type': error_type,
            'confidence': confidence,
            'root_cause': root_cause,
            'auto_repairable': error_type in self.repair_strategies,
            'suggested_fix': self._get_suggested_fix(error_type),
        }
    
    def _classify_error(self, error_message: str) -> str:
        """分类错误类型"""
        error_lower = error_message.lower()
        
        for error_type, patterns in self.error_patterns.items():
            for pattern in patterns:
                if re.search(pattern.lower(), error_lower):
                    return error_type
        
        return 'unknown'
    
    def _calculate_confidence(self, error_type: str, error_message: str, context: dict = None) -> float:
        """计算置信度"""
        confidence_config = self.config.get('confidence', {})
        base_scores = confidence_config.get('base_scores', {})
        
        # 基础分数
        confidence = base_scores.get(error_type, 0.5)
        
        return min(1.0, max(0.0, confidence))
    
    def _analyze_root_cause(self, error_type: str, error_message: str, context: dict = None) -> str:
        """分析根本原因"""
        causes = {
            'network': '网络连接问题或服务不可用',
            'element': '元素不存在或选择器错误',
            'assertion': '实际结果与预期不符',
            'authentication': '认证 token 过期或无效',
        }
        
        return causes.get(error_type, '未知错误')
    
    def _get_suggested_fix(self, error_type: str) -> str:
        """获取建议修复方案"""
        fixes = {
            'network': '检查网络连接,稍后重试',
            'element': '尝试使用更稳定的选择器,或添加等待逻辑',
            'assertion': '检查测试数据或业务逻辑是否变更',
            'authentication': '刷新 token 后重试',
        }
        
        return fixes.get(error_type, '请检查错误日志')

错误分类体系:

错误类型 匹配模式 置信度 修复策略
network connection refused, timeout, 502, 503, 504 0.8 重试(指数退避)
element element not found, stale element, not interactable 0.75 等待 + 重试 / 选择器降级
authentication 401, 403, token expired, session expired 0.9 重新登录 / 刷新 Token
assertion assertion failed, expected, actual 0.6 截图 + 日志收集

自愈决策树:

复制代码
错误发生
  ↓
错误分类(正则匹配)
  ↓
计算置信度
  ↓
置信度 >= 阈值?
  ├─ 是 → 执行自动修复 → 返回修复结果
  └─ 否 → 返回诊断建议 → 人工介入

6.5 StaticCheckExecutor 静态代码检查执行器

python 复制代码
# backend/app/skills/executors/static_check_executor.py

class StaticCheckExecutor(SkillExecutor):
    """StaticCheck 执行器"""
    
    def __init__(self, config: dict):
        super().__init__(config)
        self.tools = config.get('tools', [])
    
    def execute(self, code: str, language: str = "python", **kwargs) -> Dict:
        """执行静态代码检查"""
        issues = []
        
        if language <span class="wx-em-red"> "python":
            issues.extend(self._check_python(code))
        else:
            issues.append({
                "severity": "warning",
                "message": f"暂不支持的语言:{language}"
            })
        
        return {
            "issues": issues,
            "count": len(issues),
            "language": language
        }
    
    def _check_python(self, code: str) -> List[Dict]:
        """Python 代码检查"""
        issues = []
        
        # 基础检查:语法
        try:
            compile(code, '<string>', 'exec')
        except SyntaxError as e:
            issues.append({
                "severity": "error",
                "message": f"语法错误:{e.msg}",
                "line": e.lineno
            })
        
        # 安全检查:硬编码密码
        if "password" in code.lower() and ("=" in code or ":" in code):
            issues.append({
                "severity": "warning",
                "message": "可能包含硬编码密码",
                "line": None
            })
        
        # 安全检查:eval/exec
        if "eval(" in code or "exec(" in code:
            issues.append({
                "severity": "warning",
                "message": "使用 eval/exec,可能存在安全风险",
                "line": None
            })
        
        return issues

检查规则:

规则 类型 严重级别 说明
语法检查 compile() error Python 语法错误
硬编码密码 正则匹配 warning 检测 password= 或 password: 模式
eval/exec 使用 正则匹配 warning 动态代码执行风险

6.6 BrowserUseExecutor 浏览器自动化执行器

python 复制代码
# backend/app/skills/executors/browser_use_executor.py

class BrowserUseExecutor(SkillExecutor):
    """BrowserUse 执行器"""
    
    def __init__(self, config: dict):
        super().__init__(config)
        self.tools = config.get('tools', [])
    
    def execute(self, url: str, actions: list = None, **kwargs) -> Dict:
        """执行浏览器自动化操作"""
        if actions is None:
            actions = []
        
        results = []
        for action in actions:
            action_type = action.get("type", "")
            result = self._execute_action(action_type, action)
            results.append(result)
        
        return {
            "url": url,
            "actions_executed": len(results),
            "results": results,
            "status": "completed"
        }
    
    def _execute_action(self, action_type: str, action: dict) -> Dict:
        """执行单个操作"""
        action_map = {
            "click": self._do_click,
            "type": self._do_type,
            "navigate": self._do_navigate,
            "screenshot": self._do_screenshot,
        }
        
        handler = action_map.get(action_type)
        if handler:
            return handler(action)
        
        return {"status": "skipped", "message": f"未知操作:{action_type}"}
    
    def _do_click(self, action: dict) -> Dict:
        return {"status": "completed", "action": "click", "selector": action.get("selector")}
    
    def _do_type(self, action: dict) -> Dict:
        return {"status": "completed", "action": "type", "selector": action.get("selector"), "value": action.get("value")}
    
    def _do_navigate(self, action: dict) -> Dict:
        return {"status": "completed", "action": "navigate", "url": action.get("url")}
    
    def _do_screenshot(self, action: dict) -> Dict:
        return {"status": "completed", "action": "screenshot", "path": action.get("path", "/tmp/screenshot.png")}

支持的操作类型:

操作 说明 参数
click 点击元素 selector(CSS 选择器)
type 输入文本 selector + value
navigate 导航到 URL url
screenshot 截图 path(可选)

完整调用示例:

ini 复制代码
from app.skills import load_skill

skill = load_skill('BrowserUse')
result = skill.execute(
    url="https://example.com/login",
    actions=[
        {"type": "type", "selector": "#username", "value": "admin"},
        {"type": "type", "selector": "#password", "value": "secret"},
        {"type": "click", "selector": "#login-btn"},
        {"type": "screenshot", "path": "/tmp/result.png"}
    ]
)
# 输出:{'url': '...', 'actions_executed': 4, 'status': 'completed'}

七、全局实例与便捷函数

7.1 模块级单例

python 复制代码
# backend/app/skills/engine.py 末尾

# 全局 Skill 引擎实例
skill_engine = SkillEngine()

# 便捷函数
def load_skill(name: str) -> SkillWrapper:
    """加载 Skill"""
    return skill_engine.load_skill(name)

def list_skills() -> List[dict]:
    """列出所有 Skills"""
    return skill_engine.list_skills()

def reload_skills():
    """重新加载所有 Skills"""
    skill_engine.reload_all()

7.2 init.py 导出

csharp 复制代码
# backend/app/skills/__init__.py

from .engine import (
    SkillEngine,
    SkillWrapper,
    SkillExecutor,
    load_skill,
    list_skills,
    reload_skills,
)

__all__ = [
    'SkillEngine',
    'SkillWrapper',
    'SkillExecutor',
    'load_skill',
    'list_skills',
    'reload_skills',
]

使用方式:

ini 复制代码
# 方式 1:直接导入便捷函数
from app.skills import load_skill, list_skills

skill = load_skill('StaticCheck')
result = skill.execute(code_path="/path/to/code")

# 方式 2:创建引擎实例
from app.skills.engine import SkillEngine

engine = SkillEngine('/custom/skills/dir')
skills = engine.list_skills()

八、完整调用链路演示

8.1 从 API 到执行器

scss 复制代码
用户请求
  ↓
API 路由(backend/app/api/skills.py)
  ↓
SkillEngine.load_skill(name)
  ↓
解析 MD 文件(YAML Frontmatter)
  ↓
创建 SkillWrapper
  ↓
懒加载执行器(动态导入)
  ↓
验证输入参数
  ↓
执行器.execute(**kwargs)
  ↓
记录日志 + 返回结果

8.2 实际调用示例

ini 复制代码
# 1. 加载技能
from app.skills import load_skill

skill = load_skill('StaticCheck')

# 2. 执行技能
result = skill.execute(
    code_path="/home/liu/ai-test-system/backend",
    language="python",
    check_types=["lint", "security"],
    severity="warning",
    output_format="json"
)

# 3. 查看结果
print(f"成功:{result['success']}")
print(f"问题数:{result['total_issues']}")
for issue in result['issues']:
    print(f"  [{issue['severity']}] {issue['file']}:{issue['line']} - {issue['message']}")

# 输出示例:
# [INFO] StaticCheck: 执行完成:StaticCheck
#   Data: {"duration_ms": 12345.67, ...}...
# 成功:True
# 问题数:15
#   [warning] main.py:42 - Unused import 'os'
#   [error] utils.py:15 - SQL injection vulnerability
#   ...

九、Skill 引擎的设计哲学

9.1 声明式 vs 命令式

维度 命令式(传统) 声明式(Skill 引擎)
定义方式 写 Python 代码 写 YAML 配置
新增技能 修改代码 + 部署 添加 MD 文件
验证逻辑 手动编写 自动验证
错误处理 手动 try-except 引擎统一处理
日志记录 手动 print/logging 引擎自动记录
执行器降级 自动降级到 GenericExecutor

9.2 开闭原则(OCP)

Skill 引擎严格遵循开闭原则

  • 对扩展开放:新增技能只需添加 MD 文件 + 执行器类
  • 对修改封闭 :引擎核心代码(engine.py)无需修改
python 复制代码
# ❌ 违反 OCP:每新增技能都要修改引擎
class SkillEngine:
    def execute(self, skill_name, **kwargs):
        if skill_name </span> 'TestCaseGen':
            return TestCaseGenExecutor().execute(**kwargs)
        elif skill_name == 'StaticCheck':
            return StaticCheckExecutor().execute(**kwargs)
        # 每新增一个技能,就要加一个 elif

# ✅ 遵循 OCP:动态导入,无需修改引擎
def _create_executor(self):
    def camel_to_snake(name):
        s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
        return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
    module_name = f"app.skills.executors.{camel_to_snake(self.name)}_executor"
    module = __import__(module_name, fromlist=[''])
    executor_class = getattr(module, f"{self.name}Executor")
    return executor_class(self.config)

9.3 单一职责原则(SRP)

职责
SkillEngine 管理技能生命周期(加载/列表/重载)
SkillWrapper 桥接配置与执行器(验证/执行/日志)
SkillExecutor 定义执行接口(抽象基类)
GenericExecutor 提供通用执行逻辑(降级方案)
TestCaseGenExecutor 测试用例生成(业务逻辑)
HealerExecutor 自愈修复(业务逻辑)

十、踩坑记录与解决方案

10.1 YAML Frontmatter 解析失败

问题: 正则表达式 ^---\s*\n(.*?)\n---\s*\n 在某些情况下匹配失败。

原因: Markdown 文件中 --- 前后可能有空格或空行。

解决方案:

python 复制代码
# 更健壮的正则表达式
match = re.match(r'^---\s*\n(.*?)\n---\s*\n', content, re.DOTALL)

# 如果失败,尝试宽松模式
if not match:
    match = re.match(r'---\s*\n(.*?)\n---\s*\n', content, re.DOTALL)

10.2 动态导入路径错误

问题: __import__() 导入执行器时路径不正确。

原因: 驼峰命名转模块名时,self.name.lower() 会把 StaticCheck 转成 staticcheck 而不是 static_check

解决方案:

python 复制代码
# 驼峰转蛇形命名
def camel_to_snake(name):
    s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
    return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()

module_name = f"app.skills.executors.{camel_to_snake(self.name)}_executor"
# 例如:StaticCheck → static_check_executor

module = __import__(module_name, fromlist=[''])
executor_class = getattr(module, f"{self.name}Executor")

10.3 exec() 安全风险

问题: GenericExecutor._execute_python() 使用 exec(code, {}, local_vars) 执行用户代码,空 globals 导致连 len/str 等内置函数都不可用。

解决方案:

python 复制代码
def _execute_python(self, code: str, context: dict) -> Any:
    """执行 Python 代码片段(受限沙箱)"""
    # 提供安全的内置函数子集
    safe_builtins = {
        'len': len, 'str': str, 'int': int, 'float': float,
        'list': list, 'dict': dict, 'tuple': tuple, 'set': set,
        'range': range, 'enumerate': enumerate, 'zip': zip,
        'min': min, 'max': max, 'sum': sum, 'abs': abs,
        'sorted': sorted, 'reversed': reversed,
        'isinstance': isinstance, 'type': type,
        'True': True, 'False': False, 'None': None,
    }
    safe_globals = {'__builtins__': safe_builtins}
    local_vars = {'inputs': context, 'result': None}
    exec(code, safe_globals, local_vars)
    return local_vars.get('result')

安全加固:

  • ✅ 提供 20+ 常用内置函数(len/str/int/float/list/dict 等)
  • ❌ 禁止 __import__openevalexec 等危险函数
  • ❌ 禁止 ossyssubprocess 等模块访问

十一、总结

11.1 Skill 引擎的核心价值

价值 说明
声明式 用 Markdown + YAML 定义技能,无需修改代码
插件式 新增技能只需添加 MD 文件 + 执行器类
可验证 输入参数自动验证,类型/长度/必填检查
可观测 执行日志、耗时统计、错误追踪
可降级 专用执行器不存在时,自动使用通用执行器
可扩展 遵循 OCP,对扩展开放,对修改封闭

11.2 与其他引擎的关系

scss 复制代码
┌─────────────────────────────────────────────────────────┐
│                    LangGraph State Graph                 │
│  analyze_node → generate_node → execute_node → report  │
├─────────────────────────────────────────────────────────┤
│                    SkillEngine 桥接                      │
│   load_skill() → SkillWrapper → Executor.execute()     │
├─────────────────────────────────────────────────────────┤
│              三大引擎协同工作                            │
│  RAG(知识增强) + MCP(工具调用) + Skills(技能扩展)  │
└─────────────────────────────────────────────────────────┘

在测试流程中的应用:

  • analyze_node:调用 RAG 检索历史用例,调用 Skill 分析需求
  • generate_node:调用 TestCaseGen Skill 生成用例
  • execute_node:调用 StaticCheck/BrowserUse 等 Skill 执行测试
  • report_node:调用 Skill 生成报告

11.3 下一步

下一篇将介绍 RAG 知识增强,讲解系统如何通过 MySQL 全文搜索 + ChromaDB 向量检索,为测试用例生成提供知识增强。


附录:完整代码索引

文件 行数 说明
backend/app/skills/engine.py 284 SkillEngine + SkillWrapper + 驼峰转蛇形修复
backend/app/skills/executors/base.py 78 SkillExecutor 基类 + 安全沙箱 GenericExecutor
backend/app/skills/executors/test_case_gen_executor.py 246 真实 LLM + 规则引擎降级 + Mock 兜底
backend/app/skills/executors/healer_executor.py 126 自愈修复执行器
backend/app/skills/executors/static_check_executor.py 52 静态代码检查执行器(新增)
backend/app/skills/executors/browser_use_executor.py 55 浏览器自动化执行器(新增)
backend/app/skills/StaticCheck.md 52 静态代码检查技能配置(真实文件)
backend/app/skills/BrowserUse.md 54 浏览器自动化技能配置(真实文件)

作者注: 本文所有代码均来自实际项目 backend/app/skills/engine.py,可直接运行验证。Skill 引擎的设计体现了声明式编程和插件式架构的思想,是系统可扩展性的核心保障。

相关推荐
fundroid1 小时前
AI Coding 知识库最佳实践:三层结构重建可维护工程
人工智能·skill·ai 编程·ai coding·skill.md·agent.md
武帝为此1 小时前
【Selenium 屏幕截图】
python·selenium·测试工具
Cosolar1 小时前
封神级 TTS!VoxCPM2 凭连续表征,玩转多语言合成 + 创意音色 + 无损声纹克隆
人工智能·llm·github
SCBAiotAigc1 小时前
2026.5.1:`DockerDesktop must be owned by an elevated account`错误的解决办法
人工智能·docker·具身智能
码流怪侠1 小时前
【GitHub】andrej-karpathy-skills:让 AI 编程助手告别三大通病
人工智能·程序员·github
user29876982706541 小时前
九、深入 Claude Code CLI 源码:Bridge/Remote Control 远程执行
人工智能
码流怪侠1 小时前
【GitHub】OpenClaw:开源个人AI助手的新标杆
人工智能·程序员·github
码农小白AI1 小时前
AI报告审核 IACheck:质量证明文件从“看得懂”走向“说得准”,术语一致性成为合规关键
人工智能
qq_283720051 小时前
Vibe Coding 氛围编程入门教程:AI 时代的全新开发范式(零基础到实战)
大数据·人工智能