Zum Inhalt springen

Dateien editieren ohne Editor

In Teil eins dieser Serie — Die Hände der KI — haben wir argumentiert: Ein Sprachmodell ist eine Stimme ohne Hände; alles, was nach Handeln aussieht, ist eine Werkzeugschicht um das Modell herum, und in dieser Schicht sitzt die Kontrolle. Dieser Beitrag geht eine Ebene tiefer, in die Mechanik eines einzelnen Fingers. Denn selbst mit Händen steht ein Modell vor einem Problem, über das selten gesprochen wird:

Es hat keinen Editor.

Kein Cursor. Keine Auswahl. Kein Strg+F, das den dritten Treffer markiert. Ein Mensch editiert Text, indem er darauf zeigt. Ein Modell kann nicht zeigen. Es kann nur beschreiben.

Wie also ändert eine KI tatsächlich eine Datei?

Die naive Antwort: Positionen beschreiben. „Ersetze die Zeilen 45–52 durch Folgendes.” So reden Menschen über Code, so funktioniert sed — und für Modelle scheitert es auf zwei sich verstärkende Arten.

Erstens: Sprachmodelle können erstaunlich schlecht zählen. Sie nehmen eine Datei nicht als nummerierte Zeilen wahr, sondern als Token-Strom. Wer ein Modell nach der 45. Zeile einer langen Datei fragt, bekommt eine Näherung — selbstsicher vorgetragen.

Zweitens, und grundsätzlicher: Positionsadressen verfallen in dem Moment, in dem man sie benutzt. Jeder erfolgreiche Edit verschiebt jede Zeilennummer darunter. In einer mehrschrittigen Editing-Session driftet die Karte, die das Modell von der Datei hat, mit jedem Schritt weiter von der Realität weg. Und ein nackter Zeilennummern-Edit scheitert leise — Zeile 45 existiert immer, sie ist nur nicht mehr die Zeile, die gemeint war. (Das klassische patch übersteht Drift genau deshalb, weil seine Hunks zitierte Kontextzeilen tragen: Die Notlösung zeigt längst auf Inhalt.)

Die Forschung ist an dieser Stelle deutlich. Eine Studie zu Edit-Formaten von 2026 (To Diff or Not to Diff?) benennt fragile Offsets und fragmentierte Hunks als Kerngrund, warum klassische Unified Diffs für Modelle unnatürlich zu erzeugen sind — der @@ -45,7 +45,9 @@-Hunk-Header ist eine kleine Zählaufgabe, und Modelle verrechnen sich dabei zuverlässig. Ein anderes Paper (Copy-as-Decode) hat das kontrollierte Experiment dazu gemacht und ansonsten korrekte positionale Edit-Referenzen um einzelne Schritte verrutschen lassen: Der gepoolte Exact Match fiel von 100 % auf 15,48 %.

Eine Zeile Drift, 85 Punkte weg. Positionen sind der falsche Adressraum.

Was Modelle deutlich besser können: Text reproduzieren, den sie gerade gelesen haben — Zitieren liegt viel näher an ihrer nativen Operation als Zählen. Das robuste Design, auf das die meisten Tools, die einem LLM direkt Datei-Editing anbieten, unabhängig voneinander gekommen sind, lautet deshalb:

Der zu ersetzende Text IST die Adresse.

Das Modell schickt ein exaktes Zitat dessen, was sich ändern soll (old_str), und den Ersatz (new_str):

{
"old_str": "timeout: 30\nretries: 3",
"new_str": "timeout: 60\nretries: 5"
}

Nirgendwo Positionen. Die API sucht das Zitat und verlangt, dass es eindeutig ist.

Die stille Superkraft dieses Schemas: Es ist selbstvalidierend — in der einen Dimension, die eine API tatsächlich prüfen kann: Entweder der zitierte Text existiert eindeutig, oder es passiert nichts und der Aufruf scheitert laut. Dieses Scheitern trägt Bedeutung: Das Bild, das das Modell von der Datei hat, ist veraltet. Falsche Zeilennummern erzeugen das Gegenteil — einen Edit, der an ungewollter Stelle „gelingt”. Das ist stille Korruption, das schlimmste Versagen einer Editing-API, denn in Agenten-Workflows schaut sich danach niemand die Datei an.

Lautes Scheitern ist ein Feature. Es ist die API, die dem Modell sagt: Lies neu, versuch es dann noch einmal.

Der naheliegende Einwand: Was, wenn timeout: 30 viermal vorkommt?

Die verführerische Lösung: der API Dokumentstruktur beibringen — „ersetze in Abschnitt X”, „innerhalb von Funktion Y”. Dieser Weg endet bei einer formatbewussten API für Markdown, einer weiteren für YAML, einer dritten für Go. Die bessere Lösung wendet dasselbe Prinzip rekursiv an: Auch der Geltungsbereich wird per Zitat adressiert.

