12-Factor Agents 审计报告:Hermes Agent Fallback 链路

Codex 核验(2026-10-09):本报告是 2026-05-28 工作目录的历史审计,HEAD 未给出具体 SHA,配置、源代码行号及插件能力不代表当前环境,本次未读取凭据或重跑服务。按表中五项 5 分、七项 4 分重算为 53/60(88.3%);这是自定主观评分,不能理解为官方符合率。文内同时描述了 fallback 状态通知,故“无通知”应理解为通知粒度/覆盖不足。是否增加 fallback 需先核对当前配置与免费额度;付费例子不是已授权部署方案。

来源: humanlayer/12-factor-agents (HN 🔥475) 审计目标: Hermes Agent fallback 链路(provider 切换、错误恢复、凭证管理、可观测性) 审计日期: 2026-05-28 当前配置: fallback_providers: [](空链)、fallback_model 未设置


12 项原则速览

#原则核心思想
1Natural Language → Tool CallsNL 到结构化工具调用的翻译
2Own Your Prompts提示词工程不外包给框架
3Own Your Context Window掌控喂给 LLM 的每一段上下文
4Tools Are Structured Outputs工具 = LLM 输出的结构化 JSON + 确定性代码
5Unify Execution & Business State执行状态与业务状态合一
6Launch/Pause/Resume with Simple APIs简单 API 启动/暂停/恢复
7Contact Humans with Tool Calls用工具调用通知人类
8Own Your Control Flow自主控制流程(循环/中断/等待)
9Compact Errors into Context Window错误信息压缩回上下文
10Small, Focused Agents小粒度、单一职责的 Agent
11Trigger from Anywhere多入口触发(Slack/Email/Webhook)
12Stateless ReducerAgent 作为无状态 reducer

逐项审计

原则 1: Natural Language to Tool Calls(NL → 工具调用)

符合度: ★★★★★ (5/5)

Hermes 的整个对话循环就是建立在这个原则上。agent/conversation_loop.py 中的主循环:

  1. LLM 接收自然语言 + 工具 schema
  2. 输出结构化 tool_calls
  3. Hermes 的 handle_function_call() 将工具调用派发到对应实现
  4. 结果追加回消息列表

核心路由:resolve_provider_client() → resolve_runtime_provider() → 构建 OpenAI-compatible client → chat.completions.create(tools=tool_schemas)。

评价: Hermes 从设计上完全遵循此原则。工具 schema 自动从注册的工具生成,无需手动维护。


原则 2: Own Your Prompts(掌控提示词)

符合度: ★★★★☆ (4/5)

Hermes 提供多层级的 prompt 控制:

  • Skills 系统(~/.hermes/skills/)——用户可自定义 skill 注入系统提示
  • Prefill messages(prefill_messages_file)
  • 每个 profile 有独立的 system prompt
  • 不依赖外部框架生成的”黑盒”prompt

扣分点:

  • 内置的 scope-recall、context-engine 等插件会动态注入 prompt 片段,用户不一定完全感知
  • 没有类似 BAML 的声明式 prompt 版本管理
  • 技能 prompt 的覆盖优先级链(skill → profile → default)文档化不充分

fallback 相关关注: 切换 fallback 后,system prompt 不变(skills 不变),但模型能力变化可能导致提示词不再最优。


原则 3: Own Your Context Window(掌控上下文窗口)

符合度: ★★★★☆ (4/5)

Hermes 在 context 管理上功能丰富:

  • Context Compressor(agent/context_compressor.py)——自动压缩历史消息
  • Prompt Caching——支持 Anthropic 原生缓存和 OpenAI 缓存
  • Scope Recall——插件级长期记忆
  • hygiene_hard_message_limit——硬上限防溢出
  • protect_last_n / protect_first_n——关键消息保护

扣分点:

  • Fallback 激活时(try_activate_fallback)虽重建了 context compressor,但压缩策略仍基于新的 context_length
  • 没有显式的”窗口预算”追踪——无法在接近极限前主动告警
  • 不同 provider 的 context 窗口差异在 fallback 时可能导致截断

