Hermes-Agent:轻量级Agent核心引擎,让工具调用更简单
发布时间:2026/9/9 15:38:47来源:尧图网络
1. Hermes-Agent 是什么我为什么决定自己做一版“信使”先别急着看代码我们先聊一个很实际的问题市面上已经有了那么多 Agent 框架——有 LangChain、有 AutoGPT、有各种 XAgent——为什么还要自己写一个叫 Hermes-Agent 的东西Hermes赫尔墨斯在神话里是众神的信使负责传话、跑腿、对接各路神祇。我做这个项目的核心理念就是这个Agent 的本质不是“生成”而是“传递与执行”——把用户的一句话翻译成工具调用把工具返回的结果再翻译回人能看懂的语言。市面上很多框架把重点放在“思维链”“规划”上折腾半天 Prompt结果还是要花大量精力去处理工具接入、鉴权、重试、结果格式化这些脏活。我想要一个足够轻、足够快、足够可控的底座于是自己动手写了 Hermes-Agent。这个项目适合谁两类人第一类是刚接触 Agent 开发但被 LangChain 的抽象绕晕的新手第二类是已经在生产环境里跑过 Agent 服务、受不了现有框架“黑盒太多、定制太死”的工程师。如果你只是想三天内做一个 Demo那直接用现成框架就行但如果你想搞清楚“一个 Agent 从收到消息到最终返回结果每一步到底发生了什么、能不能换掉某个环节”那 Hermes-Agent 会是很好的参考。先说结论——这个项目不是一个重量级平台而是一个可嵌入式的 Agent 核心引擎。它把“AI 大脑”和你自己的业务代码之间的通道彻底打通你给它一个工具清单它就能根据用户请求自动选择合适的工具、生成参数、发起调用、处理异常然后把结果用自然语言回给你。整条链路是透明的每一个环节都有日志、有回调你能清楚地看到它是怎么“思考”的。2. 整体架构与设计思路为什么叫“信使”而不是“大脑”2.1 分层的核心架构Hermes-Agent 的整体架构非常简单我把它分成四层接口层负责接收用户请求支持同步HTTP和异步WebSocket/消息队列两种模式。调度层也就是 Agent 的核心循环负责意图理解、工具选择、参数生成、执行调用和结果解析。工具层统一封装各类外部能力包括自定义函数、HTTP API、数据库操作等。记忆层负责短期对话上下文和长期知识存储。这个分层的思路其实借鉴了操作系统的设计——内核态和用户态分离。调度层是内核工具层是用户态。这样带来的好处非常明显你可以随时替换任何一层而不影响其他层。比如你从 OpenAI 换到本地模型只需要改调度层里 LLM 客户端的接口工具层完全不需要动你的业务逻辑变了只需要增删工具调度层也不用动。很多框架的问题是层次太深调用链动不动就七八层抽象出了问题根本不知道在哪一层。Hermes-Agent 刻意保持了扁平化整个核心循环只有两层决定做什么决策以及做执行。2.2 为什么不选择 LangChain 这类成熟框架两年前我刚开始做 Agent 相关项目的时候也用 LangChain 搭过原型。LangChain 的优势是生态全、集成多但它的“全”恰恰是问题所在——它的核心抽象Chain、Agent、Tool相互重叠演进版本之间破坏性改动又多升级一次就要改一批代码。更关键的痛点是它把 Prompt 和逻辑深度耦合在一起你想微调某个行为模式必须理解它内部那一大堆 prompt 模板是怎么拼接的。而我想要的架构是Prompt 模板是可配置的、工具注册是一行代码的事、调用链路的每一步都可以被观测。这些需求在 LangChain 里做起来很别扭——你不是在写业务而是在不断搞懂框架本身的约定。所以 Hermes-Agent 在设计上就绕开了所有“重抽象”全部用最基础的数据结构和显式的 Python 函数实现。核心代码不到 2000 行你可以轻松读完、改完、跑起来。2.3 设计取舍同步优先、异步可选先说一个反共识的决定Hermes-Agent 默认走同步模式异步是可选能力。为什么因为大部分 Agent 的使用场景是“用户等一个结果”而不是“提交一个任务就完事”。同步模式让调试变得极其简单——你可以把核心循环当成普通函数来测试不需要事件循环、不需要 mock。当你确认链路没问题了再在外面包一层异步接口把核心循环放进线程池或者消息队列消费者里一样能达到高吞吐。这个取舍是我踩了不少坑之后总结出来的。一开始我强行上 asyncio凡事都 await结果发现真正导致性能瓶颈的根本不是 IO 并发而是 LLM 推理时间和工具调用链路的可靠性。与其把复杂度铺满整个核心不如先把单次请求的质量做扎实再用进程级并发来扛量。3. 核心模块详解从收到请求到返回结果中间发生了什么3.1 工具注册机制写一个工具 10 秒钟Hermes-Agent 最受我自己欢迎的设计就是极简的工具注册机制。核心代码只有几行agent.register_tool( nameget_weather, description获取指定城市的当前天气信息, parameters{ city: {type: string, description: 城市名称如北京}, unit: {type: string, enum: [celsius, fahrenheit], default: celsius} } ) def get_weather(city: str, unit: str celsius) - dict: # 这里写你自己的业务逻辑调用天气 API 或者查数据库 return {city: city, temperature: 23, unit: unit}设计上是纯装饰器加 docstring 风格的结构化声明完全不需要继承任何基类不依赖框架的任何内部状态。register_tool 内部做的事情很实在——把函数对象、名称、描述、参数结构登记到一个内部注册表里。在调度时框架会把这份注册表转换成模型可读的工具定义格式发给 LLM 做工具选择。极简设计是有代价的代价就是默认不做参数校验。参数合法性完全靠 LLM 生成时的自律和大模型平台侧的工具调用规范来兜底。我建议在实际业务中还是在函数内部做防御性校验毕竟让模型直接生成的参数偶尔还是会飘。3.2 调度核心循环一次用户请求的完整生命周期同步模式下核心循环长这样def run(self, user_message: str, session_id: str None): # 1. 加载或创建会话上下文 history self.memory.load(session_id) if session_id else [] # 2. 组装 Prompt系统指令 工具描述 对话历史 当前提问 prompt self.prompt_builder.build( system_promptself.system_prompt, tools_schemaself.get_tools_schema(), historyhistory, user_messageuser_message ) # 3. 调用大模型让模型决定是直接回答还是调用工具 response self.llm.chat(prompt) # 4. 如果模型要调用工具 while response.tool_calls: for tool_call in response.tool_calls: result self.execute_tool(tool_call.name, tool_call.arguments) # 把工具结果以消息形式回填给模型 response self.llm.chat( self.prompt_builder.with_tool_result(prompt, tool_call, result) ) # 5. 返回最终回答并保存上下文 self.memory.save(session_id, user_message, response.content) return response.content这段代码是整个项目的心脏所有“智能”都发生在第 3 步和第 4 步的循环里。模型看到工具清单如果判断用户问题需要查数据或者调接口就会返回一个 tool_calls 结构框架拿到这个结构后去执行对应的函数再把结果用消息回填继续让模型生成。这个过程会一直循环直到模型不再想调用工具、直接输出最终答案为止。这就是 ReActReason and Act模式的一种工程实现。但你没有看到任何花哨的 ReAct 术语和复杂状态机它就是一个 while 循环。每轮循环的 token 消耗大约是一次工具调用加一次结果回填如果需要连续调用 3 个工具就会产生 3 次往返。这也是 Agent 最常见的成本构成我在后面的费用调优里会展开讲。3.3 记忆体系短期上下文与大模型的输入预算在刚开始设计时我犯了一个很经典的错误把所有对话历史一股脑塞给 LLM。到了第 20 轮对话Prompt 里光历史就有上万字既浪费 token又把系统指令淹没了。Hermes-Agent 做了一个简单的分级策略设置一个窗口大小默认最近 10 轮对话完整保留。再往前的内容由摘要器压缩成一段 200 字以内的“过往记忆摘要”。如果超过摘要预算则丢弃最早的内容。这个策略是成本和质量之间的一个折中。从我的测试数据看窗口 10 摘要 200 字可以稳定应对大部分客服类和工具调用类场景同时单次请求的 token 消耗控制在 2000 到 3000 之内。如果你追求更长的上下文可以把这个策略换成向量检索把记忆存到向量数据库里按相关性召回。不过说实话对于工具调用型的 Agent短期窗口加摘要已经够用了长期记忆的真正价值在于积累用户的个性化偏好而不是记住每一句无关紧要的闲聊。3.4 系统提示词让 Agent 按规矩办事的底层约束我在调试初期Agent 的行为非常“飘”——该调工具时不调、不该调时乱调。后来发现问题出在系统提示词写得过于笼统。如果你想只用一句话“你是一个有用的助手”就让 Agent 正确使用几十个工具那基本是在赌大模型的理解能力。Hermes-Agent 内置了一套可替换的系统提示词模板核心逻辑就三条明确决策规则“当且仅当用户请求需要实时数据、需要操作外部系统、或需要执行计算时才调用工具。否则直接用知识回答。”参数规范“调用工具时确保每个参数都来自用户请求或上下文不要猜测缺失参数。”错误处理“当工具返回错误时不要编造成功结果。将错误信息如实反馈给用户并主动提出可能的备选方案。”这三条看着简单但实测下来非常有效。我把它们放在系统提示词的最开头并且用具体例子解释什么场景该调用什么工具。模型对“该不该调工具”的判断准确率从最初的 65% 提升到了 90% 以上。不要小看系统提示词的作用它不是在浪费时间而是在给最贵的 token 制定使用规范。4. 实操过程与核心实现从零搭一个可用的 Hermes-Agent4.1 环境准备与项目初始化先说明一下运行环境我用的是 Python 3.10、pip 管理依赖LLM 调用用的是 OpenAI 兼容接口可以换成任意本地部署的模型服务。项目本身不依赖任何重型框架唯一的硬依赖是 requests 和 pydantic。git clone https://github.com/yourname/hermes-agent.git cd hermes-agent pip install -r requirements.txt然后新建一个 config.yaml配置模型接口和 Agent 行为参数model: provider: openai base_url: https://api.openai.com/v1 api_key: ${OPENAI_API_KEY} model_name: gpt-4o-mini temperature: 0.2 agent: max_iterations: 5 history_window: 10 summary_max_chars: 200 default_timeout: 30这里要注意temperature 对工具调用的影响非常大。我自己测试temperature 超过 0.5 之后模型开始出现“脑补”参数的现象——比如用户没提供城市名它自己编一个。调到 0.2 之后这个问题几乎绝迹。在纯对话场景里温度高点没关系但凡是涉及工具调用的 Agent 场景低温永远是更稳妥的选择。max_iterations 是安全阀。防止 Agent 陷入工具调用的死循环比如调了 A 工具发现结果不对又调 B 工具还是不对再调 A……。我把它默认设为 5也就是最多允许 5 轮工具调用超过就强制终止返回当前已获取的信息。4.2 编写第一个工具并测试完整链路按照前面的 register_tool 装饰器我写了一个查询本地数据库用户信息的工具这是 Agent 场景里最常见的需求——把自然语言翻译成 SQL 查询。import sqlite3 from hermes_agent import Agent agent Agent.from_config(config.yaml) agent.register_tool( namequery_user_info, description根据用户姓名查询用户的注册信息包括邮箱、手机号和注册日期。, parameters{ name: {type: string, description: 用户的完整姓名} } ) def query_user_info(name: str) - dict: conn sqlite3.connect(app.db) cursor conn.cursor() cursor.execute( SELECT email, phone, register_date FROM users WHERE name ?, (name,) ) row cursor.fetchone() conn.close() if row: return {found: True, email: row[0], phone: row[1], register_date: row[2]} return {found: False, message: f未找到用户: {name}}接下来模拟用户请求“帮我查一下李明的邮箱和手机号是什么。”试一下agent.run(帮我查一下李明的邮箱和手机号是什么。)返回结果大概是根据数据库记录李明的注册信息如下 - 邮箱limingexample.com - 手机号138-0000-1234整个链路执行了LLM 识别意图 - 生成 query_user_info 调用参数 {name: 李明} - 执行函数 - 数据库返回结果 - LLM 组织成自然语言。这个过程中 agent.run() 内部会打印每一步的日志包括模型原始返回、工具调用名称和参数、工具返回结果方便追踪问题。我强烈建议你在自己的场景里也先写一个最小的工具跑通全链路后再加复杂度。因为你真正需要验证的不是“模型能不能理解工具描述”而是“你的工具返回结构是否适合模型改写”。常见的问题是工具返回一个巨大的 JSON光这一坨就占了大量 token模型反而抓不住重点。技巧是工具返回时只返回必需字段把冗长的细节放到一个 summary 字段里。4.3 工具返回结果的结构化设计工具返回结果的结构会直接影响最终回答的质量和 token 消耗。我总结了一套比较实用的返回规范成功与失败用显式字段标识{success: true, data: ...}或{success: false, error: ...}data 里只放核心字段不要整个数据库行无脑返回时间、金额等需要展示的数据在工具里就格式化好别让模型自己换算返回结果控制在 200 到 400 字以内超出部分提前截断比如查询订单列表千万不要把原始数据库的几十个字段全返回只返回用户真正关心的订单号、商品名、金额、状态。模型不需要知道内部 id、逻辑删除标记这些东西给得越多它越容易挑花眼。4.4 错误处理与异常兜底Agent 跑在生产环境工具调用失败是家常便饭。API 超时、数据库锁死、返回空数据每种情况都要处理否则 Agent 就会“一本正经地胡说八道”。我在 Hermes-Agent 里做了三层兜底工具执行层工具函数内部必须 try-except任何异常都要转成错误描述返回确保不会把堆栈抛给模型。调度层如果某次工具调用的结果明确是失败框架会在回填给模型时附上一条系统级别的提示“该工具调用失败请根据错误信息决定是重新调用、换一个工具还是直接告知用户。”循环终止超过 max_iterations 之后强制返回最后一条结果并提示用户“操作未完成建议重试”。举个例子如果数据库服务宕机了错误返回如下{success: false, error: DATABASE_TIMEOUT: query_user_info 执行超时(30s)}模型看到这个反馈后会自动生成类似“抱歉查询用户信息的服务当前超时了请稍后再试”的回答而不是假装查到了用户数据。这层设计非常重要——诚实应答比自作聪明更能赢得用户信任。5. 工具选型与配置调优我觉得值得展开聊的几个点5.1 LLM 模型的选择对 Agent 能力上限的决定性影响Agent 的能力其实是被模型决定的框架只负责把事情变简单。我在实际测试中发现Hermes-Agent 在 gpt-4o-mini 上能达到约 90% 的工具调用准确率但在更小参数的模型比如 7B-13B 级别的开源模型上面准确率会掉到 60% 左右。原因很典型小模型在“根据工具描述生成参数”这个任务上容易漏参数、多参数、参数名写错。如果要用开源本地模型我建议优先选择在函数调用数据集上做过专项微调的版本比如 Qwen2.5 系列、GLM-4 系列。同样一个 prompt微调过函数调用能力的模型和没微调过的通用对话模型工具调用的稳定性差出一个数量级。另外我用的是 OpenAI 兼容接口的 base_url 配置这样只要你的本地模型服务vLLM、Ollama 等暴露了 /v1/chat/completions 接口就能无缝切换。切换底层模型之后一定要重新跑一遍工具调用回归测试不要天真地以为换了个模型就能保持同样的行为。5.2 Prompt 模板里的工具描述怎么写模型才爱看工具描述写得好不好直接决定模型在多个工具之间决策的正确率。我踩过很多坑总结出几条规则名称要像函数名一样语义化get_user_info 比 method1 好一万倍。description 写“什么时候该用”而不是“这个工具是什么”比如“当用户询问某个用户的联系方式或注册信息时使用”这种条件式描述比“查询用户信息”有效得多。参数名必须千锤百炼模型生成的参数名完全参照你提供的参数名所以如果你用 name就不要指望它生成 user_name。从根源上避免歧义。枚举值要写完整如果你的参数只有固定几种取值务必在描述里写清楚或者直接在装饰器的 parameters 里声明 enum。我见过很多同学在工具描述里瞎写长句结果模型根本抓不住重点。最有效的格式是名称用动词开头description 写一句典型触发场景parameters 用 JSON Schema 标准格式。这套组合实测在各种模型上兼容性都很好。5.3 token 消耗与延迟的平衡策略Agent 跟普通聊天的最大不同是它不是一次调用就结束而是多次往返。每次往返都要重新发送系统提示词、工具描述、历史对话、工具结果。我做个粗略估算如果你注册了 20 个工具工具描述这一块就可能占 1500 到 2000 token历史对话 10 轮算 1500 token再加上工具结果回填单次完整请求平均消耗可能到 5000 到 8000 token。控制成本的策略有三个方向减少工具描述体积只给模型发送“当前场景可能用到的工具”而不是全部工具。Hermes-Agent 支持按 session/场景给工具分组按需挂载。压缩历史窗口窗口设 5 轮而不是 10 轮配合摘要策略能把历史 token 砍掉一半。用便宜的模型做工具调用用贵的模型做最终回答这是进阶玩法。第一轮工具决策用 fast-llm拿到工具结果之后如果发现工具结果需要复杂推理再用更强的模型生成最终回答。不过这个方案目前需要你自行在业务侧实现我暂时还没有把它合并进核心代码。延迟方面gpt-4o-mini 单次调用大约 1 到 2 秒一轮工具调用加上一次最终生成总延迟大约 3 到 5 秒。如果超过 5 秒用户就会觉得卡。实测下来换更大的模型比如 gpt-4o后工具调用准确率有提升但延迟会翻倍性价比不高。所以如果只是做工具调用mini 级别的模型已经足够。5.4 多工具协作当一个任务需要连续调用好几个工具Hermes-Agent 天然支持多工具协作因为它的核心循环就是循环调用直到模型认为任务完成。我做一个电商客服场景的示例用户问“我最近买的 iPhone 手机壳发货了吗”这个请求至少需要两个工具get_user_orders 拿订单列表以及 get_order_logistics 查物流信息。agent.run(我最近买的 iPhone 手机壳发货了吗)执行过程是第一轮模型调用 get_user_orders 获取订单列表框架把订单结果回填给模型模型对比订单里的商品名包含“iPhone 手机壳”然后调用 get_order_logistics 查询对应订单的物流状态物流结果回填后模型生成最终回答。整个链路是透明的你可以从日志里看到每一次模型选择的工具。这种多步调用对模型的推理能力要求比较高尤其是“从上一个工具的大量返回结果中提取关键信息作为下一个工具的入参”这一步。我在测试中发现如果第一步返回的订单列表特别长模型可能会选错订单号。这时候有两个招一是让工具返回时自动过滤只返回最近一个订单二是在工具描述里明确写“请提取订单号作为参数”。总之不要把复杂的筛选逻辑留给模型能在这工具函数里做掉的就提前做掉。6. 常见问题与排查技巧实录我这几个月排过的雷6.1 模型比想象中“笨”工具调用链路失效的排查顺序如果你发现 Agent 回答得不知所云或者干脆不调用工具先别急着怀疑模型笨按下面顺序排查一遍90% 的问题都出在这些地方工具描述是不是太绕了把你写的 description 拿给一个没有背景知识的同事看看他能不能看懂什么时候该用。看不懂就重写。工具的 JSON Schema 是不是格式有问题用一段独立脚本先做验证确认 schema 能被模型 API 正确识别。是不是系统提示词的锅把系统提示词换成最简单的“你是一个助手”只保留工具描述看模型能否正确调用工具。能的话就一点点把你的系统提示词加回来找到干扰项。检查日志里的原始模型返回看模型是返回了 tool_calls 字段还是返回了空内容。如果返回空多半是 API 层的参数封装有问题。这个排查顺序我建议直接固化到自己的检查清单里每次遇到“模型不调用工具”的问题从上往下扫一遍基本十五分钟之内能定位。6.2 参数幻觉模型编造了一个用户没提供的参数怎么办这是我被问得最多的问题。用户问“帮我查一下订单”但订单查询需要订单号模型就自己编了一个“ORD-12345”。处理办法有三种Prompt 层面在系统提示词里明确写“如果用户请求中缺少必须参数主动询问用户补充不要猜测”。我在前面已经提过这条简单有效。工具设计层面把必填参数设计成默认值加兜底逻辑。比如订单号缺失时工具内部改为查询“最近一个订单”。这是一种为了保住核心体验的妥协。结果校验层面工具函数里对参数做合法性检查例如订单号格式正则校验不合法直接返回参数错误。通过报错驱动模型重新询问用户。我个人的偏好是组合使用第一和第三种既不强迫工具去猜也给模型一个明确的错误反馈循环。这套机制跑了两周之后“参数幻觉”出现的概率降到了非常低的水平。6.3 工具返回值太大导致上下文爆炸有朋友拿 Hermes-Agent 去做数据分析报表场景一个查询返回了几万行数据结果把模型输入窗口塞爆了。这类问题的通用解法是分页 摘要。工具函数支持 limit 和 offset 参数默认只返回前 20 条结果并在返回结构里带上 total 字段。如果 total 大于 20模型会给出提示“共找到 X 条记录已展示前 20 条如需更多请提供查询条件”。然后用户自然会继续追问。在必要的时候还可以加一段预先聚合的统计信息比如总金额、平均值让模型有全局感。切忌把海量原始数据扔给模型让它自己分析这是最贵、最容易出错的做法。工具的价值在于把不可控的原始数据清洗成模型最容易消化的结构化摘要。6.4 常见问题速查表为了方便你快速定位问题我整理了一份高频问题对照表现象可能原因排查建议模型从不调用工具工具描述不清晰或系统提示词过度约束简化提示词单独测试工具描述模型总是调用同一个工具工具描述里触发条件写得太宽泛增加条件限定词缩小触发范围工具参数提示缺失或错误模型没理解参数来源在参数描述中写明“从用户请求的第 X 句话提取”工具调用到了但返回结果没用上工具返回值结构太复杂精简返回字段加入 summary 字段循环调用停不下来缺少终止条件或模型判断力弱降 temperature调低 max_iterations最终回答用工具结果编造系统提示词没强调诚实应答增加错误处理提示词和工具失败标识并发请求时上下文串了session_id 没有正确传递检查每次调用是否都带了独立 session_id7. 我个人的一些“私藏”经验和后续扩展想法开发 Hermes-Agent 这段时间我最深的体会是Agent 真正难的地方不是智能而是工程。你可以用非常简单的 while 循环把 ReAct 模式跑通但要让这套系统稳定地跑在线上要考虑的问题多到超出想象——工具失败怎么兜、token 怎么省、上下文怎么管、并发怎么控、日志怎么打。这些都不是“模型能力”能解决的而是纯粹的工程问题。有一个小技巧想分享在开发阶段给 Agent 加一个“解释模式”。也就是让模型在每个决策步骤后面额外输出一行简短的原因说明。虽然这会增加一点 token 开销但对你理解模型行为和定位问题帮助巨大。不需要在生产环境开启开发调试时开一下就够了。后续我准备给 Hermes-Agent 加两个能力一个是人机确认机制当 Agent 要执行高风险操作比如删除数据、发送消息之前先暂停并请求用户确认另一个是工具调用结果缓存对同样的参数返回同样的结果这种场景直接命中缓存省掉一次工具执行和一次 LLM 往返。这两个能力其实都是生产环境里非常刚需的如果你现在就有类似的需求完全可以自己在现有架构上扩展——核心思路我已经在这篇文章里讲透了。
网站建设高端定制企业官网