Startseite / Artikel

Wie ein KI-Agent zur Behebung von Docs-Fehlern sich selbst täuschte: Falschpositive Ergebnisse in einer Docker-Sandbox

Ein LangGraph-Dokumentbereinigungs-Agent hat eine wirkungslose Korrektur bereitgestellt, die anschließend bei den src-layout-Paketen zu Fehlern führte. Der dahinterliegende Docker-Sandbox-Bug sowie die eigentliche Lösung wurden in click gefunden.

Verlag: REACTAPP.TOP7 Min. Lesezeit1666 Wörter

Quellen: **I built an AI Doc-Fixing Agent. Here’s every way it fooled itself first.**

Ein KI-Agent, der dazu entwickelt wurde, fehlerhafte Codebeispiele in Dokumentationen zu reparieren, erzielte bei seinem ersten echten Repository ein seltsames Ergebnis. Er „reparierte“ einen nicht vorhandenen Fehler, nahm einen völlig intakten Codeabschnitt, änderte nichts daran und veröffentlichte den identischen Text als verifizierte Korrektur.

- print(tqdm.__version__, sys.version, sys.platform)
+ print(tqdm.__version__, sys.version, sys.platform)

Sowohl Zeilen passen überein. Der Agent hat sich selbst davon überzeugt, etwas zu reparieren, das nie falsch war, und das war erst die erste von drei Methoden, mit denen er sich selbst täuschte, bevor der eigentliche Schuldige auftauchte.

Was DocuPatch tut

DocuPatch ist ein von LangGraph angetriebener Agent. Er durchsucht die Markdown-Dateien eines GitHub-Repos, extrahiert Python-Codeblöcke und führt sie in einem sandboxierten Docker-Container aus. Beispiele, die tatsächlich fehlschlagen, werden an Gemini zur Reparatur gesendet, die Korrektur wird erneut im selben Sandbox überprüft, und nur ein verifiziertes Ergebnis wird zu einem Branch und Pull Request.

