LLM lokal ausführen: wann man ein Modell selbst hosten sollte
1. Übersicht
APIs von LLM-Cloud-Providern aufzurufen ist der schnellste Weg, um AI in eine Anwendung zu bringen. Aber immer mehr Teams möchten Models auf ihrer eigenen Infrastruktur ausführen – möglicherweise auf einem Laptop, internen Server oder mit gemieteten GPUs.
In diesem Artikel erfährst du, was “LLM lokal ausführen” bedeutet und warum dieser Trend an Bedeutung gewinnt. Anschließend wirst du ein lokales Modell aufsetzen, es von einer Spring-Boot-Anwendung aus aufrufen und Tests für die Integration schreiben. Am Ende werden dir Kriterien vorgestellt, mit denen du entscheiden kannst, wann du selbst hosten solltest und wann nicht.
2. Was bedeutet “LLM lokal ausführen”?
2.1. Drei Grundkomponenten
Ein selbst gehostetes LLM-System besteht normalerweise aus drei Ebenen:
- Model weights: Die Gewichtsdatei des Modells. Typischerweise lädst du Open-Weight-Modelle aus öffentlichen Repositories.
- Inference runtime: Die Software, die Gewichte in RAM oder VRAM lädt und Token generiert. Beispiele sind Ollama, llama.cpp oder vLLM.
- API layer: Ein HTTP-Endpoint, den deine Anwendung aufruft. Viele Runtimes bieten eine OpenAI-kompatible API, sodass der Client-Code kaum geändert werden muss.
Deine Anwendung sieht nur die dritte Ebene. Das ist wichtig, damit du später zwischen lokal und Cloud wechseln kannst, ohne die Geschäftslogik umzuschreiben.
2.2. Quantisierung und Speicher
Der entscheidende Faktor, ob “meine Maschine das schafft”, ist der Speicher. Du kannst den Speicher für Gewichte grob schätzen wie folgt:
Gewichte-Speicher ≈ Anzahl Parameter × Bytes pro Parameter
16-bit (FP16): 2 Bytes / Parameter
8-bit: 1 Byte / Parameter
4-bit: ~0,5 Bytes / Parameter
Neben den Gewichten brauchst du auch Platz für den KV-Cache. Dieser wächst mit der Context-Länge und der Anzahl der parallel laufenden Anfragen. Quantisierung ist eine Technik, die die numerische Genauigkeit reduziert – zum Beispiel von 16-bit auf 4-bit –, um das Modell deutlich kleiner zu machen. Dafür sinkt die Antwortqualität leicht. Bei vielen Aufgaben wie Klassifikation, Zusammenfassung oder Informationsextraktion ist dieser Rückgang normalerweise akzeptabel.
3. Warum wird LLM-Selbsthosting immer beliebter?
Es gibt vier Hauptgründe:
- Datenschutz und Compliance: Kundendaten, Verträge oder interner Code verlassen nie deine Infrastruktur.
- Vorhersagbare Kosten: Cloud-APIs berechnen pro Token. Bei großem und gleichmäßigem Datenaufkommen können die Fixkosten einer GPU billiger sein als Pay-per-Token.
- Niedrige Latenz und Offline-Betrieb: Keine Round-Trips über das Internet, keine Abhängigkeit von Rate Limits von Drittanbietern, Betrieb möglich in isolierten Umgebungen.
- Immer bessere Open-Weight-Modelle: Kleinere Modelle leisten bereits für viele spezialisierte Aufgaben gute Dienste. Du kannst sie auch für deine spezifische Domain fine-tunen.
Hinweis: “Lokal” muss nicht dein Laptop sein. Ein GPU-Cluster in deiner privaten VPC zählt ebenfalls als Selbsthosting. Der Kern ist, dass du die Kontrolle über Modell und Daten behältst.
4. Lokales Modell mit Ollama aufsetzen
Ollama ist eine einfache Wahl für den Anfang. Es bündelt Runtime, Modellverwaltung und stellt eine HTTP-API bereit. Du kannst es mit Docker Compose ausführen:
services:
ollama:
image: ollama/ollama
ports:
- "11434:11434"
volumes:
- ollama-data:/root/.ollama
restart: unless-stopped
volumes:
ollama-data:
Das Volume ollama-data behält die geladenen Modelle. So musst du sie nicht jedes Mal neu laden, wenn der Container neu gestartet wird. Port 11434 ist der Standard-Port der Ollama-API.
Danach lädst du ein Modell und versuchst es:
docker compose up -d
docker compose exec ollama ollama pull llama3.2
curl http://localhost:11434/api/chat -d '{
"model": "llama3.2",
"messages": [{"role": "user", "content": "Zusammenfassung: Bestellung verspätet um 3 Tage, Kunde möchte Rückerstattung."}],
"stream": false
}'
Du kannst llama3.2 durch jedes andere Modell ersetzen, das zu deiner Hardware passt. Mit "stream": false wird das Ergebnis als ein einziges JSON zurückgegeben:
{
"model": "llama3.2",
"message": { "role": "assistant", "content": "Der Kunde fordert Rückerstattung, weil die Bestellung 3 Tage zu spät ankam." },
"done": true,
...
}
Wenn deine Maschine eine NVIDIA-GPU hat, musst du das NVIDIA Container Toolkit installieren und die GPU dem Container zuweisen. Andernfalls wird das Modell auf der CPU ausgeführt. Das funktioniert, aber deutlich langsamer.
5. Lokales Modell von Spring Boot aufrufen
Das Beispiel in diesem Abschnitt ist ein reales Szenario: einen Antwort-Entwurf für eine Kundenservice-Nachricht verfassen. Die Nachrichtendaten bleiben vollständig in deiner internen Infrastruktur.
5.1. Abhängigkeiten und Konfiguration
Du brauchst nur RestClient aus Spring Web. Keine zusätzliche AI-Bibliothek nötig:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
Konfiguriere application.properties:
local-llm.base-url=http://localhost:11434
local-llm.model=llama3.2
local-llm.timeout=60s
Das Timeout sollte großzügig gesetzt sein. Ein lokales Modell, besonders auf der CPU, kann viele Sekunden brauchen, um eine lange Antwort zu generieren.
5.2. Properties und RestClient
@ConfigurationProperties(prefix = "local-llm")
public record LocalLlmProperties(String baseUrl, String model, Duration timeout) {}
@Configuration
@EnableConfigurationProperties(LocalLlmProperties.class)
public class LlmClientConfig {
@Bean
RestClient ollamaRestClient(RestClient.Builder builder, LocalLlmProperties props) {
HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
JdkClientHttpRequestFactory factory = new JdkClientHttpRequestFactory(httpClient);
factory.setReadTimeout(props.timeout());
return builder
.baseUrl(props.baseUrl())
.requestFactory(factory)
.build();
}
}
Die zwei Timeout-Typen haben unterschiedliche Aufgaben. Connect timeout ist kurz (5 Sekunden) und hilft zu erkennen, wenn Ollama nicht läuft. Read timeout ist lang, um das Modell abzuwarten, bis es die Antwort fertig generiert hat.
5.3. DTO und Service
public record ChatMessage(String role, String content) {}
public record ChatRequest(String model, List<ChatMessage> messages, boolean stream) {}
@JsonIgnoreProperties(ignoreUnknown = true)
public record ChatResponse(ChatMessage message, boolean done) {}
@Service
public class SupportReplyService {
private static final String SYSTEM_PROMPT =
"Du bist ein Kundenservice-Mitarbeiter. Antworte höflich, prägnant und auf Deutsch.";
private final RestClient ollama;
private final String model;
public SupportReplyService(RestClient ollamaRestClient, LocalLlmProperties props) {
this.ollama = ollamaRestClient;
this.model = props.model();
}
public String draftReply(String customerMessage) {
ChatRequest request = new ChatRequest(model, List.of(
new ChatMessage("system", SYSTEM_PROMPT),
new ChatMessage("user", customerMessage)
), false);
ChatResponse response = ollama.post()
.uri("/api/chat")
.contentType(MediaType.APPLICATION_JSON)
.body(request)
.retrieve()
.body(ChatResponse.class);
if (response == null || response.message() == null) {
throw new IllegalStateException("LLM gab keinen Inhalt zurück");
}
return response.message().content().strip();
}
}
Der Ablauf von draftReply ist wie folgt:
- Der Service erstellt eine
system-Nachricht, um den Schreibstil zu lenken, und fügt die Kundennachricht alsuser-Nachricht hinzu. streamist auffalsegesetzt, daher gibt Ollama die komplette Antwort in einer Response zurück.@JsonIgnorePropertiesignoriert Statistikfelder, die der Service nicht nutzt, wie Verarbeitungszeit oder Token-Anzahl.- Eine leere Response wird in eine klare Exception konvertiert, statt einen rätselhaften
NullPointerExceptionzu verursachen.
6. Integration Tests
Du solltest Unit Tests nicht von einem echten Modell abhängig machen, da das langsam ist und nichtdeterministische Ergebnisse liefert. Verwende stattdessen MockRestServiceServer, um sicherzustellen, dass der Service die richtige Request sendet und die Response korrekt liest:
class SupportReplyServiceTest {
private MockRestServiceServer server;
private SupportReplyService service;
@BeforeEach
void setUp() {
RestClient.Builder builder = RestClient.builder().baseUrl("http://localhost:11434");
server = MockRestServiceServer.bindTo(builder).build();
var props = new LocalLlmProperties("http://localhost:11434", "llama3.2", Duration.ofSeconds(60));
service = new SupportReplyService(builder.build(), props);
}
@Test
void givenCustomerMessage_whenDraftReply_thenReturnsAssistantContent() {
server.expect(requestTo("http://localhost:11434/api/chat"))
.andExpect(method(HttpMethod.POST))
.andExpect(jsonPath("$.model").value("llama3.2"))
.andExpect(jsonPath("$.stream").value(false))
.andExpect(jsonPath("$.messages[1].content").value("Wo ist meine Bestellung?"))
.andRespond(withSuccess("""
{"model":"llama3.2",
"message":{"role":"assistant","content":" Wir überprüfen gerade Ihre Bestellung. "},
"done":true,
"total_duration":123456}
""", MediaType.APPLICATION_JSON));
String reply = service.draftReply("Wo ist meine Bestellung?");
assertThat(reply).isEqualTo("Wir überprüfen gerade Ihre Bestellung.");
server.verify();
}
}
Dieser Test überprüft vier Punkte:
- Die Request wird an den korrekten Endpoint
/api/chatmit POST-Methode gesendet. - Der Body hat das richtige Modell und
stream=false. - Die Kundennachricht steht in der zweiten Nachricht.
- Der Service ignoriert unbekannte Felder wie
total_durationund schneidet Whitespace an Anfang und Ende der Antwort.
Wenn du die Antwortqualität überprüfen möchtest, schreibe einen separaten Test mit echtem Modell, zum Beispiel mit Testcontainers. Dieser Test sollte regelmäßig laufen, nicht bei jedem Build.
7. Die Fähigkeit bewahren, zwischen lokal und Cloud zu wechseln
Ollama unterstützt auch OpenAI-kompatible Endpoints bei /v1/chat/completions. Wenn du den Client nach diesem Standard schreibst, wird das Wechseln zwischen lokal und Cloud nur noch eine Konfigurationsänderung je Profil:
# application-local.properties
llm.base-url=http://localhost:11434/v1
llm.model=llama3.2
# application-cloud.properties
llm.base-url=https://api.your-provider.example/v1
llm.model=your-cloud-model
llm.api-key=${LLM_API_KEY}
Du solltest LLM-Aufrufe hinter eine separate Interface wie ReplyDrafter abstrahieren. So kannst du lokale Modelle für sensible Daten und Cloud für Aufgaben mit hohem Anspruch verwenden, ohne dass die Geschäftslogik wissen muss, wo das Modell läuft.
8. Wann sollte und sollte man nicht selbst hosten?
8.1. Selbsthosting ist sinnvoll, wenn
- Daten sensibel sind oder gesetzliche Vorschriften nicht erlauben, Daten an Dritte zu senden.
- Die Last groß und stabil ist, sodass die Token-Kosten die Fixkosten einer GPU überschreiten, auch unter Berücksichtigung der Betriebskosten.
- Die Aufgabe eng und wiederholt ist, wie Ticket-Klassifikation, Feldextraktion aus Rechnungen oder Log-Zusammenfassung. Kleine Modelle leisten hier oft gute Dienste.
- Offline-Betrieb nötig ist, in isolierten Umgebungen oder am Edge.
- Du tiefes Fine-Tuning brauchst oder die Modellversion vollständig kontrollieren musst, damit die Ergebnisse über die Zeit stabil bleiben.
8.2. Selbsthosting ist nicht sinnvoll, wenn
- Du die beste Reasoning-Fähigkeit für komplexe Aufgaben brauchst. Große Cloud-Modelle sind oft noch überlegen.
- Das Datenaufkommen niedrig oder unvorhersehbar ist. Eine GPU, die die meiste Zeit idle läuft, ist fast sicher teurer als Pay-per-Token.
- Dein Team keine Erfahrung mit GPU-Betrieb, Überwachung, Autoscaling und Sicherheits-Updates für Runtimes hat.
- Du noch in der Experimentierphase bist. Validiere den Wert mit Cloud-APIs, bevor du die Kosten optimierst.
Ein einfacher Kostenvergleich:
Kosten Selbsthosting / Monat = Hardware (Abschreibung oder Miete) + Strom/Hosting + Arbeitsstunden
Kosten Cloud / Monat = Tokens / Monat × Token-Preis
Du solltest nur selbst hosten, wenn die Selbsthosting-Kosten deutlich niedriger sind bei realem Datenaufkommen, nicht bei erhofftem.
8.3. Häufige Fehler
Hinweis: Ollama horcht standardmäßig nur auf localhost und hat keine Authentifizierung. Falls du den Port ins Netzwerk öffnest, setze ihn hinter einen Reverse Proxy mit Authentifizierung. Expose ihn niemals direkt ins Internet.
- Modell nur subjektiv bewerten: ein paar Anfragen testen, scheint ok, dann in Production gehen. Du brauchst ein festes Testdataset für deine spezifische Aufgabe.
- KV-Cache vergessen: Modell passt in VRAM, aber läuft aus Speicher bei langem Context oder vielen gleichzeitigen Requests.
- Timeout zu kurz: Der erste Aufruf nach dem Start muss das Modell laden und ist üblicherweise sehr langsam. Du kannst das Modell beim App-Start “aufwärmen”.
- Falsche Runtime für Production: Ollama ist praktisch für Entwicklung und mittlere Last. Bei vielen gleichzeitigen Nutzern solltest du Runtimes in Betracht ziehen, die für Durchsatz optimiert sind, wie vLLM.
9. Fazit
Lokales Selbsthosting von LLMs ist nicht automatisch “kostenloser” oder “besser”. Es ist ein Kompromiss: du hast Kontrolle über Daten, vorhersagbare Kosten und niedrige Latenz, bezahlst aber mit Verantwortung für den Betrieb und musst oft mit kleineren Modellen leben.
In diesem Artikel hast du Ollama mit Docker aufgesetzt und von Spring Boot aus mit RestClient aufgerufen. Du schriebst auch Tests für die Integration ohne echtes Modell und weißt jetzt, wie du die Architektur gestaltest, um zwischen lokal und Cloud zu wechseln. Der praktischste Ansatz ist oft eine Mischung aus beidem: lokale Modelle für sensible Daten und wiederholte Aufgaben, Cloud für Aufgaben mit höchsten Anforderungen.