AIREITER

329 Befehle, 128 fehlen noch: Migrationen mit Mengenoperationen abgleichen, nicht mit einem Modell

Zuletzt aktualisiert: 2026-07-31 07:48:17

Zweihundert sauber übersetzte Funktionen, alle Tests grün – und trotzdem kann eine Migration noch weit von fertig sein. Denn „übersetzt“ und „migriert“ sind zwei verschiedene Dinge: Ein Modell kann eine einzelne Funktion hervorragend übertragen. Ob wirklich das gesamte System angekommen ist, lässt sich dagegen nur als Mengenoperation beantworten.

Genau dort sollte kein Modell entscheiden. Bei einem vollständigen Abgleich übersieht es Einträge, ordnet sie falsch zu oder liefert bei identischer Eingabe unterschiedliche Ergebnisse. Der folgende Stand aus einer echten Go-zu-Python-Migration zeigt, warum der Soll-Ist-Abgleich ein Skript braucht – und das Modell anschließend nur noch erklären sollte.

Drei Zahlen, die den Migrationsstand zeigen

Das alte Go-Registry umfasste 23 Plattformen und 329 Befehle. Die neue Python-Seite leitet ihre Befehlsmenge aus den argparse-Deklarationen ab; mit dem alten Registry überschneiden sich davon exakt 201. Damit existieren 128 Befehle ausschließlich auf der Go-Seite: Sie wurden weder nach Python migriert noch bewusst als Stub belassen.

329 = 201 + 128. Diese Rechnung ist technisch banal, beantwortet aber als Einzige die Frage „Ist die Migration fertig?“. Beim Übersetzen einzelner Funktionen taucht sie nie auf. Ein fehlender Eintrag ist ein Fehler durch Abwesenheit: keine Exception, kein fehlgeschlagener Test, kein Alarm – nur ein Name, der existieren müsste und nicht da ist. Er war nie Teil des Chatverlaufs; auch zweihundert grüne Häkchen machen ihn nicht sichtbar.

Warum Stubs und Kompatibilitäts-Proxys keine Lösung sind

Mitten in einer Migration liegt die Versuchung nahe, unfertige Befehle als Platzhalter stehen zu lassen: etwa als raise NotImplementedError oder als Kompatibilitäts-Proxy zum alten Binary, damit „der Endpoint-Katalog vollständig aussieht“. Genau das sollte man nicht tun. Eine leere Hülle ist aus drei Gründen teurer als eine sichtbare Lücke.

Erstens verfälscht ein Stub den Abgleich. Der Befehlsname landet in der neuen Menge, der Diff wird 0, und die Migration scheint abgeschlossen. Eine Lücke ist ehrlich rot; ein Stub ist eine grüne Lüge, die aus „noch 128 offen“ ein „alles vorhanden“ macht.

Zweitens konserviert ein Kompatibilitäts-Proxy eine nicht bereinigte Abhängigkeit. Leitet er an das alte Go-Binary weiter, lässt sich die alte Runtime nie entfernen. Ziel der Migration ist, den alten Stack loszuwerden. Ein Forwarding-Proxy lässt ihn unter dem Etikett „temporäre Kompatibilität“ einziehen – und dauerhaft bleiben.

Drittens täuscht ein halbfertiger Endpoint seine Aufrufer. Ein Agent oder Mensch sieht den Katalog, geht von einer funktionierenden Schnittstelle aus und stößt dann auf runtime_unavailable – oder schlimmer noch auf einen scheinbaren Erfolg, der stillschweigend ein leeres Ergebnis zurückgibt.

Die ehrliche Lücke ist tatsächlich am günstigsten: Der Diff schlägt sofort rot an, und jeder sieht, wie viel noch fehlt. Das entspricht demselben Prinzip wie der Evidenzschwelle beim App-Reverse-Engineering: Etwas als „noch nicht nutzbar“ zu markieren, ist immer billiger, als eine halbfertige Variante auszuliefern.

Das Grundgerüst für den Abgleich

Der Kern des Abgleichs ist eine einfache Regel: Beide Befehlsmengen werden aus Deklarationen abgeleitet, nichts wird manuell abgeschrieben. Eine handgepflegte Liste der „migrierten“ Befehle schafft eine dritte Quelle der Wahrheit. Sie driftet vom Code weg – und nach zwei Wochen ist genau das meist der erste Fehler.

