Multi-Tenant RAG · DEBau · WasserversorgerDIN / EN / DWA-NormenKunden unter NDA

Deutsche technische RAG — als das Standard-Framework nicht reichte, haben wir es selbst gebaut

Eine mandantenfähige KI-Wissensbasis für zwei deutsche Kunden (unter NDA): ein Hersteller von Betonprodukten (Rohre, Drainage, Abwassersysteme) und ein regionaler kommunaler Wasserversorger. Beide leben auf Terabytes deutscher technischer Dokumente und DIN-/EN-/DWA-Normen, in denen jede Ziffer und jede Normreferenz zählt. Wir sind mit einem populären Python-RAG-Framework gestartet. Es ließ stillschweigend Chunks fallen, versteckte Zwischenzustände und machte jede Iteration teuer. Also haben wir den DAG durch eine einzige Orchestrator-Funktion ersetzt und das Retrieval von Grund auf neu aufgebaut — und ab diesem Moment waren die Antworten richtig.

2Produktive Mandanten
0DAG-Frameworks (rag2)
DIN / EN / DWANormen pro Chunk erhalten
23+Gold-Fälle / Mandant

Kunden

Zwei deutsche Mandanten (unter NDA): ein Hersteller von Betonprodukten (Rohre, Drainage, Abwassersysteme) und ein regionaler kommunaler Wasserversorger. Jeder auf eigener Subdomain, beide auf derselben mandantenfähigen Plattform.

Engagement

Discovery → Architektur → Produktion. Der entscheidende Schritt kam mitten im Projekt: das Standard-RAG-Framework hinauswerfen und eine eigene Control-First-Pipeline (rag2) schreiben. Das hat die Antworten verlässlich genug für Produktion gemacht.

Generisches RAG zerbricht an deutschen technischen Dokumenten. Das Framework hat es noch schlimmer gemacht.

Die Dokumente auf beiden Seiten sind technisch und unnachgiebig: Datenblätter und Produktkataloge für das Beton-Geschäft, Bauprotokolle, Kanalnetzstandards und DIN-Normen für den Wasserversorger. Die Fragen sind ebenfalls technisch — welcher Rohrdurchmesser passt zu diesem Projekt? was sagt die einschlägige DIN-Norm zur Kanalsanierung? welche Expositionsklasse brauche ich in dieser Tiefe? Generische, auf Englisch trainierte Embeddings versagen an deutschen Komposita. Naive Chunker zerreißen Tabellen. Und eine „fast richtige" Antwort ist in dieser Domäne die falsche Antwort.

Die erste Version der Plattform basierte auf einem bekannten Python-RAG-Framework — der Sorte mit DAG, Komponenten, Graph-Executor. Auf der Demo lief es. Auf dem echten Korpus lief es nicht. Komponenten haben stillschweigend übersprungen, wenn Inputs die falsche Form hatten. Eigene Logik nach dem Reranking (Page-Floor, Coverage-Retry) war auf dem Graph brittle. Profiling und Schritt-für-Schritt-Debugging mussten gegen den Scheduler kämpfen. Jede Regression war eine mehrstündige Ermittlung. Das Framework war der Bottleneck.

Also haben wir das Unmodische gemacht: ihn gelöscht und eine einzige Orchestrator-Funktion geschrieben — rag2 — in der jeder Retrieval-Schritt ein expliziter Python-Aufruf ist. Debugger funktionieren. Profiler funktionieren. Asserts funktionieren. Jeder Booster ist Feature-geflaggt, sodass die Bench Gewinne pro Feature zuordnen kann. Um diese Architektur geht es in dieser Fallstudie.

Eine Orchestrator-Funktion. Explizite Stufen. Selbst gebautes Retrieval, auf deutsche technische Dokumente abgestimmt.

