Startseite / Artikel

Ein LangGraph-Pipeline, das den Speicher der Codebasis für KI-Agenten präzise erhält

Erstellen Sie einen LangGraph-Pipeline, der Wissensdateien pro Service erstellt, diese mit Ragas und DeepEval überprüft, Ausführungen in Langfuse nachverfolgt und lokal auch auf Ollama ausführt.

Verlag: REACTAPP.TOP11 Min. Lesezeit2531 Wörter

Quellen: Codebase Memory for AI Agents: A LangGraph Pipeline That Actually Stays Accurate

Jeder, der Code-Agenten auf ein großes Repository eingesetzt hat, stößt auf eine Frage: Wie kann ein Agent die Codebasis verstehen, ohne für jede Aufgabe wieder alle Dateien durchzulesen? Was folgt, ist der dritte Versuch, dieses Problem zu lösen. Die ersten beiden Versuche nutzten embeddingbasiertes RAG über Docstrings. Beide versagten innerhalb eines Monats, da die Dokumentation jeder Codeänderung hinterherhinkte. Diese dritte Version hat es mit einem Team geschafft, das dreißig oder vierzig Commits pro Tag vornimmt.

Es handelt sich nicht um eine Bewertung des Dateiformats eines anderen. Es ist vielmehr der Prozess selbst: Ein LangGraph-Zustandsgraph, der ein Repository überwacht, für jeden Service eine strukturierte Wissensdatei erstellt, die Genauigkeit seiner eigenen Ausgabe bewertet, bevor ein Agent sie liest, und jede Ausführung nachverfolgt, sodass die Kosten sichtbar sind. Auch die Fehlschläge aus dem ersten Durchlauf werden berücksichtigt.

Was behandelt wird: Warum das Problem mit dem Token-Budget in mehrere Agenten umfassenden Codingszenarien eigentlich ein Problematik der Veraltetheit ist, das vollständige LangGraph-Design (Zustandschema, Knoten, bedingte Kanten, DynamoDB-Checkpointing), ein Zitierverfahren in Amazon Bedrock mit Claude Haiku 4.5, eine Bewertungsmethode basierend auf Ragas und DeepEval, Überwachungsfunktionen mit selbst gehostetem Langfuse, eine vollständig lokale Variante von Ollama für Teams, die keinen Code an ein Cloud-Modell senden können, der git-Hook, der alles miteinander verbindet, sowie eine Analyse möglicher Fehlermuster.

Das Problem

Jedes Mal, wenn ein Codier-Agent eine Aufgabe in einem Repository startet, weiß er nicht, dass beide seiner Optionen mangelhaft sind. Er kann alles lesen, was Token für Dateien verbraucht, die mit der Aufgabe nichts zu tun haben. Oder er kann eine auf Embeddings basierende Suchmethode verwenden, die semantisch ähnliche Abschnitte liefert, aber keine klare Antwort darauf gibt, wer was aufruft oder wem ein Modul gehört.

Die funktionierende Alternative besteht aus einer Handvoll sorgfältig zusammengestellter, strukturierter Dateien – eine pro Service –, die beschreiben, was sie leisten und worauf sie angewiesen sind. Diese Dateien werden einmal erstellt und anschließend automatisch aktualisiert. Ein Orchestrierungsagent konsultiert einen Index, wählt die zwei oder drei relevanten Dateien aus und leitet nur diese an einen Unteragent weiter. Es geht dabei genauso sehr darum, Tokens klug einzusetzen, wie darum, Wissen darzustellen.

Das Dateiformat ist nicht der Schwierigkeitsfaktor. Markdown zusammen mit YAML-Frontmatter ist kaum neuartig. Die Herausforderung liegt in der Mechanik, die verhindert, dass diese Dateien veraltet werden, wenn sich der darunterliegende Code ändert.

