Startseite / Artikel / Warum deine Codebasis ein Graph ist: Tree-sitter-Parsing für einen KI-Coding-Assistenten

Warum deine Codebasis ein Graph ist: Tree-sitter-Parsing für einen KI-Coding-Assistenten

Warum KI-Coding-Assistenten nicht ganze Repos lesen können, wie Graphen und Syntaxbäume Struktur erfassen und wie man Code mit Tree-sitter parst, um einen Symbolindex aufzubauen.

1386 Wörter

Code ist ein Graph, kein Haufen Text

Wer neu in ein Projekt kommt, kennt den Moment. Man klont das Repo und fragt sich, wo eigentlich die Retry-Logik für Zahlungen steckt. Der Reflex ist einer von diesen beiden:

Ctrl + Shift + F

oder

grep -r "retry" .

Manchmal reicht das, oft aber nicht. Der Retry-Code kann hinter einem Interface liegen, in einem anderen Modul, in einem Decorator, in einer Basisklasse oder hinter einem Strategy-Pattern. Um ihn zu finden, genügt kein Stringvergleich, denn es kommt darauf an, wie die Teile zusammenhängen.

Fragt man dagegen Cursor „Wie funktionieren die Zahlungs-Retries?“, markiert es ein paar Sekunden später mehrere Dateien, erklärt den Ablauf und schlägt sogar Verbesserungen vor. Das wirkt ein bisschen wie Magie. Überraschend daran: Es liest gar nicht dein ganzes Repository, und das könnte es auch nicht.

Ein verbreitetes falsches Bild

Die meisten Entwickler stellen sich das Werkzeug zuerst so vor:

Repository
      │
      ▼
Large Language Model
      │
      ▼
Answer

Klingt plausibel, kann aber nicht funktionieren. Nehmen wir eine mittelgroße Codebasis:

12,000 Python files
≈ 2 million lines of cod

Selbst Modelle mit sehr großem Kontextfenster können nicht bei jedem Prompt effizient ein ganzes Repository verarbeiten, und selbst wenn, wäre es teuer. Jede Quelldatei, jedes README, jede Konfiguration, jeden Test und jede generierte Datei mitzuschicken, hieße grausame Latenz und eine riesige Rechnung. Schlimmer noch: Fast alle diese Tokens wären irrelevant. Geht es um Zahlungs-Retries, muss das Modell keinen Bildverarbeitungscode lesen.

Du liest ja auch nicht alles

Schau dir an, wie eine erfahrene Entwicklerin Dinge findet. Gefragt, wo die Authentifizierung implementiert ist, öffnet niemand main.py und liest Datei für Datei der Reihe nach. Sofort entsteht eine innere Landkarte, ungefähr so:

Request
   │
   ▼
API Router
   │
   ▼
Authentication Middleware
   │
   ▼
JWT Validator
   │
   ▼
User Service

Man folgt Beziehungen, durchläuft also einen Graphen. Dieser Unterschied ist gewaltig. Erfahrene Entwickler merken sich keine Millionen Zeilen. Sie merken sich Verbindungen.

Was ein Repository wirklich ist

Ein Repository ist kein Sack voller Dateien. Es ist ein Netz verbundener Symbole. Nehmen wir ein kleines Projekt:

src/
├── api.py
├── payment.py
├── retry.py
├── queue.py
└── logger.py

Auf den ersten Blick sind das fünf Dateien ohne Bezug zueinander. Zeichnet man die Beziehungen ein, ergibt sich ein anderes Bild:

              api.py
                │
                ▼
         payment.py
           │     │
           ▼     ▼
      retry.py  logger.py
           │
           ▼
        queue.py

Diese Skizze erklärt mehr, als ein Verzeichnisbaum je könnte. Jeder Funktionsaufruf, jeder Import, jede Vererbung, jede Interface-Implementierung und jede Abhängigkeit ist eine Kante im Graphen. So betrachtet ergeben KI-Coding-Assistenten mehr Sinn: Sie navigieren entlang von Beziehungen, statt nur Dateien zu durchsuchen.

Wo Textsuche versagt

Ein kleines Beispiel:

class PaymentProcessor:
def charge(self, order):
        return self.gateway.execute(order)

Wo findet die Zahlung tatsächlich statt? Das lässt sich nicht sagen. gateway könnte Stripe sein, Razorpay, PayPal, ein Mock oder ein Test-Double, und eine Suche nach „charge“ klärt das nicht. Kommt Dependency Injection dazu, wird es noch schlimmer:

processor = container.resolve(PaymentProcessor)

Jetzt wird die Implementierung erst zur Laufzeit gewählt, und Textsuche ist nahezu nutzlos. Moderne Softwareentwicklung setzt stark auf Abstraktion, und ausgerechnet Abstraktion macht naiver Suche zu schaffen.

Code hat Struktur

Programmiersprachen sind kein bloßer Text. Sie sind strukturiert. Nimm diese Funktion:

def calculate_total(price, tax):
    return price + tax

Du siehst Zeichen, ein Compiler sieht Struktur, etwa so:

FunctionDefinition
├── Name
│     └── calculate_total
├── Parameters
│     ├── price
│     └── tax
└── Return
      └── BinaryExpression(+)

