☰
Hermes Agent 技能进化系统拆解:Skill 的元数据结构、自注册加载与退化机制
2026/9/28 6:51:44 网站建设 项目流程

1. 为什么 Skill 系统值得单独拆一层

如果你正在给 Agent 搭技能体系,大概率会遇到三个绕不开的问题:技能文件写成什么样才算「机器可读」、Agent 启动时怎么知道有哪些技能可用、以及技能在什么条件下应该被跳过而不是硬加载。Hermes Agent 的 Skill 系统把这三件事拆得很清楚——它用 SKILL.md 承载元数据,用 Skills Index 做启动期注入,用一套退化机制决定「这个技能现在能不能用」。

我试过把这套结构搬到自己的 Agent 项目里,最直观的感受是:Skill 不是静态文档,而是 Agent 的「程序性记忆」。它需要被解析、被索引、被条件加载,还要在环境不满足时优雅退场。这篇就按元数据结构、自注册加载、退化机制三条线拆开讲,每一步都给可复制的字段模板和配置骨架。适合已经写过一两个 Agent 工具、想把手写 prompt 升级成可扩展技能目录的开发者。

2. 前置准备:统一 Key 与 API 通道

在动手写 Skill 之前,先把模型调用通道固定下来。原因很实际:Skill 里经常要声明required_environment_variables,如果每个技能各自管一套 Key,环境变量检查会变成一团乱麻。用一个统一的 API 通道,技能只需要声明「我需要一个模型 Key」,而不用关心背后接的是哪家。

TaoToken 在这里的角色就是统一入口。官网在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 端点是 https://taotoken.net/api 。你可以在控制台创建 Key,然后在 Skill 的 frontmatter 里声明它。

具体操作路径:

  • 打开控制台 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,创建一个 API Key。
  • 到 API Keys 页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 复制 Key。
  • 把它写进环境变量,比如TAOTOKEN_API_KEY,后面 Skill 的required_environment_variables就引用这个名字。

这样做的收益是:技能目录里不出现任何具体厂商的 Key 名,退化机制里的环境变量检查只需要认一个变量。如果你后面要接 Claude Code 这类编码场景,也可以走 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claudecode&utm_campaign=rewrite 的通道,Key 体系保持一致。

3. SKILL.md 元数据结构:字段模板与解析逻辑

Skill 的核心是一个目录,主文件固定叫 SKILL.md。目录结构建议这样组织:

~/.hermes/skills/ ├── mlops/ │ └── axolotl/ │ ├── SKILL.md # 必需,主文件 │ ├── references/ # 可选,参考文档 │ │ └── dataset-formats.md │ └── templates/ # 可选,输出模板 │ └── config.yaml ├── devops/ │ └── check-server/ │ └── SKILL.md └── ...

SKILL.md 的头部是 YAML frontmatter,下面接 Markdown 正文。frontmatter 定义结构化信息,正文写工作流。一个可直接套用的模板:

--- name: check-server description: 检查远程服务器健康状态,返回 CPU、内存、磁盘摘要 version: 1.0.0 category: devops platforms: [linux, macos] required_environment_variables: - name: TAOTOKEN_API_KEY prompt: 请输入 TaoToken API Key metadata: hermes: tags: [ops, monitoring] related_skills: [restart-service] --- # Check Server Skill 用于快速检查服务器健康状态的工作流。 ## 步骤 1. 通过 SSH 连接目标主机 2. 执行 `uptime`、`free -m`、`df -h` 3. 汇总输出为表格

字段的约束需要记牢,否则解析会静默失败:

字段必需说明上限
name是Skill 唯一标识64 字符
description是列表展示摘要1024 字符
version否语义化版本—
category否分类路径—
platforms否运行平台约束—
required_environment_variables否环境变量需求—
metadata.hermes否tags / related_skills—

解析入口是_parse_frontmatter(),它本身不重复实现,而是委托给agent.skill_utils.parse_frontmatter()。这种「薄封装 + 单一实现」的写法值得借鉴:工具层只负责调用,真正的解析规则集中在一处,避免两套逻辑漂移。

环境变量字段支持三种输入格式,解析时要都兼容:

def _get_required_environment_variables(frontmatter, legacy_env_vars=None): required_raw = frontmatter.get("required_environment_variables") # 支持三种输入格式: # 1. 字符串列表: ["TAOTOKEN_API_KEY"] # 2. 带描述的 dict: {"name": "TAOTOKEN_API_KEY", "prompt": "输入 key"} # 3. 旧版 prerequisites.env_vars for item in required_raw: if isinstance(item, str): _append_required({"name": item}) if isinstance(item, dict): _append_required(item) # 旧版兼容 if legacy_env_vars is None: legacy_env_vars, _ = _collect_prerequisite_values(frontmatter)

