用 Rust 扩展 AI 网关:agentgateway 与 kgateway 的自定义转换实践

2026-05-15 27 预计阅读时间:1 分钟
来源:cncf.io AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:11 分钟

AI 网关正在成为大模型应用的基础设施层——认证、限流、路由、Prompt 防护,这些内置策略覆盖了大多数场景。但当你需要给请求注入业务上下文、对响应做字段裁剪、或者把多个模型的输出合并成统一格式时,内置策略就不够用了。agentgateway 和 kgateway 的做法是:把扩展点交给 Rust,让你用代码而不是 YAML 声明来定义转换逻辑。

内置策略的边界

典型的 AI 网关内置策略大概这几类:

  • 认证与鉴权——API Key 校验、OAuth 代理、RBAC
  • 限流与配额——按租户、按模型、按 token 数限速
  • 路由与负载均衡——根据模型名、region、权重分发请求
  • Prompt 防护——拦截敏感词、限制最大 token 数、拒绝特定内容类别

这些策略的共同特点是:逻辑相对通用,配置驱动就能解决。一旦你的需求变成"把用户 ID 映射成系统内部的租户标签再注入 system prompt",或者"从向量数据库查出相关文档拼进上下文窗口",纯配置就很难表达——你需要代码。

Rust 扩展的设计思路

agentgateway 和 kgateway 的扩展机制有几个关键设计决策:

1. 转换函数签名统一

无论是请求阶段(pre-transform)还是响应阶段(post-transform),扩展函数接收的是结构化的请求/响应对象,返回的是修改后的对象或错误。签名大致如下:

/// 请求转换:在请求被路由到上游模型之前执行
async fn transform_request(
    ctx: &RequestContext,
    request: LlmRequest,
) -> Result<LlmRequest, TransformError>;

/// 响应转换:在上游模型返回结果之后、返回给客户端之前执行
async fn transform_response(
    ctx: &RequestContext,
    response: LlmResponse,
) -> Result<LlmResponse, TransformError>;

RequestContext 携带元信息(租户 ID、路由决策、调用链 trace),LlmRequest / LlmResponse 是 OpenAI 兼容格式的结构体。你不需要手动解析 JSON,也不需要担心序列化细节。

2. 编译进网关,而非旁路插件

扩展不是独立进程、不是 sidecar、不是 WASM 模块——它直接编译进网关二进制。这意味着:

  • 零序列化开销,函数调用就是内存操作
  • 和内置策略共享同一个类型系统,不会出现"插件能读字段但网关核心不认识"的割裂
  • 代价是扩展需要随网关一起构建发布,不能热加载

3. 错误处理不吞异常

转换函数返回 ResultTransformError 可以选择"拒绝请求"或"放行但记录告警"。这比很多网关的"插件失败就静默跳过"要可靠得多。

实战:给请求注入租户级上下文

下面是一个完整可改造的示例——在请求到达上游模型之前,根据租户 ID 从配置中查找对应的 system prompt 片段,拼进请求的 messages 列表。

1. 定义扩展模块

// src/extensions/tenant_context.rs

use agentgateway::transform::{RequestContext, LlmRequest, TransformError};
use agentgateway::llm::Message;
use std::collections::HashMap;

/// 租户上下文配置——实际项目中可以从文件或数据库加载
#[derive(Debug, Clone)]
pub struct TenantContextConfig {
    pub prompts: HashMap<String, String>,
}

pub struct TenantContextTransform {
    config: TenantContextConfig,
}

impl TenantContextTransform {
    pub fn new(config: TenantContextConfig) -> Self {
        Self { config }
    }

    /// 核心转换逻辑:根据 RequestContext 中的 tenant_id,
    /// 查找对应的 system prompt,插入到 messages 最前面
    pub async fn transform_request(
        &self,
        ctx: &RequestContext,
        mut request: LlmRequest,
    ) -> Result<LlmRequest, TransformError> {
        let tenant_id = ctx.tenant_id();

        // 查找租户专属 prompt,找不到就用默认值
        let system_content = self.config.prompts
            .get(tenant_id)
            .cloned()
            .unwrap_or_else(|| "You are a helpful assistant.".to_string());

        // 构造 system message 并插入到 messages 列表头部
        let system_msg = Message {
            role: "system".to_string(),
            content: system_content,
            ..Default::default()
        };

        request.messages.insert(0, system_msg);

        // 记录日志,方便排查哪个租户用了哪段 prompt
        tracing::info!(
            tenant_id = %tenant_id,
            injected_prompt_len = system_content.len(),
            "Injected tenant-specific system prompt"
        );

        Ok(request)
    }
}

2. 注册扩展到网关

// src/main.rs

use agentgateway::GatewayBuilder;
use extensions::tenant_context::{TenantContextTransform, TenantContextConfig};
use std::collections::HashMap;

