Alle Beiträge

RAG an Confluence, Teams und Slack anschliessen — ohne daraus eine Plattform zu machen

Ein RAG-System wird dort nützlich, wo Fragen ohnehin passieren: im Chat. Drei Konnektoren, jeder ein dünner Adapter, jeder standardmässig aus, jeder mit Signaturprüfung.

Kurz beantwortet

Teil 3 der RAG-Tutorial-Serie ergänzt drei optionale Konnektoren: Confluence-Ingestion (Seite oder ganzer Space), einen Microsoft-Teams-Outgoing-Webhook und einen Slack-Events-API-Bot. Drei Regeln verhindern, dass daraus eine Plattform wird: Jeder Konnektor ist per @ConditionalOnProperty standardmässig aus (unkonfiguriert heisst: die Beans existieren nicht, die Endpunkte antworten 404), jede Webhook-Signatur wird über die rohen Request-Bytes geprüft, bevor JSON geparst wird, und jeder Konnektor ist ein dünner Adapter in dieselbe Ingestion-Pipeline. Bonus-Muster: Beantwortete Chat-Q&A-Paare werden zurück in den Vektorspeicher embedded — der Bot kann seine früheren Antworten wiederfinden.

Ein RAG-System auf einem Dashboard ist eine Demo. Nützlich wird es dort, wo Fragen ohnehin passieren — im Team-Chat — und wo die Dokumente ohnehin liegen — im Wiki. Teil 3 meiner RAG-Tutorial-Serie schliesst die Pipeline aus Teil 1 und Teil 2 an Confluence, Microsoft Teams und Slack an. Der interessante Teil ist keine der einzelnen Integrationen. Es sind die drei Regeln, die verhindern, dass drei Integrationen aus einer kleinen Codebasis eine Plattform machen.

Regel eins: standardmässig aus — und „aus" heisst weg

Jeder Konnektor aktiviert sich erst, wenn seine Schlüssel-Property gesetzt ist — rag.confluence.base-url, rag.teams.hmac-secret, rag.slack.signing-secret. Nicht „deaktiviert, aber vorhanden": Die Beans tragen @ConditionalOnProperty, ein unkonfigurierter Konnektor trägt also nichts zum Application Context bei. Seine Endpunkte antworten 404. Es gibt keinen halbkonfigurierten Client, der auf seinen Fehler wartet, keinen toten Codepfad, der abgesichert werden müsste.

Ich habe das auf die stumpfe Art verifiziert — App ohne Konnektor-Konfiguration gestartet und alle drei Endpunkte gecurlt: 404, 404, 404. Eine Umgebungsvariable setzen, neu starten, und der Slack-Endpunkt existiert, prüft Signaturen und antwortet. Feature-Flags auf Bean-Ebene sind Springs unterschätztestes Deployment-Werkzeug: Dasselbe Artefakt läuft als nackte RAG-API oder als voll verdrahteter Chat-Bot — entschieden durch die Umgebung.

Regel zwei: Signaturen über die rohen Bytes prüfen — vor dem Parsen

Beide Chat-Plattformen signieren ihre Webhook-Aufrufe. Teams schickt Authorization: HMAC <base64> — ein HMAC-SHA256 über den Request-Body mit dem Security-Token des Webhooks. Slack signiert v0:<timestamp>:<roher Body> mit dem Signing Secret und schickt den Hex-Digest in X-Slack-Signature, plus einen Timestamp, den man begrenzen muss (~5 Minuten) — gegen Replays.

Der Fehler, der Leute einen Nachmittag kostet: das Framework zuerst deserialisieren lassen und eine re-serialisierte Version prüfen. Key-Reihenfolge und Whitespace verschieben sich, und jede Signatur passt nicht mehr. Die Controller nehmen den Body deshalb als rohen String, prüfen über genau diese Bytes und parsen erst danach:

@PostMapping("/slack/events")
public ResponseEntity<?> onEvent(@RequestHeader("X-Slack-Request-Timestamp") String ts,
                                 @RequestHeader("X-Slack-Signature") String sig,
                                 @RequestBody String rawBody) {
    if (!verifier.verify(ts, sig, rawBody)) {
        return ResponseEntity.status(401).build();
    }
    // ...jetzt parsen
}

Digests konstantzeitig vergleichen (MessageDigest.isEqual), nicht mit String.equals. Und ehrlich testen: Ich habe simulierte Requests mit openssl signiert und alle drei Pfade geprüft — gültige Signatur akzeptiert, falsche Signatur 401, abgelaufener Timestamp 401. Diese curl-Schnipsel stehen im README des Repos, denn ein Webhook, den man ohne die echte Plattform nicht testen kann, ist ein Webhook, den man in Produktion debuggt.

Regel drei: Konnektoren sind Adapter, keine Features

Alle drei Konnektoren münden in dieselbe Methode, die der Datei-Upload seit Teil 1 benutzt: Text extrahieren, chunken, embedden, speichern. Confluence holt eine Seite (oder paginiert einen ganzen Space), flacht das Storage-Format-HTML mit Jsoup ab und übergibt es dieser Pipeline. Die Chat-Bots gehen die Gegenrichtung — Frage rein, RAG-Antwort raus — und jeder ist eine dünne Hülle um denselben RagService.

