Profeto — 给 LLM 提供数据,让 LLM 预测足球比分。 核心模块: - FastAPI 后端 + PostgreSQL (SQLAlchemy async) - 多 Agent LLM 预测 (5 专家 + 终裁) - 数据采集 (bzzoiro / understat / injuries) - React 前端 (Vite + Tailwind) 包含: - 数据源抽象 (DataSource 协议 + 注册表) - Alembic 数据库迁移 - Prompt 模板 (单/多 Agent) - 核心路径单元测试
6.6 KiB
6.6 KiB
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 预测)
POST /predict {match_id}→ orchestratorload_match_header: 查比赛 + 双方 + 联赛(一次 eager load)- 5 个专家 agent 并行(
asyncio.gather),每个:- 各自的数据切片函数查库(近况/交锋/积分榜 SQL 聚合/伤停/xG)
- 切片无数据 → 跳过 LLM,直接
no_datastub(省 token、防幻觉) - 有数据 → 专属 prompt(专家模型,便宜快)→ 结构化 JSON 报告(
home_edge方向性评分 + 证据)
- 终裁 agent:5 份报告 + 比赛信息 → 权衡采信度(
agent_weights)→ 最终预测 JSON - 存
predictions表(含agent_outputs全部报告) - 赛后
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) |