原则 4: Tools Are Structured Outputs(工具即结构化输出)

符合度: ★★★★★ (5/5)

Hermes 的工具系统完全基于 OpenAI 的 tool_calling 格式:

# 每个工具注册到 registry,生成 JSON schema
tools/registry.py → register() → tool_schemas → LLM API

工具调用结果格式化为标准 tool role 消息,追加回对话。与 12-factor 建议完全一致:

  • LLM 输出 JSON → 确定性代码执行 → 结果回填

Fallback 切换后工具集不变,但 provider 的 function calling 能力差异可能导致某些模型表现不一致。Hermes 对此有 _should_sanitize_tool_calls() 等适配逻辑。


原则 5: Unify Execution State and Business State(统一执行与业务状态)

符合度: ★★★★☆ (4/5)

Hermes 将 agent 状态统一存储在 SessionDB(SQLite)中:

  • 消息列表(business state)
  • 迭代计数、label、时间戳(execution state)
  • 两者储存在同一数据库

扣分点:

  • _try_activate_fallback() 修改 agent.provider / agent.model 等运行时属性,这些是执行状态,但切换后旧 session 仍记录原 provider
  • 没有在 session 层面记录 fallback 事件作为一等公民
  • 凭证池状态(CredentialPool)与 session 状态分离管理

原则 6: Launch/Pause/Resume with Simple APIs(简单 API 启动/暂停/恢复)

符合度: ★★★★☆ (4/5)

Hermes Gateway 架构:

  • 启动: HTTP API Server / Discord / Slack 等平台触发器
  • 会话持久化: SQLite SessionDB、JSON snapshot(可选)
  • 暂停/恢复: agent 睡眠通过 clarify_timeout + 人类审批流程
  • 看板系统: kanban 插件——支持异步派发和 Worker

扣分点:

  • Fallback 链路上没有 pause/resume 语义——primary 失败后要么立即切换到 fallback,要么失败
  • 没有”等待人类选择 fallback”的流程
  • 没有”凭证耗尽后暂停并等待充值”的持久状态

原则 7: Contact Humans with Tool Calls(工具调用联系人类)

符合度: ★★★★☆ (4/5)

Hermes 提供多层级的审批/人类介入机制:

  • Approval system(approvals.mode: auto|confirm)
  • 命令白名单(command_allowlist)
  • Gateway 平台通知(Discord/Slack 消息推回)
  • TIRITH 安全执行沙箱

扣分点:

  • Fallback 切换时没有通知人类或记录审批事件
  • 自动化 fallback 可能导致用户在不知道的情况下切换到不同(可能更贵/更慢)的 provider
  • 没有”所有 fallback 耗尽后通知管理员”的 hook

原则 8: Own Your Control Flow(掌控控制流)

符合度: ★★★★★ (5/5)

Hermes 完全拥有自己的控制流,这是其最核心的优势:

  • 原生异步循环(conversation_loop.py 中的 while True)
  • 完整的 error classification pipeline(agent/error_classifier.py)
  • 细粒度的重试策略:不同的 FailoverReason 触发不同的恢复动作
  • 插件 hooks(pre_api_request 等)允许外部扩展

fallback 控制流:

API 调用失败
  → error_classifier 分类(auth/rate_limit/timeout/...)
  → 重试(最多 api_max_retries 次)
  → 重试耗尽 → _try_activate_fallback()
    → fallback_chain 迭代 → resolve_provider_client()
    → 切换 client/model/provider/api_mode
    → 重置 retry_count = 0 → 继续
  → fallback 链耗尽 → 返回错误消息

问题: 此控制流仅在主对话循环内。Cron 作业的 fallback 在 cron/scheduler.py 中独立实现(行 1541-1576),没有共享 try_activate_fallback()。


原则 9: Compact Errors into Context Window(错误压缩回上下文)

符合度: ★★★★★ (5/5)

Hermes 在错误处理上非常成熟:

中心化错误分类器 (agent/error_classifier.py):

class FailoverReason(enum.Enum):
    auth / auth_permanent / billing / rate_limit
    overloaded / server_error / timeout
    context_overflow / payload_too_large / image_too_large
    model_not_found / provider_policy_blocked

每种错误类型映射到不同的恢复策略:

  • auth → 刷新凭证 → 重试
  • rate_limit → 冷却(60s cooldown)→ fallback
  • context_overflow → 压缩 → 重试(不 fallback)
  • overloaded → 退避 → 重试

凭证池冷却机制 (agent/credential_pool.py):

EXHAUSTED_TTL_401_SECONDS = 5 * 60    # 5分钟
EXHAUSTED_TTL_429_SECONDS = 60 * 60   # 1小时
EXHAUSTED_TTL_DEFAULT_SECONDS = 60 * 60

评价: 这是 Hermes 在错误恢复上最强的部分。错误信息被结构化为确定的分类,分类结果指导恢复策略。

扣分点: 没有把 fallback 切换本身产生的错误(如 fallback provider 也失败)压缩回上下文——当前是 logger.error + 尝试下一个。


原则 10: Small, Focused Agents(小粒度、单一职责的 Agent)

符合度: ★★★★☆ (4/5)

Hermes 提供了多个粒度级别的 agent 机制:

  • Delegation system(agent.delegation)——子 agent 派发
  • 看板系统(kanban)——异步任务分解和工作池
  • Orchestrator——多 agent 编排
  • 子 agent 超时(child_timeout_seconds: 600)
  • 最大递归深度(max_spawn_depth: 1)

扣分点:

  • 子 agent 的 fallback 配置继承了主 agent 的 fallback_chain,但 runner 可能不同(oneshot.py 中的独立 resolve_runtime_provider 调用)
  • 当前 max_spawn_depth: 1 限制了嵌套深度,但子 agent 的 fallback 行为与父 agent 耦合
  • 没有”为特定子任务指定独立 fallback 链”的机制

原则 11: Trigger from Anywhere, Meet Users Where They Are(随处触发)

符合度: ★★★★★ (5/5)

Hermes Gateway 是多平台接入的典范:

  • Discord / Slack / Telegram / WhatsApp / Matrix
  • Web API Server / Webhook
  • Feishu / DingTalk / WeCom
  • Email / SMS
  • CLI / TUI
  • 每个平台有独立的 adapter(gateway/platforms/)

fallback 相关: 所有平台共享同一个 agent 实例和 fallback 链。fallback 事件通过 _emit_status() 推送到平台,但非阻塞。


原则 12: Stateless Reducer(无状态 Reducer)

符合度: ★★★★☆ (4/5)

Hermes 的会话模型本质上是一个 reducer:

  • 输入: [初始消息, 工具调用, 工具结果, ...](状态列表)
  • reducer: LLM determine_next_step()
  • 输出: 追加的新消息
# 概念等价于:
while True:
    messages.append(llm_response)  # 新状态
    if done: break
    result = execute_tool(tool_call)
    messages.append(result)  # 新状态

SessionDB 持久化了消息列表,重启后可恢复。

扣分点:

  • Fallback 切换修改了 agent 状态(provider/model/client),不是纯函数式的
  • 当前没有 message-level 的”replay”支持——如果 fallback 后想回到 primary,需要手动 hermes model
  • Provider 凭证状态(pool)与消息状态分离

综合评分

原则符合度关键发现
1. NL → Tool Calls★★★★★原生支持,架构核心
2. Own Your Prompts★★★★☆Skills 系统强大,但动态注入不可见
3. Own Context Window★★★★☆压缩+缓存完整,fallback 时可能截断
4. Tools as Structured Outputs★★★★★完全遵循
5. Unify State★★★★☆SessionDB 统一,但凭证状态分离
6. Launch/Pause/Resume★★★★☆Gateway 架构好,fallback 无 pause
7. Contact Humans★★★★☆审批系统存在,fallback 通知覆盖仍需核验
8. Own Control Flow★★★★★核心优势,但 cron fallback 独立实现
9. Compact Errors★★★★★最强项,中心化分类器
10. Small Focused Agents★★★★☆子 agent 系统好,fallback 耦合
11. Trigger Anywhere★★★★★多平台完备
12. Stateless Reducer★★★★☆Session 模型对,fallback 修改了状态

