Files
Profeto/docs/01-architecture.md
T
WorkBuddy 983363dab7 feat(ingest): 采集任务状态跟踪(ingest_jobs),解决 fire-and-forget 不可观测
- 新表 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 张表与端点说明
2026-09-21 21:19:21 +08:00

8.1 KiB

01 · 架构总览

系统架构

┌─────────────────────────────────────────────────────┐
│  前端(单页 React + Vite + Tailwind)                   │
│  比赛列表 → 选比赛 → LLM 预测 → 专家报告 + 最终结论     │
└──────────────────────────▲──────────────────────────┘
                           │ REST (/api/v1)
┌──────────────────────────┴──────────────────────────┐
│  FastAPI(单进程,全 async)                            │
│                                                     │
│  数据查询   预测编排          采集(手动/cron 触发)    │
│  ┌──────┐  ┌────────────┐  ┌───────────────────┐    │
│  │matches│  │ orchestrator│  │ bzzoiro (唯一源)  │    │
│  │leagues│  │  ┌─ 5 专家并行(便宜模型)            │    │
│  └──┬───┘  │  │   h2h / form / stats /         │    │
│     │      │  │   home_away / standings        │    │
│     │      │  └─ aggregator 终裁(强模型)        │    │
│     │      └────────────┘  │ events / standings │    │
│  ┌──┴──────────────┴──┐   │ /stats 三条管线     │    │
│  │ PostgreSQL (13 张表)│   └───────────────────┘    │
│  └────────────────────┘        httpx → 外部 API     │
└─────────────────────────────────────────────────────┘

核心数据流(一次多 Agent 预测)

  1. POST /predict {match_id} → orchestrator
  2. load_match_header: 查比赛 + 双方 + 联赛(一次 eager load)
  3. 5 个专家 agent 并行(asyncio.gather),每个:
    • 各自的数据切片函数查库(近况/交锋/积分榜聚合/射门控球/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:迁移校验/定时任务/生产限流提醒)
│   │   ├── deps.py              # 依赖:管理接口鉴权(Cookie/X-API-Key)+ 限流
│   │   ├── schemas.py           # Pydantic v2 请求/响应
│   │   └── routes/
│   │       ├── matches.py       # 联赛/比赛/上下文/积分榜(公开只读)
│   │       ├── predict.py       # 预测(限流)+ 预测历史(需鉴权)
│   │       ├── ingest.py        # 采集触发(需鉴权)
│   │       ├── eval.py          # 赛后回填 + 准确率汇总(需鉴权)
│   │       ├── backtest.py      # 历史回测(需鉴权)
│   │       ├── auth.py          # 登录/登出/改密
│   │       ├── admin_settings.py # /admin/** 配置/日志/数据质量(router 级鉴权)
│   │       └── schedules.py     # 定时任务 + 死信重试(router 级鉴权)
│   ├── db/
│   │   ├── base.py              # async engine + get_db/get_db_read
│   │   ├── models.py            # 13 张表 ORM
│   │   ├── repositories.py      # 仓储层
│   │   └── unit_of_work.py      # 事务边界
│   ├── data/
│   │   ├── bzzoiro.py           # 唯一数据源:events/standings/stats 三管线 + Bronze 层
│   │   ├── normalize.py         # NormalizedMatch 清洗契约
│   │   ├── team_names.py        # 队名归一映射
│   │   ├── team_names_zh.py     # 队名中文名映射
│   │   ├── key_ring.py          # 多 key 轮换(429 冷却,进程内)
│   │   ├── sources.py           # 数据源注册表
│   │   └── config.py            # 联赛代码映射
│   ├── llm/
│   │   ├── provider.py          # OpenAI-compatible 抽象(共享连接池/JSON 兜底解析)
│   │   ├── context_builder.py   # 数据切片(h2h/form/stats/home_away/standings)+ 单 agent 拼接
│   │   ├── predict.py           # 预测入口(mode 分派 + 缓存)
│   │   ├── baseline.py          # 基线预测(均值模型,mode=baseline)
│   │   ├── eval.py              # 准确率统计
│   │   ├── utils.py             # LLM 工具函数
│   │   ├── validation.py        # LLM 输出严格校验(Pydantic)
│   │   ├── backtest.py          # 回测执行
│   │   ├── agents/
│   │   │   ├── base.py          # AgentSpec / AgentReport / run_agent
│   │   │   └── orchestrator.py  # 并行专家 → 终裁 → 存库
│   │   └── prompts/
│   │       ├── match_prediction_v1/v2.md   # 单 agent 模板
│   │       └── agents/{form,stats,home_away,standings,h2h,aggregator}_v1.md
│   └── core/
│       ├── config.py            # pydantic-settings
│       ├── crypto.py            # 加密/哈希
│       ├── http_client.py       # 共享 httpx 客户端
│       ├── log_buffer.py        # 内存日志缓冲(admin 日志页)
│       ├── runtime_config.py    # DB 配置覆盖(app_settings)
│       ├── scheduler.py         # 进程内 cron 调度器
│       └── security_check.py    # 启动安全校验
├── alembic/versions/            # 0001~0018(建表 → Bronze 层 → 单一数据源 → 基线模式等)
├── frontend/src/                # pages/(公开站) + admin/(管理后台) + lib/http.ts(唯一 HTTP 实现)
├── tests/                       # 核心 + agent 测试(250+ 项,自包含)
├── 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(250+ 项,自包含)
部署 Docker Compose(api + postgres)