直接回答

**AI Agent 模型路由是一套受约束的决策系统:在满足步骤质量、延迟与安全契约的候选路径中,选择完整成本最低的一条。**完整成本不仅包含模型 Token,还包含路由器、验证器、失败首轮、重试、回退、工具执行、人工审批与错误修复。因此,生产系统应优化的是“单次验收 Agent 轨迹成本”,而不是最便宜的模型或最少的参数量。

本文只讨论 AI Agent 内部如何按步骤选择执行路径。托管 API、自部署与端侧部署如何统一核算,属于另一层问题,可参阅 AI 推理成本经济学。

核心要点

  • 当规划、检索、工具调用和答案合成需要不同能力时,应在 Agent 步骤级路由,而不是整段会话固定一个模型。
  • 路由器在生成前选模型;级联根据首轮输出决定验收、重试、升级、拒答或转人工。
  • 核心指标是单次验收轨迹成本,而非供应商报价、参数量或平均基准分。
  • 对有副作用的工具调用,误放行通常比误升级更危险,因为更强模型也无法撤销已经提交的错误操作。
  • 历史回放、影子流量、受控灰度和自动回滚缺一不可;离线基准获胜不等于可以上线。
  • 路由策略、Prompt、模型端点、验证器与评测集必须绑定版本,任何一项变化都要重新核验。

Agent 模型路由解决什么问题?

Agent 模型路由解决的是工作负载分配问题:**同一条 Agent 轨迹里的不同步骤,可能需要完全不同的能力与风险控制。**规划步骤可能依赖跨上下文推理,而一个受 Schema 约束的查询步骤更看重参数构造是否稳定。全部交给同一个模型可能浪费成本;把所有“看起来简单”的步骤交给小模型,又可能在不报错的情况下损害最终成功率。

这类系统至少包含三种决策:

决策方式 决策时可用证据 常见动作 主要风险
静态分配 设计期已知步骤类型 将工作流节点绑定固定路径 工作负载漂移后配置失效
生成前路由 请求、状态、风险和模型元数据 推理前选择一个候选 路由器误判能力需求
输出级联 首轮输出与验证信号 验收、重试、升级、拒答或审批 浪费首轮推理和验证成本

RouteLLM 与 FrugalGPT 分别展示了学习型路由和级联如何在特定模型池、数据集和质量指标上权衡成本。它们证明了机制可行,不证明生产环境存在通用节省比例。LLMRouterBench 进一步指出,结果高度依赖候选模型池,并非所有复杂路由器都优于简单基线。

Agent 场景还有额外边界。普通问答可以生成后再评价,但工具调用可能立刻修改生产数据。因此,查询级聊天路由的结论不能直接迁移到条件化、步骤级 Agent 执行。TwinRouterBench 与 Switchcraft 更接近这一问题,分别覆盖逐调用路由与工具调用正确性。

路由器与级联:先确定决策发生在哪

当输入特征足以预测能力需求时,优先用生成前路由;当首轮输出能显著提高判断质量时,使用级联。生产系统也可以先路由,再在候选路径内级联。

flowchart LR A["Agent 步骤与状态"] --> B["风险与策略检查"] B --> C["生成前路由器"] C --> D["候选模型"] D --> E["类型化输出与证据"] E --> F{"质量门禁通过?"} F -->|通过| G["提交步骤结果"] F -->|可重试| H["重试或回退"] F -->|不确定| I["升级或拒答"] F -->|高风险| J["人工审批"] H --> E I --> E

可按以下条件选择:

条件 优先方案 原因
步骤分类稳定、路由歧义低 静态或规则路由 容易审计,运行开销低
请求特征能稳定预测模型适配度 生成前路由器 避免先支付失败首轮成本
生成后可以可靠核验正确性 输出级联 输出证据有助于决定是否停止
操作不可逆或受监管 审批或拒答门禁 回退模型无法撤销错误提交
没有候选满足契约 拒答 强制选择只会掩盖不支持的工作

置信度分数不天然等于正确概率。把它用作升级阈值前,要按路由、任务族、语言、工具和风险等级做校准。即使路由器代码不变,供应商、模型或 Prompt 变化也可能让旧校准失效。

选模型前先定义路由契约

路由契约规定“无论哪个模型执行,哪些条件都必须成立”。如果没有验收边界,“换成更便宜模型”就不是可审计的工程决策。

至少定义以下字段:

yaml
step_contract:
  step_type: "create_refund"
  risk_tier: "high"
  allowed_tools: ["payments.get_order", "payments.create_refund"]
  output_schema: "refund_decision_v3"
  max_end_to_end_ms: 3500
  max_attempts: 2
  requires_evidence: true
  requires_human_approval: true
  abstain_when:
    - "订单归属不确定"
    - "退款金额超出策略"

