不是所有任务都需要最贵的模型。简单的 Bug 修复用 Sonnet 就够了,复杂的架构设计才需要 Opus,而代码格式化用 Haiku 就能搞定。本文设计一个多模型路由系统,根据任务难度、风险等级和成本预算自动选择最合适的模型,在保证质量的前提下把成本压到最低。

多模型路由系统:任务难度、风险等级与成本预算如何决策

不是所有任务都需要最贵的模型。简单的 Bug 修复用 Sonnet 就够了,复杂的架构设计才需要 Opus,而代码格式化用 Haiku 就能搞定。本文设计一个多模型路由系统,根据任务难度、风险等级和成本预算自动选择最合适的模型,在保证质量的前提下把成本压到最低。

一、为什么需要模型路由

一个团队的典型 Agent 使用情况:

任务类型 占比 适合模型 单次成本
代码格式化 / Lint 修复 30% Haiku $0.01
简单 Bug 修复 / 测试编写 35% Sonnet $0.10
复杂 Bug 修复 / 代码审查 25% Opus $0.50
架构设计 / 大型重构 10% Opus $2.00+

如果所有任务都用 Opus:日均成本 = 100 任务 × $0.50 = $50/天 = $1500/月。 如果按路由分配:$0.01×30 + $0.10×35 + $0.50×25 + $2.00×10 = $0.3+$3.5+$12.5+$20 = $36.3/天 = $1089/月。 节省 27% 成本,且质量不下降。

二、路由决策架构

text
┌──────────────┐
│   任务输入    │
│  (Issue/命令) │
└──────┬───────┘
       │
┌──────▼───────────────────────────────────────────┐
│              任务分类器 (Task Classifier)          │
│  ┌─────────┐  ┌──────────┐  ┌──────────────────┐ │
│  │ 难度评估 │  │ 风险评估  │  │ 上下文大小估算   │ │
│  └────┬────┘  └────┬─────┘  └────────┬─────────┘ │
│       └─────────────┼────────────────┘            │
│                     │                             │
│              ┌──────▼──────┐                      │
│              │  路由决策引擎 │                      │
│              │  (Rule-based │                      │
│              │   + ML)      │                      │
│              └──────┬──────┘                      │
└─────────────────────┼────────────────────────────┘
                      │
         ┌────────────┼────────────┐
         │            │            │
    ┌────▼────┐  ┌────▼────┐  ┌───▼─────┐
    │ Haiku   │  │ Sonnet  │  │ Opus    │
    │ (简单)  │  │ (中等)  │  │ (复杂)  │
    └────┬────┘  └────┬────┘  └───┬─────┘
         │            │            │
         └────────────┼────────────┘
                      │
               ┌──────▼──────┐
               │  质量守卫    │
               │  (失败升级)  │
               └─────────────┘

三、路由规则引擎

3.1 路由规则配置

yaml
# model-routing.yaml
version: "2.0"

# 模型定义
models:
  haiku:
    id: "claude-haiku-4-5-20251001"
    cost_per_1k_tokens: 0.001
    speed: "fast"          # 响应速度
    max_context: 200000
    strengths: ["formatting", "simple_edits", "test_generation"]
    
  sonnet:
    id: "claude-sonnet-4-6"
    cost_per_1k_tokens: 0.003
    speed: "medium"
    max_context: 200000
    strengths: ["bugfix", "code_review", "refactoring"]
    
  opus:
    id: "claude-opus-4-8"
    cost_per_1k_tokens: 0.015
    speed: "slow"
    max_context: 200000
    strengths: ["architecture", "complex_debugging", "large_refactor"]

