Blog

Kundenerfolge und Neues direkt aus der Werkstatt

Entdecken Sie in diesen Fallstudien von info/matik, wie wir unseren Kunden das Leben erleichtert haben, und was uns und die Technikwelt gerade begeistert.

KLAUS der KI Assistent: Mit Daten sprechen - EF Core, Semantic Kernel und OpenAI

Vorschaubild

Alles was du über KLAUS, deinem neuen KI Assistenten für Unternehmensdaten, wissen musst.

Intro

In jedem Unternehmen sind Geschäftsdaten die Grundlage einer jeden fundierten Entscheidung. Doch damit diese Ihren Zweck erfüllen können müssen drei Vorraussetzungen erfüllt sein:

  1. Sie müssen verfügbar sein.
  2. Sie müssen verständlich sein.
  3. Sie müssen aktuell sein.

SQL-Abfrage

Häufig liegen diese Daten in SQL-Datenbanken und es gibt Leistungsstarke Tools wie Microsoft PowerBI, Salesforce Tableau oder auch das Google Data Studio. Hier zeigt sich jedoch ein zentrales Problem. Diese Tools werden von zwei unterschiedlichen Gruppen genutzt. Während die Anwender meist die Entscheidungsträger sind, werden die Auswertungen von Entwicklern konfiguriert und umgesetzt.

An dieser Schnittstelle entsteht Reibung. Die Kommunikation der beiden Gruppen ist nicht immer fehlerfrei. Wie bei jeder Art von Zusammenarbeit entstehen bei dem Erstellen und der Umsetzung von Anforderungen Fehler. Doch selbst wenn Anforderungen korrekt aufgestellt und umgesetzt werden, geht dabei oft wertvolle Zeit verloren.

Viele Auswertungen werden auch nur einmals benötigt, dabei lohnt es sich nicht große Auswertungen zu beauftragen

Wir bei info/matik sind von dieser Problematik nicht ausgenommen. Unsere Erfahrung zeigt: Erkenntnisse aus Auswertungen führen zu neuen Auswertungen, es gibt immer noch eine letzte Ansicht welche der Kunde wünscht. Der Umfang ist endlos.

Das ist wo KLAUS ins Spiel kommt, aber bevor wir näher auf ihn eingehen nehmen wir euch mit auf eine Reise, wie er entwickelt wurde.

Neue Möglichkeiten durch LLMs

Das eigentliche Problem liegt in der Übersetzung der Semantik von menschlicher Sprache in einer Fachdomain in SQL-Abfragen und deren Auswertung. In vielen Bereichen werden in dieser Domäne bereits LLMs verwendet. Warum nicht auch hier?

Wäre es Möglich eine SQL-Datenbank mit einem LLM zu verbinden? Dann wäre es Möglich direkt Fragen an die eigenen Daten zu schicken.

Mit dieser Fragestellung machten wir uns Sommer 2025 daran einen Prototypen zu entwickeln. Unsere SQL-Server Datenbank eines ERP-Systems haben wir mittels lokalen MCP an Claude Desktop angebunden. Für alle Nicht-Techniker unter uns: Claude kann damit auf die lokale Datenbank zugreifen.

Die Ergebnisse waren: Verblüffend! Claude schrieb T-SQL Abfragen und lieferte plausible Ergebnisse. Dabei fragte er selbstständig das Schema ab, und änderte im Fehlerfall die Abfrage, bis diese gültig war.

Es gab jedoch einige Probleme:

  • Die Metadaten der Datenbank (Namen der Tabellen und Spalten) sind als Erklärung unzureichend.
  • Die Geschwindigkeit ist nicht sehr hoch.
  • Einige Datentypen benötigen eine Erklärung
  • Jeder Benutzer kann auf alle Daten zugreifen
  • Abhängigkeit von Claude und damit der Cloud

Entwicklung einer Lösung

Diese Probleme schienen aber lösbar. Wir bei info/matik setzen voll auf das .NET Framework, und verwenden zur Datenerhaltung meistens EF Core. Wir wollten eine Lösung entwickeln, die sich nahtlos in unsere bestehenden Produkte einfügt.

Dazu haben wir einen ChatBot konzipiert, der die Sprache des Nutzers und die Fachdomäne versteht. Er generiert selbstständig gültige T-SQL-Abfragen, führt diese aus und stellt die Ergebnisse in natürlicher Sprache sowie als Diagramme dar. Ein zentraler Bestandteil des Projekts war die Evaluation, ob und welche lokalen Modelle sich dafür eignen.

LLM-Modelle

LLM-Modelle

