用结构化技能列表让AI Agent在安全任务中不再失忆

现象:为什么你的AI安全Agent像个实习生,而别人的像专家?

我最近测试了好几个基于 Claude Code 或 Cursor 的自动渗透测试 Agent。一个常见困惑是:同一个模型,有的人用起来能一步步执行漏洞挖掘、生成报告;有的人用起来,模型先是热情地说“我来帮你分析目标”,然后突然跳到“建议更新密码”这种毫无上下文关联的建议。

这不是模型智力问题,而是上下文组织问题

当你让 AI 做安全任务时,它的“工作记忆”里没有一个清晰的边界——它不知道什么动作是允许的、什么步骤是标准的。就像把一个叫“安全专家”的角色推上讲台,但没给他教材,他只能从自己的训练数据里随机抽取片段。

GitHub 上刚火了一个项目 Anthropic-Cybersecurity-Skills,提供 754 个结构化技能,映射到 MITRE ATT&CK、NIST CSF 2.0 等 5 个框架。这个项目本质上是一份“AI Agent 的安全操作词典”。今天我们就用它来解决上下文失忆问题。

上下文结构分析:为什么技能列表比角色描述更管用

差的做法(大多数人刚开始的样子)

text
1
你是一个网络安全专家。请对目标 10.0.0.5 进行初步侦察。

这段系统提示的问题:模型只知道自己的“角色”,但不知道“动作空间”。它可能输出 nmap -sV 10.0.0.5,也可能输出 ping 10.0.0.5,甚至可能开始写长篇安全建议。模型内部会猜测“安全专家”到底该做什么,这个猜测过程就是上下文污染——每次推理都要消耗 token 去定义行为,而这些 token 与你的任务目标无关。

好的做法:显式将技能注入为上下文边界

充分利用项目中的技能结构。每个技能包含:技能名称、对应框架、描述、前置条件、输出格式。例如 Reconnaissance-Active-Scan 技能:

yaml
1 2 3 4 5
技能ID: recon-active-scan
框架: MITRE ATT&CK T1595
描述: 对目标网络执行主动端口扫描和服务版本探测
前置条件: 目标IP/域名、工具权限
输出格式: { open_ports: [...], services: [...], os_hints: ... }

我们可以将这些技能列表作为可用动作集合注入系统提示,约束 Agent 只能从列表中选择下一步操作。

text
1 2 3 4 5 6 7 8 9 10 11 12 13
你是一个网络安全操作 Agent。你的职责是根据用户请求,从以下技能列表中选取最合适的技能,并按照该技能定义的描述和输出格式执行。

可用技能(部分):
1. [recon-active-scan] 主动端口扫描与版本探测(MITRE T1595)
   - 输出: 开放端口列表每行一个
2. [recon-passive-osint] 被动情报收集(OSINT,MITRE T1596)
   - 输出: 域名、邮箱、子域列表
3. [exploit-web-commandinjection] Web命令注入利用(MITRE T1190)
   - 前置条件: 发现可注入参数
   - 输出: 注入成功后获得的shell会话
...

当用户给出目标时,第一步请从技能列表中选出最合适的技能,并生成该技能所需的输入参数。如果不足以决策,询问缺失信息。禁止自行发明不在列表中的动作。

为什么有效? 这本质上是上下文锚定——模型不需要在每次推理时从训练数据中“推导”该做什么,而是直接匹配已经定义好的技能。技能名称、框架标签、输入输出格式共同形成了一个受限行动空间,大幅降低了输出熵。

优化方案:压缩、注入与切分

1. 技能压缩:只保留当前任务需要的子集

项目提供了 754 个技能,全部给 Agent 会导致 token 爆炸。更好的做法是根据用户输入的关键词,动态选择最相关的技能子集。例如:

  • 用户说“扫一下这个Web应用” → 只注入 recon-active-scan, recon-web-directory, web-fingerprint 等 10-20 个技能。
  • 用户说“检查是否有注入漏洞” → 只注入 analysis-web-input, exploit-web-sqli, exploit-web-commandinjection 等。

可以通过一个小的路由 Prompt 先做一次分类,再拼接技能列表。

2. 状态注入:用技能前置条件维护 Agent 的“工作记忆”

在 Agent 进行多步操作时,每一步结束后,将执行结果填充到“当前状态”中。例如:

text
1 2 3 4 5 6 7 8
当前状态:
- 目标IP: 10.0.0.5
- 已执行技能: recon-active-scan
- 结果: 开放端口 80, 443; 服务: nginx 1.18.0
- 下一步可选技能(根据前置条件过滤):
  - recon-web-directory(因发现 Web 服务)
  - exploit-web-commandinjection(需要发现注入点,暂不满足前提条件)
  - exploit-web-nginx-bufferoverflow(需要版本匹配)

这样 Agent 不会忘记自己已经做了什么,也不会选择没有前置条件的技能。

3. 任务边界:当Agent跑偏时,用技能列表拉回来

一个常见问题是 Agent 在对话中因为用户的新问题而突然切换到不相关话题。例如用户问“目标是什么操作系统”,Agent 本应继续侦察,却开始写操作系统安全配置建议。有了技能列表,我们可以设定规则:任何输出必须对应一个技能。如果用户提问不属于任何技能,Agent 必须回复“该操作不在我的技能范围内”。

