AgentHarness 课程

第六篇:技能系统

1.8万字·47分钟·
渐进式披露架构、SKILL.md规范、cloud-deploy实战

概述

技能(Skills)是 Hermes Agent 的知识注入机制。一个技能本质上是一份结构化的指令文档(SKILL.md),它在 Agent 运行时被动态加载到 system prompt 中,指导 Agent 如何完成特定类型的任务。技能系统采用了**渐进式披露(Progressive Disclosure)**架构——Agent 首先通过 skills_list() 获取所有技能的元数据(名称+描述),只有在确认某个技能与当前任务相关时,才通过 skill_view() 加载完整内容,最大限度地节省 token 开销。

本篇将深入分析技能的发现与加载机制、SKILL.md 格式规范、技能执行流程,并通过 cloud-deploy 部署技能这一真实案例展示如何编写生产级技能。


1. 技能发现与加载

1.1 渐进式披露架构

Hermes 的技能系统遵循 Anthropic 推荐的三层渐进式披露模型:

Tier 0: skills_categories()  →  分类名 + 描述(最省 token)
Tier 1: skills_list()        →  技能名 + 描述(元数据级)
Tier 2: skill_view(name)     →  SKILL.md 完整内容(指令级)
Tier 3: skill_view(name, file_path) → 关联文件内容(参考/模板级)

Agent 在处理任务时的工作流:

用户任务 → Agent 思考需要什么技能
        → skills_list() 浏览可用技能
        → skill_view("cloud-deploy") 加载匹配的技能
        → skill_view("cloud-deploy", "references/nginx.md") 按需加载关联文件
        → 按照技能指令执行任务

这种分层设计的关键优势是token 效率。一次 skills_list() 调用只返回每个技能的名称和描述(各不超过 64 和 1024 字符),而不是把所有技能的完整内容一次性塞入 prompt。

1.2 skills_list() 实现

skills_list() 函数(tools/skills_tool.py,第 711 行)是技能系统的核心入口:

def skills_list(category: str = None, task_id: str = None) -> str:
    """List all available skills (progressive disclosure tier 1 - minimal metadata).

    Returns only name + description to minimize token usage.
    """
    if not SKILLS_DIR.exists():
        SKILLS_DIR.mkdir(parents=True, exist_ok=True)
        return json.dumps({"success": True, "skills": [],
                          "message": "No skills found."})

    all_skills = _find_all_skills()

    if category:
        all_skills = [s for s in all_skills if s.get("category") == category]

    all_skills.sort(key=lambda s: (s.get("category") or "", s["name"]))

    return json.dumps({
        "success": True,
        "skills": all_skills,       # [{name, description, category}]
        "categories": categories,
        "count": len(all_skills),
        "hint": "Use skill_view(name) to see full content",
    })

1.3 目录扫描

_find_all_skills() 函数(第 512 行)负责递归扫描所有技能目录:

def _find_all_skills(*, skip_disabled: bool = False) -> List[Dict[str, Any]]:
    """Recursively find all skills in ~/.hermes/skills/ and external dirs."""

    skills = []
    seen_names: set = set()
    disabled = set() if skip_disabled else _get_disabled_skill_names()

    # Scan local dir first, then external dirs (local takes precedence)
    dirs_to_scan = []
    if SKILLS_DIR.exists():
        dirs_to_scan.append(SKILLS_DIR)           # ~/.hermes/skills/
    dirs_to_scan.extend(get_external_skills_dirs()) # config.yaml 中的 external_dirs

    for scan_dir in dirs_to_scan:
        for skill_md in scan_dir.rglob("SKILL.md"):
            # 排除 .git, .github, .hub 目录
            if any(part in _EXCLUDED_SKILL_DIRS for part in skill_md.parts):
                continue

            content = skill_md.read_text(encoding="utf-8")[:4000]
            frontmatter, body = _parse_frontmatter(content)

            # 平台兼容性检查
            if not skill_matches_platform(frontmatter):
                continue

            name = frontmatter.get("name", skill_dir.name)[:MAX_NAME_LENGTH]
            if name in seen_names or name in disabled:
                continue

            seen_names.add(name)
            skills.append({
                "name": name,
                "description": description,
                "category": category,
            })
    return skills

