Skip to main content

Hermes Agent 的插件系统及插件开发

· 10 min read
Kyle
CTO of the Ph.D. Creative Station

Hermes Agent 的插件系统不算庞大,但设计得相当工整。最近为了接入一个新的图像生成后端,我把它的插件机制从头到尾走了一遍——从目录结构、注册流程,到调试时的各种踩坑。这篇文章是我沿途的记录,希望能帮你少走些弯路。

1. 概述

Hermes 的插件系统采用 共享源码 + Profile 符号链接 的架构。所有 Profile 共用同一份插件代码(位于 ~/.hermes/profiles/plugins/),各 Profile 通过符号链接引用,并在自身的 config.yaml 中按需启用。

三个核心设计原则值得先记住:

  • 动态 import,无需自建 venv:插件由 Hermes 运行时动态加载,使用的是 Hermes 自身的 Python 环境(~/.hermes/hermes-agent/venv/)。插件目录下面不需要搞一个自己的虚拟环境。
  • Last-writer-wins 注册:同名 provider 后注册的会覆盖先注册的——这是个重要的行为细节,调试时容易忽略。
  • 三层发现顺序:Bundled → User → Pip 包,依次扫描,同名覆盖。这意味着你写的 user 插件天然可以覆盖官方内置的同名 provider。

hermes_plugin_architecture.png


2. 八种插件类型

Hermes 支持八种插件类型,每种都有独立的 ABC 契约和注册 API:

类型目录路径注册方式用途
通用工具plugins/<name>/registry.register()自定义工具、slash 命令、生命周期 hook
图像生成plugins/image_gen/<name>/ctx.register_image_gen_provider()替换/扩展 image_generate 工具
视频生成plugins/video_gen/<name>/ctx.register_video_gen_provider()替换/扩展视频生成工具
模型 Providerplugins/model-providers/<name>/register_provider(Profile)新增 LLM 推理端点
记忆后端plugins/memory/<name>/MemoryProvider 子类替换长期记忆存储
上下文引擎plugins/context_engine/<name>/ContextEngine 子类替换上下文压缩策略
平台适配plugins/platforms/<name>/ctx.register_platform()新增聊天通道
MCP Server外部进程MCP 协议(非 Python)接入任意 MCP 工具源
不存在"第三方 Agent 框架"插件类型

Agent 主循环是 Hermes 的固定核心,不可替换。你不能写一个插件把 Hermes 的主循环换成 CrewAI 的——但可以在 generate() 内部 import crewai 作为来调用(后面第 6.3 节会展开讲)。


3. 环境架构

理解这套目录结构是后面一切操作的基础。我第一次配的时候就因为没搞清哪个 Python 环境在干活,绕了不少路。

~/.hermes/
├── hermes-agent/ # Hermes 本体
│ └── venv/ # ★ 运行时 Python 环境(hermes 命令自动激活)
├── profiles/
│ ├── plugins/ # ★ 共享插件源(所有 profile 共用)
│ │ ├── pyproject.toml # uv workspace 根配置
│ │ ├── pyrightconfig.json # LSP 指向 hermes-agent/venv
│ │ └── <category>/<name>/ # 各插件代码
│ └── <profile>/
│ ├── config.yaml # 插件启用/禁用 + provider 配置
│ ├── .env # API keys
│ └── plugins -> ../plugins # ★ 符号链接到共享源
└── plugins -> profiles/plugins # 全局链接(无 profile 模式用)

三种 Python 环境的角色分工:

角色使用的 Python用途
Hermes 运行时~/.hermes/hermes-agent/venv/hermes 命令 import 插件
编辑器 / LSP~/.hermes/hermes-agent/venv/pyrightconfig.json 指向此处,提供代码补全
uv lock系统 Python 3.11仅锁定 workspace 依赖版本,不创建 .venv
关键认知

插件代码里能 import 什么,取决于 hermes-agent/venv 里装了什么。如果你在插件里用了 some-obscure-lib,记得:

~/.hermes/hermes-agent/venv/bin/pip install some-obscure-lib

好消息是 requestsopenaipydantichttpxPillow 这些常用的已经内置了,大多数情况不需要额外安装。


4. 首次配置:建立符号链接

如果你是新装的环境,第一步是把符号链接搭好:

# 全局链接(无 profile 模式时 Hermes 扫描此路径)
# 在 ~/.hermes/ 下执行
ln -sr profiles/plugins plugins

