源码包:pkg/routing/ · 仓库文档索引:docs/README.md
MoonHub 的 Smart Router V2 将原有的 2 级路由 (light/heavy) 升级为 4 级路由系统,同时保留了 MoonHub 现有的语言无关结构化特征、会话历史感知等优势。
| 功能模块 | 状态 | 文件 | 测试 |
|---|---|---|---|
| 4 级分类 (QueryTier) | ✅ 完成 | tier.go |
✅ |
| V2 分类器 (RuleClassifierV2) | ✅ 完成 | classifier_v2.go |
✅ 14 tests |
| V2 路由器 (RouterV2) | ✅ 完成 | router.go |
✅ 13 tests |
| 特征提取 (Features) | ✅ 完成 | features.go |
✅ |
| 配置支持 | ✅ 完成 | config/config.go |
✅ |
| Agent 集成 | ✅ 完成 | agent/instance.go, agent/loop.go |
✅ |
| Metrics 收集 | ✅ 完成 | metrics.go |
✅ 6 tests |
| 决策记录器 | ✅ 完成 | recorder.go |
✅ 9 tests |
| HTTP Endpoints | ✅ 完成 | health/server.go |
✅ |
| 权重调优 | 📋 待定 | - | - |
总计: 42 个单元测试,全部通过
pkg/routing/
├── tier.go # QueryTier 类型定义和 DefaultTierBoundaries
├── classifier.go # 原有 Classifier 接口 + 新增 Signal, ClassificationResult, ClassifierV2
├── classifier_v2.go # RuleClassifierV2 实现(短消息降权、附件硬门控、Sigmoid 置信度)
├── router.go # 原有 Router + 新增 TierModelMapping, RouterConfigV2, RouterV2
├── features.go # Features 特征提取(语言无关)
├── metrics.go # Metrics 收集器(隐私安全)
├── recorder.go # 决策记录器(隐私安全)
├── classifier_v2_test.go # V2 分类器单元测试 (14 个)
├── router_v2_test.go # V2 路由器单元测试 (13 个)
├── metrics_test.go # Metrics 单元测试 (6 个)
└── recorder_test.go # Recorder 单元测试 (9 个)
| Tier | 分数范围 | 说明 | 典型场景 |
|---|---|---|---|
simple |
[-1.0, -0.05) | 极简任务 | 问候语、确认、简单问答 |
moderate |
[-0.05, 0.15) | 中等任务 | 短问题、简单指令 |
complex |
[0.15, 0.35) | 复杂任务 | 代码编写、长上下文、工具调用 |
reasoning |
[0.35, 1.0] | 推理任务 | 多模态、深度分析、附件处理 |
| 特征 | 权重 | 说明 |
|---|---|---|
| 短消息 (<20 tokens) | -0.10 | 负分,路由到简单模型 |
| 短–中 (20–50 tokens) | +0.06 | 介于问候与中等长度之间 |
| 中等消息 (51–200 tokens) | +0.15 | 中等长度(阈值用 >50,恰好 50 token 走短–中档) |
| 长消息 (>200 tokens) | +0.35 | 长文本需要更强模型 |
| 代码块存在 | +0.40 | 编程任务需要重型模型 |
| 工具调用 >3 | +0.25 | 密集工具使用表示复杂工作流 |
| 工具调用 1-3 | +0.10 | 活跃的代理工作流 |
| 对话深度 >10 | +0.10 | 长会话隐含复杂度 |
| 附件存在 | 1.0 (硬门控) | 多模态直接路由到推理模型 |
使用 Sigmoid 函数基于分数距离边界的距离计算置信度:
- 分数在层级中心:高置信度
- 分数靠近边界:低置信度
- 附件硬门控:100% 置信度
每个分类结果包含信号追踪,便于调试和可观测性:
type Signal struct {
Name string // 信号名称,如 "short_message", "code_block"
Contribution float64 // 对分数的贡献值
RawValue any // 原始值,如 token 数量、代码块数量
}新增 4 级路由配置:
type RoutingConfig struct {
Enabled bool `json:"enabled"`
LightModel string `json:"light_model"` // 2-tier 模式
Threshold float64 `json:"threshold"` // 2-tier 模式
// V2 4-tier
TierMapping map[string]string `json:"tier_mapping,omitempty"`
TierBoundaries *TierBoundariesConfig `json:"tier_boundaries,omitempty"`
}
type TierBoundariesConfig struct {
// 指针字段:可区分「未配置」与显式 0.0(例如 simple_moderate: 0)
SimpleModerate *float64 `json:"simple_moderate,omitempty"`
ModerateComplex *float64 `json:"moderate_complex,omitempty"`
ComplexReasoning *float64 `json:"complex_reasoning,omitempty"`
}tier_mapping的 key 必须为simple/moderate/complex/reasoning(大小写敏感);未知 key 会记录日志并跳过。tier_boundaries在任一 cutpoint 被设置时生效;须满足 sm < mc < cr 且均在 [-1, 1] 内,否则回退默认边界并打日志。- 健康检查
GET /routing/decisions?tier=...仅接受上述四档;非法值返回 400。
新增字段和初始化逻辑:
type AgentInstance struct {
// ... 现有字段 ...
Router *routing.Router // 2-tier 路由器
RouterV2 *routing.RouterV2 // 4-tier 路由器
TierCandidates map[routing.QueryTier][]providers.FallbackCandidate // 每层预解析的候选
LightCandidates []providers.FallbackCandidate // 2-tier 兼容
}初始化逻辑:
- 检测
tier_mapping配置 → 启用 4-tier 模式 - 否则检测
light_model→ 使用 2-tier 模式 - 为每个 tier 预解析 provider candidates
更新 selectCandidates 支持 4 级路由:
func (al *AgentLoop) selectCandidates(
agent *AgentInstance,
userMsg string,
history []providers.Message,
) (candidates []providers.FallbackCandidate, model string) {
// V2 4-tier routing
if agent.RouterV2 != nil && agent.RouterV2.IsV2Enabled() && len(agent.TierCandidates) > 0 {
selectedModel, tier, result := agent.RouterV2.SelectModel(userMsg, history, agent.Model)
if tierCandidates, ok := agent.TierCandidates[tier]; ok && len(tierCandidates) > 0 {
logger.InfoCF("agent", "Model routing: tier selected", map[string]any{
"tier": tier.String(),
"model": selectedModel,
"score": result.Score,
"confidence": result.Confidence,
})
return tierCandidates, selectedModel
}
}
// Legacy 2-tier fallback ...
}{
"agents": {
"defaults": {
"routing": {
"enabled": true,
"tier_mapping": {
"simple": "glm-4-flash",
"moderate": "glm-4-flash",
"complex": "glm-4-plus",
"reasoning": "claude-opus-4-5"
}
}
}
}
}{
"agents": {
"defaults": {
"routing": {
"enabled": true,
"light_model": "glm-4-flash",
"threshold": 0.35
}
}
}
}{
"agents": {
"defaults": {
"routing": {
"enabled": true,
"tier_mapping": {
"simple": "model-a",
"moderate": "model-b",
"complex": "model-c",
"reasoning": "model-d"
},
"tier_boundaries": {
"simple_moderate": 0.0,
"moderate_complex": 0.25,
"complex_reasoning": 0.5
}
}
}
}
}| 场景 | 行为 |
|---|---|
只配置 light_model + threshold |
使用 2-tier legacy 模式 |
配置 tier_mapping |
使用 4-tier V2 模式 |
| 都不配置 | 禁用路由,使用 primary model |
调用 SelectModelLegacy |
自动映射 4-tier → 2-tier |
| 测试文件 | 测试函数数量 | 覆盖内容 |
|---|---|---|
classifier_v2_test.go |
14 | 短消息负分、附件硬门控、代码块分类、长消息分类、工具调用分类、对话深度、自定义边界、置信度计算、信号追踪、ScoreToTier 映射、分数范围 |
router_v2_test.go |
13 | 4-tier 模式启用、2-tier 回退、各 tier 路由选择、缺失 tier 映射、Legacy 模式、自定义边界、阈值测试、LightModel 测试、无路由配置、集成测试 |
metrics_test.go |
6 | 基本记录和检索、Reset 功能、并发安全性、NoOp 收集器、全局设置、直方图分布 |
recorder_test.go |
9 | 基本记录、Ring buffer、按 tier 过滤、统计计算、禁用状态、Clear 功能、信号深度限制、隐私保护验证 |
总计: 42 个测试函数,覆盖核心功能和边界情况
=== 所有测试通过 ===
ok github.com/RealityLink-Tech/MoonHub/pkg/routing
- 分类计算: < 1µs 每次消息(纯规则,无 API 调用)
- 内存占用: 零额外分配(除返回结构体外)
- 并发安全: RouterV2 可被多个 goroutine 安全使用
- 问题: 原有 2-tier 系统无法区分"问候语"和"简单问题"
- 解决: 为 <20 token 的消息添加 -0.10 负分,使其落入
simpletier - 效果: 问候语如"你好"、"hi"会被路由到最便宜的模型
- 问题: 多模态消息需要特定模型能力
- 解决: 检测到附件时直接返回 TierReasoning, Score=1.0, Confidence=1.0
- 效果: 跳过所有其他计算,保证多模态请求始终使用支持视觉的模型
- 问题: 需要知道路由决策的可信程度
- 解决: 使用 Sigmoid 函数计算基于分数到边界距离的置信度
- 效果: 可用于日志、监控和未来可能的 A/B 测试
- 问题: 需要解释为什么某个消息被路由到特定 tier
- 解决: 每个分类结果包含 Signals 列表,记录所有贡献因素
- 效果: 便于调试和优化权重
| 文件路径 | 说明 |
|---|---|
pkg/routing/tier.go |
QueryTier 类型和 DefaultTierBoundaries |
pkg/routing/classifier_v2.go |
RuleClassifierV2 实现 |
pkg/routing/classifier_v2_test.go |
V2 分类器单元测试 |
pkg/routing/router_v2_test.go |
V2 路由器单元测试 |
pkg/routing/metrics.go |
Metrics 收集器实现 |
pkg/routing/metrics_test.go |
Metrics 单元测试 |
pkg/routing/recorder.go |
决策记录器实现 |
pkg/routing/recorder_test.go |
Recorder 单元测试 |
| 文件路径 | 修改内容 |
|---|---|
pkg/routing/classifier.go |
添加 Signal, ClassificationResult, ClassifierV2 接口 |
pkg/routing/router.go |
添加 TierModelMapping, RouterConfigV2, RouterV2,集成 metrics/recorder |
pkg/config/config.go |
扩展 RoutingConfig 添加 TierMapping 和 TierBoundariesConfig |
pkg/agent/instance.go |
添加 RouterV2, TierCandidates 字段和初始化逻辑 |
pkg/agent/loop.go |
更新 selectCandidates 支持 4-tier 路由 |
pkg/health/server.go |
添加 /metrics, /routing/decisions, /routing/stats endpoints |
- 收集实际使用数据
- 根据用户反馈微调权重
- 可能需要针对不同语言调整
已实现完整的可观测性系统,完全保护用户隐私(不记录消息内容)。
// 全局 metrics 收集器
collector := routing.NewDefaultMetricsCollector()
routing.SetGlobalMetrics(collector)
// 获取快照
snapshot := collector.GetSnapshot()
// snapshot.TotalClassifications - 总分类次数
// snapshot.TierCounts - 各 tier 的计数
// snapshot.AvgScore - 平均分数
// snapshot.AvgConfidence - 平均置信度
// snapshot.SignalCounts - 各信号的触发次数
// snapshot.ScoreDistribution - 分数分布直方图
// snapshot.ConfidenceDistribution - 置信度分布直方图隐私保护:只收集聚合统计数据,不记录用户消息内容。
// 初始化决策记录器
cfg := routing.DefaultDecisionRecorderConfig()
cfg.BufferSize = 1000 // 保留最近 1000 条决策
cfg.MaxSignalDepth = 10 // 每条决策最多记录 10 个信号
recorder := routing.NewDecisionRecorder(cfg)
// 获取最近决策
decisions := recorder.GetRecent(50)
// 按 tier 过滤
simpleDecisions := recorder.GetByTier(routing.TierSimple, 10)
// 获取统计信息
stats := recorder.GetStats()隐私保护:
- 不记录原始消息内容
- Signal 的 RawValue 被剥离
- AgentID 应使用哈希/派生值
| Endpoint | 说明 |
|---|---|
GET /metrics |
返回聚合的 routing metrics (JSON) |
GET /routing/decisions?limit=50&tier=simple |
返回最近的决策记录 |
GET /routing/stats |
返回聚合统计信息 |
示例响应:
// GET /metrics
{
"timestamp": "2026-03-21T13:00:00Z",
"total_classifications": 1234,
"avg_score": 0.15,
"avg_confidence": 0.82,
"tier_counts": {
"simple": 400,
"moderate": 300,
"complex": 350,
"reasoning": 184
},
"signal_counts": {
"short_message": 400,
"code_block": 350,
"attachment_gate": 50
}
}
// GET /routing/decisions?limit=5
{
"total": 1234,
"limit": 5,
"decisions": [
{
"id": 1233,
"timestamp": "2026-03-21T12:59:59Z",
"tier": "complex",
"score": 0.25,
"confidence": 0.78,
"signals": [
{"name": "code_block", "contribution": 0.4}
],
"model": "glm-4-plus"
}
]
}RouterV2 自动集成 metrics 和 recorder:
// 创建 router 时自动连接全局 metrics/recorder
router := routing.NewV2(cfg)
// 使用 SelectModelWithRecord 记录决策
model, tier, result := router.SelectModelWithRecord(msg, history, primaryModel, agentID)
// 或手动设置
router.SetMetrics(customCollector)
router.SetRecorder(customRecorder)| 测试文件 | 测试数量 | 覆盖内容 |
|---|---|---|
metrics_test.go |
6 | 基本记录、Reset、并发安全、NoOp、全局设置、直方图 |
recorder_test.go |
9 | Ring buffer、按 tier 过滤、统计、隐私保护、禁用状态 |
测试覆盖:
- ✅ 基本记录和检索
- ✅ Ring buffer 行为
- ✅ 并发安全性
- ✅ 隐私保护验证(确保 RawValue 被剥离)
- ✅ 统计计算
- ✅ 重置功能
- MoonHub 原有 2-tier 路由实现