Pinned Post

OpenSpec UI: ein Live-Dashboard für deine Specs

Bild
OpenSpec UI: ein Live-Dashboard für deine Specs Wer mit einem KI-Agenten und OpenSpec arbeitet, kennt das Bild. Im Projekt wächst ein Verzeichnis openspec/ heran: Changes mit proposal.md , design.md , tasks.md und ihren Delta-Specs, daneben die kanonischen Specs der einzelnen Capabilities. Die Absicht hinter dem Code steht damit endlich geschrieben, statt sich in ihm zu verstecken. Nur verteilt sie sich über Dutzende Markdown-Dateien, und der Editor zeigt eben Dateien. Was er nicht zeigt, ist der Zustand. Woran arbeitet der Agent gerade? Welcher Task ist der nächste offene? Und was hat sich verändert, während ich zehn Minuten woanders hingeschaut habe? Genau diese Lücke füllt OpenSpec UI , ein kleines Phoenix-LiveView-Dashboard, das lokal neben deinem Projekt läuft und den OpenSpec-Workspace im Browser zeigt — live, während gearbeitet wird. Der Code liegt offen auf GitLab: https://gitlab.com/public_elixir/openspec_ui Ein Blick statt Datei-Hopping Du startest den Server, gibst d...

Das Feature, das aus Korrekturen lernt, ohne Retraining

Wenn ich in meiner App eine Kategorie von Hand korrigiere, klassifiziert sie ähnliche Transaktionen ab sofort richtig, ganz ohne Retraining-Lauf, neues Deployment oder MLOps-Pipeline. Hier ist der Trick, und nebenbei, warum „Lernen" nicht immer „Trainieren" heißt.

Im letzten Artikel habe ich gezeigt, wie ich Transaktionen klassifiziere, indem ich ihren Text-Vektor mit Vektoren von Kategorie-Beschreibungen vergleiche. Das wirft eine Frage auf: Was, wenn das Modell danebenliegt?

In den meisten ML-Systemen ist die Antwort aufwändig: Eine falsche Vorhersage wandert als Beispiel ins Trainingsset, irgendwann wird neu trainiert, evaluiert und neu deployt. Ein Zyklus, der Tage bis Wochen dauert. Genau den wollte ich vermeiden.

Die Einsicht dahinter: Wenn eine Kategorie durch Text definiert ist, dann ist „dem Modell etwas beibringen" nichts anderes als „den Text erweitern". Ordnet der Nutzer eine Transaktion manuell der richtigen Kategorie zu, hänge ich genau diesen Transaktionstext an die Definition dieser Kategorie an. Beim nächsten Mal zieht das den Vektor der Kategorie ein Stück in Richtung dieser realen Buchung.

Konkret: Jede manuelle Zuordnung wird als „Korrektur" gespeichert.

def set_group(transaction, group_id) do
with {:ok, updated} <- assign_group(transaction, group_id) do
record_correction(updated, group_id) # <- der Lern-Schritt
{:ok, updated}
end
end

Und die Kategorie-Definition, gegen die klassifiziert wird, ist nicht mehr nur meine handgeschriebene Beschreibung, sondern die Beschreibung plus alle gelernten Korrekturen:

text =
[group.description, correction_text] # Handkuratiert + Gelerntes
|> Enum.reject(&(&1 in [nil, ""]))
|> Enum.join(" ")

Sobald eine Korrektur hereinkommt, werfe ich den gecachten Vektor weg. Die nächste Klassifikation berechnet die Kategorie-Vektoren neu, jetzt inklusive des neuen Beispiels:

def record_correction(transaction, group) do
# ... Korrektur in der DB speichern ...
Classifier.invalidate_label_cache() # Cache verwerfen -> beim
end # nächsten Mal neu berechnen

Das ist der ganze „Trainings"-Loop: eine Datenbankzeile und ein verworfener Cache. Die Verbesserung wirkt sofort, nicht erst nach einem Trainingslauf.

Ein Detail, auf das ich stolz bin: Der Loop kann auch verlernen. Ordne ich denselben Text später einer anderen Kategorie zu, wird die alte Korrektur gelöscht statt beibehalten, denn sonst hätte das System widersprüchliche Signale. Umklassifizieren heißt eben: die alte Lehre ersetzen, nicht eine zweite danebenstellen.

Der zweite Punkt zum Thema Pragmatismus: ML ist die letzte Instanz, nicht die erste. Manche Buchungen muss ich gar nicht erst „erraten". Eine Mietzahlung, die jeden Monat an denselben Vermieter geht, ist eine deterministische Regel und kein Ratespiel. Deshalb ist die Klassifikations-Pipeline geschichtet:

def reclassify(transaction) do
case first_matching_rule(transaction) do
%Rule{} = rule ->
apply_rule(transaction, rule) # 1. Feste Regel? Fertig.
nil ->
case predetermined_label(transaction) do
{:ok, label} -> assign_label(transaction, label) # 2. Bekannt?
:none -> ml_classify(transaction) # 3. Erst jetzt das Modell.
end
end
end

ML läuft nur, wenn deterministische Regeln und bekannte Muster nichts hergeben. Das ist günstiger und vorhersehbarer, und es hält die Regeln bewusst aus dem ML-Lernsignal heraus. Eine Regel ist eine Anweisung, kein Trainingsbeispiel.

Das Ganze läuft asynchron über einen kleinen Agenten, der eine Transaktion pro Durchlauf abarbeitet und sich selbst neu anstößt, solange noch welche offen sind. So blockiert die erste (langsame) Modell-Inferenz nie einen Web-Request.

Die größere Lektion für alle, die ML-Features bauen: „Das Modell soll besser werden" zwingt dich nicht automatisch in einen Retraining-Apparat. Manchmal ist der Lern-Mechanismus eine Tabellenzeile und ein invalidierter Cache. Wähle die einfachste Architektur, die deinem Anspruch an Genauigkeit genügt, und nicht die, die am meisten nach „echtem ML" aussieht.

Im nächsten Teil das Architektur-Highlight: wie das Modell auf einer komplett anderen Maschine läuft, ohne dass mein Code es merkt.

→ Weiter zu Teil 4: ML-Last verschieben mit einer Zeile Code

Kommentare

Beliebte Posts aus diesem Blog

Splitting an ML model and a web app across two BEAM nodes — the technical blueprint

Jido in Practice: Agents in Elixir as Composable Actions

A Foundation Model in the BEAM: On-Chain Anomalies with Google TimesFM in Elixir