缓存 · Failover · 多平台 · 监控 · 部署 — 从 20 个 Agent 项目源码提炼的生产方法论
大多数 Agent 项目在 demo 阶段看起来很惊艳,但一上生产就崩。原因很简单——demo 只关心 happy path,生产必须处理所有 unhappy path。
| 维度 | Demo Agent | 生产级 Agent |
|---|---|---|
| LLM 调用 | 单 provider,挂了就挂了 | 多 provider failover + credential 轮换 |
| 提示词 | 硬编码字符串 | 分层组装 + 缓存 boundary + 热更新 |
| 上下文 | 无限增长直到 token 溢出 | 压缩/摘要策略 + 固定窗口 |
| 错误处理 | try-catch 打日志 | 重试 + 降级 + 熔断 + 告警 |
| 部署 | python main.py | Docker + 编排 + 健康检查 |
| 平台 | CLI 或 Web UI | 14-30+ 消息平台适配 |
| 监控 | print() | Prometheus + Sentry + OTel + Langfuse |
| 密钥 | .env 文件 | Credential Pool + 速率追踪 + 轮换 |
| 配置 | 代码里改常量 | 运行时热更新 + 环境分层 |
| 成本 | 不在意 | Token 计量 + 预算控制 + 缓存省钱 |
缓存是生产环境最直接的成本优化手段。一次 LLM 调用可能花费 $0.01-0.10,如果你的系统提示词有 3 万 token,每次请求都在重复发送同样的前缀——这是巨大的浪费。
这是 20 个项目中最精妙的缓存设计。核心思路:让 LLM provider 的 prefix cache 尽可能命中。
关键代码:src/agents/system-prompt-cache-boundary.ts (~50 行)——用 SHA256 哈希验证 stable prefix 没有被意外修改,确保 provider 的 prefix cache 始终命中。
Hermes 的 Background Review Fork 是另一个巧妙的缓存利用场景:
效果:实测约 26% 的成本降低。Review fork 不需要重新构建系统提示词,直接复用父 agent 已缓存的版本。
| 项目 | 缓存策略 | 实现方式 | 效果 |
|---|---|---|---|
| OpenClaw | Cache Boundary + LRU | stable/volatile 分割 + SHA256 验证 + LRU 64 条 | Provider prefix cache 命中率最大化 |
| Hermes | 继承父缓存 | Review fork 继承 cached prompt | ~26% 成本降低 |
| CrewAI | CacheHandler | 工具结果缓存,相同输入返回缓存 | 减少重复 API 调用 |
| SWE-agent | 命令历史缓存 | 相同 shell 命令返回历史输出 | 减少容器执行次数 |
| AutoGPT | LLM 响应缓存 | Redis 事件总线 + LLM 结果缓存 | 降低重复推理成本 |
| LangGraph | Checkpoint | State snapshot 持久化,可回溯 | 失败恢复,非典型缓存 |
LLM API 是生产环境中最不可靠的依赖——它会限流、超时、返回 500、偶尔整个服务宕机。你的 Agent 必须优雅地处理所有这些情况。
class LLM(BaseModel, RetryMixin, NonNativeToolCallingMixin):
model: str = "claude-sonnet-4-20250514"
api_key: str | SecretStr | None
base_url: str | None
num_retries: int = 5 # 最大重试次数
retry_multiplier: float = 8.0 # 退避因子
timeout: int = 300 # 单次请求超时 (秒)
max_message_chars: int = 30_000
max_output_tokens: int | None
# 只对这些瞬态错误重试:
LLM_RETRY_EXCEPTIONS = (
APIConnectionError, # 网络错误
RateLimitError, # 429 限流
ServiceUnavailableError, # 503 服务不可用
)
# 不对以下错误重试:
# - AuthenticationError (401/403): key 有问题,重试没用
# - BadRequestError (400): 请求格式错误,重试没用
# - NotFoundError (404): 模型不存在,重试没用
from langgraph.pregel import RetryPolicy
graph.add_node(
"api_call",
call_external_api,
retry=RetryPolicy(
max_attempts=3, # 最大重试次数
initial_interval=1.0, # 初始间隔(秒)
backoff_factor=2.0, # 退避因子
max_interval=10.0, # 最大间隔上限
jitter=True, # 随机抖动防惊群
)
)
# 命令式 failover: 执行时切换节点
graph.invoke(
Command(goto="fallback_node"), # 跳到备用节点
config
)
LangGraph 的独特之处:重试策略是图节点的属性,不是全局配置。不同节点可以有不同的重试参数。
| 项目 | 重试 | Provider 降级 | 模型降级 | Key 轮换 | 优雅降级 |
|---|---|---|---|---|---|
| OpenClaw | ✅ | ✅ 40+ | ✅ | ✅ | ✅ |
| Hermes | ✅ | ✅ multi | ✅ | ✅ Pool | ⚠️ |
| OpenHands | ✅ RetryMixin | ✅ fallback | ✅ ALL/STOP | ⚠️ 单 key | ⚠️ |
| LangGraph | ✅ RetryPolicy | ⚠️ 手动 | ⚠️ Command | ❌ | ❌ |
| AutoGPT | ✅ | ✅ 6 providers | ⚠️ | ⚠️ | ⚠️ |
| 其他 | ⚠️ 基础 | ❌ | ❌ | ❌ | ❌ |
生产级 Agent 最大的挑战之一是消息平台适配——用户在 Telegram 上和你聊,在 Discord 上@你,在 WhatsApp 上发消息给你。你的 Agent 需要同时支持所有这些。
| 维度 | OpenClaw 编译时绑定 | Hermes 运行时发现 |
|---|---|---|
| 架构 | Channel 编译进二进制 | Plugin 运行时加载 |
| 优点 | 类型安全,零配置启动 | 灵活扩展,用户可自定义 |
| 缺点 | 添加平台需 rebuild | 依赖解析复杂 |
| 平台数 | 14 channels | 30+ platforms |
| 语言 | TypeScript | Python |
| 典型场景 | SaaS 产品,平台固定 | 开源框架,用户自定义 |
| 类别 | 平台 | 特点 |
|---|---|---|
| 即时通讯 | Telegram, Discord, WhatsApp, Signal, Slack, iMessage, Matrix, IRC | 双向消息,富文本,reaction |
| 中国企业 | 飞书, 企业微信, QQ, LINE | 国内合规,审批流 |
| 社交/其他 | Nostr, WeChat, Google Chat | 特殊协议适配 |
统一消息模型:所有平台的消息被抽象为统一的 Message 对象,Agent 只需要处理一种格式,Channel 插件负责翻译。
| 类别 | 平台 |
|---|---|
| 即时通讯 | Telegram, WhatsApp, Signal, Discord, Slack, Matrix, Mattermost, iMessage (BlueBubbles) |
| 中国企业 | 飞书, 企业微信, 微信, 钉钉, QQ Bot, 腾讯元宝 |
| 邮件/短信 | Email, SMS |
| 智能家居 | Home Assistant |
| API/Webhook | API Server, Webhook |
| 开发工具 | CLI (hermes), TUI (Ink/React), ACP (VS Code/Zed/JetBrains) |
运行时发现机制:Hermes 在启动时扫描已安装的 plugin 包,自动注册可用的 Gateway。不需要修改代码,pip install hermes-telegram 就能添加 Telegram 支持。
| 挑战 | 描述 | 解决方案 |
|---|---|---|
| 消息长度限制 | Telegram 4096, Discord 2000, WhatsApp 65536 | 自动分段 + "续" 标记 |
| Markdown 支持 | Discord 部分支持, WhatsApp 不支持, Telegram 支持 | 平台感知的格式降级 |
| 富媒体 | 图片/文件/语音/视频 的发送方式各异 | 统一 Media 接口 + 适配器模式 |
| 交互组件 | Discord buttons, Telegram inline keyboard | 抽象 Interaction 模型 |
| Reaction | Emoji 反应的 API 完全不同 | 统一 react() 接口 |
| 速率限制 | 每个平台有不同的 rate limit | Per-channel rate limiter |
| 消息格式 | 回复/转发/引用的语义不同 | 统一 ReplyModel |
Agent 在生产环境中最让人头疼的问题是:它为什么做了这个决定? 没有可观测性,你就是在黑箱运维。
| 传统 | Agent 场景 | 关键数据 |
|---|---|---|
| Metrics (指标) | Token 使用量、工具调用频率、成功率 | tokens_per_turn, tool_call_count, error_rate |
| Traces (追踪) | Agent 决策链、工具调用链、思考过程 | reasoning_trace, tool_chain, llm_io |
| Logs (日志) | 消息收发、配置变更、异常堆栈 | message_log, config_change, exception |
AutoGPT 是唯一一个集成了完整生产监控栈的项目:
| 工具 | 用途 | 实现 |
|---|---|---|
| Prometheus | Metrics | execution_manager_active_runs, pool_size, utilization_ratio |
| Sentry | 错误追踪 | 异常自动上报,堆栈追踪 |
| Langfuse | LLM 可观测 | Prompt 版本管理,trace 追踪 |
| RabbitMQ | 任务队列 | 图执行任务的持久化队列 |
| Redis | 事件总线 | 跨进程事件广播 |
| LaunchDarkly | Feature Flags | 非本地环境的特性开关 |
| Stripe | 计费 | 按用量计费 / credits |
# Prometheus metrics
active_runs_gauge = Gauge("execution_manager_active_runs", "Number of active graph runs")
pool_size_gauge = Gauge("execution_manager_pool_size", "Maximum number of graph workers")
utilization_gauge = Gauge("execution_manager_utilization_ratio", ...)
OpenClaw 有一套独特的自监控机制——Trajectory 导出:
Trajectory 包含:
这种设计让运维可以回放任何一次 Agent 决策的完整上下文——不仅是结果,还有"为什么"。
PydanticAI 是唯一原生集成 OpenTelemetry 的项目:
这是最"标准"的可观测性实现——不造轮子,直接用业界标准。
| 项目 | Metrics | Traces | LLM 追踪 | 错误追踪 | 自监控 |
|---|---|---|---|---|---|
| AutoGPT | ✅ Prometheus | ✅ | ✅ Langfuse | ✅ Sentry | ⚠️ |
| OpenClaw | ⚠️ 基础 | ✅ Trajectory | ✅ API I/O | ⚠️ | ✅ Dashboard |
| PydanticAI | ✅ OTel | ✅ OTel | ⚠️ | ✅ OTel | ❌ |
| OpenHands | ⚠️ | ✅ Events | ⚠️ | ⚠️ | ✅ StuckDetector |
| 其他 | ❌ | ⚠️ 日志 | ❌ | ❌ | ❌ |
生产环境中,你不能每次改配置都重启服务。Agent 的配置管理需要解决三个问题:分层、热更新、审计。
OpenClaw 使用 gateway 工具进行配置管理,支持热更新:
关键设计:config.patch 优先热重载,只有当配置变更涉及核心架构(如端口、数据库连接)时才触发安全重启。这保证了大部分配置变更零停机。
AutoGPT 在非本地环境使用 LaunchDarkly 的 Feature Flags:
这是 SaaS 化 Agent 平台的必备能力——你不能因为一个功能变更就让所有用户重启。
Agent 的一大特点是它需要访问大量外部服务——LLM API、搜索 API、消息平台、数据库……每个服务都需要凭证。如何安全管理这些凭证是生产环境的硬性要求。
OpenClaw 将 credential rotation 作为 failover 的一环:
与 Hermes 不同,OpenClaw 的 credential 管理更简单——它是 failover 链的一部分,而不是独立的 Pool。
| 原则 | 做法 | 反面教材 |
|---|---|---|
| 不硬编码 | 环境变量 / Vault / Secrets Manager | 代码里写 api_key = "sk-..." |
| 最小权限 | 每个 key 只授予必要权限 | 用 admin key 做 read-only 请求 |
| 多 key 分散 | Credential Pool 避免单点 | 只用一个 key,限流就挂 |
| 速率感知 | Rate Limit Guard 追踪用量 | 暴力重试直到 429 |
| 日志脱敏 | 日志中自动 redact key | 把完整 API key 打到日志里 |
| 自动轮换 | 定期或按需更换 key | 一个 key 用三年 |
Agent 是 token 消耗大户。一个生产级 Agent 每天可能消耗数百万 token,成本控制不是可选项,而是必需品。
| 项目 | 策略 | 机制 | 估算节省 |
|---|---|---|---|
| OpenClaw | Prompt 缓存 | Cache Boundary + LRU | 50%+ input 成本 |
| Hermes | Fork 缓存复用 | Review fork 继承 cached prompt | ~26% 总成本 |
| OpenHands | 上下文压缩 | LLMSummarizingCondenser | 30-60% input token |
| AutoGPT | 计费系统 | Stripe + Credits | 商业变现 |
| SWE-agent | 输出解析优化 | 8 种解析器减少 retry | 减少无效 LLM 调用 |
# 概念实现:模型路由器
class ModelRouter:
"""根据任务复杂度选择最经济的模型"""
def route(self, task: str, tools: list, history_len: int) -> str:
# 简单问答 → 小模型
if not tools and history_len < 5:
return "gpt-4o-mini" # $0.15/1M input
# 需要工具调用 → 中等模型
if len(tools) <= 3:
return "claude-sonnet" # $3/1M input
# 复杂推理 + 多工具 → 旗舰模型
if len(tools) > 5 or "debug" in task:
return "claude-opus" # $15/1M input
# 默认中等
return "claude-sonnet"
# 成本对比:
# 全部用 opus: $15 × 每次请求 = 💸💸💸
# 路由策略: 80% sonnet + 15% mini + 5% opus = 省 70%+
Agent 的部署架构决定了它的可靠性和可扩展性。从单进程到微服务,每种架构都有适用场景。
OpenHands 的生产部署亮点是 Docker sandbox 隔离:
对话工厂模式:LocalConversation (进程内) vs RemoteConversation (连接 agent server),根据工作区类型自动路由。
| 项目 | 缓存 | Failover | 多平台 | 可观测 | 密钥管理 | 成本控制 | 部署 | 综合 |
|---|---|---|---|---|---|---|---|---|
| OpenClaw | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐ | ⭐⭐⭐ | ⭐⭐ | 生产级 |
| Hermes | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐ | 生产级 |
| AutoGPT | ⭐⭐ | ⭐⭐ | ⭐ | ⭐⭐⭐ | ⭐ | ⭐⭐ | ⭐⭐⭐ | 准生产 |
| OpenHands | ⭐ | ⭐⭐⭐ | ⭐ | ⭐⭐ | ⭐ | ⭐ | ⭐⭐⭐ | 准生产 |
| LangGraph | ⭐ | ⭐⭐ | ⭐ | ⭐⭐ | ⭐ | ⭐ | ⭐⭐ | 准生产 |
| PydanticAI | ⭐ | ⭐ | ⭐ | ⭐⭐⭐ | ⭐ | ⭐ | ⭐ | 开发级 |
| CrewAI | ⭐⭐ | ⭐ | ⭐ | ⭐ | ⭐ | ⭐ | ⭐ | 开发级 |
| SWE-agent | ⭐ | ⭐ | ⭐ | ⭐ | ⭐ | ⭐⭐ | ⭐⭐ | 开发级 |
| 其他 12 个 | ⭐ | ⭐ | ⭐ | ⭐ | ⭐ | ⭐ | ⭐ | 开发级 |
⭐⭐⭐ = 业界最佳水平 | ⭐⭐ = 可用 | ⭐ = 基础/缺失
根据 20 个项目的源码分析,以下是按优先级排序的生产级改造清单:
| # | 特性 | 参考项目 | 实现要点 |
|---|---|---|---|
| 1 | LLM 重试 + 指数退避 | OpenHands RetryMixin | 只对瞬态错误重试 (429/503/ConnectionError),初始 1s,因子 2-8,加 jitter |
| 2 | Provider 降级 | OpenClaw | 至少 2 个 provider,主挂切备。40+ provider 最佳,3 个够用 |
| 3 | Cache Boundary | OpenClaw | 系统提示词分 stable/volatile,确保 provider prefix cache 命中 |
| 4 | 上下文压缩 | OpenHands Condenser | 对话超过 N 轮自动摘要,固定 token 窗口 |
| 5 | 密钥不硬编码 | 所有项目 | 环境变量 / .env / Secrets Manager,绝对不要出现在代码里 |
| # | 特性 | 参考项目 | 实现要点 |
|---|---|---|---|
| 6 | Credential Pool | Hermes | 多 key 轮换 + 速率追踪,429 自动切下一个 key |
| 7 | 模型降级 | OpenHands | Opus 挂 → Sonnet → Haiku,用户可配置策略 |
| 8 | Stuck Detection | OpenHands | 检测循环/重复/无进展,自动 break 或换策略 |
| 9 | 错误追踪 | AutoGPT Sentry | 异常自动上报,堆栈追踪,不靠用户反馈 |
| 10 | 健康检查 | 标准实践 | /health 端点,定期检查 LLM 连通性 |
| # | 特性 | 参考项目 | 实现要点 |
|---|---|---|---|
| 11 | Token 用量监控 | OpenClaw Trajectory | per-turn token 计量,成本可视化 |
| 12 | 配置热更新 | OpenClaw | 运行时修改配置,不需要重启 |
| 13 | LLM 追踪 | AutoGPT Langfuse | 记录完整 prompt → response,可回放决策 |
| 14 | 多平台适配 | OpenClaw / Hermes | 统一消息模型 + Channel adapter |
| 15 | Feature Flags | AutoGPT | 灰度发布、A/B 测试、紧急 kill switch |
import asyncio
import random
from typing import TypeVar, Callable
T = TypeVar('T')
# 瞬态错误 — 可以重试
TRANSIENT_ERRORS = (ConnectionError, TimeoutError)
async def llm_call_with_retry(
fn: Callable[..., T],
max_retries: int = 5,
base_delay: float = 1.0,
backoff_factor: float = 2.0,
max_delay: float = 60.0,
) -> T:
"""带指数退避 + jitter 的 LLM 调用重试"""
for attempt in range(max_retries + 1):
try:
return await fn()
except TRANSIENT_ERRORS as e:
if attempt == max_retries:
raise # 重试耗尽,抛出
# 指数退避 + 随机 jitter (防惊群)
delay = min(base_delay * (backoff_factor ** attempt), max_delay)
jitter = random.uniform(0, delay * 0.1)
await asyncio.sleep(delay + jitter)
except Exception:
raise # 非瞬态错误,不重试
import hashlib
class PromptCacheBoundary:
"""系统提示词缓存分割点"""
def __init__(self):
self._stable_hash: str | None = None
self._stable_content: str | None = None
def build_system_prompt(
self,
stable_parts: dict, # 跨 turn 不变的部分
volatile_parts: dict, # 每 turn 重建的部分
) -> str:
# 组装 stable prefix
stable = "\n".join(
f"## {k}\n{v}" for k, v in stable_parts.items()
)
# 验证 stable 没有意外变化
current_hash = hashlib.sha256(stable.encode()).hexdigest()
if self._stable_hash and current_hash != self._stable_hash:
# Stable 部分变了!prefix cache 会失效
# 这可能是故意的(配置更新),记录一下
import warnings
warnings.warn("Stable prefix changed — cache boundary reset")
self._stable_hash = current_hash
self._stable_content = stable
# 组装 volatile suffix
volatile = "\n".join(
f"## {k}\n{v}" for k, v in volatile_parts.items()
)
# 关键: stable 在前, volatile 在后
# 这样 LLM provider 的 prefix cache 才能命中
return f"{stable}\n\n{volatile}"
# 用法:
cache_boundary = PromptCacheBoundary()
# 每 turn:
system_prompt = cache_boundary.build_system_prompt(
stable_parts={
"identity": agent_identity, # 不变
"tools": tool_definitions, # 不变
"skills": skill_content, # 不变
},
volatile_parts={
"memory": current_memory, # 每 turn 可能变
"runtime": runtime_context, # 每 turn 变
"conversation_summary": summary, # 每 turn 变
},
)
import time
from dataclasses import dataclass, field
@dataclass
class Credential:
key: str
provider: str
rpm_limit: int = 60 # requests per minute
tpm_limit: int = 100_000 # tokens per minute
last_used: float = 0
request_count: int = 0
token_count: int = 0
class CredentialPool:
"""多 key 凭证池 + 速率追踪"""
def __init__(self):
self._credentials: list[Credential] = []
self._window = 60 # 速率追踪窗口 (秒)
def add(self, key: str, provider: str, **limits):
self._credentials.append(
Credential(key=key, provider=provider, **limits)
)
def acquire(self, provider: str | None = None) -> Credential:
"""获取一个可用的 credential"""
now = time.time()
candidates = self._credentials
if provider:
candidates = [c for c in candidates
if c.provider == provider]
# 筛选未限流的 credential
available = []
for c in candidates:
# 清除过期窗口的计数
if now - c.last_used > self._window:
c.request_count = 0
c.token_count = 0
# 检查是否还有额度
if (c.request_count < c.rpm_limit * 0.8 and
c.token_count < c.tpm_limit * 0.8):
available.append(c)
if not available:
raise RuntimeError("All credentials rate-limited")
# 选最近最少使用的 (避免热点)
selected = min(available, key=lambda c: c.last_used)
selected.last_used = now
selected.request_count += 1
return selected
def report_tokens(self, key: str, tokens: int):
"""上报 token 使用量"""
for c in self._credentials:
if c.key == key:
c.token_count += tokens
break
# 用法:
pool = CredentialPool()
pool.add("sk-key1", "openai", rpm_limit=60)
pool.add("sk-key2", "openai", rpm_limit=60)
pool.add("sk-ant-key3", "anthropic", rpm_limit=50)
cred = pool.acquire(provider="openai")
# → 自动选择使用最少的 key
本页内容基于 20 个 Agent 项目的源码分析,提炼生产级特性的设计模式和实现细节。
最后更新: 2026-05-17 | ← 返回 Agent 研究站