Kein Haystack. Kein LangChain. Kein LlamaIndex. Nur openai, pinecone, bm25s, sentence-transformers, docling, tiktoken, pydantic — verdrahtet als eine einzige Pipeline-Funktion, die wir Zeile für Zeile durchsteppen können.

Mandantenfähig per Konstruktion, fail-closed bei Kollisionen. Jeder Mandant lebt auf eigener Subdomain, mit eigener Postgres-Datenbank (Supabase), eigenem Pinecone-Namespace, eigenen Upload-Pfaden, eigener CORS-Allow-List und eigenem MinIO-Bucket. Die Mandantenkonfiguration wird per Middleware-Resolver mit TTL-Caching zur Request-Zeit geladen. Die Konfigurationsschicht verweigert den Start, wenn zwei Mandanten versehentlich dieselbe DB-URL oder denselben Namespace teilen.

Dateinamen-Routing vor dem Retrieval. Viele technische Fragen nennen das Dokument direkt („das DIN-1610-Verfahren", „das DN-800-Datenblatt"). Eine Router-Stufe erkennt Ziel-Dateinamen aus dem Frage-Text und schaltet auf einen Full-File-Bypass: wenn das Zielset klein ist, ruft das Retrieval diese Dateien Ende-zu-Ende ab, statt Chunks zu raten. Allein das hat eine Klasse von „das System antwortet mit der falschen Produktfamilie"-Fehlern behoben.

Pro-Mandant Synonymerweiterung. Domänenvokabulare sind als pro-Mandant JSON-Wörterbücher kodiert — Beton-Rohr-Terminologie für den einen Mandanten, Kanalnetz-Terminologie für den anderen. Die Erweiterung läuft vor dem Retrieval, mit der Original-Anfrage als Fallback, sodass wir nie Recall verlieren.

Hybrides Retrieval — unseres, nicht ihres. Dense Vectors über OpenAI text-embedding-3-large (Matryoshka-trunkiert auf 1024 Dimensionen) in Pinecone, sparse über bm25s (eine schnelle BM25-Neuimplementierung), fusioniert via Reciprocal Rank Fusion. Ein Cross-Encoder-Reranker (mmarco-mMiniLMv2-L12, mehrsprachig) reranked den fusionierten Satz. Eine optionale LLM-als-Reranker-Stufe steht hinter einem Flag für schwierige Fragen bereit.

Page-Floor und Diversitäts-Cap — die unscheinbaren Fixes, die die Bench gewonnen haben. Wenn das Zielset bekannt ist, garantiert die Page-Floor-Stufe mindestens einen Chunk pro Seite aus den Zieldateien, sodass ein einzelner hochbewerteter Chunk nicht den Rest des Dokuments verdrängt. Ein Diversitäts-Cap begrenzt dann, wie viele Chunks eine einzelne Seite beisteuern darf, und verhindert, dass das Kontextfenster auf einem Abschnitt sättigt.

Anchor-Boost auf DIN-/EN-/DWA-Referenzen. Ein Chunker-Pass extrahiert DIN \d+(-\d+)?, DWA-[AM] \d+(-\d+)?, EN \d+(-\d+)?, ISO \d+(-\d+)? und speichert sie in den Chunk-Metadaten. Die Retrieval-Stufe boostet Chunks, deren Anchor-Set die Anfrage überschneidet. Der Synthesizer wird angewiesen, jede Normreferenz wortgleich zu erhalten.

Token-Budget-bewusster Kontext-Trim. Das Kontextbudget wird mit tiktoken erzwungen, nicht mit einer ungefähren Zeichenanzahl — wir wissen also exakt, was das Modell sieht, und kürzen nie versehentlich mitten in einer Tabelle.

Synthese mit pro-Mandant Few-Shot-Beispielen. Der Synthesizer (OpenAI Chat Completion) bekommt pro-Mandant Few-Shot-Beispiele, die das LLM lehren, DIN-Bezeichner wortgleich zu erhalten, Komposita-Nomenklatur unverändert zurückzugeben und auf Deutsch zu bleiben. Determinismus ist konfig-gesteuert (seed=42, temperature=0) für reproduzierbare Antworten in einem regulierten Kontext.

