做 AI 应用落地,最让团队头疼的往往不是 Prompt 写不好,也不是模型选型纠结,而是工程侧“多模型接入”这件事本身。今天接 OpenAI,明天接 Claude,后天客户要求加一个生图能力,又是 Midjourney 和 Stable Diffusion 两套协议。每个平台的鉴权方式不一样,计费逻辑不一样,限流策略不一样,连错误码都各自为政。更难受的是,账号和成本散落在不同后台,根本没法统一管控。
这个项目的标题很直白:一个站搞定全模型 AI:对话、生图、Pro 号池,95%+ 缓存稳定可对公。它想表达的不是“我写了个工具”,而是一个可以稳定对外提供服务的 AI 聚合网关系统。这类系统在 2025 年的技术圈里已经不是新鲜事,但真正能跑通、能对公、能把缓存命中率做到 95% 以上的,依然值得拆开讲讲。
读完这篇文章,你会理解这套系统的整体架构、对话与生图能力的接入方式、Pro 号池的调度原理,以及缓存命中率是怎么从“能用”做到“稳定”的。更重要的是,你会知道哪些坑是新手必踩的,哪些设计决定了一旦做错,后面就再也拉不回来。
1. 这篇文章真正要解决的问题
先说一个判断:多模型 AI 聚合网关,表面上是个 API 转发工具,本质上是个成本控制和资源调度系统。
如果只是写一个转发接口,把请求从 A 平台转发到 B 平台,那这个系统没有任何门槛。但真实场景里,模型接入方要面对的痛点远不止转发:
第一,模型渠道分散。对话用 GPT-4o,生图用 Midjourney,视频生成可能要另接一家。每个平台的 SDK、鉴权、参数格式都不一样,业务方没能力也没精力逐个适配,就需要一个统一的入口把协议差异吞掉。
第二,成本波动不可控。企业客户和个人开发者不一样。个人用 API 是随用随充,企业要的是预算可控、账单清晰。Pro 号池的玩法本质上就是通过账号池化提高资源利用率,降低整体调用成本。
第三,稳定性要求高。一旦系统是“可对公”的,意味着它要接企业客户,要签合同,要走对公转账,那 SLA 就不能是“尽力而为”。对公客户会问你缓存命中率多少、失败重试机制是什么、账号异常了能不能自动切换。
第四,权限和计费混乱。一个平台多个业务方接入,谁调了多少次、用了哪些模型、烧了多少额度,这必须要有清晰的链路追踪和计量能力。
所以这篇文章不是要教你“调一个 AI 接口”,而是要拆解一个面向对公服务的 AI 网关,需要具备哪几块核心能力。文章适合两类读者:一是正在做 AI 应用、想自己搭一套统一接入层的后端工程师;二是技术负责人,想评估这类系统靠不靠谱、技术难点在哪、值不值得自己团队投入。
2. 核心架构与关键概念
先建立一个整体认知。这套系统的架构可以概括为“一个入口,三层核心”。
一个入口指的是统一的 API 网关入口。业务方只需要对接这一个地址,不需要关心背后接的是哪家模型。
三层核心分别是:统一模型路由层、Pro 号池调度层、多级缓存层。
2.1 统一模型路由层
路由层解决的是“请求该发到哪”的问题。用户传一个 model 参数,路由层根据模型名、渠道优先级、当前渠道的健康状态、账号配额,决定把请求转发到哪个上游渠道。
举个例子,用户请求model=gpt-4o,路由层拿到这个参数后,先去渠道表查有哪些渠道支持 gpt-4o。假设有官方 API 渠道和 Pro 号池渠道,路由层会看当前哪个渠道的可用配额多、哪个渠道的响应延迟低,然后选择一个上游。
这个逻辑说起来简单,落地时最容易被忽视的是渠道健康度管理。一个渠道连续失败三次,就应该被降级;账号返回 401,要立刻冻结;渠道超时了,是重试还是切换到备用渠道,这需要有一整套策略。
2.2 Pro 号池调度层
号池是很多 AI 网关区别于普通 API 封装的核心。
不要把 Pro 号池想得太复杂。它的本质是:将多个会员账号(或者说多组凭证)池化,按一定策略分发给内部业务或下游客户使用。这样做的目的很现实:企业采购官方 API 的预算可能很高,而平台会员订阅方式的包月成本相对可控,通过合理的调度和并发隔离,能显著摊薄单次调用成本。
但号池化会引入几个新问题:
- 多个账号的资源怎么统计?谁用了多少配额?
- 账号之间的隔离怎么做?一个账号触发限流,不能影响其他账号。
- 账号失效如何发现?不能等用户反馈说“怎么不能用了”才发现。
所以在号池层,必须有账号健康检查、并发控制、自动切换、失败转移这四套机制。
2.3 多级缓存层
这个项目标题里最吸引技术人的,应该是“95%+ 缓存稳定”。这既是技术看点,也是面向对公客户时最重要的卖点。
这里的缓存体系不是单指某个 Redis 缓存,而是由请求级缓存、语义缓存、响应缓存、图片文件缓存组成的多级缓存体系。后面我会专门用一章展开讲。
3. Pro 号池设计:账号池化与智能调度
很多人一听到号池,第一反应是“这不就是卖别人账号吗”。这是对号池的误解。一个合规、可对公的号池系统,前提是所有账号都来自正规渠道,来源清晰、授权明确、可追溯。在这个前提下,号池系统的技术价值在于统一的调度与配额管理。
3.1 账号模型设计
从工程角度看,每个上游账号可以被抽象为一条记录:
- 所属平台(OpenAI、Midjourney、Claude 等)
- 账号标识与凭证信息
- 当前状态(可用、繁忙、冻结、失效)
- 今日已用额度与配额上限
- 最近健康检查时间与连续失败次数
- 并发标记(当前正在执行的请求数)
用数据库表存储账号信息,用 Redis 记录账号的实时并发数和状态,这是最基础的做法。账号池的调度决策不能每次查数据库,必须放到 Redis 里做,不然高频调用下数据库会被打爆。
3.2 调度策略
号池的调度策略有很多种,实际项目中常见的是权值分配法和最少使用法。
权值分配法是指给每个账号设定一个权重系数。比如新账号权重高,优先被使用;历史表现差的账号权重低,只在高峰期兜底。最少使用法则是每次选择当前并发数最低或者累计调用次数最少的账号,实现“雨露均沾”。
更实用的做法是结合健康状态动态调整。每间隔一段时间做一次主动健康检查,比如向上游发送一个极小成本的测试请求,确认凭证有效、配额充足。检查不通过的账号自动摘除,检查恢复后再重新加入池子。
3.3 失败转移与熔断
这部分直接决定网关在真实环境里的稳定性。
一个请求被分配到账号 A,调用失败。此时调度层应该立刻把账号 A 的失败次数加一,然后重新选择账号 B 重试。如果账号 A 在短时间内连续失败超过阈值,要自动将其置为“冻结”状态。
同时要引入熔断机制。如果整个渠道都出现问题,比如上游接口大面积超时,就不应该再往这个渠道发送新请求,而是直接返回缓存结果,或者返回一个明确的错误提示,避免请求堆积导致雪崩。
这段设计里的关键判断是:号池的正确性不取决于单次调用成功,而取决于整体可用性。单次失败不可怕,可怕的是失败后没有自动切换和恢复机制。
4. 缓存体系:95%+ 命中率是怎么做到的
缓存是这套系统里技术含量最高的部分。95% 的缓存命中率不是拍脑袋定的指标,它意味着大量用户请求根本不需要到达上游模型服务,直接从缓存就能拿到结果,这对成本控制和响应速度都是有决定意义的。
4.1 请求级缓存的粒度设计
先看最常见的场景:同一个业务方反复请求同一个 Prompt,要求同样的参数。比如客户端的健康检查请求、固定的系统 Prompt 摘要请求、或者是生图场景里被反复执行的相同图片描述。
对这种请求,直接用模型名 + Prompt + 参数哈希作为 Key,把完整响应存到 Redis,TTL 根据业务需要设置为数分钟到数小时。请求来了先查缓存,命中就直接返回,完全不触及上游模型。
但请求级缓存有一个效率陷阱:Prompt 只要多了一个空格、多了一个换行符,哈希就变了,缓存就永远无法命中。所以必须做归一化处理,把多余的空白符、大小写差异、参数顺序差异都消掉,再来计算哈希。
4.2 语义缓存:AI 场景下的近似命中
请求级缓存做的是“精确匹配”,语义缓存做的是“近似匹配”。
什么是语义缓存?还是用真实的场景说明。企业客户的客服机器人,用户可能问“你们怎么收费”和“请问你们的收费标准是什么”。这两句话表面不同,但意图几乎一致。如果每次都用不同的话术打到上游模型,既浪费成本,也没必要。
语义缓存的实现通常分两条路:
- 先用 Embedding 模型把用户输入向量化,存入向量数据库。
- 新请求进来时,先向量化,再做相似度检索。如果相似度超过阈值,比如 0.95,直接返回已有答案。
第二种做法成本更低:用关键词归一化加上简单的意图分类规则。比如“收费”“价格”“多少钱”这几个词出现,就归入价格类问题,命中同一个缓存答案。
语义缓存的价值在于,它不是用“等值判断”而是用“语义判断”来复用结果,这是 AI 网关和传统 API 网关最大的不一样。
4.3 KV Cache 与多轮会话缓存
热词里有“KV 缓存”和“token 缓存命中”,这些概念最初来自大模型推理侧的优化。大模型生成下一个 token 时,需要把上文所有 token 的 Key 和 Value 都重新算一遍吗?不需要。KV Cache 就是把已经算好的 Key 和 Value 缓存下来,复用计算结果,避免重复计算。
在网关这一层,没法直接控制上游模型的 KV Cache,但可以做一个等价的事:会话级缓存。
对多轮对话,我们可以把完整的会话上下文序列化后存储起来。用户带着会话 ID 发起新请求时,网关先去查这个会话的历史摘要和结果。如果用户只是对上一轮的回复做了小幅修改,那这一轮的完整响应完全可以从会话缓存中拼接得到,不需要重新请求上游模型。
一个容易忽略的细节是:会话缓存必须和业务侧约定好状态同步机制。比如用户主动清空会话、切换模型、或者管理员重置会话,网关需要收到明确指令并清除相关缓存,否则会出现“用户以为已经开了新对话,系统却还在返回旧记忆”的诡异问题。
4.4 生图缓存的特殊处理
对话结果的缓存相对简单,生图缓存更考验设计。
生图请求的参数很多,模型、Prompt、反向提示词、宽高、步数、采样器、种子、CFG Scale。同一个 Prompt,用不同的种子,生成的图完全不一样。
所以生图缓存的策略要分两类:
- 如果业务方要求固定的种子,那这个请求是确定性的,相同参数直接返回历史图片,命中率会非常高。
- 如果业务方不固定种子,那每次生成的图都是新的,缓存的意义只在于“同 Prompt 同参数”的完整复用,实际有效命中率会低很多。
因此,生图场景的缓存一般分为两层:一层是任务级缓存(对应图片文件的存储和复用),一层是结果元数据缓存(对应生成任务的状态和图片地址)。图片文件用对象存储或文件服务器保存,Redis 里只记录文件路径和请求参数哈希。
4.5 缓存一致性与失效策略
缓存命中率做到 95% 不难,难的是不会命中错的结果。
最典型的场景:模型版本升级了,旧缓存没清,用户还在拿旧模型的答案。这在一个对公系统里是不可接受的。
解决方案是缓存版本号机制。每次模型渠道的版本变化,就在 Redis 里递增一个全局版本号,所有缓存 Key 生成时带上这个版本号。版本号一变,旧版本缓存自然失效。注意,这里不能简单用flushall清库,那会把正在服务的其他缓存也干掉,必须用带版本的 Key 设计。
同时要给缓存设置合理的 TTL,并区分两种失效逻辑:过期失效和主动失效。主动失效的场景包括:管理员手动刷新、发生错误结果回滚、对话被用户删除等。
5. 对话与生图能力接入:协议适配层
对外要“一个站搞定全模型 AI”,对内就要有一套高效的协议适配层。适配层的设计思路是:上游模型千差万别,但下游业务方看到的应该是一致的接口。
5.1 对话接口的协议抽象
定义一个统一的对话请求结构:
- 模型名称
- 消息列表(角色 + 内容)
- 温度、最大 token 数等采样参数
- 会话 ID(可选)
网关拿到这个结构后,根据模型名称路由到对应适配器。每个适配器负责把统一的请求结构翻译成上游平台的 SDK 调用格式,再把上游返回的响应翻译成统一结构返回给下游。
这个抽象的价值在接入新模型时体现得最明显。新模型的 SDK 和旧的完全不一样,但因为有适配层,业务方无感知,只需要网关侧新增一个适配器,不需要改任何业务代码。
5.2 生图任务的异步化
生图和对话最大的区别在于:对话是同步交互,生图是异步任务。
一张高质量的生图通常需要数十秒甚至更久,如果网关同步等待,请求会长时间占用连接,体验很差。所以生图接口必须走异步任务模式:
- 客户端提交生图请求。
- 网关创建任务,返回任务 ID。
- 网关把任务投入队列,异步调用上游生图渠道。
- 客户端通过任务 ID 轮询任务状态,或等待回调通知。
- 任务完成后,客户端拿到图片地址。
这个设计里,任务队列的可靠性是关键。有人用 Redis 的 List 结构实现简单队列,但这在生产环境有明显的丢失风险。更稳妥的做法是用带消息确认的队列组件,或者用 Redis Stream,配合消费者的 ACK 机制。
5.3 统一错误码设计
多模型接入后,最难搞的不是功能,而是错误码。
上游平台返回限流、鉴权失败、余额不足、参数非法,不同平台的错误码格式不一样,网关必须把这些异常统一映射成标准错误码返回给下游。比如上游 401 统一映射为“凭证无效”,429 统一映射为“触发限流”,上游参数报错统一映射为“参数校验失败”。
错误码统一后,对公客户对接时只需要关注一套错误码表,运维排查时也不需要去翻不同平台的错误码规则。这个设计在前期看着不起眼,但到了接入第五个、第六个渠道时会发现极其重要。
6. 环境准备与项目结构
进入实操环节。下面的代码示例基于 Java Spring Boot 技术栈,Redis 做缓存和调度标记,MySQL 存账号和任务元数据。各组件具体版本请以实际项目为准,这里不写死版本号,重点演示通用思路。
6.1 基础依赖
核心依赖包括 Spring Web、Spring Data Redis、MyBatis-Plus(或 Spring Data JPA,按团队习惯选择)、对象存储 SDK、HTTP 客户端(OkHttp 或 WebClient)。
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>4.12.0</version> </dependency> </dependencies>6.2 项目结构
推荐按职责分包,而不是按技术类型分包:
com.example.aigateway ├── controller │ ├── ChatController.java │ └── ImageController.java ├── adapter │ ├── OpenAiChatAdapter.java │ ├── ClaudeChatAdapter.java │ └── SdImageAdapter.java ├── pool │ ├── AccountPoolManager.java │ └── AccountScheduler.java ├── cache │ ├── CacheKeyBuilder.java │ ├── RequestCacheManager.java │ └── SemanticCacheService.java ├── task │ ├── ImageTaskService.java │ └── TaskQueueConsumer.java └── common ├── ApiResponse.java └── ErrorCode.java6.3 核心配置文件
spring: application: name: ai-gateway redis: host: 127.0.0.1 port: 6379 database: 0 datasource: url: jdbc:mysql://127.0.0.1:3306/ai_gateway?useUnicode=true&characterEncoding=utf8 username: root password: change-me gateway: cache: enable: true default-ttl-seconds: 1800 version: 1 channel: openai: base-url: https://api.example.com timeout-seconds: 60 claude: base-url: https://api.example.com timeout-seconds: 60配置文件中,gateway.cache.version就是前面说的缓存版本号。模型渠道升级后,把这里的版本号加 1,重启后所有旧缓存自动不可命中。
7. 核心代码实现与运行验证
7.1 统一对话接口
这一层是业务方的入口,它的职责是做参数校验、读缓存、路由转发、异常映射。
// 文件路径:src/main/java/com/example/aigateway/controller/ChatController.java @RestController @RequestMapping("/api/v1/chat") public class ChatController { @Resource private ChatService chatService; @PostMapping("/completions") public ApiResponse<ChatCompletionResult> completions(@RequestBody ChatCompletionRequest request) { if (request == null || request.getModel() == null || request.getMessages() == null) { return ApiResponse.error(ErrorCode.PARAM_ERROR); } return ApiResponse.ok(chatService.completions(request)); } }注意这里的响应包装类ApiResponse,所有接口都返回统一结构,包含 code、message、data 三个字段。这个约定越早定越好,后面接对公客户时省很多事。
7.2 号池调度器
号池调度器是网关里最核心的组件之一。它的职责是按照策略选择一个可用的上游账号。
// 文件路径:src/main/java/com/example/aigateway/pool/AccountPoolManager.java @Component public class AccountPoolManager { @Resource private StringRedisTemplate stringRedisTemplate; private static final String ACCOUNT_KEY_PREFIX = "gateway:account:"; /** * 从号池中选择一个可用账号。 * 选择策略:优先选择未冻结、当前并发数最低的账号。 */ public AccountInfo selectAvailableAccount(String platform) { Set<String> keys = stringRedisTemplate.keys(ACCOUNT_KEY_PREFIX + platform + ":*"); if (keys == null || keys.isEmpty()) { throw new BizException(ErrorCode.NO_AVAILABLE_ACCOUNT); } AccountInfo selected = null; long minConcurrency = Long.MAX_VALUE; for (String key : keys) { String status = stringRedisTemplate.opsForHash().get(key, "status").toString(); if (!"ACTIVE".equals(status)) { continue; } String concurrencyStr = stringRedisTemplate.opsForHash().get(key, "concurrency").toString(); long concurrency = Long.parseLong(concurrencyStr); if (concurrency < minConcurrency) { minConcurrency = concurrency; selected = AccountInfo.fromMap(stringRedisTemplate.opsForHash().entries(key)); } } return selected; } }这段代码有几个可以优化的点,实际项目中会加上本地缓存避免每次都keys全量扫描,同时会把账号健康检查结果放在内存里,减少对 Redis 的依赖。但核心的“并发数最少优先”策略是符合真实实践的。
7.3 请求缓存管理器
请求缓存的实现,核心是 Key 的构建和命中判断。
// 文件路径:src/main/java/com/example/aigateway/cache/RequestCacheManager.java @Component public class RequestCacheManager { @Resource private StringRedisTemplate stringRedisTemplate; @Value("${gateway.cache.default-ttl-seconds:1800}") private long defaultTtlSeconds; /** * 构建缓存 Key,这里会带上缓存版本号,避免版本升级后命中旧数据。 */ public String buildCacheKey(String model, String normalizedPrompt, String paramsHash) { String version = stringRedisTemplate.opsForValue().get("gateway:cache:version"); return "gateway:cache:v" + version + ":" + model + ":" + normalizedPrompt + ":" + paramsHash; } public String getCache(String key) { return stringRedisTemplate.opsForValue().get(key); } public void putCache(String key, String value) { stringRedisTemplate.opsForValue().set(key, value, defaultTtlSeconds, TimeUnit.SECONDS); } }缓存版本号单独存一个 Redis Key。系统升级时,把版本号加 1,所有新写入的缓存 Key 都带上新版本号。旧版本号的缓存虽然还留在 Redis 里,但永远不会被读取,等 TTL 过期后自然清理掉。
7.4 生图异步任务
生图接口的异步实现,需要用任务表 + 队列来处理。
// 文件路径:src/main/java/com/example/aigateway/task/ImageTaskService.java @Service public class ImageTaskService { @Resource private StringRedisTemplate stringRedisTemplate; private static final String TASK_QUEUE_KEY = "gateway:image:queue"; private static final String TASK_STATUS_PREFIX = "gateway:image:task:"; public String submitImageTask(ImageGenerationRequest request) { String taskId = UUID.randomUUID().toString().replace("-", ""); ImageTask task = new ImageTask(); task.setTaskId(taskId); task.setStatus("PENDING"); task.setParams(request); stringRedisTemplate.opsForValue().set(TASK_STATUS_PREFIX + taskId, JSON.toJSONString(task)); stringRedisTemplate.opsForList().leftPush(TASK_QUEUE_KEY, taskId); return taskId; } public String getTaskStatus(String taskId) { String value = stringRedisTemplate.opsForValue().get(TASK_STATUS_PREFIX + taskId); return value; } }这是一个最简版本。生产环境里还需要考虑任务超时清理、失败重试、任务结果持久化到 MySQL、任务进度回调等功能。
7.5 运行验证
启动项目后,可以用 curl 验证对话接口是否正常工作。假设本地端口是 8080:
curl --location 'http://localhost:8080/api/v1/chat/completions' \ --header 'Content-Type: application/json' \ --data-raw '{ "model": "gpt-4o", "messages": [ {"role": "user", "content": "你好,介绍一下你自己"} ] }'如果一切正常,会返回统一结构的 JSON 响应。
验证缓存是否生效,可以连续请求两次相同的 Prompt,观察第二次请求的响应时间。如果第二次响应时间显著低于第一次,说明缓存命中生效。
生图接口的验证流程:
curl --location 'http://localhost:8080/api/v1/image/generations' \ --header 'Content-Type: application/json' \ --data-raw '{ "model": "sd-xl", "prompt": "a cat sitting on the table", "width": 1024, "height": 1024 }'返回的任务 ID,再通过/api/v1/image/tasks/{taskId}查询任务状态。
8. 缓存命中率与系统压测
95% 的缓存命中率不能嘴说,需要有一套可量化的统计口径。
8.1 命中率统计口径
网关侧需要记录两个数字:总请求数和缓存命中数。
建议在网关入口做一层拦截器,每个请求进来判断是否读取缓存。读取到缓存就记为命中,否则记为未命中。把这两个数据每隔一段时间写入 Redis 计数器,最终在监控面板中展示。
# 伪代码逻辑 if (cacheManager.getCache(key) != null) { stringRedisTemplate.opsForValue().increment("gateway:cache:hit"); } else { stringRedisTemplate.opsForValue().increment("gateway:cache:miss"); }命中率 = 命中数 / (命中数 + 未命中数)。
8.2 压测方法
用压测工具模拟同一批请求反复发送,验证缓存命中率是否达到预期。在实际压测前,需要先保证缓存是空的,也就是冷启动状态,这样才能看清真实效果。
wrk -t4 -c100 -d60s -s post.lua http://localhost:8080/api/v1/chat/completions压测完成后,查看 Redis 中的命中计数。冷启动时第一次请求必然 miss,但后续相同请求应该大量命中。如果是场景固定的 API,95% 命中率是有机会做到的;如果是完全开放的自由对话,语义缓存生效的比例就会低一些。
这里有一个很重要的判断要分享:95% 命中率不是对所有流量都适用,它取决于业务场景的重复度。如果你的业务方都在问“你是谁”“你怎么收费”“你能做什么”这些固定问题,命中率自然高;如果每个用户都在问个性化问题,那只能靠语义缓存把意图相似的问题归并,效果取决于语义相似度计算的准确率。
9. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 缓存命中率长期偏低 | 请求参数未归一化,Key 变化太大 | 查看缓存 Key 统计,观察 Key 分布 | 增加 Prompt 归一化,启用心智语义缓存 |
| 生图任务一直 PENDING | 任务队列消费端未启动或消费异常 | 查看消费者日志,检查队列长度 | 确认消费者线程运行,检查队列消息是否被死信处理 |
| 号池账号频繁被冻结 | 账号并发过高触发上游限流 | 查看账号并发监控和失败日志 | 调低单账号并发阈值,增加账号数量 |
| 旧模型响应出现在新模型中 | 缓存版本号未更新 | 检查缓存 Key 中的版本号 | 升级时递增缓存版本号 |
| 上游渠道超时导致请求堆积 | 渠道熔断策略未生效 | 查看熔断器状态和超时日志 | 配置渠道级熔断阈值和超时时间 |
| 同一账号并发数统计不准确 | Redis 并发计数未释放 | 查看 finally 块是否有释放逻辑 | 在调用结束时使用 finally 释放并发标记 |
| 语义缓存命中错误结果 | 相似度阈值过低 | 检查语义缓存阈值配置和向量相似度 | 提高阈值,增加人工审核机制 |
最常见的坑有两个。
第一个是缓存 Key 设计不合理。很多新手把整个 Prompt 原文作为 Key,这就导致即使只多了一个换行符,缓存也无法命中。正确的做法是归一化后再做哈希,用一个定长字符串作为 Key,而不是用长文本直接做 Key。
第二个是号池并发计数没有及时释放。Java 代码里如果忘记在 finally 中释放 Redis 中的并发标记,账号的并发数会不断累加,最终导致该账号永远不会再被选中。这类问题排查看日志不容易发现,建议在账号状态面板里直接展示并发数和最后调用时间,一对比就能发现问题。
10. 最佳实践与工程建议
把这套系统的实践经验和踩坑教训总结成几条建议,给准备做类似系统的团队参考。
第一,缓存设计从第一天就要考虑版本号。不要觉得“先不加版本号,等出了问题再说”。一旦缓存数据积累到一定量级,再想加版本号就需要停服迁移,成本很高。缓存版本号本质上是缓存 Schema 的版本管理,和数据库迁移是一个道理。
第二,号池账号状态必须是实时可观测的。账号池的可观测性比路由逻辑重要得多。建议为每个账号建立一张状态卡片,展示当前并发数、今日调用量、今日成功/失败数、最近失败原因、冻结状态。运维人员每天只需要看这个页面就能判断整个网关的健康状态。
第三,异步任务必须有超时和补偿机制。生图任务提交到队列后,如果消费者进程崩溃,任务就会卡在“处理中”状态。建议在任务表中记录创建时间和最后更新时间,启动一个定时扫描,把超过设定时间仍未完成的任务重新入队或标记为失败。
第四,统一错误码要贯穿全链路。网关层要处理的不只是上游错误,还有自己内部的错误:缓存不可用、号池无可用账号、任务队列写入失败。这些错误需要和上游错误一样有清晰的 code,方便调用方做分支处理。比如NO_AVAILABLE_ACCOUNT这个错误,调用方可能需要在界面上提示“当前模型繁忙,请稍后再试”。
第五,对公系统要格外注意合规边界。号池化本身不是问题,但前提是所有账号的获取和使用都必须遵循对应平台的条款和法律法规。如果你准备把这套系统用于商业化服务,建议在架构上先梳理清楚:哪些渠道有官方代理资质,哪些账号是否允许转售和代调用,是否需要在用户协议里明确说明渠道来源。合规风险一旦出问题,技术上的稳定性毫无意义。
第六,能选消息队列就不要用 Redis List 当队列。项目里用 Redis Stream 或专业消息队列,能带来消息确认、重试、死信处理这些能力。尤其在生图这种耗时长、涉及外部依赖的场景,消息丢失是不可接受的。有条件的团队直接用 RabbitMQ 或 Kafka 这类成熟组件,省心很多。
第七,日志要打出上游渠道的完整链路。一次请求从网关入口到上游平台,经过了哪些适配器、用了哪个账号、消耗了多少 token,需要在日志里完整串联起来。建议给每次请求生成一个全局 Request ID,并在日志中统一打印。对公客户排查问题时,你只需要让客户提供 Request ID,就能从上到下查完整个链路,这比让客户反复截图有效得多。
11. 总结与后续学习方向
这篇文章从一套可对公的 AI 聚合网关项目切入,讲清楚了四件事:统一模型路由怎么做、Pro 号池的调度机制是什么、缓存命中率 95% 的落地路径、以及对话和生图两类核心能力的接入方式。
如果把多模型 AI 网关当成一个转发工具,那确实没什么可研究的。但它本质上是一个成本控制平台、一个稳定性保障平台、一个多渠道融合平台。号池调度解决的是成本,缓存体系解决的是效率和成本,路由适配层解决的是接入复杂度,错误码统一解决的是可维护性。每一层的设计都有独立价值,组合起来才是“一个站搞定全模型 AI”的真实含义。
如果你正在做类似项目,建议下一步优先补两块:一是把缓存命中率的监控面板搭起来,先量化现状,再谈优化;二是把账号池的健康检查做成定时任务,不要等用户反馈问题才发现账号失效。这两件事做完,系统的稳定性和可维护性会上一个台阶。
再往后,可以深入研究的方向包括:语义缓存中向量检索的召回精度优化、KV Cache 在网关侧的请求合并与上下文压缩、多模型路由策略的自动学习、以及基于历史调用数据的成本预测。AI 网关这个方向正在从“能转发”走向“能治理”,早一点把治理能力做进去,后面就不会被业务追着改架构。