Zurück zum Blog

Eine Wissensbasis aus 2000 Bruchstücken

Eine Wissensbasis aus 2000 Bruchstücken

Datum 14. August 2026
Kategorie KI & Automatisierung
Lesezeit 8 Minuten

Ein Bot, der auf alles mit demselben Link antwortet

Der Chatbot auf dieser Seite beantwortet Fragen zu den Blogbeiträgen. Er sucht die passenden Textstellen in einem Vektor-Index und lässt ein lokales Sprachmodell daraus eine Antwort formulieren. Seit Monaten lief er, ohne aufzufallen. Genauer gesagt: ohne dass jemand hinsah.

Ein Gesprächsverlauf machte deutlich, wie schlecht er tatsächlich war:

> hallo
Hallo! Wie kann ich Ihnen heute helfen? Für technische Details zu
Chatbot-Implementierungen, Docker-Problemen oder n8n-Workflows finden Sie
weitere Informationen auf https://matzka.cloud/blog

> was gibt es neues
Neue Services-Seiten erklären Cloud-Dienste verständlich:
https://matzka.cloud/blog  Ein neues Design-System wurde eingeführt:
https://matzka.cloud/blog

> zeig mir den neuesten blogpost
Der neueste Blogbeitrag ist vom 9. Januar 2026 und beschreibt die
Infrastruktur und Ziele von Matzka Cloud. Sie finden ihn hier:
https://matzka.cloud/blog

Jede Antwort endet auf derselben Übersichtsseite. Der genannte Beitrag ist der älteste, nicht der neueste. Und die Begrüßung dauerte 24 Sekunden.

Die naheliegende Erklärung wäre: das Modell ist zu klein, die Hardware zu schwach. Beides war falsch. Es waren vier voneinander unabhängige Fehlkonfigurationen, von denen keine eine Fehlermeldung erzeugt hat.

Die Zahl, die seit jeher null war

Der Chatbot schreibt zu jeder Anfrage eine Protokollzeile: Frage, Antwort, ob überhaupt Quellen gefunden wurden und wie viele davon in die Antwort eingingen. Genau dieses Zählfeld stand in jeder Zeile auf null. Nicht seit gestern, sondern seit die Tabelle existiert.

Das ist ein nützlicher Befund, weil er eine ganze Klasse von Verdächtigen ausschließt. Die Suche fand durchaus Treffer, das Protokoll wies sie aus. Aber keiner dieser Treffer brachte eine Quellenangabe mit. Wenn eine Zahl seit Beginn konstant null ist, liegt der Fehler selten im Betrieb und meistens im Aufbau.

Kennzahlen, die nie von null abweichen, sind Kennzahlen, die niemand liest. Ein Wert, der von Anfang an falsch ist, fällt nie als Abweichung auf. Er hat keine Vorgeschichte, gegen die er sich absetzen könnte.

31 Zeichen pro Wissensfetzen

Der Vektor-Index meldete rund 2000 Einträge und war damit prall gefüllt. Erst eine Stichprobe zeigte, woraus er bestand. Die mittlere Länge eines Abschnitts lag bei 31 Zeichen. Die Einträge sahen so aus:

"blog_post"
"Infrastructure"
"2026-08-13T00:00:00.513Z"
"Reinhard Matzka"

Das sind keine Textabschnitte, sondern die Feldwerte der Datensätze. Der Index enthielt statt der Blogtexte deren Metadaten, jeden Wert einzeln als eigenen Eintrag. Ein Suchtreffer auf „Infrastructure" beantwortet keine Frage.

Was fehlte, war ebenso aufschlussreich: Titel und Adresse der Beiträge kamen in den gespeicherten Daten überhaupt nicht vor. Damit war die Ursache der Null aus dem vorigen Abschnitt gefunden. Wo keine Adresse gespeichert ist, kann keine Quelle genannt werden.

Ein Vorgabewert, der still das Falsche tut

Die Aufbereitung läuft in n8n. Ein Schritt holt die Beiträge aus dem CMS, entfernt HTML und Emojis und baut daraus saubere Dokumente mit Fließtext und Metadaten. Dieser Schritt funktionierte einwandfrei. Der nächste Schritt, der die Dokumente in Abschnitte zerlegen und einbetten soll, war schlicht leer konfiguriert.

Der zuständige Baustein heißt in n8n „Default Data Loader". Ohne Konfiguration nimmt er die Voreinstellung, das komplette Eingabeobjekt zu verarbeiten. Er serialisiert also den ganzen Datensatz als JSON und zerlegt ihn entlang der Blattwerte. Der vorbereitete Fließtext wird dabei zu einem Feld unter vielen, gleichrangig mit dem Autorennamen und dem Zeitstempel.

