1. 账单翻四倍这件事问题不在 Agent 数量先说结论开多个 Agent 之后账单暴涨绝大多数情况下不是因为你多开了几个进程而是因为每个 Agent 都在用同一个最贵的模型干所有活。我最初也是受害者之一——同时跑了四个 Agent 分别处理代码审查、文档生成、单元测试补全和日志分析月底一看账单直接是单 Agent 时期的四倍出头。当时第一反应是并发调用把单价抬上去了但仔细拆完调用记录才发现单价根本没变变的是调用总量和模型分布。这个现象在 Claude Code 这类终端 Agent 工具上特别典型。它的默认行为是不管你让它干什么只要没显式指定它都会走同一个默认模型。你开一个 Agent 的时候这个默认模型可能是合理的但你开四个、五个、六个 Agent每个 Agent 都在用这个全能但昂贵的模型去处理那些本来用便宜模型就能搞定的任务——比如格式化输出、简单文件读写、正则匹配、日志过滤。这些任务占了调用量的六成以上却消耗了八成的费用。所以真正要解决的问题不是怎么少开 Agent而是怎么让不同的任务走不同的模型。这就是模型路由Model Routing要干的事。我后来用一个配置文件把这件事解决了账单从四倍回落到一点三倍左右同时 Agent 的响应速度还快了不少。下面我把整套思路和配置拆开讲。提示本文讨论的是本地开发环境下的 Agent 编排与模型调用成本优化所有配置均基于公开可用的工具链不涉及任何网络访问相关的特殊设置。2. 为什么多 Agent 场景下模型路由是刚需2.1 多 Agent 的调用结构决定了成本分布要理解为什么路由能省钱得先看清楚多 Agent 场景下调用是怎么发生的。假设你有四个 AgentAgent A代码审查需要理解上下文、做推理判断Agent B文档生成需要组织语言、保持格式一致Agent C单元测试补全需要理解函数签名和边界条件Agent D日志分析主要是模式匹配和关键词提取这四个 Agent 里A 和 C 是真正的重推理任务B 是中等推理 大量文本生成D 基本是轻推理 高吞吐。如果你让它们全部走同一个高能力模型那么 D 这种任务就是在用大炮打蚊子——每次调用都在为不需要的推理能力付费。我实测过一组数据在同样的任务量下如果四个 Agent 全部走同一个高能力模型日均调用成本是 X如果把 D 全部切到轻量模型、B 切到中等模型、只保留 A 和 C 走高能力模型日均成本降到约 0.35X。这个比例不是拍脑袋是连续跑了两周、对比了三组配置之后得到的稳定值。2.2 模型路由的本质是任务分级模型路由听起来很技术但它的核心逻辑特别朴素把任务按需要的推理深度分级然后给每一级配一个性价比最合适的模型。我一般把 Agent 任务分成三档档位任务特征典型场景模型选择倾向重推理档需要多步推理、上下文理解、代码逻辑判断代码审查、架构建议、复杂 bug 定位高能力模型中等档需要一定语言组织能力但推理链短文档生成、注释补全、commit message中等模型轻量档模式匹配、格式转换、简单提取日志过滤、文件重命名、正则生成轻量模型这个分级不是固定的你可以根据自己的任务分布调整。关键是你得先知道自己每个 Agent 到底在干什么而不是笼统地觉得Agent 就是需要强模型。2.3 不路由的隐性代价不只是钱除了账单不路由还有两个容易被忽略的代价。第一是延迟轻量任务走重模型响应时间会被拉长因为重模型的推理链更长、输出更慢。我做过对比同一个日志过滤任务轻量模型平均 1.2 秒返回重模型要 4.5 秒。四个 Agent 并发的时候这个延迟差异会被放大。第二是上下文窗口浪费重模型的上下文窗口通常更大但轻量任务根本用不到那么大的窗口等于你在为用不上的容量付费。3. 配置文件长什么样一份可直接抄的路由方案3.1 配置文件的结构设计我用的是一个 JSON 配置文件放在项目根目录下的.agent-routing.json。它的结构分三层默认策略、Agent 级覆盖、任务级覆盖。优先级从低到高任务级最高。{ default: { model: claude-sonnet, max_tokens: 4096, temperature: 0.3 }, agents: { code-review: { model: claude-opus, max_tokens: 8192, temperature: 0.2 }, doc-gen: { model: claude-sonnet, max_tokens: 4096, temperature: 0.5 }, test-fill: { model: claude-opus, max_tokens: 6144, temperature: 0.1 }, log-parse: { model: claude-haiku, max_tokens: 2048, temperature: 0.0 } }, taskOverrides: [ { match: format|rename|extract, model: claude-haiku, max_tokens: 1024 }, { match: review|audit|analyze, model: claude-opus, max_tokens: 8192 } ] }这个配置的核心逻辑是默认走中等模型Agent 级按角色覆盖任务级按关键词再覆盖。三层叠加之后绝大多数调用都能落到合适的档位上。3.2 为什么用 JSON 而不是 YAML很多人喜欢用 YAML 写配置因为可读性好。但在 Agent 路由这个场景下我坚持用 JSON原因有三个。第一JSON 的解析在几乎所有语言里都是零依赖的你不需要额外装库。第二JSON 的结构更适合程序化生成和修改如果你后面想写脚本动态调整路由JSON 处理起来更顺手。第三JSON 的嵌套层级在视觉上更清晰尤其是当你有三层覆盖逻辑的时候YAML 的缩进容易让人看错层级。当然如果你团队已经统一用 YAML那也没问题逻辑是一样的只是格式差异。3.3 关键词匹配的写法与坑taskOverrides里的match字段用的是正则表达式。这里有个坑我踩过正则不要写得太宽泛否则会误伤。比如你写match: test那所有包含 test 的任务都会被路由到重模型包括 test file rename 这种明显该走轻量模型的任务。我的做法是用更具体的关键词组合比如review|audit|analyze而不是check因为 check 太泛了。另外正则匹配是按顺序执行的第一个匹配上的规则会生效所以要把更具体的规则放在前面。注意如果你的任务描述是自然语言关键词匹配可能会有漏网之鱼。这时候可以加一层兜底规则比如所有未匹配的任务默认走中等模型而不是默认走重模型。4. 把配置接进 Claude Code 的实际操作4.1 环境准备与依赖确认在接配置之前先确认你的环境。我用的是 Node.js 20.x 和 Claude Code 的最新稳定版。如果你还没装 Claude Code可以通过 npm 全局安装npm install -g anthropic-ai/claude-code装完之后用claude --version确认版本。这里有个小细节不同版本的 Claude Code 对配置文件的读取路径可能不同有的版本读项目根目录有的版本读用户目录下的.claude文件夹。我建议两个位置都放一份或者用软链接指向同一份配置避免出现改了配置但不生效的情况。4.2 让 Agent 读取路由配置Claude Code 本身不直接读我这个.agent-routing.json它需要一层适配。我的做法是写一个轻量的包装脚本在启动每个 Agent 之前先根据 Agent 名称和任务描述查路由表然后把对应的模型参数通过环境变量或命令行参数传给 Claude Code。// route-agent.js const fs require(fs); const routing JSON.parse(fs.readFileSync(.agent-routing.json, utf8)); function resolveModel(agentName, taskDesc) { // 任务级覆盖优先 for (const rule of routing.taskOverrides) { if (new RegExp(rule.match, i).test(taskDesc)) { return { model: rule.model, maxTokens: rule.max_tokens }; } } // Agent 级覆盖 if (routing.agents[agentName]) { const cfg routing.agents[agentName]; return { model: cfg.model, maxTokens: cfg.max_tokens }; } // 默认 return { model: routing.default.model, maxTokens: routing.default.max_tokens }; } const [,, agentName, ...taskParts] process.argv; const taskDesc taskParts.join( ); const resolved resolveModel(agentName, taskDesc); console.log(JSON.stringify(resolved));这个脚本的输出可以直接被 shell 脚本消费用来设置环境变量。比如#!/bin/bash AGENT_NAMElog-parse TASK_DESCextract error lines from app.log ROUTE$(node route-agent.js $AGENT_NAME $TASK_DESC) MODEL$(echo $ROUTE | jq -r .model) MAX_TOKENS$(echo $ROUTE | jq -r .maxTokens) claude --model $MODEL --max-tokens $MAX_TOKENS --agent $AGENT_NAME4.3 验证路由是否生效配置接进去之后一定要验证。我见过太多人改完配置就直接跑结果发现根本没生效白折腾。验证方法很简单在路由脚本里加一行日志把每次解析出来的模型名打到文件里跑一天之后看分布。// 在 resolveModel 返回之前加 fs.appendFileSync(routing.log, ${new Date().toISOString()} ${agentName} ${resolved.model}\n);然后统计一下awk {print $3} routing.log | sort | uniq -c | sort -rn如果发现轻量模型占比低于预期说明你的关键词匹配有问题或者 Agent 名称没对上。这个验证步骤我强烈建议做因为路由配置的 bug 很隐蔽——它不会报错只会默默多花钱。5. 路由之后账单降了多少实测数据与调优过程5.1 第一版配置的效果我第一版配置上线之后跑了一周账单从四倍降到约 1.8 倍。这个降幅已经不小但离我的目标还有距离。拆开调用记录看问题出在文档生成 Agent 上它虽然被路由到了中等模型但因为文档任务量大、每次输出长总 token 消耗还是很高。5.2 第二版给文档生成加缓存第二版我加了一个简单的缓存层对相同输入的文档生成请求直接返回缓存结果不重复调用模型。这个改动让文档生成 Agent 的调用量降了约四成。缓存用文件系统实现就行不需要引入 Redis 之类的重依赖。const crypto require(crypto); const cacheDir .agent-cache; function cacheKey(agentName, taskDesc) { return crypto.createHash(md5).update(agentName taskDesc).digest(hex); } function getCached(key) { const path ${cacheDir}/${key}.json; if (fs.existsSync(path)) { return JSON.parse(fs.readFileSync(path, utf8)); } return null; }提示缓存要注意失效策略。我的做法是给缓存加 24 小时过期超过就重新调用。对于代码审查这种对时效性要求高的任务我直接不缓存。5.3 第三版动态调整 max_tokens第三版我发现一个细节很多任务的 max_tokens 设得过大导致模型倾向于生成更长的输出。比如日志分析任务我原本设了 2048但实际输出平均只有 300 token。把 max_tokens 降到 512 之后不仅省了 token响应还更快了。这里的原则是max_tokens 设成你预期输出的 1.5 倍左右不要设成上限。因为模型看到大的 max_tokens有时候会凑字数生成一些冗余内容。5.4 最终效果对比三版调优之后账单稳定在约 1.3 倍。下面是三版配置的对比版本核心改动账单倍数平均响应时间原始无路由4.0x3.8s第一版三层路由1.8x2.9s第二版加缓存1.5x2.1s第三版调 max_tokens1.3x1.7s这个数据是在我的任务分布下测的你的场景可能不同但趋势应该类似路由是最大的一刀缓存和参数调优是后续的细活。6. 几个容易踩的坑和我的应对方式6.1 路由配置改了但不生效这是最常见的坑。原因通常有三个一是配置文件路径不对二是 Agent 名称和配置里的 key 不一致三是缓存了旧的解析结果。我的排查顺序是先确认配置文件路径再打印 Agent 名称最后清缓存重跑。建议在路由脚本里加一个--debug参数开启后打印完整的解析链路。6.2 轻量模型处理复杂任务时质量下降路由不是万能的。有些任务看起来简单实际上需要推理。比如提取日志里的错误行听起来是模式匹配但如果日志格式不统一就需要模型做判断。我的应对方式是给每个 Agent 设一个质量兜底机制——如果轻量模型的输出被后续步骤判定为不合格就自动用重模型重跑一次。这个机制会增加一点成本但比全量走重模型便宜得多。6.3 多 Agent 并发时的配置竞争如果你多个 Agent 共享同一份配置文件并且运行时会动态修改它就可能出现竞争。我的做法是配置文件只读运行时状态放在内存或单独的临时文件里。这样多个 Agent 并发读取不会互相干扰。6.4 模型名称硬编码的问题我一开始把模型名称直接写在配置里后来发现换模型的时候要改很多地方。后来改成用别名比如model: heavy、model: light然后在另一个文件里定义别名到实际模型名的映射。这样换模型只需要改一处。{ aliases: { heavy: claude-opus, medium: claude-sonnet, light: claude-haiku } }这个改动看起来小但在实际维护中省了很多事。尤其是当你需要临时把某个 Agent 从 heavy 切到 medium 做对比测试的时候改一个词就行。7. 从单机路由到多 Agent 编排的扩展思路7.1 把路由逻辑抽成独立服务当你的 Agent 数量超过十个或者多个项目共享同一套路由规则时把路由逻辑抽成一个独立的本地服务会更方便。我用 Node.js 写了一个简单的 HTTP 服务Agent 启动时向它查询模型配置。这样配置只需要维护一份所有 Agent 都能用。const http require(http); const routing require(./routing-logic); http.createServer((req, res) { const url new URL(req.url, http://localhost); const agent url.searchParams.get(agent); const task url.searchParams.get(task); const result routing.resolve(agent, task); res.writeHead(200, { Content-Type: application/json }); res.end(JSON.stringify(result)); }).listen(3456);这个服务的开销极小但带来的便利很大。尤其是当你需要做 A/B 测试的时候改服务端的配置就能影响所有 Agent不用逐个改。7.2 按时间段动态调整路由还有一个进阶玩法按时间段调整路由策略。比如白天开发高峰期把更多任务路由到轻量模型以保证响应速度晚上跑批量任务的时候再切回重模型保证质量。这个逻辑可以在路由服务里加一个时间判断。function resolveWithTime(agent, task) { const hour new Date().getHours(); const isPeak hour 9 hour 18; if (isPeak isLightTask(task)) { return { model: light, maxTokens: 1024 }; } return resolve(agent, task); }7.3 监控与告警路由上线之后一定要有监控。我用的是一份简单的日报脚本每天早上把前一天的调用分布、成本估算、异常调用打到终端。异常调用的判定规则是轻量任务走了重模型或者单次调用 token 超过阈值。#!/bin/bash echo 昨日调用分布 awk {print $3} routing.log | sort | uniq -c | sort -rn echo 异常调用 grep -E heavy.*(format|rename|extract) routing.log | head -20这个脚本帮我抓到过好几次配置错误。有一次我把一个 Agent 的名称写错了导致它一直走默认的重模型日报里立刻就看出来了。8. 我在这套方案上的一些个人体会这套路由方案我用了大概三个月最大的感受是成本优化不是一次性的工作而是一个持续调优的过程。你的任务分布会变模型的价格和能力会变Agent 的数量和职责也会变。所以配置要设计得容易改、容易验证、容易回滚。另外一点是不要为了省钱牺牲质量。我见过有人把所有任务都路由到最便宜的模型结果输出质量下降反而要花更多时间返工。路由的目标是让合适的任务走合适的模型而不是全部走最便宜的。重推理任务该用重模型就用省下来的钱应该来自那些本来就不需要重模型的任务。最后分享一个小技巧给每个 Agent 的输出加一个质量评分步骤用轻量模型给重模型的输出打分如果分数低于阈值就触发重跑。这个机制的成本很低但能有效防止路由带来的质量滑坡。我用了之后几乎没有再遇到过路由之后输出变差的问题。