完整契约应覆盖:

  1. 任务边界:该步骤能决定什么,什么必须交给其他组件。
  2. 类型化输出:Schema、必需证据和允许调用的工具。
  3. 授权约束:用户身份、租户、数据范围与操作权限。
  4. 质量门禁:确定性检查、任务专属评估器或人工复核。
  5. 延迟预算:路由、验证、重试与回退在内的总时延。
  6. 失败策略:重试、换模型、换工具、拒答或转审批。
  7. 副作用边界:哪些动作可以模拟,哪些动作必须通过提交门禁。

外围的 Agent Harness 应在模型之外强制执行这些约束。模型可以提出动作,但授权与不可逆提交必须由策略代码掌控。

按能力、风险与运行状态路由

可靠路由特征描述的是工作及其后果,而不是预设某一类模型永远“够用”。

能力特征

  • 步骤类型:规划、抽取、排序、合成、代码修改或工具调用
  • 输入语言与模态
  • 上下文长度与证据密度
  • 输出 Schema 与可用工具数量
  • 是否需要长程状态或跨步骤一致性
  • 候选模型在同类切片上的历史验收率

风险特征

  • 只读操作还是状态变更
  • 可逆性与资金影响
  • 是否涉及个人、机密或受监管数据
  • 是否向外部发送消息或执行代码
  • 是否需要人工审批与审计凭证

运行特征

  • 当前端点延迟与错误率
  • 剩余 Token 和上下文预算
  • 本条轨迹此前失败次数
  • 供应商可用性与区域限制
  • 路由队列深度与私有运行时容量

不要只按 Prompt 长度、参数量或一个笼统的“复杂度”标签路由。这些代理特征可能只在某个数据集上相关。进入决策的特征必须可以记录、复现,并能与真实验收结果关联。

同时衡量验收步骤与验收轨迹

只有步骤与完整轨迹都满足契约,路由才算成功。局部格式正确并不够:合法 JSON 仍可能选择错误工具,合法参数仍可能指向错误账户,每一步看似合理也可能无法完成用户目标。

应同时记录两层证据:

层级 必需证据
步骤验收 Schema 合法、工具与参数正确、证据可归因、策略允许、副作用已核对
轨迹验收 用户目标达成、没有危险动作、最终状态一致、延迟满足 SLO、恢复流程完整

核心指标包括:

text
步骤验收率 =
  已验收步骤数 / 尝试步骤数

轨迹验收率 =
  已验收轨迹数 / 启动轨迹数

误放行率 =
  门禁放行的无效输出数 / 门禁放行总数

升级率 =
  升级步骤数 / 路由步骤数

浪费首轮成本 =
  被拒绝的首轮尝试成本

单次验收轨迹成本 =
  路由总成本 / 已验收轨迹数

路由总成本 必须覆盖路由器、每次模型尝试、验证器、回退、工具执行、人工复核与错误修复。私有运行时还要按照与通用推理成本模型相同的边界分摊基础设施和运维,否则 API 与自部署路径不可比较。

级联期望成本

对于两级级联,最低限度的期望模型是:

text
路由期望成本 =
    首选路径成本
  + 升级概率 * 回退成本
  + 验证器成本
  + 误放行概率 * 修复与风险成本

最后一项可以阻止“便宜但不可靠”的门禁在报表中显得高效。如果误放行后果无法可信定价,就把它设为硬约束,不要随意填一个金额。

构建可复现的路由策略评估

下面的 Python 标准库程序对已记录的路由结果进行策略对比。它不直接调用模型,而是评估回放或影子执行器产生的证据,从而把验收边界和成本口径写进可运行逻辑。

保存为 evaluate_routes.py:

python
from __future__ import annotations

import argparse
import json
import math
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable


@dataclass(frozen=True)
class Outcome:
    policy: str
    trajectory_id: str
    accepted: bool
    gate_accepted: bool
    escalated: bool
    latency_ms: float
    route_cost: float

    @classmethod
    def from_json(cls, value: dict) -> "Outcome":
        required = {
            "policy",
            "trajectory_id",
            "accepted",
            "gate_accepted",
            "escalated",
            "latency_ms",
            "route_cost",
        }
        missing = required - value.keys()
        if missing:
            raise ValueError(f"missing fields: {sorted(missing)}")
        for key in ("policy", "trajectory_id"):
            if not isinstance(value[key], str) or not value[key]:
                raise ValueError(f"{key} must be a non-empty string")
        for key in ("accepted", "gate_accepted", "escalated"):
            if type(value[key]) is not bool:
                raise ValueError(f"{key} must be a boolean")
        for key in ("latency_ms", "route_cost"):
            if (
                isinstance(value[key], bool)
                or not isinstance(value[key], (int, float))
            ):
                raise ValueError(f"{key} must be numeric")
        outcome = cls(**{key: value[key] for key in required})
        if outcome.latency_ms < 0 or outcome.route_cost < 0:
            raise ValueError("latency_ms and route_cost must be nonnegative")
        return outcome


