Ollama – Wikipedia (und Wikidata) als Wissensquelle nutzen, RAG in C# (Teil 8)

Dr.-Ing. Erhard Henkes, Stand: 29.11.2025

Zurück

 

Einleitung

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:

Voraussetzungen / Installationen

    Für diesen Teil benötigen wir die gleiche Umgebung wie in den vorherigen Kapiteln:

    Software

    Modelle in Ollama

    Für das Tutorial reicht ein kleines, schnelles Modell, z. B.:

    ollama pull gemma3:4b
    
    	

    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.

    Zugriff auf 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.

 

Architekturüberblick: RAG mit Wikipedia

Das System besteht aus drei klar getrennten Bausteinen.
Jeder Teil übernimmt eine exakt definierte Aufgabe. Die komplette Logik ist übersichtlich und erweiterbar.

1. Wikipedia-Client (Retrieval)

Der Wikipedia-Client ruft die API auf und liefert drei Datentypen:

Diese Wissens-Daten bilden die Basis für das gesamte RAG-System.


2. RAG-Engine (Augmentation)

Die RAG-Engine übernimmt die gesamte Aufbereitung der Wikipedia-Daten:

  1. Suchbegriffe erzeugen
    Ein kleines KI-Modell extrahiert passende Wikipedia-Titel aus der Nutzerfrage.

  2. Artikel abrufen
    Der Wikipedia-Client holt Titel, Summary und kompletten Volltext.

  3. 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:

    Chunking sorgt also dafür, dass die KI nur die richtigen Textstellen als Wissensbasis verwendet – und nicht vom Rest „überschwemmt“ wird.

  4. 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:

    In unserem Fall:

    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.

  5. 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.

  6. Antwort erzeugen (Generation)
    Ollama erzeugt Bulletpoints – wunschgemäß basierend auf dem Wikipedia-Kontext.

  7. 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.


3. WinForms User Interface (Interaktion & Streaming)

Das UI zeigt:

Der Benutzer kann mehrere Fragen stellen, ohne dass die Anwendung neu gestartet werden muss.

 

 

Wikipedia-Client implementieren

Für das RAG-System brauchen wir Zugriff auf drei Funktionen der Wikipedia-API:

  1. Suche → wir finden den passenden Artikeltitel

  2. Summary → wir prüfen schnell, ob der Artikel thematisch zur Frage passt

  3. 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.


C#-Implementierung: WikipediaOnlineClient

Diese Klasse nutzt HttpClient und liefert drei Methoden:

 

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

Hiermit ist die „Retrieval“-Schicht des RAG komplett.


RAG-Engine implementieren

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:

  1. Moderator-Modell extrahiert Suchbegriffe

  2. Wikipedia wird abgefragt

  3. Artikel werden gechunked

  4. Für jedes Chunk wird ein Embedding erzeugt

  5. Chunks werden per Cosine Similarity mit der Frage verglichen

  6. Die relevantesten Textstücke fließen in den Prompt

  7. Die KI erzeugt eine Antwort aus diesen Textstücken

  8. FactGuard filtert unzulässige Aussagen

Wir implementieren das Schritt für Schritt.


RagEngine.cs

Die Klasse verfügt über:

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);
}
}
}

 

 

2. WinForms-UI: 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):

 

  • Frage eingeben (z. B. „Wer ist der aktuelle Schachweltmeister?“ - interessant, weil zurzeit hinter dem Zeithorizont der Modelle)

  • Antwort enthält:



  • Was hat man damit erreicht?

    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:

    Damit verfügt diese Anwendung über eine robuste Grundarchitektur, die sich wie ein kleines „Offline-ChatGPT mit Wikipedia-Wissen“ verhält.

     

    Grenzen des Systems

    Trotz aller Robustheit gibt es natürliche Einschränkungen:

    Alle diese Punkte sind für ein sicheres, lokal laufendes RAG-System akzeptabel und teilweise sogar gewünscht.


    Ausblick – Wie geht es weiter?

    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.




    Hier ist mein aktuelles C#-Programm: Stand 26.11. 2025 (Visual Studio 2022, .Net8, WinForms):

    WikipediaRagWinForms.zip  (Version 0.01, nur zum Experimentieren, keine Gewähr oder Garantie)

    Voraussetzung: Windows 10/11, Ollama mit gemma3:4b (empfohlen) und gemma3:12b (nur bei 16 GB VRAM)




    Hier ist mein aktuelles C#-Programm: Stand 29.11. 2025 (Visual Studio 2022, .Net8, WinForms):
    Dieses enthält zusätzlich
    - Frage/Antwort in Englisch oder Spanisch
    - Zugriff auf Wikidata
    - Verfeinerte Zugriffstechniken gesteuert mit Parameter in Ragconfig.json

    WikipediaRagWinForms_DE_EN_ES.zip  (Version 0.02, nur zum Experimentieren, keine Gewähr oder Garantie)


    Ich denke, man sieht die Bedeutung lokaler KI mit diesem Projekt sehr gut. Ziel ist Vertraulichkeit und zuverlässiges Wissen.
    Bitte selbst mit diesem Programm und den Prompts experimentieren!

    In einem weiteren Tutorial werde ich versuchen, experimentell ein "künstliches Bewusstsein" zum Programm hinzuzufügen.

     

    Hier geht es weiter