这里有个容易踩的坑:required_environment_variables写成纯字符串列表时,Agent 只能拿到变量名,没法给用户友好提示。生产环境建议统一用 dict 格式,把prompt写清楚,CLI 弹窗时用户才知道该填什么。

4. 自注册加载:Skills Index 与工具注册

Skill 写完不会自动生效,它需要经过「索引构建 → System Prompt 注入 → 工具注册」三步。理解这条链路,才能知道为什么技能没被加载。

4.1 Skills Index 的构建与两层缓存

Skills Index 是连接文件系统和 System Prompt 的桥梁。它不是每次从磁盘全量扫描,而是用两层缓存:

  • 进程内 LRU 缓存:内存中缓存 index 构建结果,同一进程内重复构建直接命中。
  • 磁盘快照:~/.hermes/.skills_prompt_snapshot.json,跨进程重启也能恢复。

在prompt_builder.py里,是否注入 Skills Index 取决于当前 Agent 是否挂载了 skills 工具:

has_skills_tools = any( name in agent.valid_tool_names for name in ['skills_list', 'skill_view', 'skill_manage'] ) if has_skills_tools: skills_prompt = _r.build_skills_system_prompt( available_tools=agent.valid_tool_names, available_toolsets=avail_toolsets, compact_categories=_compact_cats or None, )

build_skills_system_prompt()返回的是索引,不是全部技能内容。每一行是一条记录,形如tools/skill_name: 简短描述。这个设计直接决定了 Token 成本——如果注入全文,几十个技能就能把上下文吃满。

compact_categories参数控制压缩策略。当当前目录和平台被识别为编码场景时,非编码类 Skill 在索引里只显示名称、不显示描述,能省下大约 60% 到 80% 的 Index Token。这个开关对技能数量多的项目很关键。

4.2 工具注册与 check_fn

Skill 的操作通过三个工具暴露给 Agent:skills_list、skill_view、skill_manage。它们在skills_tool.py模块顶层注册:

registry.register( name="skills_list", toolset="skills", schema={ "description": "按类别列出所有可用的技能,返回元数据", "parameters": { "type": "object", "properties": { "category": {"type": "string", "description": "按类别筛选(可选)"} } } }, handler=skills_list, check_fn=check_skills_requirements, )

注意check_fn是一个常量函数,始终返回 True:

def check_skills_requirements() -> bool: """Skills are always available — the directory is created on first use if needed.""" return True

这意味着 Skills 工具集永远可用,不会因为「环境不满足」被整体禁用。真正的条件判断下沉到单个 Skill 的加载阶段,而不是工具集层面。这个分层很重要:工具集是能力入口,必须稳定;单个技能是否可用,才交给退化机制处理。

4.3 斜杠命令绑定

skill_commands.py把 Skill 加载和 CLI 斜杠命令绑在一起。用户在终端敲/check-server时,走的是_load_skill_payload():

def _load_skill_payload(skill_identifier, task_id=None): raw_identifier = (skill_identifier or "").strip() if not raw_identifier: return None try: from tools.skills_tool import SKILLS_DIR, skill_view identifier_path = Path(raw_identifier).expanduser() if identifier_path.is_absolute(): # 安全检测:只允许在 SKILLS_DIR 和 external_skills_dirs 内的路径 for root in trusted_roots: try: normalized = str(identifier_path.relative_to(root)) break except ValueError: continue else: normalized = raw_identifier.lstrip("/") loaded_skill = json.loads( skill_view(normalized, task_id=task_id, preprocess=False) ) except Exception: return None

路径安全检测是这里最值得抄的一段。绝对路径只允许落在SKILLS_DIR和外部技能目录范围内,skill_view("/etc/passwd")这类调用会被挡掉。如果你自己实现技能加载,务必加这层白名单,否则技能系统会变成任意文件读取入口。

5. 退化机制:技能什么时候不该加载

「退化」不是 bug,是设计。Hermes 不会无差别加载所有 Skill,以下情况会跳过或标记不可用。

5.1 平台不匹配

_PLATFORM_MAP = { "macos": "darwin", "linux": "linux", "windows": "win32", } def skill_matches_platform(frontmatter) -> bool: from agent.skill_utils import skill_matches_platform as _impl return _impl(frontmatter)

如果 SKILL.md 声明了platforms: [macos],在 Linux 机器上就不会被加载。这个检查发生在索引构建阶段,技能连列表都进不去。

5.2 环境不匹配

skill_matches_environment()是「offer-time」级别的检查,只在工具发现阶段生效。也就是说,它影响的是技能是否出现在推荐列表里,而不是强制拦截。如果你显式调用skill_view("my-skill"),它会强制加载,不受环境检查影响。这个区分很实用:自动发现要保守,显式调用要给用户留后门。

5.3 所需环境变量缺失

当 Skill 声明了required_environment_variables而变量不存在时,行为分两种界面:

