Files
Profeto/docs/01-architecture.md
WorkBuddy f965650c10 test+docs: P0 回归测试与文档同步
回归测试(P0 三项,不依赖 DB 的静态断言):
- tests/test_regressions.py:
  · context_builder 三个查询函数 + backtest 必须 eager-load 关系
  · Match.{stats,home_team,away_team,league} 必须 lazy="selectin"
  · bzzoiro normalized_matches 必须携带并解包 (nm, raw) 配对
  这些是「集成方式」缺陷,原先的单元测试(全 mock slice_fn/provider)
  照不出来,故此专项静态锁定。

文档同步(修正与实际不符的表述):
- docs/01-architecture.md: app.py 标注改为「lifespan 仅验证连接,不建表」;
  补 deps.py / repositories.py / unit_of_work.py / validation.py /
  backtest.py / sources.py / http_client.py / retry.py;迁移范围改 0001~0006
- docs/07-development.md: 目录树修正 leagues.py→matches.py,补齐
  backtest.py / deps.py 及 0003~0006 迁移文件

附带: src/core/retry.py 加 NOTE 说明其当前无调用点(P3),
避免「看似有重试实则未生效」的误判。
2026-09-15 16:55:54 +08:00

7.3 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 (6 张表) │        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 仅验证 DB 连接,不建表)
│   │   ├── deps.py              # 依赖:管理接口鉴权(X-API-Key)
│   │   ├── schemas.py           # Pydantic v2 请求/响应
│   │   └── routes/
│   │       ├── matches.py       # 联赛/比赛查询(游标分页)
│   │       ├── predict.py       # 预测 + 预测历史
│   │       ├── ingest.py        # 采集触发(自管 session,需鉴权)
│   │       ├── eval.py          # 赛后回填 + 准确率汇总
│   │       └── backtest.py      # 历史回测(需鉴权)
│   ├── db/
│   │   ├── base.py              # async engine + get_db/get_db_read
│   │   ├── models.py            # 6 张表 ORM
│   │   ├── repositories.py      # 仓储层
│   │   └── unit_of_work.py      # 事务边界
│   ├── data/
│   │   ├── bzzoiro.py           # 赛果采集 + 幂等入库
│   │   ├── understat.py         # xG 回填
│   │   ├── injuries.py          # 伤停采集(带文件缓存)
│   │   ├── normalize.py         # NormalizedMatch 清洗契约
│   │   ├── team_names.py        # 队名归一映射
│   │   ├── sources.py           # 数据源注册表
│   │   └── config.py            # 联赛代码映射
│   ├── llm/
│   │   ├── provider.py          # OpenAI-compatible 抽象(共享连接池/JSON 兜底解析)
│   │   ├── context_builder.py   # 数据切片(h2h/form/stats/home_away/injuries)+ 单 agent 拼接
│   │   ├── predict.py           # 预测入口(mode 分派 + 缓存)
│   │   ├── eval.py              # 准确率统计
│   │   ├── validation.py        # LLM 输出严格校验(Pydantic)
│   │   ├── backtest.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
│       ├── http_client.py       # 共享 httpx 客户端
│       └── retry.py             # 重试工具
├── alembic/versions/            # 0001~0006(0001 建表 → 0006 漂移清理)
├── frontend/src/pages/Matches.tsx  # 单页(预测面板 + 专家报告折叠区 + 游标分页)
├── tests/                       # 核心 + agent 测试
├── 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)