Diese Darstellung heißt Abstract Syntax Tree, kurz AST. Sie bewahrt die Bedeutung und verwirft die Formatierung. Diese beiden Schreibweisen:

return price + tax

und

return (
    price
    +
    tax
)

ergeben im Wesentlichen denselben AST. Genau deshalb beginnen moderne Coding-Assistenten damit, Quellcode zu parsen, statt ihn als rohen Text zu behandeln.

Tree-sitter

Viele Entwicklerwerkzeuge setzen auf Tree-sitter. Statt eine Datei Zeile für Zeile zu scannen, baut es einen Syntaxbaum, der die Sprache versteht. Nehmen wir diesen Code:

class PaymentService:
def retry_payment(self):
        pass

Tree-sitter sieht darin keine zwei Textzeilen, sondern eher so etwas:

Module
│
├── Class
│     └── PaymentService
│
└── Function
      └── retry_payment

Diese Struktur zahlt sich später aus, denn damit lassen sich Fragen beantworten wie: Zu welcher Klasse gehört eine Methode, welche Funktionen sind öffentlich, welche Methoden rufen eine bestimmte Funktion auf und welche Symbole werden exportiert? Textsuche kann das nie zuverlässig beantworten.

Ein Repository parsen

Zeit für den ersten Baustein des Assistenten. Abhängigkeiten installieren:

pip install tree-sitter tree-sitter-python

Dann eine Datei parsen:

from tree_sitter import Language, Parser
import tree_sitter_python as tspython

parser = Parser()
PY_LANGUAGE = Language(tspython.language())
parser.language = PY_LANGUAGE
code = """
class PaymentService:
    def retry_payment(self):
        print("Retrying...")
"""
tree = parser.parse(bytes(code, "utf8"))
print(tree.root_node)

Die Ausgabe sieht so aus:

(module
    (class_definition
        (identifier)
        (block
            (function_definition))))

Hübsch ist das nicht, aber nützlich. Man sieht, wo jede Klasse beginnt, wo jede Funktion endet, wo Importe stehen und wo Decorators definiert sind, und niemand musste dafür einen Parser von Hand schreiben.

Alle Funktionen herausziehen

Als Nächstes durchlaufen wir den Syntaxbaum und sammeln alle Funktionen ein:

from tree_sitter import Parser, Language
import tree_sitter_python as tspython


parser = Parser()
parser.language = Language(tspython.language())

def extract_functions(node, source):
    if node.type == "function_definition":
        name = node.child_by_field_name("name")
        print(source[name.start_byte:name.end_byte].decode())
    for child in node.children:
        extract_functions(child, source)

source = open("payment.py", "rb").read()
tree = parser.parse(source)
extract_functions(tree.root_node, source)

Ausgabe:

retry_payment
refund
calculate_total

Achte darauf, was hier passiert ist. Der Code hat nicht nach dem Schlüsselwort def gesucht. Er hat den Syntaxbaum nach jeder Funktionsdefinition gefragt, und das ist ein bedeutender Unterschied.

Einen Index aufbauen

Statt ganze Dateien zu speichern, legst du einen strukturierten Index mit Einträgen wie diesem an:

{
  "symbol": "retry_payment",
  "type": "function",
  "file": "payment.py",
  "class": "PaymentService",
  "start_line": 42,
  "end_line": 67
}

Mach das für jede Funktion, jede Klasse, jedes Interface, jedes Enum und jede Konstante, und aus einem unstrukturierten Repository wird durchsuchbares Wissen.

Was bisher steht

Die bis hierhin gebaute Pipeline sieht so aus:

                Git Repository
                       │
                       ▼
              Repository Scanner
                       │
                       ▼
                Tree-sitter Parser
                       │
                       ▼
             Abstract Syntax Tree
                       │
          ┌────────────┴────────────┐
          ▼                         ▼
     Function Index           Class Index
          │                         │
          └────────────┬────────────┘
                       ▼
                Repository Graph

Nach einem KI-System sieht das noch nicht aus, und das liegt daran, dass es keins ist. Es ist eine Pipeline zur Wissensextraktion, und genau dort beginnt jeder moderne KI-Coding-Assistent.

Warum hier anfangen

Viele Tutorials springen direkt zu Embeddings und Vektordatenbanken. Das ist, als wollte man Google bauen, bevor es überhaupt Webseiten gibt. Wie gut ein Assistent ein Repository versteht, bevor irgendein maschinelles Lernen ins Spiel kommt, bestimmt weitgehend, wie gut er ist. Ist der Index schlecht, rettet ihn auch kein Prompt Engineering. Schlechter Input ergibt schlechten Output.

Wie es weitergeht

An diesem Punkt weiß der Assistent, wo jede Funktion und jede Klasse liegt, versteht aber noch keine Bedeutung. Das nächste Problem ist das Chunking. Die meisten Entwickler zerschneiden Repositories in beliebige Blöcke von 500 oder 1.000 Tokens. Das klingt vernünftig und ist einer der schnellsten Wege zu einem mittelmäßigen Assistenten. Besser ist semantisches Chunking: Aufteilen entlang von Funktionen, Klassen, Symbolen und logischen Grenzen, damit die Suche so verläuft, wie erfahrene Entwickler über Code denken. Das Ergebnis ist ein Repository-Index aus sinnvollen Einheiten, bereit für Embeddings, Vektorsuche und klügeres Retrieval.