Der teure Teil daran: Der Schritt davor hatte die Arbeit richtig gemacht. Titel, Adresse, Kategorie und bereinigter Text lagen fertig vor. Der Loader warf diese Struktur weg und begann von vorn. Ein defekter Schritt sieht in einer Ablaufkette genauso aus wie ein funktionierender: grün, mit Datenausgabe.

Die Korrektur ist klein. Der Loader muss wissen, welches Feld den Text enthält, welche Felder als Metadaten mitwandern sollen und wie zerlegt wird. Danach hatte jeder gespeicherte Abschnitt Titel und Adresse, und die mittlere Abschnittslänge stieg von 31 auf 996 Zeichen.

Stolperfalle beim Nachbauen: Eine ältere Fassung derselben Ablaufkette lag noch im Repository und setzte die Abschnittsgröße über Umgebungsvariablen. Diese Variablen sind im laufenden Container gar nicht gesetzt. Wer die alte Fassung als Vorlage nimmt, bekommt eine leere Größenangabe statt einer Fehlermeldung. Feste Zahlen sind hier die ehrlichere Wahl.

Die Schutzfunktion, die den Schaden anrichtete

Bleibt die Frage, warum ausgerechnet die Blog-Übersicht in jeder Antwort auftauchte. Die Erklärung ist eine Schutzfunktion, die genau umgekehrt wirkte als gedacht.

Sprachmodelle erfinden Links. Deshalb prüft ein nachgelagerter Schritt jede Adresse in der Antwort gegen eine Liste erlaubter Adressen und ersetzt alles Unbekannte durch einen sicheren Verweis auf die Blog-Übersicht. Vernünftig, solange die Liste gefüllt ist. Diese Liste wurde aber aus den Quellenangaben der Treffer gebaut, und die waren aus dem oben beschriebenen Grund immer leer.

Damit galt jede Adresse als erfunden. Auch die richtigen. Das Modell nannte durchaus den passenden Beitrag, und die Schutzfunktion ersetzte den korrekten Link kurz vor der Auslieferung durch die Übersichtsseite. Was nach einem ahnungslosen Bot aussah, war zu einem guten Teil ein Filter, der die richtige Antwort unkenntlich machte.

Zwei Änderungen daran. Unbekannte Adressen werden jetzt entfernt statt ersetzt, sodass ein leerer Filter zu einem Satz ohne Link führt und nicht zu einem falschen Link. Und die Liste der erlaubten Adressen enthält zusätzlich alle Adressen, die im Volltext der Treffer stehen, nicht nur die in den Metadaten.

Wenn eine Erlaubnisliste leer laufen kann, muss der Fehlerfall harmlos sein. Ersetzen ist der falsche Fehlerfall, weil das Ergebnis plausibel aussieht. Weglassen fällt auf.

Ein Kontextfenster, das sich nicht beschwert

Der dritte Fehler saß im Prompt. Das Modell arbeitet mit einem Kontextfenster von 4096 Token. Hineingereicht wurden 15 Textabschnitte, bis zu 16 Nachrichten Gesprächsverlauf und 800 reservierte Token für die Antwort. Das passt nicht.

Ollama meldet das nicht als Fehler. Es kürzt den Prompt und zwar von vorn. Vorn stand die Wissensdatenbank. Das Modell bekam also die Anweisungen und den Gesprächsverlauf, aber nicht die Quellen, aus denen es antworten sollte. Es tat daraufhin das Vernünftigste, was ihm möglich war, und verwies auf den Blog.

Das Kontextfenster ließ sich nicht einfach vergrößern. Ein 14B-Modell belegt in 4-Bit-Quantisierung bereits 11,4 von 12 GB Grafikspeicher. Mehr Kontext bedeutet mehr Zwischenspeicher für die Aufmerksamkeit, und der verdrängt Modellgewichte in den Arbeitsspeicher, was alles deutlich langsamer macht. Der Prompt musste also kleiner werden: drei Quellen zu je 700 Zeichen, Gesprächsverlauf auf drei Runden begrenzt, Antwortlänge auf 500 Token. Statt 15 angerissener Fundstellen jetzt drei vollständige.

Mehr Kontext ist nicht besser. 15 Treffer klingen gründlicher als drei. Wenn sie das Fenster sprengen, sind sie schlechter als keine, weil das Modell nicht erfährt, dass etwas fehlt. Ein Grenzwert ohne Fehlermeldung ist gefährlicher als ein harter Abbruch.

Warum es dazu noch langsam war

Die Langsamkeit hatte mit alldem nichts zu tun. Die Laufzeiten der einzelnen Schritte einer Anfrage sind eindeutig: Einbettung der Frage 0,9 Sekunden, Vektorsuche 0,1 Sekunden, sämtliche Datenbankzugriffe und Aufbereitungsschritte zusammen unter einer Zehntelsekunde. Der Aufruf des Sprachmodells: 13 Sekunden.