Auf der neuen Python-Seite sind die argparse-Deklarationen in der cli.py jeder Plattform die einzige Quelle der Wahrheit. Ein catalog-Modul durchläuft die Subcommands, exportiert eine Menge im Format {platform/command} und gibt sie über python -m reverse describe --format json aus. Warum Deklarationen diese Rolle übernehmen können und wie der Katalog vollständig automatisch entsteht, behandelt der Beitrag zu Interface as Code. Die alte Go-Seite liegt bereits als platform -> command-Map vor – eine unveränderliche, ins Binary kompilierte Allowlist. JSON in derselben Struktur zu exportieren, ist dort trivial.

Sind beide JSON-Dateien vorhanden, bleibt nur noch Mengenrechnung:

# Both sides' command sets derive from declarations, not transcription.
# Transcribe by hand and you've added a third source of truth that will drift.
import json
from collections import Counter

def ids(path):
    doc = json.load(open(path))
    return {f"{p['name']}/{c['name']}"
            for p in doc["platforms"] for c in p["commands"]}

old = ids("go-registry.dump.json")      # old registry: immutable platform->command allowlist
new = ids("python-catalog.dump.json")   # python -m reverse describe --format json

missing = old - new     # old side only: each one needs a keep-or-drop verdict
added   = new - old     # new side only: new capability, logged separately
kept    = old & new     # intersection: migrated, but still check for semantic drift

assert missing | kept == old            # every old-side item classified, none dropped

by_platform = Counter(pc.split("/")[0] for pc in missing)  # goes straight into the README table

Das läuft in wenigen Millisekunden, kostet nichts, ist deterministisch und zu 100 % korrekt. missing enthält diese 128 Befehle; nach Plattform aggregiert ergibt sich folgende Tabelle:

Plattform

Noch nicht migrierte Befehle

xiaohongshu

33

tiktok

30

hotspot

21

douyin

19

reddit

8

weibo

7

bilibili

5

zhihu

3

linkedin

1

netease_music

1

Gesamt

128

Für ein Modell gibt es in diesem Schritt keinen Platz.

Warum Modellvergleiche Zeile für Zeile teuer und falsch sind

Lässt man das Skript weg, kopiert beide Listen in ein Chatfenster und fragt „Welche der 329 erscheinen nicht in diesen 201?“, passieren zuverlässig drei Dinge.

Das Modell lässt Einträge aus: Bei langen Listen bildet es keine echte elementweise Mengendifferenz, sondern orientiert sich an einem „sieht ungefähr richtig aus“. Einträge am Ende gehen unter, und die Antwort wirkt vollständig, obwohl ein Dutzend fehlt. Es erfindet Einträge: Befehle, die auf beiden Seiten vorhanden sind, werden als fehlend gemeldet – oder tatsächlich fehlende gelten als migriert. Das Modell imitiert den Stil eines Abgleichberichts, statt die Differenz zu berechnen. Und das Ergebnis ist nicht reproduzierbar: Dieselbe Eingabe kann zu unterschiedlichen Listen fehlender Befehle führen. Ein „Abgleich“, der jedes Mal ein anderes Ergebnis liefert, ist keiner.

Auch wirtschaftlich lohnt es sich nicht: Das Skript braucht wenige Millisekunden; ein Modellvergleich kostet einige hunderttausend Tokens plus mehrere Selbstprüfungsrunden. Er ist teuer, langsam und nicht vertrauenswürdig. Mengenoperationen an das Werkzeug zu geben, das Mengenoperationen beherrscht, ist die unstrittigste Aussage dieses Beitrags.

Die richtige Modellaufgabe: Abweichungen erklären, nicht Migrationen bestätigen

Das Skript liefert 128 Fakten vom Typ „nicht migriert“. Aber ein Fakt ist noch keine Schlussfolgerung. Für jeden Eintrag braucht es eine Entscheidung – behalten oder streichen – und diese Entscheidung braucht eine Begründung. Das ist die Domäne des Modells.

