1. 从 model.generate() 到模型服务器单机跑模型为什么撑不住多并发如果你已经能在本地用 Transformers 跑通model.generate()下一步大概率会撞上同一个问题一个 Python 脚本只能服务一个进程三个应用想用同一个模型就得加载三份权重。RTX 3060 12GB 这种卡一份 7B AWQ 权重就吃掉大半显存根本不可能复制三份。这时候你需要的是 Inference Serving Engine也就是把模型变成长期驻留、通过 OpenAI-Compatible API 对外服务的模型服务器。vLLM 就是当前最主流的开源选择之一它把 GPU 上的模型包装成一个 HTTP 服务多个客户端共享同一份权重和同一块 GPU。这篇是 AI Infra 实验室系列的第⑨篇聚焦一个具体演进从「跑模型」到「模型服务器」。我会用一台 Ubuntu 24.04 RTX 3060 12GB 的机器把 vLLM 真正跑起来暴露 OpenAI-Compatible API再用 curl 和 Python SDK 分别调用最后用并发压测对比直跑和服务的吞吐差异。同时我会引入 TaoToken 统一 Key/API 通道解决一个很现实的问题本地 vLLM、云端模型、不同厂商的 API如果每个都维护一套 Key 和 base_url代码里会到处是 if-else。TaoToken 在这里的角色是统一调用入口让客户端代码只认一个 base_url 和一套 Key。适合谁看已经会用 Transformers 或 Ollama 跑单模型但还没搭过推理服务或者搭过 FastAPI 包model.generate()但一上并发就崩或者想理解 vLLM 到底解决什么问题、参数怎么配。这篇不深挖 PagedAttention 和 Continuous Batching 的原理那是后面的事这篇先把服务器跑起来、调通、压出数字。先明确一个边界vLLM 不是替代 Ollama 或 llama.cpp它解决的是另一个层次的问题。Ollama 让你方便地在一台机器上跑模型vLLM 让 GPU 上的模型高效地服务大量请求。两者可以共存甚至可以让 vLLM 做后端Ollama 做本地管理。这篇的重点是把 vLLM 作为 Inference Serving Engine 的完整链路走通。2. TaoToken 统一 Key 通道多后端调用的前置准备在正式启动 vLLM 之前先解决一个架构问题客户端怎么统一调用多个后端。假设你本地有 vLLM 服务云端还想调几个不同厂商的模型如果每个后端都写一套 base_url 和 api_key代码会变成这样本地用http://127.0.0.1:8000/v1EMPTY云端 A 用https://api.a.com/v1sk-aaa云端 B 用https://api.b.com/v1sk-bbb。每换一个后端就要改代码、改环境变量、改配置。TaoToken 在这里的作用是提供一个统一的 OpenAI-Compatible API 通道。你只需要在客户端配置一个 base_url 和一个 Key后端具体路由到哪个模型由服务端决定。这样本地 vLLM 和云端模型可以用同一套客户端代码调用切换模型只需要改model字段。具体操作先到 TaoToken 官网注册账号然后进入控制台创建 API Key。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册后在控制台里找到 API Keys 页面创建一个新的 Key复制保存。这个 Key 就是你客户端里要用的凭证。创建完 Key 之后你需要确认两件事Base URL 和 Model ID。Base URL 是 https://taotoken.net/api 注意这里不加 UTM 参数这是 API 调用的正式地址。Model ID 在模型列表里可以查到比如你想调 Claude 系列或者 GPT 系列都有对应的模型标识。如果你只是想让 TaoToken 作为本地 vLLM 的统一入口也可以把本地 vLLM 的地址注册进去这样客户端只认 TaoToken 一个 base_url。这里要强调一个安全边界TaoToken 是统一调用通道不是让你把生产数据库直连出去也不是替代编辑器。它的定位是 API 网关和 Key 管理帮你把多个后端的凭证收敛到一处。本地 vLLM 实验阶段仍然建议监听127.0.0.1不要直接暴露公网。配置方式有两种环境变量和代码内联。推荐环境变量避免 Key 写进代码提交到 Git。在~/.bashrc或~/.zshrc里加export TAOTOKEN_API_KEY你的Key export TAOTOKEN_BASE_URLhttps://taotoken.net/api然后source ~/.bashrc生效。Python 代码里用os.environ读取。这样本地 vLLM 和 TaoToken 可以共用同一套客户端初始化逻辑只是 base_url 不同。如果你用的是 Claude Code 或者 Cline 这类工具TaoToken 也提供了对应的接入方式。Claude Code 的配置在~/.claude/settings.json或者项目级.claude/settings.jsonCline 的 MCP 配置在cline_mcp_settings.json。这些配置的核心都是三件套Base URL、API Key、Model ID。后面在排障章节我会具体讲配置格式。3. 可复制配置vLLM 启动参数与 OpenAI-Compatible 请求示例这一节是全文的核心操作部分。我会给出完整的 vLLM 启动命令、参数解释、以及客户端调用示例。所有配置都可以直接复制粘贴路径和参数保持和实际一致。3.1 环境准备先建一个独立的 Python 环境不要污染已有的 venv。vLLM 对 PyTorch 和 CUDA 版本有要求独立环境最省心。cd ~/ai-infra-lab uv venv --python 3.12 --seed --managed-python .venv-vllm source .venv-vllm/bin/activate uv pip install vllm --torch-backendauto安装完成后验证python -c import vllm; print(vllm.__version__) nvidia-smi如果 vLLM 能正常 importnvidia-smi 能看到 RTX 3060环境就 OK 了。3.2 vLLM 启动命令vllm serve Qwen/Qwen2.5-7B-Instruct-AWQ \ --served-model-name qwen2.5-7b-awq \ --host 127.0.0.1 \ --port 8000 \ --max-model-len 4096 \ --gpu-memory-utilization 0.85参数逐个解释Qwen/Qwen2.5-7B-Instruct-AWQ是 Hugging Face 上的模型仓库 ID。vLLM 会自动从缓存或远程拉取。AWQ 是 4-bit 量化格式12GB 显存跑 7B 模型必须用量化否则权重就占满显存。--served-model-name qwen2.5-7b-awq是 API 里看到的模型名。客户端调用时model字段填这个值而不是 Hugging Face 的完整仓库名。这是把「模型文件」和「API 标识」分开的关键参数。--host 127.0.0.1只监听本机。实验阶段不要用0.0.0.0避免局域网其他机器直接访问。--port 8000是 HTTP 端口和 OpenAI 默认的 443 不同客户端 base_url 要写http://127.0.0.1:8000/v1。--max-model-len 4096限制当前 Server 实例的上下文总长度Prompt Output。Qwen2.5 本身支持更长但 12GB 显存下限制到 4096 更稳。这是 Runtime 配置不是模型能力上限。--gpu-memory-utilization 0.85告诉 vLLM 最多用 85% 的 GPU 显存做资源规划。默认是 0.92调低到 0.85 给系统和显示留空间。注意这个参数不是「模型权重占显存比例」vLLM 还要在里面划分 KV Cache 等运行时资源。3.3 客户端配置OpenAI SDK 指向 vLLM安装 OpenAI Python SDKuv pip install openai新建vllm_client.pyfrom openai import OpenAI client OpenAI( base_urlhttp://127.0.0.1:8000/v1, api_keyEMPTY, ) response client.chat.completions.create( modelqwen2.5-7b-awq, messages[ {role: system, content: You are an AI infrastructure assistant.}, {role: user, content: 从 Infra 角度解释什么是 Model Serving。}, ], temperature0, max_tokens128, ) print(response.choices[0].message.content) print(Usage:, response.usage)注意api_keyEMPTY因为本地 vLLM 默认不校验 Key。如果你在 vLLM 启动时加了--api-key这里要填对应的值。3.4 客户端配置TaoToken 统一通道如果你想让同一套代码既能调本地 vLLM又能调 TaoToken 上的云端模型可以这样写import os from openai import OpenAI def get_client(backendlocal): if backend local: return OpenAI( base_urlhttp://127.0.0.1:8000/v1, api_keyEMPTY, ) elif backend taotoken: return OpenAI( base_urlos.environ[TAOTOKEN_BASE_URL], api_keyos.environ[TAOTOKEN_API_KEY], ) else: raise ValueError(fUnknown backend: {backend}) client get_client(taotoken) response client.chat.completions.create( modelclaude-3-5-sonnet-20241022, messages[{role: user, content: 解释一下 vLLM 的 PagedAttention。}], ) print(response.choices[0].message.content)这样切换后端只需要改backend参数模型名改model字段。TaoToken 的 base_url 是 https://taotoken.net/api Key 从环境变量读。3.5 Claude Code / Cline 配置三件套如果你用 Claude Code配置文件在~/.claude/settings.json{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: 你的Key, ANTHROPIC_MODEL: claude-3-5-sonnet-20241022 } }Cline 的 MCP 配置在cline_mcp_settings.json{ mcpServers: { taotoken: { command: npx, args: [-y, taotoken/mcp-server], env: { TAOTOKEN_BASE_URL: https://taotoken.net/api, TAOTOKEN_API_KEY: 你的Key, TAOTOKEN_MODEL: claude-3-5-sonnet-20241022 } } } }Codex 的auth.json配置{ base_url: https://taotoken.net/api, api_key: 你的Key, model: gpt-4o }三件套的核心永远是Base URL Key Model ID。任何接入问题先检查这三个值。4. 验证请求与成功结果从 curl 到并发压测配置写完接下来验证。分三步先确认 Server 起来了再用 curl 发单请求最后用 Python 做并发压测。4.1 确认 Server 状态保持 vLLM 终端不动新开一个 SSH 窗口curl -s http://127.0.0.1:8000/v1/models | jq正常返回类似{ object: list, data: [ { id: qwen2.5-7b-awq, object: model, created: 1730000000, owned_by: vllm } ] }看到id是qwen2.5-7b-awq说明 Server 正常模型名和启动参数一致。4.2 curl 发单请求curl -s http://127.0.0.1:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: qwen2.5-7b-awq, messages: [ {role: system, content: You are an AI infrastructure assistant.}, {role: user, content: 从 Infra 角度解释什么是 Model Serving。} ], temperature: 0, max_tokens: 128 } | jq返回 JSON 里关键字段{ id: chatcmpl-xxx, object: chat.completion, created: 1730000000, model: qwen2.5-7b-awq, choices: [ { index: 0, message: { role: assistant, content: Model Serving 是把模型... }, finish_reason: stop } ], usage: { prompt_tokens: 32, completion_tokens: 128, total_tokens: 160 } }choices[0].message.content是回答usage是 Token 统计。到这里模型已经通过 HTTP 服务对外提供推理能力了。4.3 Python 客户端验证运行vllm_client.pypython vllm_client.py输出应该和 curl 类似能看到回答和 usage。这一步验证了 OpenAI SDK 能正常对接 vLLM。4.4 并发压测直跑 vs 服务这是本篇的重点对比。先写一个压测脚本benchmark.pyimport time import asyncio from openai import AsyncOpenAI client AsyncOpenAI( base_urlhttp://127.0.0.1:8000/v1, api_keyEMPTY, ) async def single_request(i): start time.time() response await client.chat.completions.create( modelqwen2.5-7b-awq, messages[{role: user, content: f用一句话解释第 {i} 个请求。}], temperature0, max_tokens64, ) elapsed time.time() - start tokens response.usage.completion_tokens return elapsed, tokens async def benchmark(concurrency): start time.time() tasks [single_request(i) for i in range(concurrency)] results await asyncio.gather(*tasks) total_time time.time() - start total_tokens sum(r[1] for r in results) avg_latency sum(r[0] for r in results) / len(results) throughput total_tokens / total_time print(f并发数: {concurrency}) print(f总耗时: {total_time:.2f}s) print(f平均延迟: {avg_latency:.2f}s) print(f总 Token: {total_tokens}) print(f吞吐: {throughput:.2f} tokens/s) print(- * 40) async def main(): for c in [1, 4, 8, 16]: await benchmark(c) if __name__ __main__: asyncio.run(main())运行python benchmark.py你会看到类似输出并发数: 1 总耗时: 2.10s 平均延迟: 2.10s 总 Token: 64 吞吐: 30.48 tokens/s ---------------------------------------- 并发数: 4 总耗时: 2.80s 平均延迟: 2.75s 总 Token: 256 吞吐: 91.43 tokens/s ---------------------------------------- 并发数: 8 总耗时: 3.50s 平均延迟: 3.40s 总 Token: 512 吞吐: 146.29 tokens/s ---------------------------------------- 并发数: 16 总耗时: 5.20s 平均延迟: 5.10s 总 Token: 1024 吞吐: 196.92 tokens/s ----------------------------------------关键观察并发从 1 涨到 16单请求延迟从 2.1s 涨到 5.1s但总吞吐从 30 tokens/s 涨到 197 tokens/s。这就是 Serving Engine 的价值它用延迟换吞吐让 GPU 尽量不闲着。如果你用 FastAPI 包model.generate()并发一上来要么排队串行要么 OOM因为每个请求都独立占显存。对比直跑写一个direct.py用 Transformers 直接加载模型跑 16 次from transformers import AutoModelForCausalLM, AutoTokenizer import time model_id Qwen/Qwen2.5-7B-Instruct-AWQ tokenizer AutoTokenizer.from_pretrained(model_id) model AutoModelForCausalLM.from_pretrained(model_id, device_mapcuda) start time.time() for i in range(16): inputs tokenizer(f用一句话解释第 {i} 个请求。, return_tensorspt).to(cuda) outputs model.generate(**inputs, max_new_tokens64) text tokenizer.decode(outputs[0], skip_special_tokensTrue) total time.time() - start print(f直跑 16 次总耗时: {total:.2f}s) print(f吞吐: {16 * 64 / total:.2f} tokens/s)直跑的结果通常是串行的16 次总耗时可能是 30s 以上吞吐只有 30 tokens/s 左右。而 vLLM 并发 16 能到 197 tokens/s差距在 6 倍以上。这个数字会随模型、GPU、请求长度变化但趋势是稳定的Serving Engine 在高并发下吞吐优势明显。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth这一节列出实际会撞到的报错和排查路径。每个报错都给出真实错误信息和解决步骤。5.1 401 Unauthorized错误信息openai.AuthenticationError: Error code: 401 - {error: {message: Invalid API key, type: invalid_request_error}}原因客户端 api_key 和 Server 端不匹配。本地 vLLM 如果没加--api-key客户端填EMPTY就行。如果加了--api-key yourkey客户端必须填yourkey。用 TaoToken 时检查环境变量TAOTOKEN_API_KEY是否正确设置Key 是否过期或被删除。排查步骤先echo $TAOTOKEN_API_KEY确认环境变量有值再检查代码里是否用了os.environ读取最后到 TaoToken 控制台确认 Key 状态。5.2 local proxy failed错误信息openai.APIConnectionError: Connection error: local proxy failed原因客户端配置了代理但代理不可用。常见于环境变量HTTP_PROXY或HTTPS_PROXY设置了无效地址。注意这里说的是本地代理配置问题不是让你去配代理。解决方法是检查环境变量echo $HTTP_PROXY echo $HTTPS_PROXY如果有值且不需要unset HTTP_PROXY HTTPS_PROXY临时清除或者从~/.bashrc里删掉。本地 vLLM 调用127.0.0.1不需要任何代理。5.3 reading choices 报错错误信息AttributeError: NoneType object has no attribute choices或者KeyError: choices原因response 对象为 None通常是请求失败但没抛异常。检查 base_url 是否正确vLLM 是否在运行。用 curl 先验证curl -s http://127.0.0.1:8000/v1/models如果 curl 也失败说明 Server 没起来或端口不对。检查 vLLM 终端有没有报错端口是否被占用。5.4 OAuth 相关报错错误信息Error: OAuth token expired或者invalid_grant原因用 TaoToken 接入 Claude Code 或 Cline 时如果配置了 OAuth 流程token 可能过期。解决方法是重新生成 API Key或者检查配置文件里的 Key 是否和 TaoToken 控制台一致。Claude Code 的配置在~/.claude/settings.jsonCline 在cline_mcp_settings.jsonCodex 在auth.json。三件套检查Base URL 是否是 https://taotoken.net/api Key 是否有效Model ID 是否在支持列表里。5.5 vLLM 启动 OOM错误信息torch.cuda.OutOfMemoryError: CUDA out of memory原因显存不够。12GB 跑 7B AWQ 时如果--max-model-len设太大或--gpu-memory-utilization设太高会 OOM。解决降低--max-model-len到 2048或者降低--gpu-memory-utilization到 0.8。也可以换更小的模型比如 Qwen2.5-1.5B-Instruct-AWQ。5.6 模型名不匹配错误信息openai.NotFoundError: Error code: 404 - {error: {message: The model xxx does not exist.}}原因客户端model字段和 Server 的--served-model-name不一致。用curl http://127.0.0.1:8000/v1/models查看实际模型名然后改客户端代码。6. 语义一致 CTA把 vLLM 和 TaoToken 接进你的 AI Infra 工作流到这里vLLM Server 已经跑起来OpenAI-Compatible API 调通了并发压测也出了数字。下一步是把这套东西接进你的日常工作流。如果你主要做本地推理实验继续用 vLLM 的127.0.0.1:8000就行。需要看模型对话效果可以用 TaoToken 的模型对话页面快速验证 prompt地址是 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。这个页面可以直接和多个模型对话不用写代码适合调 prompt 和对比模型输出。如果你要长期做编码和 Agent 开发建议用 TaoToken 的 Coding Plan地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。它把编码场景常用的模型和额度打包配合 Claude Code、Cline 这类工具用省去每个模型单独配 Key 的麻烦。需要管理多个 API Key、查看用量、创建新 Key到控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。API Keys 管理页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里面有各语言 SDK 的配置示例。Claude Code 的接入说明在 https://taotoken.net/ClaudeCodeAnthropic?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 包含 settings.json 的完整配置。API 调用的正式地址是 https://taotoken.net/api 这个不加 UTM 参数直接用于代码里的 base_url。最后给一个实用建议本地 vLLM 和 TaoToken 不是二选一而是互补。本地 vLLM 适合数据敏感、需要完全控制的场景TaoToken 适合需要多模型切换、不想管理多个 Key 的场景。用同一套 OpenAI SDK 代码通过 base_url 切换后端是最省心的做法。下一篇会讲 TTFT、TPOT、ITL 这些 Serving 性能指标到时候你会需要今天这套压测脚本作为基础。