☰
Hermes-Agent源码深度解析:架构分层、核心流程与二次开发实践
2026/10/17 5:58:54 网站建设 项目流程

上周把 Hermes-Agent 的源码从头到尾完整过了一遍,好几处设计让我有一种“原来是这么回事”的感觉。这个项目不是那种简单的 Demo 级 Agent,而是一个把大模型编排、工具调用、记忆管理、事件驱动全部串在一起的完整框架。这篇文章我不打算做成官方文档的翻译件,而是想站在一个准备拿它做二次开发的人的角度,把架构分层、核心流程、关键模块,以及真正动手改代码时最容易踩坑的地方一次性讲透。

不管你之前有没有做过 Agent 开发,看完这篇文章之后,你应该能回答这样几个问题:Agent 框架到底在解决什么问题?任务从进来到最终返回结果,中间经历了什么?如果要给它新增一个工具、换一个大模型、改一套规划策略,代码应该从哪里入手?如果这些问题正好也是你想搞清楚的,这篇源码解析就是冲着你写的。

1. 这个项目到底做了什么?先看懂它的定位与架构分层

1.1 Agent 框架解决的是“大脑没有手脚”的问题

先说一个很基础的判断:大模型本身是一个“大脑”,但它没有手脚。它可以在对话里说得头头是道,却没办法真正去查询数据库、调用接口、读写文件、操作浏览器。Agent 框架存在的意义,就是给这个大模型装上手脚,并且设计一套神经系统去控制手脚。

这个思路说起来简单,真去实现的时候才会发现问题。假如用户提了一个任务:“帮我查一下模拟项目 X 的订单数量”,模型只凭训练记忆是不可能知道这个数字的,它必须被允许去调用一个“查询订单”的工具。但这就引出一连串问题:模型怎么知道该调用哪个工具?调用时参数从哪来?工具返回的结果要怎么喂回去?如果工具报错了,是终止还是换一种方式再试?整个循环什么时候才算结束?

所以一个 Agent 框架的源码,本质上就是在编排三样东西:任务目标、可用工具、当前状态。Hermes-Agent 的代码里最厚、最绕的部分,几乎全都在处理这三者之间的关系。如果你只是简单地把大模型 API 和几个 Python 函数拼在一起,那根本不叫 Agent,那叫“带函数调用的聊天机器人”。Agent 和聊天机器人之间真正的分水岭,就是有没有那一套“观察—思考—行动—反馈”的循环机制。

1.2 分层架构:每层管好自己的事

读完整个项目的源码结构,我第一感受是它的目录划分非常规整。虽然不同版本的代码细节有差异,但大体上可以分成四层。

第一层是接入层,负责接收外部请求,做参数校验、会话创建,然后把任务交给核心层。这一层通常很薄,主要做一些 HTTP 接口或者 CLI 命令的适配。第二层是核心层,这是整个框架的心脏,里面运行着 Agent 的主循环、状态机、任务调度逻辑。第三层是能力层,包括大模型 Provider、工具 Provider、记忆 Provider,它们通过抽象接口挂在核心层下面。第四层是基础设施层,包括配置管理、日志系统、事件总线、存储组件。

这样的分层带来的最大好处是替换成本极低。你今天用商业大模型 API,明天想换成某个本地开源的模型,只需要在 Provider 层面做适配,核心循环完全不用动。你想新增一个工具,也只需要在工具层注册一个函数,Agent 的逻辑不会因为你多了一个工具而需要改代码。

我用一个类比来解释这个架构:把 Agent 框架看成一个餐厅。核心层是店长,负责整体调度;大模型是主厨的大脑,负责判断该做什么菜;工具是后厨的设备,负责把菜做出来;记忆模块是食材仓库,记录着客人喜欢什么口味。你二次开发的时候,通常只需要换设备或者给仓库加货,不会动不动就把店长撤掉重招。

