十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AI Gateway设计与实践:从路由到计费的核心实现

AI Gateway设计与实践:从路由到计费的核心实现 最近在搭建大模型应用接入层时我遇到了一个非常普遍的问题每接入一家大模型服务商就要重新处理一遍不同的 endpoint、鉴权头、错误码和计费字段业务代码里慢慢堆满了模型名判断和供应商分支。更麻烦的是调用方越来越多之后谁用了多少 Token、该扣谁的费用、请求有没有被限流完全变成一笔糊涂账。后来我把这些逻辑从业务代码里抽出来专门做了一个轻量级的 AI Gateway核心只做三件事route、guard、charge。这篇文章会把整套设计完整拆开讲一遍包含概念、代码实现和排错思路适合正在做多模型接入、API 聚合或团队内模型服务治理的开发者参考。1. 背景与核心概念1.1 什么是 AI GatewayAI Gateway 是客户端与大模型服务之间的中间层也可以理解为一个统一入口。业务方不再直接请求某个模型服务商的接口而是先把请求发给网关由网关负责转发到真实的大模型服务、校验调用方权限、控制请求频率并记录每次调用的 Token 用量和费用。这样做最直接的好处是上游模型服务怎么变业务方不需要感知。今天用供应商 A明天换成供应商 B或者同一个模型同时配置多家供应商做故障转移这些逻辑都可以收敛在网关层。网关对外暴露的是统一 API对内做路由、防护和计量。1.2 为什么 route、guard、charge 是核心三件事很多刚接触 AI Gateway 的开发者会把它的定位和普通 Nginx 反代混淆。其实 AI Gateway 最核心的价值可以拆成三块route路由决定请求应该转发到哪个模型、哪个供应商。它不只是简单的 URL 转发还要考虑模型名映射、供应商优先级、成本策略和故障转移。guard守卫决定请求是否允许进入后端。包括调用方身份校验、API Key 验证、限流、配额检查、参数格式校验甚至敏感内容过滤。charge计费统计每次请求消耗的 Token 数量按模型单价计算出费用并把费用归属到对应的调用方、项目或部门。这是大模型网关区别于普通 API 网关的关键能力。把这三块拆开设计代码才能保持清晰。路由只管“去哪”守卫只管“能不能去”计费只管“用了多少、该付多少”彼此职责单一后续扩展也方便。1.3 与普通 API 网关的差异传统 API 网关关注的是 REST 接口的流量治理比如请求转发、负载均衡、熔断、限流、认证。AI Gateway 同样需要这些能力但额外增加了模型层特有的处理模型路由同一个请求里可能包含 model 字段网关需要根据模型名映射到正确的上游。Token 计量从响应中提取 usage.prompt_tokens、usage.completion_tokens并按模型单价计算成本。上下文校验检查 messages 里的 token 估算值是否超过模型上下文窗口避免请求发送到上游后被拒绝。错误码归一化不同服务商返回的错误格式不同网关可以把它们统一成一套错误结构返回给调用方。所以说AI Gateway 不是普通网关的替代品而是在普通网关能力之上叠加了 AI 场景的专用逻辑。理解这一点后面看代码时会清晰很多。2. 环境准备与版本说明2.1 运行环境本文示例使用 Node.js 实现主要基于 Express 构建 HTTP 服务。如果你更熟悉 Python也可以按照同样的思路用 FastAPI 重写核心设计不变。建议环境如下Node.js 18 或更高版本。高版本对 fetch 等 API 支持更好示例中会使用原生 fetch 转发上游请求。npm 作为依赖管理工具。终端工具 curl或者 Postman / Apifox 等接口调试工具。一个代码编辑器推荐 VS Code。版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。2.2 示例项目结构为了便于阅读我使用一个精简的目录结构ai-gateway-demo/ ├── package.json ├── .env ├── config/ │ └── gateway.config.json └── src/ ├── server.js ├── router.js ├── guard.js └── charge.jsgateway.config.json保存供应商、模型、单价等配置。server.js是网关入口负责组装中间件和启动服务。router.js负责选路和转发请求到上游。guard.js负责鉴权、限流、参数校验。charge.js负责 Token 计量与费用计算。接下来我会先把这三个模块的设计原理讲清楚再给出完整可运行的代码。3. 核心语法、配置或原理拆解3.1 route路由策略设计路由模块的输入是客户端请求输出是“选择哪一个上游地址”。最简单的路由策略是精确匹配模型名。比如配置里定义了模型名上游服务gpt-4oopenai 供应商deepseek-chatdeepseek 供应商当请求体里model字段是gpt-4o时网关就选择 openai 的上游地址。如果配置了多个供应商提供同一个模型就需要额外考虑优先级、成本或随机策略。更高级一点的做法是主供应商失败后自动尝试备用供应商实现故障转移。在真实项目中路由策略还应该支持权重分配。比如两个供应商都提供gpt-4o可以在配置里写入权重网关按比例分发请求从而避免单一供应商过载。路由模块通常会返回一个内部统一的上游请求对象里面包含url最终要请求的地址。headers需要透传或替换的请求头。body要发送的请求体。model实际使用的模型标识用于后续计费。3.2 guard请求守卫设计guard 的核心是“先拦截再放行”。它一般以中间件链的形式存在。常见的守卫检查点包括API Key 校验从请求头中读取x-api-key判断调用方是否存在、是否被封禁。速率限制按调用方或 IP 统计单位时间内的请求次数超过阈值则返回 429。参数校验检查model字段是否在支持列表内messages是否为非空数组。内容安全在请求转发前对输入文本做敏感词过滤避免违规内容进入模型服务。上下文长度预检估算 messages 的 token 数超过模型上限时提前拒绝。在实际设计中guard 不应该把所有逻辑都塞进一个函数而是拆成多个小中间件。每个中间件只做一件事执行完就调用next()。这样既方便测试也能在出问题的时候快速定位是哪个环节拦截了请求。guard 返回错误时应该使用统一的错误响应结构例如{ error: { code: RATE_LIMITED, message: 请求过于频繁请稍后再试 } }这样调用方只需要解析一种错误格式。3.3 charge计量计费设计计费是 AI Gateway 比较有代表性的能力。大模型服务商通常会在响应中返回 token 使用量比如{ usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 } }计费模块要做的事情是从上游响应中提取usage查出当前模型的单价计算出本次调用的费用然后把结果写入计量存储。通常计费可能需要区分输入和输出因为很多模型对 prompt token 和 completion token 的定价不同。计算公式是费用 prompt_tokens * 输入单价 completion_tokens * 输出单价网关在把响应返回给调用方之前应该把计费信息记录好。常见的做法先读取上游响应体。解析 usage 字段。调用 charge 模块计算费用。把费用信息写入内存或数据库。最后把响应原样返回给客户端。为了不影响响应速度计量写入可以设计为异步操作。但要注意异步操作可能带来的可靠性问题比如服务在写入前崩溃导致计量丢失。生产环境中建议把计量记录写入消息队列或使用独立的日志采集管道。4. 完整实战案例4.1 初始化项目并安装依赖先创建项目目录并进入mkdir ai-gateway-demo cd ai-gateway-demo npm init -y安装 Expressnpm install express为了方便读取.env文件也可以安装 dotenvnpm install dotenv安装完成后package.json的内容大致如下{ name: ai-gateway-demo, version: 1.0.0, description: A lightweight AI Gateway demo for route, guard and charge, main: src/server.js, scripts: { start: node src/server.js }, dependencies: { dotenv: ^16.4.5, express: ^4.19.2 } }依赖版本以实际安装为准这里不要求完全一致。4.2 修改 package.json为了让启动命令更直观可以在package.json中修改 scripts{ scripts: { start: node src/server.js } }4.3 配置文件在项目根目录创建.env文件PORT3000 API_KEYSkey1,key2 OPENAI_API_KEYsk-your-openai-key DEEPSEEK_API_KEYsk-your-deepseek-key这里API_KEYS表示允许访问网关的调用方密钥多个用逗号分隔。实际生产环境建议将这些密钥保存到密钥管理系统而不是直接写入环境变量。然后创建config/gateway.config.json{ providers: [ { name: openai, baseURL: https://api.openai.com, apiKeyEnv: OPENAI_API_KEY, models: [ { name: gpt-4o, inputPricePer1K: 0.005, outputPricePer1K: 0.015 } ] }, { name: deepseek, baseURL: https://api.deepseek.com, apiKeyEnv: DEEPSEEK_API_KEY, models: [ { name: deepseek-chat, inputPricePer1K: 0.001, outputPricePer1K: 0.002 } ] } ] }需要说明的是这里的 baseURL 仅为示例实际请替换为真实可访问的服务商地址inputPricePer1K和outputPricePer1K表示每 1000 个 token 的输入和输出价格请以实际计费为准。4.4 路由模块src/router.js路由模块负责根据请求中的 model 选择上游 provider并计算最终的请求地址。// 文件路径src/router.js const fs require(fs); const path require(path); const configPath path.join(__dirname, ../config/gateway.config.json); const gatewayConfig JSON.parse(fs.readFileSync(configPath, utf-8)); function findProviderByModel(model) { for (const provider of gatewayConfig.providers) { const matched provider.models.find((m) m.name model); if (matched) { return { provider, modelConfig: matched }; } } return null; } function buildUpstreamRequest(model, messages, apiKey) { const matched findProviderByModel(model); if (!matched) { const error new Error(Model ${model} is not supported); error.code MODEL_NOT_FOUND; throw error; } const { provider, modelConfig } matched; const apiKeyEnv provider.apiKeyEnv; const providerApiKey process.env[apiKeyEnv] || apiKey; return { url: ${provider.baseURL}/v1/chat/completions, headers: { Content-Type: application/json, Authorization: Bearer ${providerApiKey} }, body: { model: modelConfig.name, messages: messages }, modelConfig }; } module.exports { buildUpstreamRequest };这里有一个细节gateway 可以选择使用自己的上游密钥也可以选择透传调用方密钥。示例中优先使用网关配置的环境变量密钥如果环境变量不存在则使用调用方传进来的apiKey。这种设计灵活性更高。4.5 守卫模块src/guard.js守卫模块实现三个核心能力API Key 校验、限流和参数校验。// 文件路径src/guard.js const API_KEYS (process.env.API_KEYS || ).split(,).map((k) k.trim()); const rateLimitMap new Map(); function checkApiKey(req) { const apiKey req.headers[x-api-key]; if (!apiKey || !API_KEYS.includes(apiKey)) { const error new Error(Invalid or missing API key); error.code UNAUTHORIZED; throw error; } } function checkBody(req) { const { model, messages } req.body || {}; if (!model || typeof model ! string) { const error new Error(model is required and must be a string); error.code INVALID_PARAM; throw error; } if (!Array.isArray(messages) || messages.length 0) { const error new Error(messages must be a non-empty array); error.code INVALID_PARAM; throw error; } } function checkRateLimit(req, limit 10, windowMs 60 * 1000) { const apiKey req.headers[x-api-key] || anonymous; const now Date.now(); const record rateLimitMap.get(apiKey) || { count: 0, resetAt: now windowMs }; if (now record.resetAt) { record.count 0; record.resetAt now windowMs; } record.count 1; rateLimitMap.set(apiKey, record); if (record.count limit) { const error new Error(Too many requests, please try again later); error.code RATE_LIMITED; throw error; } } function guardMiddleware(req, res, next) { try { checkApiKey(req); checkBody(req); checkRateLimit(req); next(); } catch (err) { const statusMap { UNAUTHORIZED: 401, INVALID_PARAM: 400, RATE_LIMITED: 429 }; res.status(statusMap[err.code] || 500).json({ error: { code: err.code || INTERNAL_ERROR, message: err.message } }); } } module.exports { guardMiddleware };限流这里使用了简单的内存 Map 实现适合演示。生产环境建议使用 Redis 等分布式存储否则多实例部署时限流数据不共享。4.6 计费模块src/charge.js计费模块负责根据 usage 和模型配置计算费用并打印日志。为了方便演示这里把计费记录打印到控制台同时保存在内存数组中。// 文件路径src/charge.js const chargeRecords []; function calculateCost(modelConfig, usage) { const promptTokens usage.prompt_tokens || 0; const completionTokens usage.completion_tokens || 0; const inputCost (promptTokens / 1000) * (modelConfig.inputPricePer1K || 0); const outputCost (completionTokens / 1000) * (modelConfig.outputPricePer1K || 0); return { promptTokens, completionTokens, totalTokens: promptTokens completionTokens, inputCost, outputCost, totalCost: inputCost outputCost }; } function recordCharge(apiKey, model, modelConfig, usage) { const costInfo calculateCost(modelConfig, usage); const record { apiKey, model, timestamp: new Date().toISOString(), ...costInfo }; chargeRecords.push(record); console.log([CHARGE], JSON.stringify(record)); return record; } module.exports { calculateCost, recordCharge, chargeRecords };在实际系统中chargeRecords应该替换为数据库写入操作。而且要注意并发安全如果使用内存数组在 Node.js 单线程模型下 push 操作不会产生同步问题但如果是多进程部署每个进程的数组相互独立统计数据会不完整。4.7 主服务src/server.js最后把路由、守卫、计费组装到 Express 服务中。// 文件路径src/server.js require(dotenv).config(); const express require(express); const { buildUpstreamRequest } require(./router); const { guardMiddleware } require(./guard); const { recordCharge } require(./charge); const app express(); app.use(express.json()); app.post(/v1/chat/completions, guardMiddleware, async (req, res) { const { model, messages } req.body; const apiKey req.headers[x-api-key]; let upstreamRequest; try { upstreamRequest buildUpstreamRequest(model, messages, apiKey); } catch (err) { return res.status(400).json({ error: { code: err.code || BAD_REQUEST, message: err.message } }); } try { const upstreamResponse await fetch(upstreamRequest.url, { method: POST, headers: upstreamRequest.headers, body: JSON.stringify(upstreamRequest.body) }); const upstreamData await upstreamResponse.json(); if (!upstreamResponse.ok) { return res.status(upstreamResponse.status).json(upstreamData); } if (upstreamData.usage) { recordCharge(apiKey, model, upstreamRequest.modelConfig, upstreamData.usage); } res.json(upstreamData); } catch (err) { res.status(502).json({ error: { code: UPSTREAM_ERROR, message: Upstream request failed: ${err.message} } }); } }); const PORT process.env.PORT || 3000; app.listen(PORT, () { console.log(AI Gateway is running at http://localhost:${PORT}); });这里用到了 Node.js 18 的原生fetch如果使用旧版本 Node.js需要安装node-fetch或改用 axios。4.8 运行与验证启动服务npm start看到以下输出说明启动成功AI Gateway is running at http://localhost:3000在另一个终端中使用 curl 发送一个请求。这里以 deepseek-chat 为例curl -X POST http://localhost:3000/v1/chat/completions \ -H Content-Type: application/json \ -H x-api-key: key1 \ -d { model: deepseek-chat, messages: [ { role: user, content: 你好请介绍一下你自己 } ] }如果网关配置和上游密钥都正确你会收到上游模型服务返回的 JSON 响应同时网关控制台会打印一条计费记录内容类似[CHARGE] {apiKey:key1,model:deepseek-chat,timestamp:2025-01-01T12:00:00.000Z,promptTokens:20,completionTokens:50,totalTokens:70,inputCost:0.00002,outputCost:0.0001,totalCost:0.00012}这说明一次请求已经从“路由 → 守卫 → 转发 → 计费”完整走通了。如果使用不支持的模型curl -X POST http://localhost:3000/v1/chat/completions \ -H Content-Type: application/json \ -H x-api-key: key1 \ -d { model: unknown-model, messages: [ { role: user, content: hello } ] }返回结果应该是{ error: { code: MODEL_NOT_FOUND, message: Model unknown-model is not supported } }如果漏掉x-api-keycurl -X POST http://localhost:3000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: deepseek-chat, messages: [ { role: user, content: hello } ] }返回结果应该是 401{ error: { code: UNAUTHORIZED, message: Invalid or missing API key } }至此一个最小可运行的 AI Gateway 已经完成。5. 常见问题与排查思路在实现和使用 AI Gateway 的过程中比较容易遇到以下几类问题问题现象常见原因解决思路请求返回 MODEL_NOT_FOUND配置文件中没有定义该模型或请求体里 model 拼写错误检查 gateway.config.json 的 models 列表确认模型名完全一致返回 401 UNAUTHORIZED缺少 x-api-key 请求头或密钥不在 API_KEYS 中检查请求头名称和 .env 中的 API_KEYS 配置返回 429 RATE_LIMITED单位时间请求次数超过阈值调大 guard.js 中 checkRateLimit 的 limit 参数或优化调用频率返回 502 UPSTREAM_ERROR上游服务不可达、网络超时、上游密钥无效先用 curl 直接请求上游地址确认上游本身是否正常再检查 .env 中的上游密钥计费记录缺失上游响应中没有 usage 字段或者响应格式发生变化打印上游原始响应确认 usage 字段路径再调整 charge 解析逻辑上游返回 400 或 404baseURL 拼接错误或请求路径不对查看服务商文档确认 chat completions 接口的完整路径排查问题时建议按下面的顺序进行先确认网关进程是否正常启动。再确认请求头、请求体是否符合网关设计要求。然后直接 curl 上游地址排除上游故障。最后查看网关控制台日志定位是哪一层逻辑出错。如果使用了自定义配置还要注意 JSON 格式是否正确。配置文件里多余或缺少逗号会导致解析失败网关启动时直接报错。6. 最佳实践与工程建议6.1 配置外部化与密钥管理上文示例把价格、模型、供应商信息都写在gateway.config.json中把密钥放在.env中。这样做的好处是配置与代码分离。在真实项目中建议使用配置中心比如 Apollo、Nacos 或云平台的配置管理服务这样修改路由和费率时不需要重启网关。密钥管理要特别注意。不要在生产环境的代码仓库中提交任何真实密钥。即使.env被.gitignore忽略也仍然有被误提交的风险。更稳妥的方式是使用专门的密钥管理服务例如云厂商的 KMS、Vault 等。网关启动时从密钥服务动态拉取密钥。6.2 日志与可观测性AI Gateway 是调用链路上的关键节点必须做好日志记录和监控。每一条请求建议至少记录以下信息请求 IDAPI Key 归属方请求的 model上游供应商响应耗时状态码Token 用量和费用在多实例部署时建议为每个请求生成一个 traceId方便串联上下游日志。监控指标至少包括 QPS、错误率、平均延迟、Token 消耗量、费用消耗速率。当费用消耗异常增长时要及时告警。6.3 计费与限流的并发安全如果网关是多实例部署内存限流和内存计费都会失效。限流需要改用 Redis 等外部存储使用原子操作保证计数准确。计费数据建议写入数据库或者通过消息队列异步落库避免直接在请求处理线程内操作磁盘。另外费用计算的精度也很重要。价格单位建议统一比如都按“每 1000 token 的价格”配置计算时统一除以 1000。涉及金额时如果要精确到分可以使用整数最小单位进行运算避免浮点数误差。6.4 安全边界与合规提醒gateway 处于外部请求和内部模型服务之间是安全防护的重要位置。生产环境下应该遵循最小权限原则上游模型的密钥只保存在网关服务端不向调用方暴露。调用方只能看到转发后的模型响应不能拿到上游服务的真实密钥。对调用方做 API Key 隔离不同业务方使用不同的 key便于审计和追溯。在修改路由、费率、密钥等配置前先在测试环境验证再走配置审批流程。对外网暴露的网关入口建议配合 WAF 或安全组策略只开放必要的端口和路径。6.5 性能优化方向AI Gateway 本身不应该成为性能瓶颈。核心优化点包括使用 keep-alive 连接池复用上游连接避免每次请求都重新建立 TCP 连接。对模型配置做缓存避免每次请求都解析 JSON 文件。对上游响应流式转发时尽量使用流式 API而不是把完整响应体缓冲到内存后再返回。流式响应可以显著降低首字延迟提升用户体验。计费记录异步化处理避免计费逻辑阻塞主响应链路。本案例中使用的是非流式转发真实生产环境建议扩展对stream: true的支持。7. 总结与学习路线到这里一个包含 route、guard、charge 三个核心模块的 AI Gateway 最小实现就完整跑通了。你可以自己做一个小练习在配置文件中增加一个新的供应商和模型然后不改动server.js直接通过 curl 验证新的路由是否自动生效。这个实验能帮助你理解配置与逻辑分离的好处。下一步可以往几个方向深入支持流式响应让网关能够实时转发 SSE 流。把限流存储从内存替换为 Redis支持多实例共享限流数据。把计费记录从内存替换为 MySQL 或 MongoDB并增加按调用方、按时间段的统计查询。增加可视化看板展示每个模型的 Token 消耗和费用趋势。引入模型自动故障转移主上游不可用时自动切换备用上游。实际项目中AI Gateway 还需要面对很多工程细节比如超时控制、重试策略、幂等处理、审计日志、成本预算预警等。你可以从本文的最小示例出发逐步补全这些能力。动手把 route、guard、charge 这三个模块跑通再去阅读主流开源 AI Gateway 项目的源码会轻松很多。
返回列表