Eigene Lektionen für Adaptive Learner: von der ersten Aufgabe zum teilbaren Content-Repository
Ein Folgeartikel zu Adaptive Learner ist jetzt eine App im Browser. Jener Artikel hat die App für alle geöffnet, dieser zeigt, wie du sie…
Eigene Lektionen für Adaptive Learner: von der ersten Aufgabe zum teilbaren Content-Repository

Ein Folgeartikel zu Adaptive Learner ist jetzt eine App im Browser. Jener Artikel hat die App für alle geöffnet, dieser zeigt, wie du sie mit deinen eigenen Inhalten füllst. Er baut auf der Serie Adaptives Lernen: Die Serie auf, einem Strang von Von Theorie zur Praxis.
Kein Abo nötig: Der Friend Link führt dich direkt zum ganzen Artikel.
Die meisten Lern-Apps geben dir Inhalte. Adaptive Learner gibt dir das Format.
Der Unterschied klingt klein und ist es nicht. Wenn eine App die Inhalte besitzt, lernst du, was jemand anderes für wichtig hält. Wenn du das Format besitzt, lernst du, was du selbst brauchst: den Wortschatz deines Fachgebiets, die Kernaussagen eines Buchs, das Vokabular für deine nächste Reise. Und wenn die App eines Tages verschwindet, sind deine Inhalte immer noch da, als Dateien, die dir gehören.
Der vorherige Artikel hat die erste Hürde beseitigt, nämlich die zur Nutzung der App: kein Download, kein Konto, du öffnest die Seite und lernst. Dieser Artikel beseitigt die nächste Hürde, das Erstellen eigener Inhalte. Deine Lektionen liegen in einem einfachen Git-Repository aus Textdateien, das dir gehört, das jede kompatible App lesen kann und das kein Anbieter wegsperren kann.
Dieser Artikel zeigt beides: wie du in fünf Minuten ohne eine Zeile Code deine erste Aufgabe baust, und wie du daraus ein vollwertiges Content-Repository machst, das du teilen kannst.
Zur Offenlegung: Ich habe Adaptive Learner gebaut, ich bin also kein neutraler Rezensent. Was ich zusichern kann: Jedes Code-Beispiel unten stammt entweder aus der test-validierten Referenzdokumentation des Formats oder wurde gegen denselben Validator geprüft, keins ist aus dem Gedächtnis geschrieben. Was du kopierst, besteht die Validierung.
Teil 1: Das Denkmodell und deine ersten Aufgaben (ohne Code)
Zwei Dinge, die häufig verwechselt werden
Eine Karte ist ein Faktenpaar. Vorderseite, Rückseite. hola bedeutet hallo. Das ist der Stoff, der wiederholt wird.
Eine Aufgabe ist eine Frage, die prüft, ob du diesen Stoff kannst. Sie kann sich auf eine Karte beziehen, muss aber nicht.
Der Unterschied ist wichtig, weil das Wiederholungssystem Karten plant, nicht Aufgaben. Eine gute Lektion hat wenige, klare Karten und mehrere Aufgaben, die dieselben Fakten aus verschiedenen Richtungen angehen.
Wie eine Aufgabe aussieht, in einfachen Worten
Eine Aufgabe hat einen Typ, eine Frage, eine richtige Antwort und meist ein paar falsche.
Die gängigen Typen:
- cloze: ein Satz mit einer Lücke. Drei Varianten, dazu später mehr.
- free_text: eine offene Frage, die der Lerner tippt. Du gibst die akzeptierten Antworten an und, als Qualitätsregel des Templates (nicht des Engine-Schemas), mindestens einen
distractors-Eintrag: plausible falsche Antworten, die die Aufgabe überhaupt erst aussagekräftig machen. - matching: Begriffe den passenden Bedeutungen zuordnen. Mindestens drei Paare.
- word_tiles: einen Satz aus vorgegebenen Bausteinen zusammensetzen.
Die entscheidende Qualitätsschraube sind nicht die richtigen Antworten, sondern die falschen. Eine Aufgabe mit offensichtlich absurden Alternativen prüft nichts. Gute Distraktoren sind Fehler, die ein echter Lerner machen würde, gebunden an eine konkrete Fehlvorstellung.
Ausprobieren, ohne etwas einzurichten
Du brauchst kein Repository, um anzufangen. Öffne die App, geh zu “Meine Inhalte” und leg eine Lektion direkt in der Oberfläche an. Das reicht für die ersten Aufgaben und zeigt dir, wie sich das Format anfühlt, bevor du dich mit Dateien beschäftigst.
Wenn du dabei merkst, dass du mehr willst, geht es hier weiter.
Teil 2: Dein eigenes Content-Repository (für Entwickler)
Ab hier arbeitest du mit Dateien, Git und einem Validator. Das Ergebnis ist ein Repository, das du versionieren, teilen und in die App laden kannst.
Das Format hat ein eigenes Zuhause
Das Lektionsformat lebt nicht in der App, sondern in einem eigenständigen npm-Paket, learn-content-engine. Dort liegen das Schema, die Validierung und die Referenzdokumentation. Das ist kein Detail: Es bedeutet, dass dein Content nicht an diese eine App gebunden ist. Jedes Werkzeug, das die Engine einbindet, kann deine Lektionen lesen.
Schritt 1: Vom Template starten
Der schnellste Einstieg ist das Template-Repository [adaptive-learner-content-template](https://github.com/astrapi69/adaptive-learner-content-template). Klick auf GitHub auf Use this template, und du bekommst ein frisches eigenes Repository: keine Fork-Beziehung, keine geerbte Historie, ein sauberer Startpunkt. Struktur, Schema-Spiegel, CI-Validierung und der KI-Generator sind bereits eingerichtet, dazu ein Beispiel-Set, das du durch dein eigenes ersetzt.
adaptive-learner-content-template/
Makefile # ein Befehl: make validate
manifest.yaml # Wurzel-Manifest: listet deine Sets
requirements.txt # Abhängigkeiten des Validators
schema/
engine-version.txt # pinnt die Engine-Version, z. B. 0.4.0
lesson.schema.json # Spiegel des kanonischen Schemas
scripts/
validate_content.py # lokale Validierung
generate_exercises.py # optionaler KI-Generator
sets/
en/
es-a1/ # Beispiel-Set: Spanisch A1 für Englischsprecher
manifest.yaml
lessons/
01-example.json
Sobald dein Repository steht, ersetzt du das Beispiel-Set durch dein eigenes. Welche Verzeichnisstruktur gilt, hängt davon ab, was du unterrichtest, und ein falscher Pfad lässt den Validator dein Set ablehnen. Deshalb genau:
- Sprachlern-Sets (du bringst Sprache B jemandem bei, der Sprache A spricht) liegen unter
sets/<quelle>/<ziel>-<niveau>/, zum Beispielsets/de/es-a1/für Spanisch A1 für deutschsprachige Lerner. Der Ordnername ist nicht frei wählbar, er kodiert Zielsprache und Niveau. - Wissens-Sets (du vermittelst ein Thema in einer Sprache, Quell- und Zielsprache sind identisch) liegen unter
sets/<sprache>/<themen-slug>/, zum Beispielsets/de/stoische-philosophie/. Hier ist der Ordnername ein freier Themen-Slug.
Jedes Set-Verzeichnis enthält einen lessons/-Ordner. Halte dich an die Konvention, die Werkzeuge und andere Autoren erwarten sie.
Schritt 2: Dein Set im Manifest eintragen
Ein Content-Repository nutzt zwei Manifest-Dateien. Beide sind nötig, und die zweite wird leicht übersehen.
Das Wurzel-manifest.yaml (im Repository-Root) listet auf, was dein Repository anbietet. Das einzige Pflichtfeld auf oberster Ebene ist name; jedes Set braucht id, title, title_native, target_language, level, version und lesson_count. Das Template liefert YAML, also siehst du beim Öffnen der Datei genau das:
schema_version: "1.5"
name: Meine spanischen Inhalte
description: Ein kleines Repository mit spanischen Lektionen.
sets:
- id: es-a1
title: Spanisch A1
title_native: Español A1
target_language: es
source_language: de
domain: language
level: A1
version: "1.0.0"
lesson_count: 1
path: sets/de/es-a1
tags: [spanisch, a1]
Die Engine liest YAML und JSON, beides funktioniert, aber das Template gibt dir YAML, und für eine handgepflegte Datei ist es die lesbarere Wahl. Der path des Sets ist das repo-relative Verzeichnis mit dem lessons/-Ordner und ist erforderlich; er muss der oben genannten Konvention folgen (sets/<quelle>/<ziel>-<niveau> bei Sprachsets, sets/<sprache>/<themen-slug> bei Wissens-Sets). Halte lesson_count ehrlich, die Zahl muss zu den tatsächlich ausgelieferten Lektionen passen. schema_version ist optional und nicht streng geprüft.
Zum Feld level: Bei einem Sprachset ist es die GER-Stufe (A1). Bei einem Wissens-Set gibt es keine natürliche Stufe, das Feld wird aber erwartet. Nimm einen beschreibenden String wie grundlagen oder einfuehrung, der Validator akzeptiert jede Zeichenkette.
Das Set-eigene manifest.yaml (in sets/<...>/) ist die Datei, aus der der Content-Loader deine Lektionsdateien liest. Das übersieht man leicht: Das Wurzel-Manifest listet das Set, das Set-Manifest listet die Lektionen, über metadata.lessons. Ohne diese Datei weiß die App nicht, welche Lektionen sie laden soll:
schema_version: "1.5"
name: Spanisch A1
description: Spanisch für deutschsprachige Anfänger.
sets:
- id: es-a1
title: Spanisch A1
title_native: Español A1
target_language: es
source_language: de
domain: language
level: A1
path: sets/de/es-a1
version: "1.0.0"
lesson_count: 1
tags: [spanisch, a1]
metadata:
author: Dein Name
license: CC-BY-SA-4.0
lessons:
- 01-begruessungen.json
Jede Lektions-JSON in deinem lessons/-Ordner bekommt einen Eintrag unter metadata.lessons. Halte die Liste synchron mit den Dateien, die du tatsächlich anlegst.
Schritt 3: Eine Lektion schreiben
Eine Lektion ist eine einzelne JSON-Datei: ein paar Metafelder, eine optionale Liste von cards (die Fakten, die sie vermittelt) und eine geordnete Liste von steps (Theorie zum Lesen, Aufgaben zum Lösen).
Die Pflichtfelder des Schemas sind nur id, title und ein nicht-leeres steps-Array. Der Template-Validator erzwingt darüber hinaus Qualitätsminima, die eine echte Lektion erfüllen muss: mindestens ein Theorie-Step, mindestens fünf Aufgaben und mindestens zwei verschiedene Aufgabentypen (eine matching-Aufgabe braucht mindestens drei Paare, eine free_text mindestens zwei akzeptierte Antworten).
Eine Lektion mit einer einzigen Aufgabe ist also gegen das reine Schema gültig, wird aber von dem Validator abgelehnt, den du tatsächlich ausführst. Hier eine vollständige Lektion, die durchläuft:
{
"id": "01-begruessungen",
"title": "Begrüßungen",
"description": "Begrüßen und verabschieden auf Spanisch.",
"target_language": "es",
"source_language": "de",
"domain": "language",
"estimated_minutes": 8,
"cards": [
{ "id": "hola", "front": "hola", "back": "hallo", "tags": ["begruessung"] },
{ "id": "buenas-noches", "front": "buenas noches", "back": "guten Abend", "tags": ["begruessung"] },
{ "id": "gracias", "front": "gracias", "back": "danke", "tags": ["begruessung"] },
{ "id": "adios", "front": "adiós", "back": "auf Wiedersehen", "tags": ["begruessung"] }
],
"steps": [
{
"id": "intro",
"type": "theory",
"title": "Begrüßen auf Spanisch",
"body": "**hola** = hallo, **buenas noches** = guten Abend, **gracias** = danke, **adiós** = auf Wiedersehen."
},
{
"id": "s-select",
"type": "exercise",
"exercise": {
"id": "e-select",
"type": "cloze",
"cloze_mode": "select",
"prompt": "Wähle die Begrüßung für den Tag.",
"sentence": "Am Morgen sagst du ___.",
"blanks": [{ "accept": ["hola"] }],
"distractors": ["buenas noches", "adiós"],
"card_ids": ["hola"]
}
},
{
"id": "s-type",
"type": "exercise",
"exercise": {
"id": "e-type",
"type": "cloze",
"cloze_mode": "type",
"prompt": "Fülle die Lücke.",
"sentence": "Um dich zu bedanken, sagst du ___.",
"blanks": [{ "accept": ["gracias"] }],
"card_ids": ["gracias"]
}
},
{
"id": "s-free",
"type": "exercise",
"exercise": {
"id": "e-free",
"type": "free_text",
"prompt": "Wie verabschiedest du dich auf Spanisch?",
"accept": ["adiós", "Adiós", "adios"],
"distractors": ["hola"],
"card_ids": ["adios"]
}
},
{
"id": "s-match",
"type": "exercise",
"exercise": {
"id": "e-match",
"type": "matching",
"prompt": "Ordne jedem spanischen Wort seine deutsche Bedeutung zu.",
"pairs": [
{ "left": "hola", "right": "hallo" },
{ "left": "buenas noches", "right": "guten Abend" },
{ "left": "gracias", "right": "danke" }
],
"card_ids": ["hola", "buenas-noches", "gracias"]
}
},
{
"id": "s-multi",
"type": "exercise",
"exercise": {
"id": "e-multi",
"type": "cloze",
"cloze_mode": "multiselect",
"prompt": "Wähle alle Begrüßungen aus.",
"sentence": "Welche dieser Wörter sind Begrüßungen?",
"accept": ["hola", "buenas noches"],
"distractors": ["gracias"]
}
}
]
}
Zwei Dinge sind erwähnenswert. Das cards-Array hält die Fakten; Aufgaben referenzieren sie über card_ids, und jede referenzierte ID muss in cards existieren, der Validator prüft das. Und target_language, source_language sowie domain werden normalerweise vom Set geerbt, in der Praxis kannst du sie in der Lektion also oft weglassen.
Schritt 4: Die drei Formen von Multiple Choice
Alle drei sind cloze-Aufgaben, unterschieden durch cloze_mode. Die Unterscheidung ist der häufigste Stolperstein für neue Autoren.
Einfachauswahl ist cloze_mode: "select": pro Lücke ein Dropdown, gespeist aus distractors. Der Satz trägt einen ___-Marker, und die Anzahl der Marker muss der Anzahl der blanks entsprechen. Das accept[0] der Lücke ist die richtige Option. Das optionale Feld card_ids verbindet die Aufgabe mit den Fakten in deinem cards-Array, so weiß die App, welches Wissen die Frage prüft, und jede aufgeführte ID muss in cards existieren.
{
"id": "hauptstadt",
"title": "Hauptstädte",
"cards": [
{ "id": "spanien", "front": "Spanien", "back": "Hauptstadt: Madrid" }
],
"steps": [
{
"id": "s1",
"type": "exercise",
"exercise": {
"id": "c1",
"type": "cloze",
"cloze_mode": "select",
"prompt": "Wähle die richtige Ergänzung.",
"sentence": "Madrid ist die Hauptstadt von ___.",
"blanks": [ { "accept": ["Spanien"] } ],
"distractors": ["Portugal", "Frankreich"],
"card_ids": ["spanien"]
}
}
]
}
Freies Tippen ist cloze_mode: "type": derselbe Aufbau, aber ohne distractors. Der Lerner tippt die Antwort. Nutze diesen Modus, wenn Wiedererkennen zu leicht wäre.
Mehrfachauswahl ist cloze_mode: "multiselect": mehrere richtige Antworten. Und hier ist der Unterschied, der Autoren stolpern lässt: Es gibt keine Marker und keine blanks. Der sentence ist der Fragestamm, accept enthält alle richtigen Antworten und distractors die falschen.
{
"id": "primzahlen",
"title": "Primzahlen",
"steps": [
{
"id": "s1",
"type": "exercise",
"exercise": {
"id": "c1",
"type": "cloze",
"cloze_mode": "multiselect",
"prompt": "Wähle alle Primzahlen aus.",
"sentence": "Welche dieser Zahlen sind Primzahlen?",
"accept": ["2", "3", "5"],
"distractors": ["4", "6"]
}
}
]
}
Dass multiselect ohne Marker und Lücken auskommt, ist kein Versehen, sondern folgt daraus, dass die Frage kein Lückentext ist, sondern eine Auswahl.
Ein Wort zu den Hinweisen: Ein Hint soll die Antwort nicht verraten. Das ist Handwerk, keine maschinell geprüfte Regel, mit einer Ausnahme: Der Validator markiert Längenangaben automatisch, also jede Aussage über die Länge der Antwort im Hint, egal ob die Zahl stimmt oder nicht, weil die App die Länge ohnehin selbst anzeigt. Ein Hint wie “vier Buchstaben” wird nicht abgelehnt, weil er falsch wäre, sondern weil er redundant ist. Das allgemeinere Prinzip, die Antwort nicht zu verraten, bleibt deine Aufgabe.
Schritt 5: Validieren, bevor du committest
Dieser Schritt trennt Inhalte, die funktionieren, von Inhalten, die richtig aussehen. Das Template liefert ein Makefile mit, damit du keine Python-Umgebung von Hand einrichten musst. Ein Befehl validiert deine Inhalte und richtet beim ersten Lauf alles Nötige selbst ein:
make validate
Kein make auf deinem System, etwa unter Windows ohne WSL? Zwei Alternativen: eine virtuelle Umgebung selbst anlegen (python3 -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt, dann python3 scripts/validate_content.py), oder lokal gar nicht validieren, sondern committen, pushen und die GitHub-Actions-CI des Repositories prüfen lassen. Die CI prüft dasselbe.
Was die Validierung abdeckt und was nicht, lohnt sich präzise zu sagen. Der lokale Validator (validate_content.py) prüft die Struktur: Das Schema ist streng, unbekannte Felder werden abgelehnt statt ignoriert, und Pflichtfelder müssen vorhanden sein. Das fängt die meisten Tippfehler ab, bevor sie zur Laufzeit still versagen.
Die tieferen semantischen Regeln, also Lücken gleich Marker, disjunkte Mengen von accept und distractors, genau eine richtige Antwort wo verlangt, und referenzielle Integrität der card_ids, erzwingt die Validierung der Engine selbst. Das Template ruft sie auf, wenn learn-content-engine installiert ist, und die CI des Repositories führt sie bei jedem Push aus. Für die vollständige semantische Prüfung installierst du also entweder die Engine lokal oder verlässt dich auf die CI: Eine grüne CI bedeutet, dass Struktur und Semantik bestanden haben.
Grüne CI ist notwendig, aber für alles Visuelle oder Interaktive nicht hinreichend. Öffne deine Lektion einmal in der App, bevor du dich auf sie verlässt.
Schritt 6: Die App auf dein Repository zeigen lassen
Öffne in der App die Einstellungen, geh zu “Daten” und füge dein Repository über die GitHub-URL hinzu. Öffentliche Repositories brauchen kein Token. Für private Repositories hinterlegst du einen persönlichen Zugriffstoken, der nur im Browser gespeichert wird und niemals an einen Server geht.
Wenn du deine Inhalte teilen willst, ohne die Repository-URL öffentlich zu machen, erzeugt die App einen Einladungscode, den andere direkt einlesen können.
Aufgaben mit KI generieren (die ehrliche Variante)
Aufgaben von Hand zu schreiben, ist für ein paar Lektionen in Ordnung. Für größere Mengen hilft ein KI-Modell, aber nur, wenn du seine Ausgabe als zu validierenden Entwurf behandelst und niemals als fertigen Inhalt.
Du musst das nicht selbst bauen: Das Template liefert ein lauffähiges, anbieterunabhängiges Skript unter scripts/generate_exercises.py. Es ruft dein gewähltes Modell auf (Claude, OpenAI oder Gemini) mit einem Schlüssel aus deiner Umgebung, bettet ein gültiges Beispiel des Zieltyps in den Prompt ein, jagt jedes erzeugte Element durch den Validator, spielt Fehlermeldungen ans Modell zurück und schreibt ausschließlich in einen generated/-Zwischenordner, nie direkt in deine Sets.
Eine ehrliche Einschränkung: Der reine Python-Validator prüft die Struktur, nicht die tieferen semantischen Regeln der Engine. Das Skript ruft die Engine dafür auf, wenn sie installiert ist, und warnt, wenn nicht. Für die vollständige semantische Prüfung installierst du also learn-content-engine oder verlässt dich auf die CI, die es erzwingt.
Der Aufruf sieht so aus:
export ANTHROPIC_API_KEY="sk-..."
make generate ARGS="--topic 'Im Café bestellen' --target-lang es --source-lang de --level A1 --set-id es-a1"
Der Prompt fordert bewusst plausible falsche Antworten an, gebunden an konkrete Fehlvorstellungen. Genau das ist die Qualitätsschraube, und genau das kann ein Modell gut, wenn man es explizit verlangt.
Zwei Grenzen, die kein Werkzeug für dich zieht. Erstens: Ein Entwurf, der lokal durchgeht, kann in der CI trotzdem scheitern. Zweitens, und wichtiger: Für eine Sprache, die du nicht als Muttersprachler beherrschst, braucht KI-generierter Inhalt eine Muttersprachler-Prüfung, bevor du ihn veröffentlichst. Kein Validator fängt eine plausibel formulierte, aber falsche Aussage.
Zusammenfassung
- Ohne Code legst du Lektionen direkt in der App an, unter “Meine Inhalte”. Das reicht, um das Format zu verstehen.
- Als Entwickler startest du vom Template-Repository über Use this template, trägst deine Sets in
manifest.yamlein, schreibst Lektionen als JSON gegen das eigenständige Paketlearn-content-engine, validierst mitmake validateund verbindest das Repository mit der App. - Das Format ist offen und bewusst app-unabhängig. Deine Inhalte sind an keine einzelne Anwendung gebunden.
- Die Qualität einer Aufgabe entscheidet sich an den falschen Antworten, nicht an den richtigen.
- KI-Generierung ist ein Entwurfswerkzeug mit zwei Toren: dem Validator, den ein Skript für dich bedient, und der Muttersprachler-Prüfung, die niemand automatisieren kann.
Wenn du etwas gebaut hast, das andere nutzen könnten: Das Format existiert genau dafür.

메타데이터
- post_id
- 52c3d5b9ccf9
- slug
- eigene-lektionen-für-adaptive-learner-von-der-ersten-aufgabe-zum-teilbaren-content-repository-52c3d5b9ccf9
- url
- https://medium.com/@asterios-raptis/eigene-lektionen-f%C3%BCr-adaptive-learner-von-der-ersten-aufgabe-zum-teilbaren-content-repository-52c3d5b9ccf9
- canonical_url
- https://medium.com/@asterios-raptis/eigene-lektionen-f%C3%BCr-adaptive-learner-von-der-ersten-aufgabe-zum-teilbaren-content-repository-52c3d5b9ccf9
- author_url
- https://medium.com/@asterios-raptis
- status
- ok
- fetched_at
- 2026-07-11 06:06:52