1. 从一张天价账单说起:为什么企业级 AI 网关不是可选项
去年年底,我帮一个做 SaaS 客服系统的团队做架构复盘。他们上线了基于大模型的智能问答功能,三个月内调用量涨了 40 倍,账单也跟着涨了 40 倍。最离谱的是,财务发现某一天的费用是平日的 6 倍——排查下来,是一个内部测试账号的密钥被前端打包进了客户端,被人扒出来当免费接口刷了一整晚。
这件事之后,他们做的第一件事不是换模型,也不是砍功能,而是加了一层网关。这层网关要干三件事:挡住不该进来的请求、缓存本该复用的结果、管住每个租户能花多少钱。这三件事对应到技术方案上,就是动态语义缓存、敏感词熔断和多租户 Token 预算治理。
很多人对"AI 网关"的理解还停留在"反向代理 + 转发 API Key"的阶段,觉得用 Nginx 加个 upstream 就够了。但真正跑过生产环境的人都知道,大模型接口和普通 HTTP 接口有本质区别:它的成本是按 Token 线性计费的,它的响应是概率性的,它的输入是不可控的自然语言。这三点决定了传统网关那一套限流、鉴权、负载均衡的玩法,只能解决一半问题。
这篇文章面向的是正在或即将把大模型能力接入生产系统的后端工程师、架构师和技术负责人。我会把这三个核心模块的设计思路、关键参数、踩过的坑和可直接抄的配置逻辑讲清楚。不管你现在用的是哪家模型服务,这套网关思路都是通用的。读完之后,你应该能自己搭出一个"不会被薅破产"的 AI 接入层。
2. 动态语义缓存:为什么精确匹配缓存几乎没用
2.1 传统 KV 缓存在对话场景下的失效
先说一个反直觉的结论:如果你用 Redis 做key = prompt的精确匹配缓存,命中率大概率不会超过 5%。我实测过一个日均 20 万次调用的客服场景,精确匹配的命中率只有 3.7%。
原因很简单。用户问"你们的退款政策是什么"和"退款怎么弄",语义完全一样,但字符串完全不同。再加上多轮对话里每轮都会带上历史上下文,同一个意图的 prompt 几乎不可能逐字重复。传统缓存的前提是"相同的输入产生相同的输出",而大模型场景里,输入是发散的,输出是概率的,这个前提直接不成立。
所以我们需要的是语义缓存:把 prompt 转成向量,在向量空间里找"意思相近"的历史请求,如果相似度超过阈值,直接返回缓存结果,根本不调用模型。
2.2 语义缓存的完整链路拆解
一个能上生产的语义缓存,链路是这样的:
- 请求进来,先做归一化:去掉多余空格、统一标点、剥离无关的系统提示词前缀。这一步能显著提升后续相似度判断的准确率。
- Embedding 编码:用一个小而快的 embedding 模型把归一化后的文本转成向量。这里不要用大模型自己的 embedding 接口,延迟太高,用本地部署的小模型(比如 384 维的轻量模型)就够了,单次编码控制在 10ms 以内。
- 向量检索:在向量库里做近似最近邻搜索,取 Top-K 个候选。
- 相似度阈值判定:这是最关键的一步。余弦相似度超过阈值才认为是"同一个问题"。
- 命中则返回,未命中则调用模型并把结果写回缓存。
这里有个容易忽略的细节:缓存写入要带 TTL,而且 TTL 要分层。政策类、FAQ 类的问题答案稳定,TTL 可以设 24 小时甚至更长;而涉及实时数据(比如"我的订单到哪了")的问题,缓存了反而会返回错误答案,这类请求应该在归一化阶段就被识别出来并跳过缓存。
2.3 阈值怎么定:一个必须用数据说话的参数
相似度阈值是语义缓存的命门。设高了,命中率低,缓存形同虚设;设低了,会把"退款政策"和"退货政策"当成同一个问题,返回错误答案,这是生产事故。
我的经验做法是:先跑一周的 shadow 模式。所有请求正常调用模型,但同时把 embedding 和相似度计算跑一遍,记录下"如果命中会返回什么"和"实际模型返回什么"。然后人工抽检 200 组,找出误判的边界,反推阈值。
下面是我在几个不同场景下实测出来的参考区间:
| 场景类型 | 推荐阈值 | 说明 |
|---|---|---|
| 标准 FAQ 问答 | 0.92 - 0.95 | 答案固定,宁可漏命中不可错命中 |
| 开放式咨询 | 0.85 - 0.90 | 容忍一定语义漂移,追求命中率 |
| 代码生成辅助 | 0.95 以上 | 差一个字符结果可能完全不同 |
| 多轮对话 | 0.88 - 0.92 | 需结合上下文向量一起判断 |
注意:阈值不是一劳永逸的。模型版本升级、业务话术变化、用户群体迁移,都会让最优阈值漂移。建议每月做一次抽样回归。
2.4 缓存击穿与并发穿透的处理
还有一个坑:热门问题在缓存过期瞬间会遭遇并发穿透。比如某个爆款问题缓存刚过期,同一秒进来 50 个相同请求,全部未命中,全部打到模型接口,瞬间产生 50 倍成本。
解决办法是加一层"单飞"(singleflight)机制:同一个语义 key 的并发请求,只放行第一个去调用模型,其余请求等待并复用结果。实现上可以用分布式锁,锁的粒度就是归一化后的语义 key。这个机制我在生产环境验证过,能把缓存过期瞬间的模型调用量压到原来的 1/50。
3. 敏感词熔断:不是简单过滤,而是分级响应
3.1 为什么敏感词处理不能只做"拦截"
很多团队对敏感词的理解就是"命中就拒绝",返回一个"您的输入包含违规内容"。这在 To C 产品里勉强能用,但在企业级场景里问题很大。
第一,误伤率高。中文里很多词是多义的,比如某些词在医疗语境下是正常术语,在别的语境下才敏感。一刀切拦截会让正常业务无法进行。
第二,拦截本身也是成本。用户输入被拦截,他会换个说法再试,反复试探反而增加了请求量。更糟的是,如果拦截逻辑写在应用层而不是网关层,每个业务系统都要重复实现一遍。
第三,有些请求不该拦,该降级。比如一个包含敏感词但整体意图正常的请求,完全可以走一个更便宜的小模型,或者返回一个模板化的安全回复,而不是直接报错。
3.2 三级熔断策略的设计
我在网关里用的是三级策略,按风险等级分流:
- 一级(提示级):命中低风险词,正常放行,但在响应里附加安全提示,同时打标记录,用于后续审计。
- 二级(降级级):命中中风险词,不调用主模型,改走一个经过安全微调的小模型,或者返回预设的安全话术。成本几乎为零。
- 三级(熔断级):命中高风险词或明显的攻击模式(比如 prompt 注入、越狱尝试),直接拒绝,并把这个租户/用户加入短时观察名单。
这个分级的核心价值在于:把"拦截"这个二元动作,变成了一个成本可控的连续光谱。大部分灰色地带的请求,用降级处理就够了,既保证了安全,又不至于把用户赶走。
3.3 敏感词库的工程化维护
敏感词库不是写死在代码里的一个数组。生产环境里它应该是一个可热更新的配置,支持:
- 分级标签:每个词带风险等级,而不是简单的黑名单。
- 正则与变体匹配:处理谐音、拆字、插入符号等绕过手法。这部分要控制好性能,正则太复杂会拖慢网关。
- 白名单上下文:某些词在特定业务线里是允许的,需要支持按租户配置白名单。
- 热更新不重启:词库变更通过配置中心推送,网关监听变更,秒级生效。
性能上,我建议用AC 自动机(Aho-Corasick)做多模式匹配,一次扫描就能找出所有命中词,复杂度是 O(n),跟词库大小无关。几万条词库下,单次检测能控制在 1ms 以内。如果用简单的for循环遍历词库,几万条词下延迟会直接爆炸。
3.4 熔断后的可观测性
熔断最怕的是"黑盒"——拦了什么、为什么拦、拦了多少,全都没记录。这在出问题时根本没法排查。
我的做法是:每一次熔断都写一条结构化日志,包含请求 ID、租户 ID、命中的词、风险等级、处理动作、原始输入哈希(注意不是原文,避免日志里存敏感内容)。这些日志汇总到监控面板上,能实时看到熔断率的变化。如果某个租户的熔断率突然飙升,要么是他在被攻击,要么是他的业务话术和词库冲突了,两种情况都需要立刻介入。
4. 多租户 Token 预算治理:把成本关进笼子
4.1 为什么"限流"解决不了成本问题
传统限流是按 QPS 限的,比如每个租户每秒最多 10 个请求。但大模型的成本跟请求数没关系,跟 Token 数有关系。一个请求可能只花 50 个 Token,也可能花 50000 个 Token(比如塞了一整篇文档做总结)。按 QPS 限流,等于放任那些"大请求"随便烧钱。
所以预算治理的计量单位必须是Token,而且是输入 Token + 输出 Token 分开算。因为输出 Token 通常比输入贵好几倍,一个租户如果疯狂让模型生成长文本,成本会失控。
4.2 预算模型:从租户到用户的四级账户
我设计的预算体系是四级的:
- 租户级总预算:每个租户每月一个总额度,这是最外层的硬约束。
- 项目级子预算:一个租户下可能有多个应用,各自分配额度。
- 用户级配额:防止单个用户刷爆整个租户的额度。
- 单请求上限:单个请求最多消耗多少 Token,超过直接拒绝或截断。
这四级是层层收敛的关系。任何一级超限,请求都会被拦下。这样做的好处是,即使某一级配置失误,其他级还能兜底。比如某个用户配额设错了,租户总预算还能挡住他。
4.3 Token 预估:在调用之前就把账算清楚
预算治理最难的点在于:你必须在调用模型之前就知道这次要花多少 Token,否则没法做准入判断。
输入 Token 好办,用分词器提前算出来就行,误差很小。难的是输出 Token——你根本不知道模型会生成多长。
我的做法是双轨制:
- 预估阶段:根据请求类型给一个输出 Token 的预估值。比如问答类预估 500,总结类预估 2000,代码生成预估 1500。这个预估值参与准入判断。
- 结算阶段:模型返回后,拿到真实的 Token 用量,用真实值扣减预算,并把这个差值反馈给预估模型,动态修正后续的预估值。
这样跑一段时间后,预估值会越来越准。我实测下来,经过两周的自适应修正,预估误差能收敛到 15% 以内。
4.4 超预算时的降级而非拒绝
预算耗尽就直接报错,用户体验很差。更好的做法是分级降级:
| 预算使用率 | 处理策略 |
|---|---|
| < 70% | 正常服务,走主模型 |
| 70% - 90% | 提示租户,同时把非核心请求路由到便宜模型 |
| 90% - 100% | 只保留核心业务,其余请求排队或降级 |
| > 100% | 硬熔断,返回明确提示,引导充值或等下月重置 |
这套策略的关键是提前预警。等到 100% 才动作就晚了,70% 就要开始给租户发通知,让他有时间调整。
4.5 计量的准确性:别让账对不上
预算治理最怕的是"账对不上"——网关算的用量和模型服务商账单差一大截。这通常有几个原因:
- 流式响应的 Token 统计:流式返回时,Token 是逐块产生的,必须在流结束时汇总,不能中途漏算。
- 重试的重复计量:请求失败重试时,如果第一次其实已经产生了消耗(比如模型已经开始生成但连接断了),这部分也要计入。
- 缓存命中的计量:缓存命中不消耗模型 Token,但可能消耗 embedding 的算力,这部分要不要计入预算,需要明确规则。
我的建议是:网关的计量日志和模型服务商的账单,每周做一次对账。差异超过 5% 就要查原因。这个习惯能帮你及早发现计量漏洞。
5. 三个模块如何协同:网关的整体请求生命周期
5.1 一次请求的完整流转
把三个模块串起来看,一个请求进网关后的完整流程是这样的:
- 鉴权与租户识别:解析 API Key,确定租户、项目、用户身份。
- 预算预检:查当前预算余额,预估本次 Token 消耗,判断是否准入。超限则走降级或拒绝。
- 敏感词检测:AC 自动机扫描,按风险等级决定放行、降级还是熔断。
- 语义缓存查询:归一化 + embedding + 向量检索,命中则直接返回,跳过模型调用。
- 单飞控制:未命中时,对相同语义 key 加锁,防止并发穿透。
- 模型调用:转发到后端模型,流式或非流式处理。
- 结算与写回:统计真实 Token 用量,扣减预算,写入缓存,记录日志。
这个顺序很重要。预算预检必须在敏感词检测之前,因为敏感词检测本身也有成本(虽然很低),而且如果预算已经超了,根本没必要做后续处理。缓存查询必须在模型调用之前,这是省钱的核心。
5.2 各模块的失败隔离
网关是单点,任何一个模块挂掉都可能拖垮整个链路。所以每个模块都要有降级预案:
- 向量库挂了:跳过语义缓存,直接走模型。宁可多花钱,不能服务不可用。
- 敏感词服务挂了:走一个内置的极简词库兜底,只拦最高风险的词。
- 预算服务挂了:切换到本地内存计数,虽然可能不准,但能保证服务继续。
这里的原则是:安全模块可以降级,但不能阻塞主流程。我见过有团队把敏感词检测做成同步强依赖,结果词库服务一抖动,整个 AI 功能全挂,这是典型的架构失误。
5.3 配置的热更新与灰度
网关的很多参数(阈值、预算额度、词库、降级策略)都需要能动态调整。我的做法是全部走配置中心,网关监听变更。但直接全量生效有风险,所以加了灰度能力:新配置先对 5% 的流量生效,观察指标正常后再逐步放量。
这个灰度机制救过我一次。有次调整相似度阈值,全量生效后发现某个业务线的误判率飙升,幸好是灰度,只影响了 5% 的流量,及时回滚了。
6. 上线后踩过的坑与调优心得
6.1 缓存"污染":错误答案被反复返回
语义缓存最大的坑是缓存污染。如果某次模型返回了一个错误答案,这个错误答案被缓存下来,后续所有相似问题都会拿到这个错误结果,而且因为命中缓存,你根本不知道它在错。
我的应对是:给缓存条目加"质量分"。每次命中缓存后,如果用户对结果做了负反馈(比如点了"没用"),就降低该条目的质量分。质量分低于阈值的条目,即使相似度命中也不返回,强制走模型重新生成。同时,定期抽样人工审核缓存内容,发现系统性错误就批量清理。
6.2 预算的"月末效应"
上线预算治理后,我观察到一个有趣的现象:很多租户在月末几天用量骤降。一开始以为是业务波动,后来发现是他们的预算快用完了,系统开始降级,用户体验变差,他们就少用了。
这其实是个信号:预算额度设置得不合理。要么是额度太低,要么是月初没有做好用量预测。后来我加了一个"用量趋势预测"功能,根据租户前几天的用量,预测本月总消耗,如果预测会超,提前一周就通知租户。这个功能上线后,月末的降级率下降了一大半。
6.3 敏感词误伤引发的客诉
敏感词熔断上线第一周,我们收到了十几条客诉,都是正常业务被误拦。排查发现,问题出在词库的上下文缺失。比如某个词在通用语境下敏感,但在某个垂直行业里是标准术语。
解决办法是引入租户级白名单:允许租户为自己的业务线配置豁免词。同时,把熔断日志开放给租户查看,让他们能看到自己被拦了什么,主动反馈误伤。这个反馈闭环建立后,误伤率从最初的 2% 降到了 0.3% 以下。
6.4 性能调优:把网关延迟压到最低
网关本身不能成为瓶颈。我做过一轮性能压测,发现几个优化点:
- Embedding 编码是最大延迟来源。用 GPU 加速后,单次编码从 30ms 降到 8ms。如果预算有限,至少要用批处理,把多个请求的编码合并成一批。
- 向量检索的索引要选对。小规模(百万级以下)用 HNSW 就够,召回率和速度平衡得好。规模再大要考虑量化索引。
- 敏感词检测用 AC 自动机,前面提过,这里不重复。关键是词库要预编译,不能每次请求都重新构建自动机。
优化后,网关自身引入的额外延迟控制在 20ms 以内,相比模型动辄几百毫秒到几秒的响应时间,完全可以接受。
6.5 一个容易被忽略的点:日志的成本
网关会产生大量日志,尤其是每次请求的 Token 用量、缓存命中、熔断记录。如果全量存下来,日志存储成本可能比省下来的钱还多。
我的做法是分级采样:正常请求的详细日志采样 1%,异常请求(熔断、超预算、缓存未命中)全量记录。这样既保证了问题可追溯,又控制了存储成本。日志保留期设 30 天,超过的归档到冷存储。
7. 写在最后:网关是省出来的,也是设计出来的
这套网关上线半年,那个团队的模型账单从每月六位数降到了五位数,降幅超过 70%。其中语义缓存贡献了大约一半的节省,预算治理贡献了三成,剩下的来自敏感词降级和并发控制。
但我想说的是,省钱只是结果,不是目的。真正的价值在于,这层网关让 AI 能力从"不可控的成本中心"变成了"可计量、可治理、可预测的基础设施"。当你能清楚地知道每个租户花了多少钱、每个请求为什么被拦、每次缓存省了多少,你才敢放心地把 AI 能力开放给更多业务。
如果你现在正准备做类似的事情,我的建议是:先上预算治理,再上缓存,最后上敏感词。因为预算治理是保命的,缓存是省钱的,敏感词是合规的。优先级不同,但三个都迟早要做。别等到账单爆炸或者出了合规问题才想起来补,那时候成本就高多了。