Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

deutsches-ki-toolkit

Deutsche Dokumente. Deutsche Daten. Antworten, auf die man sich verlassen kann.

deutsches-ki-toolkit ist eine Python-Bibliothek für alle, die KI-Anwendungen mit deutschsprachigen Dokumenten bauen. Sie kümmert sich um die vielen kleinen und großen Eigenheiten, die generische Frameworks übersehen: Umlaute und ß, Komposita, deutsche Firmen- und Personennamen, IBAN und Steuernummern, Verträge mit Paragraphen, Rechnungen, Personalunterlagen und technische Dokumentation. Und sie sorgt dafür, dass sensible Daten das eigene Netz nicht verlassen.

Das Toolkit tritt nicht an, um Docling, spaCy, Presidio, BGE-M3, pgvector oder Ollama zu ersetzen. Es legt sich davor und verbindet diese Werkzeuge zu einer Kette, die deutsche Sprache ernst nimmt – vom ersten Byte eines PDFs bis zur belegten Antwort.

Deutsches Dokument  →  Struktur  →  PII  →  Chunks  →  Embeddings  →  Suche  →  Antwort mit Quelle

Stand

Diese Übersicht sagt, was heute funktioniert. Alles darunter beschreibt das Ziel des Projekts, nicht den Lieferstand der aktuellen Version.

Läuft und ist getestet

  • Deutsche Textnormalisierung, Satzsegmentierung, Komposita-Zerlegung
  • Deutsche PII-Erkennung mit Prüfsummen, fünf Anonymisierungsarten
  • Strukturbasiertes Chunking mit Abschnitts- und Seitenangaben
  • Suche im Arbeitsspeicher: Vektor und lexikalisch, vereint per RRF
  • PostgreSQL mit pgvector, deutsche Volltextsuche ohne unaccent-Trick (gegen echtes PostgreSQL geprüft, auch in der CI)
  • Lexikalisches Reranking, RAG-Engine mit Prüfung der Quellenangaben
  • Prompt-Injection- und Geheimniserkennung
  • Bewertung mit Recall, MRR und nDCG samt Datensatz
  • Kommandozeile, MCP-Werkzeuge, Docker-Image
  • spaCy- und Presidio-Detektoren gegen ein echtes deutsches Modell geprüft, samt CI-Lauf. Dabei bestätigt sich der Ausgangspunkt des Projekts: Presidio findet eine deutsche Steuernummer und eine Handelsregisternummer nicht, die eigenen Muster schon.

Der mitgelieferte Bewertungssatz (12 Fragen über die Testdateien) ergibt mit dem Hashing-Modell: Recall@1 0,75, Recall@5 1,00, MRR 0,84, nDCG@5 0,88. Nachvollziehbar mit:

deutsches-ki evaluate datasets/benchmark/deutsch_rag.yaml --corpus datasets/fixtures

Vorhanden, aber noch nicht gegen echte Systeme geprüft

Die folgenden Bausteine sind geschrieben und lassen sich einschalten, wurden aber noch nicht mit den jeweiligen Bibliotheken ausgeführt:

  • GLiNER-Detektor (gliner)
  • Docling für PDF und DOCX (docling)
  • BGE-M3 und Cross-Encoder-Reranker (embeddings)
  • Ollama- und vLLM-Anbindung, bisher nur gegen Attrappen getestet
  • MCP-Server selbst (die Werkzeuge dahinter sind getestet)

Noch nicht gebaut

  • Bewertung der Antwortqualität (Treue, Relevanz) über ein Sprachmodell
  • Dokumentklassifikation, Terminologie- und Stilprüfung
  • Domänen-Pakete und Enterprise-Datenbankanbindung
  • Web-Demo und Benchmark-Website
  • Feintuning

Warum es dieses Projekt gibt

Wer schon einmal eine RAG-Anwendung über deutsche Unternehmensdokumente gebaut hat, kennt die Überraschungen. Sie tauchen nicht im Modell auf, sondern davor und danach.

Komposita verstecken Bedeutung. „USBCKabel“, „USB C Kabel“ und „USB-C-Kabel“ meinen dasselbe. Für einen Tokenizer sind es drei verschiedene Dinge. Deutsche, niederländische, schwedische und finnische Komposita verstecken ganze Wörter in einem Token. Wer nicht zerlegt, bevor er indiziert, verliert Treffer – und die semantische Suche rettet das nur selten. (Bitext)

Presidio kennt deutsches Recht nicht. Die Standardinstallation liefert rund 30 Erkennungsmuster, die vor allem auf US-Formate ausgelegt sind. Deutsche Steuer-IDs und Steuernummern sind nicht dabei, Personalausweisnummern auch nicht. Die Steuernummer allein hat 16 Formate, eines pro Bundesland. Ein Praxistest ergab für IBAN, BIC, Steuer-ID und Handelsregisternummer eine Trefferquote von 0 Prozent mit den Voreinstellungen. Selbst die Sprachumschaltung hilft nicht überall: In Presidio-Issue #1343 liefert get_supported_entities(language="de") nur noch BANK_ACCOUNT, und Kontextwörter wirken für Deutsch gar nicht.

spaCy erkennt Organisationen im Deutschen nicht. Das deutsche Modell ist darauf trainiert, Firmennamen zu ignorieren, weil zu viele Fehlalarme entstehen. In einem öffentlichen Vergleich erreichte de_core_news_lg knapp 78 Prozent, GLiNER rund 95 Prozent. Wer nur auf ein Modell setzt, verschenkt viel.

PostgreSQL sucht Umlaute nicht so, wie man denkt. „Schnösel“ und „Schnoesel“ finden sich mit der Standardkonfiguration nicht gegenseitig. Auch default_text_search_config = 'german' ändert daran nichts. Es braucht eine eigene Textsuchkonfiguration und angepasste unaccent-Regeln, damit ä → ae abgebildet wird. (dbi services)