层与层之间的调用关系也非常克制,核心层不直接依赖某个具体的大模型 SDK,也不直接调用你的业务函数,它依赖的是抽象接口。源码里大量出现 BaseProvider、BaseTool、BaseMemory 这样带 Base 前缀的类,本质上就是在为扩展留口子。我做二次开发时的体会是:读懂这几个抽象接口的约束,比读懂几百个具体类都有用。

2. 核心流程源码级拆解:从任务进来到结果返回的完整链路

2.1 主循环:观察—思考—行动—反馈的四步舞

Agent 的核心流程如果压缩成代码,大概就是下面这个循环:

while not agent.is_done(): observation = agent.collect_input() # 获取用户输入、工具结果、系统状态 thought = agent.think(observation) # 调用大模型生成决策 action = agent.parse_action(thought) # 解析出工具调用指令 result = agent.execute(action) # 执行工具并得到结果 agent.update_memory(observation, thought, result) # 更新上下文

这段伪代码虽然只有六行,但源码里对应的是六套完整的机制。collect_input 要做的不仅仅是把用户最新的消息拼进去,还要考虑历史消息、系统提示词、记忆检索结果;think 要做的是把消息列表转换成模型需要的数据结构,调用 Provider,再解析返回值;parse_action 是最容易出问题的一步,因为大模型返回的内容并不总是合法 JSON;execute 要处理工具找不到、参数校验失败、执行超时等各种异常。

主循环的终止条件也是源码里值得琢磨的地方。项目里至少会有三道保险:最大迭代轮数、模型主动返回“最终答案”标记、外部中断信号。我曾经见过一个没做过终止控制的简易 Agent 在大模型反复自我质疑时无限循环下去,token 烧掉一大把才被手动终止。所以看源码的时候我特别注意了 max_iterations 这个配置项,它本质上是给 Agent 上的保险丝。

2.2 规划器与执行器:两套机制的协同

Agent 圈子里一直有两套主流的任务处理范式,Hermes-Agent 源码里对它们做了兼容,并且允许通过配置切换。

第一种是 Plan-and-Execute,先规划再执行。大模型收到任务后,先生成一份步骤列表,比如“第一步查库存,第二步计算均价,第三步汇总报告”,然后 Agent 按照这个列表逐步执行。这种方式的优势是计划清晰、token 开销相对可控,适合目标任务明确、步骤相对固定的场景。缺点是一旦中间某一步的实际结果和预期不符,后续步骤可能全部作废。

第二种是 ReAct 风格,边想边做。模型每一轮都基于当前最新的观察结果做决策:现在看,我现在该调用哪个工具,或者我现在已经可以回答问题了。这种方式的灵活度很高,特别适合需要多轮试探的场景,缺点是 token 开销大、可能绕弯子。

Hermes-Agent 的默认风格更偏向 ReAct,因为它能比较好地应对不确定性。源码里这个选择是有意为之的:与其赌模型一次规划就能想出所有步骤,不如让它每一步都基于真实反馈做决策。我在实际使用中也发现,对于“多轮试探”型任务,ReAct 的成功率明显更高。

2.3 消息协议与上下文管理:Agent 流的“物流系统”

主循环跑的每一轮,中间都在传递各种数据结构。源码里会看到一组常见的消息类型:UserMessage、AssistantMessage、ToolCall、ToolResult、SystemMessage。这组数据协议是整个 Agent 流的“物流系统”,所有信息都靠它们在不同模块间传递。

一个特别容易让新手误解的地方是:工具结果并不只是一条日志,它必须被当作一条真实的消息送回给大模型,否则模型根本看不到工具返回的数据。比如你让 Agent 查订单量,工具返回了“12345”,这个“12345”必须进入下一轮大模型调用的上下文,模型才能写出“订单量为 12345”的最终答复。如果只把“工具调用成功”六个字写进上下文,模型拿什么编答案?它只能胡编。

既然工具结果要回填上下文,上下文管理就成了源码里最不能马虎的机制。因为大模型的输入长度是硬约束,你不可能让历史消息无限膨胀下去。Hermes-Agent 的做法包括:滑动窗口裁剪旧消息、对过长历史做摘要压缩、把记忆模块检索出的关键信息置顶。这些策略单独看都不复杂,难的是把它们组合在一起,既要控制 token 预算,又不能把关键决策信息丢掉。

