Training-Harness (ACO, Multi-Fidelity), Prüfstand-Benchmark, Agenten-README

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
team3
2026-07-04 12:47:19 +02:00
parent 8d8f6c8e51
commit 8488737303
14 changed files with 772 additions and 267 deletions

View File

@@ -1,4 +1,4 @@
.PHONY: install dev prod stop logs remove auth sync sync-projects sync-all sync-all-reverse projects searxng ollama qa test test-e2e train .PHONY: install dev prod stop logs remove auth sync sync-projects sync-all sync-all-reverse projects searxng ollama qa test test-e2e train train-init
COMPOSE = docker compose COMPOSE = docker compose
@@ -127,4 +127,9 @@ test-e2e:
# Achtung: jeder Trial ist ein echter Mini-Lauf (MiniMax-Tokens, Minuten). # Achtung: jeder Trial ist ein echter Mini-Lauf (MiniMax-Tokens, Minuten).
train: train:
@set -a; [ -f .env ] && . ./.env; set +a; \ @set -a; [ -f .env ] && . ./.env; set +a; \
cd backend && python3 train.py --trials $(or $(TRIALS),40) --stunden $(or $(STUNDEN),12) cd backend && python3 train.py --trials $(or $(TRIALS),40) --stunden $(or $(STUNDEN),12) --ameisen $(or $(AMEISEN),3)
# Frozen-Inventar-Vorlage für das Training bauen (einmalig, echter Mini-Lauf)
train-init:
@set -a; [ -f .env ] && . ./.env; set +a; \
cd backend && python3 train.py --init

125
README.md
View File

@@ -1,4 +1,127 @@
# Creator — Features # Creator
KI-Lernguide-Generator: Aus einem Thema, Uni-Skript, Projektordner oder Web-Link entsteht
ein vollständiger, belegter Lernguide mit Übungssystem. FastAPI-Backend (`backend/`),
Vue-Frontend (`frontend/`), SQLite (`storage/creator.db`). MiniMax generiert über die
OpenCode-CLI, Judge-Agenten prüfen jede Stufe.
**Diese README ist das Onboarding für den nächsten KI-Agenten.** Endnutzer-Features stehen
unten. Persistente Detail-Notizen liegen im Claude-Memory des Projekts; dieses Dokument
trägt das Wesentliche.
## Wofür das Projekt gebaut wird (die Gründe des Betreibers)
- Lernen mit **Entscheidungsautomatik statt Wahlfreiheit**: Das System zerlegt, priorisiert
und prüft — der Lernende folgt dem Pfad, statt ihn zu bauen.
- **100-%-Zerlegung** des Stoffs in Bausteine und Subbausteine, jede Aussage mit Beleg.
- **MECE-Nordstern** (Kern des Projekts, wörtlich): „Man kann keinen Baustein entfernen,
ohne eine Lücke zu erzeugen, und keinen hinzufügen, ohne dass eine Dopplung entsteht."
- Ideal: „Die Pipeline läuft durch, es ist eine 10/10, die Inhalte sind super —
nicht zu viel, nicht zu wenig, korrekt."
## Entwicklungsphase: die vier Optimierungsziele
Alle Arbeit optimiert, themenunabhängig:
1. **Qualität** — Korrektheit maximieren.
2. **Auswahl** — Lücken und Dopplungen minimieren (MECE).
3. **Performance** — Gesamtlaufzeit minimieren.
4. **Tokenverbrauch** — Generierung günstig halten.
Fixes gehören in die **Pipeline** (Generierung). QA misst nur — Detektor-Konstanten und
Notengewichte sind nie Teil einer Optimierung (Messinvarianz).
## Architektur in einer Minute
Drei Kanban-Boards (Engine: `kanban.py`, Karten in SQLite, resümierbar):
1. **Inventar** (`board_inventory.py`): Research-Reader → Titel-Ingest → Cluster →
Konsens-Gate (+ Anker-Beleg gegen Kanon-Halluzination) → Naming (darf abstrahieren,
Anker-Pflicht) → Fragment-Filter → Dedup → Gruppierung → fertige Blöcke.
Danach QA-Gate (Note < 9.5 pausiert vor Board 2).
2. **Artefakte** (`board_artefacts.py`): Subbausteine finden (Panel-Konsens) → Facts
(extract-once, Grounding für alles Spätere) → In-Block-Konsolidierung + Lücken-Nachfass
→ Levels → Relevanz → Cross-Block-Dedup (Barriere, gechunkt) → Fragen → Flashcards/
Beispiele → Finalize (DB-Spiegel, Hygiene) → Outline.
3. **Guide** (`guide_board.py`): Lernziele → Writer (Marker-Format, Längen-Budget) →
Fakten-Gate (CoVe; „falsch" fixt immer, „unbelegt" ab Schwelle) → Coverage → Lesbarkeit.
Agenten-Rollen: `quick`/`fast` generieren, `judge` prüft (native MiniMax-Route — die
kalt-Route stallte 20 % der Calls), `guide` schreibt. Große JSON-Antworten kommen als
TEXT zurück (`_sink_or_file`) — Datei-schreibende Agenten verloren 40 Runden in
JSON-Reparatur-Schleifen.
## Arbeitsregeln (verbindlich, aus Erfahrung destilliert)
- **Nie committen/pushen.** Der Betreiber committet selbst.
- **Ändern nur auf Auftrag.** Beobachtungen berichten, nicht eigenmächtig fixen.
Ergebnisse nie nachträglich schönen.
- **Kein Backend-Edit bei laufendem Flow**: vorher `curl -s localhost:8000/api/blocks/active`
== `[]` prüfen — `backend/*.py`-Edits triggern den uvicorn-Reload und killen Läufe.
`templates/` und `Makefile` sind gefahrlos.
- **Generisch bleiben**: keine Domänen-Sonderregeln in Pipeline/Prompts. Quellen-Spezifika
löst der Import.
- **Ursachen statt Symptome**: erst messen (Events, QA-Reports, OpenCode-Session-DB),
dann fixen. Kein Raten.
- **Fragen zuerst beantworten**, dann handeln. Antworten knapp und auf Deutsch.
- Keine Bewertungen fremder KI-Modelle/Provider (Geschwindigkeit, Qualität).
- `.env` enthält echte API-Keys — nie exponieren.
## Werkzeuge für Entwicklung und Diagnose
| Kommando | Zweck |
|---|---|
| `make test` | ganze Suite (~240 Tests, ~20 s), inkl. Fake-E2E |
| `make test-e2e` | nur Fake-E2E: kompletter Generierungspfad in Sekunden, ohne LLM |
| `make qa TOPIC=… [LLM=1]` | Inventar-/Artefakt-QA read-only, Note 010 |
| `make qa-guide TOPIC=… [LLM=1]` | Guide-QA |
| `make train-init` | Frozen-Inventar-Vorlage für das Training bauen (einmalig) |
| `make train [TRIALS] [STUNDEN] [AMEISEN]` | Ameisen-Optimierung der Parameter (anytime) |
| `CREATOR_FAKE_AGENTS=1 make dev` | Server antwortet aus der Fake-Welt — UI-Smoke in Sekunden |
| `CREATOR_PARAMS='{"X":1}'` | Parameter-Override pro Prozess (Registry: `backend/train_params.py`) |
Diagnose-Quellen: `events`-Tabelle (Agent-Dauern/Tokens/Status je Lauf),
`storage/qa/<topic>/*.json` (Report-Historie), `arbeit/lauf-summary.json`,
OpenCode-Session-DB (`~/.local/share/opencode/opencode.db` — Turns/Tokens je Agent).
## Training (`make train`)
Ameisen-Algorithmus (ACO), anytime: Pheromon-Gewichte je Parameter-Stufe steuern die
Kandidaten; je länger er läuft, desto gezielter die Tests. Drei Fidelity-Stufen:
F0 Fake-E2E (0,5 s, Invarianten + Struktur-Proxy), F1 Frozen-Inventar (~58 min, Board 2
auf kopiertem Inventar), F2 Volllauf mit Soll-Abgleich gegen
`benchmarks/pruefstand/soll.json` (konstruiertes Thema mit bekannter Lösung und
eingebauten Fallen). Übernahme nur nach Bestätigungslauf. Ergebnis:
`storage/train/aco/{report.md, beste_params.json}`; Übernahme nach `config.py` ist
manuell.
## Entwicklungs-Meilensteine (was schon gelernt wurde)
- MECE-Regelkreis: In-Block-Konsolidierung (2-Judge-Panel, Einstimmigkeit), Lücken-Nachfass
mit hartem Beleg-Gate, Cross-Block-Dedup mit Stichentscheid — Sub-Zahl 425→~213 bei
steigender Note.
- Drei stabile QA-Noten (Inventar/Artefakte/Guide) mit Bestätiger-Pässen gegen
Judge-Rauschen; Repair arbeitet Befunde gezielt ab.
- Resume-Dateien tragen einen Sub-Satz-Hash — Re-Runs übernehmen nie stale Ergebnisse;
Finalize löscht Alt-Reste (Lösch-Hygiene überall).
- Facts-Nachfass: kein consensus-Sub ohne Grounding (sonst flutet das Fakten-Gate).
- Judge-Stalls (20 % Timeouts) lagen an einer Provider-Route — Messen vor Raten.
- Naming darf abstrahieren, aber nur korpus-verankert (Kanon-Halluzinations-Schutz).
## Offene Ideen / nächste Schritte
- Training auf dem Server laufen lassen (siehe unten), wirksame Parameter übernehmen.
- Facts-Chunks parallelisieren; Live-Aktivität (Token-Zähler) an laufenden Karten zeigen.
- Bekannte Cross-Dubletten knapp unter dem 0.75-Kandidaten-Floor.
- Roadmap-Lernarchitektur: ein Guide + Stufen-Ansichten, ELO-Score mit wachsendem Cap.
## Server-Betrieb mit 8 GB RAM
- `.env`: `MAX_CONCURRENT_AGENTS=6`, `MAX_CONCURRENT_AGENTS_PER_TOPIC=6`.
- Training: `make train AMEISEN=1` (jeder parallele Trial lädt das Embedding-Modell, ~1 GB).
- Embedding + Readability halten zusammen ~1 GB im Backend-Prozess. 24 GB Swap anlegen.
---
# Features (Endnutzer-Sicht)
## Quellen ## Quellen
- Freies Thema: die KI recherchiert den Stoff selbst im Web. - Freies Thema: die KI recherchiert den Stoff selbst im Web.

