13. 插件系统:可扩展的 Agent 架构
13.1. 为什么需要插件系统
任何复杂的软件系统都面临一个核心挑战:如何在保持核心稳定的同时允许外部扩展。在 AI Agent 的场景下,这一挑战尤为突出——不同的用户、团队和组织有着截然不同的需求:有人需要对接特定的内存服务(如 Honcho、Mem0),有人需要自定义的上下文压缩引擎,还有人希望在 Agent 运行时注入消息以实现远程控制。
如果所有这些功能都硬编码在核心代码中,代码库会迅速膨胀,维护成本指数级增长。更关键的是,不同的扩展可能相互冲突,导致系统不稳定。
Hermes Agent 的插件系统解决了这一问题。它的设计哲学是:
核心做减法 :核心代码只提供必要的抽象和接口,不实现具体的扩展逻辑。
插件做加法 :每个功能扩展作为独立插件,通过标准接口与核心交互。
隔离与安全 :每个插件在自己的模块空间中运行,钩子回调被独立的 try/except 包裹,单个插件的异常不会影响核心或其他插件。
多源发现 :支持用户级、项目级和 Pip 安装三种来源,满足个人和团队的不同需求。
classDiagram
class PluginManager {
-_plugins: Dict~str, LoadedPlugin~
-_hooks: Dict~str, List~Callable~~
-_plugin_tool_names: Set~str~
-_cli_commands: Dict
-_plugin_commands: Dict
-_context_engine: ContextEngine
-_plugin_skills: Dict
-_cli_ref: Any
+discover_and_load()
+invoke_hook(hook_name, **kwargs) List
+list_plugins() List~Dict~
+find_plugin_skill(qualified_name) Path
+list_plugin_skills(plugin_name) List~str~
}
class PluginManifest {
+name: str
+version: str
+description: str
+author: str
+requires_env: List
+provides_tools: List~str~
+provides_hooks: List~str~
+source: str
+path: Optional~str~
}
class LoadedPlugin {
+manifest: PluginManifest
+module: ModuleType
+tools_registered: List~str~
+hooks_registered: List~str~
+commands_registered: List~str~
+enabled: bool
+error: Optional~str~
}
class PluginContext {
<<interface>>
+manifest: PluginManifest
+_manager: PluginManager
+register_tool(name, toolset, schema, handler)
+register_hook(hook_name, callback)
+register_command(name, handler, description)
+register_cli_command(name, help, setup_fn)
+register_context_engine(engine)
+register_skill(name, path, description)
+inject_message(content, role)
+dispatch_tool(tool_name, args)
}
PluginManager "1" --> "*" LoadedPlugin : 管理
LoadedPlugin "1" --> "1" PluginManifest : 包含
PluginContext "1" --> "1" PluginManager : 引用
PluginContext ..> PluginManifest : 通过 manager 创建
13.2. 插件发现
Hermes Agent 从三个来源发现插件,按优先级顺序扫描:
13.2.1. 用户插件
路径:~/.hermes/plugins/<name>/
用户级插件存放在 Hermes Home 目录下的 plugins/ 子目录中。每个插件是一个独立的子目录,包含 plugin.yaml 清单文件和 __init__.py 入口模块。这类插件对当前用户的所有项目生效。
13.2.2. 项目插件
路径:./.hermes/plugins/<name>/
项目级插件存放在当前工作目录的 .hermes/plugins/ 子目录中。与用户插件不同,项目插件需要通过环境变量 HERMES_ENABLE_PROJECT_PLUGINS 显式启用。这一设计是出于安全考虑——项目目录通常受版本控制,自动加载其中的代码可能带来供应链攻击风险。
项目级插件通常用于团队协作场景:团队成员可以共享针对特定项目的自定义工具和钩子。
13.2.3. Pip 入口点插件
入口点组:hermes_agent.plugins
通过 Pip 安装的 Python 包可以通过 pyproject.toml 或 setup.py 中声明的 hermes_agent.plugins 入口点注册为 Hermes 插件。这类插件适合发布到 PyPI 供社区使用。
# setup.py / pyproject.toml 中的入口点声明
[project.entry-points."hermes_agent.plugins"]
my_plugin = "my_package.hermes_plugin:register"
13.2.4. 发现流程
PluginManager.discover_and_load() 方法执行以下步骤:
扫描用户插件目录(
~/.hermes/plugins/)。如果启用了
HERMES_ENABLE_PROJECT_PLUGINS,扫描项目插件目录。扫描 Pip 入口点。
读取配置文件中的
plugins.disabled列表,跳过被禁用的插件。对每个未禁用的插件调用
_load_plugin()。
整个发现过程是幂等的——多次调用不会重复加载。
sequenceDiagram
autonumber
participant Main as Agent 启动
participant PM as PluginManager
participant UserDir as ~/.hermes/plugins/
participant ProjDir as ./.hermes/plugins/
participant Pip as importlib.metadata
participant Config as config.yaml
participant Plugin as 插件模块
Main->>PM: discover_and_load()
PM->>UserDir: _scan_directory(source="user")
UserDir-->>PM: [PluginManifest, ...]
PM->>ProjDir: _scan_directory(source="project")
Note over ProjDir: 仅当 HERMES_ENABLE_PROJECT_PLUGINS=true
ProjDir-->>PM: [PluginManifest, ...]
PM->>Pip: _scan_entry_points()
Pip-->>PM: [PluginManifest, ...]
PM->>Config: _get_disabled_plugins()
Config-->>PM: {"plugin_a", "plugin_b"}
loop 每个清单
alt 在禁用列表中
PM->>PM: 标记为 disabled
else 正常加载
PM->>Plugin: _load_plugin(manifest)
Plugin-->>PM: register(ctx) 调用完成
end
end
PM-->>Main: 发现完成
13.3. plugin.yaml 格式
每个目录型插件必须包含一个 plugin.yaml (或 plugin.yml)清单文件。该文件声明了插件的基本信息、依赖和能力。
# ~/.hermes/plugins/my-memory/plugin.yaml
name: my-memory
version: "1.2.0"
description: "Memory provider using Honcho for persistent conversation context"
author: "Developer Name"
# 运行所需的环境变量(可选)
requires_env:
- HONCHO_API_KEY
- name: HONCHO_PROJECT_ID
description: "Honcho project identifier"
# 声明此插件提供的工具(仅文档用途)
provides_tools:
- honcho_recall
- honcho_store
# 声明此插件注册的钩子(仅文档用途)
provides_hooks:
- pre_llm_call
- post_llm_call
13.3.1. 字段说明
name:插件名称。如果省略,使用目录名作为名称。version:语义化版本号。description:人类可读的描述。author:作者信息。requires_env:运行所需的环境变量列表。支持两种格式——纯字符串或包含name、description的字典。如果环境变量不满足,插件可以选择降级运行或报错。provides_tools:插件注册的工具名称列表(仅文档用途,不影响实际注册)。provides_hooks:插件注册的钩子名称列表(仅文档用途,不影响实际注册)。
13.3.2. 清单解析
_scan_directory() 方法遍历指定路径下的子目录,查找 plugin.yaml 或 plugin.yml 文件。找到后使用 yaml.safe_load() 解析内容并构建 PluginManifest 数据类实例。解析失败会记录 WARNING 日志但不影响其他插件。
13.4. PluginContext API
PluginContext 是插件系统的核心接口。每个插件在 register() 函数中接收一个 PluginContext 实例(通常命名为 ctx),通过它注册工具、钩子、命令和其他扩展点。
# ~/.hermes/plugins/my-plugin/__init__.py
def register(ctx):
"""插件入口函数,由 PluginManager 在加载时调用。"""
ctx.register_tool(
name="my_tool",
toolset="my-plugin",
schema={
"name": "my_tool",
"description": "A custom tool from my plugin",
"parameters": {"type": "object", "properties": {}}
},
handler=lambda args, **kw: '{"result": "hello"}',
description="My custom tool",
emoji="🔧",
)
ctx.register_hook("pre_llm_call", my_pre_llm_hook)
ctx.register_command("mycmd", my_command_handler, "My slash command")
13.4.1. register_tool
在全局工具注册表中注册一个工具。注册后,该工具出现在 LLM 可见的工具列表中,可以通过标准的工具调用机制使用。
参数:
name:工具名称(字符串)。toolset:所属工具集(用于hermes toolsTUI 分组)。schema:OpenAI Function Calling 格式的 JSON Schema。handler:工具处理函数,签名handler(args: dict, **kwargs) -> str。check_fn:可选的连接检查函数,返回bool。requires_env:可选的环境变量列表。is_async:是否异步处理函数(默认False)。description:工具描述。emoji:在 TUI 中显示的图标。
13.4.2. register_hook
注册一个生命周期钩子回调。回调函数接收与钩子类型匹配的关键字参数。未知钩子名称会产生 WARNING 日志但仍被存储(前向兼容)。
13.4.3. register_command
注册一个会话内斜杠命令(如 /mycmd)。处理器签名 fn(raw_args: str) -> str | None ,支持异步。与内置命令冲突的名称会被拒绝。
13.4.4. register_cli_command
注册一个 CLI 子命令(如 hermes myplugin ...),用于终端级别的操作(如配置、初始化)。
13.4.5. register_context_engine
注册一个上下文引擎来替代内置的 ContextCompressor 。全局只允许一个上下文引擎插件,第二个注册尝试会被拒绝并输出 WARNING。
13.4.6. register_skill
注册一个只读技能(SKILL.md)。技能通过限定名 "<plugin_name>:<skill_name>" 访问。插件技能不会出现在系统提示的 <available_skills> 索引中——它们是显式按需加载的。
13.4.7. inject_message
向当前活跃的会话注入消息。如果 Agent 正在运行(处理工具调用),消息被放入中断队列;如果 Agent 空闲,消息被放入待处理输入队列。这一功能使插件能够实现远程控制、消息桥接等高级功能。
13.4.8. dispatch_tool
通过全局工具注册表派发工具调用。插件斜杠命令可以使用此方法调用其他工具(如 delegate_task),而无需直接访问 Agent 实例。在 CLI 模式下自动注入 parent_agent 上下文,在网关模式下优雅降级。
13.5. 生命周期钩子
Hermes Agent 定义了一组标准化的生命周期钩子(VALID_HOOKS),插件可以在 Agent 运行的关键节点注入自定义逻辑。
13.5.1. 钩子列表
钩子名称 |
触发时机 |
|---|---|
|
工具调用之前。参数: |
|
工具调用之后。参数: |
|
LLM 调用之前。参数: |
|
LLM 调用之后。参数: |
|
API 请求发送之前。参数: |
|
API 请求返回之后。参数: |
|
会话开始时。参数: |
|
会话结束时。参数: |
|
会话最终确认时。参数: |
|
会话重置时。参数: |
13.5.2. pre_tool_call:策略执行
pre_tool_call 钩子最强大的功能是阻断工具调用 。插件可以返回一个字典 {"action": "block", "message": "原因"} 来阻止工具执行。这支持以下场景:
速率限制 :限制特定工具的调用频率。
安全策略 :阻止对敏感资源的访问。
审批流程 :某些操作需要用户确认后才能执行。
get_pre_tool_call_block_message() 函数遍历所有 pre_tool_call 钩子的返回值,找到第一个有效的阻断指令并返回。无效或无关的返回值被静默忽略,不影响观察者模式的钩子。
# 插件中的策略钩子示例
def my_policy_hook(tool_name, args, **kwargs):
if tool_name == "bash" and "rm -rf" in str(args.get("command", "")):
return {
"action": "block",
"message": "Dangerous rm -rf command blocked by security plugin"
}
return None # 允许执行
def register(ctx):
ctx.register_hook("pre_tool_call", my_policy_hook)
13.5.3. pre_llm_call:上下文注入
pre_llm_call 钩子允许插件在每次 LLM 调用前注入上下文信息。回调可以返回字符串或字典:
def memory_recall_hook(messages, tools, **kwargs):
# 从内存服务中检索相关信息
recalled = memory_service.recall(messages[-1].get("content", ""))
if recalled:
return {"context": recalled}
return None
注入的上下文始终注入到用户消息中,而非系统提示 。这是一个重要的设计决策——保持系统提示不变,使得跨回合的 prompt cache 前缀保持一致,缓存的 token 可以被复用。所有注入的上下文是临时的,不会持久化到会话数据库。
13.5.4. on_session_start/end:会话生命周期
这两个钩子允许插件在会话开始和结束时执行初始化和清理操作。例如,内存提供者可以在会话开始时加载历史上下文,在会话结束时保存新的记忆。
13.6. Hook 执行机制
PluginManager.invoke_hook() 是钩子分发的核心方法。它按注册顺序调用特定钩子的所有回调,收集非 None 的返回值。
13.6.1. 异常隔离
每个钩子回调被独立的 try/except 包裹。如果某个回调抛出异常,异常被捕获并记录 WARNING 日志,但不影响其他回调的执行。这一设计确保了单个有缺陷的插件不会破坏核心 Agent 循环。
def invoke_hook(self, hook_name: str, **kwargs: Any) -> List[Any]:
callbacks = self._hooks.get(hook_name, [])
results: List[Any] = []
for cb in callbacks:
try:
ret = cb(**kwargs)
if ret is not None:
results.append(ret)
except Exception as exc:
logger.warning(
"Hook '%s' callback %s raised: %s",
hook_name,
getattr(cb, "__name__", repr(cb)),
exc,
)
return results
13.6.2. 模块级便捷函数
为了简化核心代码中的调用,插件系统提供了一组模块级便捷函数:
invoke_hook(hook_name, **kwargs):调用get_plugin_manager().invoke_hook()。get_pre_tool_call_block_message(tool_name, args, ...):检查pre_tool_call钩子的阻断指令。get_plugin_context_engine():返回插件注册的上下文引擎。get_plugin_command_handler(name):返回插件注册的斜杠命令处理器。get_plugin_commands():返回所有插件斜杠命令的字典。get_plugin_toolsets():返回插件工具集的元组列表,用于 TUI 显示。
flowchart TD
classDef start fill:#dbeafe,stroke:#3b82f6,color:#1e3a8a
classDef success fill:#dcfce7,stroke:#16a34a,color:#166534
classDef warn fill:#fef9c3,stroke:#ca8a04,color:#854d0e
classDef fail fill:#fee2e2,stroke:#dc2626,color:#991b1b
classDef info fill:#f1f5f9,stroke:#64748b,color:#334155
EVENT["Agent 事件<br/>(工具调用/LLM调用/...)"] --> INVOKE["invoke_hook(hook_name, **kwargs)"]
INVOKE --> CB1["回调 1<br/>(插件 A)"]
INVOKE --> CB2["回调 2<br/>(插件 B)"]
INVOKE --> CB3["回调 3<br/>(插件 C)"]
CB1 --> R1["返回值 1"]
CB2 -->|"异常!"| LOG["记录 WARNING 日志"]
CB3 --> R3["返回值 3"]
R1 --> COLLECT["收集非 None 返回值"]
R3 --> COLLECT
COLLECT --> RESULTS["返回 [value1, value3]"]
LOG --> COLLECT
class EVENT start
class RESULTS success
class LOG fail
class INVOKE,CB1,CB2,CB3,R1,R3,COLLECT info
13.7. 内存提供者插件
Hermes Agent 支持通过插件集成外部内存服务,为 Agent 提供跨会话的持久记忆能力。目前已支持的内存提供者包括:
提供者 |
描述 |
|---|---|
honcho |
Honcho AI 的对话理解与记忆服务,提供深度的对话上下文追踪 |
mem0 |
Mem0 的智能记忆层,自动提取和检索关键信息 |
holographic |
Holographic 内存服务,专注于结构化知识存储 |
byterover |
ByteRover 的记忆管理平台,支持多模态记忆 |
supermemory |
SuperMemory 的统一记忆接口,整合多种记忆源 |
retaindb |
RetainDB 的基于数据库的持久记忆,适合结构化数据 |
openviking |
OpenViking 的开源记忆方案,强调隐私和本地部署 |
hindsight |
Hindsight 的反思式记忆,支持从过往经验中学习 |
这些插件通常通过 pre_llm_call 钩子在每次 LLM 调用前注入相关记忆,通过 post_llm_call 或 on_session_end 钩子保存新的记忆。
内存提供者插件的设计遵循以下原则:
非侵入式 :通过标准的钩子接口集成,不修改核心代码。
可替换 :用户可以随时切换或禁用内存提供者。
安全 :所有注入的内容进入用户消息(不修改系统提示),不会影响 prompt cache。
13.7.1. Hindsight 的后续改进
以内存提供者 hindsight 为例,该插件在后期经历了一系列重要的改进:
Probe API 的 ``update_mode='append'`` :hindsight 引入了探测(probe)接口, 允许以追加模式存储记忆条目。新的上下文信息可以增量添加到已有记忆中, 而无需覆盖或重写整个记忆块。这大幅降低了在高频交互场景下的记忆丢失风险。
跨进程去重 :在多 Agent 并发运行或会话频繁重启的场景中, 相同的记忆片段可能被多个进程重复存储。hindsight 的去重机制确保同一条记忆 在全局范围内只保留一份,提高了记忆检索的精确度并降低存储开销。
13.8. 上下文引擎插件
Hermes Agent 的内置上下文管理使用 ContextCompressor 进行上下文压缩。然而,不同的使用场景可能需要不同的上下文管理策略——例如,某些场景需要基于向量的检索,某些需要基于图的关联,还有些需要完全自定义的压缩逻辑。
通过 PluginContext.register_context_engine() ,插件可以替换内置的 ContextCompressor :
from agent.context_engine import ContextEngine
class MyContextEngine(ContextEngine):
name = "my-engine"
def compress(self, messages, **kwargs):
# 自定义压缩逻辑
return compressed_messages
def register(ctx):
ctx.register_context_engine(MyContextEngine())
全局唯一性约束 :只允许一个上下文引擎插件。如果第二个插件尝试注册,会被拒绝并输出 WARNING 日志。这确保了上下文管理行为的一致性和可预测性。
插件还必须通过 isinstance(engine, ContextEngine) 检查,确保实现了必需的接口。
13.8.1. 插件技能注册
除了工具和钩子,插件还可以通过 register_skill() 注册技能。插件技能使用限定名格式 "<plugin_name>:<skill_name>" ,例如 "my-plugin:setup-guide" 。
插件技能的特点:
只读 :不能通过
skill_manage工具编辑。显式加载 :不出现在系统提示的
<available_skills>索引中,需要用户显式请求。名称隔离 :技能名称不能包含冒号(
:),因为冒号用于分隔插件名和技能名。平台兼容 :受
platforms前置元数据约束。安全扫描 :加载时检查提示注入模式,发现可疑内容记录 WARNING 日志。
插件技能支持"捆绑上下文":当加载一个插件技能时,如果同一插件注册了其他技能,返回的内容会包含一条捆绑提示,告知 Agent 还有哪些同级技能可用。
13.8.2. 插件管理的全局单例模式
PluginManager 通过模块级单例模式管理(get_plugin_manager()),确保整个进程只有一个实例。这一设计保证了:
钩子注册的唯一性:不会因为多次实例化导致钩子被重复注册。
状态一致性:所有插件共享同一个管理器,工具集和命令注册不会冲突。
惰性初始化:管理器在第一次被请求时才创建,不会影响不使用插件功能的启动速度。
13.8.3. 插件加载的错误处理
每个插件的加载被独立的 try/except 包裹。如果某个插件的 register() 函数抛出异常,该插件会被标记为 enabled=False 并记录错误信息,但不影响其他插件的加载。list_plugins() 方法返回所有插件的状态(包括失败原因),方便调试。
备注
模型提供者不是插件
Hermes 的 LLM Provider(如 Anthropic、Gemini、DeepSeek、Bedrock 等)并非通过插件系统管理。
Provider 的身份、路由和认证逻辑统一由 hermes_cli/providers.py 中的核心叠加层(overlay)
和 models.dev 目录数据合并实现。这种设计是刻意的——模型路由属于核心基础设施,
不适合走插件的延迟加载和异常隔离路径。
plugins/ 目录下不存在 model-providers 子目录,也不存在 ProviderProfile
抽象基类。新增 Provider 不需要创建插件目录,只需在 hermes_cli/providers.py 的
HERMES_OVERLAYS 字典中添加条目即可。
详细的 Provider 架构(数据源合并、传输类型、认证模式)请参见 模型路由:多 Provider 的统一接入层。
13.9. 运营类插件
除了核心的内存和上下文引擎插件,Hermes 的插件生态已扩展到多个运营领域:
插件 |
功能描述 |
|---|---|
disk-cleanup |
磁盘空间管理。扫描临时文件、旧日志、过期缓存,提供清理建议和自动清理。 |
image_gen (openai, xai, openai-codex) |
图片生成。支持 OpenAI DALL-E、xAI Grok Imagine、OpenAI Codex 三种后端,
通过 |
google_meet |
Google Meet 集成。包含会议机器人( |
hermes-achievements |
成就系统。为 Agent 使用引入游戏化元素——追踪使用里程碑、技能掌握进度, 提供可视化仪表板展示成就等级。 |
kanban |
看板任务管理。包含 Web 仪表板( |
这些插件展示了 Hermes 插件架构的 广度 ——从基础设施维护(磁盘清理)
到用户交互(成就系统、看板),所有扩展都遵循相同的 plugin.yaml + register()
范式。
13.10. 插件工具合并与命令发现
插件注册的工具现在会自动合并到内置工具集中。这意味着:
LLM 视角统一 :LLM 看到的是一个统一的工具列表,无需区分工具来源 是核心模块还是插件
工具集过滤生效 :
hermes tools命令中的工具集分组对插件工具同样有效Schema 一致性 :插件工具的 JSON Schema 与内置工具遵循相同的验证规则
此外,插件的 CLI 命令在 hermes 命令分发阶段被自动发现。
当用户执行 hermes <subcommand> 时,CLI 框架不仅查找内置子命令,
还会扫描所有已加载插件注册的 register_cli_command 条目。
这使得插件可以无缝扩展 CLI 命令空间,例如 hermes myplugin config 。
13.10.1. 总结
Hermes Agent 的插件系统是一个设计良好的扩展框架,通过标准化的接口和清晰的隔离机制,实现了核心与扩展的解耦:
三源发现 (用户 / 项目 / Pip)满足不同规模的使用需求。
PluginContext API 提供了丰富的扩展点(工具、钩子、命令、上下文引擎、技能)。
生命周期钩子 覆盖了 Agent 运行的所有关键节点,支持策略执行和上下文注入。
异常隔离 确保单个插件的故障不会影响系统稳定性。
内存提供者生态 支持多种外部记忆服务,用户可按需选择。
上下文引擎替换 允许完全自定义的上下文管理策略。
运营类插件 涵盖磁盘清理、图片生成、会议集成、成就系统和看板管理等功能,展示插件生态的广度。
工具合并与命令发现 使插件能力无缝融入核心体验,用户无需区分工具和命令的来源。
备注
模型提供者不属于插件系统 。LLM Provider 的路由和认证由 hermes_cli/providers.py
中的核心叠加层管理,不属于插件系统的扩展点。详见 模型路由:多 Provider 的统一接入层。
这一架构使得 Hermes Agent 能够在不修改核心代码的情况下,灵活地适应各种使用场景和集成需求。从核心的记忆管理到运营层面的会议机器人和项目看板,插件系统为 Agent 的能力扩展提供了统一而强大的基础设施。