Hermes Agent 高级技能(Skill)开发实战:打造你的专属智能体工具链


title: Hermes Agent 高级技能(Skill)开发实战:打造你的专属智能体工具链
date: 2026-06-04
tags: [Hermes Agent, AI, 自动化, Skill开发, 智能体]


Hermes Agent 高级技能(Skill)开发实战:打造你的专属智能体工具链

引言

当部署好 Hermes Agent 并体验到其强大的自动化能力后,下一个问题自然浮现:如何让智能体做我们想做的事?

答案就在 Skill(技能) 中。Skill 是 Hermes Agent 的核心扩展机制——就像给 AI 配备了一把瑞士军刀,每个技能文件就是一把功能独特的刀片。无论是文件批处理、数据库查询、代码审查,还是对接企业内部 API,都可以通过编写一个 Skill 来实现。

本文将带你从零开始,手写三个实用 Skill:文件清理工具、PostgreSQL 查询助手、以及自定义 Slack 通知器。全程代码实战,无需任何框架知识,仅需基础的 Python 和 YAML 理解。

核心概念:Skill 到底是什么?

Hermes Agent 中的 Skill 本质上是一个定义了 工具(tools)指令(instructions) 的模块。它告诉 Agent:

  • 什么时候调用这个技能(通过 triggers 或 instruction 描述)
  • 有哪些工具可用(shell 命令、Python 函数、API 调用等)
  • 工具的输入输出格式(参数类型、返回结构)

Skill 的目录结构

每个 Skill 存放在独立的目录中,典型结构如下:

~/.hermes/skills/my-custom-skill/
├── skill.yaml          # 技能元数据与工具定义
├── tools/
│   ├── cleanup.py      # Python 工具实现
│   └── slack_notify.py
└── templates/          # 可选:提示词模板
    └── report.md.j2

skill.yaml 核心字段

# ~/.hermes/skills/my-custom-skill/skill.yaml
name: "my-custom-skill"
description: "自定义技能集合:文件清理、数据库查询、Slack通知"
version: "1.0.0"
author: "your-name"

# 当用户提到以下关键词时,优先调用该技能
triggers:
  - "清理"
  - "数据库查询"
  - "Slack通知"

# 工具定义
tools:
  - name: "cleanup_temp_files"
    description: "清理指定目录下的临时文件(.tmp, .log, .cache)"
    command: "python3 {{SKILL_DIR}}/tools/cleanup.py"
    args:
      directory:
        type: string
        description: "目标目录路径"
        required: true
      days_old:
        type: integer
        description: "保留最近 N 天的文件(默认 7 天)"
        default: 7

  - name: "query_postgres"
    description: "执行 PostgreSQL 查询并返回结果"
    command: "python3 {{SKILL_DIR}}/tools/query_db.py"
    args:
      query:
        type: string
        description: "SQL 查询语句"
        required: true
      limit:
        type: integer
        description: "返回行数限制"
        default: 10

实战步骤:从零编写三个 Skill

Skill 1:文件清理工具

这是最实用的入门级 Skill。它能根据文件年龄、类型和大小,安全地清理指定目录。

第一步:创建目录结构

mkdir -p ~/.hermes/skills/file-cleanup/tools

第二步:编写 skill.yaml

# ~/.hermes/skills/file-cleanup/skill.yaml
name: "file-cleanup"
description: "智能文件清理:按类型、年龄、大小批量清理"
version: "1.0.0"
triggers:
  - "清理文件"
  - "释放空间"
  - "删除临时文件"

tools:
  - name: "cleanup_by_pattern"
    description: "按文件模式清理(如 *.tmp, *.log)"
    command: "python3 {{SKILL_DIR}}/tools/cleanup.py --action pattern"
    args:
      directory:
        type: string
        description: "要清理的目录"
        required: true
      pattern:
        type: string
        description: "文件匹配模式(如 '*.tmp')"
        required: true
      dry_run:
        type: boolean
        description: "预览模式,不实际删除"
        default: true

  - name: "cleanup_by_age"
    description: "清理超过指定天数的文件"
    command: "python3 {{SKILL_DIR}}/tools/cleanup.py --action age"
    args:
      directory:
        type: string
        description: "要清理的目录"
        required: true
      days:
        type: integer
        description: "保留最近 N 天内的文件"
        default: 30
      dry_run:
        type: boolean
        description: "预览模式,不实际删除"
        default: true