Die Architektur

                       git push / commit
                               |
                               v
                  +-------------------------+
                  |   scan_diff (node)      |
                  |   git diff --name-only  |
                  |   scope to changed pkgs |
                  +-------------------------+
                               |
                               v
                  +-------------------------+
                  |   draft_concept (node)  |
                  |   Bedrock Claude Haiku  |
                  |   one file per service  |
                  +-------------------------+
                               |
                               v
                  +-------------------------+
                  |   add_citations (node)  |
                  |   link to runbooks/PRs  |
                  +-------------------------+
                               |
                               v
                  +-------------------------+
                  |   eval_gate (node)      |
                  |   Ragas faithfulness    |
                  |   DeepEval assert_test  |
                  +-------------------------+
                          pass |  | fail
                               |  +----> re-draft (max 2 retries) --> human review queue
                               v
                  +-------------------------+
                  |   relink (node)         |
                  |   fix cross-references  |
                  +-------------------------+
                               |
                               v
                  +-------------------------+
                  |   publish (node)        |
                  |   commit bundle to repo |
                  +-------------------------+

        every node emits a Langfuse trace
        every super-step checkpoints to DynamoDB

Das Diagramm ist absichtlich klein, mit fünf echten Knoten und einer Wiederholungsverbindung. LangGraph ermutigt dazu, etwas Komplexeres zu erstellen, als das Problem eigentlich erfordert. Ein erster Versuch mit elf Knoten erwies sich als unerklärlich, da niemand sagen konnte, warum ein bestimmtes Datei existierte.

Der Stack und warum jedes Element dort ist

+------------------+---------------------------+--------------------------------------+
| Layer            | Tool                      | Why this one                          |
+------------------+---------------------------+--------------------------------------+
| Orchestration    | LangGraph                 | Explicit state, conditional retries,  |
|                  |                           | resumable from checkpoint             |
| Draft model      | AWS Bedrock, Claude       | Cheap enough to run on every commit,  |
|                  | Haiku 4.5                 | fast enough not to block CI            |
| Checkpoint store | DynamoDB                  | Single-digit ms reads, TTL for expiry, |
|                  | (langgraph-checkpoint-aws)| survives a crashed worker mid-run      |
| Eval, faithfulness| Ragas                    | Reference-free scoring of a generated  |
|                  |                           | file against the actual code diff     |
| Eval, CI gate    | DeepEval                  | pytest-native, hard-fails a bad draft  |
|                  |                           | instead of silently publishing it     |
| Observability    | Langfuse (self-hosted)   | Per-node token/cost/latency traces,    |
|                  |                           | MIT licensed, own your own data        |
| Local alternative| Ollama (llama3.1:8b,     | Same pipeline, zero cloud calls, for   |
|                  | nomic-embed-text)         | air-gapped or cost-sensitive teams     |
+------------------+---------------------------+--------------------------------------+

Schritt 1: Das Zustandsschema

Alles in LangGraph beginnt mit der Form des Zustands, der zwischen den Knoten übertragen wird. Wenn man das falsch macht, muss jeder nachfolgende Knoten raten, was der vorherige erzeugt hat.

from typing import TypedDict, Literal
from langgraph.graph import StateGraph, END
class ConceptDraft(TypedDict):
    path: str              # e.g. "services/billing-service.md"
    concept_type: str      # "Service", "Runbook", "API Endpoint", ...
    title: str
    body: str              # the drafted markdown body
    citations: list[str]
    faithfulness_score: float
    retry_count: int
class PipelineState(TypedDict):
    repo_root: str
    commit_sha: str
    changed_files: list[str]
    drafts: list[ConceptDraft]
    failed_drafts: list[ConceptDraft]
    status: Literal["scanning", "drafting", "evaluating", "publishing", "done", "needs_review"]

Darin ist nichts Besonderes. retry_count ist das entscheidende Feld, denn es verhindert, dass ein mangelhafter Entwurf endlos gegen das Evaluationsverfahren anläuft.

Schritt 2: Die Überprüfung auf den Unterschied beschränken