Es soll für jeden Fall erklären, warum er nicht migriert wurde. Handelt es sich um toten Code? Wurde ein Upstream-Endpoint eingestellt? Ist die Umsetzung aufgeschoben? Oder, besonders knifflig: Der Befehl wurde nicht entfernt, sondern in einem anderen Command aufgegangen. Der Name ist verschwunden, die Fähigkeit aber weiterhin vorhanden. Diese versteckte Zuordnung „zusammengeführt, nicht gelöscht“ erkennt man nicht allein anhand der Missing-Liste; dafür müssen beide Registries gleichzeitig gelesen und abgeglichen werden.

Auch die 201 Einträge in der Schnittmenge sind nicht automatisch sicher. Migriert heißt nicht, dass die Semantik erhalten blieb: Ein gleichnamiger Befehl kann einen still veränderten Default haben, die Paginierungssemantik kann vertauscht sein oder zwei Fehlercodes wurden zu einem zusammengelegt. Das ist semantischer Drift – heimtückischer als eine Lücke, weil der Diff grün bleibt und der Fall nie in missing auftaucht. Drift-Prüfung verlangt, dass das Modell beide Implementierungen liest und bewertet: „Ist dieses Verhalten äquivalent?“ Abschließend wird das per Differential Testing bestätigt, also durch Fixture-Vergleich in Stufe drei des vierstufigen Workflows. Genau diese Fähigkeit, eine scheinbar erfolgreiche Übersetzung anzusehen und dennoch zu sagen „Hier hat sich das Verhalten geändert“, beschreibt der Abschnitt zu Gegenbelegen im Fingerprinting-Beitrag. Ein schwaches Modell wiederholt lediglich „erfolgreich migriert“.

Die Aufgabenverteilung ist damit klar: Das Skript entscheidet, ob etwas vorhanden ist. Das Modell beurteilt, ob es bleiben sollte und ob sich sein Verhalten verändert hat. Der Mensch trifft die Entscheidung. In diesem Fall gehörten 4 der vom Diff rot markierten Befehle nach der Prüfung doch dazu und wurden wieder als vollwertige neue Commands ergänzt. Drei Ebenen, jede an ihrem Platz.

Welches Modell für welchen Arbeitsschritt?

Alle vier folgenden Stufen liegen in der Erklärungsschicht. Die Entscheidungsschicht – der Diff – verwendet überhaupt kein Modell. Genau das unterscheidet diesen Ansatz von anderen Artikeln über „KI-Migrationen“.

Schritt

Benötigte Fähigkeit

Empfehlung

model id

Beide Registries zugleich verarbeiten und Zuordnungen „nicht gelöscht, sondern anderswo zusammengeführt“ erkennen

Langer Kontext, liest die vollständigen Deklarationen beider Seiten gleichzeitig

Kimi K3

kimi-k3

Erste Behalten-oder-Streichen-Bewertung für 128 fehlende Einträge, strukturierter Entwurf

Günstig, Hunderte Aufrufe mit hoher Parallelität

Claude Sonnet 5

claude-sonnet-5

Bewertung von semantischem Drift: migriert, aber hat sich das Verhalten geändert?

Starkes Reasoning, bereit festzustellen: „Das hat sich geändert“

Claude Opus 5

claude-opus-5

Ein Endpoint ist migriert, aber das Fixture passt nicht: Abweichung über Parameter oder Antwortform erklären

Mittleres Reasoning für Ursachenanalyse

GPT-5.6 Sol

gpt-5.6-sol

Am meisten lohnt sich der Test der dritten Stufe. Die Bewertung semantischen Drifts prüft exakt, ob ein Modell einer bereits erfolgreich wirkenden Übersetzung widerspricht. Dort verändert ein Modellwechsel das Ergebnis am stärksten. Das Protokoll:

  1. Nimm eine eigene reale Migration zwischen zwei Sprachen und ermittle mit dem Skript die Menge missing. In diesem Schritt kommt kein Modell zum Einsatz.

  2. Versehe 10 bis 15 dieser Einträge als Kontrollgruppe manuell mit Ground Truth: streichen, behalten, anderswo zusammengeführt oder aufgeschoben.

  3. Gib claude-opus-5 und einem günstigen Modell dieselbe Aufforderung: „Erkläre pro Eintrag, ob er behalten oder gestrichen werden soll.“ Prüfe zwei Punkte: Verweist die Begründung auf einen konkreten Code-Fakt oder liefert sie nur vage Aussagen wie „möglicherweise veraltet“? Und wie viele Zuordnungen zu anderswo zusammengeführten Funktionen erkennt jedes Modell?

  4. Die Zahl der erkannten versteckten Zuordnungen ist die Grundlage dafür, ob du dem Modell den ersten Durchgang anvertrauen willst.

