安全分析技能路由器并不是一个通用的 Agent 框架插件,它是一类把安全扫描流程标准化的调度层。在 AI 辅助安全分析的工作场景里,最让人头疼的不是某个扫描工具不会用,而是 AI 调用工具的过程没有标准:它可能跳过资产核验,可能把端口扫描和 Web 扫描混在一起,也可能拿到结果后无法和上一次扫描对比。安全分析技能路由器要解决的,正是这个问题。
这篇文章会围绕“给 AI 一个标准化的安全扫描流程”这条主线展开:先解释技能路由器是什么,再把扫描流程拆成可路由的阶段,接着用 Python 和配置实现一个最小可运行的示例,最后给出验证方法、常见坑、排查路径和生产环境落地建议。适合希望把 LLM 接入安全扫描流程的 DevSecOps 工程师、安全平台开发者和正在做 AI Agent 工程化的开发者阅读。
1. 先理解安全分析为什么需要“技能路由器”
1.1 什么是安全分析技能路由器
“技能路由器”可以理解成一个位于 AI 控制层和安全扫描工具之间的调度组件。它接收来自大模型或用户的意图,把意图翻译成标准化的扫描任务,再根据技能注册表选择合适的执行器,最后把不同工具的原始输出收敛成统一结构。
这个路由器不是简单的 if-else 分发器。它至少承担四件事:
- 技能发现:系统里有哪些可调用的安全扫描能力。
- 参数校验:AI 给出的参数是否合法,目标是否在授权范围内。
- 风险控制:高风险的扫描动作是否需要人工审批或禁止执行。
- 结果规范化:不同工具的输出格式差异很大,路由器要把它们转成统一模型。
在工程上,它像中间层。没有这层中间层时,AI 直接调用工具,prompt 一变,执行顺序和参数可能完全不可控。加了路由器之后,AI 只负责理解意图和生成参数,真正执行和收敛结果的工作由路由器完成。
1.2 没有标准流程时,AI 做安全扫描会暴露哪些问题
直接让大模型调用扫描工具,看起来效率很高,实际上问题非常集中。常见现象包括:
- 流程不固定。同样一句话,有时只扫描端口,有时又自动触发全量漏洞扫描,执行结果无法横向对比。
- 工具边界不清晰。Web 应用扫描和网络端口扫描混在一起,一个任务里出现多种风马牛不相及的动作。
- 参数不可控。AI 可能生成不合理的端口范围、超时时间或扫描深度,导致扫描长时间不结束。
- 回滚困难。扫描过程中一旦出现误操作,缺少中间状态记录,无法定位问题出在哪一步。
- 输出没法直接进入报告。Nmap、ZAP、Trivy 的输出格式各不相同,AI 分析时要么截断,要么误读。
这些问题的根因不是 AI 不够聪明,而是安全扫描本身缺少标准化任务边界。安全扫描是一个容易产生副作用的过程,必须让“决策”和“执行”分离,再让“执行”和“报告”分离。
1.3 路由器带来的改变
引入技能路由器之后,AI 侧的工作量反而会减少。AI 不再需要知道工具的具体命令行怎么写,只需要输出标准 JSON,比如“意图是 web_scan,目标是 example.com,风险等级是 medium”。
执行侧的变化更明显:
| 能力 | 没有路由器 | 有路由器 |
|---|---|---|
| 工具调用 | 分散在 prompt 和代码中 | 集中在技能注册表 |
| 参数来源 | 模型自由生成 | schema 校验后使用 |
| 扫描范围 | 依赖模型自觉 | 用 allowed_targets 限制 |
| 结果格式 | 每个工具自带格式 | 统一 SkillResult |
| 审计 | 很难追踪决策链路 | 每次路由都有记录 |
这个设计并不是限制 AI,而是让 AI 的“判断力”用在意图理解和结果解读上,而不是消耗在拼命令和处理工具差异上。
2. 把安全扫描流程拆成可路由的标准步骤
2.1 标准扫描流程的六个阶段
要设计技能路由器,先定义流程。安全扫描流程可以拆成六个阶段,每个阶段路由器都有明确职责。
| 阶段 | 主要目的 | 路由器责任 |
|---|---|---|
| 1. 请求理解 | 把用户输入转成结构化意图 | 提取扫描类型、目标、约束条件 |
| 2. 范围确认 | 确认目标在授权范围内 | 校验 allowed_targets、黑名单、CIDR |
| 3. 技能路由 | 从技能注册表选择可用技能 | 规则匹配或 LLM 决策 |
| 4. 隔离执行 | 在沙箱或独立容器中执行扫描 | 设置超时、资源限制、日志采集 |
| 5. 结果规范 | 把工具输出转成统一模型 | 字段映射、去重、摘要生成 |
| 6. 报告复核 | 输出给人工或 LLM 做结论 | 生成审计记录、标记高风险项 |
这六个阶段应该作为项目主流程,而不是散落在工具脚本里。每个阶段的产物都要落盘或进入日志,这样后续排查时有据可查。
2.2 技能注册表:工具和技能的边界
很多人会把“技能”和“工具”混为一谈。在设计路由器时,它们是两个概念。
工具是实际执行动作的程序,比如 Nmap 是一个工具,OWASP ZAP 是一个工具,Trivy 也是一个工具。技能则是面向业务场景封装出来的能力单元,一个技能可以调用一个工具,也可以组合多个工具。
例如,“Web 资产基础检查”这个技能,内部可能先做端口发现,再读取 HTTP 响应头,最后检查 TLS 证书信息。这三个动作可以由三个不同工具完成,但对 AI 而言,它只是调用了一个技能。
技能注册表就是描述这些能力单元的元数据。每一条技能记录都要包含:
- 技能名称和描述
- 对应工具和命令模板
- 输入参数 schema
- 输出结果 schema
- 超时时间和风险等级
- 允许访问的目标范围
- 是否需要人工审批
2.3 统一输入输出协议
标准化流程的核心是统一输入输出协议。输入统一叫ScanContext,输出统一叫SkillResult。
ScanContext包含:
- scan_id:任务唯一标识
- target:扫描目标
- scan_type:扫描类型
- options:扩展参数字典
SkillResult包含:
- skill_name:技能名称
- status:success、failed、skipped
- summary:人工可读的执行摘要
- findings:规范化后的发现项列表
- raw_output:原始输出,用于排查
- error:失败时的错误信息
有了这套协议,路由器不需要关心每个工具返回的是什么格式,业务层也不需要对不同工具做特殊处理。新增一个工具时,只要实现一个技能类,保证输出符合SkillResult即可。
3. 最小实现:用 Python 搭建一个可扩展的技能路由器
下面代码用于说明思路,实际项目要结合自己的包名、文件路径和依赖版本调整。先看目录结构,再逐步补齐核心代码。
3.1 工程目录结构
sec_skill_router/ ├── main.py ├── registry.yaml ├── router.py ├── llm_router.py ├── executor.py └── skills/ ├── __init__.py ├── base.py ├── port_scan.py └── web_common_scan.pyregistry.yaml描述技能元数据,router.py负责规则路由,llm_router.py负责基于大模型的路由决策,executor.py负责执行技能并处理超时,skills/base.py定义基类和数据结构。
3.2 定义技能接口和结果模型
# skills/base.py from abc import ABC, abstractmethod from dataclasses import dataclass, field from typing import Any @dataclass class ScanContext: scan_id: str target: str scan_type: str options: dict[str, Any] = field(default_factory=dict) @dataclass class SkillResult: skill_name: str status: str # success | failed | skipped summary: str findings: list[dict[str, Any]] raw_output: str = "" error: str = "" class BaseSkill(ABC): name: str = "base" @abstractmethod def validate(self, context: ScanContext) -> list[str]: """返回参数校验错误列表;为空表示通过""" ... @abstractmethod def execute(self, context: ScanContext) -> SkillResult: """执行实际扫描逻辑,并规范输出""" ...这样设计的好处是路由器只依赖BaseSkill接口,不依赖具体工具实现。后续新增技能时,只需要继承BaseSkill并实现validate和execute。
3.3 用 YAML 描述技能注册表
# registry.yaml version: 1 default_route: human_review skills: - name: web_port_scan tool: nmap type: network_scan risk_level: medium timeout_seconds: 300 allowed_targets: - "*.example.com" input_schema: target: string ports: string output_schema: open_ports: array - name: web_common_scan tool: owasp_zap type: web_scan risk_level: high timeout_seconds: 900 allowed_targets: - "*.example.com" input_schema: target: string scan_profile: string output_schema: alerts: array - name: container_image_scan tool: trivy type: artifact_scan risk_level: low timeout_seconds: 600 allowed_targets: - "registry.internal" input_schema: image: string output_schema: vulnerabilities: array注册表的作用是让“新增技能”不用改主流程代码,只改配置和新增技能类。risk_level用于控制审批策略,timeout_seconds防止工具卡死,allowed_targets是扫描范围的安全底线。
3.4 先实现规则路由,再引入模型
规则路由的意义是保证基础请求 100% 可预测。它适合处理“端口”“镜像”“Web 漏洞”这类明确关键词。
# router.py from skills.base import BaseSkill, ScanContext class SkillRouter: def __init__(self, registry: dict, skills: dict[str, BaseSkill]): self.registry = registry self.skills = skills def route_by_rules(self, user_request: str, context: ScanContext): request_lower = user_request.lower() if "端口" in user_request or "port" in request_lower: skill = self.skills.get("web_port_scan") return [skill] if skill else [] if "容器" in user_request or "镜像" in user_request or "image" in request_lower: skill = self.skills.get("container_image_scan") return [skill] if skill else [] if "web" in request_lower or "漏洞" in user_request or "http" in request_lower: skill = self.skills.get("web_common_scan") return [skill] if skill else [] return []规则路由会有覆盖不到的场景。比如用户说“检查一下这个服务有没有暴露不该暴露的端口”,没有出现“端口”两个字时,规则就失效。这时可以交给 LLM 做补充识别,但结果必须回传到校验层。
3.5 用 LLM 做意图识别和技能选择
在接入大模型时,建议不要直接让模型返回“要执行的 shell 命令”,而是让模型返回“技能名称和参数”。下面以 OpenAI 兼容接口为例,实际项目可以替换成私有化模型或其他接口。
# llm_router.py import json from typing import Any import openai def route_with_llm( user_request: str, skill_manifest: list[dict[str, Any]], client: openai.OpenAI, ) -> dict[str, Any]: prompt = ( "你是安全扫描技能路由器。请根据用户请求,从技能清单中选择合适的技能。" "不要执行扫描,不要生成 shell 命令,只输出技能选择结果。\n" f"技能清单:\n{json.dumps(skill_manifest, ensure_ascii=False)}\n" f"用户请求:\n{user_request}\n" "只输出 JSON,格式如下:\n" '{"intent": "web_scan", "skills": ["web_port_scan"], "parameters": {"target": "demo.example.com"}}' ) resp = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": prompt}], response_format={"type": "json_object"}, ) return json.loads(resp.choices[0].message.content)这段代码的关键点是约束模型只输出 JSON,并且把技能清单放在 prompt 里。实际生产中,技能清单越长,token 消耗越大,所以对技能描述要做精简,或者先经过规则初筛,再让模型从候选技能中选择。
拿到模型返回后,必须校验skills字段里的每个名称是否真的存在于注册表,以及参数是否通过技能类的validate。模型返回的任何内容都不能直接执行。
4. 标准化扫描流程的运行示例与验证方法
4.1 一条完整调用链路
假设用户输入:“扫描一下 https://demo.example.com 的常见 Web 漏洞。”
主流程可以写成下面的简化代码:
# main.py import uuid from llm_router import route_with_llm from router import SkillRouter from skills.base import ScanContext def run_scan(user_request: str): scan_id = uuid.uuid4().hex[:12] context = ScanContext( scan_id=scan_id, target="https://demo.example.com", scan_type="web", ) router = SkillRouter(registry={}, skills={}) selected_skills = router.route_by_rules(user_request, context) # 规则没有命中时,再由 LLM 补充 if not selected_skills: decision = route_with_llm( user_request, skill_manifest=[], client=llm_client, ) for skill_name in decision.get("skills", []): skill = router.skills.get(skill_name) if skill: selected_skills.append(skill) for skill in selected_skills: errors = skill.validate(context) if errors: print(f"[{scan_id}] validate failed: {errors}") continue result = skill.execute(context) print(f"[{scan_id}] {skill.name}: {result.status}") print(f"[{scan_id}] summary: {result.summary}") print(f"[{scan_id}] findings: {result.findings}")在这条链路里,scan_id贯穿整个流程。日志输出会类似:
[3f2a9c1b2d01] intent=web_scan, target=https://demo.example.com [3f2a9c1b2d01] route=rule, selected_skills=[web_common_scan] [3f2a9c1b2d01] validate: target in allowed_targets, pass [3f2a9c1b2d01] web_common_scan: success [3f2a9c1b2d01] summary: 共发现 3 个中危告警、1 个低危告警 [3f2a9c1b2d01] findings: [{"severity": "medium", "name": "..."}]只要日志里能看到 route 决策、validate 结果、技能执行状态和 findings 数量,就说明标准化流程已经跑通。
4.2 如何确认路由结果正确
验证路由器不是只看它能不能启动,还要看决策是否符合预期。推荐用测试用例固定关键行为:
# tests/test_router.py from router import SkillRouter def test_port_request_routes_to_port_scan(): router = build_test_router() context = build_test_context() selected = router.route_by_rules("帮我扫描一下端口", context) assert len(selected) == 1 assert selected[0].name == "web_port_scan"测试用例至少要覆盖四类场景:
- 规则命中的场景,结果是否可预测。
- 规则未命中的场景,是否进入 LLM 决策分支。
- LLM 返回了不存在的技能名时,是否被丢弃。
- 参数校验失败时,是否跳过执行并记录错误。
4.3 最小验证检查清单
| 检查项 | 预期结果 |
|---|---|
| 输入合法 URL 的目标 | 路由到对应技能,validate 通过 |
| 输入不在 allowed_targets 中的目标 | 拒绝执行并记录原因 |
| LLM 返回不存在的技能名 | 校验失败,不执行任何工具 |
| 工具执行超时 | 标记 failed,保留原始日志 |
| 多个技能被选中 | 串行执行或按配置并发,统一写审计日志 |
这套检查清单可以直接用于上线前的自动化回归。
5. 关键参数、选型与常见坑
5.1 路由器关键参数
技能路由器的核心参数不是模型参数,而是流程控制参数。调错这些参数可能导致扫描不可控或不可审计。
| 参数 | 建议默认值 | 作用 | 误配影响 |
|---|---|---|---|
| timeout_seconds | 300 | 控制单个技能执行时长 | 太短导致扫描中断,太长导致任务堆积 |
| risk_level | medium | 决定是否走审批 | 高风险技能被自动执行,存在越权风险 |
| approval_mode | auto | auto 或 manual | 设成 auto 后高风险动作缺乏人工确认 |
| max_concurrency | 1 | 并发技能数量 | 过高会打满目标资源,产生误报 |
| result_max_length | 2000 | 进入 LLM 的文本上限 | 超长结果被截断,影响分析准确度 |
| retry_times | 1 | 工具失败重试次数 | 过高会重复扫描,产生大量冗余日志 |
这些参数建议放在配置文件或配置中心,不要写在代码里。每个扫描任务可以覆盖默认值,但覆盖行为必须记录到审计日志。
5.2 安全扫描工具选型对照
工具选型必须结合团队能力、资产类型和合规要求。下面表格只做思路示例,不代表推荐某一款特定工具。
| 技能名称 | 常见工具 | 适用场景 | 风险等级 |
|---|---|---|---|
| 端口发现 | Nmap | 确认资产暴露端口 | medium |
| Web 应用扫描 | OWASP ZAP | 常见 Web 漏洞检查 | high |
| 容器镜像扫描 | Trivy | 镜像依赖漏洞 | low |
| 静态代码扫描 | Semgrep | 代码仓库安全审计 | low |
| 基线合规检查 | OpenSCAP | 系统配置基线校验 | medium |
| 依赖漏洞检查 | Grype | 软件供应链风险 | low |
选型时要优先考虑工具是否容易在容器中运行、是否支持 JSON 输出、是否有稳定的退出码,以及是否允许限定扫描目标范围。
5.3 最常踩的四个坑
第一个坑是路由命中错误。用户说“扫一下服务地址”,规则把“服务”理解成了“Web 漏洞”,结果执行了全量 Web 扫描。原因是规则关键词太宽泛。解决方式是在规则命中后增加确认步骤,或要求 LLM 输出完整 intent 后再匹配。
第二个坑是 AI 幻觉出技能名。模型可能返回一个看起来合理但注册表中不存在的技能,比如web_full_scan。直接执行会报 KeyError,直接忽略又可能导致任务静默失败。解决方式是在丢弃不存在的技能时打印 warning,并让流程进入人工复核。
第三个坑是工具原始输出太长。Nmap 扫描大网段时可能产生几十 MB 输出,直接塞给 LLM 会超 token 上限。解决方式是先在技能层做摘要,只把 findings 中的结构化字段传给后续分析环节,原始输出落盘即可。
第四个坑是扫描范围失控。用户传入scan_type=web,但目标被误解析成整个网段,导致路由器同时触发多个扫描任务。解决方式是目标解析后必须做白名单校验,不允许 CIDR 范围直接进入高并发扫描。
6. 生产环境落地的排查路径与最佳实践
6.1 从日志回溯一次扫描任务
生产环境排查技能路由器问题,核心抓手是scan_id。所有日志、结果文件和审计信息都必须带上 scan_id。一次任务的理想日志链路是:
请求接入 -> 意图识别 -> 范围校验 -> 技能选择 -> 技能执行 -> 结果规范 -> 报告生成排查时如果发现“扫描结果缺失”,先看技能执行阶段是否生成结果文件;再看结果规范阶段是否因为字段映射报错丢弃数据;最后看报告生成阶段是否因为读取权限遗漏输出。从日志链路倒推,比在代码里盲找更高效。
6.2 常见错误与排查顺序
| 问题现象 | 可能原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 请求没有匹配到任何技能 | 规则和 LLM 都没有命中 | 查看意图识别日志 | 补充技能描述或增加规则关键词 |
| 模型返回了不存在的技能 | 技能清单和注册表不一致 | 对比 LLM 输出和 registry.yaml | 在路由层强制二次校验 |
| 扫描工具退出码非零 | 权限不足、依赖缺失 | 查看 raw_output 和 stderr | 先复现工具命令,再调整权限 |
| 结果中大量误报 | 扫描参数过宽或工具版本旧 | 对比已知基线数据 | 升级工具版本,收紧扫描参数 |
| 超时后任务没有清理 | 缺少超时终止逻辑 | 检查进程残留 | 执行器用 subprocess 超时或容器销毁 |
| LLM 分析结果和扫描结果不一致 | 喂给模型的 findings 被截断 | 检查 result_max_length | 增加摘要前置,减少原始文本 |
排查顺序建议是:先确认输入是否合法,再确认目标是否通过范围校验,接着看路由决策日志,再看技能执行状态,最后才检查模型分析环节。
6.3 生产级技能路由器需要补齐的能力
学习环境里跑通流程后,生产落地还需要补齐以下能力:
- 配置外置化。技能列表、目标白名单、超时参数全部放到配置中心,不能写死在代码里。
- 任务隔离。每个扫描任务运行在独立容器或隔离环境中,避免工具互相影响。
- 审计闭环。LLM 决策、规则命中、参数覆盖、审批记录都要落日志。
- 人工审批。risk_level 为 high 的技能必须进入人工审批队列。
- 限流和配额。同一目标同时只能有一个任务,避免扫描风暴。
- 结果保留策略。原始扫描结果不应无限期保存,要按合规要求设置保留周期。
- 模型回退。LLM 不可用时,路由器要能回退到规则路由,保证基础任务不中断。
这些能力不是一次性开发完,而是按使用频率逐步补充。第一阶段先保证可审计,第二阶段做隔离和审批,第三阶段再接 CI/CD 和工单系统。
6.4 分阶段落地路线
如果团队第一次做“AI + 安全扫描”,建议不要直接做通用 Agent。先按下面路线推进:
| 阶段 | 目标 | 关键动作 |
|---|---|---|
| 阶段一 | 规则路由可用 | 固定若干技能,跑通注册表和校验 |
| 阶段二 | LLM 辅助决策 | 规则未命中时使用模型,强制二次校验 |
| 阶段三 | 审批和隔离 | 接入审批队列、容器化执行 |
| 阶段四 | 闭环集成 | 接入资产中心、漏洞工单、CI 管道 |
每个阶段都要有明确的验收指标。例如阶段一验收标准是“端口类请求 100% 路由到端口技能”;阶段二验收标准是“模型输出中不存在的技能名 0 次进入执行器”;阶段三验收标准是“高风险扫描 100% 有人工审批记录”。有了指标,路由器的演进才能验证而不是凭感觉。
安全分析技能路由器的本质,是把“AI 很聪明”这件事约束到“流程可预期”的框架里。对新手来说,最有效的练习不是一开始就接大模型,而是先用规则路由跑通一个端口扫描技能,然后再逐步加入 Web 扫描、镜像扫描和 LLM 意图识别。只要保证每次路由决策都可审计、每个技能输入都经过校验、每个扫描结果都按统一结构输出,这套流程就能从 demo 平滑走向生产。