从 0 到 1 设计一个混合分析引擎,确定性规则 + LLM-as-a-Judge,6 维度 × 45+ 规则,给 AI Skill 打分并自动修复。本文拆解架构决策、核心实现和踩过的坑。
为什么需要 SkillScope?
Agent / Skill / MCP 生态在爆发,但质量工具严重缺位。前端有 ESLint + Lighthouse,Python 有 Bandit + Ruff,AI Skill 领域什么都没有。
我们在实际开发中遇到的问题:
python
# 问题 1:API Key 硬编码
API_KEY = "sk-abc123def456ghi789..."
# 问题 2:Prompt 注入
prompt = f"分析以下内容:{user_input}" # 用户输入直接拼接到 LLM 指令
# 问题 3:幻觉诱导
# system_prompt.md 中写着"如果不确定,请编造一个合理的答案"
# 问题 4:厂商锁定
response = openai.chat.completions.create(
model="gpt-4", functions=[...], tool_choice="auto"
)
这些问题不是功能 Bug,测试覆盖不了。需要专门的质量扫描工具。
架构设计:三层混合分析
SkillScope 的核心架构是 确定性分析 + AI Judge + 自动修复 三层混合:
yaml
┌──────────────────────────────────────────────────────┐
│ SkillScope CLI │
├──────────────────────────────────────────────────────┤
│ │
│ Layer 1: 确定性分析(快速、可靠、可复现) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Prompt │ │ Security │ │ Maintain │ ... │
│ │ Analyzer │ │ Scanner │ │ Analyzer │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └──────┬──────┘──────┬─────┘ │
│ │ │ │
│ Layer 2: AI Judge(语义理解、上下文感知) │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ PromptQuality │ │ Hallucination │ │
│ │ Judge │ │ Judge │ │
│ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │
│ Layer 3: 修复引擎(三级安全体系) │
│ ┌─────────────────────────────────────┐ │
│ │ FixManager: Safe → Suggested → Danger│ │
│ └─────────────────────────────────────┘ │
│ │
├──────────────────────────────────────────────────────┤
│ Reporters: Console / JSON / SARIF / HTML │
│ Web GUI: Flask + Chart.js │
└──────────────────────────────────────────────────────┘
设计原则
| 原则 | 决策 | 原因 |
|---|---|---|
| 确定性优先 | 规则扫描为主,AI 为辅 | 速度、可靠性、可复现性 |
| 优雅降级 | AI 不可用不影响扫描 | 生产环境 API 可能超时/限流 |
| 安全修复 | 三级体系 (Safe/Suggested/Dangerous) | 自动修复不能引入新问题 |
| 插件化 | 动态注册分析器 | 社区可扩展新维度 |
| 增量缓存 | 文件哈希比对 | 重复扫描秒级完成 |
核心实现拆解
1. 插件注册表:动态发现分析器
分析器通过基类 + 注册表实现插件化:
python
# analyzers/base.py
class BaseAnalyzer(ABC):
dimension: str = "" # 维度标识,如 "S"
name: str = "" # 维度名称,如 "安全性"
weight: float = 0.0 # 权重
@abstractmethod
def analyze(self, manifest: SkillManifest) -> DimensionScore: ...
# core/registry.py
class AnalyzerRegistry:
def auto_discover(self, package: str):
"""自动扫描包下所有模块,注册 BaseAnalyzer 子类"""
for _, mod in importlib.import_module(package).__dict__.items():
if isinstance(mod, type) and issubclass(mod, BaseAnalyzer):
self._registry[mod.dimension] = mod
def build_analyzers(self, enabled_dimensions=None, config=None):
"""按配置构建分析器实例"""
return [self._registry[dim](**config.get(dim, {}))
for dim in (enabled_dimensions or self._registry)]
好处:新增分析器只需继承 BaseAnalyzer 并放到 analyzers/ 目录,无需修改引擎代码。
2. 安全扫描器:AST + 正则双层检测
安全扫描是 SkillScope 最复杂的分析器,采用 正则快速筛选 + AST 语义分析 双层架构:
python
class SecurityScanner(BaseAnalyzer):
dimension = "S"
name = "安全性"
weight = 0.25
def analyze(self, manifest: SkillManifest) -> DimensionScore:
# Layer 1: 正则快速筛选(毫秒级)
secrets_score, secret_issues = self._scan_secrets(manifest)
danger_score, danger_issues = self._scan_dangerous_functions(manifest)
# Layer 2: AST 语义分析(秒级,更精准)
# 检测数据流中的危险函数调用链
# Layer 3: 依赖漏洞检查(框架预留,可接入 OSV/Snyk)
dep_score, dep_issues, dep_evidence = self._scan_dependencies(manifest)
# Layer 4: MCP 权限模型检查
mcp_score, mcp_issues = self._scan_mcp_permissions(manifest)
Secrets 检测的 12 种模式:
python
SECRET_PATTERNS = {
"openai_api_key": {"pattern": r"sk-[a-zA-Z0-9]{20,}", ...},
"anthropic_key": {"pattern": r"sk-ant-api03-[a-zA-Z0-9\-]{90,}", ...},
"aws_access_key": {"pattern": r"AKIA[A-Z0-9]{16}", ...},
"github_token": {"pattern": r"gh[pousr]_[A-Za-z0-9_]{36,}", ...},
"jwt_token": {"pattern": r"eyJ[A-Za-z0-9-_]+\.eyJ[A-Za-z0-9-_]+", ...},
"private_key": {"pattern": r"-----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----", ...},
# ... 共 12 种
}
3. AI Judge:LLM-as-a-Judge 的工程实践
AI Judge 的核心挑战不是模型调用,而是工程可靠性:
python
class BaseAIJudge(ABC):
timeout: int = 30
max_retries: int = 2
retry_delay: float = 1.0
def _get_client(self):
"""获取 OpenAI 兼容客户端,支持 DeepSeek/OpenAI"""
api_key = os.environ.get("DEEPSEEK_API_KEY") or os.environ.get("OPENAI_API_KEY")
if not api_key:
return None # 优雅降级:无 Key 则跳过 AI Judge
return OpenAI(api_key=api_key, base_url=base_url, timeout=self.timeout)
def judge(self, content: str) -> tuple[list[Issue], AIJudgeMeta]:
"""执行评估,含超时、重试、降级"""
client = self._get_client()
if not client:
return [], AIJudgeMeta(status="skipped") # 降级
for attempt in range(self.max_retries + 1):
try:
response = client.chat.completions.create(...)
return self._parse_response(response)
except Exception as e:
if attempt < self.max_retries:
time.sleep(self.retry_delay * (2 ** attempt)) # 指数退避
else:
return [], AIJudgeMeta(status="error") # 降级
关键设计决策:
| 决策 | 原因 |
|---|---|
| DeepSeek 优先 | 成本低、中文能力强、API 兼容 OpenAI |
| 无 Key 不报错 | AI Judge 是增强,不是必需 |
| 指数退避重试 | API 限流是常态 |
结果标记 source: "ai_judge" |
与确定性结果区分,方便用户判断 |
4. 修复引擎:三级安全体系
自动修复最怕的是"修出新 Bug"。SkillScope 的三级安全体系:
python
class FixSafety(str, Enum):
SAFE = "safe" # 确定性高,无副作用
SUGGESTED = "suggested" # 建议但需确认
DANGEROUS = "dangerous" # 可能改变语义,必须人工审核
Safe 级别修复示例:
python
# security_fixer.py
def _fix_secret(self, manifest, issue):
"""Secrets → 环境变量(Safe:确定性替换,不改变语义)"""
file_path = manifest.source_path / issue.location.split(":")[0]
content = Path(file_path).read_text()
# 精确匹配原始行,替换为 os.environ.get()
replacement = f'import os\n{var_name} = os.environ.get("{env_name}", "")'
return FixPatch(
file_path=issue.location,
original=original_line,
replacement=replacement,
safety=FixSafety.SAFE,
)
Suggested 级别修复示例:
python
def _fix_dangerous_function(self, manifest, issue):
"""eval() → ast.literal_eval()(Suggested:语义可能变化)"""
# eval("1+1") 返回 2,ast.literal_eval("1+1") 报错
# 需要人工确认原始用法是否依赖 eval 的动态执行能力
return FixPatch(safety=FixSafety.SUGGESTED, ...)
5. 增量缓存:文件哈希比对
python
class FileCache:
"""基于文件内容哈希的增量缓存"""
def __init__(self, cache_dir: str = ".skillscope_cache"):
self.cache_dir = Path(cache_dir)
self.cache_dir.mkdir(exist_ok=True)
def _file_hash(self, file_path: str) -> str:
p = Path(file_path)
if p.is_file():
return hashlib.sha256(p.read_bytes()).hexdigest()[:16]
elif p.is_dir():
h = hashlib.sha256()
for f in sorted(p.rglob("*")):
excluded = ("__pycache__", ".git", ".skillscope_cache")
if f.is_file() and not any(part in f.parts for part in excluded):
h.update(f.relative_to(p).as_posix().encode())
h.update(f.read_bytes())
return h.hexdigest()[:16]
return ""
def get(self, file_path: str, analyzer_name: str) -> dict | None:
h = self._file_hash(file_path)
cache_file = self.cache_dir / f"{h}_{analyzer_name}.json"
if cache_file.exists():
return json.loads(cache_file.read_text(encoding="utf-8"))
return None
def set(self, file_path: str, analyzer_name: str, data: dict) -> None:
h = self._file_hash(file_path)
cache_file = self.cache_dir / f"{h}_{analyzer_name}.json"
cache_file.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
效果:首次扫描 ~3s,二次扫描(文件未变)< 500ms。
6. 并行分析:ThreadPoolExecutor
python
# engine.py
def audit(self, path, apply_fixes=False, fix_safety_level="safe"):
manifest = load_skill(path, max_workers=self.config.max_workers)
dimension_scores = {}
with ThreadPoolExecutor(max_workers=self.config.max_workers) as executor:
futures = {
executor.submit(analyzer.analyze, manifest): dim
for dim, analyzer in self._analyzers.items()
}
for future in as_completed(futures):
dim = futures[future]
dimension_scores[dim] = future.result()
# AI Judge 串行执行(避免 API 限流)
if self.config.ai_enabled:
for judge in self._ai_judge_metas:
ai_issues, meta = judge.judge(content)
# 合并到对应维度
设计选择:确定性分析器并行(CPU 密集),AI Judge 串行(API 限流)。
SARIF 报告:对接 GitHub Code Scanning
SARIF (Static Analysis Results Interchange Format) 是 GitHub Code Scanning 的原生格式:
python
def generate_sarif_report(result: AuditResult) -> str:
return json.dumps({
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": [{
"tool": {
"driver": {
"name": "SkillScope",
"rules": [_issue_to_rule(i) for i in result.issues],
}
},
"results": [_issue_to_result(i) for i in result.issues],
}]
}, indent=2, ensure_ascii=False)
URI 转义处理(安全修复):
python
def _parse_location(location: str) -> dict:
from urllib.parse import quote
m = re.search(r":(\d+)\s*$", location)
if m:
uri = location[:m.start()]
return {"uri": quote(uri, safe="/:@!$&'()*+,;=-._~"), "line": int(m.group(1))}
return {"uri": quote(location, safe="/:@!$&'()*+,;=-._~")}
GUI 安全加固:路径遍历防护
Web GUI 的 API 端点接受用户输入的文件路径,存在路径遍历风险:
python
MAX_PATH_LENGTH = 512
def _validate_path(path_str: str) -> Path | None:
"""验证路径在允许范围内(CWD 或 HOME 目录下)"""
if not path_str or len(path_str) > MAX_PATH_LENGTH:
return None
resolved = Path(path_str).resolve()
if not resolved.exists():
return None
try:
resolved.relative_to(Path.cwd())
except ValueError:
try:
resolved.relative_to(Path.home())
except ValueError:
return None # 路径不在允许范围内
return resolved
@app.route("/api/scan", methods=["POST"])
def api_scan():
path_str = request.get_json().get("path", "").strip()
resolved = _validate_path(path_str)
if not resolved:
return jsonify({"error": "路径无效或不在允许范围内"}), 400
# 使用 resolved 而非原始输入
测试策略
137 个测试,覆盖单元 + 集成:
bash
tests/
├── unit/
│ ├── test_analyzers.py # 分析器单元测试
│ ├── test_fixers.py # 修复器单元测试
│ ├── test_ai_judges.py # AI Judge 单元测试(mock API)
│ ├── test_reporters.py # 报告器单元测试
│ ├── test_gui.py # GUI 单元测试
│ ├── test_cache.py # 缓存单元测试
│ ├── test_cli.py # CLI 单元测试
│ ├── test_config.py # 配置单元测试
│ ├── test_engine.py # 引擎单元测试
│ └── test_utils.py # 工具库单元测试
└── integration/
└── test_e2e.py # 端到端集成测试
AI Judge 测试的关键:mock API 调用,不依赖外部服务:
python
class TestPromptQualityJudge:
def test_judge_with_mock_api(self, monkeypatch):
monkeypatch.setenv("DEEPSEEK_API_KEY", "sk-test")
# mock OpenAI client 的返回值
...
性能优化与实测数据
| 优化 | 效果 | 实现 |
|---|---|---|
| 并行分析 | 3-5x 提速 | ThreadPoolExecutor |
| 增量缓存 | 重复扫描 < 20ms | 文件 SHA-256 哈希 |
| 正则缓存 | 规则匹配提速 | 模块级全局模式字典,避免重复编译 |
| Token 估算缓存 | 避免重复编码 | tiktoken + 字符估算降级 |
实测 Benchmark(本地机器:Python 3.11, AMD Ryzen 7)
| 项目规模 | 首次扫描(冷缓存) | 二次扫描(热缓存) | 加速比 |
|---|---|---|---|
| 小型 Skill(2 文件) | ~410ms | ~15ms | 27x |
| 中型 Skill(5 文件) | ~400ms | ~15ms | 26x |
| 有问题的 Skill(3 文件) | ~16ms | ~10ms | 1.6x |
注:小型项目冷扫描主要耗时在模块导入和初始化;缓存生效后瓶颈仅剩 JSON 序列化和文件读取。
踩过的坑
1. AI Judge 的乐观偏差
LLM 倾向于给高分。解决方案:
- 在 Prompt 中明确评分标准(1-10 分,附示例)
- 结果校准:AI Judge 发现的问题标记
source: "ai_judge",与确定性结果区分
2. 自动修复的语义安全
eval() → ast.literal_eval() 看似安全,但 eval("1+1") 返回 2 而 ast.literal_eval("1+1") 报错。解决方案:
- 分级标记:Suggested 级别,需人工确认
- 修复前展示 diff,修复后验证
3. SARIF URI 特殊字符
文件路径含中文/空格时,SARIF 解析失败。解决方案:
urllib.parse.quote编码 URI,保留合法字符
4. GUI 路径遍历
/api/scan 接受任意路径,可读取服务器任意文件。解决方案:
_validate_path限制路径范围- 绑定非 localhost 时打印安全警告
路线图
| 版本 | 计划 |
|---|---|
| v0.2.x(当前) | 六维评估 + 45+ 规则 + AI Judge + 自动修复 + GUI |
| v0.3.x | tree-sitter AST 数据流追踪 + OSV 实时漏洞查询 + VS Code 插件 |
| v0.4.x | 团队 Dashboard + 行业合规预设 + 高级架构重构 |
总结
SkillScope 的核心工程决策:
- 混合分析:确定性规则为主(快速可靠),AI Judge 为辅(语义理解)
- 优雅降级:AI 不可用不影响扫描,API Key 缺失不报错
- 安全修复:三级体系,自动修复不引入新问题
- 插件化:动态注册,社区可扩展
- CI 原生:SARIF + GitHub Actions,融入开发流程
GitHub: github.com/JuneDylan/S...
如果这篇文章对你有帮助,点个赞再走吧 👋