技能的搜索路径按优先级排列:

  1. ~/.hermes/skills/:本地技能目录(最高优先级)
  2. config.yaml 中的 skills.external_dirs:外部技能目录

get_all_skills_dirs()agent/skill_utils.py,第 227 行)封装了这一逻辑:

def get_all_skills_dirs() -> List[Path]:
    dirs = [get_skills_dir()]           # ~/.hermes/skills/
    dirs.extend(get_external_skills_dirs())  # 外部目录
    return dirs

外部目录的配置和解析(get_external_skills_dirs(),第 174 行):

def get_external_skills_dirs() -> List[Path]:
    """Read skills.external_dirs from config.yaml and return validated paths."""
    raw_dirs = skills_cfg.get("external_dirs")
    # 支持 ~ 和 ${VAR} 展开
    for entry in raw_dirs:
        expanded = os.path.expanduser(os.path.expandvars(entry))
        p = Path(expanded).resolve()
        # 去重,跳过不存在的目录
        if p.is_dir():
            result.append(p)
    return result

1.4 技能目录结构

一个典型的技能目录组织如下:

~/.hermes/skills/
├── software-development/
│   ├── cloud-deploy/
│   │   ├── SKILL.md           # 主指令文件(必需)
│   │   ├── references/        # 参考文档
│   │   │   └── api.md
│   │   ├── templates/         # 模板文件
│   │   │   └── service.conf
│   │   ├── assets/            # 补充资源 (agentskills.io 标准)
│   │   └── scripts/           # 可执行脚本
│   │       └── deploy.sh
│   └── dev-team/
│       └── SKILL.md
├── mlops/
│   └── axolotl/
│       ├── SKILL.md
│       └── references/
│           └── dataset-formats.md
└── DESCRIPTION.md             # 分类描述文件(可选)

2. SKILL.md 格式

2.1 YAML Frontmatter

每个技能的入口是 SKILL.md 文件,它由两部分组成:YAML Frontmatter(元数据)和 Markdown 正文(指令内容)。

完整 Frontmatter 规范

---
name: skill-name                # 必填,最长 64 字符
description: Brief description  # 必填,最长 1024 字符
version: 1.0.0                  # 可选
author: Author Name             # 可选
license: MIT                    # 可选 (agentskills.io)
platforms: [macos, linux]       # 可选 — 平台限制
                                #   macos / linux / windows
                                #   省略则所有平台可用

# 运行时依赖声明
prerequisites:                  # 可选(旧版,兼容)
  env_vars: [API_KEY]           # 需要的环境变量
  commands: [curl, jq]          # 需要的系统命令

# 环境变量声明(新版,推荐)
required_environment_variables:
  - name: API_KEY
    prompt: Enter your API key
    help: https://example.com/get-key

# 配置界面声明
setup:
  help: https://docs.example.com/setup
  collect_secrets:
    - env_var: API_KEY
      prompt: Enter your API key
      secret: true              # 标记为敏感值
      provider_url: https://example.com

# 凭证文件声明
required_credential_files:
  - ~/.ssh/id_rsa
  - ~/.config/gcloud/application_default_credentials.json

# 条件激活
metadata:
  hermes:
    tags: [deployment, nginx, cloud]     # 标签
    related_skills: [subagent-driven-development]  # 关联技能
    config:                              # 配置变量声明
      - key: wiki.path
        description: Wiki knowledge base path
        default: "~/wiki"
        prompt: Wiki directory path
    requires_toolsets: [filesystem]      # 需要的工具集
    requires_tools: [terminal]           # 需要的工具
    fallback_for_toolsets: [...]         # 降级工具集
    fallback_for_tools: [...]            # 降级工具
---

2.2 parse_frontmatter() 解析

