# Task-aware model routing 技术方案(讨论稿 v0.4) > 状态:设计目标,尚未全部实现。本文把研究稿中的完整 Agent 调度理论收缩为 `dsh-quota-router` 的一个最小工程切片:**对已经拆分好的 subtask,按照任务类型、复杂度、精度要求和可用模型范围选择模型,并在 subtask 内保持模型稳定,在基础设施失败时执行有界 retry/fallback。本文同时补充 quota-router 所需的最小任务契约字段、分类置信度字段、模型租约状态约束和可回放事件边界。** > > 本方案暂不实现 Planner、复杂任务拆解、上下文压缩、跨 Harness 状态迁移、语义质量判断、自动 replan,也不涉及 `dsh-agent-suite` 的 Receipt/Ledger。现有 source/provider 候选链继续按当前实现工作;本方案新增的是“任务能力 → 模型候选”的策略层,不重新设计多源选择。 ### 当前实现基线 当前 `dsh-quota-router` 已实现的是 v0.1 的 source/profile 路由:关键词 profile 匹配、按 source priority 展开的候选链、DSH 原生模型校验、turn 级决策身份、基础设施失败 retry/fallback、cooldown 和内存 ledger。本文中的 `SubtaskRouteRequest`、`SubtaskModelLease`、契约 hash、分类置信度、能力下限过滤和 `RouteOutcome` 属于后续迁移目标,不应被解释为当前公开 API 已经提供的能力。 迁移实现必须保持 v0.1 行为可用;每个阶段只增加显式的请求字段和状态约束,不把 Planner、Judge 或自动 replan 偷渡进 router。 ## 1. 定位与边界 ### 1.1 完整系统中的位置 ```text Planner / Agent ├─ 任务规划与拆解 ├─ 创建 subtask ├─ 标记 taskClass / complexity / precision └─ 定义验收标准 │ ▼ dsh-quota-router ├─ 读取模型策略 ├─ 过滤 allowedModels ├─ 选择 primary model ├─ 锁定 subtask 内模型 ├─ retry / 有界 fallback └─ 记录路由统计 │ ▼ DSH Harness └─ 执行当前 subtask │ ▼ 上层验收器 ├─ accepted ├─ quality-failed / review └─ needs-replan ``` ### 1.2 quota-router 负责什么 1. 消费上层已经拆分好的 `SubtaskRouteRequest`; 2. 根据 `taskClass`、`complexity`、`precision` 选择策略; 3. 在策略候选与调用方 `allowedModels` 的交集中选择模型; 4. 为 subtask 保存轻量模型 lease,后续 turn 默认继续使用已选模型; 5. 对可恢复的基础设施失败执行同模型 retry 或有序 fallback; 6. 记录选择、失败、切换、使用量、延迟和可选 outcome。 ### 1.3 quota-router 不负责什么 - 不生成任务计划,不决定如何拆分任务; - 不判断子任务之间的依赖,不调度并行 DAG; - 不合并主任务和子任务上下文; - 不执行 context compaction、摘要、检索或记忆; - 不判断模型返回结果的语义质量; - 不自动创建 review/repair subtask; - 不自动 replan; - 不自动跨 Harness 迁移工具、文件或审批状态; - 不实现 `dsh-agent-suite` Receipt/Ledger; - 不根据模型名称猜测能力、价格或质量; - 不在本方案中重新设计 source/provider 的多源选择。 ## 2. 核心概念 ### 2.1 SubtaskRouteRequest 上层不需要把完整 transcript 交给 router,只需提供路由所需的结构化元数据: ```ts interface SubtaskRouteRequest { taskId: string subtaskId: string /** Planner 生成的子任务契约身份;同一 lease 内必须保持不变。 */ contractVersion: string contractHash: string taskClass: | 'default' | 'simple' | 'coding' | 'planning' | 'analysis' | 'research' | 'writing' | 'review' complexity: 'low' | 'medium' | 'high' precision: 'normal' | 'high' | 'critical' /** 只声明会影响候选兼容性的少量能力要求;不做运行时能力推理。 */ requiredCapabilities?: { toolCalling?: boolean structuredOutput?: boolean longContext?: boolean codeExecution?: boolean } /** 分类来自哪里以及是否需要保守处理。 */ classificationConfidence?: number classificationSource?: 'user' | 'planner' | 'rule' | 'inferred' uncertaintyPolicy?: 'conservative' | 'manual' | 'default' /** 调用方允许的模型集合;不提供时使用策略候选。 */ allowedModels?: string[] /** 明确指定时优先使用;必须经过原生模型校验。 */ preferredModel?: string /** auto 允许策略 fallback;manual/none 不自动切换。 */ fallbackMode?: 'auto' | 'manual' | 'none' /** 用于复现和统计,不用于推断模型能力。 */ policyVersion?: string benchmarkVersion?: string } ``` 兼容旧调用方时,`taskClass` 可以由现有 profile/关键词规则产生;新调用方应尽量显式传递分类,避免 router 在每个 turn 重新猜测任务类型。 #### 2.1.1 最小任务契约字段 `contractVersion` 和 `contractHash` 不要求 router 理解完整任务内容,它们只用于确认:后续 turn 仍然执行同一个 subtask contract。Planner 或 Harness 仍然负责保存完整的目标、输入、输出、验收和交接信息。router 只做以下确定性检查: ```text contractVersion 非空 contractHash 非空且格式合法 同一 active lease 的 contractHash 不变 新 contractHash 不得复用旧 lease ``` 缺少契约身份时,新接口返回: ```text invalid-subtask-contract ``` 为了兼容旧调用方,可以在 legacy profile 模式下生成受限的临时 contract identity,但该请求不能获得 subtask 粘性保证;迁移完成后应关闭这个兼容分支。 `requiredCapabilities` 只保留影响路由兼容性的少量字段。它不是能力评测系统,也不允许 router 根据模型名称猜测能力;模型能力由策略配置和已发布的模型目录声明。 `classificationConfidence` 的取值范围为 `0..1`。低置信度不会自动触发语义判断,而是交给 `uncertaintyPolicy`: ```text conservative → 选择不低于 precision 下限的更保守候选 manual → 返回 ambiguous-classification,等待上层确认 default → 使用发布策略的默认行为并记录不确定性 ``` ### 2.2 能力档位 第一版不建立复杂的自动能力推理,只使用少量稳定档位: ```text economy 低复杂度、成本优先 balanced 常规任务的成本—能力平衡 strong 高复杂度或高精度任务 critical 关键研究、审查和不可轻易降级的任务 ``` `taskClass`、`complexity` 和 `precision` 共同决定能力档位。模型属于哪个档位由策略配置声明,不由模型名称推断。 ### 2.3 策略与候选链 不要把配置写成不可解释的“任务类型直接绑定一个模型”,而使用: ```text taskClass + complexity + precision → capability profile → ordered model candidates → allowedModels 过滤 → primary + fallback chain ``` 策略应带 `policyVersion`。Benchmark 用于离线形成和更新策略,运行时只执行已经发布的策略,不在每个请求现场运行 benchmark。 ### 2.4 SubtaskModelLease 第一版只需要轻量模型 lease,不需要把 Harness、上下文压缩版本和完整任务图塞进 lease: ```ts interface SubtaskModelLease { leaseId: string taskId: string subtaskId: string policyId: string policyVersion?: string selectedModel: string fallbackModels: string[] fallbackIndex: number contractVersion: string contractHash: string capabilityFloor: 'economy' | 'balanced' | 'strong' | 'critical' status: 'active' | 'retrying' | 'fallback' | 'completed' | 'failed' createdAt: number updatedAt: number } ``` 最小不变量: 1. 同一个 `subtaskId + contractHash` 同一时间最多有一个 active lease; 2. 正常 turn 不重新进行 profile/关键词匹配; 3. fallback 只能沿创建时保存的候选链向前推进,不能被 live config 重新排序; 4. fallback 不改变 `taskId`、`subtaskId`、`contractVersion` 或 `contractHash`; 5. `subtaskId` 只有在上层创建新子任务时才改变; 6. `completed`、`failed` 是终态,终态 lease 不得自动恢复; 7. lease 释放后不再自动恢复旧路由; 8. `precision=critical` 时不得自动跨越 capability floor 降级; 9. 质量失败不自动推进 fallbackIndex,必须由上层发起新的 escalation/review 路由请求。 #### 2.4.1 Lease 状态机 ```text created → active active → retrying → active active → fallback → active active → completed active → failed retrying → active retrying → fallback retrying → failed fallback → active fallback → failed ``` 状态转移约束: - `created` 只表示 lease 已分配但尚未开始执行;对外返回时应已经是 `active`,避免暴露半初始化 lease; - `active → retrying` 只允许基础设施瞬态失败,重试仍使用同一模型和同一契约; - `active/retrying → fallback` 只允许基础设施失败,且只能沿创建时保存的候选链前进; - `retrying` 或 `fallback` 成功后回到 `active`,候选耗尽或不可恢复时进入 `failed`; - `completed`、`failed` 是终态,任何终态 lease 都不得自动恢复、改写契约或重新排序候选; - 语义质量失败不触发 lease 内状态推进;上层必须创建 review/repair/escalation subtask; - `contractHash`、`capabilityFloor`、`fallbackMode` 等影响执行契约的字段改变时,必须创建新 lease,不能修改旧 lease。 状态语义: - `active`:正常执行,后续 turn 继续使用 `selectedModel`; - `retrying`:同一模型、同一契约的有界重试中; - `fallback`:已确定当前模型发生可恢复基础设施失败,正在切换到候选链下一项; - `completed`:Harness 已结束当前执行,路由器不再自动接管; - `failed`:候选耗尽、契约失效、不可恢复错误或人工终止。 路由请求应具备幂等语义:重复提交同一个 `taskId + subtaskId + contractHash` 时,若 active lease 仍存在,应返回已有 lease,而不是创建第二个 primary。若 contractHash 改变,必须返回 `lease-contract-mismatch` 或创建新的 subtask lease,不能修改旧 lease 的契约。 ## 3. 默认模型策略 以下是第一版建议的默认策略。模型名是可替换的配置值,不是 router 对模型能力的永久判断。 | 策略 ID | 任务类型 | Primary | 自动 fallback | fallback 模式 | | --- | --- | --- | --- | --- | | `default` | 未分类 | `opencode-go/mimo-v2.5` | 使用默认链 | `auto` | | `simple` | 简单任务 | `opencode/mimo-v2.5-free` | `token-share/gpt-5.4-mini` | `auto` | | `coding` | 普通编码 | `opencode-go/mimo-v2.5` | `token-share/gpt-5.6-luna` | `auto` | | `planning-analysis` | 普通规划/分析 | `opencode-go/deepseek-v4-flash/high` | `token-share/gpt-5.6-luna` | `auto` | | `research-critical` | 深度研究/关键审查 | `token-share/gpt-5.6-terra` | 无 | `none` 或 `manual` | | `writing` | 文档与普通写作 | `opencode-go/deepseek-v4-flash` | `token-share/gpt-5.6-luna` | `auto` | | `coding-high` | 高难编码 | `opencode-go/mimo-v2.5` | `token-share/gpt-5.6-terra` | `auto` | | `claude-special` | Claude 专项 | `starchasing/claude-sonnet-5` | 无 | `manual` | | `emergency` | 最终灾备 | `starchasing/gpt-5.6-terra` | 无 | `manual` | 建议的选择优先级: ```text 显式 preferredModel(仅在 allowedModels、能力下限和原生校验均满足时) > 策略候选与 allowedModels 的交集 > profile 兼容策略 > default 策略 ``` `preferredModel` 不是越权入口:调用方提供 `allowedModels` 时,preferred model 必须属于该集合;它还必须满足当前 `capabilityFloor`、`fallbackMode` 和 DSH 原生模型目录校验。若显式偏好不满足这些条件,返回 `no-compatible-model` 或 `preferred-model-not-allowed`,不得静默扩大权限、降低能力下限或改用未声明模型。 若调用方提供 `allowedModels`,最终候选必须是: ```text policyCandidates ∩ allowedModels ``` 交集为空时返回 `no-compatible-model`,不得静默扩大权限或降到未知模型。 ## 4. 路由决策流程 ```text route(request) 1. 校验 taskId / subtaskId / taskClass / complexity / precision 2. 解析 policyId 和 policyVersion 3. 生成有序候选模型 4. 与 allowedModels 求交集 5. 对候选执行 DSH 原生 provider/model 校验 6. 跳过 autoEligible=false 或不满足 fallbackMode 的候选 7. 选择第一个可用模型 8. 创建 SubtaskModelLease 9. 返回 selectedModel + fallback chain + leaseId ``` ### 4.1 显式分类优先于关键词猜测 第一阶段建议: ```text 显式 SubtaskRouteRequest > 上层 Agent/Planner 的分类 > 兼容性的 profile/关键词分类 > default ``` quota-router 不应嵌入一个新的 LLM 分类器。未来即使增加分类器,也不能覆盖调用方明确传入的 `allowedModels`、`preferredModel`、`precision` 或 `fallbackMode`。 ### 4.2 模型能力由策略声明 router 只消费模型策略,不负责判断“某模型是否擅长编码”。能力评估来自: - 离线 benchmark; - 线上 accepted/fallback 统计; - 人工维护的策略配置; - DSH 原生模型目录提供的可用性校验。 其中 benchmark 形成策略,线上数据验证策略: ```text benchmark → policyVersion → runtime routing → outcome statistics ``` ## 5. Retry、Fallback 与质量失败 ### 5.1 Retry Retry 保持: ```text same subtask + same model + same policy ``` 适用场景: - 短暂超时; - 网络抖动; - 偶发 5xx; - 流式传输中断; - 可由 DSH 原生 retry 处理的瞬态错误。 ### 5.2 Fallback Fallback 保持: ```text same taskId same subtaskId same task contract same precision floor ``` 只改变: ```text selected model ``` 自动 fallback 必须满足: 1. 当前失败属于可恢复的基础设施失败; 2. 下一候选已经通过 DSH 原生模型校验; 3. 下一候选属于同一能力下限或更高能力档位; 4. `fallbackMode === 'auto'`; 5. 候选没有 `manual`/`emergency`/未授权标记。 ### 5.3 质量失败 模型返回正常但结果质量不合格时,quota-router 不直接把它当作普通 fallback: ```text 语义质量失败 → 上层验收器 ├─ review / repair subtask ├─ explicit fallback request └─ needs-replan ``` 如果上层明确发起新的路由请求,可以继续使用 quota-router;但这是一次显式的新决策,不是 router 静默地无限换模型。 ### 5.4 Critical 策略 `precision === critical` 或 `fallbackMode === 'none'` 时: - 可以使用同模型 retry; - 可以在明确声明的同能力 deployment/provider 候选中故障转移; - 不自动降级到较低能力模型; - 候选耗尽后返回原始错误或 `manual-intervention-required`; - 质量问题交给上层验收和人工/Planner 处理。 因此,“不自动 fallback”在 critical 策略中应解释为: ```text 不自动 capability downgrade ≠ 禁止同能力故障转移 ``` ## 6. 错误码与边界行为 第一版只需要一组可回放、可由上层处理的稳定错误码: | 错误码 | 含义 | router 行为 | 上层动作 | | --- | --- | --- | --- | | `invalid-subtask-contract` | 缺少或无法验证契约身份 | 拒绝创建 lease | 补齐 SubtaskSpec | | `ambiguous-classification` | 分类冲突且策略要求人工确认 | 不自动选模型 | Planner/用户确认 | | `no-compatible-model` | 策略、允许集合和能力下限无交集 | 不越权选择 | 修改约束或显式升级 | | `lease-contract-mismatch` | 同一 subtask 复用不同契约 | 不修改旧 lease | 创建新 subtask | | `manual-intervention-required` | critical 候选耗尽或不可安全恢复 | 结束自动恢复 | 人工或上层处理 | 以下行为明确禁止: ```text 禁止用关键词变化覆盖 active lease 禁止用 quality-failed 直接推进 fallback 链 禁止用 live config 改写已有 lease 的候选顺序 禁止在 allowedModels 为空交集时静默扩大权限 禁止把新的 contractHash 写回旧 subtask lease ``` ## 7. 配置示例 ```yaml quota-router: modelPolicies: default: taskClass: default complexity: medium precision: normal capabilityFloor: balanced primary: opencode-go/mimo-v2.5 fallback: - token-share/gpt-5.6-luna fallbackMode: auto simple: taskClass: simple complexity: low precision: normal capabilityFloor: economy primary: opencode/mimo-v2.5-free fallback: - token-share/gpt-5.4-mini fallbackMode: auto coding: taskClass: coding complexity: medium precision: normal capabilityFloor: balanced primary: opencode-go/mimo-v2.5 fallback: - token-share/gpt-5.6-luna fallbackMode: auto coding-high: taskClass: coding complexity: high precision: high capabilityFloor: strong primary: opencode-go/mimo-v2.5 fallback: - token-share/gpt-5.6-terra fallbackMode: auto research-critical: taskClass: research complexity: high precision: critical capabilityFloor: critical primary: token-share/gpt-5.6-terra fallback: [] fallbackMode: none compatibility: legacyProfileMatching: true explicitSubtaskMetadataWins: true activeLeasePolicy: preserve ``` 本方案中的模型策略是模型选择层。现有 `sources`、`modelBySource`、cooldown、provider/model 原生校验等机制继续保留,作为底层可用性和资源策略;不在本方案中重新定义 source priority 或余额系统。 ## 8. 统计与收益评估 ### 8.1 quota-router 直接可统计的指标 ```text route_requests primary_selections primary_successes retry_count fallback_count fallback_recovery_count no_compatible_model_count manual_intervention_count model_requests model_tokens model_latency model_error_rate ``` 按以下维度聚合: ```text taskClass complexity precision policyId policyVersion model fallbackIndex ``` ### 8.2 上层可选回传的 outcome quota-router 不判断语义质量,但可以接受上层事件: ```ts interface RouteOutcome { taskId: string subtaskId: string leaseId?: string status: 'accepted' | 'quality-failed' | 'needs-replan' | 'failed' rework?: boolean reason?: string at: number } ``` 这样可以计算: ```text accepted_rate quality_failure_rate rework_rate fallback_accepted_rate resource_per_accepted_subtask ``` 但这些结果的判断权属于上层验收器,不属于 quota-router。 ### 8.3 收益的定义 第一阶段只报告可审计的路由效果: - 便宜 primary 的使用比例; - primary 直接完成比例; - fallback 是否真正恢复; - 不同策略的 token、延迟和错误率; - 相同验收标准下的 accepted outcome。 只有存在配对 baseline 时,才估算: ```text saving = baseline_resource_for_accepted_subtasks - router_resource_for_accepted_subtasks ``` “选择了便宜模型”不等于“产生了收益”;如果它带来更多返工,应该计入总成本。 ## 9. 事件模型 建议事件保持 append-only、结构化、轻量: ```text route-requested route-selected route-retried route-fallback route-completed route-failed route-outcome-linked ``` 最小字段: ```ts interface RouteTelemetry { eventId: string at: number taskId?: string subtaskId?: string turnId?: string | number leaseId?: string policyId?: string policyVersion?: string benchmarkVersion?: string contractVersion?: string contractHash?: string classificationConfidence?: number classificationSource?: 'user' | 'planner' | 'rule' | 'inferred' taskClass?: string complexity?: 'low' | 'medium' | 'high' precision?: 'normal' | 'high' | 'critical' model?: string candidateIndex?: number transition?: 'initial' | 'retry' | 'fallback' | 'complete' | 'failed' reason?: string failureCode?: string inputTokens?: number outputTokens?: number latencyMs?: number outcome?: 'accepted' | 'quality-failed' | 'needs-replan' | 'failed' } ``` 默认不记录 prompt 全文、reasoning 全文、完整工具参数或原始工具结果。调度遥测和主任务上下文分离:上层负责把子任务结果组织回主链路,router 只记录路由事实。 ## 10. 迁移计划 ### v0.2:显式模型策略 - 增加 `modelPolicies` 配置; - 增加 `taskClass`、`complexity`、`precision`; - 支持 `allowedModels`、`preferredModel`、`fallbackMode`; - 保留 legacy profile/关键词匹配; - 保留现有 source/provider 选择和错误恢复。 ### v0.2.1:最小契约与不确定性补丁 - 新接口要求 `contractVersion` 和 `contractHash`; - 增加 `requiredCapabilities` 的少量兼容性字段; - 增加 `classificationConfidence`、`classificationSource` 和 `uncertaintyPolicy`; - 增加 `invalid-subtask-contract`、`ambiguous-classification`、`lease-contract-mismatch` 错误码; - 保留 legacy 调用,但 legacy 不承诺 subtask 粘性。 ### v0.3:subtask 内模型锁定 - 接收 `taskId/subtaskId`; - 首次路由创建 `SubtaskModelLease`; - 后续 turn 继承 selected model; - fallback 只沿有序链前进; - active lease 默认不受 live config 回写影响; - 以 `taskId + subtaskId + contractHash` 保证幂等和契约稳定; - 明确 `active → retrying/fallback → active` 以及终态约束; - critical 只允许同能力故障转移,不允许自动 capability downgrade。 ### v0.4:统计闭环 - 记录 primary/retry/fallback/recovery; - 支持上层 `RouteOutcome` 回传; - 按 policyVersion 和任务维度聚合; - 支持 benchmark 策略与线上 accepted outcome 对照。 ### 暂不排期 - Planner 和自动任务拆解; - 主任务上下文合并和 context compaction; - 多 Harness 能力目录与跨 Harness 迁移; - Receipt/Ledger; - LLM Judge 和自动 replan; - 独立的价格、余额和长期经营账本。 ## 11. 最小验收矩阵 | 场景 | 预期行为 | 必须验证的约束 | | --- | --- | --- | | 同一 subtask 重复 route | 返回已有 active lease | 幂等,不创建第二个 primary | | 同一 subtask contractHash 改变 | 拒绝复用旧 lease | `lease-contract-mismatch` | | 缺少 contractVersion/hash | 拒绝新接口请求 | `invalid-subtask-contract` | | 低置信度 + manual | 不自动路由 | `ambiguous-classification` | | 瞬态错误 | 同模型有界 retry | 不改变 selectedModel | | 可恢复 quota/auth 错误 | 沿候选链 fallback | 只前进,不重排 | | critical 候选故障 | 同能力 failover 或人工介入 | 不能力降级 | | quality-failed | 不自动 fallback | 上层创建 repair/escalation | | live policy 更新 | active lease 不变 | 可回放、可复现 | 原有验收标准保留为实现检查项: 1. 给定显式 `taskClass/complexity/precision` 时,选择结果不依赖每轮关键词变化; 2. `allowedModels` 能正确限制最终候选集合; 3. 无候选交集时返回 `no-compatible-model`,不静默越权; 4. 同一个 subtask 的正常后续 turn 继续使用已选模型; 5. transient failure 先按既有策略 retry,超过阈值后才进入 fallback; 6. stable quota/auth failure 可以沿有序候选链前进; 7. fallback 不改变 `taskId/subtaskId`; 8. `precision=critical` 或 `fallbackMode=none` 时不自动降级; 9. `manual`/`emergency` 候选不被自动选择; 10. 路由事件能统计 primary、retry、fallback 和 recovery; 11. 上层 outcome 可关联到对应 lease 和 policyVersion; 12. 现有测试、检查和构建保持通过。 ## 12. 设计结论 `dsh-quota-router` 的最小正确定位是: > **已拆分子任务的能力约束模型路由器。** 它不需要知道完整任务图,也不需要替代 Planner。它只需要在明确的任务分类和模型策略上,做一次可解释的模型选择,并保护这个选择在 subtask 内的连续性;只有基础设施失败时,才沿兼容候选链进行有界恢复。 ```text SubtaskSpec → taskClass / complexity / precision → capability policy → ordered model candidates → model lease → bounded retry/fallback → route telemetry ```