Alle Beiträge

LLM austauschen ohne Code-Änderung: eine RAG-App auf Azure, OpenAI und lokalem Mistral

Der Anwendungscode blieb unverändert, als ich das LLM dreimal getauscht habe. Die Konfiguration hätte mich fast erwischt — zwei Fallen lohnen den Post.

Kurz beantwortet

Teil 2 der RAG-Tutorial-Serie macht den LLM-Anbieter (Azure OpenAI, OpenAI-API oder komplett offline Mistral via Ollama) und den Vektorspeicher (in-memory oder PostgreSQL + pgvector) per Spring-Profil austauschbar — der RAG-Code selbst ändert sich nie. Zwei Fallen lohnen den Post allein: Mit mehreren Anbieter-Startern auf dem Classpath muss jeder spring.ai.model.*-Selektor auf none stehen (nicht nur chat und embedding — die Image- und Audio-Autokonfigurationen sind standardmässig aktiv und crashen ohne Zugangsdaten), und der Wechsel des Embedding-Anbieters ist eine Datenmigration, keine Config-Änderung: Dimensionalität und Vektorräume unterscheiden sich (1536 vs. 768) — es muss neu ingestiert werden.

Ich habe letzte Woche dreimal das LLM unter einer laufenden RAG-Anwendung getauscht — Azure OpenAI, dann die normale OpenAI-API, dann ein lokales Mistral im Docker-Container. Der Anwendungscode blieb unverändert. Die Konfiguration hätte mich fast erwischt, und die zwei Stellen, an denen sie sich gewehrt hat, sind genau das Thema von Teil 2 meiner RAG-Tutorial-Serie.

Teil 1 hat den kleinsten funktionierenden RAG-Loop gebaut: Dokumente ingestieren, Chunks holen, Prompt augmentieren, generieren. Ein Anbieter (Azure OpenAI), ein In-Memory-Speicher, alles sichtbar. Teil 2 stellt die Frage, an der sich entscheidet, ob eine Abstraktion echt ist: Kannst du die teuren Entscheidungen — welches Modell, welcher Speicher — ändern, ohne den Code anzufassen, der die Arbeit macht?

Das Design: Profile wählen aus, Code injiziert

Der RAG-Service und die Ingestion-Pipeline injizieren drei Spring-AI-Interfaces: ChatModel, EmbeddingModel, VectorStore. Welche Implementierungen ankommen, entscheiden beim Start Spring-Profile — eine Achse für den Anbieter (azure, openai, ollama), eine für den Speicher (simple in-memory, pgvector auf PostgreSQL). Jede Kombination funktioniert:

mvn spring-boot:run -Dspring-boot.run.profiles=pgvector,openai
mvn spring-boot:run -Dspring-boot.run.profiles=simple,ollama   # komplett offline

Die ollama-Variante ist mir die wichtigste: Mistral für Chat, nomic-embed-text für Embeddings, beides aus einem dockerisierten Ollama. Kein API-Key, keine Cloud, kein Dokument verlässt die Maschine. Wer Fragen über interne Dokumente unter schweizerischen oder europäischen Datenerwartungen beantwortet, für den ist „das Offline-Profil existiert und ist gleichberechtigt" kein Demo-Feature — es ist das Argument.

Falle eins: none heisst alle

Mit drei Anbieter-Startern auf dem Classpath aktiviert Spring AI jede Autokonfiguration, deren Selektor-Property nicht gesetzt ist. Also deaktiviert man standardmässig alle und lässt jedes Profil seine eigene wieder einschalten:

spring.ai.model:
  chat: none
  embedding: none

Das habe ich zuerst geschrieben. Es kompiliert, es sieht vollständig aus, und es crasht beim Start unter dem ollama-Profil — mit einem Azure-Fehler: „Endpoint must not be empty."

Der Grund: chat und embedding sind nicht die einzigen Modelltypen. Die Azure- und OpenAI-Starter bringen auch **Image-, Audio- und Moderation-**Autokonfigurationen mit, jede mit eigenem Selektor (spring.ai.model.image, spring.ai.model.audio.transcription, …) — und jede standardmässig aktiv, wenn nichts gesetzt ist. Die Image-Autokonfiguration hat pflichtbewusst versucht, einen Azure-Client zu bauen, in einem Profil ohne Azure-Zugangsdaten. Der Fix ist langweilig und vollständig: jeden Selektor im Basis-Config auf none. Die Lektion ist weniger langweilig: Wer standardmässig deaktiviert, muss die ganze Oberfläche aufzählen, nicht den Teil, den er benutzt. Conditional Beans mit Default „an" scheitern im Profil, das du nicht getestet hast.

Falle zwei: Embeddings tauschen ist eine Datenmigration

Das Chat-Modell ist wirklich austauschbar: gleicher Prompt rein, Antwort raus, nichts gespeichert. Das Embedding-Modell nicht — aus zwei gestapelten Gründen.

