REPOSITORY-REPORT · WORKING GROUP BACKEND

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.

Java 21 Spring Boot 4.1.0-RC1 Spring AI 2.0.0-M8 Anthropic Claude Haiku 4.5 PostgreSQL 17 + pgvector

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.

01

Das Repository in Zahlen

Feature-Pakete
22
Java-Klassen
113
Zeilen Produktivcode
6270
Zeilen Testcode
2179
HTTP-Endpunkte
49
@Tool-Definitionen
41
Testfälle
109
Java-Records
69
DAS PRINZIP

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.

DAS QUALITY-GATE

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.

02

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.

Kategorie
Ansicht

25 von 25 Bausteinen sichtbar

01ChatClientfeature01_chatclient›
GET/api/chat?message=…

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.

LOC33 / 0
02Streaming (SSE)feature02_streaming›
GET/api/stream?message=… → text/event-stream

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.

LOC29 / 0
03Prompt Templatesfeature03_prompttemplate›
GET/api/joke?topic=…&language=…

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.

LOC38 / 0
04Structured Outputfeature04_structured · +TicketRepository›
GET/api/recipe?dish=…
POST/api/tickets/analyze (text/plain)
GET/api/tickets/stats

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.

LOC207 / 70
05Chat Memoryfeature05_memory›
POST/api/memory/{conversationId}?message=…

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.

LOC51 / 0
06Tool Callingfeature06_tools · 8 Tools · 6 Klassen›
GET/api/tools?message=…

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.

LOC509 / 154
07RAG (Basis)feature07_rag · +Import-Konsole›
GET/api/rag?question=…
GET/api/rag/sources?question=…&topK=…
POST/api/rag/import/folder · /upload · /clear
GET/api/rag/import/sources · /ask

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.

LOC444 / 68
08Embeddingsfeature08_embeddings›
GET/api/embeddings?text=…
GET/api/embeddings/similarity?text1=…&text2=…

Text → Vektor → Kosinus-Ähnlichkeit. Läuft dank des eingebauten HashingEmbeddingModel vollständig ohne API-Key und macht damit begreifbar, was ein Vektorspeicher intern tut.

LOC61 / 0
09Multimodalitätfeature09_multimodal›
POST/api/multimodal (multipart: image, message)

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.

LOC46 / 0
10Advisorsfeature10_advisors · MetricsLoggingAdvisor›
GET/api/advisors?message=…

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.

LOC206 / 52
11MCP — Server & Clientfeature11_mcp›
GET/api/mcp/client/tools
GET/api/mcp/client/ask?message=…
+ MCP-SSE-Endpunkt (Server-Rolle)

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.

LOC111 / 32
12Observabilityfeature12_observability›
GET/api/observability/ask?message=…
GET/api/observability/metrics
GET/actuator/metrics/gen_ai.client.token.usage

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.

LOC51 / 41
13Evaluation (LLM-as-a-Judge)feature13_evaluation›
GET/api/evaluate/relevancy?question=…&context=…&answer=…

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.

LOC40 / 57
14Text-to-SQL mit Guardfeature14_dbquery · 20 Tools›
GET/api/db/ask?question=…
POST/api/db/execute (text/plain SQL)

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.

LOC429 / 254
15Gate-Decider (KI-Router)feature15_gateway · 7 Klassen · 10 Routen›
GET/api/gateway?question=…

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.

LOC529 / 220
16aEvaluator-Optimizerfeature16_evaluator›
GET/api/evaluator/sql?question=…

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.

LOC267 / 83
16bTool Calling Expertfeature16_downloads · OpenAlex›
GET/api/downloads/chat?message=…
POST/api/downloads/papers
GET/api/downloads · /api/downloads/{id}/file

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.

LOC568 / 104
17pgvectorfeature17_pgvector›
GET/api/pgvector/info
POST/api/pgvector/documents?text=…&category=…
GET/api/pgvector/search?query=…&category=…

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).