Zwei generische Mechanismen decken alles ab:

Anker. Optionale Parameter after und before nehmen Zitate entgegen, die ein Fenster aufspannen; old_str muss nur noch innerhalb des Fensters eindeutig sein. Überschriften, Funktionssignaturen, Config-Schlüssel — die API muss nicht wissen, dass das „Struktur” ist. Es sind einfach Strings, die von Natur aus eindeutig sind, und das Modell ist bestens ausgestattet, sie auszuwählen. Die API bleibt content-agnostisch.

Der Occurrence-Roundtrip. Scopet das Modell nicht und das Zitat ist mehrdeutig, lehnt die API den Aufruf ab — mit nummerierten Fundstellen:

{
"error": "ambiguous",
"matches": [
{ "occurrence": 1, "context": "connect:\n timeout: 30" },
{ "occurrence": 2, "context": "read:\n timeout: 30" },
{ "occurrence": 3, "context": "write:\n timeout: 30" }
],
"retry": "resend with occurrence: N, or narrow with after/before"
}

Das Modell wiederholt den Aufruf mit occurrence: 2. Man beachte, was nicht passiert ist: Das Modell hat nie gezählt. Der Index wurde von der API vergeben, ist per Konstruktion frisch und lässt sich am mitgelieferten Kontext verifizieren. Das Zählen ist von der Partei, die es schlecht kann, zu der Partei gewandert, die es trivial gut kann.

Anker sind nicht gratis — der zitierte Kontext kostet Tokens und frisst einen Teil dessen, was das Scoping spart. Für einen kleinen Edit in einer großen Datei bleibt es trotzdem weit billiger als jede Alternative.

Manche Edits ersetzen einen ganzen Block. Vierzig Zeilen als old_str zu reproduzieren ist Token-Verschwendung und eine Fehlerquelle — ein Abschreibfehler in Zeile 23, und nichts matcht. Die Editor-Geste dafür ist Klick, dann Shift-Klick. Das API-Äquivalent ist replace_range: Anfang zitieren, Ende zitieren, alles dazwischen ersetzen, einschließlich der Ränder:

{
"from_str": "## Deployment",
"to_str": "systemctl restart toolmesh",
"new_str": "## Deployment\n\nShip it with the new runbook: ..."
}

Das Modell beschreibt die Grenzen, statt den Inhalt zu reproduzieren. Für die Grenz-Zitate gelten dieselben Regeln wie für jedes Zitat — eindeutig im Geltungsbereich, Anfang vor Ende, validiert gegen dieselbe Revision. Und weil der Inhalt dazwischen ungesehen bleibt, bietet eine gut erzogene API einen dry_run, der zeigt, was ersetzt würde — und liefert nach einem echten Write den tatsächlich ersetzten Text zurück.

Agenten editieren nicht allein. Ein anderer Agent, ein Mensch, ein Cronjob — alles kann die Datei angefasst haben, seit sie gelesen wurde. Ein Edit auf Basis eines veralteten Lesestands ist das klassische Lost-Update-Problem, und Content-Adressierung allein löst es nicht vollständig: Das Zitat kann noch matchen, obwohl das Dokument drumherum längst weitergezogen ist.

Die Lösung ist dreißig Jahre alt: Optimistic Concurrency. Jeder Lesevorgang liefert eine Revision. Jeder Schreibvorgang trägt sie als base_rev zurück. Hat sich die Datei zwischenzeitlich geändert, wird der Write abgelehnt — das Modell liest neu und zitiert neu. Webentwickler kennen das als ETag und If-Match.

Fast nichts an diesem Protokoll ist neu, und genau das ist der Punkt. Die einzige wirklich neue Entscheidung ist, was adressiert wird — Inhalt statt Positionen —, weil der Aufrufer ein System ist, das fehlerfrei zitiert und schlecht zählt.

In der Praxis ist das Protokoll eine Leiter, und die Tool-Beschreibung sollte das auch sagen:

  1. Kleine Korrektur → nacktes str_replace.
  2. Mehrdeutiger Treffer → after/before ergänzen, oder die occurrence nehmen, die der Fehler gerade geliefert hat.
  3. Substanzieller Blockumbau → replace_range, oder str_replace mit dem ganzen Absatz als Zitat.
  4. Dokument unter ein-, zweitausend Tokens → ein Full Rewrite ist legitim und der robusteste Zug überhaupt. Er skaliert nur nicht.

Das Anti-Pattern sitzt auf der obersten Sprosse: der ungescopte Full Rewrite einer großen Datei. Er lädt zu Kürzungs-Faulheit ein („… Rest der Datei unverändert …”) und zu Drive-by-Änderungen in Passagen, die niemand anfassen wollte. Gescopte Werkzeuge existieren genau deshalb: damit das Modell nie die ganze Datei in seiner Ausgabe halten muss.

