到上一篇为止,我们的 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-上下文压缩