总分: 53/60 (88.3%)


Fallback 链路专项审计

当前配置状态

# ~/.hermes/config.yaml (相关部分)
model:
  default: deepseek-v4-flash
  provider: opencode-go
  base_url: https://opencode.ai/zen/go/v1
  api_mode: chat_completions
fallback_providers: []         # ← 空链!
credential_pool_strategies: {} # ← 未配置
api_max_retries: 3

危险信号: fallback_providers 为空意味着:

  • 主模型 deepseek-v4-flash(opencode-go)如果 rate-limit / 超时 / 500,不会有任何 fallback
  • 用户完全暴露在服务中断风险中
  • credential_pool_strategies 也未配——单 key 无轮换

Fallback 链路架构图

用户请求
  ↓
resolve_runtime_provider()          ← 凭证/端点解析
  ├─ Azure Anthropic short-circuit
  ├─ Azure Foundry
  ├─ custom_providers(自定义端点)
  ├─ CredentialPool(多凭证池)
  │   └─ fill_first / round_robin / random / least_used
  ├─ 内建 provider 解析(nous/openai-codex/xai-oauth/...)
  └─ 环境变量回退
  ↓
agent 初始化(client, model, provider)
  ↓
对话循环(conversation_loop.py)
  └─ API 调用失败 →
       error_classifier 分类 →
       重试(×api_max_retries)→
       _try_activate_fallback()
         ├─ 迭代 fallback_chain
         ├─ resolve_provider_client()
         ├─ 替换 agent.client/model/provider/base_url
         └─ 重置 retry_count → 继续

已实现的最佳实践

  1. 中心化错误分类: FailoverReason 枚举 + classify_api_error() 管道
  2. 凭证冷却: 401/429 超时回退,reset_at 时间戳覆盖
  3. 去重保护: fallback 不会切换到与当前相同的 provider+model(_entry_identity)
  4. Base URL 防欺骗: base_url_host_matches() 防御域名仿冒
  5. Azure 专有路径: Azure short-circuit 防止 key 泄露
  6. Context 压缩同步: fallback 切换时更新 context_compressor
  7. 可观测性 hook: Langfuse 插件、pre_api_request hook
  8. Rate limit guard: Nous Portal 主动率限检测

发现的问题

1. fallback_providers 全面为空

  • 严重度: 🔴 高
  • 影响: 当前无任何 fallback。若 opencode-go 服务降级/rate-limit,整个 Agent 不可用
  • 建议: 至少添加一个 fallback,如 openrouter + deepseek-v4-flash 或 anthropic + claude-sonnet-4-20250514

2. Cron 作业 fallback 与主循环不一致

  • 严重度: 🟡 中
  • 位置: cron/scheduler.py:1541-1576
  • 问题: Cron 作业有自己的 fallback 解析逻辑(读 fallback_providers 然后调用 resolve_runtime_provider()),但没有使用 try_activate_fallback() 的去重/冷却机制
  • 建议: 统一调用 try_activate_fallback() 或 extract 一个共享的 FallbackManager

3. Fallback 通知机制不完善

  • 严重度: 🟡 中
  • 问题: _emit_status() 向用户发送”Primary failed, switching to fallback”消息,但:
    • 没有区分自动化 fallback 和用户手动选择的 fallback
    • 没有 fallback 成功/失败的告警事件
    • 没有通知到所有平台(仅当前 platform)
  • 建议: 添加 on_fallback_activated / on_fallback_exhausted hook,可联动 gateway 推送到所有平台

