把派发从「能力子集 + 空闲」升级为任务为中心的多维可解释打分匹配。 新增/改动: - orchestrator/dispatch_score.py(新):DispatchCandidate/DispatchScore + 加权掩码归一 打分(score_candidate/rank_candidates)+ build_dispatch_decision_event。 - orchestrator/main.py:抽出三模式共用的 finalize_dispatch;新增 scored_matchmake (ENABLE_DISPATCH_SCORE,默认关,与 ACO 择一)——为每个就绪任务在有能力的空闲 Agent 间按 capability/历史成功(τ)/负载/预算压力/权限择优,记录可解释决策;_run_budget_pressure 计算真实预算占比。 - orchestrator/swarm_runtime.py:SwarmRun.dispatch_decisions + record_dispatch_decision (内部状态,非 Manager 事件)。 - 测试:scripts/test-dispatch-score.py(公式 24 项)+ scripts/test-dispatch-scored.py (集成 11 项:按 τ/负载多 Agent 择优 + 排除原因 + 可回放记录);CI 纳入两者。 - docs/scheduling/dispatch-score-schema.md、CLAUDE.md 同步。 诚实边界:risk_score/estimated_cost/estimated_time 本仓无来源 → None 并在 payload uncollected_dimensions 披露(不伪造,规则 #9);dispatch.decision_made 暂为 Swarm 内部 记录,未进 Manager 事件契约(需 event-schema 注册,跨端)。flag 关闭时贪心/ACO 路径逐字节不变。 Closes #9 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
12 KiB
Dispatch 评分模型与 dispatch.decision_made 事件(Issue #9)
让派发从「仅按能力集合匹配」升级为「多维、可解释、加权」的调度评分:在能力之外引入负载、预算压力、风险、权限契合、历史质量、预估成本/时间等维度,并把每次派发的候选列表、被选 Agent、逐维度评分细分与排除原因,通过 dispatch.decision_made 事件暴露出来。本文档是该评分层的唯一入口。
实现:orchestrator/dispatch_score.py(纯评分)+ orchestrator/main.py: scored_matchmake/finalize_dispatch(已接入派发环,门控 ENABLE_DISPATCH_SCORE,默认关)。测试:scripts/test-dispatch-score.py(公式)+ scripts/test-dispatch-scored.py(集成)。详见 §4。
1. 与 #10(τ/η/P 决策)的边界
#9 与 #10(orchestrator/decision_engine.py,已实现)耦合但分工明确:
| Issue | 产出 | 性质 |
|---|---|---|
| #10 | 把历史归一为 τ、把先验归一为 η,按 P=τ^α·η^β/Σ 概率采样一个任务(ε-greedy),并产出可回放的内部 DecisionTrace |
概率决策、含 RNG、内部状态(不进 Manager 事件流) |
| #9(本文) | 候选 Agent×任务的可解释评分输入(capability_match / load / budget_pressure / risk_score / permission_fit / historical_success / estimated_cost / estimated_time)+ 每次派发的候选列表与排除原因(dispatch.decision_made 事件) |
确定性(无 RNG)、可审计、面向 Manager |
- 共享输入:
historical_success直接复用 #10 的 τ trail(τ 是跨 run 学到的「实测声誉」,已在[0.05, 1.0]),经normalize_tau归一到[0,1]。capability_match、load与DecisionEngine.compute_eta的match/resource同定义,保证两层对「能力契合」「负载」理解一致。 dispatch_score.py模块本身是纯计算、无 RNG、不发事件。选择行为由main.py的集成提供:开ENABLE_DISPATCH_SCORE时,scored_matchmake用本评分确定性地在多个候选 Agent 中择优(任务为中心,与 #10 的概率采样互斥、择一启用)。即 #9 = 确定性可解释打分匹配;#10 = 概率采样。两者都默认关,关闭时为原贪心派发。
2. 评分模型
2.1 维度与权重(DISPATCH_WEIGHTS)
| 维度 | 权重 | 方向 | 含义 / 归一来源 |
|---|---|---|---|
capability_match |
0.30 | 收益 | required ∩ caps 的 Jaccard(奖励专精);无 required → 1.0(任何人可做,对齐 can_agent_run_task) |
historical_success |
0.20 | 收益 | 复用 #10 的 τ trail,normalize_tau 映射到 [0,1] |
load |
0.15 | 收益 | 空闲槽位余量(越空越高),对齐 AGENT_SLOTS / RESOURCE_NORM_SLOTS |
permission_fit |
0.12 | 收益 | Agent 是否被允许执行该任务(1.0 允许 / 0.0 不允许) |
budget_pressure |
0.10 | 成本 | run 预算已消耗比例(越高越扣分) |
risk_score |
0.08 | 成本 | 任务风险(越高越扣分) |
estimated_cost |
0.03 | 成本 | 预估 $(越高越扣分) |
estimated_time |
0.02 | 成本 | 预估墙钟(越高越扣分) |
- 权重显式、可审计,不必和为 1.0——评分时按「实际有信号的维度」重归一(见 §2.2)。
- 成本维度(
budget_pressure/risk_score/estimated_cost/estimated_time)的贡献取(1 − 值),即「越高越差」;其余为收益维度,贡献取原值。
2.2 打分公式(掩码 + 重归一)
total = Σ_{d∈present} w_d · contribution_d / Σ_{d∈present} w_d
contribution_d = (1 − x_d) if d 是成本维度
= x_d 否则 (x_d 夹紧到 [0,1])
- 值为
None的维度同时退出分子与权重归一(masked + renormalized),因此「未采集」的信号既不加分也不扣分,total始终落在[0,1]。这与benchmark.metrics.quality_score的掩码纪律一致(规则 #9:绝不为未采集量伪造 0/1)。 - 权重为 0 的维度仍写入
breakdown(透明),但不参与计分。
2.3 数据结构
DispatchCandidate:一个 (agent, task) 配对的原始信号。除标识字段外每个字段都是评分输入;无信号的维度必须留None,不得伪造。score由score_candidate()回填。DispatchScore:total:重归一加权总分([0,1]);breakdown:每个维度的原始值(或None),供审计;weights_used:实际施加到各「在场」维度的重归一权重;missing:本次无信号的维度名列表(显式列出「未采集」集合,而非静默省略)。
2.4 函数
| 函数 | 职责 |
|---|---|
normalize_capability_match(required, caps) |
Jaccard;无 required → 1.0 |
normalize_tau(tau) |
τ → [0,1];None → None |
normalize_load(free_slots) |
空闲槽位 → [0,1];None → None |
score_candidate(candidate) |
纯函数打分(无 RNG),回填 candidate.score |
rank_candidates(candidates) |
全部打分并按 total 降序(稳定排序,决定性) |
build_dispatch_decision_event(candidates, chosen, excluded_reasons, task_id=...) |
构造 dispatch.decision_made 事件 payload |
3. dispatch.decision_made 事件
3.1 payload 结构
{
"task_id": "swarm-X-impl", // 与 envelope 顶层 task_id 一致
"chosen_agent_id": "agent-A", // 被选 Agent(无派发时为 null)
"chosen_task_id": "swarm-X-impl",
"candidate_count": 3,
"candidates": [ // 全部候选 + 逐维度细分
{
"agent_id": "agent-A",
"task_id": "swarm-X-impl",
"agent_role": "implementation",
"score": {
"total": 0.83,
"breakdown": { // 原始值(或 null)
"capability_match": 1.0,
"historical_success": 0.95,
"load": 1.0,
"permission_fit": 1.0,
"budget_pressure": 0.10,
"risk_score": null,
"estimated_cost": null,
"estimated_time": null
},
"weights_used": { "capability_match": 0.30, "historical_success": 0.20, "load": 0.15, "permission_fit": 0.12, "budget_pressure": 0.10 },
"missing": ["risk_score", "estimated_cost", "estimated_time"]
}
}
// ... 其余候选
],
"excluded": { // agent_id → 排除原因(人读)
"agent-B": "lower_score",
"agent-C": "capability_mismatch"
},
"weights": { /* DISPATCH_WEIGHTS 快照 */ },
"uncollected_dimensions": ["estimated_cost", "estimated_time", "risk_score"]
}
3.2 Envelope 与事件流约定
- 本 payload 由 orchestrator 的
swarm_runtime.emit_event(run, "dispatch.decision_made", task_id=..., payload=...)包裹,自动补齐event_id/swarm_id/occurred_at/correlation_id/source等标准 envelope 字段(见docs/integration/event-schema.md §2)。 - 该事件类型当前不在
event-schema.md §4的 HM 注册表中,属于 Swarm 内部可观测扩展。是否需要 HM 入库/前端展示需与 Manager 侧对齐——在对齐前,事件只进 Swarm 本地事件流,不应宣称已被 HM 校验/消费(见 §5)。
4. 集成现状(已落地)
已接入
orchestrator/main.py(issue #9 关闭 PR)。下述为实际实现,非提案。
启用开关 ENABLE_DISPATCH_SCORE(默认关,与 ENABLE_ACO_DISPATCH 择一)。开启后,task_dispatch_loop 在每个 tick 走 scored_matchmake(...) 任务为中心的匹配,而非 Agent 拉取:
- 枚举所有就绪 PENDING 任务(按
created_at稳定排序)。 - 对每个任务,遍历有能力且有容量的空闲 Agent,构造
DispatchCandidate,信号取自既有状态:capability_match=normalize_capability_match(required, caps)(Jaccard)historical_success=normalize_tau(decision_engine.get_tau(role, agent))(复用 #10 的 τ trail)load=normalize_load(AGENT_SLOTS[agent])(空闲槽位)permission_fit= 1.0(can_agent_run_task通过;不通过的 Agent 直接以capability_mismatch进excluded,不参与排序)budget_pressure= 真实计算_run_budget_pressure(task)(run 已消耗成本 /max_cost_usd;无预算或无用量 → None)risk_score/estimated_cost/estimated_time= None(本仓无来源,见 §5,payloaduncollected_dimensions披露)
rank_candidates(...)确定性择优;落选 Agent 记lower_score,无能力 Agent 记capability_mismatch。- 选中对经
finalize_dispatch(agent, task, dispatch_event=...)(三种派发模式共用的统一收尾)完成 assign +task.claimed+ 派发上下文 + 下发;同一 tick 内被选 Agent 不重复分配。 dispatch.decision_madepayload(候选明细 + 排除原因)经swarm_runtime.record_dispatch_decision存入SwarmRun.dispatch_decisions(内部状态,可审计/回放)。
刻意不发 Manager 事件:
dispatch.decision_made暂为 Swarm 内部可观测记录,未经swarm_runtime.emit_event进入回调流——避免污染未注册的 Manager 事件契约。若 HM 需消费,需先在docs/integration/event-schema.md §4注册(跨端,超出 #9 范围)。build_dispatch_decision_event与dispatch_score_event_enabled()已备好,供注册后启用 Manager 侧发送。行为隔离:
ENABLE_DISPATCH_SCORE关闭时,贪心/ACO 路径逐字节不变(共用的finalize_dispatch保持原语义)。测试:
scripts/test-dispatch-score.py(纯公式 24 项)+scripts/test-dispatch-scored.py(集成:按 τ/负载在多 Agent 中择优 + 记录可回放,11 项)。运行命令见各测试 docstring 与docs/TESTING.md。
5. 诚实边界(本仓未采集 / 未做的)
严格遵循规则 #9,以下维度在本仓无真实来源,一律以 None("not collected")表示,绝不伪造数值;并通过 payload 的 uncollected_dimensions 显式披露:
estimated_cost/estimated_time:本仓无「派发前」成本/时长预估器。模型成本与runtime_seconds只在任务完成后经emit_usage_event得到(事后量),派发时不可得 →None。risk_score:本仓无每任务风险分类器。风险/审批(risk_level)归属 Manager 审批链,不在队列Task上 →None。- region / GPU / 硬件契合:
AgentMetadata无任何此类字段(仅agent_id/status/last_heartbeat/capabilities/current_task_id)→ 不引入该维度(不造假维度)。 budget_pressure:理论上可由 run 预算与已消耗比例算出,但派发前缺少可靠的「run 级已消耗」累计来源(usage 事件按任务事后发出),故集成示例中留None;若后续聚合 run 级已消耗,可填真实比例。permission_fit:当前仅能用「能力子集」近似(can_agent_run_task)。真正的 RBAC/审批级权限校验在 Manager 侧,Swarm 派发面无此信号——本维度是「能力可行性」的近似,不是完整权限判定。- 决策质量未证:本评分维度/权重是否能产出更优派发是经验命题,需 Group C 的对比 harness 才能回答;在此之前,本评分层默认不发事件(
ENABLE_DISPATCH_SCORE_EVENT关)、不改派发行为。
6. 文件
| 文件 | 职责 |
|---|---|
orchestrator/dispatch_score.py |
DispatchCandidate/DispatchScore 数据结构、归一函数、score_candidate/rank_candidates、build_dispatch_decision_event |
scripts/test-dispatch-score.py |
24 项断言(归一器、同能力不同代价/负载/历史 → 不同分且可解释、≥4 个非能力维度各自影响评分、None 掩码不扣分、事件携带候选细分 + 排除原因 + 未采集披露) |
docs/scheduling/dispatch-score-schema.md |
本文档 |