news 2026/9/8 12:28:27

hermes-agent实战:从零搭建自主智能体框架,让大模型真正动手执行任务

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
hermes-agent实战:从零搭建自主智能体框架,让大模型真正动手执行任务

1. 项目定位:为什么说hermes-agent是解决“AI只会聊天”的一把钥匙

做AI应用开发这两年,我一直被一个问题反复折磨:大模型本身再聪明,它也只会“说”不会“做”。让它写一段代码没问题,让它去执行一段pytest测试用例、把结果整理成表格再发到钉钉群里,它就彻底没辙了。说白了,模型是大脑,但缺手缺脚。

hermes-agent这个项目的出发点,就是把这双手脚补上。它是一个基于大模型的自主智能体框架,核心思路是把大模型从“对话窗口”里解放出来,让它能够自主规划任务、调用外部工具、读取中间结果、根据反馈调整下一步动作。你可以把它理解为给大模型配了一位“信使”——你只需要说清楚目标,它负责传递指令、协调工具、把结果带回来。

这个项目最适合三类人来参考:第一类是像我一样做AI应用开发的工程师,想在项目里集成一个靠谱的agent框架;第二类是运维和自动化测试的同事,手上有一堆脚本和API,想用自然语言统一调度它们;第三类是刚接触智能体的朋友,想搞懂agent内部到底是怎么运转的,从零搭一个出来边跑边看日志,比读一百篇概念帖都管用。

我当时起名时借用了希腊神话里Hermes(赫尔墨斯)的概念,他是宙斯的信使,跑得快、传话准、能在神与人之间穿梭。我希望这个智能体框架也能干这活——在用户和大模型之间传递意图,在大模型和工具之间调度请求,最终把“用户意图”准确翻译成“机器已执行的行动”。

2. 核心架构设计:一个能跑起来的agent框架,内部到底分了哪几层

2.1 整体架构:认知层、执行层、记忆层三层分离

先聊聊hermes-agent的整体架构设计。我见过很多新手一上来就写一个巨大的Python类,把prompt拼接、工具调用、日志输出全塞在一起,代码跑通一次之后几乎没法扩展。hermes-agent从一开始就按三层来拆解。

认知层(Cognition Layer)负责理解用户意图并把目标拆解成可执行计划,核心是大模型本身加一套高质量的系统提示词。执行层(Execution Layer)负责真正调用工具,包括函数注册、参数校验、结果解析。记忆层(Memory Layer)负责保存上下文,既包括当次会话的短期上下文,也包括跨会话的长期记忆。

举个场景你就明白了。用户说“帮我把logs目录下最近三天的error日志统计一下,按小时汇总,输出成CSV”。认知层首先把这句话拆成三个子任务:扫描日志文件、按小时聚合统计、格式化输出CSV。执行层依次调用文件扫描函数、统计聚合函数、CSV写入函数。记忆层则记录下“用户习惯看小时粒度”“喜欢CSV而不是Excel”“logs目录的路径是./logs”等等。没有记忆层的话,用户每一次都得重新交代这些信息。

分层带来的最大好处是可替换性。今天用GPT-4o做认知层,明天想换成Claude或者开源模型,只需修改认知层的适配器,执行层和记忆层完全不用动。同理,工具从普通Python函数换成HTTP API,也只需要在执行层增加一个远程工具适配器即可。做工程的人都懂,这种“可替换”的架构能省掉多少重构的坑。

2.2 工具选型与Function Calling封装思路

工具层是整个agent的“手”,怎么让大模型准确地调用工具,是hermes-agent最花心思的地方。当前业界主流的方案是大模型的Function Calling能力——模型在生成回复时,不是直接输出最终回答,而是输出一个结构化的函数调用指令。

但有一个坑:不同厂商的Function Calling格式并不一样。OpenAI用的是JSON Schema描述函数,Claude用的是tool schema,开源模型比如Qwen用的又是另一种格式。如果直接在业务代码里写死某一家格式,将来切换模型就要推翻重写。

