资讯动态

从零搭建AI Agent消息中枢:hermes-agent架构设计与实践

发布时间:2026/9/9 12:00:35 来源:尧图企业网站定制
做AI Agent这段时间我越来越觉得工具链里缺一个真正能“跑起来”的消息中枢。很多项目都是模型很强、Prompt写得很花但落到实际任务上各种工具调用、状态同步、记忆管理的问题就全冒出来了。hermes-agent 这个名字借的是希腊神话里信使之神赫尔墨斯的名号——干的事情也差不多在用户、大模型、外部工具之间做消息的搬运、路由和编排。这篇东西不聊概念就聊聊我把 hermes-agent 从零搭起来的过程包括架构怎么定的、核心模块怎么切的、踩了哪些坑以及一些可以直接抄走的配置和代码。这个项目适合谁我觉得主要有三类人一是正在做个人助理类 Agent 的开发者二是想把多个大模型 API 封装成统一工具出口的团队三是对 Agent 内部的消息流转机制好奇、想搞明白“模型怎么一步步调工具完成任务”的朋友。如果你只是想要一个现成的“一键部署的智能体”那这篇东西对你帮助有限——我讲的更多是“怎么把一个 Agent 内部的事情理清楚”而不是“怎么最快跑通一个 Demo”。1. hermes-agent 的整体设计思路1.1 为什么要把 Agent 拆成“消息路由”的形态做 Agent 的第一直觉往往是先想“我要一个什么样的助手”然后直接写一个巨大的循环读用户输入、调大模型、拿到回复、再读、再调。这个思路在 Demo 阶段完全没问题但一旦任务复杂度上来你会发现所有逻辑都挤在一个循环里每个新功能都要改主流程改到最后自己都不敢动了。hermes-agent 换了个思路把 Agent 看作一个消息路由器。用户的一句话进来经过意图识别后变成一条结构化消息这条消息在内部流转被规划模块拆解成子任务每个子任务带着自己的参数被路由到对应的工具执行器工具执行完结果又变成一条消息回到模型手里模型决定下一步是继续调工具还是给出最终答复。也就是说整个系统的主干不是“模型的推理循环”而是“消息的流转管线”。这样做的好处有三个。第一每个环节都能独立测试规划模块、工具模块、记忆模块各管各的出问题只要看消息在哪一步断了就行。第二扩展新能力基本不用动主干注册一个新工具就是往路由表里加一条记录。第三多智能体协作变得很自然两个 Agent 之间本质上就是互相投递消息无非是消息的“消费者”从工具变成了另一个 Agent。对应的代价是前期设计成本高你必须在写代码之前就把消息格式、状态机、错误处理这些定义清楚。很多人觉得 Agent 项目“不需要设计跑起来再说”我恰恰认为 Agent 是最需要先设计消息格式的项目——因为模型的输出天然不稳定如果消息格式都不稳定后面所有环节都会跟着抖。1.2 核心架构四个层次和一条主线hermes-agent 实际落地时分成四个层次接入层负责对接各种来源的输入包括命令行、HTTP 接口、定时任务、Webhook。这一层只做一件事把各种格式的输入统一转换成内部消息。编排层核心逻辑所在包含任务规划、工具调度、上下文组装、结果裁决。它是整个 Agent 的“大脑皮层”。工具层所有外部能力的抽象比如 HTTP 请求工具、数据库查询工具、文件读写工具、搜索工具。每个工具只负责“把输入参数变成输出结果”不感知上层逻辑。存储层短期上下文缓存、长期记忆库、任务状态的持久化。存储层看起来不显眼但 Agent 能不能在多轮对话里“稳住”主要看这一层。四个层次之外还有一条贯穿始终的主线就是消息。我在 hermes-agent 里定义了一个统一的消息结构不管哪一层之间传递什么内容都走同一种格式。这个结构大概长这样dataclass class AgentMessage: msg_id: str # 消息唯一 ID msg_type: str # 消息类型user_input / plan / tool_call / tool_result / final_answer role: str # 发送方角色user / planner / executor / memory content: dict # 核心内容按 msg_type 不同而不同 meta: dict # 元信息时间戳、token 消耗、重试次数等 parent_id: str | None # 父消息 ID用于追踪整条链路所有消息都带parent_id这是我最坚持的一个设计。有了这一条任何一个任务都可以回溯完整链路用户当时说了什么、模型分了几步、每步调了什么工具、工具返回了什么、最后怎么得到的结论。排查问题的时候这条链路就是案发现场的监控录像省了无数口舌。这一节先讲整体接下来把每个层次的细节拆开说。2. 核心模块的细节设计与选型理由2.1 任务规划ReAct 循环在 hermes-agent 里的实现ReAct 是目前单体 Agent 里最稳的模式之一。它的核心思路特别朴素模型不是在“一口气回答问题”而是在“一步一步做决策”。每一步模型先输出自己的思考Thought再决定要不要调用工具Action然后观察工具返回的结果Observation接着再进入下一步思考。这个循环一直持续到模型认为自己拿到了足够的信息才输出最终答案Final Answer。hermes-agent 的规划模块就是把这个循环显式地做成了一个状态机。你看到的所谓 Agent“会自己思考”其实就是这个状态机在后台转圈。状态机的状态有NEED_PLAN刚收到用户输入准备开始规划WAITING_TOOL模型请求调用工具等待工具执行PROCESSING_RESULT工具执行完毕正在把结果喂回模型READY_ANSWER模型认为可以给出答案停止循环为什么用状态机而不是一个while循环加几个if因为 Agent 在真实环境里不是一直往下走的。工具调用可能超时、可能报错、可能返回的数据不完整这些情况都需要在状态层面单独处理。举个例子工具超时的时候你不想让整个循环死掉而是想让规划器生成一条“工具超时请换一种方式”的消息继续走流程。用状态机来管理每条边的条件都清清楚楚调试的时候一目了然。这里有一个很多人容易犯的错误把所有的 Prompt 都堆在一个巨大的 system prompt 里然后指望模型自动“ReAct”。实际上ReAct 的推理格式最好通过函数调用来约束而不是靠 Prompt 提示。你可以告诉模型“请一步一步思考”但更可靠的是给模型一个明确的工具调用接口让它只能在规定的工具里选、只能用规定的参数结构。为什么会这样因为大模型在自由文本输出时格式漂移的概率远比我们想象的高而一旦走函数调用通道模型输出的就是结构化的 JSON后续解析就不容易出错。2.2 工具注册与 Function Calling 的参数约束工具层是 hermes-agent 里最简单、也最容易写乱的一层。简单在于每个工具本质就是“输入参数转输出结果”的函数容易写乱在于工具的输入参数描述如果不规范模型根本不知道该传什么。我在项目里做了一个工具注册表核心是一个装饰器tool( namehttp_get, description发起一个 HTTP GET 请求返回响应正文。适用于抓取网页、调用公开 API。, parameters{ type: object, properties: { url: {type: string, description: 目标 URL}, timeout: {type: integer, description: 超时时间默认 10 秒} }, required: [url] } ) def http_get(url: str, timeout: int 10) - str: # 实际实现省略 return response_text这个注册表有两个关键作用。第一它在系统启动时把所有工具的描述和参数 Schema 汇总成一份 JSON塞进模型接口的tools参数里模型据此决定调用哪个工具、填什么参数。第二它像一个插件系统业务方只要按这个格式写一个函数不需要改任何主流程代码新工具就自动接入。参数描述这件事我踩过最大的坑是“描述写得太简单”。比如一个查询数据库的工具参数只写了sql: string模型就会自由发挥生成各种表名不存在的 SQL。后来我把描述改成sql: { type: string, description: 要执行的 SQL 语句。注意只能使用 employees、orders、products 三张表必须包含 WHERE 条件禁止使用 DELETE 和 UPDATE。 }模型调用的准确率立刻上了一个台阶。工具描述的详细程度直接决定了模型在边界场景下的表现。你把它当 API 文档写它就会表现得像一个认真读文档的程序员你只给它一个函数名它就只能靠猜。2.3 记忆管理短期上下文与长期存储的取舍记忆是 Agent 里最容易被低估的模块。很多 Demo 用一次对话或者临时会话就完事了但真实场景下用户会在不同的时间点问相关的问题期望 Agent 记得之前的上下文。我把记忆分成两层短期上下文和长期记忆。短期上下文就是当前任务窗口里的对话和工具调用记录直接通过消息列表传给大模型。长期记忆则放在外部存储里一般是向量数据库按语义相似度检索后把相关片段作为背景信息拼进当前上下文。为什么不能把所有历史都塞进去主要受制于上下文窗口长度和成本。上下文越长单次请求的 token 消耗越大响应延迟也越高而且模型在超长上下文里的注意力会稀释反而容易忽略关键信息。所以 hermes-agent 的做法是当前任务相关的消息完整保留任务结束之后做一次摘要摘要入库原始消息按策略清理或压缩。这个取舍过程其实很像人的记忆。你不会记得昨天每一顿饭吃了什么但你会记得“昨天和朋友讨论过某件事的结论”。Agent 也一样保留结论、丢弃过程才能在长期运行中既不爆 token 又不丢关键信息。实际项目里我通常设定一个阈值短期消息超过 30 条或者超过 8000 token 时触发一次摘要归档。归档不是简单删掉而是把关键事实、结论、待办事项抽取出来以结构化文本存入长期记忆库。3. 实操把 hermes-agent 跑起来3.1 项目初始化与环境配置先看目录结构。我当时建项目的时候刻意保持了目录的克制没有一上来就堆一堆微服务单体优先方便迭代hermes-agent/ ├── hermes/ │ ├── __init__.py │ ├── messages.py # 消息结构定义 │ ├── router.py # 消息路由与主循环 │ ├── planner.py # 任务规划器 │ ├── memory.py # 记忆管理 │ └── tools/ │ ├── __init__.py # 工具注册表 │ ├── http_tools.py │ ├── db_tools.py │ └── file_tools.py ├── config.py ├── main.py └── requirements.txt依赖方面我当时最核心的只有四个大模型 SDKOpenAI 兼容接口即可、pydantic 做数据校验、tenacity 做重试、chromadb 做长期记忆存储。没有特殊情况不要随便引入重量级框架Agent 这类项目最大的变数在逻辑编排不在基础设施越轻越好跑。环境变量主要配两个模型 API 的地址和 Key还有默认的模型名称。这里有个经验之谈把模型名称也放到配置里别写死在代码中。开发调试的时候用便宜的轻量模型正式跑的时候切到更强的模型这是个能省不少钱的操作。3.2 核心代码消息循环与工具调用的最小实现整个 hermes-agent 的灵魂是这个主循环。我把它精简到只剩核心逻辑方便看清结构# router.py 核心逻辑精简版 def run_agent(user_input: str): # 1. 组装初始消息 messages [system_prompt(), {role: user, content: user_input}] max_steps 10 for step in range(max_steps): # 2. 调用大模型传入工具 Schema response llm.chat(messagesmessages, toolstool_schemas) # 3. 如果模型没有请求调用工具说明可以给出最终答复 if not response.tool_calls: return response.content # 4. 把模型的工具调用请求追加到消息列表 messages.append(response.message) # 5. 逐个执行工具并把结果回传给模型 for tool_call in response.tool_calls: result execute_tool(tool_call) messages.append({ role: tool, tool_call_id: tool_call.id, content: result }) return 抱歉步骤太多我还没整理好结论。这段代码是浓缩后的骨架实际项目里你还要加异常重试、工具超时、上下文截断策略、每一步的日志记录。但从阅读角度这个骨架已经能让人理解 Agent 的运行逻辑了模型提需求环境给结果循环往复直到模型认为信息足够。max_steps这个参数是我强烈建议保留的。很多 Agent “失控”的场景本质就是模型在一个任务上绕圈子反复调同一个工具拿不到有意义的结果。加了上限之后至少不会无限烧 token。我一般设 8 到 15具体看任务复杂度。3.3 实战案例让 agent 自动完成一次信息收集与总结举个完整的例子。假设用户说“帮我查一下杭州未来三天的天气然后告诉我适不适合户外跑步。”hermes-agent 处理这个任务时消息流转是这样的第一轮模型判断需要天气数据于是发起一次工具调用。内部的 tool_call 大致是{ name: http_get, arguments: { url: https://api.example.com/weather?cityhangzhoudays3, timeout: 15 } }第二轮工具返回天气数据包括温度、降水概率、风力。模型拿到数据后并不会立刻回答而是先做一次推理未来三天有没有雨、气温适不适合跑步、空气质量如何。这一步没有工具调用模型直接输出 final answer循环结束。整个过程的 token 消耗我实测下来大约在 2000 到 4000 之间具体取决于天气数据的长度和模型的输出习惯。如果你发现某次任务消耗异常高多半是模型在“反复试探”——调了搜索工具、又调了天气工具、还调了地图工具最后才确定答案。这种时候要么限制工具数量要么在工具描述里写明“这个任务只需要查天气”。实际跑起来还有个容易被忽略的点工具返回的数据格式。如果天气 API 返回的是嵌套 JSON直接原样塞给模型模型也能读但会浪费 token 而且容易理解偏。更稳妥的做法是在工具内部先做一层“文本化”把 JSON 转成一行摘要比如“杭州 3 月 15 日晴12-22 度降水概率 10%风力 3 级”。工具的输出越接近自然语言模型的理解成本越低这算是我在 hermes-agent 里踩过坑之后总结出的一个通用原则。4. 常见问题与排障实录4.1 工具参数格式不对导致调用失败这是我在 hermes-agent 里遇到频率最高的问题。症状很统一模型明明调用了工具但服务端报参数校验失败或者工具执行时直接抛异常。排查思路分三步。第一步看模型传的参数到底是什么。我在工具执行器入口加了一行日志把 tool_call 的完整参数打印出来几乎 80% 的问题在这一步就能定位。第二步看参数 Schema 是否合理。最常见的是必填项没标清楚或者描述里没写取值范围模型只能靠猜。第三步如果是嵌套参数结构考虑把它拆扁平。模型对深层嵌套 JSON 的生成稳定性远不如对扁平结构。另外我强烈建议在每个工具内部做一层容错参数解析失败时不是直接抛异常让整个 Agent 崩溃而是返回一条结构化错误消息给模型比如“参数错误缺少 city 字段请补充后重试”。模型看到这条消息后通常会自己修正参数再调一次。这个机制大大提高了工具调用的成功率。4.2 上下文长度超限怎么处理多轮任务跑多了一定会碰到 context length exceeded。这个问题的根源是消息列表只增不减工具返回的结果动辄几百上千 token积累几轮就爆了。我的处理策略按优先级排序先裁剪不重要的工具输出只保留最终结论再压缩历史对话把旧对话用摘要替换最后才考虑换更大上下文的模型因为这只是把问题延后不是解决。实际代码里就是一段简单的文本处理def compress_messages(messages, max_tokens8000): # 保留 system prompt # 保留最新 2 轮用户消息 # 中间的历史消息调用摘要模型生成一句总结塞回原位置 return compressed这个压缩动作要放在主循环里每次调用模型之前检查消息总长度。我一般以 token 数而不是消息条数为判断依据因为不同工具返回的 token 差异太大。4.3 多轮对话中的“失忆”问题“失忆”和“上下文超限”是两个相反方向的病。一个是塞太多一个是存太少。最典型的表现用户第二句话提到“刚才说的那个任务”Agent 一脸茫然。原因无非两种短期上下文在上一轮结束后被清掉了或者长期记忆没有接入。解决办法是给 hermes-agent 加一层“会话级持久化”。每一轮对话结束时把这一轮的关键结论抽出来写入长期记忆库新一轮开始时先在记忆库检索与当前问题相关的历史片段组装进上下文。我在 memory.py 里用了 chromadb检索时按相似度取 Top 3 片段每条片段不超过 300 token。这样既不会丢关键信息也不会把乱七八糟的历史全填进去。这个机制跑通之后你会明显感觉到 Agent 的“人味”上来了它记得你上次提到过的项目、记得你偏好的回复风格、甚至记得你讨厌在回复里用表情符号。如果说主循环是 Agent 的心脏记忆模块就是它的长期肌肉记忆。5. 一些零散但重要的经验写到这里再补几个不写成单独章节但很要紧的实践心得。第一日志一定要带 trace_id。我在 AgentMessage 里保留 parent_id 的另一个原因就是日志系统可以直接按链路 ID 汇聚所有相关日志。没有这个排查多轮任务的问题会痛不欲生。第二控制一次任务里暴露给模型的工具数量。工具太多模型容易“挑花眼”明明用 A 工具就能完成非要去调 B 工具绕一圈。我通常把工具按领域分组根据用户意图先做一轮粗筛只把相关组的工具塞给模型。第三为每个工具调用设置超时和重试上限。我用 tenacity 统一控制HTTP 类工具默认 15 秒超时、最多重试 2 次。工具长时间不返回整个 Agent 的响应都会被拖住这种问题在真实环境里比模型出错还常见。第四不要迷信“更强的模型就能解决所有问题”。我试过把某个特别复杂的 Agent 任务从轻量模型切换到旗舰模型结果确实好了不少但代价是单次调用成本翻了十倍。后来我仔细看日志发现真正救场的是我修好了工具参数描述模型只是正常发挥。先优化工程再升级模型这个顺序能省不少钱。我个人在实际操作里的体会是hermes-agent 这类项目真正难的不是写代码而是把“模型的不确定性”和“工程的确定性”之间的缝隙填平。状态机、消息结构、工具注册、记忆管理所有这些设计本质上都是在这个缝隙里打补丁。你打的补丁越规整Agent 就越像一个可靠的系统而不是一个偶尔灵光的玩具。如果你也正在做类似的 Agent 项目我建议你先别急着堆功能花一个下午把消息格式和主循环理清楚后面的路会顺很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价