
1. 为什么一台机器要同时装 Ollama、vLLM 和 SGLang很多人第一次做 LLM 私有化部署都会卡在同一个问题上到底选哪个推理引擎。我见过太多团队在选型阶段反复横跳最后要么全押 Ollama 结果并发一上来就崩要么直接上 vLLM 却发现本地调试体验很差。真实情况是这三个引擎解决的根本不是同一类问题把它们放在一台机器上用 LiteLLM 做统一入口才是性价比最高的做法。先说 Ollama。它的定位是「个人开发者的本地模型管理器」安装一条命令拉模型一条命令自带 OpenAI 兼容接口。你写 LangChain 代码时把 base_url 指到http://localhost:11434/v1就能跑。但它的调度器是为单用户设计的实测在 128 并发下吞吐会断崖式下跌和 vLLM 差一个数量级。所以它适合开发调试、写 prompt、验证模型能力不适合对外提供服务。vLLM 是生产级推理引擎的代表。它的 PagedAttention 把 KV cache 按页管理显存利用率比朴素实现高很多配合 continuous batching多请求并发时吞吐能拉满。它的 OpenAI 兼容服务vllm serve是很多团队上生产的默认选择。缺点是启动慢、显存占用高、参数多调不好就 OOM。SGLang 是这两年 agent 场景里冒出来的黑马。它的 RadixAttention 用基数树管理 prefix cache对于 system prompt 很长、多轮对话、多 agent 共享前缀的场景命中率极高。我实测过一组 prefix-heavy 的请求同样 100 个请求vLLM 跑 60 秒SGLang 跑 42 秒差距接近 30%。如果你的应用是 RAG 多 agent 编排SGLang 往往比 vLLM 更合适。那为什么还要 LiteLLM因为你的应用层不应该关心后端是哪个引擎。LiteLLM Proxy 提供一个统一的 OpenAI 兼容网关你在 YAML 里定义 model_name 到实际后端的映射应用层永远只调一个名字。后端从 Ollama 换成 vLLM或者加一个云端 fallback应用代码一行不用改。这就是「统一入口」的价值。最后用 Docker Compose 把这一套编排起来一条docker compose up -d就能把推理引擎、网关、缓存、数据库全部拉起来。这套组合适合谁适合想在一台带 GPU 的机器上跑通完整私有化 LLM 服务的开发者尤其是做 agent、RAG、代码助手这类 prefix 复用率高的场景。下面我把可直接复制的配置和踩过的坑都写出来。2. TaoToken 前置准备统一入口与 Key 管理在动手编排之前先把「统一入口」这件事想清楚。本地这套 Compose 栈解决的是推理引擎的编排但实际项目里你往往还需要一个稳定的外部入口来做模型对比、云端 fallback、或者团队共享。这时候可以用 TaoToken 作为统一接入层它的 API 地址是https://taotoken.net/api兼容 OpenAI 协议你现有的 OpenAI SDK 代码改个 base_url 就能用。为什么要在私有化部署的教程里提这个因为真实工程里纯本地部署和纯云端调用都不是最优解。本地跑主力模型保证数据不出内网云端做 fallback 和效果对比LiteLLM 的 router 正好能把两者串起来。TaoToken 在这里扮演的角色是「云端那一侧的稳定入口」你不需要为每个模型单独维护一套鉴权逻辑。先拿到 Key。访问 API Keys 管理页面https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite在控制台里创建一个新的 API Key复制出来保存好。这个 Key 后面会写进 LiteLLM 的配置里作为云端 fallback 的凭证。注意不要把它硬编码进代码仓库用环境变量注入。如果你还没确定要用哪些模型可以先在模型对话页面里试一下效果确认模型 ID 再写进配置https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite接入文档在这里里面有完整的 Base URL、鉴权方式和请求示例写 LiteLLM 配置时对照着看https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite这里要强调一个概念LiteLLM 配置里的model字段和model_name字段是两回事。model_name是你应用层看到的名字比如veri-copilot-defaultmodel是 LiteLLM 实际去调用的后端标识。对于本地引擎model写成openai/模型名加上api_base指向本地端口对于 TaoToken 这类兼容 OpenAI 的入口model写成openai/模型IDapi_base指向https://taotoken.net/apiapi_key从环境变量读。把 Key 写进.env文件和 compose 放同一目录# .env TAOTOKEN_API_KEYsk-你的key OPENAI_API_KEYsk-可选的云端key ANTHROPIC_API_KEYsk-可选的云端key LITELLM_MASTER_KEYsk-1234-changeme POSTGRES_PASSWORDsecure_passLiteLLM 的master_key是网关自己的鉴权 key应用层调 LiteLLM 时用它不要用 default 值上生产。这个 key 和 TaoToken 的 key 是两个层级的东西别搞混master_key 管的是「谁能访问你的 LiteLLM 网关」TaoToken 的 key 管的是「LiteLLM 去调云端时用什么凭证」。前置准备做到这里就够了一个 TaoToken Key、一个 LiteLLM master key、一份 .env。接下来进入配置环节。3. 可复制配置docker-compose.yml 与 LiteLLM 路由这一节是全文的核心所有配置都可以直接复制。先给目录结构避免路径对不上llm-stack/ ├── docker-compose.yml ├── litellm_config.yaml ├── prometheus.yml ├── .env └── models/ └── Qwen2.5-Coder-14B-Instruct-GPTQ-Int4/先写docker-compose.yml。这里用 SGLang 作为本地主力引擎LiteLLM 做网关Redis 做缓存Postgres 存 LiteLLM 的用量数据Prometheus Grafana 做监控。GPU 部分用runtime: nvidia和NVIDIA_VISIBLE_DEVICES指定卡号。# docker-compose.yml version: 3.9 services: sglang: image: lmsysorg/sglang:latest runtime: nvidia environment: NVIDIA_VISIBLE_DEVICES: 0 ports: - 30000:30000 volumes: - ./models:/models - sglang-cache:/root/.cache command: python -m sglang.launch_server --model-path /models/Qwen2.5-Coder-14B-Instruct-GPTQ-Int4 --host 0.0.0.0 --port 30000 --tp-size 1 --mem-fraction-static 0.85 --enable-prefix-caching --context-length 16384 healthcheck: test: [CMD, curl, -f, http://localhost:30000/health] interval: 30s timeout: 10s retries: 5 start_period: 600s litellm: image: ghcr.io/berriai/litellm:main-stable ports: - 4000:4000 volumes: - ./litellm_config.yaml:/app/config.yaml environment: TAOTOKEN_API_KEY: ${TAOTOKEN_API_KEY} OPENAI_API_KEY: ${OPENAI_API_KEY} ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY} LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY} depends_on: - sglang - redis - postgres command: [--config, /app/config.yaml, --port, 4000] redis: image: redis:7-alpine ports: - 6379:6379 postgres: image: postgres:16 environment: POSTGRES_USER: litellm POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: litellm volumes: - pgdata:/var/lib/postgresql/data prometheus: image: prom/prometheus:latest volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml ports: - 9090:9090 grafana: image: grafana/grafana:latest ports: - 3000:3000 volumes: - grafana-data:/var/lib/grafana volumes: sglang-cache: pgdata: grafana-data:注意start_period: 600s这一行。模型冷启动第一次加载权重可能要几分钟健康检查如果没给足启动时间容器会被反复重启日志里全是 restart 记录。这个坑我在第一次编排时踩过排查了半天才发现是 healthcheck 太激进。再写litellm_config.yaml。这里定义三个 model_name本地主力、本地大模型、云端 fallback。应用层只认veri-copilot-default这个名字LiteLLM 按路由策略分发。# litellm_config.yaml model_list: # 本地主力SGLang - model_name: veri-copilot-default litellm_params: model: openai/Qwen2.5-Coder-14B-Instruct api_base: http://sglang:30000/v1 api_key: EMPTY model_info: mode: chat tags: [local, code, private] - model_name: veri-copilot-large litellm_params: model: openai/Qwen2.5-Coder-32B-Instruct api_base: http://sglang:30000/v1 api_key: EMPTY model_info: mode: chat tags: [local, code, private] # 云端 fallbackTaoToken - model_name: veri-copilot-default litellm_params: model: openai/你的模型ID api_base: https://taotoken.net/api api_key: os.environ/TAOTOKEN_API_KEY model_info: tags: [cloud, fallback] router_settings: routing_strategy: simple-shuffle fallbacks: - veri-copilot-default: [veri-copilot-large] retry_policy: BadRequestErrorRetries: 0 AuthenticationErrorRetries: 0 TimeoutErrorRetries: 2 litellm_settings: drop_params: True set_verbose: False cache: True cache_params: type: redis host: redis port: 6379 general_settings: master_key: os.environ/LITELLM_MASTER_KEY database_url: postgresql://litellm:${POSTGRES_PASSWORD}postgres:5432/litellm这里有几个关键点。第一api_base用的是容器名http://sglang:30000/v1不是 localhost因为 LiteLLM 和 SGLang 在同一个 compose 网络里用服务名互相访问。第二本地引擎的api_key填EMPTY就行SGLang 默认不校验。第三TaoToken 那一段的model字段要填你在模型对话页面确认过的模型 IDapi_base是https://taotoken.net/api注意不要带 UTM 参数。fallbacks的写法是「主 model_name 挂掉时切到哪个」。这里配的是veri-copilot-default失败时切到veri-copilot-large。如果你想让本地全挂时切云端可以把云端那条也加进 fallback 列表。retry_policy里AuthenticationErrorRetries: 0很重要鉴权失败重试没意义只会浪费配额。prometheus.yml简单配一下抓取目标# prometheus.yml global: scrape_interval: 15s scrape_configs: - job_name: sglang static_configs: - targets: [sglang:30000] metrics_path: /metrics - job_name: litellm static_configs: - targets: [litellm:4000] metrics_path: /metrics配置写完docker compose up -d启动。第一次启动会拉镜像、加载模型耐心等几分钟。用docker compose logs -f sglang看加载进度看到The server is fired up and ready to roll就说明起来了。4. 验证请求从 SGLang 直连到 LiteLLM 统一入口配置起起来之后要分三层验证先验证 SGLang 本身能跑再验证 LiteLLM 网关能路由最后验证应用层通过统一入口调用成功。这样出问题时能快速定位是哪一层。第一层直接打 SGLang 的 OpenAI 兼容接口。用 curl 最直观curl http://localhost:30000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: Qwen2.5-Coder-14B-Instruct, messages: [ {role: system, content: 你是 SVA 验证专家。}, {role: user, content: 写一条 AXI4 AWVALID handshake 断言。} ] }如果返回里有choices[0].message.content说明 SGLang 正常。这一步失败通常是模型路径不对或者显存不够看docker compose logs sglang里的报错。第二层验证 LiteLLM 网关。注意这里要用 master_key 鉴权model 填model_name而不是实际模型名curl http://localhost:4000/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer sk-1234-changeme \ -d { model: veri-copilot-default, messages: [ {role: user, content: 用一句话解释什么是 prefix caching。} ] }这一步能通说明 LiteLLM 到 SGLang 的路由是通的。如果返回 401检查 master_key 是否和 .env 里一致如果返回 500 且日志里有local proxy failed说明 LiteLLM 连不上 SGLang检查api_base里的服务名和端口。第三层用 Python SDK 走统一入口这也是应用层真实的调用方式from openai import OpenAI client OpenAI( base_urlhttp://localhost:4000/v1, api_keysk-1234-changeme, ) resp client.chat.completions.create( modelveri-copilot-default, messages[ {role: system, content: 你是 SVA 验证专家。}, {role: user, content: 写一条 AXI4-Lite WVALID 的 SVA。}, ], ) print(resp.choices[0].message.content)应用层永远只认veri-copilot-default这个名字后端是 SGLang 还是 TaoToken它不关心。这就是统一入口的意义。再验证一下云端 fallback 是否生效。把 SGLang 容器停掉再发一次请求docker compose stop sglang然后重跑上面的 Python 脚本。如果 LiteLLM 的 fallback 配置正确请求会切到 TaoToken 那条路由返回正常结果。这一步验证通过说明你的「本地优先 云端兜底」链路是完整的。验证完记得docker compose start sglang把本地引擎拉回来。最后验证 prefix caching 是否真的生效。SGLang 的 RadixAttention 对重复前缀有缓存你可以发两次相同 system prompt 的请求第二次的 TTFT 应该明显更低。用下面的脚本对比import time from openai import OpenAI client OpenAI(base_urlhttp://localhost:4000/v1, api_keysk-1234-changeme) system 你是 SVA 验证专家。 * 50 # 构造长前缀 for i in range(2): t0 time.time() resp client.chat.completions.create( modelveri-copilot-default, messages[ {role: system, content: system}, {role: user, content: f第 {i} 次请求写一条 FIFO 的 SVA。}, ], ) print(f第 {i} 次 TTFT: {time.time() - t0:.2f}s)第二次的耗时应该比第一次短差距越大说明 prefix cache 命中越好。如果两次差不多检查 SGLang 启动参数里有没有--enable-prefix-caching。5. 本篇常见错排查401、local proxy failed、OAuth 报错这一节把我在编排过程中真实遇到的报错和排查路径列出来你大概率会碰到其中几个。报错一LiteLLM 返回 401 Unauthorized。这个最常见分两种情况。如果是调 LiteLLM 网关时 401说明 master_key 不对检查请求头里的Authorization: Bearer和 .env 里的LITELLM_MASTER_KEY是否一致。如果是 LiteLLM 调云端时 401说明 TaoToken 的 key 没注入进去检查 compose 里TAOTOKEN_API_KEY环境变量有没有传以及 litellm_config.yaml 里写的是os.environ/TAOTOKEN_API_KEY而不是硬编码。注意环境变量名要和配置文件里完全一致大小写敏感。报错二日志里出现local proxy failed或APIConnectionError。这是 LiteLLM 连不上后端引擎。排查顺序先docker compose ps看 sglang 容器是不是 healthy再docker compose exec litellm curl http://sglang:30000/health从 LiteLLM 容器内部测连通性。如果容器内 curl 不通但宿主机 curl 通说明是 compose 网络问题检查两个服务是否在同一个 network 下。如果api_base写的是http://localhost:30000在容器里 localhost 指向容器自己必然连不上要改成服务名http://sglang:30000。报错三Error reading choices或返回体解析失败。这个通常出现在流式响应场景。LiteLLM 转发 SSE 流时如果后端返回的格式和 OpenAI 规范有细微差异客户端解析就会报reading choices。排查方法是先用非流式请求验证streamFalse能通说明后端没问题问题在流式转发。检查 LiteLLM 版本是否过旧main-stable标签有时候会落后可以指定具体版本号。另外drop_params: True这个配置能过滤掉后端不支持的参数建议保持开启。报错四OAuth 相关报错比如OAuth token expired或invalid_grant。如果你用的是需要 OAuth 的云端服务token 过期会导致鉴权失败。TaoToken 用的是 API Key 方式不涉及 OAuth 刷新所以配置里直接用api_key就行。如果你在 LiteLLM 里配了其他需要 OAuth 的后端记得单独处理 token 刷新逻辑不要和 API Key 混在一起。报错五vLLM/SGLang 启动时 CUDA OOM。显存不够。先nvidia-smi看卡上有没有其他进程占着显存。然后调低--gpu-memory-utilization从 0.9 降到 0.85 甚至 0.8。再不行就降--max-model-len上下文长度直接决定 KV cache 大小。如果模型本身太大换量化版本GPTQ-Int4 能把 14B 模型压到 8.5GB 左右24GB 卡跑起来很轻松。报错六Docker 容器里nvidia-smi找不到命令。宿主机装了驱动不代表容器能用 GPU。需要装nvidia-container-toolkit然后在 compose 里配runtime: nvidia。装完之后docker run --rm --gpus all nvidia/cuda:12.0-base nvidia-smi验证一下能输出显卡信息才算配好。报错七健康检查一直失败导致容器反复重启。模型冷启动慢healthcheck 的start_period给太短。改成 600s 甚至更长给模型加载留足时间。另外 healthcheck 的test命令要确认容器里有 curl有些精简镜像不带 curl会一直报 command not found。排查这类问题的通用思路是分层定位先确认引擎本身能跑再确认网关能连上引擎最后确认应用层能连上网关。每一层都有独立的验证命令不要跳步。6. 从本地栈到统一入口把调用切到 TaoToken 的连通性验证前面搭的这套栈本地引擎是主力TaoToken 是 fallback。但有些场景下你想反过来把 TaoToken 作为主入口本地引擎做兜底或者纯粹想验证一下从这套栈到 TaoToken 的连通性。这一节给出具体的切换动作和验证方法。先明确一个概念LiteLLM 的model_name是应用层看到的逻辑名litellm_params里的model和api_base决定实际走哪个后端。所以「切换入口」本质上就是改litellm_config.yaml里某个 model_name 对应的后端配置应用层代码不动。假设你想把veri-copilot-default的主路由从本地 SGLang 切到 TaoToken改法是把 TaoToken 那条配置的model_name改成veri-copilot-default并调整它在列表里的顺序。LiteLLM 的simple-shuffle策略会在同名 model_name 的多个后端之间轮询如果你只想走 TaoToken就把本地那条的 model_name 改成别的名字或者直接注释掉。改完配置后LiteLLM 需要重载。最直接的方式是重启容器docker compose restart litellm然后做连通性验证。第一步确认 LiteLLM 能读到新配置docker compose logs litellm | grep veri-copilot-default日志里应该能看到 model_name 和对应的 api_base。第二步发一个实际请求curl http://localhost:4000/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer sk-1234-changeme \ -d { model: veri-copilot-default, messages: [{role: user, content: 你好确认一下连通性。}] }如果返回正常说明 LiteLLM 到 TaoToken 的链路是通的。如果返回 401检查TAOTOKEN_API_KEY环境变量有没有正确注入到 litellm 容器里可以用docker compose exec litellm env | grep TAOTOKEN确认。如果返回连接超时检查容器所在网络能不能访问外网以及api_base是不是写成了https://taotoken.net/api注意结尾不要多加斜杠。第三步验证模型 ID 是否正确。TaoToken 的model字段要填实际的模型 ID填错了会返回model not found。如果你不确定用哪个 ID先去模型对话页面确认https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite在页面里选一个模型发条消息确认能通然后把页面上的模型 ID 抄到配置里。这一步别偷懒模型 ID 写错是最容易犯的低级错误。第四步做一次端到端的应用层验证。用 Python SDK 走 LiteLLM 统一入口确认返回内容正常from openai import OpenAI client OpenAI( base_urlhttp://localhost:4000/v1, api_keysk-1234-changeme, ) resp client.chat.completions.create( modelveri-copilot-default, messages[{role: user, content: 用一句话说明你的身份。}], ) print(resp.choices[0].message.content) print(usage:, resp.usage)usage字段里能看到 token 消耗这个数据会写进 Postgres后面可以在 Grafana 里做成本统计。如果你想让本地和云端同时可用按标签路由是个好办法。LiteLLM 支持在请求里带metadata指定 tags比如强制走本地resp client.chat.completions.create( modelveri-copilot-default, messages[{role: user, content: 敏感数据只走本地。}], metadata{tags: [local]}, )这样即使veri-copilot-default同时挂了本地和云端两个后端带local标签的请求也只会路由到本地引擎数据不出内网。这个能力在合规场景下很有用。最后说一个实际经验切换入口之后一定要跑一遍完整的回归测试不要只测一条「你好」。因为不同后端的 tokenizer、上下文长度、参数支持度可能不一样某些 prompt 在本地能跑通切到云端可能因为参数不支持而报错。drop_params: True能缓解这个问题但不能完全避免。跑一遍你真实的业务 prompt确认输出质量没有明显下降再正式切流量。整套栈跑通之后你就有了一台机器上的完整私有化 LLM 服务本地引擎保证数据不出内网LiteLLM 统一入口让应用层无感切换TaoToken 提供云端兜底和模型对比能力Prometheus Grafana 做可观测。后面要加模型、换引擎、调路由策略都只改 YAML不动业务代码。