Session-Prompt für Phase 5 — LLM-Catalog-Anreicherung

Aufgabe: Catalog mit LLM-extrahierten strukturierten Attributen (brand, model, size, material, function) anreichern + Hybrid-Search um strukturierte Filter erweitern. Ziel: strict R@10_any von 80.63% auf 92-97%.

Stand bis hierher (2026-05-17)

Phase 4 ist abgeschlossen und live. Pipeline inference.agenticventures.de auf Task-Def Rev 7 (:phase4-final Image). Synth-Eval N=191 zeigt:

MetrikWert
R@1 strict52.36%
R@10 strict80.63% (Plan-Ziel)
R@10 cluster-aware (cross-wgr threshold 0.05)86.39%
MRR0.638

Branch: feat/search-quality-phase4-wgr-boost. Letzte Commits enthalten Catalog-Cleanup als WIP (Migration 004 + apply-Skript + archived-Filter), aber nicht deployed — wir machen direkt LLM-Anreicherung statt Embedding-Cluster-Cleanup, weil das die echte Modellnummer-Differenzierung löst.

Warum LLM-Anreicherung statt Cluster-Cleanup

Catalog-Befund:

  • 32670 Einträge mit Cohere-v4-Embedding
  • 71.9% sind in Embedding-Clustern (≥2 Mitglieder, threshold 0.05) → das ist NICHT echte Duplikation
  • Top-Cluster: 233 VELUX-Schwingfenster-Varianten in EINEM Cluster
  • Embedding-Cluster verschmilzt Produkt-Varianten zu aggressiv — Größen, Materialien, Modelle gehen verloren

Das eigentliche Problem ist Modellnummer-Differenzierung: das System verwechselt “Roto Eindeckrahmen ESR 73” mit “Roto Eindeckrahmen ESR 50”, weil das Embedding 95% gleich aussieht. Ein LLM erkennt das Modell sofort.

Plan Phase 5

Schritt 1 — LLM-Catalog-Anreicherung (one-time, ~25 USD)

Was: Pro Catalog-Eintrag ein Haiku-4.5-Call mit kurztext + rtftext → JSON-Output strukturierter Attribute.

Output-Schema pro ID:

{
  "brand": "Roto" | "VELUX" | "Bauder" | null,
  "model": "ESR 73" | "GGL MK04" | "PIR FA" | null,
  "function": "Eindeckrahmen" | "Schwingfenster" | "Wärmedämmung" | null,
  "material": "Aluminium" | "Polyurethan" | "Titanzink" | null,
  "dimension": "78x98" | "DN 100" | "140 mm" | null,
  "size_class": "klein" | "mittel" | "gross" | null
}

Implementierung:

  • Neue Migration 005_catalog_attrs.sql: ADD COLUMN attrs JSONB DEFAULT NULL
  • Neues Skript scripts/enrich_catalog_attrs.py:
    • Liest alle 32k Catalog-Einträge in Batches von 100
    • Pro Batch: parallel Haiku-Calls mit konkurrenz-limit (max 10 in-flight) wegen Bedrock-Quota
    • System-Prompt: “Du extrahierst strukturierte Attribute aus GAEB-Position-Texten. Gib NUR JSON zurück mit den Feldern brand, model, function, material, dimension, size_class. null wenn nicht erkennbar.”
    • Few-shot mit 3 Beispielen aus echtem Catalog
    • Schreibt attrs zurück in DB
    • Idempotent: SKIP wenn attrs IS NOT NULL (Resume-Fähigkeit)
  • ECS RunTask mit AWS_RETRY_MODE=adaptive, AWS_MAX_ATTEMPTS=20
  • ETA: ~45-60 Min für 32k × Haiku-Call

Cost:

  • Haiku 4.5 EU: 4.00/M output
  • 32k × (700 in + 80 out) tokens = 22.4M in + 2.6M out
  • ~25 USD total

Verifikation:

  • Sampling: 20 random IDs nach Anreicherung manuell checken (echte vs erwartete attrs)
  • Coverage: wie viele IDs haben non-null brand/model? Erwartung: >70% für L7-Markenartikel, ~30% für Standard-Einträge

Schritt 2 — Query-Understanding (Search-time, ~0.001 USD/Query)

Was: Pro Search-Request ein Haiku-Call der dieselben Felder aus der Query extrahiert.