第三步:编写 Python 工具脚本

#!/usr/bin/env python3
# ~/.hermes/skills/file-cleanup/tools/cleanup.py
"""Hermes Agent Skill: 智能文件清理工具"""

import argparse
import os
import time
from pathlib import Path
from datetime import datetime, timedelta

def cleanup_by_pattern(directory: str, pattern: str, dry_run: bool = True):
    """按文件模式匹配并清理"""
    path = Path(directory).expanduser().resolve()
    if not path.exists():
        return {"error": f"目录不存在: {directory}"}

    deleted_count = 0
    deleted_size = 0
    results = []

    for f in path.rglob(pattern):
        if not f.is_file():
            continue
        size = f.stat().st_size
        if dry_run:
            results.append(f"[预览] 将删除: {f} ({size} bytes)")
        else:
            f.unlink()
            results.append(f"[已删除] {f} ({size} bytes)")
        deleted_count += 1
        deleted_size += size

    return {
        "status": "preview" if dry_run else "cleaned",
        "directory": str(path),
        "pattern": pattern,
        "files_matched": deleted_count,
        "total_size_bytes": deleted_size,
        "total_size_human": human_size(deleted_size),
        "details": results,
    }

def cleanup_by_age(directory: str, days: int = 30, dry_run: bool = True):
    """清理超过指定天数的旧文件"""
    path = Path(directory).expanduser().resolve()
    if not path.exists():
        return {"error": f"目录不存在: {directory}"}

    cutoff = time.time() - days * 86400
    deleted_count = 0
    deleted_size = 0
    results = []

    for f in path.rglob("*"):
        if not f.is_file():
            continue
        if f.stat().st_mtime < cutoff:
            size = f.stat().st_size
            mtime = datetime.fromtimestamp(f.stat().st_mtime).isoformat()
            if dry_run:
                results.append(f"[预览] 将删除: {f} (修改于 {mtime}, {size} bytes)")
            else:
                f.unlink()
                results.append(f"[已删除] {f} (修改于 {mtime}, {size} bytes)")
            deleted_count += 1
            deleted_size += size

    return {
        "status": "preview" if dry_run else "cleaned",
        "directory": str(path),
        "older_than_days": days,
        "files_matched": deleted_count,
        "total_size_bytes": deleted_size,
        "total_size_human": human_size(deleted_size),
        "details": results,
    }

def human_size(bytes_size: int) -> str:
    """将字节转换为人类可读格式"""
    for unit in ["B", "KB", "MB", "GB", "TB"]:
        if bytes_size < 1024:
            return f"{bytes_size:.2f} {unit}"
        bytes_size /= 1024
    return f"{bytes_size:.2f} PB"

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Hermes 文件清理工具")
    parser.add_argument("--action", required=True, choices=["pattern", "age"])
    parser.add_argument("--directory", required=True)
    parser.add_argument("--pattern", default="*.tmp")
    parser.add_argument("--days", type=int, default=30)
    parser.add_argument("--dry-run", action="store_true", default=True)
    parser.add_argument("--no-dry-run", action="store_false", dest="dry_run")

    args = parser.parse_args()

    if args.action == "pattern":
        result = cleanup_by_pattern(args.directory, args.pattern, args.dry_run)
    else:
        result = cleanup_by_age(args.directory, args.days, args.dry_run)

    print(json.dumps(result, indent=2, ensure_ascii=False))

Skill 2:PostgreSQL 查询助手

这个 Skill 让你的 Hermes Agent 能直接查询数据库并返回结构化结果。

skill.yaml 配置:

# ~/.hermes/skills/db-query/skill.yaml
name: "db-query"
description: "PostgreSQL 数据库查询助手"
version: "1.0.0"
triggers:
  - "查询数据库"
  - "执行SQL"
  - "数据库"

