这几年我一直在折腾AI智能体,技能扩展这块真是一言难尽。早期做过的几个项目里,每个智能体都有一套自己的技能方式:有的用自然语言指令、有的写死函数调用、有的自己搞一套插件协议,结果就是换一个场景就要重写一遍。后来接触到Hermes Skills System的思路,核心就是把“渐进式技能”和“Skills Hub”结合起来,用一套开放标准把技能的定义、注册、发现、调用串起来,才真正把这块理顺了。这篇文章就聊聊这套系统的设计逻辑、我落地过程中的实操记录,以及那些文档里不会写的坑。
不管你是正在做AI Agent应用,还是在设计企业内部的知识助手,或者单纯对“技能如何标准化”感兴趣,这篇都值得花十分钟看看。我尽量说人话,把原理和细节揉开来讲,保证你能直接抄作业。
1. 技能扩展的碎片化困局与渐进式思路
1.1 为什么每个智能体都在重复造轮子
先说一个我观察到的普遍现象:很多做AI应用的人,第一版都是让模型直接背一堆工具函数,函数写在主程序里,参数靠系统提示词硬描述。然后智能体一多、场景一多,就开始乱了。
我自己之前在某个跨平台系统上接过多个渠道的咨询助手,当时光是“查订单”这一个功能,就在三个项目里分别实现过三遍,接口签名还不一样。更难受的是,每个技能都只能服务当下的模型和框架,换一个底层模型,很多技能就跑不动了,因为它们耦合了太多模型私有的能力格式。
问题根源就在于:技能缺少统一的描述规范和执行协议。大家都把技能当成“给某个模型写的一段提示词”,而不是当成一个独立、可复用的模块。
而Hermes Skills System的设计思路恰好相反:它把技能看作一份独立的资源,用标准化的manifests描述它,用统一的接口暴露它,再通过Skills Hub这个中心化的仓库去注册和发现。这样一来,技能和数据、模型彻底解耦,谁都能发布,谁都能调用,只要大家遵守同一个标准。
1.2 渐进式技能的核心:从原子能力到复杂流程
“渐进式技能”这个词,说白了就是技能是有层级的,不是一把梭。我理解它的核心是把技能拆成从小到大的积木块,按需组合。
我用了三层划分,实际操作中比较顺手:
| 层级 | 能力类型 | 典型例子 | 依赖关系 | 维护成本 |
|---|---|---|---|---|
| L1 原子技能 | 单次操作 | 查汇率、查天气、发消息 | 无依赖 | 低 |
| L2 组合技能 | 编排多个原子技能 | 比价、行程规划 | 依赖L1技能 | 中 |
| L3 流程技能 | 有状态的长流程 | 商品下单、客服工单处理 | 依赖L1+L2 | 高 |
为什么要这么做?直接原因是一次性实现一个庞大技能,太容易失败。技能越大,参数就越多,模型越难正确调用,一旦出错,排查成本也线性上升。
渐进式的价值在于:你不需要一开始就把所有能力做全,而是先做最小可用的原子技能,让业务先跑起来,然后再根据实际使用数据,组合出更复杂的L2、L3技能。这跟写代码从函数到模块再到系统是一个道理。我见过太多上来就想做一个“全能助手”的项目,技能描述写得像论文,结果模型根本不知道该调哪个,准确率感人。
所以,渐进式不只是技术设计,更是一种实施策略:先跑通最小闭环,再逐步加深能力,每层之间通过统一标准衔接,互不拖累。
1.3 开放标准为什么能破局
类比一下:大家都用USB-C口,是因为线上有了统一物理协议,不需要每家设备一个充电头;技能也是一样,智能体是设备,技能是配件,Skills Hub是认证和分发渠道,开放标准就是那条统一协议。
没有标准的时候,A团队的“翻译技能”B团队用不了,因为输入输出格式、调用方式都不同。有了Hermes这套规范之后,只要技能发布方按规范写manifest、暴露标准接口、提交到Skills Hub,任何遵守相同标准的调用方都能无缝使用。
这套标准的三个层面,我拆开看是这么个结构:
- 描述层:技能“长什么样”能被外界理解。包括名称、用途、输入输出Schema、版本、权限。
- 执行层:技能“怎么被调用”。定义统一的invoke接口、事件机制、错误码。
- 治理层:技能“凭什么被信任”。包括签名校验、健康度监控、下线机制。
Skills Hub在这个体系里扮演的是注册中心加可信源的角色,它不负责执行技能,只负责让人找到对的技能、用对的方式调用。
2. Hermes Skills System 的架构拆解与关键设计
2.1 技能清单(Manifest):让智能体知道“有什么可用”
Manifest是一部技能的“说明书”,它解决了智能体最头疼的问题——到底有哪些能力、参数怎么填、会返回什么。
刚开始我自己写技能描述的时候非常简略,比如“翻译用户输入”,结果模型经常把目标语言参数漏掉,或者把source_text和target_lang的顺序搞反。后来我参考了Hermes的规范,把manifest结构化,问题少了一大半。
一个标准的manifest长这样:
{ "schema_version": "1.2", "skill": { "name": "get_exchange_rate", "namespace": "finance.currency", "version": "1.4.0", "title": "查询实时汇率", "description": "根据源货币和目标货币返回最新汇率,支持常用法币和主流数字货币。", "tags": ["finance", "currency", "rate"], "intents": [ {"positive": "1 USD equals how many EUR?"}, {"positive": "人民币对日元汇率是多少"}, {"negative": "帮我买100美元"} ], "input_schema": { "type": "object", "properties": { "from": {"type": "string", "description": "源货币代码,ISO 4217", "example": "USD"}, "to": {"type": "string", "description": "目标货币代码", "example": "EUR"}, "amount": {"type": "number", "description": "可选,转换金额", "minimum": 0} }, "required": ["from", "to"] }, "output_schema": { "type": "object", "properties": { "rate": {"type": "number", "description": "实时汇率"}, "timestamp": {"type": "string", "description": "汇率时间戳"} }, "required": ["rate", "timestamp"] }, "permissions": ["network:finance"], "timeout_ms": 3000 } }别小看那几个附加字段,它们都解决实际问题:
intents字段里的负例(negative)很有用,它告诉智能体“这不是我这个技能该管的”,防止技能被乱调用。example字段给模型提供了填充参数的参考,模型在不确定的时候更容易照着示例来。timeout_ms是给自己留的后路,防止技能卡死拖垮整个链路。
写manifests最核心的心得就一句话:字段不是越少越好,而是“用模型能理解的方式把边界描述清楚”。一份好的manifest,等于给模型做了一次有效的工具预训练提示。
2.2 执行接口:技能与智能体之间的“插头”
描述做得好,实际调用时还得有个统一接口,不然每个技能都各搞一套,调用方写适配器就得写疯掉。
Hermes这套设计里,执行接口被强制规定为:
POST /v1/skills/{namespace}/{name}/invoke请求体统一包一层,比如:
{ "request_id": "a6f3c21e-9f42-4a2b-9a3d-8f8e4d2b1a7c", "params": { "from": "USD", "to": "EUR" }, "context": { "user_id": "u_10086", "channel": "web" } }响应也做了标准化:
{ "request_id": "a6f3c21e-9f42-4a2b-9a3d-8f8e4d2b1a7c", "status": "ok", "result": { "rate": 0.9234, "timestamp": "2025-04-05T10:20:30Z" }, "trace_id": "trace_8f8e4d2b" }这里有几个容易被新手忽略的细节:
第一,request_id和trace_id一定要保留,它们是排查问题的主线。没有它们,出错以后只能靠猜。
第二,status字段用枚举值:“ok”、“error”、“timeout”,不要花哨,机器好判断是第一位的。
第三,技能内部如果还有长时间操作,比如拉取数据要很久,不要阻塞调用方,应该主动返回status: "in_progress",并通过异步事件频道推送结果。这是渐进式技能里很关键的一环,后面实操我会展开。
接口统一的最大好处是:调用方不需要感知技能内部的实现语言、框架、部署位置,只要通过HTTP和标准JSON交互就行。我后来把一些技能用不同语言重写,调用方代码一行都没改,体验非常舒服。
2.3 注册与发现:Skills Hub 如何运作
Skills Hub是整个体系的大脑中枢。它做了几件事:接收技能发布者的提交、对manifest做格式校验、执行健康度检测、提供检索接口、管理版本与下线。
调用方的使用流程是:先通过Skills Hub的关键词或语义检索找到技能,拉取manifest,再按权限配置直接去调用技能地址。如果技能是在Hub里托管的,Hub还会做灰度发布和流量切换,技能发布方不需要自己处理这些运维细节。
早期我实现一个轻量版Hub的时候,只做了两件最基本的事:技能列表的增删改查,加上一个/api/discover接口,用关键词匹配名字和描述。这个极简版本已经能支撑项目运转,后面随着技能多了,再慢慢加健康度评分、调用量统计、安全签名校验这些功能。
这是一个典型的渐进式演进思路——Hub本身也可以从最小可用开始,不要一开始就追求大而全。
3. 从零接入一个渐进式技能的实操记录
这一节我不讲空理论,直接带你走一遍完整过程:定义技能、实现执行函数、发布到Skills Hub、渐进式增强。
3.1 第一步:定义你的第一个最小技能
为了演示,我选一个很常见的场景——“汇率查询”。这是典型的L1原子技能,边界清晰、输入输出简单、没有外部状态,非常适合作为入门案例。
我定义的manifest还是用上一节那个例子,但这里我建议你在开发初期把timeout_ms设成3000毫秒以内,宁可先设为超时失败,也不要让调用方在等待中失去耐心。
一个细节:技能名get_exchange_rate不要叫exchange_rate或currency。命名规则遵循“动词+名词”,这样模型一看到名字就能猜出它的行为,调用命中率会高很多。这是我把技能库里上百个技能的命中率对比之后总结出来的规律,没有例外。
3.2 第二步:实现执行函数与参数校验
我直接用一个Python示例来说明,这是技能执行服务的核心代码片段,不依赖任何重型框架:
import json import time import logging from datetime import datetime, timezone logger = logging.getLogger("skill_executor") # 模拟汇率查询的底层数据源 MOCK_RATES = { ("USD", "EUR"): 0.9234, ("CNY", "JPY"): 20.354, ("HKD", "USD"): 0.1276, } def validate_params(params): errors = [] from_code = params.get("from") to_code = params.get("to") if not from_code or not to_code: errors.append("缺少货币代码") if from_code and len(from_code) != 3: errors.append("货币代码必须为3位") if to_code and len(to_code) != 3: errors.append("目标货币代码必须为3位") return errors def invoke_get_exchange_rate(params): start = time.time() errors = validate_params(params) if errors: return { "status": "error", "code": "INVALID_PARAM", "message": "; ".join(errors), } from_currency = params["from"].upper() to_currency = params["to"].upper() # 故意加一次超时判断,模拟业务常见问题 if time.time() - start > 2.8: return { "status": "timeout", "code": "SKILL_TIMEOUT", "message": "技能执行超时", } rate = MOCK_RATES.get((from_currency, to_currency)) if rate is None: return { "status": "error", "code": "RATE_NOT_FOUND", "message": f"暂不支持 {from_currency} -> {to_currency} 的汇率查询", } return { "status": "ok", "result": { "rate": rate, "timestamp": datetime.now(timezone.utc).isoformat(), }, }这段代码看起来简单,但有几个关键设计不是随便写的:
validate_params独立成一个函数,不直接写在主逻辑里。这样方便扩展,后续有新的参数校验规则可以直接加。- 所有错误返回都有
code和message,而不是只返回一个失败状态。有了错误码,调用方才能做针对性处理,比如RATE_NOT_FOUND就可以直接提示用户换币种,而INVALID_PARAM则需要调整参数。 - 超时判断放在了业务逻辑之前,这有点“预防性编程”的味道。我实践下来,技能执行中最容易被吐槽的就是超时,把超时检测前置,能少接很多投诉。
3.3 第三步:发布到 Skills Hub 并验证调用链路
执行函数写好之后,把服务跑起来,然后用任意HTTP客户端调用一次接口,验证基本通路。
curl -X POST http://127.0.0.1:8080/v1/skills/finance.currency/invoke \ -H "Content-Type: application/json" \ -d '{ "request_id": "manual-test-001", "params": {"from": "USD", "to": "EUR"} }'正常的响应应该是:
{ "status": "ok", "result": { "rate": 0.9234, "timestamp": "2025-04-05T10:20:30Z" } }确认执行服务没问题后,接下来把技能的manifest提交到Skills Hub。
Hub收到提交后会自动做几件验证:检查schema_version是否满足最低要求、校验input_schema/output_schema格式是否符合JSON Schema规范、扫描permissions字段是否在允许列表内、并运行一次“冒烟调用”来确认技能真实可用。
我之前在内部实践的时候,用了一个极简的Skills Hub,注册接口就是一个POST /v1/skills,接收manifest JSON。冒烟调用只是发一次空参数请求,看返回是不是标准的错误。这已经能过滤掉很多明显错误。
注册成功后,技能就可以通过Hub检索到了。我常用的检索方式是语义搜索,比如用户问“人民币换成日元多少钱”,Hub会找到get_exchange_rate技能,并带上对应的参数映射关系返回给调用方。
如果检索出来的技能有多个,Hub还会返回一个相关度分数,调用方可以根据这个分数来决定用哪个。这一块实际效果好不好,很大程度取决于manifest里的description和intents写得到不到位。
3.4 三条渐进式增强路径
技能接入之后,业务方自然会提出更多需求。这时候不要慌,渐进式增强有三条典型路径,我根据自己的实践给你梳理一下。
路径A:从单一技能到组合技能。汇率查询上线后,产品经理很快会提:能不能查完汇率顺便算一下“100美元能换多少欧元”?
这个需求不需要重写技能,只需要新增一个L2技能calc_conversion,它内部依赖get_exchange_rate,执行时先调汇率再算乘法。组合技能的manifest里,dependencies字段要写明依赖的原子技能:
{ "skill": { "name": "calc_conversion", "namespace": "finance.currency", "version": "0.9.0", "dependencies": ["finance.currency/get_exchange_rate@^1.4.0"], "input_schema": { "properties": { "from": {"type": "string"}, "to": {"type": "string"}, "amount": {"type": "number"} }, "required": ["from", "to", "amount"] } } }如果汇率查询失败,组合技能要能优雅降级,比如直接返回“汇率查询暂时不可用,请稍后再试”,而不是把底层错误原样抛给用户。
路径B:从同步调用到流式进度反馈。有的技能耗时较长,比如生成报告、批量图片处理,如果同步等待,用户很可能以为卡死了。
渐进式做法是把技能拆成两步:先返回一个task_id,技能执行过程中通过事件接口推送进度,调用方拿到进度再决定何时拉取最终结果。
{ "status": "in_progress", "task_id": "task_7f3a9c2e", "progress_url": "/v1/tasks/task_7f3a9c2e" }这种改造不需要动核心业务逻辑,只是在执行入口包一层异步壳,但用户体验会有质的提升。
路径C:从显式编排到策略化选择。技能跑到一定数量后,智能体不该每次都精确调用某一个技能,而是可以根据用户意图、上下文、历史成功率灵活选择。
这一层就不是简单加个函数了,而是要给技能加“能力画像”。包括支持场景标签、性能指标、历史调用成功率,让智能体有足够信息做策略判断。渐进式地做到这一步,你的技能系统就已经从“工具集合”真正变成了“智能体能力的一部分”。
4. 落地中的坑:实战问题与排查实录
4.1 最伤脑筋的:技能描述引发参数幻觉
先说一个我踩过的最大坑。早期的某个技能,描述只写了“把文本翻译成英文”,结果模型调用时完全不传参数,或者把原文和目标语言搞混,平均失败率超过四成。
后来我学乖了,所有技能的description都按“输入是什么+输出是什么+边界是什么+举例是什么”的模板来写。上面汇率技能的manifest里,"description"就是这么设计的。实践下来,参数错误率从40%降到10%以下。
还有一个技巧:把常见的反例写进intents的negative里,比如翻译技能里写明“如果用户没提供源语言,不要默认猜成中文,先向用户确认”。模型看到这些硬规则后,行为会稳定很多。
4.2 版本升级把调用方搞崩了
技能也会迭代,但版本升级不是你想怎么改就怎么改。我吃过一次苦头:某个技能从v1.2升到v1.3,把result.kind改成了result.type,结果忘记通知下游,某天凌晨线上大量报错,排查了半天才发现是字段名变了。
这之后我定了几条铁律:
- 严格按SemVer语义化版本,主版本号变了就说明有不兼容变更,必须走技能下线重发流程。
- manifest里的字段只做加法,老字段至少保留两个大版本周期。
- 每次发布新版本前,先在测试环境跑一遍冒烟测试,再切实时流量。
这些规则不是Hub强制的,而是团队内部约定,但待到技能数量超过五十个的时候,你会庆幸自己立了这些规矩。
4.3 Skills Hub 里技能越来越多,怎么建立信任和淘汰僵尸技能
技能一多,Hub就成了一个小生态,这时候最大的两个问题是信任和淘汰。
信任方面,我在Hub里加了技能发布者的签名校验,并增强技能清单。运行技能时优先跑在受限的沙箱环境里,避免恶意技能偷数据或者做危险操作。这就是所谓“开放但不失控”。
淘汰方面,我定义了一套“僵尸技能”判定标准:连续六十天没有任何调用、或者失败率持续高于50%的技能,会被自动标记为不推荐,再过三十天自动下线。
这个机制保障了Hub里的技能质量,否则用户搜到一堆不可用的技能,整个系统信誉都会崩掉。
4.4 一个典型超时故障的完整排查过程
有一次某个技能在高峰期偶尔超时,但是接口状态码全是200。每次用户反馈问题,看日志才发现内部已经跑了6秒多。
整个排查过程我分成四步:
第一步,先看调用链日志。拿到trace_id后,把该请求的耗时曲线拉出来,发现大部分时间花在技能内部调用的另一个外部接口上。
第二步,压测那个外部接口,发现它在并发超过一定量时响应会很慢,单次最长达到4秒。
第三步,在技能执行层加上超时熔断,外部接口超过2秒就直接降级返回缓存数据。同时把技能的整体timeout_ms调成2500毫秒,宁可偶尔失败,也不拖垮整体。
第四步,上线一周后观察失败率,超时问题下降了九成,缓存命中率稳定在80%以上。
这个案例最值得记住的一点是:排查问题的时间轴里,第一阶段永远是看日志和trace,不是猜。没有统一的request_id和trace_id,这种问题至少要排查一天以上。
4.5 我常用的三个小技巧
分享几个代码和文档里不会教你的经验:
第一个技巧:在manifest中加一个“典型调用样例”字段,把用户最容易问的问题直接写好。比如“100美元能换多少日元”对应会传什么参数。模型在few-shot场景下特别吃这一套。
第二个技巧:技能命名统一用“动词+对象”格式,比如“查询库存”、“生成摘要”、“发送通知”。那些形容词式的命名像是“库存功能”、“有用的工具”,几乎都会降低模型调用准确率。
第三个技巧:给技能加一个隐藏的“自检模式”。技能发布者用内部参数调用一次,技能返回自身状态和依赖健康度。上线前先自检一遍,能拦住大量低级错误。
5. Skills Hub 开放标准对生态的更深影响
5.1 规范的层次:协议、运行时、治理各自解决什么问题
我接触这套标准越久,越觉得它其实是在做一次“分工”。
协议层解决的是互通问题——大家用同一套manifests和调用接口,技能才能跨系统复用。运行时解决的是执行问题——技能该跑在哪里、怎么隔离、如何提供流式反馈。治理层解决的是信任问题——谁能发布、怎么验证、怎么退出。
把这三层分开,整个体系才不容易乱。
我见过一些团队做技能平台,只做了协议层,忽略了运行时和治理,结果平台变成“技能垃圾场”,没有质量底线。反过来,只做治理层而没有协议层,那不过是个审批系统。三者的关系做一张表更清楚:
| 层次 | 核心问题 | 关键机制 | 失败场景 |
|---|---|---|---|
| 协议层 | 不同系统之间怎么互通 | Manifest规范、统一接口 | 技能格式各异,无法复用 |
| 运行时层 | 技能在哪里、怎么稳定执行 | 沙箱隔离、超时熔断、流式事件 | 技能互相拖累,环境不可控 |
| 治理层 | 技能凭什么可信 | 签名校验、健康度评分、僵尸下线 | 技能质量下滑,调用方失去信心 |
5.2 从“模型能力竞争”到“技能生态竞争”
以前聊AI应用,大家比的是模型参数多强、推理多厉害。但实际做产品的时候你会发现,模型是引擎,技能是配件,再好的引擎没有适配的配件也跑不出效果。
拿智能客服举例,模型对话能力和意图理解固然重要,但真正让用户觉得“好用”的,往往是那些像“查订单”、“改地址”、“退差价”这样的小技能,能不能无缝衔接起来。
一个开放的技能生态,意味着第三方团队可以围绕某个垂直行业做技能包,不需要和具体某个模型厂商绑定。今天这个模型不好用了,换一个模型,技能照常复用。这才是可持续的东西。
5.3 作为技能贡献者,怎样设计更受欢迎的技能
我自己既是技能的消费者,也维护过不少技能。有一些设计习惯,能让你的技能更容易被采纳。
第一,边界清晰。一个技能只做一个判断,不要试图覆盖一堆场景。比如“查询库存”就只管查库存,不要顺带改库存。
第二,输入输出可预测。调用方最怕的就是结果和预期不一致。严格按照schema定义输出,不要返回一个任何字段都没有的大JSON,大家都难受。
第三,提供稳定的版本维护。技能发布不是一锤子买卖,要有明确的废弃策略。没人希望装上一个技能,几个月后突然没法用了。
第四,主动提供错误信息。技能出错时,除了状态码,尽量给出可读的错误原因和改进建议。
我的体会是,越是负责人的技能作者,越会花心思在描述和错误处理上,而不是把工作重心都放在堆功能上。
这套系统我越用越觉得,核心不是技术在炫技,而是它把“能力共享”这件事变简单了。我现在手头几乎所有智能体项目,都统一走Hermes的规范来定义技能,哪怕暂时不接Skills Hub,只是把 manifests 和标准接口定出来,后面的维护和扩展都轻松太多。最后再分享一个小经验:如果你准备在自己项目里引入这套思路,从最小的地方开始,挑一个像汇率查询这样边界清晰的原子技能,把它完整地跑通,比写十页架构文档有用得多。