你可能会遇到这样的情况:团队里的大模型 Agent 已经跑通了问答和对话,但一到具体业务动作就卡住——不能查真数据、不能调内部系统、不能按业务规则执行多步任务。每次想让它做一件新的事,都得改主代码、重新发布,甚至要动模型提示词。这种“僵化”的本质,是模型本身不具备工具使用和规则执行能力。
这也是为什么“技能化扩展”成了当前 Agent 工程里最热门的方向之一。把能力从模型体内剥离出来,做成可插拔、可组装、可独立维护的技能包,Agent 才能真正从“聊天机器人”变成“干活的人”。Hermes Skill 正是这类架构中一个很典型的实现思路。
这篇文章会围绕三个问题展开:Hermes Skill 到底是什么、解决了哪些工程痛点;怎么把一个自定义技能装进 Agent 并跑起来;在实际项目中,有哪些坑和最佳实践。内容以社区常见实现和通用工程模式为例,涉及版本和路径请以你实际使用的项目文档为准。
1. 为什么 Agent 需要 Skill,而不是“继续写代码”
很多人第一次接触 Agent 时,会有一个误解:既然模型能力不够,就给模型堆更多提示词;还不行,就直接在代码里硬编码业务逻辑。这种做法在 demo 阶段没问题,但一上生产就会出现三类典型问题。
第一,主程序变得越来越重。每增加一个工具函数,都要改动 Agent 的主入口代码,然后重新打包、重新部署。团队里多个人同时改一个入口文件,冲突几乎是不可避免的。
第二,模型与业务逻辑耦合过深。如果你把业务规则直接写进 system prompt,模型的输出波动会直接导致规则执行不稳定;如果你把规则写进代码分支,Agent 的扩展性又没了——每加一个需求就要加一个条件判断。
第三,能力的复用和隔离做得不好。一个“查询订单状态”的能力,可能在客服场景、运营场景、数据报表场景都要用。如果每次都是复制粘贴一份代码,后面的维护成本会让你怀疑人生。
Skill 机制要解决的就是这件事:把一项完整的能力,比如“调用某个 API”“执行一组业务动作”“查询某个数据库”,封装成一个独立单元。这个单元有自己的描述、触发条件、执行逻辑和输入输出约束。Agent 在运行时通过“技能清单”发现这些能力,再根据用户意图选择合适的技能去执行。
你可以把它理解成“手机上的 App Store 模式”。模型是操作系统核心,只负责理解和调度;Skill 则是装在系统上的应用,需要时安装,不用时卸载,互不干扰。对工程团队来说,新增一个能力不再意味着修改核心系统,而是提交一个技能包。整个发布节奏、职责边界和回滚策略都会清晰很多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先理解 Hermes Skill 的核心概念
在动手安装之前,有必要先把几个基本概念对齐。不同的 Agent 框架命名可能有差异,但结构和逻辑高度相似。基于当前社区的常见实现,Hermes Skill 通常包含以下五个核心部分。
2.1 Skill(技能单元)
Skill 是一个最小的能力封装单位。它由三部分组成:技能清单文件(manifest)、执行入口(handler)、以及可选的资源文件(配置、模板、脚本)。
一个 Skill 应当做到“输入可预期、输出可解析”。也就是说,Agent 调用它时只需要按照约定的参数格式传参,它返回的结果也应该是结构化的,比如 JSON。这种做法让模型侧的解析成本大幅降低,也让技能之间的组合成为可能。
2.2 Trigger(触发条件)
每个 Skill 都有触发条件。触发条件可以是关键词、意图匹配结果、正则表达式、事件类型、定时任务,甚至是另一个技能的输出。触发条件不是必须写在代码里,通常会在技能清单中用声明式的方式描述。
声明式的触发条件有一个好处:模型在决定使用哪个技能之前,可以先根据技能描述做语义匹配,而不是依赖硬编码的关键词列表。比如“查询天气”技能的描述是“根据城市名查询实时天气”,模型看到这条描述,就能在用户说“北京今天冷不冷”时,把这个请求路由到该技能。
2.3 Action(动作集合)
Action 是技能内部实际要执行的动作。一个技能可以包含多个 Action。Action 可以是 HTTP 请求调用,可以是一段 Python 函数,可以是数据库查询,也可以是文件的读写。
设计 Action 时,最重要的原则是“幂等”和“可重试”。Agent 在执行过程中可能遇到超时或异常,系统会尝试重试。如果 Action 不是幂等的,重试就可能导致重复下单、重复扣款等严重问题。幂等设计可以在接口层引入请求 ID,或者在数据层做唯一约束。
2.4 Manifest(技能清单)
Manifest 是技能的“身份证”和“说明书”。它通常是 YAML 或 JSON 格式的文件,包含技能名称、版本、描述、作者、依赖模块、触发规则、参数定义等元信息。
Agent 在启动时会扫描所有技能目录,读取各自的 Manifest,构建出一份技能注册表。模型在决策时看到的就是这份注册表的摘要。因此,Manifest 中的描述写得清不清楚,直接决定了模型能不能在正确的时机选择到正确的技能。
2.5 Runtime(执行环境)
Skills 不能凭空运行,它们需要一个宿主环境,也就是 Runtime。Runtime 负责加载技能、管理技能的生命周期、提供日志与监控、执行沙箱策略和安全检查。
Hermes Skill 的 Runtime 通常以本地进程或轻量级容器的方式运行。本地进程的方式部署简单,适合开发和内部工具;容器方式则更适合多租户或高隔离要求的生产环境。选择哪种方式,取决于你所在团队的运维能力和安全水位。
3. 环境准备:安装前先确认这些前提
下面进入实操部分。为了让步骤可复现,建议先准备一个干净的实验环境。目标不是一步到位上生产,而是先用最小路径跑通一个技能。
3.1 操作系统与基础软件
Hermes Skill 的常见实现基于 Python,因此以下条件都是围绕 Python 环境给出的:
- 操作系统:Linux / macOS / Windows 均可,但生产环境更推荐 Linux。
- Python 版本:3.8 及以上,建议使用 3.10 或更高版本,避免老版本在类型提示和异步支持上的兼容问题。
- Git:用于拉取项目源码,Windows 用户建议一并安装 Git Bash。
- 包管理器:pip 或 poetry,二选一即可。
- 虚拟环境工具:venv 或 conda。
如果你使用的是 3.12 等较新版本,请先确认你安装的依赖包是否都已适配。部分老旧的依赖库可能还不支持新版本 Python,遇到编译报错时可以先用 3.10 环境重试。
3.2 创建干净的工作目录
不建议直接在系统全局环境里做实验。先创建一个项目目录,并建立虚拟环境,避免依赖冲突。
bash复制mkdir -p hermes-demo && cd hermes-demo
python3 -m venv .venv
source .venv/bin/activate # Windows 下执行 .venv\Scripts\activate
看到命令行前缀出现 (.venv) 后,说明虚拟环境已生效。后续所有安装和运行命令都应在该环境中执行。
3.3 获取 Hermes Skill 框架代码
获取源码有两种常见方式。如果你所在网络环境可以访问仓库,直接通过 Git 克隆;如果只能拿到离线包,则用 pip 安装本地路径。
bash复制# 方式一:通过 pip 安装已发布的包(具体包名以官方发布为准)
pip install hermes-skill
# 方式二:克隆源码后以可编辑模式安装
git clone https://example.com/hermes/hermes-skill.git
cd hermes-skill
pip install -e .
安装完成后,可以验证命令是否存在:
bash复制hermes --version
如果能输出版本号,说明基础安装成功。如果没有这个命令,说明可执行入口没有正确写入 PATH,检查安装时是否有 console_scripts 配置。
4. 核心流程:从创建技能到注册运行
在实际项目中,一个 Skill 的完整生命周期包括创建、注册、加载、调用、验证和下线。下面按顺序拆解,并解释每一步背后的原因。
4.1 创建技能目录结构
一个标准的技能包,目录结构大致如下:
code复制skills/
└── get-time/
├── SKILL.md # 技能清单文件(Manifest)
├── handler.py # 技能入口实现
└── requirements.txt # 技能依赖声明
这里建议在项目根目录下统一建一个 skills/ 目录,每个技能一个子目录。Think 一下:如果把所有技能文件平铺在根目录,后面技能数量一多,管理和权限控制都会变得很麻烦。
4.2 编写技能清单文件
Manifest 是第一个要写的文件。用 YAML 写,因为它对人类可读性好,且支持注释。以下是一个“获取当前时间”技能的最小示例。
yaml复制# 文件路径:skills/get-time/SKILL.md
name: get_time
version: 1.0.0
description: 获取当前系统时间,可返回指定时区的时间。
author: your-team
runtime: python3
trigger:
type: intent
intents:
- query_time
- get_current_time
keywords:
- 现在几点
- 当前时间
- 几点了
actions:
- name: get_time
description: 返回当前时间字符串
input:
- name: timezone
type: string
required: false
default: "Asia/Shanghai"
output:
type: object
properties:
time: { type: string }
timezone: { type: string }
这段配置里,name 是技能的全局唯一标识,description 会作为模型判断是否调用该技能的重要依据。trigger 声明了两种触发方式:意图匹配和关键词匹配。actions 定义了技能对外暴露的动作及其输入输出结构。
注意:很多初学者会把 description 写得很模糊,比如“时间功能”。这句话对模型来说几乎没有任何帮助。更有效的写法是说明“这个技能做什么、输入是什么、输出是什么、适合什么场景”,描述越长越具体,模型选错技能的概率就越低。
4.3 实现技能入口
Handler 是技能的实际执行代码。它读取入参,执行动作,并返回一个可解析的字典或 JSON 字符串。
python复制# 文件路径:skills/get-time/handler.py
from datetime import datetime
from zoneinfo import ZoneInfo
import json
def handler(inputs: dict) -> str:
tz_name = inputs.get("timezone", "Asia/Shanghai")
try:
tz = ZoneInfo(tz_name)
now = datetime.now(tz)
result = {
"time": now.strftime("%Y-%m-%d %H:%M:%S"),
"timezone": tz_name,
}
except Exception as exc:
# 时区无效时返回错误信息,不要抛异常
result = {
"error": str(exc),
"message": f"无效的时区: {tz_name}",
}
return json.dumps(result, ensure_ascii=False)
这里有一个很重要的工程习惯:技能内部要自己捕获已知异常,并把错误信息作为正常返回值输出,而不是直接向 Runtime 抛异常。因为 Agent 的决策层通常是根据技能返回值来判断下一步动作的,如果技能直接崩掉,模型看到的是“运行失败”这样毫无信息量的信号;如果技能返回结构化错误信息,模型就能尝试换一个时区,或者提示用户修正输入。
4.4 注册技能并启动 Runtime
技能写完后,需要让 Runtime 扫描到它。通常有两种方式:在配置文件中声明技能目录路径,或者在启动命令里传入路径。
bash复制# 注册技能目录并启动交互式 Runtime
hermes run --skill-dir ./skills
启动后,如果出现类似以下日志,表示技能注册成功:
text复制[INFO] Loaded skill: get_time (v1.0.0)
[INFO] Registered action: get_time
[INFO] Listening for user input...
如果你使用的是配置文件方式,可以在项目根目录创建一个 hermes.yaml:
yaml复制# 文件路径:hermes.yaml
runtime:
skill_dirs:
- ./skills
log_level: INFO
然后在启动时指定配置:
bash复制hermes run --config hermes.yaml
5. 完整示例:让 Agent 学会“查本地时间”
为了完整演示,我们把上面三块内容整合成一个可以直接跑通的最小示例。假设个人电脑上已经有了 Python 3.10 和 Git,下面从零开始操作。
5.1 初始化环境
bash复制mkdir -p hermes-demo/skills/get-time
cd hermes-demo
python3 -m venv .venv
source .venv/bin/activate
pip install hermes-skill # 以你实际可用的包名为准
5.2 写入技能清单
用你喜欢的编辑器创建 skills/get-time/SKILL.md,内容就用上面 4.2 节的 YAML。
注意保存时不要用记事本默认的 ANSI 编码,统一用 UTF-8。否则技能描述里的中文在运行时可能乱码,导致模型意图匹配失败。
5.3 写入执行入口
创建 skills/get-time/handler.py,写入 4.3 节的 Python 代码。同样注意文件编码。
5.4 启动并测试
启动 Runtime 后,在交互界面输入以下测试语句:
text复制>>> 现在几点了?
[get_time] 触发意图: query_time
[get_time] 参数: {"timezone": "Asia/Shanghai"}
[get_time] 返回: {"time": "2025-04-02 10:30:00", "timezone": "Asia/Shanghai"}
如果能看到 [get_time] 相关的日志和返回内容,说明这个技能已经成功接入 Agent,并且模型能够根据自然语言做意图路由。
5.5 追加一个可以调用 HTTP API 的技能
光看时间还不够有说服力,再看一个稍微实用的技能:调用第三方天气 API。这个技能可以让你直观地感受“能力解耦”带来的好处——你只需要新增一个目录,不需要改动核心程序。
python复制# 文件路径:skills/get-weather/handler.py
import json
def handler(inputs: dict) -> str:
city = inputs.get("city", "北京")
# 这里只是演示框架,实际项目中请替换为你的真实 API 地址
url = f"https://api.example.com/weather?city={city}"
try:
# 实际请求代码省略,这里模拟返回
resp = {"city": city, "temperature": 23, "condition": "多云"}
return json.dumps(resp, ensure_ascii=False)
except Exception:
return json.dumps({"error": "Weather API request failed"}, ensure_ascii=False)
对应的 Manifest 中 description 可以写:“根据城市名称查询当前天气温度与天气状况,返回 JSON 格式数据,适合回答‘北京天气’这类问题。”
通过这个例子可以看到,一个技能的开发过程完全可以脱离主工程进行。团队里一个后端工程师负责写技能,另一个工程师负责维护 Agent 主框架,两者通过清晰的接口文档协作,效率会高很多。
6. 运行效果验证与调试方法
安装完技能,不是“能跑”就够了。你需要用一套可重复的验证方法,确认技能在真实调用链路中表现稳定。
6.1 验证技能清单是否能被正确解析
bash复制hermes skill check --skill-dir ./skills/get-time
如果清单有语法错误,比如 YAML 缩进不对、字段类型不匹配,命令会直接报错。这是最底层的验证,相当于语法检查。
6.2 验证技能能否被独立调用
在 Runtime 之外,你还可以直接用 Python 调用 handler 函数来验证核心逻辑:
bash复制python -c "from handler import handler; print(handler({'timezone': 'UTC'}))"
输出应包含当前 UTC 时间。如果这里都不通过,问题出在技能代码本身,而不是框架。先把这一层调通,再回到 Runtime 里测意图路由。
6.3 验证端到端意图路由
这是最接近用户真实操作的验证方式。在 Runtime 交互界面里,分别输入以下几种类型的语句,观察技能是否被正确触发:
- “现在几点了”
- “帮我查一下东京时间”
- “今天下午开会的时间是什么” (这条不应该触发,因为没有会议技能)
重点看第三类语句。如果模型把一条无关请求错误路由到了时间技能,说明技能描述太宽泛,或者关键词触发的优先级设置得太高。你可以把 trigger 中的 keywords 改得更精确,或者降低关键词匹配的权重。
6.4 查看日志定位问题
运行时日志是排查问题最重要的入口。Hermes Skill 常见的日志级别包括 DEBUG、INFO、WARNING、ERROR。遇到路由不准确时,建议临时把日志级别调整为 DEBUG:
bash复制hermes run --config hermes.yaml --log-level DEBUG
在 DEBUG 日志里,你会看到模型对用户输入的意图打分、候选技能列表、最终选中的技能名和原因。这些信息是优化技能描述和触发规则的一手依据。
7. 常见问题与排查思路
实践过程中,下面这些问题是出现频率最高的。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 技能加载失败,提示 Manifest 无法解析 | YAML 缩进错误、字段名拼写错 | 用 hermes skill check 校验清单 |
逐字段对照模板,检查是否缺少 name 或 description |
| 技能已加载,但 Agent 从不调用它 | 描述写得太宽泛或太长,模型无法理解触发场景 | 查看 DEBUG 日志中候选技能评分 | 重构 description,加入典型用户问题和输出说明 |
| 中文关键词无法触发 | 文件编码不是 UTF-8 | 检查文件编码,查看日志乱码 | 统一使用 UTF-8 保存清单和代码文件 |
| 技能抛异常后 Agent 直接崩溃 | Handler 没有捕获异常,Runtime 没有配置错误隔离 | 查看异常堆栈位置 | 在 Handler 内部捕获已知异常,返回结构化错误信息 |
| 调用外部 API 超时 | 网络环境问题,代码未配置超时时间 | 查看技能执行耗时日志 | 给 HTTP 调用设置合理超时,并在超时后返回可读错误 |
| 多个技能使用相同触发词互抢 | 触发条件重叠 | 调整 Trigger 中 priority 或优化关键词 |
为每个技能设置更明确的语义边界,避免通用词如“查一下” |
| 升级后老技能不能用 | 依赖版本冲突 | 对比依赖树 | 使用虚拟环境锁定依赖版本,升级前在测试环境验证 |
这里要特别提醒:如果技能里含数据库操作或者写文件的逻辑,排查问题时不要直接在现网数据上试。先复制到测试环境,用仿真数据验证,确认无误后再灰度上线。任何情况下都不要在生产环境执行不带条件限制的更新或删除操作。
8. 最佳实践:把 Skill 用在工程里的正确姿势
谈到这,你已经能写一个简单的技能,也能跑通调用链路了。但如果想让这个体系在团队里长期健康地运转,下面几条工程建议值得参考。
8.1 每个技能必须独立可测试
技能的 Handler 应当设计成“无状态”的函数式结构:输入字典,输出字典。这样的好处是,任何人都可以在不启动完整 Agent 的情况下,用单测或者命令行直接验证它。推荐为每个技能至少写一组单元测试,覆盖正常输入、边界输入和异常输入三组用例。
8.2 技能描述要写在清单里,而不是写在模型提示词里
技能的描述和调用参数必须统一由技能自身声明。模型侧只需要拿到编译后的技能注册表。这样就不会出现“提示词和代码对不上”的经典灾难。
8.3 给技能设置超时与限流
Agent 是外部可控的,技能在执行外部调用时,必须设置请求超时和并发限制。不然的话,一个失控的技能可能把上游系统打挂。建议在技能运行时加入两层控制:第一层是 Runtime 层面的全局超时,第二层是技能代码内部的局部超时。
8.4 最小权限与加密配置
技能如果需要访问数据库、云服务或内部系统,账号权限必须按最小权限原则申请。只授予它完成自己任务所需的最低权限,不要把整个集群的读写权限都塞给一个技能。密钥不要硬编码在代码里,通过环境变量或配置中心注入。一旦发现密钥有泄露风险,立即轮转。
8.5 技能也要做版本管理
技能是代码,也是业务资产。它应当纳入 Git 管理,并遵循语义化版本号。Manifest 中的 version 字段是运行时做兼容性判断的依据。发布新版本时,至少要保留上一个版本的回滚能力。如果是 A/B 测试,还可以通过在配置中心调整技能版本路由,实现灰度发布。
8.6 日志要结构化
日志不要只写给人看,还要方便系统排查。推荐输出 JSON 格式的结构化日志,包含技能名、请求 ID、入参摘要、出参摘要、耗时、错误码。这些字段是所有后续监控、告警与会话追踪的基础。没有结构化日志,线上出问题的时候,你会在服务器上翻到怀疑人生。
9. 总结与后续学习方向
Hermes Skill 这类技能化框架,本质上是把“模型能力”和“业务能力”做了清晰的分层。模型负责意图理解和决策调度,技能负责具体的动作执行和业务规则落地。对开发者来说,最直观的收益是:新增一个能力不再需要改动大系统,只需要新增一个技能目录;排查问题也不再需要通读全量代码,只看对应技能的日志即可。
如果你想把这条技术路线走得更深,建议按以下几个方向继续探索:第一,研究技能之间的调用编排,也就是一个技能执行完后如何触发另一个技能,形成多步任务流;第二,了解技能的沙箱与安全机制,在生产环境用容器隔离不是可选项,而是必选项;第三,关注技能评测体系,技能加得越多,越需要有自动化的回归测试来保证基础能力不劣化;第四,可以在团队内部建一个“技能市场”,把可复用的技能沉淀为公共组件,减少重复造轮子。
最后,给你一个简单的启动建议:不要一上来就规划几十个技能,先挑两个最常用的业务动作做成技能,跑通完整的流程——开发、注册、调用、灰度、回滚。等验证了这套机制真的能降低团队协作成本,再逐步扩展。技能化不是说越多越好,而是要让每一次能力扩展都变得更快、更可控、更安全。
