源码版本: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 返回)内部,跟踪工具调用的失败次数和重复程度
三种检测模式:
- exact_failure: 完全相同的工具 + 完全相同参数连续失败 → 2 次 warning,5 次 block
- same_tool_failure: 同一工具(不管参数)连续失败 → 3 次 warning,8 次 halt
- 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 block | 5 | 3 | 同参数失败 3 次一定有问题,5 次太浪费 |
| same_tool_failure halt | 8 | 5 | 同一工具失败 5 次足够判断方向错误 |
| idempotent_no_progress block | 5 | 3 | 只读调用重复出同样结果 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 缺失的重试能力
分析过程中发现一些值得关注的空白地带,不属于本配置方案但可供参考:
- 跨 session 的全局速率限制器:目前只有 Nous Portal 有 Rate Guard,其他 provider 的 429 只在单 session 内退避
- Docker 级别的容器健康检查:Hermes 容器的
restart: unless-stopped不会感知 MCP 子进程失败(已有排查表,但无自动熔断) - MCP 子进程的重试:MCP 工具调用失败(如 Trilium 临时 500)走的是
run_agent.py的 tool 返回处理,没有独立的 MCP 连接重试