Strukturelle Schutzmechanismen für KI-Agenten: Im Inneren des ResolveFlow-Pipelines
Erklärt, wie ein auf LangGraph basierender Agent die Trennung zwischen Reasoning und Ausführung durch Code-Ebene-Prüfungen statt durch Prompt-Anweisungen gewährleistet, einschließlich eines auftretenden Abruffehlers unterwegs.
Die meisten Demonstrationen von agierenden KI-Systemen folgen dem gleichen grundlegenden Muster: Ein Modell entscheidet sich für eine Aktion und führt sie sofort aus. Ein Tool wird an einen Prompt angehängt, das Modell ruft es auf, und das Tool läuft ohne weitere Überprüfung. In einem kurzen Demo-Video mag das überzeugend wirken, doch genau diese Konfiguration bereitet denen Sorgen, die darüber nachdenken, autonomen Systemen die Kontrolle über bedeutende Prozesse zu überlassen – denn oft ist der einzige Schutz zwischen einer korrekten Diagnose und einer schädlichen Änderung in einem laufenden System ein Warnhinweis innerhalb eines Prompts.
Das hier beschriebene Projekt wurde darauf ausgerichtet, diese Abhängigkeit von einer einzigen warnenden Anweisung zu vermeiden.
ResolveFlow akzeptiert einen Link zu einem GitHub-Issue, sammelt unterstützende Beweise, ordnet das Issue einer Kategorie zu und führt anschließend je nach Kategorie eine von drei Aktionen aus: es führt eine festgelegte, unverhandelbare Maßnahme durch, startet eine von einem LLM gesteuerte Untersuchung auf der Grundlage der gesammelten Beweise oder leitet das Issue direkt an einen menschlichen Prüfer weiter. Anstatt eines einzigen Zyklus, in dem ein Modell einen Toolaufruf auslöst, ist es als LangGraph-Zustandsmaschine strukturiert, die auf einer zentralen Idee beruht:
Das Reasoning und die Ausführung bleiben getrennt, weil das System so konstruiert ist – und nicht wegen einer vorgeschriebenen Konvention.
Das bedeutet nicht einfach, dass das Modell angewiesen wird, zuerst jemanden zu konsultieren. Stattdessen prüft und bewertet ein zweiter, unabhängiger LLM-Aufruf die Diagnose des ersten Modells, bevor ein Mensch sie überhaupt zu Gesicht bekommt. Die einzige Funktion im gesamten Codebasis, der es gestattet ist, wieder in GitHub zu schreiben, überprüft einen expliziten Freigabeflag innerhalb ihrer eigenen Logik – nicht weil davon ausgegangen wird, dass das Netzwerk die Aufrufe auf diese Weise leitet, sondern weil die Funktion selbst ohne dieses Flag nicht ausgeführt wird, selbst wenn zukünftige Codeänderungen einen direkten Weg schaffen würden, der die üblichen Schritte überspringt.
Der Rest dieses Leitfadens behandelt, wie das System tatsächlich zusammengesetzt wird, unter Verwendung der eigentlichen Implementierung, sowie einen Fehler, der während der Entwicklung auftrat. Dieser Fehler liefert eine nützliche Lektion: Ein Modell, das auf ein echtes Quelldokument verweist, ist nicht dasselbe wie ein Modell, das auf eine Quelle verweist, die tatsächlich relevant für die gestellte Frage ist.
Die Struktur der Pipeline
Sechs Phasen laufen nacheinander ab, und nur einer von ihnen darf etwas außerhalb der Pipeline selbst ändern:
- fetch_evidence – tatsächliche Aufrufe an die GitHub REST API, um den Text des Issues, dessen Kommentarthread sowie alle durchgeführten CI-Prüfungen abzurufen.
- normalize_evidence – die von diesen Aufrufen zurückgegebenen, unverarbeiteten JSON-Daten werden überprüft und in ein typisiertes IssueEvidence-Objekt umgewandelt.
- classify – ein leichtgewichtiger, regelbasierter Schritt, bei dem keine LLM-Aufrufe stattfinden.
In den folgenden Abschnitten werden die wichtigsten Aspekte erläutert.
Klassifizierung: bewusst kein Aufruf eines LLM
Dass ein LLM stets verfügbar ist, macht es verlockend, jede Entscheidung darüber laufen zu lassen – selbst dann, wenn keine solche Art der Argumentation erforderlich ist. Der Klassifizierungs Schritt bestimmt, inwieweit ein bestimmtes Problem überhaupt den kostspieligen und risikoreichen Weg – basierende Diagnose, Abrufvorgänge sowie anschließende Schreibvorgänge – einschlagen darf. Aufgrund dieser Kontrollfunktion muss er günstig, schnell und in sich selbst vollständig vorhersehbar sein:
def classify(state: GraphState) -> dict:
if state["evidence"].has_failing_ci:
return {"classification": "deterministic"}
elif state["evidence"].is_information_sparse:
return {"classification": "ai_investigation"}
else:
return {"classification": "human_review"}
Der Schritt liefert drei mögliche Ergebnisse, von denen jedes ein anderes Maß an Vertrauen in den nachfolgenden Prozess erfordert:
- deterministisch – ein fehlgeschlagener CI-Check ist an sich ein klares, mechanisches Signal. Es gibt nichts zu untersuchen; das Problem wird einfach gekennzeichnet und an einen Betreuer weitergeleitet.
Es ist erwähnenswert, dass human_review statt ai_investigation die Rückfalllösung ist. Immer dann, wenn das System nicht erkennen kann, was vor sich geht, versucht es nicht, eine clevere Antwort improvisiert zu geben.
Diagnose: Abfrage, anschließend ein Schema – keine freie Texteingabe
Sobald ein Problem in den Branch ai_investigation geleitet wird, durchsucht der Schritt generate_diagnosis einen Pinecone-Index, der etwa 2.750 Fragmente enthält, die aus echten, abgeschlossenen Problemen aus vier verschiedenen Repositorien stammen (facebook/react, langchain-ai/langchain, microsoft/terminal und vercel/next.js). Anschließend werden diese abgerufenen Ausschnitte an den LLM als unterstützender Kontext für den Aufruf gesendet:
def generate_diagnosis(state: GraphState) -> dict:
evidence = state["evidence"]
query = f"{evidence.title}\n\n{evidence.body}"
snippets = retrieve_evidence(query, k=3)
snippet_block = "\n\n".join(
f"[{s['id']}] (relevance: {s['score']:.2f}) {s['text']}" for s in snippets
)
prompt = (
f"Issue: {evidence.title}\n{evidence.body}\n\n"
f"Comments:\n{chr(10).join(evidence.comments) or '(none)'}\n\n"
f"Evidence snippets (cite by id in square brackets):\n{snippet_block}"
)
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
structured_llm = llm.with_structured_output(Diagnosis)
diagnosis = structured_llm.invoke([("system", _SYSTEM_PROMPT), ("human", prompt)])
return {
"diagnosis": diagnosis,
"retrieved_ids": [s["id"] for s in snippets],
"retrieved_scores": {s["id"]: s["score"] for s in snippets},
}
Mehrere Details in diesem Diagnoseschritt verdienen eine genaue Betrachtung.
Zunächst ist die Form des Ausgangsergebnisses nicht etwas, das erst im Nachhinein extrahiert wird – sie ist bereits von vornherein garantiert. Die Diagnose wird als Pydantic-Modell definiert:
class Diagnosis(BaseModel):
root_cause: str
severity: Literal["low", "medium", "high"]
missing_info: list[str] = Field(default_factory=list)
recommended_next_steps: list[str]
citations: list[str] = Field(
default_factory=list,
description="IDs of retrieved evidence/doc snippets that support each claim above",
)
Durch Aufruf von .with_structured_output(Diagnosis) wird das Modell beim Erstellen seiner Antwort gezwungen, genau diese Struktur zu verwenden. Es gibt keine regulären Ausdrücke, die danach versuchen würden, die Ursache aus einem Textblock herauszufinden – wenn der Aufruf erfolgreich ist, wird bereits ein typisiertes Objekt zurückgegeben, nicht Text, den man noch interpretieren muss.
Zweitens sind Zitate keine Frage des Tonfalls oder des Vertrauensniveaus – sie müssen echte Identifikatoren sein. Die Systemanweisung besagt ausdrücklich, dass jedes Zitat zu einem der bereitgestellten Snippet-IDs passen muss; nichts Erfundenes und nichts, was einer Behauptung zugeordnet wird, die vom Snippet tatsächlich nicht gestützt wird. Diese Einschränkung sollte eigentlich ausreichen – doch das ist nicht der Fall, und genau deshalb gibt es die nächste Stufe im Prozess.
Unabhängige Überprüfung: Das Modell erklärt, der Code entscheidet
Das ist wohl die wichtigste architektonische Entscheidung im gesamten System.
Der Knoten independent_review startet einen zweiten, völlig separaten Aufruf von ChatOpenAI mit eigenem Prompt und ohne gemeinsamen Kontext zum Aufruf, der die Diagnose erzeugt hat. Seine Aufgabe besteht darin, diese Diagnose zu bewerten.
Doch hier kommt der entscheidende Punkt: Die eigentliche Entscheidung, ob etwas genehmigt oder weitergeleitet wird, bleibt niemals dem Modell überlassen. Sie beruht auf drei in gewöhnlichem Python berechneten Booleschen Werten, und die Ausgabe des LLM wird auf menschlich lesbare Kommentare reduziert, von denen die Genehmigungslogik tatsächlich nicht abhängt.
def independent_review(state: GraphState) -> dict:
evidence = state["evidence"]
diagnosis = state["diagnosis"]
retrieved_ids = set(state.get("retrieved_ids", []))
retrieved_scores = state.get("retrieved_scores", {})
groundedness_ok = bool(diagnosis.citations) and all(
citation_id in retrieved_ids
and retrieved_scores.get(citation_id, 0.0) >= MIN_RELEVANCE_SCORE
for citation_id in diagnosis.citations
)
risk_ok = diagnosis.severity in _ALLOWED_SEVERITIES # {"low", "medium"}
permission_ok = True # comment/label are the only writes available today
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
reasoning = llm.invoke(
[("system", _SYSTEM_PROMPT), ("human", prompt)]
).content # human-readable critique — not what the gate checks
outcome = "approve" if (groundedness_ok and risk_ok and permission_ok) else "escalate_to_human"
return {"review_result": ReviewResult(
outcome=outcome,
groundedness_ok=groundedness_ok,
risk_ok=risk_ok,
permission_ok=permission_ok,
reasoning=reasoning,
)}
Das ist das allgemeine Muster, dem jede zuverlässige Version von „LLM-as-Judge“ folgen muss: Das Modell kann sich selbst erklären, aber der Code trifft die Entscheidung.
Falls Sie ein Modell bitten, die Arbeit eines anderen Modells zu bewerten und anschließend einfach dem Urteil in seiner Antwort zu vertrauen, haben Sie im Grunde ein System geschaffen, dessen Zuverlässigkeit durch genau das begrenzt wird, was Sie eigentlich überprüfen möchten.
In dieser Konstruktion dient die Ausgabe des LLM als nützliche Erklärung für einen menschlichen Leser, doch die entscheidende Schranke lässt sich nicht durch überzeugende Formulierungen zu einem schlechten Ergebnis verleiten, da sie nichts von dem analysiert, was das Modell geschrieben hat, um eine Entscheidung zu treffen.
Noch ein Detail, das erwähnt werden sollte: Eine hohe Schweregradstufe kommt niemals in _ALLOWED_SEVERITIES vor. Jede Diagnose mit hoher Schweregradstufe wird unabhängig davon, wie gut ihre Quellenüberprüfung ausgefallen ist, automatisch an einen Menschen weitergeleitet. Richtig zu liegen und sicher für eine automatische Freigabe zu sein, sind einfach nicht dasselbe.
Der Fehler: „grounded“ ist nicht dasselbe wie „relevant“
Hier wurde es in der Praxis wirklich schwierig.
Die ursprüngliche Überprüfung von groundedness_ok prüfte lediglich, ob eine Zitier-ID mit etwas in der abgerufenen Menge übereinstimmte – eine echte ID, im Gegensatz zu einer erfundenen. Auf dem Papier scheint das eine vernünftige Überprüfung zu sein. Doch es reichte nicht ganz aus.
Bei Tests anhand eines kleinen Korpus von 40 Fällen wurde ein tatsächlich leerer React-Fall durch den Pipeline-Prozess geführt (facebook/react#36932, „experimental_taintUniqueValue wirft bei großen binären Werten einen RangeError aus“). Die Auswertung lieferte drei Abschnitte, alle gültig und alle korrekt identifiziert – doch keiner von ihnen hatte etwas mit diesem spezifischen Fehler zu tun. Trotz dieser mangelhaften Ausgangsdaten stellte das Modell weiterhin eine zuversichtliche, präzise klingende Diagnose auf, die völlig falsch war: Es behauptete ein „Kompatibilitätsproblem mit der React DevTools-Erweiterung“. Jede Quelle bestand problemlos den Grounding-Test. Die Diagnose selbst blieb jedoch nutzlos.
Die Lösung bestand darin, aufzuhören, „wurde abgerufen“ als Ersatz für „ist relevant“ zu betrachten. tools/retrieval.py wurde aktualisiert, sodass jeder Auszug nun zusammen mit seinem Text auch seinen Kosinusähnlichkeitswert zurückgibt:
def retrieve_evidence(query: str, k: int = 3) -> list[dict]:
results = vector_store.similarity_search_with_score(query, k)
return [
{"id": doc.metadata["id"], "text": doc.page_content,
"source": doc.metadata["source"], "score": score}
for doc, score in results
]
Dann habe ich untersucht, wie die tatsächlichen Ähnlichkeitswerte im Vergleich zum Live-Index aussehen, anstatt einfach einen Schwellenwert zu vermuten. Eine Abfrage, die stark mit echtem Inhalt im Korpus übereinstimmte, erzielte Werte zwischen 0,53 und 0,63. Eine Abfrage zu etwas, das in keiner der vier eingebundenen Repositorien vorhanden war, erzielte Werte zwischen 0,21 und 0,22. Aufgrund dieses Unterschieds setzt independent_review.py nun eine feste Untergrenze: Jeder zitierte Auszug muss den Wert MIN_RELEVANCE_SCORE = 0,35 erreichen. Dieser Schwellenwert liegt absichtlich näher am oberen Ende des Unterschieds als in der Mitte, denn es ist weitaus geringerer Schaden, wenn eine gute Diagnose versehentlich als solche eingestuft wird, als wenn eine schlechte Diagnose trotzdem als genehmigt durchgeht.
Neben der Korrektur der Bewertunglogik musste auch das Suchkorpus selbst erweitert werden. Es begann mit 40 Artikeln aus einem einzigen Repository, was etwa 130 Fragmente ergab – so klein, dass ein zufälliger Artikel aus der Realwelt oft von vornherein keine echte thematische Übereinstimmung zum Abrufen hatte. Später wurde dieses Korpus auf rund 600 Artikel aus vier verschiedenen Repositories erweitert, was fast 2.750 Fragmente ergab.
Sobald das größere Korpus verfügbar ist, führt das gleiche Problem mit taintUniqueValue nun dazu, dass zwei echte Nahe-Duplikat-Meldungen zurückgehalten werden, und es wird eine genaue, spezifische Ursache ermittelt: String.fromCharCode.apply überschreitet bei der Verarbeitung großer Puffer die Argumentanzahlsgrenze des JavaScript-Engines. Bemerkenswerterweise wird dieser Fall dennoch von independent_review zur menschlichen Überprüfung weitergeleitet, da seine Schweregrad als hoch eingestuft wird – und Funde mit hohem Schweregrad werden aus Designgründen immer weitergeleitet, unabhängig davon, wie zuverlässig oder korrekt die Diagnose ist. Korrektheit gewährt keine Befreiung von der Risikokontrolle.
Die wichtigere Erkenntnis hier ist, dass eine Quellenangabe, die auf einen echten, abgerufenen Chunk-ID verweist, eine notwendige, aber keine ausreichende Bedingung ist. Zu behaupten, „das Modell hat etwas zitiert“, ist eine andere Aussage als zu behaupten, „das Modell hat etwas Wahres und Relevantes zu diesem Fehler zitiert“. Ein System, das nur die erste Behauptung überprüft, wirkt zwar sorgfältig, handelt in Wirklichkeit jedoch nur wie ein Automatismus, der Unsinnsinhalte bestätigt.
Der Freigabeschritt ist eine echte Pause, kein rein kosmetischer Ladezustand
Jede bisher beschriebene Phase schlägt lediglich eine Aktion vor. Nichts wird bis zu einem bestimmten Punkt im Workflow wieder an GitHub geschrieben. Alle vorangegangenen Schritte – Klassifizierung, Diagnose, Überprüfung – schlagen lediglich vor; erst dieser einzige Schritt führt zu einer Änderung in GitHub.
Der await_approval-Node erstellt den genauen Kommentar (und gegebenenfalls die genaue Bezeichnung), der veröffentlicht werden soll, unter Verwendung derselben Logik – unabhängig davon, ob das Problem über den deterministischen Pfad weitergeleitet wurde oder von einer genehmigten ai_investigation-Diagnose stammt. Anschließend ruft er LangGraphs interrupt() auf:
def await_approval(state: GraphState) -> dict:
proposed_action = _build_proposed_action(state)
approved = interrupt({
"classification": state["classification"],
"proposed_action": proposed_action,
})
return {"proposed_action": proposed_action, "approved": bool(approved)}
Die Aufrufung von interrupt() bewirkt nicht nur das Anzeigen eines „Wartet auf Freigabe“-Spinners, während der Prozess untätig ist – sie stoppt tatsächlich die Ausführung des Graphen mitten im Lauf. Um diesen Lauf später wieder aufzunehmen, ist ein völlig separater Anfragen erforderlich, um den gleichen pausierten Thread zu finden und von dort aus fortzusetzen, unter Verwendung eines Aufrufs wie result = await compiled_graph.ainvoke(Command(resume=True), config) mit derselben thread_id, die der ursprüngliche Lauf verwendet hat. Damit das überhaupt funktioniert, muss der Zustand des Graphen zwischen zwei unabhängigen HTTP-Anfragen erhalten bleiben, was eine Verwendung reiner In-Process-Memory zur Speicherung ausschließt.
Nur wenn dieser Ablauf abgeschlossen ist, wird der execute-Node ausgeführt. Zwei Punkte sind hier besonders hervorzuheben. Erstens findet die Berechtigungsprüfung innerhalb des eigenen Codes des Nodes statt und nicht ausschließlich als Graphenkante – sodass selbst bei einem zukünftigen Refactoring, bei dem versehentlich eine Abkürzungs-Kante direkt zum execute-Node hinzugefügt wird, diese Prüfung dies weiterhin erkennen würde. Zweitens sendet execute den Wert von state[„proposed_action“] genau so, wie er genehmigt wurde; danach wird der Kommentar nie erneut generiert. Was auch immer von einem Menschen freigegeben wurde, wird genau so veröffentlicht.
def execute(state: GraphState) -> dict:
if not state.get("approved"):
raise PermissionError("execute() called without explicit approval")
evidence = state["evidence"]
action = state["proposed_action"]
token = state.get("github_token")
result = {
"comment": post_comment(evidence.repo, evidence.issue_number,
action["comment"], token=token)
}
if action.get("label"):
try:
result["label"] = add_label(evidence.repo, evidence.issue_number,
action["label"], token=token)
except requests.HTTPError as exc:
result["label_error"] = str(exc)
return {"execution_result": result}
Warum der Speicher-Backend für pausierte Ausführungen wichtiger ist, als es scheint
Die ursprüngliche Implementierung setzte auf SqliteSaver, der den Zustand in einer lokalen Datei auf der Festplatte des Backends speichert. Diese Konfiguration funktioniert ohne Probleme auf dem Rechner eines Entwicklers.
In der Produktion versagt es auf eine Weise, die leicht übersehen werden kann: Auf einem Host mit kostenlosem Tarif wie Render ist der Datenspeicher nicht über Neustarts hinweg persistent. Der Prozess wird nach einer Zeit der Inaktivität beendet und startet bei der nächsten eingehenden Anfrage in einem völlig neuen Container neu.
So sieht das konkret aus: Eine Ausführung erreicht await_approval und pausiert, um auf eine Aktion eines Benutzers zu warten. Bevor jemand „Genehmigen“ klickt, wird die kostenlose Instanz inaktiv und heruntergefahren. Die nächste Anfrage startet anschließend einen neuen Container mit einer völlig leeren Datenbank. Wenn Command(resume=...) ausgeführt wird, gibt es nichts mehr zu fortsetzen – der Historie des gepausierten Threads fehlt ohne jeglichen Fehler oder Warnhinweis.
Die Lösung ist AsyncPostgresSaver, der von einer echten Postgres-Datenbank (in dieser Konfiguration Neon) unterstützt wird, die unabhängig vom Container existiert, in dem die Anwendung läuft:
async with (
AsyncPostgresSaver.from_conn_string(DATABASE_URL, serde=get_serde()) as saver,
AsyncConnectionPool(DATABASE_URL, open=False,
check=AsyncConnectionPool.check_connection) as pool),
):
Auch wenn der Container zerstört und von Grund auf neu erstellt wird, überleben alle pausierten Threads, solange DATABASE_URL weiterhin auf dieselbe Datenbank verweist. Die Mechanismen zur Pause und Wiederaufnahme durch interrupt() ändern sich überhaupt nicht – nur der Ort, an dem dieser Pausenzustand gespeichert wird, wechselt.
Es sei besonders hervorgehoben: Schreibvorgänge nach der Freigabe erfolgen unter der Identität der Person, die sie freigegeben hat, und nicht unter irgendwelchen gemeinsamen Bereitstellungsanmeldeinformationen. Die Live-App ermöglicht es jedem GitHub-Nutzer, sich anzumelden, wobei jeder Lese- oder Schreibvorgang für diese Aktion dann das eigene OAuth-Token dieser Person verwendet. Ein Aufruf wie post_comment(evidence.repo, evidence.issue_number, action["comment"], token=token) nutzt das Token der Freigabeperson, nicht ein Token der Person, die zufälligerweise die App bereitgestellt hat.
Dies ist von Bedeutung – nicht nur aus Gründen der sicheren Authentifizierung. Dadurch wird „Die Person, die dies freigegeben hat, hat es auch veröffentlicht“ zu einer Aussage, die GitHub selbst durch Überprüfung des Autors des Kommentars bestätigen kann, anstatt nur einer Behauptung, die von der Benutzeroberfläche der App selbst aufgestellt wird.
Dadurch kann das bestehende Berechtigungssystem von GitHub ohne zusätzlichen Code effektiv angewendet werden: Jemand, der lediglich ein Repository besucht, das ihm nicht gehört, kann einen Kommentar hinterlassen, doch das Hinzufügen einer Kennzeichnung erfordert entweder eine spezielle Freigabe oder Schreibzugriff auf dieses Repository. execute.py handhabt dies geschickt – ein fehlgeschlagener Versuch, eine Kennzeichnung hinzuzufügen, gilt als teilweiser Erfolg, anstatt den gesamten Antrag zu stören.
Die Aufrufe an das LLM sowie das Embedding-Modell laufen weiterhin mit den eigenen API-Schlüsseln des Deployment-Besitzers, unabhängig davon, wer sie auslöst – genau deshalb gibt es eine tägliche Gebührenbegrenzung pro Benutzer, um diese Kosten in Schach zu halten.
Es gibt einen Unterschied, bei dem Klarheit geboten ist: Eine Regressionstestbewertung und eine Funktionalitätsbewertung prüfen im Grunde völlig unterschiedliche Aspekte, und sie auf dieselbe Weise zu bewerten ist ein Fehler, in den man leicht verfallen kann. Die Routungslogik innerhalb von classify(), die Gate-Logik in independent_review() sowie die Berechtigungsprüfung in execute() weisen jeweils genau ein korrektes Verhalten auf, und der einzige akzeptable Bestehensgrad für diese Art von Test ist 100 Prozent, Punkt. Ein Sicherheitsschutzmechanismus, der auch nur einmal bei beliebig vielen Ausführungen umgangen wird, stellt einen kritischen Fehler dar – keinen Wert, den man durch Durchschnittsberechnung ausgleichen kann.
Dieser Standard unterscheidet sich völlig von der Beurteilung, ob generate_diagnosis gute Diagnosen erzeugt. Eine solche Bewertung wird von einem Modell vorgenommen, verbessert sich tatsächlich im Laufe der Zeit und wird niemals realistischerweise 100 Prozent erreichen. Beide Arten von Überprüfungen als gleichwertig zu betrachten, ist eine häufige Falle – ein Sicherheitsschutz, der „meistens“ funktioniert, erfüllt seine Funktion als Sicherheitsschutz überhaupt nicht.
Was ist im Moment tatsächlich der Fall
Es ist besser, dies zu unterschätzen als zu übertreiben; daher wird hier der reine Zustand der Dinge dargestellt anstelle einer Verkaufspräsentation:
Die Erhebung von Beweismitteln erfolgt über echte GitHub REST-Anfragen, die in typisierte Objekte umgewandelt werden.
Die Klassifizierung basiert auf Regeln und ist deterministisch, wobei keine LLMs beteiligt sind.
Die Diagnose stammt von einer tatsächlichen OpenAI-Anfrage, die strukturierte Ausgaben liefert; diese basieren auf echten Pinecone-Auswertungen von etwa 2.750 Datensätzen, die wöchentlich erneut verarbeitet werden.
Die unabhängige Überprüfung erfolgt durch eine separate OpenAI-Anfrage, doch das eigentliche Kontrollmechanismus ist Code – nicht die eigene Meinung des Modells.
Die menschliche Freigabestufe bedeutet eine echte interrupt()-Pause, die über Command(resume=...) fortgesetzt wird, und wird byte für byte von Anfang bis Ende überprüft.
Sowohl die Frontend- als auch die Backend-Komponenten sind verfügbar – auf Vercel bzw. Render – vollständig asynchron und werden in Postgres gespeichert.
GitHub OAuth ermöglicht es jedem Besucher, sich mit seinem eigenen Konto anzumelden; die Schreibvorgänge laufen anschließend unter dieser Person, wobei ein täglicher Limit gilt.
Das Evaluations-Set umfasst Regressionstests für Sicherheitskontrollen, die r
Diese beiden letzten Lücken werden nicht verschwiegen – sie stehen auf dem Roadmap, weil sie tatsächlich die wertvollsten Aspekte sind, die als Nächstes entwickelt werden sollten, und nicht weil sie übersehen wurden.
Lektionen, die für Ihr eigenes Projekt nützlich sind
Die Entwicklung eines Agenten, der auf echten Systemen agieren soll, bringt eine Reihe von Prinzipien zum Vorschein, die über dieses spezielle GitHub-Issue-Tool hinausgehen:
Bewahren Sie das Denken und die Ausführung in getrennten Codepfaden auf, nicht nur in separaten Anfragen. Eine Anweisung wie „Fragen Sie immer vor dem Handeln“ bleibt letztlich nur ein Verhalten, und Verhaltensweisen neigen dazu, genau in dem Moment zu versagen, in dem man ihre Konstanz am meisten braucht. Eine Funktion, die kategorisch ablehnt, ausgeführt zu werden, es sei denn, ein überprüftes Flag ist gesetzt, stellt eine echte Grenze dar – keine bloße Empfehlung.
Wenn Sie behaupten, ein Überprüfungs Schritt sei „unabhängig“, stellen Sie sicher, dass das buchstäblich zutrifft: ein separater Aufruf, kein gemeinsamer Ablaufverlauf sowie ein Urteil, das durch Code erzeugt wird und nicht durch Texte, die das Modell selbst generiert hat. Das Modell kann seine Überlegungen erklären, sollte aber nicht dasjenige sein, das sie bewertet.
Lassen Sie es nicht zu, dass „das Modell auf etwas Reales verweist“ anstelle von „das Modell auf etwas Relevantes verweist“ verwendet wird. Messen Sie tatsächlich die Qualität der Informationsbeschaffung anhand echter Abfragen, bevor Sie einen Ähnlichkeitsschwellenwert festlegen.
Falls eine Pause mit menschlicher Einmischung für Ihr Design wichtig ist, prüfen Sie ausdrücklich, was passiert, wenn der Prozess vor der Reaktion des Menschen neu gestartet wird. Der In-Memory-Zustand sowie temporäre Festplatten scheinen bei lokalen Tests in Ordnung zu sein, versagen jedoch genau dort, wo das Versagen am teuersten ist.
Zuletzt sollten Sie klar benennen, was tatsächlich abgeschlossen ist und was noch nur ein Ersatz darstellt. Eine Statustabelle, die ihre Lücken zugibt, gewinnt mehr Vertrauen als eine README-Datei, die stillschweigend andeutet, alles sei erledigt.
Verwandte Artikel
- Agentic AI Explained: Von Sprachmodellen zu autonomen Agenten — Eine strukturierte Übersicht darüber, wie LLMs durch Tools, Speicher, Planung, Mehr-Agenten-Architekturen und MCP-Integration zu agierenden Systemen werden.