Das eingesetzte Modell ist ein Reasoning-Modell. Ohne gegenteilige Anweisung denkt es erst sichtbar nach und erzeugt hunderte Token, die niemand zu sehen bekommt, bevor die eigentliche Antwort beginnt. Die Anweisung dagegen ist ein einzelnes Schlüsselwort im System-Prompt. In der Kette gab es zwei Modellaufrufe, und nur der unwichtigere von beiden hatte es.

Mit dem Schlüsselwort im Antwort-Aufruf fiel die Zeit von 13 auf 3,3 Sekunden. Dazu kommt, dass der erste Modellaufruf, der eine Rückfrage in eine eigenständige Suchanfrage umschreibt, bei jeder Nachricht lief. Auch bei „hallo". Er läuft jetzt nur noch, wenn es einen Gesprächsverlauf gibt und die Frage sich erkennbar darauf bezieht.

Der Wächter zählte die falsche Größe

Für die Wissensbasis gibt es seit einem früheren Ausfall eine Überwachung. Sie prüft, ob der Index leer ist und ob die nächtliche Aktualisierung noch läuft. Beides war in Ordnung. Der Index enthielt 2000 Einträge und wurde jede Nacht neu gebaut.

Genau das ist das Problem. Die Überwachung entstand nach einem Ausfall, bei dem die Wissensbasis leer war, und sie misst seither zuverlässig die Menge. Gegen eine gefüllte, aber wertlose Wissensbasis ist sie blind. Der Defekt lief monatelang, und gefunden hat ihn am Ende ein Mensch, der einen Gesprächsverlauf gelesen hat.

Eine Überwachung, die nach einem Vorfall gebaut wurde, misst den Vorfall. Nicht die Funktion. Beim nächsten Mal sieht der Fehler anders aus, und die Prüfung bleibt grün. Nach der Menge lag die Wissensbasis bei 2000 Punkten und damit weit über jedem Schwellwert.

Die Prüfung bekommt deshalb drei zusätzliche Kriterien: den Anteil der Einträge mit hinterlegter Quellenadresse, die mittlere Abschnittslänge über eine Stichprobe und die Frage, ob das Übersichtsdokument existiert. Alle drei hätten diesen Fehler am ersten Tag gemeldet.

Ein Nachtrag zur Aktualität. Auf „was gibt es Neues" konnte der Bot prinzipiell nicht antworten, weil die Beiträge im CMS kein Veröffentlichungsdatum haben. Eine Ähnlichkeitssuche kann Aktualität nicht ermitteln, sie kennt nur Bedeutungsnähe. Die Aufbereitung legt jetzt zusätzlich ein kurzes Übersichtsdokument an, das die sieben jüngsten Beiträge mit Titel und Adresse in der richtigen Reihenfolge auflistet. Das Dokument bleibt bewusst unter einer Abschnittslänge, damit es nicht zerteilt wird und das Bruchstück ohne Einleitung obenauf landet.

Vorher und nachher

Messgröße vorher nachher
Antwortzeit (Median) 12,3 s 4,7 s
Genannte Quellen je Antwort 0 3
Mittlere Abschnittslänge im Index 31 Zeichen 996 Zeichen
Einträge mit Quellenadresse keine alle

Auf „zeig mir den neuesten Blogpost" nennt der Bot jetzt in drei Sekunden den richtigen Titel mit direktem Link. Verändert wurde dafür kein Modell und keine Hardware, sondern die Konfiguration von vier Verarbeitungsschritten.

Was bleibt. Alle vier Fehler waren stumm. Ein Vorgabewert, der etwas Sinnvolles tut, nur nicht das Gewünschte. Eine Schutzfunktion, deren Erlaubnisliste leer lief. Eine Grenze, die still kürzt statt abzubrechen. Und eine Überwachung, die den vorigen Vorfall misst. Kein einziger dieser Punkte erzeugt eine Fehlermeldung, und zusammen ergeben sie ein System, das grün meldet und nichts kann.

Nachtrag: der Bericht als Fehlerquelle

Noch am selben Tag stellte sich heraus, dass eine Frage weiterhin falsch beantwortet wurde. Auf „zeig mir den neuesten Blogpost" nannte der Bot einen vier Tage älteren Beitrag und datierte ihn auf den 9. Januar 2026. Die Quellenangabe verwies auf einen dritten Beitrag, der mit der Auskunft nichts zu tun hatte.

Der Index war zu diesem Zeitpunkt bereits repariert: 833 Abschnitte, im Mittel knapp 1000 Zeichen, jeder mit Titel und Adresse. Auch das Übersichtsdokument stand darin, und seine Reihenfolge stimmte. Der Bot hatte die richtige Information im Kontext und gab trotzdem eine falsche Antwort.

