Dr.-Ing. Erhard Henkes, Stand:
29.11.2025
In diesem Teil
erweitern wir die lokalen KI-Modelle aus den vorherigen Kapiteln um
Wikipedia als externe Wissensquelle.
Dafür bauen wir ein vollständiges
RAG-System (Retrieval Augmented
Generation), das gezielt Wikipedia-Artikel findet, verarbeitet und das Ergebnis
dem lokalen KI-Modell bereitstellt.
Der Ansatz nutzt nur das lokale Ollama, die offizielle Wikipedia-API, und es läuft wie gewohnt unter C# / WinForms.
Am Ende dieses Teils kann die Anwendung:
passende Wikipedia-Artikel zu einer Frage automatisch finden
Texte extrahieren, in "chunks" zerlegen und nach Relevanz sortieren ("ranking")
eine Antwort auf Basis der zuverlässigen Daten der deutschen Wikipedia erzeugen
Fakten prüfen und ungesicherte Aussagen entfernen
alles im Stream
anzeigen, wie im Chat-Stil
Für diesen Teil benötigen wir die gleiche Umgebung wie in den vorherigen Kapiteln:
Windows 10 oder 11
Visual Studio 2022 mit .NET-Desktop-Workload
.NET 8 SDK
Ollama (aktuelle Version)
Für das Tutorial reicht ein kleines, schnelles Modell, z. B.:
Optional kannst du
größere Modelle testen, aber für die Wikipedia-Abfragen bringen sie in der
Praxis kaum Vorteile,
erzeugen aber deutlich höhere Latenzen.
Unser Modell arbeitet eher als
Moderator. Das gesicherte Wissen kommt aus Wikipedia.
Wir verwenden
in diesem Teil die offizielle
Wikipedia-API (REST), noch kein kiwix-serve und ZIM-File.
Die Suche und die Artikelabrufe funktionieren mit
HTTP-Requests – komplett kostenlos und ohne Registrierung.
Das System besteht
aus drei klar getrennten Bausteinen.
Jeder Teil übernimmt eine exakt definierte Aufgabe.
Die komplette Logik ist übersichtlich und erweiterbar.
Der Wikipedia-Client ruft die API auf und liefert drei Datentypen:
gefundene Artikeltitel zu einem Suchbegriff
die kurze Summary eines Artikels
den vollständigen Artikeltext (Plaintext)
Diese Wissens-Daten bilden die Basis für das gesamte RAG-System.
Die RAG-Engine übernimmt die gesamte Aufbereitung der Wikipedia-Daten:
Suchbegriffe erzeugen
Ein kleines KI-Modell extrahiert passende
Wikipedia-Titel aus der Nutzerfrage.
Artikel abrufen
Der Wikipedia-Client holt Titel, Summary und
kompletten Volltext.
Artikel zerlegen (Chunking)
Lange Artikel werden in handliche Textstücke
zerlegt, um die wichtigsten Passagen
gezielt auswählen zu können.
Das KI-Modell arbeitet nicht mit dem gesamten
Artikel auf einmal, sondern nur mit den
relevantesten Chunks.
Dadurch:
werden irrelevante Abschnitte automatisch ausgefiltert
sinkt die Promptgröße signifikant
verbessert sich die Antwortqualität, weil nur thematisch passende Inhalte an die KI gehen
bleibt die Latenz niedrig, selbst bei langen Wikipedia-Einträgen
Chunking sorgt also dafür, dass die KI nur die richtigen Textstellen als Wissensbasis verwendet – und nicht vom Rest „überschwemmt“ wird.
Semantische Relevanz berechnen
Jedes Chunk erhält ein Embedding,
wird mit der Frage verglichen (Cosine Similarity) und
sortiert.
Ein Embedding
ist eine numerische
Vektordarstellung eines Textes.
Das KI-Modell wandelt einen Satz („Wer ist der Schachweltmeister?“) in einen
Vektor um, z. B.: [0.12, -0.44, 0.88, ...]
Der Vektor beschreibt die
Bedeutung des Textes –
nicht die Wörter selbst. Wichtiger Punkt:
Ähnliche
Inhalte → ähnliche Embeddings.
Beispiel: „Schachweltmeister“ und „Weltmeister im
Schach“ erzeugen Vektoren, die sehr nah beieinander liegen.
Die Cosine Similarity misst, wie ähnlich zwei Embeddings sind. Sie liefert einen Wert zwischen:
1.0 = fast identisch
0.0 = keine Ähnlichkeit
–1.0 = gegensätzliche Bedeutung (bei Text praktisch irrelevant)
In unserem Fall:
Wir erzeugen ein Embedding der Frage. Für jeden Wikipedia-Chunk erzeugen wir ebenfalls ein Embedding. Durch Cosine Similarity finden wir die besten Übereinstimmungen.
Kurz gesagt: Cosine
Similarity sagt uns, welche Textstücke fachlich zur Frage passen.
Dadurch wählt das Programm aus mehreren Artikeln genau jene Passagen, die
für die Fragestellung relevant sind.
Kontext für das KI-Modell aufbauen
Die besten Chunks werden zu einem großen Prompt
zusammengeführt. Das KI-Modell kümmert sich um die Weiterverarbeitung.
Antwort erzeugen (Generation)
Ollama erzeugt Bulletpoints – wunschgemäß
basierend auf dem Wikipedia-Kontext.
Faktenprüfung
Ein Filter entfernt Bulletpoints, die im Kontext
nicht belegt sind.
Damit entstehen stabile, überprüfte Antworten, die exakt auf Wikipedia basieren. Halluzinationen werden unterdrückt.
Das UI zeigt:
die verwendeten Wikipedia-Artikel
die Antwort in Echtzeit (Streaming aus Ollama)
die geprüften Fakten
Latenzen einzelner Schritte
interne Debug-Ausgaben für Analyse und Fehlersuche
Der Benutzer kann mehrere Fragen stellen, ohne dass die Anwendung neu gestartet werden muss.
Für das RAG-System brauchen wir Zugriff auf drei Funktionen der Wikipedia-API:
Suche → wir finden den passenden Artikeltitel
Summary → wir prüfen schnell, ob der Artikel thematisch zur Frage passt
Volltext → wir liefern den Inhalt für Chunking und Embeddings
Die API ist offen,
kostenlos und benötigt keinen Token.
Wir implementieren alles in einer Klasse.
WikipediaOnlineClientDiese Klasse nutzt
HttpClient und liefert drei Methoden:
SearchAsync()
GetSummaryAsync()
GetFullExtractAsync()
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
namespace
WikipediaRagWinForms.Services
{
public class WikipediaOnlineClient : IDisposable
{
private readonly HttpClient
_http = new HttpClient();
public void Dispose()
{
_http.Dispose();
}
//
------------------------------------------------------------
// 1) Titel-Suche
//
------------------------------------------------------------
public async Task<string>
SearchAsync(string term, CancellationToken token =
default)
{
string url =
"https://de.wikipedia.org/w/api.php?action=query&list=search&format=json&srsearch="
+
Uri.EscapeDataString(term);
var json = await
_http.GetStringAsync(url, token);
var obj = JObject.Parse(json);
var first =
obj["query"]?["search"]?.First;
if (first == null)
return null;
return
first["title"]?.ToString();
}
//
------------------------------------------------------------
// 2) Summary holen
//
------------------------------------------------------------
public async Task<string>
GetSummaryAsync(string title, CancellationToken token =
default)
{
string url =
"https://de.wikipedia.org/api/rest_v1/page/summary/" +
Uri.EscapeDataString(title);
var json = await
_http.GetStringAsync(url, token);
var obj = JObject.Parse(json);
return
obj["extract"]?.ToString();
}
//
------------------------------------------------------------
// 3) Vollständigen Artikeltext holen
//
------------------------------------------------------------
public async Task<string>
GetFullExtractAsync(string title, CancellationToken
token = default)
{
string url =
"https://de.wikipedia.org/w/api.php?action=query&prop=extracts&explaintext=true&format=json&titles="
+
Uri.EscapeDataString(title);
var json = await
_http.GetStringAsync(url, token);
var obj = JObject.Parse(json);
var pages =
obj["query"]?["pages"];
if (pages == null) return null;
foreach (var p in pages)
{
var extract =
p.First?["extract"]?.ToString();
if (!string.IsNullOrWhiteSpace(extract))
return extract;
}
return null;
}
}
}
Dieser Code
liefert zuverlässige Wikipedia-Titel passend zur Frage
lädt die Summary (REST-API)
lädt den vollständigen Artikel (Wikitext-API)
funktioniert ohne Key, ohne Login, ohne Throttling
ist vollständig asynchron und thread-safe
benötigte Pakete: Newtonsoft.Json (via NuGet)
Hiermit ist die „Retrieval“-Schicht des RAG komplett.
Das Herzstück des Systems ist die RAG-Engine, welche Wikipedia-Daten sammelt, in verwertbare Wissensstücke zerlegt und daraus eine korrekte KI-Antwort erzeugt.
Der Ablauf:
Moderator-Modell extrahiert Suchbegriffe
Wikipedia wird abgefragt
Artikel werden gechunked
Für jedes Chunk wird ein Embedding erzeugt
Chunks werden per Cosine Similarity mit der Frage verglichen
Die relevantesten Textstücke fließen in den Prompt
Die KI erzeugt eine Antwort aus diesen Textstücken
FactGuard filtert unzulässige Aussagen
Wir implementieren das Schritt für Schritt.
RagEngine.csDie Klasse verfügt über:
gute Fähigkeit für Debugging
Robustheit gegen "Overconfidence" des Modells
ein Embeddings Cache
einfache Erweiterbarkeit
Es folgt
Tutorial-Code, der schlank
genug ist, um verständlich zu sein, aber vollständig einsatzbereit ist:
1. RAG-Engine Grundstruktur
public class
RagEngine
{
private readonly WikipediaOnlineClient _wiki;
private
readonly OllamaClient _ollama;
// Cache: Text → Embedding
private
readonly Dictionary<string, List<float>> _embeddingCache =
new(StringComparer.OrdinalIgnoreCase);
public
RagEngine(WikipediaOnlineClient wiki, OllamaClient ollama)
{
_wiki = wiki;
_ollama = ollama;
}
}
2. Suchbegriffe aus Frage extrahieren (Moderator-Modell)
Der Moderator, also das
Programm mit dem KI-Modell, soll keine Antwort geben, sondern nur
Wikipedia-Suchbegriffe erzeugen.
private async Task<List<string>>
GetSearchTermsAsync(
string question, CancellationToken token)
{
string
prompt =
"Extrahiere 1–3 passende Wikipedia-Suchbegriffe.\n" +
"Nur
Begriffe, keine Sätze.\n" +
"Pro Zeile ein Begriff.\n\n" +
"Frage: " +
question;
string raw = await _ollama.GenerateAsync(prompt, token);
if (string.IsNullOrWhiteSpace(raw))
return new() { question };
return raw.Split('\n')
.Select(l => l.Trim())
.Where(l => l.Length > 1)
.Take(3)
.ToList();
}
3. Wikipedia abrufen
private async
Task<List<(string Title, string Text)>> RetrieveArticlesAsync(
List<string>
searchTerms, CancellationToken token)
{
var result = new List<(string,
string)>();
foreach (var term in searchTerms)
{
string title =
await _wiki.SearchAsync(term, token);
if (title == null) continue;
string summary = await _wiki.GetSummaryAsync(title, token);
if (summary ==
null || summary.Length < 40)
continue;
string extract = await
_wiki.GetFullExtractAsync(title, token);
if (extract == null ||
extract.Length < 200)
continue;
result.Add((title, extract));
}
return result;
}
4. Chunking: lange Artikel aufteilen
Ein Chunk hat ca. 1000–1500 Zeichen – optimal für Embeddings (s.o.).
private
List<(string Title, string Text)> ChunkArticles(
List<(string Title, string Text)> articles, int
maxSize = 1200)
{
var chunks = new List<(string, string)>();
foreach (var (title, text) in
articles)
{
int pos = 0;
while (pos < text.Length)
{
int size = Math.Min(maxSize,
text.Length - pos);
int
end = text.LastIndexOf('.', pos + size);
if (end <= pos)
end = pos + size;
string chunk =
text.Substring(pos, end - pos).Trim();
if (chunk.Length > 200)
chunks.Add((title, chunk));
pos = end + 1;
}
}
return chunks;
}
5. Embeddings + Cosine Similarity
Embeddings cachen
private
async Task<List<float>> GetEmbeddingAsync(string text, CancellationToken token)
{
if (_embeddingCache.TryGetValue(text, out var emb))
return emb;
emb = await
_ollama.GetEmbeddingAsync(text, token);
_embeddingCache[text] = emb;
return emb;
}
Cosine Similarity
private static float
CosineSimilarity(List<float> a, List<float> b)
{
float dot = 0, na = 0, nb = 0;
for (int i = 0; i < a.Count;
i++)
{
dot += a[i] * b[i];
na += a[i] * a[i];
nb += b[i] * b[i];
}
return dot /
(float)(Math.Sqrt(na) * Math.Sqrt(nb));
}
6. Chunks nach Relevanz
sortieren
private async Task<List<(string Title, string Text)>> RankChunksAsync(
string question, List<(string Title, string Text)>
chunks, int topK, CancellationToken token)
{
var qEmb = await GetEmbeddingAsync(question, token);
return (await Task.WhenAll(
chunks.Select(async c =>
{
var emb = await
GetEmbeddingAsync(c.Text, token);
float score = CosineSimilarity(qEmb, emb);
return (score, c);
})))
.OrderByDescending(x => x.score)
.Take(topK)
.Select(x => x.c)
.ToList();
}
7. Prompt generieren + KI-Antwort
private async
Task<string> GenerateAnswerAsync(
string question, List<(string Title, string Text)>
context, CancellationToken token)
{
string ctx = string.Join("\n\n", context.Select(c =>
$"Artikel: {c.Title}\n{c.Text}"));
string prompt =
"Beantworte die Frage ausschließlich anhand folgender
Wikipedia-Auszüge.\n" +
"Nur Fakten, keine Vermutungen.\n" +
"Antworte in Bulletpoints, jeder Punkt genau ein
Fakt.\n\n" +
ctx +
"\n\n" +
"Frage: " +
question + "\nAntwort:";
return await _ollama.GenerateAsync(prompt, token);
}
8. FactGuard (Regelbasiert, LLM-frei)
Das schützt stabil gegen Halluzinationen:
private string
FactGuard(string answer, string context)
{
var ctx = context.ToLower();
var lines = answer.Split('\n')
.Select(l => l.Trim())
.Where(l => l.StartsWith("•"))
.ToList();
var approved = new
List<string>();
foreach (var line in lines)
{
string fact =
line.Substring(1).Trim().ToLower();
// mindestens 1 Wort > 4
Zeichen muss im Kontext vorkommen
bool ok = fact.Split(' ')
.Any(w => w.Length > 4 && ctx.Contains(w));
if (ok)
approved.Add(line);
}
return string.Join("\n",
approved);
}
9. Hauptfunktion – AskStreamAsync()
Hier läuft alles zusammen.
1.
AskAsync
in die RAG-Engine einbauen
Datei:
RagEngine.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
// ... deine bestehenden Usings
namespace
WikipediaRagWinForms.Rag
{
public class RagEngine
{
// ... bestehende Felder, Konstruktor und Methoden
//
------------------------------------------------------------
// Öffentliche Hauptfunktion für das UI
//
------------------------------------------------------------
public async Task<(List<string> Titles, string
Answer)> AskAsync(
string question,
CancellationToken token)
{
// 1) Suchbegriffe aus der Frage extrahieren
var terms = await GetSearchTermsAsync(question,
token);
// 2) Artikel aus Wikipedia holen
var articles = await RetrieveArticlesAsync(terms,
token);
if
(articles.Count == 0)
return (new List<string>(), "Wikipedia: Keine passenden Artikel gefunden.");
// 3) Artikel in Chunks
zerlegen
var chunks =
ChunkArticles(articles);
if (chunks.Count == 0)
return (articles.Select(a => a.Title).ToList(),
"Wikipedia: Keine nutzbaren Textpassagen gefunden.");
// 4) Chunks nach Relevanz
sortieren (Embedding + Cosine Similarity)
var ranked = await RankChunksAsync(question, chunks,
4, token);
// Kontext-String für FactGuard
string context = string.Join(
"\n\n",
ranked.Select(c => "Artikel: " + c.Title + "\n" +
c.Text));
// 5) KI-Antwort aus den Top-Chunks generieren
string rawAnswer = await GenerateAnswerAsync(question,
ranked, token);
// 6) Faktenprüfung (regelbasiert)
string filtered = FactGuard(rawAnswer, context);
// 7) Artikeltitel-Liste
zurückgeben
var titles =
ranked.Select(c => c.Title)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
return (titles, filtered);
}
}
}
MainForm mit RAG verknüpfen
Datei:
MainForm.cs
using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using WikipediaRagWinForms.Rag;
using WikipediaRagWinForms.Services;
namespace WikipediaRagWinForms
{
public partial class MainForm
: Form
{
private readonly RagEngine _rag;
private CancellationTokenSource _cts;
public MainForm()
{
InitializeComponent();
this.Font = new
System.Drawing.Font("Segoe UI", 11F);
// Wikipedia-Client +
Ollama-Client für die RAG-Engine
var wiki = new WikipediaOnlineClient();
var ollama = new
OllamaClient("http://127.0.0.1:11434/api", "gemma3:4b");
_rag = new RagEngine(wiki,
ollama);
}
private async void
btnAsk_Click(object sender, EventArgs e)
{
string question = txtQuestion.Text.Trim();
if (string.IsNullOrWhiteSpace(question))
{
MessageBox.Show("Bitte eine
Frage eingeben.");
return;
}
// alten Request abbrechen,
falls noch laufend
_cts?.Cancel();
_cts =
new CancellationTokenSource();
var token = _cts.Token;
btnAsk.Enabled = false;
txtAnswer.Clear();
try
{
var sw =
System.Diagnostics.Stopwatch.StartNew();
var (titles, answer) = await
_rag.AskAsync(question, token);
sw.Stop();
var sb = new StringBuilder();
// verwendete
Wikipedia-Artikel anzeigen
if (titles.Count > 0)
{
sb.AppendLine("Verwendete Artikel:");
foreach (var t in titles)
sb.AppendLine(" - " + t);
sb.AppendLine();
}
sb.AppendLine("Antwort:");
sb.AppendLine();
sb.AppendLine(answer);
sb.AppendLine();
sb.AppendLine($"Dauer: {sw.ElapsedMilliseconds} ms");
txtAnswer.Text =
sb.ToString();
}
catch (OperationCanceledException)
{
txtAnswer.Text =
"Abgebrochen.";
}
catch (Exception ex)
{
txtAnswer.Text = "Fehler:\r\n" + ex;
}
finally
{
btnAsk.Enabled = true;
}
}
}
}
Damit hat man eine einfache, stabile Version ohne Streaming-Schnickschnack (im unten verlinkten Programm ist Streaming enthalten):
UI friert nicht ein (async/await)
RAG läuft vollständig im Hintergrund
Antwort erscheint, sobald sie fertig ist
verwendete Wikipedia-Artikel werden mit angezeigt
Frage eingeben (z. B. „Wer ist der aktuelle Schachweltmeister?“ - interessant, weil zurzeit hinter dem Zeithorizont der Modelle)
Antwort enthält:
Liste „Verwendete Artikel:“
Bulletpoint-Antwort
Dauer in Millisekunden
Mit diesem Teil des
Tutorials hat man ein funktionierendes
Retrieval-Augmented-Generation-System (RAG) gebaut, das ausschließlich
auf lokaler KI (Ollama) und
Wikipedia als Wissensquelle
basiert.
Der gesamte Prozess läuft auf dem eigenen Rechner,
also vertraulich. Lediglich für den Wikipedia-Zugriff verwenden wir noch das
Internet (später: kiwix-serve, zim-File).
Das System beherrscht:
semantische Wikipedia-Suche
(Moderator-Modell → Suchbegriffe → echte Artikel)
Artikelverarbeitung durch Chunking
(automatisch in sinnvolle Abschnitte zerlegt)
semantische Relevanzbewertung
(Embeddings + Cosine Similarity)
faktenbasierte Antwortgenerierung
(LLM darf nur verwenden, was im Wikipedia-Kontext
steht)
Faktenprüfung (FactGuard) ohne
Halluzinationen
(regelbasierter Filter → nur belegte Aussagen
bleiben)
komplett lokale Ausführung von
Frage und Antwort
(100 % offline, relativ schnelle Ausführung,
Kontrolle und Vertraulichkeit)
Damit verfügt diese Anwendung über eine robuste Grundarchitektur, die sich wie ein kleines „Offline-ChatGPT mit Wikipedia-Wissen“ verhält.
Trotz aller Robustheit gibt es natürliche Einschränkungen:
Wikipedia ist nicht immer vollständig oder aktuell.
Kleine Modelle (z. B. gemma3:4b) erzeugen teils knappe oder holprige Formulierungen. Dafür sind sie schnell.
Der "Moderator" trifft nicht immer perfekte Suchbegriffe.
Embeddings können bei stark ambigen Fragen danebenliegen.
FactGuard filtert streng – lieber zu viel löschen als falsche Fakten durchlassen. Halluziniert wird schon genug.
Alle diese Punkte sind für ein sicheres, lokal laufendes RAG-System akzeptabel und teilweise sogar gewünscht.
Möglichkeiten, das System zu erweitern, sind:
1. Echtzeit-Streaming
Token für Token anzeigen (so wie ChatGPT es macht).
2. Erweiterte Wikipedia-Indexe
Offline-Wikipedia per Kiwix mit Volltextsuche. Dies
ist vor allem ein Schutz gegen Manipulation der Inhalte und Zensur.
3. Mehrere Wissensquellen gleichzeitig
z.B. lokale PDFs, Fachbücher, Markdown-Wissen,
Webarchive (Lizensen beachten).
4. Query-Rewriting
Die Frage automatisch umformulieren, bevor gesucht
wird.
5. Snippet-Highlighting
Hervorheben, welche Stelle im Artikel zu welchem
Bulletpoint führte.
6. Caching & Persistenz
Wikipedia-Artikel dauerhaft speichern und
Beschleunigung massiv erhöhen.
7. Evaluation & Benchmarking
Messung der Antwortqualität und der Faktenabdeckung.
usw.