为什么个人 AI 项目也该尽早加一层 LLM Gateway
一开始做 AI 功能,最直觉的写法是:业务代码里直接 from openai import OpenAI,过几天再加 Anthropic,再过几天补一个国内模型或本地 Ollama。短期能跑;一旦 Web、桌面、IDE 插件、定时脚本一起上,密钥、超时、降级和计费会散成几坨。
当 Web、桌面、IDE 插件与定时任务同时接入模型能力时,直接在业务里拼接多家 SDK 很快会失控。更稳妥的做法是先建立一层统一网关:对外提供稳定的 OpenAI 兼容接口,对内完成鉴权、路由、降级与观测。
1. 没网关时,代码会怎么烂掉
下面是真实项目里很常见的「第一版」——看起来短,维护三个月后会很痛:
# bad_app.py —— 业务里直接握多家 Key
import os
from openai import OpenAI
import anthropic
def chat(provider: str, prompt: str) -> str:
if provider == "openai":
c = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
r = c.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
timeout=60,
)
return r.choices[0].message.content or ""
if provider == "anthropic":
c = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
r = c.messages.create(
model="claude-sonnet-4-0",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return r.content[0].text
if provider == "ollama":
c = OpenAI(
api_key="ollama",
base_url="http://127.0.0.1:11434/v1",
)
r = c.chat.completions.create(
model="qwen2.5:14b",
messages=[{"role": "user", "content": prompt}],
)
return r.choices[0].message.content or ""
raise ValueError(provider)问题不在「能不能通」,而在横切能力散落:
| 横切能力 | 散落写法的后果 |
|---|---|
| 密钥 | 每个仓库、每个客户端各存一份,轮换一次改四处 |
| 超时 / 重试 | Web 一套、脚本一套,行为不一致 |
| 降级 | 业务 if/else 越写越长,还经常忘测 |
| 成本 | 只能月末看账单,不知道哪个功能在烧 |
| 协议差异 | 流式、tool calling 字段各家不同,客户端重复适配 |
Gateway 要收口的,正是这张表。
2. 目标架构:客户端只认识一个 base_url
约定对外契约:
- URL:
https://api.your-domain.cloud/v1(或本机http://127.0.0.1:4000/v1) - 鉴权:客户端只拿虚拟 Key(
sk-xxx),真实 Provider Key 只放网关环境变量 - 协议:优先 OpenAI Chat Completions;需要 Anthropic Messages 的再单独开兼容路由
这样桌面端、插件、CI 脚本都能复用同一套 SDK,只改两行:
# good_client.py —— 业务只打网关
from openai import OpenAI
client = OpenAI(
api_key="sk-local-dev", # 网关发的虚拟 Key
base_url="http://127.0.0.1:4000/v1",
)
resp = client.chat.completions.create(
model="auto", # 网关侧映射到真实模型
messages=[
{"role": "system", "content": "你是简洁的中文技术助手。"},
{"role": "user", "content": "用三句话解释什么是 LLM Gateway。"},
],
temperature=0.3,
)
print(resp.choices[0].message.content)
print("usage:", resp.usage)3. 实践:用 LiteLLM Proxy 搭最小可运行网关
一种常见且成熟的实现是 LiteLLM Proxy:用 YAML 声明模型,经 Docker 或本机进程拉起后,对外提供 OpenAI 兼容接口。
3.1 config.yaml
model_list:
# 本地优先:开发机 / 内网 GPU
- model_name: local-qwen
litellm_params:
model: ollama/qwen2.5:14b
api_base: http://host.docker.internal:11434
rpm: 30
# 云端兜底:本地挂了再走
- model_name: cloud-mini
litellm_params:
model: openai/gpt-4o-mini
api_key: os.environ/OPENAI_API_KEY
# 对外暴露的「逻辑模型名」
- model_name: auto
litellm_params:
model: ollama/qwen2.5:14b
api_base: http://host.docker.internal:11434
router_settings:
# 主模型失败时按顺序切换
fallbacks:
- auto: ["cloud-mini"]
- local-qwen: ["cloud-mini"]
general_settings:
master_key: sk-change-me-in-prod
# 生产务必换掉,并用环境变量注入3.2 docker-compose.yml(轻量,适合 4G 小机做「网关本身」)
注意:网关进程很轻;不要把 14B 本地模型硬塞进只有 4G 内存的同机。本地推理放开发机或独立 GPU 机,云主机只跑代理 + 反代。
services:
litellm:
image: ghcr.io/berriai/litellm:main-latest
ports:
- "127.0.0.1:4000:4000" # 只绑本机,外面走 Nginx
volumes:
- ./config.yaml:/app/config.yaml:ro
environment:
OPENAI_API_KEY: ${OPENAI_API_KEY}
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
command: ["--config", "/app/config.yaml", "--port", "4000"]
restart: unless-stopped启动与冒烟:
export OPENAI_API_KEY=sk-...
docker compose up -d
curl -s http://127.0.0.1:4000/v1/chat/completions \
-H "Authorization: Bearer sk-change-me-in-prod" \
-H "Content-Type: application/json" \
-d '{
"model": "auto",
"messages": [{"role":"user","content":"ping"}]
}' | jq '.choices[0].message.content, .model, .usage'4. 案例:一次「IDE 插件偶发超时」的排障
背景(复现过多次的模式):
- 桌面 / IDE 插件直接打 Anthropic
- Web 后台直接打 OpenAI
- 夜间脚本打 Ollama
- 某天 Anthropic 区域性抖动,插件大面积超时,Web 却正常——用户以为「我们的产品坏了」
上了网关之后的改法:
- 三端全部改
base_url到网关 - 逻辑模型统一叫
coding/chat,由网关映射 coding主走 Claude,fallback 到gpt-4o-mini,再不行提示「降级中」- 网关日志固定打:
request_id, model, upstream, latency_ms, status
示例:给每次调用挂上可追踪 ID(客户端侧):
import uuid
from openai import OpenAI
client = OpenAI(api_key="sk-local-dev", base_url="http://127.0.0.1:4000/v1")
rid = str(uuid.uuid4())
resp = client.chat.completions.create(
model="auto",
messages=[{"role": "user", "content": "总结这段报错:Connection reset by peer"}],
extra_headers={"X-Request-Id": rid},
)
print(rid, resp.model, resp.usage)网关侧用 access log / 应用日志按 X-Request-Id 检索,就能回答:「这次是上游 429,还是我们自己 60s 砍断的?」——没有这一层,只能对着三个控制台猜。
5. 和「进化手记」方向怎么对齐
我们站点主线是前端 → AI 应用 → 兼容代理 → 桌面 / 部署。Gateway 正好是中间那块「插座标准」:
- 像 lingma-proxy 这类项目,本质是把 OpenAI / Anthropic / 国内协议收成稳定调用面
- 主域放内容站,
api.子域挂网关,密钥与业务仓库分离(见后文基础设施篇) - 客户端(Web / Tauri / 插件)只依赖契约,不依赖某一家 SDK 细节
6. 什么时候上、第一版做到哪
建议上线的信号(两条即够):
- 已经在用 ≥2 个供应商,或「云 + 本地」混用
- 同一个能力要被 ≥2 个客户端调用
第一版最小清单(刻意做薄):
- 统一入口 + 虚拟 Key
- 至少一条主备 fallback
- 请求日志:
model / latency / tokens / status - Provider Key 移出业务仓库
- 公网只经 HTTPS 反代,网关端口不直暴
语义缓存、复杂预算、多租户可以后补。先把调用面收口,后面谈工程化才有抓手。