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:
| Metrik | Wert |
|---|---|
| R@1 strict | 52.36% |
| R@10 strict | 80.63% (Plan-Ziel) |
| R@10 cluster-aware (cross-wgr threshold 0.05) | 86.39% |
| MRR | 0.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
attrszurü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.pyanalog zuapp/query_rewrite.py(LRU-Cache, Bedrock-Client, fail-safe-Fallback) - Output gleicher Schema wie Schritt 1
- Im
app/routers/search.py: vorhybrid_searchaufrufen, anhybrid_searchals optional struct-filter durchreichen - Latenz-Budget: 200ms (mit LRU für wiederkehrende Queries)
Schritt 3 — Structured-Filter in Hybrid-Search
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-llmmit 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), Regioneu-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:GetObjectaufs3://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 NULLPlus 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/searchmit 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
| Posten | Erwartet |
|---|---|
| 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
- Migration 005 angewendet (attrs JSONB)
- 32k Catalog-Einträge angereichert mit brand/model/function/material/dimension
- Query-Understanding live im Search-Router
- Bake-Off-Ergebnis auf Synth N=191 mit Phase-5-Pipeline
- Decision-Doc
docs/decisions/2026-05-18-phase-5-llm-enrichment.mdmit allen Befunden - Production-Cutover auf
:phase5-llmfalls 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:
- Lies diesen Prompt + Phase-4-Decision-Docs
- Starte mit Schritt 1 (Anreicherung) — das ist der zeitintensive Teil (45-60 min Wait-Time)
- Während Anreicherung läuft: Schritt 2 (query_understanding.py) bauen
- Nach Anreicherung: Schritt 3 (Search-Filter) bauen
- Bake-Off
- Cutover wenn Zahlen stimmen
Marvin will eine Pipeline die wirklich funktioniert. 95% R@10 ist das Ziel. LLM ist der Hebel.