ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

hermes-agent:打造可靠可观测的智能体任务执行引擎

hermes-agent:打造可靠可观测的智能体任务执行引擎 hermes-agent 这个名字最初是我在搭内部自动化平台时随手起的没想到后来就成了这个开源项目的主线。Hermes 是希腊神话里传递信息的信使刚好我们要做的就是一个在各个系统之间传递任务、执行动作、回传结果的 agent 执行引擎名字和用途几乎是一拍即合。如果你也在找一种方式能把“定时任务、监控告警、接口联动、AI 工具调用”这些东西统一管理起来而不是东拼西凑维护一堆 cron 脚本和回调那 hermes-agent 应该能给你一些启发。这个项目不是一个聊天机器人框架它不关心你用什么大模型、怎么编排 prompt。它更偏向一个轻量级的智能体执行底座负责把任务收进来、解析清楚、分发给对应的插件去执行然后再把结果按需通知出去。你可以把它理解成“插上翅膀的信使”任务进来之后自动找路、送信、带回复回来。核心目标只有一个让任务执行链路变得可靠、可观测、可扩展。在跑了大半年生产环境之后我把里面的通用部分抽了出来补上了文档和示例整理成了一个可以独立部署的引擎。这篇文章会从设计思路、核心链路、部署配置到生产环境的避坑经验完整拆一遍 hermes-agent 的实际玩法适合正在做自动化平台、运维工具体系、或者想给业务系统加一层“大脑调度手脚执行”能力的后端工程师和平台工程师参考。1. 项目概述hermes-agent 到底解决什么问题1.1 它是什么和主流 agent 框架的差异很多人听到 agent 第一反应是 LangChain、AutoGPT 这类面向大模型应用的框架。hermes-agent 走的是另一条路它假设任务来源是确定的比如一条告警、一个定时器、一个 Webhook 请求或者一个消息队列消息然后重点解决这些任务“怎么稳定跑完、怎么在异常情况下不丢、怎么把结果传回去”。所以它和 LangChain、Celery 这类工具定位上完全不同。维度hermes-agentLangChainCelery核心定位任务执行与 agent 调度底座LLM 应用编排框架分布式异步任务队列是否内置模型能力不内置插件可以接内置大量模型交互能力无任务可靠性生命周期管理、重试、幂等一般由开发者自行处理依赖 broker 可靠性扩展方式插件协议写普通函数即可链式、图式编排Task 函数使用场景自动化运维、监控联动、工具编排对话、RAG、Agent 推理异步耗时任务如果你需要一个能直接对接“系统事件”和“业务动作”的协调器hermes-agent 会比 LangChain 这种重编排框架轻得多。它不引入大量抽象概念也没有复杂的 agent 记忆管理核心对象就两个任务Task和插件Tool/Plugin。任务描述“要做什么”插件负责“具体怎么做”。这也是我在实践中反复取舍后的结果。早期我也想把它做成一个通用 agent 平台什么都往里放结果就是配置越来越重、用户越来越懵。后来砍到只剩任务调度和插件执行两个核心反而被团队用得最勤。轻才是工具能够活下去的关键。1.2 核心使用场景与目标用户从实际跑过的场景来看hermes-agent 最合适的几类用途是定时巡检每小时检查一批服务的健康状态、证书到期时间、磁盘水位异常自动发通知。告警联动收到监控系统 Webhook 后根据告警内容自动执行一条处置命令或拉起诊断流程。发布流程串联代码构建完成后触发部署、跑自动化测试、回写结果到内部系统。工具网关把内部已有的脚本、CLI、HTTP API 统一包装成 agent 可调用的插件供其他系统通过一个入口来使用。目标用户很清晰不是业务产品经理而是后端工程师、SRE、运维平台开发这类同学。大家手里多多少少攒了一堆脚本有的挂在 Jenkins有的放在 cron有的靠人肉手工执行hermes-agent 能把它们统一起来而且不会强制你重写业务逻辑。1.3 设计原则与架构边界项目的三条设计原则都是在被生产环境教育之后总结出来的第一一切皆任务。外部系统不直接调用插件只提交任务。这样调度、重试、超时、权限控制都收敛在任务层边界非常清晰。第二插件就是普通函数。任何插件本质上都是“输入一个 Payload输出一个 Result”。不需要写一堆类不需要继承复杂基类。普通函数加一个注册装饰器就够了。第三状态必须可追踪。任何一次任务执行都要能回答三个问题现在到哪一步了卡在哪个插件上报错信息是什么为此引擎层面会持久化状态流转事件方便排查和复盘。架构边界也砍得很明确它不负责业务数据的存储不内置大模型推理能力不替代你的消息队列。它只做一件事把“任务”变成“结果”并保证这个过程的稳定性。想复杂了反而会失去这种工具的锋利度。2. 核心链路拆解一个任务从产生到完成的全过程2.1 事件接入与任务解析任务进入 hermes-agent 的方式我统称为“事件接入”。目前内置了三种最常见的事件源HTTP Webhook、定时触发器、消息队列消费者。接入层做的事情其实很统一把不同来源的原始数据解析成统一的 Task 结构。一个 Task 的核心字段大致如下dataclass class Task: id: str # 全局唯一任务 ID幂等关键 type: str # 任务类型决定路由到哪个插件 payload: dict # 插件需要的业务参数 timeout: int 60 # 执行超时时间秒 retry: int 3 # 失败重试次数 priority: int 0 # 优先级越大越先执行 created_at: float 0所有外部事件最终都会变成这样一个 Task。这样做最大的好处是接新的事件源完全不影响执行链路。比如今天你通过 Webhook 提交任务明天想改成从 Kafka 消费只需要新加一个 consumer 把消息转成 Task后面的逻辑完全复用。在设计“任务解析”环节时有一点特别值得注意不要试图在任务字段里塞太多东西。payload 保持扁平化、参数命名和插件协议对齐这样后面做权限校验、审计日志都会简单很多。我们早期尝试过把整个业务对象序列化进去结果数据膨胀到队列消费都开始抖动后来统一改成只传业务 ID实际数据由插件按需去取问题立刻消失了。2.2 插件注册与工具调用插件是 hermes-agent 的能力单元也是很多人刚接触时最容易卡住的点。其实插件模型简单得有点“简陋”每一个插件就是一个普通函数加上一个tool注册装饰器。from hermes import tool tool(namehttp_check, descriptionHTTP 健康检查) def http_check(payload: dict) - dict: import requests url payload[url] timeout payload.get(timeout, 5) resp requests.get(url, timeouttimeout) return { status_code: resp.status_code, ok: resp.ok, }插件函数接收一个 payload 字典返回一个字典。引擎负责把它包装成可执行单元并附加超时、重试、审计等能力。调用过程也很直白任务路由层根据task.type找到对应的插件把task.payload传进去拿到返回值后写入执行结果。这里值得展开的是插件“命名空间”的概念。生产环境中经常会出现多个插件的函数名碰撞或者同一个业务能力想区分不同版本。所以 hermes-agent 支持给插件加命名空间比如aliyun.ecs.reboot、internal.dns.update。路由时支持完整匹配也支持前缀匹配这让插件多了个类似“目录”的组织维度。插件开发时还有两个隐含约定一是函数要做幂等同一个 payload 执行两次结果一致二是函数内部不要依赖进程内全局状态因为 worker 进程随时可能被重启或扩容。这两个约定不是强制语法但违背了很容易在生产环境踩坑。2.3 多 agent 协作与状态同步单个任务执行只是第一步实际业务里很多场景需要多个 agent 配合。比如一条告警进来agent A 先做初步诊断agent B 根据 A 的结果去执行恢复操作agent C 再把整个过程上报。hermes-agent 通过“任务编排图”来支持这种协作而不是搞一堆 agent 互相通信的复杂协议。最简单的编排方式是 DAG。每个节点是一个子任务节点之间通过depends_on声明依赖关系上游节点的输出会作为下游节点 payload 的一部分传入。workflow: alarm_handle nodes: - id: diagnose tool: internal.diagnose - id: recover tool: internal.recover depends_on: diagnose condition: {{ diagnose.status error }} - id: notify tool: feishu.notify depends_on: [diagnose, recover]每个节点本质上还是一个独立任务有自己的状态和日志。这样做的好处是某个节点挂了可以单独重试不用整条链路重跑。状态同步则是通过存储层完成的引擎会在每个节点状态变化时写入一条事件Workflow 控制器只负责读状态、做判断、推进度不依赖进程内共享内存。这里要强调一个经验不要用进程内变量做任务状态同步。一开始我图省事把 workflow 的当前节点记在内存里结果一个 worker 一重启整条工作流就失忆了。后来改成把状态快照持久化到数据库配合 Redis 做缓存才算真正解决了状态一致性的问题。中间的过程很狼狈但结论很简单——agent 协作的“记忆”不能放在脑子里要放在纸上。3. 快速部署与基础配置3.1 环境准备与 Docker 部署hermes-agent 对运行环境要求不算高Python 3.10 以上即可依赖也尽量保持精简。官方推荐用 Docker 部署一条命令就能把核心服务和 Web 控制台拉起来。我这里给出一份基础版的docker-compose.yml包含 hermes-agent 本体、Redis 和 PostgreSQLversion: 3.8 services: hermes: image: hermes/agent:latest restart: unless-stopped ports: - 8000:8000 environment: HERMES_STORAGE_DSN: postgresql://hermes:hermespostgres:5432/hermes HERMES_BROKER_URL: redis://redis:6379/0 HERMES_CONFIG_FILE: /app/config/config.yaml volumes: - ./config:/app/config depends_on: - postgres - redis redis: image: redis:7-alpine restart: unless-stopped postgres: image: postgres:15-alpine restart: unless-stopped environment: POSTGRES_USER: hermes POSTGRES_PASSWORD: hermes POSTGRES_DB: hermes部署起来之后默认会监听 8000 端口提供 Webhook 接入和可视化任务查询两个入口。这里有一个小提醒如果你只是本地测试可以把 PostgreSQL 换成 SQLite配置里把 DSN 改成sqlite:///./hermes.db就行少一个依赖跑起来更轻快。3.2 配置文件详解hermes-agent 的配置集中在 config.yaml 里。我会把生产环境用的配置逐段拆开说一下因为这个地方信息密度很高很多问题都出在配置理解不到位。server: port: 8000 webhook_path: /webhook scheduler: enabled: true timezone: Asia/Shanghai executor: worker_num: 4 task_queue_size: 1000 default_timeout: 60 max_retry: 3 retry_backoff: 2.0 storage: dsn: postgresql://hermes:hermeslocalhost:5432/hermes broker: type: redis url: redis://localhost:6379/0scheduler控制定时任务开关和时区。定时任务表达式用的还是标准 cron如果团队有多个时区最好统一配置成同一个时区不然排查“为什么这个任务比预期提前一小时执行”会很难受。executor是核心执行参数。worker_num 表示同时跑多少个任务task_queue_size 表示队列里最多积压多少任务。这两个参数决定系统水位配小了任务排队很久配大了内存和数据库压力会陡增。storage保存任务元数据、插件执行记录、工作流状态建议生产环境用 PostgreSQL 而不是 SQLite因为并发写多的时候 SQLite 的锁竞争非常明显。broker目前主要用 Redis任务先进队列再被 worker 拉取执行。Redis 的 List 加 Stream 是后备方案两者各有优势Stream 能支持消费组后续会有更多玩法。还有一个非常容易被忽略的配置是executor.retry_backoff。它代表重试间隔的指数退避倍数。比如第一次失败后等 2 秒第二次等 4 秒第三次等 8 秒。没有退避策略的话失败任务会在高峰期疯狂重试把下游系统打得更惨。3.3 第一个示例定时巡检并发通知配置完成之后最值得跑一遍的示例是“定时巡检 异常通知”因为它覆盖了任务创建、定时触发、插件执行、结果分发四个核心环节。假设我们要每 5 分钟检查某个内部站点的健康状态失败时往飞书群发一条告警。第一步先写两个插件一个是 HTTP 检查插件http_check另一个是飞书通知插件feishu_notify。第二步在配置里注册插件hermes-agent 支持从 PYTHON 路径自动发现插件模块只要你把插件函数所在的.py文件放进plugins/目录重启后就会被加载。第三步创建定时任务curl -X POST http://localhost:8000/admin/schedules \ -H Content-Type: application/json \ -d { name: check-internal-web, cron: */5 * * * *, task: { type: http_check, payload: { url: https://internal.example.com/health, timeout: 5 } }, notify_on_error: true, notify_plugin: feishu_notify }这个计划任务一注册scheduler 就会按照 cron 表达式定时创建任务并交给执行器。notify_on_error表示只有巡检失败才触发通知避免每次成功都在群里刷屏。如果你希望失败时把http_check的返回结果带进通知内容里可以在插件返回 dict 中增加一个__notify__特殊字段引擎在发送通知时会优先读取这个字段填充消息模板。这个小设计极大地方便了“执行后带结果通知”的场景。4. 生产环境中的避坑经验4.1 任务丢失与幂等在跑 hermes-agent 的过程中第一个让我紧张的问题就是任务偶尔“凭空消失”。现象是任务确实提交到了 broker但执行记录里找不到日志也没有任何堆栈。排查到头才发现是客户端把任务提交到了只读副本消息进了 broker 后还没来得及被 worker 消费Redis 就发生了主从切换消息丢了。这不是 hermes-agent 本身的 bug而是所有依赖消息队列的系统都会遇到的经典问题。解决方案也不复杂任务提交方必须在本地建一张任务登记表状态是“已提交”之后通过 hermes-agent 的结果回调更新状态如果一段时间没收到回调就自动重新提交。重新提交时依靠 Task id 做幂等引擎检测到相同 id 的任务已经存在就只做追加更新不再重复执行。幂等同样适用于插件层面。比如发送告警通知、扣减库存这类操作天然不能重复执行。所以插件函数在收到 payload 时最好先检查一下业务侧是否已经有这个请求 ID 的处理记录。这个习惯形成了线上事故能少一大半。4.2 插件崩溃隔离插件代码一旦跑在引擎主进程里任何内存错误、段错误甚至exit()调用都可能把整个 agent 带走。这个问题我在早期版本里遇到过不止一次某个插件对接的 SDK 内部线程突然爆炸直接导致同一个 worker 上其他任务全部失败。后面我把插件执行隔离进了子进程。每个任务默认在一个单独的 worker 进程中执行父进程只负责等待结果、处理超时和回收状态。实际上就是 Python 的ProcessPoolExecutor加上超时控制from concurrent.futures import ProcessPoolExecutor executor ProcessPoolExecutor(max_workerscfg.worker_num) future executor.submit(run_plugin, task.type, task.payload) try: result future.result(timeouttask.timeout) except TimeoutError: future.cancel() mark_task_failed(task.id, errortimeout)这样做以后效果非常明显单个插件的崩溃不再影响主进程和其他任务。代价是进程启动有开销但相比稳定性收益来说完全可以接受。如果遇到状态特别重的插件比如需要加载机器学习模型这种重活建议做成独立服务插件只做 HTTP 调用避免每次任务都重新初始化。另外插件执行环境建议做权限收窄。在容器里跑 hermes-agent 时不要用 root 用户挂载只读文件系统的插件目录命令行工具如果有敏感操作尽量在插件内部先做二次鉴权。4.3 并发控制与背压生产环境还有一个高频事故上游系统一次批量推送了几千条任务hermes-agent 的 worker 一下子被打满数据库连接数暴涨Redis 内存飙升最终整个服务不可用。这本质上是一个“背压”问题——生产速度超过了消费速度系统又没有自我保护机制。hermes-agent 在 executor 里内置了最简单的背压控制任务队列设置上限超过上限的新任务直接返回503 Busy由上游决定是重试还是丢弃。executor: worker_num: 4 task_queue_size: 1000这里的 1000 是队列里等待执行的任务数量上限。当队列满了之后新的提交会收到明确的错误提示而不是无限堆积。我在实际使用中还会给重要任务设置独立的队列或者更高的优先级防止低优任务占满了 worker导致高优告警处理不及时。还有一个常见的“雪崩”场景某个下游 API 开始变慢插件执行时间从几百毫秒涨到几十秒worker 长期被占着任务越积越多。这种情况我建议在插件层单独设置更短的超时时间并且在下游服务恢复之前人为暂停相关定时任务。比如curl -X POST http://localhost:8000/admin/plugins/disable \ -d {name: internal.recover}先止血再排查这个顺序千万不要反过来。5. 可观测性与扩展方向5.1 日志、跟踪和指标hermes-agent 在开发阶段很容易让人产生一种“明明没报错但就是不知道任务在干嘛”的无力感。所以我在开源版本里把可观测性做成了内置能力而不是事后插件。日志方面所有关键节点都使用结构化日志输出包含 task_id、plugin_name、execute_time、status 等字段。这样你在 ELK 或者 Loki 里可以直接按 task_id 检索整个链路不需要像以前一样翻各个应用的 stdout。追踪方面hermes-agent 原生支持 OpenTelemetry 协议会自动为每个任务创建一条 span。从事件接入、任务解析、插件执行到结果通知每一段耗时都能在 Jaeger 或 Tempo 上看到。之前定位过一个“偶尔慢 3 秒”的问题靠 trace 发现不是插件本身慢而是数据库连接池在高峰期排队这种问题没有链路追踪基本很难查。指标方面暴露了一个/metrics接口可以直接接入 Prometheus。最常用的几个指标我列一下指标名含义hermes_task_submitted_total任务提交总数hermes_task_succeeded_total任务成功总数hermes_task_failed_total任务失败总数hermes_task_duration_seconds任务执行耗时分布hermes_executor_queue_size当前队列积压数量生产环境报警一般就盯两个失败率升高、队列积压持续增长。有了这些指标agent 的运行状态就变成了一张趋势图而不是黑盒。5.2 如何扩展自定义插件插件扩展不复杂但需要按照规范来否则后续维护会非常想骂人。插件的入口函数是同步函数还是异步函数都可以。如果返回结果里包含了next_tasks字段引擎会把它们当作新的任务继续派发。这种模式特别适合做“分而治之”的扫描类任务一个任务拿到一批 IP拆成多个子任务并发去检测再把结果汇总回来。自定义插件还有一个很有用的装饰器参数是permission。比如某些插件只能被定时任务调用不能被 Webhook 外部提交调用。你可以在注册时加上permissioninternal然后在接入层配置规则判断外部请求来源并过滤无权限的任务类型。这比在插件内部自己做判断要干净得多。下面是一个带“拆子任务”逻辑的示例插件from hermes import tool import asyncio tool(namebatch.ping) async def batch_ping(payload: dict): hosts payload[hosts] sub_tasks [] for h in hosts: sub_tasks.append({ type: single.ping, payload: {host: h, timeout: 3} }) return { next_tasks: sub_tasks, summary: fstart ping {len(hosts)} hosts }开发插件时建议保持一个约定每个插件目录下只放同类操作插件文件不要互相 import。一旦插件之间产生了依赖你就无法单独升级、下线某个能力整个系统会慢慢变成一坨互相拉扯的代码。5.3 后续规划hermes-agent 目前在我这边已经承担了大部分的“任务执行”工作但它还有不少可以往下走的地方。第一是跨语言插件协议。当前插件主要是 Python团队里其他语言的同事接入还是需要包一层 HTTP 服务后续计划推出一种基于 JSON-RPC 的插件协议让 Go、Java、Node 都能通过标准协议接入真正变成语言无关的工具网关。第二是工作流编排的可视化。现在的 DAG 配置是 YAML虽然 Git 管理很方便但非工程背景的同学看着还是一头雾水。后续会做一个简单的画布页面把节点、依赖关系、执行状态画出来让操作岗也能看懂任务走到了哪一步。第三是更细粒度的资源治理。比如给每个插件配置独立的并发上限、超时兜底、失败熔断阈值避免一个上游抖动把整个 agent 打垮。这个方向本质上是在做“流量治理”也正好是把 agent 从玩具变成生产力的必经之路。回到最开始的定位hermes-agent 不追求成为什么“通用人工智能平台”它就是做好一个信使该做的事情把送的过程走稳、把回执带回来、把每一次路线都记录下来。这半年多的实践让我最深的体会是agent 能不能在真实环境里活下来关键不在于某一个模型有多聪明而在于当一切都不顺利的时候它能不能知道自己该干什么能不能把失败的痕迹完整地留给人类去复盘。先活下来再谈智能。
返回列表