← All posts

00-blog-outline

Teddy Lazar·Invalid Date·10 min read

Blog Posts (3 posts 6 ai ones, ranked by signal)

REAL POST 1: How AI Scoring Systems Drift (and How I Fixed it with Comparative Batching)

Hook How we solved the cold start problem of have an app that needs ratings without users

Outline

  • FAIL: Attempt 1 Naive: blindly passing in AI recipes to AI asking for reviews (turns out all my recipes are high quality 8-10/10), this attempt can be seen in the ARCHIVE_ columns and is in scripts/recipe_rating/v1
  • FAIL: Attempt 2 Role Based Naive - Create roles or "personalities" added to the context of the model, pass recipes in and try to hit a statistical threshold of reviews for recipes based on # of reviews scripts/recipe_rating/v2 (was overwritten maybe there is a batch job for it but that whole batch did not get written only 5k)
  • SUCCESS: Attempt 3 - Rating Recipes in Context - Use roles and show 5 recipes at a top, rank the recipes against each other. Randomly show recipes in batches of 5 with at least 2 categories per recipe batch. Show bell curve outcme (scripts/recipe_rating/v2)
  • Cost for each of the experiemnts + per token output
  • Show data analysis on the bell curve etc
  • Conclusion and plans for the future -- how this is used in the app

REAL POST 2: Where AI Actually Belongs in Production Systems and Where it Breaks

  • Core featuree of the app is food intake for inventroy. At a high level this takes in inputs from voice, text, reciepts, images, transforms the inputs into output text and then cleans the data and matches it against my food database.
  • The problem of natural language and parsing individual pieces of datsa out of it.
  • The way I solved this is by keeping as many pieces of it deterministic
  • Using AI as the smallest point of contact accross multiple use cases in the pipeline
  • Breaking down larger problems into smaller pieces (EX Voice -> Whisper -> text -> LLM Parser -> Deterministic Parser -> Food matcher) (EX Text -> -> LLM Parser -> Deterministic Parser -> Food matcher) (EX Reciept Image -> LLM for OCR -> Special receipt normalization prompt -> LLM Parser -> Deterministic Parser -> Food matcher) (EX Image of Food -> LLM for Item detection -> LLM Parser -> Deterministic Parser -> Food matcher)
  • The food matcher is a strong deterministic matcher that matches the LLM output of canonical name food items to canonical names, aliases, and then fuzzy matches when searching for food
  • Places to improve - breaking down the food image -> text into a few different api calls, levels of success in this is difficult with high numbers of food only using one call
  • If you are wondering how I made the food dataset to match on aliases see my next article (My Data is Alive?!? Agentic Dataset Improvement)
  • Cost and token usage

REAL Post 3: (My Data is Alive?!? Agentic Dataset Improvement)

  • How I created the food dataset by pulling information from USDA foundation, USDA legacy, and having tiered backups for Open Food Facts and USDA Branded
  • Redis for scraping and calls
  • Testing dataset = all the ingredients from my recipes to seed aliases. Ranking of ingredients based on frequency in recipes (this ingredient is present in 10, this one is only in 1)
  • Defining Accuracy and Precision and how I calculated it
  • First attempt at dataset: Using AI passes to add in aliases, deduplication scripts to find duplicates, manally updated duplicate ones, using OFF and Branded as fallbacks (caching in redis). The fuzzy matching problem and duplication problem
  • Solutions - The food improver agent and the food accuracy agent. Building out agentic pipeline - deterministic sql read only queries - logic pass to AI with all relevant context including most frequently seen together ingredients for context etc, alias dedup checks, manually human verification steps to verify aliases to add, remove, delete. Accuracy agent to search OFF and Branded USDA for matches to improve the unamtched ingredient number (all have low recipe frequency and do not have common aliases)
  • Trade offs and final product: imperfect matching or closest match based on nutrition, unmatchable items, the point of overfitting and diminishing returns
  • Maybe the fill alias gaps agent and v2 of me building them (context context context by imrpvoing the context of the prompts they got much better... wow)
  • Cost predicitons and token usage

REAL Post 4: Solving Search - Why Embeddings Beat Filters:

  • Initial: The keyword search problem

  • HOW I GOT HERE - Using AI to improve and clean recipe datasets

  • (MAYBE) - the problem with online recipes, ingredients are not copy rightable but procedures are. Cleaned procedures, describe first vs second ai cleaning passes

  • Pass data through AI, have AI clean the procedure to solely be the necessary steps, clean the steps, etc

  • Add descriptions, add tags, add cooking times, alternative titles, canonical titles, using specific prompts and structured outputs

  • Build text embeddings using the text-embedding openai model

  • keyword vs semantic searching

  • what fails when searching with both

  • Why embeddings win

  • Cost of embedding

  • I could have another article solely on recipe cleaning