def read_jsonl(path: Path) -> Iterable[Outcome]:
    with path.open(encoding="utf-8") as handle:
        for line_number, line in enumerate(handle, 1):
            if not line.strip():
                continue
            try:
                yield Outcome.from_json(json.loads(line))
            except (TypeError, ValueError, json.JSONDecodeError) as error:
                raise ValueError(f"{path}:{line_number}: {error}") from error


def percentile(values: list[float], quantile: float) -> float:
    if not values:
        raise ValueError("cannot calculate a percentile for an empty list")
    ordered = sorted(values)
    index = max(0, math.ceil(len(ordered) * quantile) - 1)
    return ordered[index]


def evaluate(outcomes: Iterable[Outcome], latency_slo_ms: float) -> dict:
    grouped: dict[str, list[Outcome]] = defaultdict(list)
    for outcome in outcomes:
        grouped[outcome.policy].append(outcome)
    if not grouped:
        raise ValueError("input contains no outcomes")

    report = {}
    for policy, rows in sorted(grouped.items()):
        accepted = sum(row.accepted for row in rows)
        false_accepts = sum(
            row.gate_accepted and not row.accepted for row in rows
        )
        total_cost = sum(row.route_cost for row in rows)
        report[policy] = {
            "trajectories": len(rows),
            "acceptance_rate": accepted / len(rows),
            "false_acceptance_rate": false_accepts
            / max(1, sum(row.gate_accepted for row in rows)),
            "escalation_rate": sum(row.escalated for row in rows) / len(rows),
            "p95_latency_ms": percentile(
                [row.latency_ms for row in rows], 0.95
            ),
            "latency_slo_pass_rate": sum(
                row.latency_ms <= latency_slo_ms for row in rows
            )
            / len(rows),
            "total_cost": total_cost,
            "cost_per_accepted_trajectory": (
                total_cost / accepted if accepted else None
            ),
        }
    return report


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("outcomes", type=Path, help="JSONL route outcomes")
    parser.add_argument("--latency-slo-ms", type=float, required=True)
    args = parser.parse_args()
    if args.latency_slo_ms <= 0:
        parser.error("--latency-slo-ms must be positive")
    try:
        report = evaluate(read_jsonl(args.outcomes), args.latency_slo_ms)
    except (OSError, ValueError) as error:
        parser.error(str(error))
    print(json.dumps(report, indent=2, sort_keys=True))


if __name__ == "__main__":
    main()

每行 JSONL 表示某个策略下完成的一条轨迹:

json
{"policy":"baseline","trajectory_id":"t-001","accepted":true,"gate_accepted":true,"escalated":false,"latency_ms":1800,"route_cost":0.042}
{"policy":"candidate","trajectory_id":"t-001","accepted":true,"gate_accepted":true,"escalated":true,"latency_ms":2400,"route_cost":0.031}
{"policy":"candidate","trajectory_id":"t-002","accepted":false,"gate_accepted":true,"escalated":false,"latency_ms":900,"route_cost":0.008}

执行:

bash
python3 evaluate_routes.py outcomes.jsonl --latency-slo-ms 3000

不能只看聚合结果批准策略。还要按任务族、语言、风险等级、工具、供应商和路由决策切片。一个策略可能改善总体平均值,却让规模很小但影响很大的高风险切片退化。

上生产前如何评估?

安全发布要把策略评估与用户影响分开。

1. 历史回放

用冻结且有版本的评测集重放所有候选路径。样本应覆盖普通请求、历史失败轨迹、工具错误、授权拒绝、歧义输入和对抗场景,并防止未来信息泄漏到历史状态。

2. 影子评估

候选策略与生产路径并行执行,但不提交候选动作。比较路由选择、验收结果、成本和延迟。对状态变更工具使用模拟器或只读验证,避免影子路径重复产生副作用。

3. 受控灰度

只开放给明确符合条件的小范围流量。高风险动作在审批路径获得独立证据前不进入灰度。保留旧路径,并使用确定性分流,让对照结果可以复现。

4. 自动回滚

回滚应由质量与安全门禁触发,而不是只看成本:

  • 轨迹验收率跌破阈值;
  • 误放行率超过阈值;
  • 出现未授权或重复副作用;
  • p95 或 p99 端到端延迟违反 SLO;
  • 升级率或拒答率超出预期区间;
  • 路由分布塌缩到单一候选;
  • 模型、供应商、Prompt 或验证器变更但未重新评测。

