我花了一整天,就为了让远端服务器上的 AI Agent 调到一个模型。curl 能通,Agent 死活不认。
这不是网络问题,不是 key 过期,不是防火墙。问题藏在一个我完全没预料到的地方:Agent 框架的 provider 发现机制有一道隐形的门槛,文档里一个字没提。
我要做什么
需求很朴素:一台远端 VPS 跑着 AI Agent 框架,我想让它用 GPT-5.3 而不是默认的国产模型。中间隔了一层 API 代理服务——把请求分发到十几个上游站点,按 round-robin 轮转,哪个能用就走哪个。
架构画出来大概是这样:
本地配置(真相源)
→ 同步到远端代理服务
→ Agent 框架通过代理访问模型
→ 代理分发到多个上游站点
三层 provider,三套认证,三种 API 协议。OpenAI Chat Completions、Anthropic Messages、还有国产模型的原生接口。
看起来不复杂。实际上,每一层都有自己的脾气。
"注册"和"发现"是两件事
代理服务配好了,远端 curl 直接打代理端口,GPT-5.3 秒回。但 Agent 框架就是不认这个 provider,每次都回退到国产模型兜底。
查状态,返回一个让人头疼的字段:missingProvidersInUse: ["proxy"]。
翻了一个多小时配置文件,最后拼出来的真相是这样的:
Agent 框架有两层配置。全局配置文件(我叫它"户口本")和 agent 级的模型定义文件("身份证")。我的代理 provider 只在 agent 级模型定义里声明了,没在全局配置里注册。
关键在于框架的 provider 发现逻辑:
全局配置 providers → 注册为已知 provider → 可解析认证 ✓
Agent 级 models.json → 只能给已知 provider 追加模型 → 不注册新 provider ✗
agent 级配置能给已有 provider 加模型,但没法注册新 provider。这就像你去银行开了个账户,往里存了钱,但银行系统里没你这个人——钱在,人不存在。
另一个 provider(用 Anthropic 协议的那个)能正常工作,我还纳闷了好久,后来发现它一开始就在全局配置里声明过。差别就在这一步。
"两道门":注册了还不够
把 provider 注册到全局配置后,模型能识别了。切到 Claude Sonnet,报了个新错:Model "proxy/claude-sonnet-xxx" is not allowed。
又是一道门。
Agent 框架对模型有两层管控:
- 第一道门:
providers.models—— 注册,告诉系统这个模型存在 - 第二道门:
agents.defaults.models—— 白名单,允许 agent 实际使用
只过第一道门不过第二道门,就是"系统知道你在,但不让你进"。
我批量同步了 36 个模型到 provider,只有 1 个进了白名单(第一次手动注册的那个)。剩下 35 个全卡在第二道门外面。
修复方案不难——同步脚本里加一行,注册模型的同时往白名单里也写一份。但这个坑的恶心之处在于:注册成功了,状态查询也显示正常,直到你真正切换模型的那一刻才爆炸。

改了配置不生效:缓存的代价
两道门都过了,模型也切了,白名单也加了。结果?还是报 "not allowed"。
这次的原因更隐蔽:Agent 框架的 gateway 是常驻进程,启动时把配置读进内存缓存住。你改了磁盘上的 JSON,进程里跑的还是旧的。
必须重启 gateway。
这个问题在开发阶段特别折磨人,因为你改了配置 → 验证 → 不生效 → 怀疑改错了 → 反复检查 → 配置明明是对的 → 崩溃。整个循环能重复三四次,直到你意识到进程根本没读新配置。
后来我把所有涉及配置变更的脚本命令里都加了 gateway 自动重启。sync-models、switch、fallback,改完就重启,不留侥幸。