View File

@@ -1701,7 +1701,8 @@ async def _write_run_summary(topic: str, flow: Flow):
**await db.events_run_summary(topic, run_id)} **await db.events_run_summary(topic, run_id)}
try: # Abschluss-QA MIT Judges: sub_dubletten/unechte werden beurteilt — erst damit try: # Abschluss-QA MIT Judges: sub_dubletten/unechte werden beurteilt — erst damit
import qa # ist note_artefakte belastbar (Kandidatenliste allein zählt nicht) import qa # ist note_artefakte belastbar (Kandidatenliste allein zählt nicht)
report = await qa.qa_report(topic, llm=True) from config import ABSCHLUSS_QA_LLM # Training schaltet die Judges ab (misst selbst)
report = await qa.qa_report(topic, llm=bool(ABSCHLUSS_QA_LLM))
if report: if report:
summary["note"] = report["note"] summary["note"] = report["note"]
summary["note_artefakte"] = report.get("note_artefakte") summary["note_artefakte"] = report.get("note_artefakte")

View File

@@ -196,6 +196,7 @@ RETRY_BACKOFF = 30.0 # base seconds; backoff = base · 2^(retries-1)
MAX_RESTARTS = 2 # agent restart cap per race slot MAX_RESTARTS = 2 # agent restart cap per race slot
JUDGE_CHUNK = 40 # repair: findings per judge call JUDGE_CHUNK = 40 # repair: findings per judge call
EVIDENCE_PER_BLOCK = 6000 # repair: excerpt chars per fremd candidate EVIDENCE_PER_BLOCK = 6000 # repair: excerpt chars per fremd candidate
ABSCHLUSS_QA_LLM = 1 # 0 = Abschluss-QA ohne LLM-Judges (Training misst selbst; spart Minuten)
# Timeouts per agent step: (base seconds, seconds per block/section). # Timeouts per agent step: (base seconds, seconds per block/section).
# Applies equally to all providers — whoever is too slow gets restarted or overtaken. # Applies equally to all providers — whoever is too slow gets restarted or overtaken.

View File

