AI/ML Engineering Conventions
Rules for training pipelines, inference services, and LLM features in the house Python
stack. Models are served through FastAPI services (framework conventions in the
std-fastapi skill); general Python layout, typing, and layering live in std-python.
Stack
| Concern |
Library |
| Dataframes |
polars for new pipelines; pandas acceptable in existing code |
| Numerics |
numpy |
| Classical ML |
scikit-learn — baseline first (see below) |
| Deep learning |
PyTorch |
| Pretrained models |
transformers |
| Experiment tracking |
MLflow — tracking AND the model registry |
| Dataframe contracts |
pandera — validate at pipeline boundaries |
| Portable CPU inference |
onnxruntime |
| Embeddings store |
pgvector on the house PostgreSQL |
| LLM features |
the anthropic SDK |
- Ship a linear/tree baseline with a held-out eval before any deep learning — the
baseline tells you whether the problem needs a neural net at all, and the eval harness
you build for it outlives the baseline: every later model is judged on the same held-out
set.
- Use pgvector before adding a dedicated vector database — the ops burden of a new
datastore needs a proven ceiling first. Move only when measured recall or latency on
real corpus size demands it.
- Validate dataframes with pandera schemas at every pipeline boundary — a silent schema
drift upstream becomes a silent quality drop downstream.
Notebooks vs Modules
notebooks/ is for exploration only — plots, one-off analysis, scratch work.
- Promote production logic into
src/ modules and import it into the notebook —
never import from a notebook: notebooks have no tests, no types, no review gate,
and hidden execution-order state.
- Strip outputs before commit (
nbstripout as a git filter) — outputs bloat diffs and
leak data samples into the repo.
Reproducibility
- The uv lockfile pins the environment — a training run is only reproducible if its
dependencies are.
- Seed
random, numpy, and torch in one set_seed(seed) helper called at process start —
scattered seeding is unverifiable seeding.
- Training runs are config-driven via pydantic models (one
TrainingConfig, loaded from a
file or CLI) — no hand-edited hyperparameters buried in code; the config is logged with
the run, so the run can be re-created from it.
- Never train against a mutable "latest" table — datasets are versioned snapshots
(DVC, or versioned S3 parquet paths). A model trained on data nobody can reproduce is a
model nobody can debug.
Experiment Tracking (MLflow)
- Every training run logs params, metrics, and artifacts to MLflow — an unlogged run may
as well not have happened.
- Promote models through registry aliases (
@staging → @production), never by copying
artifacts around by hand — the registry is the single source of "what is deployed".
- No model ships without a recorded eval on a pinned dataset — a metric on an
unpinned dataset cannot be compared to the previous model's, so it cannot justify a
promotion.
Serving (FastAPI)
- Load the model once in the FastAPI lifespan — never per-request: model load is
seconds and hundreds of MB; per-request loading turns every call into a cold start.
- Pin the model artifact version in config (a registry alias or explicit version) — a
service that loads "latest" changes behavior without a deploy.
- The health endpoint reports the loaded model version — so an operator can see at a
glance which model each instance is actually serving.
- Set request timeouts and a max payload size — inference endpoints are a
denial-of-service magnet without both.
- Watch p95 inference latency — dashboards and alerting conventions are owned by
std-monitoring.
LLM Integration (anthropic SDK)
- Model choice:
claude-sonnet-5 by default for product features;
claude-haiku-4-5-20251001 for cheap high-volume classification; claude-opus-5 when
reasoning depth is the product. Pin the model id in config, not inline.
- Prompts are versioned code — files in the repo, reviewed in PRs — not database strings:
a prompt change is a behavior change and gets the same diff, review, and rollback.
- Structured outputs come from tool use, not from parsing prose — a tool schema is a
contract; a regex over prose is a hope.
- A pinned eval set runs in CI before a prompt or model change ships — LLM behavior
shifts across prompts and model versions, and an eval is the only regression test that
catches it.
- Retry transient API errors with exponential backoff and a capped attempt count (the
SDK's built-in retries or
tenacity).
- Never log full prompts or completions containing user PII — log request ids, token
counts, latency, and model id instead; logging rules are owned by
std-monitoring.
- Give every LLM feature a cost budget (per-request and monthly) and review it like a
performance budget — token spend regresses as silently as latency does.
Embeddings (pgvector)
- Pin the embedding model and its dimension together in config — vectors from different
models share a column type but not a space; mixing them silently ruins retrieval.
- Index with HNSW by default — better recall/latency than IVFFlat at house corpus sizes.
- Changing the embedding model means re-embedding the entire corpus — plan it as a
migration (dual-write or backfill, then cut over), not an in-place swap.
Rollout
- New models ship behind a flag with shadow or canary traffic first — offline eval
numbers do not guarantee online behavior.
- Monitor input drift and prediction distributions in production; alert on divergence
from the training distribution — models fail silently by degrading, not by crashing.
Related, owned elsewhere — do not duplicate: the JSON error envelope and pagination
response format live in std-api-design; migration safety and indexing depth in
std-database; OWASP and secret management in std-security; structured logging and
PII-in-logs rules in std-monitoring; AAA and coverage targets in std-testing; general
Python layout, typing, and layering in std-python; ORM query performance in
std-python-performance; FastAPI framework specifics in std-fastapi.
1---2name: std-python-ai-ml3description: AI/ML engineering conventions — notebooks vs modules, MLflow experiment tracking, reproducibility, FastAPI model serving, pgvector embeddings, LLM (Anthropic) integration, evals. Use when writing training pipelines, inference services, or LLM features.4paths:5 - "**/*.ipynb"6 - "**/ml/**/*.py"7 - "**/notebooks/**"8 - "**/training/**/*.py"9 - "**/inference/**/*.py"10 - "**/pipelines/**/*.py"11---1213# AI/ML Engineering Conventions1415Rules for training pipelines, inference services, and LLM features in the house Python16stack. Models are served through FastAPI services (framework conventions in the17`std-fastapi` skill); general Python layout, typing, and layering live in `std-python`.1819## Stack2021| Concern | Library |22|---------|---------|23| Dataframes | **polars** for new pipelines; pandas acceptable in existing code |24| Numerics | numpy |25| Classical ML | scikit-learn — **baseline first** (see below) |26| Deep learning | PyTorch |27| Pretrained models | transformers |28| Experiment tracking | MLflow — tracking AND the model registry |29| Dataframe contracts | pandera — validate at pipeline boundaries |30| Portable CPU inference | onnxruntime |31| Embeddings store | pgvector on the house PostgreSQL |32| LLM features | the `anthropic` SDK |3334- **Ship a linear/tree baseline with a held-out eval before any deep learning** — the35 baseline tells you whether the problem needs a neural net at all, and the eval harness36 you build for it outlives the baseline: every later model is judged on the same held-out37 set.38- **Use pgvector before adding a dedicated vector database** — the ops burden of a new39 datastore needs a proven ceiling first. Move only when measured recall or latency on40 real corpus size demands it.41- Validate dataframes with pandera schemas at every pipeline boundary — a silent schema42 drift upstream becomes a silent quality drop downstream.4344## Notebooks vs Modules4546- `notebooks/` is for exploration only — plots, one-off analysis, scratch work.47- Promote production logic into `src/` modules and import it into the notebook —48 **never import from a notebook**: notebooks have no tests, no types, no review gate,49 and hidden execution-order state.50- Strip outputs before commit (`nbstripout` as a git filter) — outputs bloat diffs and51 leak data samples into the repo.5253## Reproducibility5455- The uv lockfile pins the environment — a training run is only reproducible if its56 dependencies are.57- Seed `random`, numpy, and torch in one `set_seed(seed)` helper called at process start —58 scattered seeding is unverifiable seeding.59- Training runs are config-driven via pydantic models (one `TrainingConfig`, loaded from a60 file or CLI) — no hand-edited hyperparameters buried in code; the config is logged with61 the run, so the run can be re-created from it.62- **Never train against a mutable "latest" table** — datasets are versioned snapshots63 (DVC, or versioned S3 parquet paths). A model trained on data nobody can reproduce is a64 model nobody can debug.6566## Experiment Tracking (MLflow)6768- Every training run logs params, metrics, and artifacts to MLflow — an unlogged run may69 as well not have happened.70- Promote models through registry aliases (`@staging` → `@production`), never by copying71 artifacts around by hand — the registry is the single source of "what is deployed".72- **No model ships without a recorded eval on a pinned dataset** — a metric on an73 unpinned dataset cannot be compared to the previous model's, so it cannot justify a74 promotion.7576## Serving (FastAPI)7778- **Load the model once in the FastAPI lifespan — never per-request**: model load is79 seconds and hundreds of MB; per-request loading turns every call into a cold start.80- Pin the model artifact version in config (a registry alias or explicit version) — a81 service that loads "latest" changes behavior without a deploy.82- The health endpoint reports the loaded model version — so an operator can see at a83 glance which model each instance is actually serving.84- Set request timeouts and a max payload size — inference endpoints are a85 denial-of-service magnet without both.86- Watch p95 inference latency — dashboards and alerting conventions are owned by87 `std-monitoring`.8889## LLM Integration (anthropic SDK)9091- Model choice: `claude-sonnet-5` by default for product features;92 `claude-haiku-4-5-20251001` for cheap high-volume classification; `claude-opus-5` when93 reasoning depth is the product. Pin the model id in config, not inline.94- Prompts are versioned code — files in the repo, reviewed in PRs — not database strings:95 a prompt change is a behavior change and gets the same diff, review, and rollback.96- Structured outputs come from tool use, not from parsing prose — a tool schema is a97 contract; a regex over prose is a hope.98- **A pinned eval set runs in CI before a prompt or model change ships** — LLM behavior99 shifts across prompts and model versions, and an eval is the only regression test that100 catches it.101- Retry transient API errors with exponential backoff and a capped attempt count (the102 SDK's built-in retries or `tenacity`).103- **Never log full prompts or completions containing user PII** — log request ids, token104 counts, latency, and model id instead; logging rules are owned by `std-monitoring`.105- Give every LLM feature a cost budget (per-request and monthly) and review it like a106 performance budget — token spend regresses as silently as latency does.107108## Embeddings (pgvector)109110- Pin the embedding model and its dimension together in config — vectors from different111 models share a column type but not a space; mixing them silently ruins retrieval.112- Index with HNSW by default — better recall/latency than IVFFlat at house corpus sizes.113- Changing the embedding model means re-embedding the entire corpus — plan it as a114 migration (dual-write or backfill, then cut over), not an in-place swap.115116## Rollout117118- New models ship behind a flag with shadow or canary traffic first — offline eval119 numbers do not guarantee online behavior.120- Monitor input drift and prediction distributions in production; alert on divergence121 from the training distribution — models fail silently by degrading, not by crashing.122123Related, owned elsewhere — do not duplicate: the JSON error envelope and pagination124response format live in `std-api-design`; migration safety and indexing depth in125`std-database`; OWASP and secret management in `std-security`; structured logging and126PII-in-logs rules in `std-monitoring`; AAA and coverage targets in `std-testing`; general127Python layout, typing, and layering in `std-python`; ORM query performance in128`std-python-performance`; FastAPI framework specifics in `std-fastapi`.