第六篇:技能系统
概述
技能(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
技能的搜索路径按优先级排列:
~/.hermes/skills/:本地技能目录(最高优先级)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
解析策略:
- 检测
---分隔符 - 使用
yaml.CSafeLoader(C 扩展,性能优先)解析 YAML - 如果 YAML 解析失败,降级为简单的
key: value行级解析 - 返回
(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 才能避免冲突。
铁律的每个条目都包含:
- 明确的规则:做什么、不做什么
- 原因说明:为什么要这样规定
- 错误后果表:违反规则会导致什么问题
- 正确/错误对比:给出正反示例
| 错误操作 | 后果 |
|---------|------|
| `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 + 发送消息
每一步都包含:
- 检查项/规则:明确的条件判断
- 验证命令:可直接执行的 shell 命令
- 期望结果:明确的成功标准
### 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 铁律约束
铁律是技能中最关键的部分,以下是编写铁律的原则:
- 明确性:使用"绝不"、"永远"等绝对性语言,不留模糊空间
- 原因驱动:每条铁律必须解释为什么,LLM 理解原因后更可能遵守
- 后果可预测:列出违反规则的具体后果,利用 LLM 的"避免损害"倾向
- 正反对比:给出正确和错误的示例命令
- 数量控制:铁律数量控制在 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 决定是否加载该技能的主要依据。好的描述应该:
- 说明触发条件:"Use when deploying a Flask/web application..."
- 覆盖核心功能:"Handles port assignment, nginx config, systemd service..."
- 突出价值:"Ensures repeatable, safe deployments without breaking existing services"
- 控制在 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"))
思考题
-
渐进式披露的权衡:
skills_list()只返回名称和描述,Agent 需要额外调用skill_view()才能获取完整内容。这种设计在什么场景下会导致效率下降?你会如何优化? -
铁律的可靠性:LLM 有时可能忽略指令中的约束。cloud-deploy 技能的铁律设计使用了哪些策略来最大化 LLM 的遵从率?你能想到更强的约束机制吗?
-
技能冲突:假设两个技能同时匹配当前任务(例如
cloud-deploy和docker-deploy),Agent 应该如何选择?当前的技能系统是否支持优先级或互斥机制? -
环境变量安全:技能通过
required_environment_variables声明需要的环境变量。在 Gateway 模式(即时通讯平台)下无法安全输入密钥。除了提示用户手动配置.env文件外,你能想到更好的密钥管理方案吗? -
技能版本管理:当前的技能系统没有内置版本管理机制。如果一个技能被更新,已经加载了旧版本指令的 Agent 如何感知到变化?请设计一个技能版本通知机制。