# 路由规则(按优先级排序,匹配第一条即停止)
routing_rules:
  # 强制规则(优先级最高)
  - name: "production_deploy"
    condition: "task.risk_level == 'CRITICAL'"
    model: "opus"
    reason: "生产部署相关任务强制使用最强模型"
  
  - name: "security_fix"
    condition: "task.labels contains 'security'"
    model: "opus"
    reason: "安全修复必须用最强模型"
  
  - name: "budget_guard"
    condition: "daily_budget_remaining < daily_budget * 0.2"
    model: "haiku"
    reason: "预算低于 20% 时降级到最便宜模型"
  
  # 基于难度的规则
  - name: "trivial_edit"
    condition: |
      task.type in ['formatting', 'lint_fix', 'rename']
      AND task.files_changed <= 1
      AND task.estimated_lines <= 5
    model: "haiku"
    reason: "单文件少量修改,简单任务"
  
  - name: "standard_bugfix"
    condition: |
      task.type == 'bugfix'
      AND task.complexity == 'low'
      AND task.files_changed <= 3
    model: "sonnet"
    reason: "标准 Bug 修复"
  
  - name: "complex_bugfix"
    condition: |
      task.type == 'bugfix'
      AND (task.complexity == 'high' OR task.files_changed > 5)
    model: "opus"
    reason: "复杂 Bug 修复需要更强的推理能力"
  
  - name: "code_review"
    condition: "task.type == 'code_review'"
    model: "sonnet"
    reason: "代码审查 Sonnet 够用"
  
  - name: "test_generation"
    condition: "task.type == 'test_generation'"
    model: "sonnet"
    reason: "测试生成 Sonnet 质量好且性价比高"
  
  - name: "large_refactor"
    condition: |
      task.type == 'refactor'
      AND task.files_changed > 10
    model: "opus"
    reason: "大规模重构需要全局理解能力"
  
  # 默认规则
  - name: "default"
    condition: "true"
    model: "sonnet"
    reason: "默认使用 Sonnet(性价比最优)"

# Fallback 策略
fallback:
  on_error: "upgrade"       # 出错时升级到更强的模型
  max_upgrade: "opus"       # 最多升级到 Opus
  max_retries: 2            # 最多重试 2 次
  on_budget_exceeded: "downgrade"  # 超预算时降级

3.2 路由决策引擎

python
# app/routing/router.py
"""
多模型路由决策引擎。
根据任务属性和路由规则选择最合适的模型。
"""
import yaml
from dataclasses import dataclass
from typing import Optional

@dataclass
class TaskAttributes:
    """任务属性(路由决策的输入)"""
    task_id: str
    task_type: str              # bugfix / review / refactor / test / format / ...
    risk_level: str             # LOW / MEDIUM / HIGH / CRITICAL
    complexity: str             # low / medium / high
    labels: list[str]
    files_changed: int
    estimated_lines: int
    context_tokens: int
    approval_required: bool

@dataclass
class RoutingDecision:
    """路由决策结果"""
    model: str
    model_id: str
    reason: str
    matched_rule: str
    fallback_model: Optional[str] = None
    estimated_cost: float = 0.0

