【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__、open、eval、exec、os、sys 等危险函数

使用场景:

  • 简单技能:只需在 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__、open、eval、exec 等危险函数
  • ❌ 禁止 os、sys、subprocess 等模块访问

十一、总结

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 引擎的设计体现了声明式编程和插件式架构的思想,是系统可扩展性的核心保障。

相关推荐
sell-cloud1 小时前
Prompt Engineering要凉了?Fable 5.5能“猜“意图,但银行正在封杀AI Agent——企业AI的真正护城河是什么
人工智能·prompt
Navicat中国2 小时前
Navicat 18 内置 AI 助手让数据库开发从繁琐操作迈向智能协作
大数据·人工智能·数据库开发
4SAPI3 小时前
多模型请求路由怎么设计?成本、速度和能力如何平衡
人工智能·python·gateway·php
魔乐社区3 小时前
翼芯・智算测试与适配优化专区,让国产算力“选得准、跑得通、用得好“
人工智能·开源·大模型
XCDoris5 小时前
郴州logo设计机构
大数据·人工智能·python
Goboy5 小时前
一个人带三个Agent,我用全新TRAE从0到1搭了个资讯周刊站
人工智能·trae
俊哥V5 小时前
每日 AI 研究简报 · 2026-10-19
人工智能·ai
AI_AGENT_DEV_AI5 小时前
AI英语教培智能体的开发
人工智能
物联网软硬件开发-轨物科技5 小时前
【技术白皮书】光伏清检一体化机器人系统解决方案技术白皮书
大数据·运维·网络·人工智能·机器人
gCode Teacher 格码致知5 小时前
Pandas教学-17:Series与Dataframe的关系
开发语言·python·pandas