Agent模型选型先定路由规则:把智能路由下沉到Runtime 1. 别急着选 Agent 模型先想清楚你家路由规则长什么样做 AI Agent 项目的人十有八九都会在同一个地方卡住模型选型。打开模型榜单一看各家分数咬得很紧今天 A 家领先 0.5 分明天 B 家又在某个榜单上反超。于是团队里开始争论有人想上闭源大模型求稳有人想用开源模型省钱还有人觉得干脆兼容十几个厂商消费者想用哪个用哪个。我见过不少项目就是在这样反复摇摆里耗掉了大半个月最后做出来的 Agent 连一次像样的任务调度都跑不通。问题出在哪很多人把模型选型当成一个纯模型问题但实际上对大部分业务场景来说模型能力的差距远没有你想的那么大。真正决定 Agent 能不能稳定、可靠、可维护的是模型背后的路由规则也就是决定“哪个请求应该交给哪个模型去处理”的那套逻辑。而且这套逻辑不该散落在业务代码里它应该被写进 Runtime成为 Agent 基础设施的一部分。这篇文章想聊的就是这件事为什么路由规则是 Agent 选型的前置条件以及如何把路由规则真正落到 Runtime 里。我会结合自己做过的几个实际项目把思路、代码、踩坑记录都放出来给正在做 Agent 基础设施选型的朋友一个参考。2. 为什么选模型之前先得解决路由问题2.1 模型选型的本质困境单一模型扛不住所有场景先讲一个很现实的困境。你在做一个企业知识库问答 Agent需求清单大概长这样:员工问“报销流程是什么”需要快速、准确的检索回答成本要低响应要快。法务同事上传一份几十页的合同需要理解复杂条款做风险分析这个场景上下文非常长逻辑要求高。客服那边收到用户投诉需要判断用户情绪决定是安抚还是升级人工处理。这三个场景的需求是完全不同的。第一个场景要求低延迟、低成本模型不用太聪明能把检索到的内容组织成通顺回答就行。第二个场景要求长上下文、强推理哪怕贵一点慢一点也没关系。第三个场景要求指令跟随能力强、输出格式稳定最好能直接从回复里抽结构化标签。如果你试图用一个模型扛下所有场景结局通常只有两种要么用强模型处理简单任务钱烧得飞快延迟还高要么用弱模型处理复杂任务结果质量惨不忍睹Agent 频繁出错来回重试。这两种结局我都见过而且都挺惨烈的。2.2 路由规则的本质把请求分成该花的钱和不该花的钱路由规则解决的就是这个问题。它的本质是根据请求特征把不同复杂度、不同要求的任务分发给不同能力层级的模型处理。用便宜模型解决大部分简单问题把预算集中在少量真正需要强模型的任务上。听起来很简单对吧但实际做起来会遇到两个麻烦。第一个麻烦是路由规则藏在业务代码里。业务同学在写某个功能时顺手写了个 if-then判断关键词决定走哪个模型。刚开始还挺好用但随着业务迭代规则越堆越多散落在各个模块里没人说得清现在到底有哪些路由规则它们之间什么关系哪个规则优先级更高。最后这个项目就变成了一个谁也改不动的屎山。第二个麻烦是路由规则没有统一的观测入口。你不知道每个规则命中了多少请求每个模型的实际负载是多少成本超了到底是因为哪条规则这些全是一笔糊涂账。所以我的观点很明确路由规则必须从业务代码里抽出来下沉到 Runtime 层。这个话怎么理解呢你看业务代码每天都在变科学家在调 prompt后端在改接口但 Runtime 是相对稳定的它像操作系统一样负责管理资源、分配任务。路由规则放进 Runtime意味着所有请求在进入业务逻辑之前就已经被一个统一的调度层处理了。业务代码只管自己的逻辑模型怎么选、选哪个那是 Runtime 的事。2.3 Runtime 路由和网关路由的边界很多人这里会问我把路由规则放进 Runtime跟现在市面上那些 AI 网关Gateway有什么区别网关不也能做模型路由吗这里要区分一下。网关做的是模型路由的最后一跳它解决的是已经决定要用模型 A 了那么具体是调用模型 A 的哪个供应商、哪个版本、哪个 region如果挂了怎么办要不要重试。这层路由是偏运维的它的价值在于稳定性和容灾。而 Runtime 层的路由解决的是更前置的问题这个请求该用什么模型该走什么策略该分配多少预算。它更接近业务语义需要感知任务类型、用户画像、内容主题、上下文长度这些信息。网关是低层的“怎么调”Runtime 是上层的“调哪个”。简单说Runtime 路由是大脑AI 网关是手脚。做 Agent 基础设施两层都要有但很多团队只做了网关层把大脑的逻辑随意丢在业务代码里这是本末倒置的。3. 路由规则到底怎么写三个核心维度和一套配置模型3.1 第一维度按任务类型路由任务类型是最直观的路由维度。你需要先梳理业务里到底有哪些类型的任务然后为每种任务类型定义默认的模型策略。我习惯的做法是在项目启动时就拉一个任务清单表头长这样任务类型能力要求典型模型级别延迟要求成本敏感度意图识别低只需分类L1轻量模型极快高信息抽取中需结构化输出L2快中摘要生成中高需长文本理解L2 / L3中中推理分析高需多步推理L3旗舰模型可容忍低代码生成高需准确语法L3中低创意文案中需风格控制L2中中这个表的价值不在于它本身有多完备而在于它逼着团队在写第一行业务代码之前先想清楚自己到底有哪些任务。等这个表梳理完了路由规则的骨架其实已经出来了。实际项目里任务类型通常不是直接写死的而是从一个请求解析出来的。请求进来之后Runtime 先看它的语义特征比如意图识别的结果、内容类型标签、目标接口等然后映射到任务类型上。这段映射逻辑会用一个单独的模块维护后续新增任务类型时只需要扩展这个表不用动业务代码。3.2 第二维度按代价预算路由成本因素很多人会忽略但等你账单出来的时候眼泪都是咸的。我的建议是把成本写进路由规则里用预算控制模型选择。具体做法是这样为每个请求或每个用户会话定义一个最大可接受成本Runtime 在路由时优先选用满足质量和延迟要求的最便宜模型。举个例子。假设你的业务里存在这样的配置普通用户请求的模型成本预算是 0.001 美元/次VIP 用户是 0.01 美元/次。那么同样一个摘要任务普通用户可能被路由到 L2 模型VIP 用户因为预算更充足可以路由到 L3 模型拿更好的效果。这里还有一个很关键的点成本预算不应该是静态的。我实际上会在 Runtime 里做动态调整比如某个用户某次请求内容特别复杂系统会自动申请追加预算从备用池里扣而不是死板地拒绝升级模型。这个机制有点像云厂商的按量计费和包年包月并存灵活度要够。3.3 第三维度按质量与延迟动态路由任务类型和成本预算解决的是“静态该选谁”但实际线上环境是动态的。同样的模型今天响应快明天可能因为负载高就变慢同样的任务用户在北京和在纽约感知到的延迟完全不同。所以路由规则里要做动态维度主要看两个信号当前模型池的健康状态和请求的实时延迟容忍度。这个动态决策逻辑我建议放在 Runtime 里用独立模块实现它会周期性检测各模型供应商的响应时间、错误率、队列长度然后更新每个模型的路由权重。比如某个模型供应商当前错误率超过 5%Runtime 会自动降低它在新请求中的分配比例直到它恢复健康。这里需要说明的是动态路由并非要做得像自适应机器学习那样复杂——不需要一个在线训练的模型来决策。用简单的熔断、加权、健康检查机制组合就已经能解决绝大多数问题了。过度设计是这类系统最常见的坑。3.4 把这些规则变成一份可读的配置规则理清楚了下一步是让它们可配置、可维护。我强烈建议不要把这些规则硬编码在代码里而是用一个配置文件或配置中心统一管理。这是我实际项目里用过的配置结构你可以参考route_rules: default_policy: task_types: - name: intent_classify model_tier: L1 max_cost: 0.0003 timeout_ms: 800 - name: summary model_tier: L2 max_cost: 0.003 timeout_ms: 5000 - name: reasoning model_tier: L3 max_cost: 0.02 timeout_ms: 30000 user_segment_overrides: - segment: vip task_types: - name: summary model_tier: L3 max_cost: 0.01看到这个结构你会发现它实际上就是一个路由决策表默认策略 用户段覆盖。每一行都是一个完整的路由决策什么任务、用什么档次的模型、最多花多少钱、最多等多久。这让团队里的任何人都能看懂路由规则也方便后续调整。4. 把路由规则写进 Runtime一个轻量但完整的实现方案4.1 Runtime 层需要具备的三个基础能力要把路由规则真正下沉到 Runtime关键是先明确 Runtime 要提供什么能力。我把它归纳为三个请求解析、规则匹配、执行与观测。请求解析指的是在请求进入业务逻辑之前Runtime 先对请求做预处理提取任务类型、用户段、内容长度、关键词等路由所需的信息。这里有个原则要把握——Runtime 不需要理解业务的具体细节它只需要拿到路由所需的特征字段就够了。规则匹配指的是根据解析出的特征在路由决策表里找到对应的模型配置。这个模块要快最好能做到毫秒级否则路由本身会成为性能瓶颈。执行与观测指的是拿到模型配置后Runtime 负责调用模型、处理重试、上报日志和指标。日志里至少要包含命中的路由规则、模型的供应商和版本、响应时间、Token 消耗、成本估算。有了这些数据你才能回答“这个模型到底值不值”这类问题。4.2 一个可运行的决策引擎示例下面这段代码我简化自一个生产项目的核心模块。它不是最完善的但完全可以直接跑起来给你一个 Runtime 路由决策引擎的落地雏形。import yaml import time import random from dataclasses import dataclass, field from typing import Optional # 配置加载 class ConfigLoader: def __init__(self, pathroute_rules.yaml): with open(path, r) as f: self.data yaml.safe_load(f) def get_default_policy(self): return self.data[route_rules][default_policy] def get_segment_override(self, segment: str): for override in self.data[route_rules].get(user_segment_overrides, []): if override[segment] segment: return override return None # 请求解析结果 dataclass class RoutingRequest: task_type: str user_segment: str payload_size: int priority: int 0 # 模型路由结果 dataclass class RoutingDecision: task_type: str model_tier: str max_cost: float timeout_ms: int rule_source: str default # 熔断器记录连续错误并熔断 class CircuitBreaker: def __init__(self, threshold3, cooldown_ms10000): self.threshold threshold self.cooldown_ms cooldown_ms self.fail_count 0 self.last_fail_time 0 def is_open(self): if self.fail_count self.threshold: if time.time() * 1000 - self.last_fail_time self.cooldown_ms: self.reset() return False return True return False def record_failure(self): self.fail_count 1 self.last_fail_time time.time() * 1000 def record_success(self): self.fail_count 0 def reset(self): self.fail_count 0 # 模型执行器模拟调用不同档次的模型 class ModelExecutor: def __init__(self): self.circuit_breakers { L1: CircuitBreaker(), L2: CircuitBreaker(), L3: CircuitBreaker(), } def invoke(self, tier: str, request: RoutingRequest) - str: if self.circuit_breakers[tier].is_open(): raise RuntimeError(f{tier} tier is circuit-broken) # 模拟调用耗时 latency random.randint(200, 1500) time.sleep(latency / 1000) # 模拟错误 error_rate {L1: 0.02, L2: 0.03, L3: 0.01}[tier] if random.random() error_rate: self.circuit_breakers[tier].record_failure() raise RuntimeError(f{tier} tier call failed) self.circuit_breakers[tier].record_success() return f[{tier}] result for {request.task_type} # 路由引擎 class RoutingEngine: def __init__(self, config_pathroute_rules.yaml): loader ConfigLoader(config_path) self.rules loader.get_default_policy().get(task_types, []) self.segment_override_policies {} for override in loader.data[route_rules].get(user_segment_overrides, []): self.segment_override_policies[override[segment]] override.get(task_types, []) self.executor ModelExecutor() def _get_rule_for_task(self, rules, task_type: str): for rule in rules: if rule[name] task_type: return rule raise KeyError(fNo rule for task type: {task_type}) def decide(self, request: RoutingRequest) - RoutingDecision: # 先看用户段覆盖 override_rules self.segment_override_policies.get(request.user_segment) if override_rules: try: rule self._get_rule_for_task(override_rules, request.task_type) return RoutingDecision( task_typerequest.task_type, model_tierrule[model_tier], max_costrule[max_cost], timeout_msrule[timeout_ms], rule_sourcesegment_override ) except KeyError: pass # 回退到默认策略 rule self._get_rule_for_task(self.rules, request.task_type) return RoutingDecision( task_typerequest.task_type, model_tierrule[model_tier], max_costrule[max_cost], timeout_msrule[timeout_ms], rule_sourcedefault ) def execute(self, request: RoutingRequest): decision self.decide(request) start time.time() try: result self.executor.invoke(decision.model_tier, request) latency int((time.time() - start) * 1000) return { request: request, decision: decision, result: result, latency_ms: latency, status: success, } except RuntimeError as e: return { request: request, decision: decision, error: str(e), latency_ms: int((time.time() - start) * 1000), status: failed, }这个决策引擎的逻辑并不复杂核心就三步解析请求里的任务类型和用户段查路由表找到对应规则按规则调用模型。但请注意这个引擎有几个对生产非常重要的细节用户段覆盖优先于默认策略且支持部分覆盖。VIP 用户只有摘要任务走强模型其他任务还是默认策略这很灵活。熔断器加持某个模型档位连续失败多次后在一段时间内不再把请求打给它避免雪崩。所有决策和执行都在同一个模块里日志可以统一上报不会散落到业务代码各处。4.3 在 Runtime 里的接入方式中间件模式最省事有了决策引擎接下来要解决的是怎么把它嵌入现有系统。这里我的经验是优先采用中间件模式而不是让业务代码显式调用路由引擎。所谓的中间件模式就是所有进入 Agent 的请求都会先经过 Runtime 处理Runtime 看完路由规则后如果有必要再把请求转给对应的模型执行器。业务代码里完全不需要感知模型选择这回事。我举一个实际用的例子。假设你们内部有一个 Agent 服务框架请求进入 Controller 之前加一层 AOP 拦截器或者中间件里面注入路由引擎请求来了自动解析、自动路由、自动执行。业务代码里只需要拿到最终模型返回的结果至于这个结果是怎么选路出来的业务不用管。这样做有一个立竿见影的好处当产品经理提出“这个新场景应该用另一个模型”时你只需要更新一份配置加一条路由规则业务代码零改动。如果路由逻辑散落在业务代码里光是找到所有需要改的地方就够你加三天班了。4.4 观测与日志没有数据的路由规则是盲目的说句实在话很多团队做了路由规则但从不看路由效果这跟闭着眼睛开车没两样。路由规则的“路况信息”全部来源于观测数据。所以在落地 Runtime 路由时我会同步把 Metrics 埋好至少包含以下几项每个任务类型的请求量分布。哪个任务类型占比最高占比趋势如何这能帮你判断路由配置是否符合业务分布。每个模型的 Token 消耗与成本趋势。这个不用多说月底账单对不上账时你会感激这组数据。每个模型的错误率和 P95/P99 延迟。动态路由和熔断策略的依据都在这。路由规则命中分布。默认策略命中率多少用户段覆盖命中率多少这能反映出你的规则需不需要调整。我用过的比较轻量的方案是让路由引擎在执行时把决策结果写进结构化日志然后接入日志系统或时序数据库再用现成的图表工具做面板。不需要一开始就上重型的可观测性平台起步阶段简单直接反而更好维护。5. 动态路由让 Runtime 自己学会“见风使舵”5.1 为什么静态路由不够用静态路由解决的是“规则清晰、场景明确”的问题。但线上的模型市场和服务稳定性不会一直如你所愿。举个例子你上线时选了个模型供应商 A 的 L3 模型做推理任务效果很好。三个月后供应商 A 的推理服务频繁超时错误率居高不下而供应商 B 的 L3 模型已经迭代了一版在同样任务上的效果和稳定性都超过了 A。按照静态路由你的用户会持续忍受 A 带来的糟糕体验直到有人发现异常并手动改配置。如果路由规则只是一份静态配置那它就是个麻烦的“半成品”。Runtime 的价值正是在于有能力在规则基础上叠加“动态调整”的能力。5.2 动态路由的三个实用策略基于错误率的熔断降级某个模型或供应商连续错误超过阈值Runtime 自动把流量切到备用模型等供应商恢复后再切回来。这个策略是保底手段至少保证 Agent 不会因为某个供应商的故障而整体不可用。基于延迟的权重调节Runtime 周期性地检测各候选模型的响应延迟如果某个模型持续变慢就降低它的权重把更多请求分给更快的模型。注意这里是权重调节不是直接把某个模型禁掉因为慢并不代表不能用。基于成本预算的动态升级降级这是把成本维度和动态维度结合的一个例子。当某个请求的实际消耗远低于预算时Runtime 可以决定升级到更高质量的模型提升用户体验反过来当成本快超预算时Runtime 会优先选择更省钱的模型。5.3 从观察者到决策者动态路由的反馈闭环动态路由要做到真正的闭环核心不只是“调整”而是“反馈”。路由引擎需要根据上一次执行的结果更新决策参数。比如用户请求进来Runtime 先选中了 L3 模型结果执行超时。那么这个超时信息要反馈给路由引擎让它在未来一段时间内降低该模型在此类任务上的优先级。或者某个任务连续几次在 L2 模型上产生低质量输出路由引擎应该考虑把这类任务稳定地提升到 L3。在实现上我倾向于把这些反馈做成“近期滑动窗口统计”而不是全局统计。因为线上的流量分布时刻在变用 5 分钟或 10 分钟的滑动窗口比用全天累计数据更能反映即时情况。这里要特别提醒一点动态路由要保守不要激进。一次超时或一次低分输出不代表模型不行。触发调整的阈值要高一些调整的幅度要小一些宁可多观察一轮也不要让路由规则在几个模型之间反复横跳那样反而会引入大量不稳定因素。6. 模型选型的最终决策表把路由规则作为前置约束6.1 先定路由规则再选模型参数回到最开头的问题Agent 模型怎么选到这一步答案已经很清楚了。模型选型不是拍脑袋比较一堆模型跑分而是先定义你的路由规则明确每个任务类型对延迟、成本、质量的要求然后反推每个档位的模型应该具备什么参数和能力。有了这套前置约束选型就变成一个按图索骥的过程。比如你的规则里定义 L1 档位是意图识别专用那么你选 L1 模型时只看意图识别的准确率、延迟和成本而不会因为它在数学推理上分数高就去选它。同理L3 档位如果承担推理分析任务你选型时就侧重看多步推理、长上下文理解这类能力。6.2 给每个档位准备两个候选模型选型经验里还有一条比较重要的每个档位不要只绑定一个模型至少准备两个候选一个主、一个备。主模型是常态下用的备模型在动态路由触发熔断或降级时会顶上去。这么做不单是为了容灾也给了团队在模型效果出现分歧时做 AB 对比的空间。运营同学可以说“备模型在新文案任务上表现更好”那就可以在规则里临时切换一批流量过去验证而不是改代码或者换模型上线。6.3 用“路由演练”验证模型选型是否真的合理最后分享一个我自己比较推崇的实践路由演练。具体做法是选型完成后不要直接上生产先把路由规则和历史请求数据放到一个仿真环境里跑一遍全量回放。看看各个任务被分到了哪些模型延迟和成本是否符合预期有没有哪个规则产生了意外情况比如某个冷门任务把大量请求导向了 L3 导致成本爆表。我跑过一次演练之后发现原本以为只占 5% 流量的推理任务在路由规则里定义过宽把很多类似的分类任务也兜进去了结果 L3 模型的实际流量比预期翻了三倍。如果没做演练直接上生产下个月账单可能就要吓到财务同事了。路由演练的工具不需要多复杂把线上 Nginx 或网关日志回放一遍即可核心是让路由规则的每一个分支都能被覆盖到。7. 常见问题与排查技巧实录7.1 “改了一条规则怎么线上还是走老模型”这个问题的原因九成是配置没生效。先检查配置中心是否真的推送到了所有 Runtime 节点再检查是不是因为配置带缓存需要等缓存过期或者手动 refresh。还有一个很隐蔽的坑某些团队把用户段覆盖规则放在默认策略之前但覆盖规则里没有新增的任务类型导致新任务类型悄悄落回了默认策略看起来就像“改了没生效”。排查口诀是先看配置版本再看节点日志最后看路由命中记录。三步走完一般都能定位到问题。7.2 “动态路由频繁切换模型用户反馈结果不稳定”动态路由的反馈机制要设置“死区”也就是当候选模型的得分差异在一个可接受范围内时保持原有选择不触发切换。很多团队的动态路由做成了“每轮都重新选择最优”结果在模型效果接近时来回跳转用户体验奇差。记住动态路由的目标是在一个窗口期内保持决策稳定而不是每分每秒都追求最新最优。我的做法是候选模型得分差异超过 10% 才允许切换低于这个阈值则沿用当前模型。这样可以过滤掉大量噪声干扰。7.3 “路由规则越加越多配置越来越乱怎么梳理”我见过团队里路由规则膨胀到上百条最后没人敢动。这个问题根源在于没有做好规则的“分类分层”。我建议把规则分成三层全局策略、任务类型策略、用户段策略。新增规则时先判断它属于哪一层能放进已有层的就别新增独立规则。定期清理那些命中量为 0 的“僵尸规则”这些规则往往是历史迭代残留既增加理解成本又可能在某个角落里干扰正常路由。7.4 “买了贵的模型但业务方觉得效果提升不明显怎么办”这种情况太常见了。先把观测数据拉出来对比贵模型和便宜模型在同一任务上的关键指标比如回答准确率、用户满意度、任务重试率。如果差异确实不大那就果断把该任务降回便宜模型。很多时候贵的模型提升的只是跑分不是业务指标这时候省钱是优化而不是降级。8. 最后分享一点小经验我在做 Agent 路由这套东西时最大的感受就是模型选型这件事别把它看成一步到位的决策它更像一个持续迭代的过程。今天选中的模型可能三个月后就被更好的替代了今天定义的路由规则可能一个季度后就要根据业务变化重新调整。所以与其花大量时间比较模型跑分不如把精力花在搭好 Runtime 的路由框架上。框架稳了换模型就像换零件一样简单框架不稳每次换模型都是一次伤筋动骨的重构。如果你也在做 Agent 基础设施建议从今天开始把路由规则从业务代码里一件件往外挪挪进 Runtime 里。等挪完的那天你会发现模型选型这件事突然就变得没那么纠结了。按我个人经验这个改造带来的收益比把三个顶级模型集成的收益都大而且大得多。