3. 关键模块源码导读:三个必读的核心类

3.1 LLM Provider:为什么值得单独做一层封装

如果项目里只有一两个模块可以选着读,我一定会推荐先读 LLM Provider 这个抽象接口,因为它是整个 Agent 的“算力入口”。

不做封装会是什么样子?每个大模型平台的请求格式都不一样,有的用 messages 数组,有的用 prompt 字符串;有的支持 function calling,有的不支持;有的输出 JSON 模式需要额外指定 format,有的则是靠提示词“骗”出来的。如果业务代码直接面向某一个平台 SDK 开发,以后每换一次平台都要改一遍 Agent 主循环,代价非常大。

Hermes-Agent 的做法是定义一套统一的内部消息格式和调用接口,比如 chat(messages, tools=None, stream=False),然后写若干平台各自的适配实现。这套抽象的价值在流式输出上体现得最明显。流式返回是一段一段的,Provider 需要把流式解析成事件回调,比如每来一个 token 就触发一次回调,这样调用方既能实时看到生成过程,又不用关心底层是 SSE 还是 WebSocket。

重试机制也是 Provider 层要认真处理的细节。我见过不少人把超时重试写成了无脑重试,结果遇到速率限制时越重试越糟糕。正确的做法是先判断异常类型:网络错误和服务器 5xx 可以重试,认证失败和参数错误重试多少次都没用,速率限制则需要按响应头里的时间窗口等待。源码里这些分类处理逻辑虽然不起眼,但直接决定了 Agent 在线上跑得稳不稳。

3.2 工具注册中心:从函数到 function calling schema

工具模块是我认为 Hermes-Agent 里设计得最漂亮的部分。传统开发模式下,你写一个函数,想让大模型能调用它,需要手动写一份 JSON Schema 描述函数名、参数、含义。问题是函数一变,Schema 就过期,两边经常对不上。

Hermes-Agent 用装饰器把这个过程自动化了。你在源码里会看到类似这样的写法:

@tool def query_order(product_id: str) -> str: """查询某个产品的订单数量。 Args: product_id: 产品编号。 Returns: JSON 字符串,包含 total_count 字段。 """ ... return result

装饰器背后做的事情,是利用语言的反射能力提取函数名、参数签名、docstring,自动生成一份 function calling 用的 JSON Schema。这样开发者只需要写一个普通 Python 函数,工具注册中心就能自动完成注册。

工具调用真正执行的时候,源码要处理的问题就多了:解出模型返回的 tool_name 和参数 JSON,找到对应的注册函数,做参数类型转换和必填项校验,执行函数,捕获异常,把返回值封装成 ToolResult。整个流程里最容易翻车的就是参数解析,因为大模型生成的参数有时候字段名会缺一个字母、值类型会写错,所以项目里会在执行前做一层容错纠正。这个细节我在做二次开发时还专门测试过,确实能拦住不少低级错误。

3.3 记忆模块:短期、长期与上下文压缩

记忆模块是 Agent“像人”的关键,也是源码里区分度最大的一块。

短期记忆其实就是当前会话的上下文消息列表,它的生命周期和一次 Agent 运行是一致的。长期记忆则通常依赖向量化存储:把重要的历史信息做嵌入,存入向量数据库,需要时通过相似度检索捞回来。你可以把长期记忆想象成一个人的笔记本,重要的线索记下来,下次遇到类似问题时翻一翻。

关于长期记忆,源码里有一个很关键的设计抉择:什么时候写入记忆?如果每一轮都写,记忆库会迅速膨胀,检索效率下降;如果只在整个任务结束时写,中间很多有价值的中间结论就丢了。项目里往往会选择在“关键节点”写,比如一次工具调用成功之后、拿到阶段性结论之后,而不是每轮无脑记录。

