这两年做 AI 应用落地,团队最头疼的往往不是模型能力不够,而是“模型太多、入口太散、账号太乱、成本不可控”。对话要接 GPT、Claude、国产模型,生图要接 Midjourney、SD、ComfyUI,每个服务商一套 Key、一套协议、一套计费规则,前端对接时恨不得给每个模型写一个适配器。后来我们把这块梳理成了一个统一的 AI 网关站:对话、生图、号池管理、缓存加速都在一个系统里完成。这篇文章就围绕这套系统的设计与实现展开,重点讲多模型聚合、Pro 号池调度、以及如何把缓存命中率做到 95% 以上并稳定对公服务。
如果你正在做 AI 聚合类项目、企业内部 AI 平台,或者想了解网关层怎么做模型路由与缓存治理,这篇文章可以作为一份系统性的工程参考。内容不挑模型供应商,核心思路和代码在任何支持 OpenAI 风格协议的服务上都适用。
1. 一个站搞定全模型 AI:到底要解决什么问题
1.1 多模型接入的痛点
先看一个很常见的场景:公司要做一款面向客户的 AI 产品,产品里需要对话、图片生成、图片理解等多个能力。于是你开始接各家 API,问题马上来了:
- 不同模型服务商的接口风格不统一,有的兼容 OpenAI,有的是自定义协议。
- Key 分散在各业务线手里,轮换、过期、限流全靠人工盯。
- 对话和生图是两套体系,前端要维护两套请求逻辑。
- 同一个 Prompt 反复请求,服务商侧成本重复消耗,响应速度也没有优化。
- 部分渠道需要维护“Pro 号”这类高配额账号,如果只用一个号,触发限流后整个服务就不可用。
这些问题累积到一定程度,就不是“多写几个 Service”能解决的了,必须有一个统一入口层来做协议转换、流量调度、账号池和缓存治理。这套入口层,就是我们常说的 AI 网关。
1.2 聚合网关的核心职责
AI 网关站在大模型服务和业务系统之间,看起来只是一个反向代理,实际核心职责要重得多:
| 职责 | 说明 |
|---|---|
| 协议统一 | 把不同供应商的请求/响应格式转换为内部统一协议 |
| 模型路由 | 根据模型名、渠道权重、业务线配置选择实际服务商 |
| 号池管理 | 维护多个 Key/Token 的健康状态,自动剔除异常、自动切换 |
| 配额控制 | 对每个 Key、每个业务线做限流和费用统计 |
| 缓存加速 | 对可复用结果、语义相似请求、Token 级缓存做分层处理 |
| 稳定性保障 | 熔断、降级、重试、超时控制,保证对公服务不中断 |
一个站搞定全模型 AI,并不是把所有模型装进一个进程里,而是让所有模型通过同一个入口对外提供标准化服务。上游业务不需要关心你接的是哪家供应商,也不需要关心 Key 是否还有余额,它只需要说“我要什么模型,传什么参数”。
1.3 适用场景与读者范围
这套设计适合以下场景:
- 企业内部搭建统一的 AI 能力平台,供多个业务线使用。
- 开发 AI 聚合站、AI 助手类产品,需要同时支持对话和生图。
- 需要高可用保障,不能因为某个供应商限流就导致线上故障。
- 希望在大模型 API 调用成本上做优化,通过缓存降低重复消费。
如果你是后端开发、架构师或独立开发者,读完这篇文章可以掌握一套从网关设计、号池调度到缓存治理的完整落地思路。下面我们先从整体架构开始。
2. 总体架构与模块拆分
2.1 系统整体分层
我们最终落地的系统分为五层,每层职责单一,不跨层调用:
客户端 / 业务后端 ↓ 接入网关层(统一API入口、认证、限流) ↓ 路由调度层(模型路由、号池选择、重试) ↓ 核心服务层(对话服务、生图服务、任务队列) ↓ 缓存层(Redis Cluster + 本地缓存) ↓ 模型供应商适配层(OpenAI、Claude、SD、MJ 等)接入网关层负责所有外部请求的统一认证、参数校验和基础限流。路由调度层是核心,它决定这次请求应该打到哪个供应商、使用哪个 Key、是否先查缓存。核心服务层把对话和生图拆成独立服务,避免互相影响。缓存层采用 Redis 集群作为主缓存,热点场景再用本地缓存兜底。最底层是供应商适配层,不同服务商通过 SPI 方式接入,新增渠道不需要改动上层代码。
2.2 模块职责清单
从工程实现角度,我们把代码拆成了下面几个模块:
| 模块 | 职责 | 关键点 |
|---|---|---|
| gateway-api | 对外 API 接口层 | 统一协议、参数校验、鉴权 |
| gateway-router | 模型路由与负载均衡 | 权重、按模型名路由 |
| account-pool | 号池管理 | Key 健康检查、自动剔除、自动恢复 |
| chat-service | 对话服务 | 流式输出、上下文管理 |
| image-service | 生图服务 | 异步任务、结果查询、图生图 |
| cache-core | 缓存核心 | 多级缓存、命中率统计、失效策略 |
| adapter-openai | OpenAI 风格适配器 | 兼容开源模型、兼容服务商中转 |
| adapter-image | 图片供应商适配器 | SD、MJ 等渠道接入 |
这个拆分的好处是每个团队可以并行开发,也方便后续扩展新供应商。下面我们直接进入环境准备和代码实现环节。
3. 环境准备与版本选型
3.1 运行环境
本文示例以 Java 17 + Spring Boot 3.x 为基础,这也是目前企业级 AI 网关项目比较常用的组合。如果你用 Python FastAPI 或 Go,核心设计思路同样适用,只是代码示例需要按语言迁移。
- 操作系统:Linux(CentOS 7+/Ubuntu 20.04+),本地开发可 Windows/macOS。
- JDK:17 及以上。
- 构建工具:Maven 3.8+。
- 中间件:Redis 6.x 以上,建议使用 Redis Cluster。
- 消息队列:RabbitMQ 或 RocketMQ,用于生图异步任务。
- 数据库:MySQL 8.x,用于保存账号池、费用记录、任务状态。
版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示配置思路。如果你的 Spring Boot 版本不同,个别注解和依赖坐标需要按官方文档更新。
3.2 项目基础依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>com.alibaba.fastjson2</groupId> <artifactId>fastjson2</artifactId> <version>2.0.48</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>WebFlux 主要用于生图服务的异步任务推送和 SSE 流式对话转发。实际项目中如果不想引入 WebFlux,也可以用 Spring MVC 的异步请求 + SseEmitter,效果接近。
3.3 项目基础配置
server: port: 8080 spring: application: name: ai-gateway data: redis: cluster: nodes: 10.0.0.11:6379,10.0.0.12:6379,10.0.0.13:6379 timeout: 3000ms lettuce: pool: max-active: 50 max-idle: 20 min-idle: 5 gateway: # 统一请求前缀,用于区分业务线 prefix: /api/v1 # 默认超时时间 timeout: 60s # 生图任务超时时间 image-timeout: 120sRedis 使用 Lettuce 连接池,超时时间不要设置太长,避免某个 Redis 节点抖动导致请求线程堆积。后续缓存治理中我们会专门处理超时和降级。
4. 核心模块一:对话服务设计与实现
4.1 统一对话协议
对外暴露的对话接口不直接透传供应商参数,而是封装成内部协议。前端只需要传模型名、消息列表和参数即可:
public class ChatRequest { private String model; // 模型标识,如 gpt-4o、claude-3-5-sonnet、qwen-max private List<Message> messages; // 对话消息列表 private Double temperature; // 温度参数 private Integer maxTokens; // 最大生成 token 数 private Boolean stream; // 是否流式输出 } public class Message { private String role; // system / user / assistant private String content; }4.2 路由与号池选择
收到请求后,路由层先根据 model 找到对应的渠道配置,再从该渠道下的号池中选择一个健康 Key。选择策略我们默认使用加权随机,权重在渠道管理后台配置。
@Service public class ChatRouter { @Resource private AccountPoolManager accountPoolManager; public ChannelAccount selectAccount(String model) { // 根据模型名获取可用渠道 List<String> channelIds = channelConfigService.getChannelsByModel(model); // 从号池中按权重选一个可用账号 return accountPoolManager.selectAccount(channelIds); } }号池选择的关键不是随机,而是必须在选号时判断账号当前是否可用、是否触发限流、是否处于冷却期。我们稍后会单开一节讲号池调度。
4.3 流式对话转发
对话接口最大的特点就是流式输出。用户发出的请求需要以 SSE(Server-Sent Events)方式把 token 逐个推给前端。如果用普通 HTTP 同步等待,用户会感觉“卡住”,体验很差。
@RestController @RequestMapping("/api/v1/chat") public class ChatController { private final ChatService chatService; public ChatController(ChatService chatService) { this.chatService = chatService; } @PostMapping("/completions") public SseEmitter completions(@RequestBody ChatRequest request) { SseEmitter emitter = new SseEmitter(0L); chatService.streamChat(request, emitter); return emitter; } }ChatService 内部调用供应商适配层,把上游返回的流式数据实时转发到 SseEmitter:
@Service public class ChatService { public void streamChat(ChatRequest request, SseEmitter emitter) { // 1. 先查缓存,如果命中直接返回缓存结果 String cacheKey = CacheKeyBuilder.buildChatKey(request); String cached = cacheService.get(cacheKey); if (StringUtils.hasText(cached)) { sendCacheResult(emitter, cached); return; } // 2. 选择渠道和账号 ChannelAccount account = router.selectAccount(request.getModel()); // 3. 调用供应商适配器,流式返回 adapterInvoker.streamChat(request, account, new StreamCallback() { @Override public void onToken(String token) { try { emitter.send(SseEmitter.event().data(token)); } catch (IOException e) { // 客户端断开,终止上游调用 adapterInvoker.cancel(request.getRequestId()); } } @Override public void onComplete(String fullText) { // 4. 完整结果写缓存 cacheService.set(cacheKey, fullText, CacheTtl.CHAT); emitter.complete(); } @Override public void onError(Throwable t) { emitter.completeWithError(t); } }); } }这里要注意 SseEmitter 的超时时间设置为 0L,表示不自动超时,由我们自己控制结束时机。客户端断开后必须调用上游取消接口,否则会造成供应商侧资源浪费,还可能产生费用。
4.4 对话缓存策略
对话请求是否适合缓存?这是很多团队纠结的点。我们实际落地中采用“混合策略”:
- 完全相同的请求直接命中缓存,不再调用供应商。
- system 相同、user 内容语义相似但没有完全相同文本的,不做强缓存,只做提示词优化。
- 流式请求边生成边返回,但完整结果会缓存一份,后面对相同请求再次访问时直接返回。
为了实现“相同请求”的缓存命中,我们需要对请求做标准化排序,避免因为 messages 数组顺序不同导致缓存不命中。
public class CacheKeyBuilder { public static String buildChatKey(ChatRequest request) { // 模型名小写 + 消息序列化 + 参数序列化 String model = request.getModel().toLowerCase(); String messagesJson = JSON.toJSONString(request.getMessages()); String param = request.getTemperature() + "_" + request.getMaxTokens(); String raw = model + "|" + messagesJson + "|" + param; // 使用 SHA-256 压缩 key 长度 return DigestUtils.sha256Hex(raw); } }这样设计后,相同请求第二次访问的延迟从秒级降到了毫秒级。后面缓存治理部分还会详细讲命中率监控。
5. 核心模块二:生图服务设计与实现
5.1 生图与对话的区别
生图服务和对话服务有一个本质区别:生图是异步任务。文生图模型通常需要几十秒甚至更久,HTTP 长连接直接等待不现实。我们的方案是任务化:提交生图任务后立即返回 taskId,前端轮询任务状态,生成完成后通过通知或轮询拿到图片 URL。
5.2 异步任务流程
@Service public class ImageTaskService { public ImageTask submit(ImageGenRequest request) { ImageTask task = new ImageTask(); task.setTaskId(UUID.randomUUID().toString().replace("-", "")); task.setStatus(TaskStatus.PENDING); task.setModel(request.getModel()); task.setPrompt(request.getPrompt()); task.setCreateTime(LocalDateTime.now()); // 保存任务状态 imageTaskMapper.insert(task); // 发送到消息队列,异步执行 rabbitTemplate.convertAndSend("ai.image.task", JSON.toJSONString(task)); return task; } }消费者收到任务后,先查结果缓存,如果之前生成过完全相同的 prompt,直接复用图片 URL,不再调用供应商接口。这一点对生图成本控制非常重要。
@Component public class ImageTaskConsumer { @RabbitListener(queues = "ai.image.task") public void onMessage(String message) { ImageTask task = JSON.parseObject(message, ImageTask.class); // 1. 查结果缓存 String cacheKey = "img:result:" + DigestUtils.sha256Hex(task.getPrompt() + "|" + task.getModel()); String cachedUrl = cacheService.get(cacheKey); if (StringUtils.hasText(cachedUrl)) { finishTask(task, cachedUrl); return; } // 2. 选择账号并调用生图供应商 ChannelAccount account = accountPoolManager.selectAccount(task.getModel()); String imageUrl = imageProvider.generate(task, account); // 3. 写缓存,TTL 根据业务需要设置 cacheService.set(cacheKey, imageUrl, CacheTtl.IMAGE); finishTask(task, imageUrl); } }生图缓存不仅缓存最终图片 URL,还可以对供应商的“中间结果”做缓存。比如 SD 的放大参数、修复参数如果变化不大,可以按 prompt+参数 组合做结果级缓存,减少重复计算成本。
5.3 生图模型路由
生图模型路由和对话路由类似,但更需要注意的是图片模型之间的差异很大:
- SD 开源模型适合私有化部署,成本低但画质需要调优。
- Midjourney 类渠道画质好,但需要通过第三方接口中转,形态多样。
- ComfyUI 工作流适合复杂图生图、局部重绘等场景。
所以生图路由我们增加了“能力标签”概念。每个模型配置里声明支持的能力,路由时先过滤掉不满足能力的渠道,再按权重选择。比如用户传了 reference_image 参数,路由时只选择支持图生图的渠道,避免请求发过去后报参数错误。
gateway: channels: - id: sd-local type: image capabilities: [ txt2img, img2img, inpaint ] weight: 10 - id: mj-proxy type: image capabilities: [ txt2img, upscale ] weight: 56. 核心模块三:Pro 号池与调度策略
6.1 号池的本质
很多文章把“号池”描述得很神秘,其实它就是一个账号/Key 的集合管理模块。在 AI 网关里,一个“号”代表一个可调用的凭证,可能是 OpenAI 的 API Key,也可能是某个聚合平台的 Token。号池要解决的核心问题是:多个凭证如何分配、如何保证高可用、如何避免单个凭证超限导致整个服务不可用。
我们提到的 Pro 号,本质上是一个配额更高、稳定性更好的凭证。号池服务负责在所有可用凭证之间做调度,并自动处理故障切换。
6.2 账号状态机
每个账号都有状态流转:
AVAILABLE(可用) ↓ 调用失败/触发限流 COOLING(冷却中) ↓ 冷却时间结束 AVAILABLE(可用) ↓ 连续失败达到阈值 DISABLED(禁用) ↓ 人工干预/定时探活成功 AVAILABLE(可用)6.3 核心实现
@Service public class AccountPoolManager { @Resource private List<AccountHealthChecker> healthCheckers; public ChannelAccount selectAccount(String model) { // 获取该模型下所有可用账号 List<ChannelAccount> accounts = accountRepository.findAvailableByModel(model); if (CollectionUtils.isEmpty(accounts)) { throw new BizException("当前模型无可用的渠道账号"); } // 过滤掉冷却中和禁用的账号 List<ChannelAccount> candidates = accounts.stream() .filter(a -> a.getStatus() == AccountStatus.AVAILABLE) .collect(Collectors.toList()); // 加权随机选择一个 return weightedRandom(candidates); } public void reportFailure(String accountId, String errorCode) { ChannelAccount account = accountRepository.findById(accountId); if (isRateLimitError(errorCode)) { // 触发了限流,进入冷却状态 account.setStatus(AccountStatus.COOLING); account.setCoolingUntil(LocalDateTime.now().plusMinutes(5)); accountRepository.update(account); } else if (account.getFailCount() >= 5) { // 连续失败5次,禁用账号 account.setStatus(AccountStatus.DISABLED); accountRepository.update(account); } else { account.setFailCount(account.getFailCount() + 1); accountRepository.update(account); } } }这里有个细节:账号进入冷却状态后不能只等时间结束,应该有一个异步探活线程主动检测账号是否恢复。恢复的判断方式是发一个最小的测试请求,比如对话模型发送“ping”,生图模型发送一个极小的生成任务。探活成功则立即恢复 AVAILABLE,避免可用账号白白浪费。
6.4 号池配额与限流
号池在对外服务时还要做到“配额隔离”。比如 A 业务线使用了 100 万 token,B 业务线只能使用 10 万 token,两者不能互相挤占。我们在网关层按业务线+账号维度做了令牌桶限流:
@Component public class QuotaLimitService { // key: bizId:accountId, value: 剩余配额 public boolean tryAcquire(String bizId, String accountId, int tokens) { String key = "quota:" + bizId + ":" + accountId; Long remain = redisTemplate.opsForValue().decrement(key, tokens); if (remain == null || remain < 0) { // 配额不足,回滚 redisTemplate.opsForValue().increment(key, tokens); return false; } return true; } }配额扣减和回滚不是原子的,严格场景下需要用 Lua 脚本保证原子性。实际项目里我们使用 Lua 脚本完成“检查额度、扣减、回滚”逻辑。
7. 缓存稳定性:把命中率做到 95%+ 的治理实践
7.1 缓存分层与 key 设计
缓存是这套系统降本增效的核心。我们的缓存分为三层:
| 层级 | 存储位置 | 适用场景 | 命中耗时 |
|---|---|---|---|
| L1 本地缓存 | JVM 内存(Caffeine) | 热点 Prompt、账号状态 | < 1ms |
| L2 Redis 缓存 | Redis Cluster | 对话完整结果、生图结果、限流计数 | 1-5ms |
| L3 数据库/对象存储 | MySQL / OSS | 长期保存的任务记录、图片 URL | 10ms+ |
Key 设计遵循“业务域:对象:标识:版本”的规则,例如:
chat:result:sha256(标准化请求) img:result:sha256(prompt|model|params) account:status:acct_123456 quota:bizA:acct_123456统一前缀方便在 Redis 中按业务域排查问题,也方便后续做数据迁移。
7.2 Token 缓存命中和不命中
在大模型调用场景里,Token 级缓存是一个容易被忽略但收益很高的优化点。对于 Prompt 中固定不变的前缀部分(例如系统提示词、上下文模板),它们每次请求都要发送给模型,实际上这部分 token 在供应商侧是可以缓存的。
在我们自建网关里,对 Token 的缓存命中做了两层处理:
- 缓存 Key 复用:上下文前缀相同、只有尾部问题不同的请求,尽量复用前缀的拼接结果,减少重复处理。
- 完全缓存的请求不消耗 Token:如果请求和之前完全一样,直接从缓存返回,不调用供应商,也就不产生 Token 费用。
这里的命中和不命中,直接影响成本与延迟。命中缓存时请求耗时平均在 50ms 以内;不命中时加上模型推理时间,通常需要 1-10 秒。所以我们做了一个“Token 缓存命中率看板”,按模型、按业务线分别展示命中率,低于阈值会触发告警。
7.3 缓存穿透、击穿、雪崩治理
命中率做到 95% 不算难,难的是高命中率下依旧稳定。这里必须处理三个经典问题:
缓存穿透:反复请求一个一定不存在的结果,每次都会打到供应商层。解决方式是布隆过滤器 + 空值缓存。
public String getChatCache(String key) { String value = cacheService.get(key); if (StringUtils.hasText(value)) { return value; } // 空值缓存,防止穿透 if (cacheService.hasEmptyMark(key)) { return null; } return null; }缓存击穿:某个热点 key 过期瞬间,大量请求同时打到上游。解决方式是互斥锁重建缓存,只允许一个线程去调用供应商,其他线程等待。
public String getOrLoad(String key, Supplier<String> loader) { String value = cacheService.get(key); if (StringUtils.hasText(value)) { return value; } String lockKey = "lock:" + key; boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", Duration.ofSeconds(5)); if (locked) { try { value = loader.get(); cacheService.set(key, value, CacheTtl.DEFAULT); return value; } finally { redisTemplate.delete(lockKey); } } else { // 等待后重试 try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return getOrLoad(key, loader); } }缓存雪崩:大量 key 在同一时间过期。解决方式是 TTL 增加随机偏移量,让过期时间均匀分布。
public class CacheTtl { public static Duration randomTtl(Duration base) { long randomSeconds = ThreadLocalRandom.current().nextLong(0, 60); return base.plusSeconds(randomSeconds); } }7.4 缓存命中率监控与告警
没有监控的命中率没有意义。我们在网关层封装了统一的缓存访问切面,自动记录每个 key 的命中状态:
@Aspect @Component public class CacheMonitorAspect { private final MeterRegistry meterRegistry; @Around("@annotation(cacheable)") public Object around(ProceedingJoinPoint pjp, Cacheable cacheable) throws Throwable { boolean hit = true; long start = System.currentTimeMillis(); try { return pjp.proceed(); } catch (CacheMissException e) { hit = false; throw e; } finally { long cost = System.currentTimeMillis() - start; meterRegistry.counter("cache.hit.total", "cache", cacheable.cacheName(), "hit", String.valueOf(hit)) .increment(); meterRegistry.timer("cache.cost", "cache", cacheable.cacheName()) .record(Duration.ofMillis(cost)); } } }通过这些指标,我们在 Grafana 上配置了“缓存命中率趋势图”和“缓存耗时分布图”。一旦某个模型的命中率连续 5 分钟低于 90%,就推送告警到企业微信/钉钉,运维需要立刻排查是不是 key 设计变了或者缓存被误清理了。
7.5 缓存稳定性保障
缓存系统稳定性比命中率优先。Redis 万一抖动,不能拖垮整个 AI 网关。我们做了以下降级策略:
- Redis 读取超时时间设置为 200ms,超时直接跳过缓存,调用上游。
- Redis 不可用时,本地 Caffeine 缓存继续工作,保护热点数据。
- 本地缓存容量限制,避免 OOM。Caffeine 配置最大条目数和空闲淘汰。
@Configuration public class LocalCacheConfig { @Bean public Cache<String, String> localCache() { return Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(Duration.ofMinutes(30)) .recordStats() .build(); } }对公服务最怕的是下游模型供应商正常,业务却因为缓存中间件挂掉而不可用。所以缓存必须设计为“旁路依赖”:缓存不可用时,直接直连供应商,保证核心业务可用。
8. 常见问题与排查思路
8.1 常见问题表格
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 不同用户请求相同 Prompt,缓存命中率仍然很低 | key 里包含了用户 ID、时间戳等无关参数 | 标准化 key,只保留模型、消息、参数维度 |
| 流式对话出现乱序或丢失 token | SseEmitter 并发发送导致 | 对每个 emitter 加锁,使用阻塞队列 |
| 生图任务一直 PENDING,消费者不消费 | RabbitMQ 队列堆积或消费者异常 | 检查消费者日志,查看队列积压数量 |
| 账号被限流后,服务恢复时间过长 | 只依赖冷却时间恢复 | 增加异步探活线程,主动探测并恢复 |
| Redis 抖动导致上游调用高延迟 | 缓存超时时间设置过长 | 缩短 cache timeout,加入本地缓存降级 |
| token 缓存明明配置了却不生效 | 请求体 messages 顺序不一致 | 标准化消息顺序,按 role+content 排序 |
| 生图结果缓存命中但图片 URL 失效 | 对象存储过期清理 | 生图 URL 使用 CDN 绝对地址并设置合理 TTL |
| 对公接口偶发 5xx | 账号切换时没有做重试 | 增加幂等重试,切换备用渠道 |
8.2 排查清单
如果你遇到缓存命中率上不去的问题,按下面顺序排查:
- 确认业务请求是否真的存在重复。如果用户每次都输入不同 Prompt,缓存命中率低是正常的。
- 检查缓存 key 的生成逻辑,排除用户 ID、时间戳等干扰项。
- 确认 Redis 缓存是否被提前清理,检查 TTL 设置是否过短。
- 看监控面板,确认是 L1 未命中还是 L2 未命中。
- 抓取几个典型请求,对比标准化后的 key 是否真的相同。
如果遇到渠道账号频繁被限流的问题,优先检查号池是否均衡分配了请求,而不是一直打到同一个账号上。权重随机算法要配合账号的实时健康状态,避免把请求路由到已经快触达限额的账号。
9. 最佳实践与工程建议
9.1 配置管理
AI 网关涉及大量渠道配置、模型参数、号池信息,这些都属于敏感配置。建议:
- 使用 Nacos 或 Apollo 做配置中心,模型权重、渠道启停支持动态刷新。
- 账号密钥加密存储,禁止明文保存在数据库或配置文件里。
- 配置变更要走发布流程,生产环境需要审批和回滚方案。
- 多环境隔离:dev、test、prod 使用不同的配置命名空间。
9.2 日志与链路追踪
每次请求都要能追踪到完整的调用链:
- 生成 requestId,在网关入口注入。
- 日志中记录模型、渠道、账号、耗时、token 消耗、缓存命中状态。
- 使用 MDC 把 requestId 写入日志上下文。
- 对公服务建议接入 SkyWalking 或 Zipkin,拿到跨服务调用链。
9.3 安全边界
- 网关层必须做身份认证,不能裸暴露大模型 API。
- 对不同业务线做配额隔离,防止某个业务线无限调用拖垮整体服务。
- Prompt 内容做合规过滤,涉及敏感信息需要拦截。
- 生图内容也需要做审核,不能只依赖模型供应商。
9.4 生产环境注意
- 发布时先灰度一个渠道,确认稳定再全量。
- 账号池容量要预留 20% 以上余量,避免批量账号失效时无号可用。
- 缓存命中率告警和上游供应商故障告警要分开,避免误报。
- 定期做压测,验证高并发下的缓存策略和号池切换能力。
9.5 成本治理
一个站搞定全模型 AI,很重要的价值是成本可控。我们建议对每个业务线、每个模型、每个账号分别做费用统计,月度汇总后分析成本构成。缓存命中率与成本之间有直接关系:每提升 5% 的命中率,重复请求费用就能减少一大块。
10. 总结
围绕“一个站搞定全模型 AI”,本文从工程角度拆解了 AI 网关的核心模块:统一对话服务、异步生图服务、Pro 号池调度和多层缓存治理。关键点可以归纳为三条:
第一,协议统一是基础。上游业务只跟你的网关打交道,不直接对接任何模型供应商,这样后续新增模型、切换渠道对业务无感。
第二,号池调度是稳定性的保障。账号健康检查、自动冷却、自动恢复、加权随机分配,这些机制保证了单个账号限流或失效时系统不受影响。
第三,缓存命中率是成本和性能的胜负手。通过标准化请求 key、分层缓存、防穿透防击穿防雪崩、命中率监控,完全可以把重复请求的缓存命中率稳定在 95% 以上,同时保证缓存组件故障时核心链路不中断。
下一步建议你先从对话模块入手,把统一协议和号池调度跑通,再逐步接入生图和缓存治理。关于多级缓存、流式转发、账号探活这几个模块,都可以单独深入优化。实际项目里踩过坑的同学会发现,网关层做得好不好,直接决定后面模型越接越多时是越来越顺还是越来越乱。