你的 AI Agent 刚刚与用户进行了 30 分钟的对话。他们讨论了项目需求,分享了偏好,做出了决定。然后用户输入 /new 来开始一个新会话。
Agent 尝试将该对话整合到长期记忆中。LLM 调用失败了。速率限制(Rate limit)。超时。或者模型返回了文本,而不是调用所需的 tool。
记忆消失了。三十分钟的上下文,烟消云散。
这种情况发生的频率比你想象的要高。在 Hermes Agent 运行时中,在我们添加专用回退机制之前,任何单一模型的记忆整合失败率约为 15%。对于一个本应是不可见的基础设施功能来说,这是不可接受的。
如果你正在构建围绕产品的表面层,而不仅仅是记忆子系统,请将此页面与 单 Key 聊天机器人指南 以及 AI API 速率限制指南 结合阅读。只有当 Agent 真正存在于一个可用的应用程序中时,记忆的持久性才有意义。
其他框架如何处理(其实它们并不处理)
大多数 AI Agent 框架将记忆整合视为一个简单的 LLM 调用。如果成功了,那很好。如果失败了,记忆就丢失了。
Hermes Agent 的基准测试在整合时使用了与对话相同的模型。一个 Claude Sonnet 调用成本为 $0.003,耗时 8 秒以上,仅仅是为了总结一段用户永远不会看到的聊天内容。当该调用失败(速率限制、超时、模型错误)时,框架会记录一条警告并继续运行。用户的上下文就这样丢失了。
nanobot,另一个流行的框架,也采用了相同的架构。单一模型,尝试一次,没有回退。整合函数甚至没有超时设置。缓慢的上游(Cloudflare 524 错误很常见)会阻塞整个会话,直到连接断开。
这两个框架都没有将整合与主模型分离。两者都没有针对记忆操作的回退逻辑。两者都没有区分“API 调用失败”和“API 调用成功但模型没有按要求执行”。
这些并非边缘案例。在任何单一模型上都有 15% 的失败率,一个每天运行 100 次整合的框架会在其中 15 次丢失记忆。一周下来,就有 105 次对话会被 Agent 彻底遗忘。
问题比重试逻辑更深
显而易见的修复方法是带有指数退避(exponential backoff)的重试。我们曾有过这种机制。它能很好地处理瞬时 HTTP 错误:
# 重试循环:1s → 2s → 4s 退避
for attempt in range(3):
try:
response = await acompletion(**kwargs)
return await self._collect_stream(response)
except (RateLimitError, APIConnectionError) as e:
await asyncio.sleep(RETRY_DELAYS[attempt])
这可以捕获 429 错误和网络波动。但有两种失败模式会漏掉:
失败模式 1:模型无法进行 tool 调用。某些模型,特别是在快速推理引擎上运行的小型模型,偶尔无法在复杂的 prompt 下生成有效的函数调用。API 返回 200,但内部包裹着 ServiceUnavailableError 或 MidStreamFallbackError。你的重试逻辑看到一个异常,重试同一个模型,得到同样的错误。
失败模式 2:模型“成功”但没有调用 tool。LLM 返回了一个完全有效的响应。HTTP 200。没有错误。但它没有使用结构化数据调用 save_memory,而是写了一段纯文本总结。你的重试引擎认为这是成功的。整合函数检查 tool 调用,没发现任何调用,于是放弃了。
第二种失败模式是最隐蔽的。传输层认为一切正常。业务层知道它失败了。对于一个不理解你的 tool schema 的模型,任何 HTTP 级别的重试都无济于事。
双层回退架构
我们通过在不同层级运行的两个独立回退循环解决了这个问题:
用户发送 /new
│
▼
consolidate() ─── 业务层回退 (Business Layer Fallback)
│ "模型调用 save_memory 了吗?"
│ 否 → 尝试链中的下一个模型
│
▼
_chat_with_retry() ─── 传输层回退 (Transport Layer Fallback)
│ HTTP 错误 → 指数退避
│ 重试耗尽 → 遍历回退链
│
▼
MODEL_MAP 回退链:
llama-3.3-70b ─$0.59/M─→ qwen3-32b ─$0.29/M─→ llama-4-scout ─$0.11/M─→ gpt-4.1-mini ─→ claude-haiku
(394 TPS) (662 TPS) (594 TPS) (可靠) (最后手段)
第 1 层处理传输失败。第 2 层处理业务逻辑失败。回退链在两层之间共享,在中央目录中定义一次。
这与“重试同一模型”的方法有着本质的区别。当一个模型无法调用 tool 时,使用相同的 prompt 重试很少奏效。切换到具有不同权重和不同 tool 调用行为的另一个模型则有效。
模型目录:唯一事实来源
我们目录中的每个模型都有一个可选的 fallback 字段,指向下一个要尝试的模型:
@dataclass(frozen=True)
class ModelEntry:
id: str
label: str
tier: str
description: str
fallback: str | None = None
hidden: bool = False # 对用户侧的 /model 列表隐藏
MODEL_CATALOG = [
# 用户可见模型(用户可以切换的 16 个模型)
ModelEntry("claude-sonnet-4-6", "Claude Sonnet 4.6", "standard",
"Recommended", fallback="claude-sonnet-4-5"),
ModelEntry("gpt-4.1-mini", "GPT-4.1 Mini", "economy",
"Stable tool calling", fallback="claude-haiku-4-5"),
# 隐藏的整合模型(仅限内部使用)
ModelEntry("llama-3.3-70b-versatile", "Llama 3.3 70B (Groq)", "economy",
"394 TPS", fallback="qwen3-32b", hidden=True),
ModelEntry("qwen3-32b", "Qwen3 32B (Groq)", "economy",
"662 TPS", fallback="llama-4-scout-17b-16e-instruct", hidden=True),
# ...
]
hidden=True 标志将内部模型排除在用户可见的 /model 命令之外,同时仍参与回退链。用户看到 16 个可以切换的模型。系统实际使用 19 个。这三个隐藏模型专门用于记忆整合等后台任务,在这些任务中,速度和成本比对话质量更重要。
该目录是所有模型路由的唯一事实来源。在回退链中添加新模型只需添加一行代码。无需同步配置文件,无需更新环境变量,无需修改部署脚本。
传输层:带循环检测的链式回退
重试引擎使用已访问集合(visited set)遍历回退链,以防止无限循环:
async def _chat_with_retry(self, kwargs, original_model):
# 第一阶段:主模型上的指数退避
for attempt in range(3):
try:
response = await acompletion(**kwargs)
return await self._collect_stream(response)
except (RateLimitError, APIConnectionError, APIError) as e:
await asyncio.sleep(RETRY_DELAYS[attempt])
except AuthenticationError:
return LLMResponse(content="API key invalid.", finish_reason="error")
# 第二阶段:遍历回退链
visited = {original_model}
current = original_model
while True:
entry = MODEL_MAP.get(current)
if not entry or not entry.fallback or entry.fallback in visited:
break
current = entry.fallback
visited.add(current)
# 为此模型解析正确的网关
gw = self._resolve_gateway_for_model(current)
resolved = self._resolve_model(current, gateway=gw)
fb_kwargs = {**kwargs, "model": resolved}
# 为目标模型的协议修正 api_base
if gw and gw.default_api_base:
fb_kwargs["api_base"] = gw.default_api_base
try:
response = await acompletion(**fb_kwargs)
return await self._collect_stream(response)
except Exception:
continue # 尝试链中的下一个
return LLMResponse(content="Service unavailable.", finish_reason="error")
visited 集合至关重要。如果没有它,像 A→B→A 这样的链将永远循环。有了它,引擎会确保每个模型只尝试一次。
网关解析也很重要。不同的模型需要不同的 API 格式。Claude 模型通过 Anthropic 格式的网关路由(没有 /v1 后缀)。GPT 模型通过 OpenAI 兼容的网关路由(带有 /v1)。Groq 模型使用另一个端点。回退引擎为链中的每个模型解析正确的网关,防止协议不匹配,例如将 Anthropic 请求发送到 OpenAI 端点。
这是大多数框架完全忽略的细节。它们假设所有模型都使用相同的协议。在生产环境中,跨越 4 种不同 API 格式的 19 个模型,这种假设会立即破裂。
业务层:Tool 调用验证
整合函数在顶部添加了自己的回退循环:
async def consolidate(self, session, provider, model, **kwargs):
visited = set()
current_model = model
while current_model and current_model not in visited and len(visited) <= 3:
visited.add(current_model)
response = await asyncio.wait_for(
provider.chat(messages=messages, tools=SAVE_MEMORY_TOOL, model=current_model),
timeout=30,
)
if response.has_tool_calls:
# 成功:提取并保存记忆
args = response.tool_calls[0].arguments
self.write_long_term(args["memory_update"])
self.append_history(args["history_entry"])
return True
# 模型没有调用 tool —— 尝试链中的下一个模型
entry = MODEL_MAP.get(current_model)
next_model = entry.fallback if entry else None
if next_model and next_model not in visited:
current_model = next_model
continue
return False # 没有更多回退选项
return False
这捕获了 _chat_with_retry 返回成功响应(HTTP 200,有效内容)但模型未使用 tool 的情况。整合函数检查 has_tool_calls,如果缺失,则移动到链中的下一个模型。
超时包装器(asyncio.wait_for)也会触发回退。如果一个模型耗时超过 30 秒(在缓慢的上游出现 Cloudflare 524 错误时很常见),该函数会捕获 TimeoutError 并尝试下一个模型,而不是无限期地阻塞用户的会话。
为什么选择 Groq 进行整合
记忆整合是一项后台任务。用户看不到输出。他们只需要它起作用。这使其成为快速、廉价模型的完美候选者。
大多数框架对所有任务都使用昂贵的模型。如果你运行 Claude Sonnet 进行对话,你也在运行 Claude Sonnet 进行记忆整合。对于一项生成人类永远不会阅读的输出的任务,这需要每百万输入 token 花费 $3 且每次整合耗时 8 秒以上。
我们将整合与对话模型完全解耦。对话使用用户选择的任何模型。整合使用由 Groq 托管的专用模型链:
| 模型 | 速度 | 输入成本 | 输出成本 |
|---|---|---|---|
| llama-3.3-70b-versatile | 394 TPS | $0.59/M | $0.79/M |
| qwen3-32b | 662 TPS | $0.29/M | $0.59/M |
| llama-4-scout-17b-16e | 594 TPS | $0.11/M | $0.34/M |
| gpt-4.1-mini (此前) | ~150 TPS | $0.40/M | $1.60/M |
主模型(llama-3.3-70b)在约 5 秒内整合一个包含 60 条消息的会话。之前的默认模型(gpt-4.1-mini)需要 8 秒以上。每次整合的成本从约 $0.003 降至约 $0.001。
权衡之处在于:Groq 模型在复杂 prompt 下的 tool 调用可靠性较低。这正是双层回退存在的原因。当 llama-3.3-70b 无法调用 tool 时,qwen3-32b 会接手。如果这也失败了,llama-4-scout 会尝试。如果所有三个 Groq 模型都失败了,gpt-4.1-mini 以近乎 100% 的 tool 调用可靠性来处理它。
在生产环境中,我们看到主模型在大约 85% 的时间内都能成功。在不到 2% 的整合中,链条会到达 gpt-4.1-mini。总失败率:实际上为零。
生产环境结果
我们将此部署到两个 Hermes Agent 运行时实例,并使用真实的 Telegram 对话进行了测试。
第一次部署(仅单层回退):
Memory consolidation (archive_all): 56 messages
llama-3.3-70b-versatile → "Failed to call a function"
Falling back → qwen3-32b
qwen3-32b: LLM did not call save_memory, skipping
→ "Memory archival failed, session not cleared."
传输层捕获了第一次失败并进行了回退。但 qwen3-32b 返回了文本而没有调用 tool。单层回退无法处理这种情况。这正是其他所有框架都会默默丢失记忆的典型场景。
第二次部署(双层回退):
Memory consolidation (archive_all): 60 messages
model=llama-3.3-70b-versatile → success
Memory consolidation done: 60 messages remaining
相同的模型,相同的消息量。这次第一次尝试就成功了。tool 调用失败的间歇性特征正是你需要回退链而不是单一备份模型的原因。
当主模型确实失败时,链条会捕获它:
llama-3.3-70b → tool call failed
→ consolidate() fallback → qwen3-32b
→ qwen3-32b didn't call tool
→ consolidate() fallback → llama-4-scout
→ llama-4-scout didn't call tool
→ consolidate() fallback → gpt-4.1-mini
→ gpt-4.1-mini called save_memory ✓
Memory consolidation done
尝试了四个模型,记忆保存成功。用户看到“新会话已开始”,完全不知道发生了这一切。
架构差距
记忆系统与替代方案的功能对比:
| 能力 | 典型的 AI Agent 框架 | 双层回退设计 |
|---|---|---|
| 整合模型 | 与对话相同(昂贵、缓慢) | 独立模型链,Groq 加速 |
| 失败处理 | 记录警告,丢失记忆 | 双层回退,5 层深度 |
| 传输回退 | 重试同一模型 3 次 | 跨不同模型的链式回退 |
| 业务逻辑回退 | 无 | Tool 调用验证 + 模型切换 |
| 超时保护 | 无(Cloudflare 524 阻塞会话) | asyncio.wait_for(timeout=30) + 回退 |
| 会话截断 | 无(上下文无限增长) | 整合后截断旧消息 |
| 历史搜索 | 无 | HISTORY.md 滚动窗口,可 grep 搜索 |
| 内部模型 | 不支持 | hidden=True 用于仅系统模型 |
| 循环预防 | 不需要(没有链) | visited 集合防止 A→B→A 循环 |
| 网关解析 | 假设单一 API 格式 | 带协议检测的单模型网关 |
表中的每一行都代表了我们亲身经历过或在其他框架的问题追踪器中观察到的生产环境失败。双层回退、隐藏模型目录、单模型网关解析、超时触发的回退:这些在 nanobot 或我们研究过的任何其他开源 Agent 框架中都不存在。
我们学到了什么
“请求成功”并不等于“任务成功”。通用的重试引擎在 HTTP 级别运行。它们无法知道一个带有有效 JSON 的 200 响应实际上是一个失败,因为模型没有使用你要求的 tool。业务关键型操作需要它们自己的成功标准和回退逻辑。
小模型与大模型的失败方式不同。大模型(GPT-4.1, Claude Sonnet)在被要求时几乎总是会调用 tool。快速推理引擎上的小模型有时会生成看起来有效但完全忽略 tool schema 的响应。这不是你可以通过 prompt 工程修复的 bug。这是一个需要架构缓解的能力差距。
使用生产数据测试,而不是合成数据。我们最初使用 6 条合成消息进行的测试在每个模型上都通过了。包含 tool 调用历史、时间戳和混合语言的真实 60 条消息会话在三个 Groq 模型中的两个上都失败了。真实数据的复杂性会暴露干净的测试数据永远无法暴露的失败模式。
这也是为什么 AI API 速率限制指南 在这里很重要。记忆系统不仅仅需要一个“更好的模型”。它需要一个传输策略、一个业务逻辑成功检查,以及一个在普通供应商故障下不会崩溃的回退阶梯。
本文描述了来自 Hermes Agent 的可靠性模式。将此处的架构模式作为记忆持久性、回退设计和网关感知模型路由的参考。
需要通过一个 API key 访问 300 多个 AI 模型?tokenlab.sh 提供对 OpenAI、Anthropic、Google、DeepSeek、Groq 等的统一访问。
