gsd-core 动态模型路由(Dynamic Routing)与失败分层升级实战指南

【免费下载链接】gsd-core

Git. Ship. Done - Core

项目地址: https://gitcode.com/gh_mirrors/ge/gsd-core
点击查看 免费下载

导读

本文围绕 gsd-core 的 dynamic_routing 配置块(位于项目根目录 .planning/config.json),讲解如何实现"默认使用廉价模型、仅在编排器检测到软失败时自动升级到更强模型"的成本控制策略。读完本文,你将掌握 dynamic_routing 的完整配置语法、三级(light / standard / heavy)分层规则、升级链路与封顶机制、与 model_overrides / models.<phase_type> 的优先级组合方式,以及从源码层理解新解析器 resolveModelForTier(cwd, agent, attempt) 的工作原理。

功能背景:为什么需要"按失败升级"的模型路由

gsd-core 是一个多 Agent 编排系统(见 docs/explanation/multi-agent-orchestration.md),在规划、研究、执行、验证等各阶段会多次派生出不同职责的 Agent(gsd-executor、gsd-codebase-mapper、gsd-planner 等)。传统做法是为所有 Agent 固定一个模型档位,但这存在明显浪费:

  • 大量低风险任务(代码库扫描、文档分类、模式映射)只需廉价模型即可完成;
  • 只有验证结论存疑、计划检查返回 FLAG 等"软失败"场景才需要更强模型重试。

dynamic_routing 正是为此设计:先按廉价档位起跑,编排器发现软失败后逐级升级,从而在保持结果质量的同时压低模型成本。该特性源自 issue #3024,其变更说明记录于 .changeset/archived/dynamic-routing.md,完整功能文档见 docs/features/dynamic-routing-with-failure-tier-escalation.md。

配置总览:.planning/config.json 中的 dynamic_routing 块

dynamic_routing 配置块必须写在项目根目录的 .planning/config.json 中,完整示例(来自 docs/CONFIGURATION.md):

{
  "dynamic_routing": {
    "enabled": true,
    "tier_models": {
      "light":    "haiku",
      "standard": "sonnet",
      "heavy":    "opus"
    },
    "escalate_on_failure": true,
    "max_escalations": 1
  }
}

该块的核心行为可概括为:

  • enabled: false(默认值)或整块缺失时,特性完全关闭,所有 Agent 沿用原有解析链路,行为零变化(向后完全兼容);
  • enabled: true 时,解析器为首次派生(first spawn)选取 tier_models[default_tier],当编排器检测到软失败后,每次重试向上一档升级,升级次数由 max_escalations 封顶。

配置项说明:类型、默认值与取值范围

Key类型默认值说明
dynamic_routing.enabledbooleanfalse总开关。为 true 时启用动态路由解析器进行档位选择
dynamic_routing.tier_models.lightenum(无)light 档模型别名,通常为 haiku
dynamic_routing.tier_models.standardenum(无)standard 档模型别名,通常为 sonnet
dynamic_routing.tier_models.heavyenum(无)heavy 档模型别名,通常为 opus
dynamic_routing.escalate_on_failurebooleantrue置为 false 时禁用升级,无论尝试次数如何,每次重派生都使用默认档模型
dynamic_routing.max_escalationsinteger1每次 Agent 调用的升级硬上限。超过上限后解析器返回上限档模型;同时约束下方的 provider 阶梯
dynamic_routing.provider_escalationstring[](无)可选:配额/限流时按顺序尝试的备用模型 ID 列表(v1.43 新增)

配置默认值同时固化在 src/configuration.cts(CONFIG_DEFAULTS 中 "dynamic_routing": null),并在 docs/CONFIGURATION.md 的参数表中逐一登记,方便查阅。

三层档位与 Agent 默认档:light / standard / heavy

每个 Agent 在模型目录中声明一个默认路由档位(routingTier)。首次派生时解析器取 tier_models[default_tier]。当前仓库中的 Agent 默认档位分布(依据 gsd-core/bin/shared/model-catalog.json 与 docs/CONFIGURATION.md 的整理):

