
Hermes Studio 正在开发的 Agent 模块化管理切中的是当前 AI Agent 项目里最容易被低估的一类问题Agent 做出原型容易做成产品难。早期的 Agent 项目往往把模型配置、系统提示词、工具调用、记忆和编排逻辑全部写在同一个脚本里Demo 能跑一旦加了一个新工具、换了一版模型或者要多 Agent 协作代码立刻膨胀改一处崩三处。模块化管理的目标就是把这些散落的职责拆成边界清晰的模块让 Agent 像一个由多个可插拔部件组成的系统而不是一个黑盒脚本。这篇文章会围绕 Hermes Studio 正在做的 Agent 模块化管理展开先说明模块化管理解决什么问题再理清 Agent、Skill、MCP、Subagent 这些容易混淆的概念然后给出一套可落地的架构设计和最小 Python 实现最后补充常见报错的排查链路和工程化清单。读者可以把它当作 Agent 项目做模块化改造时的设计参考也可以直接照着第 4 章的代码搭出一个最小可运行示例。1. Hermes Studio 的 Agent 模块化管理到底在解决什么问题1.1 Agent 开发进入工程化阶段后耦合是最大的债一个最简单的 Agent核心是 LLM 决策 工具执行 循环观察 三段式。原型阶段一个脚本里塞下 prompt、模型 API、两三个工具函数就够了。但进入工程化阶段问题会立刻暴露出来模型从 A 厂商换到 B 厂商配置散落在代码各处改不完。新增一个分析日志的工具主逻辑文件越来越大别人不敢动。记忆、提示词、工具调用互相牵制单测没法写。多 Agent 协作时谁调用谁、谁负责什么完全靠代码里 if else 硬编码。生产环境出现超时或调用失败日志堆在一起分不清是哪一层出的问题。这些问题的本质是耦合。Agent 的职责没有拆分生命周期没有统一管理能力之间没有清晰边界。Hermes Studio 正在开发的 Agent 模块化管理就是在回答一个问题一个 Agent 应该由哪些模块组成这些模块如何被统一注册、调度、替换和观测。1.2 模块化管理不是微服务先分清边界听到模块化有人会下意识往微服务架构上靠这里要先划清界限。微服务拆分的是部署单元每个服务独立进程、独立数据库、独立发布Agent 模块化管理拆的是能力单元模块可以是一个类、一个目录、一个可加载插件不一定要独立进程。Agent 模块通常更轻更强调插拔、复用和职责单一而不是独立扩容。举例来说一个日志分析工具模块可以是 Agent 进程内的一个 Python 类一个 MCP 服务则可能是独立进程。两者可以共存模块化管理负责 Agent 内部如何组织和调度这些能力MCP 负责 Agent 外部如何标准化接入这些能力。理解这个区别才能避免把模块化管理做成过度设计。学习环境和中小型项目里模块可以全部放在一个应用内只有到了多团队协作、独立发布、独立扩缩容的阶段才需要考虑把部分能力下沉成独立服务。1.3 模块化后能获得什么复用、测试、安全、可观测模块化管理不是形式主义它带来的收益可以落到四个具体方面复用。同一个 Skill 或 Tool 模块在 A Agent 和 B Agent 里注册一次就能用不需要复制粘贴代码。可测试。模块实现统一接口后可以单独构造输入验证输出不需要把整个 Agent 跑起来才能测一个工具函数。安全。工具模块可以在统一入口做鉴权、参数校验和敏感信息阻断而不是散落在每个工具函数里。可观测。模块有统一的生命周期和调用入口就能统一打日志、加 trace_id、统计 token 和耗时。下面用一张表对比学习环境和生产环境的预期差异。维度学习环境生产环境模块数量3 到 5 个跑通即可20 个以上需要考虑命名和版本配置方式本地 config 文件配置中心、环境差异隔离日志print 足够结构化日志、trace_id 串联故障处理报错重跑超时、重试、熔断、告警安全基本鉴权工具白名单、敏感信息拦截、操作审计2. 先统一概念Agent、Skill、MCP、Tool、Subagent 和 Harness 的关系2.1 Agent 的最小运行单元是 loop聊模块化之前先回到 Agent 的运行本质。一个 Agent 的每一次任务通常跑在一个循环里把用户输入和当前状态交给 LLM。LLM 决定是直接回答还是调用某个工具。如果调用工具执行工具并拿到结果。把结果作为新的上下文回到第 1 步。直到 LLM 认为任务完成输出最终结果。这个循环经常被称为 Agent Loop。模块化管理的对象就是这个循环里出现的每一个部件模型提供方、系统提示词、工具、记忆、子任务路由、结果验证。理解了这一点就知道模块化管理并不是给 Agent 外面套一层壳就完事而是要管理循环内部每个环节的可替换件。2.2 Skill 与 MCP 有什么不同Skill 和 MCP 是当前 Agent 生态中出现频率很高的两个词但它们在模块化管理里的角色完全不同。Skill 更接近技能封装。它是一组完成特定任务的提示词、脚本、工作流或领域知识。比如写 SQL 排查慢查询是一个 Skill它内部可能包含一段推理 prompt、一个数据库查询工具和一个结果格式化脚本。Skill 解决的是如何把一件事做对。MCPModel Context Protocol是一种协议。它定义了 Agent 如何发现、调用外部工具和数据源解决的是Agent 如何与外部系统标准化通信。一个 MCP 服务可以暴露多个工具Agent 只需要遵守协议就能调用不需要针对每个外部系统写独立的接入代码。在模块化管理中两者可以搭配使用Skill 是业务能力层MCP 是接入协议层。一个模块内部既可以自带工具实现也可以作为 MCP client 去调用外部的 MCP server。2.3 主从模式里 Subagent 本质上是特殊 Tool目前最新的多 Agent 设计里主从模式Master-Slave / Orchestrator-Worker很常见。在这种模式下主 Agent 负责拆解任务、调度和汇总子 Agent 负责执行具体子任务。这里有一个容易被误解的设计点主从模式中的 subagent本质上可以当作一种特殊的 Tool 来调用。主 Agent 看到可用的工具列表里有一项叫日志分析子任务选中它执行它拿到结果继续下一轮。区别只是这个工具的内部不是一个普通函数而是一个独立的 Agent Loop有自己的模型、提示词和工具集。把这个思想落到模块化管理里非常有用subagent 可以和 Tool、Skill 一样实现同一个模块接口注册到同一个注册表里由编排模块统一路由。对外统一对内自治。2.4 一张表理清常见术语术语一句话理解在模块化管理中的角色Agent由 LLM、提示词、工具和循环组成的最小智能体被管理的运行单元HarnessAgent 的外壳负责模型调用、工具执行、重试和日志模块的宿主环境Tool一个可被模型调用的外部函数或 API一个可插拔模块Skill面向任务的技能封装包含提示词、脚本和流程比 Tool 粒度更大的模块MCP统一定义工具和数据源的协议模块对外接的标准协议Subagent被另一个 Agent 调用的子智能体主从模式下可作为特殊 Tool 注册Memory保存会话、事实和向量记忆的能力独立模块Orchestrator决定每一步调用哪个模块编排模块这张表也可以直接当作 Agent 相关面试题的速查。经常有同学分不清 Harness 和 Agent 的区别Harness 是运行骨架Agent 是骨架里真正的决策体。模块化管理管的是骨架和决策体之间的边界。3. Agent 模块化管理的架构设计模块边界、注册与编排3.1 规划模块边界配置、技能、工具、记忆、模型、编排设计模块化管理第一步不是写代码而是划分模块边界。好的边界应该保证每个模块只有一个明确的职责模块之间通过接口通信不直接访问对方内部状态。常见 Agent 项目中建议拆出以下模块模块职责接口示例失败影响配置模块管理模型参数、外部连接、开关load_config / reload_config配置错乱影响所有模块工具模块封装外部函数和 API 调用execute(input, ctx)单点功能不可用技能模块封装任务级能力组合 prompt 和脚本run(input, ctx)任务能力不可用记忆模块读写会话记忆、事实记忆、向量记忆store / retrieve多轮对话失忆模型模块统一封装模型提供方chat(messages)整个 Agent 不可用编排模块决定调用哪个模块管理 subagent 路由route(input, ctx)任务路径错乱观测模块记录日志、耗时、token、tracelog_event(event)故障难定位边界划分的原则是模块之间只通过调用入口交互不共享全局变量。如果发现某个模块需要修改另一个模块的内部状态就要重新审视边界是否合理。3.2 模块注册表与依赖管理模块化管理需要一个注册表用来登记系统中所有可用模块。注册表解决三个问题名称唯一。同一个名字只能注册一个模块避免冲突。动态发现。Orchestrator 能拿到全部可选模块而不是在代码里写死。依赖解析。模块 A 初始化需要模块 B 的实例注册表可以统一完成注入。生命周期一般分成四步initialize准备资源、register登记到注册表、start启动模块、stop释放资源。注意两点initialize 阶段只做资源准备不要执行业务逻辑依赖注入要放在 register 之后统一解析避免模块之间互相 new造成循环依赖。3.3 模块接口设计要同时容纳 Tool、Skill 和 Subagent模块化管理的核心是统一接口。如果 Tool 一种接口、Skill 一种接口、Subagent 又一种接口编排模块就要写大量分支模块化反而增加复杂度。推荐的做法是定义一个统一的执行接口def execute(input_text: str, ctx: AgentContext) - str:Tool 的实现是一个函数级封装Skill 的实现是一个带内部提示词和流程的封装Subagent 的实现是内部再跑一个 Agent Loop。从编排模块的视角看它们没有区别给一段输入拿一段输出。这样做的好处是编排逻辑可以非常薄坏处是 Subagent 的执行时间会明显长于普通 Tool因此接口设计里要额外考虑超时、成本和上下文长度限制。3.4 目录结构与配置示例下面是一个可参考的模块化 Agent 项目结构。它不绑定 Hermes Studio 的具体实现但可以用于理解模块化在工程里的样子。agent_modular_demo/ ├── agent_module.py # 模块基类和上下文 ├── registry.py # 模块注册表 ├── config.py # 配置加载 ├── es_log_tool.py # 工具模块ES 日志分析 ├── memory.py # 记忆模块 ├── subagent_router.py # 编排模块subagent 路由 ├── main.py # 组装和运行入口 └── config.json # 外部配置配置文件保持外置{ es_url: http://localhost:9200, es_timeout: 10, es_index: logs }配置外置的目的是让“换环境不换代码”。测试环境指向测试 ES生产环境指向生产 ES同一套代码可以部署到不同环境。4. 用最小 Python 实现跑通 Agent 模块化管理4.1 环境准备这个示例只依赖 Python 3.10 以上和一个 HTTP 客户端库。学习环境可以直接在任意目录创建虚拟环境python -m venv .venv source .venv/bin/activate pip install httpx示例里的 ES 日志查询是工具模块的演示目标实际跑通时如果本地没有 ES可以先把 execute 里的 HTTP 调用替换成模拟返回验证模块化链路本身是否正常。4.2 定义模块基类与注册表模块基类定义统一的执行契约# agent_module.py from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass, field from typing import Any dataclass class AgentContext: config: dict[str, Any] field(default_factorydict) memory: dict[str, Any] field(default_factorydict) class AgentModule(ABC): name: str def initialize(self, ctx: AgentContext) - None: 模块初始化只准备资源不执行业务逻辑。 pass abstractmethod def execute(self, input_text: str, ctx: AgentContext) - str: 统一执行入口Tool、Skill、Subagent 都实现这个方法。 raise NotImplementedError注册表负责登记和查询模块# registry.py from agent_module import AgentModule class ModuleRegistry: def __init__(self) - None: self._modules: dict[str, AgentModule] {} def register(self, module: AgentModule) - None: if module.name in self._modules: raise ValueError(fmodule {module.name} already registered) self._modules[module.name] module def get(self, name: str) - AgentModule: if name not in self._modules: raise KeyError(fmodule {name} not found, registered: {self.list_names()}) return self._modules[name] def list_names(self) - list[str]: return sorted(self._modules.keys())这里的关键点是 register 时检查名称唯一。Skill 和 MCP 工具经常会产生同名冲突例如两个模块都叫 file_reader注册表直接拒绝才能避免后续误调用。4.3 实现工具模块调用 ES REST API 分析日志以“让 Agent 通过 ES REST API 智能分析日志”为例工具模块的职责是接收一段查询语句返回日志结果# es_log_tool.py import httpx from agent_module import AgentContext, AgentModule class EsLogTool(AgentModule): name es_log_tool def initialize(self, ctx: AgentContext) - None: self.es_url ctx.config.get(es_url, http://localhost:9200) self.es_timeout ctx.config.get(es_timeout, 10) self.es_index ctx.config.get(es_index, logs) def execute(self, input_text: str, ctx: AgentContext) - str: body { query: {query_string: {query: input_text}}, size: 10, } resp httpx.post( f{self.es_url}/{self.es_index}/_search, jsonbody, timeoutself.es_timeout, ) resp.raise_for_status() hits resp.json().get(hits, {}).get(hits, []) if not hits: return 没有匹配的日志 return \n.join(h[_source].get(message, str(h)) for h in hits)这个模块把 ES 地址、索引名和超时时间都放到 initialize 阶段从上下文配置读取。好处是更换 ES 环境时只改 config.json不碰代码。4.4 实现记忆模块与配置加载记忆模块是最适合独立成模块的能力之一# memory.py from agent_module import AgentContext, AgentModule class MemoryModule(AgentModule): name memory_module def initialize(self, ctx: AgentContext) - None: self.memory ctx.memory def execute(self, input_text: str, ctx: AgentContext) - str: if input_text.startswith(记:): key, value input_text[2:].split(, 1) self.memory[key] value return f已记录 {key} if input_text.startswith(查:): key input_text[2:].strip() return self.memory.get(key, 无此记忆) return str(self.memory)配置加载模块很简单读取 JSON 文件即可# config.py import json from pathlib import Path def load_config(path: str) - dict: config_file Path(path) if not config_file.exists(): return {} return json.loads(config_file.read_text(encodingutf-8))4.5 实现主从模式的 subagent 路由编排模块演示了 2.3 节的设计思想subagent 作为特殊 Tool 被注册和调用# subagent_router.py from agent_module import AgentContext, AgentModule class SubAgentRouter(AgentModule): name subagent_router def __init__(self) - None: self._subagents: dict[str, AgentModule] {} def register_subagent(self, route_key: str, module: AgentModule) - None: self._subagents[route_key] module def execute(self, input_text: str, ctx: AgentContext) - str: route_key self._decide_route(input_text) subagent self._subagents.get(route_key) if subagent is None: return 没有匹配的 subagent return subagent.execute(input_text, ctx) def _decide_route(self, input_text: str) - str: if 日志 in input_text or error in input_text.lower(): return es_log_tool if 记忆 in input_text or input_text.startswith((记:, 查:)): return memory_module return unknown路由规则在实际项目中应该由 LLM 或规则引擎动态决定这里用关键字只是为了演示最小闭环。关键设计是register_subagent和普通模块注册一样只接收一个 AgentModule 实例说明 subagent 就是模块体系里的普通成员。4.6 运行验证组装入口把模块初始化、注册和路由绑定完成# main.py from agent_module import AgentContext from config import load_config from es_log_tool import EsLogTool from memory import MemoryModule from registry import ModuleRegistry from subagent_router import SubAgentRouter def build_agent(config_path: str): config load_config(config_path) ctx AgentContext(configconfig) registry ModuleRegistry() es_tool EsLogTool() memory MemoryModule() router SubAgentRouter() for module in (es_tool, memory, router): module.initialize(ctx) registry.register(module) router.register_subagent(es_log_tool, es_tool) router.register_subagent(memory_module, memory) return router, registry, ctx if __name__ __main__: agent, registry, ctx build_agent(config.json) print(registered:, registry.list_names()) print(agent.execute(查一下最近日志中的 error 关键字, ctx)) print(agent.execute(记:userzhangsan, ctx)) print(agent.execute(查:user, ctx))运行命令python main.py预期输出依次是注册表列出三个模块名ES 工具返回日志结果如果本地没有 ES 会抛连接异常记忆模块返回已记录 user最后返回zhangsan。验证的重点不只是程序能启动而是三条路径都通了工具调用路径、记忆写入路径、记忆读取路径。这意味着模块注册、路由分发、上下文传递都是正常的。5. 常见报错与排查链路5.1 execution provider did not respond in time 超时问题Agent 运行时报错 the agent execution provider did not respond in time. this may indicate the ...通常含义是执行提供方没有在预期时间内返回。这类超时在 Agent 项目里出现的频率很高不能简单认为多等一会就行。排查顺序先确认是哪个 provider 超时。是模型 API、工具 API还是 subagent 内部的子循环。直接请求该接口测出真实耗时。用 curl 或调用一次接口排除网络波动。检查配置里的 timeout 值。ES 查询慢时10 秒可能就不够。检查 provider 服务状态。本地 ES 没启动、远程 API 限流都会表现为超时。检查是否任务粒度过大。一个 subagent 要分析一天日志超时是正常的应该拆小任务。解决方案可以从三个层面同时做合理调大超时配置在工具模块内增加重试和熔断把长任务拆成多轮短任务让 Agent 分批执行。问题现象可能原因检查方式处理建议execution provider did not respond in time模型或工具接口超时provider 不可用timeout 过短查看日志定位是哪个 provider直接调接口测耗时按真实耗时调大 timeout加重试和熔断长任务拆分模块初始化时报 KeyError注册顺序错误依赖模块未注册打印 registry.list_names()调整初始化顺序register 后再做依赖注入Skill 与 MCP 工具同名导致误调用名称唯一性未校验工具描述歧义检查注册表名称查看模型选中了哪个工具注册时严格查重使用命名空间前缀写清 description修改配置后行为不变配置只在启动时加载一次确认进程是否重启配置文件路径是否正确生产环境接入配置中心或加文件监听触发 reload5.2 模块初始化顺序与循环依赖典型现象是模块 A 的 initialize 阶段需要模块 B但此时 B 还没有注册代码抛 KeyError 或拿到 None。原因有两种一是注册顺序不对二是模块之间形成了循环依赖。比如 A 初始化需要 BB 初始化又需要 A。解决方案是把初始化拆成两个阶段。第一阶段只做自身资源准备不访问其他模块第二阶段统一通过注册表解析依赖完成装配。如果发现循环依赖说明模块边界设计有问题需要把公共依赖抽到第三个模块里。5.3 Skill 与 MCP 工具命名冲突当系统里同时存在 Skill 和 MCP 工具而且两者功能接近时模型很可能选错。例如一个叫 analyze_log 的 Skill 和一个 MCP 暴露的 analyze_log 工具同时存在。防止方式有三层注册表强制名称唯一模块名使用命名空间前缀如skill:analyze_log和mcp:es:analyze_log在模块的 description 里写清楚适用场景和参数限制。模型选工具主要靠名称和描述这两项写得越清晰误选概率越低。5.4 配置热更新不生效这是生产环境最容易被忽略的问题。开发环境改 config.json 后重启进程就生效了生产环境进程常驻修改文件不会自动生效。如果 Agent 模块化设计里有配置模块就应该在配置模块里统一提供 reload 能力。后续演进方向是接入配置中心监听配置变更事件再让各模块通过配置模块订阅变更而不是各自读文件。6. 工程化清单与扩展方向6.1 可复用的模块化落地清单在做 Agent 模块化改造时可以按下面这份清单逐项检查模块是否只有一个明确职责。模块间是否只通过统一接口调用。注册表是否保证名称唯一。初始化是否分成 prepare、register、start 三阶段。配置是否外置是否区分环境。模块是否实现了统一的 execute 入口。Tool、Skill、Subagent 是否都纳入同一套模块体系。每个模块是否有独立的错误码或日志标识。工具模块是否做了鉴权、参数校验和敏感信息阻断。是否有超时、重试、熔断和成本统计。这份清单既可以用在代码审查也可以用在 Agent 项目的架构评审。6.2 测试、安全与可观测性模块化之后测试策略要跟着调整。每个模块都应该有独立的单元测试用 mock 数据验证 execute 的输入输出。编排模块单独测路由规则比如输入包含 error 时是否命中日志工具。集成测试则把多个模块组装起来验证真实调用链路。安全方面需要专门关注工具模块。统一执行入口是一个理想的鉴权点哪些模块只能内网调用哪些模块需要用户授权哪些输入包含敏感字段不允许写入日志都应该在入口统一处理。不要指望每个工具作者自己记得做安全校验。可观测性建议在 AgentContext 里增加 trace_id每个模块执行时把它传到日志中。这样一次 Agent 任务从入口到 subagent 内部的所有调用都能通过同一个 trace_id 串起来排错效率会高很多。6.3 下一步多 Agent 协作与版本化第 4 章的示例只演示了单个主 Agent 路由到两个模块。实际项目里多 Agent 协作会更复杂主 Agent 拆解任务多个 subagent 并行或串行执行最后主 Agent 汇总结果。模块化管理正好为这种主从模式提供了骨架subagent 依然是模块只是模块内部多了一层自己的 Agent Loop。另一个容易被忽视的方向是版本化。Skill 会迭代工具会被替换模型会升级。模块化管理应该支持为每个模块维护版本号在一次 Agent 任务里记录用了哪个版本的哪个 Skill这样才能在模型行为变化时回溯原因也才能安全回滚。对新手来说最有价值的练习不是去背 Agent 概念而是把第 4 章的最小示例扩展成自己的小项目给模块加一个远程配置给工具模块加一个 mock 测试再给编排模块加一个按 LLM 输出动态路由的规则。把注册、调度、注入、验证、排错这条链路亲手走一遍比看十篇概念文章更管用。Agent 模块化管理最终要解决的不是技术炫技而是让 Agent 项目在团队协作、功能迭代和生产部署中都能像普通软件工程一样被稳定地维护和扩展。