Große Dateien, die mein Regelwerk selbst gebaut hat
Von 1,8 auf 2,6 Sessions je Aufgabe in drei Tagen, eine Datei mit 1076 Zeilen, eine Regel aus meinem eigenen Kern, die zum Anhängen einlädt, und die Frage, ob git grep noch reicht.
Teil 5 von 5 der Reihe Kettenhemd
Seit dem 8. November baue ich mit meinen Agenten ein Spiel, eine in C# geschriebene Wirtschaftssimulation mit starkem historischem Hintergrund. Für das Projekt habe ich mir viele eigene Skills erzeugt, vor allem zum Prüfen, und mein Liebling darunter ist der Historien-Check. Ich lese selbst schon viel, „aber was so eine Suche wirklich rauskommt, da lernt man selbst jeden Tag was hinzu“.
Am Anfang ging es schnell voran, und ich kam relativ zügig zu einem Spielabschnitt. Am 8. November brauchte eine Aufgabe im Schnitt 1,8 Sessions und 28 Minuten aktive Arbeit. Eine Session ist bei mir ein Chat mit einem Agenten, wobei jede Aufgabe von einer Untersession umgesetzt wird – also einer eigenen Session nur für dieses eine Issue.
Mehr Warten als Reagieren
Zwei Tage später sah das anders aus. Am 10. November brauchte eine Aufgabe im Schnitt 2,6 Sessions und 34 Minuten, während eine einzelne Aufgabe an dem Tag auf 6 Sessions und 5 Übergaben kam. Eine solche Übergabe schreibt eine Session, wenn sie aufhören muss: Sie hält im Issue fest, wo sie steht, und eine frische Session macht weiter.
Parallel dazu stiegen die Kosten, die Kettenhemd je Issue in KettenhemdDaten schreibt, meine eigene Datenbank für Kosten und Befunde. Am 8. November kostete eine Aufgabe im Schnitt 6,87 $, am 9. November 8,40 $ und am 10. November 11,03 $. Ich nutze ein Abo (Claude Ultra); die Beträge sind ein Richtwert aus Claude Code und keine echte Rechnung, doch sie zeigen trotzdem, wie viel Arbeit in einer Aufgabe steckt.
Gespürt habe ich das, bevor ich es gemessen habe: Es wurde zäh und langsam, und ich verbrachte mehr Zeit mit Warten als mit Reagieren.
Eine einfache Frage am Abend
An einem Abend, an dem kaum Tempo in die Sache kam, fragte ich den Agenten, wohin unsere Zeit beim Entwickeln geht, worauf eine einfache Frage folgte: „Haben wir ein monolithisches Design?“
Eigentlich hatte ich gedacht, dass wir das durch die Entwicklermuster verhindern würden, aber die Antwort war ja. Die meistgelesene Datei der Simulation war in drei Tagen auf 1076 Zeilen und 42,7 kB angewachsen. Sechs weitere Dateien hatten 470 bis 690 Zeilen erreicht, und die Architektur-Doku lag bei 41 kB.
Ein Spike bestätigte das am 11. November mit Zahlen – ein Spike ist bei mir eine Session, die ein Thema erkundet und nichts baut. Er las die Protokolle von 96 Untersessions zu 45 Issues. Bis zur ersten Änderung lag der Kontext einer Untersession im Median bei rund 98.000 von 200.000 möglichen Token, wovon rund 29.000 ins Lesen von Code und rund 11.000 ins Lesen von Doku flossen. Von 51 Übergaben kamen 33 von der Kontextgrenze; die Session war also nicht fertig, sie war voll, und jede neue Session kostete rund 3 bis 4 Minuten Anlauf sowie rund 4 bis 5 $.
Die Regel, die zum Anhängen einlädt
Ich ließ mir von den Agenten erklären, wie es dazu kommt. Mir war das gar nicht bewusst.
Die Entwicklungsregeln in Version 19, die jede Untersession lädt, sagten nichts dazu, wie Code auf Dateien verteilt wird. Dafür stand dort mit „Kleinste korrekte Änderung“ eine Regel, die ich für vernünftig hielt und die verhindern sollte, dass ein Agent auf Vorrat baut. Sie lädt allerdings auch dazu ein, neuen Code einfach an die bestehende Datei anzuhängen, was nun einmal die kleinste Änderung ist. Mit jedem Issue wuchsen so dieselben Dateien, und jede mischte mehr Zuständigkeiten.
Langsam bin ich es gewohnt, dass der Agent etwas ganz anderes macht, als ich es mir im Kopf ausmale, aber das war mit der stetigen Weiterentwicklung auch das Ziel des Projekts. Eigentlich freue ich mich, wenn ich sehe, dass etwas aus dem Ruder läuft – das ist sofortiges Lernen.
Version 20: neue Zuständigkeit, neue Datei
Version 20 habe ich extra für die Spielentwicklung gemacht, doch sie gilt für alle meine Projekte, denn das Anhängen passiert überall.
Jeder Plan vor größerer Arbeit enthält jetzt eine Dateikarte, in der für jede neue oder geänderte Datei in einem Satz steht, wofür sie zuständig ist und was andere Dateien von ihr benutzen. Neue Zuständigkeit heißt dabei neue Datei. Ordner und Dateinamen folgen den Fachbegriffen des Projekts, und in C# gibt es genau einen öffentlichen Typ je Datei. Bei einem neuen Fachbereich kommt zuerst das Gerüst aus Ordnern, Dateien und Signaturen als eigener Commit, und erst danach wird es gefüllt.
Eine harte Zeilengrenze gibt es nicht, wie ich im Spike gesagt habe: „Es gibt keine harte Grenze, wann eine Datei aufgesplittet werden soll.“ Eine Grenze würde nach Länge teilen statt nach Zuständigkeit. Stattdessen meldet das Prüfskript vor dem Review einen Hinweis für jede Datei, die gewachsen ist und danach über 20 kB liegt. Der Reviewer, ein zweiter Agent, der jede Aufgabe prüft, wertet einen Verstoß als MITTEL, was nicht blockiert, denn was eine Zuständigkeit ist, bleibt Ermessen.
Wenn die großen Dateien bleiben
Ganz verhindern lassen sich große Dateien nicht; Tests, Spielwerte und Doku wachsen weiter. Also habe ich gefragt, ob git grep noch das Richtige ist und wie man besser in großen Dateien sucht.
Das hat eine Vorgeschichte. In Teil 2 habe ich einen Wissensgraphen gegen git grep antreten lassen. Der Graph fand 0 von 42 Dateien, git grep fand 19, und seitdem suchen meine Agenten mit Text.
Die Protokolle zeigten jetzt, wo diese Textsuche teuer wird. Die große Datei der Simulation wurde selten ganz gelesen; stattdessen gab es 71 Suchen mit grep -n und 101 Ausschnitte mit sed -n; insgesamt hatten 104 von 209 gelesenen Ausschnitten 60 Zeilen und mehr. Der Agent sucht mit geratenen Wörtern und liest danach zu große Ausschnitte.
Kettenhemd selbst hat dasselbe Problem. Meine Konzeptdatei hat 201 kB und Zeilen bis 3,5 kB, wo ein Treffer mit grep einen ganzen Absatz liefert. In 286 Session-Protokollen brachten 202 Suchen zusammen 761 kB Ausgabe, im Schnitt 3,8 kB je Aufruf.
Version 20 bringt dafür zwei Werkzeuge mit. Für Markdown gibt es einen Umriss-Befehl, der jede Überschrift mit Zeilenbereich und Größe auflistet und bei einem Suchwort nur die Abschnitte mit Treffern zeigt. Danach liest der Agent nur den Abschnitt, den er braucht. Im Probelauf an der Konzeptdatei war der Umriss 1,3 % so groß wie die Datei, und die Suche nach „sucher“ brachte 545 Byte statt 11.160 Byte mit grep -n -i.
Für Code kommt ein Sprachserver hinzu – das, was in einem Editor hinter „Gehe zu Definition“ steckt. Claude Code bindet ihn über ein Plugin an, das für C# csharp-lsp heißt. Laut der Doku zu Code-Intelligence-Plugins findet der Agent damit Definitionen und Verweise über das Symbol statt über eine Textsuche und bekommt nach jeder Änderung die Fehler, die der Sprachserver meldet. In Cloud-Sessions startet er allerdings nicht. Der Sucher, ein Hilfsagent, der für die Session sucht und liest, holt sich in großen Dateien jetzt zuerst den Umriss oder fragt den Sprachserver; womit er das macht, steht in der Zeile „Große Dateien“ im Steckbrief, der Projektbeschreibung jedes Projekts.
Einen Navigationsserver wie Serena habe ich bewusst nicht genommen. Er lädt viele Werkzeugbeschreibungen in jede Session, und genau diese Grundlast wollte ich nicht erhöhen.
Was noch offen ist
Seit dem 11. November ist Version 20 ausgerollt und das Spielprojekt hat das Update, doch bewiesen ist damit noch nichts. Die Probe des Sprachservers im Spielprojekt steht noch aus: Lädt er das Projekt, und findet er in der großen Datei wirklich Definition und Aufrufer? Das Ziel für die nächsten Aufgaben ist ein Anlauf mit höchstens 75.000 statt rund 98.000 Token bis zur ersten Änderung – ob das klappt, weiß ich erst nach den nächsten Issues.
Gelernt habe ich trotzdem schon etwas. Am Anfang fühlt sich alles gut an, aber irgendwann kommt die Bremse, und meine Bremse stand in meinen eigenen Regeln, in einem Satz, den ich für vernünftig hielt. Deshalb hinterfrage ich meinen eigenen Prozess immer und immer wieder.
Diesen Beitrag hat mein Agent aus einem Interview mit mir geschrieben; Zahlen und Abläufe stammen aus Kettenhemd. Gelesen und freigegeben habe ich ihn selbst.