feat: 足球 LLM 预测服务初始提交
Profeto — 给 LLM 提供数据,让 LLM 预测足球比分。 核心模块: - FastAPI 后端 + PostgreSQL (SQLAlchemy async) - 多 Agent LLM 预测 (5 专家 + 终裁) - 数据采集 (bzzoiro / understat / injuries) - React 前端 (Vite + Tailwind) 包含: - 数据源抽象 (DataSource 协议 + 注册表) - Alembic 数据库迁移 - Prompt 模板 (单/多 Agent) - 核心路径单元测试
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
# 01 · 架构总览
|
||||
|
||||
## 系统架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 前端(单页 React + Vite + Tailwind) │
|
||||
│ 比赛列表 → 选比赛 → LLM 预测 → 专家报告 + 最终结论 │
|
||||
└──────────────────────────▲──────────────────────────┘
|
||||
│ REST (/api/v1)
|
||||
┌──────────────────────────┴──────────────────────────┐
|
||||
│ FastAPI(单进程,全 async) │
|
||||
│ │
|
||||
│ 数据查询 预测编排 采集(手动/cron 触发) │
|
||||
│ ┌──────┐ ┌────────────┐ ┌───────────────────┐ │
|
||||
│ │matches│ │ orchestrator│ │ bzzoiro (赛果) │ │
|
||||
│ │leagues│ │ ┌─ 5 专家并行(便宜模型) │ │
|
||||
│ └──┬───┘ │ │ h2h / form / stats / │ │
|
||||
│ │ │ │ home_away / injuries │ │
|
||||
│ │ │ └─ aggregator 终裁(强模型) │ │
|
||||
│ │ └────────────┘ └───────────────────┘ │
|
||||
│ │ │ └ understat (xG) │
|
||||
│ ┌──┴──────────────┴──┐ └ injuries (伤停) │
|
||||
│ │ PostgreSQL (5 张表) │ httpx → 外部 API │
|
||||
│ └────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 核心数据流(一次多 Agent 预测)
|
||||
|
||||
1. `POST /predict {match_id}` → orchestrator
|
||||
2. `load_match_header`: 查比赛 + 双方 + 联赛(一次 eager load)
|
||||
3. **5 个专家 agent 并行**(`asyncio.gather`),每个:
|
||||
- 各自的数据切片函数查库(近况/交锋/积分榜 SQL 聚合/伤停/xG)
|
||||
- 切片无数据 → **跳过 LLM**,直接 `no_data` stub(省 token、防幻觉)
|
||||
- 有数据 → 专属 prompt(专家模型,便宜快)→ 结构化 JSON 报告(`home_edge` 方向性评分 + 证据)
|
||||
4. **终裁 agent**:5 份报告 + 比赛信息 → 权衡采信度(`agent_weights`)→ 最终预测 JSON
|
||||
5. 存 `predictions` 表(含 `agent_outputs` 全部报告)
|
||||
6. 赛后 `POST /eval/settle` 回填实际比分 → `GET /eval/summary` 按 模型×prompt 版本 聚合准确率
|
||||
|
||||
## 关键设计决策
|
||||
|
||||
| 决策 | 理由 |
|
||||
|---|---|
|
||||
| **多专家并行而非单次大 prompt** | 每维度独立迭代 prompt;报告可归因(哪个维度分析错了);总延迟 ≈ 2 次串行调用 |
|
||||
| **专家/终裁模型分档** | 专家用便宜模型快速分析,终裁用强模型汇总决策,成本与质量平衡(`LLM_SPECIALIST_MODEL` / `LLM_AGGREGATOR_MODEL`) |
|
||||
| **no_data 门控** | 无数据维度(如伤停未接入)不调 LLM,终裁知道维度缺失,不编造 |
|
||||
| **fail-open** | 单个专家失败只标记 `status=error`,其余照常;研究场景可用性优先 |
|
||||
| **`match_date_date` 天级去重** | 不同源时间精度不同,秒级匹配会产生重复行;天级 + 数据库唯一约束 |
|
||||
| **积分榜 SQL 聚合 + season 过滤** | `UNION ALL` 主客双视角 + `GROUP BY` 在库内算,只算当前赛季(修复过跨赛季 bug) |
|
||||
| **单 agent 模式保留** | `mode="single"` 走旧单次路径,与 multi 形成天然 A/B(eval 按 `prompt_version` 分组) |
|
||||
| **无 worker/redis/队列** | 采集是 cron 触发的短任务,单进程足够;违背简化初衷的基础设施一律不加 |
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
Profeto/
|
||||
├── src/
|
||||
│ ├── api/
|
||||
│ │ ├── app.py # FastAPI 工厂(lifespan 建表)
|
||||
│ │ ├── schemas.py # Pydantic v2 请求/响应
|
||||
│ │ └── routes/
|
||||
│ │ ├── matches.py # 联赛/比赛查询(游标分页)
|
||||
│ │ ├── predict.py # 预测 + 预测历史
|
||||
│ │ ├── ingest.py # 采集触发(自管 session)
|
||||
│ │ └── eval.py # 赛后回填 + 准确率汇总
|
||||
│ ├── db/
|
||||
│ │ ├── base.py # async engine + get_db/get_db_read
|
||||
│ │ └── models.py # 5 张表 ORM
|
||||
│ ├── data/
|
||||
│ │ ├── bzzoiro.py # 赛果采集 + 幂等入库
|
||||
│ │ ├── understat.py # xG 回填
|
||||
│ │ ├── injuries.py # 伤停采集(带文件缓存)
|
||||
│ │ ├── normalize.py # NormalizedMatch 清洗契约
|
||||
│ │ ├── team_names.py # 队名归一映射
|
||||
│ │ └── config.py # 联赛代码映射
|
||||
│ ├── llm/
|
||||
│ │ ├── provider.py # OpenAI-compatible 抽象(共享连接池/JSON 兜底解析)
|
||||
│ │ ├── context_builder.py # 数据切片(h2h/form/stats/home_away/injuries)+ 单 agent 拼接
|
||||
│ │ ├── predict.py # 预测入口(mode 分派 + 缓存)
|
||||
│ │ ├── eval.py # 准确率统计
|
||||
│ │ ├── agents/
|
||||
│ │ │ ├── base.py # AgentSpec / AgentReport / run_agent
|
||||
│ │ │ └── orchestrator.py # 并行专家 → 终裁 → 存库
|
||||
│ │ └── prompts/
|
||||
│ │ ├── match_prediction_v1/v2.md # 单 agent 模板
|
||||
│ │ └── agents/{h2h,form,stats,home_away,injuries,aggregator}_v1.md
|
||||
│ └── core/config.py # pydantic-settings
|
||||
├── alembic/versions/ # 0001 建表 + 0002 agent 字段
|
||||
├── frontend/src/pages/Matches.tsx # 单页(预测面板 + 专家报告折叠区)
|
||||
├── tests/ # 33 项(核心 13 + agent 20)
|
||||
├── docker-compose.yml # api + postgres 两容器
|
||||
└── docs/ # 本文档
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 层 | 选型 |
|
||||
|---|---|
|
||||
| API | FastAPI + uvicorn(全 async) |
|
||||
| ORM | SQLAlchemy 2.0 async + asyncpg |
|
||||
| 数据库 | PostgreSQL 16(JSONB 存 agent 报告) |
|
||||
| HTTP | httpx(共享连接池)/ urllib(bzzoiro 同步限速) |
|
||||
| LLM | OpenAI-compatible 接口(openai/deepseek/ollama 等任一) |
|
||||
| 前端 | Vite + React 18 + TypeScript + Tailwind |
|
||||
| 测试 | pytest + pytest-asyncio(33 项,自包含) |
|
||||
| 部署 | Docker Compose(api + postgres) |
|
||||
@@ -0,0 +1,129 @@
|
||||
# 02 · 快速开始
|
||||
|
||||
## 前置条件
|
||||
|
||||
- Python 3.11+
|
||||
- Docker Desktop(跑 PostgreSQL)
|
||||
- 一个 OpenAI-compatible 的 LLM API Key(OpenAI / Deepseek / Ollama 等任一)
|
||||
- bzzoiro 数据源 Key(旧项目 MatchPro 的同一 Key)
|
||||
|
||||
## 1. 安装
|
||||
|
||||
```bash
|
||||
cd P:\Profeto
|
||||
pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
## 2. 配置环境变量
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
编辑 `.env`,至少填这三项:
|
||||
|
||||
```ini
|
||||
LLM_API_KEY=sk-xxx # 必填
|
||||
LLM_BASE_URL=https://api.openai.com/v1 # 换成你的提供商
|
||||
LLM_MODEL=gpt-4o-mini # 默认模型
|
||||
|
||||
# 可选分档(推荐):
|
||||
LLM_SPECIALIST_MODEL=gpt-4o-mini # 5 个专家用(便宜快)
|
||||
LLM_AGGREGATOR_MODEL=gpt-4o # 终裁用(强)
|
||||
|
||||
BZZOIRO_KEY=xxx # 数据采集用
|
||||
```
|
||||
|
||||
## 3. 启动数据库 + 建表
|
||||
|
||||
```bash
|
||||
docker compose up -d postgres
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
## 4. 启动服务
|
||||
|
||||
```bash
|
||||
# 后端
|
||||
uvicorn src.api.app:app --reload
|
||||
|
||||
# 前端(另开终端)
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
- 前端界面: http://localhost:5173
|
||||
- API 文档(Swagger): http://localhost:8000/docs
|
||||
|
||||
## 5. 首次跑通全流程
|
||||
|
||||
### 采集历史数据(英超近两个月为例)
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/v1/ingest/bzzoiro \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"leagues":["E0"],"date_from":"2026-08-01","date_to":"2026-09-08"}'
|
||||
```
|
||||
|
||||
数据量大时**直接拉整赛季**(约 380 场,含近几个赛季更好,近况/交锋/积分榜都需要历史):
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/v1/ingest/bzzoiro \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"leagues":["E0"],"date_from":"2025-08-01","date_to":"2026-09-08"}'
|
||||
```
|
||||
|
||||
### 回填 xG(可选,让攻防数据 agent 有数据)
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/v1/ingest/understat \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"league":"E0","season":2025}'
|
||||
```
|
||||
|
||||
### 查比赛
|
||||
|
||||
浏览器打开 http://localhost:5173 ,选"英超 / 未开赛";
|
||||
或:
|
||||
|
||||
```bash
|
||||
curl "http://localhost:8000/api/v1/matches?league=E0&status=scheduled"
|
||||
```
|
||||
|
||||
### LLM 预测
|
||||
|
||||
页面上点"LLM 预测",预测面板会展示最终结论 + 5 个专家 agent 的折叠报告(方向性评分/证据/分析);
|
||||
或:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/v1/predict \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"match_id": 1}'
|
||||
```
|
||||
|
||||
不传 `mode` 默认走多 agent;`"mode":"single"` 走单次调用旧路径(用于对比)。
|
||||
|
||||
### 赛后评估
|
||||
|
||||
比赛结束后,采集最新赛果(同一条 ingest 命令会自动把 scheduled 升级为 finished 并补比分),
|
||||
然后回填预测:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/v1/eval/settle \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"prediction_id": 1, "home_goals": 2, "away_goals": 1}'
|
||||
|
||||
# 汇总准确率(按 模型 × prompt 版本,含 multi/single 对比)
|
||||
curl http://localhost:8000/api/v1/eval/summary
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
| 问题 | 处理 |
|
||||
|---|---|
|
||||
| 连不上数据库 | `docker compose ps` 确认 postgres 健康;`.env` 的 `DATABASE_URL` 与 compose 一致 |
|
||||
| predict 返回 502 | 看 uvicorn 日志的 LLM error;确认 `LLM_BASE_URL`/`LLM_API_KEY`;`response_format` 不兼容的网关会报错(改用支持 json mode 的模型) |
|
||||
| 采集 0 场 | bzzoiro Key 失效或联赛代码写错;先 `GET /api/v1/leagues` 看库里有没有联赛 |
|
||||
| 专家报告全是 no_data | 历史数据不够 —— 近况需要每队近 5 场、积分榜需要本赛季已完赛比赛,多拉几周数据 |
|
||||
| xg agent 报无 xG 数据 | 先跑 understat 回填;注意 understat 只有五大联赛 |
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
# 03 · API 参考
|
||||
|
||||
Base URL: `http://localhost:8000` · 交互式文档: `/docs`(Swagger)与 `/redoc`
|
||||
|
||||
所有数据端点返回 JSON。错误统一为 `{"detail": "<message>"}` + 对应 HTTP 状态码。
|
||||
|
||||
---
|
||||
|
||||
## 数据查询
|
||||
|
||||
### `GET /api/v1/leagues`
|
||||
|
||||
列出已入库联赛。
|
||||
|
||||
```json
|
||||
[{"id": 1, "code": "E0", "name": "Premier League", "country": "England"}]
|
||||
```
|
||||
|
||||
### `GET /api/v1/matches`
|
||||
|
||||
比赛列表,游标分页。
|
||||
|
||||
| 参数 | 说明 |
|
||||
|---|---|
|
||||
| `league` | 联赛代码,如 `E0` / `SP1` / `D1` / `I1` / `F1` |
|
||||
| `status` | `scheduled` / `finished`(不传 = 全部) |
|
||||
| `date` | `YYYY-MM-DD`,当天比赛 |
|
||||
| `cursor` | 上一页返回的 `next_cursor` |
|
||||
| `limit` | 1–100,默认 50 |
|
||||
|
||||
响应(倒序,含双方中文名与 xG):
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{"id": 42, "league_code": "E0", "season": "2026-2027",
|
||||
"home_team": "Arsenal", "away_team": "Manchester United",
|
||||
"home_team_zh": null, "away_team_zh": null,
|
||||
"match_date": "2026-09-15T19:00:00Z", "match_status": "scheduled",
|
||||
"home_goals": null, "away_goals": null,
|
||||
"match_stage": "第 5 轮", "home_xg": null, "away_xg": null}
|
||||
],
|
||||
"next_cursor": "2026-09-15T19:00:00+00:00|41",
|
||||
"has_more": true
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/v1/matches/{id}`
|
||||
|
||||
单场比赛详情,字段同上。
|
||||
|
||||
---
|
||||
|
||||
## 预测
|
||||
|
||||
### `POST /api/v1/predict`
|
||||
|
||||
对一场比赛做 LLM 预测。**核心端点。**
|
||||
|
||||
请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"match_id": 42,
|
||||
"mode": "multi",
|
||||
"model": "gpt-4o",
|
||||
"prompt_version": "v1"
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `match_id` | 必填 | 比赛 ID |
|
||||
| `mode` | `multi` | `multi` = 5 专家 + 终裁;`single` = 单次调用 |
|
||||
| `model` | 配置值 | 覆盖本次模型(single 模式下生效) |
|
||||
| `prompt_version` | `v1` | prompt 版本(multi 模式即 agent prompt 版本) |
|
||||
|
||||
响应(multi 模式):
|
||||
|
||||
```json
|
||||
{
|
||||
"prediction_id": 7,
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o",
|
||||
"prompt_version": "multi_v1",
|
||||
"mode": "multi",
|
||||
"pred_home_goals": 2.1,
|
||||
"pred_away_goals": 1.0,
|
||||
"pred_1x2": "1",
|
||||
"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连胜"],
|
||||
"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,
|
||||
"key_evidence": [], "exp_home_goals": null, "exp_away_goals": null,
|
||||
"probable_score": null, "model": "", "latency_ms": null,
|
||||
"prompt_tokens": null, "completion_tokens": null}
|
||||
],
|
||||
"agent_weights": {"form": 0.9, "stats": 0.8, "home_away": 0.7, "injuries": 0.0, "h2h": 0.8},
|
||||
"context": "[5 份报告的 JSON 串]",
|
||||
"latency_ms": 9800
|
||||
}
|
||||
```
|
||||
|
||||
`agent.status` 取值:`ok` / `no_data`(维度无数据,已跳过 LLM)/ `error`(调用失败,fail-open 不阻断)/ `parse_error`。
|
||||
|
||||
错误:404 比赛不存在;502 LLM 调用失败(终裁失败时整体失败,专家失败不会)。
|
||||
|
||||
### `GET /api/v1/predictions?match_id=&limit=`
|
||||
|
||||
预测历史(倒序),含 `settled` 与实际比分回填状态。
|
||||
|
||||
### `GET /api/v1/predictions/{id}`
|
||||
|
||||
单条预测详情(含完整 `agent_outputs`)。
|
||||
|
||||
---
|
||||
|
||||
## 数据采集
|
||||
|
||||
### `POST /api/v1/ingest/bzzoiro`
|
||||
|
||||
从 bzzoiro 采集赛果/赛程并入库(幂等,重复跑安全)。
|
||||
|
||||
```json
|
||||
{"leagues": ["E0", "SP1"], "date_from": "2025-08-01", "date_to": "2026-09-08", "status": "finished"}
|
||||
```
|
||||
|
||||
- `status` 还可传 `scheduled` 拉未来赛程
|
||||
- 响应含每联赛 `inserted`/`updated`/`errors` 统计
|
||||
|
||||
### `POST /api/v1/ingest/understat`
|
||||
|
||||
回填 xG(只补空字段,不创建比赛):
|
||||
|
||||
```json
|
||||
{"league": "E0", "season": 2025}
|
||||
```
|
||||
|
||||
`season=2025` 表示 2025-2026 赛季。仅支持五大联赛。
|
||||
|
||||
### `POST /api/v1/ingest/injuries`
|
||||
|
||||
采集伤停(需 `API_FOOTBALL_KEY`,当前只返回计数,尚未接入 context):
|
||||
|
||||
```json
|
||||
{"date": "2026-09-10"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 评估
|
||||
|
||||
### `POST /api/v1/eval/settle`
|
||||
|
||||
赛后回填实际比分:
|
||||
|
||||
```json
|
||||
{"prediction_id": 7, "home_goals": 2, "away_goals": 1}
|
||||
```
|
||||
|
||||
### `GET /api/v1/eval/summary`
|
||||
|
||||
按 `provider × model` 聚合已结算预测:
|
||||
|
||||
```json
|
||||
{"summary": [
|
||||
{"provider": "openai", "model": "gpt-4o", "total": 12,
|
||||
"accuracy_1x2": 58.3, "avg_score_rmse": 1.21, "avg_confidence": 0.65}
|
||||
]}
|
||||
```
|
||||
|
||||
> 提示:multi 模式存的 `model` 是终裁模型、`prompt_version` 是 `multi_v1`,
|
||||
> 因此 summary 里天然可对比 multi vs single、以及不同 prompt 版本的效果。
|
||||
|
||||
---
|
||||
|
||||
## 基础
|
||||
|
||||
| 端点 | 说明 |
|
||||
|---|---|
|
||||
| `GET /health` | 存活检查 |
|
||||
| `GET /docs` | Swagger UI |
|
||||
@@ -0,0 +1,165 @@
|
||||
# 04 · 多 Agent 预测架构
|
||||
|
||||
Profeto 的核心预测路径是 **5 个领域专家 Agent 并行分析 + 1 个终裁 Agent 汇总决策**。
|
||||
每个专家只拿到自己维度的数据切片,输出结构化 JSON;终裁综合 5 份报告给出最终预测。
|
||||
|
||||
## Agent 一览
|
||||
|
||||
| Agent | 职责 | 数据切片 | 输出核心 |
|
||||
|---|---|---|---|
|
||||
| `form` 近期状态 | 分析比分与关键事件,判断近期走势 | 两队近 N 场赛果(含 xG) | `home_edge` + 走势判断 |
|
||||
| `stats` 攻防数据 | 评估进球、射门与控球,量化攻防强度 | 近 N 场进球/射门/控球/xG 统计 | `home_edge` + 攻防强度 |
|
||||
| `home_away` 主客因素 | 对比主场与客场表现,评估地理优势影响 | 主队主场战绩 + 客队客场战绩 | `home_edge` + 地理优势 |
|
||||
| `injuries` 阵容完整性 | 汇总伤停与停赛名单,评估战力缺失程度 | 伤停数据(当前无源 → no_data 门控) | `home_edge` 或 `no_data` |
|
||||
| `h2h` 历史交锋 | 分析过去数年以及近期的交手数据,提取交手规律 | 近 N 次交锋(含主客方向 + 总计统计) | `home_edge` + 交手规律 |
|
||||
| `aggregator` 终裁 | 权衡 5 份报告 → 最终结论 | 5 份结构化报告 + 比赛头信息 | 最终预测 + 各报告采信度 |
|
||||
|
||||
## 数据流
|
||||
|
||||
```
|
||||
POST /predict {match_id, mode: "multi"}
|
||||
│
|
||||
├─ load_match_header ──► MatchHeader(比赛基础信息,所有 agent 共享)
|
||||
│
|
||||
├─ asyncio.gather(并行执行 5 专家, before=match_date 防未来信息泄漏)
|
||||
│ ├─ form agent ─┐
|
||||
│ ├─ stats agent │ 每个 agent 拿到专属数据切片
|
||||
│ ├─ home_away agent │ → no_data 门控 → 调 LLM → 输出 JSON 报告
|
||||
│ ├─ injuries agent │ (无数据 → 跳过 LLM,返回 stub)
|
||||
│ └─ h2h agent ─┘
|
||||
│
|
||||
├─ aggregator agent(5 份报告 + 比赛头 → 最终 JSON)
|
||||
│
|
||||
└─ 存 predictions(mode="multi", agent_outputs JSONB)
|
||||
```
|
||||
|
||||
## 执行语义
|
||||
|
||||
### 1. 并行执行
|
||||
5 个专家通过 `asyncio.gather` 并发,总延迟 ≈ `max(专家延迟) + 终裁延迟` ≈ 2 次串行 LLM 调用。
|
||||
|
||||
### 2. no_data 门控(省 token、防幻觉)
|
||||
数据切片为空时(如伤停数据源未接入),**跳过 LLM 调用**,直接返回:
|
||||
```json
|
||||
{"agent": "injuries", "status": "no_data", "data_sufficiency": "none",
|
||||
"analysis": "该维度无数据,跳过分析。"}
|
||||
```
|
||||
终裁 Agent 会看到这个 `no_data` 状态,不会编造伤停分析。
|
||||
|
||||
### 3. fail-open(单专家失败不阻断)
|
||||
单个专家 LLM 调用失败 → 其报告标记 `status: error`,其余 4 份 + 终裁照常执行。
|
||||
只有**终裁 Agent 失败**才会整体返回 502。
|
||||
|
||||
### 4. 防未来信息泄漏
|
||||
所有切片查询都带 `before=match_date`,确保只用比赛**之前**的数据。
|
||||
这对历史回测(对已完赛比赛跑预测)尤其重要。
|
||||
|
||||
## 输出契约
|
||||
|
||||
### 专家 Agent 统一输出(JSON)
|
||||
|
||||
```json
|
||||
{
|
||||
"agent": "h2h",
|
||||
"status": "ok",
|
||||
"data_sufficiency": "high",
|
||||
"analysis": "近 5 次交锋主队 3 胜 1 平 1 负,主场交锋 3 连胜……",
|
||||
"home_edge": 0.4,
|
||||
"confidence": 0.7,
|
||||
"key_evidence": ["近5次交锋主队3胜", "主场交锋3连胜"]
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 说明 |
|
||||
|---|---|
|
||||
| `status` | `ok` / `no_data` / `error` / `parse_error` |
|
||||
| `data_sufficiency` | `high` / `medium` / `low` / `none` |
|
||||
| `home_edge` | -1.0 ~ 1.0,正数=利主队,负数=利客队 |
|
||||
| `confidence` | 0.0 ~ 1.0,该专家对自己分析的信心 |
|
||||
| `key_evidence` | 关键证据列表(最多 5 条) |
|
||||
|
||||
### 终裁 Agent 输出
|
||||
|
||||
在现有最终预测 schema 基础上新增 `agent_weights`:
|
||||
|
||||
```json
|
||||
{
|
||||
"pred_home_goals": 2.1,
|
||||
"pred_away_goals": 1.0,
|
||||
"1x2": "1",
|
||||
"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),可用于后续分析"哪个维度对预测贡献大"。
|
||||
|
||||
## 模型分档配置
|
||||
|
||||
专家用便宜快模型,终裁用强模型,各自回落 `LLM_MODEL`:
|
||||
|
||||
| 环境变量 | 说明 | 回落 |
|
||||
|---|---|---|
|
||||
| `LLM_SPECIALIST_MODEL` | 5 个专家共用模型 | `LLM_MODEL` |
|
||||
| `LLM_AGGREGATOR_MODEL` | 终裁模型 | `LLM_MODEL` |
|
||||
|
||||
示例(专家用 gpt-4o-mini,终裁用 gpt-4o):
|
||||
```bash
|
||||
LLM_MODEL=gpt-4o
|
||||
LLM_SPECIALIST_MODEL=gpt-4o-mini
|
||||
LLM_AGGREGATOR_MODEL=gpt-4o
|
||||
```
|
||||
|
||||
## Prompt 版本化
|
||||
|
||||
每个 Agent 有独立 prompt 文件,位于 `src/llm/prompts/agents/`:
|
||||
|
||||
```
|
||||
agents/
|
||||
├── form_v1.md # 近期状态专家
|
||||
├── stats_v1.md # 攻防数据专家
|
||||
├── home_away_v1.md # 主客因素专家
|
||||
├── injuries_v1.md # 阵容完整性专家
|
||||
├── h2h_v1.md # 历史交锋专家
|
||||
└── aggregator_v1.md # 终裁
|
||||
```
|
||||
|
||||
调用时传 `prompt_version: "v1"` 即加载所有 `*_v1.md`。
|
||||
迭代 prompt 时:复制 `h2h_v1.md` → `h2h_v2.md`,改内容,传 `prompt_version: "v2"`。
|
||||
`predictions.prompt_version` 存的是 `multi_v2`,与 single 模式的 `v1`/`v2` 天然分组,可在 eval summary 中 A/B 对比。
|
||||
|
||||
## 如何新增一个专家 Agent
|
||||
|
||||
三步:
|
||||
|
||||
**1. 写切片函数**(`src/llm/context_builder.py`):
|
||||
```python
|
||||
async def weather_slice(header: MatchHeader, *, before=None) -> str:
|
||||
"""天气切片示例。"""
|
||||
return "── 天气 ──\n 比赛日: 小雨 15°C"
|
||||
```
|
||||
|
||||
**2. 注册 AgentSpec**(`src/llm/agents/orchestrator.py` 的 `SPECIALIST_SPECS`):
|
||||
```python
|
||||
AgentSpec(name="weather", system_prompt="你是足球天气影响分析专家。只输出 JSON。",
|
||||
slice_fn=weather_slice)
|
||||
```
|
||||
|
||||
**3. 写 prompt 文件**(`src/llm/prompts/agents/weather_v1.md`):
|
||||
```markdown
|
||||
你是足球天气影响分析专家。分析以下天气数据对比赛的影响。
|
||||
{{context}}
|
||||
严格按此 JSON 输出……
|
||||
```
|
||||
|
||||
重启服务即生效,终裁会自动收到第 6 份报告。
|
||||
|
||||
## 与单 Agent 模式的关系
|
||||
|
||||
`mode: "single"` 走原有单次调用路径(一个大 context + 一个 prompt),用于:
|
||||
- 与 multi 模式做 A/B 基线对比
|
||||
- 快速验证(省 token)
|
||||
- 调试单个 prompt
|
||||
|
||||
eval summary 按 `prompt_version` 分组:`v1`/`v2` 是 single,`multi_v1`/`multi_v2` 是 multi,可直接对比准确率。
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
# 05 · 数据层与数据库
|
||||
|
||||
## 数据源
|
||||
|
||||
| 数据源 | 用途 | 必需 Key | 说明 |
|
||||
|---|---|---|---|
|
||||
| bzzoiro | 赛果/赛程(主源) | `BZZOIRO_KEY` | 五大联赛历史 + 实时 |
|
||||
| understat | xG 回填 | 无(公开) | 仅五大联赛,补 `match_stats.xg` |
|
||||
| api-football | 伤停 | `API_FOOTBALL_KEY` | 当前只采集计数,未接入 context |
|
||||
|
||||
### bzzoiro
|
||||
|
||||
- 端点:`/api/v2/events/`,按 `league_id` + 日期范围分页
|
||||
- 限速:`REQUEST_INTERVAL = 1.2s`,429 自动重试 3 次(轮换 key)
|
||||
- 联赛映射(`src/data/config.py`):
|
||||
```python
|
||||
BZZOIRO_LEAGUE_IDS = {"E0": 1, "SP1": 3, "D1": 5, "I1": 4, "F1": 6, "CL": 7, "EL": 8}
|
||||
```
|
||||
|
||||
### understat
|
||||
|
||||
- 端点:`/getLeagueData/{league}/{season}`,返回 JS 包裹的 JSON(需正则提取)
|
||||
- 只回填 xG(`match_stats.home_xg`/`away_xg`),**不创建新比赛**
|
||||
- 通过"天级日期 + 队名归一"匹配已有比赛
|
||||
|
||||
## 数据清洗契约
|
||||
|
||||
所有数据源统一清洗为 `NormalizedMatch`(`src/data/normalize.py`),字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `league_type` | str | 联赛代码(小写,如 `E0`) |
|
||||
| `date` | datetime | UTC 时间 |
|
||||
| `home_team` / `away_team` | str | 归一化后的规范名 |
|
||||
| `match_status` | str | `finished`/`scheduled`/... |
|
||||
| `home_goals` / `away_goals` | int? | 全场比分 |
|
||||
| `home_ht_goals` / `away_ht_goals` | int? | 半场比分 |
|
||||
| `home_xg` / `away_xg` | float? | 期望进球 |
|
||||
| `home_shots` / `away_shots` | int? | 射门 |
|
||||
| `home_shots_on_target` / `away_shots_on_target` | int? | 射正 |
|
||||
| `home_corners` / `away_corners` | int? | 角球 |
|
||||
| `home_possession` | float? | 主队控球率 |
|
||||
| `home_yellow_cards` / `away_yellow_cards` | int? | 黄牌 |
|
||||
| `home_red_cards` / `away_red_cards` | int? | 红牌 |
|
||||
| `match_stage` | str? | 轮次(如"第 5 轮") |
|
||||
| `season_label` | str | 赛季标签(如 `2026-2027`) |
|
||||
|
||||
### 校验规则(`validate()`)
|
||||
|
||||
- `finished` 无比分 → 抛错(或降级为 `scheduled`)
|
||||
- 比分 0–30,xG 0–20,射门/射正/角球 0–100,红黄牌 0–20,控球率 0–100
|
||||
- 半场比分 ≤ 全场比分
|
||||
- 无比分小数(`_to_int` 严格: `"2.8"` → `None`,不截断)
|
||||
|
||||
### 队名归一化
|
||||
|
||||
`src/data/team_names.py` 维护 `NORMALIZE_MAP`(如 `Man City` → `Manchester City`),未命中映射的队名原样返回。
|
||||
归一前先做 Unicode NFKD 去重音。
|
||||
|
||||
## 数据库 Schema
|
||||
|
||||
5 张表:
|
||||
|
||||
```sql
|
||||
-- 联赛
|
||||
CREATE TABLE leagues (
|
||||
id SERIAL PRIMARY KEY,
|
||||
code VARCHAR(20) UNIQUE NOT NULL, -- 'E0' / 'SP1'
|
||||
name VARCHAR(100) NOT NULL,
|
||||
country VARCHAR(50),
|
||||
created_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- 球队
|
||||
CREATE TABLE teams (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(120) UNIQUE NOT NULL, -- 规范名(归一后)
|
||||
name_zh VARCHAR(60), -- 中文名
|
||||
team_type VARCHAR(20) DEFAULT 'club',
|
||||
created_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- 比赛(核心)
|
||||
CREATE TABLE matches (
|
||||
id SERIAL PRIMARY KEY,
|
||||
league_id INT REFERENCES leagues(id),
|
||||
season VARCHAR(12), -- '2026-2027'
|
||||
home_team_id INT REFERENCES teams(id),
|
||||
away_team_id INT REFERENCES teams(id),
|
||||
match_date TIMESTAMPTZ NOT NULL,
|
||||
match_date_date DATE NOT NULL, -- 天级日期(去重键)
|
||||
match_status VARCHAR(20) DEFAULT 'scheduled',
|
||||
home_goals INT, away_goals INT,
|
||||
home_ht_goals INT, away_ht_goals INT,
|
||||
match_stage VARCHAR(100),
|
||||
created_at TIMESTAMPTZ, updated_at TIMESTAMPTZ
|
||||
);
|
||||
-- 唯一约束: 同联赛同对阵同天只存一场(天级去重)
|
||||
CREATE UNIQUE INDEX ix_matches_unique
|
||||
ON matches(league_id, home_team_id, away_team_id, match_date_date);
|
||||
|
||||
-- 比赛统计(xG、射门、控球等)
|
||||
CREATE TABLE match_stats (
|
||||
match_id INT PRIMARY KEY REFERENCES matches(id) ON DELETE CASCADE,
|
||||
home_xg FLOAT, away_xg FLOAT,
|
||||
home_shots INT, away_shots INT,
|
||||
home_shots_on_target INT, away_shots_on_target INT,
|
||||
home_corners INT, away_corners INT,
|
||||
home_possession FLOAT,
|
||||
home_yellow_cards INT, away_yellow_cards INT,
|
||||
home_red_cards INT, away_red_cards INT,
|
||||
updated_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- LLM 预测记录
|
||||
CREATE TABLE predictions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
match_id INT REFERENCES matches(id) ON DELETE CASCADE,
|
||||
provider VARCHAR(30) NOT NULL, -- 'openai' / 'anthropic'
|
||||
model VARCHAR(80) NOT NULL,
|
||||
prompt_version VARCHAR(20) NOT NULL DEFAULT 'v1',
|
||||
mode VARCHAR(20) NOT NULL DEFAULT 'single', -- 'single' / 'multi'
|
||||
prompt_tokens INT, completion_tokens INT,
|
||||
latency_ms INT,
|
||||
pred_home_goals FLOAT, pred_away_goals FLOAT,
|
||||
pred_1x2 VARCHAR(3), -- '1' / 'X' / '2'
|
||||
confidence FLOAT,
|
||||
reasoning TEXT,
|
||||
raw_response JSONB, -- LLM 完整原始响应
|
||||
agent_outputs JSONB, -- multi 模式: 5 份专家报告
|
||||
created_at TIMESTAMPTZ,
|
||||
actual_home_goals INT, actual_away_goals INT, -- 赛后回填
|
||||
settled BOOLEAN DEFAULT FALSE
|
||||
);
|
||||
```
|
||||
|
||||
### 关键设计点
|
||||
|
||||
1. **`match_date_date`(天级日期)**: 用于天级去重。bzzoiro 返回的时间带时分秒,精确匹配不可靠,故拆出 `DATE` 列做唯一键。
|
||||
|
||||
2. **`ix_matches_unique`**: `(league_id, home_team_id, away_team_id, match_date_date)` 唯一,保证同一场比赛重复采集时 upsert 而非插入重复行。
|
||||
|
||||
3. **`predictions` 级联删除**: `ON DELETE CASCADE`,删比赛时自动清其预测。
|
||||
|
||||
4. **`mode` + `prompt_version`**: `single` 模式存 `v1`/`v2`,`multi` 模式存 `multi_v1`/`multi_v2`,eval summary 按这两列天然分组对比。
|
||||
|
||||
## 入库语义(幂等)
|
||||
|
||||
`ingest_bzzoiro` 的 upsert 逻辑:
|
||||
|
||||
- **不存在**: 插入新比赛 + 初始 stats
|
||||
- **已存在**: 只补空字段
|
||||
- 比分:只在原记录为 `None` 时覆盖
|
||||
- 状态:只允许单向升级(`scheduled` → `finished`),防止完赛行被覆盖成赛程
|
||||
- stats:只补空(`home_xg` 已有值时不覆盖)
|
||||
|
||||
`ingest_understat` 只回填 xG(也只补空),不创建比赛。
|
||||
|
||||
## 采集建议
|
||||
|
||||
```bash
|
||||
# 1. 首次采集: 5 大联赛近 2 赛季赛果
|
||||
curl -X POST /api/v1/ingest/bzzoiro \
|
||||
-d '{"leagues":["E0","SP1","D1","I1","F1"],"date_from":"2024-08-01","date_to":"2026-09-08"}'
|
||||
|
||||
# 2. 增量采集(每日 cron): 只拉最近 7 天
|
||||
curl -X POST /api/v1/ingest/bzzoiro \
|
||||
-d '{"leagues":["E0"],"date_from":"2026-09-01","date_to":"2026-09-08"}'
|
||||
|
||||
# 3. xG 回填(可选,提升 xg agent 质量)
|
||||
curl -X POST /api/v1/ingest/understat -d '{"league":"E0","season":2025}'
|
||||
curl -X POST /api/v1/ingest/understat -d '{"league":"E0","season":2026}'
|
||||
```
|
||||
|
||||
建议用外部 cron(如系统 crontab)定时触发,不引入 worker/redis。
|
||||
@@ -0,0 +1,148 @@
|
||||
# 06 · 部署
|
||||
|
||||
## 前置要求
|
||||
|
||||
- Docker & Docker Compose
|
||||
- bzzoiro API Key(必填)
|
||||
- LLM API Key(必填,OpenAI / Deepseek / 兼容接口)
|
||||
|
||||
## Docker Compose 部署(推荐)
|
||||
|
||||
```bash
|
||||
# 1. 配置环境变量
|
||||
cp .env.example .env
|
||||
# 编辑 .env: 填 LLM_API_KEY / BZZOIRO_KEY
|
||||
|
||||
# 2. 启动(自动建表)
|
||||
docker compose up -d --build
|
||||
|
||||
# 3. 验证
|
||||
curl http://localhost:8000/health
|
||||
```
|
||||
|
||||
`docker-compose.yml` 仅 2 个服务:
|
||||
|
||||
| 服务 | 端口 | 说明 |
|
||||
|---|---|---|
|
||||
| `postgres` | 5432 | PostgreSQL 16 |
|
||||
| `api` | 8000 | FastAPI 应用 |
|
||||
|
||||
数据卷 `pgdata` 持久化数据库,重启不丢数据。
|
||||
|
||||
## 本地开发部署
|
||||
|
||||
```bash
|
||||
# 1. 安装依赖
|
||||
pip install -e ".[dev]"
|
||||
|
||||
# 2. 启动 PostgreSQL(单独)
|
||||
docker run -d --name profeto-pg \
|
||||
-e POSTGRES_USER=football -e POSTGRES_PASSWORD=football -e POSTGRES_DB=football \
|
||||
-p 5432:5432 postgres:16-alpine
|
||||
|
||||
# 3. 配置 .env
|
||||
cp .env.example .env
|
||||
|
||||
# 4. 建表
|
||||
alembic upgrade head
|
||||
|
||||
# 5. 启动 API
|
||||
uvicorn src.api.app:app --reload
|
||||
|
||||
# 6. 启动前端(另一个终端)
|
||||
cd frontend && npm install && npm run dev
|
||||
```
|
||||
|
||||
访问:
|
||||
- API 文档: http://localhost:8000/docs
|
||||
- 前端界面: http://localhost:5173
|
||||
|
||||
## 环境变量
|
||||
|
||||
| 变量 | 必需 | 默认值 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `APP_ENV` | ❌ | `development` | `production` / `development` |
|
||||
| `LOG_LEVEL` | ❌ | `INFO` | 日志级别 |
|
||||
| `DATABASE_URL` | ✅ | — | PostgreSQL 连接 URL |
|
||||
| `LLM_PROVIDER` | ❌ | `openai` | 提供商名(仅标记) |
|
||||
| `LLM_API_KEY` | ✅ | — | API Key |
|
||||
| `LLM_BASE_URL` | ❌ | `https://api.openai.com/v1` | 接口地址(Ollama/Deepseek 用) |
|
||||
| `LLM_MODEL` | ❌ | `gpt-4o` | 默认模型 |
|
||||
| `LLM_TIMEOUT` | ❌ | `60` | 单次调用超时(秒) |
|
||||
| `LLM_SPECIALIST_MODEL` | ❌ | — | 专家模型(回落 `LLM_MODEL`) |
|
||||
| `LLM_AGGREGATOR_MODEL` | ❌ | — | 终裁模型(回落 `LLM_MODEL`) |
|
||||
| `BZZOIRO_KEY` | ✅ | — | bzzoiro 数据源 Key |
|
||||
| `BZZOIRO_BASE` | ❌ | `https://sports.bzzoiro.com/api/v2` | bzzoiro 接口地址 |
|
||||
| `API_FOOTBALL_KEY` | ❌ | — | 伤停数据源 Key |
|
||||
| `CORS_ORIGINS` | ❌ | `http://localhost:5173,...` | 允许的跨域来源 |
|
||||
|
||||
## LLM 提供商配置示例
|
||||
|
||||
### OpenAI
|
||||
```bash
|
||||
LLM_API_KEY=sk-xxxx
|
||||
LLM_BASE_URL=https://api.openai.com/v1
|
||||
LLM_MODEL=gpt-4o
|
||||
```
|
||||
|
||||
### Deepseek
|
||||
```bash
|
||||
LLM_API_KEY=sk-xxxx
|
||||
LLM_BASE_URL=https://api.deepseek.com/v1
|
||||
LLM_MODEL=deepseek-chat
|
||||
```
|
||||
|
||||
### Ollama(本地)
|
||||
```bash
|
||||
LLM_API_KEY=ollama
|
||||
LLM_BASE_URL=http://localhost:11434/v1
|
||||
LLM_MODEL=llama3.1
|
||||
```
|
||||
|
||||
### 分档配置(专家用便宜模型)
|
||||
```bash
|
||||
LLM_MODEL=gpt-4o
|
||||
LLM_SPECIALIST_MODEL=gpt-4o-mini
|
||||
LLM_AGGREGATOR_MODEL=gpt-4o
|
||||
```
|
||||
|
||||
## 数据库迁移
|
||||
|
||||
Alembic 管理 schema 变更:
|
||||
|
||||
```bash
|
||||
# 查看当前版本
|
||||
alembic current
|
||||
|
||||
# 升级到最新
|
||||
alembic upgrade head
|
||||
|
||||
# 回退一级
|
||||
alembic downgrade -1
|
||||
|
||||
# 生成新迁移(改 models.py 后)
|
||||
alembic revision --autogenerate -m "描述"
|
||||
|
||||
# 空迁移(手动写 SQL)
|
||||
alembic revision -m "描述"
|
||||
```
|
||||
|
||||
已有迁移:
|
||||
- `0001_initial`: 初始 5 张表
|
||||
- `0002_agent_outputs`: predictions 加 `mode` + `agent_outputs`
|
||||
|
||||
## 备份与恢复
|
||||
|
||||
```bash
|
||||
# 备份
|
||||
docker exec profeto-postgres pg_dump -U football football > backup.sql
|
||||
|
||||
# 恢复
|
||||
cat backup.sql | docker exec -i profeto-postgres psql -U football football
|
||||
```
|
||||
|
||||
## 监控
|
||||
|
||||
- `/health`: 存活检查
|
||||
- 日志:容器 stdout(`docker compose logs -f api`)
|
||||
- 评估汇总:`GET /api/v1/eval/summary`(准确率/RMSAE/校准度)
|
||||
@@ -0,0 +1,209 @@
|
||||
# 07 · 开发指南
|
||||
|
||||
## 本地开发环境搭建
|
||||
|
||||
```bash
|
||||
# 1. 克隆并进入项目
|
||||
cd Profeto
|
||||
|
||||
# 2. 安装依赖(含 dev)
|
||||
pip install -e ".[dev]"
|
||||
|
||||
# 3. 启动 PostgreSQL
|
||||
docker run -d --name profeto-pg \
|
||||
-e POSTGRES_USER=football -e POSTGRES_PASSWORD=football -e POSTGRES_DB=football \
|
||||
-p 5432:5432 postgres:16-alpine
|
||||
|
||||
# 4. 配置环境变量
|
||||
cp .env.example .env
|
||||
# 编辑 .env 填 LLM_API_KEY / BZZOIRO_KEY
|
||||
|
||||
# 5. 建表
|
||||
alembic upgrade head
|
||||
|
||||
# 6. 启动 API(热重载)
|
||||
uvicorn src.api.app:app --reload
|
||||
|
||||
# 7. 启动前端(另一个终端)
|
||||
cd frontend && npm install && npm run dev
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
Profeto/
|
||||
├── src/ # 后端源码
|
||||
│ ├── api/
|
||||
│ │ ├── routes/ # FastAPI 路由
|
||||
│ │ │ ├── leagues.py # 联赛/比赛查询
|
||||
│ │ │ ├── predict.py # 预测入口
|
||||
│ │ │ ├── ingest.py # 数据采集
|
||||
│ │ │ └── eval.py # 评估回填
|
||||
│ │ ├── schemas.py # Pydantic 模型
|
||||
│ │ └── app.py # FastAPI 工厂
|
||||
│ ├── db/
|
||||
│ │ ├── base.py # SQLAlchemy async engine + session
|
||||
│ │ └── models.py # 5 张表 ORM
|
||||
│ ├── data/
|
||||
│ │ ├── bzzoiro.py # bzzoiro 采集 + 入库
|
||||
│ │ ├── understat.py # understat xG 回填
|
||||
│ │ ├── injuries.py # 伤停采集
|
||||
│ │ ├── normalize.py # 数据清洗契约
|
||||
│ │ ├── team_names.py # 队名归一化映射
|
||||
│ │ └── config.py # 联赛映射常量
|
||||
│ ├── llm/
|
||||
│ │ ├── provider.py # LLM 提供商抽象(OpenAI-compatible)
|
||||
│ │ ├── context_builder.py # 数据切片 + 拼接
|
||||
│ │ ├── predict.py # 预测入口(单/多模式分派)
|
||||
│ │ ├── eval.py # 评估统计
|
||||
│ │ ├── agents/
|
||||
│ │ │ ├── base.py # AgentSpec + run_agent
|
||||
│ │ │ └── orchestrator.py # 多 agent 编排
|
||||
│ │ └── prompts/
|
||||
│ │ ├── match_prediction_v1.md # 单 agent prompt
|
||||
│ │ └── agents/ # 多 agent prompt
|
||||
│ │ ├── form_v1.md
|
||||
│ │ ├── stats_v1.md
|
||||
│ │ ├── home_away_v1.md
|
||||
│ │ ├── injuries_v1.md
|
||||
│ │ ├── h2h_v1.md
|
||||
│ │ └── aggregator_v1.md
|
||||
│ └── core/
|
||||
│ └── config.py # pydantic-settings 配置
|
||||
├── frontend/ # React 单页前端
|
||||
├── alembic/ # 数据库迁移
|
||||
│ └── versions/
|
||||
│ ├── 0001_initial.py
|
||||
│ └── 0002_agent_outputs.py
|
||||
├── tests/ # 测试
|
||||
│ ├── test_core.py # 核心逻辑测试
|
||||
│ └── test_agents.py # 多 agent 测试
|
||||
├── docs/ # 本文档
|
||||
├── pyproject.toml # 依赖 + 构建配置
|
||||
├── alembic.ini # Alembic 配置
|
||||
├── Dockerfile # 生产镜像
|
||||
├── docker-compose.yml # 本地/生产编排
|
||||
└── .env.example # 环境变量模板
|
||||
```
|
||||
|
||||
## 测试
|
||||
|
||||
```bash
|
||||
# 跑全部测试
|
||||
pytest
|
||||
|
||||
# 详细输出
|
||||
pytest -v
|
||||
|
||||
# 只跑 agent 测试
|
||||
pytest tests/test_agents.py -v
|
||||
|
||||
# 覆盖率
|
||||
pytest --cov=src --cov-report=term-missing
|
||||
```
|
||||
|
||||
当前测试覆盖:
|
||||
- `test_core.py`(13 项):数据清洗、队名归一、赛季标签、LLM 解析
|
||||
- `test_agents.py`(20 项):no_data 门控、fail-open、prompt 加载、报告解析、终裁渲染
|
||||
|
||||
### 写新测试的模式
|
||||
|
||||
mock LLM 提供商(避免真实调用):
|
||||
|
||||
```python
|
||||
class MockProvider:
|
||||
model = "test-model"
|
||||
async def chat(self, system, user, **kw):
|
||||
from src.llm.provider import LLMResponse
|
||||
return LLMResponse(
|
||||
content="{}",
|
||||
parsed={"data_sufficiency": "high", "analysis": "ok",
|
||||
"home_edge": 0.5, "confidence": 0.8,
|
||||
"key_evidence": ["证据"]},
|
||||
prompt_tokens=10, completion_tokens=5, latency_ms=100,
|
||||
)
|
||||
```
|
||||
|
||||
## 常见开发任务
|
||||
|
||||
### 1. 修改 Agent Prompt
|
||||
|
||||
直接编辑 `src/llm/prompts/agents/{name}_v1.md`,无需重启(有 `lru_cache`,改完清缓存或重启)。
|
||||
|
||||
迭代版本:
|
||||
```bash
|
||||
cp src/llm/prompts/agents/h2h_v1.md src/llm/prompts/agents/h2h_v2.md
|
||||
# 编辑 h2h_v2.md
|
||||
# 调用时传 prompt_version: "v2"
|
||||
```
|
||||
|
||||
### 2. 新增 Agent
|
||||
|
||||
见 [04-agents.md § 如何新增一个专家 Agent](04-agents.md)。
|
||||
|
||||
### 3. 新增数据源
|
||||
|
||||
1. 在 `src/data/` 写采集模块(参考 `understat.py`)
|
||||
2. 在 `normalize.py` 加清洗函数
|
||||
3. 在 `context_builder.py` 加切片函数
|
||||
4. 在 `api/routes/ingest.py` 加端点
|
||||
5. 在 `api/schemas.py` 加请求/响应模型
|
||||
|
||||
### 4. 新增联赛
|
||||
|
||||
编辑 `src/data/config.py`:
|
||||
```python
|
||||
BZZOIRO_LEAGUE_IDS["新代码"] = league_id
|
||||
LEAGUE_NAMES["新代码"] = "联赛名"
|
||||
LEAGUE_COUNTRIES["新代码"] = "国家"
|
||||
```
|
||||
|
||||
### 5. 数据库 Schema 变更
|
||||
|
||||
```bash
|
||||
# 1. 改 src/db/models.py
|
||||
# 2. 生成迁移
|
||||
alembic revision --autogenerate -m "描述"
|
||||
# 3. 检查生成的迁移文件(自动推断不完美)
|
||||
# 4. 应用
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
## 前端开发
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev # 开发(热重载,代理 /api → localhost:8000)
|
||||
npm run build # 生产构建 → dist/
|
||||
npm run preview # 预览生产构建
|
||||
```
|
||||
|
||||
技术栈:React + TypeScript + Tailwind CSS + Vite。
|
||||
|
||||
主要页面:`src/pages/Matches.tsx`(比赛列表 + 预测 + agent 报告展示)。
|
||||
|
||||
## 编码约定
|
||||
|
||||
- **异步优先**:所有 IO 用 `async/await`,SQLAlchemy 用 async session
|
||||
- **session 管理**:
|
||||
- 路由读操作:依赖注入 `get_db_read`(不自动 commit)
|
||||
- 路由写操作:依赖注入 `get_db`(自动 commit)
|
||||
- 内部/ingest:直接用 `AsyncSessionLocal()` 自己管事务
|
||||
- **错误处理**:领域层抛 `ValueError`/`RuntimeError`,路由层转 HTTP 状态码
|
||||
- **日志**:用 `logging.getLogger(__name__)`,关键路径打 info/debug
|
||||
- **类型提示**:全量标注,`Mapped[T]` + `mapped_column`
|
||||
|
||||
## 提交前检查清单
|
||||
|
||||
```bash
|
||||
# 1. 测试全绿
|
||||
pytest
|
||||
|
||||
# 2. 语法检查
|
||||
python -m py_compile src/**/*.py
|
||||
|
||||
# 3. 确认 .env 不提交(已在 .gitignore)
|
||||
|
||||
# 4. 文档同步(改功能时更新 docs/)
|
||||
```
|
||||
@@ -0,0 +1,46 @@
|
||||
"""检查 injuries 数据源 api-football 的响应结构。"""
|
||||
API_FOOTBALL_INJURY_RESPONSE_EXAMPLE = """
|
||||
{
|
||||
"response": [
|
||||
{
|
||||
"player": {
|
||||
"id": 12345,
|
||||
"name": "Bukayo Saka",
|
||||
"photo": "https://...",
|
||||
"type": "Missing Fixture"
|
||||
},
|
||||
"team": {"id": 42, "name": "Arsenal"},
|
||||
"fixture": {"id": 100000, "date": "2026-09-15T19:00:00+00:00"},
|
||||
"league": {"id": 39, "name": "Premier League"},
|
||||
"reason": "Hamstring Injury",
|
||||
"type": "Missing Fixture"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
"""
|
||||
injuries 表设计:
|
||||
|
||||
CREATE TABLE injuries (
|
||||
id SERIAL PRIMARY KEY,
|
||||
player_id INT, -- api-football 球员 ID
|
||||
player_name VARCHAR(120) NOT NULL, -- 球员名
|
||||
team_id INT REFERENCES teams(id), -- 关联球队(按名归一后匹配)
|
||||
fixture_id INT, -- api-football 比赛 ID(无法直接关联 matches.id)
|
||||
league_id INT,
|
||||
injury_type VARCHAR(50), -- 'Missing Fixture' / 'Suspended'
|
||||
reason VARCHAR(200), -- 伤停原因(如 'Hamstring Injury')
|
||||
injury_date DATE, -- 伤停日期
|
||||
return_date DATE, -- 预计回归日期(如有)
|
||||
retrieved_at TIMESTAMPTZ DEFAULT now(),
|
||||
UNIQUE(player_id, fixture_id, injury_type) -- 幂等
|
||||
);
|
||||
|
||||
-- 查询某队某场比赛的伤停:
|
||||
SELECT player_name, injury_type, reason
|
||||
FROM injuries
|
||||
WHERE team_id = :team_id
|
||||
AND injury_date <= :match_date
|
||||
AND (return_date IS NULL OR return_date >= :match_date);
|
||||
"""
|
||||
@@ -0,0 +1,26 @@
|
||||
# Profeto 文档索引
|
||||
|
||||
| 文档 | 内容 |
|
||||
|---|---|
|
||||
| [01-架构总览](01-architecture.md) | 系统架构、数据流、技术选型、与旧项目对比 |
|
||||
| [02-快速开始](02-quickstart.md) | 安装、启动、首次跑通全流程 |
|
||||
| [03-API 参考](03-api.md) | 全部 12 个端点、请求/响应示例、curl 全流程 |
|
||||
| [04-多 Agent 预测](04-agents.md) | 5 专家 + 终裁架构、执行语义、输出契约、prompt 版本化、如何新增 agent |
|
||||
| [05-数据层与数据库](05-data.md) | 三数据源、清洗契约、5 张表 schema、入库语义、采集建议 |
|
||||
| [06-部署](06-deployment.md) | Docker Compose、本地部署、环境变量、LLM 提供商配置、迁移、备份 |
|
||||
| [07-开发指南](07-development.md) | 项目结构、测试、常见开发任务(prompt/agent/数据源/联赛)、前端开发 |
|
||||
|
||||
## 项目简介
|
||||
|
||||
Profeto 是一个**足球 LLM 预测服务**:FastAPI 提供干净的数据层(赛果/xG/积分榜),
|
||||
5 个领域专家 agent(近期状态/攻防数据/主客因素/阵容完整性/历史交锋)并行分析,
|
||||
终裁 agent 汇总输出结构化预测,赛后回填实际结果持续评估准确率。
|
||||
|
||||
与旧项目 MatchPro(自研 7 套统计模型 + 6 容器)相比:~45 个文件、2 容器、预测完全交给 LLM。
|
||||
|
||||
## 推荐阅读顺序
|
||||
|
||||
1. **新手**:01 → 02 → 03(跑通第一个预测)
|
||||
2. **调优 prompt**:04(理解 agent 契约)→ 03(eval summary 看效果)
|
||||
3. **加数据源/联赛**:05 → 07
|
||||
4. **上线**:06
|
||||
Reference in New Issue
Block a user