【hermes】异常的处理

$

到上一篇为止,我们的 agent 已经很强了。循环跑起来了,工具系统装好了,对话能存到磁盘,提示词动态组装,上下文太长还能自动压缩。它不再是一个 demo,是一个真的在做事的程序。

但它有一个致命弱点——脆弱。

真实世界里什么都会出错。网络抖一下,超时了。请求太频繁,被限流了。模型输出写到一半,断了。API key 额度用完,直接 401。模型被提供商下线,404。

如果主循环没有错误处理,agent 会在第一个错误上直接崩溃。就像一个能力很强的人,什么活都能干,但一遇到堵车就原地放弃。这不是能力问题,是韧性问题。

这篇要做的事情就一件:让 agent 遇到错误不崩溃,而是自动恢复。不聊代码,聊决策——面对同样的问题,你会怎么设计?Hermes Agent 又是怎么选的?

你的直觉方案:try/except + retry

遇到错误怎么办?大多数人的第一反应是:捕获异常,然后重试。



try:

    调用模型()

except:

    重试()

简单直接。但问题马上就来了。

限流了应该等一等再试,你却在立刻重试,结果被限得更狠。上下文太长了应该压缩,你却在反复重试一个注定失败的请求。认证失败了应该换凭证,你却在死循环里永远出不来。

所有错误走同一条路,结果就是:该等的去压缩了,该放弃的在死循环。

这就像医院急诊室,不管什么病人都走同一个流程。骨折的去眼科排队,心脏骤停的在挂号窗口等。不是医生不行,是没有分诊。

第一步:先分类——急诊分诊台

Hermes Agent 的做法不是直接重试。它先对错误做分类,再根据分类选择恢复策略。

这就像医院急诊的分诊台。 护士不会给每个病人同样的处理。她先看一眼:这个能等,那个不能等,这个需要马上送手术室。先判断伤情,再决定走哪条路。

错误分类器做的事情一样:把”HTTP 状态码 + 错误消息”翻译成”该重试 / 该压缩 / 该转移 / 该放弃”。主循环不需要理解每种错误的具体内容,只看分类结果里的几个布尔标记就知道该走哪条路。

分类之后,四条恢复路径就清楚了:

路径一:输出被截断。 模型输出写到一半断了(finish_reason: length)。不是模型不会了,是这一轮输出空间不够了。处理方式是续写。

路径二:上下文太长。 API 返回 400,错误消息里提到 context overflow。处理方式是触发上下文压缩(S05 讲过),然后重试。

路径三:临时故障。 429 限流、503 过载、网络超时。处理方式是退避等待,然后重试。

路径四:不可恢复。 401 认证失败、404 模型不存在、额度用完。处理方式是故障转移到备用模型,或者直接放弃。

错误分类与四条恢复策略
错误分类与四条恢复策略

每条路径有独立的重试次数上限——就像排队叫号,你最多排 3 次。3 次还叫不到,换一种方式。没有这个上限,循环可能永远不结束。

第二条路:被截断了——”请从刚才断掉的地方接着说”

模型输出被截断,是真实环境里非常常见的情况。模型不是不会回答,是这一轮能输出的 token 用完了。就像你跟人打电话,信号不好,说到一半断了。

你的直觉方案是什么?

方案一:重新问一遍。——浪费。模型之前的输出其实是对的,只是没说完。重新问一遍,模型可能换个说法,甚至给出不同的答案。

方案二:告诉模型”继续”。——好一点,但不够。你只说”继续”,模型经常会重新总结前面的内容,或者换个开头重新开始。

方案三:精确告诉模型”从刚才断掉的地方接着写,不要重新开始,不要重复,不要总结”。——这才是正确做法。

Hermes 的续写提示是这样写的:”Your response was cut off. Continue EXACTLY from where you stopped. Do not restart, do not repeat, do not summarize what came before.”