hermes-agent的解决方案是自建一个统一的工具描述中间层。内部定义了一套标准的工具描述结构,包括工具名称、功能描述、参数Schema、返回值类型、错误处理策略。底层接不同模型时,只需要写一个适配器,把统一描述翻译成对应模型的格式。业务侧注册工具时,只需要按统一规范写就好。

以注册一个“查询天气”的工具为例,实际代码如下:

@hermes.tool( name="query_weather", description="根据城市名查询当前天气", params_schema={ "city": {"type": "string", "description": "城市名,例如:北京"}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"], "default": "celsius"} } ) def query_weather(city: str, unit: str = "celsius") -> dict: data = weather_api.fetch(city) return {"city": city, "temperature": data["temp"], "condition": data["condition"]}

这段代码里有几个细节值得注意。description字段一定要写清楚工具的功能边界和适用场景,因为大模型就是靠这个描述来判断什么时候该调用这个工具。描述写得太泛,模型会在不该调用的时候乱调;写得不够全面,模型遇到合适的场景又想不起来调。unit参数设置默认值并限定枚举值,降低了模型传参时出现“北京°F”这种奇怪组合的概率。返回结果强制用dict包裹,方便后续把结构化结果拼进上下文供模型参考。

2.3 记忆模块:短期上下文和长期记忆的协同配合

记忆层是很多agent项目最容易偷懒的部分。很多demo直接把所有历史对话一股脑塞进prompt里,token预算马上爆掉,而且旧信息和新指令互相干扰,模型表现反而变差。hermes-agent把记忆拆成两层。

短期上下文保留当前任务链条里最核心的信息:用户原始指令、子任务列表、最近几次工具调用的输入输出。长期记忆则负责沉淀用户的长期偏好和跨会话的知识。比如用户经常处理日志分析任务,长期记忆里就存上“日志路径通常为./logs,输出格式偏好CSV”。

长期记忆的实现方式,我用的是向量数据库加摘要文本的混合方案。每次任务结束时,agent会把关键结果和用户偏好做一次摘要,存入向量库。下次用户提出类似任务时,通过语义检索把最相关的长期记忆取出来,拼入系统提示词。

这中间有一个我踩过的大坑:长期记忆如果写入太频繁,会把噪声也记进去,反而污染后续任务的质量。比如用户偶发一次“这次用Excel吧”,agent就记住“用户偏好Excel”,下次任务误导了模型。最终在hermes-agent里引入了一个确认机制——只有用户明确表达偏好,或者同一偏好出现至少两次,才会写入长期记忆。多数场景下这个策略的效果远比“全记下来”要好。

3. 从零搭建:手把手跑通一个hermes-agent实例

3.1 环境准备与基础依赖安装

这套框架我只依赖了最少量的外部组件,避免一上来就把整个项目搞得太臃肿。基础运行环境建议Python 3.10+,核心依赖只有openai(兼容各家API)、pydantic(做参数校验)、tiktoken(做token计数)。如果你想启用长期记忆,需要再装一个chromadb。

python -m venv venv source venv/bin/activate pip install hermes-agent openai pydantic tiktoken chromadb

启动文件我建议命名为agent_app.py。新建目录结构的时候,把tools、memory、logs分别建出来,后续维护的时候按照目录就能快速定位问题。

3.2 模型接入配置:不同大模型的统一适配

hermes-agent通过环境变量来管理模型接入配置,不把密钥写在代码里。

export HERMES_MODEL_PROVIDER="openai" export HERMES_MODEL_NAME="gpt-4o" export HERMES_API_KEY="your-api-key" export HERMES_API_BASE="https://api.openai.com/v1"

如果你用的是兼容OpenAI接口格式的国产模型或自部署模型,只需要修改HERMES_API_BASE指向对应的服务地址即可。这个配置方式,在团队协作和部署到服务器时非常方便,也避免了密钥泄露到git仓库的问题。

初始化Agent实例的代码如下:

from hermes_agent import Agent agent = Agent( model_provider="openai", model_name="gpt-4o", max_steps=15, max_tokens_per_step=1024, memory_enabled=True, )

这里有三个参数建议你重点关注。max_steps是单个任务最多执行多少轮“思考-行动-观察”循环,默认15步,防止模型陷入死循环无限烧token。max_tokens_per_step限制每一轮模型输出的最大token数量。memory_enabled开关控制是否启用长期记忆,刚调试的时候建议先关掉,减少变量。

3.3 注册两个实用工具并跑通第一个任务

为了验证agent真的能干活,我给它注册了最朴素但最常用的两个工具:执行本地Shell命令和读写文件。

import subprocess from hermes_agent import tool @tool(name="run_shell", doc="在本地执行shell命令,返回标准输出与退出码") def run_shell(command: str, timeout: int = 30) -> dict: result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=timeout) return {"stdout": result.stdout, "stderr": result.stderr, "exit_code": result.returncode} @tool(name="read_file", doc="读取指定路径的文本文件内容") def read_file(path: str) -> dict: with open(path, "r", encoding="utf-8") as f: return {"content": f.read()}

然后把工具注册进agent:

agent.register_tool(run_shell) agent.register_tool(read_file) task = "请查看当前目录下的data.csv,统计一共有多少行数据,并告诉我前三行内容。" result = agent.run(task) print(result.output)

整个执行过程大致是这样的:模型先判断当前任务需要查看文件系统的目录结构,调用run_shell执行“ls”;然后判断需要读取data.csv,调用read_file;再基于返回的内容做统计,最终用自然语言输出结果。

第一次跑通的时候,我的感受是:它真的不是“假装”在完成,而是每一步都实际执行了系统调用,错误了还会自己纠正。这种真实感是普通聊天大模型完全给不了的。

3.4 任务调度与并发控制机制

单任务跑通之后,你很快会遇到新问题:用户一次性提了多个任务,或者多个用户同时在用这个agent,怎么处理并发?

hermes-agent内置了一个轻量级的任务调度器。每收到一个用户请求,会创建一个独立的Agent实例上下文,包括独立的记忆胶囊和独立的工具调用会话。这样的好处是任务之间互相隔离,不会出现A用户的会话状态被B用户干扰的情况。

并发数控制通过线程池参数来调整:

from hermes_agent import TaskOrchestrator orchestrator = TaskOrchestrator( agent_factory=lambda: agent.clone(), max_concurrent_tasks=5, ) results = orchestrator.submit_many([ "处理数据A", "生成报表B", "发送通知C", ])

max_concurrent_tasks我建议不要盲目调大。因为每个任务背后都会产生大模型API调用,并发太高一方面会让API限流,另一方面如果工具执行的是CPU密集操作,还会拖垮服务器。实测下来个人开发机5个并发是舒适区,8个以上就开始出现响应延迟明显上升。

4. 核心机制拆解:ReAct循环、上下文管理和可观测性

4.1 ReAct循环的工作流程与超参调整经验

hermes-agent的核心执行逻辑采用的是ReAct(Reason + Act)循环模式。每一次迭代里,模型先思考当前状态和下一步动作,然后输出一个动作指令,agent执行该动作获得观察结果,模型再基于观察结果继续思考。整个过程循环往复,直到模型判断任务完成。

一个完整的循环大致是这样的:

第1轮 思考: 用户想要统计data.csv的行数,我需要先读取文件 动作: read_file(path="data.csv") 观察: 文件内容为CSV格式,共32行(含表头) 第2轮 思考: 文件已读取,共31条数据记录。任务完成。 动作: finish(answer="data.csv共有31条数据记录,前三条为...")

这套循环里的关键是超参调整。max_steps设太小,复杂任务容易在没完成前就被掐断;设太大,又可能让模型在简单任务上反复绕圈浪费token。我的建议是日常任务设8~12步,涉及多轮工具调用的复杂数据分析任务设20~30步。另外一个重要参数是早停策略(early_stopping),当模型连续两轮输出相同动作和相同参数时,说明它已经陷入原地打转,此时应当强制终止并向上报送错误。

4.2 上下文管理:控制Token预算的几种实用策略

跑过agent项目的人都懂,token消耗是最大的隐形成本。如果不加控制,模型每轮都要把全部历史记录重新读一遍,一个10轮循环的任务可能吃掉上万token。

hermes-agent的上下文管理做了三件事。第一,全局限制单轮context最大token数,默认6000。第二,对工具返回内容做截断,比如run_shell返回的标准输出超过2000字符时,只保留头部1500字符和尾部500字符,中间用“...[内容已截断]...”代替。第三,对历史消息做滑动窗口,最近的5轮消息完整保留,更早的消息压缩成摘要存留。

这个“截断+压缩”的组合拳实测有效。在我的一次日志分析任务中,不启用上下文管理时一次任务消耗约24000 token,启用之后降到了9000 token左右,降幅超过60%,而最终结果的质量几乎没有变化。对大模型API按token计费的场景来说,这是实打实的成本优化。

4.3 可观测性设计:如何看到agent每一步在做什么

调试agent是一个极其痛苦的体验,因为每一步都是模型不确定性的产物。你说不清它为什么走这条路,也不知道它哪一步开始走错的。如果没有日志追踪,整个调试过程就像在黑箱里猜谜。

hermes-agent从一开始就在框架层集成了结构化日志。每一轮循环都会记录四个信息:模型输入的思考摘要、模型输出的动作指令、工具返回的观察结果、token累计消耗。日志格式是JSON Lines,方便用jq等工具做过滤分析。

一个典型的日志片段:

{"step": 1, "type": "thought", "content": "需要读取data.csv文件", "timestamp": "2025-06-20T10:12:33"} {"step": 1, "type": "action", "name": "read_file", "params": {"path": "data.csv"}, "timestamp": "2025-06-20T10:12:34"} {"step": 1, "type": "observation", "content": "file read success, 32 lines", "timestamp": "2025-06-20T10:12:35"} {"step": 1, "type": "usage", "prompt_tokens": 820, "completion_tokens": 45, "timestamp": "2025-06-20T10:12:36"}

我在实际开发中还加了一个“重放模式”:把一次任务的所有日志收集起来,按step逐帧回放。这样即使任务跑完了,也能从头到尾复盘agent的每一步决策过程,查找它是在哪个环节产生了错误判断。工具选型时排除了纯文本print调试的方式,因为print输出的信息量太有限,无法支撑复杂任务的追踪需求。

5. 我踩过的坑与排查实录:给后来者的一份避坑指南

5.1 工具调用失效:模型不按schema传参数怎么办

这是我遇到的第一个高频问题。明明工具注册时定义了params_schema,模型就是会传一些乱七八糟的参数进来。比如把int类型传成字符串、把必填参数漏掉、把参数名拼写错误。

排查后发现了根因:不同模型对工具描述的理解能力差异很大,一些开源小模型理解长描述有困难,工具一多就混淆。hermes-agent最终在工具调用执行前加了一层参数校验和强制类型转换:

from pydantic import BaseModel, create_model def validate_tool_call(tool_name, tool_schema, raw_params): model = create_model(tool_name, **{k: (v["type"], ...) for k, v in tool_schema["properties"].items()}) try: return model(**raw_params).model_dump() except ValidationError as e: return None, str(e)

校验失败时不直接终止整个流程,而是把错误信息返回给模型,作为观察结果让模型自己修正参数。这个“设错-报错-自纠”的二轮机制效果显著,把工具调用的整体成功率从82%提升到了94%。

5.2 模型陷入死循环和瞎编路径的处理策略

第二个绕不开的坑是模型陷入死循环。曾经有过一次,让agent写月度总结文件,它不断地open文件、看内容、关闭文件、再打开,整整循环了14步。日志显示前两三步还正常,后面完全是重复输出同样的动作。

我从这个问题提炼出的兜底策略是:三步一停的原则。如果发现模型连续3轮动作完全相同,就把它这次的目标和已执行的步骤重新整理后塞回给模型,明确提示“你已经执行过这个操作,不要重复,请尝试新方案或者直接结束”。这相当于在对话中加了一个“人工提醒”的角色,实测对减少死循环非常有效。

另一个常见问题是模型在处理文件路径时瞎编。问它当前目录在哪,它回答说/home/ubuntu/project,但实际上当前工作目录是/Users/admin/tmp。这个问题的根源在于基础模型的训练数据里有大量Linux路径,它会产生“记忆幻觉”。解决方式是在工具层增加了一个get_workdir工具,并在一开始就把真实工作目录写入系统提示词,从源头上减少瞎猜。

5.3 记忆污染的预防:宁缺毋滥的写入策略

前面提到过记忆污染的问题,这里展开详细说。曾经有一次,用户聊天时随口说了句“今天天气挺热”,agent的长期记忆模块就把“用户关注天气”写入了记忆库。结果下一次用户问“帮我写份季度报告”,模型居然在思考过程里加入了“用户可能也关心天气信息”这种毫无必要的联想,白白浪费了两步token。

最后的解决方案是双通道记忆写入策略。通道一是显式偏好,用户用非常明确的指令表达了偏好,比如“以后都用CSV格式输出”,这种直接写入;通道二是隐式观察,agent的推导结果需要经过阈值判断。阈值我设成:同类型事件出现2次及以上,且事件期望收益超过设定值才写入。简单说就是“多看几次、确认没看走眼,才当回事”。

这套策略牺牲了一部分记忆“灵敏度”,但换来了记忆“准确度”。从实际项目来看,用户对于一个agent的信任程度,主要取决于它会不会反复犯低级错误,而不是它能不能记住每一个细节。

6. 扩展方向:从单机demo走向生产可用的agent服务

6.1 多智能体协作:主Agent不再大包大揽

跑通单个agent之后,下一步我之前最想做的是多agent协作。核心思路是不再让一个主Agent做所有事情,而是拆成多个专职Agent,由主Agent做任务分发和结果汇总,专职Agent各管一摊。

举个例子,一个数据分析任务可以拆成三个专职Agent:数据清洗Agent负责处理源文件里的脏数据,统计建模Agent负责做聚合计算,报告生成Agent负责把结果整理成可读性强的文档。每个Agent只需关注自己的小领域,工具列表也精简到和自身职责相关的那几个。这样显著降低了单个Agent的工具选择难度,大模型需要决策的空间变小,犯错的概率也随之下降。

多Agent协作的通信协议我用的是共享任务板模式。每个Agent执行完自己的步骤后,把结果写入任务板对应的字段,后续Agent从任务板读取自己感兴趣的数据。这种解耦方式比Agent直接互相调用更可控,主Agent可以在关键节点介入检查质量。你可以在hermes-agent的配置文件里通过agent_roles字段来定义多个专职agent,并指定它们各自的工具访问权限。

6.2 权限与安全控制:让“手”不乱动

当agent能执行Shell命令、能读写文件、能访问网络时,安全问题就变得异常重要。权限最小化的原则必须从一开始就贯彻,而不是等出了问题再补。

我的建议是两层控制。第一层是工具级白名单,agent能调用哪些工具、不能调用哪些工具,在注册时就明确写死。比如,读文件的工具可以调用,删除文件的工具不允许注册。对于Shell执行这类高危工具,我建议默认禁用,仅在特定受控环境下开启。第二层是运行时审批,当agent准备执行一条高危命令时,http请求会先进入pending状态,通过回调通知管理员,管理员批准后才真正放行。

我最初在项目里并没有做权限控制,直到有一次测试中agent真的执行了“rm -rf temp”,虽然删的只是临时目录,但那一刻我后背发凉。如果一个agent在外网环境里被恶意注入提示词,后果不堪设想。凡是让agent连接外部数据的项目,权限控制永远不应该靠自觉,而应该靠机制。

6.3 效果评估:怎么量化agent干得好不好

最后写一个国内技术博客很少聊到的话题——agent的评估体系。没有评估就没有优化方向,然而大多数agent项目都是靠开发者“看感觉”来判断好坏。

我用的评估指标有两类。第一类是成功率,任务是否完成了,产出物是否与预期一致。第二类是效率指标,用了多少步才完成、消耗了多少token、是否存在重复调用。效率和成功率往往是一对矛盾,需要根据具体场景取舍。

评估用的测试集是关键。从近期真实任务里抽20个有代表性的样本,组成一个固定回归集。每次改动框架或提示词后,跑一遍回归集,对比成功率和步数变化。这个习惯帮我挡住了很多“改了A发现B坏了”的问题。比如我优化过一次工具描述词,测试中天气预报类任务成功率涨了8%,但日志分析类任务却下降了3%,原因就是描述词里加的内容干扰了模型对日志分析工具的判断。如果没有回归集,这种隐蔽的性能回退根本发现不了。

把评估做成自动化的那几天,我才真正觉得这个项目从“写着玩”变成“能用”。agent的行为很像一个模糊系统,不量化评估就永远在玄学调参,量化之后才能系统性地迭代。

我再分享一个日常使用的小技巧:做这类agent项目,一定要注意保护模型输出过程中的上下文窗口。如果你在Agent回调函数里既要做业务逻辑又要往上下文塞大量数据,很快就会触发context length限制。我的做法是把不必要的数据排除在上下文之外,必要时用一个buf变量做中间存储,只在最终提交结果时统一写入。这个小改动让我的hermes-agent在跑长文本任务时的稳定性提升了好几个档次。这套框架我还在持续迭代,目前已经在处理定时任务和跨工具流程编排,后续如果遇到新的坑,再找机会和你细聊。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 12:27:01

SSM框架出租车管理系统实战解析:从项目结构到核心模块开发

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 12:26:55

用AI智能体构建数学建模工作流:从读题到可验证结论的完整实践

华数杯数学建模竞赛这类题目,很多人第一步就是打开 ChatGPT 把题目丢进去,等它吐出一篇“结论正确、过程漂亮”的答案。但真正参加过比赛的人都知道,AI 直接生成的建模方案往往存在两个问题:要么过于泛化,没有针对数据…

作者头像 李华
网站建设 2026/9/8 12:26:10

ANSYS+MATLAB车桥耦合振动仿真:两套模型与路面不平整度影响分析

公路车桥耦合振动这个话题,这几年在研究生论文里出现的频率越来越高,尤其是桥梁工程和车辆工程方向。原因很直接:一方面,桥梁在车辆荷载下的动力响应直接关系到安全评估和疲劳寿命预测;另一方面,随着高速重…

作者头像 李华
网站建设 2026/9/8 12:25:41

SuperGlue特征匹配实战:从原理到PyTorch部署与私有数据微调

简介:该资源是一套面向图像匹配与视觉定位场景的SuperGlue PyTorch可训练实现,适合具备一定深度学习基础、希望二次开发特征匹配模型的研究者与开发者。项目在官方超点实现基础上做了多项工程优化:支持batchsize大于1训练,损失计算…

作者头像 李华
网站建设 2026/9/8 12:24:28

vLLM 显存泄漏如何观测?Kvcachescope 实战剖析 KV Cache 分配与释放

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 12:24:25

MCP协议在工业物联网中的落地实践:谁在用、怎么用、卡在哪

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华