tools:
  - name: "run_sql_query"
    description: "执行 SQL 查询并返回 JSON 结果"
    command: "python3 {{SKILL_DIR}}/tools/query_db.py"
    args:
      query:
        type: string
        description: "完整 SQL 查询语句"
        required: true
      db_host:
        type: string
        description: "数据库主机地址(默认从环境变量读取)"
      db_name:
        type: string
        description: "数据库名称(默认从环境变量读取)"
      limit:
        type: integer
        description: "最大返回行数"
        default: 50

对应的 Python 实现:

#!/usr/bin/env python3
# ~/.hermes/skills/db-query/tools/query_db.py
"""Hermes Agent Skill: PostgreSQL 查询工具"""

import argparse
import json
import os
import psycopg2
from psycopg2.extras import RealDictCursor

def query_database(
    sql: str,
    host: str = None,
    port: int = 5432,
    dbname: str = None,
    user: str = None,
    password: str = None,
    limit: int = 50,
) -> dict:
    """执行查询并返回结构化结果"""
    # 从环境变量获取默认值
    host = host or os.environ.get("PGHOST", "localhost")
    dbname = dbname or os.environ.get("PGDATABASE", "postgres")
    user = user or os.environ.get("PGUSER", "postgres")
    password = password or os.environ.get("PGPASSWORD", "")

    # 安全限制:添加 LIMIT 防止全表扫描
    limited_sql = sql.rstrip().rstrip(";")
    if "limit" not in limited_sql.lower():
        limited_sql += f" LIMIT {limit}"

    conn = psycopg2.connect(
        host=host,
        port=port,
        dbname=dbname,
        user=user,
        password=password,
    )

    try:
        with conn.cursor(cursor_factory=RealDictCursor) as cur:
            start = time.time()
            cur.execute(limited_sql)
            rows = cur.fetchall()
            elapsed = time.time() - start

            return {
                "status": "success",
                "query": limited_sql,
                "row_count": len(rows),
                "execution_time_ms": round(elapsed * 1000, 2),
                "columns": list(rows[0].keys()) if rows else [],
                "rows": rows,
            }
    except Exception as e:
        return {"status": "error", "query": sql, "error": str(e)}
    finally:
        conn.close()

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--query", required=True)
    parser.add_argument("--db-host")
    parser.add_argument("--db-name")
    parser.add_argument("--limit", type=int, default=50)
    args = parser.parse_args()

    result = query_database(
        sql=args.query,
        host=args.db_host,
        dbname=args.db_name,
        limit=args.limit,
    )
    print(json.dumps(result, indent=2, ensure_ascii=False, default=str))

Skill 3:自定义 Slack 通知器

将 Agent 的自动化结果直接推送到 Slack 频道。

skill.yaml 配置:

# ~/.hermes/skills/slack-notify/skill.yaml
name: "slack-notify"
description: "发送通知到 Slack 频道"
version: "1.0.0"
triggers:
  - "发Slack"
  - "通知团队"
  - "推送消息"

tools:
  - name: "send_slack_message"
    description: "发送 Markdown 格式消息到指定 Slack 频道"
    command: "python3 {{SKILL_DIR}}/tools/slack_notify.py"
    args:
      channel:
        type: string
        description: "Slack 频道名称(如 #general, #dev-alerts)"
        required: true
      message:
        type: string
        description: "消息内容(支持 Markdown 格式)"
        required: true
      title:
        type: string
        description: "消息标题(可选)"
        default: "Hermes Agent 通知"
      color:
        type: string
        description: "消息颜色:good(绿)、warning(黄)、danger(红)、#自定义十六进制"
        default: "good"

Python 实现:

#!/usr/bin/env python3
# ~/.hermes/skills/slack-notify/tools/slack_notify.py
"""Hermes Agent Skill: Slack 消息推送"""

import argparse
import json
import os
import requests