Agent 可观测性 应把每次路由决策连接到最终轨迹结果。至少记录策略版本、决策特征、候选集、选中路径、门禁结果、升级原因、成本、延迟与副作用回执。不要为了调试路由而记录敏感 Prompt 或凭证。

小型语言模型应该放在哪里?

小型语言模型 只是候选模型类别,不是路由策略。对于重复、边界清楚的步骤,只要评测证明工具调用和完整轨迹可验收,它可能是合适路径。NVIDIA Research 的 Agent SLM 立场论文 主张异构 Agent 系统,但它是立场论文,不是“小模型一定更便宜或可靠”的通用证据。

所有候选模型都要回答同一组问题:

  • 能否稳定生成所需类型化输出?
  • 在目标分布上能否选择正确工具和参数?
  • 量化或运行时配置是否改变验收率?
  • 重试率、升级率与拒答率是多少?
  • 是否满足延迟和基础设施约束?
  • 计入全部回退工作后,单次验收轨迹成本是多少?

参数量不能回答这些问题,通用推理基准也不能。模型选择必须基于工作负载证据;证据不足时,路由应允许回退或拒答。

常见失败模式

门禁只检查语法,不检查语义

JSON Schema 能证明形状正确,不能证明决策正确。还要校验标识符、策略上限、证据来源与预期状态变更。

路由器学到了供应商或数据集伪特征

学习型路由器可能利用格式、延迟或基准数据中的偶然模式,而这些模式在生产中并不存在。应使用留出的时间窗口、新工具和变更后的模型池测试。

升级掩盖了浪费工作

很高的最终验收率可能掩盖昂贵的失败首轮。被拒绝尝试的成本与回退延迟必须单独报告。

路由改变了 Agent 状态

不同模型可能用不同方式总结证据或构造工具参数,导致后续步骤分叉。评测对象应是完整轨迹,而不是孤立调用。

回退重复执行副作用

所有重试都应幂等。允许回退模型调用状态变更工具前,必须使用操作 ID、去重机制和提交记录。

降本覆盖了安全目标

质量与安全应先作为硬约束,只在通过门禁的策略中优化成本。对于不支持或高风险任务,拒答本身可以是正确的策略结果。

生产检查清单

  1. 比较路由前先定义步骤与轨迹验收。
  2. 建立简单静态基线,不预设学习型路由一定更好。
  3. 对候选集、策略、Prompt、验证器和数据集统一版本化。
  4. 计入路由、验证、重试、回退、工具、复核与修复成本。
  5. 按有业务意义的工作负载切片校准阈值。
  6. 将授权和不可逆提交保留在模型控制之外。
  7. 严格按历史回放、影子、灰度、回滚顺序发布。
  8. 将路由决策追踪到最终结果,同时保护敏感数据。
  9. 供应商、模型、Prompt、工具或流量变化后重新评测。
  10. 没有路径满足契约时,优先拒答,不强制执行。

常见问题

Agent 是否应该用一个模型规划、另一个模型调用工具?

只有在相同安全与延迟约束下,评测证明这种分工提高验收轨迹时才应该采用。“大模型规划、小模型执行”是值得验证的假设,不是通用架构。有些工作流需要更强模型选工具,另一些工作流可以用确定性代码规划,只让模型负责合成。

能否直接用模型置信度决定升级?

置信度可以作为一个特征,但必须用真实验收结果校准。模型自报置信度尤其不能单独作为正确性证据,应与确定性验证、任务专属评估器、风险规则和历史路由表现结合。

模型级联一定比生成前路由更准确吗?

不一定。级联能看到首轮输出,但弱验证器可能放行错误结果或错误升级正确结果,同时增加延迟与成本。两种方案都应在相同工作负载上与简单静态基线比较。

路由策略多久需要重新评测?

候选模型、供应商、Prompt、工具 Schema、验证器、流量结构或风险策略变化时都要重新评测。还应持续监控路由分布和验收率,因为供应商静默变化与工作负载漂移都会破坏旧校准。

所有模型路径都失败时怎么办?

策略应按步骤契约选择拒答、请求澄清、确定性备用路径或人工审批。反复把同一个请求发给更大的模型,不是完整的失败策略。

参考资料

总结

Agent 模型路由是一项评测与控制工程,而不是模型排行榜。先定义步骤契约,只在合格候选中路由,在产生副作用前验证输出,并把结果归因到完整轨迹。只有当低成本首轮在不违反质量、延迟和安全门禁的前提下降低了单次验收轨迹成本,它才创造了真实价值。