Die Überprüfung des gesamten Repositoriums bei jedem Commit macht Pipelines wie diesen zu teuer, um kontinuierlich ausgeführt zu werden. Die Beschränkung der Überprüfung auf den Unterschied macht sie kostengünstig genug für jeden Commit und nicht nur für eine nächtliche Ausführung.

import subprocess
def scan_diff(state: PipelineState) -> PipelineState:
    result = subprocess.run(
        ["git", "diff", "--name-only", f"{state['commit_sha']}~1", state["commit_sha"]],
        cwd=state["repo_root"], capture_output=True, text=True, check=True
    )
    changed = [f for f in result.stdout.splitlines() if f.endswith((".py", ".go", ".ts"))]
    # map file paths to the service/package that owns them
    packages = sorted({f.split("/")[1] for f in changed if "/" in f})
    state["changed_files"] = packages
    state["status"] = "drafting"
    return state

Die Zuordnung von Dateien zu Paketen ist der einzige Aspekt, der wirklich für jedes Repository spezifisch ist. Ein monorepos mit einer ordentlichen services//-Struktur benötigt etwa drei Zeilen. Ein umfangreiches Legacy-Repository erfordert eine getrennt aufbewahrte Eigentumsübersicht – idealerweise durch das Auswerten einer CODEOWNERS-Datei und nicht durch Raten anhand der Pfade.

Schritt 3: Entwurf mit Bedrock

Dieser Schritt kostet echtes Geld, daher verdient er sorgfältige Handhabung. Claude Haiku 4.5 auf Bedrock ist schnell und kostengünstig genug, um bei jedem Commit ausgeführt zu werden, während größere Modelle nur für den Weg zur menschlichen Überprüfung aufbewahrt werden – niemals für die routinemäßige Entwurfsphase.

import boto3
import json
bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")
MODEL_ID = "us.anthropic.claude-haiku-4-5-20251001-v1:0"  # cross-region inference profile
DRAFT_PROMPT = """You are documenting a software service for an AI coding agent's memory.
Given the file contents below, write a concept file with:
- A one-paragraph Responsibilities section
- A Dependencies section listing what this service calls and what calls it
- Nothing else. Do not invent dependencies you cannot see in the code.
Files:
{file_contents}
"""
def draft_concept(state: PipelineState) -> PipelineState:
    drafts = []
    for package in state["changed_files"]:
        contents = read_package_source(state["repo_root"], package)
        response = bedrock.invoke_model(
            modelId=MODEL_ID,
            body=json.dumps({
                "anthropic_version": "bedrock-2023-05-31",
                "max_tokens": 800,
                "messages": [{"role": "user", "content": DRAFT_PROMPT.format(file_contents=contents)}]
            })
        )
        body = json.loads(response["body"].read())
        text = body["content"][0]["text"]
        drafts.append(ConceptDraft(
            path=f"services/{package}.md",
            concept_type="Service",
            title=package,
            body=text,
            citations=[],
            faithfulness_score=0.0,
            retry_count=0
        ))
    state["drafts"] = drafts
    state["status"] = "evaluating"
    return state

Eine Anweisung in diesem Prompt leistet tatsächlich Arbeit: Erfinden Sie keine Abhängigkeiten, die im Code nicht sichtbar sind. Wenn man sie unberührt lässt, beschreibt Haiku selbstbewusst eine Abhängigkeit von einem nicht existierenden Service. Berichten zufolge verringerte dieser eine Satz die Rate der erzeugten falschen Abhängigkeiten von etwa einem File auf sechs auf fast keine im Test. Betrachten Sie das jedoch nicht als universelle Konstante, denn es hängt davon ab, wie viel Quellcode in den Prompt eingefügt wird.

Schritt 4: das Evaluationsverfahren, das die meisten überspringen