LOC84 / 133
18Docs-RAG / Spec-Ingestionfeature18_docsrag · Profil „specs“ · 7 Klassen›
GET/api/docs-rag/stats
POST/api/docs-rag/seed · /start?batch=1|5|10 · /stop
GET/api/docs-rag/search?question=…&topK=…

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.

LOC555 / 46
19Moderation-Guardrailfeature19_moderation›
GET/api/moderation?message=…
GET/api/moderation/check?text=…

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.

LOC250 / 219
20Modulare RAG-Pipelinefeature20_modularrag›
GET/api/modular-rag?question=…
GET/api/modular-rag/retrieve?question=…

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.

LOC95 / 43
21KI-Exception-Triagefeature21_exceptiontriage · 11 Klassen›
GET/api/errors/trigger?kind=…
GET/api/errors/live · /api/errors/stats

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 @Async in 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.

LOC756 / 238
GENeGK-Testdaten-Generatortestdata · CheckDigits›
POST/api/testdata/generate
GET/api/testdata/stats

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.

LOC656 / 203
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.

LOC108 / 41
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.

LOC94 / 0
Kern-API (10) Vertieft (6) Agentisch (6) Infrastruktur (3) Balken: Produktivcode Balken: Testcode
03

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.

Request-Fluss der Spring-AI-Demo Ein HTTP-Request läuft über Spring MVC in den ChatClient, durchquert die Advisor-Kette, erreicht die Anthropic-API und kann über Tool-Callbacks auf Fach-Services, PostgreSQL und den Vektorspeicher zugreifen. Client Web-Konsole · curl Spring MVC 49 Endpunkte 25 Controller @RestControllerAdvice ChatClient prompt() · call() stream() Advisor-Kette Moderation (in) Memory · QA/RAG Metrics · Logging Moderation (out) Anthropic API claude-haiku-4-5 Tool-Call: das Modell fordert Daten an Tool-Callbacks 41 × @Tool dünne Fassaden vor Services Fach-Services EgkCheckService SqlGuard · PaperSearch PostgreSQL 17 11 Tabellen JdbcTemplate VectorStore Simple (H2) ⇄ pgvector HNSW · Cosine Kontext für RAG durchgezogen = Aufruf gestrichelt = Datenzugriff
Der Weg eines Requests. Der Rückweg unten ist der eigentlich interessante: Tool-Callbacks laufen synchron innerhalb des Modellaufrufs — deshalb kann der AspectJ-Recorder aus Feature 15 den gesamten Aufrufbaum in einem Request mitschneiden.

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.

Prompt Moderation ModerationGuardrail Advisor · F19 Memory MessageWindow ChatMemory · F5 Retrieval QuestionAnswer · F7 RetrievalAugm. · F20 Metrics MetricsLogging Advisor · F10 Modell Claude rot = Hinweg (Anfrage wird angereichert) grün = Rückweg (Antwort wird geprüft und vermessen) Moderation prüft beide Richtungen — Input- und Output-Guardrail. Metrics misst Latenz und Tokens erst auf dem Rückweg.
Jeder Advisor sieht die Anfrage auf dem Hinweg und die Antwort auf dem Rückweg. Deshalb kann derselbe Moderations-Advisor Eingabe und Ausgabe filtern, und deshalb kennt der Metrics-Advisor die Token-Zahlen, die der Controller nie zu Gesicht bekommt.
04

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.

Nutzerfrage GET /api/gateway Gate- Decider System-Prompt: „genau EIN Tool“ chatfreie Frage jokeF3 · Prompt-Template toolsF6 · eGK-Einzelsatzprüfung ragF7 · Wissensspeicher embeddingsF8 · Ähnlichkeit advisorsF10 · Interzeptoren mcpF11 · entfernte Tools observabilityF12 · Metriken evaluateF13 · Judge dbF14 · Text-to-SQL
Die rote Strecke zeigt einen konkreten Lauf: Die Beispielfrage „Wie viele Versicherte haben E11.9?“ landet bei der db-Route, die ihrerseits Feature 14 mit seinen 20 Tools anstößt. Kaskadiertes Tool-Calling über zwei Ebenen.
DIE BEWEISKETTE

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.

