Spring AI × Spring Boot 4
25 lauffähige Bausteine an einer echten Fachdomäne — der
deutschen Telematikinfrastruktur. Von ChatClient bis zur
selbstkorrigierenden Agenten-Schleife, offline testbar, ohne API-Key im Quality-Gate.
Dieses Repository ist keine Sammlung von Hello-World-Schnipseln. Es ist eine durchgehend verdrahtete Spring-Boot-Anwendung, in der jedes Spring-AI-Konzept an derselben, nicht-trivialen Domäne durchgespielt wird: elektronische Gesundheitskarten, Prüfziffern, Zertifikats-Trust-Chains, ICD-10-Diagnosen. Genau dadurch werden die Unterschiede zwischen den Features sichtbar — ein Tool-Call auf synthetischen Stammdaten verhält sich anders als ein RAG-Treffer auf einem Fachglossar, und beides anders als ein LLM, das SQL gegen ein echtes Schema schreibt und sich dabei selbst korrigiert.
Das Repository in Zahlen
Jedes Feature liegt in einem eigenen Paket featureNN_thema
mit eigenem Controller, eigenen Tests und ausführlichem Javadoc. Kein gemeinsamer
Utility-Sumpf, keine Vererbungshierarchie — wer Feature 14 lesen will, liest fünf Dateien
und ist fertig.
25 Pakete unter com.example.springai.demo,
davon 22 Feature-Pakete (Feature 16 zweimal) plus config,
embedding und testdata.
Die komplette Testsuite läuft offline und ohne API-Key: H2 statt
PostgreSQL, ein deterministisches Hashing-Embedding statt eines Cloud-Modells, gestubbte
Chat-Clients statt echter Modellaufrufe. mvn verify braucht kein
Secret — deshalb läuft es in GitHub Actions bei jedem Push durch.
Der einzige Test, der Docker braucht
(PgVectorPostgresIntegrationTest), ist per
@Tag("docker") ausgeschlossen und läuft in einem eigenen,
manuell ausgelösten Workflow.
Feature-Landkarte
Vier Ausbaustufen: die Kern-API von Spring AI, vertiefte Themen für den Betrieb, ausgewachsene agentische Muster und die Infrastruktur, die den Offline-Betrieb überhaupt erst möglich macht. Jede Karte lässt sich aufklappen.
25 von 25 Bausteinen sichtbar
01ChatClientfeature01_chatclient›
Die Fluent-API, auf der alles andere aufsetzt: prompt().user(…).call().content().
Der ChatClient.Builder wird injiziert, nicht selbst gebaut — dadurch
hängen Auto-Konfiguration, Observability und Advisors automatisch mit drin.
02Streaming (SSE)feature02_streaming›
Statt .call() ein .stream(), das
einen Flux<String> liefert. Spring MVC serialisiert ihn dank
produces = TEXT_EVENT_STREAM_VALUE direkt als Server-Sent-Events —
29 Zeilen für den Unterschied zwischen „lädt…“ und einer Antwort, die mitläuft.
03Prompt Templatesfeature03_prompttemplate›
Prompts mit benannten Platzhaltern statt String-Konkatenation. Der Punkt ist nicht die Bequemlichkeit, sondern die Trennung: Vorlage im Code, Werte aus dem Request — was den Prompt versionierbar und die Eingabe nachvollziehbar macht.
04Structured Outputfeature04_structured · +TicketRepository›
Die Modellantwort landet direkt in einem Java-record — inklusive
Enums für Category, Priority und
Sentiment. Der eigentliche Trick liegt danach: die typisierte
Analyse wird in die Tabelle tickets geschrieben und über
/stats per SQL GROUP BY ausgewertet.
Aus einer KI-Extraktion werden abfragbare Geschäftsdaten.
05Chat Memoryfeature05_memory›
MessageWindowChatMemory mit einem gleitenden Fenster von
20 Nachrichten je Konversations-ID. Gespeichert wird über ein austauschbares
ChatMemoryRepository — in der App das autokonfigurierte
JdbcChatMemoryRepository auf PostgreSQL, in den Tests dasselbe
Repository auf H2. Der Verlauf überlebt einen Neustart, ohne dass eine Codezeile am
Controller geändert wurde.
06Tool Callingfeature06_tools · 8 Tools · 6 Klassen›
Nicht die übliche Wegwerf-Methode, sondern das produktive Muster: die
@Tool-Methoden sind dünne Fassaden vor per DI injizierten
Fach-Services (EgkCheckService,
ProductCatalogService). Fachlogik bleibt testbar, ohne dass ein
Modell im Spiel ist.
Die Tools prüfen KVNR-Prüfziffern nach GKV-Verfahren, Luhn-Prüfziffern von
Institutionskennzeichen, den Kartenstatus einer eGK, lösen ICD-10-Codes auf und bewerten die
Gültigkeit von Zertifikaten. StammdatenPlausibilitaetTool fasst
einen ganzen Versichertensatz zusammen.
07RAG (Basis)feature07_rag · +Import-Konsole›
Der QuestionAnswerAdvisor in seiner einfachsten Form:
Frage → Ähnlichkeitssuche → Antwort mit Kontext. Das Wissen liegt bewusst außerhalb
des Codes in knowledge/telematik-wissen.md; ein Absatz wird zu
einem Dokument (KnowledgeLoader), pflegbar ohne Neukompilierung.
Der lehrreichste Endpunkt ist /api/rag/sources:
er zeigt die abgerufenen Dokumente samt Score, ohne das Modell zu bemühen — RAG-Debugging
ohne API-Key und ohne Halluzinationsrisiko.
08Embeddingsfeature08_embeddings›
Text → Vektor → Kosinus-Ähnlichkeit. Läuft dank des eingebauten
HashingEmbeddingModel vollständig ohne API-Key und macht damit
begreifbar, was ein Vektorspeicher intern tut.
09Multimodalitätfeature09_multimodal›
Bild und Text im selben Prompt via Media-Objekt. Konfiguriert
sind 25 MB Upload-Limit je Datei — die einzige Stelle im Projekt, an der
spring.servlet.multipart überhaupt eine Rolle spielt.
10Advisorsfeature10_advisors · MetricsLoggingAdvisor›
Das Erweiterungskonzept, auf dem Memory und RAG selbst aufbauen. Der selbst geschriebene
MetricsLoggingAdvisor (implementiert CallAdvisor)
misst Latenz und Token-Verbrauch und legt beides in den Antwort-Kontext.
ModelPricing rechnet daraus die realen Kosten in USD —
mit Präfix-Matching für datierte Modell-IDs und einem bewusst günstigen Fallback-Tarif,
damit unbekannte Modelle die Kosten nicht überschätzen.
11MCP — Server & Clientfeature11_mcp›
Das Projekt ist beides zugleich: Es veröffentlicht eigene Tools
(McpInventoryTools) über einen MCP-Server, und es konsumiert
fremde MCP-Server als Client. /api/mcp/client/tools listet auf,
was ein angebundener Server anbietet — ohne API-Key, weil kein Modell beteiligt ist.
12Observabilityfeature12_observability›
Spring AI instrumentiert jeden Modellaufruf von sich aus über Micrometer. Der Endpunkt
macht die gen_ai.*-Metriken direkt sichtbar, statt sie im
Actuator zu verstecken. Vor dem ersten Aufruf ist die Liste leer — das ist kein Fehler,
sondern die Natur von Metrik-Registries.
13Evaluation (LLM-as-a-Judge)feature13_evaluation›
Ein zweites Modell bewertet, ob eine Antwort durch den gelieferten Kontext gedeckt ist — die praktikabelste Halluzinations-Bremse für RAG-Systeme. Der zugehörige Test läuft offline gegen einen gestubbten Judge und prüft damit die Verdrahtung, nicht das Sprachmodell.
14Text-to-SQL mit Guardfeature14_dbquery · 20 Tools›
Die zweistufige Antwort auf „das Modell soll die Datenbank befragen“: Zuerst stehen
19 kuratierte Abfrage-Tools (EgkQueryTools)
bereit — Anzahl Versicherter, Diagnose-Verteilung, Zertifikats-Status, Top-Orte. Erst wenn
keines passt, darf das Modell selbst SQL schreiben und über
ReadOnlySqlTool ausführen.
Davor steht SqlGuard: genau ein Statement, nur
SELECT, keine DDL/DML, keine Mehrfach-Statements. Das Schema
steht komplett im System-Prompt, damit das Modell überhaupt sinnvolles SQL formulieren kann.
15Gate-Decider (KI-Router)feature15_gateway · 7 Klassen · 10 Routen›
Das architektonisch interessanteste Feature: ein Modell entscheidet, welches der zehn Features eine Frage beantworten soll, und ruft genau ein Tool auf. Routing per Sprachverständnis statt per URL.
Der eigentliche Clou ist die Beobachtbarkeit. Ein AspectJ-@Around
über com.example.springai.demo..* schneidet den echten
Methoden-Aufrufbaum mit (CallFlowAspect →
CallNode), während der SqlTraceRecorder
parallel die rohen Tool-Rückgaben festhält — also genau die Zahlen, die das
Modell bekam, bevor es Fließtext daraus machte. Antwort und Beweiskette liegen nebeneinander.
16aEvaluator-Optimizerfeature16_evaluator›
Ein Modell erzeugt SQL, ein zweites bewertet es gegen das Schema; bei Mängeln fließt die
Begründung als Feedback in den nächsten Versuch. Abbruch bei Erfolg oder nach
DEFAULT_MAX_ITERATIONS = 3.
Vorbildlich ist die Bauform: SqlEvaluatorOptimizer ist eine
reine, statische Funktion über zwei funktionalen Interfaces
(Generator, Evaluator) und kennt
weder Spring noch ein LLM. Die komplette Schleifenlogik ist damit offline testbar — das
Ergebnis (Outcome) trägt die vollständige Versuchshistorie als
Beweiskette mit sich.
16bTool Calling Expertfeature16_downloads · OpenAlex›
Das produktive „Tool lädt — App stellt bereit“-Muster. Das Modell sucht über OpenAlex nach Papern und liefert Treffer zurück; die Bytes der Open-Access-PDFs holt und speichert der Server, ausgeliefert werden sie von einem ganz normalen REST-Endpunkt. Die Nutzlast fließt am Modell vorbei — kein Token wird für Binärdaten verbrannt, und es gibt keine Paywall-Umgehung.
Konfiguriert ist eine Kontakt-Mail für den OpenAlex-„polite pool“
(app.openalex.mailto) — leerer Wert bedeutet anonymen Zugriff.
17pgvectorfeature17_pgvector›
Ein Profil-Switch statt eines Umbaus: Die
SimpleVectorStore-Bean trägt
@Profile("!postgres & !specs"). Fällt sie weg, greift die
Spring-AI-Autokonfiguration (@ConditionalOnMissingBean) und
verdrahtet einen PgVectorStore — HNSW-Index,
Cosine-Distance, Metadaten-Filter DB-seitig. Der RAG-Code bleibt unverändert.
/api/pgvector/info nennt die zur Laufzeit aktive
Implementierung — der ehrlichste Weg zu prüfen, ob der Switch wirklich griff.
Fallstrick: Die konfigurierte Dimension muss zur
Ausgabe des Embedding-Modells passen — 256 im Profil postgres
(Hashing-Modell), 384 im Profil specs (ONNX-Transformer).
18Docs-RAG / Spec-Ingestionfeature18_docsrag · Profil „specs“ · 7 Klassen›
Die industrielle Ausbaustufe von RAG: ein @Scheduled-Worker
zieht die gematik-Spezifikations-PDFs aus dem Katalog (sitemap.xml), verarbeitet sie
batchweise zu Chunks und schreibt den Fortschritt in die Tabelle
docs_rag (incoming → processed | error).
Start/Stop zur Laufzeit, Fortschritt über /stats.
/search ist eine Hybrid-Suche: lexikalischer
Volltext-Treffer fusioniert mit semantischer Vektor-Nähe — reines Retrieval, ohne LLM und
ohne API-Key. Hier läuft ein echtes ONNX-Embedding
(paraphrase-multilingual-MiniLM-L12-v2, 384 Dimensionen)
in-process, weil Hashing für Fachprosa nicht reicht.
19Moderation-Guardrailfeature19_moderation›
Anthropic bietet — anders als andere Provider — keinen dedizierten
Moderations-Endpunkt, für den Spring AI ein ModerationModel
autokonfigurieren könnte. Die Lösung folgt der Herstellerempfehlung: Claude selbst als
Klassifikator über die normale Chat-API.
Umgesetzt als eigener CallAdvisor, der beide Richtungen
prüft — Eingabe und Antwort. Getestet wird gegen die Abstraktion
ContentModerator, sodass das Quality-Gate offline bleibt.
219 Zeilen Test auf 250 Zeilen Code: die höchste Testdichte im Projekt.
20Modulare RAG-Pipelinefeature20_modularrag›
Derselbe Ablauf wie Feature 7, aber in austauschbare Bausteine zerlegt:
RewriteQueryTransformer schärft die Suchanfrage,
MultiQueryExpander erzeugt Formulierungsvarianten,
VectorStoreDocumentRetriever sucht,
ContextualQueryAugmenter baut den Kontext zusammen.
Der ehrlich benannte Trade-off: Transformation und Expansion sind je ein
zusätzlicher LLM-Aufruf vor der eigentlichen Antwort. Der deterministische
Retrieval-Schritt ist über /retrieve ohne API-Key sichtbar und
offline getestet.
21KI-Exception-Triagefeature21_exceptiontriage · 11 Klassen›
Das größte Feature im Projekt (756 LOC) und das mit dem klarsten Produktivbezug: ein
globaler @RestControllerAdvice, der jede unbehandelte Ausnahme in
ein RFC-9457-ProblemDetail übersetzt und bei Serverfehlern um eine
typisierte KI-Analyse anreichert (Kategorie, Severity, retriable, Root-Cause,
Handlungsempfehlung, nutzerfreundliche Meldung).
Drei Entscheidungen tragen das Design:
- Das LLM blockiert nie den Antwortpfad — die Analyse läuft
@Asyncin einem eigenen Executor. - Fingerprint-Dedup gegen Fehlerstürme — pro Fingerabdruck wird einmal analysiert, Wiederholungen zählen nur hoch und bekommen die Meldung synchron aus dem Cache.
- 4xx bleibt 4xx, ohne KI — erwartete Client-Fehler sind kein Incident: kein Modellaufruf, keine Kosten, kein Rauschen.
Sauber gelöst ist auch die Rekursionsgefahr: Der Async-Thread ist
kein Web-Request, kann den @RestControllerAdvice also nicht erneut
auslösen — und wird trotzdem defensiv gekapselt. Persistenz ist explizit best-effort: eine
DB-Panne verhindert weder Cache noch Alarmierung.
GENeGK-Testdaten-Generatortestdata · CheckDigits›
Die Voraussetzung dafür, dass alle datengetriebenen Features überhaupt etwas zu sagen haben: 656 Zeilen, die eine kohärente kleine Telematik-Welt erzeugen — Krankenkassen, Versicherte mit eGK, Ärzte und Apotheken, ICD-10-Diagnosen und die zugehörigen Zertifikate samt ausstellender CA.
Alles rein synthetisch und DSGVO-sicher. Die Nummern tragen dabei
korrekte Prüfziffern (CheckDigits: KVNR nach
GKV-Verfahren, IK und ICCSN nach Luhn), gehören aber zu keiner realen Person oder Institution.
Die X.509-Felder sind Metadaten — keine echte Kryptografie.
EMBHashingEmbeddingModelembedding · Offline-Fallback›
Der Anthropic-Provider liefert kein Embedding-Modell. Statt einen zweiten Cloud-Provider (zweiter API-Key) oder einen HuggingFace-Download in die CI zu zwingen, enthält das Projekt ein kleines, deterministisches Hashing-Embedding.
Entscheidend ist, dass es das Standard-Interface
EmbeddingModel implementiert — für Produktion ließe es sich 1:1
gegen ein echtes Modell tauschen, ohne den übrigen Code anzufassen. Genau das passiert im
Profil specs auch tatsächlich.
Eine bewusste Designentscheidung, keine Notlösung: Sie ist der
Grund, warum mvn verify ohne Secrets durchläuft.
CFGDemoBeansconfig · zentrale Verdrahtung›
94 Zeilen, die den Rest schlank halten: EmbeddingModel,
ChatMemory und VectorStore an einer
Stelle, jeweils mit den Profil-Bedingungen, die den Austausch der Implementierung steuern.
Die Feature-Controller bleiben dadurch frei von Infrastruktur-Code.
Bemerkenswert ist die Dichte der Javadoc: Jede Bean erklärt nicht nur was sie tut, sondern warum die Profil-Bedingung genau so lautet und was die Spring-AI-Autokonfiguration daraufhin tut.
Architektur — ein Request, alle Schichten
Alle Feature-Controller teilen sich dieselbe Strecke. Was sie unterscheidet, ist, welche Seitenäste sie aktivieren: Advisors vor dem Modell, Tool-Callbacks nach der ersten Antwort, ein Vektorspeicher davor, eine Datenbank dahinter.
Die Advisor-Kette im Detail
Advisors sind das Erweiterungskonzept von Spring AI — Interzeptoren um den Modellaufruf. Zwei der Features im Projekt sind Advisors, zwei weitere benutzen mitgelieferte.
Der Gate-Decider — Routing per Sprachverständnis
Feature 15 dreht die übliche Richtung um: Nicht der Entwickler entscheidet per URL, welches Feature zuständig ist, sondern das Modell — es bekommt zehn Routen als Tools angeboten und darf genau eines aufrufen. Passt keines, ruft es keines auf.
db-Route, die
ihrerseits Feature 14 mit seinen 20 Tools anstößt. Kaskadiertes Tool-Calling über zwei Ebenen.Ein KI-Router ist nur dann betriebstauglich, wenn nachvollziehbar bleibt, warum eine Antwort so lautet. Feature 15 löst das mit zwei parallelen Aufzeichnungen:
CallFlowAspect legt sich per AspectJ um jede Methode unterhalb
von com.example.springai.demo und baut den echten Aufrufbaum als
CallNode-Struktur — mit dem Endpunkt als Wurzel.
SqlTraceRecorder fängt zusätzlich die rohen
String-Rückgaben der Datenbank-Tools ab: die Zahlen, die das Modell gesehen hat, bevor es
Fließtext daraus formulierte.
Beide Recorder prüfen zuerst isRecording()
— außerhalb einer Gateway-Anfrage kostet der Aspekt praktisch nichts.
- Route
- welches Tool das Modell gewählt hat
- Antwort
- das Ergebnis des delegierten Features
- Call-Flow
- der Aufrufbaum als verschachtelte Knoten
- SQL-Trace
- rohe Tool-Rückgaben, unverändert
- Latenz
- Wall-Clock über die gesamte Verarbeitung
- Tokens
- Input/Output aus der
Usage - Kosten
- USD über
ModelPricing
Die freie Schlussformulierung des Modells wird bewusst verworfen — geliefert wird, was das Tool zurückgab. Ein Router soll routen, nicht nacherzählen.
RAG in drei Ausbaustufen
Das Projekt zeigt RAG nicht einmal, sondern dreimal — und macht dadurch sichtbar, was jede Stufe kostet und was sie bringt. Gleiche Aufgabe, wachsende Komplexität.
| Kriterium | Feature 7 | Feature 20 | Feature 18 |
|---|---|---|---|
| LLM-Aufrufe je Frage | 1 | 3 | 1 (Suche: 0) |
| Embedding | Hashing (offline) | Hashing (offline) | ONNX MiniLM, 384-dim |
| Speicher | SimpleVectorStore | SimpleVectorStore | pgvector · HNSW |
| Wissensquelle | 1 Markdown-Datei | dieselbe | gemspec-PDF-Katalog |
| Ohne API-Key nutzbar | /sources | /retrieve | /search |
| Läuft im Quality-Gate | ja | ja | nur Katalog-Test |
Vektorraum
Was RAG, Ähnlichkeitssuche und Hybrid-Retrieval gemeinsam haben, ist eine einzige Operation: Text wird zu einem Punkt in einem hochdimensionalen Raum, und Nähe bedeutet Bedeutung. Das Projekt macht diese Operation an drei Stellen ohne API-Key beobachtbar.
Der Zeiger drängt die Partikel auseinander — sie finden von selbst
in die Formation zurück. 256 Dimensionen im Profil postgres,
384 im Profil specs.
Was ein Vektorspeicher tatsächlich verwaltet: eine Wolke von Punkten,
zwischen denen nur eines zählt — der Abstand. Die Linien verbinden, was nah beieinander liegt;
genau diese Nachbarschaft liefert ein similaritySearch zurück.
Anthropic liefert kein Embedding-Modell. Die naheliegenden Auswege wären ein zweiter Cloud-Provider (zweiter API-Key, zweite Rechnung) oder ein HuggingFace-Download in der CI (Minuten Laufzeit, Netzabhängigkeit).
Das Projekt wählt einen dritten: ein eigenes, deterministisches
HashingEmbeddingModel hinter dem Standard-Interface. Semantisch
schwach — aber reproduzierbar, sofort verfügbar und ohne jede Abhängigkeit.
Der Preis wird offen benannt: Für Fachprosa reicht Hashing nicht.
Genau deshalb schaltet das Profil specs auf ein echtes
ONNX-Modell um — und beweist damit nebenbei, dass die Abstraktion trägt.
Diese drei Endpunkte sind der didaktische Kern des Projekts: Sie zeigen die Retrieval-Hälfte von RAG isoliert — ohne Modell, ohne Kosten, ohne die Möglichkeit einer Halluzination. Wer verstehen will, warum eine RAG-Antwort falsch war, schaut hier zuerst nach.
Umgekehrt gilt: Liefert bereits das Retrieval die falschen Absätze, kann kein noch so gutes Modell das reparieren.
Datenmodell — die synthetische Telematik-Welt
Elf Tabellen in vier Schema-Dateien, alle bewusst ANSI-nah formuliert, damit dasselbe SQL auf
PostgreSQL (Anwendung) und H2 (Tests) läuft. IF NOT EXISTS
macht jedes Skript idempotent — es wird bei jedem Start ausgeführt.
fx-bend) — nützlich, um eine einzelne Tabelle herauszuheben, ohne
den Zusammenhang zu verlieren. Dazu kommen die von Spring AI selbst angelegten Tabellen für
Chat-Memory und Vektorspeicher (vector_store bzw.
spec_vector_store).Ein Text-to-SQL-Feature auf einer Zwei-Tabellen-Spielzeug-DB beweist nichts. Hier muss das
Modell Fremdschlüssel über drei Ebenen verfolgen (Versicherter → Diagnose → Leistungserbringer),
polymorphe Zuordnungen verstehen (zertifikate.inhaber_typ zeigt je
nach Wert auf zwei verschiedene Tabellen) und den klassischen Fehler vermeiden, ein Alter aus
einer Spalte lesen zu wollen, die es nicht gibt — es gibt nur
geburtsdatum. Genau dieser Fehler ist das Lehrbeispiel im
Evaluator-Optimizer-Handbuch.
Profile — dieselbe Anwendung, vier Betriebsarten
Das durchgängigste Architekturmuster des Projekts: Beans werden getauscht, nicht Code. Jede Betriebsart ist ein Spring-Profil, und die Feature-Controller merken davon nichts.
| Aspekt | default |
postgres |
specs |
test |
|---|---|---|---|---|
| Datenbank | H2 (eingebettet) | PostgreSQL 17 | PostgreSQL 17 | H2 |
| Docker Compose | aus | an | an | aus |
| EmbeddingModel | Hashing | Hashing | ONNX MiniLM | Hashing |
| VectorStore | SimpleVectorStore | PgVectorStore | PgVectorStore | SimpleVectorStore |
| Dimensionen | — | 256 | 384 | — |
| Tabelle | — | vector_store |
spec_vector_store |
— |
| Netzzugriff nötig | nein | Docker | Docker + HF-Download | nein |
@Bean
@Profile("!postgres & !specs")
public VectorStore vectorStore(EmbeddingModel m) {
return SimpleVectorStore.builder(m).build();
}
Entfällt diese Bean, greift die Spring-AI-Autokonfiguration, die mit
@ConditionalOnMissingBean(VectorStore.class) annotiert ist, und
verdrahtet den PgVectorStore. Kein Schalter im Code, keine
Verzweigung im Controller — nur eine Bean, die es gibt oder nicht gibt.
Dasselbe Muster beim EmbeddingModel
(@Profile("!specs")) und beim
ChatMemoryRepository — dort sogar ganz ohne eigene Bean, allein
über den Starter im Klassenpfad.
Dank spring-boot-docker-compose erkennt Spring Boot die
compose.yaml im Projektwurzelverzeichnis: Beim
mvn spring-boot:run wird der Postgres-Container gestartet, die
DataSource automatisch verdrahtet und der Container beim Beenden
wieder gestoppt.
Das Image ist pgvector/pgvector:pg17 statt
postgres:17 — funktional identisch, bringt aber die
vector-Extension mit, ohne die der PgVectorStore nicht starten kann.
Ein Healthcheck auf pg_isready verhindert, dass die Anwendung gegen
eine noch bootende Datenbank läuft.
Die Tests brauchen davon nichts: @ActiveProfiles("test")
plus H2 halten das Quality-Gate frei von Docker.
Guardrails — wo das Projekt dem Modell misstraut
Drei Stellen, an denen bewusst nicht auf das Sprachmodell vertraut wird. Sie sind der interessanteste Teil des Repositorys, weil sie zeigen, wie eine KI-Funktion produktionsreif wird: nicht durch bessere Prompts, sondern durch deterministische Schranken drumherum.
Bevor selbst erzeugtes SQL die Datenbank sieht, prüft SqlGuard
deterministisch: genau ein Statement, ausschließlich
SELECT, keine DDL, keine DML, keine gestapelten Anweisungen.
Der Guard ist reiner Java-Code ohne Spring-Bezug und hat mit
SqlGuardTest einen eigenen Testsatz. Er ist damit die Komponente,
die man wirklich beweisen kann — im Gegensatz zum Prompt, der das Modell nur bittet.
Doppelt abgesichert: Bevor das Modell überhaupt selbst SQL schreiben darf, bekommt es 19 kuratierte Abfrage-Tools angeboten. Freies SQL ist der Notausgang, nicht der Hauptweg.
Ein Guardrail-Advisor prüft Eingabe und Ausgabe. Weil Anthropic keinen dedizierten Moderations-Endpunkt anbietet, übernimmt Claude selbst die Klassifikation über die normale Chat-API — der vom Hersteller empfohlene Weg, ohne zweiten Provider und mit demselben Key.
Getestet wird gegen das Interface ContentModerator, nicht gegen
das Modell. Deshalb kann das Quality-Gate die Guardrail-Logik vollständig prüfen, ohne je eine
Anfrage zu senden.
Ein Klick auf diese Karte zeigt einen Treffer auf dem Schild.
Exception-Triage — der Fehlerpfad als Feature
Erreicht eine Analyse die Stufe CRITICAL, ruft der Orchestrator
zusätzlich den CriticalAlertNotifier. Alles darunter landet nur in
Cache, Datenbank und Log.
Persistenz ist dabei explizit best-effort gekapselt: Fällt die Datenbank aus, verhindert das weder den Cache noch die Alarmierung. Eine Störung im Fehlerpfad darf den Fehlerpfad nicht mitreißen.
Und weil die Analyse auf einem eigenen Thread läuft, kann eine dort
auftretende Ausnahme den globalen @RestControllerAdvice gar nicht
erneut auslösen — Rekursion ist strukturell ausgeschlossen, nicht nur abgefangen.
Die dritte Design-Entscheidung ist die, an die man zuletzt denkt: Client-Fehler
werden nicht triagiert. Eine ungültige Eingabe oder eine
ResponseStatusException ist kein Incident.
Ohne diese Regel würde jeder Tippfehler eines Nutzers einen Modellaufruf auslösen — Kosten, Latenz und Rauschen in der Triage-Tabelle, ohne jeden Erkenntnisgewinn.
Der Handler trägt außerdem
@Order(LOWEST_PRECEDENCE): Existiert im Projekt ein spezifischerer
@ControllerAdvice, hat dieser Vorrang. Der KI-Handler ist die
auffangende letzte Instanz, nicht der Platzhirsch.
Code in Zahlen
Die Verteilung des Codes verrät, wo die Substanz liegt. Die vier größten Pakete sind Exception-Triage, der Testdaten-Generator, Downloads und Docs-RAG — allesamt keine klassischen Spring-AI-Kernthemen, sondern das, was drumherum nötig ist, damit die Kernthemen etwas Echtes zu tun bekommen.
Zeiger über das Diagramm bewegen: Eine Lupe
(fx-magnify) vergrößert den Ausschnitt, Mausrad ändert den
Vergrößerungsfaktor. Rot ist Produktivcode, grün Testcode — der Tooltip nennt zusätzlich die
Testdichte je Paket.
41 Tool-Definitionen
Fast die Hälfte aller Tools sind kuratierte Datenbank-Abfragen. Das ist kein Zufall, sondern die Konsequenz aus dem SqlGuard-Ansatz: Je mehr sichere Tools es gibt, desto seltener muss das Modell selbst SQL schreiben.
Testabdeckung nach Paket
Testzeilen je 100 Produktivzeilen. Auffällig: Die Pakete mit dem höchsten Risiko — pgvector, Evaluation, Moderation, Text-to-SQL — haben die höchste Testdichte. Die dünn getesteten Pakete sind die trivialen Controller mit 30–60 Zeilen.
Entstehung
63 Commits, drei Autoren, im Wesentlichen eine intensive Woche im Juni 2026 plus einen Nachzügler im Juli. Der Verlauf zeigt eine klassische Explorationskurve: erst die Kern-Features in dichter Folge, dann zunehmend große, eigenständige Ausbauten.
- 02.–04. Juni 2026 · 42 Commits Die Kern-Features entstehen. ChatClient, Streaming, Templates, Structured Output, Memory, Tools, RAG, Embeddings, Multimodalität, Advisors — plus die Entscheidung für das Offline-Embedding, ohne die nichts davon in der CI laufen würde.
- 05.–08. Juni 2026 · 19 Commits Vertiefung und Domäne. MCP in beiden Rollen, Observability, Evaluation, pgvector, Moderation. Parallel wächst die Telematik-Domäne heran: eGK-Schema, Testdaten-Generator, Text-to-SQL mit SqlGuard und der Gate-Decider mit seinem AspectJ-Recorder. In dieselbe Phase fällt der dokumentierte StackOverflowError im Moderations-Feature.
- 06.–07. Juli 2026 · 2 Commits Agentische Muster und der Fehlerpfad. Der Evaluator-Optimizer ersetzt RAG im Foliendeck, und Feature 21 bringt die KI-Triage an den globalen Exception-Handler — das bislang größte Einzelfeature.
Quality-Gate
on: push [main, claude/**] · pull_request · workflow_dispatch JDK 21 (temurin) · maven-cache run: mvn -B -ntp verify
Ein einziger Befehl, keine Secrets, concurrency-Gruppe mit
cancel-in-progress, damit ältere Läufe desselben Branches
automatisch abgebrochen werden.
Dass das ohne Key funktioniert, ist keine Selbstverständlichkeit, sondern die Summe mehrerer bewusster Entscheidungen: Offline-Embedding, H2-Dialekt für das Chat-Memory-Schema, Tests gegen Abstraktionen statt gegen Modelle.
on: workflow_dispatch # nur manuell mvn -B -ntp test -Dtest.excludedGroups= -Dgroups=docker
Der PgVectorStore lässt sich nicht auf H2 prüfen. Dafür gibt es
genau einen Integrationstest, der per Testcontainers eine echte pgvector-PostgreSQL hochfährt
und Ablegen und Suche inklusive Metadaten-Filter verifiziert.
Bewusst aus dem Standardlauf ausgeschlossen — schwer, aber ehrlich: Der Test, der nicht offline laufen kann, wird nicht wegdefiniert, sondern separiert. Ein API-Key ist auch hier nicht nötig.
| Was getestet wird | Wie es offline bleibt |
|---|---|
| Prüfziffern-Logik | reine Java-Tests, kein Spring-Kontext |
| Evaluator-Schleife | Generator/Evaluator als Lambdas gestubbt |
| Moderation-Guardrail | gegen das Interface ContentModerator |
| Relevancy-Evaluator | gestubbter Judge, geprüft wird die Verdrahtung |
| Vektorsuche | deterministisches Hashing-Embedding |
| Chat-Memory-Schema | H2-Dialekt aus Spring AI M8 |
| Call-Flow-Aspekt | eigener Test auf die Baumstruktur |
| Spring-Verdrahtung | ein Kontext-Smoke-Test über alle Controller |
| Statische Web-Konsole | eigener Test auf die ausgelieferten Ressourcen |
| pgvector real | Testcontainers, separater Workflow |
Befunde
Bei der Durchsicht sind fünf Abweichungen zwischen Dokumentation und Code aufgefallen. Keine davon bricht etwas — aber jede kostet jemanden, der das Repository zum ersten Mal liest, unnötige Zeit.
-
B1mittelDie README beschreibt nicht mehr den aktuellen Stand. Angegeben ist Spring Boot
4.0.6, diepom.xmlsteht auf4.1.0-RC1. In den Feature-Tabellen fehlen die Features 14 (Text-to-SQL), 15 (Gate-Decider), 16b (Downloads), 18 (Docs-RAG) und der Testdaten-Generator — zusammen rund 2.700 Zeilen, also mehr als 40 % des Produktivcodes. -
B2mittelDie Feature-Nummer 16 ist doppelt vergeben. Es gibt
feature16_evaluator(Evaluator-Optimizer, so auch in der README) undfeature16_downloads, dessen Javadoc sich ebenfalls als „FEATURE 16 – Tool Calling Expert“ bezeichnet. In einem Projekt, dessen gesamte Ordnung auf durchnummerierten Paketen beruht, ist das die verwirrendste denkbare Kollision. -
B3mittelDie Web-Konsole ist nicht offline, obwohl die README das behauptet. Der Text sagt, die Visualisierung komme „ganz ohne externe Chart-Bibliothek aus … und ist damit voll offline“. Tatsächlich lädt
static/index.htmlReact, ReactDOM, Babel-Standalone undmarkedvon unpkg sowie zwei Schriftfamilien von Google Fonts. Die Aussage stimmt für die Diagramme (reines SVG/CSS), nicht für die Seite. Ohne Netz startet die Konsole nicht. -
B4geringVerwaiste Pfadangaben. Die README verweist für das RAG-Wissen auf
knowledge/spring-ai-faq.md; die vorhandene Datei heißtknowledge/telematik-wissen.md. Analog verweist der Kommentar inschema-downloads.sqlauf eine Dateischema-downloads-postgres.sql, die es nicht gibt — praktisch folgenlos, weilBYTEAauf beiden Datenbanken funktioniert, aber irreführend. -
B5geringDas Verzeichnis
rag-import/ist leer, obwohlRagImportControllereinen Ordner-Import anbietet. Für jemanden, der die Import-Funktion ausprobieren will, fehlt damit der Startpunkt.
Erwähnenswert, weil es selten dokumentiert wird: Das Repository hebt einen echten Produktions-
Stacktrace auf. Ein StackOverflowError aus der JSON-Schema-Generierung
für Tool-Definitionen (victools/jsonschema-generator), ausgelöst über
den Dispatcher-Servlet-Pfad — festgehalten am 05. Juni 2026, mitten in der intensivsten
Entwicklungsphase.
Ein aufbewahrter Fehler ist mehr wert als eine geglättete Historie: Er erklärt, warum an manchen Stellen defensiver gebaut wurde als nötig aussieht.
Stack & Betrieb
Laufzeit
| Java | 21 |
| Spring Boot | 4.1.0-RC1 |
| Spring AI | 2.0.0-M8 (BOM) |
| Modell | claude-haiku-4-5-20251001 |
| Temperatur | 0.7 |
| Datenbank | PostgreSQL 17 · pgvector/pgvector:pg17 |
| Build | Maven · 23 Dependencies |
Spring-AI-Starter im Einsatz
- starter-model-anthropic
- starter-model-transformers (Profil specs)
- starter-vector-store-pgvector
- starter-mcp-server-webmvc
- starter-mcp-client
- starter-model-chat-memory-repository-jdbc
- advisors-vector-store
- rag
- pdf-document-reader
Dazu spring-boot-starter-aspectj für
den Call-Flow-Recorder, spring-boot-docker-compose für den
Container-Autostart und spring-dotenv für den lokalen API-Key.
Start in zwei Zeilen
export ANTHROPIC_API_KEY=sk-ant-… # für echte Modellaufrufe mvn spring-boot:run # startet den Postgres-Container selbst # ohne Key läuft alles, was kein Modell braucht: curl "localhost:8080/api/rag/sources?question=Was+ist+eine+eGK?&topK=2" curl "localhost:8080/api/embeddings/similarity?text1=Hund&text2=Katze" curl "localhost:8080/api/pgvector/info"
Unter http://localhost:8080 liegt eine React-Konsole
(2.390 Zeilen JSX/CSS, per Babel im Browser übersetzt), die die Feature-Endpunkte direkt
bedienbar macht: Ticket-Klassifikation mit SVG-Gauge für das Sentiment, Tool-Calling gegen den
Produktkatalog, RAG wahlweise mit Antwort oder nur mit Quellen, Kosten-Badges nach jedem Lauf und
ein Theme-Switcher.
Ein bewusster Kompromiss: kein Build-Schritt, dafür Babel zur Laufzeit und CDN-Abhängigkeiten (siehe Befund B3). Für eine Demo-Konsole vertretbar — für den produktiven Einsatz wäre ein Bundling-Schritt der nächste Schritt.
Fazit
Der Wert dieses Repositorys liegt nicht in der Anzahl der Features, sondern darin, dass jedes einzelne lauffähig, getestet und ohne Zugangsdaten nachvollziehbar ist.
Drei Dinge heben es über eine übliche Feature-Sammlung hinaus. Erstens die durchgehende Fachdomäne: Weil alle Features auf denselben eGK-Daten arbeiten, lassen sie sich direkt vergleichen, statt nebeneinander zu stehen. Zweitens der Offline-Pfad: Jedes Feature hat einen Endpunkt oder einen Test, der ohne API-Key etwas Wahres zeigt — das ist der Grund, warum das Quality-Gate überhaupt existieren kann. Drittens die Guardrails: SqlGuard, Moderation und die Exception-Triage zeigen, dass der Weg von der KI-Funktion zur Produktionsreife nicht über bessere Prompts führt, sondern über deterministische Schranken drumherum.
Die offene Flanke ist die Dokumentation. Der Code ist der README um fünf Features und rund 2.700 Zeilen davongelaufen. Wer das Repository heute zum ersten Mal öffnet, findet die interessantesten Teile — Gate-Decider, Text-to-SQL, Exception-Triage — nur, wenn er im Verzeichnisbaum nachsieht.
Gebaut mit mzone-ui@1.1.0 — Report-Chrome, Chart-Preset auf
chart.js@4.4.6 und zwanzig der einundzwanzig fx-Module (hero,
decrypt, glitch, hexfloat, glyphrain,
particles, particle-reveal, particle-scroll,
ripple, laser, magnify, forcefield,
flame, frost, bend, peel, vhs,
shatter, confetti, scroll). Jeder Effekt wird erst
geladen, wenn sein Abschnitt in Sichtweite kommt, und schaltet sich bei
prefers-reduced-motion selbst ab. Ohne JavaScript bleibt der Report vollständig
lesbar — die Zahlen stehen im HTML, nicht im Skript.
Nicht eingesetzt: fx-object3d. Es scheitert in dieser Version
in jedem Modus mit Cannot access 'loop' before initialization — ein
Temporal-Dead-Zone-Fehler in der Bibliothek selbst, reproduzierbar auch mit dem
Standard-Ikosaeder ohne eigenes GLB. Die Punktwolke im Abschnitt Vektorraum kommt deshalb aus
fx-particles.