应用部分 9. 模型路由与令牌预算
状态: 推荐。 将廉价模型用于常规任务、将昂贵模型用于关键审查——这是一种稳健的实践。具体的阈值、故障转移(fallback)切换公式以及作为独立服务的预算守护者——属于前沿领域:Qwen Code 自身不管理预算,其实现取决于基础设施。
对于教学性的练习,只需模拟 local-coder 在 examples/budget-keeper/ 中的失效,并验证并非所有任务都涌向昂贵的层级。独立的预算守护者和与提供商的集成属于完整生产轨道。
在教学版的 AgentClinic 中,我们在第一卷第 4 部分中选择了一个模型,并保持流程与模型无关(第 15 部分)。在生产环境中,一个模型是不够的。昂贵的模型不应自发地吞噬所有事故队列。廉价的模型不应在边缘案例上无声地退化。这里增加了一个在教学项目中不存在的维度:按管道各阶段管理模型组合。将路由方便地放入用户命令或钩子中——采用第 14 部分. 通过 Qwen Code 技能构建自定义流程中的技巧。
阅读前
- 来自第一卷的支撑:第 15 部分要求代理的可替换性,第 14 部分展示了项目技能和钩子。
- 本地教学案例:
autoscale_200pct,因为廉价层级的失效会给出可观察的预算模拟。 - 适用于
capstone/的痕迹:high_memory_usage的一个风险:当local-coder失效时会发生什么,frontier-reviewer允许多少任务,哪个token_health阻止切换。
- 首次阅读的主要术语:层级(tier)和
token_health。预算守护者(budget keeper)、failover_to_frontier、manual_queue_after_120s——作为参考。 - 需要推迟的内容:与提供商的集成、独立的预算守护者服务和定期演练。
目标
本章的目标是将每日的令牌预算(示例中为 10M)从静态限制转变为 SDD 管道可管理的路由表。这就是层级预算(tier-budgeting):按工作阶段在模型层级之间分配令牌。廉价模型(local-coder)处理常规任务。昂贵模型(frontier-reviewer)仅在关键审查和有争议的决策中启用。
10M 这一数字的选择是为了在每个阶段平均成本约 50K 令牌时覆盖每天约 200 个事故的流量。对于更大的流量,请按比例扩大预算;对于较小的流量,请按比例缩小,同时保持各阶段之间的比例。9M / 1M 的层级划分反映了一个观察:在平稳模式下,有争议的审查消耗了约 10% 的总预算。如果您的项目更频繁地处理复杂任务,请将上层层级的比例增加到 15–20%。
阅读本节后,您将能够构建事故管理各阶段的令牌分配,为每个层级设置 SLA 阈值,验证廉价模型宕机时系统的行为,并证明节省不会破坏 MTTR(平均恢复时间)、升级质量和事后分析的稳健性。local-coder 和 frontier-reviewer 是您基础设施中的角色,而不是模型名称:在同一个项目中,它们可能来自同一提供商的的不同模型;在另一个项目中,它们可能是本地模型和云模型。
基础级别:按任务选择模型的表格
在按阶段、风险和队列进行分层(本章其余部分所描述的内容)之前,最好先确定模型选择的初始启发式方法。它操作的不是一个特定提供商的模型名称,而是按能力和成本划分的类别:轻量、中量和重量。不同项目中这些类别对应不同的模型——重要的不是品牌,而是"贵/聪明"的比率。
| 模型类别 | 何时使用 |
|---|---|
| 轻量 | 探索和文件搜索、简单的单文件编辑、生成文档 |
| 中量 | 多文件实现、pull 请求审查——约 90% 编码任务的默认模型 |
| 重量 | 复杂架构、安全分析、复杂 bug 调试 |
将中量类别作为默认值:它涵盖大多数编码任务。轻量用于廉价的常规任务,其中的错误易于检测和回滚。重量不是按习惯启用,而是按升级规则启用:
- 中量类别的第一次尝试失败;
- 任务涉及五个或更多文件;
- 正在做出架构决策;
- 代码对安全至关重要。
该表格是本章后面展开的相同逻辑的基础级别。关系是直接的:local-coder 对应于流程常规工作中的轻量/中量类别,frontier-reviewer 对应于有争议和高风险决策中的重量类别。区别在于,基础表格根据单个任务的类型选择模型,而分层(tier-budgeting)增加了基础启发式中不存在的三个维度:决策的风险和可恢复性、队列压力以及具有自己令牌预算的 SDD 管道阶段。也就是说,发展路径是这样的:"按任务选择的基础级别 → 按风险、队列和阶段分层"。向重量类别升级的规则并未消失——它成为从 local-coder 升级到 frontier-reviewer 的条件之一。
最小教学场景
教学案例
来自 book/part-12-mvp.md 的 appointments-api MVP 阶段的生产事故 autoscale_200pct。早晨,本地层级不可用 45 分钟(11:00–11:45),20 个事故涌入队列,手动超时为 120 秒。教学运行的目标是确保故障转移仅将高风险而非整个队列传送到上层层级,并保持 token_health_min 高于安全阈值。
准备
book2/examples/budget-keeper/specs/budget_network.yaml— 10M 令牌计划描述。book2/examples/budget-keeper/specs/budget_network_5m.yaml— 5M 令牌的现成校准变体,具有相同的比例。book2/examples/budget-keeper/scenarios/fail_local_45m.json和fail_local_15m.json— 两种失效场景。book2/examples/budget-keeper/outputs/budget_plan.example.json、outputs/fail_result.example.json— 用于比较的参考。book2/examples/budget-keeper/scripts/compile.py、simulate.py、inspect.py。
步骤
cd book2/examples/budget-keeper。预期:您位于示例目录中,没有额外的依赖项。
python3 scripts/compile.py --budget-spec specs/budget_network.yaml --out out/budget_plan.json。*预期:在out/budget_plan.json中,字段daily_budget_tokens: 10000000,local 层级之和等于 9,000,000,frontier 等于 1,000,000(90/10)。*- 通过
diff将out/budget_plan.json与outputs/budget_plan.example.json进行比较。预期:没有差异,或仅在注释中存在偏差。 python3 scripts/simulate.py --plan out/budget_plan.json --scenario scenarios/fail_local_45m.json --out out/fail_result.json。*预期:failover_to_frontier == 5,degraded_queue == 15,token_health_min >= 0.5。*python3 scripts/inspect.py --result out/fail_result.json --query "failover_to_frontier==5 && degraded_queue==15 && manual_queue_after_120s==15 && token_health_min>=0.5"。预期:返回码为 0,所有四个条件同时满足。
不佳: 一次检查一个指标——5 个任务转到 frontier,其余"看起来还可以",token_health 被遗忘。 良好: 一次 inspect 运行具有 && 的四个条件——任何一个指标的失败都会破坏运行。
- 短暂失效。
python3 scripts/simulate.py --plan out/budget_plan.json --scenario scenarios/fail_local_15m.json --out out/fail_15m_result.json && python3 scripts/inspect.py --result out/fail_15m_result.json --query "token_health_min>=0.7"。*预期:返回码为 0,token_health_min >= 0.7(短暂失效对 frontier 的消耗不那么激进)。* - 将此次运行记录为简短的预算结论:
local-coder不可用,上层层级仅接收 5 个任务,其余进入降级/手动,token_health_min保持高于阈值。*预期:在下次token_health回归时,比较不是"绿色对比旧基线",而是针对两次模拟。*
如果您安装了 Qwen Code 并且需要审查说明,请执行单独的可选步骤:
qwen -p "Прочитай @out/fail_result.json и @out/fail_15m_result.json. Объясни, почему 45-минутный отказ снижает token_health сильнее, чем 15-минутный. Файлы не меняй." --approval-mode plan
此类输出作为评论很有用,但不能替代 inspect.py,也不被视为可运行的事实。
验收事实
步骤 5 中的四个条件同时满足。token_health_min 在 45 分钟失效时不低于 0.5,在 15 分钟失效时不低于 0.7。没有两次模拟,该场景被视为不完整:单个数据点不显示预算对失效持续时间的敏感性。
它如何进入 capstone/
将一个风险和一个限制器(而不是整个预算表)转移到 capstone/budget-note.md:当 local-coder 失效时会发生什么,多少任务将进入 frontier-reviewer,哪个 token_health 阈值阻止进一步切换。如果主要的评分案例是 high_memory_usage,请将此运行记录为同一管线的预算风险:不是整个 autoscale_200pct,而是"昂贵层级在廉价层级失效时不接受整个队列"的原则。完整的 budget_plan.json 仅完整轨道需要。
最小片段:
risk: "local-coder 不可用 45m"
effect: "5 个任务转到 frontier-reviewer,15 个保持降级/手动"
simulated_floor: "token_health_min == 0.5(45m 时的下滑)"
alert_threshold: "token_health_min < 0.60(来自反古德哈特表的守卫)"
decision: "不将整个队列转移到昂贵层级"
不应混淆两个不同的阈值。0.5 是模拟中观察到的下限;0.60 是低于该值守卫会阻止生产中自动切换的线。教学场景显示,45 分钟的失效会突破守卫,因此需要手动决策。
可审查的痕迹
out/ 目录是模拟的本地结果,不应进入存储库。对于教学性的练习,capstone/budget-note.md 中包含风险、效果、守卫阈值和决策的一行就足够了。
在您自己的生产存储库中,您还可以存储一份简短的演练运行报告:45m 和 15m 场景的链接、token_health_min 不变量以及不将整个队列转移到昂贵层级的决策。这样的报告仅在审查者或 CI 读取时才有意义;提交本身并不是 SDD 事实。
关键思想
模型路由从将事故分为阶段开始:triage(初步分类)、分类、诊断、计划、补救、事后分析。对于每个阶段,记录三个参数:哪个模型为其服务、令牌的预期成本是多少以及在何种风险下升级到上层层级。
分诊和分类是密集的、模板化的流,对延迟敏感。因此 local-coder 将其作为常规工作的主要消费者:快速规范化警报、对类似症状进行分组、提取服务、严重性、最近事件以及初步的影响半径(爆炸半径)。
frontier-reviewer 占据网络的上层,用于有争议的诊断、冲突的计划、关键补救和事后分析。在这些情况下,错误的成本可能超过整个模型调用的成本。
在层级之间划定界限的依据不是模型的声望,而是决策的可恢复性。如果操作易于回滚且可以通过本地验证器进行检查,则它保留在廉价的回路中。如果回滚成本高昂或后果涉及多个服务,则需要昂贵的回路。
flowchart TD IN[事故流] S[SDD 阶段 S 信号收集和规范化] D1[SDD 阶段 D1 异常检测] D2[SDD 阶段 D2 诊断和评估] Q[处理队列长度] R[风险级别] B[令牌预算作为能量] P[流量分配器] A[local-coder 基础级别] G[frontier-reviewer 上层级别] O[事故解决和反馈] IN --> S --> D1 --> D2 --> O D1 --> Q D2 --> R Q --> P R --> P B --> P P -->|稳定模式| A P -->|队列和风险增长| G A --> O G --> O A -->|升级复杂案例| G O -->|修正限制和队列| B
上图仅显示 SDD 周期的输入和决策阶段(信号收集、检测、诊断);完整的事故周期由 plan、remediation、postmortem 阶段继续,它们具有单独的 SLA 和配额——它们出现在下面的 YAML 中。也就是说,图表中的三个抽象阶段(S、D1、D2)展开为六个具体配额(triage、classification、diagnosis、plan、remediation、postmortem)加上作为缓冲的 control_reserve。
根据负载形式构建令牌配额,而不仅仅根据所需的节省。对于每天 10M 令牌,基础分配可以将 9M 分配给 local-coder,将 1M 分配给 frontier-reviewer。廉价层级涵盖分诊、分类、粗略诊断和初步计划。昂贵层级获得验证、有争议的补救操作和事后分析的储备。
为每个阶段单独设置 SLA 阈值。例如:分诊必须在几十秒内完成,诊断可以花费更多上下文,而事后分析允许更长的进程以获得完整的证据链。
不要将储备变成"剩菜剩饭"。储备是一个安全层,仅在风险、队列或不确定性增加时激活。
项目文件模板:.specify/memory/budget_network.yaml。
daily_budget_tokens: 10000000
phases:
triage:
local-coder: 3000000
frontier-reviewer: 120000
sla_p95: "30s"
classification:
local-coder: 2000000
frontier-reviewer: 140000
sla_p95: "45s"
diagnosis:
local-coder: 1500000
frontier-reviewer: 180000
sla_p95: "90s"
plan:
local-coder: 800000
frontier-reviewer: 120000
sla_p95: "120s"
remediation:
local-coder: 700000
frontier-reviewer: 200000
sla_p95: "180s"
postmortem:
local-coder: 300000
frontier-reviewer: 240000
sla_p95: "10m"
control_reserve:
local-coder: 700000
frontier-reviewer: 0
在您自己的项目中,相同的步骤被形式化为 tools/budget_keeper.py compile|assert|simulate|inspect,位于与提供商和 CI 的集成之上。在教材内,运行可运行的类似物:
> [runnable] — 预算守护者的可运行示例位于 [examples/budget-keeper/](examples/budget-keeper/)(参见 [examples/budget-keeper/README.md](examples/budget-keeper/README.md)):其中有 budget_network.yaml 示例、compile.py、simulate.py、inspect.py 脚本以及故障转移场景。
cd book2/examples/budget-keeper
python3 scripts/compile.py \
--budget-spec specs/budget_network.yaml \
--out out/budget_plan.json
将级联失效建模为分级的故障转移,而不是一个模型替换另一个模型的简单过程。此处的故障转移(failover)是在层级失效时切换负载的计划。让我们看看不同方法之间的差异。
不佳: > 当 local-coder 宕机时,所有流量都转到 frontier-reviewer。
问题:昂贵的层级将在几分钟内消耗每日配额,并且当真正的 P0/P1 到来时将无法为其提供服务。
良好:
> 当 local-coder 宕机时,只有 severity in [P0, P1] 且 age > 90s 的任务转到 frontier-reviewer,其余进入降级队列(degraded queue)。
如果 local-coder 宕机,请勿自动将所有传入流路由到 frontier-reviewer。否则,昂贵层级将迅速耗尽配额,并失去服务真正关键案例的能力。
相反,预算守护者(budget-keeper,令牌预算控制服务)每分钟计算几个参数:spent[p] 和 queue[p](在阶段 p 中花费的和队列长度)、quota[p](剩余配额)、事故年龄、爆炸半径和模型置信度差距(confidence-gap)。基于此,它仅选择那些延迟比消耗更危险的任务。这种分级的故障转移改变了升级时间:部分事故立即转到 frontier-reviewer,部分保持在降级模式(degraded mode),部分在给定超时后转移到手动通道。
紧急模式,或称"红色按钮"(red button),是切换到受保护模式的开关。形象化的名称是可以接受的,但在工件中应记录紧急模式的确切条件。需要它作为单独的管理模式,因为自动故障转移本身可能成为事故的来源。启用条件是形式化的:token_health(令牌预算健康综合指标)风险连续两个窗口增长、队列超过限制、关键严重性的 SLA 超时或为 local-coder 提供服务的本地端点宕机。
触发后,系统限制新队列、禁止大规模自动补救、为 P0/P1 保留 frontier-reviewer,并将其余决策转移到手动或准手动模式。手动模式不是回到混乱。让它继承相同的文件协议、证据链和 PostToolUse 检查,以便在稳定后可以恢复每个决策的原因。
validation.md 中的反古德哈特逻辑关闭了预算优化的主要风险:以实际事故管理的隐性恶化为代价来改善报告指标。反古德哈特规则是禁止在由于其他指标的恶化而使一个指标增长时认为发布成功。
如果仅控制 MTTR,系统可以更快地将复杂事故关闭为非关键、降低升级比例或将不便的 P0 排入没有完整事后分析的手动通道。因此,MTTR 应与四个守护指标和一个验证激活条件一起验证。它们的角色方便地放在一个表中。
| 指标 | 测量内容 | 阻止内容 |
|---|---|---|
escalation_share | 升级占总流量的比例 | 验证激活条件——在 MTTR 快速的同时低于历史走廊 |
silent_p0 | 无升级关闭的 P0 比例 | 增长超过 2% |
unresolved_manual_ratio | 未关闭的手动任务比例 | 增长超过 5% |
postmortem_gap | 事后分析中的差距 | 差距超过 10% |
token_health_min | 预算健康的最低水平 | 降至 0.6 以下 |
如果任何守护指标超出其边界,则认为 MTTR 改善无效。成对检查正是出于此目的:漂亮的报告指标不应掩盖稳健性的恶化、安静的 P0 失败或证据链的断裂。
validation.md 中带有预算网关规则的片段。
checks:
- id: anti_goodhart_budget
if:
all:
- mttr_p95 < "5m"
- escalation_ratio < 0.08
then:
fail_if:
- silent_p0 > 0.02
- unresolved_manual_ratio > 0.05
- postmortem_gap > 0.10
- token_health_min < 0.60
- id: ecology_warn
if:
any:
- token_health_trend_5m < -0.12
- queue_pressure > 0.80
- degraded_mode_duration > "120s"
then:
require:
- red_button_review == true
- manual_channel_open == true
- frontier_reserved_for_p0_p1 == true
在您自己的项目中,此检查被形式化为 python3 tools/validation_runner.py run --spec validation.md --out .specify/artifacts/validation_health.json,随后使用 jq 检查 anti_goodhart_budget 和 ecology_warn。反古德哈特检查本身的可运行类似物是 examples/goodhart-validator/scripts/run_validation.py(参见第 10 章)。
完整轨道:阈值校准
预算大小、local/frontier 比例和 manual_timeout_sec 的"低/默认/高"表、压缩 5M 变体的练习以及重新校准的信号——位于附录 D,D.3 节。在第一次阅读中,两次失效模拟和 budget-note.md 中的一行 token_health 就足够了。
示例和应用
场景 B 的实际模拟验证了 local-coder 的下降不会将 frontier-reviewer 转变为整个队列的紧急备用。在 11:00,廉价模型的本地端点不可用 45 分钟。队列包含 20 个事故。手动超时为 120 秒。
策略选择三个方向:具有最大爆炸半径和年龄的 5 个任务转到 frontier-reviewer,15 个任务保留在降级队列中,两分钟后打开手动通道。检查被认为是成功的,不是因为所有任务都是自动处理的。成功在于:系统保留了昂贵层级,限制了队列,并且不允许 token_health_min 降至安全阈值以下。
在您自己的项目中,此场景作为 tools/budget_keeper.py simulate ... --failure "11:00,local-coder,down,45m" --queue 20 --manual-timeout-sec 120 运行,随后按条件 failover_to_frontier==5 && degraded_queue==15 && manual_queue_after_120s==15 && token_health_min>=0.5 进行 inspect。可运行的类似物是相同的:
> [runnable] — 场景 examples/budget-keeper/scenarios/fail_local_45m.json。
cd book2/examples/budget-keeper
python3 scripts/simulate.py \
--plan out/budget_plan.json \
--scenario scenarios/fail_local_45m.json \
--out out/fail_result.json
python3 scripts/inspect.py \
--result out/fail_result.json \
--query "failover_to_frontier==5 && degraded_queue==15 && manual_queue_after_120s==15 && token_health_min>=0.5"
在稳定后逐步回滚:否则廉价层级的恢复将创建第二个级联。首先返回 30% 的 local-coder 配额,且仅用于分诊/分类(这些阶段更容易通过形式特征验证,并且更快地减轻输入流);在三个稳定的 token_health 窗口、silent_p0_ratio 没有增长和队列正常化之后,再返回 30% 用于诊断/计划;仅在 PostToolUse 审计之后才允许完全返回。原因:过早解除手动模式可能会掩盖退化期间累积的错误。
在运营中,此模型方便地作为每日预算教学演练(budget-drill)进行检查。团队获取昨天的警报流,通过当前的 budget_network.yaml 运行,并人为地将 local-coder 关闭 15、30 和 45 分钟。然后比较四个指标:MTTR、升级比例、手动队列量和最小 token_health。
审查的信号:
- 如果在短暂失效期间
frontier-reviewer开始处理非关键任务——故障转移过于宽泛; - 如果手动通道在中等队列时已打开——SLA 阈值过于紧张。
运行的目标是找到退化可预测的配置文件,而不是在配额耗尽之前一直不可见。
小结
令牌预算只有在五个元素链接到一个管理回路中时才成为可管理的资源:SDD 阶段、模型分层(model tiering)、SLA 阈值、故障转移和验证。在此回路中,local-coder 为大规模常规任务提供吞吐量;frontier-reviewer 保护有争议和高风险的决策;紧急模式在风险增加时限制自动化;validation.md 不允许以隐藏的 P0 和被破坏的事后分析为代价来改善 MTTR。这样的方案不仅显示了当前消耗,还显示了退化顺序:哪些阶段将首先饥饿,哪些任务应转移到昂贵层级,以及手动模式何时比进一步自动化更安全。接下来,此回路将转向古德哈特指标和成对的守护指标。
工件和就绪标准
| 工件 | 就绪条件 |
|---|---|
book2/examples/budget-keeper 的本地运行 | 配额总和对应 10M 令牌和指定的 local/frontier 划分 |
out/budget_plan.json、out/fail_result.json、out/fail_15m_result.json | 45 分钟场景给出 failover_to_frontier==5,degraded_queue==15,manual_queue_after_120s==15,token_health_min>=0.5;15 分钟场景保持 token_health_min>=0.7;out/ 未提交 |
precedents.md 或 capstone/budget-note.md 中的记录 | 解释当 local-coder 失效时会发生什么,哪些任务转到 frontier-reviewer 以及哪个 token_health_min 阈值保护预算 |
完整轨道添加了 .specify/memory/budget_network.yaml(带有阶段和 SLA)、compile 后的 budget_plan.json、fail_scenario_B.json、带有反古德哈特预算网关的 validation.md 和 validation_health.json。如果紧急模式为 P0/P1 保留 frontier 并打开手动通道,反古德哈特网关阻止以 silent_p0 或审计中断为代价的节省,并且预算模拟包含在定期演练或 CI 中,则认为其就绪。
实践
cd book2/examples/budget-keeper && python3 scripts/compile.py --budget-spec specs/budget_network.yaml --out out/budget_plan.json— *预期:daily_budget_tokens == 10_000_000,local 层级之和 9M,frontier 1M(90/10)。*
python3 scripts/simulate.py --plan out/budget_plan.json --scenario scenarios/fail_local_45m.json --out out/fail_result.json && python3 scripts/inspect.py --result out/fail_result.json --query "failover_to_frontier==5 && degraded_queue==15 && manual_queue_after_120s==15 && token_health_min>=0.5"— 预期:返回码 0,四个条件同时满足。- 将五行转移到
capstone/budget-note.md:risk、effect、simulated_floor、alert_threshold、decision。*预期:格式与"它如何进入capstone/"部分中的参考匹配;完整的budget_plan.json不进入capstone/。*
复习问题
- 为什么故障转移不应将整个队列路由到昂贵层级?
- 哪些指标揭示了预算路由的退化?
- 何时手动模式比继续自动化更安全?
- 本地模型在高峰时段宕机 45 分钟。您有 60% 的每日预算,但 MTTR 正在上升。您将切换什么——模型、路由策略还是分诊模式?