Frontmatter 的解析由 agent/skill_utils.py 中的 parse_frontmatter() 函数(第 52 行)处理:

def parse_frontmatter(content: str) -> Tuple[Dict[str, Any], str]:
    """Parse YAML frontmatter from a markdown string.

    Uses yaml with CSafeLoader for full YAML support
    with a fallback to simple key:value splitting.
    """
    frontmatter: Dict[str, Any] = {}
    body = content

    if not content.startswith("---"):
        return frontmatter, body

    end_match = re.search(r"\n---\s*\n", content[3:])
    if not end_match:
        return frontmatter, body

    yaml_content = content[3 : end_match.start() + 3]
    body = content[end_match.end() + 3 :]

    try:
        parsed = yaml_load(yaml_content)
        if isinstance(parsed, dict):
            frontmatter = parsed
    except Exception:
        # Fallback: simple key:value parsing for malformed YAML
        for line in yaml_content.strip().split("\n"):
            if ":" not in line:
                continue
            key, value = line.split(":", 1)
            frontmatter[key.strip()] = value.strip()

    return frontmatter, body

解析策略:

  1. 检测 --- 分隔符
  2. 使用 yaml.CSafeLoader(C 扩展,性能优先)解析 YAML
  3. 如果 YAML 解析失败,降级为简单的 key: value 行级解析
  4. 返回 (frontmatter_dict, body_text) 元组

2.3 平台兼容性检查

skill_matches_platform() 函数(agent/skill_utils.py,第 92 行)确保技能只在兼容的操作系统上加载:

def skill_matches_platform(frontmatter: Dict[str, Any]) -> bool:
    """Return True when the skill is compatible with the current OS."""
    platforms = frontmatter.get("platforms")
    if not platforms:
        return True  # 未声明则兼容所有平台

    current = sys.platform
    for platform in platforms:
        normalized = str(platform).lower().strip()
        mapped = PLATFORM_MAP.get(normalized, normalized)
        # PLATFORM_MAP = {"macos": "darwin", "linux": "linux", "windows": "win32"}
        if current.startswith(mapped):
            return True
    return False

2.4 关联文件

技能可以包含关联文件,按类型组织在不同目录中:

目录用途文件类型
references/参考资料、API 文档、示例*.md
templates/输出模板、配置文件*.md, *.py, *.yaml, *.json, *.sh
assets/补充资源(agentskills.io 标准)任意
scripts/可执行辅助脚本*.py, *.sh, *.bash, *.js, *.ts

skill_view() 在加载主文件时会自动扫描这些目录:

# skill_view() 中关联文件的发现逻辑
if skill_dir:
    references_dir = skill_dir / "references"
    if references_dir.exists():
        reference_files = [str(f.relative_to(skill_dir)) for f in references_dir.glob("*.md")]

    templates_dir = skill_dir / "templates"
    if templates_dir.exists():
        for ext in ["*.md", "*.py", "*.yaml", "*.yml", "*.json"]:
            template_files.extend([...])

    assets_dir = skill_dir / "assets"
    scripts_dir = skill_dir / "scripts"
    # ...

3. 技能执行流程

3.1 Agent 匹配

当 Agent 接收到一个任务时,它会自主判断是否需要加载技能:

用户消息 → Agent 分析任务 → 判断是否匹配已知技能
  → 不匹配:直接处理
  → 可能匹配:调用 skills_list() 浏览
  → 确认匹配:调用 skill_view(name) 加载完整指令

Agent 通过 SKILLS_TOOL_DESCRIPTION(注册到工具系统时提供的描述)了解何时使用技能:

SKILLS_TOOL_DESCRIPTION = """Access skill documents providing specialized instructions,
guidelines, and executable knowledge.

Progressive disclosure workflow:
1. skills_list() - Returns metadata for all skills
2. skill_view(name) - Loads full SKILL.md content
3. skill_view(name, file_path) - Loads specific linked file

Skills may include:
- references/: Additional documentation, API specs, examples
- templates/: Output formats, config files, boilerplate code
- assets/: Supplementary files
- scripts/: Executable helpers
"""