Coverage-Retry. Nach der Generierung vergleicht die Pipeline die Anchors im abgerufenen Kontext mit den Anchors in der Antwort. Wenn das LLM einen Anchor fallen gelassen hat, der hätte zitiert werden müssen, wird die Synthese erneut mit explizit aufgeführtem Anchor im Prompt ausgeführt. Billig, mechanisch — und es hat eine ganze Klasse von „das Modell hat die DIN-Nummer wegzusammengefasst"-Fehlern eliminiert.

Empirische Bench, kein Bauchgefühl. Ein 23-Fälle-Goldset pro Mandant läuft in CI. Jeder Booster ist Feature-geflaggt, sodass die Bench Gewinne pro Feature zuordnet, nicht pro Release. Wir wussten, welches Custom-Stück seinen Platz verdient hat — und welches nicht.

rag2-Pipeline (eine Funktion, zehn explizite Stufen)

1.RouterZiel-Dateinamen aus dem Frage-Text erkennen
2.SynonymerweiterungPro-Mandant JSON-Vokabular (Beton / Kanal / DIN)
3.RetrievalFull-File-Bypass · ODER dense (Pinecone) + sparse (bm25s) → RRF
4.Anchor-BoostKandidaten boosten, deren DIN-/EN-/DWA-Refs mit der Anfrage überlappen
5.RerankingCross-Encoder mmarco-mMiniLMv2-L12 (mehrsprachig)
6.Page-FloorFür Zieldateien: ≥1 Chunk pro Seite garantieren
7.Diversitäts-CapChunks pro Seite begrenzen, um Kontextsättigung zu verhindern
8.Kontext-Trimtiktoken-budget-bewusster Slice (10K Tokens, 12 Docs max)
9.SyntheseOpenAI Chat mit pro-Mandant Few-Shot · seed=42 · temp=0
10.Coverage-RetryHat das LLM einen Anchor fallen gelassen → erneut synthetisieren mit gepinntem Anchor
OpenAI text-embedding-3-large OpenAI Chat (deterministisch) Pinecone v6 bm25s (sparse) mmarco-mMiniLMv2-L12 Docling tiktoken pydantic / pydantic-settings tenacity pdfplumber · pdfminer.six python-docx · python-pptx · PyMuPDF Flask 3.1 · Gunicorn Supabase (Postgres) MinIO (S3-kompatibel) Docker · systemd Tailwind

Zwei Mandanten in Produktion. Norm-verankerte deutsche Antworten. Eine Pipeline, die wir tatsächlich debuggen können.

2

Produktive Mandanten auf einer Plattform

Ein mandantenfähiger SaaS, jeder Mandant auf eigener Subdomain mit isolierter Datenbank, Pinecone-Namespace, Upload-Pfaden und CORS. Fail-closed-Konfigurationsdurchsetzung bei Kollisionen.

0

DAG-Framework-Abhängigkeiten

Der Orchestrator ist eine Python-Funktion. Komponenten überspringen nicht stillschweigend. Stack-Traces zeigen auf Zeilen. Profiler funktionieren. Asserts funktionieren. Die Iterationskosten sind entsprechend gesunken.

DIN / EN / DWA

Normen pro Chunk erhalten

Normreferenzen werden zur Chunk-Zeit extrahiert und in Metadaten gespeichert. Die Anchor-Boost-Stufe nutzt sie. Der Synthesizer wird angewiesen, sie wortgleich zu erhalten. Die Coverage-Retry-Stufe führt die Synthese erneut aus, wenn welche fallen gelassen werden.

23+

Gold-Fälle pro Mandant