Generisches Chunking zerschneidet Paragraphen. Deutsche Verträge und Gesetzestexte leben von § 4 Abs. 2. Ein Chunker, der stur nach Token zählt, reißt Absatz eins von Absatz zwei weg – und genau dort steht die Ausnahme. Eine abschnittsbezogene Zerlegung schneidet in Benchmarks besser ab als die übliche Feste-Größe-Strategie.

Unfertige Bausteine im Ökosystem. Docling zerlegt deutsche PDFs gelegentlich mit falschen Leerzeichen und getrennten Wörtern (Issue #1042). Adressparser scheitern an „Straße des 17. Juni“ oder an Postleitzahlen, die in der Hausnummer landen (libpostal #510). Der deutsche spaCy-Tokenizer stolpert über Ergänzungsstriche wie „Haupt- und Nebensatz“ und Kontraktionen wie „unterm“ (spaCy #2486).

Und der rechtliche Rahmen. Seit dem 2. August 2026 greift der EU AI Act in seiner breiten Anwendung; Verstöße können bis zu 35 Millionen Euro kosten. Für den Mittelstand ist Datensouveränität längst kein Marketingwort mehr, sondern eine Bedingung. Viele Unternehmen wollen Dokumente gar nicht erst an einen Cloud-Anbieter geben.

Diese Liste ist der Grund, warum es dieses Toolkit gibt. Jeder Baustein oben ist für sich lösbar – aber niemand will ihn zehnmal lösen.


Was drin ist

  • Normalisierung für Umlaute, ß, Anführungszeichen, Gedankenstriche und Ergänzungsstriche – getrennt für Anzeige und Suche, ohne den Originaltext zu zerstören
  • Satz- und Wortsegmentierung, die Abkürzungen wie z. B., Dr., Dipl.-Ing., GmbH, e.V., Nr., Art., Abs. und § kennt
  • Entitätenerkennung über mehrere Detektoren gleichzeitig (spaCy, Presidio, Regex, GLiNER, Transformer)
  • Deutsche PII mit Prüfsummen: IBAN, BIC, USt-IdNr., Steuer-ID, Steuernummer, Sozialversicherungsnummer, Personalausweisnummer, Handelsregisternummer, Adressen, Telefonnummern, E-Mails sowie Geschäftskennungen wie Rechnungs-, Kunden-, Auftrags- und Vertragsnummern
  • Anonymisierung und Pseudonymisierung mit den Modi redact, replace, mask, hash und pseudonymize, deterministisch innerhalb einer Sitzung
  • Dokumenteinlesung über Docling für PDF, DOCX, XLSX, PPTX, HTML, Markdown, Bilder und Klartext – mit erhaltener Struktur statt einem langen String
  • Strukturbasiertes Chunking, das an Überschriften, Paragraphen, Absätzen und Sätzen schneidet und die Token-Grenze zuletzt beachtet
  • Komposita-Analyse und Zerlegung für die Suche
  • Suche mit angepasster Anfrage: Wörterbuch, Lemmatisierung, Komposita, Fachglossar, Embedding-Ähnlichkeit
  • Embeddings mit BGE-M3 als Ausgangspunkt und austauschbaren Anbietern
  • PostgreSQL + pgvector mit deutscher Volltextsuche in derselben Datenbank
  • Hybride Suche aus Vektor- und lexikalischer Suche, zusammengeführt per Reciprocal Rank Fusion
  • Reranking als eigener, austauschbarer Schritt
  • RAG mit Pflicht zur Quelle: jede Antwort kommt mit Dokument, Seite und Abschnitt
  • Lokaler Betrieb mit Ollama oder vLLM, ohne dass ein Byte das Haus verlässt
  • Sicherheitsschicht gegen Prompt-Injection und für klare Vertrauensgrenzen
  • Auswertung mit deutschem Benchmark, Retrieval- und Generierungsmetriken
  • MCP-Server und Kommandozeile für den Alltag

Was das Toolkit bewusst nicht ist

Es ist keine Chat-Oberfläche, kein Agenten-Framework, keine Vektordatenbank, kein PDF-Parser, kein LLM und keine Modelle zum Trainieren. All das gibt es bereits, und zwar gut. Dieses Projekt konzentriert sich auf die Ebene darüber: deutsche Sprache, deutsche Personenbezüge, deutsche Dokumentstruktur, nachvollziehbare Antworten und überprüfbare Ergebnisse.


Installation

pip install deutsches-ki-toolkit

Oder mit uv:

uv add deutsches-ki-toolkit

Voraussetzung ist Python 3.12 oder neuer.

Die Standardinstallation ist bewusst schlank. Schwere Bausteine lädst du bei Bedarf:

pip install "deutsches-ki-toolkit[docling]"    # Dokumente einlesen
pip install "deutsches-ki-toolkit[embeddings]" # BGE-M3 und Co.
pip install "deutsches-ki-toolkit[postgres]"   # pgvector + Volltextsuche
pip install "deutsches-ki-toolkit[mcp]"        # MCP-Server
pip install "deutsches-ki-toolkit[all]"

Das deutsche spaCy-Modell wird einmalig nachinstalliert:

python -m spacy download de_core_news_lg

Schnellstart

from deutsches_ki import GermanDocument

doc = GermanDocument.from_file("vertrag.pdf")

# Struktur steht, jetzt sensible Stellen finden
pii = doc.detect_pii()
print(pii)

# Vertraulich arbeiten und trotzdem suchen
doc.anonymize(mode="pseudonymize")

# Deutsches Chunking an §-Grenzen
chunks = doc.chunk(strategy="structural")

# Fragen stellen, Antwort mit Quelle erhalten
antwort = doc.search("Welche Kündigungsfrist gilt?")
print(antwort.answer)
for quelle in antwort.citations:
    print(quelle.document, quelle.page, quelle.section)

Mehr geht immer, aber das ist die kürzeste Strecke von einem PDF zu einer belegten Antwort.


Deutsche Texte normalisieren

from deutsches_ki.text import normalize_german

normalize_german("Die Straße ist schön.", mode="display")
# "Die Straße ist schön."

normalize_german("Die Straße ist schön.", mode="search")
# "die strasse ist schoen"  (nur als Suchform, das Original bleibt erhalten)

Der wichtigste Grundsatz: Originaltext nie zerstören. ß wird nicht überall zu ss, und Umlaute werden nicht überall zu ae, oe, ue. Es gibt zwei getrennte Wege:

  • display – so, wie der Text aussehen soll. Straße bleibt Straße.
  • search – eine zusätzliche Repräsentation, die strasse als Treffer erlaubt.

Deutsche Texte bringen weitere Eigenheiten mit, die hier aufgeräumt werden:

from deutsches_ki.text import (
    normalize_unicode,  # ─ vs. –, „ vs. ", nicht umbrechende Leerzeichen
    normalize_quotes,  # „richtig“ und »so«
    normalize_dashes,  # Gedankenstrich, Bis-Strich, Bindestrich
    normalize_ergaenzung,  # "Haupt- und Nebensatz" → "Hauptsatz und Nebensatz"
    normalize_whitespace,
    normalize_for_search,
)

Der Ergänzungsstrich ist ein gutes Beispiel: „Haupt- und Nebensatz“ und „Hauptsatz und Nebensatz“ bedeuten exakt dasselbe, sehen für ein Modell aber völlig verschieden aus. normalize_ergaenzung macht daraus dieselbe Form.


Sätze und Wörter trennen

from deutsches_ki.text import split_sentences

saetze = split_sentences(
    "Die Frist beträgt 30 Tage (vgl. § 4 Abs. 2). "
    "Zahlungen sind an die Beispiel GmbH, z. B. per Überweisung, zu leisten."
)
for s in saetze:
    print(s.start, s.end, s.text)

Die Segmentierung ist auf die Fälle ausgelegt, an denen Standardlösungen scheitern:

z. B.   Dr.   Prof.   Dipl.-Ing.   GmbH   AG   e.V.   Nr.   Art.   Abs.   §   bzw.   usw.

Zahlen mit Punkt am Satzende („… 3.500 Euro.“) werden nicht falsch getrennt, und Aufzählungen wie „1.“ oder „a)“ bleiben zusammen.


