LLM 网关这个叫法,最近经常出现在 AI 工程团队的技术方案里。它并不是一个官方标准概念,而是把 LLM 应用与模型服务之间的流量统一起来的一层基础设施。只要一个团队有多个项目在调用 OpenAI、Azure OpenAI、Anthropic、Qwen 或本地私有化模型,网关的价值立刻就会显现:密钥统一管理、限流与配额集中控制、成本可计量、失败自动回退、调用链路可观测。
但“在文档里画一个网关”和“真正把网关跑在生产环境”是两回事。前者只要一个方框和几条箭头,后者要面对限流失效、流式响应超时、多实例状态不同步、上游 429 风暴、预算核算混乱、密钥泄漏等一系列问题。这篇文章按生产化 LLM 网关的落地顺序展开,先讲清楚为什么需要网关,再拆解功能边界和架构权衡,然后给出一份最小可运行网关的实现思路,最后把生产环境中常见的故障、排查链路和发布检查清单整理出来。
1. 为什么 LLM 网关会从“可选”变成“基础设施关卡”
很多团队最初只是把 LLM 当成一个远程 API 来用,每个业务模块各自引入 SDK,直接调模型服务。当同类服务只有一两个时,这种方式还能撑住。一旦业务应用增加到十几个,AI 工程师会被同一类问题反复打断。
1.1 没有网关时,LLM 应用直连模型服务会暴露哪些问题
第一个问题是密钥分散。每个业务服务都需要配置自己的模型服务密钥,前端页面如果需要直连模型接口,密钥还会被打进浏览器包。一旦某个项目组把密钥写到公共代码仓库,攻击者就能绕过计费用到你的模型额度。
第二个问题是行为不一致。同一个 429 限流错误,A 团队做了指数退避重试,B 团队直接抛异常,C 团队原地循环重试。模型服务被打爆时,各服务表现完全不同,线上表现就是“有的应用卡死,有的应用报错,有的应用自动恢复”。
第三个问题是成本无法核对。模型服务账单只显示一个总账,如果想知道哪个部门、哪个应用、哪个模型消耗了多少 token,只能靠人工估算。月底对账时,团队之间互相推诿是常态。
第四个问题是失败处理零散。主模型服务不可用时,有的业务希望自动切换到备选模型,有的希望降级到规则答案,有的希望排队重试。没有统一网关,这些容错逻辑就要在每个应用里各写一遍,代码量不大,但维护成本很高。
1.2 网关解决的核心问题:统一入口、统一治理、统一可观测性
LLM 网关是位于应用和模型服务之间的一个中间层。应用只感知网关的地址和协议,不直接关心后端是哪个模型供应商、密钥存在哪里、限流策略是什么。
请求经过网关时,网关至少要做四件事:认证租户身份,检查限流和配额,路由到指定模型服务,最后把响应返回给应用。流式请求还要额外处理事件流透传,不能把数据全部缓冲完再一次性返回。
加了网关之后,模型服务的密钥只存在于网关所在的服务端环境中,业务应用不需要再保存任何模型密钥。限流策略、重试策略、超时配置、模型路由规则都可以在网关层统一修改,而不需要逐个应用升级。每次调用的模型、token 数量、耗时、成本和归属租户也都有了统一的审计入口。
1.3 两种常见接法:嵌入式包装与集中式网关
在实际工程里,“网关”不一定是一个独立服务,也可以是一个被业务应用引用的 SDK 封装库。这两种模式需要放到一起对比判断。
嵌入式包装模式是写一个LLMClient工具包,项目里引入依赖后直接调用,内部封装了密钥读取、重试、日志和 provider 选择。优点是部署简单,不需要额外运维;缺点是每个业务应用仍然各自持有调用逻辑,升级工具包需要所有项目同步发布,限流和成本统计依然难以集中。
集中式网关模式是把 LLM 调用单独部署成服务,业务应用通过 HTTP 或 gRPC 调用网关。优点是治理能力集中,密钥收敛到网关,策略升级不需要业务发版;缺点是多了一次网络跳转,增加了延迟和故障点,而且网关本身变成需要重点保障的高可用组件。
| 对比维度 | 嵌入式包装 | 集中式网关 |
|---|---|---|
| 部署成本 | 低,跟随业务应用发布 | 高,需要单独部署和运维 |
| 密钥管理 | 仍然分散在各业务环境 | 集中在网关节点的服务端 |
| 限流配额 | 难以跨应用统一 | 可以按租户、应用、模型统一控制 |
| 策略升级 | 需要各业务重新发版 | 改网关配置即可 |
| 故障影响面 | 故障局限在单个业务 | 网关故障影响所有接入应用 |
| 可观测性 | 依赖业务埋点 | 网格天然捕获全量调用 |
实际生产环境里,很多团队会先从嵌入式包装开始,等业务量增长到需要统一治理时再演进到集中式网关。两种模式不是绝对互斥,关键是要清楚当前的治理边界在哪里。
2. 生产化 LLM 网关的功能边界:必须做什么,不应该做什么
网关的功能不是越多越好。很多团队在搭建网关时喜欢把 prompt 组装、agent 编排、知识库检索都塞进去,最后网关变成一个大杂烩,业务复用困难,性能也开始恶化。生产化网关的职责应该是“流量治理”而不是“业务能力”。
2.1 网关应当承担的职责
第一,认证与租户隔离。应用向网关发起请求时必须携带租户标识和应用标识,网关校验该租户是否有权限访问某个模型。这一层决定了后续所有限流、配额和成本统计是否可靠。
第二,限流与配额。限流通常分为两类:一类是保护模型供应商,避免瞬间请求超过上游速率限制;另一类是成本分配,每个租户每月只能消耗一定额度。网关同时承担这两件事,前者用速率限制算法,后者用累计费用判断。
第三,路由与回退。根据模型名称、租户等级、上游健康状态选择目标模型服务。主服务返回 429、5xx 或超时时,网关可以按照配置切换到备用模型,前提是响应格式兼容。
第四,统一的请求与响应适配。不同模型供应商的接口参数和响应结构可能有细微差异,网关应尽量对应用提供稳定协议,把差异收敛在网关内部。
第五,可观测性。每条请求至少记录租户、应用、模型、provider、提示 token、生成 token、首次响应延迟、总耗时、重试次数和估算成本。
2.2 网关不应该承担的职责
网关不应该成为业务逻辑的执行器。不要在网关里写“如果用户问题包含退货则调用退货工具”这样的规则,那是 agent 编排层要做的事。网关一旦开始理解业务内容,就会和具体业务强耦合,任何一个业务调整都会导致网关发版,这违背了网关作为基础设施的定位。
网关也不应该长时间存储对话内容。出于审计需要,网关可以保存调用元数据和部分请求摘要,但把完整聊天记录沉淀在网关上,会带来数据合规和存储成本问题。对话存储应由上层业务系统按需处理。
网关不需要在大模型调用之外实现完整业务流程。一个 LLM 网关的典型职责边界是:应用请求进来,网关做认证、限流、路由、转发、回退、记录,然后返回。它不做权限系统本身,只对接现有的统一身份源;它不做对话管理系统,只关心单次请求的成功率。
2.3 用请求生命周期理解职责分布
一条走到生产网关的请求,从上到下的处理顺序大致如下:
| 阶段 | 处理内容 | 失败时的表现 |
|---|---|---|
| 鉴权 | 校验租户标识、应用标识和模型权限 | 返回 401 或 403 |
| 预检 | 校验请求体、模型是否在授权清单、流式参数 | 返回 400 或 422 |
| 限流 | 检查速率限制和预算额度 | 返回 429,并携带重试时间 |
| 路由 | 根据模型和策略选择 provider | 无可用 provider 时返回 503 |
| 调用 | 转发请求并设置超时和重试 | 上游异常时触发回退 |
| 响应 | 透传流式事件或返回 JSON | 异常时返回标准化错误体 |
| 审计 | 写入日志、指标、成本记录 | 审计失败只告警,不影响响应 |
这里的核心原则是:每一步只做一件事,每一步都留下可检查的痕迹。如果某个阶段失败后没有日志,后续排查就会非常被动。
3. 三个需要认真做的架构权衡
不同团队搭建 LLM 网关的方式差异很大,但背后真正需要权衡的问题就那么几个。这里选三个最关键的展开,它们直接影响网关的性能、可靠性和运维复杂度。
3.1 集中式网关与嵌入式 SDK 的权衡
集中式网关解决了治理问题,但引入了一个新的问题:每一条 LLM 请求都要多经过一跳网络。对于聊天应用来说,如果首次 token 响应本身需要 1 到 2 秒,网关多出的几毫秒到几十毫秒延迟通常可以接受;但如果业务内网质量很差或者网关自身性能不稳定,延迟会被明显放大。
嵌入式 SDK 在延迟上更有优势,因为业务进程直接调用模型服务,不再有中间层。可一旦团队规模变大,SDK 的版本分裂会成为灾难。有的项目停留在旧版本,有的项目用了新限流参数,线上行为无法对齐。
比较务实的做法是分层:核心团队维护一个嵌入式 SDK,但 SDK 内部指向集中式网关,而不是直接指向模型服务。这样业务方拿到的是简单易用的封装,平台方又能通过网关统一治理。这个方案牺牲了“嵌入式模式直连模型”的延迟优势,但换来了集中式治理能力,是一种平衡选择。
3.2 状态存储:内存、Redis 还是数据库
网关系限流和配额必须记住每个租户的当前状态。如果网关是单实例部署,用一个内存 Map 就能实现令牌桶;但生产环境为了高可用通常会部署多个副本,请求会负载均衡到不同实例。如果限流状态存在单机内存里,某个租户在 10 个副本上各拿到一份额度,总速率会放大到接近 10 倍,上游照样会被打爆。
必须使用多实例共享状态时,Redis 是最常见的选型。限流可以用 Redis 加 Lua 脚本实现原子性,配额和预算可以放在 Redis 里做快速读写,最终异步落库。数据库适合保存审计记录和月度账单,但不适合直接放在请求链路上做实时计数,因为高频写入会拖垮数据库。
状态存储的选择应该按数据特性来分:需要跨实例实时同步的用 Redis,需要持久化对账的用数据库,允许重启丢失的用内存。
3.3 同步流式转发还是异步队列处理
很多团队在设计网关时会陷入一个误区:把所有上报数据都丢到消息队列里异步处理,认为这样吞吐更高。但聊天类应用的默认交互是同步请求,用户发出 prompt 之后要等待模型逐个 token 返回。网关的核心优化目标应该是低首 token 延迟、流畅的流式透传,而不是单纯的高吞吐。
异步队列适合的是离线场景,比如批处理文档摘要、批量生成最终结果后存储到数据库。真正的在线 LLM 网关必须优先支持同步流式转发,并且要能处理断连、超时、半路错误等流式场景。网关自身的日志和成本统计可以异步写,但转发路径必须是同步的。
| 特性 | 同步流式转发 | 异步队列处理 |
|---|---|---|
| 典型场景 | 聊天、助手、在线生成 | 批量摘要、离线任务 |
| 用户体感 | 实时返回 | 需要轮询或回调 |
| 实现复杂度 | 高,要处理 SSE 和超时 | 中,需要消费端控制速率 |
| 回退 | 难,已出流的响应无法回退 | 容易,任务可重抛 |
| 网关定位 | 在线流量治理 | 批处理调度 |
4. 最小网关落地:从路由到限流与流式透传
先不要一步到位做几十个插件。一个能跑通的最小 LLM 网关只需要具备三个能力:对外提供统一的 OpenAI 兼容接口,内部按租户做限流,然后转发到真实模型服务并支持流式响应。
4.1 最小网关目录结构与技术选型
示例采用 TypeScript 和 Fastify 实现,原因是代码量少、异步处理能力强、中间件生态丰富。实际项目也可以选择 Go 或 Java,核心逻辑不变。
llm-gateway/ ├── src/ │ ├── server.ts # 服务入口 │ ├── routes/ │ │ └── completion.ts # /v1/chat/completions 路由 │ ├── middleware/ │ │ ├── tenantAuth.ts # 租户鉴权 │ │ └── rateLimit.ts # 限流 │ ├── providers/ │ │ ├── openai.ts # OpenAI/兼容服务调用 │ │ └── azure.ts # Azure OpenAI 调用 │ ├── services/ │ │ ├── router.ts # 模型路由与回退 │ │ └── quota.ts # 预算与额度 │ └── utils/ │ ├── sse.ts # 流式处理工具 │ └── estimateTokens.ts # token 估算 ├── config/ │ └── providers.yaml # provider 配置 ├── package.json └── .env.example这是最小结构。生产环境还会增加 OpenTelemetry、Prometheus 指标暴露、健康检查、配置热更新等模块,但核心转发链路不需要那么多文件。
4.2 核心转发接口:统一协议的设计
面向应用的接口采用 OpenAI 兼容协议,因为大多数 LLM 应用已经按这个协议调用模型。业务方不需要为网关开发新的客户端,直接把 baseUrl 指向网关地址即可。
// src/server.ts import Fastify from 'fastify'; import { completionRoute } from './routes/completion'; import { tenantAuthHook } from './middleware/tenantAuth'; import { rateLimitHook } from './middleware/rateLimit'; const app = Fastify({ logger: true, bodyLimit: 5 * 1024 * 1024, }); app.addHook('preHandler', tenantAuthHook); app.addHook('preHandler', rateLimitHook); app.register(completionRoute, { prefix: '/v1' }); app.listen({ port: 8080, host: '0.0.0.0' }, (err) => { if (err) { app.log.error(err); process.exit(1); } });Fastify 的preHandler钩子会在每个请求处理前依次执行。租户鉴权和限流放在这里,可以保证所有路由都受到保护,不需要在业务路由里重复调用。
转发路由如下:
// src/routes/completion.ts import { FastifyInstance } from 'fastify'; import { selectProvider } from '../services/router'; import { callProvider } from '../providers/openai'; export async function completionRoute(app: FastifyInstance) { app.post('/chat/completions', async (request, reply) => { const tenantId = request.headers['x-tenant-id'] as string; const body = request.body as any; const provider = await selectProvider(body.model, tenantId); const upstream = await callProvider(provider, body, { timeoutMs: provider.timeoutMs, stream: body.stream === true, }); if (body.stream === true) { reply.type('text/event-stream'); reply.header('Cache-Control', 'no-cache'); reply.header('Connection', 'keep-alive'); return reply.send(upstream.stream); } return reply.send(upstream.data); }); }关键点在于流式请求的判断。如果业务方传了stream: true,网关应当直接透传上游的事件流,而不是等模型生成完再返回。很多第一次实现网关的人在这里会犯错误:使用普通 HTTP 客户端请求模型服务,默认会缓冲整个响应体,等模型完整返回后再转发给应用,首 token 延迟从几百毫秒变成几十秒。
4.3 租户鉴权和限流中间件
租户标识尽量从网关自定义请求头读取,也可以从业务的 JWT 中解析。示例使用X-Tenant-Id请求头简化处理。
// src/middleware/tenantAuth.ts export async function tenantAuthHook(request: any, reply: any) { const tenantId = request.headers['x-tenant-id']; const appId = request.headers['x-app-id']; if (!tenantId || !appId) { return reply.code(401).send({ error: 'missing_tenant', message: '缺少租户或应用标识', }); } const allowed = await checkTenantModelPermission(tenantId, appId, request.body?.model); if (!allowed) { return reply.code(403).send({ error: 'model_forbidden', message: '当前租户无权访问该模型', }); } request.tenant = { tenantId, appId }; }限流在网关里至少有两层。第一层是按租户整体限流,第二层是按模型限流,防止某个租户用高消耗模型把配额耗尽。最低成本的实现可以使用内存令牌桶,但生产环境多副本部署时必须换成 Redis 加 Lua 脚本。
-- src/middleware/rate_limit.lua -- KEYS[1] 限制流桶 key -- ARGV[1] 桶容量 capacity -- ARGV[2] 每秒补充速率 refillPerSecond -- ARGV[3] 当前时间 now -- ARGV[4] 本次请求消耗的 token 数 requested local current = tonumber(redis.call('get', KEYS[1]) or '0') local last = tonumber(redis.call('get', KEYS[1] .. ':last') or '0') local delta = (tonumber(ARGV[3]) - last) * tonumber(ARGV[2]) local tokens = math.min(tonumber(ARGV[1]), current + delta) if tokens >= tonumber(ARGV[4]) then redis.call('set', KEYS[1], tokens - tonumber(ARGV[4])) redis.call('set', KEYS[1] .. ':last', ARGV[3]) return 1 end return 0Redis 脚本要求原子执行,避免多个网关实例并发扣减时出现超额放行。限流桶的 key 建议写成ratelimit:{tenantId}:{model},这样每个租户每个模型拥有独立额度。
4.4 接入多个模型供应商:配置与路由
真实生产环境中,同一个模型可能部署在不同供应商,例如 Azure OpenAI 和 OpenAI 提供相似的模型能力。网关需要维护一个供应商列表,并按租户配置决定路由顺序。
# config/providers.yaml providers: - name: openai-gpt4o type: openai base_url: ${OPENAI_BASE_URL} api_key: ${OPENAI_API_KEY} timeout_seconds: 180 - name: azure-gpt4o type: azure_openai endpoint: ${AZURE_OPENAI_ENDPOINT} deployment: gpt-4o api_key: ${AZURE_OPENAI_KEY} timeout_seconds: 240 fallback: - name: openai-gpt4o priority: 1 - name: azure-gpt4o priority: 2路由服务读取这份配置,把应用层请求的模型名映射到具体供应商调用。
// src/services/router.ts export async function selectProvider(model: string, tenantId: string) { // 生产环境建议从配置中心读取,并带有缓存 const tenantRoute = await getTenantRoute(tenantId); for (const item of tenantRoute.fallback) { const provider = await getProvider(item.name); if (await isProviderHealthy(provider)) { return provider; } } const err: any = new Error('no_available_provider'); err.statusCode = 503; throw err; }健康检查不一定要做真实请求。网关可以在内存中维护上游近一分钟的错误率和延迟,错误率超过阈值就把该 provider 标记为不健康,路由时优先跳过。这样可以避免每次都把请求打到已经过载的上游。
5. 再往生产走一步:回退、预算、缓存和可观测性
最小网关能跑通之后,生产化还差几块关键能力。这些能力不会影响第一行代码能不能跑通,但没有它们,线上就缺少保护。
5.1 多模型路由与故障回退
回退不是简单地在 catch 里换一个 provider。真正生产级的回退要考虑流式请求的特殊性:如果请求已经开始向用户流式输出 token,中途上游报错,这时已经无法无缝切换到备用模型,因为用户已经看到了一半的生成内容。
合理策略是:非流式请求可以完整回退;流式请求应该在发起主请求之前就判断主 provider 是否健康,如果主 provider 近期错误率过高,直接选择备用 provider 发起。真正传输过程中出现中断时,网关只能向客户端发送一个 SSE 错误事件,并记录审计日志。
// 简化版回退逻辑 async function callWithFallback(providers: Provider[], body: any) { let lastError: any = null; for (const provider of providers) { try { return await callProvider(provider, body, { stream: body.stream === true, }); } catch (err) { lastError = err; if (!isRetryable(err)) { break; } // 流式已经打开时不可回退,这里只记录错误 if (body.stream === true && hasStartedStreaming()) { throw err; } await delay(200 * providers.indexOf(provider) + 100); } } throw lastError; }回退策略的要点是:只有可重试错误才回退,限流和超时属于可重试错误,鉴权失败和请求体错误不需要回退。
5.2 成本与额度控制:token 估算和实时审计
成本控制依赖两个数据:提示 token 和生成 token。提示 token 可以在请求发出前估算,生成 token 只能等响应结束才能获得准确值。生产推荐分两阶段处理:事前用保守估算扣减预算,事后用模型响应中的实际用量校准。
// 简化版 token 估算 export function estimateTokens(body: any): number { const messages = body.messages ?? []; let textLength = 0; for (const msg of messages) { if (typeof msg.content === 'string') { textLength += msg.content.length; } else if (Array.isArray(msg.content)) { for (const part of msg.content) { if (part.type === 'text') textLength += part.text?.length ?? 0; } } } return Math.ceil(textLength / 3) + messages.length * 4; }额度控制要按租户设置月预算和实时消耗。实时消耗不能只靠模型返回的 usage 字段,因为如果网关调用失败,模型可能已经产生了费用但响应没有正常返回。更稳妥的方式是结合网关侧的请求记录和模型供应商账单做定期对账。
| 租户 | 模型 | 月预算 | 已用 token | 已估算成本 | 剩余预算 |
|---|---|---|---|---|---|
| content-team | gpt-4o | 500 美元 | 1230000 | 321.5 美元 | 178.5 美元 |
| search-team | gpt-4o-mini | 200 美元 | 5600000 | 42.0 美元 | 158.0 美元 |
预算控制有一个常见陷阱:只用请求前估算扣减会导致过高或过低。估算法通常按字符数换算,对于非英文内容偏差较大。正确做法是估算值只用于事前拦截明显超支,最终扣减以实际 usage 为准。
5.3 缓存与性能注意
LLM 请求的缓存比普通 HTTP 缓存复杂得多。输入只有完全一致时才能命中精确缓存,而且不同租户之间不能共享缓存,否则可能造成数据泄漏。
适合缓存的场景包括:固定话术生成、系统提示词固定的摘要任务、廉价的 prompt 前缀复用。不适合缓存的场景是开放式聊天和需要实时事实性的问答。
如果要加缓存,建议用 Redis 保存请求体哈希到响应体的映射,同时 key 中写入租户 ID。
const cacheKey = `llm:cache:${tenantId}:${hash(requestBody)}`; const cached = await redis.get(cacheKey); if (cached && !body.stream) { return JSON.parse(cached); } const result = await callProvider(provider, body); if (!body.stream) { await redis.set(cacheKey, JSON.stringify(result.data), 'EX', 300); }缓存时间不宜过长。默认 5 分钟比较安全,同时要关注合规要求:如果业务场景不允许缓存用户输入,网关层应该默认关闭缓存,只对白名单路由开放。
5.4 可观测性:网关日志与管理指标
LLM 网关的观测与普通 API 网关不同,除了请求量、状态码、延迟之外,还要记录 token 用量、成本、回退次数、缓存命中率和首 token 延迟。
调用链路建议通过traceId串联。应用发起请求时带上 trace ID,网关透传给下游 provider 的调用链路,同时把 trace ID 写进日志和指标。
{ "traceId": "a1b2c3d4", "tenantId": "content-team", "appId": "blog-assistant", "provider": "azure-openai", "model": "gpt-4o", "operation": "/v1/chat/completions", "stream": true, "cacheHit": false, "retryCount": 1, "firstTokenMs": 620, "totalMs": 1240, "promptTokens": 820, "completionTokens": 156, "estimatedCost": 0.0031, "statusCode": 200 }这一行日志是排查问题的核心依据。没有租户、模型、 provider、耗时和 token 数据的日志,即使拿到报错也只能靠猜。
6. 生产踩坑记录:这几类问题最容易出现
这里记录的是生产化 LLM 网关时最常遇到的几类坑。每一条都来自真实工程里的典型现象,不一定每个团队都会遇到,但遇到后排查成本通常很高。
6.1 流式响应代理超时,客户端迟迟等不到第一个 token
网关使用普通的 HTTP 客户端转发stream: true的请求,默认等上游响应体接收完毕才返回。这样首 token 延迟变成完整生成时间,短的十几秒,长的几十秒,客户端超时后直接断开。
原因是没有区分“响应头返回”和“响应体结束”两个时刻。流式场景里,网关应该在上游响应头到达后立刻把流管道接到客户端。
解决方式:使用支持流式读取的 HTTP 客户端,例如 Node 环境用undici或axios的responseType: 'stream',然后把上游 body 通过管道交给网关响应。绝对不能在网关里先await response.json()再返回。
6.2 只做 429 重试,结果造成上游负载放大
上游返回 429 是因为速率受限。如果网关不等待退避时间就立刻重试,加剧了上游压力,导致上游连续返回 429。更糟的是多副本网关同时重试,请求量成倍放大。
解决方式:限流重试必须带指数退避和抖动。第一次退避 200ms,第二次 400ms,第三次 800ms,并增加随机抖动。还要设置最大重试次数,一般不超过 3 次。另一个关键点是,网关对外返回的 429 必须携带Retry-After响应头,让调用方也参与退避,不能只靠网关内部消化。
6.3 在网关上统一改写请求体,导致 prompt 被反复截断
有的团队为了给所有请求追加系统提示词,在网关里强制往 messages 数组头插入内容。这种做法看起来方便,但问题在于不同业务对系统提示词的位置和内容要求完全不同,强行统一改写会破坏业务语义,还容易在调试时产生“模型没有按预期回答”的困惑。
更好的分寸是:网关只负责路由、限流、回退和观测,不修改业务消息内容。如果确实需要注入公共提示词,应该由业务侧明确传入,而不是网关自行拼接。网关只做最小必要改写,例如把 OpenAI 兼容参数映射到 Azure OpenAI 的 deployment 格式。
6.4 使用日志表记录配额,数据库被打爆
有团队把每个租户的实时预算放在关系型数据库里,每次请求都更新一次。网关峰值请求高的时候,数据库写入压力迅速成为瓶颈,甚至拖慢请求响应。
正确的分层是:Redis 保存实时计数和配额快照,数据库只做周期性审计落库。Redis 数据丢失时,可以用数据库审计记录重建配额。请求链路上不要出现同步写数据库操作。
6.5 内存限流在多副本下失效
开发环境单实例部署时,内存限流一切正常。到了生产环境,负载均衡器把请求分发到多个网关实例,内存桶各自独立,每个实例都放行相同额度的请求,整体限流额度变成实例数倍的放大。
解决方式是把限流状态迁移到 Redis。优先使用 Redis 加 Lua 脚本,保证判断和扣减操作原子性。如果暂时不想依赖 Redis,至少要通过网关实例编号把同一租户的请求固定路由到同一实例,保证该租户在某一时刻只用某一个内存桶。
7. 排查 LLM 网关问题的完整链路
网关中间层出现问题时,错误来源比直连模式更多。应用可能报错,网关可能报错,上游可能报错,三者的日志还可能不一致。建立一套标准排查链路,能显著缩短定位时间。
7.1 拿到异常响应后,按哪条链路排查
排查顺序建议从最外层逐步向内层推进:先看应用调用参数,再看网关限流鉴权,然后看路由选择,最后看上游响应。
| 错误现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 返回 401 | 租户没有传递网关 token | 检查请求头X-Tenant-Id、X-App-Id | 补全鉴权信息,检查网关密钥表 |
| 返回 403 | 租户没有该模型权限 | 查询租户模型白名单 | 在配置中心开通模型权限 |
| 返回 429 | 租户速率超限或上游限流 | 查看限流日志、Redis 桶 key | 提高配额或等待退避 |
| 返回 5xx | 上游 provider 故障 | 查看 provider 错误率和 trace | 触发回退,检查 provider 健康状态 |
| 响应超时 | 网关超时配置过短或上游慢 | 对比 totalMs 和上游 firstTokenMs | 重新配置 provider 超时参数 |
| 流式中断 | 上游断流或网关超时 | 检查 SSE 事件和错误事件 | 在客户端做断点续传或重试 |
排查时需要先确认请求是否真正到达网关。如果网关日志里没有这条 trace,问题大概率出在调用方或负载均衡层;如果网关日志有记录但没有上游调用记录,问题在限流或路由;如果上游调用有记录但业务仍然报错,问题在响应适配或网络传输。
7.2 日志里必须留下的字段和排查口诀
建议在网关入口生成 trace ID,并把以下字段贯穿所有日志和指标:
| 字段 | 说明 | 缺失时的后果 |
|---|---|---|
| traceId | 全局调用链 ID | 无法串联调用方、网关、上游三层日志 |
| tenantId / appId | 租户和应用标识 | 无法定位是哪个业务出问题 |
| model / provider | 模型名和供应商 | 无法判断路由是否正确 |
| retryCount | 重试次数 | 无法发现重试风暴 |
| firstTokenMs | 首 token 延迟 | 无法定位流式卡顿节点 |
| totalMs | 总耗时 | 无法判断是网关慢还是上游慢 |
| promptTokens / completionTokens | token 用量 | 成本核算缺失 |
| estimatedCost | 估算成本 | 成本统计无法在线展示 |
| statusCode | 上游和网关状态码 | 无法判断错误来源 |
一套简单可用的排查口诀是:先看 trace 在不在,再看限流有没有拦,然后确认 route 选了谁,最后看上游返回了什么。只要每一步都有日志,LLM 网关的绝大多数问题都能在几分钟内定位到具体环节。
8. 发布检查清单与下一步扩展方向
最后给出生产环境发布 LLM 网关前需要执行的检查清单。这里的每一项都对应具体操作,不是说一句“注意安全”就结束。
8.1 生产环境发布前检查清单
| 检查项 | 具体操作 | 验证方式 |
|---|---|---|
| 密钥收敛 | 检查所有接入应用不再保存模型密钥 | 在代码仓库中搜索密钥变量名 |
| 流式透传 | 用stream: true请求压测,确认首 token 不被缓冲 | 观察 firstTokenMs 是否远小于 totalMs |
| 限流共享 | 确认多实例共用 Redis 限流桶 | 双实例压测,看速率是否接近配置上限 |
| 重试上限 | 配置最大重试次数和指数退避 | 观察 retryCount 日志,确认没有重试风暴 |
| 超时分级 | 配置 provider 粒度的超时时间 | 模拟上游断连,确认网关按预期超时 |
| 回退策略 | 确认可回退的错误类型和流式回退限制 | 手动停掉主 provider,验证备用 provider 生效 |
| 可观测告警 | 配置错误率、延迟、成本告警 | 检查 Prometheus 或对等监控平台 |
| 审计落库 | 确认日志和成本记录异步写入 | 模拟高并发,观察数据库连接数 |
| 回滚方案 | 网关配置和镜像保留上一版本 | 执行一次演练回滚 |
发布检查的意义在于把“看起来能跑”变成“真实生产环境可承受故障”。网关是共享组件,出问题影响的是所有接入业务,所以发布前投入时间做演练是值得的。
8.2 学习环境与生产环境的差异
很多网关代码在本地写得飞快,一到生产就出问题,因为它们默认是学习环境假设。下面这份对比可以帮助团队在开发阶段就主动补齐生产差异。
| 维度 | 学习环境 | 生产环境 |
|---|---|---|
| 密钥 | 本地环境变量 | 密钥管理服务,网关服务端读取 |
| 限流 | 内存 Map 即可 | Redis + Lua,多实例原子扣减 |
| 日志 | console.log | 结构化日志、集中采集、trace 串联 |
| 部署 | 单进程 | 多副本、滚动发布、健康检查 |
| 上游容错 | 不做回退 | 多 provider 回退、熔断、退避重试 |
| 成本 | 不关注 | 实时配额、月度对账 |
| 安全 | 可信任内网 | 租户隔离、灰度、审计留痕 |
8.3 可以继续扩展的方向
网关稳定运行后,可以围绕它继续建设四类能力。
第一类是模型成本中心。把 token 消耗、成本、配额和账单整合到一个管理后台,每个租户都能查看自己的调用趋势,平台团队能看到全局消耗。
第二类是模型评测与回归。网关可以保存线上请求样本,在模型版本升级或供应商切换后运行回归评测,对比答案质量,避免“换了个模型但效果变差”的问题。
第三类是语义缓存。精确缓存只能命中完全一致的请求,生产价值有限。如果业务允许,可以接向量检索做语义缓存,找到语义相似的历史回答直接返回。但这类缓存对数据合规要求高,需要业务方明确确认后才能启用。
第四类是配置中心化。网关的路由、限流、配额和回退策略不要写死在文件里,应该接入统一配置中心,支持发布时灰度、回滚和审计。网关本身成为基础设施后,它自己的策略管理也要有版本记录。
从实际收益来看,建议先完成成本中心和监控告警,再考虑语义缓存和评测平台。因为对多数团队来说,先保障“不失控、可排查、可记账”,远比快速优化 token 消耗更重要。LLM 网关的价值不是把所有能力堆在一个服务里,而是让每一条模型请求都变得可控、可观察、可复盘。把这层流量管好之后,AI 应用的重试、成本、回退和故障恢复,就不再依赖每个业务开发者的个人自觉。这是生产化 LLM 基础设施里,最值得先做的一件事。