很多人都觉得只要把大模型 API 一接再丢给它几个工具函数一个 AI 智能体就算做完了。可真到了实际项目里你会发现 prompt 写得再花哨只要工具一多、任务一长agent 就开始“胡言乱语”要么调错工具要么绕来绕去就是不肯收尾。这也是为什么我折腾了大半年之后最终把自己的智能体架构收敛成了一个叫hermes-agent的项目。名字借用了希腊神话里那个跑得飞快、负责传递信息的信使神说白了就是想让智能体把“理解请求、调度工具、汇总结果”这件事干得像个靠谱传话人一样利落。hermes-agent不是一个颠覆式的“新框架”而是一整套我自己在生产环境里反复打磨过的智能体骨架。它核心解决的是三个问题工具多了怎么不乱、任务长了怎么不丢上下文、出了问题怎么快速定位。适合正在自己搭智能体、或者想从“demo 玩具”走向“可维护项目”的开发者参考。这篇文章会把设计思路、核心模块、关键代码、常见坑从头到尾讲清楚。1. 为什么叫 hermes-agent定位与设计思路1.1 名字背后的定位信使神与智能体路由层在希腊神话里赫尔墨斯Hermes是奥林匹斯山的信使负责在众神之间传递消息、引导灵魂、调解纠纷。这个名字放在 agent 项目里其实特别贴切一个智能体本质上就是大模型和外部世界之间的信使——模型负责思考和生成agent 负责把模型的想法变成真实的动作再把动作的结果带回来。我当时给这个项目起名就是想强调一个容易被忽视的定位agent 不该是一个什么都往里塞的“万能程序”而应该是一层薄薄的路由和调度层。它不需要知道天气 API 内部怎么实现也不需要懂数据库表结构它只需要做到三件事听懂用户要什么、找到能干的工具、把结果整理成人话。这个定位听起来简单实际做起来却很容易走偏。很多人一开始就往 agent 里塞记忆模块、多智能体协议、向量数据库结果项目越做越重反而连最基础的“查个天气再算个乘法”都跑不顺。hermes-agent的初始设计原则特别朴素先解决“路由精度”再谈扩展能力。1.2 和 LangChain / AutoGPT 这类框架有什么不一样现在市面上的 agent 框架很多LangChain、AutoGPT、BabyAGI 各有拥趸。但说句实话这些框架在 demo 里都惊艳一上真实业务就开始暴露问题LangChain 抽象层太厚出了问题追到源码里要翻好几层AutoGPT 太“放飞”任务稍微复杂一点就开始自我发挥不可控。hermes-agent跟他们最大的区别是我刻意做了减法。不去发明复杂的 agent 协议就用最经典的 模型-工具-记忆 三角结构。不追求全自动每一步执行都有可观测的日志和中断点方便人工介入。不把编排逻辑藏进框架执行循环就是一段能读懂的 Python 代码谁接手都能快速上手改。有人可能会问这样不是“开倒车”吗智能体不就应该越自动越好吗我的观点是自动化的前提是可控。一个每十次就有一次会调错工具的 agent就已经不具备无人值守的资格。与其给它更多自主权不如先把路由和调度打磨到 99% 的准确率。这也是hermes-agent和那些“重量级框架”最大的理念分叉。2. 老生常谈但必须拆细智能体最核心的四个模块2.1 模型接入层所有对话的“大脑皮层”模型接入层是整个 agent 的地基。这个模块做的事情看起来简单——把用户请求发给大模型拿到回复——但实际设计的时候要回答几个问题支持哪些模型多模型之间怎么切换模型返回的格式不稳定怎么办hermes-agent在这一层做了一个非常朴素的抽象不管底层是哪个厂家的模型对外只暴露一个chat(messages, tools)接口。内部把各个模型厂商的 API 封装成统一的请求格式同时在配置里留好model_provider字段方便随时切换。这里有一个真实的经验教训永远不要信任模型返回的 JSON 格式。不管是再强的模型都有概率在输出里多一个注释、少一个引号、甚至突然开始“自言自语”。所以我在模型接入层做了三层防护第一层是让模型强制以 JSON 输出第二层是对返回结果做“提取式解析”用正则把最像 JSON 的部分抓出来第三层是解析失败后自动重试一次。这三层下来格式异常的几率从最初的 10% 降到了 1% 以下。2.2 工具注册与调用给 agent 一把把“趁手的兵器”工具层是hermes-agent里迭代次数最多、也最值得讲的一个模块。它的核心职责有两个让模型知道有哪些工具可用以及让模型学会正确地调用它们。很多教程会教你用一个大数组把所有工具描述丢给模型然后让模型自己选。这在工具少于五个的时候确实没问题但工具一多——比如我自己的项目里挂了二十多个工具——模型就开始“选择困难”了。不是把参数填错就是选了不是最优的那个工具。hermes-agent的工具层为此做了一件事给每个工具都绑定一个“路由规则”。每个工具在注册时除了提供名字、描述、参数 schema还要提供一个routing_keywords字段用来告诉 agent“这个工具适合什么场景”。比如天气查询工具会绑定“天气、气温、下雨、PM2.5”计算器工具会绑定“计算、加减乘除、算术”。模型在收到请求后系统 prompt 里会明确要求它先根据routing_keywords做一次粗筛再在候选列表里精确选择。这一招的效果立竿见影。工具数量从 5 个增加到 20 个之后调用准确率不仅没有下降反而因为召回范围变小而提升了。2.3 记忆与上下文管理不要让对话变成“金鱼记忆”做过 agent 的人都有一个直觉上下文越长模型表现越差成本还越高。但完全不要记忆agent 就是一个没有灵魂的问答机器用户说了“帮我查下刚才那个”它就懵了。hermes-agent的记忆模块采用了一个非常实用的分层策略工作记忆 滚动摘要。工作记忆就是最近几轮对话的完整消息保持在上下文窗口内滚动摘要是对更早对话的压缩总结每隔几轮触发一次由模型自己把之前的聊天记录整理成摘要。这两层记忆合在一起既保证 agent 不会忘记关键信息又不会让上下文无限膨胀。这部分还有一个很容易被忽略的细节记忆不只是存对话记录还要存“事实”和“状态”。比如用户说了一个重要的偏好“我平时都在上海办公”或者一个任务执行到一半“刚才查到的那个数据还没用上”这些信息如果只是躺在聊天记录里模型不一定能准确回忆出来。我后来单独加了一个fact_store把模型从对话中抽取出的关键事实存成结构化表单每次请求时自动注入到系统提示里。这个改动把多轮交互中的“记忆相关错误”减少了大约一半。2.4 执行循环与任务编排agent 的心脏跳动方式执行循环是 agent 的“主循环”也是hermes-agent里最核心的一段代码。它的工作流大致是这样接收用户请求组装 messages。把 messages 发给模型带上工具列表。模型返回两种可能直接回答或者请求调用某个工具。如果是工具调用就执行工具把结果追加到 messages 里回到第 2 步。如果模型给出最终回答就返回给用户结束。这个循环看起来简单真正写起来有很多细节要考虑最大轮数限制设多少合适工具调用报错了怎么反馈给模型模型连续调用同一个工具三次以上是不是死循环hermes-agent的做法是默认最大轮数设为 8超过即中断并提示用户“任务复杂度超出限制”工具调用失败时会把错误信息格式化后追加到上下文里并且要求模型“换一种方式实现同一个目标”而不是让模型重复报错。这些细节单独看都不起眼合在一起才让整个循环真正“稳”。3. 手把手搭一个 hermes-agent关键代码与配置3.1 最小可运行的骨架麻雀虽小五脏俱全与其空谈设计不如直接看代码。hermes-agent的最小骨架我用 Python 写出来大概是这样的感觉from dataclasses import dataclass, field from typing import Callable, Any, Optional dataclass class Tool: name: str description: str parameters: dict function: Callable[..., Any] routing_keywords: list[str] field(default_factorylist) class ToolRegistry: def __init__(self): self._tools {} def register(self, tool: Tool): self._tools[tool.name] tool def list_tools(self) - list[dict]: return [ { name: t.name, description: t.description, parameters: t.parameters, } for t in self._tools.values() ] def get(self, name: str) - Optional[Tool]: return self._tools.get(name)这看着非常简单但它已经是整个工具系统的“地基”。ToolRegistry 的职责非常单一注册、列出、按名字取。后面所有复杂的设计——路由、校验、容错——都是在这个基础上长出来的。具体的执行循环核心逻辑也不复杂大概长这样def run_agent(user_input: str, registry: ToolRegistry, max_rounds: int 8): messages [{role: user, content: user_input}] tools registry.list_tools() for step in range(max_rounds): response chat_completion(messages, toolstools) msg response.choices[0].message if msg.tool_calls: messages.append(msg) for call in msg.tool_calls: tool registry.get(call.function.name) if not tool: messages.append({ role: tool, tool_call_id: call.id, content: f工具 {call.function.name} 不存在, }) continue try: result tool.function(**json.loads(call.function.arguments)) except Exception as e: result f工具执行出错: {e} messages.append({ role: tool, tool_call_id: call.id, content: str(result), }) else: return msg.content return 任务太复杂了请拆分成多步操作后再试。这个循环虽然短但已经是hermes-agent的核心。每次工具调用后结果会以tool消息追加回上下文模型就能基于真实执行结果继续思考而不是凭空猜测。3.2 工具注册的规范参数 schema 写得好不好直接决定调用准不准在hermes-agent里工具注册是一个“写文档”的过程你必须把工具的参数定义成 JSON Schema模型才能理解怎么调用。而这一步恰恰是大多数人做得最敷衍的。举一个我实际踩过的例子。我给一个“发送邮件”工具注册时参数 schema 里to字段只写了“收件人”没写格式。结果模型调用的时候直接把一个数组传给了to导致校验失败。后来我把描述改成“收件人邮箱地址如果是多个收件人请用逗号分隔拼成一个字符串”模型就再也没犯过这个错。工具描述这件事本质上是写给模型看的不是写给程序看的。hermes-agent对这个规范做了两条硬性要求每个参数的 description 都必须回答“我应该怎么填”而不是“这个参数是什么”。所有枚举值、单位、格式都要写清楚模型不会“猜”你的意思。靠近这两条工具调用成功率会肉眼可见地上升。这也是我反复跟身边的人强调的如果你的 agent 工具调用总出错检查一下是不是工具描述写得太“人类”了——那个只有人类才懂的“你懂的”模型真的不懂。3.3 请求处理流程从用户输入到最终输出的完整链路如果把hermes-agent的一次请求全过程画出来大概是这样的流程文字版预处理检查输入是否包含攻击性内容过滤异常符号提取用户 ID 和会话 ID。上下文组装从记忆模块拉取当前会话的工作记忆和滚动摘要再带上fact_store里的关键事实一起塞进系统提示。工具粗筛根据系统提示里的路由规则和当前用户输入从注册表里筛出候选工具子集。模型推理把完整上下文和候选工具列表发给模型拿到回复。动作执行如果模型要求调用工具就按第 3 节里的循环执行否则直接输出最终结果。记忆更新把这一轮对话写入工作记忆如果触发条件就启动摘要压缩。日志记录把这次请求的完整耗时、令牌消耗、工具调用链写入结构化日志。这 7 个环节任何一个做不好都会直接影响用户体验。比如记忆更新不及时用户会察觉 agent“忘了事”日志记录不全出了问题只能靠猜。在实际项目里我几乎从不在原有链路里新增“一次性”逻辑而是坚持所有能力都拆成模块再接入主流程。这样做的收益是主流程永远保持清晰任何环节出了问题都能单独降级或关闭而不是整个 agent 一起崩溃。4. 实操过程让它真正跑起来4.1 从零到一环境准备与依赖清单hermes-agent的依赖非常轻核心只有一个openai风格的 SDK用来调大模型外加一个pydantic用来做参数校验。如果你打算接国内的大模型也只需要把 base_url 换一下就行其他逻辑不变。建议的安装方式pip install openai pydantic配置方面我用一个 YAML 文件来管理所有模型和工具相关设置model: provider: openai name: gpt-4o-mini temperature: 0.2 agent: max_rounds: 8 memory_window: 10 summary_trigger: 15 tools: - name: get_weather enabled: true - name: calculator enabled: true这里把temperature设成 0.2是因为 agent 工具调用场景需要的是确定性而不是创造性。温度太高模型会开始乱发挥太低模型可能变得死板连多义词都分不清。实测下来 0.1 到 0.3 之间是工具调用场景的“甜区”。4.2 典型场景演练查天气、做计算、查文档的三合一光看不练没意思我用hermes-agent跑一个典型的多工具场景用户先问“上海今天要去见客户需要带伞吗”紧接着又说“顺便帮我算一下打车到徐家汇大概多少钱距离按 8 公里算”。这个请求涉及三个能力天气查询、位置理解、计算。hermes-agent的处理过程大致是这样第一轮模型识别出用户想查天气于是调用get_weather(city上海, date今天)。天气接口返回“阴转小雨降水概率 70%”。第二轮模型看到要下雨的结论结合用户“带伞吗”的提问正准备组织回答。但用户又追加了“打车价格”的请求所以模型继续调用calculator(expression8 * 2.5)其中 2.5 是当地出租车每公里报价这个报价来自另一个工具先查了计价规则。第三轮模型拿到天气和计算两个结果后最终给出完整回复“今天上海有小雨建议带伞。打车到徐家汇按 8 公里计算大约 20 元左右但下雨天可能有溢价。”在这个例子里要注意一个关键点一次请求里模型可以连续调用多个工具只要它觉得有必要。hermes-agent的执行循环天然支持多步调用每个中间结果都会作为上下文继续给到模型直到模型认为信息充分给出最终回答。4.3 性能与成本的控制手段别让你的账单“飞上天”智能体项目的一大痛点就是成本不可控。一个复杂任务跑下来可能要调用几十次模型每次都要把大量上下文重发一遍token 消耗十分可观。hermes-agent在控制成本方面做了三个务实的设计工具粗筛后只发送候选工具的描述而不是把全部二十个工具一股脑塞进系统提示每条工具描述省下的 token 虽然不多但累积起来很可观。记忆摘要压缩把早期对话转换成摘要避免上下文无限增长。执行轮数上限默认 8 轮。如果模型 8 轮还搞不定说明这个任务超出了当前能力边界继续跑纯属烧钱。另外还有一个容易忽视的点工具返回的内容本身也会占上下文。如果一个工具返回了一个一万字的文档然后模型只用了其中一句话剩下九千多字都是浪费。所以我给工具层加了一个max_output_length自动截断逻辑超出部分直接裁掉因为被截断的工具结果通常不会影响最终回答质量。这三招叠加起来我的智能体在同等任务量下月均模型调用成本大约下降了 40%。控制成本这件事与其等账单炸了再优化不如在设计时就提前留好“节流阀”。5. 常见问题与排查技巧实录5.1 工具调用失灵不是模型变笨了而是上下文脏了我调试hermes-agent的时候遇到最多的一类问题就是“模型突然开始调用不存在的工具”。最开始我以为是模型理解能力不行后来发现真正的原因是上下文里残留了旧错误。比如某次工具调用因为网络超时报错报错信息被写回上下文模型看到了就会在后续轮次里反复尝试同一个错误工具。这就是所谓的“上下文污染”。解决办法有两个每次工具报错后在上下文中追加一句提示“之前的工具调用失败了请换一种方式不要重复相同的调用。”这能有效阻止模型钻牛角尖。严格控制工具列表的更新方式只有在模型应该看到新工具时才把新工具追加到列表里避免模型引用已经“下架”的工具。本质上模型没有人类那样的“清零能力”它会带着之前的信息继续思考。所以 agent 框架要主动帮助模型随时“纠偏”而不是指望它自己发现问题。5.2 上下文爆炸为什么 agent 越跑越慢、越跑越贵上下文爆炸可能是所有 agent 项目里最普遍的问题。症状是任务刚开始很流畅几轮对话之后模型的响应开始变慢甚至开始丢三落四。原因其实很好理解——每次请求都要把所有历史消息重新发送一遍。对话越长每次消耗的 token 就越多模型处理时间也越长。hermes-agent的记忆模块就是为这个问题准备的。我在前面提到过“工作记忆 滚动摘要”这里具体展开一下参数设置memory_window10保留最近 10 轮完整对话。summary_trigger15当累计对话超过 15 轮时触发一次摘要压缩。摘要本身存放为一条system消息放在消息队列最前面。这个策略能保证上下文长度始终维持在一个稳定区间不会无限膨胀。但也要注意摘要压缩是有损的假如早期对话里有某个关键事实被遗漏后面 agent 就可能“失忆”。所以我后来把“关键事实抽取”单独抽出来做成fact_store相当于给 agent 配了一个“重点笔记”即使摘要丢失细节笔记还在。5.3 工具参数校验失败的三种典型场景工具参数校验失败几乎是 agent 项目里最让人抓狂的错误。明明工具列表就在那里模型却总是把参数填错。hermes-agent的实践中我总结出三个高频场景字符串枚举出错模型在city字段里填了一个 “Shanghai City”而接口期望的是 “Shanghai”。这时候你需要在参数描述里把合法值尽量列全干脆写成“请使用城市代码例如 shanghai、beijing、guangzhou”并且去做归一化。缺失必填参数模型漏掉了某些必填项。我采用的办法是在parameters的required列表里明确标记并且在调用工具前做一次本地校验缺参就直接返回友好错误不调工具。数组和对象的嵌套格式错模型把tags数组写成了逗号分隔字符串。这个问题靠描述很难彻底解决我最后是用一个“修复层”调用工具前先把参数 JSON 解析出来然后按照 schema 做一次类型强制转换。这个“修复层”是我非常推荐大家尝试的思路。与其指望模型每次都能完美输出不如在工具入口处加一道“清洗工序”把模型偶尔的小毛病驯化掉。5.4 可观测性怎么补没有日志排查问题等于大海捞针开发智能体项目的时候最痛苦的事情就是“我明明给了答案为什么不按我的想法来”。靠肉眼看 LLM 的回答、靠猜 prompt 哪里有问题效率极低。必须有结构化的日志。hermes-agent里每个请求我都会记录这几个字段存在本地文件或日志平台字段说明request_id一次请求的唯一标识session_id会话 ID用于关联多轮对话steps每一轮模型调用的完整轨迹tool_calls本次请求触发了哪些工具入参出参是什么token_usage输入输出 token 数及总量latency_per_step每一步的耗时明细error_info如果出错记录是模型异常、工具异常还是超时有了这份日志之后绝大多数问题都能“一看定位”。比如用户投诉“agent 答非所问”打开日志一看原来是系统提示里混进了一段无关的历史对话再比如“计算错误”看日志发现模型把 8 公里乘成了 8.5。没有日志这些问题就只能靠复现而复现 LLM 的随机性几乎不可能。6. 写在最后的一点个人体会hermes-agent从最初的一个玩具脚本到现在能稳定跑在生产环境里中间踩过的坑比我预想的多得多。最大的体会是智能体项目最难的从来不是模型能力而是工程化。模型输出的不确定性、工具调用的边界情况、上下文的膨胀这些东西不自己动手跑一遍光看文档永远感受不到。另外一个让我印象很深的经验是别把所有希望都寄托在“更强的模型”上。很多人觉得调用 GPT-5 之后agent 就能自动变聪明。但实测下来更强的模型确实能减少一部分工具调用错误可它依然会犯“参数填错”“上下文污染”这类基础错误。这些问题的解法仍然要靠框架层面的设计去兜底。所以如果你也在折腾自己的智能体我建议你从小而稳入手先把一条工具调用链路跑通再逐步加记忆、加路由、加多智能体。别一开始就追求“大而全”那只会让你连“哪里坏了”都查不清楚。hermes-agent这个项目本质上就是我在一次次“哪里坏了”的排查中慢慢长出来的答案。