Entitäten und deutsche PII

from deutsches_ki.pii import detect

treffer = detect(
    "Überweisung an Max Mustermann, IBAN DE89 3704 0044 0532 0130 00, USt-IdNr. DE123456789."
)
[
  { "type": "PERSON", "text": "Max Mustermann", "confidence": 0.94, "source": "spacy" },
  { "type": "DE_IBAN", "text": "DE89 3704 0044 0532 0130 00", "confidence": 0.99, "source": "regex" },
  { "type": "DE_VAT_ID", "text": "DE123456789", "confidence": 0.99, "source": "regex" }
]

Erkannt werden unter anderem:

Finanzen – IBAN (mit Mod-97-Prüfung), BIC, Kartennummern.

Steuern – USt-IdNr./VAT-ID, Steuer-ID (elfstellig) und Steuernummer. Die Steuernummer wird für alle 16 Bundesländer in ihren jeweiligen Formaten erkannt, jeweils mit Plausibilitätsprüfung. Genau hier scheitern generische Werkzeuge.

Kontakt – Festnetz- und Mobilnummern in deutschen Schreibweisen, E-Mail-Adressen.

Adressen – Postleitzahl, Straße, Hausnummer, Hausnummernzusätze wie 12a, Bereiche wie 414-424, Ort. Die Erkennung ist auf die deutsche Reihenfolge ausgelegt (Straße vor Hausnummer) und nicht auf das US-amerikanische Muster.

Persönliche Kennungen – Sozialversicherungsnummer, Personalausweisnummer.

Geschäftliches – Handelsregisternummer (HRA/HRB), Rechnungsnummer, Kundennummer, Auftragsnummer, Vertragsnummer.

Personen und Organisationen – über mehrere Detektoren hinweg, damit Firmennamen nicht durchs Raster fallen.

Mehrere Detektoren statt eines Modells

Kein einzelner Ansatz findet alles. Deshalb laufen die Detektoren zusammen und einander ergänzend:

EntityDetector
    ├── RegexDetector        # strukturierte Kennungen + Prüfsumme
    ├── PresidioDetector     # etablierter Rahmen, erweitert um deutsche Muster
    ├── SpaCyDetector        # Morphologie, Syntax, Personen und Orte
    ├── GliNERDetector       # Organisationen und unbekannte Namen
    └── TransformerDetector  # kontextabhängige Fälle

Jeder Treffer trägt seine Herkunft (source) und einen Konfidenzwert. Wer weniger Fehlalarme braucht, kann nach Detektor filtern; wer mehr Recall braucht, schaltet GLiNER dazu.

Kontextwörter auf Deutsch

Erkennung wird deutlich besser, wenn ein Hinweiswort in der Nähe steht. Daher bringt das Toolkit eigene deutsche Kontextlisten mit – „Rechnungsnummer“, „Kundennummer“, „Steuernummer“, „IBAN“, „Ansprechpartner“, „geboren am“ und weitere. Englische Kontextwörter helfen hier nicht, wie auch das Presidio-Projekt bestätigt.


