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. 错误处理不吞异常
转换函数返回 Result,TransformError 可以选择"拒绝请求"或"放行但记录告警"。这比很多网关的"插件失败就静默跳过"要可靠得多。
实战:给请求注入租户级上下文
下面是一个完整可改造的示例——在请求到达上游模型之前,根据租户 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 编译链路太重
- 对故障隔离有硬性要求,不能接受扩展代码拖垮网关进程
上手清单
- 确认需求边界——先列出内置策略能覆盖的部分,剩下的才是扩展的候选
- 从请求转换开始——响应转换往往更复杂(流式响应需要逐 chunk 处理),先做最简单的注入类需求
- 本地构建验证——clone agentgateway 或 kgateway 仓库,在
extensions/目录下加你的模块,cargo build确认编译通过 - 单元测试先行——转换函数签名清晰,很容易脱离网关独立测试,先写测试再写逻辑
- 日志埋点——
tracing::info!记录关键决策点,生产环境排查靠这些日志 - 灰度发布——新扩展先只挂到一条路由上,观察延迟和错误率,再逐步扩大范围
Rust 扩展不是万能方案,但它给了你一个在网关层写"真正代码"的出口——当 YAML 声明不够用时,你不需要绕到外部服务去补逻辑,直接在网关内部用类型安全的 Rust 解决。