SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor

如何自托管 LiteLLM,搭建统一 LLM 网关

在 Ubuntu 24.04 VPS 上用固定版本镜像部署 LiteLLM,提供一个 OpenAI 兼容端点,配置虚拟密钥、单密钥预算、模型故障转移和费用日志。

自托管 LLM 网关的作用

LiteLLM 是一个由您自行托管的开源 LLM 网关:它提供一个 HTTP 端点,所有应用都调用该端点,网关再将每个请求转发给应答的服务商。LLM 是大型语言模型的缩写。该网关使用 OpenAI 聊天补全 API(应用程序编程接口),因此任何已经能够调用 OpenAI 的客户端库,只需修改两项配置即可使用:基础 URL 和密钥。

这一层间接关系正是网关的价值所在。应用不再保存服务商凭据。更换模型时,只需修改服务器上的一行配置,而不必修改 5 个服务中的代码。由于所有调用都经过同一个进程,您可以在此设置预算,并记录费用支出。

运行后,您将获得以下功能:

  • 一个端点。 应用访问 https://gateway.example.com/v1,并请求您自定义的模型名称,例如 bulk 或 strong。
  • 虚拟密钥。 每个应用都有独立的密钥、模型允许列表和支出上限。撤销某个密钥不会影响其他密钥。
  • 故障转移。 请求失败或提示词过大时,网关会自动改用其他模型重试。
  • 日志记录。 每个请求都会写入一条包含费用的记录,因此可以查明“哪个应用产生了这笔支出”。

为什么自行运行网关

托管路由器的结构相同,只是每个请求中间都有第三方进程参与。自行运行后,服务提供商密钥和提示词文本都保存在您控制的服务器上。代价也很明确:您需要运维这个所有应用都依赖的组件。本指南的最后一节将介绍这项代价,因为大多数文章都会忽略这一部分。

所需条件

  • 一台运行 Ubuntu 24.04 的 VPS(虚拟专用服务器),并已安装 Docker 和 Compose 插件。
  • 一个指向该 VPS 的域名。如果服务器外部的设备将通过 TLS(传输层安全)访问网关,则需要此域名。
  • 至少一个提供商 API 密钥。

网关不执行推理。它只转发请求并流式返回响应,因此 CPU 负载取决于请求量,而不是模型大小。一台配备 1 vCPU 的服务器可以稳定运行多个内部应用。增长较快的是数据库,因为网关会为每个请求写入一条支出记录。

先编写 config.yaml

配置文件决定客户端可以请求哪些模型。需要关注4个顶级部分:model_list、litellm_settings、router_settings 和 general_settings。

model_list:
  - model_name: bulk
    litellm_params:
      model: anthropic/claude-haiku-4-5
      api_key: os.environ/ANTHROPIC_API_KEY
  - model_name: strong
    litellm_params:
      model: anthropic/claude-sonnet-5
      api_key: os.environ/ANTHROPIC_API_KEY
  - model_name: strong
    litellm_params:
      model: openai/gpt-5.5
      api_key: os.environ/OPENAI_API_KEY

litellm_settings:
  num_retries: 2
  request_timeout: 120
  json_logs: true
  set_verbose: false

router_settings:
  allowed_fails: 3
  cooldown_time: 30
  fallbacks: [{"bulk": ["strong"]}]
  context_window_fallbacks: [{"bulk": ["strong"]}]

general_settings:
  background_health_checks: true
  health_check_interval: 300

model_name 是客户端发送的名称。litellm_params.model 是实际模型,写作 provider/model。模型名称应按用途命名,而不是按供应商命名。请求 bulk 的应用在您下个月决定将 bulk 切换为其他模型后,仍可继续运行。

api_key: os.environ/ANTHROPIC_API_KEY 告诉 LiteLLM 在运行时读取该变量。文件中不会出现实际密钥,因为 config.yaml 是您提交到版本库的文件。