Ein Markdown-Datei mit einem frontmatter-Typ-Feld kann nicht erkennen, ob sie die Wahrheit sagt. Nichts im Format verhindert, dass ein LLM eine selbstbewusste, gut formatierte, aber falsche Beschreibung eines Services erstellt. Wenn man diese Phase überspringt, kann das System heimlich das Verständnis eines Agents für seine eigene Codebasis verfälschen, während es genauso vertrauenswürdig erscheint wie eine korrekte Beschreibung.

Ragas ermittelt die Treuebewertung, da sie unterschiedliche Probleme löst – konkret misst sie, inwieweit der Entwurf auf dem Originaltext beruht, wobei keine manuell geschriebene Referenzlösung erforderlich ist. DeepEval wandelt diese Zahl in einen Wert um, auf den CI reagieren kann.

from ragas.metrics import faithfulness
from ragas import evaluate
from datasets import Dataset
def score_faithfulness(draft: ConceptDraft, source_contents: str) -> float:
    eval_dataset = Dataset.from_dict({
        "question": ["Describe this service's responsibilities and dependencies."],
        "answer": [draft["body"]],
        "contexts": [[source_contents]],
    })
    result = evaluate(eval_dataset, metrics=[faithfulness])
    return result["faithfulness"][0]
from deepeval import assert_test
from deepeval.test_case import LLMTestCase
from deepeval.metrics import FaithfulnessMetric
def eval_gate(state: PipelineState) -> PipelineState:
    passed, failed = [], []
    for draft in state["drafts"]:
        source = read_package_source(state["repo_root"], draft["title"])
        score = score_faithfulness(draft, source)
        draft["faithfulness_score"] = score
        test_case = LLMTestCase(
            input="Describe this service.",
            actual_output=draft["body"],
            retrieval_context=[source]
        )
        metric = FaithfulnessMetric(threshold=0.75)
        try:
            assert_test(test_case, [metric])
            passed.append(draft)
        except AssertionError:
            draft["retry_count"] += 1
            failed.append(draft)
    state["drafts"] = passed
    state["failed_drafts"] = failed
    state["status"] = "publishing" if not failed else "needs_review"
    return state

Der Schwellenwert von 0,75 ist ein Ausgangspunkt und keine gesetzliche Vorgabe. Er wurde von 0,6 erhöht, nachdem zwei Entwürfe mit leicht fehlerhaften Abhängigkeitsangaben durchgekommen waren. Ein Team, das diesen Wert verwendet, sollte damit rechnen, eine Woche lang die eigenen Raten an Falschpositiven und Falschnegativen zu überwachen, bevor es dem Wert vertraut.

Schritt 5: Verbindung des Diagramms mit DynamoDB-Checkpoints

from langgraph_checkpoint_aws import DynamoDBSaver
checkpointer = DynamoDBSaver(
    table_name="codebase-memory-checkpoints",
    ttl_seconds=60 * 60 * 24 * 7  # expire checkpoints after a week
)
graph = StateGraph(PipelineState)
graph.add_node("scan_diff", scan_diff)
graph.add_node("draft_concept", draft_concept)
graph.add_node("eval_gate", eval_gate)
graph.add_node("relink", relink)
graph.add_node("publish", publish)
graph.set_entry_point("scan_diff")
graph.add_edge("scan_diff", "draft_concept")
graph.add_edge("draft_concept", "eval_gate")
graph.add_conditional_edges(
    "eval_gate",
    lambda s: "relink" if s["status"] == "publishing" else END,
    {"relink": "relink", END: END}
)
graph.add_edge("relink", "publish")
graph.add_edge("publish", END)
pipeline = graph.compile(checkpointer=checkpointer)

Die Wahl von DynamoDB ist nicht willkürlich. Der Pipeline-Prozess läuft innerhalb von CI ab, wo Worker häufiger während der Ausführung abgeschaltet werden, als man zugibt. Ein mit Checkpoints versehener Graph setzt seine Ausführung an dem letzten abgeschlossenen Super-Schritt fort, anstatt fünf Dateien erneut erstellen zu müssen, nur weil die sechste Zeitüberschreitung aufwies.

Schritt 6: Erkennen der Kosten