Ein Blick auf die Editing-Tools, die real im Produktionseinsatz stehen:

  • Anthropics öffentliches text_editor-Tool basiert auf str_replace mit erzwungener Eindeutigkeit.
  • Aider hat sich nach ausgiebigem Benchmarking auf Search/Replace-Blöcke festgelegt; das Leaderboard bewertet Modelle auch danach, ob sie das Edit-Format korrekt emittieren, nicht nur danach, ob sie die Aufgabe lösen.
  • OpenAIs apply_patch (das von Codex genutzte V4A-Patch-Format) verzichtet auf Zeilennummern und adressiert jeden Hunk über zitierte Kontextzeilen.

Die Benchmark-Lage stützt das. Diff-XYZ von JetBrains Research hat Edit-Repräsentationen direkt verglichen: Search-Replace „performs best for larger models across most tasks” — GPT-4.1 wendet Edits damit mit 0,96 Exact Match an.

Es gibt eine zweite Denkschule — Cursors Fast-Apply und Merge-Modelle à la Morph —, die bewusst faule, menschenartige Edits vom großen Modell akzeptiert und ein kleines Spezialmodell darauf trainiert, sie anzuwenden. Andere Mechanik, dasselbe grundlegende Misstrauen gegen rohen positionalen Output an einer echten Datei.

Wenn Anthropic, OpenAI, Aider und die Benchmark-Lage beim selben Kernzug landen — Edits über zitierten Inhalt adressieren —, ist das keine Mode. Das ist die Form der Randbedingung.

Ein lebender Beweis: das Wiki, in dem dieser Beitrag geplant wurde

Abschnitt betitelt „Ein lebender Beweis: das Wiki, in dem dieser Beitrag geplant wurde“

Nichts davon haben wir am Reißbrett entwickelt. Unser Team-Wiki — in dem die Gliederung genau dieses Artikels lag, bevor er ein Beitrag wurde — ist ein kleines internes Werkzeug namens Tabula: ein LLM-natives Markdown-Wiki über einem gewöhnlichen Git-Working-Tree, gebaut, um von Agenten genauso editiert zu werden wie von Menschen.

Sein gesamter Schreibpfad ist das Protokoll von oben:

  • read_page liefert {content, rev} — die Revision ist das spätere base_rev.
  • edit_page ist str_replace: zitieren, ersetzen, fertig. Ein leerer old_str wird abgelehnt; Einfügungen reproduzieren stattdessen einen umgebenden Anker.
  • Ein mehrdeutiges Zitat liefert 422 ambiguous mit nummerierten Fundstellen und Retry-Optionen — occurrence: N oder after/before.
  • replace_range deckt Blockumbauten ab; batch_edit wendet mehrere Edits atomar an — alle validieren oder keiner.
  • Jeder Write verlangt eine Änderungszusammenfassung und landet als ein Git-Commit. git log ist der Audit-Trail.

Agenten editieren dieses Wiki täglich. Mehrdeutige Treffer kommen vor — selbstähnliche Prosa wie Konfigtabellen und Statuslisten löst sie deutlich häufiger aus als Code — und lösen sich in einem einzigen Roundtrip auf. Was wir kein einziges Mal gesehen haben: eine still korrumpierte Seite. Dieser Tausch, lautes Scheitern gegen billige Recovery, ist das ganze Design.

Teil eins endete mit einem Claim: Intelligenz werden Sie mieten — Ihre Hände sollten Ihnen gehören. Dieser Beitrag ist eine dieser Hände aus der Nähe: kein Fenster mit blinkendem Cursor, sondern ein schmales Protokoll aus Zitaten, Revisionen und ehrlichen Fehlern.

Die Schmalheit macht die Hand regierbar: Jeder Edit benennt exakt den Text, den er anfasst, deklariert die Revision, auf der er basiert, und landet als Commit, den jemand diffen, zurücknehmen und zuordnen kann. In ToolMesh, unserem self-hosted Gateway, ist ein Editing-Backend wie dieses einfach ein weiteres Tool hinter der üblichen Berechtigungs- und Audit-Oberfläche — ein Read-only-Agent sieht die Schreibwerkzeuge gar nicht erst.

Der Editor einer KI würde nie aussehen wie unserer. Für einen unbeaufsichtigten Aufrufer ist das besser so: Er kann keine Zeile anfassen, ohne sie vorher beim Namen zu nennen.

Wer selbst eine Editing-Schnittstelle für Agenten gebaut hat — oder mit einer kämpft: Wir würden uns ehrlich gern austauschen, in den GitHub Discussions.