AI Inference Gateway 生产级工程实战:从请求路由到多模型编排
在 AI 推理服务落地的过程中,工程师们往往专注于单个模型的优化——KV Cache 管理、量化压缩、算子融合等。然而,当企业内的 AI 应用从"一个模型一个接口"演进到"几十个模型、多种推理后端、跨地域部署"时,真正决定系统可靠性的,往往不是某个模型本身的吞吐量,而是推理网关(Inference Gateway) 的设计——它负责请求路由、负载均衡、降级熔断、语义缓存和多模型编排。
本文基于生产环境中的实战经验,从零构建一个 AI 推理网关,涵盖语义路由、优先级调度、电路熔断、可观测性等核心模块。
一、为什么需要推理网关
直接让客户端调用推理服务,在规模化后会遇到三类问题:
后端异构性:不同模型可能运行在不同的推理引擎上(vLLM、Triton、自研引擎),或部署在不同类型的硬件上(A100、H100、Gaudi)。客户端不应该感知这些差异。
流量管理复杂度:当 A/B 测试两个模型版本时,需要按比例切流;当某个后端出现预热延迟时,需要自动剔除;当请求量突增时,需要排队而非直接打垮后端。
安全与成本控制:提示注入检测、异常流量拦截、用量计费、成本预算——这些横切关注点不适合散落在各个推理服务中。
推理网关将这些问题收敛到一个独立层,类似于传统微服务中的 API Gateway,但其核心逻辑围绕 AI 推理的特点而设计。
二、系统架构总览
+-----------------------------------------+
客户端请求 | Inference Gateway |
+-----------------------------------------+
| Auth & Rate Limit | Prompt Injection |
| Token Budget | Detection |
+-----------------------------------------+
| Semantic Router |
| (意图识别 -> 模型映射 -> 参数推断) |
+-----------------------------------------+
| Priority Queue + Circuit Breaker |
+-----------------------------------------+
| KV Cache Router (prefix 感知) |
+-----------------------------------------+
| vLLM Backend | Triton | Custom Engine |
+-----------------------------------------+
核心模块包括:请求接入层(认证、限流、注入检测)、语义路由层(理解请求意图,选择最佳模型实例)、调度层(优先级队列、熔断、重试)和后缀缓存路由(感知 KV Cache 前缀,将相同系统提示的请求路由到同一实例)。
三、语义路由:让请求找到对的模型
传统路由基于 URL 或请求头中的模型字段,但生产环境中用户常常不问"该用哪个模型",而是直接提出需求。语义路由通过语义理解自动将请求分配到最合适的模型实例。
3.1 基于向量相似度的路由
将用户输入向量化后,与每个模型能力的描述向量做最近邻检索:
use serde::{Deserialize, Serialize};
#[derive(Clone)]
struct ModelCapability {
model_id: String,
embedding: Vec<f32>,
description: String,
cost_per_1k_tokens: f64,
avg_latency_ms: u64,
}
struct SemanticRouter {
models: Vec<ModelCapability>,
threshold: f64,
}
impl SemanticRouter {
fn route(&self, query_embedding: &[f32]) -> Option<&ModelCapability> {
self.models.iter()
.map(|m| (cosine_similarity(query_embedding, &m.embedding), m))
.filter(|(sim, _)| *sim >= self.threshold)
.max_by(|a, b| a.0.partial_cmp(&b.0).unwrap())
.map(|(_, m)| m)
}
}
fn cosine_similarity(a: &[f32], b: &[f32]) -> f64 {
let dot: f64 = a.iter().zip(b.iter()).map(|(x, y)| (*x as f64) * (*y as f64)).sum();
let norm_a: f64 = a.iter().map(|x| (*x as f64).powi(2)).sum::<f64>().sqrt();
let norm_b: f64 = b.iter().map(|x| (*x as f64).powi(2)).sum::<f64>().sqrt();
dot / (norm_a * norm_b)
}
3.2 分层路由策略
纯向量相似路由在生产中有两个问题:对短文本敏感度低、无法处理硬性约束(如某团队只能访问经合规审查的模型)。因此实际系统中采用分层路由:
- 硬规则层:正则匹配、白名单、合规标签
- 向量路由层:对长文本使用混合检索(Dense + BM25)
- 兜底层:默认模型 + 置信度检查
关键设计是路由结果的可解释性——每次路由决策都附带 reason 字段,便于后续分析和调试。
四、Prompt 缓存路由:感知 KV Cache 状态
vLLM 的 PagedAttention 仅管理单实例的 KV Cache 内存,而推理网关可以在实例级别实现智能路由——将共享相同前缀的请求(如相同的 system prompt)路由到同一实例,让 PagedAttention 直接复用已计算的 KV Cache。
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
struct BackendInstance {
id: String,
address: String,
queue_depth: Arc<AtomicU64>,
}
struct PrefixCacheRouter {
prefix_index: Arc<RwLock<HashMap<String, Vec<BackendInstance>>>>,
}
impl PrefixCacheRouter {
fn route_with_prefix(&self, system_prompt: Option<&str>) -> Option<BackendInstance> {
let prefix = system_prompt.unwrap_or("");
if prefix.len() < 256 {
return None;
}
let fingerprint = format!("{:x}", md5::compute(prefix));
let index = self.prefix_index.blocking_read();
if let Some(candidates) = index.get(&fingerprint) {
return candidates.iter()
.min_by_key(|inst| inst.queue_depth.load(Ordering::Relaxed))
.cloned();
}
None
}
}
这意味着如果 1000 个请求共享同一个 2048 token 的系统提示,gateway 可以将它们集中路由到同一实例,使 PagedAttention 的命中率从随机路由的约 20% 提升至接近 90%,理论上将吞吐量提升 3-4 倍。
五、电路熔断与优雅降级
推理后端的故障模式与传统 HTTP 服务不同:推理服务可能"活着"但所有 GPU 都已 OOM,或者健康检查通过但负载过高导致超时。因此需要多层熔断机制。
5.1 基于响应窗口的熔断器
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Instant, Duration};
enum CircuitState {
Closed,
Open,
HalfOpen,
}
struct InferenceCircuitBreaker {
state: AtomicU64,
failure_count: AtomicU64,
success_count: AtomicU64,
last_failure_time: AtomicU64,
config: BreakerConfig,
}
struct BreakerConfig {
failure_threshold: u64,
half_open_max_requests: u64,
recovery_threshold: u64,
open_duration_secs: u64,
}
impl InferenceCircuitBreaker {
fn allow_request(&self) -> bool {
match self.state.load(Ordering::Relaxed) {
0 => true,
1 => {
if self.should_try_reset() {
self.state.compare_exchange(1, 2, Ordering::SeqCst, Ordering::Relaxed).ok();
true
} else {
false
}
}
2 => {
self.success_count.load(Ordering::Relaxed) < self.config.half_open_max_requests
}
_ => false,
}
}
fn record_failure(&self) {
let failures = self.failure_count.fetch_add(1, Ordering::Relaxed) + 1;
if failures >= self.config.failure_threshold {
self.state.store(1, Ordering::Relaxed);
}
}
}
5.2 降级链路
熔断触发后,系统不需要直接返回错误,而是按下沉降级链路处理:
首选模型 (GPT-4级) -> 降级到中等模型 (Llama-70B) -> 降级到轻量模型 (Phi-3) -> 缓存兜底 -> 友好排队
降级的核心挑战是对客户端透明——降级后的模型能力边界不同。解决方案是在响应头中添加 X-Actual-Model 和 X-Degradation-Level,让调用方根据降级级别调整后续逻辑。
六、优先级调度与公平性
在共享推理集群中,不同业务的 QoS 需求差异极大。推理调度需要在吞吐量和公平性之间取得平衡。
6.1 多级反馈队列
struct ScheduledRequest {
id: String,
priority: usize,
estimated_tokens: u64,
created_at: Instant,
}
struct PriorityScheduler {
queues: Vec<Vec<ScheduledRequest>>,
weights: Vec<f64>,
deficit_counters: Vec<f64>,
}
impl PriorityScheduler {
fn next_request(&mut self) -> Option<ScheduledRequest> {
for i in 0..self.queues.len() {
self.deficit_counters[i] += self.weights[i];
}
for i in (0..self.queues.len()).rev() {
if !self.queues[i].is_empty() && self.deficit_counters[i] >= 1.0 {
self.deficit_counters[i] -= 1.0;
return Some(self.queues[i].remove(0));
}
}
None
}
}
6.2 基于 Token 预算的速率限制
不同于简单的 QPS 限流,AI 推理的负载应同时考虑 token 消耗量——一个 8K context 的长文本请求的推理成本可能抵 10 个短对话:
struct TokenBudgetLimiter {
api_key: String,
tokens_remaining: AtomicU64,
refill_rate_per_sec: f64,
last_refill: AtomicU64,
}
impl TokenBudgetLimiter {
fn try_consume(&self, estimated_tokens: u64) -> bool {
self.refill();
loop {
let current = self.tokens_remaining.load(Ordering::Relaxed);
if current < estimated_tokens { return false; }
if self.tokens_remaining.compare_exchange(
current,
current - estimated_tokens,
Ordering::SeqCst, Ordering::Relaxed
).is_ok() { return true; }
}
}
}
七、可观测性:推理监控的特殊指标
AI 推理的监控与传统 Web 服务有本质区别——需要关注 Token 级指标而非仅请求级指标。
7.1 核心指标矩阵
| 指标 | 类型 | 说明 |
|---|---|---|
| TTFT (Time To First Token) | Histogram | 首 token 延迟,直接影响用户体验 |
| TPOT (Time Per Output Token) | Histogram | 输出 token 间延迟 |
| Inter-Token Latency | Histogram | 流式响应中的 token 间隔 |
| KV Cache Hit Ratio | Gauge | PagedAttention 缓存命中率 |
| Queue Depth by Priority | Gauge | 各级优先级队列深度 |
| Token Usage by Model | Counter | 模型级 token 消耗计费 |
| Routing Accuracy | Gauge | 语义路由的决策准确率 |
7.2 分布式追踪中的特殊考虑
推理请求的特殊性在于延迟分布极不均匀——TTFT 可能是 100ms(缓存命中),也可能是 2000ms(缓存 miss)。因此追踪 Span 需要特殊标注:
// 在 Span 中记录推理阶段分解
span.set_attribute("kv_cache.hit", kv_hit);
span.set_attribute("prefill.duration_ms", prefill_ms);
span.set_attribute("decode.duration_ms", decode_ms);
span.set_attribute("scheduler.queue_wait_ms", queue_wait_ms);
span.set_attribute("batch.size", batch_size);
这些标注让运维团队能快速区分是调度延迟、prefill 瓶颈还是 decode 瓶颈。
八、实战踩坑记录
8.1 流式响应的压力传递问题
当 Gateway 使用 Nginx 做 TLS 终止时,Nginx 默认会缓冲后端响应。这导致 SSE 流式输出被缓冲,TTFT 看似正常但实际客户端直到整个响应结束才收到第一个 token。
解决:必须显式关闭 proxy_buffering,并对 SSE 响应设置 X-Accel-Buffering: no 头。
8.2 长上下文请求的 OOM 级联
一个 128K 上下文的请求若不加限制,可能在 prefill 阶段独占 GPU 显存,导致同实例上其他请求被 OOM 驱逐,触发级联熔断。
解决:在 Gateway 层根据模型的 max_model_len 做预检——超过阈值直接拒绝或拒绝 + 提示用户缩短输入。这种前置保护比依赖后端 OOM 后的熔断更高效。
8.3 SSE 连接复用的诡异行为
多个推理后端对 HTTP/2 连接复用的支持不一致。Triton 在某些版本中若多个流共享同一连接,会出现响应流错位(stream interleaving bug)。
解决:为 Triton 后端禁用 HTTP/2 连接复用,强制每个推理请求独占连接。虽然增加了连接握手开销,但避免了响应错位的灾难性后果。
九、总结
AI 推理网关远不止是一个反向路由器——它是 AI 应用流量的"操作系统",需要理解推理负载的特殊模式:长尾延迟分布、前缀局部性、GPU 显存约束和按 token 计费的特性。
核心设计原则可归纳为:
- 前置保护优于后置熔断:在 Gateway 层做 token 预算和上下文长度预检
- 缓存感知提升效率:Prefix-aware routing 比负载均衡对吞吐量提升更显著
- 分层降级保证可用性:牺牲模型能力换取可用性,而非直接返回错误
- TTFT 优先于所有:在 AI 推理场景下,用户体感延迟几乎只取决于 TTFT
随着 AI 应用从"尝鲜"走向"基础设施化",推理网关将成为每一个 AI 生产平台的标配组件。深入理解它,才能构建真正可靠的 AI 服务。

发表评论 取消回复