Zurück zum Blog

Debugging der KI-Pipeline: Wenn Denken den Chatbot stumm macht

Datum 9. Juli 2026
Kategorie KI-Infrastruktur / Debugging
Betroffener Service Chatbot RAG-Pipeline
Kernproblem: Qwen3:14b erzeugt im Thinking-Modus <think>-Blöcke. Bei zu kleinem max_tokens-Budget wurde nur das Denken eingebettet — nie die eigentliche Suchanfrage.

Der Fehler, der keiner war

Der Chatbot auf matzka.cloud beantwortete Fragen wie „Was macht VitalCare?" oder „Erkläre mir Wazuh" seit dem 9. Juli 2026 nur noch mit generischen Antworten: keine Inhalte aus der Wissensdatenbank, kein konkretes Wissen — nur ein Umweg auf den Blog.

Auf den ersten Blick sah die Datenbank in Ordnung aus: Qdrant hatte 726 Vektoren, alle Dienste liefen, keine Fehler in den Logs. Dennoch kamen die Antworten nicht aus den Blog-Inhalten.

Die Architektur der RAG-Pipeline

Der Chatbot-Workflow in n8n arbeitet in vier Stufen:

  1. Query Rewrite — Die Nutzerfrage wird durch Qwen3:14b (via LiteLLM) in eine optimierte Suchanfrage umgeschrieben.
  2. Embedding — Die umgeschriebene Anfrage wird als 768-dimensionaler Vektor kodiert (nomic-embed-text).
  3. Qdrant-Suche — Der Vektor wird per Cosine-Ähnlichkeit gegen die Wissensdatenbank abgeglichen.
  4. LLM-Antwort — Die gefundenen Textpassagen werden als Kontext an das LLM übergeben.

Das LLM hinter dem Alias llama3.1 in LiteLLM ist Qwen3:14b — ein leistungsfähiges Modell, das standardmäßig im Thinking-Modus arbeitet.

Das unsichtbare Problem: <think>-Blöcke

Qwen3 gibt seine internen Überlegungen als <think>...</think>-Block aus, bevor es die eigentliche Antwort schreibt. Mit einem max_tokens-Limit von 150 lief der Ablauf so:

  • Qwen3 beginnt mit dem Denken: <think> Ich soll eine Suchanfrage...
  • Nach ~150 Tokens greift das Limit — mitten im Denk-Block, ohne </think> und ohne eigentliche Antwort
  • Das Embedding-Modell erhielt dann 665 Zeichen pures Denken statt einer Suchanfrage
  • Ergebnis: Cosine-Scores von 0,55 auf irrelevante Inhalte — Wazuh- oder VitalCare-Blöcke (0,8+) wurden nie gefunden
Heimtückisch: Die Pipeline lief ohne Fehler durch. Kein Error, kein Warning — nur ein semantisch bedeutungsloser Embedding-Vektor, der zufällig ähnliche Texte fand.

Diagnose Schritt für Schritt

Warum waren die Qdrant-Scores so niedrig?

Ein Blick in die Qdrant-Antwort zeigte Treffer auf „Umami Analytics" und „Chatbot-Einrichtung" mit Score 0,55 — obwohl der Nutzer nach VitalCare fragte.

Was hat das Query-Rewrite-Modell zurückgegeben?

Der rohe Output des Query-Rewrite-Calls enthielt nur:

<think> Ich soll eine präzise Suchanfrage für die Vektordatenbank erstellen.
Der Nutzer fragt nach VitalCare. Ich muss überlegen, welche Begriffe...

Abgeschnitten. Kein </think>, keine eigentliche Suchanfrage.

Warum fehlte das schließende Tag?

max_tokens: 150 war zu klein. Qwen3 hatte seinen Gedanken noch nicht fertiggeschrieben, bevor das Limit griff.

Zweites Problem: Leere Qdrant-Datenbank

Parallel dazu hatte das Docker-Upgrade vom Vortag eine weitere Baustelle hinterlassen: Der n8n-Ingestion-Workflow (täglich um 2 Uhr) hatte die Qdrant-Kollektion gelöscht und wollte neu befüllen — scheiterte aber beim Embedding-Aufruf an der Tailscale-WireGuard-Key-Staleness (siehe Incident Report: Docker-Upgrade Juli 2026).

Ergebnis: Leere Qdrant-Kollektion, keine Vektoren.

Fix: Direktes Python-Ingest-Skript auf dem Server, das alle Directus-Blog-Posts (53 Stück) und Services (14) abruft, in 800-Zeichen-Chunks aufteilt und per PUT in Qdrant einspielt. 726 Vektoren in ~3 Minuten.

Dritter Fix: Hostname-Auflösung

Der Hostname llm-client — den der n8n-Workflow für den Embedding-Aufruf nutzte — war nicht auflösbar. Kein Docker-Container trug diesen Namen, kein DNS-Eintrag existierte.

Fix in n8n-deployment/docker-compose.yml:

extra_hosts:
  - "llm-client:100.64.0.3"  # Win11 LiteLLM via Tailscale

Damit wird der Hostname direkt im n8n-Container auf die Tailscale-IP des Win11-Rechners aufgelöst — ohne DNS-Konfiguration.

Die Lösung: /no_think

Qwen3 unterstützt eine einfache Methode, den Thinking-Modus zu deaktivieren: das System-Prompt /no_think.

Neue Konfiguration im Query-Rewrite-Node (HTTP-Aufruf an LiteLLM):

{
  "messages": [
    { "role": "system", "content": "/no_think" },
    { "role": "user", "content": "{{$json.prompt}}" }
  ],
  "max_tokens": 800
}

Zusätzlich eine Fallback-Logik im Embedding-Node, die auch dann greift, wenn ein <think>-Block ohne schließendes Tag ankommen sollte:

content
  .replace(/<think>[\s\S]*?<\/think>/gi, '')
  .replace(/<think>[\s\S]*/gi, '')
  .trim() || fallback_query

Das Ergebnis

Nach dem Fix gibt Qwen3 die Suchanfrage direkt aus — ohne Denk-Block. Die Embedding-Qualität verbessert sich sofort:

Pipeline wieder funktionsfähig:
  • Score 0,798 auf den VitalCare-Eintrag bei der Frage „Was macht VitalCare?"
  • 15 relevante Treffer aus Qdrant für typische Fragen
  • Konkrete, informationsreiche Antworten statt Blog-Weiterleitungen

Lektionen

Thinking-Modelle brauchen Thinking-Budget. Ein LLM im Reasoning-Modus mit max_tokens: 150 ist kein sparsames Modell — es ist ein abgeschnittenes. Entweder Thinking deaktivieren (/no_think) oder ausreichend Tokens einplanen (800+).
Embedde nie ungeprüfte LLM-Ausgaben. Vor dem Embedding-Aufruf immer prüfen, ob die Ausgabe sinnvoll ist. Ein Regex-Strip für <think>-Blöcke gehört in jede Pipeline, die mit Qwen3 oder anderen Thinking-Modellen arbeitet.
Niedrige Cosine-Scores sind ein Warnsignal. Score 0,55 bei einer fachlichen Frage ist kein „passables Ergebnis" — es bedeutet, dass das Embedding-Input Müll war. Scores unter 0,70 für inhaltliche Fragen sollten eine Alarm-Regel auslösen.