# 每个 profile 也需要链接到共享源
# 在 ~/.hermes/profiles/<profile>/ 下执行
unlink plugins 2>/dev/null
ln -sr ../plugins plugins
绝对不要 rm -rf plugins/

末尾的斜杠会让 shell 跟随符号链接,直接删掉原始目录里的所有插件代码。用 unlink 来移除符号链接——它只删链接本身,不会误伤原始数据。


5. 开发实战

下面走一遍完整的开发流程。以图像生成 Provider 为例——这是最常见也最有代表性的插件类型。

5.1. 创建插件目录

cd ~/.hermes/profiles/plugins

# Provider 类插件需要按类别建子目录
mkdir -p image_gen/my-backend

5.2. 加入 uv workspace

在插件目录下创建 pyproject.toml

[project]
name = "my-backend"
version = "0.1.0"
requires-python = ">=3.11,<3.14"
dependencies = []

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

确保 workspace 根 pyproject.tomlmembers 数组里包含了你的路径:

[tool.uv.workspace]
members = ["image_gen/*", "model-providers/*", ...]

然后:

uv lock # 解析依赖,不创建 venv

5.3. 编写 plugin.yaml

name: my-backend # 唯一标识符,必须与 provider.name 一致
version: 1.0.0
description: "接入某个图像生成后端"
author: Your Name
kind: backend # Provider 类需要;通用工具可省略
requires_env: # 声明需要的环境变量
- MY_API_KEY

5.4. 编写插件代码

这是重头戏。一个 ImageGenProvider 的骨架长这样:

image_gen/my-backend/__init__.py
from agent.image_gen_provider import (
ImageGenProvider, success_response, error_response,
save_b64_image, save_url_image,
resolve_aspect_ratio, normalize_reference_images,
DEFAULT_ASPECT_RATIO,
)
from typing import Any, Dict, List, Optional
import os, logging, requests

logger = logging.getLogger(__name__)

class MyProvider(ImageGenProvider):
@property
def name(self) -> str:
return "my-backend" # 必须与 plugin.yaml 的 name 一致

@property
def display_name(self) -> str:
return "My Backend"

def is_available(self) -> bool:
return bool(os.environ.get("MY_API_KEY"))

def list_models(self) -> List[Dict[str, Any]]:
return [{"id": "model-a", "display": "Model A", "speed": "~5s"}]

def capabilities(self) -> Dict[str, Any]:
return {
"modalities": ["text", "image"],
"max_reference_images": 1
}

def get_setup_schema(self) -> dict:
return {
"name": "My Backend",
"env_vars": [
{"key": "MY_API_KEY", "prompt": "API key", "url": "https://..."},
],
}

def generate(self, prompt, aspect_ratio=DEFAULT_ASPECT_RATIO, *,
image_url=None, reference_image_urls=None, **kwargs):
# 1. 校验 prompt
if not prompt or not prompt.strip():
return error_response("prompt 不能为空")

# 2. 检查 API key
api_key = os.environ.get("MY_API_KEY")
if not api_key:
return error_response("MY_API_KEY 未设置")

# 3. 路由 text-to-image vs image-to-image
# 4. 调用后端 API
# 5. save_b64_image() 或 save_url_image() 保存到缓存
# 6. 返回 success_response(image=..., provider=self.name)

return success_response(image="/path/to/output.png", provider=self.name)

def register(ctx) -> None:
ctx.register_image_gen_provider(MyProvider())
别忘了 **kwargs

generate() 的签名末尾必须接受 **kwargs。工具层会传入前向兼容的参数——如果你的函数因为不认识某个 kwarg 而抛出 TypeError,整个工具就崩了。这是最常见的坑,没有之一。

如果是通用工具(非 Provider 类),注册方式稍有不同:

plugins/my-tool/__init__.py
from . import schemas, tools

def register(ctx):
ctx.register_tool(
name="my_tool",
toolset="my-plugin",
schema=schemas.MY_TOOL_SCHEMA,
handler=tools.my_tool_handler,
)

5.5. 配置 Profile

编辑 ~/.hermes/profiles/<profile>/config.yaml

plugins:
enabled:
- image_gen/my-backend # 嵌套布局(带类别前缀)
- my-tool # 扁平布局

在对应的 .env 里加上 API key:

