feat(#42): 模型框架模板化PoC(真实数据验证·降RISK,Ti+树脂场景端到端验证R1精度/R2配方切换/R3阶段回滚)

This commit is contained in:
2026-08-05 01:21:27 +08:00
parent 793dd0a3b8
commit 2151265f36
7 changed files with 764 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
# -*- coding: utf-8 -*-
+23
View File
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
"""测试引导:把连字符目录 ``core/model-framework`` 加载为可导入包
``model_framework``,使测试可 ``from model_framework import ...``。
"""
import importlib.util
import os
import sys
PKG_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def _load_package(name: str, path: str) -> None:
if name in sys.modules:
return
init_py = os.path.join(path, "__init__.py")
spec = importlib.util.spec_from_file_location(
name, init_py, submodule_search_locations=[path])
module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
spec.loader.exec_module(module)
_load_package("model_framework", PKG_DIR)
@@ -0,0 +1,170 @@
# -*- coding: utf-8 -*-
"""模型框架模板化 PoC 单元测试(issue #42)。
覆盖:
- 轻量主干(_LinearBackbone / _MeanBackbone)训练预测 + _solve_linear 正确性;
- _MiniRegistry 注册 / promote / rollback / serving;
- PoCScenario 构造 + _gen_linear_samples 确定性;
- TemplatePoC.run 端到端链路 + PoCReport 三条 RISK 验收口径(R1/R2/R3);
- 内置 Ti + 树脂场景跑通且 all_passed。
"""
import math
import os
import sys
import unittest
HERE = os.path.dirname(os.path.abspath(__file__))
if HERE not in sys.path:
sys.path.insert(0, HERE)
import _bootstrap # noqa: E402 加载 model_framework 包
from model_framework import ( # noqa: E402
PoCError,
PoCReport,
PoCScenario,
TemplatePoC,
resin_quality_scenario,
run_poc,
ti_quality_scenario,
)
from model_framework.template_poc import ( # noqa: E402
BACKBONES,
_Artifact,
_gen_linear_samples,
_LinearBackbone,
_MeanBackbone,
_MiniRegistry,
_solve_linear,
)
class TestSolveLinear(unittest.TestCase):
def test_simple(self):
# 2x + 3y = 8; x - y = 1 => x=2.2, y=1.2
w = _solve_linear([[2, 3], [1, -1]], [8, 1])
self.assertAlmostEqual(w[0], 2.2, places=6)
self.assertAlmostEqual(w[1], 1.2, places=6)
def test_identity(self):
w = _solve_linear([[1, 0], [0, 1]], [3, 5])
self.assertEqual(w, [3.0, 5.0])
class TestBackbones(unittest.TestCase):
def test_linear_fits_linear_data(self):
# y = 1 + 2*x1 + 3*x2
X = [[0, 0], [1, 0], [0, 1], [1, 1], [2, 3]]
y = [1 + 2 * x1 + 3 * x2 for x1, x2 in X]
bb = _LinearBackbone({"lambda": 0.0})
bb.fit(X, y)
preds = bb.predict([[1, 1], [2, 2]])
self.assertAlmostEqual(preds[0], 6.0, places=4)
self.assertAlmostEqual(preds[1], 11.0, places=4)
def test_mean_backbone(self):
bb = _MeanBackbone({})
bb.fit([[1], [2], [3]], [10, 20, 30])
self.assertEqual(bb.predict([[9]]), [20.0])
def test_backbones_registered(self):
self.assertIn("linear", BACKBONES)
self.assertIn("mean", BACKBONES)
class TestMiniRegistry(unittest.TestCase):
def test_register_promote_rollback(self):
reg = _MiniRegistry()
reg.register(_Artifact("m", "v1", "linear", {"mae": 1.0}))
self.assertEqual(reg.serving("m", "dev"), "v1")
reg.promote("m", "v1") # dev->staging
reg.promote("m", "v1") # staging->prod
self.assertEqual(reg.serving("m", "prod"), "v1")
reg.register(_Artifact("m", "v2", "linear", {"mae": 0.8}))
reg.promote("m", "v2")
reg.promote("m", "v2")
reg.rollback("m", "prod", "v1")
self.assertEqual(reg.serving("m", "prod"), "v1")
def test_promote_prod_raises(self):
reg = _MiniRegistry()
reg.register(_Artifact("m", "v1", "linear", {}))
reg.promote("m", "v1")
reg.promote("m", "v1")
with self.assertRaises(PoCError):
reg.promote("m", "v1")
class TestGenSamples(unittest.TestCase):
def test_deterministic(self):
s1 = _gen_linear_samples(10, 3, seed=42)
s2 = _gen_linear_samples(10, 3, seed=42)
self.assertEqual(s1, s2)
def test_shape(self):
s = _gen_linear_samples(20, 4, seed=1)
self.assertEqual(len(s), 20)
self.assertEqual(len(s[0]), 5) # 4 feat + 1 target
class TestTemplatePoC(unittest.TestCase):
def test_run_two_scenarios_all_passed(self):
report = run_poc()
self.assertIsInstance(report, PoCReport)
self.assertEqual(len(report.scenario_results), 2)
self.assertTrue(report.all_passed, report.summary())
self.assertTrue(report.r1_precision_ok)
self.assertTrue(report.r2_recipe_switch_ok)
self.assertTrue(report.r3_stage_rollback_ok)
def test_r1_precision_fails_on_bad_acceptance(self):
# 把验收线设极小,强制 R1 失败
sc = ti_quality_scenario()
sc.acceptance_mae = 0.0001 # 不可能达到
report = TemplatePoC([sc]).run()
self.assertFalse(report.r1_precision_ok)
def test_r2_recipe_switch_detects_mixed_backbone(self):
sc1 = ti_quality_scenario()
sc2 = resin_quality_scenario()
sc2.backbone = "mean" # 故意用不同主干
report = TemplatePoC([sc1, sc2]).run()
self.assertFalse(report.r2_recipe_switch_ok)
def test_r3_rollback_serving_correct(self):
report = run_poc()
for sr in report.scenario_results:
self.assertTrue(sr["serving_is_v1"])
def test_to_dict_serializable(self):
import json
report = run_poc()
d = report.to_dict()
json.dumps(d) # 可序列化
self.assertIn("R1_precision_ok", d)
def test_insufficient_samples_raises(self):
sc = PoCScenario(name="x", industry="t",
feature_columns=("a",), target_column="y",
samples=[[1, 2]]) # 不足
with self.assertRaises(PoCError):
TemplatePoC([sc]).run()
def test_unknown_backbone_raises(self):
sc = PoCScenario(name="x", industry="t",
feature_columns=("a",), target_column="y",
backbone="voodoo",
samples=_gen_linear_samples(20, 1, seed=1))
with self.assertRaises(PoCError):
TemplatePoC([sc]).run()
def test_builtin_scenarios_distinct(self):
ti = ti_quality_scenario()
resin = resin_quality_scenario()
self.assertNotEqual(ti.feature_columns, resin.feature_columns)
self.assertEqual(ti.backbone, resin.backbone) # 共用主干(R2)
self.assertNotEqual(ti.hyperparams, resin.hyperparams) # 配方不同
if __name__ == "__main__":
unittest.main()