Ein Interface für alle LLMs: IChatClient

Ein Interface für alle LLMs: IChatClient

- Matt

Wer in den letzten zwei Jahren etwas mit Sprachmodellen gebaut hat, kennt das Problem: Jeder Anbieter hat sein eigenes SDK, seine eigenen Typen für Nachrichten und seine eigene Art, Antworten zu streamen. Ein Wechsel von OpenAI zu Azure OpenAI oder zu einem lokalen Modell bedeutet, die halbe Anbindung neu zu schreiben.

Microsoft.Extensions.AI ist seit Mai allgemein verfügbar und setzt genau dort an. Der Kern ist ein einziges Interface, IChatClient, das jeder Anbieter implementieren kann.

Der einfache Fall

dotnet add package Microsoft.Extensions.AI
dotnet add package Microsoft.Extensions.AI.OpenAI
using Microsoft.Extensions.AI;
using OpenAI;

IChatClient client = new OpenAIClient(apiKey)
    .GetChatClient("gpt-4o-mini")
    .AsIChatClient();

var antwort = await client.GetResponseAsync("Erklär Dependency Injection in zwei Sätzen.");
Console.WriteLine(antwort.Text);

Interessant ist AsIChatClient(). Das offizielle OpenAI-SDK bleibt, wie es ist, und wird lediglich in die gemeinsame Abstraktion eingepackt. Man verliert dadurch nichts, kommt aber im eigenen Code nur noch mit IChatClient in Berührung.

Streaming sieht aus, wie man es erwartet:

await foreach (var teil in client.GetStreamingResponseAsync("Schreib mir ein Haiku über Nullable Reference Types."))
{
    Console.Write(teil.Text);
}

Warum das mehr ist als ein Adapter

Der eigentliche Gewinn zeigt sich, sobald IChatClient in den DI-Container wandert:

builder.Services.AddChatClient(sp =>
    new OpenAIClient(key).GetChatClient("gpt-4o-mini").AsIChatClient());

Ab da bekommen die eigenen Klassen ein IChatClient hereingereicht und wissen nicht mehr, wer dahintersteckt. Im Test setzt man eine Attrappe ein, ohne HTTP abfangen zu müssen. Das war vorher der unangenehmste Teil an solchen Anbindungen.

Dazu kommen Middleware-Bausteine, die sich wie bei ASP.NET Core übereinanderstapeln lassen:

IChatClient client = new ChatClientBuilder(innerClient)
    .UseDistributedCache(cache)
    .UseOpenTelemetry(sourceName: "meine-app")
    .Build();

Der Cache ist im Alltag Gold wert. Beim Entwickeln stellt man dieselbe Frage zwanzigmal, und ohne Cache zahlt man sie zwanzigmal. UseOpenTelemetry hängt sich in die übliche Telemetrie ein, sodass Modellaufrufe in denselben Traces auftauchen wie Datenbankzugriffe.

Die Grenzen

Eine gemeinsame Abstraktion kann immer nur den Schnitt abbilden. Alles, was ein Anbieter exklusiv kann, fällt entweder heraus oder muss über AdditionalProperties durchgereicht werden, und dann ist die Portabilität wieder dahin. Wer die Besonderheiten eines bestimmten Modells ausreizen will, ist mit dessen SDK direkt besser bedient.

Der zweite Punkt wird gern übersehen: Austauschbar ist die Schnittstelle, nicht das Verhalten. Ein Prompt, der bei GPT-4o zuverlässig sauberes JSON liefert, kann bei einem lokalen Modell in Prosa enden. Die Zeile, die den Client erzeugt, ist schnell getauscht. Die Prompts danach zu prüfen, bleibt Handarbeit.

Für alles, was nicht genau ein Modell voraussetzt, ist es trotzdem der richtige Standardweg. Vor allem, weil man die Entscheidung damit nicht mehr am Anfang treffen muss.