Aufbau eines datenschutzorientierten KI-Kundensupport-Agenten mit LanceDB, Ollama und Node.js
Note
Dieses Dokument beschreibt unsere Reise beim Aufbau eines selbstgehosteten KI-Support-Agenten. Wir haben über ähnliche Herausforderungen in unserem Blogbeitrag Email Startup Graveyard geschrieben. Ehrlich gesagt haben wir überlegt, einen Nachfolgebeitrag mit dem Titel „AI Startup Graveyard“ zu schreiben, aber vielleicht müssen wir noch ein weiteres Jahr warten, bis die KI-Blase möglicherweise platzt(?). Für den Moment ist dies unser Brain Dump darüber, was funktioniert hat, was nicht und warum wir es so gemacht haben.
So haben wir unseren eigenen KI-Kundensupport-Agenten gebaut. Wir haben es auf die harte Tour gemacht: selbstgehostet, datenschutzorientiert und vollständig unter unserer Kontrolle. Warum? Weil wir Drittanbietern nicht die Daten unserer Kunden anvertrauen. Es ist eine Anforderung der DSGVO und des DPA und es ist das Richtige.
Das war kein spaßiges Wochenendprojekt. Es war eine monatelange Reise durch kaputte Abhängigkeiten, irreführende Dokumentationen und das allgemeine Chaos des Open-Source-KI-Ökosystems im Jahr 2025. Dieses Dokument ist eine Aufzeichnung dessen, was wir gebaut haben, warum wir es gebaut haben und welche Hindernisse wir dabei überwinden mussten.
Kundenvorteile: KI-unterstützter menschlicher Support
Unser KI-System ersetzt nicht unser Support-Team – es macht es besser. Das bedeutet Folgendes für Sie:
Schnellere, genauere Antworten
Human-in-the-Loop: Jeder KI-generierte Entwurf wird von unserem menschlichen Support-Team überprüft, bearbeitet und kuratiert, bevor er an Sie gesendet wird. Die KI übernimmt die erste Recherche und das Verfassen, sodass sich unser Team auf Qualitätskontrolle und Personalisierung konzentrieren kann.
Trainiert auf menschlicher Expertise: Die KI lernt von:
- Unserer handgeschriebenen Wissensdatenbank und Dokumentation
- Von Menschen verfassten Blogbeiträgen und Tutorials
- Unserem umfassenden FAQ (von Menschen geschrieben)
- Vergangenen Kundengesprächen (alle von echten Menschen bearbeitet)
Sie erhalten Antworten, die auf jahrelanger menschlicher Expertise basieren – nur schneller geliefert.
Konsistenz ohne Burnout
Unser kleines Team bearbeitet täglich Hunderte von Supportanfragen, die jeweils unterschiedliche technische Kenntnisse und ständiges mentales Umschalten erfordern:
- Abrechnungsfragen erfordern Kenntnisse im Finanzsystem
- DNS-Probleme erfordern Netzwerkexpertise
- API-Integration erfordert Programmierkenntnisse
- Sicherheitsberichte erfordern Schwachstellenbewertung
Ohne KI-Unterstützung führt dieses ständige Umschalten zu:
- Längeren Antwortzeiten
- Menschlichen Fehlern durch Ermüdung
- Inkonsistenter Antwortqualität
- Team-Burnout
Mit KI-Unterstützung reagiert unser Team:
- Schneller (KI erstellt Entwürfe in Sekunden)
- Macht weniger Fehler (KI erkennt häufige Fehler)
- Hält gleichbleibende Qualität (KI bezieht sich jedes Mal auf dieselbe Wissensdatenbank)
- Bleibt frisch und fokussiert (weniger Zeit für Recherche, mehr Zeit zum Helfen)
Was Sie bekommen
✅ Geschwindigkeit: KI erstellt Entwürfe in Sekunden, Menschen prüfen und senden innerhalb von Minuten
✅ Genauigkeit: Antworten basieren auf unserer tatsächlichen Dokumentation und vergangenen Lösungen
✅ Konsistenz: Immer dieselben hochwertigen Antworten, egal ob 9 Uhr morgens oder 21 Uhr abends
✅ Menschliche Note: Jede Antwort wird von unserem Team geprüft und personalisiert
✅ Keine Halluzinationen: Die KI nutzt nur unsere verifizierte Wissensdatenbank, nicht generische Internetdaten
Note
Sie sprechen immer mit Menschen. Die KI ist ein Forschungsassistent, der unserem Team hilft, die richtige Antwort schneller zu finden. Stellen Sie sich das vor wie einen Bibliothekar, der sofort das relevante Buch findet – aber ein Mensch liest es noch und erklärt es Ihnen.
Eine persönliche Reflexion: Der zwei Jahrzehnte lange Kampf
Bevor wir in die technischen Details eintauchen, eine persönliche Anmerkung. Ich bin seit fast zwei Jahrzehnten dabei. Die endlosen Stunden an der Tastatur, die unermüdliche Suche nach einer Lösung, das tiefe, fokussierte Arbeiten – das ist die Realität, wenn man etwas Bedeutendes aufbaut. Eine Realität, die in den Hype-Zyklen neuer Technologien oft übersehen wird.
Die jüngste Explosion der KI war besonders frustrierend. Uns wird ein Traum von Automatisierung verkauft, von KI-Assistenten, die unseren Code schreiben und unsere Probleme lösen. Die Realität? Das Ergebnis ist oft Müll-Code, der mehr Zeit zum Korrigieren benötigt, als es gedauert hätte, ihn von Grund auf selbst zu schreiben. Das Versprechen, unser Leben zu erleichtern, ist falsch. Es lenkt ab von der harten, notwendigen Arbeit des Aufbaus.
Und dann gibt es das Catch-22 beim Beitrag zu Open Source. Man ist schon ausgelaugt, erschöpft vom Kampf. Man nutzt eine KI, um einen detaillierten, gut strukturierten Fehlerbericht zu schreiben, in der Hoffnung, es den Maintainer:innen leichter zu machen, das Problem zu verstehen und zu beheben. Und was passiert? Man wird getadelt. Der Beitrag wird als „off-topic“ oder „geringer Aufwand“ abgetan, wie wir in einem kürzlichen Node.js GitHub Issue gesehen haben. Das ist ein Schlag ins Gesicht für erfahrene Entwickler:innen, die einfach nur helfen wollen.
Das ist die Realität des Ökosystems, in dem wir arbeiten. Es geht nicht nur um kaputte Werkzeuge; es geht um eine Kultur, die oft die Zeit und den Aufwand ihrer Beitragenden nicht respektiert. Dieser Beitrag ist eine Chronik dieser Realität. Es ist eine Geschichte über Werkzeuge, ja, aber auch über die menschlichen Kosten des Bauens in einem kaputten Ökosystem, das trotz all seiner Versprechen grundlegend defekt ist.
Warum Datenschutz wichtig ist
Unser technisches Whitepaper behandelt unsere Datenschutzphilosophie ausführlich. Die Kurzfassung: Wir senden keine Kundendaten an Dritte. Niemals. Das bedeutet kein OpenAI, kein Anthropic, keine cloud-gehosteten Vektordatenbanken. Alles läuft lokal auf unserer Infrastruktur. Dies ist unverhandelbar für die DSGVO-Konformität und unsere DPA-Verpflichtungen.
Kostenanalyse: Cloud-KI vs Selbst-Hosting
Bevor wir in die technische Umsetzung eintauchen, sprechen wir darüber, warum Selbst-Hosting aus Kostensicht wichtig ist. Die Preismodelle von Cloud-KI-Diensten machen sie für Anwendungsfälle mit hohem Volumen wie Kundensupport unerschwinglich.
Vergleich von Cloud-KI-Diensten
| Dienst | Anbieter | Einbettungskosten | LLM-Kosten (Eingabe) | LLM-Kosten (Ausgabe) | Datenschutzrichtlinie | DSGVO/DPA | Hosting | Datenaustausch |
|---|---|---|---|---|---|---|---|---|
| OpenAI | OpenAI (USA) | $0.02-0.13/1M Tokens | $0.15-20/1M Tokens | $0.60-80/1M Tokens | Link | Eingeschränkte DPA | Azure (USA) | Ja (Training) |
| Claude | Anthropic (USA) | N/A | $3-20/1M Tokens | $15-80/1M Tokens | Link | Eingeschränkte DPA | AWS/GCP (USA) | Nein (behauptet) |
| Gemini | Google (USA) | $0.15/1M Tokens | $0.30-1.00/1M Tokens | $2.50/1M Tokens | Link | Eingeschränkte DPA | GCP (USA) | Ja (Verbesserung) |
| DeepSeek | DeepSeek (China) | N/A | $0.028-0.28/1M Tokens | $0.42/1M Tokens | Link | Unbekannt | China | Unbekannt |
| Mistral | Mistral AI (Frankreich) | $0.10/1M Tokens | $0.40/1M Tokens | $2.00/1M Tokens | Link | EU DSGVO | EU | Unbekannt |
| Selbst-Hosting | Sie | $0 (vorhandene Hardware) | $0 (vorhandene Hardware) | $0 (vorhandene Hardware) | Ihre Richtlinie | Volle Konformität | MacBook M5 + cron | Nie |
Warning
Bedenken zur Datensouveränität: US-Anbieter (OpenAI, Claude, Gemini) unterliegen dem CLOUD Act, der der US-Regierung Zugriff auf Daten erlaubt. DeepSeek (China) operiert unter chinesischem Datenschutzrecht. Während Mistral (Frankreich) EU-Hosting und DSGVO-Konformität bietet, bleibt Selbst-Hosting die einzige Option für vollständige Datensouveränität und Kontrolle.
Kostenaufstellung: 5GB Wissensbasis
Berechnen wir die Kosten für die Verarbeitung einer 5GB großen Wissensbasis (typisch für ein mittelständisches Unternehmen mit Dokumenten, E-Mails und Supporthistorie).
Annahmen:
- 5GB Text ≈ 1,25 Milliarden Tokens (angenommen ~4 Zeichen/Token)
- Initiale Einbettungserstellung
- Monatliches Retraining (vollständige Neueinbettung)
- 10.000 Supportanfragen pro Monat
- Durchschnittliche Anfrage: 500 Tokens Eingabe, 300 Tokens Ausgabe Detaillierte Kostenaufstellung:
| Komponente | OpenAI | Claude | Gemini | Selbst-gehostet |
|---|---|---|---|---|
| Initiales Embedding (1,25 Mrd. Tokens) | 25.000 $ | N/A | 187.500 $ | 0 $ |
| Monatliche Anfragen (10K × 800 Tokens) | 1.200-16.000 $ | 2.400-16.000 $ | 2.400-3.200 $ | 0 $ |
| Monatliches Retraining (1,25 Mrd. Tokens) | 25.000 $ | N/A | 187.500 $ | 0 $ |
| Gesamt erstes Jahr | 325.200-217.000 $ | 28.800-192.000 $ | 2.278.800-2.226.000 $ | ~60 $ (Strom) |
| Datenschutzkonformität | ❌ Eingeschränkt | ❌ Eingeschränkt | ❌ Eingeschränkt | ✅ Vollständig |
| Datenhoheit | ❌ Nein | ❌ Nein | ❌ Nein | ✅ Ja |
Caution
Geminis Embedding-Kosten sind katastrophal bei 0,15 $/1 Mio. Tokens. Ein einziges 5GB großes Wissensdatenbank-Embedding würde 187.500 $ kosten. Das ist 37x teurer als OpenAI und macht es für den produktiven Einsatz völlig unbrauchbar.
Selbst-gehostete Hardwarekosten
Unser Setup läuft auf vorhandener Hardware, die wir bereits besitzen:
- Hardware: MacBook M5 (bereits für Entwicklung vorhanden)
- Zusätzliche Kosten: 0 $ (verwendet vorhandene Hardware)
- Stromkosten: ~5 $/Monat (geschätzt)
- Gesamt erstes Jahr: ~60 $
- Laufend: 60 $/Jahr
ROI: Selbst-Hosting verursacht praktisch keine zusätzlichen Kosten, da wir vorhandene Entwicklungs-Hardware nutzen. Das System läuft per Cron-Jobs während der Nebenzeiten.
Eigene API selbst nutzen
Eine der wichtigsten architektonischen Entscheidungen war, alle KI-Jobs direkt über die Forward Email API laufen zu lassen. Das ist nicht nur gute Praxis – es ist ein Zwang zur Leistungsoptimierung.
Warum Dogfooding wichtig ist
Wenn unsere KI-Jobs dieselben API-Endpunkte wie unsere Kunden verwenden:
- Leistungsengpässe betreffen uns zuerst – Wir spüren die Probleme vor den Kunden
- Optimierung nützt allen – Verbesserungen für unsere Jobs verbessern automatisch die Kundenerfahrung
- Real-World-Tests – Unsere Jobs verarbeiten Tausende von E-Mails und bieten kontinuierliches Lasttesting
- Code-Wiederverwendung – Gleiche Authentifizierung, Ratenbegrenzung, Fehlerbehandlung und Caching-Logik
API-Nutzungsbeispiele
Nachrichten auflisten (train-from-history.js):
// Verwendet GET /v1/messages?folder=INBOX mit BasicAuth
// Schließt eml, raw, nodemailer aus, um Antwortgröße zu reduzieren (nur IDs benötigt)
const response = await axios.get(
`${this.apiBase}/v1/messages`,
{
params: {
folder: 'INBOX',
limit: 100,
eml: false,
raw: false,
nodemailer: false
},
auth: {
username: process.env.FORWARD_EMAIL_ALIAS_USERNAME,
password: process.env.FORWARD_EMAIL_ALIAS_PASSWORD
}
}
);
const messages = response.data;
// Rückgabe: [{ id, subject, date, ... }, ...]
// Voller Nachrichteninhalt wird später via GET /v1/messages/:id abgerufen
Vollständige Nachrichten abrufen (forward-email-client.js):
// Verwendet GET /v1/messages/:id, um vollständige Nachricht mit rohem Inhalt zu erhalten
const response = await axios.get(
`${this.apiBase}/v1/messages/${messageId}`,
{
auth: {
username: this.aliasUsername,
password: this.aliasPassword
}
}
);
const message = response.data;
// Rückgabe: { id, subject, raw, eml, nodemailer: { ... }, ... }
Entwürfe von Antworten erstellen (process-inbox.js):
// Verwendet POST /v1/messages, um Entwurfsantworten zu erstellen
const response = await axios.post(
`${this.apiBase}/v1/messages`,
{
folder: 'Drafts',
subject: `Re: ${originalSubject}`,
to: senderEmail,
text: generatedResponse,
inReplyTo: originalMessageId
},
{
auth: {
username: process.env.FORWARD_EMAIL_ALIAS_USERNAME,
password: process.env.FORWARD_EMAIL_ALIAS_PASSWORD
}
}
);
Leistungsverbesserungen
Da unsere KI-Jobs auf derselben API-Infrastruktur laufen:
- Caching-Optimierungen kommen sowohl den Jobs als auch den Kunden zugute
- Rate Limiting wird unter realer Last getestet
- Fehlerbehandlung ist erprobt
- API-Antwortzeiten werden ständig überwacht
- Datenbankabfragen sind für beide Anwendungsfälle optimiert
- Bandbreitenoptimierung – Das Ausschließen von
eml,raw,nodemailerbeim Auflisten reduziert die Antwortgröße um ~90%
Wenn train-from-history.js 1.000 E-Mails verarbeitet, werden über 1.000 API-Aufrufe gemacht. Jede Ineffizienz in der API wird sofort sichtbar. Das zwingt uns, den IMAP-Zugriff, Datenbankabfragen und die Antwortserialisierung zu optimieren – Verbesserungen, die direkt unseren Kunden zugutekommen.
Beispieloptimierung: Auflisten von 100 Nachrichten mit vollem Inhalt = ~10MB Antwort. Auflisten mit eml: false, raw: false, nodemailer: false = ~100KB Antwort (100x kleiner).
Verschlüsselungsarchitektur
Unsere E-Mail-Speicherung verwendet mehrere Verschlüsselungsebenen, die die KI-Jobs in Echtzeit für das Training entschlüsseln müssen.
Ebene 1: Postfachverschlüsselung (chacha20-poly1305)
Alle IMAP-Postfächer werden als SQLite-Datenbanken gespeichert, die mit chacha20-poly1305 verschlüsselt sind, einem quantensicheren Verschlüsselungsalgorithmus. Dies wird in unserem Blogbeitrag zum quantensicheren verschlüsselten E-Mail-Dienst ausführlich beschrieben.
Wesentliche Eigenschaften:
- Algorithmus: ChaCha20-Poly1305 (AEAD-Chiffre)
- Quantensicher: Resistenz gegen Angriffe durch Quantencomputer
- Speicherung: SQLite-Datenbankdateien auf der Festplatte
- Zugriff: Im Speicher entschlüsselt bei Zugriff über IMAP/API
Ebene 2: Nachrichtenebene PGP-Verschlüsselung
Viele Support-E-Mails sind zusätzlich mit PGP (OpenPGP-Standard) verschlüsselt. Die KI-Jobs müssen diese entschlüsseln, um Inhalte für das Training zu extrahieren.
Entschlüsselungsablauf:
// 1. API gibt Nachricht mit verschlüsseltem Rohinhalt zurück
const message = await forwardEmailClient.getMessage(id);
// 2. Prüfen, ob Rohinhalt PGP-verschlüsselt ist
if (isMessageEncrypted(message.raw)) {
// 3. Mit unserem privaten Schlüssel entschlüsseln
const decryptedRaw = await pgpDecrypt(message.raw);
// 4. Entschlüsselte MIME-Nachricht parsen
const parsed = await simpleParser(decryptedRaw);
// 5. Nodemailer mit entschlüsseltem Inhalt befüllen
message.nodemailer = {
text: parsed.text,
html: parsed.html,
from: parsed.from,
to: parsed.to,
subject: parsed.subject,
date: parsed.date
};
}
PGP-Konfiguration:
# Privater Schlüssel zur Entschlüsselung (Pfad zur ASCII-armored Schlüsseldatei)
GPG_SECURITY_KEY="/path/to/private-key.asc"
# Passphrase für privaten Schlüssel (falls verschlüsselt)
GPG_SECURITY_PASSPHRASE="your-passphrase"
Der pgp-decrypt.js Helfer:
- Liest den privaten Schlüssel einmalig von der Festplatte (im Speicher zwischengespeichert)
- Entschlüsselt den Schlüssel mit der Passphrase
- Verwendet den entschlüsselten Schlüssel für alle Nachrichtenentschlüsselungen
- Unterstützt rekursive Entschlüsselung für verschachtelte verschlüsselte Nachrichten
Warum das für das Training wichtig ist
Ohne richtige Entschlüsselung würde die KI auf verschlüsseltem Kauderwelsch trainieren:
-----BEGIN PGP MESSAGE-----
Version: OpenPGP.js v4.10.10
wcBMA8Z3lHJnFnNUAQgAqK7F8...
-----END PGP MESSAGE-----
Mit Entschlüsselung trainiert die KI auf tatsächlichen Inhalten:
Subject: Re: Bug Report
Hi John,
Thanks for reporting this issue. I've confirmed the bug
and created a fix in PR #1234...
Speichersicherheit
Die Entschlüsselung erfolgt im Speicher während der Jobausführung, und der entschlüsselte Inhalt wird in Embeddings umgewandelt, die dann in der LanceDB-Vektordatenbank auf der Festplatte gespeichert werden.
Wo die Daten liegen:
- Vektordatenbank: Gespeichert auf verschlüsselten MacBook M5-Arbeitsstationen
- Physische Sicherheit: Arbeitsstationen bleiben jederzeit bei uns (nicht in Rechenzentren)
- Festplattenverschlüsselung: Vollständige Festplattenverschlüsselung auf allen Arbeitsstationen
- Netzwerksicherheit: Firewall und Isolation von öffentlichen Netzwerken
Zukünftige Rechenzentrum-Bereitstellung: Falls wir jemals auf Rechenzentrums-Hosting umsteigen, werden die Server folgende Merkmale haben:
- LUKS-Festplattenverschlüsselung
- USB-Zugriff deaktiviert
- Physische Sicherheitsmaßnahmen
- Netzwerkisolation Für vollständige Details zu unseren Sicherheitspraktiken siehe unsere Sicherheitsseite.
Note
Die Vektordatenbank enthält Embeddings (mathematische Darstellungen), nicht den ursprünglichen Klartext. Embeddings können jedoch potenziell rückentwickelt werden, weshalb wir sie auf verschlüsselten, physisch gesicherten Arbeitsstationen aufbewahren.
Lokale Speicherung ist Standardpraxis
Das Speichern von Embeddings auf den Arbeitsstationen unseres Teams unterscheidet sich nicht von der Art und Weise, wie wir bereits mit E-Mails umgehen:
- Thunderbird: Lädt den vollständigen E-Mail-Inhalt herunter und speichert ihn lokal in mbox/maildir-Dateien
- Webmail-Clients: Zwischenspeichern E-Mail-Daten im Browser-Speicher und lokalen Datenbanken
- IMAP-Clients: Halten lokale Kopien von Nachrichten für den Offline-Zugriff vor
- Unser KI-System: Speichert mathematische Embeddings (nicht Klartext) in LanceDB
Der entscheidende Unterschied: Embeddings sind sicherer als Klartext-E-Mails, weil sie:
- Mathematische Darstellungen sind, kein lesbarer Text
- Schwerer rückentwickelbar sind als Klartext
- Dennoch der gleichen physischen Sicherheit unterliegen wie unsere E-Mail-Clients
Wenn es für unser Team akzeptabel ist, Thunderbird oder Webmail auf verschlüsselten Arbeitsstationen zu verwenden, ist es ebenso akzeptabel (und vermutlich sicherer), Embeddings auf dieselbe Weise zu speichern.
Die Architektur
Hier ist der grundlegende Ablauf. Er sieht einfach aus. War er aber nicht.
Note
Alle Jobs verwenden die Forward Email API direkt, wodurch Leistungsoptimierungen sowohl unserem KI-System als auch unseren Kunden zugutekommen.
Grober Ablauf
Detaillierter Scraper-Ablauf
Die scraper.js ist das Herzstück der Datenaufnahme. Es ist eine Sammlung von Parsern für verschiedene Datenformate.
Funktionsweise
Der Prozess ist in drei Hauptteile gegliedert: Aufbau der Wissensdatenbank, Training mit historischen E-Mails und Verarbeitung neuer E-Mails.
Aufbau der Wissensdatenbank
update-knowledge-base.js: Dies ist der Hauptjob. Er läuft nachts, löscht den alten Vektor-Speicher und baut ihn von Grund auf neu auf. Er verwendet scraper.js, um Inhalte aus allen Quellen abzurufen, processor.js, um sie zu zerteilen, und ollama-client.js, um Embeddings zu erzeugen. Schließlich speichert vector-store.js alles in LanceDB.
Datensourcen:
- Lokale Markdown-Dateien (
docs/*.md) - Technisches Whitepaper PDF (
assets/technical-whitepaper.pdf) - API-Spezifikation JSON (
assets/api-spec.json) - GitHub Issues (über Octokit)
- GitHub Diskussionen (über Octokit)
- GitHub Pull Requests (über Octokit)
- Sitemap-URL-Liste (
$LANCEDB_PATH/valid-urls.json)
Training mit historischen E-Mails
train-from-history.js: Dieser Job scannt historische E-Mails aus allen Ordnern, entschlüsselt PGP-verschlüsselte Nachrichten und fügt sie einem separaten Vektor-Speicher (customer_support_history) hinzu. Dies liefert Kontext aus vergangenen Support-Interaktionen.
E-Mail-Verarbeitungsablauf:
Hauptfunktionen:
- PGP-Entschlüsselung: Verwendet
pgp-decrypt.jsHelfer mit der UmgebungsvariableGPG_SECURITY_KEY - Thread-Gruppierung: Gruppiert verwandte E-Mails in Gesprächs-Threads
- Metadaten-Erhaltung: Speichert Ordner, Betreff, Datum, Verschlüsselungsstatus
- Antwort-Kontext: Verknüpft Nachrichten mit ihren Antworten für besseren Kontext
Konfiguration:
# Umgebungsvariablen für train-from-history
HISTORY_SCAN_LIMIT=1000 # Maximal zu verarbeitende Nachrichten
HISTORY_SCAN_SINCE="2024-01-01" # Nur Nachrichten nach diesem Datum verarbeiten
HISTORY_DECRYPT_PGP=true # PGP-Entschlüsselung versuchen
GPG_SECURITY_KEY="/path/to/key.asc" # Pfad zum PGP-Privatschlüssel
GPG_SECURITY_PASSPHRASE="passphrase" # Schlüssel-Passphrase (optional)
Was gespeichert wird:
{
type: 'historical_email',
folder: 'INBOX',
subject: 'Re: Bug Report',
date: '2025-01-15T10:30:00Z',
messageId: '67e2f288893921...',
threadId: 'Bug Report',
hasReply: true,
encrypted: true,
decrypted: true,
replySubject: 'Bug Report',
replyText: 'First 500 chars of reply...',
chunkSize: 1000,
chunkOverlap: 200,
chunkIndex: 0
}
Tip
Führen Sie train-from-history nach der Erstkonfiguration aus, um den historischen Kontext zu füllen. Dies verbessert die Antwortqualität erheblich, indem es aus vergangenen Support-Interaktionen lernt.
Verarbeitung eingehender E-Mails
process-inbox.js: Dieser Job läuft auf E-Mails in unseren Postfächern support@forwardemail.net, abuse@forwardemail.net und security@forwardemail.net (speziell im IMAP-Ordnerpfad INBOX). Er nutzt unsere API unter https://forwardemail.net/email-api (z.B. GET /v1/messages?folder=INBOX mit BasicAuth-Zugangsdaten für jedes Postfach). Der Inhalt der E-Mail wird analysiert, sowohl die Wissensdatenbank (forward_email_knowledge_base) als auch der historische E-Mail-Vektor-Speicher (customer_support_history) abgefragt und der kombinierte Kontext an response-generator.js übergeben. Der Generator verwendet mxbai-embed-large über Ollama, um eine Antwort zu erstellen.
Automatisierte Workflow-Funktionen:
-
Inbox Zero Automatisierung: Nach erfolgreichem Erstellen eines Entwurfs wird die Originalnachricht automatisch in den Archiv-Ordner verschoben. Dies hält Ihren Posteingang sauber und hilft, Inbox Zero ohne manuelles Eingreifen zu erreichen.
-
AI-Verarbeitung überspringen: Fügen Sie einfach ein Label
skip-ai(Groß-/Kleinschreibung egal) zu einer Nachricht hinzu, um die KI-Verarbeitung zu verhindern. Die Nachricht bleibt unverändert im Posteingang, sodass Sie sie manuell bearbeiten können. Dies ist nützlich für sensible Nachrichten oder komplexe Fälle, die menschliches Urteilsvermögen erfordern. -
Korrekte E-Mail-Threading: Alle Entwurfsantworten enthalten die Originalnachricht darunter zitiert (mit dem Standard-Präfix
>), gemäß den E-Mail-Antwortkonventionen mit dem Format „Am [Datum], [Absender] schrieb:“. Dies gewährleistet korrekten Gesprächskontext und Threading in E-Mail-Clients. -
Antwort-an-Alle-Verhalten: Das System behandelt automatisch Reply-To-Header und CC-Empfänger:
- Wenn ein Reply-To-Header vorhanden ist, wird dieser zur To-Adresse und der ursprüngliche From wird zu CC hinzugefügt
- Alle ursprünglichen To- und CC-Empfänger werden in der Antwort-CC eingeschlossen (außer Ihre eigene Adresse)
- Folgt den Standardkonventionen für Antwort-an-Alle bei Gruppengesprächen Quellen-Ranking: Das System verwendet ein gewichtetes Ranking, um Quellen zu priorisieren:
- FAQ: 100 % (höchste Priorität)
- Technisches Whitepaper: 95 %
- API-Spezifikation: 90 %
- Offizielle Dokumentation: 85 %
- GitHub-Issues: 70 %
- Historische E-Mails: 50 %
Verwaltung des Vector Stores
Die Klasse VectorStore in helpers/customer-support-ai/vector-store.js ist unsere Schnittstelle zu LanceDB.
Dokumente hinzufügen:
// vector-store.js
async addDocument(text, metadata) {
const embedding = await this.ollama.generateEmbedding(text);
await this.table.add([{
vector: embedding,
text,
...metadata
}]);
}
Store leeren:
// Option 1: Verwende die clear()-Methode
await vectorStore.clear();
// Option 2: Lösche das lokale Datenbankverzeichnis
await fs.rm(process.env.LANCEDB_PATH, { recursive: true, force: true });
Die Umgebungsvariable LANCEDB_PATH zeigt auf das lokale eingebettete Datenbankverzeichnis. LanceDB ist serverlos und eingebettet, daher gibt es keinen separaten Prozess zur Verwaltung.
Der Friedhof der Vektor-Datenbanken
Dies war die erste große Hürde. Wir haben mehrere Vektor-Datenbanken ausprobiert, bevor wir uns für LanceDB entschieden haben. Hier ist, was bei jeder schiefgelaufen ist.
| Datenbank | GitHub | Was schiefgelaufen ist | Spezifische Probleme | Sicherheitsbedenken |
|---|---|---|---|---|
| ChromaDB | chroma-core/chroma | pip3 install chromadb liefert eine uralte Version mit PydanticImportError. Die einzige Möglichkeit, eine funktionierende Version zu bekommen, ist die Kompilierung aus dem Quellcode. Nicht entwicklerfreundlich. |
Python-Abhängigkeitschaos. Mehrere Nutzer berichten von kaputten pip-Installationen (#774, #163). Die Dokumentation sagt "benutze einfach Docker", was keine Lösung für lokale Entwicklung ist. Abstürze unter Windows bei >99 Datensätzen (#3058). | CVE-2024-45848: Ausführung beliebigen Codes über ChromaDB-Integration in MindsDB. Kritische OS-Schwachstellen im Docker-Image (#3170). |
| Qdrant | qdrant/qdrant | Der Homebrew-Tap (qdrant/qdrant/qdrant), der in der alten Dokumentation erwähnt wird, ist verschwunden. Keine Erklärung. Die offiziellen Docs sagen jetzt nur noch "benutze Docker." |
Fehlender Homebrew-Tap. Kein natives macOS-Binary. Docker-only ist eine Hürde für schnelles lokales Testen. | CVE-2024-2221: Schwachstelle für beliebigen Datei-Upload mit Remote-Code-Ausführung (behoben in v1.9.0). Schwache Sicherheitsbewertung von IronCore Labs. |
| Weaviate | weaviate/weaviate | Die Homebrew-Version hatte einen kritischen Clustering-Bug (leader not found). Die dokumentierten Flags zur Behebung (RAFT_JOIN, CLUSTER_HOSTNAME) funktionierten nicht. Grundsätzlich für Single-Node-Setups ungeeignet. |
Clustering-Bugs selbst im Single-Node-Modus. Überkomplex für einfache Anwendungsfälle. | Keine größeren CVEs gefunden, aber die Komplexität erhöht die Angriffsfläche. |
| LanceDB | lancedb/lancedb | Diese funktionierte. Sie ist eingebettet und serverlos. Kein separater Prozess. Der einzige Ärgernis ist die verwirrende Paketbenennung (vectordb ist veraltet, nutze @lancedb/lancedb) und verstreute Dokumentation. Damit können wir leben. |
Verwirrung bei der Paketbenennung (vectordb vs. @lancedb/lancedb), ansonsten solide. Die eingebettete Architektur eliminiert ganze Klassen von Sicherheitsproblemen. |
Keine bekannten CVEs. Das eingebettete Design bedeutet keine Netzwerkangriffsfläche. |
Warning
ChromaDB hat kritische Sicherheitslücken. CVE-2024-45848 ermöglicht die Ausführung beliebigen Codes. Die pip-Installation ist aufgrund von Pydantic-Abhängigkeitsproblemen grundsätzlich fehlerhaft. Für den Produktionseinsatz vermeiden.
Warning
Qdrant hatte eine RCE-Sicherheitslücke beim Datei-Upload (CVE-2024-2221), die erst in Version v1.9.0 behoben wurde. Wenn Sie Qdrant verwenden müssen, stellen Sie sicher, dass Sie die neueste Version nutzen.
Caution
Das Open-Source-Vektor-Datenbank-Ökosystem ist noch unausgereift. Vertrauen Sie der Dokumentation nicht. Gehen Sie davon aus, dass alles fehlerhaft ist, bis das Gegenteil bewiesen ist. Testen Sie lokal, bevor Sie sich auf einen Stack festlegen.
Systemanforderungen
- Node.js: v18.0.0+ (GitHub)
- Ollama: Neueste Version (GitHub)
- Modell:
mxbai-embed-largeüber Ollama - Vektor-Datenbank: LanceDB (GitHub)
- GitHub-Zugriff:
@octokit/restzum Scrapen von Issues (GitHub) - SQLite: Für die Primärdatenbank (über
mongoose-to-sqlite)
Cron-Job-Konfiguration
Alle KI-Jobs laufen via cron auf einem MacBook M5. So richten Sie die Cron-Jobs ein, damit sie um Mitternacht für mehrere Postfächer ausgeführt werden.
Umgebungsvariablen
Die Jobs benötigen diese Umgebungsvariablen. Die meisten können in der .env-Datei gesetzt werden (geladen über @ladjs/env), aber HISTORY_SCAN_SINCE muss dynamisch in der crontab berechnet werden.
In der .env-Datei:
# Forward Email API-Zugangsdaten (ändern sich pro Postfach)
FORWARD_EMAIL_ALIAS_USERNAME=support@forwardemail.net
FORWARD_EMAIL_ALIAS_PASSWORD=dein-imap-passwort
# PGP-Entschlüsselung (für alle Postfächer gemeinsam)
GPG_SECURITY_KEY=/pfad/zum/private-key.asc
GPG_SECURITY_PASSPHRASE=dein-passwort
# Konfiguration für historischen Scan
HISTORY_SCAN_LIMIT=1000
# LanceDB-Pfad
LANCEDB_PATH=/pfad/zu/lancedb
In der crontab (dynamisch berechnet):
# HISTORY_SCAN_SINCE muss inline in der crontab mit Shell-Datumsberechnung gesetzt werden
# Kann nicht in der .env-Datei sein, da @ladjs/env keine Shell-Befehle ausführt
HISTORY_SCAN_SINCE="$(date -v-1d +%Y-%m-%d)" # macOS
HISTORY_SCAN_SINCE="$(date -d 'yesterday' +%Y-%m-%d)" # Linux
Cron-Jobs für mehrere Postfächer
Bearbeiten Sie Ihre crontab mit crontab -e und fügen Sie hinzu:
# Wissensdatenbank aktualisieren (läuft einmal, für alle Postfächer gemeinsam)
0 0 * * * cd /pfad/zu/forwardemail.net && LANCEDB_PATH="/pfad/zu/lancedb" GPG_SECURITY_KEY="/pfad/zu/key.asc" GPG_SECURITY_PASSPHRASE="pass" node jobs/customer-support-ai/update-knowledge-base.js >> /var/log/update-knowledge-base.log 2>&1
# Training aus Historie - support@forwardemail.net
0 0 * * * cd /pfad/zu/forwardemail.net && FORWARD_EMAIL_ALIAS_USERNAME="support@forwardemail.net" FORWARD_EMAIL_ALIAS_PASSWORD="support-passwort" HISTORY_SCAN_SINCE="$(date -v-1d +%Y-%m-%d)" HISTORY_SCAN_LIMIT=1000 GPG_SECURITY_KEY="/pfad/zu/key.asc" GPG_SECURITY_PASSPHRASE="pass" LANCEDB_PATH="/pfad/zu/lancedb" node jobs/customer-support-ai/train-from-history.js >> /var/log/train-support.log 2>&1
# Training aus Historie - abuse@forwardemail.net
0 0 * * * cd /pfad/zu/forwardemail.net && FORWARD_EMAIL_ALIAS_USERNAME="abuse@forwardemail.net" FORWARD_EMAIL_ALIAS_PASSWORD="abuse-passwort" HISTORY_SCAN_SINCE="$(date -v-1d +%Y-%m-%d)" HISTORY_SCAN_LIMIT=1000 GPG_SECURITY_KEY="/pfad/zu/key.asc" GPG_SECURITY_PASSPHRASE="pass" LANCEDB_PATH="/pfad/zu/lancedb" node jobs/customer-support-ai/train-from-history.js >> /var/log/train-abuse.log 2>&1
# Training aus Historie - security@forwardemail.net
0 0 * * * cd /pfad/zu/forwardemail.net && FORWARD_EMAIL_ALIAS_USERNAME="security@forwardemail.net" FORWARD_EMAIL_ALIAS_PASSWORD="security-passwort" HISTORY_SCAN_SINCE="$(date -v-1d +%Y-%m-%d)" HISTORY_SCAN_LIMIT=1000 GPG_SECURITY_KEY="/pfad/zu/key.asc" GPG_SECURITY_PASSPHRASE="pass" LANCEDB_PATH="/pfad/zu/lancedb" node jobs/customer-support-ai/train-from-history.js >> /var/log/train-security.log 2>&1
# Posteingang verarbeiten - support@forwardemail.net
*/5 * * * * cd /pfad/zu/forwardemail.net && FORWARD_EMAIL_ALIAS_USERNAME="support@forwardemail.net" FORWARD_EMAIL_ALIAS_PASSWORD="support-passwort" GPG_SECURITY_KEY="/pfad/zu/key.asc" GPG_SECURITY_PASSPHRASE="pass" LANCEDB_PATH="/pfad/zu/lancedb" node jobs/customer-support-ai/process-inbox.js >> /var/log/process-support.log 2>&1
# Posteingang verarbeiten - abuse@forwardemail.net
*/5 * * * * cd /pfad/zu/forwardemail.net && FORWARD_EMAIL_ALIAS_USERNAME="abuse@forwardemail.net" FORWARD_EMAIL_ALIAS_PASSWORD="abuse-passwort" GPG_SECURITY_KEY="/pfad/zu/key.asc" GPG_SECURITY_PASSPHRASE="pass" LANCEDB_PATH="/pfad/zu/lancedb" node jobs/customer-support-ai/process-inbox.js >> /var/log/process-abuse.log 2>&1
# Posteingang verarbeiten - security@forwardemail.net
*/5 * * * * cd /pfad/zu/forwardemail.net && FORWARD_EMAIL_ALIAS_USERNAME="security@forwardemail.net" FORWARD_EMAIL_ALIAS_PASSWORD="security-passwort" GPG_SECURITY_KEY="/pfad/zu/key.asc" GPG_SECURITY_PASSPHRASE="pass" LANCEDB_PATH="/pfad/zu/lancedb" node jobs/customer-support-ai/process-inbox.js >> /var/log/process-security.log 2>&1
Cron-Zeitplan-Aufschlüsselung
| Job | Zeitplan | Beschreibung |
|---|---|---|
train-from-sitemap.js |
0 0 * * 0 |
Wöchentlich (Sonntag Mitternacht) - Ruft alle URLs aus der Sitemap ab und trainiert die Wissensdatenbank |
train-from-history.js |
0 0 * * * |
Täglich um Mitternacht - Scannt die E-Mails des Vortages pro Posteingang |
process-inbox.js |
*/5 * * * * |
Alle 5 Minuten - Verarbeitet neue E-Mails und erstellt Entwürfe |
Dynamische Datumsberechnung
Die Variable HISTORY_SCAN_SINCE muss inline im Crontab berechnet werden, weil:
.env-Dateien von@ladjs/envals wörtliche Strings gelesen werden- Shell-Befehlssubstitution
$(...)in.env-Dateien nicht funktioniert - Das Datum bei jedem Cron-Lauf frisch berechnet werden muss
Korrekte Vorgehensweise (im Crontab):
# macOS (BSD date)
HISTORY_SCAN_SINCE="$(date -v-1d +%Y-%m-%d)" node jobs/...
# Linux (GNU date)
HISTORY_SCAN_SINCE="$(date -d 'yesterday' +%Y-%m-%d)" node jobs/...
Falsche Vorgehensweise (funktioniert nicht in .env):
# Dies wird als wörtlicher String "$(date -v-1d +%Y-%m-%d)" gelesen
# NICHT als Shell-Befehl ausgewertet
HISTORY_SCAN_SINCE=$(date -v-1d +%Y-%m-%d)
Dies stellt sicher, dass bei jedem nächtlichen Lauf das Datum des Vortages dynamisch berechnet wird und redundante Arbeit vermieden wird.
Erste Einrichtung: URL-Liste aus Sitemap extrahieren
Bevor der Prozess-inbox-Job zum ersten Mal ausgeführt wird, muss die URL-Liste aus der Sitemap extrahiert werden. Dies erstellt ein Wörterbuch gültiger URLs, auf die das LLM zugreifen kann, und verhindert URL-Halluzinationen.
# Ersteinrichtung: URL-Liste aus Sitemap extrahieren
cd /path/to/forwardemail.net
node jobs/customer-support-ai/train-from-sitemap.js
Was das bewirkt:
- Ruft alle URLs von https://forwardemail.net/sitemap.xml ab
- Filtert nur nicht-lokalisierte URLs oder /en/ URLs (vermeidet doppelte Inhalte)
- Entfernt Locale-Präfixe (/en/faq → /faq)
- Speichert eine einfache JSON-Datei mit der URL-Liste unter
$LANCEDB_PATH/valid-urls.json - Kein Crawling, kein Metadaten-Scraping – nur eine flache Liste gültiger URLs
Warum das wichtig ist:
- Verhindert, dass das LLM gefälschte URLs wie
/dashboardoder/loginhalluziniert - Bietet eine Whitelist gültiger URLs, auf die der Antwortgenerator zugreifen kann
- Einfach, schnell und benötigt keine Speicherung in einer Vektordatenbank
- Der Antwortgenerator lädt diese Liste beim Start und bindet sie in den Prompt ein
Zur wöchentlichen Aktualisierung in den Crontab einfügen:
# URL-Liste aus Sitemap extrahieren - wöchentlich Sonntag Mitternacht
0 0 * * 0 cd /path/to/forwardemail.net && node jobs/customer-support-ai/train-from-sitemap.js >> /var/log/train-sitemap.log 2>&1
Cron-Jobs manuell testen
Um einen Job vor dem Hinzufügen zu Cron zu testen:
# Sitemap-Training testen
cd /path/to/forwardemail.net
export LANCEDB_PATH="/path/to/lancedb"
node jobs/customer-support-ai/train-from-sitemap.js
# Support-Inbox-Training testen
cd /path/to/forwardemail.net
export FORWARD_EMAIL_ALIAS_USERNAME="support@forwardemail.net"
export FORWARD_EMAIL_ALIAS_PASSWORD="support-password"
export HISTORY_SCAN_SINCE="$(date -v-1d +%Y-%m-%d)"
export HISTORY_SCAN_LIMIT=1000
export GPG_SECURITY_KEY="/path/to/key.asc"
export GPG_SECURITY_PASSPHRASE="pass"
export LANCEDB_PATH="/path/to/lancedb"
node jobs/customer-support-ai/train-from-history.js
Logs überwachen
Jeder Job schreibt in eine separate Datei für einfaches Debugging:
# Support-Inbox-Verarbeitung in Echtzeit beobachten
tail -f /var/log/process-support.log
# Letzten nächtlichen Trainingslauf prüfen
cat /var/log/train-support.log | grep "$(date -v-1d +%Y-%m-%d)"
# Alle Fehler über alle Jobs anzeigen
grep -i error /var/log/train-*.log /var/log/process-*.log
Tip
Verwenden Sie separate Logdateien pro Posteingang, um Probleme zu isolieren. Wenn ein Posteingang Authentifizierungsprobleme hat, werden die Logs der anderen Posteingänge nicht beeinträchtigt.
Code-Beispiele
Scraping und Verarbeitung
// jobs/customer-support-ai/update-knowledge-base.js
const scraper = new Scraper();
const processor = new Processor();
const ollamaClient = new OllamaClient();
const vectorStore = new VectorStore();
// Alte Daten löschen
await vectorStore.clear();
// Alle Quellen scrapen
const documents = await scraper.scrapeAll();
console.log(`Es wurden ${documents.length} Dokumente gescraped`);
// In Chunks verarbeiten
const allChunks = [];
for (const doc of documents) {
const chunks = processor.processDocuments([doc]);
allChunks.push(...chunks);
}
console.log(`Es wurden ${allChunks.length} Chunks generiert`);
// Embeddings generieren und speichern
const texts = allChunks.map(chunk => chunk.text);
const embeddings = await ollamaClient.generateEmbeddings(texts);
for (let i = 0; i < allChunks.length; i++) {
await vectorStore.addDocument(texts[i], {
...allChunks[i].metadata,
embedding: embeddings[i]
});
}
Training aus historischen E-Mails
// jobs/customer-support-ai/train-from-history.js
const scanner = new EmailScanner({
forwardEmailApiBase: config.forwardEmailApiBase,
forwardEmailAliasUsername: config.forwardEmailAliasUsername,
forwardEmailAliasPassword: config.forwardEmailAliasPassword
});
const vectorStore = new VectorStore({
collectionName: 'customer_support_history'
});
// Alle Ordner scannen (INBOX, Gesendete Mails, etc.)
const messages = await scanner.scanAllFolders({
limit: 1000,
since: new Date('2024-01-01'),
decryptPGP: true
});
// In Konversations-Threads gruppieren
const threads = scanner.groupIntoThreads(messages);
// Jeden Thread verarbeiten
for (const thread of threads) {
const context = scanner.extractConversationContext(thread);
for (const message of context.messages) {
// Verschlüsselte Nachrichten überspringen, die nicht entschlüsselt werden konnten
if (message.encrypted && !message.decrypted) continue;
// Bereits geparsten Inhalt von nodemailer verwenden
const text = message.nodemailer?.text || '';
if (!text.trim()) continue;
// Text in Chunks aufteilen und speichern
const chunks = processor.chunkText(`Betreff: ${message.subject}\n\n${text}`, {
chunkSize: 1000,
chunkOverlap: 200
});
for (const chunk of chunks) {
await vectorStore.addDocument(chunk.text, {
type: 'historical_email',
folder: message.folder,
subject: message.subject,
date: message.nodemailer?.date || message.created_at,
messageId: message.id,
threadId: context.subject,
encrypted: message.encrypted || false,
decrypted: message.decrypted || false,
...chunk.metadata
});
}
}
}
Abfragen für Kontext
// jobs/customer-support-ai/process-inbox.js
const vectorStore = new VectorStore();
const historyVectorStore = new VectorStore({
collectionName: 'customer_support_history'
});
// Beide Stores abfragen
const knowledgeContext = await vectorStore.query(emailEmbedding, { limit: 8 });
const historyContext = await historyVectorStore.query(emailEmbedding, { limit: 3 });
// Gewichtete Rangfolge und Duplikatentfernung finden hier statt
const rankedContext = rankAndDeduplicateContext(knowledgeContext, historyContext);
// Antwort generieren
const response = await responseGenerator.generate(email, rankedContext);
Die Zukunft: Spam Scanner F&E
Dieses ganze Projekt war nicht nur für den Kundensupport. Es war F&E. Wir können jetzt alles, was wir über lokale Embeddings, Vektor-Stores und Kontextabruf gelernt haben, auf unser nächstes großes Projekt anwenden: die LLM-Schicht für Spam Scanner. Dieselben Prinzipien von Datenschutz, Self-Hosting und semantischem Verständnis werden dabei entscheidend sein.
Fehlerbehebung
Fehler bei Vektor-Dimensionen nicht übereinstimmend
Fehler:
Error: Failed to execute query stream: GenericFailure, Invalid input, No vector column found to match with the query vector dimension: 1024
Ursache: Dieser Fehler tritt auf, wenn Sie das Embedding-Modell wechseln (z.B. von mistral-small zu mxbai-embed-large), aber die bestehende LanceDB-Datenbank mit einer anderen Vektor-Dimension erstellt wurde.
Lösung: Sie müssen die Wissensdatenbank mit dem neuen Embedding-Modell neu trainieren:
# 1. Stoppen Sie alle laufenden Customer-Support-AI-Jobs
pkill -f customer-support-ai
# 2. Löschen Sie die bestehende LanceDB-Datenbank
rm -rf ~/.local/share/lancedb/forward_email_knowledge_base.lance
rm -rf ~/.local/share/lancedb/customer_support_history.lance
# 3. Überprüfen Sie, ob das Embedding-Modell korrekt in .env gesetzt ist
grep OLLAMA_EMBEDDING_MODEL .env
# Sollte anzeigen: OLLAMA_EMBEDDING_MODEL=mxbai-embed-large
# 4. Laden Sie das Embedding-Modell in Ollama herunter
ollama pull mxbai-embed-large
# 5. Trainieren Sie die Wissensdatenbank neu
node jobs/customer-support-ai/train-from-history.js
# 6. Starten Sie den process-inbox-Job über Bree neu
# Der Job wird automatisch alle 5 Minuten ausgeführt
Warum das passiert: Verschiedene Embedding-Modelle erzeugen Vektoren mit unterschiedlichen Dimensionen:
mistral-small: 1024 Dimensionenmxbai-embed-large: 1024 Dimensionennomic-embed-text: 768 Dimensionenall-minilm: 384 Dimensionen
LanceDB speichert die Vektor-Dimension im Tabellenschema. Wenn Sie mit einer anderen Dimension abfragen, schlägt es fehl. Die einzige Lösung ist, die Datenbank mit dem neuen Modell neu zu erstellen.
Leerer Wissensdatenbank-Kontext
Symptom:
debug Retrieved knowledge base context {
total: 0,
afterRanking: 0,
questionType: 'capability'
}
Ursache: Die Wissensdatenbank wurde noch nicht trainiert oder die LanceDB-Tabelle existiert nicht.
Lösung: Führen Sie den Trainingsjob aus, um die Wissensdatenbank zu füllen:
# Training aus historischen E-Mails
node jobs/customer-support-ai/train-from-history.js
# Oder Training von Website/Dokumentation (wenn Sie einen Scraper haben)
node jobs/customer-support-ai/train-from-website.js
PGP-Entschlüsselungsfehler
Symptom: Nachrichten werden als verschlüsselt angezeigt, aber der Inhalt ist leer.
Lösung:
- Überprüfen Sie, ob der GPG-Schlüsselpfad korrekt gesetzt ist:
grep GPG_SECURITY_KEY .env
# Sollte auf Ihre private Schlüsseldatei zeigen
- Testen Sie die Entschlüsselung manuell:
node -e "const decrypt = require('./helpers/customer-support-ai/pgp-decrypt'); decrypt.testDecryption();"
- Prüfen Sie die Berechtigungen des Schlüssels:
ls -la /path/to/your/gpg-key.asc
# Sollte für den Benutzer, der den Job ausführt, lesbar sein
Nutzungstipps
Erreichen von Inbox Zero
Das System ist darauf ausgelegt, Ihnen automatisch zu helfen, Inbox Zero zu erreichen:
-
Automatisches Archivieren: Wenn ein Entwurf erfolgreich erstellt wurde, wird die Originalnachricht automatisch in den Archiv-Ordner verschoben. So bleibt Ihr Posteingang sauber, ohne manuelles Eingreifen.
-
Entwürfe prüfen: Überprüfen Sie regelmäßig den Entwurfsordner, um KI-generierte Antworten zu kontrollieren. Bearbeiten Sie diese bei Bedarf vor dem Versand.
-
Manuelle Ausnahme: Für Nachrichten, die besondere Aufmerksamkeit benötigen, fügen Sie einfach vor dem Joblauf das Label
skip-aihinzu.
Verwendung des skip-ai-Labels
Um die KI-Verarbeitung für bestimmte Nachrichten zu verhindern:
- Label hinzufügen: Fügen Sie in Ihrem E-Mail-Client einer Nachricht das Label/Tag
skip-aihinzu (Groß-/Kleinschreibung egal) - Nachricht bleibt im Posteingang: Die Nachricht wird nicht verarbeitet oder archiviert
- Manuelle Bearbeitung: Sie können selbst darauf antworten, ohne KI-Interferenz
Wann skip-ai verwenden:
- Sensible oder vertrauliche Nachrichten
- Komplexe Fälle, die menschliches Urteilsvermögen erfordern
- Nachrichten von VIP-Kunden
- Rechtliche oder Compliance-bezogene Anfragen
- Nachrichten, die sofortige menschliche Aufmerksamkeit benötigen
E-Mail-Threading und Allen-Antworten
Das System folgt den Standard-E-Mail-Konventionen:
Zitierte Originalnachrichten:
Hallo,
[KI-generierte Antwort]
--
Vielen Dank,
Forward Email
https://forwardemail.net
Am Mo., 15. Jan. 2024, 15:45 Uhr schrieb John Doe <john@example.com>:
> Dies ist die Originalnachricht
> mit jeder Zeile zitiert
> unter Verwendung des Standard-Präfix "> "
Reply-To-Verarbeitung:
- Wenn die Originalnachricht einen Reply-To-Header hat, antwortet der Entwurf an diese Adresse
- Die ursprüngliche From-Adresse wird in CC hinzugefügt
- Alle anderen ursprünglichen To- und CC-Empfänger bleiben erhalten
Beispiel:
Originalnachricht:
Von: john@company.com
Reply-To: support@company.com
An: support@forwardemail.net
CC: manager@company.com
Entwurfsantwort:
An: support@company.com (aus Reply-To)
CC: john@company.com, manager@company.com
Überwachung und Wartung
Überprüfen Sie regelmäßig die Entwurfsqualität:
# Aktuelle Entwürfe anzeigen
tail -f /var/log/process-support.log | grep "Draft created"
Archivierung überwachen:
# Nach Archivierungsfehlern suchen
grep "archive message" /var/log/process-*.log
Übersprungene Nachrichten überprüfen:
# Sehen, welche Nachrichten übersprungen wurden
grep "skip-ai label" /var/log/process-*.log
Tests
Das Kunden-Support-AI-System umfasst eine umfassende Testabdeckung mit 23 Ava-Tests.
Tests ausführen
Aufgrund von npm-Paketüberschneidungen mit better-sqlite3 verwenden Sie das bereitgestellte Testskript:
# Alle Kunden-Support-AI-Tests ausführen
./scripts/test-customer-support-ai.sh
# Mit ausführlicher Ausgabe ausführen
./scripts/test-customer-support-ai.sh --verbose
# Spezifische Testdatei ausführen
./scripts/test-customer-support-ai.sh test/customer-support-ai/message-utils.js
Alternativ können Tests direkt ausgeführt werden:
NODE_ENV=test node node_modules/.pnpm/ava@5.3.1/node_modules/ava/entrypoints/cli.mjs test/customer-support-ai
Testabdeckung
Sitemap Fetcher (6 Tests):
- Regex-Muster für Locale-Erkennung
- URL-Pfadextraktion und Locale-Entfernung
- URL-Filterlogik für Locales
- XML-Parsing-Logik
- Duplizierungsvermeidung
- Kombinierte Filterung, Entfernung und Duplizierung
Message Utils (9 Tests):
- Absendertext mit Name und E-Mail extrahieren
- Nur E-Mail verwenden, wenn Name Präfix entspricht
from.textverwenden, falls vorhandenReply-Toverwenden, wenn vorhandenFromverwenden, wenn keinReply-To- Originale CC-Empfänger einschließen
- Eigene Adresse aus CC ausschließen
Reply-TomitFromin CC behandeln- CC-Adressen deduplizieren
Response Generator (8 Tests):
- URL-Gruppierungslogik für Prompt
- Erkennung des Absendernamens
- Prompt-Struktur enthält alle erforderlichen Abschnitte
- URL-Liste ohne spitze Klammern formatieren
- Umgang mit leerer URL-Liste
- Liste verbotener URLs im Prompt
- Einbeziehung des historischen Kontexts
- Korrekte URLs für konto-bezogene Themen
Testumgebung
Tests verwenden .env.test für die Konfiguration. Die Testumgebung umfasst:
- Mock-PayPal- und Stripe-Zugangsdaten
- Test-Verschlüsselungsschlüssel
- Deaktivierte Authentifizierungsanbieter
- Sichere Testdatenpfade
Alle Tests sind so konzipiert, dass sie ohne externe Abhängigkeiten oder Netzwerkaufrufe laufen.
Wichtige Erkenntnisse
- Datenschutz zuerst: Selbsthosting ist für DSGVO/DPA-Konformität unverzichtbar.
- Kosten sind entscheidend: Cloud-AI-Dienste sind 50-1000x teurer als Selbsthosting für Produktionslasten.
- Das Ökosystem ist fehlerhaft: Die meisten Vektor-Datenbanken sind nicht entwicklerfreundlich. Testen Sie alles lokal.
- Sicherheitslücken sind real: ChromaDB und Qdrant hatten kritische RCE-Sicherheitslücken.
- LanceDB funktioniert: Es ist eingebettet, serverlos und benötigt keinen separaten Prozess.
- Ollama ist solide: Lokale LLM-Inferenz mit
mxbai-embed-largefunktioniert gut für unseren Anwendungsfall. - Typinkompatibilitäten sind fatal:
textvs.content, ObjectID vs. string. Diese Fehler sind still und brutal. - Gewichtete Rangfolge ist wichtig: Nicht jeder Kontext ist gleichwertig. FAQ > GitHub-Issues > Historische E-Mails.
- Historischer Kontext ist Gold wert: Training mit vergangenen Support-E-Mails verbessert die Antwortqualität erheblich.
- PGP-Entschlüsselung ist essenziell: Viele Support-E-Mails sind verschlüsselt; korrekte Entschlüsselung ist für das Training entscheidend.
Erfahren Sie mehr über Forward Email und unseren datenschutzorientierten Ansatz für E-Mail unter forwardemail.net.