
在实际企业 AI 应用落地中真正困难的往往不是调用某一个模型而是如何用一个站搞定全模型 AI同一个接口接入对话、生图把多个上游 Key 放进 Pro 号池统一调度再用缓存层把重复请求挡住让整个系统在面向对公业务时保持稳定。很多人把这类平台做成简单转发器上线后才发现模型切换、账号限流、重复计费和缓存失效会一起爆发。这篇文章从一个可复现的工程视角拆解全模型 AI 网关的架构设计与落地过程适合后端开发、AI 应用负责人和架构师。文章会先讲清楚多模型接入为什么要做统一网关然后给出 Spring Boot 项目的核心代码、Redis 缓存设计、Pro 号池调度逻辑最后覆盖压测、排查和生产环境检查清单。学完后你可以基于这套思路搭建一个支持对话、生图、账号池管理和高缓存命中率的对内或对公平台再根据业务量替换存储和监控组件。1. 先理解“一个站搞定全模型AI”背后要解决什么问题1.1 多模型接入不是加几个 HTTP 调用那么简单很多团队最早接入大模型时会在业务代码里直接写 OpenAI、Claude 或其他厂商 SDK。这种写法在只接一个模型时没有问题但一旦出现下面这些需求就会变得很难维护同一个接口要按用户或场景切换不同模型。某个模型提供商升级了 API需要灰度切换流量。多个上游账号之间有日调用限额单账号容易被限流。同一个问题在短时间内被大量用户问重复请求导致成本和延迟上升。客户要求对话、生图、文件解析都走统一入口便于审计和计费。这些需求背后核心不是“能不能调用模型”而是“如何治理模型”。治理模型需要统一的路由层负责模型选择、Provider 适配、账号调度、缓存、限流、日志和计费。直接转发没有办法处理限流与失败重试。举例来说一个上游 Key 达到每分钟 60 次限制后如果网关继续把请求打过去用户只会看到 429 错误。好的网关应该在号池里选择另一个可用 Key或者直接返回本地缓存而不是把上游错误原样抛给业务方。1.2 对话、生图、Pro号池、缓存四个模块如何配合一个完整的全模型 AI 平台至少包含四个模块模块作用典型问题对话接口统一文本生成、多轮对话、流式输出不同模型消息格式不一致生图接口统一图片生成、异步任务查询、图片存储生图耗时长不适合同步请求Pro 号池管理多个上游账号 Key做调度和故障隔离单 Key 限流、异常时需要自动切换缓存层缓存重复问答、生图参数、上游响应命中率低、缓存失效后打爆上游它们之间的关系可以这样理解用户请求先进入网关网关根据模型名分发到对话或生图处理链路处理链路从 Pro 号池取一个可用账号在调用上游之前先查缓存没有缓存才真正发起上游请求拿到结果后写回缓存并返回。这个顺序很重要。如果先调用上游再查缓存缓存就失去了保护作用。正确的顺序是接收请求 - 构造缓存键 - 查缓存 - 命中则返回 - 未命中则选号池账号 - 调用上游 - 写缓存 - 返回。1.3 对公场景对平台的硬性要求对公业务和内部工具最大的区别是稳定性、合规和可审计性会成为合同条款。对公场景至少要考虑以下要求可用性不能因为单条上游限流导致整个平台不可用。计费准确性每次对话和生图都要有记录能够按客户、部门、项目拆分。内容安全输入和输出都需要过滤不能把不安全内容直接返回给客户。缓存一致性用户刚提问完马上修改问题不能把旧答案当成新答案返回。数据隔离不同客户的 Prompt、图片、会话记录不能互相串。在学习环境里模型调用失败可以重启服务缓存可以随意清理。生产环境不行。所以这篇文章后面的缓存设计和号池设计都会尽量把“请求路径上的故障”拦在网关层而不是让业务方去处理上游错误。2. 核心概念统一网关、号池调度与缓存命中2.1 统一网关的作用统一网关是所有模型请求的入口。它对外暴露 REST API对内负责把请求翻译成各种上游模型认识的参数格式。举个例子。OpenAI 的对话接口消息结构是messages数组Claude 是messages加system字段国内一些模型服务兼容 OpenAI 格式但字段略有差异。如果业务方直接连上游就得针对每个模型写一套客户端代码。统一网关把这些差异收口在 Provider 适配层业务方始终只面向一个接口。网关还需要处理流式输出、超时、重试、限流和鉴权。一个最小可用的网关请求链路如下用户请求 - 认证鉴权 - 限流 - 路由选择 - 号池选号 - 缓存查询 - 上游调用 - 响应返回 - 异步计费日志认证鉴权保证只有合法客户端可以调用限流防止单个客户占用全部上游额度路由选择决定当前请求使用哪个模型号池选号解决单 Key 限流缓存查询解决重复请求成本。2.2 Pro号池的本质和调度策略Pro 号池并不是一个神秘组件它本质上是多个上游账号凭证的托管和调度层。一个对公平台通常需要采购多家模型供应商的账号或 API Key例如对话模型 A 的 3 个 Key。对话模型 B 的 2 个 Key。生图模型 C 的 2 个 Key。这些 Key 如果散落在配置文件和数据库里会造成两个问题一是 Key 变更需要重新发布二是某个 Key 触发限流时其他 Key 无法自动接管。Pro 号池需要支持以下能力按 Provider 区分 Key 集合。从空闲队列中取一个可用 Key。调用成功后归还 Key。调用失败时根据错误类型决定是否临时禁用 Key。定时健康检查恢复 Key。常见的调度策略有三种策略说明适用场景轮询每次按顺序取下一个 Key所有 Key 额度一致权重轮询按配置权重分配请求比例Key 的模型版本或套餐不同最少并发选择当前并发数最小的 KeyKey 限流阈值不同需动态感知在实现上最简单可靠的是基于队列的轮询每个 Provider 维护一个空闲队列和一个禁用集合。获取 Key 时从队列头部取归还时放回队尾失败达到阈值时移入禁用集合等待后台任务恢复。2.3 对话缓存、生图缓存和 KV 缓存的区别很多人第一次看到“缓存命中率 95%”时会以为所有请求都被缓存。实际上对话缓存的命中条件和生图缓存并不一样KV 缓存又是另一种机制。对话缓存针对的是“相同或相似的输入问题”。如果两个用户问“产品怎么收费”答案完全一致第二次请求完全可以复用第一次结果。前提是问题语义相同并且生成参数相同比如随机性参数temperature不同就不能直接复用。生图缓存针对的是“相同 Prompt、尺寸、模型、风格参数下的生成结果”。生图本身耗时长、GPU 成本高缓存命中一次可以节省一次昂贵的模型推理。但图片生成任务通常需要异步化因此缓存不仅要存请求参数还要存任务 ID 和图片地址。KV 缓存是 Transformer 模型推理内部的概念和业务层缓存不是一回事。KV 缓存用空间换时间把已经计算过的 Key 和 Value 向量保存下来避免每一轮对话都重新计算历史 token。业务网关要关注的是 Redis、Caffeine 这类业务缓存不需要直接操作模型内部的 KV Cache。但理解 KV 缓存有助于理解为什么生图和长文本对话的延迟会随上下文长度变化。2.4 95% 命中率为什么能实现“95% 缓存稳定”在标品 AI 应用中很难做到因为用户提问千差万别。但在对公业务场景里这个目标可以成立前提是业务具有重复性。典型高命中率场景有三类客服知识库用户问的是产品、价格、售后答案相对固定。文档问答合同、手册、题库反复被查询。固定生图参数营销物料生成时Prompt 模板固定只替换少量变量。在这些场景下相同和相似请求占比很高通过精确缓存加语义缓存命中率可以接近甚至超过 95%。不能简单把所有响应都缓存。如果模型是无状态生成的相同输入可能会有不同输出尤其是temperature不为 0 时。缓存设计需要把影响输出的参数全部纳入缓存键否则会出现“问了同一个问题第二次返回旧答案但用户并不满意”的问题。3. 环境准备与项目结构3.1 技术选型下面示例基于 Java 17 和 Spring Boot 3数据层使用 PostgreSQL 和 Redis图片使用本地磁盘存储并预留对象存储扩展点。这个组合适合中小型团队快速搭建也方便后续迁移到云环境。组件用途说明Spring Boot 3Web 框架、依赖注入、配置稳定版本以项目实际依赖为准PostgreSQL存储账号池、会话、计费、任务记录对公场景需要事务和审计Redis缓存、分布式锁、计数器用于两级缓存和限流统计Caffeine本地进程内缓存降低 Redis 压力提高读性能对象存储生图后的图片保存示例用本地目录生产可换 OSS/MinIO选型时要注意不要在一个服务里同时接多个模型 SDK建议统一走 HTTP 或统一适配层避免 SDK 版本冲突。3.2 项目目录all-model-hub/ ├── pom.xml ├── src/main/java/com/example/allmodel/ │ ├── AllModelHubApplication.java │ ├── api/ │ │ ├── ChatController.java │ │ ├── ImageController.java │ │ └── dto/ │ │ ├── ChatRequest.java │ │ ├── ChatResponse.java │ │ ├── ImageGenerateRequest.java │ │ └── TaskStatusResponse.java │ ├── gateway/ │ │ ├── ChatRouter.java │ │ ├── ChatProvider.java │ │ └── provider/ │ │ ├── OpenAiCompatibleProvider.java │ │ └── ImageProvider.java │ ├── pool/ │ │ ├── ProKeyPool.java │ │ └── ProviderAccount.java │ ├── cache/ │ │ ├── CacheKeyBuilder.java │ │ ├── CacheService.java │ │ ├── SemanticCacheService.java │ │ └── CacheMetric.java │ └── config/ │ └── CacheConfig.java └── src/main/resources/ └── application.yml这个目录把 API、网关、号池、缓存分成四个包。API 包只负责接收参数和返回结果网关包负责模型差异适配号池包负责 Key 管理缓存包负责命中率优化和数据一致性。3.3 基础配置server: port: 8080 spring: application: name: all-model-hub datasource: url: jdbc:postgresql://${DB_HOST:127.0.0.1}:5432/all_model_hub username: ${DB_USER:allmodel} password: ${DB_PASSWORD:change-me} redis: host: ${REDIS_HOST:127.0.0.1} port: ${REDIS_PORT:6379} app: cache: # Caffeine 本地缓存上限 local-max-size: 10000 # 本地缓存过期时间 local-expire-minutes: 10 # Redis 缓存过期时间 redis-expire-minutes: 60 # 是否启用语义缓存 enable-semantic-cache: true # 语义相似度阈值 semantic-similarity-threshold: 0.92 pool: acquire-timeout-ms: 3000 ban-duration-minutes: 5 health-check-interval-seconds: 30配置项的意思是本地缓存保存 10 分钟Redis 保存 60 分钟。这样同一个问题在一小时内第二次访问可以直接命中 Redis10 分钟内频繁访问命中本地缓存减少 Redis 读取。ban-duration-minutes控制号池中某个 Key 被临时禁用的时间。注意change-me这样的默认密码只是示例生产环境必须改成环境变量注入的强密码。3.4 依赖说明dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdcom.github.ben-manes.caffeine/groupId artifactIdcaffeine/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency如果是 Spring AI 项目可以直接引入spring-ai-starter-model-openai等依赖但号池、缓存、路由这些能力仍然需要自己实现。自定义网关层会更灵活尤其是需要同时管理多个厂商账号时。4. 实现统一对话与生图接口4.1 定义请求和响应模型对话请求和生图请求各自定义一个 DTO避免把所有参数堆到一个 Map 里。public record ChatRequest( String model, String provider, ListMessage messages, Double temperature, Integer maxTokens, String conversationId ) { public record Message(String role, String content) {} }public record ImageGenerateRequest( String provider, String model, String prompt, String negativePrompt, Integer width, Integer height, Integer steps, String callbackUrl ) {}provider字段允许调用方显式指定供应商如果不指定网关根据model自动推断。conversationId用于多轮会话关联也便于计费日志归组。4.2 路由层用策略模式处理模型差异定义统一的 Provider 接口public interface ChatProvider { String name(); boolean support(String model); ChatResult chat(ChatRequest request, ProviderAccount account) throws ProviderException; }每个上游模型服务商实现一个 Provider。这里不做复杂抽象只要让路由层不感知具体 HTTP 细节即可。Service public class ChatRouter { private final ListChatProvider providers; private final ProKeyPool keyPool; private final CacheService cacheService; public ChatRouter(ListChatProvider providers, ProKeyPool keyPool, CacheService cacheService) { this.providers providers; this.keyPool keyPool; this.cacheService cacheService; } public ChatResult route(ChatRequest request) { // 1. 找到模型对应的 Provider ChatProvider provider providers.stream() .filter(p - p.support(request.model())) .findFirst() .orElseThrow(() - new ProviderException(unsupported model: request.model())); // 2. 先在缓存中查找 String cacheKey CacheKeyBuilder.buildChatKey(request, provider.name()); Object cached cacheService.get(cacheKey); if (cached instanceof ChatResult result) { result.setFromCache(true); return result; } // 3. 从号池取账号调用上游 ProviderAccount account keyPool.acquire(provider.name()); try { ChatResult result provider.chat(request, account); cacheService.put(cacheKey, result); return result; } catch (ProviderException ex) { // 根据错误类型决定是否禁用账号 keyPool.markFailed(provider.name(), account, ex); throw ex; } finally { keyPool.release(provider.name(), account); } } }这里有个关键点cacheKey必须包含 Provider 名称否则同一个模型名在两个 Provider 之间切换时可能返回旧 Provider 的缓存结果。4.3 Pro号池管理器实现号池是多个 Key 的队列。下面给出简化实现生产环境需要加锁和持久化。public class ProKeyPool { private final MapString, LinkedListProviderAccount idle new ConcurrentHashMap(); private final MapString, SetProviderAccount banned new ConcurrentHashMap(); private final MapString, MapProviderAccount, Integer failCount new ConcurrentHashMap(); public ProviderAccount acquire(String provider) { LinkedListProviderAccount queue idle.computeIfAbsent(provider, k - new LinkedList()); ProviderAccount account queue.poll(); if (account null) { throw new ProviderException(no available key for provider: provider); } return account; } public void release(String provider, ProviderAccount account) { if (banned.getOrDefault(provider, Set.of()).contains(account)) { return; } idle.computeIfAbsent(provider, k - new LinkedList()).offer(account); } public void markFailed(String provider, ProviderAccount account, ProviderException ex) { if (ex.isRateLimit() || ex.isAuthError()) { MapProviderAccount, Integer counter failCount.computeIfAbsent(provider, k - new ConcurrentHashMap()); counter.merge(account, 1, Integer::sum); if (counter.get(account) 3) { banned.computeIfAbsent(provider, k - ConcurrentHashMap.newKeySet()).add(account); } } } }这个实现解决了两个问题多个请求并发获取 Key 时不会重复取到同一个 Key。连续失败 3 次的 Key 会被移入禁用集合避免继续被打爆。后台恢复任务可以按配置的health-check-interval-seconds定期把禁用 Key 放回队列并清空失败计数。这样当上游服务恢复后号池可以自动恢复容量。4.4 生图任务异步化图片生成耗时通常是几秒到几十秒。同步请求会导致 HTTP 超时所以生图接口需要拆成两个阶段提交任务和查询结果。PostMapping(/api/v1/image/generate) public TaskStatusResponse generateImage(RequestBody ImageGenerateRequest request) { String taskId imageTaskService.submit(request); return new TaskStatusResponse(taskId, PENDING, null); } GetMapping(/api/v1/image/task/{taskId}) public TaskStatusResponse queryTask(PathVariable String taskId) { return imageTaskService.query(taskId); }submit方法的工作流程根据请求参数生成缓存键。如果缓存已存在直接返回缓存中的图片地址。如果不存在创建任务记录丢入线程池或消息队列。后台任务调用生图 Provider拿到图片后写对象存储。更新任务状态并写入缓存。这样做的好处是用户不需要长时间连接网关也不容易因为上游慢而拖垮线程池。5. 缓存层设计与命中率优化5.1 缓存键的设计缓存键决定了命中率的上限。键设计太粗会把不同结果混在一起键设计太细每次请求都是新键缓存形同虚设。对话缓存键示例chat:{provider}:{model}:{hash(messages)}:{hash(temperature)}:{hash(maxTokens)}生图缓存键示例image:{provider}:{model}:{hash(prompt)}:{hash(negativePrompt)}:{width}:{height}:{steps}hash可以采用 SHA-256避免超长文本直接进入 Redis key。provider和model必须包含因为不同模型对同一问题的答案不同。注意不要把用户 ID 放进缓存键。如果把用户 ID 放进去同一个问题不同用户会各自生成缓存命中率会明显下降。对公业务通常希望不同用户问同一个问题时在没有敏感数据隔离需求的情况下共享答案。5.2 本地缓存 Redis 两级缓存两级缓存是提高命中率的重要手段。Redis 是分布式缓存所有实例共享Caffeine 是本地缓存每个实例各自持有。读缓存顺序public Object get(String key) { Object local localCache.getIfPresent(key); if (local ! null) { cacheMetric.record(local_hit); return local; } Object redis redisTemplate.opsForValue().get(key); if (redis ! null) { localCache.put(key, redis); cacheMetric.record(redis_hit); return redis; } cacheMetric.record(miss); return null; }由于本地缓存只有 10 分钟过期即使多个服务实例没有同步失效最多也只能在 10 分钟内出现一次不一致。对大多数 AI 问答场景这是可以接受的。写缓存时采用先写 Redis再写本地缓存。本地缓存建议设置较小过期时间避免长时间占用 JVM 堆内存。5.3 语义缓存处理相似问题精确缓存只能处理完全相同的输入。真实用户提问会有细微差异例如“产品价格是多少”“您们产品怎么收费”“你们这个产品的收费方式”这三个问题精确缓存无法互相命中但语义很接近。语义缓存可以通过向量相似度判断是否应该复用答案。实现思路把用户输入文本通过 Embedding 模型转成向量。在向量库或 Redis 中查找相似向量。如果相似度高于阈值比如 0.92则返回对应的历史答案。如果相似度不够高则走真正的模型调用。Redis 本身没有原生向量搜索能力最简单的做法是把向量序列化后存到一个有序集合里用余弦相似度逐个计算。数据量大时建议换用专门的向量数据库比如 Milvus 或 pgvector。下面是一个语义缓存服务片段public CacheResult semanticGet(String text) { if (!semanticCacheEnabled) { return CacheResult.miss(); } float[] vector embeddingService.embed(text); VectorHit hit vectorStore.search(vector, threshold); if (hit ! null) { String answer redisTemplate.opsForValue().get(hit.getCacheKey()); if (answer ! null) { return CacheResult.hit(answer); } } return CacheResult.miss(); }语义缓存需要注意一个坑不要把“相似输入”理解成“一定应该返回相同答案”。例如“帮我写一个请假申请”和“帮我写一个离职申请”语义结构相似但答案完全不同。所以需要把关键词差异也纳入判断或者对敏感业务关闭语义缓存。5.4 避免缓存击穿、穿透和雪崩缓存层另一个重要问题是异常保护。穿透查询一个不存在的数据缓存和数据库都没有。解决方法是缓存空值或布隆过滤器拦截。击穿某个热点 key 过期瞬间大量请求同时打到上游。解决方法是加分布式锁只让一个请求回源。雪崩大量 key 在同一时间过期所有请求都打到上游。解决方法是过期时间随机化。对于对公 AI 平台最危险的是热点问题缓存过期后秒级并发全部回源瞬间把上游限流打满。代码层面可以为缓存查询加入分布式锁public Object getWithLock(String key, SupplierObject loadFunction) { Object value get(key); if (value ! null) { return value; } String lockKey lock: key; boolean locked redisLock.tryLock(lockKey, 5, 10); if (!locked) { // 等待其他线程写入再查一次 sleep(100); return get(key); } try { Object loaded loadFunction.get(); put(key, loaded); return loaded; } finally { redisLock.unlock(lockKey); } }5.5 缓存失效策略缓存不能永久有效否则模型升级或知识库更新后用户仍然拿到旧答案。常见失效策略策略实现方式适用场景TTL 过期Redis expire通用默认策略主动失效调用管理端 API 删除指定 key知识库内容变更后版本号缓存键带业务版本升级后自动失效模型版本切换随机过期TTL 加随机偏移防止雪崩对于知识库问答建议维护一个“知识库版本号”。每当文档更新时版本号加一缓存键变为chat:v3:{model}:{hash}。这样旧缓存自然不再被读取不需要逐个删除。5.6 缓存命中率统计命中率指标需要真实统计。在缓存服务中维护计数器public class CacheMetric { private final AtomicLong localHit new AtomicLong(); private final AtomicLong redisHit new AtomicLong(); private final AtomicLong miss new AtomicLong(); public void record(String type) { switch (type) { case local_hit - localHit.incrementAndGet(); case redis_hit - redisHit.incrementAndGet(); case miss - miss.incrementAndGet(); } } public double hitRate() { long total localHit.get() redisHit.get() miss.get(); if (total 0) { return 0; } return (localHit.get() redisHit.get()) * 100.0 / total; } }通过 Prometheus 等监控系统采集这些指标可以直观看到命中率变化。如果命中率低于预期就可以进一步分析是缓存键设计问题、语义缓存阈值问题还是请求本身重复度太低。6. 运行验证与压力测试6.1 启动服务先准备 PostgreSQL 和 Redis。本地可以用 Docker 快速启动docker run -d --name allmodel-redis -p 6379:6379 redis:7-alpine docker run -d --name allmodel-postgres \ -e POSTGRES_DBall_model_hub \ -e POSTGRES_USERallmodel \ -e POSTGRES_PASSWORDchange-me \ -p 5432:5432 postgres:15然后启动 Spring Boot 应用mvn spring-boot:run启动后检查日志是否出现Started AllModelHubApplication。如果端口冲突需要确认 8080 是否被占用。6.2 用 curl 验证对话和生图对话接口验证curl -X POST http://localhost:8080/api/v1/chat \ -H Content-Type: application/json \ -d { model: gpt-4o-mini, messages: [ {role: user, content: 介绍一下产品的定价} ], temperature: 0.3 }第一次请求会触发上游调用。返回后再次执行相同命令第二次响应中的fromCache字段应为true。这可以直接验证缓存是否生效。生图接口验证curl -X POST http://localhost:8080/api/v1/image/generate \ -H Content-Type: application/json \ -d { model: stable-diffusion-xl, prompt: 城市夜景未来风格, width: 1024, height: 1024, steps: 25 }拿到taskId后查询任务状态curl -X GET http://localhost:8080/api/v1/image/task/{taskId}正常响应会返回PENDING、RUNNING或SUCCESS成功时带图片地址。6.3 压测观察命中率压测时建议使用固定问题集合模拟客服知识库的重复请求。用 wrk 加 Lua 脚本可以模拟同一组问题反复提交。-- post.lua wrk.method POST wrk.headers[Content-Type] application/json messages { 产品价格是多少, 怎么申请退款, 支持哪些支付方式 } local i 0 function request() i i 1 local content messages[(i % 3) 1] local body string.format( {model:gpt-4o-mini,messages:[{role:user,content:%s}],temperature:0.0}, content ) return wrk.format(nil, /api/v1/chat, nil, body) end执行压测wrk -t4 -c64 -d60s -s post.lua http://localhost:8080在固定问题集中第一次循环三个问题需要回源之后大量请求都应该命中缓存。压测结束后查看监控指标命中率通常会在 95% 以上。如果低于这个值需要检查是否是每个请求都带了不同的conversationId或随机参数。6.4 通过监控看缓存指标可以在 Spring Boot 中暴露 Actuator 指标management: endpoints: web: exposure: include: health,metrics,prometheus然后在 Prometheus 中采集cache_hit_rate、cache_local_hit_total、cache_redis_hit_total等指标Grafana 做趋势图。压测时观察本地命中率变化。Redis 命中率变化。回源请求数量的峰值。如果回源峰值仍然很高说明缓存过期时间设置过短或热点 key 击穿保护没有生效。7. 常见问题排查7.1 上游返回 429 或鉴权失败现象用户请求时经常报429 Too Many Requests或401 Unauthorized。可能原因号池中的 Key 已经达到上游限流阈值。Key 过期或权限不足。号池没有正确禁用失败的 Key。多个请求同时取到同一个 Key。检查方式查看日志中ProviderException的明细。查看号池失败计数和禁用集合大小。确认 Key 在上游控制台是否有效。处理建议将限流错误识别为rateLimit临时禁用该 Key。增加号池健康检查频率。为高频场景配置更多 Key。在缓存层加分布式锁避免回源并发过高。7.2 缓存命中率一直上不去现象压测后命中率低于 80%。可能原因请求参数包含随机值例如timestamp或随机conversationId。缓存键没有覆盖模型名和 Provider。语义缓存阈值设置过高或过低。每次请求的temperature都不同。本地缓存和 Redis 过期时间太短。检查方式打印缓存键看相同问题是否生成相同 key。查看指标中miss数量来源。确认客户端是否发送多余随机字段。处理建议去掉缓存键中的无关字段。对temperature做档位归一化比如小于 0.5 都按 0 处理。调整语义缓存相似度阈值。延长 Redis 缓存过期时间但要注意内容更新时效。7.3 缓存过期后瞬间打爆上游现象每次缓存批量过期后上游限流报警。可能原因所有 key 设置了相同过期时间。没有加分布式锁热点 key 过期后并发回源。回源响应时间太长导致大量线程等待。检查方式查看 Redis key 的 TTL 分布。查看回源请求时间曲线。查看线程池活跃线程数。处理建议TTL 增加随机偏移例如60 random(0, 30)分钟。对热点 key 使用分布式锁。生图任务异步化避免回源阻塞 Web 线程。为上游调用单独配置线程池控制并发上限。7.4 生图任务长时间 pending现象提交生图任务后状态一直是PENDING看不到结果。可能原因后台线程池队列积压。上游生图服务超时或不可用。图片存储不可写。任务状态更新失败。检查方式查看任务表记录和任务日志。查看线程池活跃线程数和队列大小。尝试在模型控制台直接调用生图接口判断上游是否正常。处理建议生图任务需要设置超时时间超时后标记为FAILED。给用户提供查询接口而不是让用户一直等待。图片地址写入对象存储后再更新任务状态为SUCCESS。问题现象可能原因检查方式处理建议上游 429Key 限流查看号池禁用集合自动禁用并健康检查恢复命中率低缓存键含随机字段打印缓存键过滤无关参数缓存雪崩TTL 相同查看 TTL 分布增加随机偏移图片任务卡住队列积压查看线程池指标设置超时和任务重试8. 生产环境最佳实践与扩展方向8.1 对公上线前的检查清单对公部署不能只验证“接口能通”。上线前至少要过一遍以下清单数据库和 Redis 是否启用了密码、访问控制和备份策略。账号池 Key 是否通过配置中心或密钥管理服务注入不硬编码在代码里。对话和生图是否有独立的超时和重试策略。是否限制了单客户端、单用户的 QPS。是否记录了每次调用的客户、模型、token 数、费用和结果状态。是否配置了内容安全过滤输入和输出都处理。缓存是否设置随机过期时间防止雪崩。是否有回源并发限制防止号池 Key 被批量封禁。是否能够从接口层区分缓存命中和真实调用的计费。是否准备了模型服务商故障时的降级方案。这个清单可以作为发布检查的一部分每次版本升级都重新确认。8.2 内容安全与隐私合规对公平台需要在上游模型之前和之后做内容安全过滤。输入过滤可以拦截恶意 Prompt 和越权请求输出过滤可以防止模型生成不合适的内容。过滤服务可以调用第三方内容安全 API也可以基于关键词和分类模型自建。隐私方面要注意不要把用户的完整对话内容写入公开日志。缓存中包含用户提问和模型答案时需要设置过期时间。如果客户有数据隔离要求缓存键要按客户维度隔离。不要把 A 客户的缓存结果返回给 B 客户。这一点和缓存键设计存在冲突。一般建议是默认共享缓存提高命中率对数据隔离要求高的客户在请求中带上隔离标识缓存键中加入客户空间前缀。8.3 计费与审计对公平台必须区分“真正调用了上游模型”和“命中了缓存”。如果缓存命中也算一次费用客户不会接受。合理做法是每次真实调用上游时记录 token 数和上游费用。缓存命中时记录“响应来源 cache”费用为 0。生图任务成功后记录图片张数和模型成本。在计费表中至少要包含以下字段字段说明request_id请求唯一 IDcustomer_id客户标识model模型名称provider上游供应商cache_hit是否命中缓存prompt_tokens输入 token 数completion_tokens输出 token 数cost_amount费用created_at调用时间审计日志不能只保存一天建议至少保留 90 天以上具体按合同要求。8.4 从网关走向 Agent 和模型融合当对话、生图、号池和缓存稳定之后很多团队会开始扩展两类能力。第一类是 Agent。用户不再满足于单轮问答而是希望平台能够调用工具、查数据库、写代码、编排多步任务。此时网关需要新增“任务编排”模块让模型具备调用插件的能力并缓存工具的返回结果避免 Agent 反复执行同一个查询。第二类是模型融合。同一个 Prompt 可以用多个模型生成候选答案再通过排序或投票选择最佳结果。这会显著提高回答质量但也会增加成本。这里缓存的价值更大相同的 Prompt 只做一次多模型融合后续请求直接复用融合结果。视频生成模型也是自然扩展方向。视频生成比生图耗时更长任务异步化、任务队列、对象存储和缓存设计都需要重新评估。不过总体架构仍然可以复用统一接口、号池调度、任务状态管理、结果缓存。需要给新手的建议是不要一开始就追求“所有模型都支持”。先把一个对话模型和一个生图模型跑通再接入号池和缓存最后才扩展 Agent 和模型融合。架构是否合理是在接入第二个模型时才会真正显现出来的。