Dem Pipeline wurde erst dann Vertrauen entgegengebracht, als die Token-Nutzung und Dauer jedes Nodes sichtbar wurden. Der @observe()-Decorator von Langfuse machte dies zu einer fünfminütigen Ergänzung statt zu einem kompletten Neuentwurf.

from langfuse.decorators import observe
@observe()
def draft_concept(state: PipelineState) -> PipelineState:
    # same body as above, now traced automatically
    ...

Für das Self-Hosting ist nur eine docker-compose-Datei erforderlich, vorausgesetzt, Ihr Code – einschließlich der Unterschiede in den Anfragen – muss innerhalb Ihres Netzwerks bleiben:

git clone https://github.com/langfuse/langfuse.git
cd langfuse
docker compose up -d

Nachdem ClickHouse das Projekt übernommen hat, speichert die selbst gehostete Langfuse Spuren sowohl in Postgres als auch in ClickHouse, und die selbst gehostete Variante scheint genauso zuverlässig zu sein wie zuvor. Es lohnt sich, dies vor der Einführung der Überwachungsfunktionen in der Produktion zu überprüfen, denn ob ein Tool eingestellt wird oder hinter einer Paywall steht, ist eine berechtigte Frage.

Das Dashboard zeigt drei Dinge an: die Kosten pro Token pro Commit, die Verteilung der Genauigkeit im Laufe der Zeit sowie die Wiederholungsrate. Ein langsamer Rückgang der Genauigkeit deutet auf eine fehlerhafte Logik bei der Auswertung der Quelldaten hin und nicht darauf, dass das Modell schlechter wird. Eine Wiederholungsrate von über etwa 10 % führte jedes Mal auf den Prompt zurück, niemals auf das Modell.

Eine lokale Version ohne Cloud

Einige Teams können nicht einmal Unterschiede zu einem gehosteten Modell senden. Man ersetzt einfach den Draft-Node durch Ollama – das gleiche Diagramm läuft anschließend ohne weitere Änderungen an der Architektur.

import ollama
def draft_concept_local(state: PipelineState) -> PipelineState:
    drafts = []
    for package in state["changed_files"]:
        contents = read_package_source(state["repo_root"], package)
        response = ollama.chat(
            model="llama3.1:8b",
            messages=[{"role": "user", "content": DRAFT_PROMPT.format(file_contents=contents)}]
        )
        drafts.append(build_draft(package, response["message"]["content"]))
    state["drafts"] = drafts
    return state

Für die Suchfunktion, die auf Embedding-Technologien basiert und über das veröffentlichte Bundle genutzt wird – nützlich, wenn es mehr als hundert Konzeptdateien gibt und allein index.md nicht ausreicht – ist nomic-embed-text über Ollama das Modell, das bereits in den meisten lokalen RAG-Einrichtungen verwendet wird, sodass keine neuen Abhängigkeiten rechtfertigt werden müssen.

+------------------+------------------------+---------------------------+
| Component        | Cloud version          | Local version             |
+------------------+------------------------+---------------------------+
| Draft model      | Bedrock Claude Haiku   | Ollama llama3.1:8b        |
| Embeddings       | Bedrock Titan Embed    | Ollama nomic-embed-text   |
| Checkpoint store | DynamoDB               | SQLite (LangGraph built-in)|
| Observability    | Langfuse (self-hosted) | Langfuse (self-hosted)    |
+------------------+------------------------+---------------------------+

In Tests mit denselben Quelldateien lag die Treue von llama3.1:8b etwa 10 bis 15 Punkte unter Haiku 4.5, hauptsächlich weil es die Beschreibungen von Abhängigkeiten stärker einschränkt. Dieser Unterschied ist der Grund für das Bewertungssystem, unabhängig davon, welches Modell den Entwurf erstellt. Man möchte schließlich nicht drei Wochen später von einem verwirrten Agenten über einen Qualitätsunterschied informiert werden.

Der git Hook

