Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- This is a well-audited design — the failure-mode analysis is better than most. But you're right that it's carrying a lot of manual machinery. A few angles, roughly in order of impact:
- 1. Your own numbers argue for inverting the architecture. Stage 2 exists to save LLM calls, but title-level caching already did the heavy lifting: the whole 17-country scope is only ~10,389 distinct titles. Classifying all of them with the LLM instead of just 4,610 costs maybe $1–2 more, once. Meanwhile Stage 2 costs you: ~150 hand-curated ILIKE patterns, category precedence rules, the PA/MD token traps, Commonwealth spelling variants, the force_ambiguous list that "hasn't had a second review pass," and every future round of "we found a collision." Consider flipping it: LLM classifies every distinct title (cached, incremental), and the keyword layer shrinks to (a) a small manual-override seed table for known traps and high-volume titles, and (b) a monitoring harness — a dbt test that flags when LLM verdicts disagree with keyword expectations, instead of being the classifier itself. You keep all the safety insight you've accumulated but stop maintaining it as production logic.
- 2. The cache grain is wrong for exactly the titles that matter most — and your own evidence proves it. Medical Director (Gynecology and Obstetrics) is 2,364 rows of mixed content (locum physicians + pharma + admin), yet the cache stores one verdict per title_key. No prompt improvement fixes that — a single answer is wrong by construction for heterogeneous buckets. For force_ambiguous titles only, classify at a finer grain: (title_key, company) or (title_key, Revelio industry), or even row-level using a truncated slice of the job description text, which is where the disambiguating signal actually lives. Sample raw titles (§5.5) are a proxy for this; the description is the real thing. The distinct-key count grows, but only inside the ambiguous slice, so cost stays trivial.
- 3. Kill the JSON-parsing risk class entirely with AI_CLASSIFY or structured outputs. Your Finding 5 (array vs. object silently failing 100% of parses) only exists because you're hand-rolling COMPLETE + regex extraction. Snowflake's AI_CLASSIFY takes text + your six category labels (with per-label descriptions) and returns structured output natively — no prompt template, no regex, no response_parse_failed column, no parse-rate monitor. If you need confidence_score and match_rationale, AI_COMPLETE with a response_format JSON schema gives you guaranteed-valid JSON. Either way, §5.3's critical-fix section and its monitoring apparatus mostly evaporate.
- 4. Embeddings as a cheap semantic middle tier. You already have a gold anchor list — the ~110 roles in Roles in Scope for CHG.xlsx. Embed those once with AI_EMBED, embed each distinct title once, and use VECTOR_COSINE_SIMILARITY (or AI_SIMILARITY) to bucket: high similarity → auto-include with the anchor's category, near-zero to all anchors → auto-exclude, middle band → LLM. Embeddings natively handle anaesthetist/paediatric/haematology, "Consultant Physician," and misspellings — the whole Commonwealth-spelling section disappears. This is a stronger deterministic tier than keywords if you keep a deterministic tier at all; it also gives you a free confidence signal (distance to nearest anchor) that's more trustworthy than LLM self-reported scores.
- 5. Better confidence than self-report. LLM self-reported confidence_score is famously miscalibrated, and your §5.4 policy leans on it. Two cheaper alternatives that fit your frequency-aware plan: (a) run two cheap models (e.g. a small and mid Cortex model) and treat disagreement as "Low confidence → review"; (b) use embedding distance from idea 4. Reserve the expensive model for disagreements only — a cascade instead of one pass.
- 6. Small fix for the Finding 4 re-classification gap. Instead of documenting --full-refresh as the process, just include prompt_version in the anti-join key (where t.title_key = n.title_key and t.prompt_version = '{{ var("prompt_version") }}') and have downstream joins take the latest version per title. Bumping the version then reclassifies everything automatically at the same trivial cost, with history preserved for auditing drift between versions.
- If I had to pick two: #3 is a pure win with no downside, and #2 fixes an actual correctness problem, not just complexity. #1 is the "out of the box" one — it's worth a cost spike in a sandbox (AI_CLASSIFY over all 10K distinct titles) to see whether Stage 2 still earns its keep.
- Sources: Snowflake Cortex AI Functions, Vector Embeddings, AI_SIMILARITY
Advertisement