
很多做智能体落地的人都会遇到同一个瓶颈演示的时候什么都好一接真实业务就发现框架不够用。要么模型不知道该调哪个能力要么你没法在过程中做监控和拦截要么能力多了之后代码变成一坨完全没理过的毛线团更别说让它每天固定时间主动干活了。我在围绕 Hermes Agent 搭建智能体系统的时候把这些问题逐一拆开最后沉淀下来的方案就是它那套扩展机制Skills、Hooks、Plugins、Cron。这篇文章不是讲空泛的理论而是讲这四个机制到底各自管哪块、怎么去实现、以及配合起来能跑出什么样的效果。不管你是在自己写 Agent 框架还是在某个开源项目里做二次开发只要把四个维度想清楚了扩展能力这件事就能少走很多弯路。我会用实际例子把我踩过的坑、验证过的做法一起写在里面可以直接抄作业。1. 四个扩展机制的定位先把地图铺开1.1 把 Hermes Agent 类比成一个操作系统我第一次看 Hermes Agent 的扩展设计时最大的感触是它把 Agent 框架当作一个精简版的操作系统来设计。这个类比能帮你很快理解四个机制为什么要分开而不是揉在一起。Skills 对应操作系统的“命令工具”。大模型本身没有能力直接操作外部世界但可以通过调用一个个 Skill 来完成“查数据库、发通知、调接口”这类具体动作。每个 Skill 就是一个命名清晰、参数明确、功能单一的小工具。Hooks 对应操作系统的“事件回调”。你在写传统 Web 服务时经常会在请求前后注册回调做日志、鉴权、限流。Hooks 在 Agent 里承担同样的职责只是它拦截的不是 HTTP 请求而是模型调用和工具调用过程中的关键节点。Plugins 对应操作系统的“软件包”。一个 Plugin 可以打包多个 Skill、多个 Hook再附带自己的配置文件、依赖声明和生命周期逻辑。它是四个机制里体量最大、功能最完整的一层解决的是“能力规模化”的问题。Cron 对应操作系统的“计划任务”。到了指定时间就触发一个动作这在传统运维场景里已经非常成熟。难的是让定时任务和 Agent 的决策能力结合起来而不是退化成简单的脚本。这四个机制不是并列的四个独立功能更像一层套一层的俄罗斯套娃。Plugin 里面可以带 Skill 和 HookCron 触发的任务最终要落到 Skill 执行Skill 执行的前后都会被 Hook 包裹。理解了这个嵌套关系后面配置或者排查问题才不会绕晕。1.2 用一张表收拢四者边界很多刚开始接触这套体系的同学会问Skills 和 Plugins 是不是重复了Hooks 能不能替代 Plugin我用下面这张表来回答这些问题它在项目评审时也帮我省了很多解释的功夫。机制解决的核心问题类比最小粒度典型场景Skill模型缺少执行能力命令行工具单个函数查数据、发消息、调接口Hook过程不可控、不可观测事件回调回调函数日志、注入上下文、拦截校验Plugin能力多到难以管理和分发软件包一组 Skill Hook 配置集成外部系统、按需装/卸Cron只能被动响应、不会主动做事计划任务定时任务定时汇总、告警检查、周期报告需要注意一个容易踩坑的点Skill 和 Plugin 不是同一个维度的东西。Skill 是“能力”Plugin 是“能力的容器”。一个插件可以只提供一个 Skill也可以注册十个 Skill 再加三个 Hook。反过来一个 Skill 也可以被不同 Plugin 共享。设计的时候别把它们放在同一种配置目录里混着管理否则项目变大之后你会被依赖关系折磨疯。我自己的习惯是先画一张全局图标出哪些是基础 Skill、哪些是外部系统相关的 Plugin、哪些节点需要 Hook 埋点、哪些任务是周期性的。这张图花不了多少时间但能避免后期改造时推倒重来。2. Skills能力的最小原子2.1 Skill 的本质是把“工具调用”封装得更好在 LLM 场景下Skill 本质上就是“工具调用”的封装。大模型是一个概率模型它并不真的会执行任何外部动作。当你问它“帮我查一下订单号 1024 的状态”时模型其实是根据你对可用技能的描述生成一个“调用某个工具”的指令然后由框架负责真正执行。所以 Skill 的设计核心从来不是“函数写得有多漂亮”而是“对外描述是否足够准确”。决定模型会不会调用这个 Skill 的关键因素就是它的名称、描述和参数 schema。函数内部哪怕写得再优雅如果描述写得含糊模型就可能在面对真实请求时选择不调用或者传进错误的参数。我见过一个真实的反面案例某同学写了一个查询用户信息的 Skill描述写的是“查询用户信息”参数只有 user_id。看起来没问题但实际运行时模型经常不调用它而是用自己的“常识”编一个答案。后来我把描述改成了“当用户询问某个用户的昵称、手机号、注册时间等个人资料时根据用户唯一ID查询该用户的完整信息。如果用户只提供了手机号请先用手机号反查用户ID再调用本工具”调用率立刻上去了。2.2 Skill 的标准注册流程在 Hermes Agent 里注册一个 Skill 并不复杂整体流程是固定的理解了它就等于理解了框架的扩展入口。我把流程拆成下面几步定义执行函数也就是真正干活的 Python 函数。用装饰器或声明式配置把函数标记为 Skill并绑定名称、描述、参数 schema。Agent 启动时扫描注册表把 Skill 序列化成 LLM 可以理解的工具描述。每次请求到来时工具描述被拼进上下文模型能看到当前有哪些工具可用。模型根据用户请求推断该调用哪个工具返回工具调用指令。框架解析指令执行对应函数把结果作为一次工具调用结果回填到对话里。模型基于工具返回的真实结果生成最终回复给用户。这套流程可以说是所有 Agent 框架的地基。理解了第 5 到第 7 步你就明白了为什么工具返回的内容必须结构化、必须让模型能读懂。如果工具返回一堆堆栈信息或未处理的异常模型很难从中提取有效信息最终回答质量也会明显下降。2.3 实操一个组件库查询 Skill 的完整实现下面我写一个可以直接参考的 Skill 示例。场景是团队内部有前端组件库Agent 需要根据用户需求帮忙查组件。这个例子非常典型因为它涉及参数校验、结构化返回、错误处理三个关键点。# skills/query_component.py from hermes import skill skill( namequery_component_library, description( 在团队组件库中查找可复用的前端组件并返回组件的版本、引入方式和兼容性说明。 当用户提到表单、弹窗、表格、按钮、日期选择器等组件名称时使用。 如果用户只说了一个功能但不确定用哪个组件也应调用本工具给出候选建议。 ), parameters{ type: object, properties: { keyword: { type: string, description: 组件名称或功能关键词例如表格、弹窗、日期选择器 }, limit: { type: integer, description: 最多返回几条候选结果默认 5最大不超过 20, minimum: 1, maximum: 20 } }, required: [keyword] } ) def query_component_library(keyword: str, limit: int 5): try: results search_in_registry(keyword, limit) if not results: return { code: 404, message: f没有找到与 {keyword} 相关的组件, data: [] } return {code: 0, message: ok, data: results} except Exception as exc: return { code: 500, message: f组件库查询失败: {str(exc)}, data: [] }这里有三个细节值得展开。第一description里我把“什么时候用、什么时候不用、用户表达模糊时怎么办”都写进去了这比只写一两句话有效得多。第二参数 schema 中limit设置了上下限可以防止用户让模型传入一个超大数字导致接口压力过高。第三函数内部用try/except捕获异常但不在函数里raise而是把错误信息放进返回值里。因为异常一旦抛给框架模型拿到的只是错误类型它并不知道该怎么补救而你把错误信息作为文本返回模型至少能理解发生了什么然后决定是换个参数重试还是向用户说明情况。2.4 Skill 越来越多之后怎么办项目初期只有五六个 Skill 感觉还可以但当 Skill 数量超过三十个问题就来了所有工具描述都会塞进上下文每多一个工具就多占用一些 token而且工具太多还会干扰模型的选择。我统计过当工具数量超过四五十个时模型的工具调用准确率会明显下降。解决的方案有几个你可以根据自己的系统选择。一是做“热门召回”也就是根据当前会话的关键词从全量 Skill 列表里先召回 Top N 个只把这部分送到模型面前二是做分类命名把同类 Skill 的名字加上同样的前缀比如order_、user_、finance_模型更容易从名字建立映射三是把低频 Skill 合并成一个“查找工具”的能力让模型先通过一个检索 Skill 找到要用的工具再发起二次调用。这些方法不是互斥的可以组合用。3. Hooks在 Agent 生命周期里埋入主动权3.1 关键的生命周期节点如果你只把 Agent 当作“输入一句话输出一句话”的黑盒那你永远无法在线上环境里安全地运行它。Hooks 存在的意义就是拆开这个黑盒让你在关键节点有一锤子买卖的机会。我把一次完整的 Agent 请求拆成了下面这些节点每一个都可以挂 HookHook 点触发时机典型用法on_session_start新会话建立时加载历史上下文注入用户偏好before_prompt_build组装 Prompt 之前注入当前时间、业务状态、天气等动态信息after_prompt_buildPrompt 组装完成后检查 Prompt 长度敏感词过滤before_llm_call请求发往模型之前限流、缓存、打印请求摘要after_llm_call模型返回之后检查格式、自动修复、统计耗时before_skill_callSkill 函数执行之前权限校验、参数改写、开关控制after_skill_callSkill 函数执行之后结果脱敏、审计日志、异常兜底on_output最终回复发给用户之前格式转换、翻译、内容安全过滤3.2 Hooks 的两种能力注入与拦截Hook 真正的价值体现在两种能力上。一种是“注入”也就是在流程中把模型原本不知道的信息塞进去。比如你的 Agent 需要知道当前时间才能回答“今天几号”但模型本身不知道实时时间那就需要在before_prompt_build里把当前时间拼进上下文。另一种是“拦截”也就是在流程的某个关键点上决定“要不要让这个请求继续”。比如某个 Skill 涉及敏感操作只有特定接口调用方允许执行那么before_skill_call里就可以做权限校验不满足就返回一个特殊标志让流程短路并给用户返回友好提示。这两种能力在实际业务里都非常有用但有一个共同的坑Hook 里不能随便抛异常。如果before_llm_call里抛了一个未捕获的异常整个请求都会被中断用户看到的结果是“系统异常”而且你很难定位到底是模型的问题还是 Hook 的问题。我的习惯是给每个 Hook 加一层框架级的异常兜底Hook 自己挂了就跳过并记录告警绝不能让单个 Hook 把主流程拖垮。3.3 我验证过的三个实用 Hook 场景第一个场景是自动修复模型返回格式。模型偶尔会返回不合法 JSON尤其是在加了函数调用约束之后。我在after_llm_call里做了一个 Hook发现返回的 JSON 解析失败时先尝试用简单的修复规则处理比如去掉多余的逗号、补全缺失的右括号如果修复不成功再自动重试一次模型调用。用了这个 Hook 之后线上因为模型返回格式问题导致的失败率下降了至少一半。第二个场景是耗时监控。在before_llm_call和after_llm_call里各记录一个时间戳两个时间戳的差就是单次模型调用的耗时。把这段日志打到指标系统里我就能看到线上环境在不同时段的响应耗时为后续做缓存和并发扩容提供了依据。Skill 调用的耗时也同样处理在before_skill_call和after_skill_call里插桩记录。第三个场景是结果脱敏。某个内部查询 Skill 会返回包含手机号、身份证号等敏感字段的数据但 Agent 的最终回复不需要展示这些信息。我在after_skill_call里做了一层清洗只保留模型生成回答所需的字段再把清洗后的结果回填给模型。这既保护了敏感数据也减少了上下文里不必要的噪声。3.4 什么时候用 Hook而不是 Plugin很多人会把 Hook 和 Plugin 搞混其实判断标准很简单如果只是想在某个节点做一个小动作比如“打印一行日志”“塞一个时间变量”那就用 Hook轻量直接如果这个能力需要携带自己的配置、有自己的数据结构、还要注册两名以上的 Skill 才能形成完整闭环那应该做成 Plugin。Hooks 更像“胶水层”Plugins 才是“完整的解决方案层”。用错了层级项目会变得非常别扭。4. Plugins把 Skill 和 Hook 做成一等公民4.1 为什么需要 Plugin 这一层当项目里的 Skill 数量增长到一定程度你会发现管理成本指数级上升。Skill 文件散落在不同目录彼此之间可能共享连接池、配置项、依赖版本这些横切的东西没人统一管理。Plugin 就是解决这个问题的“包管理”层。在 Hermes Agent 里一个 Plugin 通常是一个可独立分发、独立装卸的目录或 Python 包里面包含一个清单文件、若干 Skill 代码文件、若干 Hook 代码文件、配置模板和资源文件。框架启动时扫描所有已安装的 Plugin读取清单依次完成加载和初始化。这样你可以在不同的项目里按需装不同的能力包而不必把整个 Agent 代码库复制来复制去。4.2 Plugin 的生命周期管理Plugin 的生命周期比 Skill 要完整得多我把它拆成六个阶段加载框架发现 Plugin 目录读取清单文件。校验检查版本、依赖、权限声明是否满足当前运行环境。实例化创建 Plugin 对象注入配置。初始化执行 Plugin 的setup()逻辑建立数据库连接、加载模型等。运行注册 Plugin 内部的 Skill 和 Hook正式开始服务请求。卸载执行teardown()释放连接、注销 Skill 和 Hook。这一套生命周期看起来常规但实际上很多框架都做得不够细。最容易翻车的点是第 6 步卸载时如果只删了 Plugin 目录没有把注册过的 Skill 和 Hook 从 Agent 的运行态里移除那这个能力实际上还在“幽灵运行”。我见过有人改配置后热加载插件结果旧 Skill 还在被模型调用数据写到了旧连接池上排查了半天才找到原因。所以设计 Plugin 系统时一定要把“注销”作为和“注册”同等重要的操作对待。4.3 权限与配置Plugin 最容易翻车的两个地方Plugin 安全是很多人容易忽略的。第三方的 Plugin 如果可以直接访问文件系统、读环境变量、发网络请求那它基本上等于拿到了 Agent 进程的所有权限。我在设计时就明确要求每个 Plugin 必须在清单里声明它需要的权限框架在运行时会拦截未授权的调用。# plugins/monitor/plugin.yaml name: monitor version: 1.0.0 description: 监控与告警能力包 permissions: network: true env: - DB_CONN_STR fs: - read:/data/alert_templates/* skills: - skills/check_alerts.py - skills/query_metrics.py hooks: - hooks/audit_log.py cron: - job: check_alerts expr: */5 * * * *配置方面也有一个常见的坑配置项不能硬编码在代码里。比如数据库连接串、告警阈值、目标通知群 ID这些必须从 Plugin 的配置对象里读取。这样同一个 Plugin 在测试环境和线上环境可以直接通过配置切换而不需要改代码重发。我见过有的团队把数据库密码硬编码在 Skill 文件里结果 Plugin 每次升级都把密钥跟着分发一遍风险非常高。另外多个 Plugin 之间可能依赖同一个底层库的不同版本这在 Python 环境里容易引发冲突。我的建议是每个 Plugin 尽量使用独立的执行空间或者至少隔离依赖如果做不到就在文档里明确固定版本范围避免破坏性更新。5. Cron把智能体从被动响应变成主动行动5.1 Cron 不只是定时发请求很多 Agent 框架里都有定时任务能力但大多数人把它用成了一个“定时调用接口”的功能。到了点就发一个请求给模型然后完事。这种做法看起来能用但一旦任务依赖上下文、依赖多个步骤、依赖前一天的数据单纯“定时发请求”根本解决不了问题。Cron 在 Hermes Agent 里的定位是一个事件源它到了点之后不是直接执行一段固定的代码而是生成一个定时触发事件投递到 Agent 的执行队列里。Agent 接收到事件后会像处理用户消息一样去理解这个任务需要干什么然后再决定调用哪些 Skill。这样定时任务就和正常的用户请求共用同一套决策和执行链路灵活性高很多。5.2 从表达式到时区先解决基础问题Cron 表达式本身不复杂但新手很容易在细节上栽跟头。最常见的是只有 5 个字段的标准写法分、时、日、月、周。如果表达式带秒那就是 6 个字段秒、分、时、日、月、周。少数实现还支持 7 个字段加了“年”。我给出几个可以直接用的例子表达式含义0 9 * * *每天 09:00 执行0 8 * * 1每周一 08:00 执行*/5 * * * *每 5 分钟执行一次0 2 1 * *每月 1 号 02:00 执行30 7 * * 1-5工作日 07:30 执行0 0 * * *每天午夜零点执行时区是最容易被忽视的。如果你的服务部署在服务器上默认时区可能是 UTC而用户说的“每天 9 点”大概率是本地时间。如果不显式配置时区任务就会差 8 个小时。我的做法是所有 Cron 配置统一指定timezoneUTC8这类写死的时区不在代码里隐式依赖宿主环境的默认时区。5.3 可靠性设计任务锁、持久化、重试定时任务上线容易但真正跑起来之后你会遇到三个问题。第一个是任务丢失调度器进程重启的瞬间恰好该执行的任务被跳过。解决方法是把任务的下一次执行时间持久化到数据库启动时重新加载而不是只放在内存里。第二个是重复执行如果你的服务实例有多个副本同一个定时任务会在每个副本上都触发一次。解决方法是给任务加分布式锁或者利用数据库的唯一约束保证同一时间只有一个执行者。第三个是执行超时有的 Skill 执行时间特别长任务队列会被堵住。解决办法是为每个定时任务配置超时和隔离超时后自动标记失败并安排延迟重试。我做过一个线上巡检任务刚开始没加重入锁结果某次接口抖动导致任务执行了四十分钟下一次任务到点又触发两个任务同时跑把下游数据库连接占满了。后来我加了两层保护第一层是任务开始前写入带过期时间的锁记录第二层是结束前清理锁并写入执行日志。这样即使单个任务偶发卡顿也不会拖垮整个系统。5.4 一句话说清楚 Cron 和 Skill 怎么配合说到这里Cron、Skill、Agent 之间的关系已经清楚了Cron 负责在正确的时间点踢一脚Agent 负责接下事件并按语义决定怎么做Skill 负责具体干活。下面这个代码片段是我常用的实现方式from hermes import scheduled, AgentEvent scheduled(0 9 * * *, timezoneUTC8) async def daily_health_check(agent, context): event AgentEvent( typecron.trigger, task_iddaily_health_check, payload{channel: context.channel_id} ) await agent.accept_event(event)这里把任务逻辑从“具体操作”里抽离出来定时器只负责投递事件。你可以在 Agent 里写一个专门处理cron.trigger事件的 Handler再在里面按task_id决定调用哪些 Skill。这样新增定时任务不需要改调度器本身只需要注册事件和对应 Handler 就够了。6. 综合实战一个值班助手把四者串起来6.1 业务场景与配置设计理论讲再多不如完整跑一个场景。我以一个团队内部的值班助手为例它需要做的事包括每天早上给当天值班人推送值班提醒每五分钟检查一次监控告警每周一生成上一周的故障报告所有操作都需要记录审计日志。这个场景用到四个机制的完整配合。基础的 Skills 包括查询值班表、拉取监控状态、发送通知、汇总历史告警。Plugins 我设计成一个“监控插件”里面有检查告警和查询指标两个 Skill再挂一个记录审计的 Hook。Cron 注册了三个任务早九点值班提醒、每五分钟告警检查、每周一故障报告。配置上大致长这样# plugins/assistant/plugin.yaml permissions: network: true env: - ALERT_DB_DSN - NOTICE_WEBHOOK_URL skills: - skills/query_oncall.py - skills/fetch_alerts.py - skills/send_notice.py - skills/aggregate_report.py hooks: - hooks/audit.py cron: - job: oncall_remind expr: 0 9 * * * timezone: UTC8 - job: alert_check expr: */5 * * * * timezone: UTC8 - job: weekly_report expr: 0 10 * * 1 timezone: UTC86.2 完整执行链路拆解我挑“每五分钟的告警检查”这条链路拆开看你能直观感受到四个机制是怎么协作的。时间到达后调度器把cron.trigger事件投递到 Agent 执行队列。Agent 读取事件里的task_id识别出这是“告警检查”任务于是决定调用fetch_alerts这个 Skill 去拉最近的告警数据。在fetch_alerts执行前before_skill_call这个 Hook 会注入当前值班人的信息因为如果核验告警时需要联系到人Agent 可以直接拿到值班人上下文。Skill 跑完后after_skill_call的审计 Hook 记录了一条完整的执行日志包括调用时间、入参、返回结果概况。然后 Agent 检查返回值。如果发现有关键告警它继续调用send_notice把告警内容和值班人信息整理后发送到通知渠道如果没有告警则只把结果写入审计日志不产生额外动作。整个过程里模型在中间环节做调度决策Skills 提供具体能力Hooks 记录流程并提示上下文Cron 负责发起任务。四个机制各司其职互不越界。6.3 容量与性能上的几个建议跑这类业务时我总结了几条实操经验。第一Skill 描述会占上下文定期检查工具清单移除长期无人调用的低频 Skill。第二Cron 任务的执行队列要和用户请求队列分离开否则一个定时大任务可能把用户请求的响应时间拖长。第三给所有定时任务都加上“最大执行时长”的限制宁可标记失败也不要让任务无限重试把系统拖垮。还有一条关于测试的建议。很多人只在手动触发环境里测试定时任务上线后却因为时区、锁、队列隔离的差异出了问题。正确做法是让定时任务的执行逻辑与调度解耦这样你可以手动向队列投递一个cron.trigger事件来做端到端测试同时又不依赖真实的时间触发。这一个小小的设计能帮你省下大量定时任务调试时间。7. 常见问题与排查技巧实录7.1 一张表讲清楚高频问题我把实际运行中遇到的高频问题整理成了一张速查表方便你遇到类似情况时快速定位。现象可能原因排查思路模型总是不调用某个 Skill描述太笼统或与其他 Skill 职责重叠重写描述明确“什么时候用、什么时候不用”工具列表太长后模型乱选上下文中的工具描述过多启用热门召回或对低频工具做分组Hook 程序报错导致整个请求失败Hook 未做异常隔离给 Hook 增加 try/except 和告警不阻断主流程卸载 Plugin 后旧 Skill 仍被调用注册项未在卸载时清理在 teardown 里显式注销 Skill 和 Hook同一个定时任务重复执行多实例没有加锁引入分布式锁或数据库唯一约束定时任务时间总是不对服务时区和用户时区不一致统一使用配置化的时区不依赖宿主机默认时区Skill 返回大段 JSON 导致上下文爆炸返回结果未经精简对工具返回结果做摘要、截断或字段过滤热更新 Plugin 后连接池冲突旧实例资源没有释放卸载时主动关闭连接池重试前检查资源回收7.2 两个容易被忽略的排查技巧第一个技巧是“把 Hook 日志和业务日志分开”。很多线上问题都需要还原调用链但如果你把 Agent 的决策日志、Skill 的执行日志、Hook 的干预日志全打在一起排查时就会非常痛苦。我习惯把 Hook 日志单独打一个hook_前缀的 logger或者单独写到一个字段里这样可以根据request_id快速过滤出一次请求经历的所有 Hook 节点。第二个技巧是“给定时任务增加手动触发入口”。线上出问题时我想验证某个定时任务到底是调度问题还是执行问题如果只能等下一次到点效率太低。给每个定时任务额外暴露一个手动触发的接口通过指定task_id往队列里投递事件会大大提升排查效率也能让新接手的同学在没有到点的白天就能调试任务逻辑。7.3 还有几个小提醒有些问题不会立刻暴露但积累起来很致命。比如 Skill 的返回数据里如果带上了调用方的密钥审计日志里就会把密钥一并写进去再比如定时任务生成的文件或消息如果内容为空也要有明确的空值处理逻辑而不是让 Agent 对着空数据硬生成一份“看似正常但毫无意义”的报告。这类细节短期不影响可用性但会影响系统的可信度建议从一开始就做规范约束。最后再说几句心得玩了这么久 Hermes Agent我最大的一个体会是扩展机制的价值不在于“用了多高级的框架”而在于你用它们把系统的边界画清楚了。Skill 管能力、Hook 管过程、Plugin 管打包分发、Cron 管主动触发四者各归其位Agent 系统才真正变得可维护、可观测、可扩展。如果你现在刚开始搭建自己的智能体系统我的建议是先别贪多。第一版只做 Skill 和两个最关键的 Hook把核心链路跑通等确认调用准确率和可观测性都没问题了再引入 Plugin 做能力打包最后加 Cron 让系统从被动变主动。一步到位往往意味着全链路一起出问题到时候追责和排查都很难受。最后分享一个我自己的小习惯每次改动四个机制中的任何一个都顺手更新一次前面那张边界表并补一条对应的测试用例。这看起来不起眼但坚持下来你的系统会一直处于“改动可控、回归可查”的状态。功能再多也不怕乱。