Ein handkuratiertes Goldset pro Mandant läuft in der Bench. Jeder Booster (Router, Synonyme, Page-Floor, Diversität, Coverage-Retry) ist Feature-geflaggt, sodass Gewinne pro Feature zugeordnet werden.

Hybrid

Dense + Sparse + Reranking

OpenAI text-embedding-3-large (1024 Dimensionen) + bm25s sparse + RRF + mmarco-mMiniLMv2-L12 mehrsprachiger Reranker. Abgestimmt auf deutsche Komposita und exakte DIN-Refs.

+ Chat & E-Mail

Lead-Chatbot & KI-E-Mail-Composer

Öffentlicher Chat mit ephemeren Session-Tokens, pro-Mandant CORS, striktem Verweigerungsvertrag. KI-E-Mail-Composer über IMAP/SMTP mit Thread-Kontext und Vorlagen, für menschliche Eskalation und Bulk-Outreach.

Die Standard-RAG-Frameworks sind großartig — bis man wirklich wissen muss, warum ein Chunk fallengelassen wurde. Auf einem deutschen technischen Korpus, in dem eine fehlende DIN-Referenz die Antwort falsch macht, ist genau diese Sichtbarkeit der ganze Job. In dem Moment, in dem wir den DAG durch eine explizite Funktion ersetzt haben, wurde jedes Retrieval-Problem lösbar.

— Viktor Andriichuk, Gründer, DataFlux Software

Sechs Entscheidungen, die aus dem Löschen des Frameworks kamen — nicht aus dem Hinzufügen.

1. Das Framework war das Problem, nicht ein fehlendes Feature. DAG-basierte Pipelines verstecken Zwischenzustände. Auf einem Korpus, in dem Chunk-Verlust der dominante Fehlertyp ist, ist das katastrophal. Eine einzige Python-Funktion mit expliziten Stufen hat uns nichts gekostet — und uns den Debugger zurückgegeben.

2. Dateinamen-Routing vor dem Retrieval schlägt smarteres Retrieval. Wenn der Nutzer das Dokument benennt, ruf dieses Dokument ab, nicht seinen nächsten Nachbarn im Embedding-Raum. Trivialer Code, große Wirkung.

3. Pro-Mandant Synonym-JSON ist auf technischem Deutsch unverhältnismäßig wirksam. „Betonrohr" / „Rohr" / „DN 800" / „Druckrohr" mappen alle aufeinander. Baukklassenbezeichnungen ebenso. Domänenwörterbücher schlagen Fine-Tuning in dieser Größenordnung — und bleiben für den Kunden lesbar.

4. Page-Floor + Diversitäts-Cap reparieren die Retrieval-Form, nicht die Retrieval-Scores. Eine fehlende Seite auf einer Zieldatei hat mehr Antworten ruiniert als jedes Embedding-Modell-Upgrade gerettet hat. ≥1 Chunk pro Seite zu erzwingen und den Pro-Seite-Beitrag zu deckeln hat die Lücke geschlossen.

5. Coverage-Retry ist der billigste Genauigkeitsgewinn, den wir haben. Anchors im Kontext mit Anchors in der Antwort vergleichen. Hat das LLM einen fallen gelassen, mit gepinntem Anchor erneut prompten. Zwei Extra-Requests auf den schlechten Fällen, null Kosten auf den guten.

6. Eine Gold-Bench mit pro-Feature-Flags ist, wie man weiß, was gewirkt hat. Ohne sie sieht jeder Booster essentiell aus. Mit ihr stirbt toter Code — und lebender Code verdient seinen Platz.

Discovery → Architektur → Framework löschen → Produktion.

Wir haben die Pipeline schon gebaut, die Sie nicht schreiben wollen.

Bringen Sie uns das Korpus, die Fragetypen, die beantwortet werden müssen, und die Normen oder Vorschriften, die erhalten bleiben müssen. Wir sagen Ihnen, was im Framework steckt, was nicht — und was Sie löschen sollten.