Day 11 / 共 20 天 · 第 3 周 AI/插件/消费者

AI 供应商与 AI 路由

第 3 周开场就是 Console 最精彩的部分。今天看两条主线:LLM Provider(配置对接哪家大模型)如何变成 ai-proxy 插件配置,以及 AI 路由如何"一条变多个"展开成 Ingress + 多个插件 + EnvoyFilter。

📍 你在整门课的位置 · 第 3 周 AI/插件/消费者(Day 11-15,本周开场)
D11 AI供应商/路由 D12 插件管理 D13 消费者鉴权 D14 路由/服务源 D15 MCP管理
L01

AI 网关两条主线

🤔 痛点:应用直接对接各家大模型有多累? 你的应用要调 OpenAI、通义、Claude……每家 API 格式不同、密钥各管各的、还要自己做限流/计费/故障切换。哪天 OpenAI 挂了,你得改代码切到备用——一团乱麻。
💡 本质:一个"统一翻译官 + 前台"挡在中间 AI 网关就像公司前台:应用只用一种口径(OpenAI 格式)跟前台说话,前台负责翻译成各家格式、统一管密钥、限 token、某家挂了自动转备用。Console 就是配置这个前台的遥控器,底层靠 ai-proxy 等 Wasm 插件干活。两条主线:Provider = 对接哪家(前台的通讯录),Route = 什么请求走哪家、怎么降级(前台的转接规则)。
📝 举个例子 应用发 POST /v1/chat/completions {model:"gpt-4"}(OpenAI 格式)→ AI 路由按规则把它转给 qwen provider、并把模型名映射成 qwen-max → 通义那边正常返回。应用一行没改,就从 OpenAI 切到了通义。
  1. LLM Provider/v1/ai/providers):定义"对接哪家大模型"——OpenAI/通义/Claude/DeepSeek… 含 API 密钥、端点。
  2. AI Route/v1/ai/routes):定义"什么请求走哪个 provider、按什么模型路由/映射、失败怎么降级"。
AI 网关在解决什么问题? 你的应用要调大模型,但直接对接各家 API 很麻烦:密钥管理、格式不统一、限流、计费、故障切换……AI 网关把这些统一到网关层:应用只对网关说"用 OpenAI 格式",网关负责翻译成各家格式、管密钥、限 token、失败时自动切到备用 provider。Console 就是配置这套能力的界面,底层靠 ai-proxy 等 Wasm 插件实现(上一站 Higress 讲过 ai-proxy 支持 39 家 provider)。
L02

LlmProvider 模型

sdk/model/ai/LlmProvider.java(字段 :33-46):

name / type / protocol / proxyName
tokens          // List,各家的 API 密钥
tokenFailoverConfig
rawConfigs      // Map,直接透传给 ai-proxy 插件的原始 KV

类型 LlmProviderType.java 有 30+ 常量(qwen/openai/moonshot/azure/deepseek/zhipuai/ollama/claude/gemini/doubao/bedrock/vertex/vllm…);协议 LlmProviderProtocol.java 枚举 OPENAI_V1(默认)与 ORIGINAL

读法:注意协议有"API 层 value"和"插件层 pluginValue"两套值,fromValue/fromPluginValue:47-63)双向转换——因为前端 API 用一套命名,底层插件用另一套,中间要翻译。
L03

Handler 注册表(策略模式)

sdk/service/ai/LlmProviderServiceImpl.java 静态块(:59-90)建一个 Map<type, LlmProviderHandler>

// 大多数厂商一行注册(域名固定):
map.put("moonshot", new DefaultLlmProviderHandler("moonshot", "api.moonshot.cn", 443, ...));
map.put("deepseek", new DefaultLlmProviderHandler("deepseek", "api.deepseek.com", 443, ...));
// 有特殊逻辑的用专用 Handler:
map.put("qwen",  new QwenLlmProviderHandler());   // 注入 qwen 专属默认值
map.put("azure", new AzureLlmProviderHandler());
// Openai/ZhipuAI/Ollama/Claude/Bedrock/Vertex/Vllm ...
为什么用"注册表 + Handler"? 每家大模型的对接细节都不一样(域名、默认参数、鉴权方式)。与其写一大堆 if-else,不如给每家一个 Handler(处理器),统一接口,注册进一张表。加新厂商 = 加一个 Handler + 注册一行,老代码不动。这就是"策略模式 + 注册表"——开闭原则的经典应用。QwenLlmProviderHandler 就为通义注入了 qwenEnableCompatible=true(OpenAI 兼容模式)等专属默认值。
L04

addOrUpdate 全流程

加一个 Provider如 qwen + 密钥 handler + addOrUpdate归一化+默认值+落库 ai-proxy 全局实例 providers[] +1 一个服务来源(ServiceSource) service 级 ai-proxy 实例
一个"加 Provider"操作,落地成三种资源:ai-proxy 全局实例 providers 数组多一项 + 一个服务来源 + 一个 service 维度的 ai-proxy 实例。反向 getProviders 再从这些资源还原成列表。

LlmProviderServiceImpl.addOrUpdate:104-246)是"Provider → 网关配置"的核心:

  1. 按 type 取 handler,没有就抛 ValidationException:105-108)。
  2. handler.normalizeConfigs 归一化 + fillDefaultValues 默认协议 openai/v1(:115-117)。
  3. ai-proxy 插件的 GLOBAL 实例(没有就建空的,setInternal(true):126-133)。
  4. 从全局配置取出 providers 数组,handler.saveConfig 把该 provider 序列化成 Map,按 id 替换或追加(:148-168)。
  5. handler.buildServiceSource 生成对应的服务来源并落库(:172-183)。
  6. handler.buildUpstreamService 生成上游服务名,为该 service 再建一个 ai-proxy 实例绑定 provider(:185-222)。
  7. wasmPluginInstanceService.addOrUpdateAll 落库;必要时 syncRelatedAiRoutes 联动更新引用它的 AI 路由(:234-236)。
