【读Gemini CLI源码,品Agent架构设计】系列文章(二) —— Gemini CLI 模型路由方案深度分析

0 阅读8分钟

「写给读者的话」本系列文章记录了笔者在学习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 时
  • 子 Agentlocal-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;  // 终端策略失败必须抛出,无法兜底
  }
}

几个关键点:

  1. 非终端策略抛异常:只打 warn 日志,不中断,继续试下一个。Classifier 调 API 失败、解析失败,都不会影响整体流程。
  2. 终端策略抛异常:直接 throw,因为已经没后备了,必须让上层感知。
  3. 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',
  );
}

要加新策略,只需实现 RoutingStrategyTerminalStrategy,然后往这个数组里塞,再调整一下顺序即可,扩展成本很低。


三、路由需要什么、产出什么

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 —— 模型挂了怎么办

优先级最高,专门处理「你要的模型不可用」的情况。

流程大致是:

  1. 解析 requestedModel,查 ModelAvailabilityService.snapshot(model)
  2. 如果可用 → 返回 null,交给后面的策略
  3. 如果不可用 → 从策略链里挑第一个可用的(比如 Pro 挂了就换 Flash)
  4. 找到且和请求的不一样才返回,否则继续往下

可用性状态来自两类标记:

  • markTerminal():配额、容量等「彻底不行」的情况
  • markRetryOncePerTurn():本 turn 内只重试一次

把 Fallback 放第一是合理的——可用性是最底线的保障,比「用户指定」和「智能选择」都重要。

4.2 OverrideStrategy:用户说了算

用户显式指定了模型(比如 --model pro),就直接用,不绕弯子。

逻辑很简单:如果 requestedModelauto 系列,就返回 null 让后面的策略处理;否则解析并直接返回,后面的策略都不跑了。

4.3 ClassifierStrategy:二分类版「智能路由」

这是「智能路由」的核心之一:用一个小模型(默认 gemini-2.5-flash-lite)对任务做二分类,输出 flashpro

前提getNumericalRoutingEnabled() === false,即没开数值路由时才会用。

大致流程:

  1. 取最近 20 轮历史,过滤掉 function call/response,只留最近 4 轮
  2. baseLlmClient.generateJson(),要求输出 { reasoning, model_choice: "flash" | "pro" }
  3. resolveClassifierModel()flash/pro 映射到具体模型
  4. 解析失败或 API 报错就返回 null,交给 DefaultStrategy

复杂度规则可以概括为:

  • PRO:步骤多(4+ 步)、架构/策略设计、歧义大、深度调试
  • FLASH:简单、明确、1–3 步能搞定的

用 flash-lite 做分类器是个聪明的选择——既控制成本,又保证分类质量,毕竟分类本身不需要太强的推理能力。

4.4 NumericalClassifierStrategy:打分版「智能路由」

和 Classifier 互斥,当 getNumericalRoutingEnabled() === true 时启用。

区别在于:不是二分类,而是打一个 1–100 的复杂度分,再和阈值比较决定用 flash 还是 pro。

流程:

  1. 取最近 8 轮历史(不过滤 tool 相关)
  2. 对请求做 sanitize,减少 prompt 注入风险
  3. 调用 classifier,输出 { complexity_reasoning, complexity_score: 1–100 }
  4. 阈值来源:
    • 远程:config.getClassifierThreshold()(实验配置)
    • 本地 A/B:getComplexityThreshold(sessionId),用 FNV-1a 哈希,50% 用户阈值 80,50% 用户阈值 50
  5. 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 解析
autogemini-2.5-progemini-3-pro-preview
auto-gemini-2.5gemini-2.5-pro-
auto-gemini-3gemini-3-pro-preview-
progemini-2.5-progemini-3-pro-preview
flashgemini-2.5-flashgemini-3-flash-preview
flash-litegemini-2.5-flash-lite-

Classifier 用的模型

model: 'classifier' 默认是 gemini-2.5-flash-litemaxOutputTokens: 1024thinkingBudget: 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 模式的行为,或者想自己加一种路由策略,希望这篇文章能帮上忙。


参考文献

  1. Gemini CLI 官方文档:geminicli.com/docs/
  2. Gemini API 文档:ai.google.dev/gemini-api/…
  3. Chain of Responsibility 设计模式(责任链):refactoring.guru/design-patt…
  4. Strategy 设计模式(策略模式):refactoring.guru/design-patt…
  5. FNV-1a 哈希算法(Fowler-Noll-Vo):www.isthe.com/chongo/tech…
  6. LLM 模型路由综述(LLMRouterBench):arxiv.org/abs/2601.07…
  7. RouteLLM:基于偏好数据的 LLM 路由学习:arxiv.org/abs/2406.18…
  8. OWASP Prompt Injection 攻击说明:owasp.org/www-communi…
  9. OWASP LLM 安全:Prompt Injection 防护:genai.owasp.org/llmrisk2023…
  10. A/B 测试(维基百科):en.wikipedia.org/wiki/A/B_te…