我开源了个 AI 模型网关,半年 20 个版本都在改什么?JAiRouter开发进展汇报
前言
2025 年底我开源了 JAiRouter,一个用 OpenAI SDK 就能接入 Ollama、vLLM、GPUStack、Claude、Gemini 等后端的 AI 模型网关。半年过去,从 v2.7.0 走到 v2.9.6,测试用例从 721 涨到 3034。这篇文章不讲版本号罗列,聊聊这半年我们到底在解决什么问题、怎么解决的,以及这个项目现在还在不在活跃。
文章目录
一、为什么会做这个项目
先说我自己的处境,可能也是不少人的处境。
公司里有几套模型服务:本地 Ollama 跑开源模型、GPU 集群上 vLLM 做推理加速、线上还接了 Claude 和几家国内大模型的 API。每个服务都有自己的鉴权方式、错误格式、流式协议细节。应用侧每接入一个后端,就要写一遍适配代码,出问题了还得逐个排查。
于是就有了 JAiRouter:一个统一的网关层,对外只暴露 OpenAI 兼容的 API,对内做路由、负载均衡、限流、熔断、鉴权、监控。应用侧永远只面对一个 base_url,后端怎么变,应用不用动。
这个定位一开始就很明确,半年来的迭代也基本没跑偏——所有功能都是围绕"网关层到底能为 AI 流量多做些什么"展开的。
二、接模型这件事,从写死适配器到 13 个模板
最早适配器是写死的:新增一个后端,改代码、发版。v2.8.0 加了 Claude,v2.8.3 加了 Gemini,都这么干的。但很快发现这条路走不通——市面上的模型服务太多了,DeepSeek、智谱、Kimi、百川、通义、MiniMax、零一万物、硅基流动、Groq、OpenRouter、Together AI……总不能每个都改代码。
v2.8.4 做了插件系统:13 个预置模板,Web 控制台里四步走完——选模板、填地址和 Key、配高级参数、测试连接。PING 通不通、对话能不能通,测试完再保存。
# 适配器定义本质是一段配置,支持 YAML 定义优先合并
adapters:
my-deepseek:
type: openai-compatible # 复用 OpenAI 兼容协议
base-url: https://api.deepseek.com
api-key: ${DEEPSEEK_KEY}
models: [deepseek-chat, deepseek-reasoner]
这套设计的好处是:新增一个后端 = 一段配置,不碰代码;inherits 机制支持继承 + 覆盖,同一家的多个实例不用重复写公共字段。运维同事自己就能完成接入,不用再排队等开发。
三、路由规则:灰度发布终于不用写代码了
如果说接入模型是"广度",那路由规则解决的是"精细化"。
我们的场景很典型:新模型要灰度放量、VIP 用户要锁定到 A100 实例、内网请求走本地模型、某些来源要限流。这些需求以前全靠代码硬编码,改一次发一次版,运营同学等得崩溃。
v2.8.5 开始做可视化规则引擎,之后连续几个版本都在补体验:
-
条件支持模型名、服务类型、请求头、来源 IP、权重,运算符有等于/包含/前缀/正则/CIDR
-
动作支持模型重写、锁定实例、切换适配器、覆盖负载均衡策略,v2.8.8 又加了规则级限流
-
v2.8.6 加了 dry-run:用一条示例请求先模拟一遍,看命中哪条规则,再决定要不要保存
-
v2.8.7 加了三件套:命中统计、优先级拖拽、场景模板
命中统计这功能我自己很喜欢。规则配完之后最怕的就是"配了但没生效",现在 Prometheus 里有 jairouter_rule_hits_total,前端规则列表也有一列实时命中数,一条规则到底有没有被流量打到,一目了然。
# 示例:新模型灰度 10%
- name: canary-new-model
conditions:
- field: weight
operator: ">"
value: "90"
actions:
- type: TARGET_MODEL
value: "new-model-v2"
场景模板里我比较常用的是灰度发布和租户隔离,从模板创建会自动预填参数,改改值就能用。这半年最直观的变化就是:产品同学提的"灰度放量"需求,现在他自己在控制台五分钟就能配完。
四、资源池与 auto-model:异构 GPU 的出路
再往深一层,是资源组织的问题。
公司 GPU 不可能全是同一型号,A100、V100、4090 混着用很正常。不同用户对算力要求不一样:VIP 客户要 A100,内部测试用 V100 就行。手动维护"谁走哪台机器"的路由表,机器一多就崩。
v2.8.9 的解法是资源池 + auto-model:
model:
pools:
vip-pool:
strategy: weighted-random
members:
- instanceId: a100-node-1
weight: 3
- instanceId: a100-node-2
weight: 3
standard-pool:
strategy: round-robin
members:
- instanceId: v100-node-1
规则引擎的 TARGET_MODEL 可以直接指向池名,实现"10% 流量进 vip-pool"。请求带 model=auto-model 时,网关自动从池内健康实例里挑一个执行;池成员被删了会自动跳过。池路由后,下游请求和响应的 model 字段会改写成真实实例的模型名,调用方看到的永远是实际执行的那个模型,不会被 auto-model 这类虚拟名搞晕。
五、v2.9 的重头戏:路由开始"长眼睛"
v2.9.0 到 v2.9.6 这半年最后一段,主线非常明确——让负载均衡从"盲选"变成"有感知地选"。三个功能递进式地解决了三类真实问题。
5.1 缓存亲和:让 KV Cache 不白跑
现在推理引擎普遍支持 Prefix Caching(DeepSeek、vLLM 都有),同一租户连续请求如果命中同一实例的 KV Cache,首 Token 延迟能降一大截。但如果负载均衡把请求随机撒到不同实例,缓存命中率会非常难看——钱白花了。
v2.9.0 做了 StickyLoadBalancer:一致性哈希,按 apiKeyId | serviceType | modelName 把请求固定映射到实例,同一租户的请求尽量留在同一台机器上。
jairouter:
sticky:
enabled: true
affinityKeyScope: tenant_model
顺手把缓存命中指标也暴露出来了:DeepSeek 形态的 prompt_cache_hit_tokens、OpenAI/vLLM 形态的 prompt_tokens_details.cached_tokens,统一转成 jairouter_cache_hit_ratio 这类 Prometheus 指标,Grafana 里直接能看到缓存命中率趋势。缓存命不命中,从玄学变成了可量化、可优化的事。
实现上有个细节:一致性哈希环之前每请求重建,并发读写有风险还费 CPU。后来改成 volatile 快照 + 实例列表指纹缓存,只在实例列表变化时重建,读路径零同步。
5.2 延迟感知:把流量导向快的实例
异构集群里,A100 和 V100 处理同一个请求的耗时差几倍很正常。传统负载均衡不知道谁快谁慢,只能均匀撒。
v2.9.3 加了 latency 策略:按 EWMA(指数加权移动平均)估算每个实例的历史调用延迟,延迟越低被选中的概率越高;失败调用按 30 秒的惩罚值记账,避免刚挂的节点继续被塞流量。
model:
services:
chat:
load-balance:
type: latency
ewma-alpha: 0.2
冷启动没样本时公平探索,ewma-alpha 控制平滑程度。默认策略还是 random,想用哪个服务开哪个,零默认行为变化——这个"默认不开"的保守原则,整个 v2.9 系列一直守着,升级不会莫名其妙变行为。
5.3 请求级故障转移:别在同一个坑里摔三次
之前重试逻辑是"同一实例反复重试"——节点挂了,三次重试全浪费在同一台机器上,客户端体验就是白白等超时。
v2.9.6 改成换实例重试:请求失败后,当前实例进本次请求的黑名单,重新走一遍健康/熔断过滤 + 负载均衡,选一个没失败过的实例重试。4xx 这种不可重试的错误不换实例,单实例或全挂时回退原实例,重试次数仍然由 RetryPolicy 控制。
请求 → 实例A 失败 → 加入黑名单 → 重选 → 实例B 成功 → 返回
这个功能零配置默认启用,重试时自动降级为非 IP 感知,粘性路由在重选阶段自然旁路,互不打架。
六、调用记录:从"存不存"到"怎么存"
调用历史一直有,但以前是全量存,时间一长存储就爆炸,而且请求体响应体这种敏感内容裸存,审计那边过不了。
v2.9.2 改成三档记录级别:
-
METADATA_ONLY(默认):只存元数据,模型、实例、耗时、状态。存储量能降 95% 以上
-
SUMMARY:脱敏后的前 N 字摘要,适合日常审计
-
FULL:完整请求/响应,AES-256-GCM 加密存储,密钥来自环境变量或自动生成
jairouter:
call-history:
record-level: METADATA_ONLY
max-content-length: 64KB
有个值得一提的细节:流式请求以前根本不落历史,v2.9.2 通过入口序列化请求 + 流结束组装文本,把这个缺口补上了。解密查看完整内容有单独的 ADMIN 端点,每次解密访问都会记审计事件——FULL_CONTENT_ACCESS。合规要查的时候能查,平时不裸奔。
七、一些没写进 release notes 的东西
除了上面这些大功能,还有两件事我觉得值得说。
一个是 v2.9.4 的前端重构。以前前端是各种硬编码颜色,改主题色要全局搜索替换。现在建了 40+ 个语义化 Token(--ja-* CSS 变量),Element Plus 的变量也映射过来,暗色模式一次做齐——跟随系统、localStorage 记忆、启动前初始化防闪烁。对使用者来说就是多了个切换按钮,对后续维护来说省了大事。
另一个是代码质量。v2.9.1 把 15 个超过 500 行的大文件拆到只剩 5 个(另外 5 个是高内聚的,硬拆反而伤代码,我们选择豁免)。行为零变更,测试全绿。代码质量这事不产生用户可见的功能,但半年后回头看,能维持一天发六个版本还不翻车,靠的就是这些。
哦对,v2.9.1 到 v2.9.6 里除了 v2.9.0 是 8 月 28 号,其余六个版本都是 8 月 30 号一天之内发的。这么密集的节奏还能保持全绿,3034 个测试用例是底气的来源。
八、这个项目现在什么状态
很多人会关心开源项目是不是已经弃坑了。说几个能看到的信号:
-
发布节奏:v2.7.0(2026-06-30 前后)到 v2.9.6(2026-08-30),20+ 个版本
-
测试规模:从 v2.7.0 的 721 个用例涨到 v2.9.6 的 3034 个,全绿
-
文档治理:v2.9.5 专门做了文档知识库治理,扫描、去重、归档一条龙,还进了 CI——文档不是写完就不管了
-
质量问题:Checkstyle、SpotBugs、JaCoCo 覆盖率的门槛一直在跑
这套流程跑起来之后,新功能开发、测试、文档是同步交付的,所以迭代速度才能一直保持。Roadmap 上还有东西在排,项目处于持续演进状态,不是做完就扔的玩具。
九、怎么上手
Docker 一行启动,不需要写任何配置文件:
docker run -d --name jairouter -p 8080:8080 sodlinken/jairouter:latest
打开 http://localhost:8080/admin(默认账号 admin / ChangeMeOnFirstStartup123456,生产环境记得改),然后:
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8080/v1", api_key="not-needed")
response = client.chat.completions.create(
model="deepseek-chat", # 或 qwen-plus、glm-4、llama3.2...
messages=[{"role": "user", "content": "你好!"}]
)
print(response.choices[0].message.content)
值得试的场景:先在控制台用模板接两三个模型服务,再配一条灰度规则,然后看 jairouter_cache_hit_ratio 或者规则命中数——这套组合拳基本能覆盖网关层 80% 的日常诉求。
结语
这半年 JAiRouter 做的事情,概括起来就一句话:从"能转发请求的网关",变成"会思考的 AI 流量中枢"——接入模型不用写代码,路由规则可视化配置,负载均衡懂得看延迟和缓存亲和,实例挂了知道换人重试,调用记录按需分级加密存储。
如果你的团队也在被多模型接入、灰度发布、异构 GPU 调度、调用审计这些问题困扰,欢迎来试,也欢迎来提 issue、提 PR。
项目地址:https://github.com/Lincoln-cn/JAiRouter
Docker Hub:https://hub.docker.com/r/sodlinken/jairouter
觉得有用的话,点个 Star 就是最大的支持。
更多推荐



所有评论(0)