一、为什么需要 Skill 引擎?
1.1 传统测试系统的痛点
在传统的测试系统中,新增一个测试能力(比如代码检查、浏览器自动化、性能压测)通常需要:
- 修改代码:在核心逻辑中硬编码新的测试类型
- 重新部署:修改代码后需要重新构建和部署
- 高度耦合:测试逻辑与系统核心代码紧密耦合,难以维护
- 缺乏规范:不同开发者实现的测试能力风格迥异,难以统一
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:调用TestCaseGenSkill 生成用例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 引擎的设计体现了声明式编程和插件式架构的思想,是系统可扩展性的核心保障。