# Step 25: 模型切换
一句话导读:把「用哪个模型」从硬编码变成可切换——一张模型表 + 模糊匹配的
/model命令,切换时同步更新成本单价,让同一个引擎能在便宜的 Haiku、均衡的 Sonnet、最强的 Opus 之间自由换挡。
# 一、这一步做了什么(What)
新增 utils/models.ts,并把模型变成运行时可切换的状态:
MODELS表定义 3 个模型:Sonnet 4(默认/均衡)、Opus 4(最强/最贵)、Haiku 3.5(最快/最便宜),每个带 id、名称、input/output 单价、描述;matchModel()用模糊匹配把用户输入("opus"、"haiku")解析成模型定义;/model命令:无参列出所有模型,带参切换;- 切换后调
cost.setPrices()同步更新成本追踪的单价——这是关键联动; callWithTools新增model参数,请求时带上当前模型 id。
# 二、面试官视角:为什么要做模型切换?(Why)
面试题:一个 Agent 从头到尾用同一个够强的模型不就行了?为什么要在多个模型间切换?
因为模型能力和价格是线性挂钩的,而任务难度不是恒定的——用一个模型跑所有任务,要么贵、要么弱。
| 场景 | 用固定 Opus | 用固定 Haiku | 按需切换 |
|---|---|---|---|
| "帮我 git status" 这类杂活 | 大炮打蚊子,贵 15/75 | 刚好够用,0.8/4 | Haiku,省钱 |
| 复杂多步重构 | 胜任 | 能力不足,容易翻车 | Opus,保质量 |
| 日常对话 | 偏贵 | 有时不够 | Sonnet,均衡 |
核心动机是成本优化 = 让任务难度匹配模型档位。Opus 的单价是 Sonnet 的 5 倍、Haiku 的近 20 倍(output 75 vs 4)。一个成熟的 Agent 应该像开车换挡:平路用低挡省油,爬坡才踩到底。同时透明性也要跟上——用户得随时知道「现在跑的是哪个模型」,否则可能在不知情下用 Opus 烧钱。这一步把「换挡」的机制先做出来,为「按预算自动换挡」打基础。
# 三、原理:它是怎么工作的(How)
# 模型表与匹配(真实代码)
export interface ModelDef {
id: string; name: string;
pricePerMInput: number; pricePerMOutput: number;
description: string;
}
export const MODELS: ModelDef[] = [
{ id: "claude-sonnet-4-20250514", name: "Sonnet 4", pricePerMInput: 3, pricePerMOutput: 15, description: "default, balanced" },
{ id: "claude-opus-4-20250514", name: "Opus 4", pricePerMInput: 15, pricePerMOutput: 75, description: "most capable, expensive" },
{ id: "claude-haiku-3-5-20241022",name: "Haiku 3.5", pricePerMInput: 0.8, pricePerMOutput: 4, description: "fast, cheap" },
];
export function getModel(id: string): ModelDef | undefined { return MODELS.find(m => m.id === id); }
export function matchModel(input: string): ModelDef | undefined {
const lower = input.toLowerCase();
return MODELS.find(m => m.name.toLowerCase().includes(lower) || m.id.includes(lower)); // 模糊匹配
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
matchModel 的巧思:用户不用背完整 id claude-opus-4-20250514,输入 opus 就能命中——因为它同时在 name 和 id 里做 includes 子串匹配。降低记忆负担。
# 切换的核心联动:模型 ↔ 定价
// /model opus 时
const m = matchModel("opus");
currentModel = m;
cost.setPrices(m.pricePerMInput, m.pricePerMOutput); // ★ 同步更新成本单价
2
3
4
这一行 setPrices 是 step25 真正的题眼。step23 的 CostTracker 单价是跟着模型走的——换了模型不更新单价,累计成本就会按旧价算错。所以「切模型」不是一个动作,而是两个必须原子发生的动作:换 id + 换单价。
# 数据流
/model opus
↓
matchModel("opus") → Opus 4 定义
↓
currentModel = Opus 4
cost.setPrices(15, 75) // 定价跟着切
↓
下一次 callWithTools(..., model: currentModel.id) // 请求带上新 id
↓
cost.add(inT, outT) // 用 Opus 单价累加
2
3
4
5
6
7
8
9
10
对应 index.ts 真实接线:
cost.setPrices(currentModel.pricePerMInput, currentModel.pricePerMOutput);
// /model 命令里:
cost.setPrices(m.pricePerMInput, m.pricePerMOutput);
// 请求时:
cost.add(result.inT, result.outT);
2
3
4
5
# 设计细节表
| 细节 | 做法 | 为什么 |
|---|---|---|
| 模糊匹配 | name 和 id 都做 includes | 用户输 "opus" 即可,不用背长 id |
| 切换即改价 | setPrices 与切模型绑定 | 定价随模型走,成本才算得准 |
| 单价存进模型表 | 定价是 ModelDef 的字段 | 模型和它的价是一体的,不分散 |
| model 作为请求参数 | callWithTools(..., model) | 引擎不写死模型,可按需传入 |
# 四、深入追问(面试常见 follow-up)
Q:切换模型时如果忘了调 setPrices,会发生什么?
A:成本会按旧模型的单价继续累加,账单错得离谱。比如从 Sonnet 切到 Opus 却没换价,Opus 的高消耗会按 Sonnet 的 1/5 价计,严重低估花费。所以切模型和改价必须绑死——这正是为什么 index.ts 里两者永远相邻出现。
Q:matchModel 用 includes 模糊匹配,会不会误匹配?比如输入 "4" 命中好几个?
A:会,这是模糊匹配的固有风险。输 "4" 会命中 Sonnet 4 / Opus 4(find 返回第一个)。这一版靠「用户一般输有辨识度的词(opus/haiku)」规避,没做歧义提示。更严谨的做法是:匹配到多个时列出候选让用户确认。学习版取了简单。
Q:切换模型后,之前用旧模型积累的对话历史还能用吗?会不会有兼容问题? A:能用。对话历史是 provider 无关的 messages 数组,Sonnet 说的话 Opus 完全能读。切模型只影响「下一次由谁来生成」,不影响已有历史。这也是「运行中途换挡」可行的前提。
Q:标题说「Token 预算驱动的自动切换」,但代码里 /model 是手动的,自动在哪?
A:这一步落地的是切换机制(手动 /model),「按预算自动切」是这个机制之上的策略——有了「能切 + 切了价对」的基础,才谈得上「预算紧时自动降到 Haiku」。真实版会根据剩余预算/任务类型自动选档;学习版先把可切换、价联动这两块底座打好。
# 五、踩坑 / 设计权衡
- 模糊匹配的甜与苦:
includes让用户输 "opus" 就行,体验好;但也会在输入过短(如 "4")时误命中。便利与精确的经典权衡。 - 定价内联在模型表 vs 独立价表:把单价作为 ModelDef 字段,好处是模型和价不会走散;坏处是价格变动要改代码。真实版倾向外置定价、动态拉取。
- 3 个模型 vs 全量:学习版只列 3 个代表档位,真实
utils/model/(16 文件)要处理别名、区域、fallback、能力探测等一大堆。
# 六、与真实源码的对照
| 我们的实现 | Claude Code 源码 |
|---|---|
utils/models.ts(3 模型 + 模糊匹配) | utils/model/(16 文件) |
matchModel 子串匹配 | 别名解析 + 歧义处理 + 区域/fallback |
切换即 setPrices | 模型元数据自带定价,切换自动生效 |
/model 手动切换 | 支持按预算/任务自动选模型 |
| 单价内联在模型表 | 定价可外置/动态获取 |
# 七、一句话总结
模型切换 = 给引擎装换挡杆:一张带单价的模型表 + 模糊匹配命令,把「用哪个模型」变成运行时状态;核心纪律是「切模型必同步切定价」,这样成本才算得准,也才为「按预算自动降档省钱」这一策略铺好机制底座。
# 下一节预告
模型、预算、权限这些设置,每次重启都要重设太麻烦。step26 引入 .claude-settings.json 配置持久化,让选择记得住、活得过重启。