Getestete Modelle: Cloud vs. lokal Es wurden verschiedene Modelle – sowohl aus der Cloud als auch lokal – getestet. Voraussetzung war, dass die Modelle Tools ausführen können. Als Hardware für die lokalen Tests diente ein HP zBook Ultra G1a mit einem AMD Ryzen AI MAX+ 395 und 128 GB RAM (bis zu 96 GB als VRAM nutzbar). Die lokalen Modelle wurden in LM-Studio betrieben. Alle Modelle (Cloud und lokal) wurden über eine OpenAI-kompatible API angesprochen. Die folgende Tabelle gibt einen Überblick über die getesteten Modelle und ihre Ergebnisse:

Hersteller Modell Ergebnisse
Cloud
OpenAI GPT 5 *****
OpenAI GPT 5mini ***
Mistral Mistral Large ****
Lokal
OpenAI OSS 20B ***
OpenAI OSS 120B **
Mistral 7B ***
Google Gemma 3 kA

Fazit:

Die Cloud-Modelle lieferten durchgehend sehr gute Ergebnisse – was erwartet war. Auch lokal zeigten sich einige Modelle wie das OSS 20B von OpenAI oder das Mistral 7B als gut einsetzbar. Das OSS 120B von OpenAI wirkte zwar vielversprechend, ließ sich jedoch aufgrund von Performance-Einschränkungen (Geschwindigkeit, Kontextfenster) nicht optimal auf der Hardware betreiben.

Google Gemma 3 konnte lokal nicht genutzt werden, da es nicht für die Tool-Verwendung (bzw. die spezifische Anbindung) trainiert war.

KLAUS

LLM-Modelle

Aus diesen Vorarbeiten entstand KLAUS - KI-LLM-Assistent für Unternehmens-Statistiken.

Ein Chatbot, welcher aus Fragen Ergebnisse und Diagramme ableiten kann.

Metadata

LLM-Modelle

Damit ein LLM hochwertige Auswertungen erstellen kann, benötigt es zusätzliche Metadaten. Da EF Core bereits C#-Klassen (POCOs) nutzt, um Datenbanktabellen abzubilden, bietet es sich an, diese Klassen um LLM-spezifische Metadaten zu erweitern. So bleiben die C#-Klassen die zentrale Single Source of Truth.

Umsetzung über Attribute

Wir setzen zwei benutzerdefinierte Attribute ein:

  • LlmExplanation: Beschreibt Tabelle, Spalte oder Wert (z. B. für Fachbegriffe oder Berechnungslogik).
  • LlmIgnore: Markiert Spalten, die für das LLM irrelevant sind und ausgeblendet werden sollen.

Computed Properties

EF Core mappt C#-Properties standardmäßig auf Datenbankspalten. Computed Properties (berechnete Eigenschaften) existieren jedoch nur im Code und nicht in der Datenbank. Dennoch sind sie oft geschäftsrelevant und sollten dem LLM zugänglich sein. Dafür muss dem LLM explizit mitgeteilt werden, wie sich diese Properties aus den physischen Spalten berechnen lassen.

  [LlmExplanation("Bruttopreis: Quantity * NetPricePerItem * (1 + TaxRate).")]
  public decimal TotalGross => this.Quantity * this.NetPricePerItem * (1.0m + this.TaxRate);

Enums

Enums werden in der Datenbank (zumindest in der Standardkonfiguration) als Zahl gespeichert. Das LLM kann daraus keine domänenspezifische Informationen ableiten, deshalb muss das interne Mapping von EF Core, also welche Zahl welchen Wert darstellt ebenfalls erklärt werden.

Ein Beispiel

[LlmExplanation("ERP-Datenbank für Aufträge, Rechnungen und Produkte.")]
public class ErpDbContext : DbContext
{
  [LlmExplanation("Produktstamm mit Bezeichnung, Preis und Kategorisierung.")]
  public DbSet<ProductEntity> Products { get; set; }

  [LlmExplanation("Kundenauftrag (Verkauf), nicht Rechnung.")]
  public DbSet<OrderEntity> Orders { get; set; }

  [LlmExplanation("Positionen eines Auftrags mit Menge und Einzelpreis.")]
  public DbSet<OrderpositionEntity> Orderpositions { get; set; }

  [LlmExplanation("Rechnungskopf zu einem Auftrag.")]
  public DbSet<InvoiceEntity> Invoices { get; set; }

  [LlmExplanation("Rechnungspositionen mit Netto-/Brutto-Berechnung.")]
  public DbSet<InvoicepositionEntity> Invoicepositions { get; set; }
}

public enum ReceiptStatus
{
  Open = 0,
  Done = 1,
  Failed = 2
}

public class ProductEntity : Entity
{
  public string Name { get; set; } = string.Empty;

  [LlmIgnore]
  public bool InternalFlag { get; set; }
}

public class OrderEntity : Entity
{
  public DateTime CreatedAt { get; set; }
  public ReceiptStatus Status { get; set; }
}