档位代表 Agent适用场景
lightgsd-codebase-mapper、gsd-doc-classifier、gsd-doc-verifier、gsd-integration-checker、gsd-intel-updater、gsd-nyquist-auditor、gsd-pattern-mapper、gsd-plan-checker、gsd-research-synthesizer、gsd-ui-auditor、gsd-ui-checker廉价/快速——纯映射、扫描、低风险审计
standardgsd-advisor-researcher、gsd-ai-researcher、gsd-code-fixer、gsd-code-reviewer、gsd-doc-synthesizer、gsd-doc-writer、gsd-domain-researcher、gsd-eval-auditor、gsd-executor、gsd-phase-researcher、gsd-project-researcher、gsd-ui-researcher、gsd-verifier默认主力——研究、写作、常规验证
heavygsd-assumptions-analyzer、gsd-debug-session-manager、gsd-debugger、gsd-eval-planner、gsd-framework-selector、gsd-planner、gsd-roadmapper、gsd-security-auditor、gsd-user-profiler深度推理——已处于顶级,无法再升级

档位与模型别名的映射在 gsd-core/bin/shared/model-catalog.json 中由 adaptiveTierMap 声明:heavy → opus、standard → sonnet、light → haiku。运行时代默认值(如 claude 下的 claude-opus-4-8 等完整模型 ID)由 runtimeTierDefaults 提供。

在源码层,这些映射被加载为可用的数据结构:src/model-catalog.cts 中 VALID_AGENT_TIERS(L88)、AGENT_DEFAULT_TIERS(L118)、MODEL_PROFILES(L105)全部由该 JSON 目录推导生成,保证目录与解析器档位门槛永不失配。

升级流程:从首次派生到软失败重试

升级链路可抽象为以下流程(来自 docs/CONFIGURATION.md 的 Escalation flow):

1. Orchestrator spawns agent → resolver returns tier_models[default_tier]
2. Soft failure?
   ├─ no → ✓ done (cheap path)
   └─ yes → orchestrator re-spawns at attempt+1
            → resolver returns tier_models[next_tier_up]
            → cap at max_escalations
3. Hard failure (exception/crash) → bypass escalation, surface immediately

关键规则说明:

  • **软失败(soft failure)**包括验证结论存疑(verification inconclusive)、计划检查返回 FLAG 等编排器可检测到的非致命失败;硬失败(异常/崩溃)则绕过升级链路,立即上报;
  • 升级步长固定为一档:light → standard → heavy → heavy(heavy 已到顶,不再上移),该顺序由 src/model-catalog.cts 的 nextTier()(L223-L228)实现:
    export function nextTier(currentTier: string): string | null {
      const order = ['light', 'standard', 'heavy'];
      const idx = order.indexOf(String(currentTier));
      if (idx === -1) return null;
      return order[Math.min(idx + 1, order.length - 1)];
    }
    
  • escalate_on_failure: false 是"杀开关":即使 enabled: true,软失败也不会推进档位,每次重派生仍使用 tier_models[default_tier];
  • max_escalations 封顶整个升级链,防止成本失控。在 src/model-resolver.cts 的 dynamicRoutingModel()(L772-L804)中,attempt 被 Math.min(attempt, maxEscalations) 截断后再逐档上移,因此超过上限的尝试会停留在上限档模型。

解析优先级:dynamic_routing 在模型解析链中的位置

dynamic_routing 不是独立的解析体系,而是插入到既有解析链的固定位置。完整优先级(高 → 低):

  1. model_overrides[<agent>]——支持完整模型 ID,是逐 Agent 的定点例外,永远最高优先;
  2. dynamic_routing.tier_models[<tier>]——当 enabled: true 时生效;
  3. models[<phase_type>]——按阶段类型的粗粒度调优;
  4. model_profile——按活跃 profile 的逐 Agent 档位列;
  5. 运行时代默认值。

即:model_overrides 始终胜出;dynamic_routing.tier_models[<tier>] 高于 models.<phase_type> 与 model_profile。该组合关系在 docs/features/dynamic-routing-with-failure-tier-escalation.md 中被明确文档化,并在 src/model-resolver.cts 的 resolveModelInternal() 中以"第 4.75 步"的形式落地(L673-L691):它位于 model_overrides(第 1 步)、model_policy 预设(第 2.5 步)、运行时档位表(第 3 步)、resolve_model_ids:"omit" 门(第 4 步)、claude 档位覆盖(第 4.5 步)之后,而位于 profile 查询(第 5 步)之前。

这一"精确插位"是刻意的:dynamicRoutingModel() 返回的档位模型必须仍然尊重所有更高优先级层(例如 resolve_model_ids:"omit" 门在非 claude 运行时本应返回空串),避免动态路由绕过既有护栏。

核心解析器:resolveModelForTier(cwd, agent, attempt)

