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
+4 -4
View File
@@ -26,7 +26,7 @@ interface Prediction {
pred_home_goals: number | null pred_home_goals: number | null
pred_away_goals: number | null pred_away_goals: number | null
pred_1x2: string | null pred_1x2: string | null
confidence: number | null subjective_confidence: number | null
reasoning: string | null reasoning: string | null
agent_outputs: AgentReport[] | null agent_outputs: AgentReport[] | null
agent_weights: Record<string, number> | null agent_weights: Record<string, number> | null
@@ -40,7 +40,7 @@ interface AgentReport {
data_sufficiency: string data_sufficiency: string
analysis: string analysis: string
home_edge: number | null home_edge: number | null
confidence: number | null subjective_confidence: number | null
key_evidence: string[] key_evidence: string[]
exp_home_goals: number | null exp_home_goals: number | null
exp_away_goals: number | null exp_away_goals: number | null
@@ -239,7 +239,7 @@ export default function Matches() {
<div className="bg-green-50 rounded p-3"> <div className="bg-green-50 rounded p-3">
<div className="text-gray-500 text-xs"></div> <div className="text-gray-500 text-xs"></div>
<div className="text-xl font-bold"> <div className="text-xl font-bold">
{prediction.confidence !== null ? `${(prediction.confidence * 100).toFixed(0)}%` : '-'} {prediction.subjective_confidence !== null ? `${(prediction.subjective_confidence * 100).toFixed(0)}%` : '-'}
</div> </div>
</div> </div>
</div> </div>
@@ -271,7 +271,7 @@ export default function Matches() {
{r.home_edge > 0 ? '+' : ''}{r.home_edge.toFixed(2)} {r.home_edge > 0 ? '+' : ''}{r.home_edge.toFixed(2)}
</span> </span>
)} )}
{r.confidence !== null && <span> {(r.confidence * 100).toFixed(0)}%</span>} {r.subjective_confidence !== null && <span> {(r.subjective_confidence * 100).toFixed(0)}%</span>}
{r.probable_score && <span> {r.probable_score}</span>} {r.probable_score && <span> {r.probable_score}</span>}
</span> </span>
</summary> </summary>
+2 -2
View File
@@ -46,7 +46,7 @@ async def backtest(req: BacktestRequest):
"scored": summary.scored, "scored": summary.scored,
"accuracy_1x2": summary.accuracy_1x2, "accuracy_1x2": summary.accuracy_1x2,
"avg_score_rmse": summary.avg_score_rmse, "avg_score_rmse": summary.avg_score_rmse,
"avg_confidence": summary.avg_confidence, "avg_subjective_confidence": summary.avg_confidence,
"calibration": summary.calibration, "calibration": summary.calibration,
}, },
"results": [ "results": [
@@ -61,7 +61,7 @@ async def backtest(req: BacktestRequest):
"pred_home": r.pred_home, "pred_home": r.pred_home,
"pred_away": r.pred_away, "pred_away": r.pred_away,
"pred_1x2": r.pred_1x2, "pred_1x2": r.pred_1x2,
"confidence": r.confidence, "subjective_confidence": r.subjective_confidence,
"correct_1x2": r.correct_1x2, "correct_1x2": r.correct_1x2,
} }
for r in summary.results for r in summary.results
+5 -5
View File
@@ -1,7 +1,7 @@
"""预测路由。""" """预测路由。"""
from __future__ import annotations from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@@ -38,7 +38,7 @@ async def predict(req: PredictRequest, db: AsyncSession = Depends(get_db)):
pred_home_goals=result.pred_home_goals, pred_home_goals=result.pred_home_goals,
pred_away_goals=result.pred_away_goals, pred_away_goals=result.pred_away_goals,
pred_1x2=result.pred_1x2, pred_1x2=result.pred_1x2,
confidence=result.confidence, subjective_confidence=result.subjective_confidence,
reasoning=result.reasoning, reasoning=result.reasoning,
agent_outputs=getattr(result, "agent_outputs", None), agent_outputs=getattr(result, "agent_outputs", None),
agent_weights=getattr(result, "agent_weights", None), agent_weights=getattr(result, "agent_weights", None),
@@ -50,7 +50,7 @@ async def predict(req: PredictRequest, db: AsyncSession = Depends(get_db)):
@router.get("/predictions", response_model=list[PredictionOut]) @router.get("/predictions", response_model=list[PredictionOut])
async def list_predictions( async def list_predictions(
match_id: int | None = None, match_id: int | None = None,
limit: int = 50, limit: int = Query(50, ge=1, le=200),
db: AsyncSession = Depends(get_db_read), db: AsyncSession = Depends(get_db_read),
): ):
stmt = select(Prediction).options(selectinload(Prediction.match)) stmt = select(Prediction).options(selectinload(Prediction.match))
@@ -69,7 +69,7 @@ async def list_predictions(
pred_home_goals=p.pred_home_goals, pred_home_goals=p.pred_home_goals,
pred_away_goals=p.pred_away_goals, pred_away_goals=p.pred_away_goals,
pred_1x2=p.pred_1x2, pred_1x2=p.pred_1x2,
confidence=p.confidence, subjective_confidence=p.subjective_confidence,
reasoning=p.reasoning, reasoning=p.reasoning,
agent_outputs=p.agent_outputs, agent_outputs=p.agent_outputs,
created_at=p.created_at, created_at=p.created_at,
@@ -96,7 +96,7 @@ async def get_prediction(prediction_id: int, db: AsyncSession = Depends(get_db_r
pred_home_goals=p.pred_home_goals, pred_home_goals=p.pred_home_goals,
pred_away_goals=p.pred_away_goals, pred_away_goals=p.pred_away_goals,
pred_1x2=p.pred_1x2, pred_1x2=p.pred_1x2,
confidence=p.confidence, subjective_confidence=p.subjective_confidence,
reasoning=p.reasoning, reasoning=p.reasoning,
agent_outputs=p.agent_outputs, agent_outputs=p.agent_outputs,
created_at=p.created_at, created_at=p.created_at,
+2 -2
View File
@@ -54,7 +54,7 @@ class PredictOut(BaseModel):
pred_home_goals: float | None pred_home_goals: float | None
pred_away_goals: float | None pred_away_goals: float | None
pred_1x2: str | None pred_1x2: str | None
confidence: float | None subjective_confidence: float | None
reasoning: str | None reasoning: str | None
agent_outputs: list[dict] | None = None agent_outputs: list[dict] | None = None
agent_weights: dict | None = None agent_weights: dict | None = None
@@ -72,7 +72,7 @@ class PredictionOut(BaseModel):
pred_home_goals: float | None pred_home_goals: float | None
pred_away_goals: float | None pred_away_goals: float | None
pred_1x2: str | None pred_1x2: str | None
confidence: float | None subjective_confidence: float | None
reasoning: str | None reasoning: str | None
agent_outputs: list[dict] | None = None agent_outputs: list[dict] | None = None
created_at: datetime created_at: datetime
+11 -1
View File
@@ -5,6 +5,7 @@ from datetime import date, datetime, timezone
from sqlalchemy import ( from sqlalchemy import (
Boolean, Boolean,
CheckConstraint,
Date, Date,
DateTime, DateTime,
Float, Float,
@@ -156,12 +157,15 @@ class Prediction(Base):
pred_home_goals: Mapped[float | None] = mapped_column(Float) pred_home_goals: Mapped[float | None] = mapped_column(Float)
pred_away_goals: Mapped[float | None] = mapped_column(Float) pred_away_goals: Mapped[float | None] = mapped_column(Float)
pred_1x2: Mapped[str | None] = mapped_column(String(3)) pred_1x2: Mapped[str | None] = mapped_column(String(3))
confidence: Mapped[float | None] = mapped_column(Float) subjective_confidence: Mapped[float | None] = mapped_column(Float) # LLM 主观置信度,非概率
reasoning: Mapped[str | None] = mapped_column(Text) reasoning: Mapped[str | None] = mapped_column(Text)
raw_response: Mapped[dict | None] = mapped_column(JSONB) raw_response: Mapped[dict | None] = mapped_column(JSONB)
# multi-agent 模式: 各专家报告 # multi-agent 模式: 各专家报告
mode: Mapped[str] = mapped_column(String(20), nullable=False, default="single") mode: Mapped[str] = mapped_column(String(20), nullable=False, default="single")
agent_outputs: Mapped[dict | None] = mapped_column(JSONB) agent_outputs: Mapped[dict | None] = mapped_column(JSONB)
# 回测/可复现性
cutoff_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
input_hash: Mapped[str | None] = mapped_column(String(64))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
actual_home_goals: Mapped[int | None] = mapped_column(Integer) actual_home_goals: Mapped[int | None] = mapped_column(Integer)
actual_away_goals: Mapped[int | None] = mapped_column(Integer) actual_away_goals: Mapped[int | None] = mapped_column(Integer)
@@ -172,4 +176,10 @@ class Prediction(Base):
__table_args__ = ( __table_args__ = (
Index("ix_predictions_match", "match_id"), Index("ix_predictions_match", "match_id"),
Index("ix_predictions_provider_model", "provider", "model"), Index("ix_predictions_provider_model", "provider", "model"),
# 数据库级约束:最后一道防线
CheckConstraint("pred_home_goals >= 0", name="ck_pred_home_goals_nonneg"),
CheckConstraint("pred_away_goals >= 0", name="ck_pred_away_goals_nonneg"),
CheckConstraint("subjective_confidence >= 0 AND subjective_confidence <= 1", name="ck_confidence_range"),
CheckConstraint("pred_1x2 IN ('1', 'X', '2')", name="ck_pred_1x2_enum"),
CheckConstraint("mode IN ('single', 'multi')", name="ck_mode_enum"),
) )
+3 -3
View File
@@ -48,7 +48,7 @@ class AgentReport:
data_sufficiency: str = "medium" # high | medium | low | none data_sufficiency: str = "medium" # high | medium | low | none
analysis: str = "" analysis: str = ""
home_edge: float | None = None # -1.0 ~ 1.0, 正=利主队 home_edge: float | None = None # -1.0 ~ 1.0, 正=利主队
confidence: float | None = None # 0.0 ~ 1.0 subjective_confidence: float | None = None # 0.0 ~ 1.0
key_evidence: list[str] = field(default_factory=list) key_evidence: list[str] = field(default_factory=list)
# xg agent 专属 # xg agent 专属
exp_home_goals: float | None = None exp_home_goals: float | None = None
@@ -67,7 +67,7 @@ class AgentReport:
"data_sufficiency": self.data_sufficiency, "data_sufficiency": self.data_sufficiency,
"analysis": self.analysis, "analysis": self.analysis,
"home_edge": self.home_edge, "home_edge": self.home_edge,
"confidence": self.confidence, "subjective_confidence": self.subjective_confidence,
"key_evidence": self.key_evidence, "key_evidence": self.key_evidence,
"exp_home_goals": self.exp_home_goals, "exp_home_goals": self.exp_home_goals,
"exp_away_goals": self.exp_away_goals, "exp_away_goals": self.exp_away_goals,
@@ -122,7 +122,7 @@ def _parse_report(agent: str, parsed: dict, resp: LLMResponse, model: str) -> Ag
data_sufficiency=validated.data_sufficiency, data_sufficiency=validated.data_sufficiency,
analysis=validated.analysis, analysis=validated.analysis,
home_edge=validated.home_edge, home_edge=validated.home_edge,
confidence=validated.confidence, subjective_confidence=validated.subjective_confidence,
key_evidence=validated.key_evidence, key_evidence=validated.key_evidence,
exp_home_goals=validated.exp_home_goals, exp_home_goals=validated.exp_home_goals,
exp_away_goals=validated.exp_away_goals, exp_away_goals=validated.exp_away_goals,
+12 -3
View File
@@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import hashlib
import json import json
import logging import logging
import time import time
@@ -68,7 +69,7 @@ class MultiPredictResult:
pred_home_goals: float | None pred_home_goals: float | None
pred_away_goals: float | None pred_away_goals: float | None
pred_1x2: str | None pred_1x2: str | None
confidence: float | None subjective_confidence: float | None
reasoning: str | None reasoning: str | None
agent_outputs: list[dict] agent_outputs: list[dict]
agent_weights: dict | None agent_weights: dict | None
@@ -164,6 +165,7 @@ async def predict_match_multi(
# 1. 比赛头(各 agent 共享;不存在则 404) # 1. 比赛头(各 agent 共享;不存在则 404)
header = await load_match_header(match_id) header = await load_match_header(match_id)
cutoff_at = header.match_dt
# 2. 并行专家 # 2. 并行专家
specialist_provider = _get_specialist_provider() specialist_provider = _get_specialist_provider()
@@ -177,6 +179,11 @@ async def predict_match_multi(
latency_ms = int((time.perf_counter() - start) * 1000) latency_ms = int((time.perf_counter() - start) * 1000)
# 3.5 计算输入 hash(基于终裁报告)
input_hash = hashlib.sha256(
_reports_to_json(reports).encode("utf-8")
).hexdigest()
# 4. 存库 # 4. 存库
async with AsyncSessionLocal() as db: async with AsyncSessionLocal() as db:
m = await db.get(Match, match_id) m = await db.get(Match, match_id)
@@ -203,10 +210,12 @@ async def predict_match_multi(
pred_home_goals=validated.pred_home_goals, pred_home_goals=validated.pred_home_goals,
pred_away_goals=validated.pred_away_goals, pred_away_goals=validated.pred_away_goals,
pred_1x2=validated.pred_1x2, pred_1x2=validated.pred_1x2,
confidence=validated.confidence, subjective_confidence=validated.subjective_confidence,
reasoning=validated.reasoning, reasoning=validated.reasoning,
raw_response=final, raw_response=final,
agent_outputs=[r.to_dict() for r in reports], agent_outputs=[r.to_dict() for r in reports],
cutoff_at=cutoff_at,
input_hash=input_hash,
) )
db.add(pred) db.add(pred)
await db.commit() await db.commit()
@@ -221,7 +230,7 @@ async def predict_match_multi(
pred_home_goals=pred.pred_home_goals, pred_home_goals=pred.pred_home_goals,
pred_away_goals=pred.pred_away_goals, pred_away_goals=pred.pred_away_goals,
pred_1x2=pred.pred_1x2, pred_1x2=pred.pred_1x2,
confidence=pred.confidence, subjective_confidence=pred.subjective_confidence,
reasoning=pred.reasoning, reasoning=pred.reasoning,
agent_outputs=pred.agent_outputs, agent_outputs=pred.agent_outputs,
agent_weights=agent_weights, agent_weights=agent_weights,
+4 -4
View File
@@ -139,7 +139,7 @@ async def run_backtest(
pred_home=result.pred_home_goals, pred_home=result.pred_home_goals,
pred_away=result.pred_away_goals, pred_away=result.pred_away_goals,
pred_1x2=result.pred_1x2, pred_1x2=result.pred_1x2,
confidence=result.confidence, subjective_confidence=result.subjective_confidence,
correct_1x2=correct, correct_1x2=correct,
prediction_id=result.prediction_id, prediction_id=result.prediction_id,
) )
@@ -164,7 +164,7 @@ async def run_backtest(
summary.avg_score_rmse = round(sum(errors) / len(errors), 2) summary.avg_score_rmse = round(sum(errors) / len(errors), 2)
# 平均置信度 # 平均置信度
confs = [r.confidence for r in summary.results if r.confidence is not None] confs = [r.subjective_confidence for r in summary.results if r.subjective_confidence is not None]
if confs: if confs:
summary.avg_confidence = round(sum(confs) / len(confs), 2) summary.avg_confidence = round(sum(confs) / len(confs), 2)
@@ -184,11 +184,11 @@ def _compute_calibration(results: list[BacktestMatchResult]) -> list[dict]:
"0.0-0.3": {"range": (0.0, 0.3), "total": 0, "correct": 0}, "0.0-0.3": {"range": (0.0, 0.3), "total": 0, "correct": 0},
} }
for r in results: for r in results:
if r.confidence is None: if r.subjective_confidence is None:
continue continue
for key, b in buckets.items(): for key, b in buckets.items():
lo, hi = b["range"] lo, hi = b["range"]
if lo <= r.confidence <= hi: if lo <= r.subjective_confidence <= hi:
b["total"] += 1 b["total"] += 1
if r.correct_1x2: if r.correct_1x2:
b["correct"] += 1 b["correct"] += 1
+2
View File
@@ -36,6 +36,7 @@ class MatchContext:
text: str text: str
has_stats: bool has_stats: bool
has_injuries: bool has_injuries: bool
match_dt: object | None = None # 比赛时间(回测防泄漏 + 快照用)
@dataclass @dataclass
@@ -291,6 +292,7 @@ async def build_context(match_id: int, *, form_last: int = 5, h2h_last: int = 5)
text="\n".join(parts), text="\n".join(parts),
has_stats=has_stats, has_stats=has_stats,
has_injuries=has_injuries, has_injuries=has_injuries,
match_dt=header.match_dt,
) )
+2 -2
View File
@@ -61,8 +61,8 @@ async def get_eval_summary() -> dict:
err = ((p.pred_home_goals - p.actual_home_goals) ** 2 + err = ((p.pred_home_goals - p.actual_home_goals) ** 2 +
(p.pred_away_goals - p.actual_away_goals) ** 2) ** 0.5 (p.pred_away_goals - p.actual_away_goals) ** 2) ** 0.5
b["score_errors"].append(err) b["score_errors"].append(err)
if p.confidence is not None: if p.subjective_confidence is not None:
b["conf_sum"] += p.confidence b["conf_sum"] += p.subjective_confidence
b["conf_count"] += 1 b["conf_count"] += 1
summary = [] summary = []
+11 -3
View File
@@ -2,9 +2,11 @@
from __future__ import annotations from __future__ import annotations
import functools import functools
import hashlib
import logging import logging
import time import time
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from threading import Lock from threading import Lock
@@ -64,7 +66,7 @@ class PredictResult:
pred_home_goals: float | None pred_home_goals: float | None
pred_away_goals: float | None pred_away_goals: float | None
pred_1x2: str | None pred_1x2: str | None
confidence: float | None subjective_confidence: float | None
reasoning: str | None reasoning: str | None
context: str context: str
latency_ms: int | None latency_ms: int | None
@@ -112,6 +114,10 @@ async def _predict_single(
# 1. 拼上下文 # 1. 拼上下文
ctx = await build_context(match_id) ctx = await build_context(match_id)
# 1.5 计算快照元数据(用于可复现性)
cutoff_at = ctx.match_dt # 比赛时间 = 数据截止时间
input_hash = hashlib.sha256(ctx.text.encode("utf-8")).hexdigest()
# 2. 拼 prompt(指定版本) # 2. 拼 prompt(指定版本)
template = _load_prompt_template(version) template = _load_prompt_template(version)
user_prompt = template.replace("{{context}}", ctx.text) user_prompt = template.replace("{{context}}", ctx.text)
@@ -155,9 +161,11 @@ async def _predict_single(
pred_home_goals=validated.pred_home_goals, pred_home_goals=validated.pred_home_goals,
pred_away_goals=validated.pred_away_goals, pred_away_goals=validated.pred_away_goals,
pred_1x2=validated.pred_1x2, pred_1x2=validated.pred_1x2,
confidence=validated.confidence, subjective_confidence=validated.subjective_confidence,
reasoning=validated.reasoning, reasoning=validated.reasoning,
raw_response=resp.raw, raw_response=resp.raw,
cutoff_at=cutoff_at,
input_hash=input_hash,
) )
db.add(pred) db.add(pred)
await db.commit() await db.commit()
@@ -171,7 +179,7 @@ async def _predict_single(
pred_home_goals=pred.pred_home_goals, pred_home_goals=pred.pred_home_goals,
pred_away_goals=pred.pred_away_goals, pred_away_goals=pred.pred_away_goals,
pred_1x2=pred.pred_1x2, pred_1x2=pred.pred_1x2,
confidence=pred.confidence, subjective_confidence=pred.subjective_confidence,
reasoning=pred.reasoning, reasoning=pred.reasoning,
context=ctx.text, context=ctx.text,
latency_ms=resp.latency_ms, latency_ms=resp.latency_ms,
+4 -4
View File
@@ -15,7 +15,7 @@ class AgentReportSchema(BaseModel):
data_sufficiency: str = "medium" data_sufficiency: str = "medium"
analysis: str = "" analysis: str = ""
home_edge: float | None = Field(None, ge=-1.0, le=1.0) home_edge: float | None = Field(None, ge=-1.0, le=1.0)
confidence: float | None = Field(None, ge=0.0, le=1.0) subjective_confidence: float | None = Field(None, ge=0.0, le=1.0)
key_evidence: list[str] = Field(default_factory=list) key_evidence: list[str] = Field(default_factory=list)
exp_home_goals: float | None = Field(None, ge=0.0, le=10.0) exp_home_goals: float | None = Field(None, ge=0.0, le=10.0)
exp_away_goals: float | None = Field(None, ge=0.0, le=10.0) exp_away_goals: float | None = Field(None, ge=0.0, le=10.0)
@@ -50,7 +50,7 @@ class PredictionOutputSchema(BaseModel):
pred_home_goals: float = Field(ge=0.0, le=10.0) pred_home_goals: float = Field(ge=0.0, le=10.0)
pred_away_goals: float = Field(ge=0.0, le=10.0) pred_away_goals: float = Field(ge=0.0, le=10.0)
pred_1x2: str pred_1x2: str
confidence: float = Field(ge=0.0, le=1.0) subjective_confidence: float = Field(ge=0.0, le=1.0)
reasoning: str = "" reasoning: str = ""
@field_validator("pred_1x2") @field_validator("pred_1x2")
@@ -87,7 +87,7 @@ def validate_agent_output(raw: dict) -> AgentReportSchema:
data_sufficiency=raw.get("data_sufficiency", "medium"), data_sufficiency=raw.get("data_sufficiency", "medium"),
analysis=raw.get("analysis", ""), analysis=raw.get("analysis", ""),
home_edge=_safe_float(raw.get("home_edge")), home_edge=_safe_float(raw.get("home_edge")),
confidence=_safe_float(raw.get("confidence")), subjective_confidence=_safe_float(raw.get("subjective_confidence") or raw.get("confidence")),
key_evidence=raw.get("key_evidence", []), key_evidence=raw.get("key_evidence", []),
exp_home_goals=_safe_float(raw.get("exp_home_goals")), exp_home_goals=_safe_float(raw.get("exp_home_goals")),
exp_away_goals=_safe_float(raw.get("exp_away_goals")), exp_away_goals=_safe_float(raw.get("exp_away_goals")),
@@ -101,7 +101,7 @@ def validate_prediction_output(raw: dict) -> PredictionOutputSchema:
pred_home_goals=float(raw.get("pred_home_goals", 0)), pred_home_goals=float(raw.get("pred_home_goals", 0)),
pred_away_goals=float(raw.get("pred_away_goals", 0)), pred_away_goals=float(raw.get("pred_away_goals", 0)),
pred_1x2=raw.get("1x2") or raw.get("pred_1x2", "X"), pred_1x2=raw.get("1x2") or raw.get("pred_1x2", "X"),
confidence=float(raw.get("confidence", 0.5)), subjective_confidence=float(raw.get("confidence", 0.5)),
reasoning=str(raw.get("reasoning", ""))[:1000], reasoning=str(raw.get("reasoning", ""))[:1000],
) )
+7 -7
View File
@@ -59,14 +59,14 @@ class TestReportParsing:
"data_sufficiency": "high", "data_sufficiency": "high",
"analysis": "主队交锋占优", "analysis": "主队交锋占优",
"home_edge": 0.6, "home_edge": 0.6,
"confidence": 0.8, "subjective_confidence": 0.8,
"key_evidence": ["近5次交锋主队4胜", "主场交锋3连胜"], "key_evidence": ["近5次交锋主队4胜", "主场交锋3连胜"],
} }
resp = LLMResponse(content="{}", parsed=parsed, prompt_tokens=100, completion_tokens=50, latency_ms=500) resp = LLMResponse(content="{}", parsed=parsed, prompt_tokens=100, completion_tokens=50, latency_ms=500)
r = _parse_report("h2h", parsed, resp, "gpt-4o-mini") r = _parse_report("h2h", parsed, resp, "gpt-4o-mini")
assert r.status == "ok" assert r.status == "ok"
assert r.home_edge == 0.6 assert r.home_edge == 0.6
assert r.confidence == 0.8 assert r.subjective_confidence == 0.8
assert len(r.key_evidence) == 2 assert len(r.key_evidence) == 2
assert r.data_sufficiency == "high" assert r.data_sufficiency == "high"
@@ -78,7 +78,7 @@ class TestReportParsing:
"data_sufficiency": "medium", "data_sufficiency": "medium",
"analysis": "主队火力更强", "analysis": "主队火力更强",
"home_edge": 0.4, "home_edge": 0.4,
"confidence": 0.7, "subjective_confidence": 0.7,
"key_evidence": ["场均xG 2.1"], "key_evidence": ["场均xG 2.1"],
"exp_home_goals": 2.1, "exp_home_goals": 2.1,
"exp_away_goals": 1.2, "exp_away_goals": 1.2,
@@ -97,7 +97,7 @@ class TestReportParsing:
parsed = { parsed = {
"data_sufficiency": "bogus", # 非法 → medium "data_sufficiency": "bogus", # 非法 → medium
"home_edge": "very strong", # 非法 → None (宽容降级) "home_edge": "very strong", # 非法 → None (宽容降级)
"confidence": None, "subjective_confidence": None,
"key_evidence": "单字符串", # → [str] "key_evidence": "单字符串", # → [str]
} }
resp = LLMResponse(content="{}", parsed=parsed) resp = LLMResponse(content="{}", parsed=parsed)
@@ -109,7 +109,7 @@ class TestReportParsing:
assert r.key_evidence == ["单字符串"] assert r.key_evidence == ["单字符串"]
# 验证范围约束: confidence > 1 会被截断或拒绝 # 验证范围约束: 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") r2 = _parse_report("form", parsed2, resp, "m")
# Pydantic 会拒绝越界值 → parse_error # Pydantic 会拒绝越界值 → parse_error
assert r2.status == "parse_error" assert r2.status == "parse_error"
@@ -175,7 +175,7 @@ class TestRunAgent:
assert "A 2-1 B" in user assert "A 2-1 B" in user
return LLMResponse( return LLMResponse(
content="{}", 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, prompt_tokens=10, completion_tokens=5, latency_ms=100,
) )
@@ -200,7 +200,7 @@ class TestOrchestratorAggregation:
import json import json
reports = [ 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"), AgentReport(agent="injuries", status="no_data", data_sufficiency="none"),
] ]
text = _reports_to_json(reports) text = _reports_to_json(reports)
+1 -1
View File
@@ -96,7 +96,7 @@ class TestProviderMock:
def raise_for_status(self): pass def raise_for_status(self): pass
def json(self): def json(self):
return { 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}, "usage": {"prompt_tokens": 100, "completion_tokens": 50},
} }
return FakeResp() return FakeResp()