一个 API Key 调用 19 个模型:我把家用 Hyper-V 虚拟机改成了私人 AI 网关
起因很简单:想在虚拟机里跑点自己的 AI 项目。
结果从”SSH 都连不上”开始,一路排到网络架构、磁盘分区、公钥权限、NAT 持久性、
OAuth 回调、路由分类器……最后落在一个model=auto上。这篇不是教程,是一份排障账本——记录那些”看起来成功了”的假信号,
以及它们各自骗了我多久。(全文已对公网 IP、内网地址、域名、账号、密钥、tailnet 名等做脱敏,仅保留技术结构。
文末附自查清单。)
TL;DR
一台跑在 Windows Hyper-V 里的 Ubuntu 26.04 虚拟机,最终变成:
1 | ┌──────────────── Cloudflare Tunnel ────────────────┐ |
- 一个 Base URL + 一个 API Key,背后 19 个模型,按任务复杂度自动分派
- 路由器零端口转发,公网入口只有 Cloudflare
- 从”虚拟机彻底失联”到全链路可用,中间踩了 14 个坑,其中 4 个是假成功信号
一、起点:一台连不上网的虚拟机
新装好的 Ubuntu Server 26.04,纯命令行,目标是搭 AI 开发环境。第一天就翻车:
宿主机 Windows 重启一次,SSH 再也连不上虚拟机。控制台里 ip a 长这样:
1 | 2: eth0: mtu 1500 state DOWN |
我的第一反应是”Hyper-V 的 Default Switch 网段随重启漂移,静态地址失效了”。
这个判断对了一半——网段确实每次宿主重启都换(我机器上历史出现过 5 个不同网段),但它不是主因。因为那个静态地址,从头到尾就没生效过。
二、真凶:配置没错,但网卡”没人管”
1 | networkctl |
unmanaged 的意思是:没有任何网络管理器在管这块网卡。翻 netplan 配置:
1 | ethernets: |
而实际网卡 MAC 是 00:15:5d:02:10:05。连安装器留下的原始备份都是 :04——说明这块虚拟网卡在装完系统后被重建过,Hyper-V 重新分配了动态 MAC。
MAC 匹配不上 → 整份配置对这块网卡完全无效 → 既没有静态地址,也没有 DHCP 兜底。
可复用的判据:
ip a看不到地址时,先看networkctl的 SETUP 列,别急着改地址。unmanaged= 配置没匹配上;configuring/failed才是配置本身有问题。
三、顺手挖出三个静默炸弹
| 隐患 | 为什么危险 | 处理 |
|---|---|---|
sshd 状态 active 但 enabled=disabled |
下次重启 SSH 直接消失,而且”现在能用”会骗过你 | systemctl enable ssh |
AutomaticStopAction = Save |
宿主机关机时 VM 被冻成休眠,开机带着旧网络配置”复活” | 改 ShutDown |
AutomaticCheckpointsEnabled = True |
每次开机长一个差分盘,越用越占空间 | 关掉 + 删存量 |
第二条有个反直觉的细节:AutomaticStopAction 只能在 VM 关机状态下修改。VM 运行中执行 Set-VM 会返回成功,但 Get-VM 读回来纹丝不动——你会以为命令没生效,反复试。
四、网络定型:双网卡分工(以及一个更深的坑)
最终架构:
1 | 宿主机 |
对应 netplan 的关键两行:
1 | eth0: |
那个更深的坑就藏在”故意不写 routes”上。
我最初的版本是给 eth1 配了默认路由、出网走自建内部交换机的 New-NetNat。看起来更”干净”——永久网段、自主可控。然后宿主机重启了一次:
- portproxy 还在
- 宿主侧静态 IP 还在
- 防火墙规则还在
New-NetNat建的 NAT 对象,没了
于是虚拟机一条默认路由都没有(eth0 被我设成不抢路由),出网 100% 断死,DNS 最先死。而当时 Tailscale 显示 logged out,我一度以为是 Tailscale 故障。
教训:内部交换机可以做”永久入站”,但不要拿它做出网主路径。
出网交给 Hyper-V 自己托管的 Default Switch NAT——它每次开机自动重建,不需要你养。
五、VS Code 反复要密码,而命令行免密正常
这个现象非常迷惑:命令行 ssh devbox 秒进,VS Code Remote-SSH 每次都弹密码框。
两个原因叠加。
其一,陈旧 .pub 造成的假指纹。 我把公钥装进虚拟机后仍被拒。查了半天才反应过来:
1 | ssh-keygen -lf id_ed25519 # 报了个指纹 |
ssh-keygen -l 读私钥时会去拿同名的 .pub 文件,而那个 .pub 是上一把密钥的残留(私钥重新生成过,.pub 没跟着更新)。客户端拿 .pub 里的 blob 去 offer、却用不匹配的私钥签名 → 服务端必然拒绝。修复就是 -y 重新导出覆盖。
其二,私钥文件的 NTFS 权限太松。 Windows 自带的 OpenSSH(VS Code 用的就是它)会检查私钥 ACL,发现 BUILTIN\Users 和 Everyone 可读,就静默跳过密钥退化成密码认证。而 git-bash 里那个新版 OpenSSH 不做这个检查——所以”我能连、VS Code 不能连”。
1 | icacls $key /inheritance:r /remove "BUILTIN\Users" "Everyone" |
顺带一个 shell 坑:PowerShell 里
"$env:USERNAME:F"会被解析成带作用域的变量而变成空串,必须写"${env:USERNAME}:F"。
六、磁盘”只剩 19G”是个假象
根分区剩 8.5G,正准备停机扩虚拟磁盘,pvs 一看出问题了:
1 | PV VG PSize PFree |
pong from tailscale-termux via DERP(sfo) in 507ms
direct connection not established
1 |
|
配置过程不是一次到位,三个错误码恰好对应三个不同层次,值得存成查表:
| 现象 | 真实含义 | 我的实际问题 |
|---|---|---|
| 522 + 约 20 秒超时 | 流量没进隧道 | DNS 还是灰云 A 记录,指向了路由器 IP |
| 522 + 已解析到 CF 边缘 IP | 进了 CF,但源站不可达 | 把 A 记录直接点成橙云;隧道要求 CNAME → .cfargotunnel.com |
| 530 / 502(刚重启完) | 假故障 | cloudflared 重启后 QUIC 拨号有约 10 秒空窗,等日志出现 4 条 Registered tunnel connection 再测 |
| 502 + 1.4 秒返回 | 进了隧道,回源失败 | Service URL 写成 https://localhost:8443,而面板是明文 HTTP |
最后一条在日志里一目了然:
1 | ERR Unable to reach the origin service: |
判据:522 慢 → 查 DNS 是不是橙云 CNAME;502 快 → 查 cloudflared 日志的 originService。
配好之后把路由器所有端口转发关掉——公网入口只剩 Cloudflare 一条,被扫描爆破的面彻底收口。
九、AI 网关:把散落的 API Key 收成一个
基础设施稳了,回到真正目的。用 1Panel 装了 AI 网关,把各家模型聚合成一个入口。
账号池(5 个上游,全部健康):
| 账号 | 类型 | 内容 |
|---|---|---|
| Agnes | 文本 ×4 | 免费 beta 档 + 一个收费 pro 档 |
| 阿里云 Coding Plan | 文本 ×9 | coder 系列 + max + 多家第三方 |
| 硅基流动 | 文本 ×5 | 4B/8B 小模型 + R1 + GLM + OCR |
| Agnes | 文生图 ×1 | 图像模型 |
| 硅基流动 | 向量 ×1 | bge-m3,给智能路由用 |
模型组(关键认知:组内顺序是”故障转移链”,第一个模型吃 100% 流量,后面的只在它挂了才轮到):
1 | light : agnes-2.0-flash → qwen3-coder-next → agnes-2.5-flash → Qwen3.5-4B |
智能路由:虚拟模型名 auto,阈值 0.80。这里有个必须踩一次才知道的坑——智能路由是基于”首条消息的向量相似度”分类的,不是内置复杂度判断,而且它依赖向量服务;更关键的是,种子样本的向量索引默认没建:
1 | 188 条样本,vectorDim = 0 ← 索引没算,分类器等于空转 |
不触发一次全量向量计算,所有请求都会”无法可靠判定 → 按简单处理”。索引建完,分类立刻正常。
最终形态:一个 Base URL、一个 key,model=auto 走天下,需要特定时点名。
1 | from openai import OpenAI |
十、实测数据把我的直觉全推翻了
我原本的设计前提是:”agnes 免费又快速,应该当第一优先级;硅基流动慢,放最后。”
于是给 6 个候选模型各跑了 3 次实测:
| 模型 | 延迟 | 成功率 |
|---|---|---|
| qwen3-coder-next | 1.2 / 1.5 / 2.0s | 3/3 |
| qwen3-coder-plus | 1.9 / 2.2 / 3.0s | 3/3 |
| agnes-3.0-flash | 2.2 / 5.1 / 35s 超时 | 2/3 |
| agnes-2.0-flash | 28s / 失败 / 失败 | 1/3 |
| 硅基 Qwen3.5-4B | 41.6 / 45s | 慢但成功 |
| 硅基 Qwen3-8B | 18.6 / 45s | 慢但成功 |
三个结论,每个都和直觉相反:
- “免费又快”不成立。 agnes 是免费 beta,代价是限流 + 长尾(35 秒超时、连续失败)。放链首会让所有
auto请求先撞墙再降级,实测auto因此出现 40 秒延迟。 - 同账号内的多个模型互为备份毫无意义。 agnes 四个模型共用一个账号,账号级 cooldown 一来四个一起倒。真正的容灾必须跨供应商。
- 已经付费的 Coding Plan 反而是最好的主力。 1.2–3 秒、6/6 成功,而且额度是已经花出去的沉没成本。
最终我没有把 coder 提到首位(那会让额度消耗过度集中),而是选了折中方案:agnes 保持首位省额度,但把它的账号并发压到 1,并让 qwen3-coder-next 紧跟第二位——这样撞墙时 1~2 秒就降级到快模型,而不是掉进 40 秒的坑。
还有一个”第四个假信号”:我一度断言”cookie 的 SameSite=Strict 导致 OAuth 回调必挂”。
后来实测发现 state cookie 其实是 Lax,Strict 只在 csrf 上,真正的疑点是它只有 5 分钟有效期。
在没有对照实验之前,不要把推论当结论写进方案。
十一、接入端:两个小插曲
WorkBuddy 连不上。 报 404,且错误里目标显示为 https://aigw.example.com(没有 /v1)。真正原因有两层:Base URL 必须带 /v1;而且一旦勾选”工具调用/推理”,客户端会从 Chat Completions 切到 Responses API,而所有上游账号都只声明了 openaiChatCompletions 协议,Responses 路由无人可服务 → 404。所以那些开关就是不能勾,除非给账号补上 Responses 协议。
DeepSeek Harness。 1Panel 应用商店装的 deepseek-harness,Caddy 提供 HTTPS(自签本地 CA),配好模型后端指向网关的 auto 之后直接跑通:多轮 agent 任务、142 tok/s、77K token 上下文、缓存命中 19%。至此从”上游 API Key”到”能干活的应用”整条链路闭环。
十二、可复用的踩坑清单
| 坑 | 症状 | 解法 |
|---|---|---|
netplan match.macaddress 与实际 MAC 不符 |
接口 unmanaged,连 UP 都没有 |
先看 networkctl 的 SETUP 列,再用真实 MAC 重写 |
AutomaticStopAction=Save |
宿主重启后 VM 带旧网络配置复活 | 改 ShutDown,且只能在 VM 关机时改 |
New-NetNat 不跨宿主重启 |
出网全断、DNS 先死、Tailscale 显示 logged out | 出网走 Default Switch 自管 NAT;内部交换机网卡不配默认路由 |
| Ubuntu 装机只切 VG 的 64% | df 显示根分区很小 |
pvs 看 PFree,lvextend + resize2fs 在线扩 |
.pub 陈旧而私钥已重建 |
公钥装上仍被拒 | ssh-keygen -l 会读 .pub 报假指纹,用 -yf 反导 |
| 私钥 NTFS ACL 含 Users/Everyone | VS Code 要密码但命令行免密 | icacls /inheritance:r + 只留本人和 SYSTEM |
| 本机 curl 自己的 IP 得 200 | 误判”外部也能连入” | 必须换一台机器测 |
tailscale ping 有 pong |
误判”隧道能传业务数据” | 它是 disco 层应答,要抓包看 SYN |
| 在家里 WiFi 测自己公网 IP | 超时,误判公网不通 | 家宽多不支持 NAT 回环,用移动数据测 |
路由器”局域网主机”字段填了 IP:端口 |
端口转发全不通 | 主机字段只填 IP,内网端口填服务真实端口 |
Tunnel 的 Service URL 写 https:// |
502,TLS handshake 失败 | 明文源站填 http://,且域名须是橙云 CNAME |
智能路由种子样本 vectorDim=0 |
所有请求都判成”简单” | 触发一次全量向量索引重建 |
| Hyper-V 文本控制台手敲命令 | 字符被吞,命令变形 | 别在控制台配系统,先搞出 IP 走 SSH |
| 客户端勾了工具调用/推理 | 404 | 它会切 Responses API,而上游账号没声明该协议 |
十三、一句话总结
这一路最贵的不是配置,而是那些”看起来成功了”的瞬间:
有 pong 的tailscale ping、返回 200 的本机自测、”所有设备都超时”(其实只是路由器不支持回环)、
“免费所以该优先”(其实限流长尾最致命)。排障的真正门槛不是会配,而是知道每一个”成功”信号证明了什么、没证明什么。
凡是只验证了一半的结论,都要给它留一个被推翻的位置。
配图建议:① 双网卡分工拓扑(出网走 Default Switch NAT / 入站走内部交换机静态 IP)
② networkctl 显示 unmanaged 的截图 ③ 522 → 530 → 502 → 200 四次响应对照
④ 六个模型延迟实测柱状图 ⑤ 最终访问矩阵 + WorkBuddy / Harness 实跑截图
脱敏说明(发文前自查)
正文已做如下处理,发布前请再自查一遍:
- 公网出口 IP → 未出现,仅表述为”家宽公网 IP”
- 内网地址
192.168.x.7(宿主)、192.168.x.10(虚拟机)→ 已用 x 占位 - Cloudflare 域名与隧道主机名 → 统一改为
aigw.example.com/panel.example.com/dsh.example.com,真实子域名与主域名不出现 - Tailscale IP 与 tailnet 名 →
100.x.x.x/ 不出现;peer 名改为通用描述 - 所有账号密码、API Key、隧道 token → 一律
/,正文无任何真实凭据 - 1Panel 面板安全入口码 → 表述为”入口码”
- 飞书 / 钉钉的 Tenant ID、App ID → 未出现(该功能与主线无关,正文只保留结论)
- 网卡 MAC → 保留
00:15:5d:02:10:04/05/07,这是 Hyper-V 本机动态分配值,不具公网可定位性;若介意可改为00:15:5d:02:10:0x - 模型名与供应商 → 保留真实名称(属公开信息,且是本文技术价值所在)
如需保留真实域名作为示例,把 example.com 换回你自己的域名即可;
其余占位符已覆盖所有可直接定位到人的标识符。