1. 为什么我要把14个免费模型通道塞进一个入口手里攒了一堆免费模型通道这件事本身就挺让人头疼的。我最初的情况是这个平台送一点额度那个平台有每日免费调用量还有一个是社区维护的公益接口再加上几个自己申请到的测试key零零散散加起来有十四个能用的通道。每次写代码要调用模型得先想清楚这次用哪个、key放在哪、额度还剩多少、这个通道今天是不是又限流了。切换成本高到离谱有时候调试一个prompt光是在不同通道之间来回试就花掉半小时。后来我干脆花了一个周末把这十四个通道全部收敛到一个本地入口对外只暴露一个地址、一套鉴权内部按任务类型自动路由。做完之后的效果是我所有的脚本、工具、甚至一些日常的小自动化都只需要认准这一个入口剩下的选通道、切key、处理限流、失败重试全部在网关层消化掉。这篇文章就是把这套东西的完整思路、配置细节、踩过的坑原原本本讲清楚。WorkBuddy在这套方案里扮演的是“工作台”的角色它本身不是一个模型而是一个把各种能力编排起来的壳。我把它理解成一个调度中心你告诉它要干什么它去决定用哪个模型、走哪条通道。而models.json就是这套调度逻辑的配置文件所有通道的定义、优先级、路由规则都写在这里面。自动路由是核心机制也是整套方案里最值得花时间打磨的部分。这套东西适合谁如果你手头有多个免费模型来源又不想每次手动切换如果你在写一些需要稳定调用模型的小工具但预算有限只能用免费额度如果你对网关这个概念有基本认知知道它是流量入口和转发层——那这篇内容基本可以照着抄。不需要你懂多深的网络原理但需要你愿意动手改配置文件、看日志、做测试。我先把整体架构用一句话说清楚一个本地服务作为统一入口读取 models.json 里的通道定义根据请求里的任务标签和当前各通道的健康状态决定这次请求发给谁失败就换下一个全程对调用方透明。下面从设计思路开始拆。2. 整体设计与路由思路拆解2.1 为什么是“网关”而不是“脚本里写死”最开始我试过最笨的办法在每个脚本里写一个通道列表用随机或者轮询的方式选一个。这个方案跑了不到三天就崩了。原因很现实免费通道的状态是动态的。有的通道上午还活着下午就返回429有的通道对某些模型支持好对另一些模型直接报错还有的通道响应特别慢但质量高适合做需要仔细想的任务。把这些逻辑散落在各个脚本里意味着每加一个通道、每改一次规则都要去改所有脚本。这违背了最基本的工程直觉变化的东西要收敛到一处。网关的价值就在这里——它是唯一知道所有通道状态的地方也是唯一需要处理路由决策的地方。调用方只管发请求不关心背后是谁在干活。另一个考虑是鉴权。十四个通道意味着十四套key散落在各处本身就是安全隐患。收敛到网关之后key只存在于网关的配置文件里调用方只需要一个统一的token。哪怕这个token泄露了我也可以在网关层直接吊销不用去每个平台重新生成key。2.2 路由决策的三个维度自动路由不是简单地“随便挑一个”我最终定下来的决策依据是三个维度按优先级从高到低第一个维度是任务类型。这是最硬性的约束。有些通道只支持对话补全有些支持函数调用有些对长上下文支持好。如果请求里明确标注了“这是一个需要长上下文的任务”那路由就必须排除掉那些上下文窗口小的通道。这个维度是过滤性的不满足直接淘汰。第二个维度是通道健康度。每个通道我维护一个健康分数初始都是满分。每次调用成功加分失败扣分连续失败到阈值就临时拉黑一段时间。这个分数是动态的每次请求都会重新计算可用通道列表。第三个维度是成本与配额。免费通道也有额度限制有的按天有的按月。我会给每个通道设置一个配额上限接近上限时降低它的优先级用完则暂时移除。这样能保证不会因为某个通道被薅爆而影响整体可用性。这三个维度组合起来路由逻辑就清晰了先按任务类型过滤再按健康度排序最后按配额情况微调优先级选出一个最合适的通道。如果选中的通道调用失败自动降级到下一个直到成功或者全部失败。2.3 models.json 的结构设计配置文件的设计直接决定了这套东西好不好维护。我见过有人把配置写成一大坨嵌套很深的JSON改一个参数要数半天括号。我的原则是扁平、可读、每个通道一段独立配置。顶层是一个对象里面有一个channels数组每个元素是一个通道的完整定义。每个通道包含这些字段唯一标识、类型、接入地址、鉴权信息、支持的模型列表、能力标签、配额设置、健康度初始值。路由规则单独放在routing字段里和通道定义分开这样改规则不用动通道。我特意没有用太复杂的继承或者引用机制。免费通道的数量是有限的十几个而已重复写一些字段完全可以接受换来的是配置文件一眼能看懂。维护成本低比“优雅”重要得多。3. 核心细节解析与实操要点3.1 通道定义的字段逐个说清楚每个通道的定义看起来简单但有几个字段如果理解不到位后面路由会出各种奇怪的问题。我拿一个典型的通道配置来逐字段解释。id是通道的唯一标识我习惯用“平台名-用途”的格式比如alpha-chat、beta-long。这个id会出现在日志里所以起名要有意义别用channel1这种出问题的时候根本不知道是谁。type标识通道的协议类型。虽然大部分免费通道都兼容同一套接口规范但细节上还是有差异比如有的对请求体的某些字段敏感有的返回格式略有不同。我在网关层做了适配每个type对应一个适配器把差异消化掉。endpoint是接入地址。这里有个坑有些免费通道的地址会变或者有多个备用地址。我的做法是允许endpoint是一个数组路由时按顺序尝试。这个设计后来救了我好几次某个地址突然不通的时候自动切到备用地址调用方完全无感。models是这个通道支持的模型列表。这个字段直接参与任务类型过滤。如果请求指定的模型不在某个通道的列表里这个通道直接被排除。所以这个列表要维护准确不能偷懒写个通配。capabilities是能力标签比如long-context、function-call、vision。这些标签是路由过滤的依据。我一开始没重视这个字段结果有一次一个需要函数调用的任务被路由到了一个不支持函数调用的通道返回了一堆莫名其妙的错误。后来我把能力标签作为硬性过滤条件这类问题就再没出现过。quota定义配额包含limit总量、used已用、period周期day或month。网关每次调用成功后会更新used接近limit时降低优先级。这个字段需要持久化我用了最简单的本地文件存储每次更新写回虽然不够高效但足够可靠。health是健康度配置包含score当前分数、threshold拉黑阈值、cooldown冷却时间。健康度的更新逻辑后面单独讲。3.2 路由规则的写法与优先级路由规则我放在routing字段里是一个规则数组按顺序匹配命中第一条就停止。每条规则包含match和target两部分。match描述什么条件下触发target描述选哪些通道。match支持的条件有任务类型、指定模型、能力要求、优先级标签。比如一条规则可以写成“当任务类型是 long-task 且需要 long-context 能力时优先选择带 high-quality 标签的通道”。target则是一个通道id的列表按优先级排列。这里有个设计决策值得说我为什么用“规则数组顺序匹配”而不是“打分排序”。打分排序看起来更智能但调试起来很痛苦——你很难解释为什么这次请求走了A通道而不是B通道。规则数组的好处是决策路径完全透明日志里直接打印命中了哪条规则一目了然。对于十几个通道的规模规则数组完全够用不需要过度设计。规则的顺序很重要。我把最具体的规则放在前面最通用的兜底规则放在最后。兜底规则通常就是“所有通道按健康度排序”保证任何请求都有通道可用。3.3 健康度与配额的联动机制健康度和配额这两个维度如果各自独立工作会出现一种尴尬情况一个通道健康度很高但配额快用完了路由还是优先选它结果调用失败然后降级。虽然最终能成功但浪费了一次调用。我的做法是让两者联动。计算通道优先级时用一个综合分数健康度分数乘以配额剩余比例。配额充足时这个乘数接近1不影响健康度的排序配额紧张时乘数变小自然降低优先级。这样不需要额外的规则配额的影响就平滑地融入了路由决策。健康度的更新我用了简单的加减分机制。成功一次加1分失败一次扣5分分数上限100低于20分触发拉黑冷却时间30分钟。冷却结束后分数重置为50给通道一个恢复的机会。这些数字不是拍脑袋定的是我观察了一段时间的调用日志后调的。失败扣分比成功加分重是因为免费通道的失败往往意味着它暂时不可用需要更快地把它排除出去。注意健康度分数一定要持久化。我一开始放在内存里网关重启后所有通道都恢复满分结果重启后连续踩了好几个已经挂掉的通道。后来改成每次更新都写文件重启后状态还在。4. 实操过程与核心环节实现4.1 环境准备与依赖选择整套东西我用的技术栈很朴素Python加上几个基础库。选Python不是因为性能而是因为改起来快。免费通道的接入方式经常变用Python可以随时改适配器不用编译不用打包。对于个人使用的网关每秒几个请求的量级Python完全够用。依赖方面我只需要一个HTTP客户端和一个Web框架。HTTP客户端用的是标准库之外的轻量选择Web框架也是极简的那种。我刻意避免引入重型框架因为这东西的核心逻辑就是“收请求、做决策、转发、返回”不需要ORM、不需要模板引擎、不需要复杂的中间件。目录结构是这样的根目录下放models.json配置文件gateway.py是主程序adapters/目录下每个type一个适配器文件logs/放日志state/放健康度和配额的持久化文件。这个结构简单到任何人拿到都能在五分钟内看懂。4.2 请求处理流程的完整拆解一个请求进来网关的处理流程分六步我按顺序讲。第一步是解析请求。调用方发来的请求里除了标准的模型调用参数还带了一个自定义的task_hint字段用来标注任务类型。这个字段是可选的没有的话走默认路由。解析完请求后网关提取出模型名、任务类型、能力要求这些路由需要的信息。第二步是过滤通道。遍历所有通道排除掉不满足硬性条件的模型不支持、能力不匹配、处于拉黑状态、配额已用完。这一步之后剩下的通道就是候选集。第三步是应用路由规则。按顺序匹配routing里的规则找到第一条命中的从它的target列表里选出候选通道。如果所有规则都没命中用兜底规则即候选集按综合分数排序。第四步是选择通道。从候选列表里选综合分数最高的。如果分数相同选最近使用次数少的做个简单的负载均衡。第五步是转发请求。用对应type的适配器把请求转换成目标通道需要的格式带上该通道的鉴权信息发出去。这里要设置合理的超时时间免费通道有时候会卡住超时时间太长会拖垮整个网关。我设的是15秒超过就当作失败处理。第六步是处理响应和更新状态。成功的话更新健康度加分、配额已用加一把响应转换回标准格式返回给调用方。失败的话更新健康度扣分然后从候选列表里移除这个通道回到第四步重新选择直到成功或者候选列表为空。如果全部失败返回一个明确的错误告诉调用方所有通道都不可用。这个流程里第五步和第六步的循环是自动降级的关键。调用方完全感知不到背后换了通道它只看到最终的成功响应或者全部失败的错误。4.3 适配器层的实现要点适配器层是整套方案里最“脏”的部分因为每个免费通道的接口细节都不一样。有的要求鉴权放在header里有的放在query参数里有的返回的JSON结构多一层包装有的直接就是标准格式有的对请求里的某些字段特别敏感多传一个就报错。我的做法是给每个type写一个适配器类实现两个方法prepare_request和parse_response。前者把标准请求转换成目标通道的格式后者把目标通道的响应转换回标准格式。适配器里可以写各种if-else来处理细节差异因为这部分代码是隔离的脏一点没关系不影响主流程。写适配器的时候有个经验先把一个通道调通再抽象。我一开始想设计一个通用的适配器基类结果发现每个通道的差异太大抽象出来的东西反而更难维护。后来改成先针对单个通道写死跑通之后再提取公共部分。这样写出来的适配器虽然有一些重复代码但每个都独立可测改一个不会影响另一个。4.4 配置文件的加载与热更新models.json在网关启动时加载。但我经常需要调整路由规则或者临时禁用某个通道如果每次都要重启网关就太麻烦了。所以我加了一个简单的热更新机制网关监听配置文件的修改时间发现变化就重新加载。热更新有个坑要注意重新加载时不能直接替换正在使用的配置对象否则正在处理的请求可能会读到一半新一半旧的配置。我的做法是加载到一个新的配置对象加载成功后再原子性地替换引用。Python里可以用一个简单的锁来保证这一点。配置加载失败的处理也很重要。如果新的配置文件有语法错误不能直接让网关崩溃。我的做法是捕获加载异常保留旧配置继续运行同时打一条错误日志。这样即使我改错了配置网关也不会挂掉只是新配置不生效而已。5. 常见问题与排查技巧实录5.1 通道全部失败怎么办这是最让人紧张的情况所有通道都返回失败调用方拿到一个错误。遇到这种情况我按这个顺序排查。先看日志里每个通道的失败原因。如果都是超时那可能是网络问题检查一下本机的网络连接。如果都是鉴权失败那可能是某个key过期了需要去对应平台重新生成。如果失败原因各不相同那可能是请求本身有问题比如模型名写错了、参数格式不对。我遇到过一次所有通道都失败排查了半天发现是请求里的task_hint字段值写错了导致路由规则全部不匹配兜底规则又因为某个bug没有生效。这个教训让我在兜底规则里加了一条日志每次兜底触发都打印出来方便发现这类问题。还有一种情况是某个通道的失败被误判为全部失败。比如通道A失败了降级到通道B但通道B的响应解析出了bug被当成失败继续降级到C最后全部失败。实际上通道B是成功的只是解析错了。这类问题要靠单元测试来防每个适配器的parse_response都要有测试用例。5.2 路由结果不符合预期怎么查路由不符合预期通常是规则写错了或者通道的能力标签配错了。我的排查方法是在网关里加一个调试接口传入一个模拟请求返回完整的路由决策过程——哪些通道被过滤了、为什么被过滤、命中了哪条规则、最终选了谁。这个接口在调试路由问题时极其有用。有一次我发现一个长上下文任务被路由到了一个上下文窗口很小的通道查了半天发现是那个通道的capabilities里误加了long-context标签。这种配置错误靠看日志很难发现因为路由逻辑本身是对的只是输入数据错了。调试接口能直接告诉你“这个通道因为带有long-context标签而被选中”问题就一目了然了。5.3 配额统计不准的问题配额统计不准通常有两个原因一是并发请求导致的计数丢失二是持久化时机不对。并发问题可以用锁解决每次更新配额时加锁保证计数准确。持久化时机我改过一次一开始是每次更新都写文件后来发现请求量大时IO成为瓶颈改成批量写攒够一定次数或者每隔几秒写一次。但这样又带来了新问题网关崩溃时可能丢失最后几次的计数。权衡之后我改回了每次写因为免费通道的请求量不大IO压力可以接受准确性更重要。5.4 常见问题速查表问题现象可能原因排查方法解决方式所有通道失败网络问题、key过期、请求格式错误看日志里各通道的失败原因针对性修复检查兜底规则路由结果不符预期规则顺序错误、能力标签配错用调试接口查看决策过程修正规则或标签配额统计不准并发计数丢失、持久化时机检查锁的使用和写文件频率加锁、改回每次写通道健康度不恢复冷却时间未到、分数重置逻辑错误检查state文件里的分数和时间戳修正冷却逻辑热更新不生效文件监听失败、加载异常被吞看日志里有没有加载成功的记录检查文件权限和JSON语法响应解析错误适配器parse_response有bug对比原始响应和解析结果修适配器加测试用例提示这张表里的问题我基本都遇到过其中“路由结果不符预期”和“配额统计不准”是最耗时的。建议在搭建初期就把调试接口和日志做好后面排查问题会省很多时间。5.5 几个让我印象深刻的坑第一个坑是时区问题。配额是按天重置的我一开始用本地时间判断是否跨天结果有次服务器时区变了配额重置逻辑乱了导致某个通道的配额被重复计算。后来统一用UTC时间问题解决。第二个坑是响应体过大。有个免费通道返回的响应里带了很多调试信息响应体特别大网关转发时内存占用飙升。后来在适配器里加了裁剪逻辑只保留需要的字段。第三个坑是重试风暴。某个通道失败后降级到下一个下一个也失败又降级短时间内对多个通道发起大量请求。虽然每个通道只试一次但整体请求量还是很大。后来加了降级之间的短暂延迟避免瞬间打爆所有通道。6. 后续可以继续打磨的方向这套东西跑了一段时间基本满足了我的需求。如果继续打磨我会从这几个方向入手。第一个是更细粒度的健康度。现在的健康度是通道级别的但同一个通道对不同模型的表现可能不一样。可以细化到“通道模型”级别的健康度这样路由更精准。第二个是请求内容的感知路由。现在路由主要靠调用方传的task_hint如果调用方不传就只能走默认规则。可以加一个轻量的内容分析根据请求里的文本长度、是否包含代码、是否是多轮对话自动推断任务类型。这样调用方不用改代码就能享受智能路由。第三个是更完善的监控。现在只有日志没有可视化的监控面板。可以加一个简单的状态页面展示各通道的健康度、配额使用情况、最近的调用成功率。这样一眼就能看出哪个通道有问题。第四个是配置的版本管理。models.json改来改去有时候想回滚到之前的版本。可以加一个简单的版本快照机制每次修改前自动备份需要时一键回滚。我在实际使用中最大的体会是这套东西的价值不在于技术多复杂而在于把分散的、易变的、需要人工判断的事情收敛成了一个自动化的、可观测的、可配置的入口。省下来的切换时间和排查时间远超搭建它花的那一个周末。如果你手头也有多个免费通道在来回切换强烈建议花点时间做类似的收敛哪怕一开始只支持三四个通道后面慢慢加收益是持续的。