fn main() {
    // 构造租户配置——生产环境建议从 YAML / 环境变量 / DB 加载
    let mut prompts = HashMap::new();
    prompts.insert(
        "tenant-acme".to_string(),
        "You are Acme Corp's internal assistant. Always reference Acme policies. Answer in concise bullet points.".to_string(),
    );
    prompts.insert(
        "tenant-beta".to_string(),
        "You are Beta Inc's customer support bot. Be friendly, use emojis sparingly, never disclose internal pricing.".to_string(),
    );

    let config = TenantContextConfig { prompts };
    let transform = TenantContextTransform::new(config);

    // 把扩展注册到 GatewayBuilder
    let gateway = GatewayBuilder::new()
        .add_request_transform(transform)
        // 其他内置策略照常配置
        .add_rate_limiting(/* ... */)
        .add_auth(/* ... */)
        .build();

    // 启动网关
    gateway.run().await.expect("gateway failed");
}

3. 网关路由配置(YAML 部分)

扩展逻辑在 Rust 代码里,路由和模型映射仍然用 YAML 配置:

# gateway-config.yaml
routes:
  - name: chat-completions
    path: /v1/chat/completions
    upstream:
      model: gpt-4o
      endpoint: https://api.openai.com/v1/chat/completions
    # 引用上面注册的 request_transform
    request_transforms:
      - tenant_context

  - name: chat-completions-anthropic
    path: /v1/anthropic/chat
    upstream:
      model: claude-sonnet-4-20250514
      endpoint: https://api.anthropic.com/v1/messages
    request_transforms:
      - tenant_context

4. 验证请求效果

启动网关后,用 curl 发一个带租户标识的请求:

# 假设网关监听 localhost:8080,认证层把 API Key 映射成 tenant_id
curl -X POST http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer sk-acme-xxxx" \
  -d '{
    "model": "gpt-4o",
    "messages": [
      {"role": "user", "content": "What is our refund policy?"}
    ]
  }'

网关内部实际发出的请求会变成:

{
  "model": "gpt-4o",
  "messages": [
    {"role": "system", "content": "You are Acme Corp's internal assistant. Always reference Acme policies. Answer in concise bullet points."},
    {"role": "user", "content": "What is our refund policy?"}
  ]
}

system prompt 是代码动态注入的,客户端完全不需要知道。

响应转换:裁剪敏感字段

请求转换做注入,响应转换做裁剪——这是另一类常见需求。比如上游模型返回了 usage 字段(包含 prompt_tokens、completion_tokens),但你不想让下游客户端看到精确的 token 消耗,只给一个模糊的 cost 估算。

// src/extensions/cost_obfuscation.rs

use agentgateway::transform::{RequestContext, LlmResponse, TransformError};

pub async fn transform_response(
    _ctx: &RequestContext,
    mut response: LlmResponse,
) -> Result<LlmResponse, TransformError> {
    if let Some(usage) = &response.usage {
        // 把精确 token 数替换成模糊费用区间
        let estimated_cost = estimate_cost(usage.prompt_tokens, usage.completion_tokens);
        // 移除原始 usage,注入自定义字段
        response.usage = None;
        response.metadata.insert(
            "estimated_cost_range".to_string(),
            estimated_cost,
        );
    }
    Ok(response)
}

fn estimate_cost(prompt_tokens: u32, completion_tokens: u32) -> String {
    let total = prompt_tokens + completion_tokens;
    // 粗粒度区间,不暴露精确数字
    if total < 1000 {
        "< $0.01".to_string()
    } else if total < 5000 {
        "$0.01 – $0.05".to_string()
    } else {
        "> $0.05".to_string()
    }
}

选用 Rust 扩展的取舍

维度 Rust 编译式扩展 外部插件 / WASM
性能 几乎零开销,内存级调用 需要序列化/反序列化边界
开发体验 类型安全,IDE 补全完整 取决于插件语言和 SDK 质量
部署灵活性 需随网关一起构建发布 可独立更新、热加载
故障隔离 扩展 panic 可能影响网关 插件崩溃可被沙箱捕获
生态门槛 需要 Rust 编译环境 多语言支持,门槛低

什么时候选 Rust 扩展:

  • 转换逻辑涉及复杂的数据结构操作(重组 messages、多字段联动计算)
  • 延迟敏感——每个请求都要跑转换,不能接受额外的序列化开销
  • 团队已经有 Rust 基础,或者网关本身就是 Rust 项目

什么时候考虑其他方案:

  • 转换逻辑简单且变动频繁(比如只是加个 header),YAML 配置就够了
  • 需要多语言团队协作,Rust 编译链路太重
  • 对故障隔离有硬性要求,不能接受扩展代码拖垮网关进程

上手清单

  1. 确认需求边界——先列出内置策略能覆盖的部分,剩下的才是扩展的候选
  2. 从请求转换开始——响应转换往往更复杂(流式响应需要逐 chunk 处理),先做最简单的注入类需求
  3. 本地构建验证——clone agentgateway 或 kgateway 仓库,在 extensions/ 目录下加你的模块,cargo build 确认编译通过
  4. 单元测试先行——转换函数签名清晰,很容易脱离网关独立测试,先写测试再写逻辑
  5. 日志埋点——tracing::info! 记录关键决策点,生产环境排查靠这些日志
  6. 灰度发布——新扩展先只挂到一条路由上,观察延迟和错误率,再逐步扩大范围

Rust 扩展不是万能方案,但它给了你一个在网关层写"真正代码"的出口——当 YAML 声明不够用时,你不需要绕到外部服务去补逻辑,直接在网关内部用类型安全的 Rust 解决。


相关推荐