Implementierung:

  • Neuer Module app/query_understanding.py analog zu app/query_rewrite.py (LRU-Cache, Bedrock-Client, fail-safe-Fallback)
  • Output gleicher Schema wie Schritt 1
  • Im app/routers/search.py: vor hybrid_search aufrufen, an hybrid_search als optional struct-filter durchreichen
  • Latenz-Budget: 200ms (mit LRU für wiederkehrende Queries)

Was: Wenn Query-Attrs erkannte Marke/Modell hat, weicher SQL-Filter:

ORDER BY (
    CASE WHEN attrs->>'model' = $query_model THEN 0
         WHEN attrs->>'brand' = $query_brand THEN 1
         ELSE 2
    END,
    {emb_col} <=> $vector
)

Oder als zusätzliches Score-Signal in score_fusion.fuse_hybrid:

attrs_score = (
    1.0 if hit.attrs.get("model") == query.model else
    0.6 if hit.attrs.get("brand") == query.brand else
    0.0
)

Mit Settings attrs_match_weight: float = 0.15.

Erwarteter Effekt:

  • Query “Roto Eindeckrahmen ESR 73” wird zu {brand: Roto, model: ESR 73, function: Eindeckrahmen}
  • Nur Einträge mit attrs->>'model' = 'ESR 73' bekommen Boost — alle 451 Schwingfenster-Geschwister fallen weg
  • Strict R@10 sollte deutlich hoch

Schritt 4 — Bake-Off-Verifikation

  • Bake-Off auf bestehendem Synth-Eval N=191 + Florian N=51
  • Compare: Phase-4-Pipeline vs Phase-5-Pipeline
  • Erwartung: synth R@10 80% → 92-97%, Florian R@10 55% → 65-75% (Florian-Eval bleibt biased, aber Lift signalisiert echte Verbesserung)

Schritt 5 — Production-Cutover

  • Neues Image :phase5-llm mit Query-Understanding + Structured-Filter
  • Task-Def Rev 8 (basiert auf Rev 7 mit cloudflared, app-image upgraden)
  • Service-Update + Smoke-Test
  • Wenn Smoke OK: Phase 5 fertig

Wichtige Pfade

Code: ~/source/a-icking/

  • Plan: docs/plans/2026-05-17-005-feat-search-quality-phase4-plan.md (Phase-4-Plan, Phase-5-Pfad nicht drin)
  • Decision-Docs: docs/decisions/2026-05-17-search-pipeline-phase-2.md, 2026-05-17-search-pipeline-phase-4a.md, 2026-05-17-search-pipeline-phase-4-final.md, 2026-05-17-phase-4-shipped.md
  • Existierende LLM-Integration als Pattern: app/query_rewrite.py (Haiku 4.5 Converse via aioboto3)
  • Bake-Off-Skript mit --multi-field --wgr-weight 0.04 --top-k 20: scripts/bake_off.py
  • WIP-Cleanup-Code (Phase-5-Alternative, nicht deployed): db/migrations/004_catalog_cleanup_archived.sql, scripts/apply_catalog_cleanup.py, scripts/analyze_catalog_clusters.py

Daten:

  • Synth-Eval N=191: s3://av-production-terraform-state/inference-service/bootstrap/eval_set_synth_200.jsonl
  • Florian-Eval N=51: s3://av-production-terraform-state/inference-service/bootstrap/eval_set.jsonl
  • Embedding-Cluster-JSON (cross-wgr, threshold 0.05): s3://av-production-terraform-state/inference-service/bootstrap/clusters_xwgr.json — falls für Catalog-Analyse hilfreich, aber NICHT für Cleanup nutzen

AWS Production:

  • Account av-production (425924867359), Region eu-central-1
  • ECS Cluster inference-service-prod, Service auf Task-Def Rev 7
  • ECR inference-service-prod, Tags: :phase4-final (live), :phase4a, :phase5-cleanup (Cleanup-Image gebaut aber nicht deployed)
  • RDS inference-service-prod.cfwqykik8qyi.eu-central-1.rds.amazonaws.com
  • Task-Role hat s3:GetObject auf s3://av-production-terraform-state/inference-service/bootstrap/* — read-only
  • S3 PutObject ist blockiert für die ECS-Task-Role. Synth-Eval-Generator dumpt nach stdout, lokal mit av-production-CLI uploaden.

Vault-Cross-Refs:

  • Projekt: [[../../projekte/icking-ai-rebuild/_index]]
  • Skepsis-Pass: [[../manual-review]]
  • Phase-4-Bilanz: in ~/source/a-icking/docs/decisions/2026-05-17-phase-4-shipped.md

