KrylovENSprache wechseln: English
Zurück zu den Notizen

Wenn die Antwort nicht in einem Absatz steht

Die vorige Notiz endete mit einem Fehlschlag. Fragt man ein Retrieval-System, wie viele Fristen kürzer als drei Monate sind, liefert es drei selbstbewusste, unpassende Absätze — weil in keinem einzelnen Absatz diese Antwort steht. Zählen, vergleichen, „was hängt davon ab“, „welches ist das neueste“: Solche Fragen kommen daher wie Suchfragen und sind keine.

Die Alternative ist kein besseres Embedding. Sie besteht darin, nicht länger nach Ähnlichkeit zu stichproben, sondern die Struktur ausdrücklich aufzuschreiben, damit man sie ablaufen kann.

Wie ein Graph des eigenen Codes aussieht

Graphify ist eine Umsetzung dieser Idee, gerichtet auf Codebasen: Es zerlegt ein Projekt in Knoten und Kanten — Dateien, Funktionen, Klassen und die Beziehungen dazwischen — und übergibt einen Graphen, den man durchlaufen kann. Bemerkenswert dabei: Es verwendet weder Embeddings noch einen Vektorspeicher. Code wird mit tree-sitter gelesen, einem gewöhnlichen Parser, und das macht das Ergebnis deterministisch statt probabilistisch — dieselbe Eingabe ergibt jedes Mal denselben Graphen.

Statt das nur zu beschreiben, habe ich es über den Quelltext dieser Website laufen lassen.

105Dateien hinein
500Knoten
712Kanten
2,2 sLaufzeit
0Token verbraucht

Was die 712 Kanten aussagen

contains
342
imports_from
200
imports
106
calls
46
dynamic_import
8
extends
4
indirect_call
2
sonstige
4
Gemessen: graphify 0.9.26, graphify update (der lokale Weg, nur Parser) über den Quelltext dieser Seite — 105 Dateien, rund 62.900 Wörter, auf einem Laptop. Die Balkenlänge ist proportional zur Kantenzahl. Zwei Drittel des Graphen sind contains und imports: das Skelett aus „welche Datei enthält was" und „welche Datei braucht welche". Die 46 calls-Kanten sind die, die Verhalten tragen.

Zwei Komma zwei Sekunden, und nichts hat den Rechner verlassen — kein API-Schlüssel, keine Token, kein Modell. Das ist der praktische Unterschied zwischen Parsen und Inferenz: Ein Parser muss nicht höflich gefragt werden, und er hat keine Meinung.

Jede Kante sagt, woher sie kommt

Übernehmenswert ist — unabhängig vom Werkzeug — die Buchführung. Jede Kante trägt ihre eigene Herkunft: EXTRACTED, wenn sie so im Quelltext steht, INFERRED, wenn das Werkzeug sie erschlossen hat, AMBIGUOUS, wenn es sich nicht entscheiden konnte.

Auf diesem Korpus waren 710 von 712 Kanten EXTRACTED und 2 INFERRED — 0,28% geraten. Beide Vermutungen stehen in einer Datei, und beide lohnen den Blick, weil sie zeigen, wie sorgfältiges Erschließen aussieht.

redact.ts — contains findStructured findTerms digitsOf scan validLuhn validIban validIp mergeHits redactText EXTRACTED INFERRED
Gemessen, nicht ausgedacht: Jeder Knoten und jeder Pfeil hier ist genau das, was graphify aus src/lib/tools/redact.ts herausgezogen hat. Nur die Positionen sind der Lesbarkeit halber angeordnet. Die neun Funktionen stehen in der Datei, die sie contains — auch diese Beziehung ist echt, hier als Umschließung gezeichnet statt als neun weitere Pfeile. mergeHits und redactText haben keine Aufrufkanten zum Rest, und das ist richtig: redactText bekommt seine Eingabe als Parameter, statt sie zu holen.

Beide erschlossenen Kanten stammen aus derselben Konstruktion. findStructured ruft validIban nicht auf, sondern übergibt es an eine andere Funktion, als Argument:

scan(text, IBAN, 'IBAN', out, validIban);

Das Werkzeug hat das als indirect_call festgehalten, mit dem Kontext argument und einer Konfidenz von 0,5 — nicht als Aufruf. Das ist die richtige Lesart: Die Funktion wird aufgerufen werden, aber nicht von dieser. Die Zeile darunter, in der validLuhn innerhalb einer Inline-Funktion aufgerufen wird, steht dagegen als gewöhnliche calls-Kante im Graphen. Beide Lesarten sind richtig, und sie werden aus dem richtigen Grund unterschieden.

