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 项原则速览
| # | 原则 | 核心思想 |
|---|---|---|
| 1 | Natural Language → Tool Calls | NL 到结构化工具调用的翻译 |
| 2 | Own Your Prompts | 提示词工程不外包给框架 |
| 3 | Own Your Context Window | 掌控喂给 LLM 的每一段上下文 |
| 4 | Tools Are Structured Outputs | 工具 = LLM 输出的结构化 JSON + 确定性代码 |
| 5 | Unify Execution & Business State | 执行状态与业务状态合一 |
| 6 | Launch/Pause/Resume with Simple APIs | 简单 API 启动/暂停/恢复 |
| 7 | Contact Humans with Tool Calls | 用工具调用通知人类 |
| 8 | Own Your Control Flow | 自主控制流程(循环/中断/等待) |
| 9 | Compact Errors into Context Window | 错误信息压缩回上下文 |
| 10 | Small, Focused Agents | 小粒度、单一职责的 Agent |
| 11 | Trigger from Anywhere | 多入口触发(Slack/Email/Webhook) |
| 12 | Stateless Reducer | Agent 作为无状态 reducer |
逐项审计
原则 1: Natural Language to Tool Calls(NL → 工具调用)
符合度: ★★★★★ (5/5)
Hermes 的整个对话循环就是建立在这个原则上。agent/conversation_loop.py 中的主循环:
- LLM 接收自然语言 + 工具 schema
- 输出结构化 tool_calls
- Hermes 的
handle_function_call()将工具调用派发到对应实现 - 结果追加回消息列表
核心路由: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)→ fallbackcontext_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 → 继续
已实现的最佳实践
- 中心化错误分类:
FailoverReason枚举 +classify_api_error()管道 - 凭证冷却: 401/429 超时回退,
reset_at时间戳覆盖 - 去重保护: fallback 不会切换到与当前相同的 provider+model(
_entry_identity) - Base URL 防欺骗:
base_url_host_matches()防御域名仿冒 - Azure 专有路径: Azure short-circuit 防止 key 泄露
- Context 压缩同步: fallback 切换时更新 context_compressor
- 可观测性 hook: Langfuse 插件、
pre_api_requesthook - 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_exhaustedhook,可联动 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 — 立即修复
- 配置 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 — 短期(本周)
- 统一 Cron 与主循环的 fallback 逻辑: 提取
FallbackManager类 - 配置 credential_pool_strategies(如果有多个 key)
- 添加 fallback 事件的平台通知 hook
P2 — 中期(本月)
- 实现 primary 恢复探测:
_try_restore_primary()定时机制 - 添加 fallback 测试套件: 对
try_activate_fallback的单元测试 - 文档化 fallback 行为的重试预算: 在
hermes fallback list中显示
P3 — 长期
- 跨 profile fallback 共享: 全局 fallback profile
- Fallback 审批流程: “Primary 失败,是否切换到 $FALLBACK?“人类审批
- 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 运行配置。