自托管 AI 推理这几年越来越普及,但真正让团队头疼的往往不是模型精度,而是“怎么稳定地调用这些模型”。单机部署时,一个 vLLM 服务一个端口,倒还简单;一旦 GPU 服务器多了、模型多了、服务的团队多了,每个模型暴露一个独立端点,客户端要管理一长串 URL、密钥和超时参数,任何一个环节抖动都会把问题放大成事故。
InferCrane 这个项目从命名就能看出它要解决的问题:为一堆自托管推理服务提供一个统一的、稳定的入口端点。这篇文章不打算只介绍它是什么,而是把这类“推理网关”的架构思路拆开讲清楚——它解决了什么真实痛点、核心机制怎么设计、部署时有哪些容易踩的坑、生产环境应该怎么用。读完这篇文章,你应该能判断这类方案适不适合自己的场景,也能照着搭建一个最小可用的推理统一端点。
1. 自托管推理的端点乱象,不只是“多一个服务”的问题
先看一个非常常见的场景。你的团队有三台 GPU 服务器,分别部署了:
- 一台 7B 模型的 vLLM 服务,对外提供 OpenAI 兼容接口;
- 一台 13B 模型的 TGI 服务,走自己的 generate API;
- 一台用 Ollama 起的本地模型,给内部实验工具用。
客户端要调用这三个模型,第一反应是直接在代码里写三个 base_url。看起来也没多复杂,每多一个模型就多写一个配置。但真正运行起来就会发现问题:
- 服务端 IP 或端口变了,所有客户端都要改配置、重新发布;
- 某个模型服务负载很高,另一个却很空闲,但客户端不知道;
- 推理服务在做模型加载、分片初始化时可能短暂不可用,客户端超时直接失败;
- 不同框架的 API 格式不一样,业务代码里要写两层适配;
- 想限制某个内部应用调用哪个模型,已经很难控制;
- 一旦 GPU 服务器出问题,没有自动切换,人工改 DNS 也不是长久之计。
这就是 InferCrane 这类“推理统一端点”存在的理由:把上游多个推理服务抽象成一个稳定入口,客户端只需要知道一个地址,至于这个请求被路由到哪台服务器、哪个推理引擎,由网关层负责。
这里的重点不是“又多了一个组件”,而是把不稳定因素收敛到一层。模型服务本身仍然可能抖动,但客户端不再直接感知这些抖动,因为统一端点负责探测、重试、切换和降级。
2. 统一端点、推理网关:这些概念到底在说什么
InferCrane 的核心能力可以概括成一句话:面向自托管 AI 推理服务的反向代理与流量入口。它处在客户端和推理服务之间,接收统一的 API 请求,再转发给具体的推理引擎。
要准确理解它,需要先区分几个概念:
| 概念 | 通俗解释 | 与统一端点的关系 |
|---|---|---|
| 推理引擎 | 真正运行模型的进程,如 vLLM、TGI、Ollama | 上游服务,负责生成 token |
| 推理端点 | 推理引擎暴露的 HTTP/gRPC 地址 | 统一端点要管理的对象 |
| 统一端点 | 一个固定的对外地址 | 由网关层提供,把内部拓扑隐藏掉 |
| API 网关 | 通用网关,管鉴权、限流、路由 | 推理网关是带推理场景语义的专用网关 |
它和通用 API 网关(如 Nginx、APISIX、Kong)的差别在哪里?通用网关也能做反向代理、负载均衡,但推理场景有自己的特殊性:
- 推理请求是长连接、流式输出的,超时时间不能设成普通 HTTP 接口的几秒;
- 推理服务启动后有模型加载阶段,健康检查必须区分“进程活着”和“模型可用”;
- 上游服务之间可能共享同一个 GPU 资源,网关做流量调度时要知道后端容量;
- 重试要谨慎,因为大模型生成请求不一定幂等;
- 上游框架协议五花八门,网关可能还要做协议转换。
所以 InferCrane 做的事,不是“把 Nginx 改个名字”,而是面向推理场景设计的一层智能路由。
从架构上看,这类系统通常包含以下几个核心组件:
- 入口层:接收 HTTP/gRPC 请求,对外暴露稳定的 base_url;
- 路由层:根据模型名、请求参数或自定义策略,把请求分发给上游推理服务;
- 健康检查模块:定期探测上游服务状态,自动摘除异常节点;
- 故障转移模块:上游返回错误或超时时,自动尝试下一个可用节点;
- 配置中心:维护上游服务列表、权重、限流规则;
- 可观测模块:记录请求延迟、吞吐、错误率、GPU 使用情况。
这些机制共同保证了“稳定端点”这个承诺能够成立。
3. 为什么要关心协议兼容:框架不同,统一端点怎么扛住差异
自托管推理的另一个麻烦是协议碎片化。vLLM 提供 OpenAI 兼容接口,TGI 有自己的原生 SDK 和 API,Ollama 的接口又是另一套。客户端如果直接连这些服务,就绑死了具体框架。
InferCrane 这类方案通常采用的策略是:对外暴露一个规范化的接口,对内负责适配。最常见的外露协议就是“OpenAI 兼容”格式,因为现在绝大多数 AI 应用和开发工具都已经支持 OpenAI SDK。客户端不用变,只要换一下 base_url,就能从调用云端 API 切换到调用自托管模型。
这意味着网关里面要有协议转换层。比如上游是 TGI 或 Ollama,网关可能会把 OpenAI 格式的 chat/completions 请求转成对应框架的请求格式,再翻译返回结果。这部分实现难度不小,尤其是流式输出时的格式对齐和 token 统计。
对于开发者来说,这带来的价值是直接的:你可以用一套统一的 OpenAI SDK 风格代码,访问团队内部所有自托管模型,不再为每个框架写一个客户端封装。
还要注意一个容易混淆的点:统一端点不等于“固定模型”。客户端会在请求体里指定要用的模型名,网关根据模型名决定路由到哪个上游。例如请求里写model=llama-7b,网关就转发给 vLLM 节点;写model=qwen-13b,就转发给 TGI 节点。所以这个统一端点是逻辑上的“模型路由入口”。
4. 环境准备与前置条件
如果你准备在自己的环境中搭建一套推理统一端点方案,先确认基础条件。以 InferCrane 这类基于 Docker 部署的网关为例,通常需要:
| 资源 | 建议 |
|---|---|
| 操作系统 | Linux 服务器,Ubuntu 22.04 或类似发行版均可 |
| GPU 服务器 | 至少一台,用于承载推理模型,建议 NVIDIA 显卡并安装好驱动 |
| CPU/内存 | 网关本身消耗不大;推理节点取决于模型规模 |
| Docker | 建议 24 及以上版本,需要 docker compose 插件 |
| Python | 3.10+,用于实验脚本和客户端验证 |
| 网络 | GPU 服务器与网关之间内网互通,尽量避免跨机房转发 |
没有任何两个节点能互换
本文按通用结构说明
有一台主流 GPU 服务器跑推理
CPU 资源则无需担心
小某写一个 H2
5. 搭建阶段一:准备底层推理服务
在引入统一端点之前,先把底层推理服务跑起来。这里我用 vLLM 和 Ollama 作为两个演示上游,说明网关如何同时管理不同框架。
假设服务器 A 跑 vLLM,服务器 B 跑 Ollama,两个服务都通过 Docker 启动。
5.1 启动 vLLM 服务
# 服务器 A docker run --gpus all \ --ipc=host \ --name vllm-7b \ -p 8001:8000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ vllm/vllm-openai:latest \ --model meta-llama/Llama-2-7b-chat-hf \ --served-model-name llama-7b \ --host 0.0.0.0注意几个点:
- 宿主机端口映射为 8001,避免和后面网关端口冲突;
--served-model-name指定对外模型名,后面网关路由时会用到;- vLLM 镜像较大,首次启动要拉取镜像和模型,耐心等待。
5.2 启动 Ollama 服务
# 服务器 B docker run -d \ --name ollama \ -v /ollama:/root/.ollama \ -p 11434:11434 \ ollama/ollama:latest启动后拉一个模型:
docker exec -it ollama ollama run qwen:7b这里把 Ollama 的默认端口 11434 暴露出来,但后续客户端不要直接访问它,而是通过网关统一入口走。
5.3 验证底层服务
# 验证 vLLM curl http://<server_a_ip>:8001/v1/models # 验证 Ollama curl http://<server_b_ip>:11434/api/tags如果两个服务都能返回 JSON 列表,说明底层推理服务可用。
到这一步,你已经有了两个“各自为政”的推理端点。下面要做的,是把它们收拢到同一个稳定入口后面。
6. 搭建阶段二:部署 InferCrane 网关服务
以 Docker 方式部署网关。它的对外入口端口建议使用 8000,避免与上游重复。
假设网关配置文件config.yaml内容如下:
server: port: 8000 host: 0.0.0.0 routes: - name: llama-7b model: llama-7b upstreams: - url: http://<server_a_ip>:8001 weight: 100 health_check: path: /v1/models interval: 10s timeout: 5s fall_threshold: 2 rise_threshold: 3 timeout: read: 300s connect: 10s write: 120s - name: qwen-7b model: qwen-7b upstreams: - url: http://<server_b_ip>:11434 weight: 100 health_check: path: /api/tags interval: 10s timeout: 5s fall_threshold: 2 rise_threshold: 3 timeout: read: 600s connect: 10s write: 120s auth: enabled: true api_key_header: Authorization api_keys: - "sk-internal-123456" logging: level: info access_log: true这段配置要表达的核心逻辑是:
- 每个模型是一个 route;
- 每个 route 可以有多个 upstream(这里演示单节点);
- 健康检查决定节点是否可用;
- 超时参数按推理场景调长;
- 网关启用 API Key 鉴权,客户端必须带合法密钥才能访问。
启动网关:
docker run -d \ --name infercrane \ -p 8000:8000 \ -v $(pwd)/config.yaml:/app/config.yaml \ your-registry/infercrane:latest实际镜像地址以你部署的版本为准,关键是理解这个网关层的接入方式。
启动完成后,先验证网关本身活着:
curl http://<gateway_ip>:8000/health7. 稳定性机制拆解:InferCrane 是如何做到“稳定”的
统一端点要把外层做得足够简单,关键在于内部每个机制都够稳。下面逐个拆解。
7.1 健康检查与优雅摘除
普通后端服务的健康检查只要确认端口通就够,推理服务不一样。一个推理进程可能在启动初期还没有加载完模型,虽然 TCP 通了,但实际请求逻辑会失败。所以统一端点的健康检查通常要探测一个有“模型语义”的路径,比如/v1/models或/api/tags,确认模型已经就绪。
健康检查的另一个重要参数是连续失败阈值。推理服务偶尔一次探活失败并不代表节点不可用,可能是瞬时网络问题或 CPU 抢占。生产环境比较稳妥的做法是:连续 2 到 3 次失败才摘除节点,连续 2 到 3 次成功后再恢复节点。这样能避免节点状态在“可用/不可用”之间频繁抖动。
7.2 故障转移与重试策略
当网关发现上游请求超时或返回 5xx,它会自动尝试下一个可用节点。但如果请求体很大,或流式请求已经开始返回 token,重试就需要额外小心。
对于非流式请求,如果上游在未返回任何响应前就超时,重试风险相对可控。对于流式请求,一旦客户端已经收到部分 token,再切换到另一节点重新生成,用户会看到内容不一致。所以更稳妥的设计是:流式请求只把“连接建立前”的错误视为可重试错误,连接建立后的错误直接返回给客户端。
重试还有一个隐藏问题:大模型生成结果对重复请求未必相同。如果复制请求体重试,可能两次生成结果不同。在应用层要理解这种不确定性,而不是期望重试一定拿到“一样的答案”。
7.3 连接池与并发控制
自托管推理服务的并发能力受 GPU 显存限制,不能像普通 Web 服务那样无限接请求。统一端点需要在网关层做并发控制,防止大量并发请求把某个 GPU 节点打崩。
常见做法是给每个 route 设置最大并发数,超过后排队或快速失败。网关还可以根据上游的running和queued状态动态调整,但这需要上游暴露更细粒度的指标。
这里真正容易出问题的场景是:多个应用共享同一个推理网关,某个应用突然发了一波大流量,把所有 GPU 节点都占满,其他应用的请求全部排队超时。解决思路是给不同客户端配置不同限流配额,从网关层做“租户隔离”。
7.4 超时管理
普通 HTTP 接口超时设置 5 秒很正常,但大模型推理可能几十秒甚至几分钟才能返回完整结果,尤其是长上下文场景。统一端点必须区分多级超时:
- 连接超时:网关连接上游,通常 5-10 秒;
- 读超时:等待响应数据的时间,要按模型和请求长度设置;
- 空闲超时:SSE 流式响应中两个数据包之间的最大间隔。
这些参数要写入路由配置,而不是用全局默认值。不同模型差异很大,轻量模型可以设 60 秒读超时,重量模型可以设 600 秒甚至更长。
7.5 多模型路由
统一端点的核心路由依据是请求体里的模型名。网关解析chat/completions请求后,提取model字段,到路由表匹配出对应的上游节点列表。
如果你希望同一个模型名自动分发到多个 GPU 服务器,就在路由配置里写多个 upstream,并配置权重。生产场景下,同一模型往往会部署多副本,网关在其中做负载均衡和节点状态管理。
8. 完整示例:通过统一端点调用多个自托管模型
现在用实际请求来验证整套链路。以下示例假设:
- 网关地址:
http://<gateway_ip>:8000 - API Key:
sk-internal-123456 - 上游 1:vLLM,模型名
llama-7b - 上游 2:Ollama,模型名
qwen-7b
8.1 用 curl 调用第一个模型
curl http://<gateway_ip>:8000/v1/chat/completions \ -H "Authorization: Bearer sk-internal-123456" \ -H "Content-Type: application/json" \ -d '{ "model": "llama-7b", "messages": [ {"role": "user", "content": "用一句话解释什么是统一推理端点"} ], "temperature": 0.7, "stream": false }'这条请求直接发给网关,网关根据model=llama-7b通过健康检查路由到 vLLM 节点,返回 OpenAI 格式的响应。
如果返回的 JSON 中有choices字段和模型名信息,说明链路成功。
8.2 调用第二个模型
curl http://<gateway_ip>:8000/v1/chat/completions \ -H "Authorization: Bearer sk-internal-123456" \ -H "Content-Type: application/json" \ -d '{ "model": "qwen-7b", "messages": [ {"role": "user", "content": "你好,介绍一下你是什么模型"} ] }'网关会把qwen-7b的请求转换并转发给 Ollama 上游。你看到的响应仍然是 OpenAI 兼容格式,但底层的协议转换已经发生了。
8.3 用 Python OpenAI SDK 调用统一端点
# 文件路径:test_infercrane.py from openai import OpenAI client = OpenAI( base_url="http://<gateway_ip>:8000/v1", api_key="sk-internal-123456", ) def chat(model: str, prompt: str) -> str: resp = client.chat.completions.create( model=model, messages=[{"role": "user", "content": prompt}], temperature=0.6, ) return resp.choices[0].message.content if __name__ == "__main__": print("llama-7b:", chat("llama-7b", "今天天气如何?")) print("qwen-7b:", chat("qwen-7b", "你好!"))这段代码说明了核心收益:客户端只依赖一个 base_url 和一套 SDK,模型的选择通过请求参数完成。后续在上游增加新模型,客户端代码不需要变化,只需要在网关注册新路由。
运行方式:
python test_infercrane.py预期输出是来自两个不同模型的两段回复。
8.4 模拟流式调用
from openai import OpenAI client = OpenAI( base_url="http://<gateway_ip>:8000/v1", api_key="sk-internal-123456", ) stream = client.chat.completions.create( model="llama-7b", messages=[{"role": "user", "content": "给我讲一个三句话的童话故事"}], stream=True, ) for chunk in stream: if chunk.choices and chunk.choices[0].delta.content: print(chunk.choices[0].delta.content, end="", flush=True)流式输出对网关的压力更大,因为它要保持长连接直到生成结束。如果这里出现“客户端超时”或“连接中断”,优先检查网关的读超时和空闲超时配置。
9. 运行结果与效果验证
完成上面的部署后,不要只测一次成功就结束,稳定性方案要主动验证异常场景。
9.1 正常请求验证
curl -s http://<gateway_ip>:8000/health如果返回200 OK,网关进程正常。再看访问日志,应该能看到刚才测试请求的路由记录,包括命中的 route 名称、上游地址、延迟和状态码。
9.2 故障转移验证
这个验证很关键。手动停掉一个上游推理服务,观察网关行为:
# 在服务器 A 上停止 vLLM docker stop vllm-7b此时健康检查程序会在下一轮探活中失败,连续失败达到fall_threshold后,该节点被标记为不可用。然后再发一次同样的 chat 请求:
curl http://<gateway_ip>:8000/v1/chat/completions \ -H "Authorization: Bearer sk-internal-123456" \ -H "Content-Type: application/json" \ -d '{"model": "qwen-7b", "messages": [{"role": "user", "content": "你好"}]}'这个请求应该仍然成功,因为它路由到了另一个可用上游。如果配置了两个 vLLM 副本,即使其中一个挂了,另一个也能接管流量。
比较合理的联动验证是:在另一个终端持续观察网关日志,会看到健康检查失败记录,随后客户端请求不再转发到异常节点。
9.3 鉴权验证
curl http://<gateway_ip>:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model": "llama-7b", "messages": [{"role": "user", "content": "hello"}]}'不带 API Key 的请求应该被网关拒绝,返回 401 或 403。这一步验证了统一端点的入口安全控制,避免内部模型服务被直接扫描调用。
10. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 请求返回 404 | 路由表中没有配置该模型名 | 检查请求体 model 字段与 routes 配置是否一致 | 在配置中补充对应 route |
| 上游明明活着,请求却一直失败 | 健康检查路径配置错误,节点被摘除 | 查看健康检查请求的返回,确认路径和端口 | 修正 health_check.path |
| 流式请求收到一半被中断 | 读超时或空闲超时设置过短 | 观察网关日志中的 timeout 错误 | 调大 read/空闲超时 |
| 请求成功但响应格式不对 | 协议转换不兼容 | 检查底层框架的 API 版本,抓原始响应 | 确认上游协议版本与网关适配器匹配 |
| 并发一高,所有请求变慢 | 路由并发限制过低或 GPU 显存不足 | 查看网关指标中的排队长度和上游 GPU 占用 | 增加上游副本或调整并发配额 |
| 重试后出现了两段不同内容 | 重试导致模型重新生成 | 查看网关重试日志,确认重复请求 | 非幂等场景不开启盲目重试 |
| 多个客户端相互影响 | 缺少按客户端限流 | 检查网关是否有 tenant 维度配置 | 按 API Key 配置配额 |
遇到问题时的通用排查路径是:先看网关日志,再看上游日志,最后用 curl 直连上游确认服务状态。不要一上来就怀疑框架问题,大多数稳定性问题发生在配置和网络层。
11. 最佳实践与工程建议
11.1 从最小拓扑开始
第一次部署不要追求复杂架构。先用一个网关、两个不同框架的上游服务,跑通统一调用,再逐步加入负载均衡、限流和故障转移。先证明“客户端只需要知道一个地址”,比一开始就铺开多副本更稳妥。
11.2 健康检查路径要与框架匹配
不同推理框架的健康检查路径不同。配置 route 前,先用 curl 确认路径返回内容,再填入网关配置。如果健康检查路径错了,整个节点会被误判为不可用,表现为“天啊,为什么所有请求都失败”。
11.3 流式请求的重试要慎开
对大模型推理场景,网关默认应该对“非流式刚发起的请求”做有限重试,对流式请求更保守。可以根据应用容忍度决定:如果业务允许丢结果,就不要重试;如果业务要求高可用,就只重试“连接建立前”的请求。
11.4 做好密钥管理和租户隔离
统一端点是内部模型服务的安全关口。API Key 要按应用和团队隔离,撤销某个应用只要在网关注销密钥,不必重启上游推理服务。生产环境中不要把生产密钥写进代码仓库,使用环境变量或密钥管理服务下发。
11.5 监控不能只看网关
日志和指标层面,网关可以记录延迟、状态码、token 数、路由命中情况,但用户的真实体验还取决于上游 GPU 的显存利用率、请求排队长度和模型加载耗时。生产环境建议同时采集:
- 网关请求量、错误率、P95 延迟;
- 上游推理服务的并发数、排队数、平均生成速率;
- GPU 利用率、显存占用;
- 统一端点的健康状态和摘除事件。
11.6 配置变更要可回滚
网关配置是流量生命线,建议把配置文件纳入版本管理,修改时先在测试环境验证,再灰度发布。像超时、健康检查阈值这类参数,线上调整要谨慎,一次改动影响的是所有客户端。
11.7 设计命名规范
模型名、路由名、API Key 前缀都要有规范。例如llama-7b与llama-2-7b这种近乎相同的名字很容易把路由搞混。建议在模型名前加上业务域或版本信息,如prod-llama-7b-v1,并确保网关配置、上游 served-model-name、客户端请求体三者一致。
12. 总结与后续学习方向
围绕 InferCrane 这类自托管推理统一端点方案,核心信息可以浓缩为三点:
- 自托管推理发展到多服务、多框架阶段后,稳定端点不再是简单的端口映射,而是一个需要健康检查、故障转移、并发控制、鉴权限流和协议转换的系统设计问题;
- 这类方案真正的价值,是让客户端从“管理一堆不稳定端点”变成“只关注一个稳定入口”,模型服务的升级、扩缩容和故障处理都被隔离在网关之后;
- 落地时的难点不在于部署,而在于机制细节:健康检查路径、流式重试边界、超时分级、租户配额,这些参数决定了统一端点是否真的能承担生产流量。
如果你想深入实践,可以从这样几步继续往技术深处走:
先拿同一模型部署两个副本,在网关配置两个 upstream,验证健康检查和负载均衡是否按预期工作;然后模拟节点故障,观察摘除和恢复的完整过程;接着接入第二个框架,验证协议转换逻辑;最后把监控指标接进现有可观测体系,为团队建立一套完整的推理服务度量标准。
在这个过程中,别忘了先写好配置回滚方案和密钥管理策略。统一端点看起来只是一个路由入口,但它一旦成为所有模型调用的必经之路,稳定性和安全性就必须按基础设施的标准去建设。