Zum Inhalt springen

KI-Pipeline

Dieses Dokument beschreibt die KI-gestützten Schritte rund um das Ratsinformationssystem (Oldenburger Stadtrat): Ausschuss-Zusammenfassungen, Themen-Matching (Watcher) und die Beschluss-Aufbereitung. Alle LLM-Aufrufe laufen über OpenRouter (siehe ADR 0001).

Die Pipeline-Qualität entscheidet über den Produktwert: Sie bestimmt, wie zuverlässig relevante Tagesordnungspunkte und Beschlüsse zu den Themen der Nutzer gefunden werden — bei hoher Precision (keine Fehltreffer) und brauchbarem Recall.

Wichtig vorab: Das Scraping selbst nutzt kein LLM. Tagesordnungen und Protokolle werden per BeautifulSoup bzw. pypdf strukturiert eingelesen; das LLM kommt erst beim Zusammenfassen, Matchen und Klassifizieren zum Einsatz. Vorlagen und Anlagen werden sogar komplett ohne LLM ausgewertet.

flowchart TD
  subgraph Quellen["Gescrapte Quellen — ohne LLM eingelesen"]
      AG["Tagesordnungen"]
      PR["Sitzungsprotokolle (PDF)"]
      VO["Vorlagen & Anlagen (PDF)"]
  end

  AG --> ZSF["LLM: Sitzungs-Zusammenfassung
  je Ausschuss"]
  AG --> MATCH["LLM: Matching gegen
  Nutzer-Themen"]
  PR --> EXTR["LLM: Beschluss-Extraktion
  (ein Aufruf je Protokoll)"]
  EXTR --> KLASS["LLM: Themenfeld-
  Klassifikation"]
  KLASS --> ENRICH["LLM & Embeddings (wöchentlich):
  Entitäten · Geocoding · Ähnliche · Rückblicke"]
  VO -->|"pypdf + Wortlisten,
  ganz ohne LLM"| TEXTE["Volltexte &
  Antragsteller"]

  ZSF --> N["Benachrichtigungen
  E-Mail · Push"]
  MATCH --> N
  KLASS --> F1["Filter · Analyse ·
  Karte · Trends"]
  TEXTE --> F2["Beschluss-Seiten ·
  Suche · KI-Frage"]
Pipeline Quelle Rhythmus
Ausschuss-Zusammenfassung Tagesordnungen täglich
Watcher (Themen-Matching) Tagesordnungen mehrmals täglich
Protokoll-Extraktion + Beschluss-Klassifikation Sitzungsprotokolle täglich
„Lotti erklärt’s einfach“ (bürgernahe Kurzfassung) Beschlusstext täglich (neue), wöchentlich (Bestand)
Bewertungs-Scores (Tragweite, Gesprächswert) Beschlusstext täglich (neue), wöchentlich (Tranchen)
Fundstück des Tages (kuratierte Archivfunde) bewertete Beschlüsse wöchentlich, 21 Tage Vorlauf
Anreicherung (Entitäten, Embeddings, Rückblicke) Beschlüsse wöchentlich
Quiz-Fragen (Generierung + Verify-Pass) Wikipedia · Stadt-Website · Ratsdaten Backfill (manuell + wöchentlich)
KI-Frage „Frag den Rat“ (Ratsgespräch) Beschlüsse + Vorlagen-Chunks + Stadt-Pressemitteilungen + laufende Bauleitplan-Beteiligungen; Anschlussfragen mit Gesprächskontext auf Zuruf (Streaming)

Tragweite, Gesprächswert und der daraus gemischte Wichtigkeits-Wert haben eine eigene Seite: Bewertungs-Scores.

  • Ausschussliste aktualisieren, kommende Sitzungen (3 Monate voraus) scannen.
  • Pro Sitzung mit Tagesordnung wird ein Hash der Agenda gebildet — er dient als Cache-Schlüssel und zur Änderungserkennung (neue vs. geänderte Tagesordnung).
  • Routine-TOPs („Genehmigung der Niederschrift“, Fragestunde …) filtert bereits der Code vor, erst dann fasst das LLM zusammen (JSON-Mode). Das Ergebnis wird je Agenda-Stand gecacht — dieselbe Tagesordnung wird nie zweimal zusammengefasst.
  • Pro Nutzer werden nur zukünftige Sitzungen mit Tagesordnung klassifiziert, die dieser Nutzer noch nicht gesehen hat — ändert sich eine Tagesordnung, wird erneut geprüft und benachrichtigt.
  • Das LLM bekommt die öffentlichen TOPs und die nummerierte Themenliste des Nutzers und liefert die Treffer-Paare als JSON. Alerts werden dedupliziert.
  • Der Prompt liegt zentral in der Prompt-Verwaltung und ist über das Admin-UI editierbar.

Neu veröffentlichte Sitzungsprotokolle werden geparst, Beschlüsse extrahiert (ein LLM-Aufruf je Protokoll) und je Beschluss per LLM in Themenfelder eingeordnet — die Grundlage für Filter, Analyse und die Themenfeld-Rückblicke. Details zu den Dokumenten und dem Datenmodell: Ratsdokumente & Beschlüsse.

Amtsdeutsch ist die größte Hürde vor den Ratsdaten. Deshalb schreibt council/simple_summary.py je Beschluss zwei bis drei bürgernahe Sätze in die Spalte simple_summary — was beschlossen wurde und was es praktisch bedeutet, ohne Fachbegriffe.

Zwei Einschränkungen sind bewusst gesetzt: Nur echte Beschlüsse (kind='decision') mit einem hinreichend langen Beschlusstext bekommen eine Kurzfassung — bei einer Zwei-Zeilen-Kenntnisnahme gäbe es nichts zu vereinfachen, und eine erfundene Erklärung wäre schlimmer als keine. Neue Beschlüsse werden im täglichen Protokoll-Lauf bedient, der Bestand wandert in wöchentlichen Tranchen nach.

Zieht die schwereren LLM-/Embedding-Backfills nach, damit Themen-Seiten, Karte und „Ähnliche Beschlüsse“ frisch bleiben: Entitäten-Extraktion → Beschreibungen → Geocoding → Embeddings/Ähnliche → Themen↔Beschlüsse-Matching → Themenfeld-Rückblicke.

Bestätigte Themen-Zusammenführungen (siehe unten) wendet der Entitäten-Schritt dabei automatisch mit an. Die Suche nach neuen Dubletten läuft bewusst nicht wöchentlich mit, sondern nur auf Anstoß („Ops: Themen-Dubletten zusammenführen“) — eine Zusammenführung sollte man sehen, bevor sie wirkt.

Die Entitäten-Extraktion entscheidet je Beschluss-Batch neu, wie sie eine Sache benennt, und kennt den übrigen Bestand nicht. Über Jahre entstehen so mehrere Themen-Seiten für denselben Gegenstand — gemessen am Bestand vom 23.07.2026: 23 Gruppen mit 50 Namen, darunter vier für den Bäderbetrieb und drei für die Gebäudewirtschaft. Beschlüsse und aufsummierte Beträge verteilen sich dann auf mehrere Seiten.

scripts/merge_entity_aliases.py räumt das auf. Erkennungsmerkmal ist kontraintuitiv, aber zuverlässig: echte Dubletten haben null gemeinsame Beschlüsse bei sehr hoher inhaltlicher Nähe. Das folgt direkt aus der Ursache — nennt die Extraktion in einem Beschluss „Bäderbetrieb Oldenburg“, nennt sie dort nicht zusätzlich „Bäderbetrieb der Stadt Oldenburg“.

Der Lauf sammelt Kandidaten über Namensnormalisierung (Rechtsform, Ortszusatz, vorangestellte Gattung, Abkürzung, Teilstring) und legt jedes Paar einzeln dem LLM vor — mit Beschlusstiteln als Beleg. Nötig, weil Namensähnlichkeit allein nicht reicht: „IBIS“/„IBIS e.V.“ ist eine Sache, „Fliegerhorst“/„Grundschule Fliegerhorst“ sind zwei. Bei Abkürzungen erzwingt der Code die Langform als Hauptnamen; wer „VWG“ sucht, landet über die Zuordnung trotzdem richtig.

Die Zusammenführung ist umkehrbar: Geschrieben wird nur die Zuordnungstabelle council_entity_aliases. Die Roh-Beobachtungen bleiben unangetastet, und die Themen werden bei jedem Lauf neu daraus abgeleitet — eine Zuordnung löschen (Admin-Panel → „Themen-Dubletten“) stellt den vorherigen Stand her. Von Hand gesetzte Zuordnungen überschreibt ein automatischer Lauf nie.

Das Oldenburg-Quiz erzeugt seine Multiple-Choice-Fragen per LLM, aber streng geerdet. Pro Gebiet (Stadtteil, Wahlbereich oder stadtweites Thema) sammelt der Code zuerst Quelltexte — Wikipedia (Stadtteil-Artikel), die Stadt-Website oldenburg.de und die eigenen Ratsdaten — und legt sie dem LLM vor. Das Modell formuliert daraus Fragen mit vier Antwortoptionen, Erklärung, Kategorie und Quellenangabe; es erfindet nichts frei.

Ein zweiter, günstiger Verify-Pass (kleines Modell) verwirft Fragen, die nicht eindeutig aus der Quelle belegbar sind. Antwortoptionen werden gemischt (die richtige Antwort liegt sonst gern auf Position A), Dubletten fängt ein content_hash. Zusätzlich bewerten Nutzer:innen jede Frage nach dem Beantworten (👍/👎) — schlecht bewertete erscheinen in einer Admin-Liste und werden ausgemustert; da der Backfill nur Gebiete unter der Ziel-Fragenzahl auffüllt, erzeugt der nächste Lauf automatisch Ersatz.

Die Fragen liegen in council.sqlite (generierter, regenerierbarer Inhalt), Punkte und Bewertungen in nwz.sqlite (pro Konto). Der Backfill (scripts/generate_quiz.py) läuft als Auffüll-Schritt der wöchentlichen Anreicherung und lässt sich für den Erstlauf manuell über einen Ops-Workflow anstoßen.

Das Karten-Quiz („Wo liegt Stadtteil X?“) ist die Ausnahme: rein geografisch, ganz ohne LLM — die Fragen entstehen deterministisch aus den Stadtteil-Polygonen, die Karte selbst ist die Antwortfläche.

Auch die Haushalts-Fragen (Thema „Stadt-Haushalt“) entstehen ohne LLM: council/haushalt.py parst die „Übersicht Ergebnishaushalt“ aus den beschlossenen Haushaltsplan-PDFs der Stadt (oldenburg.de, aktuelles Jahr plus Archiv ab 2020; das 2024er-PDF hat eine defekte Text-Kodierung und wird übersprungen), validiert die Teilhaushalts-Zeilen gegen die Summenzeile und baut daraus Schätz- und Multiple-Choice-Fragen per Template — jede Zahl stammt 1:1 aus dem Plan, jede Frage verlinkt das PDF als Quelle. Die Auflösung liefert je nach Frage ein Balkendiagramm (Aufwendungen/Erträge je Teilhaushalt), einen Donut (Anteil an den Gesamtausgaben) oder eine Trendlinie über die Haushaltsjahre mit (JSON in der Frage, gerendert im Frontend) und erklärt die Zusammensetzung (Pflichtaufgaben vs. freiwillige Leistungen — beides im Glossar). Eingelesen wird einmal je Haushaltsjahr (scripts/ingest_haushalt.py); ein Re-Ingest frischt bestehende Fragen auf (z. B. verlängert er die Trendlinie um neue Jahre).

Reichere Antworten: Zu jeder Frage kann die Auflösung optional eine ausführlichere Erklärung, eine kleine Locator-Karte und ein Foto zeigen. Das LLM nennt dazu ein „subject“ (das zentrale reale Ding, z. B. „Schloss Oldenburg“), das Backend reichert best-effort an: Koordinaten per Nominatim (auf Oldenburg begrenzt) und ein Foto vom Wikimedia Commons — über das Lead-Bild des Wikipedia-Artikels. Genutzt werden nur frei lizenzierte Bilder (CC/PD; Fair-Use fliegt raus), immer mit Bildnachweis (Autor, Lizenz, Link) direkt aus der Commons-API.

Bei der Karte hat Verlässlichkeit Vorrang, in absteigender Genauigkeit: Für eindeutige, kompakte Straßen zeichnet das Backend die echte Linie (OpenStreetMap/Overpass), aber nur wenn die Geometrie klein genug ist — verstreute oder mehrfach vergebene Straßennamen (mehrere „Mittelwege“) werden verworfen, damit nie eine falsche Stelle markiert wird. Einen Punkt-Marker gibt es sonst nur für bekannte Einzelorte (die einen Wikipedia-Artikel haben). Und geht es um einen ganzen Stadtteil (oder eine Person/Sache von dort), zeichnet die Karte das ganze Gebiet als Polygon ein — die Stadtteil-Grenzen besitzen wir selbst, das ist also immer verlässlich. Bei Fragen zu einer konkreten Person oder Sache verweist der Quelle-Link außerdem auf deren eigenen Wikipedia-Artikel statt auf die Gebiets-Seite. Zu kniffligen Fragen kann es einen optionalen Tipp geben (vor dem Auflösen einblendbar), und Fachbegriffe in der Erklärung (z. B. „Vergnügungsstätte“, „Bebauungsplan“) unterlegt das Frontend mit einem Hover-Glossar.

Aspekt Status
Provider OpenRouter via openai-SDK, mit DSGVO-Provider-Routing (siehe ADR 0002)
Structured Output JSON-Mode (response_format={"type":"json_object"})
Caching Ausschuss-Zusammenfassungen per Agenda-Hash
Prompts zentral verwaltet, admin-editierbar (DB-Overrides über Code-Defaults)
Modelle je Aufgabe per Env-Variable wählbar, Defaults greifen

Das Eval-Harness (Details) misst die Qualität der beiden Matching-Pipelines gegen handgelabelte Fälle:

  • watcher — Tagesordnung → Thema (mengenbasiertes Precision/Recall/F1- Scoring über (Thema, TOP)-Paare).
  • committee — der Routine-Filter der Ausschuss-Zusammenfassungen.

Ein Runner liefert das Scoreboard; gespeicherte Baselines machen Prompt- und Modell-Änderungen vergleichbar, statt „gefühlt“ besser zu sein.