作者将编程任务按复杂度分配给不同模型,实现月度API开销降低35%、延迟下降,且难题质量反而提升,核心规则是'不用最强模型处理简单任务'。
我运行一个完全自主的 Coding Agent,负责处理从简单的 lint 修复到多文件重构的所有任务。几个月以来,我把每一个任务都指向同一个模型。后来我花了一个月时间,根据任务类型而非习惯来分配 Opus、Sonnet 和 Haiku 的工作。每月 API 费用下降了约 35%,平均任务延迟也有所下降,而且——这是让我惊讶的部分——困难任务的质量实际上提高了,因为我不再让最好的模型把注意力浪费在琐碎工作上。以下是我最终采用的路由逻辑、期间出现的问题,以及我希望在第一天就知道的那条规则。
我的 Agent 设置每天运行数十个小型 Coding 任务:修复一个broken test、在整个模块中重命名一个变量、写一行 bug 修复,但偶尔也会遇到棘手的事情,比如"重新设计这个队列消费者的重试逻辑"或"找出为什么这个竞态条件只在负载下发生"。
很长一段时间以来,我把所有任务都通过一个模型运行——当时最新的顶级 Claude 模型。我的推理很懒,但感觉很安全:"就用最好的那个,这样我就不用再考虑了。"
有两件事最终迫使我重新思考:
账单。我每天任务量中有一部分是琐碎的——重命名这个、添加这个 import、修复这个 lint 警告——但我为这些任务支付的是顶级模型的价格,而一个便宜得多的模型可以一次完成。
队列。当十个小型任务和一个真正困难的任务都使用同一个模型时,困难任务和简单任务排在同一队列。我的路由逻辑中没有说"这个需要更多关注"的内容,所以没有什么能得到更多关注。
真正的问题不是成本或速度的孤立存在——而是我把"最佳模型处理一切"当作策略,而实际上这根本不是策略。一个非常擅长在二十分钟内记住庞大架构决策的模型,在重命名变量这件事上显然不是正确的工具,而我从未真正验证过这个假设。
我把任务队列分成三个桶,每个桶匹配一个模型层级:Haiku 用于量大任务,Sonnet 用于默认工作负载,Opus 用于出错代价高昂的任务。
属于这里的内容:lint 修复、import 排序、跨文件重命名符号、从 diff 生成 commit message、分类 PR 是涉及 test 还是 source、总结日志文件。定义特征不是"小"——而是低歧义。基本上只有一个正确答案,模型不需要权衡利弊就能得到答案。
def pick_model(task):
if task.category in ("lint_fix", "rename", "commit_message", "log_summary"):
return "claude-haiku-4-5"
if task.category in ("architecture", "race_condition", "security_review"):
return "claude-opus-4-8"
return "claude-sonnet-5" # default workhorse
这是刻意简单的——一个类别查找,而不是实时决策的智能分类器。我曾尝试构建一个"元 Agent",用模型调用来决定路由到哪个模型,但对于那些从任务类型本身就能明显看出类别的情况来说,这是对模型调用的浪费。静态规则在 80% 答案不会改变的任务上击败了动态路由器。
不属于明显机械或明显高风险的所有内容都落在这一类:普通功能实现、大部分 bug 修复、为现有代码编写测试、常规重构。这是我的默认选择——如果我不确定一个任务应该属于哪个桶,它就去 Sonnet,而不是"以防万一"升级到 Opus。这一习惯改变(默认向下而非向上)比 Haiku 桶贡献了更多的成本节省。
这个桶是有意做小的:架构决策、调试间歇性故障(修复需要解决根本原因而非掩盖症状)、任何涉及 auth 或数据完整性的内容,以及 Agent 将在最小监督下长时间运行的任务。共同特征是:这里的一个错误答案不仅仅意味着重试——它会导致数小时的下游清理工作,或者引入一个代价高昂的 bug(很难追溯)。
flowchart LR
A[Incoming task] --> B{Category known?}
B -- mechanical/high-volume --> C[Haiku]
B -- default/unclear --> D[Sonnet]
B -- architecture/security/root-cause --> E[Opus]
C --> F[Result]
D --> F
E --> F
真正使这个方案可以安全发布的部分是 fallback 规则,而非路由表本身:如果 Haiku 或 Sonnet 任务验证失败两次,它会自动升级一层。"验证失败"意味着测试套件仍然失败、diff 不能干净应用,或者后续检查标记该变更只是部分完成。没有这个规则,一个被错误分类的任务只会在错误的层级燃烧重试次数,永远得不到它真正需要的额外推理。有了它,我的廉价层级可以激进地便宜,因为我不需要在第一次就把分类做对。
def run_task(task, attempt=0):
model = escalate(task.model, attempt) if attempt > 0 else pick_model(task)
result = call_model(model, task)
if not validate(result, task) and attempt < 2:
return run_task(task, attempt + 1)
return result
def escalate(model, attempt):
order = ["claude-haiku-4-5", "claude-sonnet-5", "claude-opus-4-8"]
idx = min(order.index(model) + attempt, len(order) - 1)
return order[idx]
我追踪了切换到分层路由前后的各四周时间,尽量保持工作负载组合一致:
中位周转时间的下降比成本下降更让我惊讶。我原以为延迟主要取决于任务复杂度,但很大一部分实际上是由队列造成的——Haiku 和 Sonnet 每次调用的响应都比 Opus 快,所以把不需要 Opus 的 89% 的任务从 Opus 上移走,加快了整个管道,而不仅仅是那些单个任务。
9% 的升级率是我现在最关注的数字。如果它上升了,通常意味着我的类别列表已经与实际任务组合不一致了——这表明我需要更新路由表,而不是分层路由本身失效的证据。
最大的成本节省不是来自 Haiku 桶——而是把 Sonnet 作为"不确定"任务的默认选择,而不是在我不确定时本能地伸手去拿顶级模型。我以为需要最好模型的大多数任务其实并不需要。
我最初尝试按"这会涉及多少行"来路由,结果很糟糕——一个细微竞态条件的一行修复很小,但并非低歧义。一旦我切换到"这是否有唯一一个明显正确的答案",路由就准确多了。
我在一个早期版本上烧了真钱,那个版本用模型调用来决定路由到哪个模型。对于大约 80% 类别明显的任务,这是在决定一件查表早就知道的事情上浪费模型调用。
没有自动升级,向 Haiku 路由任务是一场没有退路的赌注。有了它,就变成了一个带安全网的廉价首次尝试——这是我能够放心地将任何东西发送到最便宜层级的唯一方式。
这是真正的惊喜。一旦 Opus 只处理大约 10% 的总量而不是 100%,我就不再去想在这上面"浪费"容量了——这意味着我开始给困难任务更多上下文、更多约束、更多周围代码,而不是简洁的描述。模型没有变得更聪明;是因为我不再把它分散到所有事情上,所以对输入不再那么吝啬了。
成本节省会让分层路由看起来很成功,即使类别是错误的,因为 Haiku 即使失败和重试也是便宜的。升级率才是真正告诉你路由表是否匹配真实任务组合的指标——我现在每周检查一次,上升的数字是我重新审视类别列表的信号,在这个数字变成我后期才注意到的质量问题之前。
我正在努力让路由类别自我更新——目前"architecture"和"routine refactor"的分类是手工维护的任务类型字符串列表,它随着我的代码库和工作流变化而漂移。我更希望类别从结果中推导出来(哪些任务类型实际上需要升级),而不是从我最初的猜测中——我的初始猜测经常出错,所以才有了这篇文章。
我也很想知道同样的三层分割是否适用于非 Coding Agent 任务——研究摘要、数据提取——或者歧义轴是否需要按领域重新定义。如果你曾在 Coding Agent 之外尝试过分层路由,我真的很想听听你是如何划线的。
如果你正在运行一个 Claude Code Agent,并且仍然将每个任务指向同一个模型,尝试在你运行之前将接下来的 20 个任务分为"机械的"、"正常的"和"出错代价高昂的"三类——你可能会惊讶地发现最后那个桶里只有很少的任务。如果这篇文章对你有用,在 Dev.to 上关注我——我正在把这个公开构建系列写下去,包括犯过的错误。