做 AI 应用这几年,我一直有个固执的念头:与其拼命调整一个大模型去覆盖所有场景,不如让几个开源模型各管一摊,再通过统一的调度层把它们变成一支协同作战的舰队。K2 Horizon 这个项目,就是在这个念头下折腾出来的产物。项目的核心是用 01.AI 开源的 K2 模型作为协调中枢,搭配五个各有所长的开源模型,组成一支"六模型舰队",任务进来先做意图判断,再按类型路由给最合适的模型执行,必要时串联多个模型完成复合能力。
这套设计解决的问题很现实:单模型方案要么贵要么慢,更常见的是偏科——数学推理强的模型写代码一般,代码模型看不懂图,通用对话流畅的模型在复杂逻辑上一本正经地胡说八道。K2 Horizon 用"路由+分工+兜底"三层机制,把这些问题拆开解决。整个过程从方案设计到跑通大概花了两周,接着稳定运行了三个月,中间踩了不少坑。这篇文章会把整体架构、六模型的分工逻辑、连接层的具体实现、部署调优和排障实录一次性写清楚,适合正在做多模型服务、Agent 编排,或者想在公司内部搭一套私有模型网关的工程师参考。
1. 为什么是"舰队"而不是"单模型"
1.1 单模型方案的三个硬伤
先说结论:如果你的业务只有一种固定任务,比如只做客服问答,那老实调一个大模型就够了,搞多模型纯粹是给自己找事。但一旦任务类型开始分化,单模型方案就会暴露出三个很明显的硬伤。
第一个是算力和质量的矛盾。所有请求都走同一个大模型,简单查询也消耗同样的推理资源,高峰期排队问题立刻出现。可如果为了速度换小模型,复杂任务的质量又接不住。更难受的是参数无法按任务区分——同一个温度系数,写代码要的是确定性和严谨,写营销文案要的是发散和创意,一个模型很难两头兼顾。我最早用单个 72B 模型跑全量请求,简单问答的首 token 延迟经常飙到 3 秒以上,明显是"杀鸡用牛刀"。
第二个是模态割裂。纯文本模型处理带图请求非常痛苦,我最初的方案是先用 OCR 服务把图里的字抽出来,再把文本塞给大模型。这套流程维护成本极高,OCR 识别错一个字,后面的推理就全错。图表、截图、票据这类结构化图片,OCR 的还原度永远达不到需求。
第三个是故障爆炸半径。单模型一旦出问题,整个服务全部瘫痪,没有任何回退余地。一次偶然的模型推理异常,就可能导致线上大面积超时。这些问题叠加在一起,让我最终决定换成多模型舰队架构。
1.2 舰队模式的设计取舍
多模型协同主要有三种模式:路由分发、并联投票、串联流水线。我在 K2 Horizon 里以路由为主,局部用投票和串联补强。路由分发的核心是一个调度层,每次请求先进来,判断任务类型后再转发给对应的模型,这套方式对延迟和成本最友好;并联投票是让多个模型对同一个问题各自输出,再按多数或置信度取结果,适合内容审核、事实校验这类不能出错的场景;串联流水线则是把任务拆成多个阶段,比如先 OCR 再推理,先规划再执行。
为什么不是全都上?原因很直接:并联投票的成本是成倍增长的,每次请求都要调动两三个大模型,托底场景可以接受,日常请求根本吃不消。串联流水线则会显著放大延迟,链路里任何一个环节慢了,整个请求都被拖住。所以我最终确定的原则是:90% 的流量走单模型路由,关键判断走一次双模型交叉验证,复杂任务最多串联两个模型。
这里必须强调一点:路由决定成败,路由判错一次,后面所有模型再强也没有意义。所以路由层我单独花了很多精力打磨,这块放在第 3 章详细讲。
2. 六模型分工与选型复盘
2.1 选型标准:不追最强,只追最对
六个模型不是随便凑的,选型时我给自己定了四条硬标准。第一,许可证必须商用友好,首选 Apache 2.0 和 MIT,一定要避开传染性强的协议,不然上线前法务会让你把整个项目翻一遍。第二,生态成熟度,模型必须能被 vLLM 直接加载,量化方案要齐全,社区用户基数大,出了问题能搜到答案。第三,硬件适配,团队实际能用的 GPU 就那几台,模型再强塞不进去也是白搭。第四,模型之间的能力要有区分度,不能选六个同质化模型,否则舰队就是摆设。
基于这四条标准,我最后定的六名成员是:K2 做中枢协调和复杂推理,Qwen2.5-72B 做通用对话,DeepSeek-R1-Distill-Qwen-32B 做数学和深度推理,Qwen2.5-Coder-32B 做代码相关任务,MiniCPM-V-2.6 做视觉理解,BGE-M3 做语义向量和路由判断。K2 是 01.AI 开源的新一代 MoE 模型,激活参数只有 32B,走 4bit 量化之后实际可以在 8×80G 单机跑起来,作为舰队的大脑正合适。
2.2 六模型角色分配表
每个模型承担的角色和部署方式,我整理了一张表,方便对照。
| 模型 | 角色 | 部署方式 | 上下文 | 核心职责 |
|---|---|---|---|---|
| K2 (01.AI) | 中枢协调/复杂推理 | vLLM,TP=8,AWQ 4bit | 256K | 意图兜底分类、复杂推理、结果汇总 |
| Qwen2.5-72B-Instruct | 通用对话 | vLLM,TP=2 | 128K | 日常问答、摘要、改写、情感分析 |
| DeepSeek-R1-Distill-Qwen-32B | 深度推理 | vLLM,TP=1 | 128K | 数学题、逻辑推理、多步规划 |
| Qwen2.5-Coder-32B-Instruct | 代码生成 | vLLM,TP=1 | 128K | 代码补全、解释、测试用例生成、重构 |
| MiniCPM-V-2.6 | 视觉理解 | vLLM,TP=1 | 8K | 图像内容识别、图表理解、OCR |
| BGE-M3 | 语义向量/路由 | FlagEmbedding | 8K | 意图向量化、相似度计算、路由匹配 |
这套组合覆盖了文本、代码、视觉、检索四类能力,选型的一个重要心法是:模型参数不是越大越好,而是越匹配越好。代码任务用专门的代码模型,比通用大模型又快又准;数学推理用带思维链的蒸馏模型,比直接问对话模型稳定得多。
2.3 硬件规划与部署分布
硬件方面我用的是一台 8 卡 80G 节点跑 K2,一台 4 卡 80G 节点跑三个文本模型,外加一张 4090 跑视觉模型和向量模型。4 卡节点上三个模型不能同时常驻,否则显存会爆,我做了冷热分离——Coder 和 DeepSeek 按流量时段拉起,Qwen2.5-72B 常驻承接主要流量。4090 这张卡比较紧张,MiniCPM-V-2.6 和 BGE-M3 共享,根据请求特点设置进程优先级,视觉请求多发时向量服务自动排队。
显存分配的核心原则是给 K2 留足余量。K2 走 AWQ 4bit 量化后,权重大约占用 500GB,8 卡还剩约 140GB 给 KV cache 和激活值。实测最大上下文长度拉到 131072 时,并发 8 个请求就开始吃紧,所以我平时把 K2 的 max-model-len 限制在 65536,既保住了长文档分析能力,又留出了并发余量。
3. 连接层设计:让六个模型"说同一种话"
3.1 路由层怎么判任务类型
六个模型各说各话,连接层要做的就是让它们听懂同一个指令。路由层我试过三种方案:纯规则匹配、小模型分类、向量相似度匹配。纯规则匹配的问题是一戳就破,用户换个说法规则就失效;小模型分类准确率还行,但需要维护训练数据和模型版本;最后我选了 BGE-M3 做向量相似度匹配。
具体流程是这样的:BGE-M3 先把用户输入编码成向量,然后和预置的七类意图模板向量算余弦相似度,超过 0.78 的阈值就直接返回对应任务类型,低于阈值则升级给 K2 做兜底分类。这套混合方案的好处是简单请求完全不用惊动 K2,复杂请求又能得到模型的理解能力。我准备了大概 200 条意图模板,覆盖聊天、推理、代码、视觉、摘要、安全、未知七类,实测路由准确率在 94% 以上。
之前踩过一个很深的坑:阈值定得太低,导致很多明显是闲聊的请求被路由到了 K2,一个顶俩的成本瞬间把预算烧穿。后来我把阈值调高,并且加了一条规则——如果用户请求长度小于 20 个字且没有明显代码或图片特征,默认走通用对话模型,这样既省成本又降低了误判概率。
3.2 统一消息协议与上下文隔离
模型之间要协作,必须有一套统一的消息格式。我设计了一个 JSON 信封,包含 request_id、task_type、payload、history、meta 和 fallback_ok 六个字段。request_id 全链路透传,排查问题全靠它串日志;task_type 是路由层给出的任务类型,下游模型必须严格遵守;history 按任务类型分别维护,绝不让对话上下文和代码上下文混在一起。
上下文隔离这块特别重要。最开始我把所有历史消息一股脑塞给下一个模型,结果代码任务的上千行上下文把对话模型的窗口撑爆,导致回答质量断崖式下降。后来改成按任务类型分桶存储会话,Redis 里每个桶一个 key,TTL 24 小时自动过期,转发时只带当前任务类型的上下文。这个改动让端到端成功率提升了将近 10 个百分点。
超时和熔断机制也是连接层的重头戏。每个模型都有独立的超时配置:K2 给 120 秒,文本模型 60 秒,视觉模型 30 秒。连续 5 次超时自动触发熔断,后续请求直接跳过该模型走 fallback 链路,熔断状态每 30 秒探测一次,模型恢复后自动放量。
3.3 核心编排代码示例
路由和编排的核心逻辑并不复杂,关键在于把降级路径设计好。下面是路由判定的简化代码:
# router.py 简化版 from dataclasses import dataclass @dataclass class RouteResult: target: str confidence: float async def route_query(query: str, embedding_fn, intent_templates: dict) -> RouteResult: query_vec = await embedding_fn(query) best_target, best_score = None, 0.0 for intent, template_vec in intent_templates.items(): score = cosine_similarity(query_vec, template_vec) if score > best_score: best_target, best_score = intent, score # 相似度足够高,直接路由 if best_score >= 0.78: return RouteResult(target=best_target, confidence=best_score) # 相似度不够,升级给 K2 兜底分类 return RouteResult(target="k2_fallback", confidence=best_score)请求进来之后,编排层拿到路由结果再做分发。每个任务类型我都配了一条 fallback 链:
- reasoning 任务:DeepSeek-R1-32B 为主,失败切 K2,再失败切 Qwen2.5-72B
- code 任务:Coder-32B 为主,失败切 K2
- vision 任务:MiniCPM-V-2.6 为主,失败返回明确错误并建议用户上传更清晰的图片
- chat 任务:Qwen2.5-72B 为主,失败切 K2
fallback 链最多跳一次,绝不允许无限递归。这个设计是用一次线上事故换来的教训:有一版代码没有限制跳转次数,一个模型超时后连续触发了三个模型,整个链路雪崩了十几秒。现在所有 fallback 都带深度计数,超过一跳直接返回友好错误。
3.4 输出格式化与参数控制
六个模型输出风格千差万别,有的喜欢在 JSON 外面包一层 markdown 代码块,有的会在回答后面追加解释。我的处理方式是双保险:提示词里明确要求"只输出 JSON,不要任何额外说明",同时在代码层写了一个 extract_json 函数,用正则把最外层的大括号内容抠出来再解析。凡是解析失败的,走一次轻量修复——补全缺失的引号、去掉尾逗号,再不行就重新请求一次。
每个模型的采样参数也做了差异化配置:代码和数学任务温度设为 0.1,保证确定性;通用对话用 0.7,保持自然;创意写作单独开一个 0.9 的通道。分类类任务温度必须设为 0,否则同样的输入两次分类结果可能不一致,下游体验会很差。这套参数矩阵放在配置文件里,改参数不用动代码。
4. 实操落地:从部署到上线的完整记录
4.1 部署顺序与验证清单
六个模型的部署顺序很有讲究,我的原则是"先易后难,先外围后核心"。最先部署 BGE-M3,它是路由层的基石,没有它所有请求都进不了分发流程;接着部署 MiniCPM-V-2.6,视觉链路独立性强,容易验证;然后是三个文本模型,最后才是 K2。每部署一个模型,先跑一组 smoke test,验证 vLLM 接口连通性、首 token 延迟、上下文长度和并发表现,通过之后再接入路由层。
K2 的 vLLM 启动命令我贴在这里,注意 tensor-parallel-size 必须和 GPU 数量一致,否则直接报错:
python -m vllm.entrypoints.openai.api_server \ --model /models/K2 \ --tensor-parallel-size 8 \ --quantization awq \ --max-model-len 65536 \ --gpu-memory-utilization 0.92 \ --served-model-name k2部署完成后我建了一份验证清单,逐项打勾:模型能正常加载、第一次推理不超 5 分钟、连续 10 次推理结果稳定、上下文截断位置正确、并发 5 个请求不报显存错误。这套清单后来成了团队其他项目上线模型的通用模板,省了很多重复排查的时间。
4.2 性能实测数据与调优
稳定运行后,我统计了一组端到端延迟数据,覆盖六类主要任务,作为容量规划的参考。
| 任务类型 | 路由模型 | 平均首token延迟 | 平均总耗时 | 建议并发上限 |
|---|---|---|---|---|
| 简单问答 | Qwen2.5-72B | 0.8s | 3.2s | 20 |
| 数学推理 | DeepSeek-R1-32B | 1.5s | 9.8s | 10 |
| 代码补全 | Coder-32B | 0.9s | 4.1s | 15 |
| 图像识别 | MiniCPM-V-2.6 | 0.6s | 2.5s | 10 |
| 复杂推理 | K2 | 2.1s | 18.6s | 8 |
| 向量检索 | BGE-M3 | 30ms | 80ms | 200 |
从数据能看出来,K2 虽然最慢,但也只承接了少量高价值请求。流量分布上,大约 65% 的请求落在 Qwen/Coder/视觉模型上,20% 落在 DeepSeek,只有 15% 走 K2。这个比例是刻意控制的结果,因为算力要优先保障高频低成本的路径,让便宜的模型养着贵的中枢模型。
调优过程中做的最有效的一件事是给每个模型单独配置了队列长度和排队超时。vLLM 默认的队列策略是公平调度,但遇到突发流量时,简单请求会被长推理请求堵在后面。我给每个模型设了独立的 max_queue_size,并让编排层按任务优先级插队——视觉和简单问答优先,代码和深度推理靠后。这个改动让 P95 延迟下降了约 30%。
4.3 评测集与回归机制
多模型系统的质量评估比单模型麻烦得多,因为一个请求可能经过路由、模型、后处理三个环节,问题出在哪一层需要能定位。我建了一个 200 条的评测集,按任务类型分类,每次改动路由逻辑或替换模型,先跑一遍回归。评测维度有四项:路由准确率、端到端成功率、超时率、fallback 使用率。
最初端到端成功率只有 67%,大量失败集中在输出解析和上下文截断上。加了解析兜底和上下文隔离之后,成功率逐步爬到 93%。剩下的 7% 失败里,一半是视觉模型的图片预检问题,一半是 K2 在超长上下文下的偶发推理异常。评测集的作用不只是把关,更是给优化提供数据支撑——没有评测,你根本说不清改动到底是变好了还是变坏了。
5. 常见问题与排查技巧实录
5.1 六类高频问题速查表
运营三个月,我把遇到的高频问题整理成了一张速查表,每种问题都对应着根因和解决办法。
| 问题现象 | 根因 | 解决办法 |
|---|---|---|
| 请求排队越来越慢 | 某个模型负载过高,上游重试叠加 | 限流 + 按优先级插队 + 超时熔断 |
| 模型输出 JSON 解析失败 | 模型吐了 markdown 代码块或多余文本 | extract_json 正则兜底 + 提示词约束 |
| 模型回答突然变差 | 上下文混入了其他任务类型的历史 | 按任务类型分桶隔离 + 定期清理会话 |
| 简单请求误路由到 K2 | 向量相似度阈值偏低 | 调高阈值 + 短文本默认走通用模型 |
| 视觉模型返回空结果 | 图片格式或大小超出模型限制 | 上游预检图片格式、尺寸、base64 合法性 |
| 40G 节点 OOM | 多模型同时常驻导致显存碎片 | 冷热分离,按流量时段拉起和释放模型 |
5.2 独家避坑心得
先说熔断。熔断不是越灵敏越好,太灵敏会导致模型一抖动就被摘除,流量全挤到备用路径上,反而把 K2 打挂。我的经验是熔断窗口设 30 秒,连续 5 次失败才触发,恢复探测间隔 10 秒,给模型留足喘息空间。
再说日志。多模型系统最怕排查问题时分不清是哪个环节出的错。我要求所有日志必须带 request_id,从入口网关到路由层到模型响应,全链路串联。没有 request_id 的排查就像没手电筒进隧道,只能靠猜。这套链路日志上线后,平均故障定位时间从半小时缩短到五分钟以内。
最后说容量规划。很多多模型项目死在"模型越加越多,卡越买越贵"上。我的体会是,新模型上线前必须做容量评估:预估 QPS、单请求平均 token 数、目标延迟,然后反推需要几张卡、要不要量化、要不要做冷热分离。模型不是越多越好,而是要让每一个模型都有清晰的流量定位,承担它该承担的那部分成本。
6. 这套系统的边界与后续扩展
6.1 什么时候不适合上舰队
多模型架构不是银弹,有几种情况我明确不建议上。第一,如果场景非常单一,调用量也不大,一个大模型加提示词工程完全够用,强行上多模型只会增加维护负担;第二,如果团队连 GPU 资源都没有,只能用 CPU 推理或全量走云厂商 API,那多模型的路由收益会被网络延迟和费用吃掉;第三,如果业务对延迟要求极其严格,比如必须在 500 毫秒内返回,那路由层的开销是不可接受的,更合适的是单模型加缓存。
还有一个容易被忽略的边界:团队维护能力。六个模型意味着六套依赖、六种日志格式、六份故障预案,如果团队只有一两个人且没有完善的监控体系,多模型会变成维护泥潭。K2 Horizon 能跑稳,很大程度是因为前期在日志、监控、评测上做了足够多的投入。
6.2 后续可以扩展的方向
这套系统的下一步扩展,我打算从三个方向入手。第一,把历史路由结果收集起来,离线微调一个小型分类模型,替代当前的阈值路由方案,减少对 K2 兜底分类的依赖;第二,做一个模型健康度面板,把每个模型的延迟、成功率、队列长度、显存占用可视化,设置阈值告警,让异常在影响用户之前就被发现;第三,把模型注册做成插件化,新模型上线只需要在配置中心加一条记录,不用改编排代码。
还有一个很值得做的方向是路由策略的自适应。现在的路由规则是静态的,模型负载高的时候依然会把请求发过去。下一步可以做成动态路由,根据每个模型的实时健康度和延迟,动态调整流量分配比例,比如 K2 排队超过 10 秒时,自动把部分复杂推理降级给 Qwen2.5-72B 处理,用略微下降的质量换取整体稳定性。
最后分享一点个人体会。K2 Horizon 这个项目最值钱的地方,不是把六个模型调通了,而是逼着我把"模型能力"和"服务架构"两件事彻底分开思考。以前我总在纠结哪个模型最强,现在更关注哪个模型最适合当前请求、出错了怎么兜底、成本怎么分摊。多模型架构听起来高大上,本质上就是把人力的分工逻辑搬到了模型层——让擅长数学的去做数学,擅长代码的去做代码,擅长聊天的去聊天。如果你也想搭一套类似的系统,我的建议是别一上来就搞六个,先用两个模型把路由跑通,一个强推理一个通用对话,确认链路稳定了再加视觉、加代码、加向量,一步步扩编。舰队不是在图纸上画出来的,而是在一次次航行中修出来的。