这就像你跟朋友打电话断了,你拨回去说”从刚才断掉的地方接着说”,而不是”我们重新聊”。 朋友知道你们之前在聊什么,模型也知道。你只需要告诉它:别从头来。

但续写不能无限次。Hermes 给续写设了上限:最多 3 次。3 次还没说完,说明这个回复本身就超出了模型能力范围,应该换个策略或者向上报告。

这里有一个 Hermes 的独特设计:thinking-budget 检测。 有些支持”思考”功能的模型,可能把所有输出 token 都花在内部推理上,留给实际回复的 token 是零。这时候 finish_reason 也是 length,但续写没有意义——模型根本没开始写回复。Hermes 会检测这种情况,直接报错,不浪费 3 次续写重试。

第三条路:临时故障——排队叫号 + 随机等待

限流(429)、服务过载(503)、网络超时——这些都是临时故障。服务器没坏,只是暂时忙不过来。

你的直觉方案是什么?

方案一:立刻重试。——服务器本来就忙,你立刻重试只会加剧压力,结果是被限得更狠。

方案二:等一个固定时间再试,比如每次都等 10 秒。——好一点,但在 Gateway 模式下有多个会话同时运行。所有会话都等 10 秒,然后同时重试,又撞在一起了。这在分布式系统里有个专门的名字:雷群效应(thundering herd)。

方案三:等一段时间再试,等待时间指数递增,再加一个随机偏移。——这是 Hermes 的做法。

指数递增是什么意思? 第一次等 5 秒,第二次等 10 秒,第三次等 20 秒。每次翻倍。目的是给服务器越来越多的喘息时间。如果等了两轮服务器还是忙,说明问题不是暂时的,别继续死磕了。

随机偏移是什么意思? 在确定的等待时间上,加一个随机的浮动。比如第二次本该等 10 秒,实际可能等 8 秒或者 13 秒。这样多个会话的重试时间就错开了,不会撞在一起。

这就像十字路口没有红绿灯时,大家不会同时起步,而是各自犹豫一下再走。 这个”犹豫”就是随机抖动。如果所有人都精确地等 10 秒然后同时起步,路口又会堵死。每个人犹豫的时间不一样,自然就错开了。

退避重试的核心思想就两句话:指数递增给服务器喘息时间,随机抖动打散并发重试。

第四条路:不可恢复——备用发电机

认证失败(401/403)、模型不存在(404)、额度用完——这些不是”等一等就能好”的问题。当前的模型或凭证已经不可用了。

你的直觉方案是什么?

方案一:直接报错,让用户处理。——可以,但用户体验差。用户可能根本不知道发生了什么,也不知道该换什么模型。

方案二:自动切换到备用模型。——这就是 Hermes 的做法。

这就像备用发电机。 主供电断了,自动切到发电机,用户甚至不需要知道发生了什么。等主供电恢复了,再自动切回来。

故障转移在 Hermes 里特别优雅。因为 Hermes 通过 base_url 支持任意模型提供商——OpenRouter、Anthropic、本地端点——故障转移只是”换一组配置”。换一个 base_url,换一个 API key,换一个模型名。不需要改代码,不需要改消息格式,不需要改循环逻辑。

但有一个关键细节:故障转移是临时的。

切到备用模型后,下一轮对话开始时,Hermes 会先尝试切回主模型。如果主模型恢复了,自动回去。

这就像你常去的餐厅关门了,你去隔壁吃了一次。下次你常去的餐厅开了,你还是回去吃。 你不会因为隔壁餐厅开了一次,就永远不去老店了。

故障转移与主模型恢复流程
故障转移与主模型恢复流程

如果不做主模型恢复,用户会一直用着可能更弱或更贵的备用模型,而且自己都不知道。

隐藏的前置步骤:打电话前先检查话筒

还有一个容易忽略的步骤。

每次对话开始前,Hermes 会先检查 API 客户端的连接是否健康。如果上一轮留下了死连接——比如上次超时后 TCP 连接还挂着——主动清理掉。