Erstens die Dimensionalität: OpenAI/Azure text-embedding-3-small erzeugt 1536-dimensionale Vektoren, nomic-embed-text 768. pgvector backt die Dimension in sein Tabellenschema, deshalb macht das Tutorial daraus einen Konfigurationsknopf (rag.vector-store.dimensions), den das ollama-Profil überschreibt.

Zweitens — und das erwischt die, die die Dimensionen richtig haben — Vektoren verschiedener Modelle leben in verschiedenen Räumen. Selbst bei gleicher Grösse ergeben Distanzen zwischen einer Frage, die Modell A embedded hat, und Chunks von Modell B keinerlei Bedeutung. Das Retrieval wirft keinen Fehler; es liefert einfach leise Müll.

Die ehrliche Regel lautet also: Embedding-Anbieter wechseln heisst Vektortabelle löschen und alles neu ingestieren. Behandle es wie eine Schema-Migration, nicht wie eine Config-Änderung. Praktisch heisst das: Quellen abrufbar halten — Dateien, URLs, Wiki-Seiten —, damit Re-Ingestion ein Skript ist, das du laufen lässt, und keine Ausgrabung. Die Ingestion-Historie des Tutorials existiert genau dafür: damit sichtbar ist, was neu eingespielt werden müsste.

Was pgvector dir kauft

Die zweite Profil-Achse ergänzt PostgreSQL mit der pgvector-Extension via docker-compose. Ich habe es dedizierten Vektordatenbanken vorgezogen, und der Grund ist es wert, ausgesprochen zu werden: Es ist einfach Postgres. Backups, die du schon kennst, SQL-Zugriff auf deine Chunks (select * from vector_store ist eine Debugging-Superkraft), Betrieb, den dein Team schon macht. Eine dedizierte Vektor-DB verdient ihren Platz bei einer Grösse, die die meisten internen RAG-Systeme nie erreichen.

Der Takeaway

Eine Abstraktion ist erst echt, wenn du die Implementierung darunter getauscht hast — und beim Tausch lernst du. Zwei Dinge tragen über dieses Tutorial hinaus: Deaktivieren-per-Default muss die gesamte Autokonfigurations-Oberfläche abdecken, und Embedding-Modelle sind zustandsbehaftete Entscheidungen mit Migrationskosten, keine austauschbaren Endpoints.

Teil 3 verbindet die Pipeline mit den Orten, wo Dokumente und Fragen wirklich leben: Confluence, Microsoft Teams und Slack — als Konnektoren, die standardmässig aus sind und jede Webhook-Signatur prüfen.

Code: github.com/halviclabs/rag-tutorials — Teil 2 ist rag-tutorial-02-llms, eigenständig mit docker-compose für Postgres und Ollama.

Häufige Fragen

Wie wechsle ich den LLM-Anbieter in einer Spring-AI-Anwendung?

Die Anbieter-Starter auf den Classpath legen, alle Modell-Autokonfigurationen standardmässig deaktivieren, indem jeder spring.ai.model.*-Selektor (chat, embedding, image, moderation, audio.*) auf none steht, und jedes Spring-Profil genau ein Chat- und ein Embedding-Modell wieder einschalten lassen — etwa spring.ai.model.chat: ollama. Die Services injizieren weiterhin ChatModel, EmbeddingModel und VectorStore; welche Implementierung ankommt, entscheidet beim Start das aktive Profil.

Kann eine RAG-Anwendung komplett offline laufen?

Ja. Mit Ollama, das Mistral für Chat und nomic-embed-text für Embeddings bereitstellt, plus einem lokalen Postgres mit pgvector läuft der gesamte Stack — Ingestion, Embedding, Retrieval, Generation — auf der eigenen Maschine. Kein API-Key, keine Daten verlassen das Netz. Auf CPU sind Antworten langsamer, und kleinere lokale Modelle halten sich weniger zuverlässig an Grounding-Anweisungen als die grossen Cloud-Modelle — Qualität am eigenen Use Case beurteilen.

Warum ist die Embedding-Dimension für pgvector wichtig?

pgvector legt die Vektor-Dimensionalität fest in seinem Tabellenschema an und muss sie deshalb vorab kennen — 1536 bei OpenAI/Azure text-embedding-3-small, 768 bei nomic-embed-text. Darum stellt das Tutorial rag.vector-store.dimensions als Konfigurationsknopf bereit, den die Anbieter-Profile überschreiben.

Kann ich gespeicherte Vektoren beim Wechsel des Embedding-Anbieters behalten?

Nein. Embeddings verschiedener Modelle leben in verschiedenen Vektorräumen — selbst bei identischer Dimensionalität sind Distanzen zwischen Vektoren unterschiedlicher Modelle bedeutungslos. Den Embedding-Anbieter zu wechseln heisst: Vektortabelle löschen und alle Dokumente neu ingestieren. Plant das ein: Quellen abrufbar halten, damit Re-Ingestion ein Skript ist und keine Ausgrabung.

AI-Code ohne Tech-Schuld — die Checkliste

Trag dich ein: die Checkliste plus neue Beiträge zu KI-Engineering. Kein Spam, jederzeit abbestellbar.