plugins/,351 个文件。这一章讲第三方怎么在不改核心代码的前提下扩展系统。
~/.hermes/plugins/ 用户级 —— 对这台机器上的所有项目生效
./.hermes/plugins/ 项目级 —— 跟着代码仓库走,团队共享
pip entry points 包级 —— 用 pip install 安装某个包就自动生效
| 来源 | 适用场景 | 特点 |
|---|---|---|
| 用户级 | 「我个人习惯用的工具」 | 不进版本控制,不影响别人 |
| 项目级 | 「这个项目需要的能力」 | 提交进仓库,团队共享。新同事拉下代码就有 |
| 包级 | 「发布给社区用的插件」 | 走标准的 Python 包分发渠道,可以有版本、依赖、更新 |
「pip entry points」(入口点)是 Python 的标准机制:一个包可以在自己的元数据里声明「我提供了某类插件」,安装后框架自动发现,不需要用户手动注册。
plugins/
├── platforms/ 22 个聊天平台适配器 → 第 1 章
├── memory/ 8 种记忆后端 → 第 8 章
├── context_engine/ 上下文引擎 → 第 7 章
├── model-providers/ 模型供应商 → 第 11 章
├── cron_providers/ 定时任务提供者 → 第 12 章
├── kanban/ 看板协作 → 第 10 章
├── browser/ 浏览器自动化
├── image_gen/ 图像生成
├── video_gen/ 视频生成
├── observability/ 可观测性
├── dashboard_auth/ 仪表盘认证
├── security-guidance/ 安全指引
├── google_meet/ 会议集成
├── spotify/ 音乐
├── teams_pipeline/ Teams 流水线
├── disk-cleanup/ 磁盘清理
├── hermes-achievements/ 成就系统
├── web/ 网页相关
├── plugin_storage.py ★ 插件的持久化存储
└── plugin_utils.py ★ 插件工具函数
插件通过一套「上下文 API」向系统注册三类东西:工具、钩子、命令行子命令。
| 可叠加能力 | 互斥策略 | |
|---|---|---|
| 例子 | 工具插件、平台适配器、图像生成后端 | 记忆提供者、上下文引擎 |
| 装 3 个会怎样 | 有 3 份能力,互不冲突。装得越多能力越强 | 系统不知道该听谁的 |
| 系统的处理 | 全部加载 | 「单选」—— 只允许激活一个 |
两处源码明确了这个约束:
// 上下文引擎(第 7 章)
「Selection is config-driven: `context.engine` in config.yaml.
Default is "compressor" (the built-in). Only one engine is active.」
// 记忆提供者(第 8 章)
「The MemoryManager enforces a one-external-provider limit to prevent
tool schema bloat and conflicting memory backends.
Only one external provider runs at a time.」
在你自己的插件系统里,这个区分要在设计阶段就做出来。
判据很简单:「装两个的语义是『两份能力』还是『两个互相矛盾的答案』?」
如果是后者,就必须标记为单选,并且在加载第二个时明确报错。留到运行时会产生极难排查的问题 —— 因为症状是「行为和预期不一样」,而不是「报错了」。
plugins/plugin_storage.py
插件需要持久化自己的数据(配置、缓存、状态)。系统提供统一的存储抽象,而不是让每个插件自己决定往哪写。
这解决三个问题:
backup_paths())回顾第 4 章的工具集解析函数:
def _get_plugin_toolset_names() -> Set[str] # 插件提供的工具集
def _get_registry_toolset_aliases() -> Dict[str, str]
def resolve_toolset(name, visited=None, *, include_registry: bool = True)
插件不只是「注册几个工具」,它可以注册一整个工具集。这样用户在配置里写 toolsets: [my_plugin_set] 就能启用插件的全部能力,而不用逐个列工具名。
那个 include_registry 参数说明:系统区分「内置工具集」和「注册表里的工具集(含插件的)」,某些场景下可以只解析内置的 —— 大概是为了在插件还没加载完时也能工作,或者为了安全场景下排除第三方工具。
插件可以挂钩到系统的几个关键点:
| 钩子 | 时机与用途 |
|---|---|
pre_llm_call | 调模型前。注意它的约束:只能「追加到用户消息」,从不重写消息列表 —— 这是为了保护提示词缓存的前缀(第 7 章的 select_context() 才可以替换列表) |
post_tool_call | 工具执行后。可以观察、记录、告警 |
agent:step | 智能体每走一步(第 3.6 节的步骤回调) |
| 审批钩子 | 第 5 章的 _fire_approval_hook,让插件参与安全决策 |
| 网关钩子 | gateway/hooks.py + gateway/builtin_hooks/,消息进出的节点 |
pre_llm_call 与 select_context 的权限差别第 7 章的原文:「Unlike the pre_llm_call plugin hook (which appends to the user message and intentionally never rewrites the list, to preserve the cache prefix), select_context() may replace the message list.」
译:不同于 pre_llm_call 插件钩子(它只追加到用户消息,并且刻意从不重写列表,以保护缓存前缀),select_context() 可以替换整个消息列表。
这是一个分级授权的设计:
· 普通插件(pre_llm_call)→ 只能追加,权限小,不会破坏缓存
· 上下文引擎(select_context)→ 可以整个替换,权限大 —— 但它是「单选」的,用户明确选择了它,而且它的输出仍要过所有校验器
权限的大小和「用户是否明确授权」成正比。一个可以随便装十个的普通插件,不该有替换整个上下文的权力。
除了插件,Hermes 还支持 MCP(Model Context Protocol,模型上下文协议)—— 一个让智能体接入外部工具服务的开放标准。
tools/mcp_tool.py 378 KB MCP 客户端
mcp_serve.py 38 KB ★ 把 Hermes 自己作为 MCP 服务暴露
optional-mcps/ 65 个文件 内置的可选 MCP 服务
| 插件 | MCP | |
|---|---|---|
| 语言 | 必须是 Python | 任何语言(跨进程通信) |
| 进程 | 同进程 | 独立进程或远程服务 |
| 能力 | 深 —— 可以挂钩子、注册工具集、替换核心策略 | 浅 —— 主要是提供工具和资源 |
| 崩溃影响 | 可能影响主进程 | 隔离,不影响 |
| 生态 | Hermes 专属 | 跨智能体产品通用 |
mcp_serve.py 那一项值得注意:Hermes 可以把自己作为 MCP 服务暴露出去。也就是说另一个智能体可以把 Hermes 当成一个工具来调用 —— 这让「智能体调用智能体」成为可能。
这个梯度是有意义的:扩展的门槛和它能造成的破坏成正比。
写一个技能只需要写 Markdown,任何人都能做,最多让智能体多知道一些操作步骤。
写一个上下文引擎需要理解整套契约(缓存不变式、生命周期、版本兼容),而它一旦出错会让整个系统的上下文管理失效。
系统通过「不同层级用不同机制」把这个梯度显式化了 —— 而不是提供一个万能的插件接口让所有人都能做所有事。