WAS DER ENDPUNKT ZURÜCKGIBT
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.

05

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.

Feature 7 — einfach 1 LLM-Aufruf · SimpleVectorStore Frage VektorsuchetopK Antwort + Kontext /api/rag/sources zeigt die Treffer ohne LLM Feature 20 — modular 3 LLM-Aufrufe · austauschbare Bausteine Frage RewriteTransformer MultiQueryExpander RetrieverVectorStore ContextualAugmenter +1 Aufruf +1 Aufruf deterministisch Feature 18 — industriell Profil „specs“ · ONNX 384-dim pgvector · HNSW gemspecsitemap.xml Worker@Scheduled PDF → ChunksEmbedding spec_vector_storePostgreSQL Volltext (lexikalisch)docs_rag Vektor (semantisch)Cosine Hybrid-Fusion/search Status je PDF: incoming → processed | error
Von oben nach unten wächst nicht nur der Aufwand, sondern auch die Qualität der Quelle: Feature 7 arbeitet auf 21 handgepflegten Absätzen, Feature 18 auf einem laufend nachgeladenen Korpus echter Spezifikations-PDFs.
KriteriumFeature 7Feature 20Feature 18
LLM-Aufrufe je Frage131 (Suche: 0)
EmbeddingHashing (offline)Hashing (offline)ONNX MiniLM, 384-dim
SpeicherSimpleVectorStoreSimpleVectorStorepgvector · HNSW
Wissensquelle1 Markdown-Dateidieselbegemspec-PDF-Katalog
Ohne API-Key nutzbar/sources/retrieve/search
Läuft im Quality-Gatejajanur Katalog-Test
06

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.

TEXT → VEKTOR → NÄHE

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.

DIE ENTSCHEIDUNG, DIE ALLES TRÄGT

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.

DREI WEGE IN DEN RAUM — ALLE OHNE KEY
GET/api/embeddings/similarity?text1=…&text2=…
GET/api/rag/sources?question=…&topK=2
GET/api/docs-rag/search?question=…&topK=8

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.

07

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.

krankenkassen ik · name · typ GKV / PKV Luhn-Prüfziffer auf ik versicherte kvnr · vorname · nachname geburtsdatum · geschlecht plz · ort · strasse → krankenkasse_id KVNR: Buchstabe + 8 Ziffern + PZ egk_karten iccsn · can gueltig_von · gueltig_bis → versicherte_id AKTIV / GESPERRT / ERSETZT diagnosen icd10 · diagnose_text diagnose_datum → versicherte_id → leistungserbringer_id leistungserbringer typ · name lanr · bsnr plz · ort ARZT/APOTHEKE/KLINIK zertifikate typ · seriennummer subject_dn · status not_before · not_after → ca_id · inhaber_id HBA/SMC_B/EGK_AUT/ENC ca_zertifikate name · subject_dn seriennummer not_before/after 1:n 1:n 1:n 1:n n:1 polymorph ↑ Fachdomäne (schema-egk.sql) — ↓ Betriebs-/Feature-Tabellen tickets category · priority sentiment · summary Feature 4 — schema.sql docs_rag spec · version · category status · chunks · pdf_url Feature 18 — UNIQUE(spec, version) downloads title · source_ref content_type · data Feature 16b — Bytes in der DB exception_triage fingerprint · severity root_cause · suggested_action Feature 21 — best-effort
Der Zeiger wölbt das Diagramm wie eine Linse (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).
WARUM DAS FÜR EINEN KI-DEMO WICHTIG IST

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.

