十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

hermes-agent:一个模型无关的轻量级Agent框架,让大模型自由调度工具

hermes-agent:一个模型无关的轻量级Agent框架,让大模型自由调度工具 上周把一个内部项目重构完顺手开源出来叫hermes-agent。名字取自希腊神话里的信使赫尔墨斯——这项目干的事也确实很像信使负责在大模型和各类工具之间跑腿传话把自然语言指令翻译成实际可执行的工具调用。如果你最近在折腾 Agent 相关的东西或者想给自己的项目接一个大模型入口又不想被某个特定平台绑死那这篇应该能帮你省不少事。先交代一下项目背景。我平时要维护好几个不同业务线的自动化流程最早用的是硬编码脚本后来试过几款商业自动化工具最后发现它们的问题几乎一样流程一改就要重配工具一多就难维护而且很难把自然语言和具体操作衔接起来。hermes-agent 最初的定位就是一个轻量的 Agent 框架让大模型充当“调度中心”各种外部能力查天气、发邮件、读写数据库、调API、执行代码都做成可插拔的工具由模型根据用户意图自动组合调用。适合谁来用如果你已经接触过 LangChain 这类框架但觉得太重、透明性不够或者你只是想快速给内部系统加一个“用大白话操控工具”的能力又不想被迫选边站某厂的模型、某个云的生态那 hermes-agent 应该正对你胃口。它核心做到三件事模型无关、工具热插拔、状态可持久化。下面我从设计思路到实操细节完整拆一遍。1. 项目整体架构与设计思路很多人一提到 Agent 就想到复杂的强化学习、多智能体博弈但实际落地时80% 的需求根本用不到那些。hermes-agent 的核心是一个“任务解析-规划-执行”的单循环架构上刻意做得比主流框架简单得多。1.1 为什么叫 HermesAgent 的本质是“信使”先掰扯一下 Agent 的本质。我个人的理解Agent 不是“智能本身”而是“智能的搬运工”。大模型负责理解意图、拆解步骤但模型本身不会真的去帮你订机票、改文档——它得借助外部工具跟真实世界交互。这个过程跟赫尔墨斯的职责几乎一一对应接收指令用户输入、传达给各方工具调用、带回复命结果汇总。所以 hermes-agent 在设计上没有把重心放在“让模型更聪明”上而是放在“让模型的指令能准确触达各类工具”上。整个项目围绕一个核心抽象展开工具注册表。每个工具就是一段 JSON Schema 声明加上一个普通的 Python 函数。大模型通过读取工具列表知道自己“能做什么”然后输出结构化的调用指令Agent 负责解析、执行、把结果回传给模型。1.2 核心模块划分五个组件各司其职开源版的代码结构很清晰总共五个核心模块相互之间没有循环依赖模块职责核心类core/scheduler任务循环调度维护运行状态机AgentSchedulercore/registry工具注册、发现、参数校验ToolRegistrycore/memory会话上下文与短期记忆管理ContextMemorycore/executor工具调用执行与结果归一化ToolExecutorcore/llm模型接入层兼容多厂商接口LLMClientscheduler 是整个循环的大脑它的工作流程非常直白接收用户任务 → 组装系统提示词包含工具列表和上下文 → 调用模型 → 判断输出是“最终回答”还是“工具调用指令” → 如果是工具调用就去执行、拿结果、再传回模型 → 重复直到模型给出最终回答或达到最大轮数限制。registry 是生态的关键。每加一个新工具只需要写一个函数加装饰器tool.register填好名称、描述、参数 Schema工具就自动出现在模型可感知的工具列表里。不需要改任何框架代码也不用改配置重启即加载。memory 模块一开始我做得挺复杂后来砍成了两层会话短期记忆和文件持久化。短期记忆就是维护一个最近 N 轮的消息列表持久化则把每次会话记录存成 JSONL方便回溯调试。做持久化的目的并不仅仅是为了留痕更重要的是复现问题——Agent 类项目最大的坑就是“上次明明是好的这次为什么不行”有完整日志才能定位。1.3 设计取舍为什么不用 LangChain选择自研这个问题我每次分享几乎都有人问。不是 LangChain 不好而是它的定位和 hermes-agent 不一样。LangChain 是一个庞大而完整的生态有几百个集成、抽象层叠得很深适合团队里有人全职去啃文档、做二次开发的情形。但我想要的是一个“打开就能看懂、每次调用都知道数据流走到哪一步”的轻量框架。自研带来的直接收益有两个。第一是调试体验hermes-agent 里每个环节都有独立的日志节点工具入参、模型原始输出、执行耗时全部可见问题定位快很多。第二是依赖极简核心代码只依赖pydantic和httpx不绑定任何特定厂商的 SDK不管是用 OpenAI、Anthropic、智谱还是本地 Ollama都是同一套配置。代价当然也有。生态集成不像 LangChain 那么丰富大部分工具得自己写。但说实话自己写一个工具通常也就几十行代码而且写一次就永久属于自己不怕上游 API 变化导致整个链路崩掉。2. 环境准备与快速部署这章直接上实操。我默认你在 Linux 或 macOS 环境下操作Python 版本要求 3.10 及以上——之所以要求 3.10是因为代码里用了不少新语法糖比如match语句和|类型联合低版本跑不了。2.1 依赖安装与项目初始化先创建虚拟环境避免把系统 Python 环境搞乱python3 -m venv venv source venv/bin/activate pip install hermes-agent如果你想直接跑源码而不是装包可以 clone 仓库后以可编辑模式安装git clone https://github.com/yourname/hermes-agent.git cd hermes-agent pip install -e .安装完成后项目会自带一个命令行工具hermes。先在任意目录执行hermes init这个命令会在当前目录生成hermes_config.yaml和tools/目录。前者是 Agent 的主配置后者用来放你的自定义工具脚本。2.2 配置文件逐项解读默认生成的配置长这样llm: provider: openai model: gpt-4o-mini api_base: https://api.openai.com/v1 api_key_env: OPENAI_API_KEY temperature: 0.2 max_tokens: 2048 agent: max_iterations: 8 timeout_seconds: 30 memory_window: 20 tools: builtin_packages: - hermes_agent.tools.builtin.web - hermes_agent.tools.builtin.filesystem custom_dir: tools逐项解释一下。llm.provider目前支持openai、anthropic、zhipu、ollama四种但这不是硬编码的限制——provider 本质上是 API 格式的一个适配标签你完全可以在代码里注册新的 provider。api_key_env很关键它指定从哪个环境变量读取密钥而不是直接把密钥写进配置文件。防止手滑把 key 提交到 git 仓库这种事从一开始就要养成习惯。agent.max_iterations控制模型最多调用几轮工具默认 8。这个值太大会出现模型反复调工具停不下来的情况太小则复杂任务做不完。我的经验是日常问答类任务 5 轮足够涉及多步骤数据处理的任务至少要给到 10。timeout_seconds是每次工具调用的超时时间防止某个外部 API 卡死拖住整个会话。tools.builtin_packages是内置工具包列表。默认带 web 和 filesystem 两个包web 包里是网页抓取和搜索能力filesystem 包是文件读写和目录操作。custom_dir指向你的自定义工具目录Agent 启动时会扫描该目录下所有.py文件并自动注册其中的工具。2.3 模型无关设计的具体落地这是 hermes-agent 的亮点单独拿出来说。所谓“模型无关”是指你在配置里切换模型供应商时Agent 的逻辑代码一行都不用改。实现这一点的关键是把所有模型请求统一成 OpenAI 风格的 ChatCompletion 格式——这已经成为事实上的行业标准几乎所有主流模型厂商都提供兼容接口。举个例子你要是想用本地的 Ollama 跑 Qwenllm: provider: ollama model: qwen2.5:7b api_base: http://localhost:11434/v1 api_key_env: OLLAMA_API_KEY唯一需要处理的是 prompt 格式差异。不同的模型在工具调用指令的遵循能力上差别很大有些小模型对严格的 JSON 输出格式把握不稳。为此我在core/llm.py里加了一个输出校正层模型返回的原始输出如果是松散文本会尝试提取其中的 JSON 片段并做修复性解析如果实在解析不了就把原始输出直接当成文本回传给用户避免整个链路崩溃。3. 核心实操从零开发一个自定义工具配置好环境之后真正影响这个 Agent 好不好用的是你往tools/里塞了多少适合自己业务场景的工具。这章从三个实际用例出发展示常见的完整开发过程。每个用例我都会讲清楚背后的设计逻辑而不只是贴代码。3.1 工具一查询数据库——从需求到实现假设你想让 Agent 能查公司的业务数据库但又不希望它把整个库的敏感数据都摸透。第一步写一个工具函数# tools/db_query.py import sqlite3 from hermes_agent.core.registry import registry from pydantic import BaseModel class DBQueryArgs(BaseModel): sql: str limit: int 10 registry.register( namedb_query, description执行一条只读SQL查询并返回结果集。仅支持SELECT语句禁止其他SQL操作。, args_schemaDBQueryArgs, ) def db_query(sql: str, limit: int 10): # 强制校验只读 stripped sql.strip().lower() if not stripped.startswith(select): raise ValueError(Only SELECT is allowed) conn sqlite3.connect(/data/business.db) try: cursor conn.execute(f{sql} LIMIT {limit}) cols [desc[0] for desc in cursor.description] rows [dict(zip(cols, row)) for row in cursor.fetchall()] return {columns: cols, rows: rows, count: len(rows)} finally: conn.close()这个例子很有代表性工具函数本身的实现很简单但如果你只看“查询数据库”这个需求很容易忽略安全边界的设计。我这里做了两层防护第一层是从 SQL 语句前缀强制限定只允许 SELECT从源头掐断注入可能第二层是强制加LIMIT防止模型情急之下查出几十万行把上下文塞爆。这里有个细节值得注意我把limit设计成模型可以感知的参数。模型在调用工具时会根据用户意图自主决定拿 5 条还是 50 条。与其让模型输出一个完整 SQL 再用正则去补 limit不如把 limit 作为独立参数显式暴露给模型模型对参数级别的控制能力明显强于对文本细节的控制。注册完工具函数保存文件。你不需要重启服务——hermes-agent 在每轮任务开始时会用文件的修改时间来判断是否重新加载。当然如果改了注册名或参数 Schema最好还是重启一下避免出现旧代码内存残留。3.2 工具二定时任务编排——理解 Agent 的异步能力实际业务里经常有“每天上午 10 点把昨天的销售数据汇总成邮件发给我”这种需求。如果每个这样的需求都走实时对话既浪费模型调用费用又没法保证准点执行。目前 hermes-agent 的定位是“贴心助理”而非“调度平台”但我们可以把它和一个定时触发器结合在工具层把周期任务变为“一句话注册”。具体做法是内置一个task_register工具让模型把用户的需求转成结构化的任务定义# tools/task_scheduler.py import json, os from datetime import datetime registry.register( nametask_register, description注册一个周期性任务。cron表达式格式为分 时 日 月 周。, args_schemaTaskRegisterArgs, ) def task_register(task_name: str, cron: str, task_prompt: str): task_def { name: task_name, cron: cron, prompt: task_prompt, created_at: datetime.now().isoformat(), } path /data/tasks/tasks.json tasks [] if os.path.exists(path): with open(path) as f: tasks json.load(f) tasks.append(task_def) with open(path, w) as f: json.dump(tasks, f, ensure_asciiFalse, indent2) return {status: registered, task: task_def}配合一个外部 cron 进程比如crontab里加一行调hermes run --task xxx就能实现“先规划、后执行”的效果。这个模式是我在实际使用中不断打磨出来的经验Agent 框架最好别自己去实现调度逻辑因为调度本身是一个成熟的技术领域用 cron 或者工作流引擎来解决比在 Agent 里生造要稳得多。Agent 擅长的是“理解意图并生成指令”把“何时执行”交给更可靠的外部机制各司其职组合起来反而更灵活。3.3 工具三把“不确定性”封装成参数——一个搜索工具的进阶写法第三类例子来自信息检索场景。假设你想让 Agent 能用关键词搜索内部知识库但不同场景下的搜索参数差异很大有时要精确匹配有时要模糊搜索有时只看标题有时要全文检索。我的做法是把搜索相关度阈值和匹配模式都暴露成工具参数registry.register( namekb_search, description在内部知识库中搜索相关文档。模式支持 exact(精确), fuzzy(模糊)阈值范围0-1。, args_schemaKBSearchArgs, ) def kb_search(keyword: str, mode: str fuzzy, threshold: float 0.6, limit: int 5): # mode 和 threshold 的组合决定了搜索行为 ...实际跑下来你会发现模型对threshold这种数值参数的感知很敏锐。当你问“找一下之前关于登录报错的处理记录”模型会自动用 fuzzy 0.6当你给出一个准确的文档编号说“找到编号 DOC-2024-0123 那份”模型会自动切到 exact。这里没有写任何 if-else 逻辑去猜测用户意图而是把意图理解的职责留给模型Agent 只需要保证工具能力的表达足够清晰。这个思路贯穿了 hermes-agent 的设计哲学框架不替模型做判断但框架要确保模型能精准表达判断结果。4. 常见问题与排查技巧实录任何 Agent 项目从“demo 能跑”到“生产稳”中间都隔着一大堆莫名其妙的问题。这章把我踩过的坑按高频程度排个序每个问题都给出排查路径和根治方案。4.1 模型输出“假 JSON”导致工具调用失败现象模型明明说“我要调用 db_query”但 executor 解析报错提示 JSON format error。打开日志一看模型的输出长这样{action: db_query, args: { ... }}注意它在 JSON 外层套了 markdown 代码块标记。这是小模型特别喜欢的事——它觉得这样更“规范”但你的 JSON 解析器可不认。排查思路第一步在日志里确认原始输出到底是什么第二步看是不是所有输出都有代码标记还是间歇性的。这个问题的根源在 prompt 模板里对输出格式的约束不够强。根治方案我在core/llm.py里加了“代码块剥离预处理”如果输出包含json 或标记先把标记剥离再做解析。如果你在用本地模型还有一个更有效的办法在系统提示词里把输出格式用 XML 标签框死比如要求“只输出 ... 中的内容”容错率会大幅提升。4.2 Agent 进入循环反复调用同一个工具现象任务执行到第 3 轮后模型开始不断调用同一个工具、拿到同样的结果、然后再次调用直到max_iterations耗尽。根因分析这种情况一般发生在任务的最终目标对模型来说“不可判定”时。比如你问“帮我总结一下这份 PDF 的内容”但 PDF 工具返回的是图片型扫描件提取出的文本是空的。模型拿到了空结果但任务没完成它不知道如何结束就会陷入“再试一次”的循环。我的处理技巧第一给工具返回结构里加一个truncated字段明确提示模型“结果可能不完整”第二在系统提示词最末尾加一行固定原则“如果连续两次工具调用返回结果相同或无实质进展请结束任务并告知用户当前限制”。这条策略简单但极其有效它给了模型一个显式的收敛退出条件。4.3 上下文过长导致幻觉加剧现象任务执行到中后期模型开始“编造”工具调用结果。比如工具明明只返回了 10 条记录模型却煞有介事地总结出 20 条甚至虚构不存在的字段值。原因分析这个问题的触发机制很隐蔽。memory_window设得过大或者工具返回结果过长时模型实际能处理的上下文可能已经接近它的注意力极限。此时模型不是你想象中那样“仔细阅读全部内容”而是开始“跳读”并基于概率补全——幻觉就产生了。参数建议实践下来工具返回结果最好控制在 500 字以内超过的部分做截断并让模型按需翻页查询。这不是我拍脑袋定的你可以做一个简单实验用一个 7B 规模的本地模型连续处理 3 个长文本工具返回到第 4 个时统计它复述返回内容时的准确率下降非常明显。4.4 多工具并发调用的竞态问题现象同一时刻两个会话都触发写文件工具后写的把先写的覆盖了。排查思路Agent 框架默认是单进程串行的但如果它以服务模式部署每个请求是独立会话就可能产生并发写。这个问题在主流程里不太显眼一旦暴露就是数据丢失级别的事故。根治方案我在工具执行层加了一个基于filelock的写锁同一路径同一时刻只允许一个工具写操作。写文件工具有了锁数据库写入则依赖事务。如果你计划多进程部署这个坑不可绕过。4.5 问题排查经验速查表现象高概率原因快速验证方法根治手段工具从未被调用工具描述不清晰模型没感知打印发送给模型的工具列表重写工具描述开头动词结尾结果描述工具参数频繁不对args_schema 类型定义错误检查 pydantic 模型字段类型与描述类型尽量用 string 描述约束不用 int 枚举每次响应都很慢模型推理轮次过多查看日志中每轮的 token 数压缩工具描述精简历史消息模型答非所问温度参数过高对比 temperature 0.2 和 0.8 的输出工具调用场景温度不超过 0.3工具执行报超时外部 API 响应慢单独测试工具函数在工具内加自身超时并返回友好错误5. 从 Demo 到生产一套可落地的优化配置到这章假设你已经把基础工具跑通、也处理过前面那些奇奇怪怪的问题了。接下来做的事是如何让它真正扛住业务压力。这个过程不复杂但需要耐心做几项“雕花”级的调优。我的建议是先按下面这套参数起步agent: max_iterations: 6 timeout_seconds: 15 memory_window: 15 llm: temperature: 0.1 max_tokens: 2048max_iterations调到 6 而不是默认的 8是为了用“资源边界”倒逼任务设计更精确。你可能会担心 6 轮不够用但实测大多数内部任务在 4 轮以内就能完成。真遇到需要 10 轮的任务说明工具拆得还不够细把大工具拆成小工具收益远大于提升轮数限制。temperature调低到 0.1 是一个关键决定。工具调用场景需要的是确定性和可复现性不是创造性。我在测试阶段做过一个对比实验同样的任务temperature 0.8 时模型能给出三种不同的调用方案0.1 时稳定采用同一种最优方案。当然如果你后续要做写作辅助类的 Agent温度再调回来也不迟这个配置不是通用的只是“面向稳定执行场景”的经验值。另外一个容易忽视的点是timeout_seconds不宜设得太大。Agent 单次任务的最长耗时约等于max_iterations × (模型响应时间 工具执行时间)。如果你每个环节给 30 秒超时8 轮任务最坏情况就是 4 分钟这对交互式体验来说是灾难。15 秒是平衡点既给了多数工具足够的执行时间又不至于让用户等到丧失耐心。6. 个人经验与实践体会最后分享一些零碎但很实用的感受。第一Agent 项目最需要投入精力的地方不是模型选型而是工具描述文案。模型对工具的感知完全取决于你写的那段 description。同样是“查询订单”功能“query_orders(uid, page)”这种描述效果就不如“根据用户ID查询订单列表按创建时间倒序排列返回订单号、商品名、金额与状态”来得精准。写工具描述要遵守动词开头、说明输出结构、符合用户的提问习惯这三条原则。第二关于模型选型小模型加好工具约束效果往往超过大模型加粗放工具定义。我在本地用 Qwen 7B 搭配精心描述的工具在“查库存-下工单-生成报表”这类任务链上效果能赶上 GPT-4 的粗放配置。这说明 Agent 的上下限受工程精细度的影响可能比模型参数量的影响更明显。第三日志是 Agent 项目的灵魂。hermes-agent 里每一个工具调用都记录了完整的入参出参出问题了先看日志。很多人喜欢加各种抽象来“优雅地处理错误”我的建议是先在日志里看清错误长什么样再决定要不要写兜底逻辑。盲目加 retry 和 fallback只会让问题的真实原因越藏越深。这个项目目前还在持续完善中后续我打算加入更细粒度的权限控制让每个工具可以指定不同的访问权限级别并支持更复杂的消息路由场景。如果你也在折腾 Agent 方向欢迎直接拿这个框架去改去用。工具类的项目价值就在于每个人都能往里面塞自己的业务逻辑期待看到你往 registry 里注册的第一个自定义工具。
返回列表