Ich habe auch die Gegenrichtung geprüft, also Kanten, die es geben müsste und nicht gibt. In dieser Nachbarschaft gab es keine: Die Leerstellen sind echte Leerstellen.

Das ist die Eigenschaft, die dem vorigen Artikel fehlte. Ein Ähnlichkeitswert von 0,657 kann nicht sagen, ob er eine gute Antwort ist oder ein selbstbewusster Fehlgriff — er ist eine Zahl ohne Rechenschaft über sich selbst. Eine Kante mit dem Vermerk INFERRED · 0,5 · argument sagt genau, was sie ist und wie weit man ihr trauen darf. Wenn die Maschine ohnehin manchmal falsch liegt, ist es mehr wert, sehen zu können, wo sie geraten hat, als etwas seltener falschzuliegen.

Worin es gut ist

Die Fragen, an denen Retrieval scheiterte, beantwortet ein Graph im Ablaufen. Nach den bestvernetzten Knoten dieser Codebasis gefragt, nennt er Base.astro (28 Kanten), Workspace.astro (24), NoteCover.astro (20), useTranslations() (18) und site.config (17). Das ist eine zutreffende Beschreibung der Architektur dieser Seite: das Layout, in das jede Seite eingewickelt ist, die riesige Werkzeug-Komponente, die Titelgrafik, der Übersetzungshelfer. Niemand hat diese Liste aufgeschrieben; sie ist aus der Struktur gefallen.

Dieselben Kanten rückwärts gelesen beantworten „was geht kaputt, wenn ich das hier ändere“ — eine Frage ohne sinnvolles Embedding und mit einer mechanischen Antwort im Graphen.

Auch das Token-Argument hält stand, mit einer Einschränkung beim Wann. Auf eine lokale Frage — wie das Schwärzungswerkzeug entscheidet, was verborgen wird — kam die Traversierung etwa halb so groß zurück wie die Datei selbst. Auf eine Frage über Dateigrenzen hinweg — wo dieses Werkzeug in die Werkzeugoberfläche eingehängt ist — lieferte sie 12.468 Zeichen, während das Öffnen der drei einschlägigen Dateien 138.206 Zeichen bedeutet hätte, weil eine davon eine 127 kB große Komponente ist. Rund das 11-Fache auf diesem Korpus. Der Vorteil ist keine Konstante; er wächst mit dem, was man sonst hätte lesen müssen, um überhaupt zu wissen, wo man nachschauen muss.

Worin es nicht gut ist

Ein Graph des eigenen Codes kennt Struktur, nicht Bedeutung. Er hat mir gesagt, dass findStructured zu validIban führt; wozu eine IBAN-Prüfsumme gut ist oder ob das Schwärzen rechtlich geboten ist, weiß er nicht. Dafür liest man weiterhin den Quelltext — oder fragt ein Modell, das ihn gelesen hat. Die Aufgabe des Graphen ist zu sagen, welche zwanzig Zeilen von dreizehntausend zu lesen sind.

Man muss ihn auch bei seinen eigenen Grenzen behaften. Der erzeugte Bericht rundet seine Herkunftsübersicht auf „100% EXTRACTED · 0% INFERRED“ — in derselben Zeile, in der er festhält, dass 2 Kanten erschlossen wurden. Auf eine Nachkommastelle stimmt das, und es ist genau die Art von Rundung, die zur Denkgewohnheit wird, wenn niemand hinsieht. Und eine Traversierung, die ihr Token-Budget übersteigt, wird abgeschnitten: Bei der dateiübergreifenden Frage warnte sie, die Antwort könne unter den entfernten Knoten sein. Sie hatte zufällig keine entfernt. Gesagt hat sie es so oder so, und das ist das richtige Verhalten.

Das Paar

Retrieval und Traversierung sind keine Konkurrenten, und die Wahl zwischen ihnen ist nicht die interessante Entscheidung. Sie beantworten verschiedene Fragen:

  • „Was steht im Dokument zu X?“ — einbetten, nach Ähnlichkeit suchen und die Absicherungen gegen Halluzination aus der früheren Notiz ernst nehmen.
  • „Was hängt mit X zusammen, und was fällt ohne X um?“ — den Graphen bauen, die Kanten ablaufen und nie raten, wo die Struktur bereits aufgeschrieben ist.

Der vermeidbare Fehler besteht darin, zur Ähnlichkeitssuche zu greifen, weil alle dorthin greifen, und sich dann zu wundern, dass sie nicht zählen kann. Ein Teil dessen, was man wissen will, steht in den eigenen Daten bereits ausdrücklich drin. Er muss nicht angenähert werden — er muss gelesen werden.