dynamic_routing 为编排器集成新增的解析器是 resolveModelForTier(cwd, agent, attempt)。其实现位于 src/model-resolver.cts(L809-L835),调用关系如下:

  1. 读取项目配置 loadConfig(cwd),对 attempt 做归一化(非正整数视为 0,即"首次派生");
  2. 先检查 model_overrides[<agent>]——若存在则直接返回映射后的模型(claude 运行时将完整 ID 映射回 Agent 工具别名);
  3. 若配置了 model_policy 且当前活动运行时非 claude,则回退到 resolveModelInternal 的既有逻辑;
  4. 否则调用 dynamicRoutingModel(config, agentType, attemptN),命中则返回对应档位模型;
  5. 未命中(未启用/无档位表/Agent 无默认档)时回退到 resolveModelInternal,行为与关闭动态路由时完全一致。

值得强调的是,attempt = 0 表示"尚未升级",对应文档化的首次派生场景,而不是"跳过动态路由"。resolveModelForTier 是编排器集成的唯一调用点,保证了首次派生值与升级值来自同一实现,不会出现"生成式修复发散"(generative fix divergence)。

与编排器的集成:resolve-execution 命令与测试验证

编排器侧的实际调用体现在 gsd-tools resolve-execution 命令中。src/commands.cts 的 cmdResolveExecution 通过 resolveModelForTier(cwd, agentType, opts.attempt) 解析模型,从而使 --attempt 参数不仅影响推理努力(effort),也能驱动模型档位升级。这一行为由 tests/resolve-execution-dynamic-routing.test.cjs 提供回归覆盖:

  • gsd-executor(默认档 standard):attempt 0 → sonnet、attempt 1 → opus(一次升级到 heavy)、attempt 2/3 → opus(默认 max_escalations: 1 封顶);
  • gsd-codebase-mapper(默认档 light,升级链更长):attempt 0 → haiku、attempt 1 → sonnet、attempt 2 → opus(恰好达到 max_escalations: 2)、attempt 3 → opus(不越过上限);
  • 未配置 dynamic_routing 时,attempt 对模型解析无影响,验证了向后兼容性;
  • dynamic_routing 启用但未传 --attempt 时,模型仍走经典 profile 路径(sonnet),与 effort 的语义对称。

进阶:provider 配额升级阶梯(v1.43)

档位阶梯只在同一提供商内部升级;当提供商本身被限流(quota/rate limit)时,更高档位仍被限流。为此 v1.43 引入 provider_escalation:一个独立的、可选的备用模型 ID 列表(见 docs/CONFIGURATION.md):

{
  "dynamic_routing": {
    "enabled": true,
    "tier_models": { "light": "haiku", "standard": "sonnet", "heavy": "opus" },
    "provider_escalation": ["gpt-5", "nvidia/llama-3.3"],
    "max_escalations": 2
  }
}
  • 当执行器失败且 gsd-tools agent classify-failure 将错误分类为 quota-exceeded 时,execute-phase 从该列表重新解析模型,记录切换日志(如 sonnet → gpt-5),并遵循提供商返回的 Retry-After;
  • 阶梯上限为 min(max_escalations, provider_escalation.length);耗尽后 GSD 会报告所有尝试过的模型,并回退到人工恢复提示,而不是静默重试最后一个;
  • 该阶梯是可选的:不配置时配额失败保持原有的等待重置提示;仅针对配额类失败,其他失败类别(classify-handoff-bug、unknown-failure)不咨询此阶梯;escalate_on_failure: false 同样禁用此阶梯;
  • 空白与非字符串条目会被丢弃,其余顺序保持不变(src/model-resolver.cts 的 sanitizeProviderEscalation(),L859-L862)。

选型建议:四种模型策略如何抉择

你的诉求使用
所有 Agent 统一档位策略model_profile
按阶段类型的粗粒度调优models.<phase_type>
逐 Agent 精确控制(支持完整 ID)model_overrides
默认廉价、仅失败时升级dynamic_routing

dynamic_routing 在结构上是一个成本杠杆:只有当难题真正需要强模型时才付出 opus 级成本。它可与 model_overrides 组合使用(override 永远胜出,适合为个别 Agent 钉死例外)。由于默认关闭,存量项目无需任何迁移即可保持既有解析行为完全不变。

引用与进一步阅读

【免费下载链接】gsd-core

Git. Ship. Done - Core

项目地址: https://gitcode.com/gh_mirrors/ge/gsd-core
点击查看 免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值