Agent模型选型本质是路由设计:构建可维护的Runtime层

发布时间:2026/10/21 5:09:12
Agent模型选型本质是路由设计:构建可维护的Runtime层
1. 为什么选模型这件事最后都变成了写路由做 Agent 开发的人几乎都经历过同一个阶段一开始兴致勃勃地对比各家模型看榜单、跑评测、算 token 成本恨不得把每个模型的能力边界都摸清楚。结果真到了工程落地发现真正让人头疼的根本不是哪个模型更强而是这一堆模型到底怎么协同。我见过太多项目前期选型讨论得热火朝天最后代码里却是一坨if-else判断一下用户输入长度短的就走便宜模型长的就走贵模型判断一下有没有工具调用有就走 A没有就走 B。这种写法在 demo 阶段没问题一旦业务复杂起来路由逻辑散落在十几个文件里改一个条件要翻半天加一个新模型要动五处代码测试根本覆盖不全。所以我想说的核心观点很直接Agent 的模型选型本质上不是选型问题而是路由设计问题。你选哪些模型只是第一步真正决定系统能不能长期维护、能不能快速迭代的是你有没有把什么情况下用哪个模型这套规则从业务代码里抽出来写进一个独立的 Runtime 层。这篇文章就是围绕这个思路展开的。我会讲清楚路由规则应该长什么样、Runtime 层要承担哪些职责、规则怎么组织才不会被业务逻辑污染、以及我在实际项目里踩过的那些坑。适合正在做 Agent 系统、已经过了 demo 阶段、开始被模型越来越多、逻辑越来越乱困扰的开发者。如果你还在纠结到底选哪个模型那说明你还没到需要这篇文章的阶段但如果你已经开始写第三个if model xxx了那现在就是最好的时机。2. 把路由规则从业务代码里剥出来到底剥的是什么2.1 路由规则的本质是一组决策输入到模型的映射先把这个概念说清楚。所谓路由规则就是一套把请求特征映射到具体模型的决策逻辑。这里的请求特征包括很多东西输入文本的长度、语言、是否包含工具调用意图、任务类型问答、总结、代码生成、多轮对话、对延迟的敏感度、对成本的容忍度、历史对话轮数、甚至当前系统的负载情况。而具体模型也不只是一个名字它往往还带着一组参数温度、最大输出长度、是否开启流式、超时时间、重试策略。所以一条完整的路由规则输出的其实是一个执行配置而不是单纯一个模型 ID。我习惯把这条映射写成这样的结构dataclass class RouteDecision: model: str temperature: float max_tokens: int timeout: float fallback: list[str] reason: str # 记录为什么走了这条规则方便排查注意最后那个reason字段。很多人写路由的时候不记录决策原因出了问题只能靠猜。加上这个字段之后日志里能直接看到因为输入超过 8000 token 所以走了长上下文模型排查效率完全不一样。2.2 为什么不能把规则写在业务逻辑里有人会问我直接在调用模型的地方写判断不就行了为什么要单独抽一层原因有三个而且都是实际项目里会真实咬人的。第一业务逻辑和路由逻辑的变化频率完全不同。业务逻辑跟着产品需求走可能一周改一次路由逻辑跟着模型迭代和成本策略走可能一天改一次。两者耦合在一起改路由就要动业务代码回归测试的成本直接翻倍。第二路由规则需要被复用和组合。同一个 Agent 系统里可能有对话入口、有后台批处理、有定时任务它们都需要路由但入口不同。如果规则散在各处你没法保证它们行为一致。抽出来之后所有入口共用同一套规则行为可预期。第三路由规则需要被测试。这是最容易被忽略的一点。路由逻辑一旦独立你就可以写单元测试给定一组输入特征断言输出的是哪个模型。这种测试写起来快、跑起来快、覆盖率高。但如果规则埋在业务代码里你只能写集成测试慢且脆。2.3 Runtime 层要承担的四个职责把规则抽出来之后需要一个地方来执行它们这就是 Runtime 层。我总结下来它至少要干四件事规则匹配根据请求特征从规则集合里选出命中的那条。配置组装把规则里的模型、参数、超时、重试策略组装成最终的执行配置。降级处理主模型调用失败或超时按预设的 fallback 链切换。决策记录把每次路由的输入特征、命中规则、最终配置写进日志或追踪系统。这四件事里前两件是核心后两件是工程化的关键。很多团队只做了前两件结果线上出问题的时候两眼一抹黑不知道某个请求为什么走了某个模型。提示Runtime 层不要承担业务语义。它只认特征不认业务含义。比如它知道输入长度 8000但不知道这是用户投诉内容。业务语义的转换应该在进入 Runtime 之前完成。3. 规则怎么写才不会被自己坑优先级、匹配顺序与冲突处理3.1 规则匹配的顺序决定了系统的可预测性规则一多冲突就来了。比如你有一条规则说代码生成任务走模型 A另一条说输入超过 8000 token 走模型 B。那一个又长又是代码生成的请求到底走哪个这个问题的答案不是随便选一个而是你必须显式定义优先级。我的做法是给每条规则一个priority字段数值越小优先级越高匹配时按优先级排序命中第一条就返回。dataclass class RouteRule: name: str priority: int condition: Callable[[RequestFeature], bool] decision: RouteDecision这样写的好处是规则的执行顺序完全由数据决定而不是由代码里的书写顺序决定。你可以在配置文件里调整优先级不用改代码逻辑。3.2 用特征提取和规则匹配两段式设计隔离复杂度我强烈建议把路由拆成两个阶段特征提取和规则匹配。特征提取阶段把原始请求转换成一个结构化的RequestFeature对象。这个对象里只有客观事实没有判断逻辑dataclass class RequestFeature: input_tokens: int language: str has_tool_call: bool task_type: str turn_count: int is_streaming: bool user_tier: str规则匹配阶段只读这个对象做纯函数式的判断。这样做的好处是特征提取可以单独测试规则匹配也可以单独测试两者互不干扰。而且当你想加一个新特征的时候只需要在提取阶段加字段规则里按需使用不会影响已有逻辑。3.3 冲突处理的三种策略与适用场景即使有了优先级还是会有边界情况。我一般用三种策略来处理策略做法适用场景优先级覆盖高优先级规则直接胜出强约束场景如合规要求必须走特定模型特征加权多个规则打分取最高分软约束场景如成本与质量的权衡显式兜底所有规则都不命中时走默认配置必须保证任何请求都有模型可用大多数项目用优先级 兜底就够了。加权打分适合那种需要精细平衡成本和质量的场景但实现复杂度高不建议一开始就上。注意兜底规则一定要有而且兜底模型一定要是经过验证的稳定模型。我见过有项目兜底配了个实验性模型结果主模型一挂兜底也跟着挂整个系统直接不可用。4. 一个能跑的路由 Runtime 长什么样4.1 目录结构与模块划分先看整体结构我习惯这样组织runtime/ __init__.py feature.py # 特征提取 rules.py # 规则定义与加载 engine.py # 匹配引擎 executor.py # 执行与降级 recorder.py # 决策记录 config/ routes.yaml # 规则配置规则用 YAML 配置而不是写死在代码里。这样运营和产品也能参与调整不用每次都找开发改代码。4.2 规则配置的 YAML 写法rules: - name: long_context priority: 10 condition: input_tokens: 8000 decision: model: long-context-model temperature: 0.3 max_tokens: 4096 timeout: 60 fallback: [general-model] - name: code_task priority: 20 condition: task_type: code decision: model: code-model temperature: 0.1 max_tokens: 2048 timeout: 30 fallback: [general-model, long-context-model] - name: default priority: 999 condition: {} decision: model: general-model temperature: 0.7 max_tokens: 1024 timeout: 20 fallback: []这个配置里long_context优先级最高因为长上下文是硬约束模型不支持就是不行。code_task次之。default是兜底条件为空表示永远命中。4.3 匹配引擎的核心实现引擎的逻辑其实很简单难的是边界处理class RouteEngine: def __init__(self, rules: list[RouteRule]): self.rules sorted(rules, keylambda r: r.priority) def decide(self, feature: RequestFeature) - RouteDecision: for rule in self.rules: if rule.matches(feature): decision rule.decision decision.reason fmatched rule: {rule.name} return decision raise NoRouteMatchedError(no rule matched, check default rule)这里有个细节decide返回的是决策不是执行结果。执行是下一步的事。这种职责分离让引擎保持纯粹方便测试。4.4 执行器与降级链执行器负责拿着决策去调用模型并在失败时按 fallback 链切换class Executor: def execute(self, decision: RouteDecision, payload): candidates [decision.model] decision.fallback last_error None for model in candidates: try: return self._call(model, decision, payload) except (TimeoutError, ModelUnavailableError) as e: last_error e self.recorder.record_fallback(decision, model, e) continue raise AllModelsFailedError(last_error)降级链的顺序很重要。我一般把能力相近但更稳定的模型放在前面能力弱但绝对可用的放在最后。不要放一个完全不同的模型类型否则降级之后输出格式可能对不上反而引发更多问题。5. 实测中那些让人半夜爬起来的问题5.1 特征提取不准导致路由错位这是最常见的问题。比如你用 tokenizer 估算输入长度但不同模型的 tokenizer 不一样估算出来的值偏差可能到 20%。结果就是本该走长上下文模型的请求被判定为短请求走了普通模型然后截断报错。我的做法是特征提取用最保守的估算。宁可高估 token 数也不要低估。高估的代价是偶尔多花点钱走贵模型低估的代价是请求失败。两害相权取其轻。5.2 规则优先级写反了这个坑我踩过。有一次把成本优先规则的优先级设得比质量优先高结果所有请求都走了便宜模型用户投诉质量下降。排查了半天才发现是优先级数值写反了。后来我加了一个校验启动时检查规则集合如果兜底规则的优先级不是最大值直接报错退出。这种启动期校验能拦住很多低级错误。5.3 降级链里的模型参数不兼容不同模型对参数的支持不一样。有的模型不支持temperature0有的模型max_tokens上限是 4096有的模型不支持流式。降级的时候如果直接复用主模型的参数很可能调用失败。解决办法是在每个模型的配置里单独声明参数约束执行器在调用前做一次参数适配def adapt_params(model: str, decision: RouteDecision) - dict: constraints MODEL_CONSTRAINTS[model] params {temperature: decision.temperature, max_tokens: decision.max_tokens} if constraints.get(no_zero_temp) and params[temperature] 0: params[temperature] 0.01 params[max_tokens] min(params[max_tokens], constraints[max_tokens]) return params5.4 决策记录写得太少排查时抓瞎一开始我只记录了最终用了哪个模型结果线上出问题的时候完全不知道这个决策是怎么来的。后来把特征、命中规则、决策原因、降级过程全部记下来排查效率提升了一个量级。记录的内容建议包括请求 ID、特征快照、命中规则名、最终模型、是否降级、降级原因、总耗时。这些字段在排查为什么这个请求这么慢的时候特别有用。6. 规则迭代与灰度让路由系统能持续演进6.1 规则变更必须可灰度路由规则一改影响的是全量请求。所以规则变更一定要支持灰度。我的做法是给规则加一个enabled_ratio字段按请求 ID 哈希取模决定是否应用新规则def should_apply(rule: RouteRule, request_id: str) - bool: if rule.enabled_ratio 1.0: return True bucket int(hashlib.md5(request_id.encode()).hexdigest(), 16) % 100 return bucket rule.enabled_ratio * 100这样新规则可以先放 5% 流量观察指标没问题再逐步放大。6.2 用指标驱动规则调整路由规则不是拍脑袋定的要有数据支撑。我一般关注这几个指标各模型的调用量占比各模型的平均延迟和 P99 延迟各模型的失败率和降级率单位请求的平均成本用户侧的质量反馈如果有这些指标按规则维度聚合就能看出哪条规则在烧钱哪条规则在拖慢系统。调整的时候有据可依而不是凭感觉。6.3 规则版本管理与回滚规则配置要像代码一样管理进版本控制、有变更记录、能一键回滚。我见过有团队直接在生产环境改 YAML改错了没法回滚只能凭记忆改回去风险极高。我的做法是规则配置走 Git每次变更走 PR 流程部署时带上版本号。Runtime 启动时记录当前规则版本出问题能快速定位到是哪次变更引入的。7. 关于这套设计我个人的几点体会做了几个 Agent 项目之后我越来越觉得模型选型的讨论应该尽早结束把精力放到路由设计上。因为模型会一直变今天的最优解明天可能就被超越了但一套好的路由 Runtime 是可以长期复用的。你换模型的时候只需要改配置不用动业务代码。另外一点体会是路由规则要尽量简单。我见过有人把路由写成了一棵复杂的决策树几十个条件嵌套最后自己都说不清楚某个请求会走哪条路。规则一复杂可预测性就没了排查成本指数级上升。宁可多几条简单的规则也不要一条复杂的规则。最后分享一个小技巧在开发环境加一个路由解释接口输入一个请求样本输出它会命中哪条规则、走哪个模型、为什么。这个接口在调试和给产品解释为什么这个请求走了这个模型的时候特别好用能省下大量沟通成本。