这里有两个名称为 strong 的条目,这是有意的。当多个部署使用同一个 model_name 时,路由器会将它们视为可互换,并将发往该名称的请求分配到这些部署。这样,某个供应商短时间出现故障时,strong 仍可继续工作:一个部署进入冷却状态后(见下文),发往 strong 的新请求会转到另一个部署。

num_retries: 2 会对可重试错误最多重试2次。这些重试可以选择同一模型组中其他可用的部署:LiteLLM 的架构文档于 2026-10-29 核查,文档将组内重试与跨组回退描述为两个独立步骤。因此,strong 的重试可以转到另一家供应商,无需启用加权故障转移。只有这些重试全部用尽后,才会触发回退。

allowed_fails 和 cooldown_time 是路由器选项,因此应放在 router_settings 下。代理根据该部分构建路由器。在 v1.95.0 中,如果将 cooldown_time 写在 litellm_settings 下,它不会传递给路由器,因此冷却时间会静默保持为默认的5秒。cooldown_time: 30 设置部署进入冷却状态后离开轮询的时间。配置参考文档将 allowed_fails 定义为:部署在 cooldown_time 内必须超过的失败次数,超过后才会进入冷却状态。在 v1.95.0 的源代码中,allowed_fails: 3 是默认值;使用默认值时,路由器会采用内置触发条件,只有设置其他数值后,失败次数才会生效。这些触发条件包括:对于包含多个部署的组,收到429响应后立即进入冷却状态;此类组在当前分钟内至少处理5个请求且失败率超过50%时进入冷却状态;以及路由器不会重试的错误,例如密钥错误导致的401响应。还有一个独立触发条件:当单个部署在当前分钟内的所有请求都失败,且流量达到 SINGLE_DEPLOYMENT_TRAFFIC_FAILURE_THRESHOLD 时,该部署也会进入冷却状态;该值默认为1000个请求。供应商在短时间内连续返回500错误时,路由器不会在每个请求上反复尝试该部署;而每隔几分钟出现一次孤立的500错误,则不会触发冷却。

fallbacks 和 context_window_fallbacks 使用不同的触发条件,第二个是实用但常被忽略的选项。

  • fallbacks 在主调用失败时触发。
  • context_window_fallbacks 在供应商因请求长度超过该模型的上下文窗口而拒绝请求时触发。这样,过大的提示会转发到有足够上下文空间的模型,而不是向调用方返回错误。

此外还有 content_policy_fallbacks,用于处理供应商因内容策略原因拒绝请求的情况。只有在您有合适的目标可以转发这些调用时,才应设置该选项。

在 VPS 上使用 Docker Compose 部署 LiteLLM

创建一个目录,并在其中放置 3 个文件:config.yaml、docker-compose.yml 和 .env。上游快速入门指南使用 latest 标签。请改用固定的发布标签,这样下个月执行 docker compose up -d 时,得到的网关版本与今天相同,回滚也只需修改一行。

services:
  litellm:
    image: ghcr.io/berriai/litellm:v1.95.0
    restart: unless-stopped
    command: ["--config", "/app/config.yaml", "--num_workers", "1"]
    ports:
      - "127.0.0.1:4000:4000"
    volumes:
      - ./config.yaml:/app/config.yaml:ro
    env_file: .env
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16
    restart: unless-stopped
    environment:
      POSTGRES_USER: litellm
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
      POSTGRES_DB: litellm
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U litellm"]
      interval: 5s
      timeout: 5s
      retries: 10
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Compose 在这里会读取 .env 两次。第一次用于替换 compose 文件中的 ${POSTGRES_PASSWORD},第二次通过 env_file 将所有变量传入容器。

v1.95.0 是 2026 年 8 月的当前版本。部署时请查看项目的 releases 页面,并固定当时的当前版本。每个版本都会发布签名,因此可以在信任镜像前先进行验证:

cosign verify --key https://raw.githubusercontent.com/BerriAI/litellm/v1.95.0/cosign.pub ghcr.io/berriai/litellm:v1.95.0

端口配置为 127.0.0.1:4000:4000,因此只会在 loopback 接口上发布该端口。改为 4000:4000 后,整个互联网都可以访问网关,因为 Docker 会在 iptables 的 FORWARD 链中添加自己的规则,并且这些规则会在 ufw 的规则之前评估,所以 ufw deny 4000 无法阻止访问。这是自托管网关意外暴露的最常见方式:参见Docker 如何绕过 ufw 直接发布容器端口。外部流量会改由反向代理进入。

不要将提供商密钥放入镜像

.env 文件包含所有机密信息。运行时会将其作为环境变量传入,因此不会被写入镜像,也不会提交到版本库。

LITELLM_MASTER_KEY=sk-REPLACE_ME
LITELLM_SALT_KEY=sk-REPLACE_ME_TOO
POSTGRES_PASSWORD=REPLACE_ME_AS_WELL
DATABASE_URL=postgresql://litellm:REPLACE_ME_AS_WELL@db:5432/litellm
STORE_MODEL_IN_DB=True
LITELLM_MODE=PRODUCTION
LITELLM_LOG=ERROR
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-proj-...

使用真正的随机数生成两个 LiteLLM 密钥,然后限制文件权限:

printf 'sk-%s\n' "$(openssl rand -hex 32)"
chmod 600 .env

LITELLM_MASTER_KEY 是管理员凭据。它用于验证管理 API,并作为 /ui 处 Admin UI 的密码。任何应用都不应持有它。

LITELLM_SALT_KEY 用于加密存储在数据库中的提供商凭据。设置一次后保持不变。如果之后修改它,已存储的凭据将无法解密。因此,网关会正常启动,但随后对这些提供商的每次调用都会因身份验证失败而失败。

STORE_MODEL_IN_DB=True 允许您直接在 Admin UI 中添加和编辑模型,无需修改 config.yaml。这很方便,但也会让配置的唯一真实来源分成两处。确定哪一处是权威来源,并将决定记录在 config 旁边。

将密钥排除在配置文件之外的理由,同样适用于不将密钥交给提供给代理使用的工具。避免将提供商机密交给 AI 代理介绍了这种模式,Docker Compose 中的环境文件和机密信息介绍了具体操作。

启动服务并监控首次启动过程:

docker compose up -d
docker compose logs -f litellm

检查服务是否实际正常运行

这里有两个无需身份验证的探针和一个需要身份验证的探针。它们失败的原因不同。

curl -s http://127.0.0.1:4000/health/liveliness
curl -s http://127.0.0.1:4000/health/readiness

/health/liveliness无需身份验证,进程运行时会返回 "I'm alive!"。/health/readiness同样无需身份验证。它会返回包含 "status": "healthy"和 db字段的 JSON 对象;如果无法访问数据库,则返回 503。请将监控指向 readiness,因为对于无法查询任何虚拟密钥的网关,liveness 仍会保持正常。

需要身份验证的检查会与提供商通信:

curl -s http://127.0.0.1:4000/health \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY"

它会返回 healthy_endpoints和 unhealthy_endpoints数组。位于 unhealthy_endpoints中的模型如果出现身份验证错误,表示 .env中的提供商密钥错误或缺失。这正是当前需要定位的故障。由于已设置 background_health_checks: true,代理会自动每 health_check_interval秒运行一次这些探针,而 /health会返回最近一次结果。因此,轮询该端点不会每次都向提供商发送测试请求。

虚拟密钥与每个密钥的预算

每个应用都有自己的密钥,该密钥基于主密钥生成。

curl -s http://127.0.0.1:4000/key/generate \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "key_alias": "nightly-summariser",
    "models": ["bulk"],
    "max_budget": 5,
    "budget_duration": "30d",
    "rpm_limit": 60,
    "tpm_limit": 200000
  }'

