上一篇《AI API 网关是什么》讲了为什么企业需要网关。这篇讲怎么自己造一个——不是因为造轮子比 LiteLLM、One API 更好,而是因为亲手设计一遍,你才真正理解网关在做什么、现成方案的每个配置项意味着什么、以及你的特殊需求该怎么塞进去。核心链路(转发 + 计量 + 限流 + 计费)其实一周能出可用版本,复杂的是细节。
本文按数据流顺序拆解每个模块的设计,给出关键数据结构。语言用 Node.js 示意,思路与语言无关。
一个 AI 网关的骨架:入口鉴权 → 路由 → 转发 → 流式计量 → 落账 → 日志。
整体数据流
先把一次请求经过网关的全过程画出来,后面每个模块对号入座:
应用
│ 带虚拟 Key,OpenAI 兼容格式
▼
[1 鉴权] 校验虚拟 Key,取出所属应用/配额
▼
[2 限流] 查该应用的 token 预算是否还够
▼
[3 路由] 按模型名/策略选定上游厂商 + 真实 Key
▼
[4 转发] 翻译成厂商格式,发起请求(多为 SSE 流式)
▼
[5 计量] 边转发边统计 token,流结束得到总用量
▼
[6 计费] 按模型单价扣该应用余额/记账
▼
[7 日志] 异步落审计日志
▼
应用(拿到流式回答)关键设计决定:计量和计费发生在响应流结束时,而限流的预检发生在请求前。这个「先放行、后结算」的时间差,是所有 AI 网关都要处理的核心难点。
模块一:请求归一,统一说 OpenAI 方言
让所有应用统一用 OpenAI 兼容格式(/v1/chat/completions),网关内部再翻译成各厂商格式。这样应用换模型零改动。归一层要处理三件事:请求体字段映射(Anthropic 的 system 是独立字段、Gemini 的结构不同)、流式协议差异、错误码统一。
维护一份模型注册表作为路由和计费的单一数据源:
const MODELS = {
"gpt-5": { provider: "openai", inPrice: 1.25, outPrice: 10, ctx: 400000 },
"claude-fable-5": { provider: "anthropic", inPrice: 3, outPrice: 15, ctx: 200000 },
"deepseek-v3": { provider: "deepseek", inPrice: 0.27, outPrice: 1.1, ctx: 128000 },
// 价格为每百万 token 美元,仅示意,以厂商实时定价为准
};价格这类时效数据不要写死在代码里,应放配置中心或数据库,支持热更新——厂商调价是常态,改价不该发版。
模块二:Key 收口——虚拟 Key 与真实 Key 分离
这是网关的安全核心。厂商真实 API Key 只存在网关(加密存储),应用拿到的是网关签发的虚拟 Key。虚拟 Key 绑定一套元数据:
const virtualKey = {
key: "sk-gw-xxxx", // 发给应用
appId: "team-growth",
allowedModels: ["gpt-5", "deepseek-v3"],
dailyBudgetUsd: 50,
rateLimit: { rpm: 60, tpm: 200000 },
status: "active", // 可随时置为 revoked
};好处立竿见影:某个 Key 泄露,一键吊销且不影响其他应用;能按应用限制可用模型和预算;所有调用可归因到具体应用。真实 Key 的轮换在网关内部完成,应用无感知。
模块三:限流与配额——按 token 而非请求数
大模型请求的成本方差极大,按请求数限流没有意义。要做两层:
- 频率限流:RPM(每分钟请求数)和 TPM(每分钟 token 数),用滑动窗口或令牌桶,热数据放 Redis;
- 预算配额:按应用/用户的日或月 token 预算,超了拒绝。
难点在于请求前你不知道会消耗多少 token(尤其输出)。实用做法是「预扣 + 结算」:请求前按 max_tokens 预估上限做一次预检(预算明显不够就直接拒),响应结束拿到真实用量后回补差额。这能防住「余额还剩一点却放行了一个天价请求」的漏洞。
限流按 token 算:一个请求可能是另一个请求成本的上千倍,请求数维度毫无意义。
模块四:流式计量——最容易翻车的地方
绝大多数请求是流式(SSE)的,网关必须边转发边统计,不能等全部收完——那样首字延迟会毁掉体验。设计要点:
- 网关作为流的「中间人」,从上游读一块、转一块给应用,同时累加 token;
- 输入 token 在请求时就能算(或从上游首包的 usage 拿);输出 token 边流边数,多数厂商会在流末尾的
usage字段给出准确值,优先用它,拿不到再用分词器估算; - 必须处理中断:应用断开连接、上游超时,都要把已消耗的部分计入——半截请求也烧了钱。这一点最常被漏掉,导致计费漏账。
// 伪代码:流式转发中计量
let outputTokens = 0;
for await (const chunk of upstreamStream) {
client.write(chunk); // 先转发,保证低延迟
outputTokens += countChunk(chunk);
if (chunk.usage) usage = chunk.usage; // 优先用上游准确用量
}
// 无论正常结束还是异常中断,finally 里都要结算
settle(appId, model, inputTokens, usage?.output ?? outputTokens);模块五:计费与日志
计费就是拿最终用量套模型注册表的单价:成本 = 输入token×inPrice + 输出token×outPrice。用了 Prompt Caching 的话,缓存命中部分单价不同,要单独计。落账要保证原子性(并发请求扣同一应用余额),用数据库事务或 Redis 原子操作。
日志分两类:计费流水(必须可靠、可对账,同步写或走可靠队列)和审计日志(请求/响应内容、用于合规排查,量大,异步写、可采样、注意脱敏)。两者别混在一张表里。
计费流水和审计日志分开:一个要求准确可对账,一个要求全量可追溯,性质完全不同。
模块六:模型路由
有了统一入口,模型路由就是一层策略。从简到繁:
- 别名路由:应用请求
default-fast,网关映射到当前选定的具体模型。换模型只改映射; - 故障切换:上游返回 5xx 或超时,自动重试到备用厂商(注意幂等和重复计费问题);
- 灰度:新模型按百分比分流,观察指标再放量;
- 成本路由:按请求特征(长度、任务类型)路由到不同档位模型——收益诱人但容易误判,建议先人工规则、留好开关,别一上来就上「智能路由」。
路由策略应可配置、可灰度、可回滚,不要硬编码在转发逻辑里。
落地节奏与常见坑
别一步到位。 第一版只做模块 1/2/5 的最小集:虚拟 Key 鉴权 + 用量记录 + 基础日志,先让「所有流量过网关且能算清账」跑通。路由、缓存、审计按真实需求逐步加。
高频坑清单:
| 坑 | 后果 | 对策 |
|---|---|---|
| 流式中断不结算 | 计费漏账 | finally 里无条件结算已消耗部分 |
| 价格写死在代码 | 厂商调价要发版 | 价格进配置,热更新 |
| 网关无降级 | 网关挂 = 全公司 AI 挂 | 多实例 + 关键应用直连兜底开关 |
| 审计日志同步写 | 拖慢转发延迟 | 异步队列,采样,脱敏 |
| 重试导致重复计费 | 成本虚高 | 重试带幂等键,只对真实产生的调用计费 |
适用人群与替代方案
适合有特殊计费/路由/合规需求、或把网关本身当产品做的团队。大多数团队不该自建——LiteLLM、One API 系开源方案已经把上述模块做得很完善,直接用能省下数周。自建只在三种情况成立:现成方案的计费模型不匹配你的业务(如要按客户多级分账)、有强合规定制、或网关是你要卖的东西。即便自建,也建议先读现成方案的源码,它们踩过的坑就是你的设计检查表。
常见问题
Q:语义缓存要不要放进第一版? A:不要。缓存能省钱,但涉及「什么算相同请求」「缓存多久」等一堆判断,且对话类场景命中率低。先把主链路做稳,缓存作为独立优化项后加。
Q:网关自己会不会成为延迟瓶颈? A:设计得当的话,网关增加的是毫秒级开销,相对秒级的模型生成可忽略。真正要防的是网关的可用性——它是单点,稳定性要求高于任何单个应用。
Q:多租户怎么隔离? A:虚拟 Key 就是隔离边界。每个 Key 绑定独立的模型白名单、预算、限流、日志空间;计费按 Key 聚合。租户间不共享真实 Key,也看不到彼此用量。
小结
一个 AI 网关的核心,是把「先放行、后结算」这条流式链路做对:请求前预检限流,转发中低延迟计量,结束时准确计费,异常时不漏账。虚拟 Key 收口安全、模型注册表统一路由和计费、日志分层。想清楚这几件事,无论自建还是用现成方案,你都能把网关这层基础设施握在自己手里。