Startseite / Artikel

Überprüfung großer AI-Refaktorisierungen: Warum das Bestehen von Tests die gelöschte Absicht übergeht

Erfahren Sie, warum von KI unterstützte Refaktorisierungen unbemerkt tragenden Code entfernen, warum grüne Tests dies nicht erkennen, sowie fünf Überprüfungsmethoden, die große Refaktorisierungen sicher für den Einsatz machen.

Verlag: REACTAPP.TOP7 Min. Lesezeit1618 Wörter

Quellen: I Let Claude Refactor 4,000 Lines. Here’s the Part I Had to Undo.

KI-basierte Programmierassistenten sind inzwischen gut genug, um einen vernachlässigten Modul innerhalb eines Nachmittags zu überarbeiten, und das Ergebnis liest sich in der Regel besser als das ursprüngliche Modul. Die Gefahr liegt nicht im schlampigen Code, sondern in Code, der korrekt aussieht, aber heimlich Verhaltensweisen beinhaltet, die niemand dokumentiert hat. Dieser Leitfaden zeigt ein realistisches Beispiel für einen solchen Fehler, erklärt, warum sowohl die Testsuite als auch der Prüfer ihn übersehen haben, und liefert einen konkreten Überprüfungsprozess für umfangreiche durch KI unterstützte Refaktorisierungen.

Eine Refaktorierung, die den Code verbesserte und das System lahmlegte

Betrachten wir ein Modul, bei dem sich seit zwei Jahren ständig Änderungen angesammelt haben: etwa 4.000 Zeilen auf vier Dateien – der Teil des Codebases, den Ingenieure immer dann meiden, wenn es möglich ist. Es weist inkohärentes Fehlerbehandlungssysteme auf, drei verschiedene Methoden zum Parsen von Datumsangaben sowie mehrere Funktionen, bei denen niemand mit Sicherheit sagen kann, ob sie noch aufgerufen werden.

Ein Entwickler übergeben die Datei in zwei Sitzungen an einen KI-Assistenten und erhalten etwas deutlich Besseres zurück: einheitliche Muster, korrekte Typangaben, entfernte „ tote“ Zweige sowie etwa ein Drittel weniger Zeilen. Die Tests bestehen. Der Entwickler prüft den Diff – oder glaubt zumindest, dies getan zu haben – und veröffentlicht die Datei.

Sechs Tage später beginnt bei einer Partnerintegration das Verschwinden von Datensätzen. Es gibt keine offensichtlichen Fehler; etwa vier Prozent der Datensätze erreichen einfach nicht mehr das System. Die Ursache ist die Refaktorierung – genauer gesagt eine Änderung, die der Entwickler gelesen und genehmigt hatte.

Wo der Assistent wirklich hervorragte