这就像打电话前先检查话筒有没有声音。 不等到打通了才发现对面没声。

这个检查看似简单,但能避免一种很隐蔽的问题:新请求挂在僵尸连接上,白白等一个超时,然后才发现连接早就断了。先检查,再发请求,省掉一次无意义的等待。

多提供商的代价:更多错误种类

前面讲的分类器看起来很简单:看 HTTP 状态码,判断走哪条路。

但 Hermes 支持 200 多个模型和多个提供商。每个提供商的错误格式和状态码含义不完全一样。同样是”额度用完”,提供商 A 返回 402,提供商 B 返回 403,错误消息的措辞也不一样。

分类器要做的事情是:从不同格式的错误消息中,提取出统一的故障原因。不管错误消息长什么样,分类结果都是那几个布尔标记。主循环只看标记,不关心具体是哪个提供商报的错。

这就像急诊分诊台不管病人说的是方言、普通话还是外语,护士只关心”这个病人需要什么科室”。 语言不同,但分诊结果是一样的。

复盘:主循环现在同时维护三件事

回头看,加了错误恢复之后,主循环不再是简单的”调模型、执行工具”。它现在同时维护三件事:

职责 来源 管什么
任务推进 S01 调模型、跑工具
上下文预算 S05 压缩上下文
错误恢复 S06 分类、重试、转移

完整流程是这样的:调模型。如果调用失败,分类错误,选择恢复路径。如果输出被截断,续写。如果成功,正常执行工具。任何恢复路径失败,向上报告。

六层能力叠加之后,变化集中在错误恢复层,其他层纹丝不动:

能力 S01-S05 +S06
截断恢复 续写(最多 3 次)
上下文溢出 压缩 压缩 + 自动触发
临时故障 崩溃 退避重试
模型不可用 崩溃 故障转移
连接健康 不检查 主动检查
核心循环 不变 不变

⚠️ 初学者最容易犯的错:

一,把所有错误都当成一种错误。该续写的去压缩了,该等待的在死循环,该放弃的在无限重试。

二,没有重试预算。每条恢复路径都要有上限。续写最多 3 次,退避最多 3 次。没有预算,循环可能永远不结束。

三,续写提示写得太模糊。只写一个”continue”通常不够。你要明确告诉模型不要重复、不要重新总结、直接从中断点接着写。

四,退避不加随机抖动。确定性的退避时间在多会话场景下会导致所有重试撞在一起。随机抖动打散它们。

五,故障转移后不尝试恢复主模型。切到备用模型就再也不回来,用户会一直用着可能更弱或更贵的备用模型。

最后,三个能带走的判断。

一,先分类再恢复,是所有错误处理系统的第一原则。 不是所有错误都该同一种处理方式。医院急诊先分诊,网络协议先分类,agent 也一样。如果你的系统只有一种错误处理方式,大概率它处理不好任何一种错误。

二,每条恢复路径必须有独立的预算。 没有预算的恢复就是死循环。预算不是限制,是安全网。就像排队叫号,最多排 3 次不是不让你看病,是防止你永远排下去。做产品也一样——每个自动重试都要有上限,否则一个临时故障就能拖垮整个系统。

三,故障转移是临时的,不是永久的。 切到备用模型后还要尝试切回来。降级方案是临时措施,不是新常态。做产品也一样——系统降级后要自动恢复,不能让用户永远用着降级版本而不自知。

阶段 1 完结

到这里为止,阶段 1 的六块拼图全部到位。你已经有了一个能工作、能持久化、能组装提示词、能压缩上下文、能从错误中恢复的单 agent。

阶段 2 开始补智能层:记忆、技能、安全、委派、配置。这些是让 agent 从”能干活”变成”会干活”的东西。但那是后面的故事了。

📚 Hermes Agent 源码精读 · 系列第 06 篇

← 上一篇:05-上下文压缩