@@ -1186,6 +1186,24 @@ async def default_subblock_levels(topic: str, block_norm: str) -> None:
await db.commit() await db.commit()
async def copy_topic(quelle: str, ziel: str) -> None:
"""Trainings-Helfer: Kanban-Karten + Block-Rows der Quelle unter neuem Topic duplizieren
(Frozen-Inventar-Trials — Board 2 läuft auf identischem Board-1-Stand neu). Nur DB;
Dateien (source.json/blocks.md) kopiert der Runner."""
db = await get_db()
await db.execute("DELETE FROM kanban_cards WHERE topic = ?", (ziel,))
await db.execute("DELETE FROM blocks WHERE topic = ?", (ziel,))
await db.execute(
"""INSERT INTO kanban_cards (topic, board, card_id, kind, stage, payload, retries, not_before, last_error, updated_at)
SELECT ?, board, card_id, kind, stage, payload, 0, 0, '', updated_at
FROM kanban_cards WHERE topic = ?""", (ziel, quelle))
await db.execute(
"""INSERT INTO blocks (topic, title_norm, title, description, mentions, status, sources, reader, updated_at)
SELECT ?, title_norm, title, description, mentions, status, sources, reader, updated_at
FROM blocks WHERE topic = ?""", (ziel, quelle))
await db.commit()
async def delete_stale_consensus(topic: str, block_norm: str, keep: set[str]) -> None: async def delete_stale_consensus(topic: str, block_norm: str, keep: set[str]) -> None:
"""Drop consensus rows of a block that are NOT in this run's sidecar (`keep`): finalize """Drop consensus rows of a block that are NOT in this run's sidecar (`keep`): finalize
only upserts, so re-runs piled up orphan rows (measured: 25 subs without any board-2 only upserts, so re-runs piled up orphan rows (measured: 25 subs without any board-2

View File

@@ -287,3 +287,59 @@ async def respond(agent_key: str, prompt: str, capabilities: str) -> tuple[int,
if _WELT is None: if _WELT is None:
_WELT = Welt() _WELT = Welt()
return _WELT.respond(agent_key, prompt, capabilities) return _WELT.respond(agent_key, prompt, capabilities)
def aktivieren(welt: Welt, setattr_fn=setattr) -> None:
"""Alle Patches für einen Fake-E2E-Lauf (run_agent überall, Tempo-Bremsen raus,
Text-Identitäts-Embedding). pytest übergibt monkeypatch.setattr (auto-Rollback);
train_f0 nutzt den Default — der Prozess stirbt nach dem Lauf sowieso."""
import asyncio
import agents
import blocks
import board_artefacts as ba
import board_inventory as bi
import guide
import guide_board
import kanban
import pipeline
import qa
import repair
async def fake_run_agent(agent_key, prompt, timeout, provider="claude", role="fast",
capabilities="none", lane="batch", scope=None, on_line=None, label=""):
return welt.respond(agent_key, prompt, capabilities)
for mod in (agents, pipeline, blocks, guide, repair):
setattr_fn(mod, "run_agent", fake_run_agent)
setattr_fn(blocks, "CONSENSUS_GRACE", 0)
setattr_fn(bi, "_QA_GATE_POLL", 0.05)
setattr_fn(kanban, "RETRY_BACKOFF", 0.05)
setattr_fn(guide_board, "READABILITY_ACTIVE", False)
setattr_fn(bi, "_ingest_lock", asyncio.Lock())
class _FakeEmb: # identischer Text → cos 1.0, sonst 0.0 (deterministisch, ohne Modell)
@staticmethod
def available():
return True
@staticmethod
def embed(texts):
import numpy as np
uniq = {t: k for k, t in enumerate(dict.fromkeys(texts))}
arr = np.zeros((len(texts), max(len(uniq), 1)))
for r, t in enumerate(texts):
arr[r, uniq[t]] = 1.0
return arr
@staticmethod
def embed_sims(texts):
arr = _FakeEmb.embed(texts)
return arr @ arr.T
for mod in (blocks, ba, qa):
setattr_fn(mod, "embedding", _FakeEmb)
async def emb_ok(flow): # Board-1-Vektorpfade aus — Judge-Wellen reichen
return False
setattr_fn(bi, "_emb_ok", emb_ok)

View File

@@ -22,63 +22,10 @@ async def testdb(tmp_path, monkeypatch):
async def fake_welt(testdb, tmp_path, monkeypatch): async def fake_welt(testdb, tmp_path, monkeypatch):
"""E2E ohne LLM: run_agent überall durch die Fake-Welt ersetzt, Tempo-Bremsen raus. """E2E ohne LLM: run_agent überall durch die Fake-Welt ersetzt, Tempo-Bremsen raus.
Alle echten Schichten (_race, Quorum, Panels, Producer, QA-Gate) laufen mit.""" Alle echten Schichten (_race, Quorum, Panels, Producer, QA-Gate) laufen mit."""
import agents
import blocks
import board_inventory as bi
import guide
import kanban
import pipeline
import qa import qa
import repair from fake_agents import Welt, aktivieren
from fake_agents import Welt
welt = Welt() welt = Welt()
aktivieren(welt, setattr_fn=monkeypatch.setattr)
async def fake_run_agent(agent_key, prompt, timeout, provider="claude", role="fast", monkeypatch.setattr(qa, "QA_DIR", tmp_path / "qa") # Reports nie in echte Nutzdaten
capabilities="none", lane="batch", scope=None, on_line=None, label=""):
return welt.respond(agent_key, prompt, capabilities)
for mod in (agents, pipeline, blocks, guide, repair):
monkeypatch.setattr(mod, "run_agent", fake_run_agent)
# Tempo: grace/poll/backoff bremsen echte Läufe, nicht den Fake
monkeypatch.setattr(blocks, "CONSENSUS_GRACE", 0)
monkeypatch.setattr(bi, "_QA_GATE_POLL", 0.05)
monkeypatch.setattr(kanban, "RETRY_BACKOFF", 0.05)
monkeypatch.setattr(qa, "QA_DIR", tmp_path / "qa")
import guide_board
monkeypatch.setattr(guide_board, "READABILITY_ACTIVE", False) # kein Modell-Load im Test
import asyncio as _aio
monkeypatch.setattr(bi, "_ingest_lock", _aio.Lock()) # Modul-Lock klebt sonst am Vortest-Loop
class _FakeEmb: # identischer Text → cos 1.0, sonst 0.0 (deterministisch, ohne Modell)
@staticmethod
def available():
return True
@staticmethod
def embed_sims(texts):
import numpy as np
uniq = {t: k for k, t in enumerate(dict.fromkeys(texts))}
arr = np.zeros((len(texts), max(len(uniq), 1)))
for r, t in enumerate(texts):
arr[r, uniq[t]] = 1.0
return arr @ arr.T
@staticmethod
def embed(texts):
import numpy as np
uniq = {t: k for k, t in enumerate(dict.fromkeys(texts))}
arr = np.zeros((len(texts), max(len(uniq), 1)))
for r, t in enumerate(texts):
arr[r, uniq[t]] = 1.0
return arr
import board_artefacts as ba
for mod in (blocks, ba, qa):
monkeypatch.setattr(mod, "embedding", _FakeEmb)
async def emb_ok(flow): # Board-1-Vektorpfade aus (wie board_env) — Judge-Wellen reichen
return False
monkeypatch.setattr(bi, "_emb_ok", emb_ok)
return welt return welt

View File

@@ -1,4 +1,4 @@
"""Training-Harness: Registry↔config-Konsistenz, ENV-Override, Trainer-Logik (Stub-Runner).""" """Training-Harness: Registry↔config, ENV-Override, ACO-Trainer (Stub-Runner), Soll-Abgleich."""
import json import json
import subprocess import subprocess
@@ -6,20 +6,21 @@ import sys
from pathlib import Path from pathlib import Path
import config import config
import train
import train_params import train_params
from train import Trainer, score from train import AmeisenTrainer, score
from train_lauf import soll_abgleich
BACKEND = Path(__file__).resolve().parent.parent BACKEND = Path(__file__).resolve().parent.parent
def test_registry_spiegelt_config(): def test_registry_spiegelt_config():
"""Jeder Registry-Parameter existiert in config mit identischem Default und """Jeder Registry-Parameter existiert in config mit identischem Default, flow-sicheren
flow-sicheren Rändern — sonst optimiert der Trainer Phantome.""" Rändern und einer Fidelity-Zuordnung — sonst optimiert der Trainer Phantome."""
for name, p in train_params.PARAMS.items(): for name, p in train_params.PARAMS.items():
assert getattr(config, name, None) == p["default"], name assert getattr(config, name, None) == p["default"], name
assert p["min"] <= p["default"] <= p["max"], name assert p["min"] <= p["default"] <= p["max"], name
assert p["step"] > 0, name assert p["step"] > 0, name
assert p["fidelity"] in ("board2", "voll"), name
def test_creator_params_override_wirkt_im_subprozess(): def test_creator_params_override_wirkt_im_subprozess():
@@ -42,72 +43,120 @@ def _metrics(note=8.0, dauer=10.0, tokens=1_000_000, **quoten):
"dauer_min": dauer, "tokens": {"input": tokens, "output": 0}, "agents": {}} "dauer_min": dauer, "tokens": {"input": tokens, "output": 0}, "agents": {}}
def _stub_runner(antworten): def _stub(score_fn):
"""params-abhängige Metriken; zählt echte Aufrufe (Cache-Treffer zählen nicht).""" """Runner-Paar (F1/F2 + F0) für Tests: score_fn(params) → Metriken."""
calls = [] calls = []
async def runner(params, thema): async def runner(params, fidelity, suffix=""):
calls.append((dict(params), thema[0])) calls.append((dict(params), fidelity))
for muster, m in antworten: return score_fn(params)
if muster(params):
return dict(m) async def f0(params):
return _metrics() return {"ok": True, "invarianten_fehler": [], "calls": 100}
runner.calls = calls runner.calls = calls
return runner return runner, f0
async def test_screening_filtert_rauschen(tmp_path): def _trainer(tmp_path, runner, f0, **kw):
"""Nur Parameter mit Effekt über der Rausch-Schwelle kommen in die Feinphase; args = dict(max_trials=999, max_stunden=1, ameisen=3, seed=7, f2_intervall=1000)
ein bestätigter Gewinner wird übernommen.""" args.update(kw)
wirksam = "FACTS_CHUNK_SUBS" return AmeisenTrainer(tmp_path / "s", runner=runner, runner_f0=f0, **args)
runner = _stub_runner([
(lambda p: p.get(wirksam) == 8, _metrics(note=9.5, dauer=8.0)), # klar besser
]) async def test_aco_konvergiert_auf_optimum(tmp_path):
t = Trainer(tmp_path / "s", max_trials=999, max_stunden=1, runner=runner) """Gepflanztes Optimum (FACTS_CHUNK_SUBS=6) wird gefunden und bestätigt übernommen;
die Pheromon-Spur konzentriert sich dort."""
def bewertung(params):
return _metrics(note=9.5, dauer=7.0) if params.get("FACTS_CHUNK_SUBS") == 6 else _metrics()
runner, f0 = _stub(bewertung)
t = _trainer(tmp_path, runner, f0, max_trials=120)
best = await t.run() best = await t.run()
assert best.get(wirksam) == 8 assert best.get("FACTS_CHUNK_SUBS") == 6
# kein anderer Parameter übernommen (alle anderen Δ=0 < Schwelle) taus = t.pheromon["FACTS_CHUNK_SUBS"]
assert set(best) == {wirksam} assert max(taus, key=lambda k: taus[k]) == "6"
async def test_uebernahme_braucht_bestaetigung(tmp_path): async def test_uebernahme_braucht_bestaetigung(tmp_path):
"""Einmaliger Glückstreffer ohne bestätigten Zweitlauf wird verworfen.""" """Einmaliger Glückstreffer ohne bestätigten Zweitlauf wird nicht Bester."""
zustand = {"mal": 0} zustand = {"mal": 0}
async def runner(params, thema): def bewertung(params):
if params.get("FACTS_CHUNK_SUBS") == 8: if params.get("FACTS_CHUNK_SUBS") == 6:
zustand["mal"] += 1 zustand["mal"] += 1
return _metrics(note=9.5) if zustand["mal"] == 1 else _metrics(note=8.0) return _metrics(note=9.5) if zustand["mal"] == 1 else _metrics(note=8.0)
return _metrics() return _metrics()
t = Trainer(tmp_path / "s", max_trials=999, max_stunden=1, runner=runner) runner, f0 = _stub(bewertung)
t = _trainer(tmp_path, runner, f0, max_trials=40)
best = await t.run() best = await t.run()
assert best == {} assert best.get("FACTS_CHUNK_SUBS") != 6
async def test_cache_resume_wiederholt_keine_trials(tmp_path): async def test_f0_filter_verwirft_kaputte_kandidaten(tmp_path):
runner = _stub_runner([]) """Kandidaten mit Invarianten-Fehlern erreichen nie einen bezahlten Lauf."""
t = Trainer(tmp_path / "s", max_trials=999, max_stunden=1, runner=runner) runner, _f0 = _stub(lambda p: _metrics())
async def f0_kaputt(params):
if params: # nur Nicht-Baseline
return {"ok": True, "invarianten_fehler": ["kaputt"], "calls": 100}
return {"ok": True, "invarianten_fehler": [], "calls": 100}
t = _trainer(tmp_path, runner, f0_kaputt, max_trials=20)
await t.run() await t.run()
erste = len(runner.calls) bezahlt_mit_params = [c for c, _f in runner.calls if c]
t2 = Trainer(tmp_path / "s", max_trials=999, max_stunden=1, runner=runner) assert bezahlt_mit_params == [] # nur Baselines liefen
await t2.run()
assert len(runner.calls) == erste # alles aus trials.jsonl bedient
async def test_resume_laedt_pheromon_und_cache(tmp_path):
def bewertung(params):
return _metrics(note=9.5) if params.get("FACTS_CHUNK_SUBS") == 6 else _metrics()
runner, f0 = _stub(bewertung)
t = _trainer(tmp_path, runner, f0, max_trials=60)
await t.run()
best, tau = t.best_params, dict(t.pheromon["FACTS_CHUNK_SUBS"])
runner2, f02 = _stub(bewertung)
t2 = _trainer(tmp_path, runner2, f02, max_trials=0) # kein Budget: alles aus Persistenz
assert t2.best_params == best
assert t2.pheromon["FACTS_CHUNK_SUBS"] == tau
async def test_budget_stoppt(tmp_path): async def test_budget_stoppt(tmp_path):
runner = _stub_runner([]) runner, f0 = _stub(lambda p: _metrics())
t = Trainer(tmp_path / "s", max_trials=3, max_stunden=1, runner=runner) t = _trainer(tmp_path, runner, f0, max_trials=4)
await t.run() await t.run()
assert len(runner.calls) <= 3 assert len(runner.calls) <= 4
def test_score_richtungen(): def test_score_richtungen():
basis = _metrics() basis = _metrics()
besser = _metrics(note=9.0) assert score(_metrics(note=9.0), basis) > score(basis, basis)
teurer = _metrics(dauer=20.0, tokens=2_000_000) assert score(_metrics(dauer=20.0, tokens=2_000_000), basis) < score(basis, basis)
assert score(besser, basis) > score(basis, basis) assert score(_metrics(fremd=0.2, luecken=0.1), basis) < score(basis, basis)
assert score(teurer, basis) < score(basis, basis) mit_soll = dict(_metrics(), soll={"f1": 1.0})
mit_befunden = _metrics(fremd=0.2, luecken=0.1) ohne_soll = dict(_metrics(), soll={"f1": 0.5})
assert score(mit_befunden, basis) < score(basis, basis) assert score(mit_soll, basis) > score(ohne_soll, basis)
def test_soll_abgleich():
soll = {"bloecke": [{"titel": "Symmetrische Verschlüsselung"},
{"titel": "Asymmetrische Verschlüsselung",
"alternativen": ["Public-Key-Kryptographie"]},
{"titel": "Digitale Signaturen"}]}
r = soll_abgleich(["Symmetrische Verschlüsselung", "Public-Key-Kryptographie", "Quantencomputer"], soll)
assert r["fehlend"] == ["Digitale Signaturen"]
assert r["extra"] == ["Quantencomputer"]
assert 0 < r["f1"] < 1
async def test_copy_topic_dupliziert_karten_und_bloecke(testdb):
db = testdb
await db.kanban_upsert_card("q", "inventory", "b1", "block", "done_block", {"title": "Alpha"})
await db.upsert_block("q", "alpha", "Alpha", "Beschreibung", ["s1"], "r1")
await db.copy_topic("q", "z")
karten = await db.kanban_cards("z")
assert [c["card_id"] for c in karten] == ["b1"]
bloecke = await db.list_blocks("z")
assert [b["title"] for b in bloecke] == ["Alpha"]

View File

@@ -1,35 +1,61 @@
"""make train: Parameter-Optimierung auf Mini-Themen (Baseline → Screening → Koordinaten-Suche). """make train: Ameisen-Optimierung (ACO) der Pipeline-Parameter — anytime, multi-fidelity.
Jeder Trial ist ein Subprozess (train_lauf.py) mit CREATOR_PARAMS im ENV — so binden die Prinzip: Pheromon-Gewichte je (Parameter, Stufe) steuern, welche Kandidaten („Ameisen")
Module die überschriebenen Werte beim Import. Metriken sind deterministisch (qa_report als Nächstes getestet werden. Gute Kandidaten verstärken ihre Stufen, Verdunstung hält
ohne LLM); gegen Judge-/Lauf-Rauschen gilt: Baseline mit Wiederholung liefert die die Suche offen — je länger der Trainer läuft, desto gezielter werden die Tests.
Rausch-Schwelle, und eine Übernahme braucht einen BESTÄTIGUNGSLAUF (sonst Random Walk). Jederzeit stoppbar; der Stand (beste_params.json/report.md) ist immer aktuell.
CLI: python3 train.py [--trials 40] [--stunden 8] [--sitzung NAME] Fidelity-Kaskade pro Kandidat:
Ergebnis: storage/train/<sitzung>/{trials.jsonl, report.md, beste_params.json} F0 Fake-E2E (train_f0.py, Sekunden, 0 Tokens): Invarianten + Struktur-Proxy — Filter.
F1 Frozen-Inventar (train_lauf.py --board2, ~58 min): misst Board-2/Guide-Parameter.
F2 Volllauf inkl. Soll-Abgleich: alle N Runden für den Besten + Inventar-Parameter.
CLI: python3 train.py [--stunden 8] [--trials 60] [--ameisen 3] [--seed 0]
[--sitzung NAME] [--f2-intervall 5]
python3 train.py --init (baut das Frozen-Inventar-Vorlage-Topic, einmalig)
Ergebnis: storage/train/<sitzung>/{trials.jsonl, pheromon.json, report.md, beste_params.json}
""" """
import argparse import argparse
import asyncio import asyncio
import hashlib import hashlib
import json import json
import os
import random
import sys import sys
import time import time
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from config import STORAGE_DIR from config import STORAGE_DIR
from train_params import PARAMS, schritte from train_params import PARAMS
HAUPT_THEMA = ("train-sort", "benchmarks/sortierverfahren") VORLAGE_TOPIC = "train-vorlage"
VALIDIER_THEMA = ("train-foto", "benchmarks/fotografie") BENCHMARK = "benchmarks/pruefstand"
# Score-Gewichte: Qualität + Auswahl dominieren (Entwicklungsphase), Kosten ziehen ab. # Score-Gewichte: Qualität + Auswahl dominieren (Entwicklungsphase), Kosten ziehen ab.
W_NOTE, W_AUSWAHL, W_ZEIT, W_TOKEN = 4.0, 4.0, 1.0, 1.0 W_NOTE, W_AUSWAHL, W_ZEIT, W_TOKEN = 4.0, 4.0, 1.0, 1.0
RHO = 0.2 # Pheromon-Verdunstung je Runde
SPARSITY = 0.5 # Wahrscheinlichkeit, dass eine Ameise einen Parameter auf Default lässt
F0_CALL_FAKTOR = 1.5 # Struktur-Proxy: mehr als 1.5× Baseline-Calls → Kandidat verworfen
# Trainings-Fixa (Speed, kein Suchraum): Abschluss-QA ohne Judges, kurze Nachzügler-Gnade
TRAIN_FIXA = {"ABSCHLUSS_QA_LLM": 0, "CONSENSUS_GRACE": 60}
def stufen(name: str) -> list[float]:
p = PARAMS[name]
out, w = [], p["min"]
while w <= p["max"] + 1e-9:
out.append(round(w, 4))
w += p["step"]
return out
def score(m: dict, basis: dict) -> float: def score(m: dict, basis: dict) -> float:
"""Skalarer Vergleichswert eines Trials. note 010; auswahl aus den MECE-Quoten; """Skalarer Vergleichswert. note 010; auswahl aus Soll-Abgleich (F2) oder MECE-Quoten;
Zeit/Tokens normiert auf die Baseline (1.0 = Baseline-Kosten).""" Zeit/Tokens normiert auf die Baseline derselben Fidelity."""
if m.get("soll"):
auswahl = 10.0 * m["soll"]["f1"]
else:
q = m.get("quoten") or {} q = m.get("quoten") or {}
qa_ = m.get("quoten_artefakte") or {} qa_ = m.get("quoten_artefakte") or {}
auswahl = 10.0 * max(0.0, 1.0 - min(1.0, ( auswahl = 10.0 * max(0.0, 1.0 - min(1.0, (
@@ -46,151 +72,269 @@ def _tokens(m: dict) -> int:
return int(t.get("input") or 0) + int(t.get("output") or 0) return int(t.get("input") or 0) + int(t.get("output") or 0)
class Trainer: class AmeisenTrainer:
def __init__(self, sitzung: Path, max_trials: int, max_stunden: float, runner=None): def __init__(self, sitzung: Path, *, max_trials: int, max_stunden: float, ameisen: int = 3,
seed: int = 0, f2_intervall: int = 5, runner=None, runner_f0=None):
self.dir = sitzung self.dir = sitzung
self.dir.mkdir(parents=True, exist_ok=True) self.dir.mkdir(parents=True, exist_ok=True)
self.cache_pfad = self.dir / "trials.jsonl" self.rng = random.Random(seed)
self.cache: dict[str, dict] = {} self.ameisen = ameisen
if self.cache_pfad.exists(): # Resume: bezahlte Trials nie wiederholen self.f2_intervall = max(f2_intervall, 1)
for line in self.cache_pfad.read_text(encoding="utf-8").splitlines():
e = json.loads(line)
self.cache[e["key"]] = e["metrics"]
self.max_trials = max_trials self.max_trials = max_trials
self.deadline = time.monotonic() + max_stunden * 3600 self.deadline = time.monotonic() + max_stunden * 3600
self.gezahlt = 0 self.gezahlt = 0
self.runner = runner or self._subprozess self.runner = runner or self._subprozess # (params, fidelity) -> metrics|None
self.log = [] self.runner_f0 = runner_f0 or self._subprozess_f0 # (params) -> {"ok","calls",…}|None
self.log: list[str] = []
self.basis: dict[str, dict] = {} # Fidelity → Baseline-Metriken
self.f0_basis: int | None = None
self.best_params: dict = {}
self.best_score: float | None = None
self.rauschen = 0.5
# Pheromon + Trial-Cache (Resume)
self.pheromon: dict[str, dict[str, float]] = {
n: {str(s): 1.0 for s in stufen(n)} for n in PARAMS}
ph = self.dir / "pheromon.json"
if ph.exists():
gespeichert = json.loads(ph.read_text(encoding="utf-8"))
for n, taus in gespeichert.get("pheromon", {}).items():
if n in self.pheromon:
self.pheromon[n].update({k: float(v) for k, v in taus.items()})
self.best_params = gespeichert.get("best_params", {})
self.best_score = gespeichert.get("best_score")
self.cache_pfad = self.dir / "trials.jsonl"
self.cache: dict[str, dict] = {}
if self.cache_pfad.exists():
for line in self.cache_pfad.read_text(encoding="utf-8").splitlines():
e = json.loads(line)
self.cache[e["key"]] = e["metrics"]
# ── Kandidaten ──────────────────────────────────────────────────────────────────
def kandidat(self, fidelity: str) -> dict:
"""Eine Ameise: je Parameter der Fidelity mit SPARSITY auf Default, sonst
Pheromon-gewichtete Stufe. Sparsame Kandidaten → saubere Attribution."""
params = {}
for name, p in PARAMS.items():
if fidelity == "board2" and p["fidelity"] != "board2":
continue
if self.rng.random() < SPARSITY:
continue
st = stufen(name)
taus = [self.pheromon[name][str(s)] for s in st]
wert = self.rng.choices(st, weights=taus)[0]
if wert != p["default"]:
params[name] = wert
return params
# ── Trial-Ausführung ──────────────────────────────────────────────────────────── # ── Trial-Ausführung ────────────────────────────────────────────────────────────
def _key(self, params: dict, thema: tuple, tag: str = "") -> str: def _key(self, params: dict, fidelity: str, tag: str = "") -> str:
raw = json.dumps({"p": params, "t": thema[0], "tag": tag}, sort_keys=True) raw = json.dumps({"p": params, "f": fidelity, "tag": tag}, sort_keys=True)
return hashlib.md5(raw.encode()).hexdigest()[:12] return hashlib.md5(raw.encode()).hexdigest()[:12]
async def trial(self, params: dict, thema: tuple = HAUPT_THEMA, tag: str = "") -> dict | None: async def trial(self, params: dict, fidelity: str, tag: str = "") -> dict | None:
"""tag unterscheidet bewusste Wiederholungen (Baseline n=2, Bestätigung).""" key = self._key(params, fidelity, tag)
key = self._key(params, thema, tag)
if key in self.cache: if key in self.cache:
return self.cache[key] return self.cache[key]
if self.gezahlt >= self.max_trials or time.monotonic() > self.deadline: if self.gezahlt >= self.max_trials or time.monotonic() > self.deadline:
return None return None
self.gezahlt += 1 self.gezahlt += 1
metrics = await self.runner(params, thema) metrics = await self.runner(params, fidelity, "0")
if metrics is not None: if metrics is not None:
with open(self.cache_pfad, "a", encoding="utf-8") as f: with open(self.cache_pfad, "a", encoding="utf-8") as f:
f.write(json.dumps({"key": key, "params": params, "thema": thema[0], f.write(json.dumps({"key": key, "params": params, "fidelity": fidelity,
"tag": tag, "metrics": metrics}, ensure_ascii=False) + "\n") "tag": tag, "metrics": metrics}, ensure_ascii=False) + "\n")
self.cache[key] = metrics self.cache[key] = metrics
return metrics return metrics
async def _subprozess(self, params: dict, thema: tuple) -> dict | None: async def _subprozess(self, params: dict, fidelity: str, topic_suffix: str = "0") -> dict | None:
out = self.dir / f"metrics-{self._key(params, thema)}.json" out = self.dir / f"metrics-{self._key(params, fidelity)}{topic_suffix}.json"
env = {"CREATOR_PARAMS": json.dumps(params)} topic = f"train-t{topic_suffix}"
import os args = ([topic, VORLAGE_TOPIC, str(out), "--board2"] if fidelity == "board2"
proc = await asyncio.create_subprocess_exec( else [topic, BENCHMARK, str(out)])
sys.executable, "train_lauf.py", thema[0], thema[1], str(out), env = {**os.environ, "CREATOR_PARAMS": json.dumps({**TRAIN_FIXA, **params})}
env={**os.environ, **env}) proc = await asyncio.create_subprocess_exec(sys.executable, "train_lauf.py", *args, env=env)
rc = await proc.wait() rc = await proc.wait()
if rc != 0 or not out.exists(): if rc != 0 or not out.exists():
self._log(f"Trial fehlgeschlagen (rc={rc}, params={params})") self._log(f"Trial fehlgeschlagen (rc={rc}, {fidelity}, params={params})")
return None return None
return json.loads(out.read_text(encoding="utf-8")) return json.loads(out.read_text(encoding="utf-8"))
async def _subprozess_f0(self, params: dict) -> dict | None:
out = self.dir / f"f0-{self._key(params, 'f0')}.json"
env = {**os.environ, "CREATOR_PARAMS": json.dumps(params)}
proc = await asyncio.create_subprocess_exec(sys.executable, "train_f0.py", str(out), env=env)
rc = await proc.wait()
return json.loads(out.read_text(encoding="utf-8")) if rc == 0 and out.exists() else None
def _log(self, msg: str) -> None: def _log(self, msg: str) -> None:
line = f"{datetime.now(timezone.utc).isoformat()[11:19]} {msg}" line = f"{datetime.now(timezone.utc).isoformat()[11:19]} {msg}"
print(line, flush=True) print(line, flush=True)
self.log.append(line) self.log.append(line)
# ── Trainings-Phasen ──────────────────────────────────────────────────────────── # ── Pheromon ────────────────────────────────────────────────────────────────────
def verstaerke(self, params: dict, delta: float) -> None:
for name, wert in params.items():
taus = self.pheromon[name]
key = str(wert)
if key in taus:
taus[key] += delta
def verdunste(self) -> None:
for taus in self.pheromon.values():
for k in taus:
taus[k] = max(0.1, (1 - RHO) * taus[k] + RHO * 1.0) # Drift zurück zu uniform
# ── Hauptschleife ───────────────────────────────────────────────────────────────
async def run(self) -> dict: async def run(self) -> dict:
# Phase 0: Baseline zweimal → Score-Basis + Rausch-Schwelle # Baseline F1 ×2 → Score-Basis + Rausch-Schwelle; F0-Basis für den Struktur-Proxy
self._log("Baseline (2 Läufe)…") f0 = await self.runner_f0({})
b1 = await self.trial({}, tag="baseline-1") self.f0_basis = (f0 or {}).get("calls")
b2 = await self.trial({}, tag="baseline-2") b1 = await self.trial({}, "board2", tag="baseline-1")
b2 = await self.trial({}, "board2", tag="baseline-2")
if not b1 or not b2: if not b1 or not b2:
self._log("Baseline unvollständig — Abbruch.") self._log("Baseline unvollständig — Abbruch.")
return {} return self.best_params
self.basis = b1 self.basis["board2"] = b1
s1, s2 = score(b1, b1), score(b2, b1) s1, s2 = score(b1, b1), score(b2, b1)
self.rauschen = max(abs(s1 - s2), 0.5) # Mindest-Schwelle gegen Glücks-Übernahmen self.rauschen = max(abs(s1 - s2), 0.5)
best_params: dict = {} if self.best_score is None:
best_score = max(s1, s2) self.best_score = max(s1, s2)
self._log(f"Baseline-Score {s1}/{s2}, Rausch-Schwelle {self.rauschen}") self._log(f"Baseline {s1}/{s2}, Rauschen {self.rauschen}, F0-Basis {self.f0_basis} Calls")
# Phase 1: Screening — je Parameter ±1 Schritt, Effekt vs. Rauschen runde, stagnation, gezahlt_vorher = 0, 0, self.gezahlt
effekte: list[tuple[float, str, float]] = [] # (|effekt|, name, bester_wert) while (self.gezahlt < self.max_trials and time.monotonic() < self.deadline
for name in PARAMS: and stagnation < 20): # konvergiert: nur noch Cache-Treffer → fertig
lo, hi = schritte(name) if runde > 0:
for wert in dict.fromkeys((lo, hi)): # lo==hi am Rand nur einmal stagnation = stagnation + 1 if self.gezahlt == gezahlt_vorher else 0
if wert == PARAMS[name]["default"]: gezahlt_vorher = self.gezahlt
runde += 1
fidelity = "voll" if runde % self.f2_intervall == 0 else "board2"
if fidelity == "voll" and "voll" not in self.basis:
base = await self.trial({}, "voll", tag="baseline-voll")
if base is None:
break
self.basis["voll"] = base
kandidaten = []
for _ in range(self.ameisen * 3): # ziehen bis K einzigartige nicht-leere da sind
k = self.kandidat(fidelity)
if k and k not in kandidaten and k != self.best_params:
kandidaten.append(k)
if len(kandidaten) >= self.ameisen:
break
if not kandidaten:
continue continue
m = await self.trial({**best_params, name: wert}) # F0-Filter: Invarianten + Struktur-Proxy, parallel, kostenlos
if m is None: f0s = await asyncio.gather(*[self.runner_f0(k) for k in kandidaten])
ueberlebende = []
for k, f in zip(kandidaten, f0s):
if f is None or not f.get("ok") or f.get("invarianten_fehler"):
self._log(f"F0 verwirft {k} (Invarianten)")
elif self.f0_basis and f.get("calls", 0) > self.f0_basis * F0_CALL_FAKTOR:
self._log(f"F0 verwirft {k} (Calls {f['calls']} > {self.f0_basis}×{F0_CALL_FAKTOR})")
else:
ueberlebende.append(k)
if not ueberlebende:
self.verdunste()
continue continue
delta = score(m, self.basis) - best_score # F1/F2 parallel (eigene Topic-Namen)
self._log(f"Screening {name}={wert}: Δ{delta:+.2f}") ergebnisse = await asyncio.gather(*[
if delta > self.rauschen: self._bewertet(k, fidelity, str(i + 1)) for i, k in enumerate(ueberlebende)])
effekte.append((delta, name, wert)) bewertet = [(k, m, score(m, self.basis[fidelity]))
effekte.sort(reverse=True) for k, m in ergebnisse if m is not None]
self._log(f"Wirksam: {[(n, w) for _, n, w in effekte]}") if not bewertet:
continue
bewertet.sort(key=lambda x: -x[2])
self.verdunste()
top_k, _top_m, top_s = bewertet[0]
self._log(f"Runde {runde} ({fidelity}): top {top_s} {top_k} "
f"(best {self.best_score})")
if top_s > (self.best_score or 0):
self.verstaerke(top_k, delta=1.0)
if top_s > (self.best_score or 0) + self.rauschen:
m2 = await self.trial(top_k, fidelity, tag="bestaetigung")
if m2 is not None and score(m2, self.basis[fidelity]) > self.best_score + self.rauschen:
self.best_params = top_k
self.best_score = min(top_s, score(m2, self.basis[fidelity]))
self._log(f"NEUER BESTER {self.best_params}{self.best_score}")
else:
self._log(f"{top_k}: nicht bestätigt")
self.verstaerke(self.best_params, delta=0.5) # Elite hält die Spur warm
self._speichern()
self._speichern()
return self.best_params
# Phase 2: Koordinaten-Suche über ALLE wirksamen Parameter (keine feste Obergrenze), async def _bewertet(self, params: dict, fidelity: str, suffix: str):
# Übernahme nur nach Bestätigungslauf if self.gezahlt >= self.max_trials or time.monotonic() > self.deadline:
for _, name, start_wert in effekte: return params, None
wert = start_wert key = self._key(params, fidelity)
p = PARAMS[name] if key in self.cache:
richtung = p["step"] if wert > p["default"] else -p["step"] return params, self.cache[key]
while True: self.gezahlt += 1
kandidat = {**best_params, name: wert} m = await self.runner(params, fidelity, suffix)
m = await self.trial(kandidat) if m is not None:
if m is None: with open(self.cache_pfad, "a", encoding="utf-8") as f:
break f.write(json.dumps({"key": key, "params": params, "fidelity": fidelity,
delta = score(m, self.basis) - best_score "tag": "", "metrics": m}, ensure_ascii=False) + "\n")
if delta <= self.rauschen: self.cache[key] = m
break return params, m
m2 = await self.trial(kandidat, tag="bestaetigung")
if m2 is None or score(m2, self.basis) - best_score <= self.rauschen:
self._log(f"{name}={wert}: nicht bestätigt — verworfen")
break
best_params, best_score = kandidat, min(score(m, self.basis), score(m2, self.basis))
self._log(f"ÜBERNOMMEN {name}={wert} → Score {best_score}")
naechster = round(wert + richtung, 4)
if not p["min"] <= naechster <= p["max"]:
break
wert = naechster
# Validierung auf dem zweiten Thema def _speichern(self) -> None:
if best_params:
v_base = await self.trial({}, thema=VALIDIER_THEMA, tag="val-base")
v_best = await self.trial(best_params, thema=VALIDIER_THEMA, tag="val-best")
if v_base and v_best:
self._log(f"Validierung {VALIDIER_THEMA[0]}: Baseline {score(v_base, v_base)}"
f" → Best {score(v_best, v_base)}")
self._schreibe_report(best_params, best_score)
return best_params
def _schreibe_report(self, best_params: dict, best_score: float) -> None:
from fsutil import atomic_write_json, atomic_write_text from fsutil import atomic_write_json, atomic_write_text
atomic_write_json(self.dir / "beste_params.json", best_params, indent=1) atomic_write_json(self.dir / "pheromon.json",
report = ["# Trainings-Report", "", {"pheromon": self.pheromon, "best_params": self.best_params,
f"Trials bezahlt: {self.gezahlt}/{self.max_trials}", "best_score": self.best_score}, indent=1)
f"Bester Score: {best_score} (Baseline-Rauschen {self.rauschen})", atomic_write_json(self.dir / "beste_params.json", self.best_params, indent=1)
f"Beste Parameter: `{json.dumps(best_params, ensure_ascii=False)}`", staerkste = sorted(((n, max(t.items(), key=lambda x: x[1]))
"", "Nutzung: `CREATOR_PARAMS=$(cat beste_params.json) make dev` —", for n, t in self.pheromon.items()),
key=lambda x: -x[1][1])[:10]
report = ["# Trainings-Report (Ameisen)", "",
f"Bezahlte Läufe: {self.gezahlt}/{self.max_trials}",
f"Bester Score: {self.best_score} (Rauschband {self.rauschen})",
f"Beste Parameter: `{json.dumps(self.best_params, ensure_ascii=False)}`",
"", "Stärkste Pheromon-Spuren:",
*[f"- {n}={s} (τ={t:.1f})" for n, (s, t) in staerkste],
"", "Nutzung: `CREATOR_PARAMS=$(cat beste_params.json)` —",
"Übernahme nach config.py bleibt eine manuelle Entscheidung.", "", "## Log", ""] "Übernahme nach config.py bleibt eine manuelle Entscheidung.", "", "## Log", ""]
report += [f"- {l}" for l in self.log] report += [f"- {l}" for l in self.log[-200:]]
atomic_write_text(self.dir / "report.md", "\n".join(report)) atomic_write_text(self.dir / "report.md", "\n".join(report))
print(f"\nReport: {self.dir / 'report.md'}")
async def init_vorlage() -> None:
"""Einmalig: Prüfstand-Volllauf mit Defaults, Ergebnis bleibt als Frozen-Inventar-Vorlage
liegen (Topic train-vorlage). Nach Korpus-/Prompt-Änderungen neu ausführen."""
import agents
import database
from blocks import generate_blocks
from fsutil import atomic_write_json as awj
from paths import source_path
await database.init_db()
agents.on_event = database.add_event
await database.create_topic(VORLAGE_TOPIC)
qp = source_path(VORLAGE_TOPIC)
qp.parent.mkdir(parents=True, exist_ok=True)
awj(qp, {"type": "uni", "location": BENCHMARK, "spec": ""})
await generate_blocks(VORLAGE_TOPIC, provider="minimax", research=True, qa_force=True)
await database.close_db()
print(f"Vorlage {VORLAGE_TOPIC} steht — Training kann starten (make train).")
def main() -> None: def main() -> None:
ap = argparse.ArgumentParser() ap = argparse.ArgumentParser()
ap.add_argument("--trials", type=int, default=40) ap.add_argument("--init", action="store_true", help="Frozen-Inventar-Vorlage bauen")
ap.add_argument("--stunden", type=float, default=12.0) ap.add_argument("--trials", type=int, default=60)
ap.add_argument("--sitzung", default=datetime.now(timezone.utc).strftime("%Y%m%d-%H%M")) ap.add_argument("--stunden", type=float, default=8.0)
ap.add_argument("--ameisen", type=int, default=3)
ap.add_argument("--seed", type=int, default=0)
ap.add_argument("--f2-intervall", type=int, default=5)
ap.add_argument("--sitzung", default="aco") # fester Default: Resume über Sitzungen hinweg
args = ap.parse_args() args = ap.parse_args()
trainer = Trainer(STORAGE_DIR / "train" / args.sitzung, args.trials, args.stunden) if args.init:
asyncio.run(init_vorlage())
return
trainer = AmeisenTrainer(STORAGE_DIR / "train" / args.sitzung,
max_trials=args.trials, max_stunden=args.stunden,
ameisen=args.ameisen, seed=args.seed,
f2_intervall=args.f2_intervall)
asyncio.run(trainer.run()) asyncio.run(trainer.run())

59
backend/train_f0.py Normal file
View File

@@ -0,0 +1,59 @@
"""Fidelity 0 des Trainers: Fake-E2E-Lauf im Subprozess — Sekunden, null Tokens.
Misst mit den CREATOR_PARAMS des Kandidaten: (a) halten die Invarianten? (b) wie viele
Agenten-Calls erzeugt die Struktur (Proxy für Tokens/Laufzeit)? Unsinnige Kandidaten
fallen hier raus, bevor ein echter Lauf Geld kostet.
CLI: python3 train_f0.py <ausgabe.json> (CREATOR_PARAMS im ENV)
"""
import asyncio
import json
import sys
import tempfile
import time
from pathlib import Path
# WICHTIG: config (mit CREATOR_PARAMS) lädt vor allen Pipeline-Modulen
import database
from fake_agents import Welt, aktivieren
from fsutil import atomic_write_json
async def f0(out: str) -> None:
tmp = Path(tempfile.mkdtemp(prefix="train-f0-"))
database.DB_PATH = tmp / "f0.db"
database._db = None
await database.init_db()
welt = Welt()
aktivieren(welt)
import board_inventory as bi
import qa
qa.QA_DIR = tmp / "qa"
from pipeline import GenContext
work = tmp / "arbeit"
work.mkdir()
files = {"arbeit": work, "final": tmp / "blocks.md",
"sub_roh": tmp / "sub_roh.json", "sidecar": tmp / "subblocks.json",
"facts": tmp / "facts.json", "question_pattern": tmp / "question_pattern.json",
"artefakte": tmp / "artefakte.json", "outline": tmp / "outline.json",
"outline_slots": [tmp / f"outline-{i}.json" for i in (1, 2, 3)],
"research": [work / f"research-{i}.md" for i in (1, 2, 3, 4, 5)]}
ctx = GenContext(topic="f0", provider="claude", is_cancelled=lambda: False)
start = time.monotonic()
ok = await asyncio.wait_for(
bi.run_boards(ctx, lambda *a, **k: None, files, {"type": "thema"}, None, "",
research=True, qa_force=True), timeout=180)
from tests.invarianten import pruefe_invarianten
fehler = await pruefe_invarianten("f0", files)
atomic_write_json(Path(out), {
"ok": bool(ok), "invarianten_fehler": fehler, "calls": len(welt.calls),
"dauer_s": round(time.monotonic() - start, 1)}, indent=1)
await database.close_db()
if __name__ == "__main__":
if len(sys.argv) != 2:
raise SystemExit("Nutzung: python3 train_f0.py <ausgabe.json>")
asyncio.run(f0(sys.argv[1]))

View File

@@ -1,8 +1,12 @@
"""EIN Trainings-Trial: frischer Prozess (CREATOR_PARAMS wirkt beim Import), ein """EIN Trainings-Trial: frischer Prozess (CREATOR_PARAMS wirkt beim Import), ein Mini-Lauf,
kompletter Mini-Lauf, deterministische Metriken als JSON — danach ist das Topic weg. deterministische Metriken als JSON — danach ist das Trial-Topic weg.
CLI: python3 train_lauf.py <topic> <benchmark-location> <ausgabe.json> Fidelity-Modi:
(benchmark-location repo-relativ, z. B. "benchmarks/sortierverfahren") voll python3 train_lauf.py <topic> <benchmark-location> <ausgabe.json>
— kompletter Lauf (Research + Board 1 + Board 2) + Soll-Abgleich gegen
<benchmark-location>/soll.json (falls vorhanden)
board2 python3 train_lauf.py <topic> <vorlage-topic> <ausgabe.json> --board2
— Frozen-Inventar: Vorlage kopieren, Board 2 komplett neu (research=False)
""" """
import asyncio import asyncio
@@ -16,26 +20,55 @@ import database
import qa import qa
from blocks import generate_blocks from blocks import generate_blocks
from fsutil import atomic_write_json from fsutil import atomic_write_json
from paths import source_path, topic_dir from paths import blocks_path, source_path, topic_dir
from textkit import _norm_title
async def trial(topic: str, location: str, out: str) -> None: def soll_abgleich(ist_titel: list[str], soll: dict) -> dict:
"""Ground-Truth-Vergleich: welche Soll-Blöcke fehlen, was ist überzählig.
Match über Norm-Gleichheit gegen Titel+Alternativen, Fallback beidseitiges Containment."""
ist = {_norm_title(t): t for t in ist_titel}
treffer, fehlend, belegt = [], [], set()
for block in soll.get("bloecke", []):
formen = {_norm_title(block["titel"])} | {_norm_title(a) for a in block.get("alternativen", [])}
gefunden = next((n for n in ist if n in formen), None)
if gefunden is None:
gefunden = next((n for n in ist if any(f and (f in n or n in f) for f in formen)), None)
if gefunden:
treffer.append(block["titel"])
belegt.add(gefunden)
else:
fehlend.append(block["titel"])
extra = [t for n, t in ist.items() if n not in belegt]
n_soll = max(len(soll.get("bloecke", [])), 1)
praezision = len(treffer) / max(len(ist), 1)
recall = len(treffer) / n_soll
f1 = 2 * praezision * recall / max(praezision + recall, 1e-9)
return {"treffer": treffer, "fehlend": fehlend, "extra": extra, "f1": round(f1, 3)}
async def trial(topic: str, quelle: str, out: str, board2: bool) -> None:
await database.init_db() await database.init_db()
agents.on_event = database.add_event # sonst keine Dauer-/Token-Events (main.py-lifespan-Pendant) agents.on_event = database.add_event # sonst keine Dauer-/Token-Events (main.py-lifespan-Pendant)
try: try:
await _aufraeumen(topic) # Reste eines abgebrochenen Trials await _aufraeumen(topic) # Reste eines abgebrochenen Trials
await database.create_topic(topic) await database.create_topic(topic)
start = datetime.now(timezone.utc)
if board2:
await _frozen_inventar(topic, vorlage=quelle)
await generate_blocks(topic, provider="minimax", research=False, qa_force=True)
else:
qp = source_path(topic) qp = source_path(topic)
qp.parent.mkdir(parents=True, exist_ok=True) qp.parent.mkdir(parents=True, exist_ok=True)
atomic_write_json(qp, {"type": "uni", "location": location, "spec": ""}) atomic_write_json(qp, {"type": "uni", "location": quelle, "spec": ""})
start = datetime.now(timezone.utc) # qa_force=True: das Gate misst nichts und pausiert nie
# qa_force=True: das Gate misst, pausiert den Trial aber nie
await generate_blocks(topic, provider="minimax", research=True, qa_force=True) await generate_blocks(topic, provider="minimax", research=True, qa_force=True)
dauer_min = round((datetime.now(timezone.utc) - start).total_seconds() / 60, 1) dauer_min = round((datetime.now(timezone.utc) - start).total_seconds() / 60, 1)
report = await qa.qa_report(topic, llm=False) or {} report = await qa.qa_report(topic, llm=False) or {}
lauf = report.get("lauf") or {} lauf = report.get("lauf") or {}
metrics = { metrics = {
"topic": topic, "topic": topic,
"fidelity": "board2" if board2 else "voll",
"note": report.get("note"), "note": report.get("note"),
"note_artefakte": report.get("note_artefakte"), "note_artefakte": report.get("note_artefakte"),
"quoten": report.get("quoten") or {}, "quoten": report.get("quoten") or {},
@@ -45,17 +78,42 @@ async def trial(topic: str, location: str, out: str) -> None:
"tokens": (lauf.get("tokens") or {}), "tokens": (lauf.get("tokens") or {}),
"agents": (lauf.get("agents") or {}), "agents": (lauf.get("agents") or {}),
} }
if not board2:
soll_pfad = Path(__file__).resolve().parent.parent / quelle / "soll.json"
if soll_pfad.exists():
import json
done = await database.kanban_cards(topic, board="inventory", stage="done_block")
titel = [c["payload"].get("title", "") for c in done if c["kind"] == "block"]
metrics["soll"] = soll_abgleich(titel, json.loads(soll_pfad.read_text(encoding="utf-8")))
atomic_write_json(Path(out), metrics, indent=1) atomic_write_json(Path(out), metrics, indent=1)
finally: finally:
await _aufraeumen(topic) await _aufraeumen(topic)
await database.close_db() await database.close_db()
async def _frozen_inventar(topic: str, vorlage: str) -> None:
"""Board-1-Stand der Vorlage übernehmen und Board 2 auf Start zurücksetzen —
reset_board_from_stage räumt DB-Spiegel, globale Dateien und Resume-Slots."""
import board_inventory
from blocks import _blocks_files
await database.copy_topic(vorlage, topic)
tdir = topic_dir(topic)
tdir.mkdir(parents=True, exist_ok=True)
for src, dst in ((source_path(vorlage), source_path(topic)),
(blocks_path(vorlage), blocks_path(topic))):
if src.exists():
shutil.copy(src, dst)
files = _blocks_files(topic)
files["arbeit"].mkdir(parents=True, exist_ok=True)
await board_inventory.reset_board_from_stage(topic, "artefacts", "subblocks", files)
async def _aufraeumen(topic: str) -> None: async def _aufraeumen(topic: str) -> None:
"""Topic restlos entfernen (DELETE-/topics-Sequenz aus routes.py).""" """Topic restlos entfernen (DELETE-/topics-Sequenz aus routes.py)."""
await database.delete_topic(topic) await database.delete_topic(topic)
await database.delete_block_data(topic) await database.delete_block_data(topic)
await database.delete_topic_pipeline(topic) await database.delete_topic_pipeline(topic)
await database.kanban_reset(topic)
await database.delete_source(topic) await database.delete_source(topic)
await database.delete_guide_content(topic) await database.delete_guide_content(topic)
shutil.rmtree(topic_dir(topic), ignore_errors=True) shutil.rmtree(topic_dir(topic), ignore_errors=True)
@@ -63,6 +121,7 @@ async def _aufraeumen(topic: str) -> None:
if __name__ == "__main__": if __name__ == "__main__":
if len(sys.argv) != 4: args = [a for a in sys.argv[1:] if a != "--board2"]
raise SystemExit("Nutzung: python3 train_lauf.py <topic> <benchmark-location> <ausgabe.json>") if len(args) != 3:
asyncio.run(trial(sys.argv[1], sys.argv[2], sys.argv[3])) raise SystemExit("Nutzung: python3 train_lauf.py <topic> <quelle> <ausgabe.json> [--board2]")
asyncio.run(trial(args[0], args[1], args[2], board2="--board2" in sys.argv))

View File

@@ -8,43 +8,43 @@ bewusst NICHT hier — die Messlatte darf nie Teil des Suchraums sein.
PARAMS: dict[str, dict] = { PARAMS: dict[str, dict] = {
# Recherche / Inventar # Recherche / Inventar
"RESEARCH_THEMA_AGENTS": {"default": 5, "min": 2, "max": 8, "step": 1, "kategorie": "qualitaet"}, "RESEARCH_THEMA_AGENTS": {"default": 5, "min": 2, "max": 8, "step": 1, "kategorie": "qualitaet", "fidelity": "voll"},
"RESEARCH_READERS": {"default": 2, "min": 1, "max": 3, "step": 1, "kategorie": "qualitaet"}, "RESEARCH_READERS": {"default": 2, "min": 1, "max": 3, "step": 1, "kategorie": "qualitaet", "fidelity": "voll"},
"RESEARCH_SECTION_CHARS": {"default": 12000, "min": 6000, "max": 24000, "step": 3000, "kategorie": "tokens"}, "RESEARCH_SECTION_CHARS": {"default": 12000, "min": 6000, "max": 24000, "step": 3000, "kategorie": "tokens", "fidelity": "voll"},
"DEDUP_PAIR_FLOOR": {"default": 0.6, "min": 0.45, "max": 0.8, "step": 0.05, "kategorie": "auswahl"}, "DEDUP_PAIR_FLOOR": {"default": 0.6, "min": 0.45, "max": 0.8, "step": 0.05, "kategorie": "auswahl", "fidelity": "voll"},
"DEDUP_TITLE_AUTO": {"default": 0.95, "min": 0.9, "max": 0.99, "step": 0.01, "kategorie": "auswahl"}, "DEDUP_TITLE_AUTO": {"default": 0.95, "min": 0.9, "max": 0.99, "step": 0.01, "kategorie": "auswahl", "fidelity": "voll"},
"DEDUP_GLOBAL_FLOOR": {"default": 0.65, "min": 0.5, "max": 0.8, "step": 0.05, "kategorie": "auswahl"}, "DEDUP_GLOBAL_FLOOR": {"default": 0.65, "min": 0.5, "max": 0.8, "step": 0.05, "kategorie": "auswahl", "fidelity": "voll"},
"DEDUP_PAIRS_CHUNK": {"default": 40, "min": 15, "max": 80, "step": 10, "kategorie": "laufzeit"}, "DEDUP_PAIRS_CHUNK": {"default": 40, "min": 15, "max": 80, "step": 10, "kategorie": "laufzeit", "fidelity": "voll"},
"FILTER_CHUNK": {"default": 35, "min": 15, "max": 60, "step": 10, "kategorie": "laufzeit"}, "FILTER_CHUNK": {"default": 35, "min": 15, "max": 60, "step": 10, "kategorie": "laufzeit", "fidelity": "voll"},
"FILTER_RECHECK_PANEL": {"default": 3, "min": 1, "max": 5, "step": 1, "kategorie": "qualitaet"}, "FILTER_RECHECK_PANEL": {"default": 3, "min": 1, "max": 5, "step": 1, "kategorie": "qualitaet", "fidelity": "voll"},
"CONSOLIDATION_PANEL": {"default": 3, "min": 1, "max": 5, "step": 1, "kategorie": "qualitaet"}, "CONSOLIDATION_PANEL": {"default": 3, "min": 1, "max": 5, "step": 1, "kategorie": "qualitaet", "fidelity": "voll"},
# Subbausteine # Subbausteine
"SUBBLOCK_CHUNK": {"default": 10, "min": 4, "max": 20, "step": 2, "kategorie": "laufzeit"}, "SUBBLOCK_CHUNK": {"default": 10, "min": 4, "max": 20, "step": 2, "kategorie": "laufzeit", "fidelity": "board2"},
"SUBBLOCK_MIN": {"default": 5, "min": 2, "max": 10, "step": 1, "kategorie": "auswahl"}, "SUBBLOCK_MIN": {"default": 5, "min": 2, "max": 10, "step": 1, "kategorie": "auswahl", "fidelity": "board2"},
"SUBBLOCK_MAX_ROUNDS": {"default": 3, "min": 1, "max": 5, "step": 1, "kategorie": "auswahl"}, "SUBBLOCK_MAX_ROUNDS": {"default": 3, "min": 1, "max": 5, "step": 1, "kategorie": "auswahl", "fidelity": "board2"},
"SUBBLOCK_EXTRA_ROUNDS": {"default": 2, "min": 0, "max": 4, "step": 1, "kategorie": "auswahl"}, "SUBBLOCK_EXTRA_ROUNDS": {"default": 2, "min": 0, "max": 4, "step": 1, "kategorie": "auswahl", "fidelity": "board2"},
"SUBBLOCK_PANEL": {"default": 3, "min": 1, "max": 5, "step": 1, "kategorie": "qualitaet"}, "SUBBLOCK_PANEL": {"default": 3, "min": 1, "max": 5, "step": 1, "kategorie": "qualitaet", "fidelity": "board2"},
# Facts / Artefakte / Fragen # Facts / Artefakte / Fragen
"FACTS_CHUNK_SUBS": {"default": 10, "min": 4, "max": 20, "step": 2, "kategorie": "laufzeit"}, "FACTS_CHUNK_SUBS": {"default": 10, "min": 4, "max": 20, "step": 2, "kategorie": "laufzeit", "fidelity": "board2"},
"FACTS_CHECK_PANEL": {"default": 3, "min": 1, "max": 5, "step": 1, "kategorie": "qualitaet"}, "FACTS_CHECK_PANEL": {"default": 3, "min": 1, "max": 5, "step": 1, "kategorie": "qualitaet", "fidelity": "board2"},
"QUESTION_CHUNK_SUBS": {"default": 25, "min": 10, "max": 50, "step": 5, "kategorie": "laufzeit"}, "QUESTION_CHUNK_SUBS": {"default": 25, "min": 10, "max": 50, "step": 5, "kategorie": "laufzeit", "fidelity": "board2"},
"QUESTION_MAX_ROUNDS": {"default": 3, "min": 1, "max": 5, "step": 1, "kategorie": "qualitaet"}, "QUESTION_MAX_ROUNDS": {"default": 3, "min": 1, "max": 5, "step": 1, "kategorie": "qualitaet", "fidelity": "board2"},
"ARTEFACT_CHUNK_SUBS": {"default": 25, "min": 10, "max": 50, "step": 5, "kategorie": "laufzeit"}, "ARTEFACT_CHUNK_SUBS": {"default": 25, "min": 10, "max": 50, "step": 5, "kategorie": "laufzeit", "fidelity": "board2"},
# Embedding-Schwellen (Auswahl-Kern) # Embedding-Schwellen (Auswahl-Kern)
"SUB_VARIANT_COS": {"default": 0.90, "min": 0.85, "max": 0.96, "step": 0.01, "kategorie": "auswahl"}, "SUB_VARIANT_COS": {"default": 0.90, "min": 0.85, "max": 0.96, "step": 0.01, "kategorie": "auswahl", "fidelity": "board2"},
"SEED_COVER_COS": {"default": 0.80, "min": 0.7, "max": 0.9, "step": 0.02, "kategorie": "auswahl"}, "SEED_COVER_COS": {"default": 0.80, "min": 0.7, "max": 0.9, "step": 0.02, "kategorie": "auswahl", "fidelity": "board2"},
"SUB_DUP_KANDIDAT_COS": {"default": 0.75, "min": 0.65, "max": 0.85, "step": 0.02, "kategorie": "auswahl"}, "SUB_DUP_KANDIDAT_COS": {"default": 0.75, "min": 0.65, "max": 0.85, "step": 0.02, "kategorie": "auswahl", "fidelity": "board2"},
"EMBEDDING_BLOCK_FLOOR": {"default": 0.5, "min": 0.35, "max": 0.65, "step": 0.05, "kategorie": "auswahl"}, "EMBEDDING_BLOCK_FLOOR": {"default": 0.5, "min": 0.35, "max": 0.65, "step": 0.05, "kategorie": "auswahl", "fidelity": "voll"},
"CROSS_CHUNK_PAARE": {"default": 40, "min": 15, "max": 80, "step": 10, "kategorie": "laufzeit"}, "CROSS_CHUNK_PAARE": {"default": 40, "min": 15, "max": 80, "step": 10, "kategorie": "laufzeit", "fidelity": "board2"},
# Guide # Guide
"MAX_WRITER_ROUNDS": {"default": 2, "min": 1, "max": 3, "step": 1, "kategorie": "qualitaet"}, "MAX_WRITER_ROUNDS": {"default": 2, "min": 1, "max": 3, "step": 1, "kategorie": "qualitaet", "fidelity": "board2"},
"GATE_FIX_MIN": {"default": 3, "min": 1, "max": 6, "step": 1, "kategorie": "qualitaet"}, "GATE_FIX_MIN": {"default": 3, "min": 1, "max": 6, "step": 1, "kategorie": "qualitaet", "fidelity": "board2"},
"WRITER_SPLIT_SUBS": {"default": 30, "min": 15, "max": 45, "step": 5, "kategorie": "qualitaet"}, "WRITER_SPLIT_SUBS": {"default": 30, "min": 15, "max": 45, "step": 5, "kategorie": "qualitaet", "fidelity": "board2"},
# Engine / Kosten # Engine / Kosten
"CONSENSUS_GRACE": {"default": 300, "min": 0, "max": 600, "step": 60, "kategorie": "laufzeit"}, "CONSENSUS_GRACE": {"default": 300, "min": 0, "max": 600, "step": 60, "kategorie": "laufzeit", "fidelity": "board2"},
"MAX_RESTARTS": {"default": 2, "min": 1, "max": 3, "step": 1, "kategorie": "laufzeit"}, "MAX_RESTARTS": {"default": 2, "min": 1, "max": 3, "step": 1, "kategorie": "laufzeit", "fidelity": "board2"},
"EVIDENCE_BUDGET_CHARS": {"default": 48000, "min": 16000, "max": 64000, "step": 8000, "kategorie": "tokens"}, "EVIDENCE_BUDGET_CHARS": {"default": 48000, "min": 16000, "max": 64000, "step": 8000, "kategorie": "tokens", "fidelity": "board2"},
"QUELLE_RELEVANZ_CHUNK": {"default": 12, "min": 6, "max": 24, "step": 3, "kategorie": "laufzeit"}, "QUELLE_RELEVANZ_CHUNK": {"default": 12, "min": 6, "max": 24, "step": 3, "kategorie": "laufzeit", "fidelity": "voll"},
} }

View File

@@ -0,0 +1,27 @@
Skript: Grundlagen der Verschlüsselung
Kapitel 1: Symmetrische Verschlüsselung
Symmetrische Verschlüsselung verwendet denselben Schlüssel zum Ver- und Entschlüsseln. Sender und Empfänger müssen den Schlüssel vorab über einen sicheren Kanal austauschen. Der Schlüsselraum muss groß genug sein, dass vollständiges Durchprobieren aussichtslos bleibt: 128 Bit gelten als sicher gegen Brute-Force. Symmetrische Verfahren sind schnell und eignen sich für große Datenmengen. Blockchiffren verarbeiten die Nachricht in festen Blöcken, etwa 128 Bit bei AES; Stromchiffren verschlüsseln Bit für Bit mit einem Schlüsselstrom. Das Kerckhoffs-Prinzip verlangt, dass die Sicherheit allein am Schlüssel hängt, nie an der Geheimhaltung des Verfahrens.
Kapitel 2: Betriebsmodi von Blockchiffren
Ein Betriebsmodus legt fest, wie eine Blockchiffre Nachrichten länger als einen Block verarbeitet. Die wichtigsten Modi sind ein kleiner Katalog: ECB verschlüsselt jeden Block unabhängig, CBC verkettet jeden Block mit dem Vorgänger-Geheimtext, CTR macht aus der Blockchiffre eine Stromchiffre über einen Zähler, und GCM ergänzt CTR um einen Authentizitäts-Tag. ECB gilt als unsicher, weil gleiche Klartextblöcke gleiche Geheimtextblöcke ergeben und Muster sichtbar bleiben. CBC braucht einen zufälligen Initialisierungsvektor je Nachricht. CTR und GCM sind parallelisierbar; GCM ist der Standard für authentifizierte Verschlüsselung.
Kapitel 3: Asymmetrische Verschlüsselung
Asymmetrische Verschlüsselung, im Englischen public key cryptography, arbeitet mit einem Schlüsselpaar: Der öffentliche Schlüssel verschlüsselt, der private entschlüsselt. Der öffentliche Schlüssel darf jeder kennen; der private verlässt den Besitzer nie. Damit entfällt der sichere Kanal für den Schlüsselaustausch. Die Sicherheit beruht auf mathematisch schweren Problemen: RSA auf der Faktorisierung großer Zahlen, Elliptische-Kurven-Verfahren auf dem diskreten Logarithmus. Asymmetrische Verfahren sind um Größenordnungen langsamer als symmetrische. In der Praxis verschlüsselt man deshalb hybrid: asymmetrisch nur den Sitzungsschlüssel, die Daten symmetrisch.
Anders formuliert löst die Public-Key-Kryptographie das Verteilungsproblem: Zwei Parteien ohne gemeinsames Geheimnis können vertraulich kommunizieren, weil das Schlüsselpaar die Rollen trennt — verschlüsseln kann jeder, entschlüsseln nur der Inhaber des privaten Schlüssels.
Kapitel 4: Kryptographische Hashfunktionen
Eine kryptographische Hashfunktion bildet beliebig lange Eingaben auf einen Wert fester Länge ab, den Digest. Drei Eigenschaften machen sie kryptographisch: Einwegfunktion (aus dem Digest ist die Eingabe praktisch nicht rekonstruierbar), schwache Kollisionsresistenz (zu gegebener Eingabe ist keine zweite mit gleichem Digest findbar) und starke Kollisionsresistenz (es ist praktisch unmöglich, irgendein kollidierendes Paar zu finden). Nach dem Geburtstagsparadoxon (Satz 3.2) sinkt der Kollisionsaufwand auf die Wurzel des Werteraums: Bei einem 256-Bit-Digest liegt er bei 2 hoch 128 Versuchen. Kleine Eingabeänderungen kippen im Mittel die Hälfte der Digest-Bits; das heißt Lawineneffekt. Hashfunktionen speichern Passwörter, prüfen Datenintegrität und bilden die Basis von Signaturen.
Kapitel 5: Digitale Signaturen
Eine digitale Signatur weist Urheberschaft und Unverändertheit einer Nachricht nach. Der Absender hasht die Nachricht und verschlüsselt den Digest mit seinem privaten Schlüssel; jeder kann die Signatur mit dem öffentlichen Schlüssel prüfen. Stimmen berechneter und entschlüsselter Digest überein, ist die Nachricht unverändert und stammt vom Schlüsselinhaber. Signaturen liefern damit drei Garantien: Integrität, Authentizität und Nichtabstreitbarkeit. Signiert wird immer der Hash, nie die Nachricht selbst — aus Effizienz und weil manche Verfahren nur kurze Eingaben verarbeiten. Gängige Verfahren sind RSA-PSS und ECDSA.
Kapitel 6: Zertifikate und PKI
Ein Zertifikat bindet einen öffentlichen Schlüssel an eine Identität. Es enthält Inhaber, Schlüssel, Gültigkeitszeitraum und die Signatur einer Zertifizierungsstelle (CA). Die Public-Key-Infrastruktur (PKI) ordnet CAs hierarchisch: Eine Wurzel-CA signiert Zwischen-CAs, diese signieren Endzertifikate — die Vertrauenskette. Browser prüfen die Kette bis zu einer vorinstallierten Wurzel. Widerrufene Zertifikate landen in Sperrlisten (CRL) oder werden per OCSP live abgefragt. Am Rande: Das X.690-Format kodiert Zertifikatsfelder in ASN.1-Strukturen — ein Implementierungsdetail, das für das Verständnis der Vertrauenskette nicht nötig ist.

View File

@@ -0,0 +1,16 @@
{
"bloecke": [
{"titel": "Symmetrische Verschlüsselung", "alternativen": ["Symmetrische Kryptographie", "Symmetrische Verfahren"]},
{"titel": "Betriebsmodi von Blockchiffren", "alternativen": ["Betriebsmodi", "Blockchiffren-Modi", "Betriebsarten von Blockchiffren"]},
{"titel": "Asymmetrische Verschlüsselung", "alternativen": ["Public-Key-Kryptographie", "Asymmetrische Kryptographie", "Public Key Cryptography"]},
{"titel": "Kryptographische Hashfunktionen", "alternativen": ["Hashfunktionen", "Hash-Funktionen"]},
{"titel": "Digitale Signaturen", "alternativen": ["Signaturen", "Digitale Signatur"]},
{"titel": "Zertifikate und PKI", "alternativen": ["Zertifikate", "Public-Key-Infrastruktur", "PKI", "Zertifikate und Public-Key-Infrastruktur"]}
],
"fallen": {
"dublette": "Asymmetrische Verschlüsselung und Public-Key-Kryptographie (Kapitel 3, zwei Formulierungen + DE/EN) dürfen nur EINEN Block ergeben.",
"katalog": "ECB/CBC/CTR/GCM gehören als Katalog unter Betriebsmodi — keine vier Einzelblöcke.",
"peripheral": "X.690/ASN.1 ist als Implementierungsdetail markiert — höchstens peripheral, nie eigener Kern-Block.",
"referenz_titel": "„Geburtstagsparadoxon (Satz 3.2)" darf nicht als Referenz-Titel überleben (Naming-Regel)."
}
}