get-shit-done `dynamic_routing` 全解析:基于失败分档(Failure-Tier)升级的按需模型路由与成本控制机制
dynamic_routing 是 get-shit-done(GSD)在 .planning/config.json 中引入的一组动态路由配置(feature [#3024],变更说明见 .changeset/dynamic-routing.md),其设计哲学是"默认用廉价模型起步,仅在编排器检测到软失败(soft failure)时按档位逐级升配"。读完本文你将掌握:dynamic_routing 全部配置字段的含义与默认值、light / standard / heavy 三档 agent 归类、解析器 resolveModelForTier 的底层实现与升级流程,以及它与 model_overrides、models.<phase_type> 叠加后的五层优先级裁决规则,从而把 GSD 多 agent 协同的模型成本精确控制在"该花的才花"。
一、机制速览:为什么需要"失败分档升级"
get-shit-done 在多阶段工作流中会为 planner、debugger、executor、verifier 等大量子代理(agent)逐一挑选模型。如果所有 agent 一律使用最强模型(如 opus),成本随 agent 数量线性膨胀;如果一律使用最弱模型,复杂阶段的推理质量又无法保证。
dynamic_routing 提供第三条路——把它当作一根"成本杠杆":首次派发(attempt = 0)时解析器返回 tier_models[default_tier],即每个 agent 声明档位对应的默认模型;当编排器(orchestrator)检测到软失败(典型场景:verification 结论不明确、plan-check 返回 FLAG)时,以 attempt + 1 重新派发,解析器将档位向上升一级;升级次数受 max_escalations 硬性封顶。而硬失败(异常/崩溃)不参与升级,直接向用户暴露。
该能力在变更说明中被定性为"完全向后兼容":dynamic_routing 默认关闭(enabled: false 或省略整个块),此时解析行为与此前静态解析完全一致。
二、配置指南:dynamic_routing 完整参数解析
在项目的 .planning/config.json 中加入以下块即可启用:
{
"dynamic_routing": {
"enabled": true,
"tier_models": {
"light": "haiku",
"standard": "sonnet",
"heavy": "opus"
},
"escalate_on_failure": true,
"max_escalations": 1
}
}
官方配置总览文档见 docs/CONFIGURATION.md 的"Dynamic Routing with Failure-Tier Escalation"一节。各字段语义如下:
| Key | 类型 | 默认值 | 说明 |
|---|---|---|---|
dynamic_routing.enabled |
boolean | false |
总开关。为 true 时启用动态路由解析器做档位选择;false 或缺失时完全保持原有静态解析,不产生任何行为差异 |
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 时即使 enabled: true 也禁止升级,所有尝试一律停留在默认档 |
dynamic_routing.max_escalations |
integer | 1 |
每次 agent 调用的升级次数硬上限,防止升级失控;超出上限时解析器返回上限档对应模型 |
配置校验由 isValidConfigKey(实现在 get-shit-done/bin/lib/config-schema.cjs)负责。合法键仅限上述字段:dynamic_routing.enabled、dynamic_routing.escalate_on_failure、dynamic_routing.max_escalations 以及 dynamic_routing.tier_models.light / standard / heavy;tier_models 中任何其他档位(如 jumbo、medium)与未知的 dynamic_routing.* 键都会被拒绝,且裸 dynamic_routing(不带字段)不能作为配置写入目标。
说明:
tier_models中填的是档位别名而非全限定模型 ID。全限定 ID(如openai/gpt-5)属于model_overrides的职责范畴,详见下文"五层优先级"。
三、Agent 默认分档:light / standard / heavy
每个 agent 都在模型目录(catalog)中声明自己的默认档位。首次派发时解析器取 tier_models[default_tier] 作为模型。三档划分及对应 agent 如下:
| 档位 | 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 | 深度推理——已在顶端,无法继续升级 |
从源码层面看,档位表的来源分三层:
- 数据层:所有 33 个内置 agent 的逐档配置存放在模型目录 sdk/shared/model-catalog.json,每个 agent 带
routingTier、phaseType、golden/balanced/budget等元数据; - 常量层:
VALID_AGENT_TIERS由catalog.adaptiveTierMap的键集合构造,AGENT_DEFAULT_TIERS则由 catalog 中每个 agent 的routingTier映射而来,见 get-shit-done/bin/lib/model-catalog.cjs#L50-L69; - 再导出层:上述常量连同
nextTier经 get-shit-done/bin/lib/model-profiles.cjs 统一再导出,供解析器与测试引用。
nextTier 的档位爬升规则直接写死在 get-shit-done/bin/lib/model-catalog.cjs#L93-L98:
function nextTier(currentTier) {
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)];
}
即 light → standard → heavy → heavy:heavy 已到顶,继续调用 nextTier 仍停留 heavy;对非法输入(如 jumbo、null)返回 null,驱动解析器走安全回退。
四、解析器契约与升级流程:resolveModelForTier(cwd, agent, attempt)
动态路由的 JS 层基础设施是新增的解析器 resolveModelForTier,导出自 get-shit-done/bin/lib/core.cjs#L1881。函数签名与行为约定(见 core.cjs#L1356-L1379):
resolveModelForTier(cwd, agentType, attempt = 0) → modelAlias | fullId
cwd:项目目录,用于loadConfig读取.planning/config.json;agentType:agent 名(如gsd-verifier);attempt:0 表示首次派发,≥1 表示升级重试;内部会被max_escalations截断,超出部分返回封顶档模型,便于编排器记录 "max escalations reached" 而不再燃烧预算。
4.1 完整解析流程(源码级)
核心实现位于 core.cjs#L1381-L1442,关键分支如下:
- 用户级覆盖优先:
config.model_overrides?.[agentType]命中则直接返回(全限定 ID 直接绕过整个档位机制); - 关闭/缺省回退:
dynamic_routing缺失、非对象或enabled !== true时,等价调用旧解析器resolveModelInternal(cwd, agentType)——这正是向后兼容性的实现点,编排器可以无条件调用新解析器而无需担心破坏旧行为; - 缺档表回退:
tier_models缺失时无法路由,回退resolveModelInternal; - 未映射 agent 回退:
AGENT_DEFAULT_TIERS[agentType]不存在或不在VALID_AGENT_TIERS内时回退,避免静默选错模型; - 升级封顶:
max_escalations仅在"非负整数"时生效,否则默认1;escalate_on_failure !== false判定升级是否启用(见 4.2); - 沿档位链步进:从
defaultTier出发循环调用nextTiereffectiveAttempt次,next为空或等于当前档(已到顶)即 break; - 末位兜底:
tierModels[tier]为空串或非字符串(配置缺失槽位)时回退resolveModelInternal,绝不返回空模型 ID。
4.2 升级流程与 kill-switch
官方配置文档给出的编排器交互流程为:
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
其中 escalate_on_failure: false 是升级的 kill-switch。这一点源于一次代码评审中的 Major 级发现(CR Major #3031,见 tests/feat-3024-dynamic-routing.test.cjs#L253-L277):若某个编排器盲目地在每次重试时递增 attempt 计数器,即使用户通过 escalate_on_failure: false 明确退出升级,也会被静默升级。修复后,当开关为 false 时,无论 attempt 计数多高,每一次解析都被拉回默认档(light → haiku)。
4.3 兜底回退一览
综合源码可得到以下安全回退矩阵(均收敛到旧的静态解析 resolveModelInternal):
| 触发条件 | 后果 |
|---|---|
整块缺失或 enabled: false |
完全等价旧解析器(向后兼容) |
tier_models 缺失 |
无法路由,回退 |
| agent 未声明默认档 / 档位非法 | 回退,不静默选错模型 |
tier_models[resolvedTier] 槽位为空 |
回退,不发出空模型 ID |
attempt 超过 max_escalations |
返回封顶档模型(不无休止升级) |
五、五层优先级:动态路由与其他模型配置的合成
dynamic_routing 并非孤立存在。它加入后,agent 最终模型的裁决链自上而下为:
model_overrides[<agent>]—— 每 agent 精确覆盖,接受全限定 ID,优先级最高,且"override 永远赢":即使在已升级的 attempt 下,命中覆盖的 agent 依旧返回覆盖模型;dynamic_routing.tier_models[<tier>]—— 启用动态路由时按档位选择(本次新增,升级感知);models[<phase_type>]—— 粗粒度的阶段级档位(#3023),六个阶段槽位planning / discuss / research / execution / verification / completion;model_profile—— 当前 profile(quality / balanced / budget / adaptive / inherit)下每个 agent 的档位列;- 运行时默认 —— 兜底。
官方文档将这五层概括为"自顶向下叠加":model_profile 是基底档位,models.<phase_type> 在阶段层面覆盖,dynamic_routing(启用时)在软失败时逐次升级,model_overrides 在最上层雕刻每 agent 例外,最后才是运行时默认。例如当 models.research: "opus" 而某 research 类 agent(如 gsd-codebase-mapper)默认档为 light 且动态路由启用时,tier_models.light(haiku)胜出——动态路由的档位优先级高于阶段级 models。
详细的优先级定义同时写在两处权威位置:实现注释 core.cjs#L1365-L1370,以及配置文档 docs/CONFIGURATION.md#L943-L951。
六、验收标准与测试佐证
feature #3024 的行为契约被类型化地固化在 tests/feat-3024-dynamic-routing.test.cjs(对 resolveModelForTier / isValidConfigKey 的返回值做结构断言,而非 grep stdout)。各验收维度包括:
- schema 完备性:
AGENT_DEFAULT_TIERS非空,VALID_AGENT_TIERS恰好为{light, standard, heavy},MODEL_PROFILES中每个 agent 都有合法默认档(L74-L96); nextTier:light→standard、standard→heavy、heavy→heavy,非法输入返回null(L100-L116);- 关闭模式是 no-op:无块或
enabled: false时,无论 attempt=0/1 都等于resolveModelInternal(L120-L149); - 启用模式的档位选择:attempt=0 返回
tier_models[default_tier](如 gsd-codebase-mapper 为 light →haiku,gsd-planner 为 heavy →opus)(L158-L176); - 升级行为:light agent 在 attempt=1 升到 standard 档模型,standard agent 升到 heavy 档模型(L178-L198);
- 封顶行为:
max_escalations: 1时 attempt=2 与 attempt=5 都停在 attempt=1 的档位;heavy agent 永不越顶;省略max_escalations时默认1(L200-L251); - kill-switch:
escalate_on_failure: false时 attempt=1/5 一律返回默认档模型(L255-L277); - 优先级裁决:
model_overrides胜过动态路由(attempt=0/1 均返回覆盖 ID);动态路由胜过models.<phase_type>(L298-L332); - schema 校验:合法键全部放行,非法档位与未知键被拒,裸
dynamic_routing不可作为配置写入目标(L336-L365)。
七、实战选型与成本控制建议
官方文档把三档模型与四类需求做了对应:
| 你的诉求 | 使用 |
|---|---|
| 全 agent 统一档位策略 | model_profile |
| 粗粒度阶段级调优("planning 用 opus,其余用 sonnet") | models.<phase_type> |
| 每 agent 精确控制(全限定 ID) | model_overrides |
| 默认廉价、失败才升级 | dynamic_routing |
综合前文可以提炼出几条可落地的组合策略:
- 纯成本杠杆场景:仅配置
dynamic_routing块,保持model_overrides与models为空。此时 33 个 agent 按档位归队,只有真正卡住的重试任务才会短暂升配,且max_escalations: 1(默认)确保单次任务至多升一档,成本可预期; - 叠加例外场景:对少数必须恒定使用特定模型的 agent(例如要求全限定 ID 的非 Claude 运行时),用
model_overrides打补丁——它永远优先于档位机制,不受升级影响; - 验证回退场景:把
dynamic_routing整块删除或置enabled: false,即可在任意时刻回退到 feature #3024 引入之前的静态解析行为,验证差异、做 A/B 成本对比时尤其有用; - 彻底关停升级:保留
enabled: true但设escalate_on_failure: false,等于"仅按默认档路由、永不动用备用预算",适合预算极敏感的无人值守批次。
需要留意的是:所有档位别名最终仍会通过 runtime-aware 解析(runtime 已设置时)映射为对应运行时的原生模型 ID,因此在 Codex、OpenCode、Gemini CLI 等运行时下,动态路由的档位语义(opus/sonnet/haiku 别名按 runtime 映射)保持一致,无需为每个运行时重写 tier_models。变更说明中亦强调该块"与 model_overrides(更高优先级)和 models.<phase_type>(更低优先级)组合使用,可获得完整的成本控制灵活性"。
参考资源:功能变更说明 .changeset/dynamic-routing.md;配置总览 docs/CONFIGURATION.md;解析器实现 get-shit-done/bin/lib/core.cjs#L1356-L1442;档位常量与 nextTier 定义 get-shit-done/bin/lib/model-catalog.cjs#L50-L98;再导出层 get-shit-done/bin/lib/model-profiles.cjs;feature #3024 行为测试 tests/feat-3024-dynamic-routing.test.cjs。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python08
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00