Die Plattformen unterscheiden sich da, wo man es erwartet: Teams-Outgoing-Webhooks wollen eine synchrone JSON-Antwort; Slack wiederholt alles, was nicht binnen drei Sekunden bestätigt ist — also bestätigt der Controller sofort, und ein @Async-Handler führt den RAG-Loop aus und postet die Antwort per chat.postMessage in einen Thread. Wer diesen Split bei Slack falsch macht, bekommt jede Antwort dreimal — das Retry-Verhalten lässt langsame Controller wie Duplikat-Bugs aussehen.

Das Muster zum Stehlen: Der Bot lernt aus seinen eigenen Antworten

Jedes beantwortete Q&A-Paar — aus Teams oder Slack — wird asynchron als kleines „Frage: … / Antwort: …"-Dokument zurück in den Vektorspeicher embedded. Die Konsequenz: Wer heute in Slack fragt, was ein Kollege letzten Monat in Teams gefragt hat, dem kann das Retrieval diesen früheren Austausch als Kontext liefern. Wissen, das nur als Chat-Nachricht existierte, wird durchsuchbar.

Es schneidet in beide Richtungen, und ich sage das lieber, als das Muster zu verkaufen: Eine falsche Antwort wird genauso re-embedded. In einem Produktionssystem käme ein Feedback-Signal vor die Persistierung. Im Tutorial ist der Trade-off sichtbar und diskutiert — das ist mehr wert, als ihn wegzulassen.

Der Takeaway

Integrationen müssen deinen Kern nicht wachsen lassen. Drei externe Systeme sind zu dieser Codebasis dazugekommen, und die RAG-Pipeline hat sich nicht geändert — weil jeder Konnektor denselben drei Regeln folgt: weg, wenn unkonfiguriert; Signaturen über rohe Bytes; dünner Adapter in eine Pipeline. Diese Disziplin solltest du von jedem „wir bauen halt noch einen Slack-Bot dazu"-Ticket verlangen.

Teil 4 verpackt alles für Produktion: Docker-Images für Backend und Frontend und ein Helm-Chart, in dem der komplett offline laufende Stack — lokales Modell, eigene Datenbank — ein helm install entfernt ist.

Code: github.com/halviclabs/rag-tutorials — Teil 3 ist rag-tutorial-03-connectors, inklusive Setup-Anleitungen für Teams-Webhook, Slack-App und Confluence-Tokens plus einer Symptom→Ursache→Fix-Troubleshooting-Tabelle.

Häufige Fragen

Wie prüfe ich Slack-Request-Signaturen korrekt?

HMAC-SHA256 mit dem Signing Secret über den String v0:<timestamp>:<roher Body> berechnen und den Hex-Digest mit dem X-Slack-Signature-Header vergleichen — mit konstantzeitigem Vergleich, und nachdem Timestamps, die älter als rund fünf Minuten sind, als Replay-Schutz abgelehnt wurden. Das kritische Detail: über die rohen Body-Bytes signieren, exakt wie empfangen — deserialisiert das Framework zuerst und man re-serialisiert, verschieben sich Key-Reihenfolge und Whitespace, und jede Prüfung schlägt fehl.

Warum muss ein Slack-Bot asynchron antworten?

Slack wiederholt jedes Event, das nicht binnen drei Sekunden bestätigt wird — ein LLM-Aufruf plus Retrieval passt nie in dieses Budget. Der Controller prüft die Signatur, gibt sofort 200 zurück und übergibt das Event einem asynchronen Handler, der den RAG-Loop ausführt und die Antwort per chat.postMessage in einen Thread postet. Teams-Outgoing-Webhooks funktionieren andersherum: Sie erwarten eine synchrone JSON-Antwort, dort läuft der RAG-Aufruf inline.

Wie mache ich Spring-Beans abhängig von Konfiguration optional?

Die Beans des Konnektors mit @ConditionalOnProperty auf seine Schlüssel-Property annotieren — etwa rag.slack.signing-secret. Ohne die Property instanziiert Spring weder Client noch Service noch Controller: Die Endpunkte antworten 404, nichts Halbkonfiguriertes läuft. Eine einzige Umgebungsvariable aktiviert den ganzen Konnektor.

Was bringt das Re-Embedding von Chat-Q&A-Paaren in einem RAG-System?

Jede beantwortete Frage wird asynchron als Frage-Antwort-Dokument zurück in den Vektorspeicher geschrieben. Künftige Fragen — aus dem Chat oder dem Dashboard — können frühere Antworten als Kontext wiederfinden; Wissen, das nur als Chat-Nachricht existierte, wird durchsuchbar. Es schneidet in beide Richtungen: Auch falsche Antworten werden re-embedded — Antwortqualität zählt doppelt.

AI-Code ohne Tech-Schuld — die Checkliste

Trag dich ein: die Checkliste plus neue Beiträge zu KI-Engineering. Kein Spam, jederzeit abbestellbar.