【免费下载链接】gsd-core
Git. Ship. Done - 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.enabled | boolean | false | 总开关。为 true 时启用动态路由解析器进行档位选择 |
dynamic_routing.tier_models.light | enum | (无) | light 档模型别名,通常为 haiku |
dynamic_routing.tier_models.standard | enum | (无) | standard 档模型别名,通常为 sonnet |
dynamic_routing.tier_models.heavy | enum | (无) | heavy 档模型别名,通常为 opus |
dynamic_routing.escalate_on_failure | boolean | true | 置为 false 时禁用升级,无论尝试次数如何,每次重派生都使用默认档模型 |
dynamic_routing.max_escalations | integer | 1 | 每次 Agent 调用的升级硬上限。超过上限后解析器返回上限档模型;同时约束下方的 provider 阶梯 |
dynamic_routing.provider_escalation | string[] | (无) | 可选:配额/限流时按顺序尝试的备用模型 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 | 适用场景 |
|---|---|---|
light | gsd-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 | 廉价/快速——纯映射、扫描、低风险审计 |
standard | gsd-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 | 默认主力——研究、写作、常规验证 |
heavy | gsd-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 不是独立的解析体系,而是插入到既有解析链的固定位置。完整优先级(高 → 低):
model_overrides[<agent>]——支持完整模型 ID,是逐 Agent 的定点例外,永远最高优先;dynamic_routing.tier_models[<tier>]——当enabled: true时生效;models[<phase_type>]——按阶段类型的粗粒度调优;model_profile——按活跃 profile 的逐 Agent 档位列;- 运行时代默认值。
即: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),调用关系如下:
- 读取项目配置
loadConfig(cwd),对attempt做归一化(非正整数视为 0,即"首次派生"); - 先检查
model_overrides[<agent>]——若存在则直接返回映射后的模型(claude 运行时将完整 ID 映射回 Agent 工具别名); - 若配置了
model_policy且当前活动运行时非 claude,则回退到resolveModelInternal的既有逻辑; - 否则调用
dynamicRoutingModel(config, agentType, attemptN),命中则返回对应档位模型; - 未命中(未启用/无档位表/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 钉死例外)。由于默认关闭,存量项目无需任何迁移即可保持既有解析行为完全不变。
引用与进一步阅读
- 变更说明:.changeset/archived/dynamic-routing.md
- 功能文档:docs/features/dynamic-routing-with-failure-tier-escalation.md
- 配置手册:docs/CONFIGURATION.md(Dynamic Routing 一节)
- 核心实现:src/model-resolver.cts(
resolveModelForTier、dynamicRoutingModel、resolveProviderEscalation) - 目录与档位定义:src/model-catalog.cts、gsd-core/bin/shared/model-catalog.json
- 回归测试:tests/resolve-execution-dynamic-routing.test.cjs
【免费下载链接】gsd-core
Git. Ship. Done - Core
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



