docs: 统一文档与代码一致性

- 修复所有 confidence → subjective_confidence 残留(03-api, 04-agents, 05-data, 07-development)
- 修复 5 张表 → 6 张表残留(06-deployment)
- 同步 API schema 示例与实际模型一致
- 更新 predictions 表结构文档(新增 status/cutoff/input_hash 字段)

code: 修复 API 异常处理(eval/predict)和 context_builder stats 时间过滤
This commit is contained in:
shangfangjian
2026-09-15 02:27:48 +08:00
parent 0980c2242a
commit 60e4b89822
8 changed files with 37 additions and 15 deletions
+4 -4
View File
@@ -87,17 +87,17 @@ Base URL: `http://localhost:8000` · 交互式文档: `/docs`(Swagger)与 `/redo
"pred_home_goals": 2.1,
"pred_away_goals": 1.0,
"pred_1x2": "1",
"confidence": 0.68,
"subjective_confidence": 0.68,
"reasoning": "综合 xg 报告的进球期望 2.1-1.0 与 form 报告的三连胜势头……",
"agent_outputs": [
{"agent": "h2h", "status": "ok", "data_sufficiency": "medium",
"analysis": "近 5 次交锋主队 3 胜……", "home_edge": 0.4,
"confidence": 0.7, "key_evidence": ["近5次交锋主队3胜", "主场交锋3连胜"],
"subjective_confidence": 0.7, "key_evidence": ["近5次交锋主队3胜", "主场交锋3连胜"],
"exp_home_goals": null, "exp_away_goals": null, "probable_score": null,
"model": "gpt-4o-mini", "latency_ms": 2100,
"prompt_tokens": 380, "completion_tokens": 120},
{"agent": "injuries", "status": "no_data", "data_sufficiency": "none",
"analysis": "该维度无数据,跳过分析。", "home_edge": null, "confidence": null,
"analysis": "该维度无数据,跳过分析。", "home_edge": null, "subjective_confidence": null,
"key_evidence": [], "exp_home_goals": null, "exp_away_goals": null,
"probable_score": null, "model": "", "latency_ms": null,
"prompt_tokens": null, "completion_tokens": null}
@@ -172,7 +172,7 @@ Base URL: `http://localhost:8000` · 交互式文档: `/docs`(Swagger)与 `/redo
```json
{"summary": [
{"provider": "openai", "model": "gpt-4o", "total": 12,
"accuracy_1x2": 58.3, "avg_score_rmse": 1.21, "avg_confidence": 0.65}
"accuracy_1x2": 58.3, "avg_score_rmse": 1.21, "avg_subjective_confidence": 0.65}
]}
```
+4 -4
View File
@@ -65,7 +65,7 @@ POST /predict {match_id, mode: "multi"}
"data_sufficiency": "high",
"analysis": "近 5 次交锋主队 3 胜 1 平 1 负,主场交锋 3 连胜……",
"home_edge": 0.4,
"confidence": 0.7,
"subjective_confidence": 0.7,
"key_evidence": ["近5次交锋主队3胜", "主场交锋3连胜"]
}
```
@@ -75,7 +75,7 @@ POST /predict {match_id, mode: "multi"}
| `status` | `ok` / `no_data` / `error` / `parse_error` |
| `data_sufficiency` | `high` / `medium` / `low` / `none` |
| `home_edge` | -1.0 ~ 1.0,正数=利主队,负数=利客队 |
| `confidence` | 0.0 ~ 1.0,该专家对自己分析的信心 |
| `subjective_confidence` | 0.0 ~ 1.0,该专家对自己分析的主观信心(非概率) |
| `key_evidence` | 关键证据列表(最多 5 条) |
### 终裁 Agent 输出
@@ -87,13 +87,13 @@ POST /predict {match_id, mode: "multi"}
"pred_home_goals": 2.1,
"pred_away_goals": 1.0,
"1x2": "1",
"confidence": 0.68,
"subjective_confidence": 0.68,
"reasoning": "综合 stats 报告的攻防强度与 form 报告的三连胜势头……",
"agent_weights": {"form": 0.9, "stats": 0.8, "home_away": 0.7, "injuries": 0.0, "h2h": 0.8}
}
```
`agent_weights` 体现终裁对各专家报告的采信度(0–1),可用于后续分析"哪个维度对预测贡献大"。
`agent_weights` 体现终裁对各专家报告的采信度(0–1),可用于后续分析"哪个维度对预测贡献大"。注意:这是 LLM 主观权重,非统计权重。
## 模型分档配置
+6 -1
View File
@@ -124,7 +124,12 @@ CREATE TABLE predictions (
latency_ms INT,
pred_home_goals FLOAT, pred_away_goals FLOAT,
pred_1x2 VARCHAR(3), -- '1' / 'X' / '2'
confidence FLOAT,
subjective_confidence FLOAT, -- LLM 主观置信度(非概率)
status VARCHAR(20) NOT NULL DEFAULT 'success', -- 'success' / 'failed' / 'degraded'
match_kickoff_at TIMESTAMPTZ, -- 比赛时间
prediction_created_at TIMESTAMPTZ, -- 预测创建时间
prediction_cutoff_at TIMESTAMPTZ, -- 数据截止时间
input_hash VARCHAR(64), -- 输入快照 hash
reasoning TEXT,
raw_response JSONB, -- LLM 完整原始响应
agent_outputs JSONB, -- multi 模式: 5 份专家报告
+1 -1
View File
@@ -128,7 +128,7 @@ alembic revision -m "描述"
```
已有迁移:
- `0001_initial`: 初始 5 张表(leagues/teams/matches/match_stats/predictions)
- `0001_initial`: 初始 5 张表,0003 增加 injuries,0004 增加约束,0005 增加时间语义
- `0002_agent_outputs`: predictions 加 `mode` + `agent_outputs`
## 备份与恢复
+1 -1
View File
@@ -118,7 +118,7 @@ class MockProvider:
return LLMResponse(
content="{}",
parsed={"data_sufficiency": "high", "analysis": "ok",
"home_edge": 0.5, "confidence": 0.8,
"home_edge": 0.5, "subjective_confidence": 0.8,
"key_evidence": ["证据"]},
prompt_tokens=10, completion_tokens=5, latency_ms=100,
)
+9 -1
View File
@@ -1,12 +1,16 @@
"""评估路由。"""
from __future__ import annotations
import logging
from fastapi import APIRouter, Depends, HTTPException
from src.api.schemas import EvalSummaryOut, SettleRequest
from src.db.base import AsyncSession, get_db, get_db_read
from src.llm.eval import get_eval_summary, settle_prediction
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1", tags=["eval"])
@@ -17,7 +21,11 @@ async def settle(req: SettleRequest, db: AsyncSession = Depends(get_db)):
pred = await settle_prediction(req.prediction_id, req.home_goals, req.away_goals)
return {"id": pred.id, "settled": pred.settled}
except ValueError as e:
raise HTTPException(404, str(e))
logger.warning("settle failed: %s", e)
raise HTTPException(404, "预测记录不存在")
except Exception as e:
logger.exception("settle error")
raise HTTPException(500, "回填失败,请查看服务器日志")
@router.get("/eval/summary", response_model=EvalSummaryOut)
+11 -2
View File
@@ -1,6 +1,8 @@
"""预测路由。"""
from __future__ import annotations
import logging
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import select
from sqlalchemy.orm import selectinload
@@ -10,6 +12,8 @@ from src.db.base import AsyncSession, get_db, get_db_read
from src.db.models import Prediction
from src.llm.predict import predict_match, PredictResult
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1", tags=["predict"])
@@ -24,9 +28,14 @@ async def predict(req: PredictRequest, db: AsyncSession = Depends(get_db)):
mode=req.mode,
)
except ValueError as e:
raise HTTPException(404, str(e))
logger.warning("predict validation error: %s", e)
raise HTTPException(404, "比赛不存在")
except RuntimeError as e:
raise HTTPException(502, str(e))
logger.error("predict LLM error: %s", e)
raise HTTPException(502, "LLM 预测失败,请查看服务器日志")
except Exception as e:
logger.exception("predict unexpected error")
raise HTTPException(500, "预测失败,请查看服务器日志")
# single / multi 两种结果统一映射
return PredictOut(
+1 -1
View File
@@ -142,7 +142,7 @@ async def form_slice(header: MatchHeader, *, limit: int = 5, before=None) -> str
else: losses += 1
score = f"{fm.home_goals}-{fm.away_goals}" if fm.home_goals is not None else "vs"
xg = ""
if fm.stats and fm.stats.home_xg is not None:
if fm.stats and _is_stats_available(fm.stats, before) and fm.stats.home_xg is not None:
own = fm.stats.home_xg if side == "home" else fm.stats.away_xg
xg = f" (xG {own:.1f})"
opp = fm.away_team.name if side == "home" else fm.home_team.name