feat: Sprint 2 - 概率语义 + 轻量快照 + 数据库约束

P0-05: confidence → subjective_confidence 改名(15 文件)
       LLM 主观置信度与概率分离
P0-02: predictions 增加 cutoff_at + input_hash(轻量快照)
       MatchContext 暴露 match_dt
       单/多 Agent 路径均记录快照元数据
P2-05: 数据库 CHECK 约束
       - pred_home_goals >= 0
       - pred_away_goals >= 0
       - subjective_confidence 0~1
       - pred_1x2 IN (1,X,2)
       - mode IN (single,multi)
P2-06: limit 分页约束(ge=1, le=200)
P2-01: 新增 /health/ready 就绪检查
This commit is contained in:
shangfangjian
2026-09-15 00:14:24 +08:00
parent f3160e3062
commit cb36dc3ef9
14 changed files with 70 additions and 41 deletions
+7 -7
View File
@@ -59,14 +59,14 @@ class TestReportParsing:
"data_sufficiency": "high",
"analysis": "主队交锋占优",
"home_edge": 0.6,
"confidence": 0.8,
"subjective_confidence": 0.8,
"key_evidence": ["近5次交锋主队4胜", "主场交锋3连胜"],
}
resp = LLMResponse(content="{}", parsed=parsed, prompt_tokens=100, completion_tokens=50, latency_ms=500)
r = _parse_report("h2h", parsed, resp, "gpt-4o-mini")
assert r.status == "ok"
assert r.home_edge == 0.6
assert r.confidence == 0.8
assert r.subjective_confidence == 0.8
assert len(r.key_evidence) == 2
assert r.data_sufficiency == "high"
@@ -78,7 +78,7 @@ class TestReportParsing:
"data_sufficiency": "medium",
"analysis": "主队火力更强",
"home_edge": 0.4,
"confidence": 0.7,
"subjective_confidence": 0.7,
"key_evidence": ["场均xG 2.1"],
"exp_home_goals": 2.1,
"exp_away_goals": 1.2,
@@ -97,7 +97,7 @@ class TestReportParsing:
parsed = {
"data_sufficiency": "bogus", # 非法 → medium
"home_edge": "very strong", # 非法 → None (宽容降级)
"confidence": None,
"subjective_confidence": None,
"key_evidence": "单字符串", # → [str]
}
resp = LLMResponse(content="{}", parsed=parsed)
@@ -109,7 +109,7 @@ class TestReportParsing:
assert r.key_evidence == ["单字符串"]
# 验证范围约束: confidence > 1 会被截断或拒绝
parsed2 = {"confidence": 1.5, "home_edge": 2.0}
parsed2 = {"subjective_confidence": 1.5, "home_edge": 2.0}
r2 = _parse_report("form", parsed2, resp, "m")
# Pydantic 会拒绝越界值 → parse_error
assert r2.status == "parse_error"
@@ -175,7 +175,7 @@ class TestRunAgent:
assert "A 2-1 B" in user
return LLMResponse(
content="{}",
parsed={"data_sufficiency": "high", "analysis": "ok", "home_edge": 0.5, "confidence": 0.9},
parsed={"data_sufficiency": "high", "analysis": "ok", "home_edge": 0.5, "subjective_confidence": 0.9},
prompt_tokens=10, completion_tokens=5, latency_ms=100,
)
@@ -200,7 +200,7 @@ class TestOrchestratorAggregation:
import json
reports = [
AgentReport(agent="h2h", status="ok", home_edge=0.5, confidence=0.8, analysis="a"),
AgentReport(agent="h2h", status="ok", home_edge=0.5, subjective_confidence=0.8, analysis="a"),
AgentReport(agent="injuries", status="no_data", data_sufficiency="none"),
]
text = _reports_to_json(reports)
+1 -1
View File
@@ -96,7 +96,7 @@ class TestProviderMock:
def raise_for_status(self): pass
def json(self):
return {
"choices": [{"message": {"content": '{"pred_home_goals": 1.5, "pred_away_goals": 1.0, "1x2": "1", "confidence": 0.7, "reasoning": "test"}'}}],
"choices": [{"message": {"content": '{"pred_home_goals": 1.5, "pred_away_goals": 1.0, "1x2": "1", "subjective_confidence": 0.7, "reasoning": "test"}'}}],
"usage": {"prompt_tokens": 100, "completion_tokens": 50},
}
return FakeResp()