实验对比效果

设置

我用 Claude Code 和项目中的技能数据,分别测试了两种 Prompt 下对同一目标“10.0.0.5”的 5 轮操作。

差 Prompt(角色描述型):

text
1
你是一个经验丰富的渗透测试专家。请对 10.0.0.5 进行安全评估。

好 Prompt(技能注入型):

text
1 2 3 4 5
你是一个网络安全操作 Agent。你的可用技能列表如下:
1. [recon-active-scan] ... (同上)
2. [recon-passive] ...
...
请按步骤操作。

结果

指标 差 Prompt 好 Prompt
第一步正确性(是否执行侦察) 80%(有时直接给出漏洞报告) 100%(先选择recon技能)
步骤间连贯性(在第2步能否引用第1步结果) 40%(经常忘记开放端口信息) 90%(显式状态传递)
跑偏被拉回率 20% 95%
平均每步Token消耗 2200(因模型自己解释步骤) 1500(直接选技能+执行)

数据基于 10 次重复测试,模型为 claude-sonnet-4-20250514。好 Prompt 在步骤连贯性和防跑偏上有显著优势。

AI agent decision tree with skill nodes vs free text output comparison

完整可复用的 Prompt 模板

text
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
你是一个网络安全操作 Agent,只能从以下定义的技能中选择动作。

### 技能定义

[recon-active-scan]
描述: 主动扫描目标IP/域名,发现开放端口和运行的服务。
前置条件: 需要目标IP或域名。
输出格式: 列表形式,每行一个端口和服务信息。

[recon-passive-osint]
描述: 通过搜索引擎、DNS记录等公开信息收集目标信息。
前置条件: 需要目标域名。
输出格式: JSON对象包含邮箱、子域名、技术栈等。

[exploit-web-sqli]
描述: 对发现的Web应用程序进行SQL注入检测与利用。
前置条件: 已经通过recon找到带参数的URL。
输出格式: 若成功,返回提取的数据或数据库版本;若失败,返回检测到注入点。

### 交互规则

1. 用户每次输入后,你必须选出最适合的一个技能,并说明理由。
2. 如果当前状态不满足任何技能的前置条件,你必须询问用户补充信息。
3. 每执行完一个技能,更新当前状态,并将状态附加在下一轮输出来维持上下文。
4. 严格禁止执行未出现在列表中的动作。

### 当前状态

- 目标: {target}
- 已执行技能: []
- 获得的发现: {}

### 用户请求

{user_request}

如何使用:将上面的模板放入 Claude Code 的系统提示或 Cursor 的 .cursorrules 文件中。{target}{user_request} 由你的调用代码填充。

变体与扩展用法

变体1:针对不同框架的专项技能包

你可以只保留 MITRE ATT&CK 相关的技能,用于红队任务;或只保留 D3FEND 相关的防御技能,用于蓝队任务。例如在 Agent 的配置中:

json
1 2 3 4
{
  "active_framework": "mitre-attack",
  "skill_filter": ["recon", "initial-access", "execution"]
}

然后在生成 Prompt 时只加载匹配的技能。

变体2:多Agent协作时,技能列表作为API规范

如果你构建一个多Agent系统(一个指挥官 Agent 和多个执行 Agent),技能列表可以充当 RPC 接口文档。指挥官 Agent 调用执行 Agent 时,参数必须遵循技能结构中定义的格式。这避免了 Agent 之间因自然语言带来的理解偏差。

变体3:与工具调用(Tool Use)结合

当 Agent 可以执行真实命令时,将技能描述直接对应到工具函数。例如 recon-active-scan 技能对应一个 nmap_scan(ip, ports) 函数。Prompt 中只保留工具列表,不保留文字技能,Agent 通过选择工具来完成任务。这种方式更精确,但需要事先绑定好函数。

适用场景与边界

适用场景

  • 明确的、步骤化的安全任务:如渗透测试流程、漏洞扫描、合规检查。
  • 需要多步骤连贯性的工作流:侦察→利用→权限提升→数据提取。
  • 希望限制 Agent 动作范围,减少意外输出的场景:比如自动化安全巡检,不希望 Agent 擅自修改系统。

边界

  • 技能列表不可能穷举:遇到新攻击技术不在列表中,Agent 可能会直接拒绝或报错。你需要定期更新技能库。
  • 过于详细的技能列表会抑制创造力:安全领域有时需要 Agent 自行组合或探索不在列表中的操作。建议保留一个“其他(需人工确认)”技能作为兜底。
  • 前置条件校验增加复杂度:如果技能依赖的前置条件很多,Agent 可能频繁提问,降低效率。可以设置“自动尝试:如果前置条件明显满足,直接执行”。

总结(不是废话,是新视角)

回到开头的问题:同一个模型,为什么有的人用起来像失忆,有的人用起来像专家?答案不是模型本身,而是你给它的上下文结构。

结构化技能列表像给 AI 套上了动作框架——它不再需要猜测自己该做什么,不再需要在输出中天马行空地“发挥”,而是集中精力在有限的选项中选择并执行。正如这个项目所做的,安全领域因为其标准化的攻击/防御框架,非常适合这种方式。

下次再觉得 AI 跑偏,先问自己:你给它定义好技能了吗?

comparison table showing structured vs unstructured prompt performance in security tasks