Code-Pattern für Schritt 1 (enrich_catalog_attrs.py)

Vorlage aus app/query_rewrite.py (Haiku Converse pattern) + scripts/generate_synth_eval.py (Catalog-Iteration + stdout-Dump):

SYSTEM = (
    "Du bist GAEB-Spezialist. Extrahiere aus einer Position-Beschreibung "
    "strukturierte Attribute als JSON. Keine Erklaerung, nur JSON. "
    "Felder: brand, model, function, material, dimension, size_class. "
    "null wenn nicht aus dem Text ablesbar."
)
 
EXAMPLES = [
    {
        "input": "Position 88966\nkurztext: Roto Eindeckrahmen ESR 73 1x1 ZIE AL 10/12...",
        "output": '{"brand":"Roto","model":"ESR 73","function":"Eindeckrahmen","material":"Aluminium","dimension":"10/12","size_class":"klein"}'
    },
    # ... 2 weitere
]
 
async def enrich(client, entry):
    user = f"Position {entry['id']}\nkurztext: {entry['kurztext']}\nDetail: {entry['rtftext'][:500]}\nWgr/Tgk: {entry['wgr']}/{entry['tgk']}"
    response = await client.converse(
        modelId="eu.anthropic.claude-haiku-4-5-20251001-v1:0",
        messages=[*build_few_shot(EXAMPLES), {"role": "user", "content": [{"text": user}]}],
        system=[{"text": SYSTEM}],
        inferenceConfig={"temperature": 0.0, "maxTokens": 200},
    )
    text = response["output"]["message"]["content"][0]["text"]
    return json.loads(text)  # Fail-safe: bei JSONDecodeError → return None, attrs bleibt NULL

Plus Concurrency-Limiter via asyncio.Semaphore(10) damit Bedrock-Quota nicht explodiert.

Decision-Gates

  • Nach Schritt 1: Sampling-Quality. Wenn <60% korrekt extrahiert → Few-Shot erweitern oder maxTokens hoch. Wenn >80% → weiter.
  • Nach Schritt 2/3: Bake-Off auf Synth N=191. Wenn strict R@10 nicht ≥88% → Filter-Weight tunen oder Filter-Logik granularer (CASE-WHEN-Hierarchie).
  • Vor Schritt 5 (Cutover): Smoke-Test auf 5 echten Queries via curl https://inference.agenticventures.de/search mit API-Key.

Was NICHT machen

  • Den WIP-Cleanup-Code (Migration 004 + archived-Filter) deployen — pivot zu LLM macht das obsolet
  • Catalog-Embedding nochmal neu berechnen — Cohere v4 ist gut genug, Hebel ist LLM nicht Embedding
  • BGE-M3 Fine-Tuning starten — erst nach Phase 5 wenn dort kein 95% erreicht wird
  • Florian-Termin organisieren — Marvin macht das selbst wenn er will

Cost-Budget Phase 5

PostenErwartet
Catalog-Anreicherung Haiku-Calls~25 USD
Test-Bake-Offs (3-4 Iterationen)~0.5 EUR
Fargate Compute~0.10 EUR
Production Query-Understanding (lebenslang, nicht für Bake-Off)~0.001 USD/Query
Summe Phase 5 Bau~26 USD

Ziel-Output dieser Session

  1. Migration 005 angewendet (attrs JSONB)
  2. 32k Catalog-Einträge angereichert mit brand/model/function/material/dimension
  3. Query-Understanding live im Search-Router
  4. Bake-Off-Ergebnis auf Synth N=191 mit Phase-5-Pipeline
  5. Decision-Doc docs/decisions/2026-05-18-phase-5-llm-enrichment.md mit allen Befunden
  6. Production-Cutover auf :phase5-llm falls Bake-Off ≥92%

Bei Erfolg: Branch mergen, Phase-5-Bilanz in agentic-ventures/intern/projekte/icking-ai-rebuild/_index.md aktualisieren.


Erwarteter Ablauf der Session:

  1. Lies diesen Prompt + Phase-4-Decision-Docs
  2. Starte mit Schritt 1 (Anreicherung) — das ist der zeitintensive Teil (45-60 min Wait-Time)
  3. Während Anreicherung läuft: Schritt 2 (query_understanding.py) bauen
  4. Nach Anreicherung: Schritt 3 (Search-Filter) bauen
  5. Bake-Off
  6. Cutover wenn Zahlen stimmen

Marvin will eine Pipeline die wirklich funktioniert. 95% R@10 ist das Ziel. LLM ist der Hebel.