尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

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( namequery_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_PROVIDERopenai export HERMES_MODEL_NAMEgpt-4o export HERMES_API_KEYyour-api-key export HERMES_API_BASEhttps://api.openai.com/v1如果你用的是兼容OpenAI接口格式的国产模型或自部署模型只需要修改HERMES_API_BASE指向对应的服务地址即可。这个配置方式在团队协作和部署到服务器时非常方便也避免了密钥泄露到git仓库的问题。初始化Agent实例的代码如下from hermes_agent import Agent agent Agent( model_provideropenai, model_namegpt-4o, max_steps15, max_tokens_per_step1024, memory_enabledTrue, )这里有三个参数建议你重点关注。max_steps是单个任务最多执行多少轮“思考-行动-观察”循环默认15步防止模型陷入死循环无限烧token。max_tokens_per_step限制每一轮模型输出的最大token数量。memory_enabled开关控制是否启用长期记忆刚调试的时候建议先关掉减少变量。3.3 注册两个实用工具并跑通第一个任务为了验证agent真的能干活我给它注册了最朴素但最常用的两个工具执行本地Shell命令和读写文件。import subprocess from hermes_agent import tool tool(namerun_shell, doc在本地执行shell命令返回标准输出与退出码) def run_shell(command: str, timeout: int 30) - dict: result subprocess.run(command, shellTrue, capture_outputTrue, textTrue, timeouttimeout) return {stdout: result.stdout, stderr: result.stderr, exit_code: result.returncode} tool(nameread_file, doc读取指定路径的文本文件内容) def read_file(path: str) - dict: with open(path, r, encodingutf-8) as f: return {content: f.read()}然后把工具注册进agentagent.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_factorylambda: agent.clone(), max_concurrent_tasks5, ) results orchestrator.submit_many([ 处理数据A, 生成报表B, 发送通知C, ])max_concurrent_tasks我建议不要盲目调大。因为每个任务背后都会产生大模型API调用并发太高一方面会让API限流另一方面如果工具执行的是CPU密集操作还会拖垮服务器。实测下来个人开发机5个并发是舒适区8个以上就开始出现响应延迟明显上升。4. 核心机制拆解ReAct循环、上下文管理和可观测性4.1 ReAct循环的工作流程与超参调整经验hermes-agent的核心执行逻辑采用的是ReActReason Act循环模式。每一次迭代里模型先思考当前状态和下一步动作然后输出一个动作指令agent执行该动作获得观察结果模型再基于观察结果继续思考。整个过程循环往复直到模型判断任务完成。一个完整的循环大致是这样的第1轮 思考: 用户想要统计data.csv的行数我需要先读取文件 动作: read_file(pathdata.csv) 观察: 文件内容为CSV格式共32行含表头 第2轮 思考: 文件已读取共31条数据记录。任务完成。 动作: finish(answerdata.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在跑长文本任务时的稳定性提升了好几个档次。这套框架我还在持续迭代目前已经在处理定时任务和跨工具流程编排后续如果遇到新的坑再找机会和你细聊。
返回列表