Anonymisieren und pseudonymisieren

from deutsches_ki.pii import anonymize

anonymize("Max Mustermann arbeitet bei der Beispiel GmbH.", mode="redact")
# "[PERSON] arbeitet bei der [ORGANISATION]."

anonymize("Max Mustermann arbeitet bei der Beispiel GmbH.", mode="pseudonymize")
# "PERSON_001 arbeitet bei der ORG_001."

Fünf Betriebsarten:

Modus Ergebnis
redact Platzhalter wie [PERSON], [IBAN]
replace Ersetzung durch eine feste oder generierte Kennung
mask Teilweise Verdeckung: DE89 **** **** **** **** 00
hash Einweg-Hash, nicht umkehrbar
pseudonymize Stabile Kennung wie PERSON_001, innerhalb der Sitzung konstant

Pseudonyme bleiben stabil: Derselbe Name bekommt in derselben Sitzung immer dieselbe Kennung. Damit lassen sich Dokumente verarbeiten und vergleichen, ohne die echten Namen weiterzugeben. Wenn du die Zuordnung zurückbrauchst, kann der Vorgang mit einem Schlüssel umkehrbar gestaltet werden – standardmäßig ist er es nicht.

Der wichtigste Punkt: Anonymisierung ist kein Anhang, sondern sitzt in der Verarbeitungs- kette. Texte werden anonymisiert, bevor sie ein Embedding-Modell oder ein Sprachmodell sehen – nicht danach.


Dokumente einlesen und Struktur erhalten

from deutsches_ki.documents import parse

doc = parse("vertrag.pdf")

print(doc.title)
for section in doc.sections:
    print(section.level, section.title, section.page)

Das Toolkit wirft ein PDF nicht in einen großen String. Die Struktur bleibt über die gesamte Kette erhalten:

Dokument
├── Titel
├── Metadaten
├── Abschnitte
│   ├── Überschrift
│   ├── Absätze
│   ├── Tabellen
│   ├── Listen
│   └── Querverweise
└── Seiten

Für deutsche Verträge heißt das konkret, dass § 1 Vertragsgegenstand, § 2 Vergütung, § 3 Zahlungsbedingungen, § 4 Haftung eigene, adressierbare Einheiten bleiben – mit Seitenzahl und Positionsangabe. Tabellen werden als Tabellen weitergegeben und nicht zu Fließtext zerrieben.

Unter der Haube liest Docling die Dateien. Die Arbeit des Toolkits beginnt danach: Spracherkennung (Deutsch oder gemischt), deutsche Normalisierung, PII-Erkennung und das Herausziehen deutscher Metadaten wie Rechnungsdatum, Rechnungsnummer oder Vertragsnummer.


Deutsches Chunking

Chunking ist der Punkt, an dem die meisten deutschen RAG-Anwendungen Qualität verlieren. Hier wird nicht bei einer festen Tokenzahl blind geschnitten, sondern nach einer Rangfolge:

Dokumentstruktur
      ↓
Abschnittsgrenzen
      ↓
Absatzgrenzen
      ↓
Satzgrenzen
      ↓
Token-Grenze (nur als letztes Mittel)

Schlecht:

Chunk 1: § 4 Zahlungsbedingungen ...
Chunk 2: ... 30 Tage nach Rechnungsstellung ...

Besser:

Chunk 1: § 4 Zahlungsbedingungen

         (1) Der Auftraggeber ...

         (2) Die Zahlung ist innerhalb von 30 Tagen
             nach Rechnungsstellung fällig.

Ein Absatz, der eine Ausnahme enthält, bleibt zusammen. Überlappung ist möglich, aber nicht als Krücke, sondern um Abschnittskontext an den Rändern zu erhalten.

Jeder Chunk trägt seinen Weg im Dokument mit:

{
  "document_id": "vertrag-2024-001",
  "section": "Zahlungsbedingungen",
  "section_path": ["Vertrag", "Zahlungsbedingungen"],
  "paragraph": "§ 3",
  "page": 12,
  "language": "de",
  "document_type": "contract"
}

Diese Angaben sind später Gold wert. Man kann Treffer auf einen Paragraphen einschränken, nach Dokumenttyp filtern und die Quelle exakt benennen.


Komposita

from deutsches_ki.text import analyze_compound

analyze_compound("Versicherungsbeitrag")
{
  "word": "Versicherungsbeitrag",
  "parts": ["Versicherung", "Beitrag"],
  "strategy": "dictionary"
}

Für die Suche wird ein Kompositum zusätzlich in eine Suchform überführt, in der die Bestandteile sichtbar sind. Wer nach „Arbeitsunfähigkeitsbescheinigung“ sucht, soll auch Textstellen finden, in denen von der „Bescheinigung der Arbeitsunfähigkeit“ die Rede ist. Die Zerlegung stützt sich auf ein deutsches Wörterbuch, morphologische Analyse und – wenn nötig – ein Sprachmodell als Rückfall. Sie ist bewusst heuristisch gehalten und liefert mehrere Kandidaten statt einer einzigen, möglicherweise falschen Antwort.


Suchanfragen erweitern

from deutsches_ki.text import expand_query

expand_query("Urlaubsantrag genehmigen")
["Urlaubsantrag", "Urlaubsfreigabe", "Urlaubsgenehmigung", "Abwesenheitsantrag", "Urlaubstage"]

Erweiterung entsteht aus mehreren Quellen und nicht aus einem Sprachmodell allein:

Wörterbuch
    + Lemmatisierung
    + Komposita-Analyse
    + Fachglossar des Kunden
    + Embedding-Ähnlichkeit
    + optional: Sprachmodell

Ein selbst gepflegtes Glossar ist oft die beste Quelle, weil es die Sprache des Unternehmens kennt. Deshalb kannst du eigene Begriffe mitgeben, ohne das Toolkit zu verändern.


