Featured image of post 模型网关升级与多重免费备用链实录

模型网关升级与多重免费备用链实录

一次自建 AI 网关升级的踩坑记录,以及一套按延迟/稳定性排布的免费模型备用链设计

   
文章摘要
把某家 AI 客户端的账号变成 OpenAI 兼容接口,再挂四层免费备用模型。这篇记录了升级时的三个坑、健康检查与回滚脚本、备用链的排序原则,以及哪些运维该交给定时任务。

自建的 OpenAI 兼容网关跑了几个月,一直相安无事,直到某天发现"签到按钮还在、任务列表也拉得出来,但一个任务都完成不了"。顺藤摸瓜把版本升级了,又顺手把准备了很久的多重免费备用链接了进去。这篇把过程和判断依据写下来,方便以后照着复现。

文中涉及的所有地址、密钥、账号均为占位符。

一、先判断:这次到底该不该升

看到"有新版本"就升级,是最容易出事的做法。升级前先做三件事:

  1. 比镜像 digest,而不是比 tag。latest 这种 tag 会漂,docker inspect --format '{{index .RepoDigests 0}}' 拿到的 digest 变了才是真的换了内容。
  2. 看上游 commits/releases 里有没有和你用途相关的东西。你用它跑对话,那"模型目录调整"“限流重试"“鉴权变更"才算关键;纯文档、拼写修正的提交可以直接忽略。
  3. 翻 issue 区找别人的实测。如果症状和你一致(例如"任务列表能拉到但没有任务码”),说明不是你环境的问题,升级才值得做。

二、升级实录:三个坑

坑 1:配置被打进镜像,重建即回滚

现象:升级完,面板和 API 全部 401,原因很简单——api_key 变回了镜像里的默认占位值。

根因:/app/config.json 是构建进镜像的,容器并没有挂载它。docker compose up -d 重建容器 = 用新镜像里的默认配置覆盖你的自定义项。

应急做法:

# 升级前
docker exec <容器> cat /app/config.json > /root/gw-config.bak.json

docker compose pull -q && docker compose up -d

# 升级后还原
docker cp /root/gw-config.bak.json <容器>:/app/config.json
docker restart <容器>

根治做法是把配置挂出来,以后重建容器不再丢:

services:
  gw:
    image: <镜像>:<固定tag>
    volumes:
      - ./config.json:/app/config.json
      - gw_data:/app/data

顺带建议:升级前把镜像打成固定 tag(如 gw:2026-10-21),compose.yaml 里也别写 latest——不然哪天上游悄悄推一版,你重建一次容器就"被升级"了。

坑 2:版本号成了 dev,面板永远提示"有更新”

源码构建的版本号是靠构建参数注入的:

ARG APP_VERSION=dev
RUN go build -ldflags="-s -w -X main.appVersion=${APP_VERSION}" ...

不传这个参数,编出来的二进制里版本号就是 dev。面板拿它去跟最新 release 比,永远判定"有更新",很烦人。

docker build --build-arg APP_VERSION=$(date +%Y.%m.%d.%H%M) -t gw:local .

验证方式:构建后看 /panel/api/update,current_version 应该 ≥ latest_version 才对。

坑 3:功能"看着在,其实废了"

这是最隐蔽的一个。现象:

  • 签到页有按钮,点了也返回结果
  • 单账号任务列表能拉到 5 条
  • 但每条都是 task_code: ""、current/target 全是 0
  • 全量扫描接口返回 pending_count: 0

结论:上游改了返回结构,旧版解析不出任务码 → accept/claim 全都无从下手。界面还在,功能已经死了。

可复用的判据:列表接口返回的每一行,有没有唯一 ID 和进度字段。没有就按"只读"看待,别指望能自动执行。这比看界面元素可靠得多。

处理方式只有一个:换到仍在维护的分支。原项目已在 README 里公告停止维护,换个每天还在提交的下游 fork,任务功能立刻恢复(当天实测 25 项任务全部执行并领奖)。

三、健康检查与回滚:每次升级必做

升级脚本的骨架大概是这样,重点是带密钥探活和留回滚路径:

OLD=$(docker inspect -f '{{.Image}}' gw)

docker compose pull -q
docker compose up -d
sleep 12

code=$(curl -s -m8 -o /dev/null -w '%{http_code}' \
  http://127.0.0.1:PORT/v1/models -H "Authorization: Bearer $KEY")

if [ "$code" != "200" ]; then
  echo "health fail -> rollback"
  docker tag "$OLD" gw:latest
  docker compose up -d
  exit 2
fi

网关升级与回滚流程

三个细节别省:

  • 带 key 探活。不带 key 返回 401 只能证明进程活着,证明不了鉴权配置是对的。
  • sleep 别省。容器 Status 变 healthy 不等于端口就绪、配置加载完。
  • 留旧镜像 ID。回滚只需要 retag 再 up -d,比重装快得多。

四、多重免费备用链怎么排

结构上是一个瀑布,主链失败才往下走:

客户端
  └─> 主网关(自建,账号池轮转)────> 模型 A
        │ 失败 / 429 / 鉴权异常
        ├─> 备用 1:国内免费额度(低延迟)
        ├─> 备用 2:另一家国内免费额度
        ├─> 备用 3:海外免费额度(需代理)
        └─> 备用 4:聚合平台的 free 型号(垫底)

多重免费备用链

配置大概长这样(各家都提供 OpenAI 兼容端点,所以可以直接串):

model:
  default: "custom-gateway:model-a"

fallback_providers:
  - provider: custom:GLM
    model: glm-4.5-flash
    base_url: https://open.bigmodel.cn/api/paas/v4
  - provider: custom:HUNYUAN
    model: hy3
    base_url: https://<second-provider>/v1
  - provider: custom:GROQ
    model: qwen/qwen3.8-27b
    base_url: https://api.groq.com/openai/v1
  - provider: custom:OPENROUTER
    model: <free-model-id>
    base_url: https://openrouter.ai/api/v1

排序原则(这部分是踩出来的,不是查出来的):

  1. 先排同区域的。延迟低、不需要代理、不会因为代理掉线跟着挂。
  2. 再排额度稳定的。有明确速率限制但长期可用的,优先于"额度看着大但经常抽风"的。
  3. 要代理的放最后。链路最长,故障点最多。
  4. 同一家服务商只能占一档。同源故障会一起挂,等于没有备用。

几个已知的坑:

  • 只实现 /chat/completions、没有 /responses 的网关,接不上只认 Responses API 的客户端。选型时先确认协议,别装完才发现。
  • 大上下文切模型会全量重读。每个 provider 的 prompt 缓存是独立的,长会话中途切换等于把上下文按全价重算一遍,费用和延迟都会跳。
  • 免费层普遍限速。并发一高就 429,客户端侧要有退避重试,不然表现为"对话到一半断开"。

五、免费额度怎么"滚"起来

免费额度有两种机制,别混着理解:

类型判定能否自动化
每日活跃奖励当天通过官方客户端发起一次对话自动,次日批量发放,无需领取
成长任务列表 → accept → 执行动作 → claim需要能拿到任务码 + 进度字段才行

经验:

  • 官方规则里通常写明了"客户端发起的对话才算活跃,网页端不算",脚本、模拟器、批量注册产生的行为也不算。所以想靠网页会话去刷活跃,多数是白费功夫。
  • 奖励到账有延迟。当天跑完当场看不到余额变化很正常,看第二天的积分包明细才准。
  • 企业版账号大多没有"个人成长体系",任务接口会直接报错。这类账号留在模型池里正常调用就好,别放进任务队列——否则每轮扫描刷一堆 error 日志。

六、把运维交给定时任务

升级完后配了三个自动化,都跑在 no_agent 模式(纯脚本,不烧 token):

1. 每晚检查关键更新

git fetch -q origin main
git log --format='%h|%ci|%s' HEAD..origin/main

再按关键词过滤:任务/积分/登录/token/模型/限流/风控/api_key/配置。命中才发通知,全是文档修正就静默——不然每天一条"有更新"很快就脱敏了。

2. 每天自检:容器健康、带 key 探活、账号数、积分快照,异常才说话。

3. 日志只看三个数:TTFB、token/s、缓存命中率。缓存命中掉到 90% 以下,通常意味着 prompt 前缀被某次改动破坏了,得回头看配置。

七、隐私与安全清单

这套东西天然带着一堆敏感信息,外发前过一遍:

  • 清单:内网 IP、公网 IP、域名、API key、账号 uid、昵称、邮箱
  • 示例配置一律用占位符,别贴真实 base_url
  • 密钥不进 git:~/.config/<service>/env,权限 600,仓库里 .gitignore 兜住
  • 面板别直接开公网:反代 + 认证 + TLS,必须开放时也只绑 127.0.0.1 再反代出去
  • 健康检查用 127.0.0.1 而不是 localhost:后者可能解析到 IPv6,会报出假的"服务挂了"

八、复盘

值得自动化的:探活、备份、更新检查、额度快照。

不值得的:追每一个小提交;为了几十点积分再起一套服务。

一条原则可以收尾:能机械判断的交给脚本,需要判断力的才留给人。这套网关升级加备用链,真正花时间的不是敲命令,而是判断"这个功能是不是真的坏了"“这次升级值不值”——这些恰恰是脚本代替不了的。

晚来天欲雪,能饮一杯无