if _is_gateway_surface() and not env_var_enabled("HERMES_INTERACTIVE"): return { "missing_names": missing_names, "setup_skipped": False, "gateway_setup_hint": _gateway_setup_hint(), }

在 Gateway 界面(Telegram、Discord 等非交互场景)上,返回一条提示,告诉用户去 CLI 配置环境变量。在 CLI 上则直接弹窗让用户输入。这个分流避免了在无法交互的界面上卡死。

5.4 注入检测

skills_tool.py硬编码了一组 prompt injection 检测模式:

_INJECTION_PATTERNS: list = [ "ignore previous instructions", "ignore all previous", "you are now", "disregard your", "forget your instructions", "new instructions:", "system prompt:", "<system>", "]]>", ]

这些模式在skill_view()加载内容时检查。如果你从不可信来源复制了 SKILL.md,里面藏着「ignore previous instructions」,加载时会被探测到并告警。技能目录本质上是可执行内容,这层检测相当于给技能系统加了一道输入校验。

6. 验证请求:跑通一条完整 Skill 链路

理论讲完,动手验证。下面是从零到执行一个 Skill 的完整步骤。

第一步,创建技能目录和文件:

mkdir -p ~/.hermes/skills/devops/check-server cat > ~/.hermes/skills/devops/check-server/SKILL.md << 'EOF' --- name: check-server description: 检查服务器健康状态,返回 CPU、内存、磁盘摘要 version: 1.0.0 category: devops platforms: [linux, macos] required_environment_variables: - name: TAOTOKEN_API_KEY prompt: 请输入 TaoToken API Key metadata: hermes: tags: [ops, monitoring] --- # Check Server Skill ## 步骤 1. 执行 `uptime` 查看负载 2. 执行 `free -m` 查看内存 3. 执行 `df -h` 查看磁盘 4. 汇总为表格输出 EOF

第二步,设置环境变量:

export TAOTOKEN_API_KEY="你的Key"

第三步,启动 Agent,让它列出技能:

/skills_list

预期返回里应该能看到check-server,并且带 description。如果没出现,先检查platforms是否和当前系统匹配。

第四步,显式加载技能:

/check-server

加载成功时,Agent 会读取 SKILL.md 正文并执行其中定义的工作流。如果环境变量缺失,CLI 会弹窗要求输入,Gateway 会返回配置提示。

第五步,验证退化行为。把platforms改成[windows],重启 Agent,再执行/skills_list,check-server应该从列表里消失。但直接/check-server仍然能强制加载——这就是 offer-time 检查和强制加载的区别。

如果你要验证模型侧的调用是否正常,可以到模型对话页面 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite 发一条测试请求,确认 Key 和通道没问题。长期跑编码类技能的话,Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 更适合持续调用场景。

7. 本篇常见错排查

技能不出现,但文件确实存在。先看platforms字段。macos映射到darwin,windows映射到win32,写错平台名会导致静默跳过。其次看 frontmatter 是否合法 YAML——缩进错误会让解析整体失败,技能被当成无元数据文件忽略。

技能出现了,但加载时报环境变量缺失。检查required_environment_variables的格式。字符串列表和 dict 都支持,但 dict 格式才能带prompt。另外确认变量名拼写和实际导出的环境变量一致,大小写敏感。

索引里技能描述被截断或消失。这是compact_categories在起作用。编码场景下非编码类技能只显示名称。如果你希望某个技能始终显示描述,把它归到编码相关 category,或者检查当前目录是否被误判为编码场景。

修改 SKILL.md 后不生效。两层缓存导致的。进程内 LRU 缓存和磁盘快照~/.hermes/.skills_prompt_snapshot.json都可能返回旧数据。重启 Agent 进程,或者手动删除快照文件再启动。

加载内容触发注入告警。检查 SKILL.md 正文里是否包含_INJECTION_PATTERNS里的短语。有些正常的技术文档会写到「system prompt」这类词,如果确实需要,改写措辞避开检测模式。

绝对路径加载被拒绝。路径安全检测只允许SKILLS_DIR和外部技能目录内的路径。把技能放到标准目录下,或者用相对标识符加载。

8. 把技能体系接进你的项目

Skill 系统的价值不在于单个技能写得多漂亮,而在于「经验 → 模板 → 复用」这条管道能跑通。元数据结构决定了技能可被机器理解,自注册加载决定了技能能被自动发现,退化机制决定了技能在环境不匹配时不会拖垮整个 Agent。

落地时建议按这个顺序推进:先把 Key 通道统一到 TaoToken,控制台 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 建 Key,API Keys 页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 复制;然后按第 3 节的模板写第一个 SKILL.md,字段先求全再求简;接着用第 6 节的步骤验证加载和退化;最后把接入细节对照文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 过一遍,确认环境变量和调用方式没有偏差。

技能目录会随着项目增长越来越重,早一点把索引缓存和退化检查做对,后面加技能才不会变成负担。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询