「写给读者的话」本系列文章记录了笔者在学习Gemini CLI源码过程中的点点滴滴,更多从代码实现细节中学习如何设计一个优秀的代码Agent,希望能对大家有帮助
Gemini CLI 模型路由方案深度分析
你是否好奇,当你在 Gemini CLI 里输入一句「帮我读一下 package.json」时,它到底用的是 Flash 还是 Pro?为什么有时候响应飞快,有时候又明显更「重」?当你选 auto 模式时,背后究竟发生了什么?
这篇文章会带你拆开 Gemini CLI 的模型路由黑盒,看看它是怎么在「快」和「强」之间做取舍的。
一、先搞清楚它在干什么
简单说:模型路由就是「该用哪个模型」的决策系统。你选 auto 时,不会每次都无脑上 Pro,而是会根据当前请求的复杂度,在 Flash 和 Pro 之间做选择——简单任务用 Flash 省成本,复杂任务用 Pro 保质量。
整体设计用的是策略模式 + 责任链:一个 ModelRouterService 当总调度,下面挂一串策略,按优先级依次试,谁先给出结果就用谁。
核心组件长什么样
┌─────────────────────────────────────────────────────────────────┐
│ ModelRouterService │
│ (Config 注入,统一入口,遥测记录) │
└────────────────────────────┬────────────────────────────────────┘
│
┌────────────────────────────▼────────────────────────────────────┐
│ CompositeStrategy │
│ (责任链:按序尝试,首个非 null 即返回) │
└────────────────────────────┬────────────────────────────────────┘
│
┌───────────────────────┼───────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐
│ Fallback │ → │ Override │ → │ Classifier / Numerical │
│ Strategy │ │ Strategy │ │ Classifier Strategy │
└──────────────┘ └──────────────┘ └──────────────┬───────────┘
│
▼
┌──────────────────┐
│ DefaultStrategy │
│ (Terminal) │
└──────────────────┘
路由在哪儿被调用
两处:
- 主 Agent 循环(
client.ts):每次用户发一条消息、开始新 turn 时 - 子 Agent(
local-executor.ts):子 Agent 往上游发消息时
两处都会构造 RoutingContext,调 router.route(context),拿到 RoutingDecision 后,用 decision.model 去真正发请求。
二、CompositeStrategy —— 责任链的骨架
在讲各个子策略之前,先看看 CompositeStrategy 是怎么把它们串起来的。它是整个路由链的「编排者」,理解它的逻辑,后面的策略行为就一目了然了。
2.1 设计思路:非终端 vs 终端策略
CompositeStrategy 把策略分成两类:
- 非终端策略(
RoutingStrategy):可以返回null,表示「这事儿不归我管」 - 终端策略(
TerminalStrategy):必须返回一个决策,不能返回null
最后一个是 DefaultStrategy,作为兜底,保证一定能选出模型。TypeScript 通过这种类型区分,在编译期就保证了「不会出现没人接盘」的情况。
2.2 核心逻辑 —— 顺序尝试 + 异常隔离
// compositeStrategy.ts - route() 核心逻辑
async route(
context: RoutingContext,
config: Config,
baseLlmClient: BaseLlmClient,
): Promise<RoutingDecision> {
const startTime = performance.now();
// 把策略拆成「非终端」和「终端」两组
const nonTerminalStrategies = this.strategies.slice(0, -1) as RoutingStrategy[];
const terminalStrategy = this.strategies[this.strategies.length - 1] as TerminalStrategy;
// 依次尝试非终端策略,允许失败
for (const strategy of nonTerminalStrategies) {
try {
const decision = await strategy.route(context, config, baseLlmClient);
if (decision) {
return this.finalizeDecision(decision, startTime);
}
} catch (error) {
debugLogger.warn(
`[Routing] Strategy '${strategy.name}' failed. Continuing to next strategy. Error:`,
error,
);
}
}
// 都没接盘?交给终端策略
try {
const decision = await terminalStrategy.route(context, config, baseLlmClient);
return this.finalizeDecision(decision, startTime);
} catch (error) {
coreEvents.emitFeedback('error', `[Routing] Critical Error: ...`);
throw error; // 终端策略失败必须抛出,无法兜底
}
}
几个关键点:
- 非终端策略抛异常:只打 warn 日志,不中断,继续试下一个。Classifier 调 API 失败、解析失败,都不会影响整体流程。
- 终端策略抛异常:直接
throw,因为已经没后备了,必须让上层感知。 if (decision):非终端策略返回null时,不会return,循环继续。
2.3 finalizeDecision
子策略的决策会经过 finalizeDecision 做元数据增强:把 source 拼成 agent-router/Classifier 形式,latencyMs 为 0 时用整条链的总耗时回退,便于遥测和排查。
2.4 ModelRouterService 的初始化
策略链的组装在 ModelRouterService 里完成,顺序是写死的:
// modelRouterService.ts - initializeDefaultStrategy
private initializeDefaultStrategy(): TerminalStrategy {
return new CompositeStrategy(
[
new FallbackStrategy(),
new OverrideStrategy(),
new ClassifierStrategy(),
new NumericalClassifierStrategy(),
new DefaultStrategy(), // 必须放最后,作为 TerminalStrategy
],
'agent-router',
);
}
要加新策略,只需实现 RoutingStrategy 或 TerminalStrategy,然后往这个数组里塞,再调整一下顺序即可,扩展成本很低。
三、路由需要什么、产出什么
3.1 RoutingContext —— 路由的「输入」
interface RoutingContext {
history: Content[]; // 对话历史(curated)
request: PartListUnion; // 当前请求内容
signal: AbortSignal; // 取消信号
requestedModel?: string; // 用户/配置请求的模型(如 "auto")
}
历史 + 当前请求 + 信号,基本就是路由能拿到的全部信息了。
3.2 RoutingDecision —— 路由的「输出」
interface RoutingDecision {
model: string; // 最终选中的模型 ID
metadata: {
source: string; // 决策来源(策略名)
latencyMs: number; // 决策耗时
reasoning: string; // 决策理由
error?: string; // 异常信息(如有)
};
}
metadata 对排查问题和做实验很有用,能看出这次决策是谁做的、花了多久、理由是什么。
四、策略链:谁先说话算谁的
策略按优先级从高到低跑,第一个返回非 null 的就算数,后面的直接不执行。这种设计的好处是:逻辑清晰,扩展也方便。
4.1 FallbackStrategy —— 模型挂了怎么办
优先级最高,专门处理「你要的模型不可用」的情况。
流程大致是:
- 解析
requestedModel,查ModelAvailabilityService.snapshot(model) - 如果可用 → 返回 null,交给后面的策略
- 如果不可用 → 从策略链里挑第一个可用的(比如 Pro 挂了就换 Flash)
- 找到且和请求的不一样才返回,否则继续往下
可用性状态来自两类标记:
markTerminal():配额、容量等「彻底不行」的情况markRetryOncePerTurn():本 turn 内只重试一次
把 Fallback 放第一是合理的——可用性是最底线的保障,比「用户指定」和「智能选择」都重要。
4.2 OverrideStrategy:用户说了算
用户显式指定了模型(比如 --model pro),就直接用,不绕弯子。
逻辑很简单:如果 requestedModel 是 auto 系列,就返回 null 让后面的策略处理;否则解析并直接返回,后面的策略都不跑了。
4.3 ClassifierStrategy:二分类版「智能路由」
这是「智能路由」的核心之一:用一个小模型(默认 gemini-2.5-flash-lite)对任务做二分类,输出 flash 或 pro。
前提:getNumericalRoutingEnabled() === false,即没开数值路由时才会用。
大致流程:
- 取最近 20 轮历史,过滤掉 function call/response,只留最近 4 轮
- 调
baseLlmClient.generateJson(),要求输出{ reasoning, model_choice: "flash" | "pro" } - 用
resolveClassifierModel()把flash/pro映射到具体模型 - 解析失败或 API 报错就返回 null,交给 DefaultStrategy
复杂度规则可以概括为:
- PRO:步骤多(4+ 步)、架构/策略设计、歧义大、深度调试
- FLASH:简单、明确、1–3 步能搞定的
用 flash-lite 做分类器是个聪明的选择——既控制成本,又保证分类质量,毕竟分类本身不需要太强的推理能力。
4.4 NumericalClassifierStrategy:打分版「智能路由」
和 Classifier 互斥,当 getNumericalRoutingEnabled() === true 时启用。
区别在于:不是二分类,而是打一个 1–100 的复杂度分,再和阈值比较决定用 flash 还是 pro。
流程:
- 取最近 8 轮历史(不过滤 tool 相关)
- 对请求做 sanitize,减少 prompt 注入风险
- 调用 classifier,输出
{ complexity_reasoning, complexity_score: 1–100 } - 阈值来源:
- 远程:
config.getClassifierThreshold()(实验配置) - 本地 A/B:
getComplexityThreshold(sessionId),用 FNV-1a 哈希,50% 用户阈值 80,50% 用户阈值 50
- 远程:
score >= threshold→ PRO,否则 → FLASH
数值版更细粒度,适合做 A/B 实验和调参,但实现上也更复杂一些。
4.5 DefaultStrategy:兜底
前面所有策略都返回 null 时,用配置的默认模型。保证一定能选出一个模型,不会「路由失败」。
五、路由之后还有一道「可用性校验」
很多人会以为路由决策就是最终结果,其实不是。路由选完模型后,还会再走一遍 applyModelSelection:
// client.ts 638-644
const modelConfigKey: ModelConfigKey = { model: modelToUse };
const { model: finalModel } = applyModelSelection(
this.config,
modelConfigKey,
{ consumeAttempt: false },
);
modelToUse = finalModel;
它会根据 modelToUse 解析策略链,调用 selectFirstAvailable() 选第一个可用模型。如果路由选出的 Pro 刚好不可用,就会自动回退到 Flash。
所以完整链路是:路由选模型 → 可用性再校验 → 最终模型。两层保障,避免「选了但用不了」的尴尬。
六、模型别名和 Classifier 配置
模型别名速查
| 别名 | 默认解析 | Preview 解析 |
|---|---|---|
auto | gemini-2.5-pro | gemini-3-pro-preview |
auto-gemini-2.5 | gemini-2.5-pro | - |
auto-gemini-3 | gemini-3-pro-preview | - |
pro | gemini-2.5-pro | gemini-3-pro-preview |
flash | gemini-2.5-flash | gemini-3-flash-preview |
flash-lite | gemini-2.5-flash-lite | - |
Classifier 用的模型
model: 'classifier' 默认是 gemini-2.5-flash-lite,maxOutputTokens: 1024,thinkingBudget: 512。轻量配置,够分类用就行。
七、实验与远程配置
数值路由和阈值都受实验系统控制:
getNumericalRoutingEnabled():开不开数值分类器(二分类 vs 打分)getClassifierThreshold():远程阈值 0–100,会覆盖本地 A/B 的阈值
这样可以在不发版的情况下做灰度实验,挺实用的。
八、设计上值得留意的点
| 维度 | 做法 |
|---|---|
| 模式 | 策略模式 + 责任链,职责清晰 |
| 扩展 | 新策略实现 RoutingStrategy,往 Composite 里加就行 |
| 容错 | 策略失败返回 null,由后续或 Default 兜底 |
| 可观测 | 每次路由记 ModelRoutingEvent,便于排查和实验 |
| 成本 | Classifier 用 flash-lite,控制 token 和延迟 |
| 安全 | NumericalClassifier 对请求做 sanitize,防 prompt 注入 |
九、数据流一览
用户请求 (requestedModel: "auto")
│
▼
┌───────────────────┐
│ FallbackStrategy │ requestedModel 不可用? → 选可用模型
└─────────┬─────────┘ 否则 → null
│
▼
┌───────────────────┐
│ OverrideStrategy │ requestedModel 非 auto? → 直接返回
└─────────┬─────────┘ 否则 → null
│
▼
┌───────────────────┐
│ Classifier / │ 调用 classifier 模型 → flash / pro
│ NumericalClassifier│
└─────────┬─────────┘ 失败 → null
│
▼
┌───────────────────┐
│ DefaultStrategy │ 使用 config.getModel()
└─────────┬─────────┘
│
▼
RoutingDecision
│
▼
applyModelSelection (可用性校验)
│
▼
最终模型 (finalModel)
十、写在最后
Gemini CLI 的模型路由,本质是在可用性优先的前提下,用分类器在 Flash 和 Pro 之间做成本与能力的权衡。策略链的设计让逻辑清晰、易扩展,实验系统又支持远程调参和 A/B,整体算是一套比较成熟的路由方案。如果你在调 auto 模式的行为,或者想自己加一种路由策略,希望这篇文章能帮上忙。
参考文献
- Gemini CLI 官方文档:geminicli.com/docs/
- Gemini API 文档:ai.google.dev/gemini-api/…
- Chain of Responsibility 设计模式(责任链):refactoring.guru/design-patt…
- Strategy 设计模式(策略模式):refactoring.guru/design-patt…
- FNV-1a 哈希算法(Fowler-Noll-Vo):www.isthe.com/chongo/tech…
- LLM 模型路由综述(LLMRouterBench):arxiv.org/abs/2601.07…
- RouteLLM:基于偏好数据的 LLM 路由学习:arxiv.org/abs/2406.18…
- OWASP Prompt Injection 攻击说明:owasp.org/www-communi…
- OWASP LLM 安全:Prompt Injection 防护:genai.owasp.org/llmrisk2023…
- A/B 测试(维基百科):en.wikipedia.org/wiki/A/B_te…