拓冰建站拓冰建站
首页 / 资讯中心 / 正文

轻量级Agent中间层设计:hermes-agent统一内部API与工具调度

先说结论如果你也在为“想把一堆内部API、CI任务、告警Webhook统一交给自然语言调度”这件事发愁hermes-agent 可能是那个不太起眼但能让交付速度明显变快的选择。我并不是要做一个大而全的Agent平台而是花了几个周末把团队内部一直零散维护的自动化脚本收敛成一个可插拔、可观测、接入新工具只需要改配置的信使式代理。所谓“信使”就是它不替你做业务决策也不负责训练任何模型它只负责把请求正确送到该去的地方再把结果原样带回来。听起来很简单但真正实现并跑在生产上之后我发现这一层“传话”的活儿比想象中难得多。Hermes这个名字取自希腊神话里替众神传信的信使神所以这个项目的名字叫hermes-agent本身就是冲着“路由与分发”去的。这篇文章把我在设计和实现过程中踩过的坑、最后沉淀下来的方案、以及一些可以直接抄走的配置和代码片段一次性写清楚。适合谁看一种是团队内部想做Agent中间层的后端开发另一种是想自建个人助手、但不想被重型框架绑住的折腾型用户。1. 为什么我又造了一个叫Hermes的轮子1.1 脚本越来越多每个工具都自成一套方言最早我们团队根本没有所谓的“Agent”只有十几个脚本分散在不同仓库里有给Jira建工单的有触发GitLab CI的有查监控告警的有跑数据库只读查询的。每个脚本都有自己的参数格式有的用JSON有的用YAML有的干脆是“备注: xx”这种自然语言然后靠正则硬解析。同事之间接手成本越来越高新同学想加一个工具第一句永远是“这个脚本的参数格式是跟着哪个需求定的”这种状态下做Agent最大的问题其实不是大模型回答得准不准而是工具层完全不可控。我试过直接把十几个脚本挂给一个LLM调用第一个星期就被打回原形模型经常把必填参数漏掉脚本报错时错误信息也五花八门根本无法判断是参数问题还是服务问题还是权限问题。所以hermes-agent最早要解决的事情特别朴素统一每种工具的“方言”让它们都按同一种协议开口说话。后面再往上接大模型或者接IM机器人都只跟这一层打交道不需要逐个适配脚本。1.2 我只想要一个“信使”而不是一个“大脑”在动手之前我列过一个负面清单hermes-agent不去做的事情。第一不做大模型推理层不自己封装Prompt模板库第二不做长期向量记忆不硬塞一套向量数据库第三不做自动化工作流引擎不搞可视化拖拽编排。这些都有成熟方案把精力花在它们身上只会让项目变成一个“什么都想做但都做不深”的怪物。Hermes的位置被压缩得很窄接受任务请求做意图路由调用对应的插件汇总结果处理超时和失败把结果以标准格式返回。用人的团队做类比它更像前台和收发室而不是每个部门的业务专家。也正是因为约束了边界整个项目的核心代码量才能控制在一个很小的范围里插件开发者也只需要看一份协议文档。这个决定在后期被证明非常重要。团队后来接入新工具时几乎没有人再来问“要改Hermes哪里的源码”因为所有扩展都发生在插件层主程序变成了一个稳定的运行环境。1.3 和现成Agent框架的取舍对比构建之前我也评估过一些现成的Agent框架。但它们普遍是为公开API生态设计的内网系统的认证方式、数据结构、部署网络都很难直接套框架迭代很快每隔几个月就要处理breaking change团队内部工具还没接完升级的成本先上来了。我自己梳理了一张对比表算是当初选型的依据维度重型Agent框架hermes-agent插件适配成本需要深入了解框架概念跟随版本只需实现一个handle函数内网系统认证大多针对公网API做OAuth/API Key插件内自己处理不限制方式依赖规模重常引入向量库、任务队列轻核心依赖只有asyncio故障隔离插件崩溃可能影响主进程插件运行在隔离进程中超时可熔断可观测性自带强大但学习成本高所有事件有标准日志不额外造概念这张表并不是说现成框架不好而是说对于一个以内网脚本集成为主的场景一个自研的轻量信使协议反而更可控。如果你原本就有几十个公开API需要接那用成熟框架是对的但如果你和我一样主要痛点是“内部工具没有一个统一入口”那么从零写一个只负责路由的Agent中间层投入产出比其实很高。2. 核心架构信使只负责传话但传话要传得清楚2.1 消息总线和事件流是怎么设计的hermes-agent的入口是一个常驻进程内部跑着事件循环。所有请求进来后会被包装成一个标准消息对象包含request_id、session_id、intent、payload、source、timestamp这几个字段。我尽量不去定义复杂的领域模型因为一旦模型膨胀协议就很难保持稳定。一个最小消息对象长这样dataclass class HermesMessage: request_id: str # 每次请求的唯一ID session_id: str # 会话ID用于关联上下文 intent: str # 意图标识由路由模块给出 payload: dict # 工具参数始终是JSON可序列化的 source: str # 请求来源im、api、cron timestamp: float为什么强制payload必须是JSON可序列化因为这样才能做到插件热插拔、消息持久化、以及将来做多Agent分布式调度时不至于卡在“对象没办法传输”这种低级问题上。所有插件返回值也统一为Result Envelope里面有data、error、elapsed_ms、warning字段上层消费方不用关心某个插件内部到底抛了什么异常先看error是否存在就行。事件流方面我只保留了四个核心事件TASK_RECEIVED、TOOL_STARTED、TOOL_SUCCEEDED、TOOL_FAILED。再加一个可选的PROGRESS用于长任务进度上报。事件越多维护成本越高四个核心事件基本能覆盖所有业务场景。2.2 插件协议把每个工具封装成能收信的邮筒插件是hermes-agent最核心的扩展单元。每个插件本质是一个Python包只需要暴露一个入口函数# plugin_sample.py async def handle(message: dict, context: dict) - dict: # message: 路由后的HermesMessage数据 # context: 全局配置、密钥、日志句柄等 return {data: {result: ok}, error: None}此外还需要一个manifest.json注册元信息插件名称、描述、接收的意图列表、超时阈值、是否需要确认等。Hermes启动时会扫描插件目录把manifest里的意图列表加载到路由表中。插件间互相不能直接调用只能通过消息总线转发这样不会出现插件A依赖插件B的隐式耦合。我在设计插件协议时参考了“邮筒”的思路发件人把信投进邮筒邮筒不关心信的内容是什么只负责保证它进入正确的邮路。插件也一样它只需要声明“我能处理哪些类型的信”不必关心上游是谁。这样当一个新的内部系统接入时只需要写一个插件把对方API包装成handle函数不需要改动主流程。2.3 上下文聚合别把工具返回的垃圾都喂给大模型一开始我直接把工具返回的原始JSON塞进Prompt里后果很直接模型很快就被无关字段干扰甚至开始试图“修正”工具输出里的错误数据。比如数据库插件返回了connection_pool_size这样的运维指标模型可能误以为这是业务报表的一部分回答得莫名其妙。后来我加了一个标准化聚合层。对每个插件的返回结果做三层处理先抽取关键字段再修剪长度最后转成“人话摘要”。如果插件返回的是表格就转成Markdown表格只保留前10行如果返回的是错误信息只保留错误码和可读描述不把堆栈整个丢给模型。这样做的目的是把大模型的注意力集中在“如何完成用户任务”上而不是“如何解析垃圾结构”。关键点是聚合层要提供“原始数据”和“给模型的数据”两个版本原始数据存日志或按需返回给前端给模型的数据则经过清洗。否则排查问题时会非常痛苦。举个具体的例子一个查询GitLab流水线的插件原始响应里有几十个字段但我只会把pipeline_id、status、failed_job_name、duration这四个字段转成给模型的摘要。其余字段并不是没用而是它们应该在用户点击“查看详情”时从原始存储里取出不需要占用模型上下文。3. 路由、记忆与流式输出三个最容易被做糊的环节3.1 路由优先级和超时熔断路由不是简单关键词匹配。如果用户说“帮我把上次那个失败的构建日志拉出来”里面既有“构建”又有“日志”可能会匹配到两个插件。hermes-agent给每个插件定义了一组关键词和权重同时引入了“置信度阈值”和“优先级”两个维度。匹配分数超过0.8时直接命中如果有多个超过0.6且接近就进入“询问澄清”分支由Agent反问用户而不是瞎猜。为了防止某个插件卡死整个请求每个插件都有一个独立超时阈值默认60秒。执行插件时Hermes会创建一个Future到时间拿不到结果就直接向用户返回“该工具执行超时”同时把插件进程标记一次失败。连续失败3次熔断器打开后续请求不再路由到该插件直到30秒冷却期结束。这个机制参考了微服务里的熔断思想但实现得很轻核心只用了一个计数器和一个时间窗口。超时熔断最容易被忽略的细节是超时之后插件任务可能还在跑它可能会真的把一个工单创建成功但上层已经告诉用户失败了。所以我要求所有写操作类插件实现“幂等键”请求里带上request_id插件在接受任务时先查是否已经处理过。这样就算超时误报也不会产生重复工单。3.2 让Agent记住“上一句话”的笨办法记忆是所有对话型Agent绕不开的坑。一开始我天真地以为只要把历史消息一并发给LLM就行结果上下文很快就爆了而且无用信息太多导致答非所问。后来采用的方案分两层短期记忆用滑动窗口长期记忆用摘要缓存。短期记忆方面每个session_id最多保留最近6轮对话超过的按token数裁剪。每条消息进入LLM之前都要经过一个压缩函数如果是工具返回内容只取摘要字段如果是用户消息保留完整意图如果是Agent回复只保留结尾的主要结论。这个压缩函数是可以定制的不同场景可以配置不同策略。长期记忆我没有引入向量库而是用了一个更笨的办法每10轮对话后让大模型把这一阶段的关键信息浓缩成不超过200字的“阶段摘要”存到Redis里key是session_id:summary。下一次对话时如果是新会话优先带上摘要而不是全部历史。实践证明对内部工具类Agent来说这个方案足够用而且成本远低于向量库。3.3 流式状态上报用户等得不耐烦才是最大的bug工单类工具的响应时间动辄十几秒甚至几十秒如果用户发一条消息后干等着体验非常糟糕。后来我给hermes-agent加了基于SSE的流式状态推送服务端收到请求后立即返回“已受理”随后通过事件流把TOOL_STARTED、PROGRESS、TOOL_SUCCEEDED逐个推给客户端。插件里可以用一个简单的回调上报进度await context[progress](正在创建工单...) await context[progress](已连接到Jira API) await context[progress](等待审批人响应...)前端收到这些进度后依次展示用户至少知道“系统已经在干活了并且干到哪一步了”。这个改进给体验带来的提升比任何模型调优都明显。长任务场景下用户能忍受的等待时间其实很短但只要有过程反馈人就会安心很多。4. 配置、部署与热插拔生产环境最关心的三件事4.1 一份立即可用的最小配置我始终认为配置文件的复杂度应该与项目规模成正比。hermes-agent的配置只分为三块全局配置、插件注册表、会话策略。一份最小的启动配置长这样hermes: bind: 0.0.0.0:8080 log_level: info plugins_dir: ./plugins default_timeout: 60 redis_url: redis://localhost:6379/0 sessions: memory_window: 6 summary_every: 10 summary_max_tokens: 200 router: min_confidence: 0.6 clarify_threshold: 0.8 circuit_breaker: failure_threshold: 3 cooldown_seconds: 30插件注册表直接在plugins_dir里放manifest.json即可不需要在配置文件里维护一份插件清单。我这样设计的原因是把“插件的增删”和“主配置的变更”解耦插件开发者只需要关注自己的目录。有几个字段我特别解释一下。default_timeout是全局兜底的但每个插件可以在manifest里覆盖避免一个慢插件拖死全局。redis_url是可选的如果你不需要长期记忆和会话摘要可以把session存储设成内存模式这样甚至连Redis都不需要。4.2 Docker部署与资源占用实测我把hermes-agent打包成了单个Docker镜像基于Python 3.11-slim最终镜像体积大约是112MB。运行起来后在不包含任何外部模型请求的状态下常驻内存稳定在230MB左右CPU占用几乎为0。这个数字对于长期跑在内部服务器的场景来说完全可以接受。Dockerfile里面有一个重要细节非root用户运行。我在base image里专门创建了hermes用户然后给plugins目录设置为可写。否则插件运行时如果需要在本地写缓存文件会因为没有权限而报错。这个坑看起来小但第一次部署时很容易忽略。启动命令我放在Makefile里生产环境直接用docker compose起redis和hermes两个服务docker compose up -d # 强制构建镜像再启动 docker compose up -d --build hermes-agent日志统一打到stdout由采集系统收集。每个消息的request_id都会打进日志排查一次失败的完整链路只需要grep这个ID就能看到从入口到插件的全链路日志。4.3 不重启服务也能升级插件插件热插拔是hermes-agent被问得最多的功能。实现思路不复杂主进程启动一个后台任务用watchdog监听plugins_dir下的文件变化。当某个插件的manifest.json或代码文件被修改先校验插件包的哈希值如果变化就重新加载。加载过程使用独立的importlib loader并设置模块隔离这样插件里即使有全局变量也不会污染其他插件。为了确保热更新的安全性我加了一个“双目录”机制每个插件先复制到临时目录再执行importlib.reload。只有新的插件模块成功被加载并检查过manifest才会替换掉旧的。如果插件在加载阶段就抛异常旧版本继续生效同时日志会记录错误。这样就不会出现“改了一个插件整个Agent服务起不来了”的惨案。当然热插拔解决的是代码更新不能解决依赖变化。如果一个插件新增了第三方库镜像里没有那仍然需要执行镜像重建。对于这种场景我会在插件manifest里标注额外依赖CI检查时会统一收集依赖并重建镜像。5. 从内部工单到个人助手这类Agent到底值不值5.1 工单自动分诊的落地数据我们第一个正式接管的场景是内部工单系统的自动分诊。以前用户提交一个工单后人工需要根据内容判断归属哪个团队高峰期积压严重。接上hermes-agent后流程变成工单Webhook触发请求路由模块根据工单内容匹配“网络”“数据库”“前端”“权限”四类插件每类插件负责调用对应的查询接口并返回建议负责人。跑了一个月后的数据自动分诊准确率大概在90%左右剩余10%主要来自描述模糊的新类型问题Agent会明确回答“无法确定”而不是硬猜。这个“无法确定”很关键因为它避免了把工单派错人造成更大的返工成本。处理时效从平均40分钟降到了5分钟内而且用户能实时看到Agent正在查询哪些系统。在这个场景里Hermes的价值不在于做出什么神奇决策而在于把原本散落各处的“判断规则”变成插件让大家都能用自然语言去触发同一套流程。5.2 个人助手场景下的响应体验优化另一个我自己在用的场景是个人助手。我用hermes-agent连接了日历、待办事项、天气预报、以及几个常用网站的信息抓取。和公开的语音助手不同所有数据都保存在自己的服务器上插件也完全是自己控制。优化体验最大的一个改动是“先快速确认再慢慢办事”。当我问“明天下午有没有空的会议室”Agent会先返回一句“好的我正在查询明天下午三间会议室的占用情况预计需要8秒”然后才开始调插件。这个机制并不是简单的缓冲而是把用户的预期管理放进了系统设计而不是靠运气。通过SSE前端可以在大模型流式回复的同时同步展示工具执行的中间状态。我统计过在这个场景下用户满意度我自己打分提升最明显的不是回答内容而是“感觉它在干活”。工具型Agent不能只做一个答案生成器它需要让整个执行过程可见、可信。5.3 下一步多Agent协作与可信执行hermes-agent目前还只是一个单进程的调度中枢下一步我计划把消息协议直接复用为多Agent通信协议。不同Agent可以负责不同领域比如“工单Agent”“监控Agent”“财务Agent”它们之间通过HermesMessage互相传递任务而不是彼此直接用HTTP调用。这样做的最大好处是统一了超时、重试、权限校验的入口。可信执行也会是重点。每个插件调用时都会有独立的审计日志包括谁在什么时间调用了什么工具、传了什么参数。对于写操作类调用会额外要求用户二次确认。这里我最大的心得是协议和日志比功能更值得优先设计。只要消息协议稳定后面加多少插件、接多少个Agent都只是堆量如果协议没想清楚就急着堆功能后面返工的成本非常高。至少从我几个月的使用感受来说hermes-agent让我真正理解了“Agent”这个词的核心并不是模型而是协作。如果你也要做一个类似的东西我会建议你先别急着上K8s也别急着接向量库拿一个真实场景从一条消息开始跑通全链路然后把协议、日志、超时处理好这个基础比什么都重要。hermes-agent就是这样一个跑在协议上的信使它看起来不起眼但传话传明白了事情就成了一大半。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门