class ModelRouter:
    def __init__(self, config_path: str):
        with open(config_path) as f:
            self.config = yaml.safe_load(f)
        self.models = {m["id"]: m for m in self.config["models"].values()}
        self.rules = self.config["routing_rules"]
    
    def route(self, task: TaskAttributes) -> RoutingDecision:
        """为任务选择模型"""
        
        # 按优先级遍历规则
        for rule in self.rules:
            if self._evaluate_condition(rule["condition"], task):
                model_name = rule["model"]
                model_config = self.config["models"][model_name]
                
                # 检查预算
                if not self._check_budget(model_config, task):
                    return self._apply_fallback(task, "budget_exceeded")
                
                estimated_cost = self._estimate_cost(model_config, task)
                
                return RoutingDecision(
                    model=model_name,
                    model_id=model_config["id"],
                    reason=rule["reason"],
                    matched_rule=rule["name"],
                    fallback_model=self._get_fallback(model_name),
                    estimated_cost=estimated_cost,
                )
        
        # 不应该到达这里(default 规则兜底)
        return self._apply_fallback(task, "no_rule_matched")
    
    def _evaluate_condition(self, condition: str, task: TaskAttributes) -> bool:
        """评估规则条件(简化版,实际用 CEL 或 Python eval)"""
        # 安全评估:只允许访问 task 的属性
        context = {"task": task}
        try:
            # 实际生产环境应该用安全的表达式引擎(如 CEL)
            # 这里用简单的字符串匹配演示
            if "contains" in condition:
                # task.labels contains 'security'
                parts = condition.split("contains")
                labels = getattr(task, parts[0].strip().split(".")[-1])
                value = parts[1].strip().strip("'\"")
                return value in labels
            
            if "==" in condition:
                left, right = condition.split("==")
                left = left.strip()
                right = right.strip().strip("'\"")
                if left.startswith("task."):
                    attr = left.split(".")[-1]
                    return str(getattr(task, attr)) == right
                return False
            
            if condition == "true":
                return True
            
            return False
        except Exception:
            return False
    
    def _check_budget(self, model_config: dict, task: TaskAttributes) -> bool:
        """检查预算是否充足"""
        estimated = model_config["cost_per_1k_tokens"] * task.context_tokens / 1000
        remaining = get_daily_budget_remaining()
        return estimated <= remaining
    
    def _estimate_cost(self, model_config: dict, task: TaskAttributes) -> float:
        return model_config["cost_per_1k_tokens"] * task.context_tokens / 1000
    
    def _get_fallback(self, model_name: str) -> Optional[str]:
        fallback_chain = {"haiku": None, "sonnet": "haiku", "opus": "sonnet"}
        return fallback_chain.get(model_name)
    
    def _apply_fallback(self, task: TaskAttributes, reason: str) -> RoutingDecision:
        return RoutingDecision(
            model="haiku",
            model_id=self.config["models"]["haiku"]["id"],
            reason=f"Fallback: {reason}",
            matched_rule="fallback",
            estimated_cost=0,
        )

3.3 质量守卫与失败升级

python
# app/routing/quality_guard.py
"""
质量守卫:如果低模型失败,自动升级到更强的模型。
"""

async def execute_with_guard(task: TaskAttributes, router: ModelRouter):
    """带质量守卫的执行流程"""
    
    decision = router.route(task)
    current_model = decision.model
    retry_count = 0
    max_retries = 2
    
    while retry_count <= max_retries:
        # 执行任务
        result = await execute_with_model(task, current_model)
        
        # 质量检查
        if result.success and result.quality_score >= 0.7:
            log_routing_decision(task, current_model, decision, "success")
            return result
        
        # 失败处理
        retry_count += 1
        
        if retry_count > max_retries:
            log_routing_decision(task, current_model, decision, "max_retries_exceeded")
            return result  # 返回最后的结果,标记为需要人工介入
        
        # 升级模型
        next_model = router._get_fallback(current_model)
        if next_model:
            log.info(f"Task {task.task_id}: {current_model} failed, upgrading to {next_model}")
            current_model = next_model
        else:
            log.warning(f"Task {task.task_id}: already at strongest model, retrying")
    
    return result

四、成本预算守卫

4.1 预算控制器

python
# app/routing/budget.py
"""
成本预算守卫:防止单日/单任务超出预算。
"""
from datetime import date, timedelta

class BudgetGuard:
    def __init__(self, config: dict):
        self.daily_limit = config["daily_limit"]       # 每日预算
        self.per_task_limit = config["per_task_limit"]  # 单任务预算上限
        self.alert_threshold = config["alert_threshold"]  # 告警阈值(0.8 = 80%)
    
    def check(self, estimated_cost: float, task_id: str) -> tuple[bool, str]:
        """检查是否允许执行"""
        
        # 1. 单任务预算检查
        if estimated_cost > self.per_task_limit:
            return False, f"Estimated cost ${estimated_cost:.2f} exceeds per-task limit ${self.per_task_limit:.2f}"
        
        # 2. 每日预算检查
        spent_today = get_daily_spent(date.today())
        remaining = self.daily_limit - spent_today
        
        if estimated_cost > remaining:
            return False, f"Estimated cost ${estimated_cost:.2f} exceeds daily remaining ${remaining:.2f}"
        
        # 3. 告警检查
        usage_ratio = spent_today / self.daily_limit
        if usage_ratio >= self.alert_threshold:
            send_alert(
                f"⚠️ 每日预算已使用 {usage_ratio:.0%}",
                details=f"已花费 ${spent_today:.2f} / ${self.daily_limit:.2f}",
                task_id=task_id,
            )
        
        return True, "OK"
    
    def record_cost(self, task_id: str, model: str, tokens: int, cost: float):
        """记录实际成本"""
        record_event({
            "task_id": task_id,
            "model": model,
            "input_tokens": tokens.get("input", 0),
            "output_tokens": tokens.get("output", 0),
            "cost": cost,
            "timestamp": datetime.now().isoformat(),
        })