Embeddings

from deutsches_ki.embeddings import get_embedder

embedder = get_embedder("bge-m3")
vektoren = embedder.embed_documents(["Die Zahlungsfrist beträgt 30 Tage."])

Ausgangspunkt ist BGE-M3: mehrsprachig, 1024 Dimensionen, lange Eingaben bis 8192 Token und – wichtig – gleichzeitig für dichte, sparse und Multi-Vektor-Suche geeignet. Damit ist das Modell ein solider Startpunkt, ohne dass du dich daran bindest.

Alle Anbieter folgen derselben Schnittstelle:

class EmbeddingProvider:
    def embed_documents(self, texts): ...
    def embed_query(self, text): ...

Ausprobieren kannst du BGE-M3, multilingual-e5, deutsche Modelle, Qwen-Embeddings, Ollama-Embeddings und OpenAI. Welches Modell wann gewinnt, steht in den Benchmarks – nicht im Bauchgefühl.


Speicher: PostgreSQL und pgvector

from deutsches_ki.storage.pgvector import PgVectorStore

store = PgVectorStore("postgresql://localhost/deutsche_ki")
store.create_schema()
store.add_chunks(chunks, embedder)
CREATE TABLE chunks (
    id           UUID PRIMARY KEY,
    document_id  UUID NOT NULL,
    content      TEXT NOT NULL,
    metadata     JSONB,
    embedding    vector(1024)
);

CREATE INDEX chunks_embedding_idx
    ON chunks USING hnsw (embedding vector_cosine_ops);

Die Tabellen decken Dokumente, Abschnitte, Chunks, Entitäten und Embeddings ab. pgvector unterstützt exakte Suche sowie HNSW- und IVFFlat-Indexe. Für den Vergleich mit dem Optimum lässt sich die exakte Suche jederzeit dazuschalten.

Ein Vorteil der Kombination: Suche und Volltext liegen in derselben Datenbank. Kein zusätzlicher Suchdienst, keine zweite Datenhaltung, keine Synchronisierung.


Deutsche Volltextsuche in PostgreSQL

Hier lauert eine Falle. Standardmäßig findet PostgreSQL „Schnösel“ und „Schnoesel“ nicht gegenseitig – auch nicht mit default_text_search_config = 'german'. Das Toolkit legt deshalb eine passende Textsuchkonfiguration und eigene unaccent-Regeln an:

CREATE TEXT SEARCH CONFIGURATION deutsche_ki (COPY = german);
ALTER TEXT SEARCH CONFIGURATION deutsche_ki
    ALTER MAPPING FOR hword, hword_part, word
    WITH unaccent, german_stem, german_compound;

-- unaccent-Regeln:  ä → ae, ö → oe, ü → ue

Danach findet eine Suche nach „Schnösel“ beide Schreibweisen, und die Komposita-Zerlegung greift zusätzlich. Das ist die unspektakulärste, aber wirksamste Verbesserung für deutsche Unternehmenssuche.


Hybride Suche

Nur Vektoren reichen nicht. Wer nach § 37 Abs. 2 VOB/B sucht, will den Paragraphen und nicht semantisch Ähnliches. Deshalb laufen beide Verfahren zusammen:

                    Anfrage
                       │
              ┌────────┴────────┐
              ↓                 ↓
       Vektorsuche        PostgreSQL-FTS
              │                 │
              └────────┬────────┘
                       ↓
                  Zusammenführung (RRF)
                       ↓
                   Reranker
from deutsches_ki.retrieval import HybridRetriever

retriever = HybridRetriever(store, embedder, vector=True, lexical=True)
treffer = retriever.search("Welche Zahlungsfrist gilt?", top_k=20)

Zusammengeführt wird zunächst per Reciprocal Rank Fusion, weil das ohne Abstimmung funktioniert und gute Ergebnisse liefert. Wer feiner steuern will, kann später gewichtete oder gelernte Verfahren verwenden.


Reranking

Die Vektorsuche holt großzügig Kandidaten, der Reranker sortiert sie neu:

from deutsches_ki.reranking import get_reranker

reranker = get_reranker("bge-reranker-v2-m3")
top5 = reranker.rerank("Welche Zahlungsfrist gilt?", treffer)
Top 30 aus der Suche  →  Reranker  →  Top 5 für das Sprachmodell

Cross-Encoder-Reranker, BGE-Reranker, Jina und Cohere lassen sich über dieselbe Schnittstelle anbinden. Tausende Dokumente zu reranken ist weder sinnvoll noch schnell – deshalb bleibt die Kandidatenmenge begrenzt.


RAG mit Quellenangabe

from deutsches_ki.rag import DeutschRAG

rag = DeutschRAG(retriever=retriever, reranker=reranker, llm=llm)

antwort = rag.ask("Welche Zahlungsfrist gilt laut Vertrag?")
RAGAnswer(
    answer="Die Zahlungsfrist beträgt 30 Tage.",
    citations=[Citation(document="Vertrag.pdf", page=12, section="§ 4 Zahlungsbedingungen")],
    retrieved_chunks=[...],
    confidence=0.87,
    metadata={...},
)

Eine Antwort ohne Quelle ist nur eine Behauptung. Deshalb ist die Zitatangabe fester Bestandteil des Ergebnisses und nicht optional. Das Toolkit reicht Dokument, Seite, Abschnitt und Chunk durch die ganze Kette und prüft am Ende, ob die zitierte Stelle tatsächlich existiert und den Satz stützt.

Ausgegeben sieht das so aus:

Die Zahlungsfrist beträgt 30 Tage.

Quelle:
  Vertrag.pdf
  Seite 12
  § 4 Zahlungsbedingungen