响应中包含一个以 sk- 开头的 key 字段。该字符串就是应用获得的内容,也是应用唯一能获得的内容。

  • models 是此密钥可以请求的内容白名单。上面的密钥只能请求 bulk,不能请求其他内容。
  • max_budget: 5 配合 budget_duration: "30d" 表示每个日历月 5 美元。LiteLLM 会在每月 1 日午夜重置 30d 预算,除非你在 litellm_settings 下设置 timezone,否则使用 UTC;它不是滚动 30 天周期。因此,密钥支出达到 5 美元后会停止工作,直到下一个月的 1 日。
  • rpm_limit 和 tpm_limit 分别限制此密钥自身每分钟的请求数和令牌数。
  • key_alias 是你在 6 周后查看支出日志时仍能识别的名称。始终设置它。

预算用尽后,调用会失败,并返回类型为 budget_exceeded 的错误,消息格式如下:

Budget has been exceeded! Key=<key> Current cost: <spend>, Max budget: <max_budget>

状态码取决于版本。在固定使用的 v1.95.0 中,状态码为 HTTP 429。从 v1.104.0 开始,默认状态码为 HTTP 422;在 litellm_settings 下设置 budget_exceeded_status_code: 429 可恢复为之前的状态码。真正的 rpm_limit 和 tpm_limit 触发仍会在两个版本中返回 429。每次升级后都要确认实际返回的状态码。

状态码会造成混淆,因为这两个状态码都不会说明“预算”。客户端库会将 429 报告为速率限制,并可能通过退避机制重试;但预算要等到重置后才会恢复。客户端库会将 422 报告为无法处理的请求。将响应正文与状态码一起记录,否则每次预算耗尽都会看起来像其他问题。

通过同一个管理 API 检查和调整密钥:

curl -s "http://127.0.0.1:4000/key/info?key=sk-..." \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY"

curl -s -X POST http://127.0.0.1:4000/key/update \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"key": "sk-...", "max_budget": 25}'

网关实施的预算限制始终有效,即使出问题的是 agent 本身。因此,它是 VPS 上 AI agent 成本控制 的基础。

将批量任务发送到低成本模型

将客户端指向网关。配置基础 URL、密钥和模型名称:

curl -s http://127.0.0.1:4000/v1/chat/completions \
  -H "Authorization: Bearer sk-<the virtual key>" \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "bulk",
    "messages": [{"role": "user", "content": "Say hello in five words."}]
  }'

所有 OpenAI 客户端库的行为都相同:将 base_url 设置为 https://gateway.example.com/v1,将 api_key 设置为虚拟密钥。

config.yaml 中的路由策略现在会自动生效,调用方无需了解具体细节。对 bulk 的请求会发送到低成本模型。如果该调用在重试后仍然失败,请求会改为发送到 strong。如果提示词过长,超出 bulk 的限制,context_window_fallbacks 会将其发送到 strong,而不是直接返回错误。分类任务或摘要积压等批量工作默认使用低成本模型,只有复杂请求会产生更高费用。

对于使用工具的代理,这也是网关发挥作用的场景。位于同一 VPS 上的 MCP(模型上下文协议)服务器和驱动它的代理都可以指向同一个端点。这样无需重新部署任一组件,就可以更换它们背后的模型。

如何知道发生了回退?

这是最容易产生费用的故障模式,因为表面上没有任何异常。成功的回退会返回 HTTP 200,以及普通的响应正文。您的低价模型可能停机一天,期间每个请求都悄悄由高价模型处理,而您看到的第一条证据就是账单。

证据确实存在于响应标头中。请请求这些标头:

