🔄 Hermes 重试机制深度分析与最优配置方案

源码版本:Hermes Agent v2026.4.30 (commit: run_agent.py #11461-#13320, agent/tool_guardrails.py, agent/retry_utils.py, agent/error_classifier.py)

核心发现:Hermes 有 4 层独立的重试/循环机制

每一层的职责、触发条件、默认参数和熔断手段都不同。原文的"90 次重试"实际上对应的是 max_turns: 90——这不是 API 重试,而是 Agent 每轮对话的最大工具调用步数。


第一层:LLM API 调用重试

配置键: agent.api_max_retries: 3

源码: run_agent.py #11460-#13320

使用方式: 每次 Agent 需要调用 LLM 时自动触发,失败后重试,最多 3 次

退避算法 (agent/retry_utils.py):

  • jittered_backoff: base_delay=5.0s, max_delay=60.0s, jitter_ratio=0.5
  • 指数增长: 尝试1≈5s, 2≈10s, 3≈20s, 4≈40s, 5+→60s
  • 随机抖动 (jitter_ratio=0.5) 防止多个 session 同时重试造成惊群效应
  • Retry-After header 优先于算法(最大 120s 上限)

错误分类 (agent/error_classifier.py — 15+ 种):

auth, auth_permanent, billing, rate_limit, overloaded, server_error,
timeout, context_overflow, payload_too_large, image_too_large,
model_not_found, provider_policy_blocked, format_error,
thinking_signature, long_context_tier, oauth_long_context_beta_forbidden,
llama_cpp_grammar_pattern, unknown

熔断链: 3 次重试用完 → _try_recover_primary_transport()(重建连接池,针对 ReadTimeout/ConnectTimeout 等瞬时故障) → _try_activate_fallback()(切换到 fallback provider) → 最终失败

特殊恢复路径(不消耗重试次数):

  • 413 Payload Too Large → 压缩消息后重试(最多 3 次压缩)
  • Context Overflow → 压缩消息后重试
  • Image Too Large → 缩小图片后重试
  • llama.cpp grammar error → 剥离 regex 后重试
  • Anthropic 长上下文 tier gate → 降级到 200K 上下文后重试
  • Nous Portal rate limit guard → 跳过调用直接 fallback

第二层:Tool Loop Guardrails(工具调用循环护栏)

配置键: tool_loop_guardrails.{warnings_enabled,hard_stop_enabled,warn_after,hard_stop_after}

源码: agent/tool_guardrails.py

核心逻辑: 在同一个 turn(同一次 LLM 返回)内部,跟踪工具调用的失败次数和重复程度

三种检测模式:

  1. exact_failure: 完全相同的工具 + 完全相同参数连续失败 → 2 次 warning,5 次 block
  2. same_tool_failure: 同一工具(不管参数)连续失败 → 3 次 warning,8 次 halt
  3. idempotent_no_progress: 只读幂等工具返回完全相同结果 → 2 次 warning,5 次 block

当前配置状态: ⚠️ hard_stop_enabled: false → 熔断功能关闭,只有 warning 没有实际阻断

幂等工具列表: read_file, search_files, web_search, web_extract, session_search, browser_snapshot/browser_console/browser_get_images, mcp_filesystem_* 等

变异工具列表: terminal, execute_code, write_file, patch, todo, memory, skill_manage, browser_click/browser_type/browser_press/browser_scroll/browser_navigate, send_message, cronjob, delegate_task, process


第三层:Agent 主循环迭代预算

配置键: agent.max_turns: 90 / goals.max_turns: 20

源码: run_agent.py #11148-#14195, cli.py #309

核心逻辑:

  • 这是每轮对话的工具调用总步数上限(含 LLM 调用)
  • 使用 IterationBudget 按次消费,到 0 后触发一键摘要并退出
  • gateway 模式下通过 HERMES_MAX_ITERATIONS 环境变量传递
  • cron 任务默认 max_turns=90
  • goals 模式使用 goals.max_turns: 20

默认值来源链:

cli.py: DEFAULT_CONFIG["agent"]["max_turns"] = 90
  → gateway/run.py: os.environ["HERMES_MAX_ITERATIONS"] = cfg["max_turns"]
    → run_agent.py: IterationBudget(self.max_iterations)
      → while loop: api_call_count < max_iterations AND budget.remaining > 0

第四层:上下文压缩

配置键: compression.{enabled, threshold, target_ratio}

逻辑: 上下文接近 token 限制时自动压缩历史消息(保留最近 20 条)

与重试的关系: 413 / Context Overflow 等错误会触发强制压缩后重试(不消耗 api_max_retries 额度)


最优配置方案

✅ 推荐配置 (config.yaml)

agent:
  max_turns: 40               # 默认 90 → 40(减少无谓浪费)
  api_max_retries: 3          # 保持 3 次(合理的 API 重试)
  tool_use_enforcement: auto  # 保持 auto

tool_loop_guardrails:
  warnings_enabled: true      # 保持 warning
  hard_stop_enabled: true     # 🔥 开启熔断(默认 false!)
  warn_after:
    exact_failure: 2          # 保持 2(同参数连续失败 2 次就警告)
    same_tool_failure: 3      # 保持 3
    idempotent_no_progress: 2 # 保持 2
  hard_stop_after:
    exact_failure: 3          # 🔥 3 次同参数失败 → 熔断(原 5)
    same_tool_failure: 5      # 🔥 5 次同工具失败 → 熔断(原 8)
    idempotent_no_progress: 3 # 🔥 3 次只读重复结果 → 熔断(原 5)

goals:
  max_turns: 15               # 目标模式限定 15 步

# 可选的 fallback 链(跨 provider 高可用)
fallback_providers:
  - provider: openrouter
    model: openai/gpt-4o-mini
  # - provider: anthropic
  #   model: claude-sonnet-4

逐项说明

1. max_turns: 90 → 40

  • 原值 90 次工具调用 ≈ 45 次 LLM 调用 + 45 次工具结果处理
  • 原文的"90 次重试"感来自于这里——如果模型选错方向,会浪费 90 步
  • 40 次足够完成绝大多数复杂任务(复杂开发 ≈ 20-30 步)
  • goals 模式再加一道防线:goals.max_turns: 15

2. api_max_retries: 3

  • 保持 3 次不变——这是最佳平衡点
  • 速率限制(429)→ 指数退避等待
  • 瞬时故障(timeout, connection reset)→ _try_recover_primary_transport 重建连接池
  • 持久故障(auth, model_not_found)→ 直接跳到 fallback,不浪费重试
  • 注意:SDK 层的 max_retries: 0(run_agent.py #6090)已关闭底层重试,由 Hermes 统一管理

3. hard_stop_enabled: false → true(关键!)

  • 当前 `hard_stop_enabled: false` 意味着 guardrails 只 warning,不阻断
  • 启用后:同参数失败 3 次 → 合成工具结果返回 "Blocked xxx: the same tool call failed N times"
  • LLM 看到阻断消息后自动切换策略,避免死循环
  • 幂等工具重复 3 次也熔断("use the result already provided or try a different query")

4. 阀门阈值下调

检测类型 当前值 推荐值 理由
exact_failure block53同参数失败 3 次一定有问题,5 次太浪费
same_tool_failure halt85同一工具失败 5 次足够判断方向错误
idempotent_no_progress block53只读调用重复出同样结果 3 次就该停

5. fallback_providers(可选高可用)

  • 配置备用 provider,在主 provider 重试用尽后自动切换
  • 示例:OpenRouter 主 → GPT-4o-mini 备
  • 跨 provider 的速率限制独立,不互相影响

可视化:一次失败的工具调用路径

LLM 返回 → 工具 A 执行失败
  │
  ├─ 1st retry (api_max_retries=3): 等 5s → 再试
  │   ├─ Guardrail warning? exact_failure ≥ 2? → 提示 "看起来像循环"
  │   └─ 失败
  ├─ 2nd retry: 等 10s → 再试
  │   ├─ Guardrail warning? same_tool_failure ≥ 3? → 提示 "换策略"
  │   └─ 失败
  ├─ 3rd retry: 等 20s → 再试
  │   ├─ Guardrail: exact_failure ≥ 3 (hard_stop!) → 阻断!
  │   │   合成结果: "Blocked A: failed 3 times with identical args"
  │   │   LLM 收到后换思路 → 调用工具 B
  │   └─ 3 次用完但未触发 hard_stop (disabled) → LLM 继续用 A → 又 3 次 → ...
  │
  └─ 如果 3 次用尽 + fallback:
       → rebuild_primary_transport (连接池重建)
       → activate_fallback (切换到备用 provider)
       → 最终失败并返回

总结:原文诊断的准确性

原文观点 Hermes 实际情况 评价
90 次重试是设计陷阱90 是 max_turns(工具调用步数),不是 API 重试次数✅ 直觉方向正确,但混淆了概念
3-5 次失败应熔断tool_loop_guardrails 正是这个机制,但 hard_stop 默认关闭✅ 完全正确
可恢复错误 vs 逻辑错误应区分error_classifier.py 有完整的 15+ 种分类 + 差异化恢复✅ Hermes 已经做得很完善
熔断(Circuit Breaker)Guardrail 提供 block/halt 机制,但不是外部 Circuit Breaker⚠️ 有 per-turn 的局部熔断,缺少跨 session 的 Rate Limiter
3-5 次最合理配置原值 exact_failure=5, same_tool=8, idempotent=5 偏大✅ 建议下调到 3/5/3

延伸:Hermes 缺失的重试能力

分析过程中发现一些值得关注的空白地带,不属于本配置方案但可供参考:

  1. 跨 session 的全局速率限制器:目前只有 Nous Portal 有 Rate Guard,其他 provider 的 429 只在单 session 内退避
  2. Docker 级别的容器健康检查:Hermes 容器的 restart: unless-stopped 不会感知 MCP 子进程失败(已有排查表,但无自动熔断)
  3. MCP 子进程的重试:MCP 工具调用失败(如 Trilium 临时 500)走的是 run_agent.py 的 tool 返回处理,没有独立的 MCP 连接重试