08

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
DER MECHANISMUS
@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.

DOCKER-COMPOSE-AUTOSTART

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.

09

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.

SQLGUARD — SYNTAKTISCHE SCHRANKE

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.

MODERATION — SEMANTISCHE SCHRANKE

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

Client AiExceptionHandler TriageStore Orchestrator (@Async) Modell + DB unbehandelte Exception (5xx) Fingerprint registrieren ProblemDetail (RFC 9457) — sofort, ohne Wartezeit triageAsync(ctx) — eigener Executor, nicht der Request-Thread Analyse · persist · alert bei CRITICAL store.complete() — ab jetzt synchron aus dem Cache Jede Wiederholung desselben Fingerprints wird nur hochgezählt — ein Fehlersturm kostet genau einen Modellaufruf.
Die entscheidende Linie ist die dritte: Der Client hat seine Antwort, bevor die Analyse überhaupt begonnen hat. Latenz und Kosten des Modells liegen komplett außerhalb des Antwortpfads.
SEVERITY: CRITICAL

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.

4XX BLEIBT 4XX

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.

10

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.

11

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.

Commits
63
Autoren
3
Berührte Dateien
230
Aktivste Woche
58Commits
  1. 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.
  2. 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.
  3. 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.
12

Quality-Gate

Testklassen
34
Testfälle
109
API-Keys nötig
0
Docker nötig
0
WORKFLOW · CI.YML
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.

WORKFLOW · CI-DOCKER.YML
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 wirdWie es offline bleibt
Prüfziffern-Logikreine Java-Tests, kein Spring-Kontext
Evaluator-SchleifeGenerator/Evaluator als Lambdas gestubbt
Moderation-Guardrailgegen das Interface ContentModerator
Relevancy-Evaluatorgestubbter Judge, geprüft wird die Verdrahtung
Vektorsuchedeterministisches Hashing-Embedding
Chat-Memory-SchemaH2-Dialekt aus Spring AI M8
Call-Flow-Aspekteigener Test auf die Baumstruktur
Spring-Verdrahtungein Kontext-Smoke-Test über alle Controller
Statische Web-Konsoleeigener Test auf die ausgelieferten Ressourcen
pgvector realTestcontainers, separater Workflow
13

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.

  1. B1mittel
    Die README beschreibt nicht mehr den aktuellen Stand. Angegeben ist Spring Boot 4.0.6, die pom.xml steht auf 4.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.
  2. B2mittel
    Die Feature-Nummer 16 ist doppelt vergeben. Es gibt feature16_evaluator (Evaluator-Optimizer, so auch in der README) und feature16_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.
  3. B3mittel
    Die 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.html React, ReactDOM, Babel-Standalone und marked von 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.
  4. B4gering
    Verwaiste Pfadangaben. Die README verweist für das RAG-Wissen auf knowledge/spring-ai-faq.md; die vorhandene Datei heißt knowledge/telematik-wissen.md. Analog verweist der Kommentar in schema-downloads.sql auf eine Datei schema-downloads-postgres.sql, die es nicht gibt — praktisch folgenlos, weil BYTEA auf beiden Datenbanken funktioniert, aber irreführend.
  5. B5gering
    Das Verzeichnis rag-import/ ist leer, obwohl RagImportController einen Ordner-Import anbietet. Für jemanden, der die Import-Funktion ausprobieren will, fehlt damit der Startpunkt.
AUS DEM ARCHIV · TROUBLESHOOTING/FEATURE_19_MODERATION_BUG.MD

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.

14

Stack & Betrieb

Laufzeit

Java21
Spring Boot4.1.0-RC1
Spring AI2.0.0-M8 (BOM)
Modellclaude-haiku-4-5-20251001
Temperatur0.7
DatenbankPostgreSQL 17 · pgvector/pgvector:pg17
BuildMaven · 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"
DIE WEB-KONSOLE

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.

15

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.