- 新表 ingest_jobs(迁移 0019): id UUID/task/params JSONB/
status(pending|running|success|failed,CheckConstraint)/
result JSONB(统计摘要)/error/created_at/started_at/finished_at
- POST /ingest/bzzoiro: 启动后台前创建 pending job,响应返回 job_id;
仍 require_admin。后台 _run_bzzoiro 流转 running→success/failed,
result 按子任务(events/standings/stats)记录摘要(errors 截断 10 条)
- _update_job 尽力而为: 状态更新失败只记日志,绝不拖垮采集主流程;
与 IngestFailure 死信独立(行级 vs 任务级,可同时存在)
- 新增 admin 端点(挂 /api/v1/admin 路由,路由级 require_admin):
GET /admin/ingest/jobs/{job_id} 与 GET /admin/ingest/jobs?limit&status
- 前端采集页: 提交后凭 job_id 3 秒轮询,终态展示结果摘要/失败原因;
无 job_id 时回退旧的 30 秒盲等 + 系统日志提示
- 测试 11 项: 建 job+job_id 契约、非法 task 422、成功/失败/all 流转、
update 失败不拖垮采集、部分失败仍 success、admin 端点 200/404/列表、
结构守护(job 路由在 admin 路由且带 require_admin)
- 禁止项确认: 未动分批 UoW、BzzoiroSource、死信与 Bronze 写入;
docs(01/03/05/07/README)同步 13 张表与端点说明
294 lines
12 KiB
Markdown
294 lines
12 KiB
Markdown
# Profeto — 先知
|
|
|
|
> **给 LLM 提供结构化数据,让 LLM 预测足球比分。**
|
|
|
|
## 架构
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────┐
|
|
│ 前端 (React + Vite + Tailwind) │
|
|
│ http://localhost:5173 │
|
|
└──────────────────────┬──────────────────────────────┘
|
|
│ REST API
|
|
┌──────────────────────▼──────────────────────────────┐
|
|
│ FastAPI │
|
|
│ ├── /api/v1/matches 比赛查询(公开只读) │
|
|
│ ├── /api/v1/predict LLM 预测 (单/多 Agent) │
|
|
│ ├── /api/v1/ingest/* 数据采集(需管理员) │
|
|
│ ├── /api/v1/eval/* 评估回填(需管理员) │
|
|
│ └── /api/v1/backtest 回测(需管理员) │
|
|
└──────────┬─────────────────────────────┬────────────┘
|
|
│ │
|
|
┌──────────▼──────────┐ ┌─────────────▼────────────┐
|
|
│ PostgreSQL │ │ LLM (OpenAI-compatible) │
|
|
│ 13 张表 │ │ OpenAI / Deepseek / │
|
|
│ leagues/teams/ │ │ Ollama / 任意网关 │
|
|
│ matches/match_ │ └──────────────────────────┘
|
|
│ stats/standings/ │
|
|
│ predictions/ │
|
|
│ app_settings/ │
|
|
│ schedules + │
|
|
│ raw_events 等 4 张 │
|
|
│ 数据治理表 │
|
|
└─────────────────────┘
|
|
▲
|
|
│ 采集
|
|
┌──────────┴─────────────────────────────────────────┐
|
|
│ 数据源 (DataSource 协议 + 注册表) │
|
|
│ └── bzzoiro 比分 / 赛程 / 统计 / 积分榜 │
|
|
└────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### 分层架构
|
|
|
|
```
|
|
API Route → Application Service → Repository → UnitOfWork → DB
|
|
```
|
|
|
|
- **UnitOfWork**: 统一事务边界,业务层不再自行 commit
|
|
- **Repository**: 封装数据访问,提供类型化查询接口
|
|
- **DataSource**: 采集外部数据,通过注册表动态分发
|
|
|
|
### 多 Agent 预测
|
|
|
|
默认模式 (`mode=multi`) 采用 **5 专家 + 终裁** 架构:
|
|
|
|
```
|
|
比赛数据 → 切片 ─┬─→ A 近期状态专家 ─┐
|
|
├─→ B 攻防数据专家 ─┤
|
|
├─→ C 主客因素专家 ─┼─→ 终裁专家 ─→ 最终预测
|
|
├─→ D 联赛排名专家 ─┤
|
|
└─→ E 历史交锋专家 ─┘
|
|
```
|
|
|
|
- 各专家**只看到自己维度的数据切片**,避免信息过载
|
|
- **fail-open**: 单个专家失败不影响整体
|
|
- **no_data 门控**: 无数据维度跳过 LLM 调用,省 token 防幻觉
|
|
- 终裁根据各报告的 `subjective_confidence` / `data_sufficiency` 输出 `agent_weights`
|
|
|
|
### 数据正确性保障
|
|
|
|
- **Cutoff 机制**: 回测时只使用 `cutoff_at` 之前已采集的数据(近况/交锋/统计/积分榜切片统一生效)
|
|
- **LLM 输出校验**: Pydantic 严格校验 + 语义一致性检查
|
|
- **数据库约束**: CHECK 约束作为最后一道防线
|
|
|
|
## 快速开始
|
|
|
|
### 前置条件
|
|
|
|
- Python >= 3.11
|
|
- Docker (运行 PostgreSQL)
|
|
- LLM API Key (OpenAI / Deepseek / Ollama 等)
|
|
|
|
### 方式一:Docker Compose 部署(推荐)
|
|
|
|
```bash
|
|
# 1. 克隆仓库
|
|
git clone https://git.bilidili.cn/shangfangjian/Profeto.git
|
|
cd Profeto
|
|
|
|
# 2. 配置环境变量
|
|
cp .env.example .env
|
|
# 编辑 .env,填入 LLM_API_KEY 和 BZZOIRO_KEY
|
|
|
|
# 3. 启动全部服务(自动构建 + 执行迁移)
|
|
docker compose up -d --build
|
|
|
|
# 4. 验证
|
|
curl http://localhost:8000/health
|
|
```
|
|
|
|
启动后访问:
|
|
- API 文档: http://localhost:8000/docs
|
|
- 前端界面: http://localhost:3000
|
|
|
|
> **说明**: `api` 容器启动时自动执行 `alembic upgrade head`,无需手动运行迁移。
|
|
|
|
### 方式二:本地开发部署
|
|
|
|
```bash
|
|
# 1. 克隆 + 安装
|
|
git clone https://git.bilidili.cn/shangfangjian/Profeto.git
|
|
cd Profeto
|
|
pip install -e ".[dev]"
|
|
|
|
# 2. 配置环境变量
|
|
cp .env.example .env
|
|
# 编辑 .env,填入 LLM_API_KEY 和 BZZOIRO_KEY
|
|
|
|
# 3. 启动 PostgreSQL
|
|
docker compose up -d postgres
|
|
|
|
# 4. 执行迁移
|
|
alembic upgrade head
|
|
|
|
# 5. 启动后端 (终端 1)
|
|
uvicorn src.api.app:app --reload
|
|
|
|
# 6. 启动前端 (终端 2)
|
|
cd frontend && npm install && npm run dev
|
|
```
|
|
|
|
后端运行在 `http://localhost:8000`,前端在 `http://localhost:5173`。
|
|
|
|
## 生产上线检查清单
|
|
|
|
公网部署前逐项确认(第 1–4 项由启动校验强制,不满足拒绝启动;详见 [docs/06-deployment.md](docs/06-deployment.md#生产上线检查清单)):
|
|
|
|
- [ ] `APP_ENV=production`(安全校验 / Cookie `Secure` / 管理端点 fail-closed 的总开关)
|
|
- [ ] `SECRET_KEY` 强随机:`openssl rand -base64 32`,禁止弱值
|
|
- [ ] `ADMIN_PASSWORD` 或 `ADMIN_API_KEY` 至少配置其一
|
|
- [ ] 数据库强密码,禁止 `football:football` 等示例弱密码
|
|
- [ ] HTTPS(反代终结 TLS;production 下会话 Cookie 自动 `Secure`)
|
|
- [ ] 反代后设 `TRUST_PROXY_HEADERS=True`,仅可信反代可达 API,并配置 `X-Forwarded-For` / `X-Real-IP`
|
|
- [ ] 限流前置到 Nginx `limit_req`;应用内限流与 KeyRing 仅单进程有效,多 worker 会放大配额
|
|
- [ ] uvicorn 单 worker(默认);需扩容先网关统一限流再起多实例
|
|
- [ ] 启动后验证 `/health` 与 `/health/ready` 均 200
|
|
- [ ] 数据库迁移已内置:compose/Dockerfile 启动即执行 `alembic upgrade head`
|
|
|
|
## 安全与限流
|
|
|
|
- `/api/v1/predict`: 内存滑动窗口限流(10 次/分钟/IP),多 worker 时每进程独立计数
|
|
- 登录防爆破: 进程内内存计数,同上
|
|
- 公网部署建议 Nginx 层限流 + `TRUST_PROXY_HEADERS=True`
|
|
- 生产环境必须配置 `ADMIN_PASSWORD` 或 `ADMIN_API_KEY`(否则管理接口 503)
|
|
|
|
详见 [docs/06-deployment.md](docs/06-deployment.md#安全与限流)。
|
|
|
|
## API 概览
|
|
|
|
**公开只读**(无需登录;`predict` 带内存限流):
|
|
|
|
| 方法 | 路径 | 说明 |
|
|
|---|---|---|
|
|
| GET | `/api/v1/leagues` | 联赛列表(仅 id/code/name/country) |
|
|
| GET | `/api/v1/matches` | 比赛查询(筛选/游标分页) |
|
|
| GET | `/api/v1/matches/{id}` | 比赛详情(含统计与最近预测) |
|
|
| GET | `/api/v1/matches/{id}/context` | 比赛上下文(双方近况 + 历史交锋) |
|
|
| GET | `/api/v1/standings` | 联赛积分榜 |
|
|
| POST | `/api/v1/predict` | LLM 预测 (`mode=single`/`multi`/`baseline`) |
|
|
| GET | `/health`、`/health/ready` | 存活 / 就绪检查(含 DB) |
|
|
|
|
**需管理员**(Cookie 会话或 `X-API-Key`):
|
|
|
|
| 方法 | 路径 | 说明 |
|
|
|---|---|---|
|
|
| POST | `/api/v1/ingest/bzzoiro` | 采集赛果/赛程/统计/积分榜 |
|
|
| GET | `/api/v1/predictions` | 预测历史(列表) |
|
|
| GET | `/api/v1/predictions/{id}` | 单条预测详情 |
|
|
| POST | `/api/v1/eval/settle` | 回填实际结果 |
|
|
| GET | `/api/v1/eval/summary` | 准确率汇总 |
|
|
| POST | `/api/v1/backtest` | 历史回测 |
|
|
| `/api/v1/admin/**` | 配置/采集状态/日志/定时任务/死信等 | 管理后台(router 级鉴权) |
|
|
|
|
## 项目结构
|
|
|
|
```
|
|
Profeto/
|
|
├── src/
|
|
│ ├── api/ # FastAPI 路由层
|
|
│ │ ├── app.py # 应用工厂 + lifespan
|
|
│ │ ├── deps.py # 依赖注入:鉴权 / 限流
|
|
│ │ ├── schemas.py # Pydantic 请求/响应模型
|
|
│ │ └── routes/
|
|
│ │ ├── matches.py # 比赛查询(公开只读)
|
|
│ │ ├── predict.py # 预测入口 + 预测历史
|
|
│ │ ├── ingest.py # 数据采集(需管理员)
|
|
│ │ ├── eval.py # 评估回填(需管理员)
|
|
│ │ ├── backtest.py # 回测(需管理员)
|
|
│ │ ├── auth.py # 登录/登出/改密
|
|
│ │ ├── admin_settings.py # /admin/** 配置/日志/数据质量(router 级鉴权)
|
|
│ │ └── schedules.py # 定时任务 + 死信重试(router 级鉴权)
|
|
│ ├── core/ # 基础设施
|
|
│ │ ├── config.py # pydantic-settings 配置
|
|
│ │ ├── crypto.py # 加密/哈希
|
|
│ │ ├── http_client.py # 共享 httpx 客户端
|
|
│ │ ├── log_buffer.py # 内存日志缓冲(admin 日志页)
|
|
│ │ ├── runtime_config.py # DB 配置覆盖(.env → app_settings)
|
|
│ │ ├── scheduler.py # 进程内 cron 调度器
|
|
│ │ └── security_check.py # 启动安全校验
|
|
│ ├── data/ # 数据层
|
|
│ │ ├── sources.py # DataSource 协议 + 注册表
|
|
│ │ ├── bzzoiro.py # bzzoiro 数据源(events/standings/stats)
|
|
│ │ ├── normalize.py # 数据规范化契约
|
|
│ │ ├── config.py # 联赛映射常量
|
|
│ │ ├── key_ring.py # API Key 轮换环(429 冷却)
|
|
│ │ ├── team_names.py # 队名归一化
|
|
│ │ └── team_names_zh.py # 队名中文映射
|
|
│ ├── db/ # 数据库
|
|
│ │ ├── base.py # SQLAlchemy async engine
|
|
│ │ ├── models.py # ORM 模型 (12 表)
|
|
│ │ ├── unit_of_work.py # UnitOfWork 事务封装
|
|
│ │ └── repositories.py # Repository 数据访问
|
|
│ └── llm/ # LLM 预测核心
|
|
│ ├── predict.py # 预测服务 (缓存 + 单/多/基线模式)
|
|
│ ├── context_builder.py # 数据切片 + 上下文拼接
|
|
│ ├── baseline.py # 基线预测(均值模型)
|
|
│ ├── eval.py # 评估统计
|
|
│ ├── backtest.py # 回测框架
|
|
│ ├── provider.py # 多提供商 LLM 抽象
|
|
│ ├── utils.py # LLM 工具函数
|
|
│ ├── validation.py # LLM 输出校验
|
|
│ ├── agents/
|
|
│ │ ├── base.py # Agent 基础设施 + 解析
|
|
│ │ └── orchestrator.py # 多 Agent 编排
|
|
│ └── prompts/ # Prompt 模板
|
|
├── alembic/ # 数据库迁移
|
|
├── frontend/ # React 前端
|
|
│ └── src/
|
|
│ ├── pages/ # 公开站(赛程 Matches + 积分榜 Standings)
|
|
│ ├── admin/ # 管理后台(布局/页面/数据访问层 dal.ts)
|
|
│ ├── components/ # 共享组件
|
|
│ └── lib/http.ts # 唯一 HTTP 实现(带凭据/超时/错误处理)
|
|
├── docs/ # 详细文档
|
|
├── tests/ # 单元测试
|
|
├── docker-compose.yml
|
|
├── Dockerfile
|
|
└── pyproject.toml
|
|
```
|
|
|
|
## 核心模块
|
|
|
|
| 模块 | 作用 |
|
|
|---|---|
|
|
| `context_builder.py` | **最重要**: 数据切片 + 拼接 LLM 看到的上下文 |
|
|
| `prompts/` | Prompt 模板 (迭代最频繁) |
|
|
| `provider.py` | OpenAI-compatible 多提供商抽象 |
|
|
| `sources.py` | 数据源协议 + 注册表 |
|
|
| `unit_of_work.py` | 统一事务边界 |
|
|
| `repositories.py` | 数据访问封装 |
|
|
| `validation.py` | LLM 输出严格校验 |
|
|
| `backtest.py` | 回测框架(防未来数据泄漏) |
|
|
|
|
## 配置
|
|
|
|
通过 `.env` 或环境变量配置:
|
|
|
|
| 变量 | 说明 | 默认值 |
|
|
|---|---|---|
|
|
| `DATABASE_URL` | PostgreSQL 连接 | `postgresql+asyncpg://football:football@localhost:5432/football` |
|
|
| `LLM_PROVIDER` | 提供商标识 | `openai` |
|
|
| `LLM_API_KEY` | API 密钥 | *(必填)* |
|
|
| `LLM_BASE_URL` | API 地址 | `https://api.openai.com/v1` |
|
|
| `LLM_MODEL` | 模型名 | `gpt-4o` |
|
|
| `LLM_SPECIALIST_MODEL` | 专家模型 (空=回落 LLM_MODEL) | |
|
|
| `LLM_AGGREGATOR_MODEL` | 终裁模型 (空=回落 LLM_MODEL) | |
|
|
| `BZZOIRO_KEY` | bzzoiro API Key | *(必填)* |
|
|
| `CORS_ORIGINS` | 允许的跨域来源 | `http://localhost:5173` |
|
|
|
|
## 测试
|
|
|
|
```bash
|
|
pytest
|
|
```
|
|
|
|
## 数据库迁移
|
|
|
|
```bash
|
|
alembic upgrade head
|
|
```
|
|
|
|
## License
|
|
|
|
个人研究项目,预测结果不构成投注建议。
|