OpenClaw 模型供应商怎么接、挂了怎么自动转移:从 provider/model 到 fallback 链
跑 Agent 的人迟早会撞上同一件事:某个模型供应商限流了、余额没了、或者干脆返回一句语焉不详的 An unknown error occurred,整条对话就卡在那里。手工换模型当然可以,但半夜定时任务跑到一半没人换。
OpenClaw 的官方文档把这件事拆成了两级来处理:先在当前供应商内部轮换鉴权档案(auth profile),轮不动了再切到 agents.defaults.model.fallbacks 里的下一个模型。这两级的边界、各自认哪些错、切换后会不会一直留在备用模型上,恰恰是最容易配错的地方。这篇按官方文档的口径把这条链路串一遍。
需要先说清楚:这里讲的 provider 是 LLM 供应商,不是 WhatsApp、Telegram 那种聊天渠道,文档里这两个概念是分开的。
模型引用长什么样,配置键各管什么
模型引用统一是 provider/model 形式,例如 opencode/claude-opus-4-6。相关的配置键分工如下:
| 配置键 | 作用 |
|---|---|
agents.defaults.model.primary | 主模型(也可直接写成字符串形式的 agents.defaults.model) |
agents.defaults.model.fallbacks | 备用模型链,按顺序尝试 |
agents.defaults.models | 存别名与单模型设置;加进去不等于限制覆盖,也不等于注册了新模型 |
agents.defaults.modelPolicy.allow | 可选的覆盖白名单,支持 provider/*、provider/namespace/* 这类尾部前缀通配 |
agents.defaults.utilityModel | 短内部任务用的低成本模型(生成会话标题、进度叙述等),设为空字符串即关闭 |
agents.defaults.imageModel | 仅在主模型无法接收图片时使用 |
agents.defaults.pdfModel | pdf 工具用;未设则退到 imageModel,再退到当前会话/默认模型 |
上下文相关的三个字段容易混:models.providers.*.maxTokens 是供应商级的输出 token 默认值;在 models.providers.*.models[] 每一项上,contextWindow 声明原生窗口,contextTokens 限制活跃输入,maxTokens 覆盖该模型的输出容量。
CLI 侧常用的是这三条:
openclaw onboard
openclaw models list
openclaw models set <provider/model>
还有一个反直觉但文档专门写了的行为:加一个供应商的鉴权,不会改你的主模型。openclaw configure 会保留已有的 agents.defaults.model.primary,openclaw models auth login 也一样,除非显式带 --set-default。供应商插件在鉴权配置补丁里返回的推荐默认模型,OpenClaw 只当作「让这个模型可用」,而不是「替换当前主模型」。想真的换默认,走 openclaw models set <provider/model> 或 models auth login --provider <id> --set-default。
官方插件供应商 vs 自己写 models.providers
文档把供应商分成两类,配置方式完全不同:
- 官方供应商插件(OpenAI、Anthropic、Google、Z.AI、OpenCode、Vercel AI Gateway、DeepSeek、Moonshot、Groq、xAI 等一长串)会自己发布模型目录行,不需要写
models.providers的模型条目。启用插件、配好鉴权、选模型就行。只有当你要覆盖 base URL、请求头、模型列表,或者设超时这类窄设置时,才需要显式写models.providers.<id>。 - 自定义供应商或 OpenAI/Anthropic 兼容代理才用
models.providers(或models.json)来加。
大部分供应商专属逻辑都住在供应商插件里(registerProvider(...)),OpenClaw 自己只保留通用的推理循环。插件负责 onboarding、模型目录、鉴权环境变量映射、传输与配置归一化、工具 schema 清理、失败分类、OAuth 刷新、用量上报、thinking/reasoning 档位等等。
自定义供应商有几个默认值值得记:省略时 reasoning: false、input: ["text"]、成本全 0、maxTokens: 8192;contextWindow 省略则保持未设置状态,当发现流程和单模型元数据都拿不到上下文信息时,上下文预算按 20 万 token 兜底。如果这个代理模型能吃图片,要显式写 input: ["text", "image"],否则图片会走成纯文本媒体引用而不是原生模型输入。
另外,走非原生端点的 api: "openai-completions" 路由(任何 host 不是 api.openai.com 的非空 baseUrl),OpenClaw 会强制 compat.supportsDeveloperRole: false,并跳过只属于原生 OpenAI 的请求整形;本地慢模型或局域网主机,用 models.providers.<id>.timeoutSeconds 单独加时间,但要注意它撑不开整个运行的超时上限,agents.defaults.timeoutSeconds 更低的话得一起抬。
多把 key 怎么排优先级,什么时候才轮换
同一个供应商配多把 key 是最省事的第一道保险。文档给的来源与优先级,从高到低是:
OPENCLAW_LIVE_<PROVIDER>_KEY # 单个实时覆盖,优先级最高
<PROVIDER>_API_KEYS # 逗号或分号分隔的列表
<PROVIDER>_API_KEY # 主 key
<PROVIDER>_API_KEY_1 # 编号列表
Google 系供应商还会把 GOOGLE_API_KEY 算作兜底。选择顺序保留优先级并去重。
关键在触发条件:只有限流类响应才会用下一把 key 重试——例如 429、rate_limit、quota、resource exhausted、Too many concurrent requests、ThrottlingException、concurrency limit reached、周期性用量上限提示这些。非限流失败会立刻失败,不做任何 key 轮换。所有候选 key 都失败时,返回最后一次尝试的错误。
所以「我配了三把 key 为什么没轮换」多半不是配错了,而是那个错根本不在限流桶里。
故障转移的运行时顺序
文档给的运行时流程是这样一条线:解析会话模型与鉴权档案偏好 → 按当前模型选择和该来源的 fallback 策略构建候选链 → 在当前供应商内按轮换与冷却规则尝试 → 若该供应商以「值得转移的错误」耗尽,则前进到下一个模型候选 → 用胜出的候选跑完这一轮,但不改会话已选的 provider/model。
如果所有候选都只是因为供应商过载而失败,且这一轮还没开始执行工具、也没开始输出助手内容,回复运行器会把整条 turn 内候选链重试最多 10 次,退避从 2.5 秒起翻倍、上限 30 秒;等待满 30 秒时发一条状态提示,免得用户干等。全部失败则抛出带结构化逐次尝试详情的 FailoverError,已知时附上最近的冷却到期时间。
要特别记住:fallback 执行是 turn-local 的。回复运行器只持久化 fallback 通知状态,好让 /status 和转换提示能区分「选中的模型」和「实际回答的模型」,它不会把 fallback 记成下一轮的模型选择。
在非群组、非频道会话里,转到 fallback 时会发一条可见通知,恢复到主模型时再发一条:
↪️ Model Fallback: <fallback> (selected <primary>; <reason>)
↪️ Model Fallback cleared: <primary> (was <fallback>)
群组与频道会话保留同样的 fallback 状态和生命周期事件,但不发这两条通知。
同一个 provider/model,来源不同严格程度不同
这是最容易踩的一处。模型是从哪儿来的,决定了它允不允许走 fallback:
| 来源 | 是否走 fallback |
|---|---|
配置默认 agents.defaults.model.primary | 走 agents.defaults.model.fallbacks |
Agent 主模型 agents.entries.*.model | 严格,除非该 agent 的 model 对象自己带 fallbacks;写 fallbacks: [] 是把严格行为显式化 |
| 运行时 fallback | 仅当前轮有效,下一轮从选中的主模型重新开始 |
| 用户会话覆盖 | 严格。/model、模型选择器、session_status(model=...)、sessions.patch 写入 modelOverrideSource: "user",失败就如实报错,不会拿别的模型糊弄过去 |
| 遗留会话覆盖 | 老条目只有 modelOverride 没有 source,一律按用户覆盖处理 |
| cron 任务模型 | payload.model / --model 是任务主模型,用配置的 fallbacks,除非任务自带 payload.fallbacks;payload.fallbacks: [] 让该任务严格 |
候选链本身的拼法也有明确规则:请求的模型永远排第一;显式配置的 fallbacks 会去重但不被模型白名单过滤(当作操作者的明确意图);没有显式 fallback 覆盖时,配置的 fallbacks 会排在配置主模型之前尝试,而配置主模型会被追加到链尾,好让链条最终能落回默认;调用方传了 fallbacksOverride 时,链条就只有请求模型加这个列表,传空列表即关闭模型 fallback,也不会偷偷把主模型追加进来。
冷却、计费禁用与鉴权档案轮换
鉴权档案是 API key 和 OAuth token 的统一抽象。密钥与运行期路由状态存在每个 agent 自己的 ~/.openclaw/agents/<agentId>/agent/openclaw-agent.sqlite 里,配置里的 auth.profiles / auth.order 只是元数据与路由,不含密钥。凭据类型分 api_key、oauth、token 三种,其中 token 是静态 bearer,OpenClaw 不负责刷新。
没配显式顺序时的轮询排序是:先按档案类型(OAuth,然后静态 token,然后 API key),OAuth 内部再把当前可用 access token 的排在过期的前面(过期的仍然可选,以便运行时刷新),然后按 usageStats.lastUsed 从旧到新,冷却/禁用的挪到最后并按最近到期排序。
自动选中的档案会按会话钉住以保持供应商缓存温度,不是每个请求都轮换;会话重置、压缩完成、档案进入冷却或被禁用时才可能轮换或清除。用 /model …@<profileId> -s 做的手动选择是用户覆盖,能扛过 /new、/reset、会话滚动、压缩和冷却窗口。
常规冷却(非计费、非永久鉴权失败)按该档案近期错误次数递增:
- 第 1 次失败:30 秒
- 第 2 次失败:1 分钟
- 第 3 次及以后:5 分钟(封顶)
计数器在该档案内置的失败窗口过去后清零。状态落在每 agent 的 SQLite 鉴权状态里:
{
"usageStats": {
"provider:profile": {
"lastUsed": 1736160000000,
"cooldownUntil": 1736160600000,
"errorCount": 2
}
}
}
计费类失败(例如 insufficient credits、credit balance too low)走另一条道:它同样值得转移,但通常不是暂时的,所以 OpenClaw 直接把档案标为禁用并配更长的退避,写入 disabledUntil 和 disabledReason。文档特意提醒,不是每个计费形态的响应都是 402,也不是每个 402 都落在这里——临时的用量窗口和组织支出上限错误(例如 weekly usage limit exhausted、daily limit reached)被归类为 rate_limit,走短冷却而不是长禁用。
限流冷却还可以是模型级的:失败模型 id 已知时会记 cooldownModel,同一供应商下的兄弟模型仍可尝试;但计费/禁用窗口是跨模型封住整个档案的。
多把 key 之外,如果你想抑制反复出现的鉴权失败,可以显式开:
OPENCLAW_FALLBACK_SKIP_TTL_MS=60000
它给非主候选记一个会话内、进程内的跳过标记,键包含会话、供应商、模型和所选档案 ID;主候选永远不跳过,网关重启即清空,取值被夹在 1 秒到 10 分钟之间。
哪些错会推进 fallback,哪些不会
| 会继续走 fallback | 不会继续 |
|---|---|
| 鉴权失败 | 非超时/非转移形态的显式中止 |
| 限流与冷却耗尽 | 应留在压缩/重试逻辑里的上下文溢出错误(如 request_too_large、输入超过最大 token 数、ollama error: context length exceeded) |
| 过载/供应商繁忙 | 已无候选时的最终未知错误 |
| 超时形态的转移错误 | 供应商侧自行处理的安全拒答(按文档口径由供应商级机制接手) |
| 计费禁用 | |
| 还有剩余候选时的其它未识别错误 |
有几类错的归类值得单独记:OpenAI 兼容路径上供应商已完成的停止原因,如 Unhandled stop reason: error、Provider finish_reason: error,被归为 server_error(类 HTTP 500),仍然可转移,但诊断里保留供应商原始的 finish-reason 文本,不会被改写成「LLM 请求超时」;而 Provider finish_reason: abort、network_error、malformed_response 这类传输形态的,留在超时/转移桶(状态 408)。格式与非法请求错误一般是终态,因为重发同样的载荷还是会失败,所以 OpenClaw 直接把它暴露出来,而不是去轮换鉴权档案。
排查时最有用的是结构化日志:model_fallback_decision 会带上 fallbackStepFromModel、fallbackStepToModel、fallbackStepFromFailureReason、fallbackStepFromFailureDetail、fallbackStepFinalOutcome 这些扁平字段,即便最后一个 fallback 也失败了,也能把最初那个失败原因还原出来。日志与诊断开关怎么打开,见 网关诊断与日志。
一个典型的双保险配置是订阅账号加 API key 备份,用 auth.order 定用户可见顺序:
{
auth: {
order: {
openai: ["openai:user@example.com", "openai:api-key-backup"],
},
},
}
订阅撞上用量上限时,OpenClaw 会在供应商给出确切重置时间时记下它,转到下一个有序档案,并让这次运行留在同一个 harness 里;重置时间过了,订阅档案重新可选。密钥本身怎么存、怎么用 SecretRef 管理,见 密钥与凭据管理。
什么时候这套机制帮不上忙
- 上下文溢出不归它管。输入超窗是压缩与重试逻辑的事,换个模型并不解决,文档也明确把这类错排除在 fallback 之外。相关机制见 上下文压缩与会话修剪,重试策略的边界见 队列、插话与重试。
- 你自己
/model选的模型不会被自动救。这是设计如此:显式选择就是严格选择,挂了要看到真实报错。想要自动兜底,就别在会话里手动钉模型,让它走配置默认。 - 会话中途换模型有代价。文档写得很直白:下一个模型可能有不同的上下文窗口、提示与工具行为、不同的提示缓存实现,中途切换会削弱连续性、可能提前触发压缩、丢掉缓存复用。能新建会话就新建会话。同理,缓存复用要紧时,thinking/reasoning 档位在一个会话内也别来回调——在 OpenAI 上改推理强度会改变可复用的请求状态。
- 鉴权轮换不会放松模型选择。文档专门加了注:自动钉住和用户钉住的鉴权档案都只是「重试偏好」,同一供应商内的档案轮换不代表模型选择变松了,显式的用户 provider/model 选择在同供应商档案耗尽后照样报失败。
- 成本这一侧要自己盯。文档提到 Fast 模式属于溢价计费且按模型区分,具体倍率以官方页面为准;把一堆高价模型堆进 fallbacks,出故障时账单形态会和平时不一样。fallbacks 更适合放成本/延迟敏感的任务和低风险对话,这也是官方给的模型策略建议。
配置改完之后,最省事的验证顺序是:openclaw models status 看鉴权候选和 OAuth 到期,/model status 看每个供应商的鉴权候选与端点,/status 看当前选中模型与(有差异时)正在生效的 fallback 模型和原因。三条命令对不上,再回头翻 model_fallback_decision 日志。
延伸阅读
- 从头读起:OpenClaw 架构总览:网关、Agent、渠道、节点、插件五层各管什么,出问题该往哪查
- 本专题共 40 篇,完整分组目录见专题页
- OpenClaw 的队列、插话(steering)与重试:消息挤在一起时它到底怎么排
- OpenClaw 里工具被拦住了:沙箱、工具策略、elevated 三者的边界怎么分
本文依据 OpenClaw 官方仓库(github.com/openclaw/openclaw)docs/ 下的官方文档整理,核对日 2026-08-17。
我们没有安装或运行过 OpenClaw,因此不涉及界面外观、操作手感与实测耗时的任何描述;
文中的默认值、命令与配置项均为文档口径,不构成对实际运行结果的保证。
该项目迭代很快,请以仓库最新内容为准。接入即时通讯平台前,请自行确认所在平台的规则与合规要求。