Startseite / Artikel

Vier Arten von Speichern, die Ihr Programmier-Agent benötigt – wobei nur eine davon implementiert ist.

Code-Graphen geben Agenten Struktur, doch Ingenieure benötigen auch Gründe, Historie und Verfahren. Ein Überblick über vier Arten von Speichern, Vertrauensstufen und wie man sie kombiniert.

Verlag: REACTAPP.TOP11 Min. Lesezeit2627 Wörter

Quellen: Your Coding Agent Is Always the New Guy on Monday.

In letzter Zeit ist eine Reihe von Tools aufgetaucht, deren Ziel es ist, KI-Code-Agenten daran zu hindern, in Repositorien herumzusuchen. Sie analysieren den Code mit Tree-sitter, speichern Symbole und Verbindungen in SQLite und stellen sie über MCP zur Verfügung, oder sie betreiben Sprachserver, die dem Agenten echte IDE-Semantik bieten. Die berichteten Vorteile sind erheblich und die Idee ist gut.

Sie alle beantworten dieselbe Art von Frage: Wer ruft das auf, welche Art ist das, was würde durch eine Änderung zerstört werden?

Sehen Sie nun, wie ein erfahrener Ingenieur eine Änderung in einem unbekannten Bereich angeht. Das Finden der Aufrufer dauert etwa dreißig Sekunden. Danach folgt eine Stunde mit Fragen, die in dieser Kategorie nicht behandelt werden. Warum wurde es so gebaut? Was ist beim letzten Mal passiert, als jemand das versucht hat? Welcher ist hier die übliche Abfolge – muss ich vor dem Ausführen der Tests neu generieren?

Vier Fragen, vier verschiedene Arten von Speicher. Fast jede heutige Agentenkonfiguration nutzt eine davon und bezeichnet sie als Kontext.

Der Test, der sie voneinander trennt

Vor der Taxonomie gibt es den Test, der sie erstellt. Für jede Beziehung, die der Agent kennen soll, prüfen Sie, ob bereits ein formales Artefakt sie festhält, das von einem deterministischen Tool gelesen werden kann.

Falls ja, analysieren Sie es. Das Artefakt könnte ein Compiler, ein Typprüfer, ein Bytecode-Reader oder ein verteiltes Protokoll sein. Die Verwendung eines Sprachmodells zur Extraktion von Beziehungen, die bereits ein Parser kennt, ist langsamer, teurer und weniger präzise – außerdem wird dabei wertvolle Grundinformation, die Sie bereits besitzen, weggeworfen.

Falls nein, existiert die Beziehung nur in Prosa und menschlichem Urteilsvermögen, und die Modellextraktion ist der einzige Weg, sie zu erhalten.

Es geht hier nicht um eine Trennung zwischen Code und Dokumenten. Es geht darum, ob eine Grammatik existiert. Die Trennung ist ausreichend klar, sodass unterschiedliche Arten von Wissen unterschiedliche Extraktionsverfahren, unterschiedliche Aktualisierungsintervalle sowie unterschiedliche Zuverlässigkeitssemantiken erfordern. Wenn man alles durch denselben Prozessschritt leitet, wird mindestens ein Fall falsch behandelt.

1. Strukturelles Gedächtnis: Auswirkungen einer Änderung