REAL Post 5: Recomendation Engine

  • How the recomendation heros work the deterministic side
  • The AI ratings and how they improved the feel of the website
  • Deciding on what people want to see and how to show it
  • The caching problem - solving it like Netlfix would by precalculating all recommendations and caching the outputs

AIGEN Post 1: "Where AI Actually Belongs in Production Systems"

Hook: Most AI demos put the model at the center. In production, the model is step 3 of 4.

Outline:

  • The 4-tier food matching pipeline: exact lookup -> alias -> fuzzy -> no match
  • Why deterministic steps come first (speed, reliability, cost)
  • Where AI adds value: parsing ambiguous OCR text, expanding vague categories
  • The confidence score hierarchy: 1.0 (exact) -> 0.95 (alias) -> 0.85-0.95 (fuzzy) -> 0.0 (miss)
  • Cost: deterministic lookup is 0ms, LLM parse is ~500ms. 90%+ of matches never need AI.

Data sources in codebase:

  • app/services/food_matcher.py -- full 4-step pipeline, word-count adaptive thresholds (2w>=92%, 3w>=89%, 4+>=91%)
  • app/core/config.py:FUZZY_MATCHING_THRESHOLD (85)
  • app/utils/LLM_food_parser.py -- LLM prompt structure, deterministic fallback at confidence 0.5
  • app/services/text_cleaner.py -- modality detection regex signals
  • app/db/repositories/food_repo.py -- source_tier ranking (curated=0 -> branded=4)

AIGEN Post 2: "LLMs Are Not Data Cleaners (Until You Force Them to Be)"

Hook: We feed our LLM receipt photos, voice memos, and text blobs. It works -- but only because we don't trust it.

Outline:

  • 4 input modalities: receipt_ocr, photo_ocr, speech, text_blob
  • Regex-first detection: price signals ($) -> receipt, nutrition labels -> photo, fillers (um, uh) -> speech
  • Modality-specific noise removal BEFORE the LLM ever sees input (40+ regex patterns)
  • The LLM prompt: canonicalization rules, descriptor triage (KEEP "beef" in "beef broth", DROP "organic", "keto", "premium")
  • Vague category expansion: "spices" -> [salt, pepper, garlic powder, onion powder, paprika, cumin, oregano, chili powder]
  • Deterministic fallback: when LLM fails, regex parser takes over at confidence 0.5
  • Training data collection: every LLM call logged for future fine-tuning (non-fatal, wrapped try/except)

Data sources in codebase:

  • app/services/text_cleaner.py -- 10 receipt OCR discard patterns, 6 receipt fix patterns, 9 photo OCR discard patterns, 30+ speech filler words
  • app/utils/LLM_food_parser.py -- full system prompt with canonicalization rules, brand exceptions (Sriracha, Tabasco, Worcestershire), receipt abbreviation expansions (BEL->bell, CHKN->chicken)
  • app/utils/food_parser.py -- deterministic regex fallback parser
  • app/core/config.py:COLLECT_TRAINING_DATA (True)

AIGEN Post 3: "Why Embeddings Beat Filters (Then Don't)"

Hook: Semantic search found recipes keyword search never could. Then it started recommending pasta for every soup query.

Outline:

  • The hybrid search architecture: semantic (6.0) + FTS (3.0) + exact match (3.0) + trigram (2.0)
  • Why embeddings alone fail: pantry staples (oil, flour, eggs) dominate ingredient vectors
  • The HARD_DROP list: 25 ingredients always excluded from embeddings (salt variants, water, cooking spray)
  • The BORDERLINE list: 95 ingredients demoted unless title-reinforced (oils, butter, flour, garlic, onions)
  • Title reinforcement logic: if ingredient word appears in recipe title, keep in embedding position
  • Person name stripping: "Mary's Pasta" -> "Pasta" to prevent author name noise in embedding space
  • AI score integration: centered normalization (avg-5.0)/4.5 with human shutoff at 5 interactions
  • Query parser: 7-phase token consumption, longest-first phrase matching, 32 cuisine terms, 200+ first name strips

Data sources in codebase:

  • app/core/config.py -- SEARCH_WEIGHT_* (semantic=6.0, FTS=3.0, exact=3.0, trigram=2.0), SEARCH_V3_POOL_* sizes (200/50/100), AI_SCORE_* settings
  • app/services/recipe_embedding_service.py -- HARD_DROP (25 items), BORDERLINE (95 items), title reinforcement logic, possessive stripping, OpenAI text-embedding-3-small (1536 dims)
  • app/utils/query_parser.py -- 7-phase parser, vocabulary maps (32 cuisines, 14 meal types, 17 styles, 9 dietary), 200+ name list
  • app/db/repositories/recipe_repo.py -- hybrid search SQL with CTE-based scoring