#!/usr/bin/env bash
# .git/hooks/post-commit
set -euo pipefail

COMMIT_SHA=$(git rev-parse HEAD)
python -m codebase_memory.pipeline --commit "$COMMIT_SHA" --repo-root "$(git rev-parse --show-toplevel)"

Führen Sie es auf beide Weisen aus: als Post-Commit-Hook auf dem Rechner des Entwicklers sowie als erforderliche CI-Aufgabe auf der gemeinsamen Branch. Lokal weist es bereits vor dem Entstehen eines PRs auf Abweichungen hin. In der CI dient es als Durchsetzungsmechanismus, da einige Mitwirkende den Hook niemals installieren – ein Hook, der nicht durchgesetzt werden kann, ist lediglich ein Ratschlag.

Prüfung der Realität in der Produktion

Hier sind die Dinge, die man vor dem Aufbau wissen sollte.

  • Die Kosten sind real, aber gering. Für ein Repository mit etwa 40 Commits pro Tag, die jeweils zwei oder drei Pakete betreffen, liegen die Kosten für den Haiku-Entwurfscheck bei etwa einigen Dutzend Dollar pro Monat; der Evaluierungscheck kostet ungefähr doppelt so viel, da jeder Entwurf gegen seine Quelle bewertet wird. Das ist ein günstiger Preis, um falsche Dokumentationen nicht an einen Agenten weiterzuleiten.
  • Die Latenz ist der Kompromiss, den niemand erwähnt. Ein vollständiger Scan, Entwurf, Bewertung und Veröffentlichungszyklus für einen Commit mit einem einzigen Paket dauert bei Messungen auf einer bestimmten Konfiguration 15 bis 25 Sekunden. Das ist in Ordnung für einen asynchronen CI-Job, aber nicht geeignet als synchroner Block beim Commit – ein Fehler in der ersten Version wird so bereits vor dem Übergang zu einem Hintergrundjob erkannt.
  • Die Prüfung kann manchmal einen korrekten Entwurf blockieren. Die Zuverlässigkeitsbewertung ist unvollkommen, und ein präziser, aber knapper Entwurf kann unter die erforderliche Schwellenwert fallen. Alles, was zweimal fehlschlägt, wird in eine Warteschlange für menschliche Überprüfung geschickt und nicht stillschweigend verworfen – das entspricht dem Status needs_review in der Zustandsmaschine.
  • Es lohnt sich nicht, ohne mehrere Agenten in der eigenen Codebasis zu entwickeln. Der gesamte Nutzen entsteht durch Agenten, die das Bundle verarbeiten. Eine zunächst erstellte Version, die nur zur Einbettung diente, blieb zwei Monate lang ungenutzt, weil nichts sie las.
  • Die Zuordnung von Paketen zu Dateien wird nicht sauber übertragen. Alles andere ist fast wie Kopieren und Einfügen. Diese eine Funktion muss für Ihr eigenes Layout neu geschrieben werden, und sie erfordert Zeit – nicht nur einen einzeiligen Code.
  • Was als Nächstes entwickelt werden sollte

    Derzeit bewertet das Evaluationsverfahren nur die Treue zum Original, wenn ein Entwurf erstellt wird, und prüft nie wieder eine bereits veröffentlichte Datei. Der natürliche nächste Schritt ist eine geplante Neubewertung des gesamten Pakets anhand des aktuellen Quellcodes, um Konzeptdateien aufzuspüren, die zum Zeitpunkt der Erstellung korrekt waren, nach drei Überarbeitungen jedoch fehlerhaft wurden, ohne dass eine erneute Erstellung ausgelöst wurde. Das wurde bisher noch nicht umgesetzt.

    Die Bewertungsschwellenwerte sind Zahlen, die jeder anders einstellt, und fast niemand veröffentlicht sie. Wenn Sie etwas Ähnliches entwickeln, überwachen Sie, wo Ihre Werte liegen, und prüfen Sie, ob sie nach einigen Wochen noch gültig sind.