Das ist das Wissen des Agents über den Code selbst. Es handelt sich um das von allen aufgebauten Gedächtnis, und der übliche Fehler besteht darin, es nur aus einer einzigen Quelle zu beziehen.

  • Tree-sitter bietet Breite: eine Auflistung von Knoten und Intervallen, Unterstützung für über 40 Sprachen, Toleranz gegenüber Fehlern sowie Angaben zu den Millisekunden pro Datei. Es löst keine Typen. Es kann angeben, dass Zeile 3 einen Aufruf von etwas namens execute enthält, aber nicht, welcher execute es ist. Die Namensabgleichsfunktion stellt keine kompilatorähnliche Analyse dar, und dieser Mangel zeigt sich insbesondere in polymorphem, schichtweisen Code mit Abhängigkeitsinjektion.
  • LSP gewährleistet Korrektheit durch textDocument/definition, references und implementation, unterstützt durch einen echten Typprüfer. Es startet langsam, benötigt eine funktionierende Kompilierung und darf nicht auf dem Abfragenpfad laufen, da ein nicht vorbereiteter Sprachserver jeden verfügbaren Latenzbudget zerstört. Es sollte als Hintergrundprozess zum Erweitern der Informationen genutzt werden.
  • Bytecode zeigt Code, der nur nach Abschluss der Kompilierung existiert: von Spring Data abgeleitete Abfragen, CGLIB-Proxye, MapStruct-Mapper sowie alles, was von einem Annotation-Processor erstellt wurde. Die Quelldateien enthalten nichts davon. In target/classes ist hingegen alles zu finden.
  • AOT-Ausgabe oder /actuator/beans zeigen, wie die Abhängigkeitsinjektion funktioniert – mit anderen Worten, welche konkrete Klasse unter welchem Profil welche Schnittstelle implementiert. Aus der Quelldatei lässt sich das nicht ermitteln, denn die Antwort befindet sich dort nicht.
  • Verteilte Trace-Daten liefern Informationen über den Ablauf zwischen Diensten sowie über asynchrone Kommunikationswege: vom Controller über HTTP zu einem anderen Dienst, vom Produzenten zu einem Thema bis zum Verbraucher. Jeder Sprachserver endet an einer Modulgrenze, und verteilt Systeme bestehen im Wesentlichen aus solchen Modulen. Wenn Sie OpenTelemetry verwenden, sammeln Sie bereits einen Aufrufgraphen zwischen Diensten, werfen ihn aber anschließend weg.
  • Die wichtigste Gestaltungsentscheidung besteht darin, dass jede Kante eine Kategorie erhält:

    • resolved: Ein Typprüfer hat dies bestätigt, sodass darauf vertrauensvoll gehandelt werden kann.
    • observed: Eine tatsächliche Ausführung hat sie erzeugt, was die Grundlage für die abgedeckten Pfade darstellt.
    • inferred: Es wurde allein anhand des Namens abgeglichen. Wahrscheinlich korrekt, aber unüberprüft.
    • ambiguous: Es existieren mehrere mögliche Kandidaten. Alle werden zurückgegeben, ohne dass eine ausgewählt wird.
    • dynamic: Es wird zur Laufzeit berechnet und ist unentscheidbar, daher wird es als unentscheidbar erfasst und nicht weggelassen.

    Wenn man einem Agenten ein Diagramm ohne Kategorien gibt, behandelt er jede Kante als Fakt und refaktorisiert anschließend auf Basis einer Vermutung. Wenn man ihm mitteilt, dass eine Kante inferred ist, überprüft er sie vor dem Handeln. Genau diese Spalte zeigt den Unterschied zwischen einem System, das stillschweigend in die Irre führt, und einem, das angemessen mit Problemen umgeht.

    Eine Folge, die nur wenige Implementierungen ausdrücklich erwähnen: In einer schichtbasierten Spring- oder Django-Codebasis liefert die Auswirkungsanalyse nur eine Untergrenze, niemals eine vollständige Auflistung. Es ist sicher, zu sagen „17 Aufrufer, 3 ungelöste Interface-Dispatches, 1 generierte Methode“. Nur „17 Aufrufer“ zu nennen, ist irreführend.

    Der Datenverlust tritt stündlich auf und ein Neubau dauert Sekunden.

    2. Wissensspeicher: Die Gründe hinter dem Code

    Dieser Speicher enthält die Begründungen und befindet sich in Dokumenten, die kein Parser lesen kann: Entscheidungsprotokolle, Nachbesprechungen zu Vorfällen, Threads zu Pull-Requests, Tickets, RFCs, Designdokumente sowie Commit-Nachrichten mit eigentlichem Inhalt.

    Man sollte in einer Konfigurationsdatei eine Anzahl der Wiederholungsversuche von 3 festlegen. Die Erklärung dafür ist in einem ADR aus dem Jahr 2023, einer Nachbesprechung zu doppelten Abrechnungsanweisungen sowie in einem Thread enthalten, in dem jemand die SLA eines Geschäftspartners beschrieben hat. Der Zusammenhang zwischen ihnen – nämlich eine Richtlinie, die aufgrund eines Incidents entstanden ist und eine Integration einschränkt – wird in diesen drei Dokumenten jeweils auf unterschiedliche Weise dargestellt.

    GraphRAG ist hier ein gutes Werkzeug, und es ist hilfreich, zu wissen, wofür es dient. Es gibt sechs Schritte:

    1. Chunking teilt Dokumente deterministisch in Texteinheiten auf.
    2. Extraction ist eine Aufgabe des LLM: Ein Modell liest jede Einheit und gibt Entitäten sowie Beziehungen aus.
    3. Description summarisation ist ebenfalls eine Aufgabe des LLM, bei der dieselbe Entität, die auf fünfzig verschiedene Arten beschrieben wurde, in einer einzigen Beschreibung zusammengefasst wird.
    4. Leiden clustering benötigt kein Modell. Es teilt das Graph durch Modulartität in verschachtelte Gemeinschaften auf, also Gruppen, deren innere Kanten deutlich zahlreicher sind als die ausgehenden Kanten.
    5. Gemeinschaftsberichte sind LLM-Zusammenfassungen pro Gemeinschaft, die bottom-up erstellt werden, wobei höhere Ebenen aus den darunterliegenden Berichten gebildet werden.
    6. Embedding erzeugt Vektoren, die als Eingangspunkte für Abfragen dienen.

    Zwei Begriffe verdienen eine genaue Betrachtung. Ein Kante in dieser Konfiguration ist kein typischer Zeiger – sie enthält eine prosaische Beschreibung des Modells zusammen mit einem Gewicht sowie Quellenangaben, wodurch jede Kante die Interpretation des Modells einer Aussage festhält. Ebenso ist eine Gemeinschaft mehr als nur ein willkürlicher Teilgraph, da eine zufällige Menge an Knoten überhaupt keine Kanten teilen kann und dadurch isolierte Punkte entstehen, die nichts zum Zusammenfassen bieten. Leiden wählt Cluster aus, deren induzierte Kanten dicht sind – genau das ermöglicht einen kohärenten Bericht.

    Der Vorteil ist die globale Suche. Anstatt die dem Abfragedatum am nächsten liegenden Abschnitte abzurufen, stellt das System die Frage jeweils jedem einzelnen Community-Bericht und kombiniert die teilweisen Antworten. Dadurch werden Eigenschaften des gesamten Korpus auffindbar: Eine Frage zu den wiederkehrenden Ausfallmustern im Abwicklungsprozess erhält eine Antwort, die nicht in einem einzigen Nachbesprechungsbericht enthalten ist. Herkömmliches Vektor-RAG kann das nicht leisten, egal wie fein die Abschnitte angepasst sind.

    Die Aktualisierungen erfolgen monatlich und sind kostspielig: Drei der sechs Schritte beinhalten Aufrufe an LLMs, wodurch die Indexierungskosten mit der Größe des Korpus, der Anzahl der Durchläufe zur Datenerhebung sowie dem Preis des Modells ansteigen. Das ist akzeptabel, da sich der Korpus kaum verändert.

    3. Episodisches Gedächtnis: Was bei früheren Versuchen geschah

    Dieser speichert die eigene Historie des Agenten in diesem spezifischen Repository. Der letzte Zugriff auf SettlementService hat drei Zuweisungstests zum Scheitern gebracht. Die Änderung dieser Schnittstelle ohne Neugenerierung der Mapper führt zum Build-Fehler. Die beiden neuesten PRs, die die Validierungsstufe übersprungen haben, wurden abgelehnt.

    Er verfügt außerdem über eine Eigenschaft, die kein semantischer Index beanspruchen kann: Seine Grundwahrheit ist deterministisch. Ein Postmortem enthält eine menschliche Aussage, die von einem Modell abgeleitet wurde. „Der Agent hat X geändert und CI wurde rot“ ist ein vom Build-System entschiedenes Urteil. Für die von ihm abgedeckten Fragen macht das die episodische Erinnerung zur zuverlässigsten Quelle in dieser Konfiguration.

    Drei Gestaltungsaspekte lassen sich leicht falsch umsetzen:

    • Überspringen Sie GraphRAG hier. Die Episoden folgen einer Zeitachse und haben Ergebnisse. Eine Gruppierung nach Kantendichte ist die falsche Struktur – stattdessen ist eine Gewichtung nach Aktualität, Häufigkeit sowie Erfolg oder Misserfolg erforderlich.
    • Eine Ausschließung ist notwendig. Die semantischen Indizes können darauf verzichten, dieser jedoch nicht. Eine Lektion aus der Refaktorisierung von vor zwei Architekturen ist nicht nur veraltet, sondern führt den Agenten auch in die falsche Richtung.
    • Fehler häufen sich an. Angenommen, der Agent handelt falsch und dokumentiert das Ergebnis – es wird zu einer in Erinnerung behaltenen Tatsache, die in jeder späteren Sitzung weitergegeben wird. Behalten Sie das Ergebnis (CI fehlgeschlagen, PR abgelehnt) mit hoher Zuverlässigkeit bei, während die eigene Schlussfolgerung des Agenten mit weitaus geringerer Zuverlässigkeit bleibt; lassen Sie die Schlussfolgerung schließlich verfallen, während das Ergebnis weiterhin bestehen bleibt.

    Der Austritt erfolgt pro Sitzung.

    4. Prozedurales Gedächtnis: überhaupt kein Index

    Prozedurales Gedächtnis bezieht sich darauf, wie Dinge erledigt werden: der Build-Befehl, die Überprüfungsliste, die Migrationsreihenfolge, der Hausstil. Der erste Impuls der Menschen ist es, diese Informationen suchbar zu machen – diesem Impuls sollte man widerstehen.

    Alles, was stets relevant ist, wird durch Suchfunktionen schlecht bedient. Solche Informationen gehören in den Systemprompt oder in ein AGENTS.md, das bei jeder Sitzung ohne Suche geladen wird. Wenn man sie hinter Suchfunktionen platziert, entsteht ein Problem, bei dem der Agent nicht weiß, wo er suchen soll, und darauf zu hoffen, dass der Agent etwas findet, ist kein funktionierender Mechanismus.

    Alles, was nur unter bestimmten Umständen relevant ist, sollte als Tool mit Auslöser bereitgestellt werden. Beispielsweise sollte das Ausführen des Integrationssuites etwas sein, das der Agent aufrufen kann, anstatt ein Dokument, an das er erst denken muss, um danach zu suchen.

    Der Fall, der beobachtenswert ist, ist der erlernte Arbeitsablauf. Angenommen, der Agent stellt fest, dass in diesem Repository die Mapper vor dem Ausführen der Tests neu generiert werden müssen, und dass diese Reihenfolge weiterhin funktioniert. Das ist ein Verfahren, das aus Episoden abgeleitet wurde. Speichern Sie es als Kandidat-Regel mit einer Erfolgsanzahl, heben Sie es nachdem es sich bewährt hat zu den immer geladenen Anweisungen auf und behalten Sie die ursprünglichen Episoden bei, damit die Erhöhung überprüft werden kann.

    Prozedurales Gedächtnis ist also kein vierter Index. Es ist das, was übrig bleibt nach dem dritten, nachdem es aus dem Abruf entfernt und in Anweisungen geschrieben wurde.

    Die Verknüpfung: ein Symbolnamensraum

    Die drei abgerufenen Erinnerungen treffen auf die Identität eines Symbols zusammen, und niemand hat diesen Aspekt gelöst. Strukturelle Knoten verwenden vollqualifizierte Namen. Episodische Einträge beziehen sich auf das Symbol, mit dem der Agent tatsächlich gearbeitet hat, wodurch sie präzise zusammengeführt werden. Entitäten im Wissensindex sind Zeichenketten, die ein Modell aus Prosa ausgewählt hat, wie zum Beispiel „SettlementService“, „the settlement service“, „the settlement reconciliation path“ oder „the counterparty SLA constraint“, und ihre Zuordnung zu echten Symbolen ist eine Aufgabe der verschwommenen Abgleichung.

    Sich etwas anderes vorzumachen, ist genau das Versagen, das die verschiedenen Ebenen verhindern sollen, weshalb der Zusammenführungsprozess seine eigene Hierarchie hat:

    • exact: Der Name erscheint wortwörtlich und bezieht sich auf genau ein Symbol.
    • strong: Eine normalisierte Phrase weist eindeutig auf eine Klasse hin.
  • weak: Ein Konzept, das mehrere Klassen umfasst, deren Grenzen unklar sind. Eine Menge an Symbolen wird erneut bereitgestellt und vom Agenten überprüft.
  • unresolved: Eine reale Einschränkung, für die es nirgendwo ein passendes Symbol gibt. „Der SLA des Gegenübers“ ist eine echte architektonische Einschränkung, die im Code nicht dargestellt wird.
  • Bei dieser letzten Kategorie sollte man sie beibehalten, zurückgeben und keine Zwangseinstufung vornehmen. Eine gut lesbare, ungelöste Einschränkung ist besser als eine falsche Symbolzuordnung, und durch Zwangseinstufungen kann ein Agent fälschlicherweise die falsche Klasse bearbeiten.

    Was der Agent tatsächlich fragt

    Eine Abfrageschnittstelle könnte so aussehen:

    what_breaks(symbol)        → structural: resolved callers, tiered
    why_is_this(symbol)        → knowledge: decisions, incidents, constraints
    what_happened_before(sym)  → episodic: past attempts and their outcomes
    related_by_intent(concept) → resolve a phrase to a symbol set, tiered
    

    Dann stellt sich eine Frage, auf die keiner der Drei allein antworten kann: „Ich werde die Wiederholungspolitik bei der Abrechnung ändern. Was bricht dabei, warum sind es drei Optionen und hat jemand das schon einmal versucht?“

    Knowledge Maps verknüpfen „Wiederholungspolitik“ mit SettlementService.retry auf der Stufe strong und liefern den ADR sowie das Postmortem. Structural nimmt dieses Symbol und gibt die aufrufenden Komponenten an, darunter zwei Dienste, die nur in den Trace-Daten sichtbar sind. Episodic fügt hinzu, dass eine ähnliche Änderung im letzten Quartal die Zuweisungstests gestört hat.

    Das ist eine Antwort, die aus drei Arten von Beweisen zusammengestellt wird – jeweils von dem Mechanismus erzeugt, der sie liefern kann – und jede mit Angaben dazu versehen ist, inwieweit man ihr trauen kann.

    Was das kostet

    • Die meisten Dokumentensammlungen sind zu dünn. Neun ADRs sowie Commit-Anteile mit der Angabe „Bug beheben“ liefern dem Wissensindex nichts, womit er arbeiten könnte. Schauen Sie sich vor dem Aufbau an, was Sie bereits haben.
    • Die Entitätsauflösung wird niemals perfekt. Die Leiter hilft dabei, damit umzugehen, heilt das Problem jedoch nicht. weak wird wahrscheinlich die am stärksten genutzte Ebene sein, daher planen Sie Kontrollen statt Vertrauen.
    • Fangen Sie schmal an. Beginnen Sie mit Tree-sitter plus LSP. Fügen Sie Bytecode hinzu, sobald generierter Code falsche Ergebnisse liefert, und Spuren, wenn Anfragen zwischen Diensten weitergeleitet werden. Fügen Sie den Wissensindex erst hinzu, wenn das Korpus eine Indexierung wert ist.
  • Niemand hat alles davon entwickelt – das kann ein Anfang oder eine Warnung sein. Die einzelnen Komponenten befinden sich in verschiedenen Gemeinschaften: Leute, die sich mit Code-Intelligenz beschäftigen, lesen die GraphRAG-Papiere nicht, GraphRAG-Experten denken nicht an Typprüfer, und Personen, die sich mit Speicherarchitekturen auskennen, arbeiten meist an Chat-Assistenten statt an Codebasen.
  • Über die Einarbeitung eines neuen Mitarbeiters nachdenken

    Nehmen wir an, ein erfahrener Ingenieur tritt am Montag bei und erhält Zugriff auf das Repository – nichts weiter. Er kann jede Zeile lesen, jeden Aufruf nachverfolgen und die Tests ausführen. Bis Freitag ist er immer noch langsamer als ein Kollege, der bereits vor einem Jahr eingestellt wurde – und das liegt nicht an seinen Fähigkeiten.

    Der Kollege vom letzten Jahr verfügt über drei Dinge, die nie im Repository waren, während der Neue sie nicht hat.

    Einerseits sind es die Gründe für die ungewöhnlichen Entscheidungen. Die Wiederholung erfolgt dreimal aufgrund eines Vorfalls im März, und die Workaround-Lösung zur Abstimmung existiert, weil die API eines Geschäftspartners an Freitagen nicht richtig funktioniert. Kein Kommentar erwähnt das. Es steht in einem ADR, in einer Nachbesprechung und in einem Gespräch.

    Andererseits spielt die Geschichte eine Rolle. Jemand versuchte im zweiten Quartal, diese Schnittstelle zu vereinfachen, wodurch die Zuweisungstests eine Woche lang nicht funktionierten, weshalb die Leute sie in Ruhe lassen.

    Drittens gibt es die lokale Arbeitsweise. Mapper werden vor dem Ausführen der Tests neu generiert. Pull Requests, die die Validierung überspringen, werden abgelehnt. Niemand schaut sich dazu ein Dokument an – so arbeitet das Team einfach.

    Ein Mitarbeiter, der nur über ein Code-Graph verfügt, ist eine Einstellung am Montag: Er kennt den gesamten Repository-Inhalt nicht, hat kein Verständnis dafür, warum etwas so ist, erinnert sich an keine früheren Versuche und hat kein Bewusstsein für die lokalen Besonderheiten. Jede Sitzung beginnt wie ein neuer Montag.

    Das ist die Lücke – und kein Werkzeug trägt die Schuld. Ein Code-Graph beantwortet seine eigene Frage gut, und die heutigen Implementierungen sind solide. Was fehlt, ist, dass drei der vier Dinge, auf die ein fähiger Ingenieur angewiesen ist, überhaupt nicht im Einsatzumfeld vorhanden sind, wodurch das Vierte alles tragen muss.

    Die Einzeilenversion

    Kurz gesagt: Der Compiler kümmert sich um die Struktur, da Struktur deterministisch ist. Ein Modell kümmert sich um die Absicht, da Absicht unstrukturierte Prosa ist. Das Build-System basiert auf Erfahrung. Prozeduren werden aus Erfahrung abgeleitet und im Voraus geladen, niemals erneut abgerufen.

    Kein Werkzeug in dieser Kategorie macht bei der Speicherverwaltung einen Fehler. Jedes Werkzeug erstellt einen Speicherbereich und hört auf – nichts verbindet sie miteinander. Ein besseres Graphenmodell ist nicht die Lösung. Die Lösung sind vier Speicherbereiche, von denen jeder ehrlich angibt, wie zuverlässig er ist, und die in einem gemeinsamen Symbolnamensraum abgebildet werden.

    Die Begriffe und Pipeline-Phasen von GraphRAG folgen der Implementierung von Microsoft Research. Die Kosten schwanken stark je nach Größe des Korpus, Einstellungen zur Datenerfassung und gewähltem Modell, daher sollten Sie vor der Entscheidung mit Ihren eigenen Daten benchmarken.

    Eine Fünfzehn-Minuten-Überprüfung

    Wählen Sie in Ihrer eigenen Codebasis eine Schnittstelle mit fünf oder mehr bekannten Implementierungen, wie beispielsweise PaymentProcessor oder NotificationChannel, wobei eine über @Autowired verbunden ist und nicht direkt implementiert wurde. Fragen Sie Ihren Code-Graph, wer diese Schnittstelle aufruft, und zählen Sie die Ergebnisse.

    Veröffentlichte Benchmarks für diese Kategorie messen Tokens, Tool-Aufrufe und Latenz. Soweit bekannt, misst keiner, ob der zurückgegebene Aufruf-Graph korrekt ist. In einer Codebasis mit deklarierten Schnittstellen und bekannten Implementierungen lässt sich das in fünf Minuten anhand bereits vorhandener Referenzwerte überprüfen.