ADR-0005: Model-Provider-Abstraktion

Status
Proposed
Datum
2026-05-15
Decider
Malte (mit Claude)
Verwandt
ADR-0003 (Architektur), ADR-0004 (Framework), ADR-0012 (Observability)
Tags
llm, providers, configuration

Kontext

mvest soll nicht an einen einzelnen LLM-Anbieter gebunden sein. Gründe: unterschiedliche Stärken pro Modell, deutliche Kostendifferenzen, Provider-Ausfälle, und für den Critic-Agent ist Provider-Diversität ein zentraler Verifikations-Hebel (siehe ADR-0003).

Pydantic AI (ADR-0004) bringt Multi-Provider-Support nativ mit. Dennoch braucht es eine eigene Konfigurations-Schicht, damit pro Agent eindeutig festgelegt werden kann, welches Modell zum Einsatz kommt, und damit Wechsel ohne Code-Änderung möglich sind.

Entscheidungstreiber

Entscheidung

Wir nutzen Pydantic AIs eingebaute Provider-Abstraktion als technische Basis und ergänzen sie um eine eigene Konfigurations-Schicht:

Provider-Auswahl (unterstützt in v1)

ProviderBeispiel-ModelleUse-Case-Profil
Anthropic Opus 4.7, Sonnet 4.6, Haiku 4.5 Strategy (Opus), allgemeine Agents (Sonnet), günstige Screener (Haiku). Sehr starkes Tool-Use.
OpenAI GPT-5, GPT-4o, GPT-4o-mini Critic-Agent (anderer Provider als Strategy), Research mit JSON-Mode, günstige Screener (mini).
Google Gemini Pro, Gemini Flash Critic-Alternative, lange Kontexte für Filings, günstige Massen-Inferenz mit Flash.
Groq Llama-3.x, Mixtral Sehr schnelle Inference für Screener / einfache Filterungen.
Mistral Mistral Large, Codestral Europäischer Provider, optional.
Ollama (lokal) Llama-3.x, Qwen Dev-Loop ohne API-Cost, optional. Nicht für Produktiv-Empfehlungen.

Konfigurations-Schema

Modellauswahl pro Agent erfolgt in einer Konfigurationsdatei (config/agents.yaml), nicht im Code:

# config/agents.yaml
agents:
  screener:
    primary:  { provider: anthropic, model: claude-haiku-4-5 }
    fallback: { provider: openai,    model: gpt-4o-mini }

  research:
    primary:  { provider: anthropic, model: claude-sonnet-4-6 }
    fallback: { provider: openai,    model: gpt-4o }

  analyst:
    primary:  { provider: anthropic, model: claude-sonnet-4-6 }
    fallback: { provider: google,    model: gemini-pro }

  strategy:
    primary:  { provider: anthropic, model: claude-opus-4-7 }
    fallback: { provider: openai,    model: gpt-5 }

  critic:
    # MUSS anderer Provider als strategy sein
    primary:  { provider: openai,    model: gpt-5 }
    fallback: { provider: google,    model: gemini-pro }
    constraint: provider_must_differ_from: strategy

  risk_assessment:
    primary:  { provider: anthropic, model: claude-haiku-4-5 }
    fallback: { provider: openai,    model: gpt-4o-mini }

Eigene Schicht über Pydantic AI

Ein AgentModelFactory liest die Konfiguration und erzeugt Pydantic-AI-Model-Instanzen:

class AgentModelSpec(BaseModel):
    provider: ProviderId          # enum: anthropic | openai | google | ...
    model: str                    # modell-id-string (provider-spezifisch)

class AgentConfig(BaseModel):
    primary: AgentModelSpec
    fallback: AgentModelSpec | None = None
    constraint: dict | None = None  # z. B. provider_must_differ_from

def build_model(spec: AgentModelSpec) -> pydantic_ai.Model:
    match spec.provider:
        case ProviderId.ANTHROPIC: return AnthropicModel(spec.model)
        case ProviderId.OPENAI:    return OpenAIModel(spec.model)
        ...

Fallback-Strategie

Constraint-Validierung

Beim App-Start wird die Konfiguration validiert. Constraint provider_must_differ_from: strategy beim Critic prüft, dass Provider von Critic und Strategy verschieden sind — andernfalls Start-Abbruch mit klarer Fehlermeldung.

API-Keys & Secrets

Cost-Tracking

Konsequenzen

Positiv

Negativ / Trade-offs