curl -s -D - -o /dev/null http://127.0.0.1:4000/v1/chat/completions \
  -H "Authorization: Bearer sk-<the virtual key>" \
  -H 'Content-Type: application/json' \
  -d '{"model":"bulk","messages":[{"role":"user","content":"ping"}]}' \
  | grep -i '^x-litellm'
  • x-litellm-model-group 是请求路由到的模型名称,例如 bulk。x-litellm-model-id 是实际响应的部署 ID,您可以使用 /v1/model/info?litellm_model_id=... 查询该 ID。当该 ID 不属于您的 bulk 部署时,就发生了回退。
  • x-litellm-attempted-fallbacks 和 x-litellm-attempted-retries 用于统计回退次数。在正常请求中,这两个字段的计数都不会大于 0。
  • x-litellm-response-cost 是该请求的美元成本。
  • x-litellm-call-id 是用于在日志中查找同一请求的标识符。

为每个请求记录 x-litellm-attempted-fallbacks,并在其大于 0 时发出告警。这个数字决定了路由策略是在正常工作,还是已经悄悄变成“始终使用高价模型”。

完整方案是分布式追踪,需要单独配置:用于追踪代理调用的自托管 Langfuse。LiteLLM 提供了相应的回调,因此接线只需两行配置和凭据。

litellm_settings:
  success_callback: ["langfuse"]
  failure_callback: ["langfuse"]
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_HOST=https://langfuse.example.com

对于 Langfuse v3 及更高版本,LiteLLM 的 Langfuse 页面现在推荐使用其 langfuse_otel 预设;上面的 langfuse 回调仍有文档说明。请同时设置 failure_callback 和 success_callback。如果跳过它,您保留的唯一追踪记录就是没有发生任何错误的请求。除此之外,LiteLLM 会将每个请求的支出记录写入 Postgres,Admin UI 在 /ui 读取该表。该表会随流量增长,因此请在磁盘容量较小时监控它。

将网关置于反向代理之后

任何外部请求都不应访问端口 4000。在 nginx 或 Caddy 中终止 TLS,然后转发到 loopback 地址。

location / {
    proxy_pass http://127.0.0.1:4000;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_buffering off;
    proxy_read_timeout 600s;
}

人们经常遗漏其中两行。proxy_buffering off 很重要,因为流式完成响应由一系列服务器发送事件组成。启用缓冲后,nginx 会先将数据块收集到缓冲区,再转发这些数据,从而延迟客户端接收。proxy_read_timeout 600s 也很重要,因为 nginx 默认的 60 秒是它等待从网关进行两次读取之间的最长时间,而不是整个响应的时限。非流式完成响应会一直等到生成结束后才发送数据。因此,较长的响应可能超过 60 秒仍未收到任何字节,客户端会收到 504,而错误日志会记录 upstream timed out (110: Connection timed out) while reading response header from upstream。只要流式完成响应持续发送数据块,就可以运行远超 60 秒;只有模型静默达到该时长时,才会触发超时。

证书配置可直接参考 在 nginx 上使用 Let's Encrypt 的 Certbot。如果该服务器已经运行多个容器,在多个 Compose 应用前使用 Traefik 可以集中处理路由和证书。

网关现在是单点故障

请如实评估当前的架构。您现在运行的每个应用都依赖一台 VPS 上的一个容器。该容器停止运行期间,任何应用都无法调用模型,包括运行正常的提供商。这会带来以下 4 个后果。

  • 错误的配置会同时导致所有服务停止。 restart: unless-stopped 会重启崩溃的容器,也会反复重启无法解析 config.yaml 的容器。每次修改配置后都要查看 docker compose logs litellm,并在有时间观察的情况下修改配置。
  • 缓存未命中时,Postgres 位于请求路径中。 未缓存密钥时,虚拟密钥查询会使用 Postgres;响应返回后,消费记录会异步写入。/health/readiness 返回 503,说明网关正在运行,但无法完成这两项操作。
  • 先使用 CPU 核心,再增加实例。 项目生产环境建议每个实例使用一个 worker(--num_workers 1),适用于 Kubernetes 或其他会自动扩展 pod 实例的环境。对于本示例中的单个 VM 或裸容器,这项建议正好相反:将 worker 数量设置为机器的 vCPU 数量。上面的 1 vCPU 机器仍应设置为 --num_workers 1;更大的机器应提高该数量,同时注意每个 worker 都会占用自己的内存。在负载均衡器后面增加第二个网关,可以消除单个容器这一故障点,但无法消除数据库这一故障点。
  • 备份无法重新生成的数据。 这包括 config.yaml 和 .env,以及数据库的 pg_dump。丢失 LITELLM_SALT_KEY 会使转储文件中的加密提供商凭据失效,因此环境文件和转储文件应由同一个备份任务处理:将 restic 备份到外部存储。

