Projekte

modelab

3 min Lesezeit GitHub
Python FastAPI React LLMOps

Wir hatten im Team die Diskussion ständig: “Ist GPT-4 wirklich 10x den Preis wert gegenüber GPT-3.5 für unseren Use Case?” Oder: “Bringt der neue Prompt tatsächlich bessere Summaries?” Jedes Mal war die Antwort ein Bauchgefühl. Kein Experiment, keine Zahlen. Vibes.

Das hat mich genervt. Also hab ich modelab gebaut.

Was modelab macht

modelab ist ein Open-Source A/B Testing Framework, das speziell für LLM-Systeme designed ist. Die Idee: du definierst ein Experiment, modelab assigned User deterministisch zu Varianten, trackt Metriken wie Latenz, Kosten und Token-Verbrauch pro Variante und zeigt dir die Ergebnisse in einem Real-Time Dashboard.

import modelab
from modelab import Flag, Variant, EvalContext

modelab.init(
    server="http://localhost:8100",
    flags=[
        Flag(
            name="summarizer_model",
            variants=[
                Variant("gpt35", weight=50, config={"model": "gpt-3.5-turbo"}),
                Variant("gpt4", weight=50, config={"model": "gpt-4"}),
            ],
            rollout_pct=100,
        ),
    ],
)

ctx = EvalContext(user_id="user_123")
assignment = modelab.assign("summarizer_model", ctx)

Drei Zeilen und du hast ein laufendes Experiment. Kein Feature-Flag-Service, kein selbstgebautes Tracking, kein Spreadsheet.

Warum nicht einfach Feature Flags?

Feature Flags sagen dir: “User A sieht Feature X.” Punkt. Was sie dir nicht sagen: ob Feature X tatsächlich besser ist. Du brauchst Metriken pro Variante, du brauchst statistisch sauberes Bucketing, und du brauchst das alles in einem System, das versteht, was ein LLM-Response ist.

modelab versteht das. Du gibst ihm die OpenAI- oder Anthropic-Response, und es extrahiert automatisch Token-Counts, Latenz und Kosten:

response = openai.ChatCompletion.create(
    model=assignment.config["model"],
    messages=[{"role": "user", "content": "Fasse das zusammen..."}],
)

assignment.record(response, cost=0.013, latency_ms=250.0)
assignment.mark_success()

Duck-typed Token Extraction. Funktioniert mit OpenAI, Anthropic, oder du gibst die Werte manuell mit.

Die Designentscheidungen

Deterministisches Assignment. Gleicher User, gleiches Experiment → immer die gleiche Variante. Kein Zufall, kein Drift. Das klingt trivial, aber es ist wichtig: wenn User “alice” heute GPT-4 bekommt und morgen GPT-3.5, sind deine Metriken Müll.

Granulare Rollouts. Du kannst mit 0.01% Precision steuern, welcher Anteil deiner User überhaupt im Experiment landet. Neues Modell mit unklaren Kosten? Fang mit 5% an.

Flag(
    name="risky_feature",
    variants=[Variant("control"), Variant("experimental")],
    rollout_pct=5.0,  # nur 5% der User
)

Fail-Soft Design. Wenn der modelab-Server down ist, crashed deine App nicht. Storage-Fehler werden geloggt, aber der Assignment-Call gibt trotzdem eine Variante zurück. Dein Production-Traffic ist nie gefährdet.

Custom Events. Success und Failure reichen nicht immer. Manchmal willst du wissen, ob User das Ergebnis kopiert haben, ob sie Thumbs-Up gegeben haben, ob sie die Seite sofort verlassen haben:

assignment.mark_custom_event("thumbs_up", payload={"rating": 5})

Stack

Der Server läuft auf FastAPI mit PostgreSQL als Storage. Das Dashboard ist eine React-App, die direkt mit ausgeliefert wird. Ein docker compose up und alles läuft. Das Python SDK hat zero Dependencies und ist über PyPI installierbar.

Was ich dabei gelernt hab

Deterministisches Hashing klingt einfach, bis du Edge Cases triffst: was passiert, wenn ein Flag gelöscht und mit gleichem Namen neu erstellt wird? Was wenn sich die Gewichtung ändert, dürfen bestehende Assignments driften? Solche Fragen haben mich tiefer in die Statistik-Ecke gezogen als geplant.

Die andere Erkenntnis: Developer Experience ist alles. Die erste Version hatte eine Config-Datei, Flag-Definitionen im Dashboard, und drei verschiedene Init-Flows. Niemand hat es benutzt. Die aktuelle Version: ein init()-Call mit inline Flag-Definitionen. Adoption ging sofort hoch.