1. 混合部署的动机:别在选边站上折腾了
1.1 全本地和全云端的苦,我都吃过
做混合大模型架构之前,我先在"全云端"和"全本地"这两个极端方案里各待过一段时间,两边的问题都很真实。
全云端部署的时候,接入成本确实低,注册账号开个Key就能调,模型能力也强,今天GPT级别的推理水平很难用本地小模型直接替代。但真正放到业务里跑起来,问题就接踵而至:首先是数据链路,内部系统的工单、代码片段、客户信息一旦过云端API,隐私合规这道坎就绕不过去,很多企业不是不想用,是根本不敢把数据送出去。其次是成本,你以为按Token计费很灵活,但当Agent在复杂任务里来回调用、反复推理的时候,账单增长是肉眼可见的。再一个是延迟,每一次调用跨公网走一圈,正常情况一两秒,高峰期排队起来就完全不可控了,这对实时性要求高的场景非常致命。
全本地部署则是另一个极端。模型确实私有化了,延迟也低了,数据也不用出境了。但团队很快就发现,本地部署的模型在复杂推理、代码生成、长文本理解上的表现和云端商用模型有实质差距。业务方会说"这个模型回答的答案没法用",技术方则被困在硬件维护、模型迭代、显存管理的泥潭里。我们当时部署了一台8卡A800的机器跑开源模型,为了达到还不错的推理效果,光是量化精度、vLLM参数、并发策略就调了好几周,最后效果也只是"勉强能用"。更要命的是模型本身的迭代速度,云端大模型可能隔几个月就有一次能力跃迁,而本地方案从选型、评估到上线,一个版本跑半年很正常。
所以"全部上云"和"全部本地"都不是最优解。真正跑过业务之后你会发现,大部分实际需求是分层的:一部分数据敏感、对延迟有硬性要求的任务,适合留在本地;另一部分需要强推理能力、对创造性要求高的任务,完全可以放云端。混合部署不是炫技,而是被业务逼出来的必然选择。
1.2 什么场景真的需要混合架构
不是所有团队都需要混合部署。我见过不少项目,明明业务量很小,硬要搭一套复杂的混合架构,结果维护成本比模型调用成本还高。判断要不要走这条路,我一般看三个条件:
- 数据条目分级:业务里是否有明确区分"可出域"和"不可出域"的数据类型,且不可出域的数据量占比够大、任务够重。
- 延迟敏感度:用户交互链路中是否存在"必须几百毫秒内返回"的关键路径,比如对话中间件、实时助手、流程控制节点。
- 能力分层需求:业务任务是否能按复杂度、推理深度、专业领域做明确分层,低难度高频任务与高难度低频任务的比例是否明显。
如果三条都不沾,直接全云端就完事了。如果三条里有两条成立,混合架构的价值就很明显。
下表是我踩过很多坑之后整理的一个简单判断矩阵,可以直接拿去对照自己的业务场景:
| 判断维度 | 适合全云端 | 适合全本地 | 适合混合架构 |
|---|---|---|---|
| 数据敏感度 | 低,基本都是公开信息 | 极高,完全不能出境 | 有明显分层,部分敏感部分不敏感 |
| 延迟要求 | 秒级可接受 | 毫秒级硬性要求 | 关键路径毫秒级,非关键路径秒级可接受 |
| 任务复杂度 | 简单问答、文本分类 | 有限领域的重复性任务 | 高复杂度与高频低难度任务并存 |
| 成本预算 | 充足,可接受Token费用 | 一次性硬件投入可承受 | 需要平衡硬件与Token成本 |
| 运维能力 | 几乎为零 | 有专业算法与运维团队 | 有基础运维能力和代码能力 |
混合架构真正适配的是那种"既不想被云端绑定,又不想被本地模型能力卡死"的中间态团队,这个中间态范围其实比很多人想象的要大。
1.3 混合架构的分层视图
在设计阶段,我习惯把整个系统拆成四层,每一层只关心自己的事:
- 接入层:接收用户请求或业务系统触发的任务,做基础鉴权和参数校验。这一层不关心背后是哪个模型在回答,也不关心有多少Agent在同时干活。
- 路由层:整个架构里最核心的决策层。它拿到请求后,根据预设策略判断"这个任务应该走本地模型还是云端模型""走哪个模型""要不要切换Agent",同时负责负载均衡、故障转移、灰度支持。
- 编排层(多Agent层):处理任务拆解、Agent调度、工具调用、状态同步。这一层相当于团队的"项目经理"。
- 模型层:底部实际执行推理的模型资源,既包括本地私有化部署的开源模型,也包括云端的商用模型API。
四层之间的关系是单向依赖的:接入层只依赖路由层,路由层依赖编排层和模型层,编排层通过路由层获取模型资源。没有这种清晰分层的时候,系统会变成一团乱麻——Agent里直接写死"调用GPT-4"、"调用本地Llama",一旦模型服务变更,代码跟着改,风险极大。
我见过有些团队的代码里到处散落着对不同模型的直接调用,没有一层统一的"模型入口",这就导致每次模型升级、Key轮换、本地服务重启都必须动业务代码。路由层就是解决这个问题的最短路径——把"用哪个模型"从业务逻辑里抽出来,变成一个可配置、可观测、可运维的独立服务。
2. 路由层设计:把"模型选择"从代码里拆出来
2.1 路由到底在解决什么问题
很多人一听"路由"这个词,第一反应是网络设备里的路由表,或者是前端框架里的URL路由。在大模型架构上下文里,"路由"解决的其实是同一个本质问题:把一个请求分发到最合适的目标去处理。只不过这里的目标从一个IP地址变成了一组模型实例(本地模型服务、云端API、不同规格的模型)。
路由层需要决策的维度比网络路由复杂得多,不仅要看"哪个目标可用",还要看"哪个目标更适合当前任务"。同样一段推理请求,本地7B模型处理耗时可能只要800毫秒,但回答质量一般;云端满血模型处理可能要5秒,但回答质量高。路由层就要根据场景做平衡:这个请求是给用户看的最终答案,还是中间过程中的一个判断?如果只是做实体抽取、意图识别这类中间任务,本地小模型完全够用。
我把路由层要解决的几个核心问题总结如下:
- 可达性路由:哪些模型服务当前健康可用,被限流了没有,队列长度是否超标。
- 能力路由:当前请求需要什么样的模型能力,代码生成、复杂推理、多模态理解还是简单文本处理。
- 策略路由:数据是否敏感、预算是否充足、是否有特定模型要求,这些条件在什么优先级下生效。
- 成本路由:同样的任务,本地模型的边际成本远低于云端,能否在效果损失可控的前提下优先走本地。
2.2 路由维度的落地配置
路由策略如果写在代码里,很快就会变成一团无法维护的意大利面。我建议从一开始就把它做成独立的配置项,用结构化配置来描述"什么条件下路由到哪"。下面这个YAML示例是我在实际项目里用过的简化版本,字段基本可以覆盖多数场景:
router: default_policy: local_first # 默认策略:尽量走本地 targets: - name: local_vllm_7b type: local endpoint: http://127.0.0.1:8000/v1/chat/completions max_concurrency: 16 cost_per_1k_tokens: 0.0001 health_check_interval: 15s - name: local_vllm_32b type: local endpoint: http://127.0.0.1:8001/v1/chat/completions max_concurrency: 8 cost_per_1k_tokens: 0.0005 health_check_interval: 15s - name: cloud_gpt4o type: cloud endpoint: https://api.example.com/v1/chat/completions model_name: gpt-4o max_concurrency: 50 cost_per_1k_tokens: 0.03 health_check_interval: 30s routes: # 带敏感信息的任务,强制走本地 - name: sensitive_data when: tags_include: ["sensitive"] target: local_vllm_7b priority: 100 # 需要强推理能力的任务,走云端 - name: complex_reasoning when: task_type: "reasoning" complexity: "high" target: cloud_gpt4o priority: 80 # 中间过程判断,走本地小模型 - name: intermediate_judgement when: task_type: "extract" # 实体抽取、意图识别 target: local_vllm_7b priority: 60 # 长文本生成或代码补全,走本地32B - name: code_generation when: task_type: "code" target: local_vllm_32b priority: 50 # 默认兜底:如果上面的规则都没匹配,走本地 - name: fallback when: all_else: true target: local_vllm_7b priority: 1这个配置的核心思想是:每条路由规则由条件、目标、优先级三部分组成,运行时执行顺序按优先级降序匹配,命中即返回。这样每次模型上线、下架、调整策略,不需要改代码,只需要改配置并热加载。
2.3 动态路由与状态感知
静态配置只能解决"把请求按固定规则分发"的问题,真正让路由层变得可靠的是动态状态感知。模型服务的健康状况是实时变化的,路由层必须有能力动态调整目标权重,而不是死板地按配置走。
健康检查要做两层。第一层是基础探测,定期向模型的health接口发心跳请求,确认进程是否活着、显存是否够、是否会OOM。第二层是"流量感知",一个模型端点的历史成功率、平均响应时间、Token生成速度会实时写入一个滑动窗口,路由决策时会参考这些指标。
具体的动态调整逻辑我用过一个相对实用的方案:
全局状态维护: 1. 每个模型端点维护一个 HealthScore(0~100) 2. 健康检查成功:+10,失败:-30,连续失败3次置0 3. 端点置0后进入冷却时间,冷却期内不分配流量 4. 冷却期结束后进入半开状态,放少量探针请求,成功则恢复 路由判定逻辑: 1. 根据请求元信息(任务类型、敏感标签、复杂程度)匹配静态路由规则 2. 对候选目标按 HealthScore 过滤,低于阈值的直接剔除 3. 多个同等级候选目标之间,按权重负载均衡这个方案实现成本不高,但对高可用性的提升非常明显。有一次我们的本地模型服务因为显存碎片问题导致推理质量下降,但进程本身没挂,基础健康检查完全看不出来。后来是流量感知发现平均Token生成速度从60 tokens/s降到15 tokens/s,自动降低了它的权重,把新请求导向了备用端点,才避免了故障扩散。
3. 多Agent编排:让Agent当一个有组织的工作组
3.1 三种协作模式的取舍
多Agent架构不是简单地把多个Agent实例启动起来就算完事,关键在"协作"两个字。一个负责拆任务、一个负责执行、一个负责校验、一个负责汇总,它们之间怎么通信、怎么避免重复劳动、怎么处理互相依赖,这才是架构设计中最耗精力的部分。
实践下来,Agent之间的协作模式有三种相对成熟的选择:
- 主管-工人模式(Supervisor-Worker):用一个主管Agent负责任务分解、派发和结果汇总,工人Agent只负责执行具体的子任务。这种模式最容易理解和实现,适合"目标任务明确、子任务之间没有复杂依赖"的场景。
- 流水线模式(Pipeline):Agent A的输出是Agent B的输入,任务按固定顺序流转。适合重写、翻译、内容生产这类"后一步依赖前一步结果"的任务。
- 网状模式(Mesh/Graph):Agent之间可以互相通信、动态决定下一步谁执行。灵活度最高,但状态管理复杂度也随之指数级上升,适合高度动态的任务编排场景。
我个人的建议是:能用主管-工人模式解决的问题,绝不上网状模式。网状模式听起来很酷,但工程上对状态同步、死锁处理、任务重试的要求非常高,一旦链路复杂了,排查问题会非常痛苦。多数业务场景下,主管-工人加上流水线就已经能覆盖90%以上的需求了。
3.2 Agent并发与上下文管理
Agent并发的问题在本地部署场景下会被无限放大。云端API的并发上限通常由服务商控制,有明确的配额管理;而本地模型服务的并发能力受限于GPU显存和推理引擎配置,一旦并发过高,请求会在模型层排队,响应时间急剧恶化,甚至触发OOM。
我们当时给Agent配置并发的参数体系大致是这样设计的:
agent_concurrency: max_agents_per_task: 5 # 单个任务最多拆出多少个并行子Agent max_concurrent_tasks: 8 # 全局最多同时处理多少个任务 # 每个Agent处理时的模型调用限制 model_call_limits: local_small: 20 # 本地7B模型:Agent每秒最多调用20次 local_medium: 10 # 本地32B模型:Agent每秒最多调用10次 cloud_api: 5 # 云端API:Agent每秒最多调用5次 # Agent内模型调用的并发信号量 semaphore_pool: local_small_max_wait: 30s local_medium_max_wait: 60s cloud_api_max_wait: 90s这套配置的本质是三层限流:Agent级别限制"一个Agent内并行的模型调用数",任务级别限制"一个任务内并行的Agent数",全局限制"同一时间运行的任务数"。三层配合,才能保证底层模型服务不会因为Agent的突发请求而被打爆。
实际运行中比并发更隐性的问题往往出在上下文上。多Agent协同的上下文膨胀比单Agent更严重。一个主管Agent要记住所有子Agent的返回结果,如果每个子Agent都返回几千Token的内容,几个子Agent跑完,主管的上下文窗口就被撑爆了。我们当时的处理办法是规定子Agent回传主管的内容必须是"结构化摘要而非原文",比如让子Agent用JSON格式只回传"结论、关键依据、置信度、所需资源",把大段原始输出落盘保存,而不是塞进主管上下文。
3.3 Agent与路由层的通信契约
多Agent和路由层之间必须有一个清晰的通信契约,否则两边各写各的,最终对接时必然是一堆临时补丁。我的实践是让Agent在发起模型调用时带上明确的路由元信息,路由层根据这些元信息做决策:
{ "agent_id": "worker_code_review_01", "agent_role": "code_reviewer", "task_id": "task_xxx_001", "request_meta": { "task_type": "code_span", "complexity": "medium", "tags": ["code", "internal"], "data_sensitivity": "sensitive", "requires_reasoning": false }, "model_preference": "local_vllm_32b", "timeout_ms": 45000 }路由层拿到这份元数据后,先按data_sensitivity过滤可用的目标集合,再按task_type和complexity匹配路由规则,最后在可用的目标里做负载均衡。如果model_preference指向的目标当前不健康,路由层需要有能力自动降级到能力相近的其他目标,而不是直接返回失败。
这里有个原则特别重要:Agent的model_preference只是"偏好",不是"强制要求"。如果把这个字段设计成硬绑定关系,路由层就失去了存在的意义。我在架构评审时经常遇到这样的情况:Agent开发者觉得自己对模型能力最了解,坚持指定模型,但从系统角度看,指定模型完全没必要——明明本地小模型就能干的活,非要声明"必须用云端大模型",成本一下就上去了。正确的做法是Agent声明任务需求,路由层做决策。
任务状态同步方面,我推荐用独立的中间存储(比如Redis或PostgreSQL)来保存Agent状态,而不是依赖各Agent的内存。主要原因是Agent实例可能随时被重启、迁移、扩容,如果状态只存在于内存,一旦实例挂掉,整个任务链就断了。共享状态存储虽然会引入一些序列化开销,但换来的是可靠性和可观测性,这笔账怎么算都划算。
4. 高可用设计:模型挂了不是灾难,没有预案才是
4.1 故障类型清单
模型服务和其他基础设施的高可用设计有很大不同。普通Web服务的故障通常是"服务不可达",表现很直接;而模型服务的故障形态要复杂得多。我把实际运维中遇到的故障类型整理成了一张表:
| 故障类型 | 典型表现 | 检测方式 | 危害程度 |
|---|---|---|---|
| 服务超时 | 请求长时间无响应,直到超时 | 调用超时统计 | 高,直接拖垮用户体验 |
| 限流拒绝 | 返回429状态码或错误码 | 状态码监控 | 中,触发重试就容易雪崩 |
| 上下文溢出 | 请求内容超过模型上下文上限 | 调用前长度检查 | 中,返回异常或截断 |
| 本地OOM | 推理引擎崩溃或重启 | 健康检查探针 | 高,需要自动拉起 |
| 显存碎片化 | 推理速度断崖式下降 | 吞吐量滑动窗口 | 中,需要重启恢复 |
| 生成质量劣化 | 模型输出明显变差,但指标正常 | 结果评估采样 | 低,难以自动发现 |
| 密钥/配额失效 | 云端API鉴权失败 | 错误码监控 | 高,需要快速轮换 |
每种故障类型对应的处理策略不同,但整体思路是统一的:尽早发现、自动隔离、按预案降级、快速恢复。
4.2 降级策略:本地和云端的双向兜底
混合部署最有价值的优势就是"双活"——本地模型和云端模型可以互为备份。这个优势必须通过精心设计的降级策略才能兑现。
最常见的第一种场景是本地模型故障,自动切云端。本地模型因为GPU故障、容器重启、显存溢出等原因不可用时,路由层应该在一两次失败探测后就把新的请求全部导向云端API。但这里有个细节:切换到云端后,请求的模型参数可能要变一下。本地模型的temperature、top_p等参数和云端模型不一定适合用同一套,切换时最好连参数一起切换。
第二种场景是云端限流或不可用,降级回本地。云端API在大促、流量突增时很容易429,这时如果本地有能力兜底,路由层应该把它纳入候选。但要注意,本地模型的容量有限,云端流量突然全部压过来,可能导致本地模型直接过载。所以云端切本地的时候要做好限流保护,比如只允许把核心请求切到本地,非核心请求直接排队或返回"稍后重试"。
第三种场景容易被忽略,叫服务降级而不是降模型。当路有层发现所有模型目标都不可用时,系统不能直接报错,而是要有一个"降级输出"的预案。比如一个客服问答Agent,在模型全挂的情况下,可以通过兜底服务返回知识库里的标准答案,或者返回一个预设的"系统繁忙"模板。这个兜底可能是很朴素的规则匹配,但在真实的可靠性场景里,它的价值不亚于模型本身。
重试机制需要特别注意。模型调用和普通HTTP请求不同,Token生成是动态的,重试的代价非常高。如果请求在云端API已经执行了一半才超时,重试意味着同样的钱要花两遍。所以我的建议是:只在幂等Agent任务中启用自动重试,且重试次数最多一次,重试时选择不同的目标端点。核心任务宁可让上层做补偿,也不要依赖底层盲目重试。
4.3 会话状态与Agent执行状态的持久化
高可用不只是"请求能返回结果",更重要的是"任务在故障后能恢复"。一个复杂Agent任务可能拆成几十个子步骤,执行到一半模型挂了、Agent挂了,如果状态没有持久化,整个任务就要从头再来。这在真实业务里是不可接受的。
我们在生产环境用PostgreSQL保存Agent执行状态,表结构大概是这样的思路:
CREATE TABLE agent_task_state ( task_id VARCHAR(64) PRIMARY KEY, parent_task_id VARCHAR(64), -- 父任务ID,用于复杂任务嵌套 agent_id VARCHAR(128), -- 当前执行Agent status VARCHAR(16), -- pending/running/success/failed/paused input_summary JSONB, -- 输入摘要 output_summary JSONB, -- 输出摘要 current_step INTEGER, -- 当前执行步骤 total_steps INTEGER, -- 总步骤数 retry_count INTEGER DEFAULT 0, route_targets JSONB, -- 本次使用的路由目标 created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX idx_agent_task_state_status ON agent_task_state(status); CREATE INDEX idx_agent_task_state_parent ON agent_task_state(parent_task_id);每个Agent在开始执行、执行中、执行结束三个阶段都有明确的落库动作。一旦某个Agent实例挂了,调度器从数据库里找到status='running'且updated_at超过阈值的任务,把它重新入队,由另一个Agent实例拉起并恢复执行。恢复时通过current_step和output_summary恢复上下文,而不是从零开始。
这个机制做扎实之后,整个系统就有了"从故障中自愈"的能力。模型挂了、Agent挂了、机器重启了,任务都在数据库里留着,随时可以恢复。这是高可用方案中最容易被忽略,但价值最大的一块。
5. 工程化落地:一套可以照抄的最小实现
5.1 目录结构与核心模块
理论说了那么多,最终还是要落到工程实现上。我整理了一套经过生产环境验证的最小目录结构,新项目可以直接按这个骨架搭:
model-gateway/ ├── config/ │ ├── router.yaml # 路由策略配置 │ ├── agents.yaml # Agent定义与并发参数 │ └── high_availability.yaml # 高可用降级策略配置 ├── gateway/ │ ├── main.py # 服务主入口,基于FastAPI │ ├── router/ │ │ ├── engine.py # 路由判定核心逻辑 │ │ ├── health.py # 健康检查与状态管理 │ │ ├── weights.py # 动态权重调整 │ │ └── target_pool.py # 模型目标池管理 │ ├── agents/ │ │ ├── supervisor.py # 主管Agent实现 │ │ ├── worker.py # 工人Agent通用实现 │ │ ├── context.py # 上下文管理 │ │ └── message_queue.py # Agent间消息通信 │ └── providers/ │ ├── base.py # 模型Provider抽象接口 │ ├── vllm_local.py # 本地vLLM适配 │ ├── openai_compatible.py # 云端OpenAI兼容API适配 │ └── fallback.py # 降级输出Provider ├── storage/ │ ├── postgres.py # 任务状态存储 │ └── redis.py # 分布式锁、限流计数器 ├── observability/ │ ├── metrics.py # 指标采集 │ ├── tracing.py # 全链路追踪 │ └── alerting.py # 告警规则 └── deploy/ ├── docker-compose.yml └── k8s/ ├── gateway-deployment.yaml ├── gateway-service.yaml └── configmap.yaml这个结构的核心思路是:gateway/providers是一层抽象,任何模型服务只要能转成OpenAI兼容协议就能接进来;gateway/router负责决策逻辑,与具体模型解耦;storage和observability作为横切支撑,不掺入业务逻辑。
5.2 路由与降级的骨架实现
路由判定核心逻辑用Python写大概就是这样(生产版会复杂一些,但核心链路是清晰的):
# gateway/router/engine.py import time from typing import Optional class RouteEngine: def __init__(self, config, health_store, metrics): self.policies = sorted(config["routes"], key=lambda x: x["priority"], reverse=True) self.health_store = health_store self.metrics = metrics async def route(self, request_meta: dict) -> Optional[str]: # 1. 按配置规则匹配,得到候选目标列表 candidates = [] for policy in self.policies: if self._match_policy(policy, request_meta): candidates.append(policy["target"]) # 2. 剔除不健康的目标 healthy = [ t for t in candidates if self.health_store.get_score(t) >= 50 ] if not healthy: self.metrics.increase("route_no_healthy_target") return None # 3. 对剩余目标按健康分+权重排序 weighted = sorted( healthy, key=lambda x: self.health_store.get_score(x) * self._get_weight(x), reverse=True ) return weighted[0] def _match_policy(self, policy, request_meta) -> bool: when = policy.get("when", {}) for key, value in when.items(): if key == "all_else": return True if key == "tags_include": req_tags = set(request_meta.get("tags", [])) if not req_tags.intersection(value): return False else: if request_meta.get(key) != value: return False return True def _get_weight(self, target: str) -> float: # 基础权重 + 最近的错误率惩罚 history = self.health_store.get_recent_history(target, window=300) if len(history) < 10: return 1.0 error_rate = history.error_count / history.total_count return max(0.1, 1.0 - error_rate * 5)降级策略的核心逻辑可以放在Provider层。每个Provider调用失败后,向上抛出一个标记了错误类型的异常,路由层捕获后自动尝试下一个可用目标:
# gateway/providers/base.py class ProviderError(Exception): def __init__(self, code: str, message: str): self.code = code # timeout / rate_limited / auth_error / server_error self.message = message # 调用入口的使用方式 async def call_with_failover(request_data, route_engine, provider_pool): target = await route_engine.route(request_data.meta) retry_targets = route_engine.get_alternative_targets(target) for endpoint in [target] + retry_targets: try: provider = provider_pool.get(endpoint) return await provider.chat(request_data) except ProviderError as e: route_engine.report_failure(endpoint, e.code) if e.code == "auth_error": continue # 密钥失效时,换其他云端端点试试 if e.code == "timeout": continue # 超时换一个端点 if e.code == "rate_limited": # 限流时不能立刻重试,先等一小段 await asyncio.sleep(0.5) continue raise ProviderError("no_available_target", "all model targets failed")这里面最关键的设计是route_engine.report_failure,每一次失败都会实时更新健康状态,让后续请求自动避开故障端点。故障反馈和路由决策必须在同一条链路上闭环,否则路由层看到的永远是一个过期状态。
5.3 我自己在生产环境踩过的坑
工程实践到最后,拼的是细节。下面这几个坑是我真实在线上遇到过、花了不少时间才排查清楚的,分享出来帮你少走弯路。
坑一:本地模型冷启动被路由层误判为宕机。本地vLLM服务在刚启动或大量并发进入时需要加载模型权重、构建KV cache,这个阶段请求响应时间可能长达几十秒甚至更久。我们的健康检查超时时间最初设为5秒,结果一冷启动就误报宕机,路由层把所有流量切到了云端。等本地模型真正就绪后,又因为流量切回来太慢导致资源闲置。解决方法是健康检查分两个阶段:基础探测用短超时(3~5秒),只检查进程是否存活;能力探测用长超时(60秒以上),真正发一个简单推理请求验证模型能否正常工作。冷启动期间的路由权重也要单独处理,不能按正常状态计算。
坑二:云端API限流触发重试风暴。有段时间云端API偶发429,我们的重试逻辑是"收到429就自动重试一次",结果在高峰期多个Agent同时触发重试,瞬间把云端API的配额打满,随后所有请求持续429,形成恶性循环。后来把重试改成了带抖动的指数退避,并增加了"单窗口内重试次数上限"(比如30秒内最多重试3次),同时开启"降级开关",连续收到5次以上429就自动把对应流量切成备份端点。
坑三:Agent的幂等设计没做好,重试时任务重复执行。一个Agent任务发给本地模型后,网络抖动导致响应超时,Agent自动重试,结果模型那边实际上已经生成完了第一次的结果。第二次重试等于白白多花一次推理成本。更严重的是,如果这个Agent任务是"写入数据库",重复执行就会产生脏数据。要根治这个问题,必须为每个Agent任务生成唯一标识,在模型调用和状态落库时都以这个标识做幂等键。模型Provider层收到重复标识的请求可以直接返回上一次缓存的结果,这需要每个Provider都支持简单的请求级缓存。
坑四:多Agent并发写同一个数据库导致锁竞争。Agent数量多起来之后,同时读写PostgreSQL状态表会出现明显的锁等待,任务状态更新延迟增加。我们当时一部分状态从PostgreSQL换到了Redis,只把最终结果和关键节点落PostgreSQL,中间过程全部放Redis的哈希表,配合TTL自动清理,锁问题基本消除。
5.4 可观测性:高可用的前提,是你能看见故障
没有可观测性的高可用方案是空中楼阁。混合大模型架构里链路特别长,从用户请求到接入层、到路由层、到Agent编排、再到模型Provider,任何一环出问题都可能导致整体表现异常。没有全链路追踪的话,排查问题完全靠猜,耗时极长。
我维护的三个核心观测指标是:
- 路由决策分布:每个时间段内,路由到本地7B、本地32B、云端API的请求各占多少比例,这个指标能直接反映成本趋势和策略是否生效。
- 每个目标端点的错误率与分位数延迟:P50、P95、P99分开看,尤其是P99,本地模型和云端模型在长尾延迟上的表现差异非常大。
- Agent任务成功率与恢复次数:多少任务是一次成功的,多少任务经历了一次甚至多次恢复,这个指标直接体现整体架构的韧性。
告警规则也要围绕这几类核心指标来设置。延迟上去了不一定有故障,但延迟持续高位加上错误率上升,就需要人工介入了。全链路追踪方面,我建议给每个请求分配唯一的trace_id,从接入层一直透传到模型调用层,日志系统里只要保留trace_id,就能把整个过程串起来看。
写在最后
混合大模型架构工程实践,说到底不是模型选型的问题,而是系统工程的问题。多数团队在"用哪个模型"上纠结很久,却忽略了路由、编排、高可用这些真正决定系统上限的部分。
我个人在实际操作中最深的一个体会是:把路由策略当成一个可以随时调整的配置系统,而不是一段写死的代码。模型能力每周都在变,业务需求每月都在变,Token价格随时在变,如果你的架构不能快速响应这些变化,那今天搭好的"最优解"明天可能就成了拖累。
另一个体会是:路由层做薄,Agent业务做厚。路由层只需要关注分发、健康、降级这些纯技术问题,不要让业务规则渗入路由层;Agent层则要把业务逻辑做扎实,包括任务拆解质量、上下文管理、结果校验。这样两边都能独立演进,不会互相拖累。
最后,混合部署不是目的,稳定可用才是。每一次策略调整都值得做灰度验证,每一次模型切换都要留下可追溯的记录。这套架构能不能跑得好,靠的不是某一项高技术含量组件,而是无数个扎实的小决策叠加在一起的结果。