资讯动态

企业级AI API网关治理:动态语义缓存、熔断与Token预算实战

发布时间:2026/10/14 11:40:29 来源:尧图企业网站定制
上个月账单出来的时候我盯着那串数字看了好一会儿。不是因为营收涨了而是因为我们对外提供的大模型接口被一批调用方薅成了筛子——同一个意思的问题换几种问法反复打到模型上每天产生几十万次本质重复的请求临近发版那几天还有几个账号在批量灌请求几小时就把一天的预算跑完。老板只问了一句这个钱还能不能管住于是我把治理重心放到了企业级 AI API 网关的三件事上动态语义缓存、敏感词熔断、多租户 Token 预算治理。这篇内容就是当时落地这三个能力的完整复盘。它适合一类人如果你的团队也在对外提供大模型 API或者你正在建设一个给多个业务线共享的 AI 接入层每天被 token 成本、滥用流量和额度分配问题追着跑那这篇文章应该能帮你少走不少弯路。我尽量把设计思路、参数取舍、踩坑记录都写清楚不只是给结论也把推导过程交代明白。1. AI API 被薅的三种姿势我的账单拆解1.1 重复提问是大头治理之前我习惯性认为是用户量大导致成本高但把网关日志按 prompt 内容做聚合之后发现很多请求表达的是同一个意思。客服问答最典型用户问怎么重置密码我忘了密码怎么办密码丢了怎么改三句话背后是同一个操作指南。传统语义理解系统能让这三句话命中同一篇知识库文档但网关层面如果什么都不做这三个请求就会各自生成一大段回答每段都可能消耗上千 token。把这类请求的业务价值拿出来看会得出一个让人不太舒服的结论相当比例的 token 费用花在了重复回答已知问题上。这不是大模型的问题是流量入口缺少一层语义去重的能力。后来我们给网关加了动态语义缓存效果立竿见影。但不是传统意义上的 Redis 缓存后面会细说。1.2 失败重试和恶意刷量是暗坑翻账单时另一类成本来自失败重试。客户端拿到超时或 5xx 错误后带着一模一样的请求重试三次、五次。大模型接口普遍响应慢一次超时背后可能已经产生了上万个 token 的推理成本重试不仅不能挽回这笔钱还把这笔钱复制了好几份。更难受的是恶意刷量。某些渠道账号会短时间高频调用 API专门挑回答长的提示词去触发一次返回几千 token一天刷下来能把月预算消耗掉一大块。这种流量没有语义价值也不符合任何正常用户的行为曲线必须靠熔断机制在入口处拦下来。1.3 网关的三层防线既然问题分三类治理思路也对应拆成三块最终落成网关上串联的三道闸门防线解决什么问题核心手段动态语义缓存相似表达重复打到模型浪费推理成本向量召回 相似度判定敏感词熔断恶意/违规请求快速消耗预算与算力多层文本识别 熔断动作多租户 Token 预算治理多个业务方共用网关额度不清、成本失控配额模型 实时计量 动态调度三条防线是串联执行的请求进入网关后先做风险检测命中熔断规则直接结束安全请求进入语义缓存查询命中就返回历史结果未命中的请求才真正转发给大模型同时进入预算计量系统完成扣减。这样设计的好处是越靠前的防线越便宜真正到达大模型的那部分流量已经是清理过后值得付费的请求。2. 动态语义缓存让一模一样的意思只付一次费2.1 传统缓存为何在 AI 网关前失灵一开始团队里有人提议直接用 Redis 做缓存key 用请求内容的哈希。这个方案很快被否了原因是用户不会用完全相同的字符串提问。同一个问题写成怎么开通会员和如何成为会员哈希值完全不同传统完全匹配型的缓存根本命中不了。语义缓存的核心转变是不再拿文本当 key而是拿语义向量当 key。把用户输入放进 embedding 模型转成向量然后找历史上最相似的向量如果相似度足够高就认为这次提问在语义上是重复的直接返回缓存里的回答。这样怎么开通会员和如何成为会员就能命中同一条缓存因为它们在高维向量空间里的位置非常接近。2.2 缓存判断链路完整链路并不复杂但细节比我想象中多。我按请求生命周期拆一下请求归一化去掉多余空格、统一大小写把明显的模板变量替换掉。有时候用户只是把今天换成明天这种时间敏感的变动会影响语义归一化阶段不会强行合并。生成查询向量用轻量级 embedding 模型把用户输入编码成向量通常几百维。这一步耗时只有几毫秒成本也远低于一次大模型调用。向量召回从缓存索引中召回 topK 个候选K 一般取 5 到 10。召回阶段不追求精确只负责把可能相似的候选捞出来。相似度校验逐个计算查询向量与候选向量的余弦相似度同时做元数据过滤——模型名、temperature、top_p 这些参数必须一致带不带 system prompt 也要一致。参数不同的请求即使语义相同也不能复用缓存否则回答风格和内容可能错位。命中则返回把缓存中保存的历史回答包成与大模型一致的响应格式返回并记录命中日志。未命中则转发调用真实大模型接口拿到完整响应后异步写入缓存异步路径不能阻塞主链路。伪代码如下def handle(prompt, params, tenant_id): query_vec embedding(prompt) candidates vector_index.search( query_vec, top_k10, filter{ model: params[model], temperature: params[temperature], tenant_scoped: params.get(tenant_scoped, False) } ) best max(candidates, keylambda x: cosine(query_vec, x[vector])) if best and best[score] config.semantic_threshold: return CacheHit(best[response]) else: response call_llm(prompt, params) cache_store.save({ vector: query_vec, prompt: prompt, response: response, model: params[model], temperature: params[temperature], created_at: now() }, ttlconfig.cache_ttl_seconds) return response缓存入库前一定要判断请求是否允许缓存。带敏感数据的请求、实时性要求高的请求比如查天气、查库存必须跳过缓存否则用户会拿到过时信息。这块能力我建议做成可配置项由业务方决定自己的接口允不允许缓存。2.3 阈值不是拍脑袋定的语义缓存最核心的参数是相似度阈值它决定多像才算同一个意思。这个值不能拍脑袋定我当时的做法是拿线上真实请求回放人工标注一批应该命中和不该命中的样本然后扫阈值。实践下来是这样的情况阈值表现问题0.85命中率高经常把两个不完全一样的问题合并错误命中明显缓存误导严重0.92客服场景命中率约三成误命中很少需要业务方确认可接受度0.98几乎不会误命中接近完全匹配语义缓存形同虚设不同业务差别很大。客服问答是典型的高相似场景0.92 左右比较合适代码生成、创意写作这类开放性任务表达差异极大0.92 会频繁误命中要上调到 0.96 以上。我当时没有追求一个全局最优阈值而是给每个租户配独立阈值允许业务方自己调。还有一个易漏的关键点缓存必须做 TTL。大模型回答不是永远不变的知识库更新、政策调整都会让历史回答过时。TTL 设太短浪费缓存价值设太长会让错误答案长期滞留。我的经验是常规问答场景 15 分钟到 1 小时比较稳妥涉及价格、库存这类动态信息的场景建议直接关闭缓存。3. 敏感词熔断在钱被烧完之前叫停3.1 熔断不应该是内容审核的替代品网上讨论这个功能时容易走向极端实际上当我们讨论网关上做敏感词熔断时核心目标不是做精细的内容审核而是做止损。精细审核需要人力和复杂模型不适合放在网关的高频链路上熔断解决的是检测到明显的风险信号后快速切断是成本控制和风险兜底的手段。所以熔断器不应该追求 100% 准确它的定位是守门员判断一个请求是否属于明显的恶意或风险流量给后续更细的审核争取时间。真正的合规审核可以放在异步链路里而网关用一套相对轻量的规则把破坏力最强的流量先挡住。3.2 多级检测链路单靠一份敏感词表是防不住任何人的。我的网关里敏感词检测由三层组成每层职责不同词典层维护一份核心风险词表用前缀树实现能在微秒级完成匹配。词典的维护权收口到专人每次更新走评审流程避免误加词导致大面积误伤。正则层处理词典层解决不了的变体比如数字与字母混写、分隔符插入、同音字替换。正则层灵活但也容易产生误匹配因此规则数量严格控制每一条都要配对应的测试用例。语义层用轻量级文本分类模型评估整体风险概率不依赖具体词形能识别出词表没覆盖的换个说法。这一层成本比词典高一个量级只对前两层标记为可疑的请求启用。三层是递进关系全部通过才放行。词典层和正则层全部跑完大约耗 1 到 3 毫秒语义层单独耗约 10 到 20 毫秒对整个请求链路来说可以接受。3.3 熔断后的动作命中敏感词之后不是简单抛一个 500 错误就完事。网关一侧要做几个动作返回明确的业务错误码由统一响应格式包装方便下游识别和处理。记录完整的请求元数据租户、调用方 IP、时间、命中的规则编号但注意响应正文不能透出命中的具体信息防止攻击者基于反馈反复试探。触发告警。单个请求告警太吵我会做聚合每分钟超过 N 次熔断才通知值班人既能发现批量攻击又不至于把告警变成噪音。同时要明确熔断的语义与限流不同限流是你太快了等一会儿再来熔断是这道门不让你过。限流错了顶多让用户慢一点熔断错了会直接中断业务所以熔断器设计必须保守规则宁可漏过也不要误伤。后面我专门踩过一次误伤的坑会在第 5 章详细说。3.4 绕过与对抗只要做文本检测就一定绕不过对抗。拼音替换、谐音、emoji 插入、拆字这些都是常见手法。硬碰硬去穷举变体没有尽头我的经验是把对抗目标从识别一切变体放宽到识别异常模式同一个租户短时间内请求大量相似变体本身就是强烈的攻击信号即使单个请求没有命中任何词条频率画像也会暴露异常。所以在敏感词熔断旁边我还挂了频率联动单位时间内的请求数、失败率、内容相似度三项超过阈值后自动进入观察名单后续请求全部做语义层检测再命中就升级为熔断。这样即使词表被绕过行为层面的异常兜底仍然生效。4. 多租户 Token 预算治理让每个 Key 都活在自己的额度里4.1 预算模型租户、配额、模型策略多租户治理的第一步是搞清楚谁在花钱。网关通过 API Key 识别调用方每个 Key 关联一个租户租户拥有一套独立预算配置。下面是我当时用的配置格式tenants: internal-crm: api_key_prefix: ck_crm_ daily_tokens: 50000000 alert_threshold: 0.8 qps: 80 tpm: 30000 allows_models: [fast-model, high-quality-model] overflow_tokens: 10000000 cache_policy: true partner-ai: api_key_prefix: ck_part_ daily_tokens: 10000000 alert_threshold: 0.7 qps: 20 tpm: 5000 allows_models: [fast-model] overflow_tokens: 2000000 cache_policy: true字段含义不复杂daily_tokens 是每日 token 预算上限tpm 是每分钟 token 吞吐上限qps 是每秒请求数上限。三个维度互相独立、同时生效。比较容易被忽略的是 allows_models——如果不限制租户能用哪些模型有人会绕过贵模型随便点成本一下就失控。高价值租户才允许调用高质量模型普通租户默认走低成本的快速模型这是最朴素也最有效的成本控制手段之一。4.2 计量与扣减流式输出下的预占和结算Token 计量的难点不是数 token而是什么时候扣。大模型接口普遍支持流式输出响应在结束前拿不到准确的 completion token 数但如果等响应结束再扣款一个租户可以在流式输出过程中超支造成实际的预算穿透。我的方案是两段式结算请求开始时按 prompt 的预估 token 数预占配额预占成功后请求才允许放行流式输出期间不额外扣费响应结束后拿到真实的 token 使用量进行差额补扣或返还。def try_acquire(tenant_id, prompt): est estimate_tokens(prompt) pre_key ftoken:pre:{tenant_id}:{today()} actual_key ftoken:actual:{tenant_id}:{today()} if get(pre_key) get(actual_key) est daily_limit(tenant_id): raise QuotaExceeded() redis.incrby(pre_key, est) return est def settle(tenant_id, prompt, completion): actual len(prompt_tokens) len(completion_tokens) pre get(pre_key) diff actual - pre if diff 0: redis.incrby(actual_key, diff) else: redis.decrby(pre_key, -diff) redis.decrby(pre_key, pre if diff 0 else 0)里面有几个细节需要注意。预占时用 prompt token 做估算是因为请求开始时只能确定 prompt 长度completion 长度是未知的。如果你给租户设置了单次响应上限max_tokens预占一定要按最大值来否则流式输出可能让配额形同虚设。另一个细节是 cached 请求的计量。语义缓存命中后没有发生实际大模型调用我当时的策略是缓存命中不扣 token 预算但会计入调用次数等辅助指标。原因很简单缓存命中的边际成本几乎为零不应该让租户为重复提问重复买单这也是推动业务方愿意接入语义缓存的核心激励。4.3 配额用尽后的三条软着陆路径配额用尽后直接拒绝请求当然最简单但业务影响太大。真实场景里我用了三档处理按严重程度逐步升级弹性池。配额用尽但租户还有未使用的弹性额度overflow_tokens时超出部分从弹性池扣同时响应头里带一个X-Quota-Source: overflow标记让调用方感知自己已经走了备用额度。降级模型。弹性池也用完后如果租户配置允许把请求自动降级到更便宜的模型。比如高质量模型降级到快速模型回答质量会略降但服务不至于中断。降级前后响应字段要一致尽量不让下游体感差异太大。软限流。前两档都不能用则对超过配额的请求做排队或直接拒绝。拒绝时返回 429 和清晰的Retry-After头引导调用方做退避重试而不是反复重试。这个设计在网上没有现成标准更多是我们内部根据业务形态摸索出来的。核心原则是预算治理的目标不是严卡死而是在成本失控之前让每个租户都有合理的衰减路径。5. 上线后的坑缓存命中率、误伤和账单差账5.1 缓存命中率虚高问题藏在统计口径语义缓存上线第一周命中率看着很不错日平均超过 35%。我把数据拆给业务方看对方却反映实际调用大模型的费用没降那么多。查了很久才发现问题我把缓存命中次数 / 总请求次数当成了命中率但这个口径对成本优化没有直接意义。缓存省钱的本质是省 token不是省请求数。一个用户请求命中缓存哪怕请求量占比很高如果命中的都是短文本省下的 token 可能还不如一次长文本请求多。后来我把统计口径改成了节省 token 数 / 总 token 消耗数同时看两个指标。改完口径之后真实收益才暴露出来虽然请求数命中率只有 28%但 token 节省率超过 41%因为用户最常重复提问的长尾问题恰好都是长文本。统计口径这件事建议每一家做网关治理的团队都提前想清楚。5.2 误伤事件特征词被一夜之间污染敏感词熔断上线两周后出过一次事故。某个热点新闻事件发生后大量正常用户开始谈论相关内容而其中一些高频词恰好在我们词典层里有记录导致整个下午正常请求被误拦了不少。当天晚上我做了一个决定把词典匹配结果从直接熔断降级为进入人工复核队列网关只拦截风险分数最高的那一档低风险命中的请求放行但是打上可疑标记让异步审核系统去处理。这样牺牲了部分拦截的即时性但换来的是误伤范围的大幅收敛。熔断器的原则应该是越危险的动作越要保守宁可慢半拍也不能把正常业务打进黑洞。5.3 对不上账不是玄学是记账时点不同预算治理上线后另一个经常被业务方质疑的问题是账单对不上。业务方自己统计的 token 消耗和网关统计的出入有时达到 5%。排查之后发现原因很集中一是流式输出的计费时点。业务方按响应结束时的 token 数记账但网关在请求开始时就做了预占预占和实际结算之间有差值如果只看某一个时间点的数据自然对不上。二是重试请求。客户端第一次请求超时实际上大模型已经完整生成了回答只是响应没能发回客户端客户端重试后网关又扣了一次业务方看到的是两次扣费但自己只发起了一次表面成功的请求。三是 embedding 费用没有计入 token 消耗。语义缓存的反向向量化是有成本的金额小但如果预算治理只算大模型 token 而忽略 embedding 费用长账依旧对不齐。解决方案是统一记账口径网关在响应完成后统一落一条计费流水包含租户、模型、prompt token、completion token、embedding token、缓存标识、请求 ID所有统计报表都以这条流水为准不另做二次估算。同时给每次重试加幂等键同一请求 ID 的重试不重复扣费。5.4 别让网关自己成为单点网关堆了这么多逻辑之后自身故障的影响范围会放大。我见过网关进程内存溢出导致整个 AI 接口不可用的案例所以做了两个降级开关缓存故障时直接跳过缓存把请求转发给大模型只是费用会涨回来风险检测服务超时时默认放行只有明确命中的请求才拦截。两处都遵循可用性优先于成本优化的原则。网关内存方面语义缓存的索引常驻内存随着条目增长很容易吃满。我给缓存设置了容量上限超过上限按 LRU 淘汰淘汰掉之后再遇到相同的请求最多重算一次不会引发雪崩。最后说一点个人体会如果再让我重新做一遍网关治理我会把顺序调整一下先上多租户 Token 预算治理再上敏感词熔断最后才做动态语义缓存。原因是预算治理能让你第一时间看清钱往哪里花这是后续所有优化的数据基础熔断负责止血防止异常流量快速放大成本语义缓存是纯增益优化等到前两者稳定后再上调参和效果评估都会轻松很多。还有一个很小的技巧值得分享给每个租户的配置项都加上变更生效时间戳改配置时自动生成变更记录。调阈值、调配额这些操作频繁发生没有审计日志的话出问题之后根本说不清是哪次调整引入的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