尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring AI Gateway 实战:用 TaoToken 统一 Key 打通智能路由与语义缓存

Spring AI Gateway 实战:用 TaoToken 统一 Key 打通智能路由与语义缓存 1. 为什么要在 Spring Cloud Gateway 上做 AI 服务网关如果你正在用 Spring Cloud Gateway 做微服务入口现在业务方要求接入大模型能力最直接的做法是在每个业务服务里各自写一套 HTTP 客户端去调模型接口。我见过不少团队一开始就是这么干的结果三个月后代码库里散落着七八份 API Key、五套重试逻辑、三套计费统计改一个超时参数要发五个服务。Spring AI Gateway 要解决的就是这个问题把 AI 调用收敛到网关层业务服务只面向一个统一的/api/ai/**入口由网关负责选模型、算成本、做缓存、限流、熔断。它和传统 API 网关的区别在于AI 请求的“路由依据”不只是 URL 和 Header还包括请求体里的模型名、token 数量、问题复杂度甚至语义相似度。这篇要交付的东西很具体一套能跑起来的 Spring Cloud Gateway 路由配置、一套语义缓存的键规则、一份settings.json/config.toml骨架以及用 curl 验证“路由命中”和“缓存生效”的实际动作。适合已经在用 Spring Cloud 体系、想把多模型调用统一管起来的后端同学。核心检索词就三个Spring AI Gateway、AI 服务网关、智能路由与语义缓存。先说清楚一个前提网关本身不生产模型能力它需要一个稳定的上游通道。我这边统一用 TaoToken 作为模型接入层一个 Key 打通多个模型网关侧只认一个 Base URL省掉了在网关里维护多厂商鉴权差异的麻烦。下面所有配置都围绕这个前提展开。2. TaoToken 前置准备统一 Key 与通道接入在写路由之前先把上游通道固定下来。TaoToken 在这里扮演的角色是“模型接入层”网关不需要知道背后是哪个厂商的接口格式只需要按 OpenAI 兼容协议发请求由接入层完成转发。这样做的好处是网关代码里不会出现任何厂商特有的字段处理逻辑。你需要准备三样东西Base URL、API Key、Model ID。Base URL 用https://taotoken.net/api注意这个地址不带任何查询参数直接作为spring.cloud.gateway里uri的前缀。API Key 在控制台的 API Keys 页面生成生成后只显示一次建议直接写进环境变量而不是配置文件。export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/apiModel ID 这块要注意网关路由里用的模型名要和接入层支持的名称一致。常见的对话模型、代码模型都可以通过同一个 Key 调用切换模型只需要改请求体里的model字段不需要换 Key、不需要换 Base URL。这是统一 Key 最实际的价值网关的智能路由策略可以纯粹基于业务规则来写不用掺杂鉴权分支。如果你用的是 Claude Code 这类客户端配置骨架长这样放在~/.claude/settings.json{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_AUTH_TOKEN: sk-你的key, ANTHROPIC_MODEL: claude-sonnet-4-5 } }如果是 Codex 类的 CLI配置写在~/.codex/config.tomlmodel gpt-5 model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY wire_api chat这两个骨架的意义在于网关下游的业务服务、开发同学本地的 CLI 工具用的是同一套 Base URL 和同一批 Key排查问题时不会出现“本地能跑线上不行”的割裂。网关侧只需要在application.yml里引用环境变量即可不要把 Key 硬编码进代码仓库。有一点要提醒网关做统一接入后Key 的轮换、额度控制、调用统计都集中在接入层网关本身不需要实现复杂的配额算法只需要在过滤器里读取响应头里的用量信息做记录。这样网关的职责更单一也更容易测试。3. 可复制的路由配置与缓存键规则这一节是全文的核心直接给能粘贴进项目的配置。先看application.yml里的网关路由部分这里用声明式配置而不是 Java DSL因为声明式更容易做多环境覆盖。spring: cloud: gateway: routes: - id: ai-chat-route uri: https://taotoken.net/api predicates: - Path/api/ai/chat/completions - MethodPOST filters: - StripPrefix2 - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 20 redis-rate-limiter.burstCapacity: 40 key-resolver: #{userKeyResolver} - name: CircuitBreaker args: name: aiChatCB fallbackUri: forward:/fallback/ai - id: ai-embedding-route uri: https://taotoken.net/api predicates: - Path/api/ai/embeddings filters: - StripPrefix2 httpclient: connect-timeout: 5000 response-timeout: 60sStripPrefix2是因为外部路径是/api/ai/chat/completions剥掉两层后变成/chat/completions拼上uri就是https://taotoken.net/api/chat/completions正好是 OpenAI 兼容路径。这个细节很多人第一次配会搞错导致 404。智能路由的关键不在 YAML而在一个自定义的GlobalFilter它根据请求体内容改写目标模型。下面这段是核心逻辑放在AiRoutingFilter里Component public class AiRoutingFilter implements GlobalFilter, Ordered { private static final SetString COMPLEX_HINTS Set.of(重构, 架构, 性能优化, 并发, 分布式); Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request exchange.getRequest(); if (!request.getPath().value().contains(/chat/completions)) { return chain.filter(exchange); } return DataBufferUtils.join(request.getBody()) .flatMap(buffer - { byte[] bytes new byte[buffer.readableByteCount()]; buffer.read(bytes); DataBufferUtils.release(buffer); String body new String(bytes, StandardCharsets.UTF_8); String routed routeModel(body); ServerHttpRequest mutated request.mutate() .body(routed) .build(); return chain.filter(exchange.mutate().request(mutated).build()); }); } private String routeModel(String body) { boolean complex COMPLEX_HINTS.stream().anyMatch(body::contains); String target complex ? gpt-5 : gpt-5-mini; return body.replaceFirst(\model\\\s*:\\s*\[^\]\, \model\:\ target \); } Override public int getOrder() { return -50; } }这段代码做了两件事读请求体、按关键词把model字段替换成目标模型。实测下来关键词命中率不需要很高只要把最贵的模型留给真正复杂的请求成本就能明显下降。注意getOrder()返回 -50要排在限流过滤器之前否则限流按旧模型算配额会不准。语义缓存的键规则单独说。缓存键不能简单用请求体的 MD5因为用户换个说法问同一个问题就会 miss。我的做法是取最后一条 user message 的文本做归一化去空格、转小写、去掉标点再拼上模型名做 MD5。这样“怎么退款”和“如何退款”会命中同一个键。public static String cacheKey(String userText, String model) { String normalized userText.toLowerCase() .replaceAll([\\p{Punct}\\s], ); String raw model :: normalized; return ai:cache: DigestUtils.md5DigestAsHex( raw.getBytes(StandardCharsets.UTF_8)); }缓存写入用 RedisTTL 设 24 小时只缓存非流式请求。流式响应stream: true不缓存因为分块响应没法整体复用。这个规则要写死在过滤器里别让业务方自己决定否则缓存命中率会被流式请求拖垮。4. 验证路由命中与缓存生效配置写完不验证等于没写。这一节给两组 curl 命令分别验证路由和缓存。先启动网关确认 Redis 在跑。然后发第一个请求故意用简单问题看它是否被路由到轻量模型curl -s -X POST http://localhost:8080/api/ai/chat/completions \ -H Content-Type: application/json \ -H X-User-Id: u1001 \ -d { model: gpt-5, messages: [{role: user, content: 今天天气怎么样}], stream: false } | jq .model, .usage返回体里的model字段应该是gpt-5-mini说明路由过滤器把简单问题降级了。如果返回的还是gpt-5检查AiRoutingFilter的getOrder()是否生效、请求体是否被正确读取。这里有个坑Spring Cloud Gateway 默认不缓存请求体DataBufferUtils.join读完之后如果不重新构造 request下游会拿到空 body。上面代码里request.mutate().body(routed)就是干这个的。再发一个复杂问题验证它走高质量模型curl -s -X POST http://localhost:8080/api/ai/chat/completions \ -H Content-Type: application/json \ -H X-User-Id: u1001 \ -d { model: gpt-5, messages: [{role: user, content: 帮我做一次分布式架构的性能优化}], stream: false } | jq .model这次应该返回gpt-5。两次请求的X-User-Id相同方便后面看限流计数。验证缓存要连发两次相同语义的请求第二次看响应时间。第一次请求会 miss第二次应该命中# 第一次 time curl -s -X POST http://localhost:8080/api/ai/chat/completions \ -H Content-Type: application/json \ -d {model:gpt-5-mini,messages:[{role:user,content:如何申请退款}],stream:false} /dev/null # 第二次换个说法 time curl -s -X POST http://localhost:8080/api/ai/chat/completions \ -H Content-Type: application/json \ -d {model:gpt-5-mini,messages:[{role:user,content:退款怎么申请}],stream:false} /dev/null第二次的耗时应该明显低于第一次通常在几十毫秒级别。如果两次耗时差不多去 Redis 里查一下键是否存在redis-cli --scan --pattern ai:cache:*能看到键说明写入成功看不到就是缓存过滤器没生效。常见原因是过滤器顺序排在路由之后或者isCacheableRequest判断把请求排除了。另外注意缓存命中的响应要手动构造ServerHttpResponse不能直接chain.filter否则会真的打到上游。5. 常见报错排查401、local proxy failed 与 reading choices这一节按真实报错来。第一个高频错误是 401{error:{message:invalid api key,type:authentication_error}}网关侧看到 401先确认三件事环境变量TAOTOKEN_API_KEY是否被 Spring 读到用System.getenv打日志、请求头里的Authorization是否被网关过滤器误删、Base URL 是否写成了带路径的地址。我踩过的坑是网关的StripPrefix把/v1也剥掉了导致上游路径不对返回的却是 401 而不是 404排查了半天。第二个错误是local proxy failed或连接超时io.netty.channel.ConnectTimeoutException: connection timed out这个通常是httpclient.connect-timeout设太短或者网关所在网络到上游的出口不稳定。把connect-timeout调到 5000ms 以上response-timeout调到 60s因为大模型首 token 延迟本来就高。如果用了流式response-timeout要设得更长或者干脆对 SSE 路由单独配置。第三个错误是解析响应时抛reading choices相关异常com.fasterxml.jackson.databind.JsonMappingException: Cannot deserialize value of type java.util.ArrayList from Object value这说明网关在改写响应体时把非标准格式的响应也当成 chat completion 处理了。检查你的响应过滤器是否对所有/chat/completions响应都做了choices字段解析。有些错误响应体里没有choices直接解析就会炸。正确做法是先判断 HTTP 状态码非 200 直接透传不要碰 body。第四个是 OAuth 或鉴权头冲突。如果你在网关里同时配了 Spring Security 和上游鉴权可能出现Authorization头被覆盖。解决办法是在路由过滤器里显式设置上游鉴权头别依赖默认透传exchange.getRequest().mutate() .header(Authorization, Bearer apiKey) .build();排查顺序建议固定下来先看网关日志里的 requestId再查 Redis 里的限流计数和缓存键最后用 curl 直连上游确认 Key 本身没问题。直连命令curl -s https://taotoken.net/api/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d {model:gpt-5-mini,messages:[{role:user,content:ping}]}直连能通、走网关不通问题一定在网关配置直连也不通就是 Key 或额度的问题。6. 把网关接入落到日常开发流网关跑起来只是第一步真正省事的是把它接进日常开发流。我现在的做法是本地开发不直接连上游而是把本地服务的 AI 调用指向本地网关网关再指向 TaoToken。这样本地就能复现线上的路由和缓存行为不会出现“本地调的是 A 模型、线上走的是 B 模型”的偏差。具体操作是在本地application-local.yml里把上游地址改成http://localhost:8080/api/ai网关的uri仍然指向https://taotoken.net/api。开发同学不需要各自申请 Key统一用网关的环境变量即可。需要看某个请求走了哪个模型、有没有命中缓存直接看网关日志里的 requestId 和 Redis 键。对于长期跑 Agent 任务或批量代码生成的场景建议单独走 Coding Plan 通道和交互式请求分开限流避免批量任务把交互请求的配额挤掉。验证模型能力是否正常可以用模型对话页面直接发一条消息确认 Key 和通道没问题再去调网关。最后给一个实用技巧把缓存命中率和路由分布做成两个计数器暴露在/actuator/metrics下。每周看一眼如果缓存命中率低于 30%说明缓存键规则太严考虑放宽归一化策略如果高质量模型占比超过 40%说明路由关键词太宽该收紧阈值了。这两个指标比任何监控大盘都直接。
返回列表