免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

Hermes智能体自定义技能开发指南:从设计到调试全流程

Hermes智能体自定义技能开发指南:从设计到调试全流程 这段时间我在折腾 Hermes 智能体框架最让我上头的一个问题就是怎么给 Agent 创建自定义技能。很多朋友来问我Hermes 装好了模型也接上了为什么 Agent 还是显得很“呆”原因其实特别简单——它只长了一个大脑还没有长出能干活的“手脚”。而技能就是那双手脚。不管你是刚接触 Agent 开发还是已经在用 Hermes 做自动化测试、内部工具集成这篇文章应该都能帮到你。我会从技能机制的底层逻辑讲起然后带你完整走一遍自定义技能从设计、编写、注册到调试的全流程最后把我踩过的坑全部摊开给你看。内容偏实操建议找个键盘陪你。1. 先弄明白一件事技能到底是什么很多初学者会把“技能”想得很玄以为它是某种复杂的模型微调或者是给 Agent 加了一堆记忆。其实不是。在 Hermes 这类智能体框架里技能本质上就是一个可以复用的能力单元你给 Agent 封装好一段可执行逻辑再用一段自然语言告诉它“什么时候该调用、怎么调用”Agent 会根据用户指令的语义自动匹配并执行。1.1 技能不是一个 Prompt也不是一条工作流这三者特别容易混我刚开始也绕晕了。打个比方Prompt 是一张“说明书”告诉模型该怎么回答但它本身不产生动作。工作流是一张“流程图”规定了多个步骤的顺序和执行条件适合固定流程的批处理。技能是一个“功能插头”它把一段真实世界的操作查数据库、调接口、算数据、发消息封装好让模型在合适的时机插进去用。所以如果你要 Agent 去做一件需要动真格的事情比如查询订单状态、生成测试报告、读取某个 Excel 再汇总那大概率需要的是一个技能而不是改一版 Prompt。我以前犯过的典型错误是想用 Prompt 让 Agent“假装”去查数据结果模型一本正经地编了一个订单号给我。后来才明白模型不知道什么是真实的数据源它只会基于训练语料做预测。技能存在的意义就是把“真实世界的动作”和“模型的自然语言理解”连接起来。1.2 Hermes 技能系统主要由三部分构成以我自己使用的这套开源实现为例常见 Hermes 类框架基本都是这个结构一个技能通常由三个部分组成技能清单文件skill.json记录技能名称、版本、入口函数、参数声明、超时时间等元信息。这个文件负责告诉框架“技能叫什么、怎么启动”。自然语言描述文件description.md用普通语言描述技能的用途、适用场景、输入输出要求。这个文件是给模型看的模型靠它决定“现在该不该调用这个技能”。可执行代码execute.py 等真正干活的逻辑一般是 Python 函数。它从上下文里读取参数执行完以后把结构化结果返回给 Agent。这三部分缺一不可。如果你只写了代码但没写描述Agent 根本不知道有这个技能存在如果只写了描述但代码入口配置错了那调用时直接报错。1.3 什么时候应该考虑创建自定义技能判断标准很简单当 Agent 需要反复执行某一类固定的、非纯文本生成的操作而且这个操作有明确输入输出时就该上技能了。我整理了几个典型的场景查询类查订单、查库存、查日志、查数据库记录。操作类创建工单、发送通知、触发构建、执行测试用例。计算类算折扣、算报表、做数据格式转换。集成类调用内部 API、读写文件、操作第三方平台。反过来说如果这个任务只是“把一段文字改得更通顺”或者“根据已知信息回答一个问题”那就不需要技能直接靠模型本身就能解决。把技能用在刀刃上你的 Agent 系统才会轻量又好维护。2. 动手前先做好技能设计有些人一上来就噼里啪啦写代码写完了发现 Agent 要么不调用要么调用后参数经常传错。我后来悟了写技能 80% 的功夫在代码之外尤其是设计阶段。设计做不好后面全是补丁。2.1 技能封装边界的判断标准技能不是越多越好也不是越粗越好。边界分得不好要么技能太臃肿模型不知道该不该调要么技能太细碎调用链复杂得吓人。我现在的判断标准有三条职责单一一个技能只做一件事。比如“查订单”和“取消订单”必须拆成两个技能不要合并成“订单管理”。参数最少化能只传一个参数的绝不要传五个。参数越多模型传错的可能性越大。输出可预期技能返回的数据结构必须稳定最好统一包装成类似{status: success, result: {...}}的格式方便 Agent 后续处理。此外还有一个容易被忽略的点技能之间的依赖关系要降到最低。你说我技能 A 能不能调用技能 B技术上可以但我强烈不建议这么做一旦技能之间形成调用链排查问题时会非常痛苦。正确的做法是把公共逻辑抽出来当普通函数库让每个技能直接调用函数库而不是让技能调技能。2.2 输入参数与输出结构的约定这是设计阶段最需要较真的地方。模型不像人它看不到你的代码注释它只能通过参数声明和描述文件来理解“该传什么”。我在设计参数时一般遵循这几个原则参数名用全小写 下划线不要出现大小写混拼比如order_id而不是orderId。每个参数必须写清楚类型和是否必填能枚举的尽量枚举。所有参数都在描述文件里给一个真实示例模型会参考示例去生成传入值。输出结构方面我建议统一做成两层外层是状态信息内层是业务数据。这样做的好处是Agent 可以先判断外层状态再决定是继续处理结果还是向用户说明失败原因逻辑会清晰很多。2.3 技能描述决定 Agent 会不会用你的技能很多人把描述文件当成可有可无的东西随便写两句就完事了。实际上描述文件才是整个技能的灵魂因为模型是靠读它来理解技能的。写描述的时候我一般会反复问自己三个问题这个技能最典型的调用场景是什么什么情况下绝对不应该调用这个技能每个参数到底长什么样我会把“什么时候不要用”也写进描述里这一点很少人做但非常管用。比如我写过一个查询销售额的技能如果不注明“仅用于已支付订单”模型会在用户问“未付款订单有多少”时也去调用它返回一个错误结果还一脸自信。后来我在描述里明确加了排除条件误调率直线下降。3. 完整实操在 Hermes 里创建一个自定义技能接下来是重头戏。我以一个非常常见的需求为例给 Agent 添加一个“查询订单状态”的技能让它可以对接内部订单系统用户问“我的订单×××到哪一步了”的时候Agent 自动调接口而不是凭空回答。整个流程走完大概二十分钟你跟着做一遍就能摸到门道。3.1 环境准备与目录结构先确认你的 Hermes 环境已经能正常运行 Agent模型建议先接上 DeepSeek 这类开源模型成本低、跑得稳。本地部署相关的依赖装好以后新建技能目录。标准的目录结构大概是这样的skills/ check_order_status/ skill.json description.md execute.py requirements.txt每个技能独立一个目录目录名和技能名保持一致全小写加下划线。我习惯把requirements.txt也放在技能目录里方便单独安装依赖避免污染主环境。当前版本的框架一般会自动扫描skills/目录下的所有子目录发现里面有skill.json就认为这是一个合法技能。如果你的 Hermes 是从旧版本升级上来的可能需要手动在配置里指定技能扫描路径。3.2 编写核心文件 skill.jsonskill.json是技能的元信息文件框架靠它来加载技能。我先给出一个可以直接抄的版本{ name: check_order_status, version: 1.0.0, description_file: description.md, entry: execute.py:run, parameters: { order_id: { type: string, required: true, description: 订单号例如 ORD20240501 }, platform: { type: string, required: false, enum: [taobao, wechat, official], description: 订单来源平台不传时默认查询全部 } }, timeout: 30, authorized_roles: [assistant, tool] }字段含义拆开说一下。name是技能的唯一标识不能有空格。version建议用语义化版本号每次大改逻辑就升一位方便回溯。entry指向代码文件里的入口函数格式是文件名:函数名。parameters声明了模型可以传入的参数这部分会直接影响模型生成的 JSON 参数结构。timeout是技能执行超时时间单位是秒一定要根据实际接口情况设置设太短接口一慢就报错设太长整个会话流程就会被拖住。最后一个authorized_roles我是强烈建议保留的它限制了技能只允许被系统内部调用不要暴露给外部任意触发这是一个很好的安全习惯。3.3 实现主逻辑 execute.py接下来写真正的执行逻辑。核心要求是入口函数接收一个上下文对象返回一个可序列化的结果对象。这样框架才能把返回值交给模型继续处理。import requests import time def run(ctx: dict) - dict: order_id str(ctx.get(order_id, )).strip() if not order_id: return { status: error, error_code: PARAM_MISSING, message: order_id is required } # 内部订单服务地址实际项目里建议从环境变量读取 api_url fhttps://internal.example.com/api/orders/{order_id} try: resp requests.get(api_url, timeout10) except requests.Timeout: return { status: error, error_code: UPSTREAM_TIMEOUT, message: order service timeout } if resp.status_code ! 200: return { status: error, error_code: UPSTREAM_ERROR, message: forder service returned {resp.status_code} } data resp.json() # 只返回模型真正需要的字段不要一股脑全塞给它 return { status: success, result: { order_id: data.get(order_id), status: data.get(status), pay_amount: data.get(amount), logistics: data.get(logistics, 暂无物流信息) } }有几个细节值得注意。第一参数校验必须自己写不要指望模型每次都传得完美第二对上游接口的每个异常分支都要兜住并且返回统一的错误结构这样模型才能根据error_code向用户解释问题第三返回值里的业务字段尽量精简模型处理长文本的能力是有限的你塞一百个字段给它它就不知道该重点看哪个了。另外提一句如果你的技能要访问数据库建议把数据库连接对象放到技能初始化阶段创建而不是每次调用都新建连接否则并发一高马上爆炸。3.4 编写 description.md 并完成注册描述文件的写法我前面讲过原则这里给一份我实际在用的版本这是一个查询订单状态的技能。 当用户询问订单进度、物流节点、支付状态时使用该技能。 技能需要传入 order_id格式为十六位字母数字组合例如 ORD20240501001。 platform 参数可选可选值为taobao、wechat、official不传时默认同时查询所有平台。 注意本技能只能查询已支付的订单未支付订单请直接告诉用户“订单尚未支付”。 如果系统返回 UPSTREAM_TIMEOUT请告知用户“订单服务暂时不可用请稍后再试”。写这段描述的核心心法就是把你自己当成一个完全不懂技术的客服你在什么情况下会拿起“查询订单”这个工具描述里包含了触发条件、参数解释、边界条件和异常处理提示模型读到这篇文章就能做到“什么情况该用、用的时候该注意什么”。完成以上文件后重启 Hermes 服务或者执行技能重载命令框架就会自动扫描到check_order_status这个新技能。可以在控制台输入类似skill list的命令确认它已经注册成功。3.5 验证技能是否被正确调用注册成功不等于能正常工作。我一般会做三步验证直接调用验证在框架的调试面板里手动触发一次技能传一个真实的订单号看返回结果是否符合预期。模拟对话验证不用业务问题而是换一个口语化的问法比如“我的东西发货了没”看模型能不能自动识别并调用技能而不是直接说“我不知道”。异常场景验证故意传一个不存在的订单号看模型能不能把你返回的错误信息转成一句正常的话安抚用户。其中第二步是最容易出问题的模型经常把“查询订单”理解成“生成一段快递播报”这就是描述文件没写清楚的锅。描述里一定要加一句“用户没有明确给出订单号时先向用户询问订单号不要编造”。4. 踩坑记录与常见问题排查技能上线跑了一段时间以后我整理了一个自己的排障清单。这里面的每一条都是我或者身边同事真金白银踩出来的坑分享出来希望大家少走点弯路。4.1 技能不被调用Agent 总是自作主张这是最常见的故障表现是技能明明注册成功了但你问 Agent 相关问题它还是靠想象回答完全不理会你的技能。我排查这类问题的顺序一般是先看技能描述是否写清楚了触发条件描述太模糊模型根本不知道这个技能是干嘛的。再看描述里是否出现了太多反例把模型的判断搞混了。最后看参数声明是否正确如果必填参数声明成了required: false模型可能觉得“不一定非要调用它”。还有一个非常反直觉的原因技能名字和描述里提到的关键词不一致。比如技能叫check_order_status但你在描述里通篇写的是“查询物流”模型就会把物流和这个技能完全绑定用户问“订单状态怎么看”时就匹配不上了。建议技能名和描述里的高频词保持统一。4.2 参数被截断或传入不规范模型在生成 JSON 参数时偶尔会出现值被截断、类型对不上、甚至干脆用了中文当参数名的情况。遇到这种问题不能只靠模型侧优化技能这边要做得更健壮。我的做法是在入口处加上一层“容错清洗”把参数值统一去掉首尾空格数字类型如果传成了字符串就主动转换枚举类型的参数如果传了非法值就在技能内部做一次映射而不是直接报错。这些逻辑不需要很复杂几句话的防御性代码就能避免一大半的线上问题。另外提醒一点单次技能调用的输入参数总长度别太大例如你让技能一次性处理一个几千行的 CSV 内容模型的上下文窗口会被吃掉一大截。遇到这种场景正确的做法是让技能接收一个文件路径或文件 ID去读取文件内容而不是通过参数传全文。4.3 超时与依赖冲突有些技能内部要调外部接口或者跑一段复杂计算耗时可能超过模型会话的等待极限。我之前有个技能需要调用一个内部报表服务平均耗时 25 秒而会话超时只有 15 秒导致每次调用都在超时边缘偶尔就断。后来我改成了“异步 结果主动查询”的模式技能先把任务提交进去返回一个任务 IDAgent 再轮询查询结果。虽然实现上麻烦了一点但用户体验和稳定性都提升了一大截。依赖冲突这个问题更容易踩。如果你的多个技能各自锁定了不同版本的第三方库极有可能互相打架。我的建议是技能依赖尽量精简能用标准库的绝不装第三方包必须在技能里用的依赖也在requirements.txt里固定版本号不要用这种宽松范围不然哪天升级了一个库你的技能就莫名其妙挂了。4.4 技能之间互相干扰与上下文污染如果你的 Agent 同时挂了多个技能模型可能把技能 A 的结果错误地当成技能 B 的输入。这个问题在技能数量变多以后特别明显。我目前的解决方案有两个。第一每个技能的返回值都带有清晰的业务字段名尽量避免两个技能返回同名字段但含义不同。第二在技能内部不依赖全局变量所有状态都通过ctx显式传递防止一个技能执行时改动另一个技能需要的上下文数据。排查上下文污染时可以打开框架的调试日志查看模型实际收到了哪些技能结果。经常是中间某个技能偷偷修改了某个字段导致后续技能拿到脏数据不看日志根本发现不了。下面是一个浓缩版的排查速查表贴出来方便大家直接对照现象可能原因处理办法技能从没被调用描述触发条件不清晰重写 description.md明确使用场景和排除条件调用时参数经常出错参数声明不规范、描述示例缺失细化 skill.json 的 parameters补充真实示例技能执行超时单次执行耗时过长改为异步任务模式或拆分技能颗粒度返回值模型看不懂返回字段太多、结构混乱精简 result 字段统一错误结构多个技能结果串了上下文变量互相污染不使用全局变量显式传递 ctx字段命名保持独立我在实际项目里的体会是技能的稳定性不是写出来就有的而是在一次次的日志排查和调用分析中磨出来的。尤其是模型调技能这件事它的行为带有一定随机性所以技能设计得越简单、描述写得越明确系统的整体表现就越稳定。不要嫌这些准备工作琐碎它们才是 Agent 真正变得“能干”的前提。
返回列表