升级时,修改镜像标签并运行 docker compose up -d。LiteLLM 默认会在启动时运行 prisma migrate deploy,因此新容器会在首次启动时迁移数据库架构。修改标签前先创建转储,因为恢复旧镜像无法撤销已经执行的迁移。

请阅读所有跳过版本的发行说明,而不只是目标版本的说明。从 v1.95.0 升级到 v1.104.0 或更高版本时,会跨越多个不兼容变更:上文提到的预算状态码;未设置、为空或已公开的主密钥会导致代理拒绝启动;数据库初始化在启动时失败会导致代理退出,在 restart: unless-stopped 中表现为反复重启。从 v1.102.x 或更早版本升级时,v1.104.0 的说明还要求您自行创建两个 LiteLLM_SpendLogs 索引。

FAQ

LiteLLM 是否会为每次调用增加明显延迟?

根据项目 README 在 2026 年 10 月的说明,项目公布的指标是在每秒 1000 个请求时,95% 分位延迟为 8 ms。请将其视为供应商提供的数据。真正影响延迟的是应用与网关之间的网络距离,因为每次调用都增加了一次往返。将网关部署在调用它的应用所在区域,然后使用真实响应中的 x-litellm-overhead-duration-ms 响应头测量自身开销。

将 nginx 放在前面后,为什么流式传输停止工作?

因为 nginx 默认会缓冲上游响应,而流式完成响应由一系列服务器发送事件组成。启用 proxy_buffering 后,nginx 会先将数据块收集到缓冲区,再转发这些数据,从而延迟客户端接收。请在 location 块中设置 proxy_buffering off;。同时在同一块中增大 proxy_read_timeout。默认情况下,nginx 在 60 秒内未收到网关数据时会放弃等待;长时间的非流式生成很容易达到这一时限,客户端随后会收到 504。

虚拟密钥用尽预算后会发生什么?

调用会失败,并返回类型为 budget_exceeded 的错误,消息格式为 Budget has been exceeded! Key=<key> Current cost: <spend>, Max budget: <max_budget>。在固定版本 v1.95.0 中,状态码为 HTTP 429;从 v1.104.0 开始,默认状态码改为 HTTP 422。这两个状态码都不会说明预算问题,这正是容易误判的地方:客户端库会将 429 报告为速率限制,并可能重试,于是人们开始排查限流,而不是读取错误消息。请将响应正文与状态码一起记录。使用主密钥通过 /key/info?key=sk-... 确认该密钥的实际预算状态;如果预算设置过低,则使用 /key/update 提高上限。

网关除了托管模型外,也可以路由到本地模型吗?

可以,本地模型只是 model_list 中的另一项。使用带有 api_base 的 ollama_chat/ 前缀,例如将 model: ollama_chat/llama3.1 与 api_base: http://ollama:11434 一起使用。在 Compose 的默认网络配置中,容器内的 localhost 表示该容器自身,因此应使用 Compose 服务名或主机在 Docker 网络上的地址,绝不要使用 127.0.0.1。主机网络模式是例外;在该模式下,容器与主机共享网络协议栈。启动本地模型是另一项工作,请参阅 在 VPS 上使用 Ollama 自托管 LLM。

这篇文章对您有帮助吗?