
我最早接触 hermes-agent是去解决一个特别现实的问题手头的通知渠道一大堆邮件、IM、工单、监控告警全散着助理回个消息要来回切换五六个后台。后来试着把这套 Agent 做成一个独立的“信使中枢”把消息收拢、理解、分派、反馈串成一条自动化链路才发现事情没表面那么简单。Hermes 在神话里就是跑腿传话的这个项目名起得挺贴切——但真正跑起来之后踩过的坑也远比预想中多。如果你正准备搭一个自己的多工具 Agent或者已经卡在“接了一堆 API 却总在任务衔接上掉链子”的阶段这篇总结应该能帮上忙。我会从整体设计思路开始再到代码层面怎么组织工具调度、记忆交互、上下文管理最后聊聊那些文档里查不到的排查实录。内容偏工程实践示例代码以 Python 为准。1. 内容整体设计与思路拆解1.1 Hermes 在这个项目里代表什么先说清楚这个项目的定位。我理想中的 hermes-agent是让用户通过自然语言提一个目标然后 Agent 自己决定调用哪些工具、按什么顺序调用、拿到结果后怎么拼装成最终反馈。它更像“协调者”而不是“执行者”核心职责是理解意图、拆解步骤、调度工具、汇总结果。这个名字起得好因为它把项目最重要的特征说出来了——通传。信息要在用户、模型、工具、记忆之间来回流转任何一个环节断了整个任务就卡死。尤其是接多外部服务的时候Agent 本质上就是一套消息路由系统。你可以把它理解为公司里的前台访客说“我要找技术负责人”前台不会自己去修 bug但它知道该把这条消息转给谁、要不要预约、事后怎么回访。1.2 为什么不能一上来就上复杂框架现在社区里 Agent 框架一大堆AutoGPT、LangChain、CrewAI 都有现成的 Agent 循环。但我在设计 hermes-agent 时刻意把框架层做得很薄优先保证自己对完整流程的控制力。原因有三第一通用框架默认帮你做很多决策比如规划方式、记忆策略、工具调用格式。可实际业务里这些决策恰恰是最需要定制的地方框架封得越死后期越难调。第二框架升级带来的破坏性变更比你自己维护的代码还要频繁。尤其当你接的工具类型很杂既有 REST API 又有数据库查询的时候依赖框架的状态管理反而容易出问题。第三从零手写一个极简循环并不难核心也就是 while 循环加工具注册表。把骨架握在自己手里后续加一个工具、改一段提示词都是几分钟的事不用去翻框架的源码。所以我的建议是如果你想深入理解 Agent先徒手写一个能跑的版本再决定要不要引入现成框架。hermes-agent 的初始版本就只有三个文件一个是主循环一个是工具注册表一个是提示词模板总共不到四百行代码。1.3 这套 Agent 适合解决哪类问题经过几轮迭代我发现这类“信使型 Agent”最适合处理的是多源信息收集与结构化汇总类任务。比如每天定时从多个监控系统拉取告警按严重程度分级后汇总成一份日报。用户要求“帮我把这几个平台的工单状态整理一下顺便对比下处理时效”。在一个项目群聊里回答“上周上线版本反馈的问题都关了吗”Agent 需要去查代码仓库、工单系统、发布记录再组织语言回复。这类任务的共性是不依赖单一工具需要多次调用外部服务并且最终输出必须是统一格式的结论。让 Agent 通过自然语言去聚合这些能力能省掉大量人工 copy paste 的时间。2. 工具选型与核心技术栈2.1 为什么最终选了 FastAPI Redis 异步任务整个系统拆成三个部分API 入口层、Agent 执行层、工具适配层。执行层是核心但我把 API 入口放在最前面讲因为这是最容易搭建却最容易被忽视的部分。API 层用的是 FastAPI选它的原因很朴素自带 OpenAPI 文档调试工具调用时可以直接在 /docs 页面里手动触发省去前端联调成本。异步支持好执行 Agent 循环的时候不会阻塞并发请求。Redis 在这里不是用来做缓存的而是做任务状态暂存。因为一次 Agent 任务可能持续十几秒甚至几分钟客户端不可能一直保持 HTTP 连接等结果。我让 FastAPI 收到请求后立即返回一个 task_id后台用异步任务跑 Agent 循环再把状态和结果写进 Redis。前端通过轮询或者 WebSocket 拿最终结果。这套模型非常简单但非常稳定。执行层的核心循环代码长这样# agent_core.py class AgentCore: def __init__(self, tools: dict, llm_func, max_steps8): self.tools tools self.llm_func llm_func self.max_steps max_steps async def run(self, user_message: str, memoryNone): messages [] if memory: messages.extend(memory) messages.append({role: user, content: user_message}) for step in range(self.max_steps): response await self.llm_func(messages) messages.append({role: assistant, content: response}) action parse_action(response) if action[type] finish: return action[result] if action[type] tool_call: tool_result await self.tools[action[name]].execute(**action[args]) messages.append({ role: function, name: action[name], content: json.dumps(tool_result) }) return {error: max_steps_exceeded}2.2 工具注册表设计让新增工具像插 U 盘一样简单工具层是 Agent 的命脉。我定义了一个 BaseTool 基类每个工具只需要实现两个方法一个是描述自己“能干什么、参数是什么”的 schema另一个是 execute 方法。这个 schema 会喂给大模型模型根据它决定要不要调用、怎么填参数。class BaseTool: name: str description: str def get_schema(self) - dict: return { type: function, function: { name: self.name, description: self.description, parameters: self.parameters } } async def execute(self, **kwargs): raise NotImplementedError注册表本身就是一个 dict加载模块时自动扫描并注册。这样每加一个工具只需要新建一个类写清楚描述和参数就能立即被 Agent 使用。我实测下来工具描述怎么写直接影响模型调用工具的准确率。描述越具体模型越不容易在参数上犯错。比如“获取用户当月账单”后面要加一句“如果传入的是 user_id 而非 email需先调用邮箱转换函数”模型基本就能避开常见错误。2.3 该用函数调用还是提示词硬编码在实现工具调用时有个关键分岔路直接让模型输出 JSON 字符串还是用各家的原生 function calling 接口。我一开始用的是“提示词 正则解析”让模型输出指定格式的 JSON再用正则抽出来。优点是模型兼容性好换哪个大模型都行。缺点也很明显一旦模型返回格式稍微跑偏解析就失败整个循环卡住。后来切到了 function calling 接口实测可靠性提升非常明显。以现在的模型能力原生工具调用的成功率在九成以上而提示词硬编码方式大概在七成左右。这个差距在对稳定性要求高的生产环境里是完全不能接受的。当然也有例外。如果目标是开源通用模型并部署在本地且不能额外请求外部 API那提示词硬编码几乎是唯一的方案。这种情况下我的建议是在提示词里给出一个非常严格的 JSON 示例并限定“只输出 JSON不要包含任何其他文字”同时解析侧做好错误重试解析失败后把报错信息回传给模型让它自我纠正。3. 核心细节解析与实操要点3.1 上下文管理别一股脑把所有内容都塞给大模型Agent 循环里最容易踩的坑就是上下文长度爆炸。工具返回结果可能是一大段 JSON塞进 messages 里后越积越多不到几轮就顶到大模型上下文上限。这里我总结了三层过滤策略。第一层字段裁剪。工具返回的数据里大约一半是冗余字段在适配层就把不需要的字段剥掉只保留任务相关的。第二层摘要替换。如果工具返回结果特别长比如查出来两百条工单记录不要让模型直接看原始 JSON。让一个轻量模型先跑一遍摘要把“总共 200 条其中未解决 15 条最高优先级有 2 条集中在支付模块”这类结论喂给主模型再让主模型做决策。第三层历史压缩。超过一定轮次的对话历史需要做滚动摘要。用一个固定 prompt 把前面的内容压缩成一段 summary再拼接到后续请求里保证请求上下文始终在一个可控长度内。这三层策略分别解决不同的浪费配合起来能显著提升响应速度和稳定性。我见过很多初写 Agent 的人递归式地把所有返回都拼接进上下文结果看着好像都能跑通但实际性能非常差稍微复杂点的问题就直接炸掉。3.2 规划策略一次规划还是每步都规划关于 Agent 的任务规划方式业内有个经典之争是一次性让模型把所有步骤列出来还是每执行一步就让它重新决定下一步。我实测后更倾向于“混合模式”。简单、步骤明确的任务比如“查一下天气再告诉我”一次性规划就够省时省 token。但任务链路一旦复杂比如“拉取用户的近三十天订单统计退款率并按商品类别汇总”一次性规划经常因为信息不足而出错。原因是模型在做规划时还没看到工具返回的数据提前写出来的后续步骤往往是套模板。更好的做法是先让模型基于用户目标粗略列一个阶段计划如第一步查订单第二步统计退款第三步按类别汇总然后每执行一步结合上一步的真实返回再去细化下一步。这样既保留了计划的稳定性又允许动态调整。3.3 记忆与多轮交互短期长期分开存多轮记忆的实现我采用了短期和长期两层结构。短期记忆就是对话窗口里保留的 messages任务结束后清空。长期记忆则用 Redis 里的一张表按 user_id 为 key存用户经常问到的高频语境比如“上次那个升级任务完成了吗”。长期记忆需要被提前检索出来塞进当前上下文而不是所有历史都带上。每次对话开始先用 embedding 对当前问题做向量检索找出最相关的历史记录再作为前置上下文提供给模型。这样既不会撑爆上下文又能让 Agent 拥有“记得你”的效果。让我比较意外的是很多场景下用户并不需要精准的上下句理解只需要 Agent 记得几个高频偏好就够了。比如用户之前说过“给我看的报表默认按月维度”把这个偏好长期存住基本就能覆盖大多数后续提问的衔接需求。4. 实操过程与核心环节实现4.1 一个完整的工具接入流程从需求到上线的六步拿我最近接入“查询订单物流状态”这个工具来举例整个接入流程大概分为六步。第一步定义入参。我梳理发现订单号入参可能有三种来源用户直接给、从商品子订单推导、从外部商城系统的第三方单号查。这决定 tool 的 parameters 需要允许这三个可选字段。第二步检查数据来源。最终接的是现有订单中心 API 加一个物流公司的开放接口需要确认订单中心能不能返回物流单号缺失比例有多高。这一步如果漏了到测试阶段才发现了不少订单根本走不通。第三步写适配层代码。把内部订单查询函数和物流公司 API 封装成一个统一的物流工具内部负责单号转换、字段映射、超时重试。模型只看到“查询物流状态”这一个工具不用关心背后是两个系统在协作。第四步写工具描述。我建议描述里除了说明用途还要写明参数偏好和失败提示。比如“优先使用 order_id 查询如果该订单没有物流单号返回明确提示而非报错”。这一步直接决定了大模型能不能正确使用工具值得多花时间打磨。第五步在注册表里注册工具并跑回归。我会拿一批历史真实工单做测试集合跑一遍确保旧功能没被破坏。第六步灰度上线。先对内部用户开放观察工具调用的成功率、平均响应延时、是否有异常参数传入再逐步放量到全量用户。4.2 主循环里状态机怎么设计才不跑飞任务执行过程中Agent 的状态可以抽象为初始、工具选择中、工具执行中、结果处理中、已完成、已失败。我用一个状态字段来跟踪每次循环开始先检查状态是否允许当前动作。这里有个小细节工具执行中状态下如果外部 API 挂了或超时不能直接让整个任务失败。我的做法是给每次工具调用包一层 try-except超时或异常时把错误信息返回给模型让模型决定是换个工具还是换参数重试。模型通常能给出很聪明的应对方案比如“该订单接口超时改为用备用接口重试”。4.3 参数计算与模型选择建议模型选择直接影响 Agent 的跑通率。我把任务按难度大致分了三档简单任务单工具查询、固定格式汇总用轻量模型就够快、便宜。中等任务多工具组合、需要一定推理用中端模型兼顾质量与成本。复杂任务多轮规划、逻辑判断多、需要从工具返回中提炼结论必须用顶级模型否则很容易在中间步骤犯糊涂。从成本角度算一笔账一次复杂任务可能需要 15 轮以上模型调用每轮几千 token按顶级模型价格算大约几毛钱。如果是高频率业务场景这个成本不能忽略。所以我在架构里做了模型路由不同子任务走不同模型而不是从头到尾都用最强模型。任务理解和最终成稿用强模型工具调用的函数参数生成用中端模型摘要压缩任务用最便宜的小模型综合成本能降一半以上。4.4 提示词里那些容易被忽略的关键细节写 Agent 的主提示词和写普通问答提示词完全不一样。几个细节我深有体会。一是角色设定的“度”。如果你把 Agent 描述成“一个无所不能的智能助理”模型会倾向于在信息不足时瞎编。更好的写法是“你是系统指令的执行者请尽量使用提供的工具来回答工具没有返回结果时明确告诉用户信息不足”。二是工具列表的排序。模型选择工具时工具在列表里的位置有影响。把高频使用的工具放在最前面能提高选对概率。实测把订单查询放到第一位后相关任务的首选工具准确率提升了大约 6%。三是输出格式限定。必须明确要求“最终回答要引用工具返回的真实数据不得编造不存在的字段”。我见过不少模型拿着工具返回自己脑补出结论的场面措辞上越强硬越好。5. 常见问题与排查技巧实录5.1 工具参数乱传模型把字符串塞给整数字段这类问题出现率最高。模型在生成函数参数时偶尔会把“五个”填进一个预期是 int 的字段或者把 email 字段填成“用户邮箱”。排查时先看日志里 function_call 的 raw 参数一般能一眼看出。解决思路不是让模型“更聪明”而是在参数约束和工具内部做防御。参数规范里尽量把枚举值写全、格式写清楚比如“可选值daily / weekly / monthly”工具内部再做一次类型转换和异常提示把错误信息返回给模型。这样同一个错误第二次出现的概率就低很多。5.2 模型陷入工具调用死循环有一次任务日志里看到模型反复调用同一个工具每次参数稍微改一点但结果始终不理想最后一直循环到 max_steps 被截断。根因是模型没有“主动放弃”的意识尤其在工具返回明确报错时它能想到的应对办法只有重试。我的解法有两个一是在工具返回里加入系统级提示比如“连续两次得到相同错误时请放弃该工具并尝试其他策略”二是在主循环里做重复调用检测同一工具连续调用超过三次直接强制让模型换个方向或给出最终结论。这个策略上线后因为死循环导致的任务失败率降低了一个数量级。5.3 用户实时提问被工具返回带偏意料之外的问题有时候用户已经说了“不用查了直接把当前已知的信息告诉我”模型还是会继续调用工具。原因是当前轮的工具调用请求已经发出去来不及取消。处理办法是让主循环每一轮先判断用户最新消息里是否包含“停止”“取消”“不用”等意图如果命中直接跳过工具调用用已有信息生成结果。这种用户主动打断的场景在真实业务里比想象中频繁得多。5.4 常见问题速查症状、原因、对策症状可能原因处理对策模型不调用工具直接编答案工具描述不清晰或模型版本太弱强化系统提示词明确“不调用工具不可回答”参数格式错误频发参数 schema 约束不足完善枚举值和格式示例工具内部做兜底清洗多次重试同一失败工具缺乏放弃机制增加重试阈值和方向切换提示上下文长度持续增长未做摘要和裁剪三层过滤策略字段裁剪摘要替换历史压缩最终结果漏掉关键信息中间某步工具返回被截断记录每步返回长度超阈值时强制摘要后再拼接5.5 在线上的最大教训把工具返回原样发给用户这个教训让我印象最深。早期版本有一次 Agent 回答用户“账单是否正常”它把工具返回的一段调试级别日志直接贴了出去里面包含一个内部服务名和一段堆栈信息。用户看到后虽然没出大事但印象极差。从那之后我在最终输出前加了一个“脱敏与格式化”环节任何工具返回内容默认不可直接展示必须经过一个输出校准 prompt要求模型只提取对用户有价值的信息过滤掉内部字段和调试信息。这个环节成本很低但对专业度的提升非常明显。6. 写在最后的经验复盘6.1 Agent 系统的核心瓶颈往往不是模型而是工具质量这轮开发给我最大的认知刷新是 Agent 的智能程度并不完全取决于底层模型。如果工具返回的数据乱、字段含义模糊、接口不稳定再强的模型也白搭。反过来如果工具层做得足够干净参数清晰、错误提示友好中等规格的模型也能跑出不错的完成率。所以如果你准备做一个 Agent我真心建议把精力优先投入到工具打磨上而不是一味追最新最强模型。6.2 日志是所有排查的唯一真相Agent 循环是黑盒模型中间逻辑很难通过直接提问搞清楚。所以任务执行日志必须从一开始就设计好。每轮记录用户输入、模型输出、函数调用名、带参值、工具原始返回、截断标记。没有这套日志后面任何优化都没法做。我在这个项目里用 Redis 记原始日志、用文件记录结构化的执行轨迹排查问题时直接定位到具体某一步效率高很多。6.3 后续可以扩展的方向现在的 hermes-agent 还只支持单 Agent 顺序执行。后续我觉得可以往两个方向扩展一是多 Agent 协作不同 Agent 分管不同领域由一个协调 Agent 统一调度二是引入更好的评估体系用真实用户反馈和任务完成率来自动评估每次系统更新的效果。不过这些都是后话眼下先把已有链路做稳让 Agent 每次都能靠谱、快速、准确地完成任务才是最重要的。