造一个 AI Skill 的 Lighthouse:SkillScope 架构设计与工程实践

从 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 的核心工程决策:

  1. 混合分析:确定性规则为主(快速可靠),AI Judge 为辅(语义理解)
  2. 优雅降级:AI 不可用不影响扫描,API Key 缺失不报错
  3. 安全修复:三级体系,自动修复不引入新问题
  4. 插件化:动态注册,社区可扩展
  5. CI 原生:SARIF + GitHub Actions,融入开发流程

GitHub: github.com/JuneDylan/S...

如果这篇文章对你有帮助,点个赞再走吧 👋

相关推荐
眼眸流转12 小时前
Dify学习笔记
笔记·学习·agent·dify
ckjoker12 小时前
Day29:LLM不会调函数?Function Calling两轮对话真相+MCP/Skills/A2A四层进化全景
agent
青云计划12 小时前
多层状态机:从单变量到4层架构的工程实践
agent
Coder小相12 小时前
LangChain1.0第四篇 - 统一接口多厂商模型适配
人工智能·langchain·agent
JaydenAI13 小时前
[MAF预定义ChatClient中间件-05]动态修改对话配置的两种解决方案
ai·c#·agent·maf·chatclient管道
_未完待续13 小时前
从零打造 AI Agent (二)—— 让 AI 拥有记忆
agent·ai编程
PeterLi13 小时前
LangChain v1.x 最新官方完整教程(六大核心组件全解析+生产级代码示例)
langchain·agent
十正13 小时前
Hermes记忆预取机制深度解析
python·ai·agent·hermes
JaydenAI13 小时前
[MAF预定义ChatClient中间件-04]ReducingChatClient——通过精减对话实施又不丢失基本语义
ai·c#·agent·maf·chatclient管道·对话历史压缩