def send_slack_webhook(
    webhook_url: str,
    message: str,
    title: str = "Hermes Agent 通知",
    color: str = "good",
    channel: str = None,
) -> dict:
    """通过 Slack Webhook 发送富文本消息"""
    # 如果没有指定 webhook_url,从环境变量读取
    webhook_url = webhook_url or os.environ.get("SLACK_WEBHOOK_URL")
    if not webhook_url:
        return {"status": "error", "error": "未设置 SLACK_WEBHOOK_URL 环境变量"}

    # 构建 Slack 消息 payload
    payload = {
        "attachments": [
            {
                "color": color,
                "title": title,
                "text": message,
                "mrkdwn_in": ["text", "fields"],
                "footer": "Hermes Agent",
                "ts": int(__import__("time").time()),
            }
        ]
    }

    if channel:
        payload["channel"] = channel

    try:
        resp = requests.post(
            webhook_url,
            json=payload,
            timeout=10,
            headers={"Content-Type": "application/json"},
        )
        if resp.status_code == 200:
            return {"status": "success", "channel": channel or "default", "title": title}
        else:
            return {
                "status": "error",
                "http_code": resp.status_code,
                "response": resp.text,
            }
    except Exception as e:
        return {"status": "error", "error": str(e)}

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--channel")
    parser.add_argument("--message", required=True)
    parser.add_argument("--title", default="Hermes Agent 通知")
    parser.add_argument("--color", default="good")
    parser.add_argument("--webhook-url", default=None)
    args = parser.parse_args()

    result = send_slack_webhook(
        webhook_url=args.webhook_url,
        channel=args.channel,
        message=args.message,
        title=args.title,
        color=args.color,
    )
    print(json.dumps(result, indent=2, ensure_ascii=False))

使用与调试技巧

编写完 Skill 后,如何确认它被正确加载了?

# 1. 重新加载 Skill(在 Hermes 会话中)
reload_skills

# 2. 查看当前加载的所有 Skill
list_skills

# 3. 实时查看日志,确认 Skill 是否正确注册
tail -f ~/.hermes/logs/hermes.log | grep -i "skill"

调试三板斧

问题 排查方法
Skill 未加载 检查 skill.yaml 格式(YAML 对缩进极其敏感,用空格而非 Tab)
工具找不到 确认 command 路径中的 {{SKILL_DIR}} 变量是否正确展开
参数解析失败 在终端直接运行 Python 脚本测试参数传入:python3 tools/cleanup.py --help

最佳实践

  1. 安全优先:文件操作类工具默认开启 dry_run=true,让用户确认后再实际执行
  2. 参数验证:Python 脚本内对输入参数做二次校验,不要完全信赖 Agent 的传参
  3. 错误处理:所有工具函数都返回结构化的 dict(包含 statuserror 字段),方便 Agent 理解
  4. 环境变量:敏感信息(数据库密码、API Key)通过 ~/.hermes/config.yaml 或操作系统环境变量传递,绝不硬编码

常见问题

Q: Skill 写好但 Hermes 说找不到这个工具?
A: 执行 hermes skill reload 重新加载配置。然后检查日志 tail -f ~/.hermes/logs/hermes.log 看 YAML 解析是否报错。

Q: 一个 Skill 可以包含多个工具吗?
A: 当然可以。上面的文件清理 Skill 就一口气定义了 cleanup_by_patterncleanup_by_age 两个工具。合理组织可大幅降低技能数量。

Q: Python 工具脚本里的依赖包怎么处理?
A: 在 Skill 目录下创建 requirements.txt 文件,然后运行 hermes skill install。Hermes 会自动为每个 Skill 创建独立的虚拟环境。

Q: 能让 Skill 调用其他 Skill 的工具吗?
A: 不能直接跨 Skill 调用。但 Agent 本身可以依次调用多个工具——你只需要在对话中描述完整的流程,Hermes 会自动编排。

总结

Skill 是 Hermes Agent 生态中最强大的扩展点。通过本文的三个实战案例,你已经掌握了:

  • ✅ Skill 的目录结构和 skill.yaml 配置规范
  • ✅ 文件清理工具的完整编写流程(含 dry-run 安全机制)
  • ✅ 数据库查询工具的数据处理和返回格式化
  • ✅ Slack 通知工具的外部 API 对接模式
  • ✅ 调试、日志和最佳实践

从今天起,不必再等待官方发布新功能——任何你需要的自动化能力,都可以通过编写 Skill 来实现。

下一步可以挑战更复杂的 Skill:对接 Jira API 自动创建工单、解析 Docker 日志并智能告警、甚至让 Hermes 帮你管理 K8s 集群。欢迎在评论区分享你写的 Skill!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片快捷回复

    暂无评论内容