Sprachmodelle anbinden

class ChatProvider:
    def generate(self, messages, **kwargs): ...

Angebunden sind:

  • Ollama – lokal, unkompliziert, gut für Rechner ohne GPU
  • vLLM – lokal und schnell, mit OpenAI-kompatibler HTTP-Schnittstelle
  • OpenAI-kompatible Endpunkte – für gehostete Dienste
  • Anthropic – für den Fall, dass es passt

Der Kern des Toolkits kennt keinen bevorzugten Anbieter. Ein Wechsel ist eine Konfigurationsfrage, keine Codeänderung.


Vollständig lokal arbeiten

ollama pull llama3.1
ollama pull bge-m3
from deutsches_ki.providers import OllamaProvider

llm = OllamaProvider(model="llama3.1")

Damit steht die komplette Kette:

Deutsches PDF  →  Docling  →  PII  →  Chunks  →  BGE-M3  →  pgvector  →  Ollama  →  Antwort mit Quelle

Nichts davon verlässt den Rechner. Ein einzelnes Docker-Compose bringt PostgreSQL mit pgvector, Ollama und das Toolkit zusammen hoch. Für größere Installationen übernimmt vLLM die Inferenz und stellt dieselbe Schnittstelle bereit.

Das ist der Punkt, an dem DSGVO und EU AI Act aufhören, ein Hindernis zu sein. Standard- mäßig gibt es keine Telemetrie, keinen Dokumentenupload und keinen externen Aufruf.


Sicherheit: Dokumente sind nicht vertrauenswürdig

Ein PDF ist fremder Inhalt. Wenn darin „Ignoriere alle vorherigen Anweisungen“ steht, ist das Dokumentinhalt – und keine Anweisung an das Sprachmodell. Das Toolkit hält diese Grenze ein:

NICHT VERTRAUENSWÜRDIG
  PDF · DOCX · HTML · E-Mail · Datenbanktext
        ↓  Bereinigung
        ↓  PII- und Sicherheitsprüfung
        ↓  Prompt-Injection-Erkennung
  INTERNE, GEPRÜFTE DARSTELLUNG
        ↓  Suche
        ↓  Sprachmodell

Die Sicherheitsschicht erkennt:

  • Prompt-Injection in Dokumenten und in Suchergebnissen
  • PII vor der Verarbeitung
  • Geheimnisse wie API-Schlüssel oder Zugangsdaten in Dokumenten
  • Zitatprüfung – eine Antwort darf nur nennen, was wirklich in einer Quelle steht

Retrieval-Ergebnisse werden immer als Daten behandelt, niemals als Anweisung.


Bewerten statt behaupten

„Die Antwort sieht gut aus“ ist keine Messung. Deshalb bringt das Toolkit einen eigenen deutschen Benchmark mit, der ohne fremde urheberrechtlich geschützte Dokumente auskommt: synthetische Dokumente, gemeinfreies Material, offene Behördentexte und frei lizenzierte Dokumentation.

Beispiel einer Testfrage:

{
  "question": "Wie lange ist die Kündigungsfrist?",
  "document": "vertrag_001.pdf",
  "expected_sources": ["vertrag_001.pdf#page=8"],
  "reference_answer": "Die Kündigungsfrist beträgt drei Monate.",
  "category": "legal",
  "difficulty": "medium"
}

Gemessen wird in vier Bereichen:

Suche – Recall@1, Recall@5, Recall@10, MRR, nDCG.

Antwort – Treue zur Quelle, Relevanz der Antwort, Korrektheit der Zitate, Kontextausnutzung, Anteil erfundener Aussagen.

Deutsche Qualität – Grammatik, Fachterminologie, Anredeform, Klarheit, Umgang mit Komposita.

System – Laufzeit, Token, Kosten (bei externen Modellen), Speicher, CPU, GPU.

Kategorien des Benchmarks: Allgemeines, Wirtschaft, Technik, Recht, Verwaltung, Finanzen, Personal, Industrie. Gemischt deutsch/englisch ist ein eigener Prüfstein, weil viele Unternehmen so schreiben.

Ergebnisse landen als JSON unter benchmarks/results/ und sind reproduzierbar. Wenn ein deutsches Embedding-Modell BGE-M3 nicht schlägt, schreibt das Toolkit das hin. Wenn einfaches Chunking den komplizierten Chunker schlägt, ebenfalls. Das macht das Projekt erst glaubwürdig.


Läuft das wirklich? Tests und Qualitätssicherung

Jede Funktion hat Tests, und jede Änderung läuft durch dieselben Prüfungen, bevor sie hereinkommt. Die Teststufen sind:

Unit-Tests – Normalisierung, jede PII-Erkennung einzeln (IBAN, Steuer-ID, Steuernummer je Bundesland, Telefon, Adresse), Chunking, Komposita-Zerlegung, Metadaten.

Integrationstests – die ganze Kette: PDF → Docling → Chunks, Chunks → Embeddings, Embeddings → pgvector, Anfrage → Suche, Suche → Antwort. Diese Tests laufen gegen ein echtes PostgreSQL mit pgvector.

Regressionstests – jeder gefundene Fehler wird zu einem Testfall. Der deutsche spaCy-Tokenizer zerlegt „Haupt- und Nebensatz“ falsch? Dann gibt es dafür ab sofort einen Test. Ein Kompositum wurde nicht gefunden? Ein permanenter Test. Der Ordner tests/regression/german/ wächst mit jedem behobenen Problem.

Golden-Datensätze – feste Beispieldateien mit festen Erwartungen:

german_compounds.json     german_pii.json        german_addresses.json
german_legal.json         german_contracts.json  german_tables.json
german_rag.json

