| name | eval-pipeline |
| description | Systematically tune the RAG recommendation pipeline through measured iteration — baseline eval, one-lever-at-a-time changes, holdout validation. Use only when explicitly invoked; never auto-fire (it costs money and takes time). |
| disable-model-invocation | true |
You are the ML engineer responsible for recommendation quality. Your job is to systematically improve the RAG pipeline through measured iteration — no guessing, no vibes-based tuning.
You change one lever at a time, measure the impact, keep what works, revert what doesn't. You're skeptical of improvements that only help 1-2 queries — real gains are distributed.
Requirements: local PostgreSQL with embedded products must be running.
Relationship to /ai: /ai reviews AI design and architecture. This skill tunes scores through measured experimentation. Run /ai to decide what to build. Run this to tune what you've built.
Mode
Arguments: $ARGUMENTS
- No arguments → run baseline eval + up to 5 iterations on the full train split.
- A number (e.g.,
3) → run baseline eval + up to N iterations.
baseline → run step 1 only (eval + analysis), no iteration. Use to check current state.
- A tag name (e.g.,
pairing, occasion) → filter analysis to queries with that tag. Still iterate on the full train split, but prioritize levers that improve the tagged category.
Context gathering
Before starting, silently read the implementation files you may need to modify:
backend/services/intent.py — intent parsing prompts and logic
backend/services/recommendations.py — recommendation orchestration
backend/repositories/recommendations.py — pgvector queries, hybrid search
backend/benchmarks/eval/levers.md — available optimization levers
backend/benchmarks/eval/data/rubric.json — scoring dimensions and weights
Steps
- Run
make eval and read the full output (console + latest JSON in backend/benchmarks/eval/results/).
- This runs the train split only (14 queries) with deterministic judging (temp=0, 1 run).
- Analyze the results:
- Sort queries by overall score, focus on bottom quartile.
- Check tag averages to identify weak categories (e.g. "pairing" scores low → focus there).
- Read judge justifications to identify root causes (wrong intent? bad retrieval? poor embeddings?).
- Map each failure to a specific lever from levers.md.
- Pick the ONE lever most likely to improve the worst scores. Change only that lever.
- Re-run
make eval and compare scores using the built-in diff mode.
- If scores improved: keep the change and move to the next iteration.
- If scores regressed: revert the change, explain why it didn't work, and try a different lever.
- Repeat steps 2-6 for up to 5 iterations.
- Holdout validation — after all iterations, run:
make eval SPLIT=holdout JUDGE_RUNS=2 JUDGE_TEMP=1.0
- Compare holdout weighted average to the final train weighted average.
- If holdout is >0.5 below train, flag to Victor — the changes likely overfit to the train set.
- Report both scores in the final summary.
Iteration Budget
Run up to 5 iterations (or as specified by the user via $ARGUMENTS). Stop early only if:
- Target score is reached
- 3 consecutive iterations show no improvement (< 0.05 gain each) — the pipeline has plateaued
- All viable levers have been tried
Do NOT stop just because one change worked. Keep going — stack improvements.
Rules
- Change ONE lever at a time — never change intent prompt AND retrieval query simultaneously.
- Always iterate on the train split only (
make eval, the default). Never peek at holdout during iteration.
- Never modify
queries.json or rubric.json — those are the fixed benchmark.
- Never modify the eval framework itself (
backend/benchmarks/eval/) — you're optimizing the pipeline, not the measurement.
- Show the score diff after each iteration in a clear table.
- Check that improvements are distributed across queries, not concentrated in 1-2 queries while others regress.
- If a prompt change adds a rule that maps 1:1 to a specific test query, that's overfitting — the rule should help 3+ unseen queries.
- If a lever requires re-embedding (
compose_embedding_text()), warn before proceeding — it's expensive and takes ~30 min.
- Track which levers you've tried and their effect — don't repeat a failed approach.
Target
Default target: weighted average ≥ 4 (or as specified by the user via $ARGUMENTS).
CLI Reference
make eval # train split, temp=0, 1 judge run (default)
make eval SPLIT=holdout # holdout split only
make eval SPLIT=all # all 19 queries
make eval JUDGE_RUNS=2 JUDGE_TEMP=1.0 # multi-run with variance
make eval QUERY=4 # single query (ignores split)
Runtime artifacts (not bundled here)
The skill references but does NOT ship the eval datasets, levers reference, or rubric — those live alongside the eval runner code in backend/benchmarks/eval/ so the runner can read them at execution time:
backend/benchmarks/eval/data/queries.json — train + holdout query set
backend/benchmarks/eval/data/rubric.json — scoring dimensions
backend/benchmarks/eval/levers.md — lever catalog
backend/benchmarks/eval/results/ — per-run JSON outputs
If you ever want a baselines/snapshots folder to ship with the skill (for cross-run comparison without git archaeology), add baselines/ here and update step 4 to use it.
Output
After all iterations, provide a summary table:
| Iteration | Lever changed | File modified | Weighted avg | Delta | Kept? |
|-----------|--------------|---------------|-------------|-------|-------|
| 0 (baseline) | — | — | X.XX | — | — |
| 1 | intent prompt | services/intent.py | X.XX | +0.XX | ✅/❌ |
| 2 | ... | ... | ... | ... | ... |
Then:
- Final per-dimension scores vs baseline
- Tag-stratified scores vs baseline (flag any tag that regressed)
- Total improvement (weighted average delta from iteration 0 to final)
- Holdout validation result (holdout weighted avg vs train weighted avg, gap)
- Which levers had the most impact
- Suggested next steps if target wasn't reached
- Any patterns observed (e.g. "relevance is capped by embedding quality, not intent parsing")