3.2 加载与注入

skill_view() 加载技能内容后,将其作为工具返回值传递给 Agent。Agent 将技能内容整合到其推理上下文中。

完整的 skill_view() 返回值结构:

{
  "success": true,
  "name": "cloud-deploy",
  "description": "Use when deploying a Flask/web application to the cloud...",
  "tags": ["deployment", "nginx", "cloud"],
  "related_skills": ["subagent-driven-development"],
  "content": "---\nname: cloud-deploy\n...\n---\n\n# Cloud Deploy...",
  "path": "software-development/cloud-deploy/SKILL.md",
  "linked_files": {
    "references": ["references/api.md"],
    "templates": ["templates/service.conf"],
    "scripts": ["scripts/deploy.sh"]
  },
  "required_environment_variables": [
    {"name": "FEISHU_APP_SECRET", "prompt": "Enter Feishu app secret"}
  ],
  "setup_needed": false,
  "readiness_status": "available"
}

3.3 安全检查

skill_view() 在加载技能时执行多项安全检查:

1. 路径遍历防护

from tools.path_security import validate_within_dir, has_traversal_component

if has_traversal_component(file_path):
    return json.dumps({"success": False, "error": "Path traversal not allowed."})

target_file = skill_dir / file_path
traversal_error = validate_within_dir(target_file, skill_dir)

2. Prompt 注入检测

_INJECTION_PATTERNS = [
    "ignore previous instructions",
    "ignore all previous",
    "you are now",
    "disregard your",
    "forget your instructions",
    "new instructions:",
    "system prompt:",
]
_content_lower = content.lower()
_injection_detected = any(p in _content_lower for p in _INJECTION_PATTERNS)

3. 信任目录验证:检测技能是否来自配置的可信目录之外。

3.4 环境变量准备

技能可以声明所需的环境变量。skill_view() 在加载时会检查这些变量是否已配置:

env_snapshot = load_env()  # 从 ~/.hermes/.env 加载
missing_required_env_vars = [
    e for e in required_env_vars
    if not _is_env_var_persisted(e["name"], env_snapshot)
]

如果变量缺失且在 CLI 模式下,会触发交互式密钥收集:

if _secret_capture_callback:
    callback_result = _secret_capture_callback(
        entry["name"], entry["prompt"], metadata
    )

在 Gateway 模式下(通过即时通讯平台),由于无法安全输入密钥,会返回 gateway_setup_hint 提示用户手动配置:

GATEWAY_SECRET_CAPTURE_UNSUPPORTED_MESSAGE = (
    "Secure secret entry is not supported over messaging. "
    "Load this skill in the local CLI to be prompted, "
    "or add the key to ~/.hermes/.env manually."
)

4. 实战:编写部署技能

cloud-deploy 技能(源码位于 skills/software-development/cloud-deploy/SKILL.md)是一个生产级的部署技能,展示了技能设计的最佳实践。

4.1 技能元数据

---
name: cloud-deploy
description: Use when deploying a Flask/web application to the cloud dev environment.
  Handles port assignment, nginx config, systemd service, and Feishu notification.
  Ensures repeatable, safe deployments without breaking existing services.
version: 1.0.0
author: Hermes Agent
license: MIT
metadata:
  hermes:
    tags: [deployment, nginx, cloud, dev-environment, feishu, systemd]
    related_skills: [subagent-driven-development, writing-plans]
---

设计要点:

  • description 精准描述触发条件:明确说明"部署 Flask/web 应用到云端开发环境"这一场景,帮助 Agent 准确匹配
  • tags 覆盖多个维度:包含技术栈(nginx、systemd)、环境(cloud、dev-environment)、集成(feishu),便于多维度检索

4.2 铁律设计

cloud-deploy 技能最突出的设计是铁律(NO EXCEPTIONS)部分——这些是 Agent 在执行过程中绝对不能违反的规则:

