回归测试(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),
避免「看似有重试实则未生效」的误判。
223 lines
7.1 KiB
Markdown
223 lines
7.1 KiB
Markdown
# 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 路由
|
|
│ │ │ ├── matches.py # 联赛/比赛查询
|
|
│ │ │ ├── predict.py # 预测入口
|
|
│ │ │ ├── ingest.py # 数据采集
|
|
│ │ │ ├── eval.py # 评估回填
|
|
│ │ │ └── backtest.py # 历史回测
|
|
│ │ ├── deps.py # 依赖:管理接口鉴权(X-API-Key)
|
|
│ │ ├── schemas.py # Pydantic 模型
|
|
│ │ └── app.py # FastAPI 工厂
|
|
│ ├── db/
|
|
│ │ ├── base.py # SQLAlchemy async engine + session
|
|
│ │ ├── models.py # 6 张表 ORM
|
|
│ │ ├── repositories.py # 仓储层(查询封装)
|
|
│ │ └── unit_of_work.py # 事务边界
|
|
│ ├── data/
|
|
│ │ ├── bzzoiro.py # bzzoiro 采集 + 入库
|
|
│ │ ├── understat.py # understat xG 回填
|
|
│ │ ├── injuries.py # 伤停采集
|
|
│ │ ├── normalize.py # 数据清洗契约
|
|
│ │ ├── team_names.py # 队名归一化映射
|
|
│ │ ├── sources.py # 数据源注册表
|
|
│ │ └── config.py # 联赛映射常量
|
|
│ ├── llm/
|
|
│ │ ├── provider.py # LLM 提供商抽象(OpenAI-compatible)
|
|
│ │ ├── context_builder.py # 数据切片 + 拼接
|
|
│ │ ├── predict.py # 预测入口(单/多模式分派)
|
|
│ │ ├── eval.py # 评估统计
|
|
│ │ ├── validation.py # LLM 输出严格校验
|
|
│ │ ├── backtest.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 配置
|
|
│ ├── http_client.py # 共享 httpx 客户端
|
|
│ └── retry.py # 重试工具
|
|
├── frontend/ # React 单页前端
|
|
├── alembic/ # 数据库迁移
|
|
│ └── versions/
|
|
│ ├── 0001_initial.py
|
|
│ ├── 0002_agent_outputs.py
|
|
│ ├── 0003_injuries.py
|
|
│ ├── 0004_snapshot_and_constraints.py
|
|
│ ├── 0005_prediction_status_and_stats_provenance.py
|
|
│ └── 0006_schema_model_drift_cleanup.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, "subjective_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/)
|
|
```
|