4. Credential Pool 策略未配置

  • 严重度: 🟡 中
  • 当前: credential_pool_strategies: {} → 默认使用 fill_first(顺序使用)
  • 影响: 多 API key 场景下,第一个 key 耗尽前不会切换
  • 建议: 对有多个 key 的 provider 配置 round_robin 或 least_used

5. Fallback 后无”恢复”机制

  • 严重度: 🟢 低
  • 问题: 一旦切换到 fallback provider,不会自动探测 primary 是否恢复
  • 建议: 实现 _try_restore_primary() ——在一定时间后(如 5 分钟)或 key 冷却过期后尝试切回 primary

6. api_max_retries 与 fallback 的交互文档不清晰

  • 严重度: 🟢 低
  • 问题: api_max_retries: 3 在 fallback 切换后也重置了 retry_count = 0,这意味着总尝试次数需要区分初始调用与重试,并包含 primary 和各 fallback 的实际执行路径,不能直接用 api_max_retries × fallback_chain_length 计算,用户可能无感知
  • 建议: 在 hermes fallback list 中显示”最大尝试次数 = 重试 × fallback entries”

7. 多 profile fallback 配置碎片化

  • 严重度: 🟢 低
  • 问题: 每个 profile 有自己的 fallback_providers,但没有全局 fallback 机制。一个 profile 的 fallback 不会跨 profile 共享
  • 建议: 提供 global_fallback_providers 作为 profile 级别的 fallback 的兜底

8. 测试覆盖不足

  • 严重度: 🟡 中
  • 问题: 搜索显示 test_api_max_retries_config.py 存在(5 个测试),但 fallback 激活的核心逻辑(try_activate_fallback)缺乏独立单元测试——目前主要通过集成测试覆盖
  • 建议: 为 agent/chat_completion_helpers.py:try_activate_fallback 添加独立测试

修复建议优先级

P0 — 立即修复

  1. 配置 fallback_providers: 至少添加 1-2 条 fallback 条目
    fallback_providers:
      - provider: openrouter
        model: deepseek/deepseek-chat
      # 或
      - provider: custom
        model: claude-sonnet-4-20250514
        base_url: https://api.anthropic.com/v1

P1 — 短期(本周)

  1. 统一 Cron 与主循环的 fallback 逻辑: 提取 FallbackManager 类
  2. 配置 credential_pool_strategies(如果有多个 key)
  3. 添加 fallback 事件的平台通知 hook

P2 — 中期(本月)

  1. 实现 primary 恢复探测: _try_restore_primary() 定时机制
  2. 添加 fallback 测试套件: 对 try_activate_fallback 的单元测试
  3. 文档化 fallback 行为的重试预算: 在 hermes fallback list 中显示

P3 — 长期

  1. 跨 profile fallback 共享: 全局 fallback profile
  2. Fallback 审批流程: “Primary 失败,是否切换到 $FALLBACK?“人类审批
  3. Fallback 审计日志: 记录每次 fallback 事件(时间、原因、切换目标、耗时)

结论

Hermes Agent 的 fallback/provider 基础设施架构设计非常扎实,远超大多数 AI agent 框架:

  • 中心化错误分类器(FailoverReason)是现代 agent 架构的最佳实践
  • 凭证池冷却机制、去重保护、base URL 安全校验体现了生产级工程水准
  • gateway 多平台架构为 12-factor 的”随处触发”提供了坚实基础

核心短板在于配置面——当前 fallback_providers 为空链,意味着所有精心设计的 fallback 机制在用户环境中实际上不会被触发。此外,Cron 与主循环的 fallback 逻辑分裂、fallback 后无恢复机制、缺乏 fallback 事件人类通知也是需要关注的问题。

一句话总结: Hermes 的架构遵循了 12-factor Agents 的 88.3%(53/60),但配置空跑导致所有 fallback 能力闲置——这不是代码问题,是配置问题。


审计基于 Hermes Agent commit HEAD(2026-05-28 工作目录)、12-factor Agents v1.0 README+content、~/.hermes/config.yaml 运行配置。