读法:一个"加 provider"操作,落地成了:ai-proxy 全局实例的 providers 数组多一项 + 一个服务来源 + 一个 service 维度的 ai-proxy 实例。反向读取 getProviders:387-414)则从这些资源里还原成 LlmProvider 列表。配置常量在 AiProxyConfig.java(providers/id/type/apiTokens/protocol/failover…)。
L05

AiRoute 模型与校验

sdk/model/ai/AiRoute.java(字段 :44-73):name/domains/pathPredicate/headerPredicates/upstreams/modelPredicates/authConfig/fallbackConfig/cors/...validate:75-112):

  • upstreams 非空;
  • upstream 权重之和必须 = 100:99-101);
  • headerPredicates 不能含模型路由头(:90-93)。

AiUpstream.javaprovider / weight / modelMapping(Map<原模型,目标模型>)

读法:modelMapping 很有意思——它能把请求里的模型名改写(比如把 gpt-4 映射到某 provider 的等效模型)。这让"应用写一个模型名,网关按路由映射到不同后端模型"成为可能。
L06

一对多编排(本课最精彩)

AiRouteServiceImpl.writeAiRouteResources:242-250)——一条 AI 路由同时展开成多种资源

一条 AiRoute
Ingress 路由
buildRoute+saveRoute
model-router
模型名→请求头
model-mapper
每上游模型映射
ai-statistics
统计(默认开)
  • writeModelRouterResources:312-341):有 modelPredicates 时配全局 model-router 插件,把模型名映射到 MODEL_ROUTING_HEADER 请求头。
  • writeModelMappingResources:343-381):每个上游在 (ROUTE, SERVICE) 双维度配 model-mapper。
  • writeAiStatisticsResources:383-402):挂 ai-statistics 插件。
为什么一条路由要拆成这么多插件? "AI 路由"是个高层概念,它组合了好几种底层能力:按模型名路由(model-router)、模型名映射(model-mapper)、Token 统计(ai-statistics)。这些能力各由一个 Wasm 插件实现。Console 把"配一条 AI 路由"这个人类友好的操作,翻译成"配好这一组插件实例"——用户不用懂底层有几个插件,界面上就是一条路由。这就是"高层抽象 → 底层资源"编排的极致体现。
L07

Fallback 与 EnvoyFilter

👶 小白 vs 👨‍🏫 老师 👶:fallback(降级)不就是"主的挂了走备用"吗?为什么非要生成一个 EnvoyFilter,直接用 Ingress 注解不行?
👨‍🏫:因为"请求失败后再改道重发"这种逻辑,Ingress 注解表达不了——注解只能描述静态转发规则。
👶:那怎么办?
👨‍🏫:只能下沉到更底层的 Envoy。Console 用 Velocity 模板渲染出一个 EnvoyFilter(直接改 Envoy 配置),配一条用 FALLBACK_FROM_HEADER 匹配的 fallback 路由。备用像"轮胎的备胎":RANDOM 等权重随机挑一个备用,SEQUENCE 按顺序试。

writeAiRouteFallbackResources:252-310):若启用降级,额外建一条"fallback 路由"(用 FALLBACK_FROM_HEADER 头匹配),并用 Velocity 模板渲染出一个 EnvoyFilter(模板 /templates/envoyfilter-route-fallback.yaml:111-127 加载)实现失败重定向。

什么是 fallback(降级)? 如果主 provider(比如 OpenAI)挂了或超时,网关自动把请求转给备用 provider,用户无感。RANDOM 策略给备用上游等权重、SEQUENCE 按顺序试。因为这种"失败后重路由"的逻辑 Ingress 注解表达不了,只能下沉到 Envoy 层——所以 Console 生成一个 EnvoyFilter(直接改 Envoy 配置)来实现。Velocity 模板负责把参数填进 EnvoyFilter 的 YAML 骨架。
读法:别混淆两个 "AiProxy":AiProxyController/aiproxy)是控制台内置 AI 助手的反向代理(把界面上的"体验对话"转发到外部 AI),跟网关的 ai-proxy 插件不是一回事。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • AI 网关解决什么问题?两条主线(Provider/Route)?
  • Handler 注册表为什么用策略模式?加新厂商怎么加?
  • 加一个 Provider 落地成哪几种资源?
  • 一条 AiRoute 展开成哪四件套?为什么要拆这么多插件?
  • Fallback 为什么需要 EnvoyFilter?两个 "AiProxy" 怎么区分?

✋ 动手

cd /Users/bitmart/work/codes/github/higress-group/higress-console/backend/sdk/src/main/java/com/alibaba/higress/sdk
sed -n '59,90p' service/ai/LlmProviderServiceImpl.java
sed -n '104,246p' service/ai/LlmProviderServiceImpl.java
sed -n '242,402p' service/ai/AiRouteServiceImpl.java
明天预告 · Day 12插件管理——WasmPlugin 定义怎么从 classpath 加载、实例怎么映射成 CRD 的 matchRules、以及"JSONSchema 校验"的真相(后端其实是空 TODO!)。
← Day 10 Day 12 · 插件管理 →