# ============================================================ # 参数优化闭环(moduleId: core-param-opt, 可重生 ✅) # plan.md §9.8:候选参数 → 黄金算例集回放(沙盒重排)→ 灰度(受限生效) # → 验证 → 效果退化自动回滚;与 domain-sensitivity(SC-06)衔接,用其 # Tornado 产出推导候选方向。 # 边界说明:本模块是库层领域操作(矩阵 87 行)。真实生产写入须经门禁 # harness P2 审批(本模块不接网关),但所有写操作均留 before/after 证据, # 供调用方(harness/gateway)串进证据链。全部评测跑沙盒深拷贝,不污染主干。 # ============================================================ from __future__ import annotations import random import time import uuid from copy import deepcopy from typing import Any, ClassVar from server.aps_domain.params import get_schedule_params from server.aps_domain.sensitivity import _run_sandbox, run_sensitivity from server.timeutil import add_minutes, fmt_date, today0 World = dict[str, Any] # 默认随机种子(训练/验证集切分与复现) DEFAULT_SEED = 20260802 # 默认退化容忍度:候选 KPI 相对基线恶化超过该比例即判定退化 DEFAULT_TOLERANCE = 0.02 # 默认连续劣化阈值:线上观测连续 N 次劣化即自动回滚实验(矩阵 87) DEFAULT_MAX_CONSECUTIVE_DEGRADED = 2 # RULE 引擎策略模板(候选参数可调) STRATEGIES = ( "COMPREHENSIVE", "DELIVERY_FIRST", "FIFO", "CAPACITY_BALANCE", "CHANGEOVER_MIN", "COST_FIRST", "CAMPAIGN", ) # 候选参数取值范围(越界即拒绝,防止把排产参数调出物理合理域) _CANDIDATE_BOUNDS: dict[str, tuple[float, float]] = { "planningHorizonDays": (1.0, 90.0), "deliveryBufferRatio": (0.5, 2.0), "freezeWindowHours": (0.0, 168.0), "efficiencyScale": (0.5, 1.5), } _LEVELS = ("VIP", "A", "B", "C") def _default_start() -> str: """评测起点(与敏感性分析一致:明天 0 点)。""" return fmt_date(add_minutes(today0(), 24 * 60)) def _is_degraded(baseline_kpi: dict[str, float], candidate_kpi: dict[str, float], tolerance: float) -> bool: """退化判定:主 KPI 为总延期;基线零延期时任何明显延期即退化。""" base = float((baseline_kpi or {}).get("tardiness") or 0.0) cand = float((candidate_kpi or {}).get("tardiness") or 0.0) if base <= 1e-9: return cand > tolerance return cand > base * (1.0 + tolerance) def _scope_matches(exp_scope: dict[str, Any], query_scope: dict[str, Any] | None) -> bool: """受限生效判定:实验作用域与查询作用域按维度求交集。 - 实验作用域为空 → 全局生效(任何查询都命中) - 查询作用域为空 → 只命中全局实验(不把受限实验扩散到全量) - 否则逐维度(lineIds/orderIds/customerLevels)要求有交集 """ if not exp_scope: return True if not query_scope: return False for key, values in exp_scope.items(): qv = query_scope.get(key) if not qv: return False if not set(values) & set(qv): return False return True class ParameterOptimizer: """参数优化闭环:训练/验证集隔离 → 回放 → 灰度 → 验证 → 退化回滚。""" def __init__(self, *, seed: int = DEFAULT_SEED, tolerance: float = DEFAULT_TOLERANCE, strategy: str = "COMPREHENSIVE", max_consecutive_degraded: int = DEFAULT_MAX_CONSECUTIVE_DEGRADED) -> None: if strategy not in STRATEGIES: raise ValueError(f"strategy 须为 {STRATEGIES}") self.seed = int(seed) self.tolerance = float(tolerance) self.strategy = strategy self.max_consecutive_degraded = max(1, int(max_consecutive_degraded)) # ---------------- 训练/验证集隔离 ---------------- def split_orders(self, world: World, *, train_ratio: float = 0.7, seed: int | None = None) -> dict[str, Any]: """把可排订单切成互斥的训练/验证子集(确定性种子,可复现)。 隔离意义:回放选参只看训练子集,验证子集是"没见过的订单", 防止候选参数过拟合到特定订单集(§9.8 黄金算例集回放评测口径)。 """ ids = [so["id"] for so in world.get("salesOrders") or [] if so.get("status") not in ("CANCELLED", "COMPLETED")] if not ids: raise ValueError("世界中没有可排订单,无法切分训练/验证集") rng = random.Random(int(seed) if seed is not None else self.seed) shuffled = ids[:] rng.shuffle(shuffled) n = round(len(shuffled) * train_ratio) n = max(1, min(n, len(shuffled) - 1)) # 保证验证集至少 1 单(多单时) return { "train": sorted(shuffled[:n]), "validation": sorted(shuffled[n:]), "trainRatio": train_ratio, "seed": int(seed) if seed is not None else self.seed, } # ---------------- 候选参数校验与映射 ---------------- def _validate_candidate(self, candidate: dict[str, Any]) -> dict[str, Any]: """校验候选参数(键白名单 + 值域),返回规范化副本。""" if not isinstance(candidate, dict) or not candidate: raise ValueError("候选参数必须是非空 dict") out: dict[str, Any] = {} for key, value in candidate.items(): if key == "customerLevelWeights": if not isinstance(value, dict) or not value: raise ValueError("customerLevelWeights 必须是非空 dict") levels: dict[str, float] = {} for k, v in value.items(): lv = str(k).upper() if lv not in _LEVELS: raise ValueError(f"不支持的客户等级:{k}") try: num = float(v) except (TypeError, ValueError) as exc: raise ValueError(f"{lv} 权重必须是数字") from exc if num < 0 or num > 100: raise ValueError(f"{lv} 权重须在 0~100") levels[lv] = num out["customerLevelWeights"] = levels elif key == "strategy": if str(value).upper() not in STRATEGIES: raise ValueError(f"strategy 须为 {STRATEGIES}") out["strategy"] = str(value).upper() elif key in _CANDIDATE_BOUNDS: try: num = float(value) except (TypeError, ValueError) as exc: raise ValueError(f"{key} 必须是数字") from exc lo, hi = _CANDIDATE_BOUNDS[key] if num < lo or num > hi: raise ValueError(f"{key} 须在 {lo}~{hi}") out[key] = int(num) if key == "planningHorizonDays" else num else: raise ValueError(f"不支持的候选参数键:{key}") return out def _candidate_to_kwargs(self, candidate: dict[str, Any]) -> dict[str, Any]: """候选参数 → 沙盒重排 kwargs(对齐 domain-sensitivity 的 _run_sandbox)。""" kw: dict[str, Any] = {} if "planningHorizonDays" in candidate: kw["horizon"] = int(candidate["planningHorizonDays"]) if "deliveryBufferRatio" in candidate: kw["delivery_buffer"] = float(candidate["deliveryBufferRatio"]) if "freezeWindowHours" in candidate: kw["freeze_hours"] = float(candidate["freezeWindowHours"]) if "efficiencyScale" in candidate: kw["efficiency_scale"] = float(candidate["efficiencyScale"]) if "customerLevelWeights" in candidate: kw["vip_weight"] = float(candidate["customerLevelWeights"].get("VIP", 3.0)) if "strategy" in candidate: kw["strategy"] = candidate["strategy"] return kw # ---------------- 回放(沙盒重排) ---------------- def _sandbox_kpi(self, world: World, *, order_ids: list[int] | None = None, strategy: str | None = None, candidate: dict[str, Any] | None = None) -> dict[str, float]: """深拷贝沙盒内按候选参数重排,返回 KPI(不写主干;PO/WO 清空重排)。""" sandbox = deepcopy(world) if order_ids: wanted = set(order_ids) sandbox["salesOrders"] = [so for so in sandbox["salesOrders"] if so["id"] in wanted] sp = get_schedule_params(world) kwargs: dict[str, Any] = { "strategy": strategy or self.strategy, "horizon": int(sp.get("planningHorizonDays") or 14), "vip_weight": float((sp.get("customerLevelWeights") or {}).get("VIP") or 3.0), "delivery_buffer": float(sp.get("deliveryBufferRatio") or 0.95), "freeze_hours": float(sp.get("freezeWindowHours") or 0.0), "efficiency_scale": float(sp.get("efficiencyScale") or 1.0), } if candidate: kwargs.update(self._candidate_to_kwargs(candidate)) return _run_sandbox(sandbox, start=_default_start(), **kwargs) def replay(self, world: World, candidate: dict[str, Any], *, order_ids: list[int] | None = None, strategy: str | None = None) -> dict[str, Any]: """候选参数回放(沙盒重排):基线 vs 候选 KPI 对照,含退化标记。 幂等、无副作用:任何情况下都不修改传入的 world。 """ candidate = self._validate_candidate(candidate) baseline = self._sandbox_kpi(world, order_ids=order_ids, strategy=strategy) cand_kpi = self._sandbox_kpi(world, order_ids=order_ids, strategy=strategy, candidate=candidate) degraded = _is_degraded(baseline, cand_kpi, self.tolerance) return { "candidate": candidate, "orderIds": sorted(order_ids) if order_ids else "all", "baselineKpi": baseline, "candidateKpi": cand_kpi, "deltas": {k: round(float(cand_kpi.get(k, 0)) - float(baseline.get(k, 0)), 4) for k in ("tardiness", "conflicts", "utilization", "changeoverMin")}, "degraded": degraded, "improved": not degraded, "sandbox": True, } # ---------------- 与敏感性分析衔接 ---------------- # 敏感性 Tornado 行只回传 low/high 的 KPI(不携带 kwargs),候选取值由本表 # 按因子对齐 SC-06 的扰动档位;planningHorizonDays 依 baselineValue 推导。 _FACTOR_LEVELS: ClassVar[dict[str, dict[str, Any]]] = { "planningHorizonDays": {"low": lambda b: max(3, int(b) // 2), "high": lambda b: min(60, int(b) * 2)}, "vipWeight": {"low": 1.0, "high": 10.0}, "deliveryBufferRatio": {"low": 0.85, "high": 1.0}, "freezeWindowHours": {"low": 0.0, "high": 48.0}, "lineEfficiency": {"low": 0.8, "high": 1.2}, } _FACTOR_CANDIDATE_KEYS: ClassVar[dict[str, str]] = { "planningHorizonDays": "planningHorizonDays", "vipWeight": "customerLevelWeights", "deliveryBufferRatio": "deliveryBufferRatio", "freezeWindowHours": "freezeWindowHours", "lineEfficiency": "efficiencyScale", } def candidates_from_sensitivity(self, report: dict[str, Any], *, limit: int = 5) -> list[dict[str, Any]]: """把敏感性 Tornado 产出转成候选参数(按摆幅降序,取改善方向)。 对每个因子选 tardiness 更低的那一档作为候选方向;摆幅为 0 的 因子(无影响)不产生候选。返回 [{candidate, factorId, swing, direction}]。 """ out: list[dict[str, Any]] = [] for row in report.get("rows") or []: factor_id = row.get("factorId") swing = float(row.get("swing") or 0.0) if not factor_id or swing <= 1e-9: continue low_tard = float((row.get("low") or {}).get("tardiness") or 0.0) high_tard = float((row.get("high") or {}).get("tardiness") or 0.0) direction = "high" if high_tard < low_tard else "low" level = self._FACTOR_LEVELS.get(factor_id) if level is None: continue spec = level[direction] value = spec(float(row.get("baselineValue") or 0)) if callable(spec) else spec key = self._FACTOR_CANDIDATE_KEYS[factor_id] candidate = {key: {"VIP": float(value)} if key == "customerLevelWeights" else value} out.append({ "candidate": candidate, "factorId": factor_id, "swing": round(swing, 2), "direction": direction, "baselineValue": row.get("baselineValue"), }) out.sort(key=lambda x: x["swing"], reverse=True) return out[:max(1, int(limit))] # ---------------- 灰度(受限生效) ---------------- def promote_gray(self, world: World, candidate: dict[str, Any], scope: dict[str, Any] | None = None, *, order_ids: list[int] | None = None, strategy: str | None = None, require_replay_pass: bool = True) -> dict[str, Any]: """候选参数灰度放行:先回放(默认要求训练集不退化),再受限生效。 - require_replay_pass=True(默认):训练集回放退化即拒绝,不写世界。 - require_replay_pass=False:金丝雀场景,允许受限范围试错; 退化风险由 finalize 的验证回放兜底(效果退化自动回滚)。 灰度只写 paramExperiments 记录(作用域受限),不动 scheduleParams; 全量生效在 finalize(promote_full=True) 阶段完成。 """ candidate = self._validate_candidate(candidate) if order_ids is None: split = self.split_orders(world) else: split = {"train": sorted(set(order_ids)), "validation": [], "seed": None} train_replay = self.replay(world, candidate, order_ids=split["train"], strategy=strategy) if require_replay_pass and train_replay["degraded"]: return {"accepted": False, "reason": "replay-degraded", "replay": train_replay} experiment: dict[str, Any] = { "id": "px-" + uuid.uuid4().hex[:8], "candidate": candidate, "scope": dict(scope or {}), "status": "GRAY", "strategy": strategy or self.strategy, "tolerance": self.tolerance, "createdAt": time.strftime("%Y-%m-%d %H:%M:%S"), "split": split, "trainReplay": train_replay, "prevParams": deepcopy(world.get("scheduleParams")), "baselineParams": get_schedule_params(world), "evidence": { "sandbox": True, "note": "回放证据见 trainReplay/validationReplay;" "生效写入应经门禁 harness P2 审批后调用(矩阵 87 行)。", }, } world.setdefault("paramExperiments", []).append(experiment) return {"accepted": True, "experiment": experiment} def get_experiment(self, world: World, experiment_id: str) -> dict[str, Any] | None: for rec in world.get("paramExperiments") or []: if rec.get("id") == experiment_id: return rec return None def active_params(self, world: World, scope: dict[str, Any] | None = None) -> dict[str, Any]: """受限生效的参数投影:scheduleParams + 命中作用域的灰度/生效实验。 查询无作用域时只返回基线参数(受限实验不扩散到全量查询)。 """ out = dict(get_schedule_params(world)) for rec in world.get("paramExperiments") or []: if rec.get("status") not in ("GRAY", "ACTIVE", "FULL"): continue if not _scope_matches(rec.get("scope") or {}, scope): continue cand = rec.get("candidate") or {} if "customerLevelWeights" in cand: out["customerLevelWeights"] = { **(out.get("customerLevelWeights") or {}), **cand["customerLevelWeights"], } out.update({k: v for k, v in cand.items() if k != "customerLevelWeights"}) return out # ---------------- 验证 / 全量生效 / 退化回滚 ---------------- def finalize(self, world: World, experiment_id: str, *, promote_full: bool = True) -> dict[str, Any]: """灰度收口:在**验证集**(held-out)回放,退化自动回滚,否则生效。 - 验证回放退化 → status=ROLLED_BACK(效果退化自动回滚,基线参数不动)。 - 验证通过 → status=ACTIVE;promote_full=True 时写入 scheduleParams (status=FULL),并留存 before/after 证据。 """ rec = self.get_experiment(world, experiment_id) if rec is None: raise ValueError(f"未找到参数实验:{experiment_id}") if rec.get("status") != "GRAY": raise ValueError(f"实验状态为 {rec.get('status')},只能从 GRAY 收口") split = rec.get("split") or {} val_ids = split.get("validation") or split.get("train") or [] validation_replay = self.replay(world, rec["candidate"], order_ids=val_ids or None, strategy=rec.get("strategy")) rec["validationReplay"] = validation_replay if _is_degraded(validation_replay["baselineKpi"], validation_replay["candidateKpi"], self.tolerance): rec["status"] = "ROLLED_BACK" rec["rolledBackAt"] = time.strftime("%Y-%m-%d %H:%M:%S") rec["rollbackReason"] = "validation-degraded" return dict(rec) rec["status"] = "ACTIVE" rec["activatedAt"] = time.strftime("%Y-%m-%d %H:%M:%S") # 线上观测基线:激活时取最近一次生产观测(无观测则首个观测自校准,矩阵 87) prior_obs = world.get("paramObservations") or [] rec["productionBaseline"] = deepcopy(prior_obs[-1]["kpi"]) if prior_obs else None rec["degradedStreak"] = 0 if promote_full: full = self._apply_full(world, rec["candidate"]) rec["status"] = "FULL" rec["appliedAt"] = time.strftime("%Y-%m-%d %H:%M:%S") rec["fullApply"] = full return dict(rec) def rollback(self, world: World, experiment_id: str, *, reason: str = "manual") -> dict[str, Any]: """显式回滚:若已全量写入 scheduleParams,恢复上一参数版本。""" rec = self.get_experiment(world, experiment_id) if rec is None: raise ValueError(f"未找到参数实验:{experiment_id}") if rec.get("status") == "FULL": self._restore_params(world, rec.get("prevParams")) rec["status"] = "ROLLED_BACK" rec["rolledBackAt"] = time.strftime("%Y-%m-%d %H:%M:%S") rec["rollbackReason"] = reason return dict(rec) def history(self, world: World) -> list[dict[str, Any]]: return [dict(rec) for rec in world.get("paramExperiments") or []] # ---------------- 候选阶段(split→回放→候选,网关 /api/params/optimize) ---------------- def propose(self, world: World, candidates: list[dict[str, Any]] | None = None, *, scope: dict[str, Any] | None = None, strategy: str | None = None, limit: int | None = None) -> dict[str, Any]: """候选提案:训练/验证集隔离 → 候选回放 → GRAY 实验(不写主干参数)。 与 run_pipeline 的分工:本方法只做灰度提案,升级(GRAY→ACTIVE/FULL) 由网关 P2 门禁确认卡触发(execute_confirmed 内 finalize)。默认候选 来自敏感性分析 Tornado 的改善方向;也支持显式传入候选参数。 """ if candidates is None: report = run_sensitivity(world, strategy=strategy or self.strategy) rows = (self.candidates_from_sensitivity(report, limit=limit) if limit is not None else self.candidates_from_sensitivity(report)) candidates = [c["candidate"] for c in rows] results: list[dict[str, Any]] = [] for cand in candidates: step = self.promote_gray(world, cand, scope=scope, strategy=strategy) if not step["accepted"]: results.append({"candidate": cand, "stage": "rejected", "reason": step["reason"], "replay": step.get("replay")}) continue exp = step["experiment"] results.append({"candidate": cand, "stage": "gray", "experimentId": exp["id"], "replay": exp.get("trainReplay")}) return { "results": results, "summary": { "total": len(results), "gray": sum(1 for r in results if r["stage"] == "gray"), "rejected": sum(1 for r in results if r["stage"] == "rejected"), }, } # ---------------- 线上观测驱动回滚监控(矩阵 87) ---------------- def record_observation(self, world: World, kpi: dict[str, Any], *, source: str = "production", actor: str = "param-observer", max_consecutive_degraded: int | None = None, next_id=None) -> dict[str, Any]: """线上生产 KPI 观测回调:观测数据落盘 + 连续劣化自动回滚。 - 观测数据追加到 world["paramObservations"](append-only)。 - 对每个 ACTIVE/FULL 实验:对比生效前基线(productionBaseline, finalize 时取最近一次观测;无观测则首个观测自校准), 用 _is_degraded(tolerance 可配置)判定单次劣化。 - 连续劣化次数达到 max_consecutive_degraded(默认 2)自动回滚 该实验(恢复基线参数版本),并写审计 param.experiment.auto_rolled_back。 - 非劣化观测清零连败计数。 next_id:审计发号器(通常传 store.next_id;缺省按 auditEvents 长度自增)。 返回 {observation, autoRolledBack, degraded};调用方随后负责 save()。 """ if not kpi: raise ValueError("观测 KPI 不能为空") kpi = {k: float(v) for k, v in kpi.items()} threshold = max( 1, int(max_consecutive_degraded if max_consecutive_degraded is not None else self.max_consecutive_degraded)) at = time.strftime("%Y-%m-%d %H:%M:%S") obs_id = "po-" + uuid.uuid4().hex[:8] evaluations: list[dict[str, Any]] = [] auto_rolled_back: list[str] = [] degraded_now: list[str] = [] for rec in world.get("paramExperiments") or []: if rec.get("status") not in ("ACTIVE", "FULL"): continue exp_id = str(rec.get("id") or "") baseline = rec.get("productionBaseline") if baseline is None: baseline = dict(kpi) # 首个观测自校准为生效前基线 rec["productionBaseline"] = baseline degraded = _is_degraded(baseline, kpi, self.tolerance) streak = (int(rec.get("degradedStreak") or 0) + 1) if degraded else 0 rec["degradedStreak"] = streak rolled_back = False if degraded and streak >= threshold: rolled_back = True if rec.get("status") == "FULL": self._restore_params(world, rec.get("prevParams")) rec["status"] = "ROLLED_BACK" rec["rolledBackAt"] = at rec["rollbackReason"] = "production-degraded" rec["autoRollback"] = { "observationId": obs_id, "degradedStreak": streak, "at": at, "source": source, } auto_rolled_back.append(exp_id) self._audit_auto_rollback( world, exp_id, obs_id, baseline, kpi, streak, actor=actor, source=source, next_id=next_id) elif degraded: degraded_now.append(exp_id) evaluations.append({ "experimentId": exp_id, "status": rec.get("status"), "baselineKpi": baseline, "degraded": degraded, "degradedStreak": streak, "autoRolledBack": rolled_back, }) observation = { "id": obs_id, "at": at, "source": source, "kpi": kpi, "evaluations": evaluations, } world.setdefault("paramObservations", []).append(observation) return { "observation": observation, "autoRolledBack": auto_rolled_back, "degraded": degraded_now, } @staticmethod def _audit_auto_rollback(world: World, experiment_id: str, observation_id: str, baseline: dict[str, Any], kpi: dict[str, Any], streak: int, *, actor: str, source: str, next_id=None) -> None: """自动回滚审计(param.experiment.auto_rolled_back,WORLD_WRITE/P1)。""" from server.agent_core.audit import write_audit if next_id is None: next_id = lambda _kind: len(world.get("auditEvents") or []) + 1 write_audit( world, next_id, actor=actor, category="WORLD_WRITE", action="param.experiment.auto_rolled_back", target={"type": "PARAM_EXPERIMENT", "id": experiment_id}, power="P1", rationale={ "observationId": observation_id, "source": source, "baselineKpi": baseline, "kpi": kpi, "degradedStreak": streak, "reason": "production-degraded", }, ) # ---------------- 内部写操作(留 before/after 证据) ---------------- def _apply_full(self, world: World, candidate: dict[str, Any]) -> dict[str, Any]: """把候选参数全量写入 scheduleParams(仅 finalize 验证通过后调用)。""" before = deepcopy(get_schedule_params(world)) sp = world.setdefault("scheduleParams", {}) if "customerLevelWeights" in candidate: sp["customerLevelWeights"] = { **(sp.get("customerLevelWeights") or {}), **candidate["customerLevelWeights"], } for key, value in candidate.items(): if key != "customerLevelWeights": sp[key] = value return {"before": before, "after": get_schedule_params(world)} @staticmethod def _restore_params(world: World, prev_params: dict[str, Any] | None) -> None: """恢复到上一参数版本(prevParams=None 表示此前为纯默认)。""" if prev_params is None: world.pop("scheduleParams", None) else: world["scheduleParams"] = deepcopy(prev_params) # ---------------- 一键闭环(可选入口) ---------------- def run_pipeline(self, world: World, candidates: list[dict[str, Any]] | None = None, *, scope: dict[str, Any] | None = None, strategy: str | None = None) -> dict[str, Any]: """一键闭环:无候选时先跑敏感性分析取方向,逐候选 灰度→收口。""" if candidates is None: report = run_sensitivity(world, strategy=strategy or self.strategy) candidates = [c["candidate"] for c in self.candidates_from_sensitivity(report)] results: list[dict[str, Any]] = [] for cand in candidates: step = self.promote_gray(world, cand, scope=scope, strategy=strategy) if not step["accepted"]: results.append({"candidate": cand, "stage": "rejected", "reason": step["reason"]}) continue rec = self.finalize(world, step["experiment"]["id"], promote_full=True) results.append({"candidate": cand, "stage": rec["status"].lower(), "experimentId": rec["id"]}) summary = { "total": len(results), "rejected": sum(1 for r in results if r["stage"] == "rejected"), "rolledBack": sum(1 for r in results if r["stage"] == "rolled_back"), "active": sum(1 for r in results if r["stage"] in ("active", "full")), } return {"results": results, "summary": summary} def optimize(world: World, candidates: list[dict[str, Any]] | None = None, *, seed: int = DEFAULT_SEED, tolerance: float = DEFAULT_TOLERANCE, strategy: str = "COMPREHENSIVE", scope: dict[str, Any] | None = None) -> dict[str, Any]: """函数式入口:默认使用敏感性分析产出作为候选方向(矩阵 87 行)。""" return ParameterOptimizer(seed=seed, tolerance=tolerance, strategy=strategy).run_pipeline( world, candidates=candidates, scope=scope, strategy=strategy) def _candidate_summary(candidate: dict[str, Any]) -> str: """候选参数人可读摘要(确认卡用)。""" parts: list[str] = [] for key, value in (candidate or {}).items(): if key == "customerLevelWeights": parts.append("customerLevelWeights=" + ",".join( f"{k}:{v}" for k, v in value.items())) else: parts.append(f"{key}={value}") return " ".join(parts) or "(无)" def confirmation_for_param_promote(experiment: dict[str, Any]) -> tuple[str, list[str]]: """参数实验升级确认卡文案:实验摘要 + 候选参数 + 回放 KPI(矩阵 87 P2 门禁)。""" cand = experiment.get("candidate") or {} scope = experiment.get("scope") or {} train = experiment.get("trainReplay") or {} base_tard = float((train.get("baselineKpi") or {}).get("tardiness") or 0.0) cand_tard = float((train.get("candidateKpi") or {}).get("tardiness") or 0.0) scope_text = " / ".join( f"{k}={','.join(str(v) for v in vs)}" for k, vs in scope.items()) or "全局(无作用域限制)" lines = [ f"实验:{experiment.get('id')}(策略 {experiment.get('strategy')})", f"候选参数:{_candidate_summary(cand)}", f"作用域:{scope_text}", (f"训练集回放 KPI:基线延期 {base_tard} → 候选 {cand_tard}" f"({'退化' if train.get('degraded') else '未退化'})"), "确认后执行验证集回放:退化自动回滚;通过则 GRAY→ACTIVE/FULL(全量生效)。", ] return "参数实验升级(GRAY→ACTIVE)", lines