MY_API_KEY=your-key-here

5.6. 激活与验证

# 启用插件(user 插件需要此步;bundled 插件自动加载)
hermes plugins enable my-backend

# 设置活跃 provider
hermes config set image_gen.provider my-backend
hermes config set image_gen.model model-a

# 重启(config 在 session 启动时快照——不重启不生效)
/reset

# 验证
HERMES_PLUGINS_DEBUG=1 hermes plugins list | grep my-backend
hermes tools

hacker_relief_shinkai.png

到此为止,一个可以工作的图像生成插件就完成了。我坐在电脑前,长长舒了一口气——但转念一想,这只是开始。我们可以做的还有很多:引入其它 Agent 开发框架作为后端、建造更加强大的工具箱……下面聊聊三种包装策略。


6. 三种包装策略

6.1. OpenAI 兼容适配(最常见)

这是最省力的路径。很多厂商和自托管系统都提供 OpenAI 兼容的端点:

  • ComfyUI + OpenAI 兼容节点
  • vLLM 部署的图像模型
  • Replicate 的 OpenAI 兼容镜像
  • 企业内部代理

模式非常直白:requests.post(f"{base}/v1/images/generations", ...),然后解析 data[0].b64_jsondata[0].url 即可。

6.2. 厂商原生协议

不兼容 OpenAI 的后端就得自己写协议翻译了。几个常见厂商的对接方式:

厂商对接方式
ReplicatePOST → poll urls.get → 等待 succeeded → 取 output[0]
Google ImagenPOST with ?key= → 取 generatedImages[0].bytes.base64
xAI Grok已内置为 xai,可作为参考实现研究

无论后端协议怎么变,插件侧的套路是一致的:调 API → 取 b64 或 url → 调 success_response()。分离关注点做得好,切换后端不会伤筋动骨。

6.3. 内嵌第三方 Agent SDK

这是最有想象空间的一种:把 CrewAI / LangGraph / AutoGen 作为generate() 内部调用。

def generate(self, prompt, **kwargs):
from crewai import Agent, Crew, Task
from langchain_openai import ChatOpenAI

designer = Agent(role="设计师", goal="生成图片 URL",
backstory="你是一个创意设计师",
llm=ChatOpenAI(model="gpt-4o"))
task = Task(description=f"根据描述生成图片: {prompt}", agent=designer)
result = Crew(agents=[designer], tasks=[task]).kickoff()
image_url = parse_url_from(result.raw)
return success_response(image=image_url, provider=self.name)
能做的和不能做的
  • ✅ 在 generate() 内部调用任意 Python 库——CrewAI、LangGraph、AutoGen,随便你
  • delegate_task 只能 spawn Hermes 子 agent(不是 CrewAI agent)
  • ❌ 替换 Hermes 的 Agent 主循环——这不是插件能干的事
  • ProviderProfile 只适用于 OpenAI/Anthropic/Bedrock 形状的 LLM 端点,不能用于编排框架

7. 目录结构速查

开发时放在手边参考。

通用插件

plugins/<name>/
├── plugin.yaml # 声明 name, version, provides_tools/hooks
├── __init__.py # def register(ctx): 注册工具和 hook
├── schemas.py # 工具的 JSON Schema(LLM 读取)
└── tools.py # 工具的实际处理逻辑

Provider 类插件

plugins/<category>/<name>/
├── plugin.yaml # kind: backend, requires_env
├── __init__.py # Provider 子类 + register()
└── pyproject.toml # 声明依赖

8. 排错手册

开发过程中最可能遇到的几个问题:

症状可能原因解决
插件不显示Profile 的 plugins/ 不是符号链接或路径错误ls -l plugins,确认指向 ../plugins
provider <name> 未注册plugins.enabled 缺少 <category>/<name>确认 config.yaml 中 enabled 列表包含完整路径
ModuleNotFoundError第三方依赖未安装到 hermes-agent venv~/.hermes/hermes-agent/venv/bin/pip install <pkg>
TypeError: unexpected keywordgenerate() 未接受 **kwargs在签名末尾加上 **kwargs
同名 bundled 插件优先注册顺序:bundled → user,后者覆盖前者user 插件注册同名 provider 即可覆盖

9. 参考文档


写于一个深夜调试 session 之后。插件本身不复杂,但细节不少——希望这篇文章能帮你跳过那些我踩过的坑。