Die eigentliche Hürde sind Wechselkosten, nicht die Modellauswahl

Vier Modelle von drei Anbietern bedeuten drei SDKs, drei Authentifizierungsschemata und drei Fehlerformate. Den Client bei jedem Stufenwechsel neu zu schreiben, lohnt sich nicht. Deshalb nutzen viele ein einziges Modell durchgehend – und setzen bei der semantischen Drift-Prüfung, die gerade das starke Reasoning braucht, ein günstiges Modell ein, das nur vage Formulierungen liefert. So wird jeder grüne Drift einfach durchgewunken.

AIReiter vereinheitlicht diese Schicht: ein Key, eine OpenAI-kompatible Schnittstelle, alle vier Stufen dahinter. Für den Wechsel genügt es, das Feld model im Request-Body anzupassen.

# Semantic-drift review / per-item keep-or-drop: the reasoning tier
curl https://aireiter.com/api/v1/chat/completions \
  -H "Authorization: Bearer $AIREITER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-opus-5",
    "messages": [{"role": "user", "content": "<both implementations + this command migration status, ask if behavior is equivalent>"}]
  }'

# First pass on 128 missing items in bulk: change the model field, leave the rest
#   "model": "claude-sonnet-5"
# Diff attribution when a fixture won't match:
#   "model": "gpt-5.6-sol"

Wer bereits das OpenAI SDK nutzt, setzt base_url auf https://aireiter.com/api/v1 und muss sonst nichts ändern. Mit dem Anthropic SDK geht der Request mit demselben Key an POST /api/v1/messages.

Die Preisgestaltung passt zu diesem Ablauf: Der erste Durchgang verarbeitet Hunderte Einträge auf einmal und läuft in jeder Migrationsrunde erneut; hier ist claude-sonnet-5 mit hoher Parallelität am günstigsten. Die Drift-Prüfung umfasst ein Dutzend schwieriger Fälle, die wiederholt an claude-opus-5 gehen, und ist pro Eintrag am teuersten. Beide sind Claude-Stufen; der Rabatt von 30 % trifft damit genau die dichtesten und teuersten Teile des Ablaufs. gpt-5.6-sol übernimmt die Ursachenanalyse beim Diff, GPT zum halben Preis.

  • API-Key holen

  • Ohne Anmeldung ausprobieren: Prüfe zunächst einige fehlende Einträge manuell und sieh nach, ob die Fälle „anderswo zusammengeführt“ erkannt werden. Danach entscheidest du, ob du es in den Ablauf integrierst.

Fazit

„Übersetzt“ ist die Illusion einer einzelnen Funktion. „Migriert“ entscheidet der Diff. Mengenoperationen gehören ins Skript, Erklärungen ins Modell, Entscheidungen zum Menschen. Diese Reihenfolge lässt sich nicht vertauschen – erst recht nicht, indem man das Modell entscheiden lässt.

Einen letzten Schritt lässt man leicht aus: Die Liste fehlender Einträge gehört dauerhaft sichtbar ins README. Die 128 bleiben dort stehen, bis sie 0 sind oder jeder Eintrag eine dokumentierte Begründung trägt: „wird nicht migriert, weil X“. Ein Abgleich, der nur in einer PR-Diskussion existiert, ist kein Abgleich. Die nächste Person sieht ihn nicht und tritt erneut in dieselben 128 Fälle. Das ist die gemeinsame Grundlage dieses Beitrags, des Beitrags zu Interface as Code und des Beitrags darüber, warum kein einheitliches Response Model gebaut wird: Die einzige Quelle der Wahrheit muss für sich selbst sprechen, statt Schlussfolgerungen über Erinnerungen einzelner Personen zu verteilen.