检索策略同样是源码里一个有意思的点。它不是简单地把所有记忆都塞进上下文,而是先做相关性排序,再按 token 预算截取。这样做的背后逻辑是:模型处理大量无关信息时容易被干扰,不如少而精地提供若干条高相关记忆。我在读源码时看到这个设计,心里默默给它点了个赞。

4. 二次开发实战指南:从改工具到换模型

4.1 新增自定义工具的完整流程

如果你只打算给 Hermes-Agent 做一件事,大概率就是“让它能调用我自己系统的接口”。这个流程我拆解给你看。

第一步,写一个普通函数,函数名清晰,参数明确,返回值用 JSON 字符串。第二步,给函数加上 @tool 装饰器,并在 docstring 里写清楚工具的使用场景和参数含义。第三步,把工具类注册到工具管理器里。第四步,在配置文件中启用这个工具,或者确认自动发现机制已经扫描到它。第五步,跑一个最小对话测试,让 Agent 调用它。

这里我重点说说 description 怎么写。很多人在这一步偷懒,写一句“查询订单数量”就完事,结果模型根本不会主动调用这个工具。一个真正有效的 description 应该包括:什么场景下使用、每个输入参数的含义、输出结果的格式。我自己常用的写法是:

“当用户询问某个产品的订单数量时使用。输入 product_id 是产品编号。返回 JSON 字符串,包含 total_count 字段。”

这样的描述是在告诉模型“你什么时候该用它”,而不是机械地说明“我这个函数能做什么”。另外提醒一句,不要在描述里写“不要调用”之类的否定句式,模型经常选择性忽略否定词。

4.2 替换或新增大模型 Provider

如果想把默认的大模型换成别的,做法比想象中简单。核心是继承 BaseProvider,实现聊天接口,再把自己注册到 Provider 工厂里。

class MyProvider(BaseProvider): name = "my_llm" def chat(self, messages, tools=None, stream=False): # 把内部 messages 格式转换成这个平台的请求格式 # 调用平台 API # 把结果统一转换成 ChatResult 返回 ...

源码里最爽的一点是:你一旦注册好了这个 Provider,Agent 主循环、工具调用、记忆全部不需要改。你只需要把配置里的 provider 名换成 my_llm 即可。

但这里有几个我实际踩过的坑。第一,不同平台对系统提示词的位置要求不一样,有的需要拼到 messages 的 system 角色里,有的只能放在请求参数里。第二,部分平台不支持 function calling,你需要自己开一个“提示词兜底”模式,把工具说明写进系统提示词,让模型输出特殊格式指令后再解析。第三,不同模型的输出稳定性差异很大,建议先单独测试 provider 的返回,再挂上 Agent 跑完整流程,不要一步到位。

4.3 调整 Agent 的规划策略与提示词模板

Agent 跑得不好,很多问题不在代码,而在提示词模板。Hermes-Agent 通常会把提示词模板单独放在一个目录里,方便二次开发时修改。

我建议你至少关注三个参数。第一个是 max_iterations,控制最大循环轮数,新手建议设小一点,比如 5,先跑通再放宽。第二个是 temperature,考察任务模式建议调低到 0.2 以下,减少随机发挥。第三个是 stop 序列,有些模型会自己加戏,需要在系统提示词里明确“只输出最终答案,不要额外解释”。

这里补充一个我自己的经验:在系统提示词里加一句“你只能调用已被提供的工具,当没有可用工具时明确告知无法完成”,能显著减少模型胡编工具结果的情况。别小看这句话,它等于给模型划了一条红线。

4.4 通过 Hook 机制做监控、限流与人工干预

源码里最容易被忽略但又非常实用的部分是事件总线。项目通过事件系统暴露了一组 Hook 点,比如 on_tool_start、on_tool_end、on_llm_response。你可以注册自己的回调函数,在不修改核心代码的前提下插入额外的逻辑。