Es lohnt sich, genau zu beschreiben, was gut gelaufen ist, denn die Schlussfolgerung sollte nicht lauten: „KI ist schlecht im Refaktorieren.“ Der Assistent war bei jeder Transformation, die nur vom vorliegenden Code abhing, hervorragend:

  • Die drei Hilfsfunktionen zum Parsen von Datumsangaben in eine einzige Funktion zusammengefasst
  • Verflachung von verschachtelten bedingten Anweisungen zu Schutzklauseln
  • Annotieren des gesamten Moduls mit Typhinweisen
  • Ausfindig machen von zwei Zweigen, die tatsächlich unerreichbar waren
  • Auswahl klarerer Namen
  • Für mechanische, lokale Änderungen, die durch das Lesen überprüft werden können, war es schneller und konsistenter als ein Mensch, und es verlor nicht den Fokus nach mehreren hundert Zeilen – genau dort neigen manuelle Refaktorisierungen dazu, an Qualität einzubüßen. Wenn die Aufgabe darin besteht, „diesen Code dazu zu bringen, dasselbe Verhalten klarer auszudrücken“, leisten diese Tools hervorragende Arbeit. Die Probleme beginnen erst an der Grenze dessen, was eigentlich unter „dasselbe Verhalten“ fällt.

    Warum tragender Code gelöscht wird

    Code, der keinen sichtbaren Grund für seine Existenz hat, sieht genauso aus wie Müll. Hier ist die genehmigte Änderung:

    # before
    def normalise_timestamp(raw):
        ts = raw.strip()
        if ts.endswith("Z") and "." in ts:
            ts = ts.split(".")[0] + "Z"          # partner sends 7-digit microseconds
        return datetime.fromisoformat(ts.replace("Z", "+00:00"))
    
    # after
    def normalise_timestamp(raw):
        return datetime.fromisoformat(raw.strip().replace("Z", "+00:00"))
    

    Der entfernte Zweig sieht aus wie ein typisches Ziel für Refactoring: ein Sonderfall, eine seltsam spezifische Prüfung eines Suffixes sowie ein String-Trennen. Er erscheint überflüssig, da fromisoformat bereits ISO-Zeitstempel parsen kann.

    Das Problem liegt bei der Eingabe. Ein Partner sendet Zeitstempel mit sieben Nachkommastellen. Vor Python 3.11 verarbeitete fromisoformat nur die Formate, die isoformat() selbst erzeugte – das bedeutet höchstens sechs Nachkommastellen – wodurch ein Wert mit sieben Stellen einen ValueError auslöste. Python 3.11 und neuer akzeptieren ein viel umfassenderes Teilmenge von ISO 8601 und sind in diesem Punkt nachsichtiger; daher hängt es vom Laufzeitumfeld ab, ob diese Änderung Probleme verursacht – was ein weiterer Grund ist, zu überprüfen statt zu behaupten. Die Branch entstand aufgrund eines Support-Tickets aus achtzehn Monaten zuvor, und ihr kurzer Inline-Kommentar reichte weder für Menschen noch für Modelle aus, um angemessen abzuwägen, ob sie wirklich notwendig ist. Beachten Sie außerdem, dass die alte Branch mehr tat, als es auf den ersten Blick schien: Sie warf den ganzen Nachkommanteil einfach weg, sodass selbst die Ausgabe der „neueren“ Version bei Eingaben, die tatsächlich verarbeitet werden können, unterschiedlich ist.

    Das grundlegende Problem ist einfach: das Modell konnte den Code sehen, aber nicht den Vorfall, der ihn hervorgerufen hat. Die Ticketanfrage, die Nachbesprechung, der Chatverlauf sowie die Stunden, die mit dem Debuggen verbracht wurden, befanden sich alle außerhalb der Datei. Das Ausführen von git blame hätte eine Commit-Nachricht wie „fix timestamp parsing for partner feed“ zutage gefördert, die die gesamte Begründung darstellte – doch niemand bat den Assistenten, nachzusehen. Der Reviewer stimmte der Löschung aus demselben Grund zu wie der Assistent: Beide betrachteten den Diff und nicht die Historie.

    Warum das Testframework weiterhin grün blieb

    Die Tests überprüften, was die Funktion für von jemandem vorgestellte Eingaben zurückgab, und der problematische Eingabewert war per Definition einer, den niemand vorgesehen hatte.

    Die Testfälle enthielten Zeitstempel mit drei Nachkommastellen, weil das ist, was Menschen beim manuellen Erstellen von Testdaten eingeben. Siebenzifferige Werte traten nur im Live-Feed eines Partners auf.

    Deshalb sind umfangreiche Refaktorisierungen riskant – unabhängig davon, ob KI beteiligt ist: eine bestandene Testsuite beweist lediglich, dass das von den Tests beschriebene Verhalten erhalten blieb. Sie sagt nichts über Verhaltensweisen aus, die nicht beschrieben werden. Wenn man dreißig Zeilen per Hand refaktorisiert, denkt man an jede einzelne davon. Wenn gleichzeitig viertausend Zeilen geändert werden, übernimmt die Testsuite fast die gesamte Überprüfung, obwohl sie nie dafür konzipiert wurde.

    Coverage-Metriken halfen nicht weiter. Die Funktion hatte eine vollständige Zeilenabdeckung: Jede Zeile wurde während der Tests ausgeführt. Ein bestimmter Branch wurde einfach nie mit den relevanten Eingabedaten ausgelöst. Die Zeilenabdeckung misst lediglich, welcher Code ausgeführt wurde – nicht, welche Eingabemöglichkeiten getestet wurden.

    Sortierung von KI-Änderungen nach Risiko

    Eine praktische Lösung besteht darin, die Überprüfung der von KI erzeugten Unterschiede nicht mehr einheitlich durchzuführen, sondern sie nach Kategorien zu sortieren. Die Kategorien weisen sehr unterschiedliche Risikoprofile auf:

    • Niedriges Risiko: Typangaben, Umbenennungen, Formatierung, vorzeitige Rückgaben sowie ähnliche mechanische Überarbeitungen. Sie sind lokal begrenzt und leicht durch Lesen überprüfbar.
    • Mittleres Risiko: Zusammenführung doppelter Hilfsfunktionen. In der Regel unproblematisch, doch man sollte prüfen, ob die zusammengeführte Version alle Eingaben der Aufrufer verarbeitet.
    • Hohes Risiko: Löschung von Code, der ungenutzt zu sein scheint, Entfernung von Sonderfällen, die überflüssig erscheinen, sowie Vereinheitlichung von Verhaltensweisen, die inkonsistent wirken.

    Die Gruppe mit hohem Risiko weist ein gemeinsames Muster auf: In jedem Fall bemerkt das Modell korrekt, dass etwas nicht in Ordnung ist – und gerade die Tatsache, dass etwas falsch erscheint, ist der Grund für seine Existenz.

    Auch der letzte Punkt wirkt sich auf ruhigere Weise aus. Stellen Sie sich ein Modul vor, das Fehler anders formatiert als die anderen, sowie eine Refaktorierung, die dies korrigiert. Der Unterschied war beabsichtigt, weil ein nachgelagerter Verarbeiter genau diese Fehlerstruktur verarbeitet. Das Streben nach Einheitlichkeit war ein guter Instinkt; seine Anwendung in diesem Fall jedoch ein Fehler.

    Fünf Praktiken für die nächste umfangreiche Refaktorierung

    Diese sind grob nach dem Grad geordnet, in dem jede davon das oben beschriebene Problem verhindert hätte.

    Sorgen Sie dafür, dass jeder Unterschied klein genug ist, um ihn wirklich durchzusehen

    Viertausend Zeilen sind keine Änderung, die man noch durchsehen kann. Ab einigen hundert Zeilen scannen die meisten Menschen nur nach offensichtlichen Problemen, anstatt sich mit jedem Teil gründlich auseinanderzusetzen – egal was sie sich einreden. Beschränken Sie jede Sitzung auf ein Modul und jeden Pull Request auf die Änderungen eines Moduls. Das Engpass ist nicht die Leistungsfähigkeit des Modells, sondern die Aufmerksamkeit des Prüfers.

    Eine Begründung für jede Löschung erforderlich

    Vor der Annahme des Ergebnisses sollten Sie den Assistenten um eine Liste aller gelöschten Elemente bitten sowie für jedes Element erklären lassen, warum er zu dem Schluss kam, dass die Löschung sicher sei. Die Zwangsanforderung einer schriftlichen Begründung ist einer der wertvollsten Schritte, den Sie ergreifen können. Jede Begründung im Sinne von „dies erschien überflüssig“ sollte als Indikator für eine manuelle Überprüfung betrachtet werden.

    Geben Sie dem Modell die Historie

    Bitten Sie den Assistenten, für jede Zeile, die er zur Löschung vorschlägt, git blame durchzuführen und die Commit-Nachricht mitzuteilen. Der Grund für eine ungewöhnliche Branch wird dort sehr oft aufgezeichnet, und das Modell nutzt diesen Kontext gut, wenn es ihn hat; es wird jedoch nicht ohne Aufforderung danach suchen.

    Schreiben Sie die „Warum“-Kommentare vor dem Refactoring

    Fügen Sie zunächst erklärende Kommentare hinzu, nicht danach. Ein präziser Kommentar, der den Partner, die siebenzifferigen Brüche sowie die sechsstellige Parser-Grenze nennt, hätte diese Löschung wahrscheinlich verhindert. Eine vage Anmerkung zu den Zeitstempeln des Partners tat das nicht. Wenn die Person oder das Tool, das später den Code liest, nicht erkennen kann, warum eine Zeile vorhanden ist, steht dieser Zeile nur noch eine Bereinigung bevor, bis sie verschwindet.

    Vergleichen Sie Alt und Neu anhand echter Traffic-Daten

    Führen Sie den täglichen Umfang an echten Produktionsdaten durch sowohl die alte als auch die neue Implementierung und vergleichen Sie, was jede davon zurückgibt. Für reine Transformationen wie Parsing und Normalisierung ist das kostengünstig und erfasst genau die Art von Problemen, die bei Fixtures übersehen werden, denn der Produktionsverkehr enthält Eingaben, an die niemand gedacht hat, sie aufzuschreiben. Wenn das Wiedergeben des Verkehrs nicht möglich ist, ist das Auswählen von echten Payloads für Regression-Fixtures die nächstbeste Lösung.

    Haupterkenntnisse

    Die Refaktorierung hat sich trotzdem gelohnt: Der Modul ist verbessert, die Arbeit ließ sich in einen Nachmittag statt in eine Woche erledigen, und ein einziger Rückfall bei viertausend geänderten Zeilen ist im Vergleich zu vielen großen manuellen Refaktorierungen gering. Die Bezeichnung des Ergebnisses als „Fehler der KI“ verfehlt den Kern der Sache. Die KI zog aus dem, was sie sehen konnte – einem Sonderfall ohne genannten Grund – eine vernünftige Schlussfolgerung, und der menschliche Prüfer kam aus demselben unvollständigen Ergebnis zu derselben Schlussfolgerung.

    • Das Repository ist nur ein Teil des Systems; die Gründe für ungewöhnlichen Code finden sich in Tickets, Commit-Nachrichten, Nachbesprechungen und im Gedächtnis der Menschen.
    • Ein Tool, das nur den Code sieht, wird sicherlich alles entfernen, wofür nie eine Begründung aufgeschrieben wurde.
    • Grüne Tests und vollständige Zeilenabdeckung beweisen nicht, dass das Verhalten bei Eingaben, die niemand modelliert hat, beibehalten wurde.
  • Triage der AI-Differenzen nach Risiko, Erstellung schriftlicher Begründungen für Löschungen sowie Bereitstellung der Historie an das Modell.
  • Bereits lange vor dem Einsatz von KI zur Refaktorisierung war es gute Praxis, aufzuschreiben, warum ungewöhnlicher Code existiert; heute ist es der günstigste Schutz, den man hat.