## 铁律(NO EXCEPTIONS)

### 1. 绝不修改 Nginx listen 指令

listen 10.206.16.16:443 ssl;  ← 这一行永远不要改

**原因**: Tailscale 占用了 [::]:443 和 100.83.94.27:443。
Nginx 必须绑定到具体 IP 10.206.16.16 才能避免冲突。

铁律的每个条目都包含:

  1. 明确的规则:做什么、不做什么
  2. 原因说明:为什么要这样规定
  3. 错误后果表:违反规则会导致什么问题
  4. 正确/错误对比:给出正反示例
| 错误操作 | 后果 |
|---------|------|
| `listen 443 ssl` | 绑 0.0.0.0:443,与 Tailscale IPv6 冲突 |
| `systemctl restart nginx` | 重新绑定端口,可能失败 |

铁律的核心价值在于约束 LLM 的行为。LLM 在处理任务时可能选择"看起来合理但实际上危险"的操作(如 systemctl restart nginx),铁律以不容置疑的方式禁止这些操作。

4.3 标准化 6 步流程

技能正文定义了一个严格的 6 步部署流程:

Step 1: 确认项目就绪 → 检查目录、入口文件、依赖
Step 2: 分配端口 → 从 5000 开始,逐个检查空闲
Step 3: 添加 Nginx location → 用 sed 插入,nginx -t 验证
Step 4: 创建 systemd 服务 → 写入 unit 文件,enable + start
Step 5: 验证部署 → 本地直连 + 域名 HTTPS
Step 6: 飞书通知 → 获取 token + 发送消息

每一步都包含:

  1. 检查项/规则:明确的条件判断
  2. 验证命令:可直接执行的 shell 命令
  3. 期望结果:明确的成功标准
### Step 5: 验证部署

逐层验证,全部通过才算成功:

```bash
# 1. 本地直连
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:<端口>/
# 期望: 200

# 2. 域名 HTTPS
curl -sk -o /dev/null -w "%{http_code}" https://zhouyo.novemcloud.com.cn/dev/<项目slug>/
# 期望: 200 或 302(登录页)

### 4.4 故障排除与清理

技能还包含两个重要的补充部分:

**故障排除表**:

```markdown
| 现象 | 原因 | 解决 |
|------|------|------|
| Nginx 启动失败: bind() to 0.0.0.0:443 | listen 行被改了 | 确认 listen IP,用 start |
| 域名 502 Bad Gateway | Flask 应用没启动 | systemctl --user start |

清理/卸载流程:按部署的相反顺序执行,确保资源完全释放。


5. 技能最佳实践

5.1 铁律约束

铁律是技能中最关键的部分,以下是编写铁律的原则:

  1. 明确性:使用"绝不"、"永远"等绝对性语言,不留模糊空间
  2. 原因驱动:每条铁律必须解释为什么,LLM 理解原因后更可能遵守
  3. 后果可预测:列出违反规则的具体后果,利用 LLM 的"避免损害"倾向
  4. 正反对比:给出正确和错误的示例命令
  5. 数量控制:铁律数量控制在 3-5 条,太多会导致 LLM 忽略部分规则

5.2 错误处理

在技能中为每一步定义清晰的错误处理策略:

### 如果 `nginx -t` 失败

1. 立即回滚到备份:
   sudo cp /etc/nginx/conf.d/xxx.conf.bak.XXXX /etc/nginx/conf.d/xxx.conf
2. 不要尝试手动修复
3. 报告错误并等待用户指示

错误处理的原则:

  • 立即停止:遇到错误不要尝试"智能"修复,先停下来
  • 回滚优先:有备份时优先回滚,而不是在线修复
  • 清晰报告:向用户报告完整的错误信息和上下文
  • 等待指示:不要自行决定下一步

5.3 平台兼容性

通过 Frontmatter 的 platforms 字段声明兼容性:

# 仅 macOS
platforms: [macos]