Die Struktur des Repos:

  • agent/ingest.py kloniert das Repository und findet die Markdown-Dateien.
  • agent/graph.py startet für jede Datei einen LangGraph-Pipeline.
  • agent/parser.py extrahiert die Python-Codeblöcke.
  • agent/tester.py führt jeden Codeausschnitt innerhalb eines Docker-Containers aus.
  • agent/context_collector.py sammelt die relevanten Python-Dateien und -Code für einen Snippet.
  • agent/fixer.py erzeugt das reparierte Snippet.
  • agent/github_client.py erstellt einen Fork, speichert die Korrektur ab und öffnet Pull Requests.
  • tests/ enthält Tests für jeden Bestandteil.
  • Die Grundidee ist einfach: Dokumentationscode verfällt. APIs ändern sich, Funktionen werden umbenannt, Beispiele laufen nicht mehr, und niemand bemerkt es, bis jemand einen nicht mehr funktionierenden Snippet aus einer README einfügt. Der Agent automatisiert die Erkennung. Er findet einen verdächtigen Snippet, beweist durch Ausführung, dass er kaputt ist, bittet ein LLM um eine Korrektur, prüft erneut durch Ausführung, ob die Korrektur funktioniert, und erst danach öffnet er einen Pull Request.

    Der Pipeline umfasst eine Phase zum Klonen und Analysieren, die in einen mit Beweisen gesteuerten Zyklus mündet, und eine Korrektur wird erst nach zwei Nachweisen akzeptiert: Der ursprüngliche Snippet ist fehlerhaft, während der Ersatz funktioniert.

    Die Leitregel ist Evidenzprüfung. Nichts erreicht den LLM, es sei denn, ein Sandbox-Test zeigt, dass es fehlerhaft ist, und nichts wird veröffentlicht, es sei denn, ein erneuter Sandbox-Test zeigt, dass die Korrektur funktioniert. Dieses Prinzip hat überlebt. Der Sandbox selbst hingegen nicht lange.

    Erster Test: tqdm

    Der ursprüngliche Test in der realen Welt bezog sich auf tqdm/tqdm:

    Found 9 Markdown file(s). Starting audit...
    
    Auditing: .github\ISSUE_TEMPLATE\bug.md
      Generating repair for snippet #1
      Verifying generated repair for snippet #1
      Publishing verified repair(s)...
      VERIFIED REPAIR: Branch published
    

    Zwei Dateien wurden „bereinigt“ und veröffentlicht, was auf dem Papier ein Erfolg war. Dann wurde der tatsächliche Diff untersucht:

    - print(tqdm.__version__, sys.version, sys.platform)
    + print(tqdm.__version__, sys.version, sys.platform)
    

    Auch hier sind die beiden Zeilen identisch. Gemini hat nichts geändert und den Code Zeichen für Zeichen neu generiert. Das System testete einen Codeausschnitt, erklärte ihn für fehlerhaft, „behebte“ ihn, indem es denselben Code zurückgab, testete erneut, stellte einen Erfolg fest und führte einen sinnlosen Commit im Fork durch.

    Es handelte sich dabei nicht um einen Fehlerbehebungsfehler. Es war ein Falschpositiv auf der Testebene, weil der ursprüngliche Codeausschnitt niemals fehlerhaft war. Eine Zeile, die tqdm und sys importiert und Versionsinformationen ausgibt, ist so gewöhnlich wie die Verwendung von tqdm – warum meldete dann die Sandbox einen Fehler? Die Frage wurde als verdächtig eingestuft, und es wurde ein weiteres Repository getestet, um vor dem Fazit ziehen ein zweites Datensatz zu erhalten.

    Zweiter Test: pallets/click

    Auf pallets/click war das Muster nicht zu übersehen:

    Auditing: docs\advanced.md
      Generating repair for snippet #2
      Verifying generated repair for snippet #2
      [Reflection Loop 2/3] Revising repair for snippet #2
      ...
      [Reflection Loop 3/3] Revising repair for snippet #3
      NOT PUBLISHED: Generated repair failed verification:
        ModuleNotFoundError: No module named 'click'
    

    Jedes Mal derselbe Fehler. Sechs Dateien wurden jeweils durch drei vollständige Reflexionszyklen geführt, und jeder Fehler war derselbe: ModuleNotFoundError: No module named 'click'.

    Genau diese Wiederholung ist der Hinweis. Wenn sechs Dateien sowie sechs separate Versuche mit einem LLM alle wegen eines einzigen Importsfehlers scheitern, liegt der gemeinsame Faktor nicht im Code, sondern in der zugrunde liegenden Umgebung. Das Umschreiben eines Codeabschnitts kann keinen fehlenden Modul nicht beheben, wenn die Sandbox das Paket unabhängig davon, was geschrieben wird, nicht erkennen kann.

    Durch das Öffnen von tester.py wurde das Mechanismus aufgedeckt:

    command.extend(["-v", f"{project_dir}:/workspace", "-w", "/workspace", "-e", "PYTHONPATH=/workspace"])
    

    Im Container wird das geklonte Repository montiert und PYTHONPATH auf die oberste Ebene des Repositories gesetzt. Das funktioniert, wenn der Paketverzeichnis direkt dort liegt, bricht jedoch zusammen, wenn das Paket eine Ebene tiefer eingebettet ist. Click ist eingebettet: Sein pyproject.toml enthält den eigentlichen Code unter src/click/ anstelle eines click/-Verzeichnisses in der Wurzel, wie es das src-Layout vieler moderner Projekte vorsieht. Mit PYTHONPATH=/workspace suchte der Interpreter in einem Verzeichnis, das überhaupt kein click-Verzeichnis enthielt.

    Eine manuelle Überprüfung außerhalb des Pipelines bestätigte dies:

    docker run --rm -v "C:\temp\click_test:/workspace" -w /workspace -e PYTHONPATH=/workspace python:3.10-slim python -c "import click"
    # ModuleNotFoundError: No module named 'click'
    
    docker run --rm -v "C:\temp\click_test:/workspace" -w /workspace -e PYTHONPATH=/workspace/src python:3.10-slim python -c "import click; print(click.__version__)"
    # import works... but:
    # importlib.metadata.PackageNotFoundError: No package metadata was found for click
    

    Das zweite Ergebnis brachte die meisten Erkenntnisse. Das Ausrichten von PYTHONPATH auf den richtigen Ordner behebte das Import-Problem, doch click.__version__ fiel weiterhin ab, weil die aktuellen Click-Builds __version__ über importlib.metadata erzeugen, welches die Metadaten installierter Pakete und nicht die Quelldateien liest. Solche Metadaten existieren erst nach einem echten pip install, niemals nach einem Trick mit dem Pfad.

    So stapelten sich zwei Fehler. PYTHONPATH wies auf den falschen Ordner für src-layout-Repos hin. Und selbst wenn der Pfad korrekt gewählt war, führt ein einfacher PYTHONPATH-Trick nicht zu einem ordnungsgemäß installierten Paket, während immer mehr Pakete erwartet werden, dass sie installiert werden und nicht nur importierbar sind.

    Eine Umleitung verdient eine Erwähnung, da sie der aufschlussreichste Abschnitt war. Viel Zeit wurde mit Vermutungen über eine Besonderheit bei Docker-Mounts unter Windows verbracht, wobei Experimente mit Anführungszeichen, Pfadformaten und Argumentlisten im Vergleich zu Shell-Strings durchgeführt wurden. Die eigentliche Erklärung war jedoch banal: Der Testordner war leer, weil ein früherer git clone heimlich versagt hatte, ihn zu füllen, und niemand hatte nachgesehen. Eine leere Ausgabe von dir C:\temp\click_test war nicht „noch keine Antwort“, sondern die Antwort selbst – doch sie wurde dreimal übersehen, bevor jemand innehielt, um sie zu lesen.

    Die Lösung: Installieren, nicht raten

    Die wirkliche Lösung besteht nicht darin, einen besseren Weg zur Schätzung zu finden, sondern gar nicht erst zu raten. pip install -e . liest das eigene pyproject.toml oder setup.cfg des Projekts, ermittelt unabhängig von der Struktur den richtigen Ort für das Paket und schreibt echte Installationsmetadaten, sodass die Aufrufe von importlib.metadata genauso funktionieren wie bei jedem echten Nutzer der Bibliothek.

    Die neue Logik in tester.py läuft in dieser Reihenfolge ab:

    • Falls ein pyproject.toml, setup.py oder setup.cfg vorhanden ist, erstellt die Sandbox ein Docker-Bild und führt pip install -e . darin aus. Dies ist der „Installierungsmodus“, bei dem ein Importfehler als gültig angesehen werden kann.
    • Fehlen Metadaten zur Paketverpackung, aber es gibt ein requirements.txt, wird auf den früheren PYTHONPATH-Ansatz, den „pythonpath-Modus“, zurückgegriffen.
  • Im Pythonpath-Modus sollte ein Snippet, das bei einer nicht gelösten Import-Anfrage eines eigenen Pakets des Projekts fehlschlägt, mit ENV_ERROR gekennzeichnet werden – also ehrlich mit „kann nicht festgestellt werden“ – anstatt mit FAIL.
  • Nach der Änderung zeigte das bestehende Testset 26 von 27 unverändert bestandenen Tests. Der einzige Fehlschlag war zu Recht vorhanden, da er die alte Funktionssignatur prüfte, die absichtlich geändert worden war. Die Erwartungen wurden aktualisiert, und drei neue Tests behandelten den Randfall, wodurch insgesamt 74 Tests bestanden.

    Ein echter Fehler in Clicks Dokumentation

    Durch das Installieren des Projekts in einer Sandbox, bei dem nicht mehr geraten werden musste, wurde Click erneut ausgeführt. Die sechs zuvor identisch fehlgeschlagenen Dateien erhielten nun eine faire Chance, und eine davon enthüllte einen tatsächlichen, bisher unentdeckten Fehler in Clicks eigener Dokumentation.

    docs/commands.md beschreibt einen gekoppelten CLI-Pipeline, und eine Beispielfunktion lautet:

    @cli.result_callback()
    def process_pipeline(processors, fin):
    - iterator = (x.rstrip("\r\n") for x in input)
    + iterator = (x.rstrip("\r\n") for x in fin)
    

    Die Funktion erhält tatsächlich fin, den Datei-Handle, den click bereitstellt. Die Dokumentation durchläuft hingegen input, was auf Pythons eingebaute input()-Funktion verweist und nicht auf die Datei. Das Durchlaufen von input ist ungültig und führt bereits beim Ausführen des Codes zum Scheitern, doch ein Leser würde es nicht erkennen, da der Name plausibel erscheint.

    DocuPatch entdeckte dies, bestätigte einen echten Fehler im Sandbox-Umfeld, erstellte die Korrektur, überprüfte, dass die korrigierte Version von Anfang bis Ende funktioniert, und veröffentlichte sie. Es war das erste Ergebnis in der gesamten Untersuchung, das sowohl ein echter Dokumentationsfehler als auch eine echte, korrekte Lösung war – ohne Störungen im Sandbox-Umfeld oder falsche Positivfälle. Dafür existiert das Projekt schließlich.

    Lektionen aus der Fehlersuche

    • Überprüfen Sie das Ergebnis, bevor Sie dem Urteil vertrauen. Etiketten wie „Bestanden“ oder „Veröffentlicht“ sind nur so zuverlässig wie der Test, der darunter steht. Der tqdm-Fehler wurde nur deshalb entdeckt, weil die Unterschiede angezeigt wurden und das grüne Häkchen nicht einfach geglaubt wurde.
    • Identische Fehler in vielen Fällen deuten auf eine gemeinsame Ursache hin. Sechs Dateien, sechs unabhängige Versuche mit LLMs und eine Fehlermeldung ergeben ein Signal auf Systemebene.
    • Beobachten Sie erst, bevor Sie Theorien aufstellen. Es vergingen Stunden mit Theorien zu Docker und Windows-Pfaden, bevor jemand überprüfte, ob im Testverzeichnis tatsächlich Dateien vorhanden waren.
    • Ein fehlgeschlagener Test kann bestätigen, anstatt Schaden zu verursachen. Als die Änderung der Signatur von run_snippet einen Test zum Scheitern brachte, war die richtige Frage, was dieser Test eigentlich überprüfen sollte, und nicht, was kaputtgegangen war.

    Nun ist der Sandbox-Bug behoben, durch Tests abgedeckt und in einem Repo, mit dem es zuvor Probleme gab, bestätigt. In Clicks Dokumentation befand sich ein echter Defekt – ein Variablennamen, der bereits beim Ausführen des Beispiels zu Fehlern führte – und der Agent fand ihn, reparierte ihn korrekt und überprüfte die Reparatur vor der Veröffentlichung. Das ist die zentrale Idee des Projekts, die anhand eines echten Falls und nicht eines synthetischen nachgewiesen wurde.