Wo ein Ergebnis feststehen kann, wird es festgeprüft und nicht von einem Modell bewertet. Ein Sprachmodell als Richter ist bequem, aber schwankend. Deterministische Tests kommen zuerst.

Vor jeder Veröffentlichung laufen:

✓ Unit-Tests
✓ Integrationstests
✓ Typprüfung (mypy)
✓ Linting (ruff)
✓ Benchmark-Smoke-Test
✓ Paketbau
✓ Dokumentation
✓ Sicherheitsprüfung

Dieselben Schritte laufen in der CI bei jedem Push. Wer eine Änderung einreicht, sieht sofort, ob sie etwas kaputt macht.

Leistungsmessungen erfassen Dokumente, Seiten, Chunks und Embeddings pro Sekunde, die P95-Latenz sowie CPU- und Speicherverbrauch – als maschinenlesbares JSON, damit Entwicklung sichtbar bleibt.


Architektur

                            ┌──────────────────────┐
                            │   Deine Anwendung    │
                            └──────────┬───────────┘
                                       ↓
                            ┌──────────────────────┐
                            │ deutsches-ki-toolkit │
                            └──────────┬───────────┘
                                       │
         ┌─────────────────────────────┼─────────────────────────────┐
         ↓                             ↓                             ↓
  Dokumente                    Deutsche Sprache              Datenschutz
         │                             │                             │
      Docling                       spaCy                        Presidio
         │                             │                        + deutsche Muster
         └─────────────────────────────┼─────────────────────────────┘
                                       ↓
                              Deutsche Aufbereitung
                                       │
                     ┌─────────────────┼─────────────────┐
                     ↓                 ↓                 ↓
                 Chunking          Entitäten         Metadaten
                     └─────────────────┼─────────────────┘
                                       ↓
                                  Embeddings
                                       │
                          ┌────────────┴────────────┐
                          ↓                         ↓
                       BGE-M3                andere Modelle
                          │
                          ↓
                       pgvector
                          │
                   ┌──────┴──────┐
                   ↓             ↓
             Vektorsuche   PostgreSQL-FTS
                   └──────┬──────┘
                          ↓
                    Hybride Suche
                          ↓
                      Reranker
                          ↓
                       Kontext
                          │
              ┌───────────┴───────────┐
              ↓                       ↓
           Ollama                    vLLM
              └───────────┬───────────┘
                          ↓
                     Antwort
                          │
                          ↓
                  Zitate + Bewertung

Projektaufbau

deutsches-ki-toolkit/
├── src/deutsches_ki/
│   ├── core/          # Datenmodelle: Document, Section, Chunk, Entity
│   ├── text/          # Normalisierung, Segmentierung, Komposita, Query-Expansion
│   ├── documents/     # Docling-Anbindung, Struktur, Metadaten
│   ├── pii/           # Detektoren, deutsche Recognizer, Anonymisierung
│   ├── chunking/      # strukturbasiertes Chunking
│   ├── embeddings/    # BGE-M3 und weitere Anbieter
│   ├── retrieval/     # Vektor-, Volltext- und hybride Suche
│   ├── reranking/     # Cross-Encoder und weitere Reranker
│   ├── rag/           # RAG-Engine mit Zitaten
│   ├── evaluation/    # Metriken und Benchmark-Werkzeuge
│   ├── providers/     # Ollama, vLLM, OpenAI-kompatibel, Anthropic
│   ├── security/      # Prompt-Injection, Vertrauensgrenzen
│   ├── storage/       # PostgreSQL + pgvector
│   ├── mcp/           # MCP-Server
│   └── cli/           # Kommandozeile
├── tests/             # unit · integration · regression · benchmark
├── benchmarks/        # pii · retrieval · chunking · rag
├── datasets/          # synthetic · fixtures
├── examples/          # basic · pii · document · rag · ollama · pgvector · mcp
├── docs/              # architecture · guides · concepts · benchmarks
├── scripts/           # benchmark.py · generate_dataset.py · evaluate.py
├── docker/            # Dockerfile · compose.yaml
└── pyproject.toml

Kommandozeile

deutsches-ki parse vertrag.md
deutsches-ki pii rechnung.txt --json
deutsches-ki scan vertrag.md
deutsches-ki anonymize rechnung.txt --mode pseudonymize -o sauber.txt
deutsches-ki chunk vertrag.md --strategy structural --max-tokens 512
deutsches-ki embed ./dokumente --model bge-m3
deutsches-ki search ./dokumente "Wie lange ist die Kündigungsfrist?"
deutsches-ki ask "Welche Zahlungsbedingungen gelten?" --corpus ./dokumente
deutsches-ki evaluate datasets/benchmark/deutsch_rag.yaml --corpus ./dokumente

Ein typischer Durchlauf über einen ganzen Ordner:

deutsches-ki ingest ./dokumente --dsn postgresql://localhost/deutsche_ki

Danach beantwortet ask Fragen mit Quellenangabe. Ist in deutsches-ki.yaml ein Sprachmodell eingetragen, wird die Antwort formuliert und die genannten Quellen werden geprüft; ohne Eintrag kommt der bestpassende Abschnitt zurück.

Danach:

deutsches-ki ask "Welche Zahlungsbedingungen gelten?"
Die Zahlungsfrist beträgt 30 Tage.

Quellen:
  Vertrag.pdf
  Seite 12
  § 4 Zahlungsbedingungen

Konfiguration

Alles lässt sich in einer Datei deutsches-ki.yaml festhalten:

language: de

documents:
  parser: docling

pii:
  enabled: true
  mode: anonymize
  detectors: [regex, presidio, spacy, gliner]

embeddings:
  provider: bge-m3

retrieval:
  vector: true
  lexical: true
  fusion: rrf
  top_k: 20

