详解 Weave Router 滞回机制:为什么模型切换必须跨过 WMI 分数差才放行
【免费下载链接】routerModel router for agentic systems. Routes every prompt to the right model in <50ms. Cut costs 40-70% with just an endpoint change.项目地址: https://gitcode.com/GitHub_Trending/router101/router
Weave Router是面向 AI Agent 系统的模型路由器:它把 Claude Code、Codex 等工具发来的每一个 API 请求,在 50ms 内路由到最合适的模型,仅靠改一个端点地址就能降低 40%–70% 的成本。而**滞回(hysteresis)**是它防止模型频繁切换的核心机制——新模型必须与在用的"现任"拉开足够的WMI 分数差,切换请求才会被放行。这篇文章带你从零看懂这套设计的来龙去脉。
从一次"差点被换掉"的模型说起
假设你在一个长会话里用 Claude Code 写代码。第一句"帮我写个 Python 脚本"很简单,Weave Router 给这次请求配了claude-opus-5:low(低推理强度档)。十几轮之后你开始让它设计复杂架构,打分器认为high档更好,于是"挑战者"出现了:claude-opus-5:high。
问题是:下一句你又问了一个小问题,打分器立刻又觉得low更好。如果每一轮都听从打分器的最新意见,你的会话就会在low ↔ high之间反复横跳——
- 前缀缓存全部作废:每次切换,之前累积的上下文缓存前缀就失效了,下一轮要重新按全价重新计费;
- 回答质量忽高忽低:同一会话里风格来回变,体验很割裂;
- 成本不降反升:切换本身就是浪费。
这就是 Weave Router 引入滞回要解决的核心问题 🛡️。
什么是"滞回":给模型切换加一道缓冲带
滞回是物理和工程里的经典概念:继电器不会因为电压在临界值附近波动而疯狂开合,而是设一个"迟滞区间",让状态变化必须"明确跨过阈值"才发生。
Weave Router 把它搬进了模型路由。判定逻辑就在这一小段代码里:
核心判定:effortHysteresisHold
它做的判断只有一句话能讲清楚:
挑战者的 WMI 分数 − 现任者的 WMI 分数 ≥ 1.0,切换才放行;否则继续用现任。
这个 1.0 的分数差阈值是写死的常量:
阈值定义:effortHysteresisThreshold = 1.0
也就是说:high档必须比low档至少优 1.0 分,Router 才会真的把会话切过去。
WMI 分数是什么?
WMI是 Weave Router 为每个"候选臂(arm)"算出的综合评分。这里的"臂"指"模型 + 推理强度"的组合,例如claude-opus-5:high。
它由 Go 侧的确定性打分器产生,是两条归一化轴的加权混合:
- WII:质量轴,衡量模型干活的能力;
- WPI:评测工作负载轴(注意:它不是计费价格,预览金额永远来自模型目录价)。
官方文档对此有明确说明(权重按组织的"质量/价格偏好"调节):
docs/HMM_GO_SELECTION.md:v7 rosters score each arm as
alpha*WII - (1-alpha)*WPI… WPI is a normalized evaluation-workload axis, not a billing rate
滞回比较时,直接读取每次决策附带的逐臂分数表ArmScores,按"臂 ID"(如anthropic/claude-opus-5:xhigh)取值:
分数表定义:ArmScores:ArmScores holds per-arm WMI scores for hysteresis
三种典型场景:何时放行、何时按住?
滞回检查只在同一基础模型内、仅推理强度变化时生效(跨模型切换是规划器的职权,直接放行)。用测试用例里的数字看最直观:
| 场景 | WMI 分数(low → high) | 分数差 | 结果 |
|---|---|---|---|
| 挑战者只略优 | low=4.0, high=4.5 | 0.5 < 1.0 | 🛑 按住,继续用 low |
| 挑战者明显更优 | low=4.0, high=6.0 | 2.0 ≥ 1.0 | ✅ 放行,切到 high |
| 挑战者更差 | low=5.0, high=1.0 | −4.0 < 1.0 | 🛑 按住,继续用 low |
| 跨模型(opus → fable) | — | 不参与计算 | ✅ 直接放行 |
对应的边界条件测试覆盖了"无分数表""现任臂无分数""强度没变"等一切直通情形,可以完整对照:
完整测试:hysteresis_internal_test.go
当切换被按住时,Router 会在路由原因上追加effort_hysteresis标签(appendEffortHysteresisReason),保留规划器原本的判断同时留下"被滞回拦下"的痕迹——离线分析就能把"被拦下的轮次"和"干净留守的轮次"分开统计。
不止一层:分数差 + 连续票数,双闸门防抖
上面讲的 WMI 分数差管的是"同一模型内换强度"。跨模型场景(比如从旗舰模型降到便宜模型)还有第二道闸门:连续降级投票数。
在 turnloop.go 的注释里写得很直白:
Order matters — an unconfident cheaper vote is discarded before hysteresis sees it, so noise cannot accumulate into a switch.(低置信度的降级票在滞回看到它之前就被丢弃,噪声无法累积成一次切换。)
流程分三步:
- 置信度门槛:打分器的置信度低于阈值,这张"降级票"直接作废;
- 连续计票:置信够格的降级建议累加票数(
ConsecutiveDowngradeVotes),票数没达到hmm_downgrade_hysteresis_turns设定的轮数,继续留在原模型上; - 达标放行:票数够了才真正降级,同时记录"影子遥测"——用一套影子阈值统计"如果启用更严格的滞回,本来能拦住几次",让运营团队先量化再上线。
注意一个有趣的不对称:升级不受滞回限制(升级通常只多花钱、不掉体验),受保护的主要是降级和强度切换。
这套设计给用户带来了什么?
📌更稳的会话体验:模型不会在会话中途反复"变脸",上下文、风格和质量保持一致。
💰更低的账单:被拦下的每一次低性价比切换,都意味着前缀缓存继续生效、不产生重复的全价计费。
📈可量化的调优:所有滞回决策都带标签写入遥测(observation.go),配合影子计数器,阈值是"用数据调出来的",而不是拍脑袋。
关键配置与源码速查
| 项目 | 位置 |
|---|---|
| 强度切换分数差阈值(1.0) | internal/proxy/hysteresis.go#L7-L9 |
| 滞回判定主逻辑 | internal/proxy/hysteresis.go#L19-L48 |
| 降级连续计票闸门 | internal/proxy/turnloop.go#L1646-L1698 |
| 滞回相关特性开关定义 | internal/flags/flags.go#L63-L66 |
| WMI 分数与打分器文档 | docs/HMM_GO_SELECTION.md |
| 部署配置参考 | docs/CONFIGURATION.md |
可调开关包括hmm_downgrade_hysteresis_turns(降级计票阈值,设为 0 可禁用)和hmm_downgrade_hysteresis_shadow_turns(影子计数阈值,默认 2,只做遥测、不改变路由行为),均可按组织维度覆盖。
小结
Weave Router 的滞回机制,本质是给"模型切换"装了一个防抖器:WMI 分数差管同模型内的强度升降(必须 ≥ 1.0 才放行),连续降级票数管跨模型降级,低置信度噪声在入口就被丢弃。三道防线共同保证了:只有"明确更好的选择"才能改变你的会话——换得少,但换得准。这既守住了体验的连贯性,也守住了 40%–70% 的成本优化空间 🎯。
【免费下载链接】routerModel router for agentic systems. Routes every prompt to the right model in <50ms. Cut costs 40-70% with just an endpoint change.项目地址: https://gitcode.com/GitHub_Trending/router101/router
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考