Ein Feld, das es nie gab

Die Aufbereitung setzte das Veröffentlichungsdatum aus dem Feld date_published. Die Sammlung blog_posts führt aber nur id, title, slug, content, excerpt und published. Ein Datumsfeld gibt es nicht. Der Ausdruck lieferte also in jedem Durchlauf null, und zwar lautlos: Ein nicht vorhandenes Feld ist in JavaScript kein Fehler, sondern undefined. In allen 833 Einträgen stand als Datum nichts.

Das Muster wiederholt sich. Wie die Zählspalte, die seit jeher null war, ist auch dieses Feld nie von seinem Ausgangswert abgewichen. Ein Wert, der immer leer ist, sieht aus wie ein Wert, den niemand braucht.

Warum ausgerechnet der 9. Januar

Ohne Datum in den Metadaten bleibt dem Modell nur, eines aus dem Fließtext zu nehmen. Im Index kam „9. Januar 2026" zwölfmal vor, und sieben dieser Stellen gehören zu drei Beiträgen, die an diesem Tag erschienen sind. Vier allein entfallen auf den ältesten Beitrag überhaupt, dessen Beschreibung sich wörtlich in der falschen Antwort wiederfindet. Der Bot hat also nicht geraten, sondern das häufigste Datum genommen, das er finden konnte.

Der eigene Fehlerbericht im Index

Eine der zwölf Fundstellen steht in diesem Beitrag. Der Gesprächsverlauf ganz oben, der die falschen Antworten dokumentiert, enthält die Zeile mit dem 9. Januar im Klartext. Beim nächsten Durchlauf wurde er eingelesen, zerteilt und liegt seither als gewöhnlicher Wissensfetzen im Index, ohne Kennzeichnung als Zitat.

Damit beantwortet der Bot eine Frage nach dem neuesten Beitrag mit einem Text, der genau diese Antwort als Fehler ausweist. Wer über Fehler seines eigenen Systems schreibt und den Text anschließend indexiert, füttert es mit seinen eigenen Falschaussagen.

Das Datum steht im Text

Die Sammlung führt kein Datum, die Beiträge selbst schon: In der Kopfzeile jedes Artikels steht es als „Datum 14. August 2026". 67 der 77 Beiträge haben es dort in einheitlicher Form. Die Aufbereitung liest es jetzt mit einem Ausdruck aus dem Text, normalisiert es und hängt es als published_date an jeden Abschnitt. Das Übersichtsdokument weist es zusätzlich im Klartext aus:

1. Eine Wissensbasis aus 2000 Bruchstücken (veroeffentlicht am 14. August 2026)
2. Der Fenster-Sommelier kommt im Maßanzug (veroeffentlicht am 10. August 2026)
3. Wir haben eine Firma gegründet, die nichts verkauft (veroeffentlicht am 10. August 2026)

Sortiert wird weiterhin nach der fortlaufenden Nummer und nicht nach dem gelesenen Datum. Die Nummer haben alle 77 Beiträge, das Datum nur 67, und beide Reihenfolgen unterscheiden sich lediglich bei zwei Beiträgen desselben Tages. Die unsichere Angabe bestimmt also die Anzeige, die sichere die Reihenfolge.

Messgröße vorher nachher
Abschnitte mit Veröffentlichungsdatum 0 von 833 763 von 834
Datum im Übersichtsdokument nicht enthalten bei jedem Eintrag
Antwort auf „zeig mir den neuesten Blogpost" falscher Beitrag, 9. Januar 2026 richtiger Beitrag, 14. August 2026

Der Bot antwortet seither: „Der neueste Blogbeitrag lautet ‚Eine Wissensbasis aus 2000 Bruchstücken' und wurde am 14. August 2026 veröffentlicht." Mit passender Quelle und richtigem Link.

Was noch offen ist. Auf die knappe Frage „was gibt es neues" findet die Suche das Übersichtsdokument weiterhin nicht und antwortet mit älteren Themen. Die Einleitung des Dokuments enthält die Wörter „Neuigkeiten" und „aktuelle Artikel", offenbar reicht das für die Ähnlichkeitssuche nicht. Solche Fragen wären besser aufgehoben, wenn sie gar nicht erst in der Vektorsuche landen, sondern direkt aus dem Redaktionssystem beantwortet werden.
Die Lehre aus dem Nachtrag. Ein Ausdruck auf ein Feld, das es nicht gibt, verhält sich wie ein Vorgabewert: Er liefert etwas Plausibles und schweigt. Und eine Wissensbasis, die den eigenen Fehlerbericht enthält, kann ihn nicht von einer Tatsache unterscheiden.