4.2 路由日志

json
// routing-log-entry.json
{
  "task_id": "TASK-2024-001",
  "task_type": "bugfix",
  "risk_level": "MEDIUM",
  "routing_decision": {
    "model": "sonnet",
    "matched_rule": "standard_bugfix",
    "reason": "标准 Bug 修复",
    "estimated_cost": 0.12,
    "actual_cost": 0.09,
    "fallback_triggered": false,
    "upgrade_chain": []
  },
  "quality": {
    "tests_passed": true,
    "quality_score": 0.85,
    "human_approved": false
  },
  "cost_saving_vs_opus": 0.41
}

五、真实经验与踩坑

5.1 任务难度分类器不够准

场景:基于规则的分类器把一个"看似简单的 Bug"分到了 Haiku,结果 Haiku 修不了(涉及跨模块的隐式依赖)。 问题:规则分类器只看"文件数"和"行数",无法理解任务的真实复杂度。 解决方案:增加一个"预估上下文 Token 数"的指标——如果一个 Bug 需要读取很多文件才能理解,即使改动的代码很少,也应该用 Sonnet 或 Opus。同时增加失败升级机制:Haiku 失败自动升级 Sonnet。

5.2 Prompt Cache 改变了成本结构

场景:原来 Sonnet 和 Opus 的成本差距是 5 倍,但开启 Prompt Cache 后,重复前缀的成本降低 90%。 问题:路由规则基于原始成本制定,没有考虑 Cache 的影响。在有 Cache 的情况下,直接用 Opus 的成本可能比 Haiku 无 Cache 还低。 解决方案:路由规则的成本估算要区分"有 Cache"和"无 Cache"两种情况。如果任务可以复用前缀(如相同的 CLAUDE.md + 项目规范),优先选 Opus + Cache。

5.3 Fallback 升级的延迟问题

场景:Haiku 执行失败,升级到 Sonnet 重试。总耗时 = Haiku 耗时 + Sonnet 耗时。 问题:如果 Haiku "自信地"给出了一个错误答案(不是报错,而是结果质量差),质量守卫需要运行测试才能发现。整个过程可能浪费了 30 秒。 解决方案:给 Haiku 设置更短的超时(10 秒),如果 10 秒内没完成就直接用 Sonnet。对于质量检查,优先用"静态分析 + Lint"做快速检查,全量测试只在静态分析通过后才运行。

六、参数说明表

参数 类型 默认值 说明
models object 见 3.1 可用模型列表和配置
routing_rules list 见 3.1 路由规则(按优先级)
daily_budget float 50.0 每日预算上限(美元)
per_task_budget float 2.0 单任务预算上限
alert_threshold float 0.8 预算告警阈值
max_upgrade_level int 2 最大升级次数
quality_threshold float 0.7 质量守卫阈值
enable_cache_routing bool true 是否考虑 Prompt Cache
fallback_on_error bool true 失败时是否自动升级模型
log_all_decisions bool true 是否记录所有路由决策

七、落地检查清单

  • 每个模型的成本参数已正确配置
  • 路由规则按优先级排序,强制规则在最前面
  • 预算守卫已启用(每日 + 单任务)
  • 失败升级链路正确(Haiku → Sonnet → Opus)
  • 路由日志记录了每次决策和实际成本
  • 成本节约有量化统计(vs 全部用 Opus)
  • 预算告警能通知到负责人
  • Prompt Cache 场景下的成本估算正确
  • 质量守卫不会无限升级(有最大重试限制)
  • 规则可以热更新,不需要重启服务

八、系列导航

上一篇:Lab 006:同一需求从 PRD 到代码,哪个 Agent 最会问问题? 下一篇:Agent 成本周报:如何按项目、成员、任务类型拆账