认证的三层优先级
框架的认证解析有三层,按优先级排:
- 显式 auth profile 文件(最优先)
- 模型定义或全局配置里的内联 apiKey
- 环境变量 fallback(默认关闭)
我给代理 provider 做了双重保障:auth profile 里写一份 key,全局配置里再内联一份。冗余不优雅,但稳。
为什么要这样?因为框架对不同 provider 的认证解析路径不一样。有的 provider 走 profile,有的走内联。你没法确定框架内部先查哪个,所以两个都写上,确保至少有一条路走得通。
配置同步:单一真相源
远端服务器的配置不能手动改。所有 API key、站点列表、模型定义的真相源都在本地。本地改完,脚本同步到远端。
这个原则听起来简单,执行起来有几个细节:
不能全量覆盖。远端有些配置是机器相关的——监听端口、数据目录、服务密钥。全量覆盖会把这些也冲掉。同步脚本只同步核心字段:API key 列表和上游站点配置,其他字段不动。
要保护兜底模型。远端依赖国产模型做终极 fallback。同步脚本必须识别远端已有的 GLM 相关模型条目,合并时保留它们。我写了一段"GLM 保护"逻辑专门处理这个——遍历远端配置里的 GLM 模型,确保本地配置覆盖时不会把它们丢掉。
远端环境受限。服务器只有 Python 3.6,没有 Ruby。本地同步脚本用 Ruby 做 YAML 合并(Mac 上有 Ruby),但所有需要在远端执行的逻辑——站点探活、黑名单过滤、YAML 解析——全得用 Python 写。两种语言做同一件事,代码维护成本翻倍,但没得选。

fallback 链的设计
模型切换不只是换个名字。我给系统设了一条 fallback 链:
primary → fallback 1 → fallback 2 → 终极兜底
primary 挂了自动降级,一路降到国产模型。国产模型永远在链尾,永远不能丢。
选 primary 有讲究。同一个模型在不同上游站点可能叫不同的名字。代理服务按名字做 round-robin,名字覆盖的站点越多,可用性越高。
举个例子,某 Claude 模型有三个常见名字:
- 带完整日期后缀的版本 → 7 个站点提供
- 简短别名 → 1 个站点提供
- 另一种缩写 → 1 个站点提供
选 1 个站点提供的名字,那个站点 quota 一耗尽就全挂。选 7 个站点的名字,坏一两个站点还有五六个能用。

脚本化:不可复用的修复毫无意义
调试过程中我犯了一个错误:发现问题后直接 SSH 进服务器,用 jq 改 JSON,手动重启服务。问题解决了,但这个修复是一次性的。下次遇到同样的问题,我得重新敲一遍命令。
后来被提醒:所有操作必须写进脚本。
最终的管理脚本覆盖了这些场景:
sync-models:从本地配置提取模型,注册到远端 + 加白名单 + 重启 gatewayswitch:切换 primary 模型 + 自动放行 + 重启 gatewayfallback:设置 fallback 链 + 自动放行 + 重启 gatewayregister:首次注册 provider + 写认证status/list:查看远端状态blacklist:站点黑名单管理errors:错误日志 + 站点探活
每个命令都是幂等的,跑多少次结果一样。该重启的自动重启,该备份的自动备份。

我学到了什么
搞了一整天,最后沉淀下来的认知就几条:
Agent 框架的配置层级比你想象的深。provider 注册、模型注册、白名单放行、gateway 缓存刷新——四步全走完才算生效。少走任何一步,现象都是"配了但没用",而且每步的报错信息都不一样,很难快速定位到底卡在哪一步。
配置变更后的验证不能只看 status 命令。status 读的是磁盘文件,gateway 跑的是内存缓存。两者可以不一致。
"能用"和"稳定"之间隔着 round-robin 的站点覆盖数。一个模型名只要一个站点提供,它就是单点故障。
可复用的 shell 脚本比手动 SSH 修复有价值得多。调试时图快手动操作完全可以理解,但修完之后一定要把操作固化到脚本里。下次同样的问题再来,一行命令解决。
如果你也在折腾类似的多源 API 路由架构,建议先把 provider 发现机制搞透,再动手接模型。不然你会和我一样,curl 能通但 Agent 不认,对着正确的配置反复怀疑人生。