ADR-009: Aufgabenbasiertes Modell-Routing

Warum Owlat eine LLM-Modellauswahl pro Aufgabe unterstützt, statt ein einziges Modell für alle Pipeline-Schritte zu verwenden.

  • Status: Angenommen
  • Datum: 2026-03-24

Kontext

Die Agent-Pipeline, der Knowledge Graph und das Dateisystem führen allesamt LLM-Aufrufe durch — allerdings mit unterschiedlichen Anforderungen:

AufgabeAnforderungenVolumen
KlassifikationGeschwindigkeit, strukturierte AusgabeJede eingehende Nachricht
SicherheitsfilterGeschwindigkeit, geringe LatenzJede eingehende Nachricht
WissensextraktionStrukturierte Ausgabe, KosteneffizienzJede verarbeitete Nachricht
Datei-TaggingZusammenfassung, KosteneffizienzJede hochgeladene Datei
Handlungsplanung (geplant)Reasoning, Tool-NutzungPro Nachricht (nach der Klassifikation)
EntwurfserstellungSchreibqualität, TonalitätstreuePro Nachricht (wenn ein Entwurf benötigt wird)
KontextverdichtungZusammenfassungWenn der Kontext das Budget überschreitet
Verdrahtete vs. reservierte Tiers

Die Aufgabe plan (Handlungsplanung) ist im Typ definiert und in taskTier() einem Tier zugeordnet, hat aber noch keine Aufrufstellen. Verdrahtet sind die Aufgaben classify, draft, extract, summarize und guardguard läuft produktiv in apps/api/convex/agent/steps/security_scan/index.ts:88, die übrigen werden in apps/api/convex/agent/steps/, knowledge/extraction.ts, semanticFileProcessing.ts und translate.ts aufgerufen. Die Zeile plan beschreibt die beabsichtigte Nutzung, nicht das aktuelle Verhalten.

Alles auf dem leistungsfähigsten Modell laufen zu lassen (GPT-4o, Claude Sonnet) verschwendet Ressourcen — die Klassifikation liefert ein einfaches Enum und braucht nicht die Reasoning-Kapazität eines Frontier-Modells. Alles auf dem günstigsten Modell laufen zu lassen (GPT-4o-mini, Llama 3) spart Geld, erzeugt aber Entwürfe von schlechter Qualität.

Die betrachteten Optionen:

  1. Ein einziges Modell — die einfachste Konfiguration, erzwingt aber eine Wahl zwischen Qualität und Kosten
  2. Modellkonfiguration pro Schritt — fünf separate Modell-Umgebungsvariablen, aufwendig zu konfigurieren
  3. Zweistufiges Routing — schnelles Modell für strukturierte Aufgaben mit hohem Volumen, leistungsfähiges Modell für Reasoning und Textproduktion

Entscheidung

Den austauschbaren LLM-Provider um ein zweistufiges Modell-Routing erweitern:

LLM_MODEL=gpt-4o              # Fallback for all tasks
LLM_MODEL_CAPABLE=gpt-4o      # Drafting, planning, reasoning
LLM_MODEL_FAST=gpt-4o-mini    # Classification, extraction, summarization

Aufrufer fordern ein Modell über task an, und die Routing-Regel ordnet jede Aufgabe dem Fast- oder dem Capable-Tier zu:

// apps/api/convex/lib/llmProvider.ts (illustrative — actual tier rule)
type LLMTask = 'classify' | 'extract' | 'guard' | 'summarize' | 'draft' | 'plan'

function taskTier(task: LLMTask): 'fast' | 'capable' {
  // classify / extract / guard / summarize → fast
  // draft / plan → capable
}

// OpenAI-compatible provider resolves the model ID for the tier
// (env reads go through the typed `getOptional()` wrapper, not raw process.env):
const modelId = tier === 'fast'
  ? getOptional('LLM_MODEL_FAST') ?? getOptional('LLM_MODEL') ?? 'gpt-4o-mini'
  : getOptional('LLM_MODEL_CAPABLE') ?? getOptional('LLM_MODEL') ?? 'gpt-4o'

getLLMProvider(task) in apps/api/convex/lib/llmProvider.ts ist die Implementierung, die Aufrufer verwenden; die Fast/Capable-Entscheidung liegt in der privaten Funktion taskTier() in derselben Datei. Die Fallback-Semantik der Umgebungsvariablen steckt in modelIdForTier(), ebenfalls in apps/api/convex/lib/llmProvider.ts.

Sowohl LLM_MODEL_CAPABLE als auch LLM_MODEL_FAST fallen auf LLM_MODEL zurück, was bedeutet:

  • Minimale Konfiguration: Setzen Sie nur LLM_MODEL, und alles verwendet dieses Modell (dasselbe Verhalten wie in ADR-007)
  • Kostenoptimierung: Setzen Sie LLM_MODEL_FAST=gpt-4o-mini und LLM_MODEL_CAPABLE=gpt-4o, um nach Aufgabe zu trennen
  • Einfachheit beim Self-Hosting: Self-Hoster, die ein einzelnes Ollama-Modell betreiben, setzen LLM_MODEL=llama3, und beide Tiers verwenden es

Die oben genannten Modell-IDs (gpt-4o-mini / gpt-4o) sind die Standardwerte des OpenAI-kompatiblen Clients. LLM_PROVIDER (openai, openrouter oder ollama) wählt die Basis-URL; jeder Client ist OpenAI-kompatibel. Um Claude zu verwenden, belassen Sie LLM_PROVIDER auf dem Standardwert openai, richten LLM_BASE_URL auf einen OpenAI-kompatiblen Anthropic-Proxy und setzen anschließend LLM_MODEL_FAST / LLM_MODEL_CAPABLE auf die gewünschten Claude-Modell-IDs. Zur Provider-Schicht siehe ADR-007.

Konsequenzen

Ermöglicht:

  • Kostensenkung um 60–80 % bei Aufgaben mit hohem Volumen (Klassifikation, Extraktion) durch den Einsatz kleinerer Modelle
  • Qualitätserhalt bei Aufgaben, die ihn brauchen (Entwurfserstellung, Planung), durch den Einsatz leistungsfähiger Modelle
  • Einfacher Upgrade-Pfad — mit einem Modell starten und später bei wachsendem Volumen aufteilen
  • Self-Hoster können ein einzelnes Modell ohne jegliche Routing-Konfiguration betreiben
  • Kostenverfolgung pro Schritt wird aussagekräftig (Aufrufe des schnellen Modells sind günstig, Aufrufe des leistungsfähigen Modells sind der eigentliche Kostentreiber)

Trade-offs:

  • Zwei zusätzliche Umgebungsvariablen, die dokumentiert und unterstützt werden müssen
  • Annahmen zur Modellkompatibilität — das Verhalten bei strukturierter Ausgabe unterscheidet sich je Modell und erfordert unter Umständen modellspezifische Prompt-Anpassungen
  • Self-Hoster, die ein einzelnes kleines Modell verwenden, werden bei Entwurfsaufgaben Qualitätseinbußen sehen (derselbe Trade-off wie bereits in ADR-007)
  • Noch keine Modellüberschreibung pro Organisation — das Routing gilt systemweit und ist nicht mandantenspezifisch konfigurierbar