ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

LLM API统一管理网关:路由、限流与计费实践

LLM API统一管理网关:路由、限流与计费实践 简介在人工智能应用开发中大模型API的接入往往面临多服务商、多密钥、接口协议差异等复杂问题。API网关作为微服务架构中的关键组件天然适合承担大模型调用的统一治理职责。所谓LLM API网关是通过集中式路由将不同厂商的模型接口封装成规范协议并在此基础上实现统一鉴权、限流、计量计费与容灾切换。这种设计不仅能够收敛散落的API Key、降低泄露风险还能通过权重路由与健康检查机制保障服务高可用。实际工程中借助令牌桶算法控制请求频率、基于Lua脚本保证限流原子性并以token预占策略实现成本配额管理已经成为多模型场景下的标准实践。对于企业内部多个业务方共用大模型能力的场景部署一套轻量级统一网关配合可观测性指标与自动化告警能显著提升故障定位效率与资源利用水平。本文即从网关设计角度完整拆解这一系统的核心模块与落地经验。 做LLM应用这段时间我最大的感触是模型能力本身已经不是瓶颈真正折磨人的是API管理。团队里三四个人每人手里握着DeepSeek、GLM、通义、豆包好几把Key代码里到处是硬编码的base_url生产环境报错还得逐个去查是哪家服务商挂了。后来我干脆花了两周时间把整个LLM API调用层重构成一个统一管理系统顺便把论文和源码都整理出来了这套东西到现在还在稳定跑着。今天就把设计和实现思路完整拆一遍包括路由、鉴权、限流、计费、容灾这些核心模块以及我踩过的坑。这个系统适合什么场景如果你也在做多模型接入的AI应用、公司内部有多个业务方在调大模型接口、或者你手里有几个Key想统一管理和分配那这套设计可以直接拿来当参考。它解决的核心问题就三个Key收敛、路由分发、成本可控。下面从设计思路开始一步步说清楚。1. 为什么需要统一管理LLM API——先讲清楚根因1.1 多模型多密钥带来的管理混乱先说个我自己的真实情况。项目初期我们同时在用四家国产大模型API外加一个开源模型的私有化部署每个模型都有自己的API Key、计费方式、限流策略。业务代码里大概有七八处直接拼接HTTP请求的地方每处都要单独处理鉴权头、超时配置、错误重试。最要命的是前端同学在调试时为了方便把Key直接写进了前端代码里后来被安全扫描发现搞得整个Key作废重发。这种散养式接入的麻烦远不止这些Key泄露风险不可控每多一个人接触Key泄露概率就高一分而且泄露了很难追溯是谁干的。接口语义不统一有的服务商用Authorization: Bearer有的用自定义header有的响应里tokens用usage.total_tokens有的却是usage.completion_tokens业务方接入成本极高。故障定位困难线上偶发超时到底是网络波动、对方服务限流、还是我们自己代码的问题没有任何统一的观测手段。1.2 接口格式差异与迁移成本各家大模型API虽然都兼容OpenAI格式但细节差异非常多。比如模型名称映射同一个能力在A家叫deepseek-chat在B家叫glm-4-flash业务方根本不需要关心这个但每换一家服务商就要改一遍代码。参数兼容性有的服务商支持thinking_budget有的不支持有的支持response_format传JSON Schema有的只支持json_object。这些细微差异在接入时全是坑。流式输出格式SSE流里delta字段的嵌套结构各家略有不同解析逻辑写得不健壮就会漏内容。统一管理系统的第一个价值就是把这些差异全部在网关层抹平。业务方只对接我们定义的统一接口底层换成哪个模型、哪家服务商业务代码一行都不用动。1.3 成本治理与配额控制需要集中化散养接入还有个隐性成本问题谁在用、用了多少、花多少钱完全是一笔糊涂账。月度账单下来只看到一个总金额没法拆分到具体业务线、具体功能、具体用户。集中管理之后每一笔请求都能记录下业务方标识、模型、输入输出token数、花费金额。有了这些数据你才能回答老板三个问题AI功能到底花了多少钱哪个业务方最费钱哪些调用是浪费的可以砍掉我现在每天开盘第一件事就是看前一天的token消耗报表哪些业务在刷量、哪些功能夜间请求异常一眼就能看出来。这在散养模式下是根本做不到的。2. 系统整体设计从零搭一个LLM API网关2.1 核心模块划分整个系统我是按网关的思路设计的没有把逻辑塞进单个服务里而是拆成了六个清晰的模块。这样每个模块职责单一后续扩展也不怕牵一发动全身。模块职责关键技术点接入层对外暴露统一HTTP接口RESTful/SSE流式转发鉴权模块API Key校验、权限控制JWT本地Key双轨路由模块模型名映射、上游选择权重路由、健康检查治理模块限流、配额、熔断令牌桶算法、滑动窗口计量模块Token统计、费用计算按模型单价实时累加审计模块请求日志、调用链路结构化日志、Trace ID2.2 设计选型背后的考量技术栈我当时选的是Java Spring Boot 3 PostgreSQL Redis原因有三条。第一团队主力语言是JavaSpring Boot生态成熟做网关类项目有现成的WebFlux能用但这里我特意用了Servlet栈而不是WebFlux。为什么因为LLM API的SSE流式转发在Servlet栈里实现起来更直观而且我们大部分调用是同步请求WebFlux的响应式模型对团队心智负担太大了。第二PostgreSQL存计量数据。其实最初考虑过用ClickHouse或者MongoDB但考虑到项目里还要存用户、应用、Key等关系型数据不想维护两套存储就统一用PG了。事实证明每天几十万条token计量记录PG配合按天分区表完全扛得住没必要引入更重的组件。第三Redis在这里作用很重限流计数、令牌桶桶令牌、分布式锁、热点Key缓存都在它上面。注意限流一定要用Lua脚本保证原子性不然并发一高就超发。2.3 为什么不上APISIX这类开源网关有朋友问我这不就是API网关吗直接用APISIX或者Kong不行吗我的回答是能用但不划算。通用网关确实能做转发、限流、鉴权但LLM API有太多专用逻辑是通用网关不具备的Token级计量计费普通网关最多按请求次数计数而我们按prompt_tokens completion_tokens精确到每笔请求计费。模型名动态映射路由规则不只是路径匹配还需要根据模型名、优先级、成本策略做多级路由。上游健康状态感知LLM服务商挂掉是常态网关要能自动感知并切换备用通道。这些逻辑硬塞进OpenResty或者APISIX插件里开发调试效率远不如用业务语言写一个独立服务。通用网关适合统一流量入口不通用来适配大模型这种有明显领域特点的协议。3. 核心实现细节路由、鉴权与限流3.1 统一路由与模型映射路由模块是整个系统的心脏。它的逻辑很简单业务方在请求体里传model字段系统根据配置找到对应的上游渠道列表按优先级和权重选一个然后把请求转发过去。先看统一请求模型怎么设计。我用的是OpenAI协议的超集业务方传的请求大概长这样{ model: , messages: [{role: user, content: Hello}], stream: false, temperature: 0.7, max_tokens: 2048 }重点是model字段不用传具体服务商的名字而是传业务自定义的模型别名。比如model: chat系统自动把它映射到deepseek-chat权重70%走DeepSeek30%走GLM-4-Flash降本。核心路由伪代码如下public RouteTarget route(String modelAlias, String user, int estimatedTokens) { ModelRoute route modelRouteTable.get(modelAlias); if (route null) { throw new BizException(MODEL_NOT_FOUND, 模型别名不存在: modelAlias); } ListUpstream candidates route.getUpstreams().stream() .filter(up - up.isHealthy()) .filter(up - up.remainQuota(user) estimatedTokens) .sorted(Comparator .comparing((Upstream up) - up.priority) .thenComparing(up - up.weightRandom())) .collect(Collectors.toList()); if (candidates.isEmpty()) { throw new BizException(NO_AVAILABLE_UPSTREAM, 模型 modelAlias 暂无可用的上游服务); } return new RouteTarget(candidates.get(0), route.needRecordUsage()); }这里有几个细节值得展开说第一权重随机。我实现的是加权随机而不是轮询。为什么因为LLM请求响应时长差异太大有的请求3秒有的30秒轮询会在某次慢请求上堵住后续流量加权随机配合健康检查能更平滑地把负载分散开。第二健康检查用被动探测。系统不主动发心跳而是把每次请求的响应状态记录下来连续失败N次就摘除该上游过一段时间再放回去试。主动心跳对LLM服务商来说负担大而且很多服务商根本没有健康检查接口。第三按预估token数前置过滤。estimatedTokens可以从消息字数粗略估计如果某条渠道剩余配额不足以支撑这次请求直接跳过避免请求发过去才被上游拒绝。3.2 鉴权与密钥管理统一管理后业务方拿到的不再是各家模型的Key而是我们自己签发的AppKey。格式类似ak_6f8c34a2e1d9f0b3一个Key对应一个应用。每个应用可以配置可访问的模型范围比如只让某个应用调chat不允许调embedding每日配额上限按token或者按金额所属业务线用于计量拆账状态开关紧急熔断用鉴权流程不复杂从请求头取Key查Redis缓存校验状态和权限然后进入限流环节。Redis里的Key缓存设置5分钟过期被禁用后最多5分钟生效这个时间窗口可以接受。我额外做了一个小功能子Key机制。一个应用可以签发多个子Key每个子Key挂在不同团队成员或不同环境上。这样万一某个Key泄露只需要吊销子Key即可不用把整个应用的Key换掉。3.3 限流与配额控制限流这块我用了两把尺子请求频率限流和Token额度限流。请求频率用的是令牌桶每秒补充速率和桶容量按应用维度配置实现用Redis Lua脚本保证原子性-- KEYS[1] rate, KEYS[2] capacity, KEYS[3] now local key KEYS[1] local rate tonumber(ARGV[1]) local capacity tonumber(ARGV[2]) local now tonumber(ARGV[3]) local tokens tonumber(redis.call(HGET, key, tokens) or capacity) local last tonumber(redis.call(HGET, key, last) or now) local delta math.max(0, now - last) tokens math.min(capacity, tokens delta * rate) if tokens 1 then return 0 end redis.call(HSET, key, tokens, tokens - 1) redis.call(HSET, key, last, now) return 1Token额度限流的做法不太一样。由于token数只有在请求完成后才知道所以我的策略是请求前按预估token预占额度请求完成后按实际使用量回补/扣减。比如某个应用每天额度100万token一次请求预估消耗2000就先扣2000实际返回用了1500再补回500。预占不是绝对准确的但能有效防止突发流量把额度瞬间打穿。配额超限时返回429状态码业务方可以根据这个状态码做自己的降级逻辑。这里有个教训限流状态码一定要和错误码分开。我见过有系统把限流也返回500导致业务方无法区分是服务端故障还是配额不足最终只能往上游疯狂重试反而加重了系统压力。4. 计费、审计与可观测性4.1 Token计量与按量计费LLM API统一管理系统的另一大核心是计量计费。这个不搞好前面那些治理规则全都落不了地。我的实现思路是在路由模块拿到上游响应后统一从响应体里解析usage字段然后把计量事件异步发到消息队列由计量服务批量写入数据库。为什么走消息队列而不是同步写库因为大流量下同步写库会拖慢主链路响应。一次写操作按5ms算每秒1000并发就有5秒的写负载虽然PG能扛但不值得冒这个险。计量数据结构长这样CREATE TABLE usage_record ( id BIGSERIAL PRIMARY KEY, trace_id VARCHAR(64) NOT NULL, app_key_hash VARCHAR(64) NOT NULL, model_alias VARCHAR(64) NOT NULL, upstream_model VARCHAR(64) NOT NULL, prompt_tokens INT NOT NULL DEFAULT 0, completion_tokens INT NOT NULL DEFAULT 0, total_tokens INT NOT NULL DEFAULT 0, cost_amount DECIMAL(12, 6) NOT NULL DEFAULT 0, request_time TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_usage_time ON usage_record (request_time DESC); CREATE INDEX idx_usage_app ON usage_record (app_key_hash, request_time DESC);费用计算规则维护在另一个模型单价表里。每个上游模型都有一个单价按百万token计费。单价表要支持按时间段生效因为服务商经常调价我不能每调一次价就改代码。手动调过一轮之后我发现计费精度要求其实没想象中那么高。小数点后四位足够关键是口径要一致。有的服务商按prompt_tokens和completion_tokens分开计价有的是一个打包价。统一到同一个口径后报表上看到的数据才有可比性。4.2 全链路审计日志调用审计这块我曾经犹豫过要不要记录完整的请求和响应内容记录的话涉及用户隐私和数据合规不记录的话排查问题少了关键证据。最终的方案是默认只记录元数据不记录body。元数据包括traceId、appKey哈希、请求模型、上游模型、耗时、输入输出token数、HTTP状态码、错误信息截断到200字符。针对线上问题诊断我在响应头里加了一个X-Trace-Id业务方带着这个ID来找我我可以反查到当前这条请求的所有元信息。如果需要body级别的采样系统支持按1%概率抽样保存用于模型质量分析但不会默认全量存。还有一个细节日志里不要记录用户的敏感信息比如prompt里如果带了身份证号、手机号之类的内容审计日志会变成数据泄露源。第一次设计时我就把prompt的内容直接丢了只保存哈希值用于去重分析安全很多。4.3 指标监控与告警可观测性三个支柱日志、指标、链路追踪。日志是审计模块做的指标这块用Prometheus Grafana链路追踪通过TraceId串联。我在代码里暴露的指标主要有llm_proxy_requests_total按应用、模型、状态码分类的请求计数llm_proxy_request_duration_seconds响应延迟直方图llm_proxy_tokens_totaltoken消耗计数按应用分类llm_proxy_upstream_failures_total上游调用失败计数按上游地址分类llm_proxy_limit_reject_total限流拒绝次数告警规则我设置了三类上游错误率5分钟内超过20%告警「上游可能故障」单应用请求量5分钟内翻了三倍告警「疑似异常流量」系统整体错误率超过5%告警「入口异常」这套指标上线后我最直观的感受是以前线上有问题靠用户反馈现在基本靠告警主动发现。有一次凌晨某个服务商限流我们比上游的通告邮件更早感知到异常提前切了备用渠道一点都没影响到业务。5. 高可用与容错超时、重试与降级5.1 超时与重试策略LLM服务商的可信度说实话不太行。超时、连接中断、半路返回错误码这些情况我全遇到过。所以系统的高可用能力不是可选项而是必选项。先设置超时。总超时必须分两段连接超时和读超时。连接超时通常设置5秒读超时根据模型和场景来普通对话模型30秒长文本生成模型120秒流式请求不断流阈值60秒超过60秒没有新的SSE分片则视为超时读超时设太久有个副作用如果上游彻底挂了请求会在我们的线程池里堆积最终拖垮网关自身。所以读超时设完还必须配套线程池隔离。我用的是信号量隔离每个上游渠道一个独立信号量超过阈值直接拒绝新请求不让故障扩散到其他渠道。重试策略这块我的经验是只有幂等请求才能安全重试。LLM调到一半连接断了上游可能已经完成了这轮生成重试会导致重复消耗token还会让结果不一致。所以超时类的错误不重试直接返回502由业务方决定要不要换模型重新生成。429限流错误如果上游有Retry-After头按它等没有则退避重试最多2次。网络连接错误比如连接被拒绝安全重试一次因为请求大概率没有到达上游。5xx错误默认不重试但可以配置为切到备用渠道重试一次。5.2 故障转移与降级前面提到健康检查和权重路由这里展开讲运维上的自动化切换。我给每个模型别名配置了多个上游渠道。当主渠道连续失败超过阈值时路由模块自动把它标记为不健康请求全部转到备用渠道。实现上用了一个简单的状态机HEALTHY - (连续3次失败) - DEGRADED - (失败继续累加) - DOWN DOWN - (冷却时间到探活成功) - HEALTHY这三个状态各自对应不同的行为HEALTHY参与路由加权DEGRADED降低权重比如从70%降到10%保留少量真实流量做探活DOWN不参与路由冷却5分钟后自动转为DEGRADED探活这套机制上线后一个很典型的效果是主渠道服务商半夜公告升级我们完全无感流量自动都走到了备用渠道上。还有一个降级策略也在配置里成本降级。当主渠道价格上调时可以把一部分流量自动切到更便宜的低优先级模型上。比如chat别名下面配了三个渠道精准增强型贵、标准型中、精简型便宜按不同业务方或不同请求类型路由到不同档位。6. 部署落地与避坑清单6.1 环境准备与部署步骤整套系统部署其实不复杂依赖就是数据库和Redis。我用Docker Compose一键启动version: 3.8 services: llm-gateway: image: llm-gateway:1.0.0 ports: - 8080:8080 environment: SPRING_PROFILES_ACTIVE: prod DB_URL: jdbc:postgresql://postgres:5432/llm_gateway REDIS_HOST: redis depends_on: - postgres - redis postgres: image: postgres:16-alpine environment: POSTGRES_DB: llm_gateway POSTGRES_USER: gateway POSTGRES_PASSWORD: change_me volumes: - pgdata:/var/lib/postgresql/data redis: image: redis:7-alpine command: redis-server --appendonly yes volumes: pgdata:服务起来之后要做四件事初始化数据库表结构项目里带了一个init.sql直接执行。在控制台里创建一个应用拿到AppKey。配置模型路由表把模型别名绑定到上游服务商。用curl验证一个最简单的对话请求通不通。6.2 常见问题与排查速查表这部分是我实际运行中遇到的典型问题和排查思路直接整理给大家参考。现象可能原因排查思路请求返回400报错thinking_budget parameter must be a positive integer请求参数里thinking_budget传了非正整数在网关层增加参数校验捕获非法参数并返回友好错误请求返回400报错maximum context length is 1048576 tokens输入messages太长超过模型上下文窗口网关层估算token数超限提前拒绝并提示截断请求返回403上游鉴权失败Key错误或已被禁用检查路由配置的密钥是否过期查看审计日志确认走的是哪条上游连接中途断掉报connection lost mid-response上游服务不稳定或请求超时被断开查询该时间段的upstream错误率和响应延迟对应调整超时策略限流不生效高并发时照样打爆上游Lua限流脚本有问题或限流Key维度设置不对核对Redis中的限流计数是否在增长检查是否按应用维度生效某条渠道失败但流量仍然打过去健康检查失败阈值太高冷却时间太长查看该上游的状态机适当调低失败阈值6.3 动手做这套系统要注意的几个坑最后整理几个我在开发过程中踩过的坑给要动手做的人提个醒。第一个坑SSE流式响应做不对监控数据全是空的。流式请求的计费和指标收集跟普通请求完全不同因为响应是分片的usage字段一般被放在最后一个分片里。如果网关层直接透传SSE流不做拦截和聚合计费模块就永远拿不到token数。我的方案是把SSE流在网关层做一次“解包-处理-再打包”虽然多花一点CPU但每个分片经过网关时都能被观测到。第二个坑不要把限流和实际上游的限流混为一谈。我们自己的限流是保护自己上游也有自己的限流。即使你感觉单应用请求不算多一旦上游某个渠道的共享配额被打满所有走该渠道的应用都会一起遭殃。所以路由模块要增加一个上游维度总流量限流防止某个应用把一条上游渠道打满。第三个坑模型别名映射表一定要做版本管理。我一开始用配置文件管理模型路由改一次要重新发布。后来发现一天可能要改好几次上游加模型、调权重、换Key配置文件完全不够用。最后把路由配置挪到了数据库里控制台可视化编辑改了实时生效。这个改动直接省了运维一半的时间。第四个坑Key加密存储别明文落库。上游服务商的API Key是敏感信息存数据库时必须加密而且要在代码里脱敏。日志里打印上游Key只有前四位和后四位中间打码。这不仅是安全规范问题也是出了问题能不能追责的问题。最后说一点体会做这套系统前期最花时间的不是代码而是想清楚“统一到什么程度”。我把所有上游接口都包装成OpenAI协议看起来是省事了但也丢掉了一些上游特有的能力比如某些模型支持多模态输入某些支持特殊的推理参数。如果一开始就想面面俱到系统会变得极其复杂根本推不动。先统一核心的对话和Embedding能力其他能力后续通过扩展字段透传这是我认为最务实的路径。还有个小技巧我可以分享在做成本治理时不要只盯着模型单价。实际上请求体里的max_tokens设置对费用的影响往往比模型选型更大。很多业务方习惯性把max_tokens调到4096甚至8192但实际输出只有几百token这是极大的浪费。我在网关里加了一个“最大输出token钳制”的功能按业务方配置上限比如普通对话不许超过1024超了自动截断。上线这个功能后当月的token费用直接降了30%左右效果立竿见影。本文还有配套的精品资源点击获取
返回列表