public class InvoicepositionEntity : Entity
{
  public decimal Quantity { get; set; }
  public decimal NetPricePerItem { get; set; }
  public decimal TaxRate { get; set; }

  [LlmExplanation("Bruttopreis: Quantity * NetPricePerItem * (1 + TaxRate).")]
  public decimal TotalGross => this.Quantity * this.NetPricePerItem * (1.0m + this.TaxRate);
}

public class InvoiceEntity : Entity
{
  public ICollection<InvoicepositionEntity> Positions { get; set; } = new List<InvoicepositionEntity>();

  [LlmExplanation("Gesamtbrutto: Summiere TotalGross aller Positions. Benötigt: Invoicepositions. Dafür ist ein join mit der Invoicepositions Tabelle notwendig. .")]
  public decimal TotalGross => this.Positions.Sum(x => x.TotalGross);
}

Damit haben wir nun die Möglichkeit geschaffen dem LLM zu erklären, wie unsere Datenbank aufgebaut ist. Nun müssen wir ihm noch die Werkzeuge zu geben damit Arbeiten zu können.

Die Werkzeuge

Die Werkzeuge

Wir binden die LLMs über die .NET Library Semantic Kernel ein. Dort gibt es auch die Möglichkeit Tools (ähnlich MCP) zu implementieren. Wir benötigen im groben eine Schnittstelle die Datenbank zu erklären und dann gültige Queries auszuführen.

Die Tools im Überblick

  • list_all_tables() Überblick aller existierenden Tabellen
  • explain_table(string tableName) Erklärt eine Tabelle inklusiver aller Explanations (aus Attributen), aller Foreign Keys und erklärten computed Properties.
  • execute_query(string query) Führt SQL Queries aus und liefert die Ergebnisse als CSV zurück

Dem LLM müssen wir in der Systemprompt noch mitteilen, was wir wollen, wie es Umzusetzen ist und welche Tools es dafür zur Verfügung hat. Dort teilen wir mit, wer das LLM ist, wer wir sind, was wir wollen, und wie das LLM das Umsetzen kann.

Kurz zusammengefasst:

Du bist Klaus, BI‑Assistent in einem Handelsunternehmen.

DATENABFRAGE:
- Es besteht eine Verbindung mit einem Microsoft SQL Server durch das Plugin 'sql'.
- Rate niemals Queries; frage zuerst alle Tabellen mit 'list_all_tables' ab.
- Hole für alle benötigten Tabellen Spalten und Zusatzinformationen über 'get_table_metadata(table_name)'.
- Erstelle eine T‑SQL‑Query und führe diese über 'execute_sql_query(query)' aus.
- Bei Fehlern: Query anpassen und erneut versuchen.

usw.

Systemüberblick bis jetzt

Systemüberblick ohne Agenten

Die ersten Ergebnisse:

Mit diesem ersten Aufbau ließen sich bereits brauchbare Ergebnisse erzielen. Das LLM konnte relevante Tabellen identifizieren, zusätzliche Metadaten gezielt nachladen, daraus gültige T-SQL-Abfragen erzeugen und die Resultate als Text oder Diagramm aufbereiten. Damit war grundsätzlich gezeigt, dass natürlichsprachliche BI-Abfragen auf dieser Basis funktionieren können.

Gleichzeitig wurden aber auch die Grenzen des Ansatzes deutlich. Jeder Durchlauf war noch relativ teuer, weil das System die benötigten Tabellen, Metadaten und Query-Schritte immer wieder neu herleiten musste. Selbst bei sehr ähnlichen oder identischen Fragestellungen begann der Prozess praktisch jedes Mal von vorne. Das kostet Zeit, verbraucht Kontextfenster und erhöht die Wahrscheinlichkeit, dass sich das Modell bei längeren Ableitungsketten unnötig verrennt.

Das zentrale Problem war also nicht mehr, ob der Ansatz grundsätzlich funktioniert, sondern wie sich erfolgreiche Abfragemuster wiederverwenden lassen. Genau an dieser Stelle setzen die nächsten Optimierungen an.

Optimierungen

RAG

Damit das System nicht bei jeder ähnlichen Frage wieder bei null beginnen muss, speichern wir erfolgreiche Abfragen in einem RAG-Speicher ab. RAG steht für Retrieval Augmented Generation. Gemeint ist damit, dass das LLM vor der eigentlichen Antwort zusätzliche, bereits bekannte Informationen aus einem Speicher abrufen kann.