我实际用 Hook 做过三件事。第一件事是打点统计,每次工具调用开始和结束时记录时间和耗时,用来排查慢查询。第二件事是限流,当某个工具在短时间内被调用太多次时,主动抛错中断,防止逻辑死循环。第三件事是人工审核:在 on_tool_start 里加一个可配置的 pause,把待执行的工具参数打印出来,让人在控制台确认“Y/N”后再继续。后面这个功能在早期调试时救过我好几次,不然模型乱调工具的时候根本拦不住。

5. 高频问题与调试经验实录

5.1 五个最容易让 Agent“翻车”的场景

我见过很多人在开发 Agent 时栽在同一批坑里。这里整理成一张速查表,方便你排查时对照。

现象根因排查方向
Agent 在同一个工具上反复调用模型没有从结果里得到足够信息检查工具返回的字段是否齐全,提示词是否说明“结果无效时换路径”
模型编造了不存在的工具名工具描述不够清晰,或模型自身能力弱检查工具注册列表、Schema 完整性,必要时降级模型
上下文一长就答非所问历史消息过多、超限后被粗暴截断启动摘要压缩、滑动窗口、记忆检索
工具执行报错但 Agent 还继续异常被当成了正常 ToolResult检查异常处理分支,是否把错误消息送回给模型
模型直接给出答案但没有真正调用工具提示词没有约束“必须先调用工具”在 System Prompt 中强制要求引用工具结果

5.2 调试三板斧:日志、Mock、人审

第一板斧是开启 DEBUG 日志看原始请求和响应。不要只盯着 Agent 最后的回答看,要去看每次大模型往返的完整消息列表,尤其是中间有没有出现工具调用标记、返回参数是不是合法的 JSON。我会把原始请求导出成 JSON 文件,在编辑器里格式化后逐段观察,很多“幽灵问题”一看到原始消息就现出原形。

第二板斧是 Mock 模型。日常开发一个很痛苦的地方是:大模型的输出不可控,导致你没法确定是 Agent 逻辑的 bug 还是模型抽风。解决办法是写一个模拟 Provider,不调用真实模型,直接按预设返回指定内容,比如固定返回“调用 query_order 工具,参数是 xxx”。这样调试 Agent 流程时,每次复现的行为都完全一致,可复现性大幅提高。

第三板斧是人工审核模式,就是我们前面说到的 pause 机制。在工具执行前加一道人工确认,看起来会对自动化程度有一点影响,但早期调试阶段几乎是性价比最高的手段,它能让你看到模型每一步的决策是否正确,而不是等最终结果出来再倒推。

5.3 成本与性能控制:token 和工具调用的度量衡

Agent 项目的成本大头是 token,这一点几乎没人反对。主循环每多跑一轮,就要把越来越多的上下文重新发送给模型一次。读源码时,我看到框架已经提供了很多省 token 的设计,但作为使用者,你还需要主动做三件事。

第一,给工具结果加长度限制。很多工具会把一大坨数据返回给 Agent,但模型真正需要的可能只是其中几个关键字段。可以在工具返回前做一次后处理,只保留摘要字段。第二,设置合理的 max_iterations,既能防止失控,也能倒逼 Agent 更高效地完成任务。第三,给每次工具调用和 LLM 调用都打上耗时和 token 计数日志,定期复盘。我见过有人上线 Agent 一周之后才发现某个工具被高频空转,白白烧掉大量 token。

另外一个容易被忽略的点是并发测试。Agent 不是单次调用的 HTTP 接口,它是一段长流程。压测的时候要观察超时率、上下文内存占用、事件队列堆积情况,而不是只看 API 的成功率。

我最后再说一点个人体会吧。源码看多了你会发现,Agent 框架里真正难的不是某个算法,而是对不确定性的管理:模型输出不可控、工具随时会挂、上下文有硬上限。所有看起来复杂的架构设计,其实都是在为“不确定性”兜底。所以如果你想上手这个项目,我的建议是别急着铺开一堆功能。先把一个工具做到稳定,跑通默认循环,再把原始日志完完整整看一遍,最后再谈优化和扩展。你踩过几次坑之后会明白,那些“看起来很酷”的功能,都不如一套可靠的调试手段来得实在。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询