说实话,这个项目是在一阵烦躁里诞生的。在过去几个月,我同时维护着好几个AI应用,本地显卡上跑着开源模型,云上又开了几家大模型API,于是每天都在跟“用哪个模型更好”作斗争。最后我干脆做了一个端云协同LLM网关,把所有接入收敛成一个统一入口,把请求按隐私敏感度、上下文长度、成本和延迟要求动态路由到本地或云端模型。项目已经开源,代码整理得能跑了,现在最缺的不是新功能,而是3~5位真实业务场景里的开发者,帮我把它扔进生产环境里蹂躏一遍。
这篇文章我会把这个网关为什么这么做、路由逻辑怎么设计、代码里有哪些关键取舍、以及我为什么要小范围招募测试者,一次讲清楚。适合正在做LLM应用、被多模型接入搞到头疼、或者对“本地模型+云端模型混合调度”感兴趣的开发者参考。
1. 为什么我突然想做一个LLM网关:被模型割裂逼出来的开源项目
1.1 我的真实痛点:API key散落、账单混乱、调用代码各写各的
最早其实没有这么复杂。刚开始做AI应用时,模型就一家,SDK调一调完事。后来本地上了显卡,装了Ollama和llama.cpp,云端也开始同时用好几个不同厂商的大模型,问题就全部涌出来了。
首先是接入方式不统一。每个服务连不同的SDK,有的用官方Python包,有的走HTTP接口,有的要封装流式响应,代码风格千奇百怪。我要是想从A模型换到B模型,得逐个项目改依赖、改环境变量、改超时和重试逻辑,改完还可能因为某个参数语义不同而翻车。
其次是成本失控。月底看到云厂商账单,只知道总额,完全分不清是哪个业务线、哪个模型、哪类请求烧的钱。想优化都不知道从哪里下手。
最后也是最核心的痛点:隐私和合规问题。有些业务数据根本不敢上传到云端模型,但本地小模型的能力又不够,导致同一个功能得写两套逻辑——一查内网资料走本地,一查公开信息走云端,维护成本直接翻倍。
我把这些问题憋了一段时间后,决定不再继续打补丁,而是做一个入口:所有业务只跟网关打交道,网关去决定请求该去哪。
1.2 端云协同的本质:本地和云端不是竞品,是分工
很多人一聊到本地模型和云端模型,脑子里就是“谁更强”的对比。我自己的结论是:关键不是哪个更强,而是哪个更合适。
本地模型跑在自己的硬件上,最大的两个优势是私密和离线,而且不管调用多少次,边际成本几乎可以忽略。但它受限于显存和算力,7B、13B级别的模型能跑,70B甚至更大参数的模型基本没戏,推理速度也会随并发上升而急剧恶化。
云端模型恰恰相反。模型版本新、参数大、单次推理质量高,而且部署和运维都省心。但代价是数据要出网,长上下文的调用费用肉眼可见,一旦业务量上来,账单数字是真能让人清醒的。
端云协同这件事的本质,就是不要替用户做“二选一”的决定。把请求拆细:敏感的走本地,复杂的走云端,高频且简单的留在本地,低频且困难的放上云。网关的价值就是把这个决策自动化,业务方只关心“我要什么结果”,不需要关心“这个结果由谁算出来”。
1.3 LLM网关和传统网关有什么区别
“网关”这个词在很多领域都有。做过后台的用过API网关,搞过物联网的接过小米网关,做通信的老哥们可能还弄过短信网关、反垃圾邮件网关,流程引擎里也有BPMN网关。这些网关的共性是:把多个上游聚合成一个入口,然后做鉴权、路由、限流和协议转换。
LLM网关骨子里也是网关,但它多了一个传统网关没有的难点——路由决策不能只看请求的URL、IP或者Header,还取决于请求里的自然语言内容、上下文长度、token成本、数据敏感等级,甚至当前模型的实时负载。同一个用户发“帮我写一封邮件”,发到本地小模型和云端大模型,结果差异巨大,而网关必须能识别出这种差异并做出合理判断。
这也是这个项目和普通API网关本质不同的地方。它不是转发数据包,而是在调度“推理能力”。
2. 端云协同的调度逻辑:让每个请求自己找到对的模型
2.1 路由三步判断:请求身份、查询意图、能力代价
我在网关里实现了一套路由决策框架,思路可以用一个比喻理解——每个请求进来,我都会问它三个问题。这三个问题我习惯比喻成Key、Query、Value。
Key:我是谁。也就是请求的来源和身份。来自哪个业务线、打没打私有标签、数据敏感等级是高是低。这个信息可以由调用方在请求Metadata里显式声明,也可以由网关根据来源IP、接口路径来自动推断。
Query:我要什么。也就是用户的真实意图。是在翻译、写代码、做摘要,还是在闲聊。这部分需要网关对自然语言内容做一次轻量级理解,我目前用一个很轻的embedding模型加上规则兜底,把意图分成十几个大类,不需要调大模型就能完成。
Value:我能付出什么代价。上下文有多长、需要多强的推理能力、能让用户等多久、愿意为这次调用花多少钱。上下文长度会在路由前做预检,如果请求总token数超过本地模型的窗口,就强制上云,这一步必须前置,否则几万token的请求落到本地模型上,直接撑爆显存。
三个维度叠加到一起,一条路由规则才会真正生效。比如“私有知识库问答”这个场景,在配置里就是:Key是internal,Query是summary_or_qa,Value是“上下文可能超过8k,数据不能出内网”,最终结果就是永远路由到本地模型。
2.2 按成本、速度、质量分流:策略配置与对比
为了让策略可写,我按大多数业务的实际需求归纳了三种典型模式:成本优先、速度优先、质量优先。下面这张表可以比较直观地看出它们的区别。
| 路由策略 | 适用场景 | 典型路由倾向 | 示例 |
|---|---|---|---|
| 成本优先 | 日志分析、数据抽取、非核心链路 | 本地模型 或 云端最便宜的档位 | 大批量短文本分类 |
| 速度优先 | 实时对话、客服机器人、交互式工具 | 本地低延迟模型 或 云端就近节点 | 聊天、代码补全 |
| 质量优先 | 复杂推理、长文总结、代码生成 | 云端大参数模型 | 合同审查、架构设计 |
策略写在一个YAML文件里,由网关启动时加载。示例配置如下:
routes: - name: privacy-first match: key_tags: ["internal", "private"] target: local:qwen2.5:7b - name: quality-default match: intent_type: ["code", "reasoning"] target: cloud:claude-sonnet - name: cost-efficient match: intent_type: ["classify", "extract"] target: cloud:gpt-4o-mini - name: fallback target: cloud:gpt-4o配置本身就是路由规则的可视化,往上加规则比改代码直观得多。我在使用中还发现一件事:不要完全迷信各种公开模型榜单来配置“质量优先”,榜单分数只能代表基准测试集,真实业务效果还是得看自己的场景。这也是为什么我一定要找真实开发者测试——公开榜单和实际业务之间,隔着一整个数据集的距离。
2.3 三个典型的端云分流场景
我实际开发中遇到最多的分流场景有三个。
第一个是隐私过滤。用户上传的文档可能包含个人信息或内部敏感内容,不能直接丢给云端模型。网关的做法是:先走一个本地规则引擎做敏感内容扫描,如果命中敏感标签,整个请求就钉死在本地模型;如果文档脱敏后确实需要云端大模型做深度理解,再从脱敏后的内容走云端。这等于在云和本地之间加了一道“隐私闸门”。
第二个是上下文预检。本地模型的上下文窗口有限,显存也有限。一个长文档先丢给embedding模型做切块,每块控制在本地窗口范围内;但如果用户明确要求“总结全文”,切块后的分段摘要又不够,这时候就先把全文走云端。上下文长度是最硬的路由条件,不能靠猜,必须提前算token数。
第三个是高频低成本调度。有些业务调用量巨大但每个请求都很简单,比如批量商品标题分类,这种请求走云端成本非常高,而一个7B模型其实完全能胜任。网关会统计每个意图类型的历史token消耗和延迟,如果发现某类请求的月成本超过阈值,就自动把它默认切换成本地模型。这是端云协同最实在的价值——流量结构一变,路由策略跟着变,账单也会立刻变得温柔。
3. 网关内部怎么工作:连接、流式转发与兜底机制的取舍
3.1 统一接入层:为什么对外暴露OpenAI兼容接口
网关对外提供的是OpenAI兼容接口,也就是/v1/chat/completions和/v1/embeddings。选这个协议没有悬念:现在的LLM生态里,OpenAI兼容接口是事实标准,LangChain、LlamaIndex、各种开源客户端默认都支持。只要网关兼容它,业务方几乎不需要改代码——把base_url指到网关上就完事。
但兼容“看起来简单”和真的兼容是两回事。OpenAI SDK在流式场景下解析的是SSE格式,每一行都是data: {...}。网关做流式转发时,要把上游云端模型的SSE流原样转发给客户端,中间不能丢失数据块,也不能把不完整的数据行发出去。
这里有个特别常见的坑:网络层会把一个SSE事件拆成多个TCP包,如果网关收到一半就把数据往下游扔,客户端就会解析失败。所以我在转发逻辑里加了一个缓冲层,攒到一个完整的data:行再往下游吐。这块代码看起来不起眼,但决定了流式输出能不能稳定跑完一个长对话。
async def forward_stream(payload: dict): async for raw_chunk in upstream_stream(payload): buffer.write(raw_chunk) while buffer.has_line(): line = buffer.pop_line() if line.startswith("data:"): yield f"{line}\n\n"3.2 云端模型归一化与本地模型接入
各家云端模型的接口差异,比我想象中更磨人。鉴权方式不一样,模型命名不一样,限流后的错误码不一样,超时语义也不一样。更麻烦的是,有的厂商流式输出时会把用量信息放在最后的chunk里,有的则完全不提供。
所以我抽了一层ModelProvider抽象,把所有厂商差异都关在里面。对业务和路由器来说,不管背后是哪个模型,接口都是统一的:
class ModelProvider(Protocol): async def chat(self, messages, config) -> ChatResponse: ... async def stream(self, messages, config) -> AsyncIterator[Chunk]: ... def count_tokens(self, text) -> int: ...本地模型的接入相对省心。Ollama和llama.cpp的server都暴露了OpenAI兼容端点,本质上和云端模型走同一套协议。区别在于本地模型需要管理一个“运行状态”:哪些模型已经加载到显存、当前排队请求数是多少、是否因为并发过高而触发保护。
我在这里做了一个很简单的机制:本地模型的同一个实例可以带一个最大并发数,超过之后新请求直接排队而不是继续塞进去。不这么限制的话,并发一高,Ollama会疯狂排队,延迟线性上升,最后把显存也拖垮。
3.3 稳定性三件套:语义缓存、限流与回退链
一个网关如果只是转发请求,那它和普通的端口转发没有区别。真正让它能上生产环境的,是三个稳定性机制。
第一个是语义缓存。很多业务里的重复请求其实是同义改写,比如“这个商品怎么退货”和“退货流程是什么”。如果用传统缓存根本命中不了。网关会对query做embedding,计算向量相似度,超过0.95就直接返回历史结果。这个机制在高频问答场景里能省下大量token,实测命中率能做到30%以上。
第二个是限流。云厂商限制最多的不是QPS,而是每分钟token数。只限QPS的网关很容易“每分钟总共才发了200个请求,结果把100万token的全部分配打光了”。所以我的限流策略是QPS和token速率双控,先根据请求的预估token数做减法,超过预算直接返回限流错误。
第三个是回退链。每个路由目标都可以配置一个后备列表,主模型挂了自动切到备选模型。我目前规定了同一个请求最多回退三次,防死循环;每次向上游发起真实请求前还要做一次幂等判断,防止同一个问题在重试中被重复计费。回退不止要处理“连接失败”,还要处理“上游返回了限流错误码”“上游超时半死不活”这类灰色状态。
3.4 可观测性:没有日志,端云协同就是一团迷雾
端云协同最容易出现的问题,是流量一多之后没人知道请求到底去了哪里。所以我在网关里加了结构化请求日志,每一条请求都记录:来源业务、命中的路由规则、最终模型目标、输入输出token数、延迟、估算成本、是否命中缓存、是否发生回退。
成本估算公式也不复杂,按输入输出token分别计价:
cost = input_tokens / 1000 * input_rate + output_tokens / 1000 * output_rate测试阶段我会让测试者直接用SQLite跑查询,比如“按业务标签统计本月成本Top10”,一条SQL就能做成本归因。这个能力在纯云或纯本地模式下用不到,但一旦混合调度,它就成了判断路由策略是否合理的最重要依据。
4. 为什么开源项目最缺的是3~5位真实开发者测试
4.1 模拟压测和真实用户流量的差距到底在哪
我自己做压测时,脚本发的都是千篇一律的请求,1000次并发请求可能就3种模板。但真实业务根本不是这样——有人发两万个token的长文,有人一句话不到十个字,有人在流式输出到一半时直接关掉页面,有人一边发消息一边疯狂回车造成重复提交。
压测只能验证容量的上限,真实流量考验的是网关在“脏请求”面前的韧性。比如长请求和短请求混跑时的调度优先级,比如上游响应慢时网关是及时释放还是继续傻等。这些场景只能靠真实业务来暴露,我在模拟环境里把测试写得再漂亮,也只是自娱自乐。
这也是我坚持不搞大规模内测、只找3~5个人的原因。人少意味着每个测试者的反馈我都能真的跟进,每个P0问题都能在当天定位到具体环境。开源项目早期最怕的不是反馈太少,而是反馈太多太杂,最后变成一团噪声,反而不知道先改什么。
4.2 端云混合场景最容易翻车的五个地方
根据我自己的使用经验,端云混合调度实际跑起来,最容易出问题的场景有这几个:
- 本地模型在显存不足时会排队而不是报错,并发一上来,延迟就线性拉高,客户端先超时断开,网关却还在傻傻等上游返回。
- 云端API的限流是按分钟token数计算的,如果语义缓存命中率不高,几个高并发请求就能把配额打光。
- SSE流式输出到一半,客户端主动断开,如果网关没有及时取消上游请求,剩余token全部白烧。
- 回退链配置不当会形成A失败退回B、B失败又退回A的循环,账单翻倍,结果还是错的。
- 不同模型的分词器不同,网关自己统计的token数,和云端厂商账单里对不上,导致成本归因失真。
这些问题我都在代码里做了保护,但没有真实流量验证之前,我不敢说覆盖了所有情况。我期待测试者做的,就是拿自己业务里最刁钻的请求来打这个网关。
4.3 只找3~5人的协作效率反而更高
我见过一些开源项目搞几百人的灰度测试群,热闹是热闹,但大多数反馈都停留在“不好用”“希望加个XX功能”这类模糊描述上,真正能帮助项目定位问题的信息非常少。
小而精的测试团队不一样。三个到五个人,每个人我可以一对一拉群,告诉对方网关的路由设计思路,对方也能指着自己的业务场景告诉我哪里不合理。每个P0问题,我能在半小时内拿到对方的完整环境信息并开始复现。这种协作方式,在项目从“能跑”到“扛得住真实业务”的过渡阶段,比任何规模的内测都有效。
5. 测试安排:清单、反馈闭环与协作方式
5.1 第一阶段的实测清单:从部署冒烟到专项压测
整个测试周期预计三周,分三个阶段。
第一周是部署和冒烟测试。我会在仓库里提供完整的Docker Compose启动脚本,测试者把云厂商的API Key填进环境变量,把OpenAI SDK的base_url指到网关地址,跑通一次普通的对话请求和一个流式对话请求就算冒烟过。这一步能最快验证接口兼容性。
第二周是真实业务接入。测试者把网关设为自己应用的默认模型入口,按自己的业务场景配置端云分流规则,然后观察请求日志,核验路由是否落到了预期目标。这一步会暴露最多的路由策略缺陷。
第三周做专项压测和反馈报告。测试项目包括:
- 基础转发:普通对话、流式对话、多轮上下文,OpenAI SDK完全兼容
- 端云路由:配置至少一条分流规则,验证请求落到正确目标
- 回退演练:临时禁用主目标模型,验证fallback链路自动接管
- 语义缓存:重复发送一对同义改写请求,验证缓存命中且token统计下降
- 并发测试:20~50并发下观察延迟和限流表现
- 稳定性测试:连续运行24小时以上,观察内存、连接数、错误日志趋势
每一项我都提前写在TESTING.md里,带checklist和预期结果,测试者直接打勾即可。
5.2 反馈模板:问题分级与信息格式
反馈质量决定了修复速度。我能做的就是降低反馈成本,让测试者用最省力的方式把信息给全。
问题分为四级。P0是崩溃、数据丢失、不可恢复错误;P1是核心流程不可用;P2是功能缺口或体验问题;P3是优化建议。每一个问题都按下面这个模板提交:
环境信息:操作系统/Docker版本/是否使用本地模型 复现步骤:每一步都要可操作 预期结果:你认为应该发生什么 实际结果:实际发生了什么 日志片段:网关日志或客户端报错信息看起来基础,但真的能让issue处理速度快一倍。很多人反馈问题只写一句“流式对话会卡住”,我得花半天去追问环境和复现步骤。模板把该要的信息一次要齐,双方都省时间。
5.3 反馈之后的处理节奏
我承诺的处理节奏是:P0问题24小时内响应,先给出临时规避方案,然后一周内出修复版本;P1问题一周内出修复版;P2问题进入issue列表排期;P3建议汇总后统一评估。
每个周末,我会整理一份测试反馈汇总,列出所有问题的状态:已处理、进行中、待讨论。所有有效反馈的提交者名字,都会写进项目的THANKS文件。这个机制虽然简单,但它让测试者看得到自己的反馈被重视,协作也能持续下去。
6. 已知边界、Roadmap与参与方式
6.1 当前版本的边界和“故意不做”的事情
做开源项目,明确边界和实现功能一样重要。这个版本里,我明确不做这几件事:
- 不做多租户计费中心和用户体系。网关默认单团队使用,鉴权用最简单的API Key,目标是先跑通调度逻辑。
- 不做模型下载和模型文件管理。本地模型权重由测试者自己准备,网关只负责调用本地推理服务的OpenAI兼容端点。
- 不承诺企业级的超高吞吐。目标场景是小团队和独立开发者,先把单机部署的稳定性和调度质量做扎实。
这些边界划出来,是为了避免项目膨胀成一个“配置管理系统”,最后连核心的端云调度能力都没打磨好。等调度逻辑稳定了,再往外扩不迟。
6.2 后续版本规划
v0.2版本我计划做三件事。第一个是语义路由的自动学习:从历史请求日志里自动归纳意图标签,减少手写规则的维护量。第二个是多租户配额控制:让一个网关可以给多个团队共享,每个团队有独立的token预算。第三个是回退链灰度切换:把5%的流量切到新模型,观察P95延迟和结果质量,再决定是不是全量切换。
v0.3我考虑做一个Web控制台,集中展示路由日志、token消耗和成本归因。不过这些计划的优先级,最终都要跟着测试反馈走——如果测试中发现流式转发的稳定性更紧急,那就先修稳定性,功能往后排。
6.3 想加入测试?先看看这几个条件
如果你正在做或准备做一个LLM应用,并且符合下面任意几条,就很适合来参与这个测试:
- 手上已经有LLM应用正在开发或者准备上线,不是纯观望状态
- 能用Docker部署基础环境,编过YAML文件
- 本地有一张能跑7B左右模型的显卡会非常理想;没有显卡也可以先用纯云端模式参与
- 每天愿意投入半小时到一小时调试和反馈
申请时你只需要告诉我三件事:你的项目场景是什么、预计调用量有多高、部署环境是什么样。我会一对一拉群对接,敏感信息不要在公开issue里贴,直接私聊。
仓库里会准备好部署脚本、配置模板、测试清单和一份详细的README。不需要付费,也不需要承担服务器成本,我需要的只是真实业务流量的检验。
说句心里话,开源项目找测试者这件事,最难的其实不是技术,是信任。一个还没经过生产环境验证的网关,能被别人接进自己的业务里,是对这个项目最大的肯定。所以我不打算搞报名从速那一套,就老老实实把代码开源、把反馈透明、把问题修到可追踪。希望这几个“真实测试门票”,最后能被真正需要端云协同能力的开发者接住。