reranking:
  enabled: true
  provider: lexical
  top_k: 5

llm:
  provider: ollama
  model: llama3.1

storage:
  provider: pgvector
  dsn: postgresql://localhost/deutsche_ki

Ein Aufrufparameter schlägt die Datei, die Datei schlägt die Vorgabe. Fehlt die Datei, gelten die Vorgaben. Für Container empfiehlt es sich, die Datei im Repository zu lassen und Zugangsdaten beim Start zu übergeben.


Docker

docker compose -f docker/compose.yaml up -d postgres

Gestartet wird PostgreSQL mit pgvector. Ollama kommt über ein Profil dazu, damit niemand eine Grafikkarte braucht:

docker compose -f docker/compose.yaml --profile local-llm up -d

Das Toolkit selbst lässt sich ebenfalls als Image bauen:

docker build -f docker/Dockerfile -t deutsches-ki-toolkit .
docker run --rm -v "$PWD/datasets/fixtures:/daten:ro" deutsches-ki-toolkit pii /daten/rechnung.txt

Läuft Docker Hub im eigenen Netz nicht, lassen sich beide Images über eine andere Registry bauen; die Basis ist jeweils ein --build-arg:

docker build -f docker/postgres/Dockerfile -t deutsches-ki-postgres:17 docker/postgres
POSTGRES_IMAGE=deutsches-ki-postgres:17 docker compose -f docker/compose.yaml up -d postgres

Häufige Fragen

Warum nicht einfach LangChain oder LlamaIndex? Kannst du. Beide sind gute Rahmenwerke. Sie kennen deutsche Komposita aber genauso wenig wie deutsche Steuernummern. Dieses Toolkit lässt sich daneben betreiben und ergänzt genau diese Lücken. Haystack und LlamaIndex lassen sich bei Bedarf anbinden, sind aber keine Voraussetzung.

Muss ich zwingend ein Sprachmodell einsetzen? Nein. Suche, PII-Erkennung, Chunking und Anonymisierung funktionieren ohne. Ein Modell brauchst du erst für die Antwortformulierung und für die Bewertung von Antworten.

Verlassen meine Daten den Rechner? Nur, wenn du einen externen Anbieter ausdrücklich konfigurierst. Standardmäßig gibt es keine Telemetrie, kein Hochladen und keine externen Aufrufe.

Ist das ein deutsches Sprachmodell? Nein. Das Toolkit ist sprachbewusst, aber modellneutral. Es arbeitet mit dem Modell deiner Wahl – lokal oder gehostet.


Mitmachen

Gute Einstiegspunkte sind ausdrücklich erwünscht:

  • einen deutschen PII-Recognizer ergänzen
  • einen Benchmark-Fall beisteuern
  • das Chunking verbessern
  • eine Testdatei hinzufügen
  • Fachterminologie für eine Branche ergänzen
  • ein Embedding-Modell vergleichen
  • die Dokumentation schärfen
  • einen Anbieter anbinden

Beiträge werden mit good first issue, help wanted, German NLP, RAG, privacy, benchmark und documentation gekennzeichnet, damit man schnell findet, wo man anfangen kann. Neue Fälle gehören immer mit einem Test dazu. Wer ein Problem meldet, hilft am meisten mit einem Dokument, das es auslöst – anonymisiert oder synthetisch.


Lizenz

Apache-2.0. Die Lizenz ist bewusst permissiv gewählt, inklusive ausdrücklicher Patentregelung, damit auch Unternehmen das Toolkit ohne Bedenken einsetzen können.


  • Presidio, offene Issues zur deutschen Unterstützung – #1343
  • Docling, Formatierung deutscher PDFs – Issue #1042
  • libpostal, deutsche Adressen – Issue #510
  • spaCy, deutsches Lemmatisieren und Tokenisieren – Issue #2486
  • PostgreSQL und deutsche Umlaute in der Volltextsuche – dbi services
  • Komposita und Suchrelevanz – Bitext
  • Deutsche NER im Vergleich: Presidio, spaCy, GLiNER – Nils Durner
  • BGE-M3 – Modellkarte
  • German-NLP: Sammlung deutschsprachiger Ressourcen – adbar/German-NLP

deutsches-ki-toolkit – gebaut für echte deutsche Daten. Dokumente, Datenschutz, Suche, Antworten mit Quellen. Ehrlich gemessen.

Release files for deutsches-ki-toolkit 0.2.0a1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for deutsches-ki-toolkit 0.2.0a1
File Size Uploaded
deutsches_ki_toolkit-0.2.0a1.tar.gz 114.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for deutsches-ki-toolkit 0.2.0a1
File Interpreter ABI Platform
deutsches_ki_toolkit-0.2.0a1-py3-none-any.whl Python 3 none any Details

Total release size:223.9 kB

Release files / deutsches_ki_toolkit-0.2.0a1.tar.gz

Download URL deutsches_ki_toolkit-0.2.0a1.tar.gz
Size 114.0 kB
Tags Source
SHA-256 checksum
How to use checksums
823cdf7178d261d662b67050cf77f2db3fdb3203302c34558779250ef88d73b2
BLAKE2b-256 checksum
How to use checksums
7c6864944ed199318e9a9e0b046be99807820b33919f62aa754ec2c9e0ab6311
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.

Transparency log

Release files / deutsches_ki_toolkit-0.2.0a1-py3-none-any.whl

Download URL deutsches_ki_toolkit-0.2.0a1-py3-none-any.whl
Size 109.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cf6ed67702c256046eb79084076f4000767140da6ef372d3daf953ae9772546b
BLAKE2b-256 checksum
How to use checksums
0736bcce0368d03121b1c75958ded1e197170aeb655e2f1091586725fd40c037
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.

Transparency log
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page