In unserem Fall sind das keine beliebigen Textdokumente, sondern bewährte SQL-Muster. Sobald KLAUS zu einer Benutzerfrage eine gültige Query erzeugt und tatsächlich ausgeführt hat, wird diese Abfrage zusammen mit einer fachlichen Kurzbeschreibung im Speicher abgelegt. Diese Beschreibung enthält nicht die exakten Eingabewerte des Benutzers, sondern den verallgemeinerten analytischen Intent, zum Beispiel welche Kennzahl gesucht wurde, welche Gruppierung relevant war und welche Filter oder Zeiträume eine Rolle gespielt haben.

Technisch verwenden wir dafür einen Vektor-Speicher. Die textuelle Beschreibung einer erfolgreichen Query wird in ein Embedding umgewandelt, also in einen Zahlenvektor, der die semantische Bedeutung des Textes abbildet. Stellt ein Benutzer später eine ähnliche Frage, wird auch diese Frage in eine verallgemeinerte Form gebracht und als Embedding dargestellt. Anschließend wird nicht nach exaktem Wortlaut gesucht, sondern nach semantischer Ähnlichkeit. Dadurch können auch Fragen wiedergefunden werden, die anders formuliert sind, aber fachlich dasselbe meinen.

Der Vorteil ist zweifach. Zum einen wird das System schneller, weil ähnliche Fragestellungen nicht jedes Mal vollständig neu hergeleitet werden müssen. Zum anderen steigt die Qualität, weil bereits erfolgreiche Query-Muster wiederverwendet werden können. KLAUS lernt also nicht durch klassisches Training nach, sondern über einen kontrollierten, nachvollziehbaren Wissensspeicher erfolgreicher Abfragen.

Die Agenten kommen

Die Agenten

Nachdem die RAG-Basis stand, haben wir den Ablauf in spezialisierte Agenten aufgeteilt. Das Ziel war eine klarere Verantwortlichkeit pro Schritt, weniger Kontextballast pro Modellaufruf und bessere Wiederverwendbarkeit im Gesamtsystem.

Systemüberblick mit Agenten

Der zentrale Orchestrator (BIChatBot) steuert den Ablauf. Dabei gilt: Jeder Agent hat eine klar abgegrenzte Aufgabe und kommuniziert nur mit den Komponenten, die er dafür benötigt.

Agent / Komponente Aufgabe Kommuniziert mit
NormalizeUserQuestionForRagQueryAgent Normalisiert die Benutzerfrage zu einem kurzen Retrieval-Key für semantische Suche. Empfängt Frage von BIChatBot, liefert Retrieval-Key an BIChatBot.
InMemorySqlRagStorage Sucht ähnliche, bereits erfolgreiche SQL-Muster und speichert neue Muster. Erhält Suchanfrage/Einträge von BIChatBot, gibt Treffer an BIChatBot zurück.
Hauptmodell (LLM) Plant und führt den SQL-Dialog über Tool-Calls aus (Tabellen, Metadaten, Query, Retry). Kommuniziert über BIChatBot mit SQL-Tools (list_all_tables, get_table_metadata, execute_sql_query).
QueryToRagSummarizationAgent Fasst erfolgreiche SQL-Ausführung fachlich zusammen, damit sie wiederverwendbar wird. Erhält Frage+SQL von BIChatBot, gibt Zusammenfassung an BIChatBot zurück.
SummarizeChatAgent Verdichtet lange Chatverläufe als rollierende Kontextzusammenfassung. Wird bei Bedarf von BIChatBot aufgerufen; liefert aktualisierte Chat-Summary zurück.
SummarizeChatToHeadlineAgent Erzeugt einen kurzen, prägnanten Titel für einen Chat. Erhält Chatinhalt von BIChatBot, gibt Titel an BIChatBot zurück.
CreateWidgetFromChatAgent Baut aus Chatkontext, Antwort und SQL ein wiederverwendbares Widget. Erhält Daten von BIChatBot, liefert Widget-Definition (Name, Query, ChartJsFactory) zurück.

Oder hier in kurz, wie die Agenten zusammenarbeiten.

  1. BIChatBot nimmt die Benutzerfrage an.
  2. NormalizeUserQuestionForRagQueryAgent erzeugt den Retrieval-Key.
  3. BIChatBot fragt damit InMemorySqlRagStorage und ergänzt Treffer als Erinnerung.
  4. Das Hauptmodell beantwortet die Frage über SQL-Tool-Calls.
  5. Nach erfolgreicher SQL-Ausführung erstellt QueryToRagSummarizationAgent die RAG-Zusammenfassung.
  6. BIChatBot speichert Zusammenfassung + SQL erneut in InMemorySqlRagStorage.
  7. Antwort (Text/Diagramm) wird an den Benutzer gestreamt.

Demo

Wir freuen uns darauf, auch mit Ihnen Ihre Daten zum Leben zu erwecken. Kontaktieren Sie und jetzt für ein kostenloses Kennenlerngespräch.

Druckansicht / PDF-Export