AIGEN Post 4: "AI Scoring Systems Drift (and How Relative Ranking Fixes It)"

Hook: Our first AI scoring run rated 80% of recipes between 6 and 8 out of 10. The distribution was useless.

Outline:

  • Problem: absolute scoring compresses to the center. LLMs are agreeable raters.
  • Solution: comparative batching -- 5 recipes per prompt, forced to use >=3 different values per dimension
  • 8 user personas: Busy Parent, Budget Cook, Health Runner, Beginner Cook, Experienced Chef, Comfort Seeker, High-Protein Prepper, Quick Weeknight
  • Two-axis scoring: appeal (0.6 weight) x practicality (0.4 weight) = composite
  • Distribution constraints: at least 1 score >=7 AND at least 1 score <=4 per batch
  • Target: 15-20% in 8-10 range, 15-20% in 1-3 range
  • Normalization for search: (avg - 5.0) / 4.5 -- centers at zero so unscored recipes get no boost/penalty
  • Kill switch: AI_SCORE_ENABLED + human shutoff at 5 interactions (once users have enough history, AI scores fade out)

Data sources in codebase:

  • scripts/recipe_rating/v2/personas.py -- 8 persona definitions with constraints (time limits, ingredient counts, protein targets)
  • scripts/recipe_rating/v2/prompts.py -- comparative batch prompt, scoring scale definitions (1-10 for appeal and practicality)
  • scripts/recipe_rating/v2/import_scores.py -- JSONL parsing, validation, DB insert
  • app/core/config.py -- AI_SCORE_CENTER (5.0), AI_SCORE_SPREAD (4.5), AI_SCORE_HUMAN_SHUTOFF (5), AI_SCORE_ENABLED (True)
  • app/db/repositories/recipe_repo.py:fetch_ai_quality_batch() -- CTE-based batch query for recommendation integration

AIGEN Post 5: "Building a Recommendation Engine That Actually Explains Itself"

Hook: Most recommendation engines are black boxes. Ours is a dot product you can read.

Outline:

  • 6 features, all interpretable: pantry_match, expiry_urgency, preference_affinity, popularity, quality, time_fit
  • 3 lane profiles as weight vectors: cook_now [0.35,0.25,0.10,0.10,0.10,0.10], you_like [0.10,0.05,0.40,0.20,0.15,0.10], balanced [0.20,0.15,0.20,0.15,0.15,0.15]
  • Score = dot product of feature vector and lane weights. That's it.
  • Diversity penalties: -0.15 for duplicate cuisine, -0.10 for duplicate meal type, -0.05 for duplicate time bucket
  • Affinity with time decay: 1.0 (<=7d), 0.7 (8-30d), 0.4 (31-90d), 0.2 (>90d)
  • Expiry urgency boost: +0.4 (<=3d), +0.2 (4-7d), +0.1 (8-14d)
  • Engagement ladder: shown -> clicked -> saved -> cooked (skipped is terminal from shown only)
  • Precomputed JSONB cache: <200ms serving, background refresh on inventory changes
  • Confidence thresholds: heroes >= 0.25, feed >= 0.15

Data sources in codebase:

  • app/services/recommendation_service.py -- full scoring engine, all 6 feature computations, lane weights, diversity penalties
  • app/core/config.py -- all REC_* settings (pool size=300, expiry days, diversity penalties, weights, confidence thresholds)
  • app/db/repositories/recommendation_repo.py -- update_outcome() with _VALID_PRIOR transition map
  • app/db/repositories/recipe_repo.py:fetch_ai_quality_batch() -- quality feature data source

AIGEN Post 6: "What I Got Wrong Building an AI System (So Far)"

Hook: I shipped 42 database migrations, 2 AI agents, and a recommendation engine. Here's what I'd do differently.

Outline:

  • Fuzzy matching at 85% cached "apple" -> "parmesan" as an alias. Fixed with >=99 persistence threshold.
  • OCR text cleaning: first version sent raw OCR to LLM. Hallucinated "organic quinoa" from a receipt that said "ORG QN." Regex-first cleaning fixed it.
  • Embedding noise: recipe embeddings were dominated by salt, oil, and flour. Every recipe looked similar. Fixed with HARD_DROP (25 items) and BORDERLINE demotion (95 items).
  • Author name pollution: "Mary's Famous Chicken Soup" embedded closer to other "Mary's" recipes than to chicken soups. Possessive stripping fixed it.
  • AI scoring compression: first run, 80% of scores landed between 6-8. Comparative batching with forced distribution fixed it.
  • Overengineered: built 2 full AI agents (food_improver, accuracy_improver) before having enough data to justify them. Should have started with simpler scripts.
  • Underengineered: didn't track recommendation events from day 1. Had to backfill the engagement ladder (shown/clicked/saved/cooked) after the fact.