hermes-agent 是我在大量业务需求里踩坑踩出来的一个轻量级 Agent 框架,前后重构了三轮,目前已经在好几个内部项目里稳定跑了大半年。它的定位很直接:让大模型不再只是对话框里的问答机器,而是能自己拿工具、按流程、带记忆地把一件实际任务干完的执行体。很多团队一开始都以为,接入大模型就是调一个 API,真正做起来才发现,任务编排、工具调用、记忆管理、失败重试这些工程问题才是大头。hermes-agent 就是围绕这些工程问题设计的。如果你正准备开始做 Agent,或者已经在做但总觉得链路不稳定、问题不好排查,这篇文章应该能给你不少可落地的参考。
1. hermes-agent 整体定位与设计思路
1.1 为什么叫 Hermes:信息流转才是 Agent 的真身
项目起名 Hermes,源自希腊神话中的信使神。这个名字不是随便拍的,它恰好点明了 Agent 系统最核心的东西——消息与信息的流转。一次完整执行里,用户指令进来、任务被拆成子步骤、模型决定调用哪个工具、工具结果返回、模型再判断下一步,这一整条链路本质上就是无数条消息在多个模块之间流动。我在设计 hermes-agent 的时候,第一个原则就是把这条链路捋直:每个环节只干一件事,每个环节的输入输出都有明确格式,绝不把关键逻辑藏在模型 prompt 里碰运气。
这里的取舍值得多说一句。早期版本里,我尝试让模型通过"自由发挥"来决定整个流程,结果就是线上表现神鬼莫测,同一个任务今天能跑通明天就挂掉。后来我改成"状态机 + 结构化决策"的模式:模型只负责在几个明确的动作里做选择,比如 plan、call_tool、observe、finish,剩下的具体执行和校验全部靠确定性代码收尾。这样等于把随机性压缩在几个决策点,而不是散落在整条流程里,稳定性提升非常明显。
1.2 架构选型:为什么没有直接上 LangChain
很多人会问,现成框架那么多,为什么要自己写一个?我最早确实是从 LangChain 入手的,但用下来的体感是:抽象太多、调试链路长、版本升级频繁,很多高级概念对中小型项目来说根本用不上。hermes-agent 的底层设计哲学可以总结成几句话:
- 工具即函数:一个工具就是一个带名字、描述、参数约束的普通 Python 函数,不做花哨包装。
- 流程即状态机:任务生命周期用有限状态迁移来管理,谁在什么时候该做什么,代码里一眼能看全。
- 可观测性优先:从 trace_id 到每一步的工具入参出参、token 消耗、耗时,全部留结构化记录。
- 状态可恢复:任务执行到一半失败,不推倒重来,可以从最近的稳定状态恢复。
这套取舍不是说 LangChain 不好,而是它的目标用户是"什么场景都要兼容"的通用框架,自然要付出通用性成本。hermes-agent 选了另一条路:把 80% 的常见需求做到又简单又稳,剩下 20% 交给使用者自己扩展。做技术选型,最怕的就是为了一个可能永远用不上的特性,背上整套框架的重量。
1.3 适用场景与不适用场景
从我实际跑过的项目来看,hermes-agent 最适合这几类场景:
- 客服工单自动处理:读用户描述、查知识库、调用业务接口生成回复,原来人工处理平均 15 分钟,现在能到分钟级响应。
- 数据分析辅助:让 Agent 读取 CSV 或数据库表,自动写查询、做统计、生成结论,运营同学可以直接对话完成日报。
- 自动化测试:根据需求描述自动生成测试用例并调用执行工具,结果回传后判断是否通过。
- 私有知识库的问答加操作:不止是"查出来给你看",还能接着执行下一步动作。
但也有明显不合适的地方。比如毫秒级响应的强交互场景,Agent 的决策链路天然有延迟,不适合硬上;再比如需要强分布式事务保证的重型业务,也不应该让一个 Agent 去承担一致性责任。做选型时先想清楚,Agent 解决的是"智能决策"问题,而不是"高性能、高可靠"问题,后面才不会跑偏。
2. 核心模块拆解与关键实现
2.1 任务编排:有限状态机管住模型
这是 hermes-agent 里最值得展开的部分。模型在你给它的自由度越大,"发挥"空间越大,出错概率也越大。所以我用有限状态机把 Agent 的执行过程框起来。状态定义很简单:
- PLANNING:根据用户目标制定执行计划,产出一个有序的工具调用序列。
- TOOL_CALLING:从计划中取出下一步,让模型结构化输出要调用的工具名和参数。
- TOOL_RUNNING:由确定性代码实际执行工具,不经过模型,避免模型"手抖"改参数。
- OBSERVING:把工具执行结果交给模型,让它判断下一步是继续调用、修改计划还是结束。
- FINISHED:任务正常结束,汇总结果。
- FAILED:达到最大迭代次数或出现不可恢复错误,进入失败分支。
状态迁移全部由代码控制,模型只是每个状态下做决策的那个组件。这样做还有个额外好处:一旦出错,你能明确知道是在哪个状态挂的,日志里能直接定位问题环节,而不是对着一大段对话记录瞎猜。
2.2 工具注册与动态路由:参数 schema 是命根子
工具注册我用的是装饰器,一个 Python 函数就是一个工具。关键不在于函数本身,而在于它的元信息:名称、描述、参数 JSON Schema。模型决定调不调这个工具,基本只靠这些元信息,所以描述的措辞直接影响成功率。
我举一个真实教训。我注册过一个用错率极高的工具,一开始描述只写了"计算订单金额",模型经常把它用在"计算运费"上。后来我把描述改成了"根据商品价格和数量计算订单总金额,不含运费、不含折扣,适用于订单明细行级别的汇总",并给参数加上了详细约束,准确率立刻从 70% 出头提到了 95% 以上。这个小例子很能说明问题:工具描述不是写给代码维护者看的,是写给模型看的,要把边界条件和例外说清楚。
动态路由方面,hermes-agent 采用"结构化输出优先"策略。每次决策时,模型输出的是一个严格 JSON,包含 tool_name、tool_args、reason。拿到 JSON 之后,去工具注册表匹配,再用 JSON Schema 做参数校验,校验不过就返回给模型一次错误信息,让它自己修正。这个错误回传机制非常关键,能大幅减少因参数格式问题导致的整单失败。
提示:给工具的 name 加上领域前缀,比如 weather__get_temperature,而不是笼统的 get_data。模型在多个相似工具之间做区分时,前缀能显著提高选择的准确率。
2.3 记忆与上下文管理:窗口不够时怎么办
记忆是 Agent 最容易翻车的地方,翻车通常有两种姿势:一种是什么都往 prompt 里塞,上下文直接爆掉;另一种是窗口压缩太激进,模型把关键事实给丢了。hermes-agent 把记忆分了两层:
短期记忆用滑动窗口,保留最近 N 轮的结构化交互记录,包括用户本轮输入、Agent 决策、工具调用和关键返回。长期记忆用向量库,每次任务结束把结论性内容总结后写入,下次任务如果要查历史,先做相似度召回,把命中的片段放回上下文。
上下文管理里有几个很值得处理的细节。工具返回结果如果太大,比如查出 10 万行数据,我不会直接把全量塞回 prompt,而是先做摘要:行数、字段、统计值、抽样前 50 行。对模型来讲,决策通常不需要全文,只需要"数据大概长什么样"。历史对话压缩也不只是简单截断,而是让模型做一次总结,把"用户真实意图"和"已经完成的事实"提炼出来,再作为下一轮的 system 前缀。这套做法实测下来,能把长达三小时的复杂对话压缩到几百 token,模型对上下文的把握反而更准。
2.4 可观测性:没有日志链路的 Agent 根本不敢上线
Agent 和普通接口最大的不同在于,它不是一次调用就返回,而是一串有依赖关系的决策链。所以上线前我做的第一件事,就是把整条决策链记录下来。每条链路以 trace_id 开头,记录模型决策、工具入参、工具出参、状态迁移、耗时和 token 消耗。
这里我想特别提示一个很多人都会踩的坑:工具入参和出参必须原样记录,但绝不能只记录最终结果。早期版本里,我只记录了工具返回值,没记录参数,结果线上排查问题时,完全不知道模型当时传了什么导致结果异常。后来把参数加回去,很多诡异问题的定位时间从几个小时缩短到了十分钟以内。不过记录敏感数据时一定要脱敏,工具参数里如果包含密钥或用户隐私,日志里要打码,这个在医疗、金融类场景下尤其重要。
3. 从零搭建 hermes-agent 的实操步骤
3.1 环境准备与项目结构
说再多理论,不如直接动手。我建议你用 Python 3.10 以上版本,依赖尽量精简。整个项目跑起来后的核心结构是这样:
hermes-agent/ ├── agent/ │ ├── core.py # Agent 主循环与状态机 │ ├── memory.py # 短期与长期记忆 │ ├── registry.py # 工具注册表 │ └── trace.py # 日志链路 ├── tools/ │ ├── weather.py │ ├── calculator.py │ └── data_analysis.py ├── config.yaml # 模型、参数、限流配置 ├── requirements.txt └── main.py依赖就四个核心库:openai(或对应模型 SDK)、pydantic、pyyaml、httpx。向量库按需接入,早期甚至可以用一个简单的 JSON 文件代替,先把链路跑通再优化,不要一上来就上重组件。
3.2 核心执行循环代码骨架
hermes-agent 的主循环是一个典型的 while-until 结构,核心代码不长,但每一行都有讲究:
from enum import Enum class State(str, Enum): PLANNING = "planning" TOOL_CALLING = "tool_calling" TOOL_RUNNING = "tool_running" OBSERVING = "observing" FINISHED = "finished" FAILED = "failed" class HermesAgent: def __init__(self, llm, registry, max_iterations=10): self.llm = llm self.registry = registry self.max_iterations = max_iterations def run(self, user_input: str) -> dict: state = State.PLANNING context = {"user_input": user_input, "history": []} for step in range(self.max_iterations): if state == State.FINISHED: return {"status": "ok", "data": context.get("answer")} if state == State.FAILED: return {"status": "failed", "reason": context.get("error")} if state == State.PLANNING: plan = self.llm.plan(context) context["plan"] = plan state = State.TOOL_CALLING elif state == State.TOOL_CALLING: decision = self.llm.decide_next_tool(context) tool = self.registry.get(decision["tool_name"]) validated = tool.validate_params(decision["tool_args"]) if validated.ok: context["pending_tool"] = (tool, validated.args) state = State.TOOL_RUNNING else: context["validation_error"] = validated.msg state = State.TOOL_CALLING # 让模型修正参数 elif state == State.TOOL_RUNNING: tool, args = context["pending_tool"] result = tool.fn(**args) context["history"].append( {"tool": tool.name, "args": args, "result": result} ) state = State.OBSERVING elif state == State.OBSERVING: verdict = self.llm.observe_and_decide(context) if verdict.done: context["answer"] = verdict.answer state = State.FINISHED else: state = State.TOOL_CALLING return {"status": "failed", "reason": "max_iterations exceeded"}这段代码是核心逻辑的简化版,但状态机的流转已经表达得很清楚。每一步迭代里,模型只做决策,实际执行一律走确定性代码。注意参数校验失败的时候,状态会切回 TOOL_CALLING,把校验错误信息带回给模型,让模型自己修正参数——这个闭环是整个链路稳定性的关键。
3.3 接入一个真实工具链
光有框架没有工具是跑不起来的。我用一个很常见的场景演示:用户问"北京和上海今天的温差是多少",Agent 需要先调两个城市的天气接口,再算温差,最后给出结论。先注册一个天气工具:
# tools/weather.py from agent.registry import register_tool @register_tool( name="weather__get_city_temperature", description="获取指定城市当天的实时温度,城市名必须是中文标准名称,例如北京、上海,不支持缩写", params_schema={ "type": "object", "properties": { "city": {"type": "string", "description": "城市名,中文标准名称"} }, "required": ["city"], }, ) def get_city_temperature(city: str) -> dict: # 这里接真实天气 API,返回 {"city": city, "temperature": 28} return mock_weather_api(city)第二个工具是计算温差,模型通过第一个工具拿到两组温度后,会自动决定调用 calculator 工具做减法,而不是在模型内部心算。因为数值计算交给确定性代码更可靠,模型一拍脑袋算出的 28 - 31 = -3,你很难判断是算错了还是真就那样。整个流程里,模型负责"选什么工具、传什么参数",计算器负责"结果准不准",分工非常干净。
3.4 调参经验:这几个参数直接决定成败
模型决策环节的参数不能照搬对话场景。我整理了一份自己在生产环境里反复试出来的参数表,你可以直接拿去当初始值:
| 参数 | 推荐取值范围 | 说明与踩坑记录 |
|---|---|---|
| temperature | 0.1~0.3 | Agent 决策要低随机性,默认对话的 0.7 会频繁导致选错工具 |
| max_iterations | 8~15 | 太低了复杂任务完不成,太高容易拖长链路、增加成本 |
| timeout | 30~60s | 工具接口要单独设超时,避免第三方 API 卡死整个链路 |
| retry | 2~3 次 | 工具执行失败不要立刻放弃,重试通常能覆盖瞬时故障 |
| 上下文窗口目标 | 不超过窗口的 60% | 给模型决策留足余量,接近满窗时强制触发压缩 |
这里重点说下 temperature。很多人在 Agent 里沿用 Chat 场景的参数设置,结果模型一会儿换一种工具组合,根本没法复现。我实测下来,Agent 场景里 temperature 超过 0.5,任务成功率会明显下降。把这组参数写进 config.yaml,按环境隔离配置,能帮你省掉绝大多数"刚才还能跑,现在不行了"的玄学问题。
4. 实战效果与性能对比
4.1 用 hermes-agent 跑一次数据分析任务
纸上谈兵没意思,我说一个真实任务:给运营同事做一个"销售数据月度汇总"的自动分析。输入是一份 CSV,包含订单编号、日期、品类、销售金额、销售数量。用户只需要在对话框里说一句:把 4 月的销售按品类汇总,找出销量最高的前三个品类,并给出环比变化。
hermes-agent 的执行过程大致是:
- PLANNING:模型判断需要先读文件,再按品类聚合,然后做排序,最后生成结论。
- 调用 read_file 工具读取 CSV 前 50 行和列名,确认数据结构。
- 调用 run_python_code 工具,让模型生成一段 pandas 聚合代码,由沙箱环境执行。
- 拿到聚合结果表后,模型判断还需要环比数据,于是再调用一次 run_python_code。
- 所有数据齐了,模型写出一段带数字引用的结论,任务结束。
整个过程一共 6 轮工具调用,耗时 40 秒左右,大头是 LLM 推理。人工做同样的分析,从拉数据到出结论大概需要 10 到 15 分钟。这不是说 Agent 比你更聪明,而是它把"看结构、写代码、跑结果、看数字"这类机械步骤压缩了,人只需要审核最终结论。
4.2 对比直接单次调用 LLM 的差距
可能有人会说:这个任务直接让大模型写一段 pandas 代码不就完了吗?为什么还要绕 Agent?我还真做过对比测试。同样的需求,方案 A 是单次调用 LLM 让它直接给出代码和答案,方案 B 是走 hermes-agent 的多轮工具链路。我跑了一百组不同类型的任务,结果差异非常明显:
| 指标 | 单次调用 LLM | hermes-agent |
|---|---|---|
| 一次成功比例 | 34% | 82% |
| 需要人工修正的比例 | 58% | 12% |
| 平均成本(按 token 计) | 1x | 1.8x |
| 平均耗时 | 8s | 42s |
| 结果可复现性 | 低,每次输出风格差异大 | 高,相同输入稳定复现 |
单次调用的优势是快、便宜,但"快"建立在高失败率上。一旦模型给的代码跑不通,你还得反复给它传错误信息,来回多轮之后,成本和时间反而超过 Agent 方案。hermes-agent 多花的成本,买的是每一步都有校验、失败可定位、中途可干预,这种确定性在生产环境里远比那几毛钱 token 重要。
5. 常见问题与排查经验
5.1 工具描述写不好,模型总是选错
这是最高频的问题。有人抱怨"模型很蠢,老是调错工具",我让他把工具库的 name 和 description 打印出来看一遍,马上原因就清楚了:描述太笼统、边界条件不清、参数示例缺失。模型像一个刚入职的实习生,你给它的工具说明书越具体,它上手越快。
排查方法很简单,把某个任务的决策日志翻出来,看模型选工具前读到的 description 是什么,再对照它的选择,基本就能发现是描述歧义还是 schema 缺陷。描述改清楚后,成功率往往立竿见影。
5.2 上下文越跑越长,窗口爆掉
复杂任务通常跑着跑着上下文就接近窗口上限。我常用的三板斧:第一,工具返回全量数据前先做摘要;第二,历史对话周期性压缩成梗概;第三,每一个子任务结束时,把中间过程从上下文里移除,只保留结论。这三板斧组合用,目前最长的任务跑到 50 多轮工具调用,上下文依然稳定在窗口的一半以内。
5.3 死循环和"原地打转"
有一次线上任务卡了 20 多分钟,日志显示模型反复调用同一个工具,每次参数还一模一样。这类问题根因是模型在某个决策点陷入了重复,它自己意识不到没有进展。除了设置 max_iterations,我还加了一个"无进展检测":如果连续三轮工具调用的输出和上一轮完全相同,或者状态没有发生任何有意义的迁移,就直接判定失败并终止,别再让它绕弯了。
5.4 并发上量后的限流问题
Agent 一次任务可能调用好几次模型接口,并发一上来,API 限流几乎是必然的。我的办法是两层兜底:外层用信号量控制同时进行的 Agent 任务数,内层对单次模型调用做指数退避重试。另外把任务拆成可重试队列,失败的任务标记好断点位置,恢复后从断点继续,而不是整体重跑,能省下大量 token。
5.5 最后分享两个小技巧
一是给每个 Agent 任务加一个 brief,也就是任务开始时生成的一句话目标描述,在之后的每一轮 prompt 里都重复这句 brief。别小看它,模型在长时间多轮执行中很容易偏题,反复看见原始目标能显著降低跑偏概率。二是所有工具函数里建议加统一的异常包装,让工具自己返回结构化错误,而不是抛异常打断主循环。工具报错本身也是一种观察结果,模型可以根据错误自动调整方案,这比整个任务直接崩掉要优雅得多。
我个人在把 Agent 接到业务接口的时候,还会在工具执行前加一个"人工确认开关",对高影响操作默认拦一道。宁可多一步确认,也不让模型在没人盯着的情况下做不可逆的修改。这是 Agent 落地过程中,我学到的最重要的一条经验。