# macOS 和 Linux
platforms: [macos, linux]

# 所有平台(省略或空列表)
platforms: []

skill_matches_platform() 使用以下映射:

PLATFORM_MAP = {
    "macos": "darwin",    # sys.platform == "darwin"
    "linux": "linux",     # sys.platform.startswith("linux")
    "windows": "win32",   # sys.platform == "win32"
}

5.4 描述编写

技能的 description 是 Agent 决定是否加载该技能的主要依据。好的描述应该:

  1. 说明触发条件:"Use when deploying a Flask/web application..."
  2. 覆盖核心功能:"Handles port assignment, nginx config, systemd service..."
  3. 突出价值:"Ensures repeatable, safe deployments without breaking existing services"
  4. 控制在 1024 字符以内:超过会被截断

5.5 关联文件使用

对于内容较长的参考资料,应放在 references/ 目录中,而非直接写在 SKILL.md 正文中:

skills/cloud-deploy/
├── SKILL.md                     # 部署流程(主指令)
└── references/
    ├── nginx-location.md         # Nginx location 配置详解
    └── systemd-service.md        # systemd unit 文件规范

Agent 只在需要时才通过 skill_view("cloud-deploy", "references/nginx-location.md") 加载关联文件,避免浪费 token。

5.6 配置变量声明

技能可以通过 metadata.hermes.config 声明配置变量,这些变量会在 hermes skills 配置界面中展示:

metadata:
  hermes:
    config:
      - key: wiki.path
        description: Path to the Wiki knowledge base directory
        default: "~/wiki"
        prompt: Wiki directory path

extract_skill_config_vars()agent/skill_utils.py,第 261 行)负责提取这些声明:

def extract_skill_config_vars(frontmatter: Dict[str, Any]) -> List[Dict[str, Any]]:
    """Extract config variable declarations from parsed frontmatter."""
    metadata = frontmatter.get("metadata")
    hermes = metadata.get("hermes") or {}
    raw = hermes.get("config")
    # ... 解析和验证
    return [{"key": "...", "description": "...", "default": "...", "prompt": "..."}]

5.7 技能禁用

用户可以在 config.yaml 中禁用特定技能:

skills:
  disabled:
    - axolotl
    - deprecated-skill
  platform_disabled:
    feishu:
      - terminal-heavy-skill

get_disabled_skill_names()agent/skill_utils.py,第 121 行)支持平台级别的禁用:

def get_disabled_skill_names(platform: str | None = None) -> Set[str]:
    resolved_platform = (
        platform
        or os.getenv("HERMES_PLATFORM")
        or get_session_env("HERMES_SESSION_PLATFORM")
    )
    if resolved_platform:
        platform_disabled = (skills_cfg.get("platform_disabled") or {}).get(
            resolved_platform
        )
        if platform_disabled is not None:
            return _normalize_string_set(platform_disabled)
    return _normalize_string_set(skills_cfg.get("disabled"))

思考题

  1. 渐进式披露的权衡skills_list() 只返回名称和描述,Agent 需要额外调用 skill_view() 才能获取完整内容。这种设计在什么场景下会导致效率下降?你会如何优化?

  2. 铁律的可靠性:LLM 有时可能忽略指令中的约束。cloud-deploy 技能的铁律设计使用了哪些策略来最大化 LLM 的遵从率?你能想到更强的约束机制吗?

  3. 技能冲突:假设两个技能同时匹配当前任务(例如 cloud-deploydocker-deploy),Agent 应该如何选择?当前的技能系统是否支持优先级或互斥机制?

  4. 环境变量安全:技能通过 required_environment_variables 声明需要的环境变量。在 Gateway 模式(即时通讯平台)下无法安全输入密钥。除了提示用户手动配置 .env 文件外,你能想到更好的密钥管理方案吗?

  5. 技能版本管理:当前的技能系统没有内置版本管理机制。如果一个技能被更新,已经加载了旧版本指令的 Agent 如何感知到变化?请设计一个技能版本通知机制。