
这阵子我把日常里大量重复动作都交给了本地跑着的 hermes-agent它从一个只有几十行代码的实验脚本慢慢长成了我电脑上几乎每天都在用的常驻服务。如果你也在折腾 AI Agent或者正在纠结要不要自己动手组装一个这篇文章应该能给你一些参考。hermes-agent 不是一个商业产品也不是什么全新的框架它就是用我自己的方式把大模型、工具调用、任务规划和外部系统粘合在一块儿的智能体。核心定位非常窄本地优先、工具可插拔、行为可配置。我选 Hermes 系列模型作为默认的“大脑”因为它开箱即用、函数调用表现稳定而且跑在本地就能完成大部分工作。1. 我为什么放着现成Agent框架不用自己攒了hermes-agent先交代一下背景。我手头有一台不带独立显卡的迷你主机16G 内存日常跑着几个容器服务。最早我用的是网页版聊天工具来帮我总结文章、列计划后来发现一个问题所有任务都要靠我手动复制粘贴而且数据一来一回全在云端过一遍。我想要的不是聊天而是让一个大模型能够直接帮我调工具、查接口、整理文件最好是本地就把事情办了。1.1 本地跑大模型的成熟度比很多人以为的高很多人对本地大模型的印象还停留在“只能聊天、一复杂就乱”的阶段。实际上这两年开源模型的迭代速度非常快尤其是 Hermes 系列这种偏指令微调、工具使用的模型在 8B 级别就能表现出相当稳定的函数调用能力。我最早尝试的是 7B 到 13B 的量化版本模型格式统一用 GGUF跑在 llama.cpp 和 Ollama 上。量化到 Q4_K_M 之后显存占用不高速度也能接受。关键点是Hermes 系列在训练时加入了大量多轮对话和工具调用数据它不像一些纯基座模型那样让它输出 JSON 就满嘴跑火车。它知道什么时候该说话什么时候该调工具什么时候该停下来等结果。一开始我也怀疑本地模型做 Agent 会不会太勉强实测下来发现只要把 prompt 和工具协议设计得清楚8B 模型处理“查天气、建日程、读文件、调接口”这类任务完全够用。真正跑不动的不是模型而是把模型和外部工具粘起来的工程。1.2 现成框架不是不好是我不确定改起来要花多久我认真对比过市面上主流的 Agent 编排框架包括 Autogen、LangGraph、CrewAI 这些。它们的思路没问题抽象层次也很高但对我这种“想完全掌控行为细节”的人来说框架反而成了负担。框架会替你决定消息怎么传递、状态怎么保存、多智能体之间怎么协作。如果只是 demo跟着文档走很快一旦你要做定制比如给某些工具加独立的历史记忆或者对敏感操作加一道人工确认就得去翻源码看钩子在哪里。框架层加得越多黑盒就越多出问题的时候越难定位。我后来想明白一件事Agent 本身不是一个复杂的机器学习问题它更像一个“带工具的循环调度器”。与其去适配别人的抽象不如自己写一个足够简单的核心把所有变量暴露在自己面前。hermes-agent 就这样诞生了核心逻辑只有主循环、工具注册表、上下文管理、策略控制四块没有多余概念。1.3 为什么偏偏选 Hermes 系列当“大脑”名字确实有点误导好像“Hermes”这个词有什么特殊含义。对我来说它有两层意思一是底层模型我主要用的是 Nous Research 开源的 Hermes 系列二是希腊神话里 Hermes 本来就是神的信使负责传递消息、执行指令这个名字和 Agent 的定位非常契合。模型选型上我并不是一开始就选了 Hermes。中间试过 Qwen、Llama 和 Mistral 系列各有各的强项。但 Hermes 在函数调用这块给我的感觉是最“省心”的它生成的工具调用 JSON 结构干净很少有夹带解释文字或者格式错乱的情况。对于本地小参数模型来说这一点特别重要因为解析容错率低模型一旦在 JSON 前后夹带多余内容整个 Agent 循环就会断掉。2. hermes-agent的整体结构一个带工具夹的规划器而不是聊天机器人很多人会把 Agent 理解成“更聪明的聊天机器人”这是最大的误区。聊天机器人是“你问我答”Agent 是“你说目标我去执行”。所以 hermes-agent 的内部结构从一开始就不是围绕对话展开的而是围绕“任务循环”展开的。2.1 主循环计划、执行、观察然后再来一轮Agent 的灵魂是一个循环我用伪代码来说就是这个样子模型根据当前情况和可用工具决定下一步动作如果动作是调用工具就执行工具并拿到返回值工具结果回到模型上下文里模型再判断任务是否完成如果没完成就继续。在 hermes-agent 里这个循环被我精简成了一个run()函数加上一个上下文对象def run(task: str, max_steps: int 8): ctx Context() ctx.system(SYSTEM_PROMPT) ctx.user(task) for step in range(max_steps): resp llm.complete(ctx.messages, toolsregistry.all_schemas()) if resp.tool_calls: for call in resp.tool_calls: ctx.assistant_tool_call(call) result registry.execute(call.name, call.arguments) ctx.tool_result(call.id, result) else: return resp.text raise LoopExceeded(超过最大步数任务结束)这个循环看起来简单但它隐含了一个重要设计模型并不是一次性生成完整答案而是每走一步都站在最新的工具结果上重新判断。这就像人做菜不是提前把所有步骤都写死而是切完菜看火候再决定下一步放多少盐。最大步数上限是必须的否则模型有可能陷入无限调用。默认 8 步是我反复试出来的平衡点足够完成大多数调用链组合又不至于让单次任务拖太久。2.2 工具不是写死的而是注册出来的hermes-agent 里面的工具本质上就是普通函数。我设计了一个注册器让新增工具变成“加一个函数、写一段描述、声明参数结构”三件事tool( nameget_weather, description获取指定城市的实时天气, parameters{ type: object, properties: {city: {type: string}}, required: [city], }, ) def get_weather(city: str): # 调用天气 API return {city: city, temperature: 12, condition: 晴}注册器内部会把函数包装成模型能理解的 schema同时把所有工具统一放进一个字典。主循环执行工具时只需要从字典里取函数、传参数、拿结果完全不需要知道工具内部怎么实现。这个设计的好处是“高内聚、低耦合”。我后来加了日历工具、HTTP 请求工具、文件阅读工具每个都只需要单独写函数不用动主循环任何代码。这就是我想要的插件化核心永远保持简单工具则可以无限扩展。2.3 策略层不是所有工具都允许模型随便调工具能力越强越需要一个控制层。hermes-agent 里我加了三道策略第一道是工具启用列表。即使代码里注册了 20 个工具每轮对话真正暴露给模型的也只有配置里打开的那几个。模型看不到没被启用的工具就不会乱调。第二道是最大步数和重复检测。如果模型连续 N 轮调用同一个工具且参数没有变化我会强制打断并提醒它直接给结论。第三道是敏感操作确认。比如删除文件、发送消息这类有副作用的工具我要求它必须返回一个确认标记确认通过后才实际执行否则只返回“等待授权”。这是惨痛教训换来的后面讲坑的时候细说。agent: max_steps: 8 repeat_detection: true confirm_mode: [shell, send_message]策略层让我在“让模型自动跑”和“防止模型闯祸”之间找到了一个相对舒服的位置。3. Hermes系列模型的Function Calling我是这么接入的模型选好了循环写好了接下来最核心的问题就是模型怎么知道有哪些工具工具调用了之后怎么把结果送回模型。这块做好Agent 才真正“活”起来。3.1 我统一走 OpenAI 兼容工具接口而不是让每个模型各说各话现在很多本地推理服务比如 Ollama、llama.cpp server、vLLM都提供了 OpenAI 兼容的 API。这意味着我可以直接用标准tools参数把工具描述发给模型模型返回的工具调用也会以标准 JSON 结构呈现。我选择统一走这个协议好处很实际以后想换底模、换推理后端hermes-agent 的代码几乎不用动。这不代表 Hermes 官方只支持这一种方式而是说在我的工程实现里把所有模型差异全部挡在了llm.complete()这一个接口后面。具体来说发送给模型的 messages 是一个数组工具 schema 是另一个数组模型内部把它们合并理解。调用代码大概是client OpenAI(base_urlhttp://127.0.0.1:11434/v1) resp client.chat.completions.create( modelhermes3:8b-q4_K_M, messagesmessages, tools[schemas], stop[|im_end|], )如果模型决定调用工具它会返回一个tool_calls数组里面包含id、name、arguments这三个关键字段。arguments本身就是 JSON 字符串解析之后就是工具函数的入参。3.2 一次完整的工具调用在 hermes-agent 里长什么样以“北京现在温度多少”这个任务为例理想状态下的消息流转是这样你是 hermes-agent 的规划器。需要执行外部操作时只输出一个 JSON 对象不要解释。 用户提问北京现在温度多少 模型输出 {name: get_weather, arguments: {city: 北京}}注意这里我要求模型“只输出一个 JSON 对象不要解释”。为什么要这么严格因为本地小模型如果允许它自由发挥它经常会在 JSON 前后加上“好的我来查询”之类的话。一旦混入这些内容解析端就得多做一层清洗清洗逻辑多了解析稳定性的坑也就多了。执行完工具后hermes-agent 会把结果以tool角色的消息放回对话工具返回 {city: 北京, temperature: 12, condition: 晴}然后模型基于这个新消息继续回答“北京当前气温 12 度晴。”整个对话历史里既保留了用户任务也保留了工具调用记录和工具结果模型才能拼出完整的上下文。3.3 工具结果不是越大越好我会先压缩再回填刚开始接工具的时候我犯过一个想当然的错误把工具的完整返回结果原样塞回上下文。比如查一个接口返回了 5000 行数据结果模型还没开始分析上下文窗口就先爆了或者模型被大量无关字段干扰完全抓不住重点。后来我在工具执行和上下文回填之间加了一个压缩层原则是只保留对当前任务可能有用的关键信息别把原始流水账全倒给模型。def _compress_tool_result(text: str, max_len: int 800) - str: if len(text) max_len: return text return text[:max_len] \n...[已截断]有些工具返回本身就是结构化 JSON我会用字段路径提取关键部分。比如 HTTP 请求工具返回完整 response如果请求时指定了parse_jsonTrue我就从中提取data或result字段而不是把 headers、status、cookies 也一并塞进去。模型不需要的信息给了反而添乱。4. 真实运行一段时间后我踩到的几个大坑写完第一版的时候我以为最难的已经过去了。结果真正跑起来才发现让 Agent 在真实环境里稳定工作绝大多数时间都在处理各种边界情况。我把印象最深的几个坑列出来希望能帮你少走弯路。4.1 模型偶尔会把工具调用包在 Markdown 代码块里Hermes 模型训练得再好也不是每一次都老实按约定输出。遇到提示词稍微复杂或者模型“想帮忙”的时候它会把工具调用包裹在 Markdown 代码块里好的我来查询。 json {name: get_weather, arguments: {city: 北京}}如果你直接用 json.loads() 去解析这段文本必然报错。解决这个问题我加了一个解析兜底函数先去掉代码块标记再提取第一个左括号到最后一个右括号之间的内容 python def parse_tool_json(text: str) - dict: text re.sub(r^(?:json)?|$, , text.strip(), flagsre.M) text text[text.find({): text.rfind(}) 1] return json.loads(text)别小看这个兜底。工具调用解析的成功率决定了 Agent 循环能否持续下去。一次解析失败整个任务就可能变成纯文本回答或者直接报错退出。4.2 反复调用同一个工具像是掉进了死循环另一个高频问题模型会反复调用同一个工具即使没有新的信息增量。比如它已经拿到了天气结果下一步还是继续调get_weather而且参数一模一样就好像某些不完善的代码逻辑陷入了无限循环。我在主循环里加了重复检测逻辑统计最近几轮消息里的工具调用记录如果发现连续 3 次以上都是同一个工具、同一组参数就直接强制中断并给模型追加一条提示recent_calls [m.tool_call.name for m in ctx.messages[-6:] if m.tool_call] if len(recent_calls) 3 and len(set(recent_calls)) 1: ctx.user(你刚才已经调用过这个工具且没有新输入禁止重复调用直接给结论。)这个提示挺管用。模型看到之后通常会意识到自己的问题转而生成最终回答。你也可以理解为 Agent 需要一个“外置的自我纠错机制”因为模型不会像人一样自然而然意识到自己在原地打转。4.3 上下文被工具返回结果撑爆任务越长越容易崩上下文管理是 Agent 工程里最容易被低估的问题。单轮工具调用还好一旦任务需要多步工具调用每一步的结果累加起来很快就会逼近模型的上下文窗口。我做过一次统计一个 6 步的任务每步工具返回平均 1500 字最后上下文里光是工具结果就积累了近万字再加上原始对话和系统提示8K 上下文的模型几乎必然超过限制。我的解决方案是分层处理比较新的工具结果保留原文比较远的只保留摘要。摘要怎么生成让模型自己用小上下文窗口做一次压缩或者用工具结果里的标题、状态码、关键字段拼一个精简版本。工程实现上不必太精致目的是“别把模型撑爆”而不是“精确还原每一条原始数据”。4.4 工具多起来之后模型开始出现选择偏差工具少的时候模型基本不会选错。当我注册了十几个工具里面既有get_weather当天天气又有get_historical_weather历史天气模型偶尔就会在应该调历史查询的时候去调了当天天气。这种问题不怪模型是我工具定义的锅。我给工具描述里加上了明确的区分语义和使用场景get_historical_weather获取过去指定日期的天气数据适合用于回顾、对比、统计场景。同时在参数的 description 里加上示例值。改完之后选择准确率明显提升。经验是工具描述不要写得太“学术”要写“什么情况下该用我”模型才能把场景和工具对上号。5. hermes-agent的落地形态从命令行一次调用到常驻后台服务到这一步功能已经能跑通但要真正融入日常还得解决“怎么用起来方便”的问题。我不可能每次都打开终端敲命令行所以 hermes-agent 被我拆成了三层入口命令行、常驻服务、消息接口。5.1 配置全部摊开一个人也能维护一整套 Agent所有行为参数我收敛到一个 YAML 文件里。模型地址、工具开关、步数限制、确认模式、连接器配置全部一目了然model: provider: ollama name: hermes3:8b-q4_K_M base_url: http://127.0.0.1:11434 context: 8192 agent: max_steps: 8 repeat_detection: true confirm_mode: [shell, send_message] tools: enabled: [get_weather, read_file, http_request, datetime, note] connectors: - type: cli enabled: true - type: telegram enabled: true token: 你的token我把配置文件当成 Agent 的中控台。改模型后端、加一个工具、调高步数限制都只需要改配置重启服务不用动代码。5.2 用 systemd 把它变成常驻服务我日常跑在 Linux 迷你主机上最省心的方式就是用 systemd 托管。写一个 service 单元文件配置好工作目录和虚拟环境它就能开机自启、崩溃自动重启[Unit] Descriptionhermes-agent Afternetwork-online.target ollama.service [Service] ExecStart/opt/hermes-agent/.venv/bin/python -m hermes_agent --config /opt/hermes-agent/hermes.yaml Restarton-failure Userhermes WorkingDirectory/opt/hermes-agent [Install] WantedBymulti-user.target启动命令就一行systemctl enable --now hermes-agentRestarton-failure很重要。模型偶尔会返回一些奇怪的参数导致工具抛异常如果没有这个配置Agent 挂了就是挂了等你发现的时候半天已经过去了。5.3 接入日常入口消息机器人、定时任务和 HTTP 接口常驻服务跑起来之后我给它加了一层薄薄的消息机器人入口。比如在 Telegram 里把 hermes-agent 添加为一个机器人给它发消息它就能调用整套工具链再把结果返回。机器人本身只是一个连接器真正干活的是 Agent 核心。除了被动响应我也做了定时任务。比如每天早上 9 点cron 触发一行命令让 hermes-agent 汇总当天的日程和天气然后推送到指定对话。这本质上就是给 Agent 一个“定时任务”的输入它会自己决定调哪些工具、按什么顺序调。这些入口加在一起hermes-agent 才从一个终端玩具变成一个真正参与日常工作的系统。现在它每天早上帮我整理信息平时帮我查资料、调接口、做笔记基本达到了我最初“本地优先、工具可插拔、行为可配置”的目标。如果你也想复刻一个类似的 Agent我最想留给你的一句话是先把单轮工具调用跑通再把循环加上不要一上来就堆记忆、规划、多智能体协作。hermes-agent 的核心复杂度从来不在模型本身而在于你如何处理“模型说了但你不想让它直接执行”的情况——把确认机制、工具白名单和上下文管理做扎实Agent 才能真正从玩具变成工具。