Startseite / Artikel

Wenn zwei Agenten einen besiegen: Ein LangGraph-Code-Review-Copilot, der in beide Richtungen funktioniert

Erstellen Sie einen Code-Reviewer als einzelnen Agenten sowie erneut als Analyzer zusammen mit einem Risk Reviewer in LangGraph, und lernen Sie anschließend die drei Fälle, in denen mehrere Agenten die Kosten rechtfertigen.

Verlag: REACTAPP.TOP14 Min. Lesezeit3219 Wörter

Quellen: Multi-Agent Systems: When 2 Agents Beat 1 (and When They Don’t)

„Multi-Agent“ ist das Schlagwort der Stunde. In Demonstrationen werden fünf KI-Perücken gezeigt, die in einem simulierten Raum miteinander chatten, um eine Website zu durchforsten und einen Blogbeitrag zu schreiben – mit Namen wie Forscher, Schreiber und Redakteur – sowie ein Terminal voller Agenten, die miteinander diskutieren. Es sieht beeindruckend aus, ist aber nicht die Methode, mit der zuverlässige Software entwickelt wird.

Das Hinzufügen von Agenten macht ein System von allein nicht intelligenter. Es vermehrt vielmehr die Möglichkeiten, dass etwas schiefgeht. Nehmen wir die grundlegende Arithmetik: Ein Agent, der seine Aufgabe zu 90 Prozent korrekt erledigt, weist eine Zuverlässigkeit von 0,90 auf. Wenn man drei solcher Agenten hintereinander setzt, multiplizieren sich die Wahrscheinlichkeiten, sodass die Basiszuverlässigkeit auf etwa 72 Prozent sinkt. Die Latenz verdoppelt sich, die API-Kosten treiben sich auf das Dreifache, und das Endergebnis ist nicht besser.

Die wichtige Frage ist, wann mehr als ein Agent tatsächlich zu einer Lösung führt. Eine gute Möglichkeit, das herauszufinden, besteht darin, zweimal einen Assistenten für Code-Reviews mit KI zu entwickeln. Die erste Version ist ein herkömmlicher Reviewer mit einem einzigen Agenten. Die zweite Version teilt die Arbeit zwischen einem Analyzer und einem Risk Reviewer auf und nutzt Funktionen von LangGraph wie bedingte Kanten, interrupt() sowie ein auf Command-Befehlen basierendes Fortsetzen, um bei Bedarf einen Menschen einzubeziehen. Der Vergleich zeigt, warum der einzelne Agent eine ernsthafte Abrechnungsfehler übersehen kann und warum das Duo ihn entdeckt.

Warum Code-Reviews mit nur einer Anweisung schwierig sind

Ein guter Review erfordert zwei völlig unterschiedliche Arten des Denkens.

Die erste ist das Verstehen. Man liest den Diff, ordnet die veränderten Funktionen zu, ermittelt, welche Dateien betroffen sind, und schließt daraus, was der Autor gemeint hat. Das Ziel ist ein mentales Modell der neuen Funktion.

Der zweite Ansatz ist feindlich. Man sucht nach Randfällen, fehlenden Tests und logischen Lücken und versucht, den Code zu zerstören.

Fordert man von einem LLM, beides in einer einzigen Anfrage zu erledigen, hat es Schwierigkeiten. Das Zusammenfassen und Kritisieren in derselben Schrittfolge neigt dazu, Kohärenz vor Zweifeln zu bevorzugen. Sobald das Modell sich auf eine Erklärung dafür festlegt, was der Code tut, investiert es weniger Energie in die Angriffe auf diese Erklärung und beschränkt sich stattdessen auf geringfügige Stilkritiken wie fehlende Dokumentationsstrings oder Namenskonventionen.

Ein konkreter Fall macht das klar. Stellen Sie sich einen fiktiven Pull Request gegen ein Rechnungssystem vor, bei dem der Autor einer Rechnungsberechnungsfunktion einen Rabattfunktion hinzufügt. Dies ist der Diff, den beide Designs erhalten:

--- a/src/billing/invoice.py
+++ b/src/billing/invoice.py
@@ -42,7 +42,9 @@
 class InvoiceService:
-    def calculate_total(self, items):
-        return sum(i.price * i.qty for i in items)
+    def calculate_total(self, items, discount_pct=0):
+        subtotal = sum(i.price * i.qty for i in items)
+        return subtotal * (1 - discount_pct)

--- a/src/billing/api.py
+++ b/src/billing/api.py
@@ -18,6 +18,8 @@
 @router.post("/invoice")
 def create_invoice(req: InvoiceRequest):
+    discount = req.discount_pct   # NEW: from user input
     svc = InvoiceService()
-    total = svc.calculate_total(req.items)
+    total = svc.calculate_total(req.items, discount)
     return {"total": total}

--- a/config/feature_flags.yaml
+++ b/config/feature_flags.yaml
@@ -5,3 +5,4 @@
 flags:
   new_dashboard: true
+  discount_billing: true   # rollout: 100% immediately

Wenn man genauer hinschaut, fallen mehrere ernsthafte Probleme auf. Der Wert von discount_pct kommt direkt von der Benutzereingabe in die API-Schicht und wird ohne Überprüfung in die Berechnung einbezogen. Ein Benutzer könnte einen negativen Rabatt senden, um den Preis zu erhöhen, oder einen Wert über 1, um einen negativen Gesamtbetrag zu erhalten. Die Tests decken diesen neuen Ablauf nicht ab. Zudem ist die Feature-Flag so eingestellt, dass sie sofort 100 % der Benutzer erreicht. Dies ist ein Preisberechnungsablauf, der besonders anfällig für Missbrauch ist. Wie geht ein typischer Reviewer mit dieser Situation um?

Design eins: Einzelner Reviewer

Zuerst steht das Werkzeugset des Reviewers zur Verfügung: ein Tool zum Anzeigen von Unterschieden, ein Tool zum Öffnen von Dateien, ein Tool zum Suchen nach Symbolen sowie ein Tool zum Berichten über vorhandene Tests.

from langchain_core.tools import tool
import textwrap

MOCK_DIFF = """...""" # The diff shown above
MOCK_FILES = {
    "src/billing/invoice.py": "class InvoiceService:\n    def calculate_total(self, items, discount_pct=0):\n        subtotal = sum(i.price * i.qty for i in items)\n        return subtotal * (1 - discount_pct)\n",
    "src/billing/api.py": '@router.post("/invoice")\ndef create_invoice(req: InvoiceRequest):\n    discount = req.discount_pct\n    svc = InvoiceService()\n    total = svc.calculate_total(req.items, discount)\n    return {"total": total}\n',
    "tests/test_billing.py": "def test_calculate_total():\n    # only tests no-discount path\n    assert svc.calculate_total(items) == 300\n",
}

@tool
def get_diff(pr_id: str) -> str:
    """Fetch the PR diff."""
    return MOCK_DIFF

@tool
def read_file(path: str) -> str:
    """Read a file from the repo."""
    return MOCK_FILES.get(path, f"FILE NOT FOUND: {path}")

@tool
def search_symbol(name: str) -> str:
    """Search for a symbol across the codebase."""
    if "discount" in name.lower():
        return "Found: InvoiceService.calculate_total(discount_pct) — src/billing/invoice.py:43"
    return f"No results for '{name}'"

@tool
def list_tests(path: str) -> str:
    """List test files covering a source path."""
    if "billing" in path:
        return "tests/test_billing.py — covers calculate_total (no-discount path only)"
    return "No tests found"

ALL_TOOLS = [get_diff, read_file, search_symbol, list_tests]

Dann der Zustand. Ein TypedDict sorgt dafür, dass alles mit LangGraph kompatibel bleibt, und enthält die PR-ID, die Nachrichtenhistorie, den Rohdiff sowie eine Liste der Erkenntnisse. Ein Pydantic-Modell beschreibt jede Erkenntnis und zwingt das Modell dazu, die gefundenen Elemente mit festen Literalen zu klassifizieren, was die spätere Verarbeitung erheblich erleichtert.

from typing import Literal
from typing_extensions import TypedDict
from pydantic import BaseModel
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
from langgraph.graph import END, START, StateGraph

class ReviewFinding(BaseModel):
    severity: Literal["critical", "high", "medium", "low", "info"]
    category: Literal["bug", "regression", "missing_test", "rollout_risk",
                       "security", "edge_case", "style"]
    file: str
    description: str
    confidence: Literal["high", "medium", "low"]

class SingleAgentState(TypedDict):
    pr_id: str
    messages: list
    diff: str
    findings: list[ReviewFinding]

llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", temperature=0)

Dann kommen die Knoten. Das Diagramm besteht aus einer geraden Linie: ein Abrufknoten, der den Kontext lädt, gefolgt von einem Überprüfungsknoten, der den Prompt ausführt.

def sa_fetch(state: SingleAgentState) -> dict:
    diff = get_diff.invoke({"pr_id": state["pr_id"]})
    tests = list_tests.invoke({"path": "src/billing"})
    return {"diff": diff, "messages": [
        AIMessage(content=f"Diff loaded. Test coverage: {tests}")
    ]}

def sa_review(state: SingleAgentState) -> dict:
    """Single agent does BOTH jobs: summarize + critique in one pass."""
    prompt = f"""\
You are a senior code reviewer. Read this PR diff, summarize the change,
and produce a list of risk findings. Be specific.

DIFF:
{state['diff']}

Respond with JSON: {{"summary": "...", "findings": [
  {{"severity": "...", "category": "...", "file": "...",
    "description": "...", "confidence": "..."}}
]}}
"""
    resp = llm.invoke([SystemMessage(content="You are a code review bot."),
                       HumanMessage(content=prompt)])

    # In a real app we parse the JSON response here.
    # We simulate the typical single-agent output for this diff.
    findings = [
        ReviewFinding(
            severity="medium",
            category="missing_test",
            file="tests/test_billing.py",
            description="Add unit tests for the new discount_pct parameter.",
            confidence="high"
        ),
        ReviewFinding(
            severity="low",
            category="style",
            file="src/billing/invoice.py",
            description="Consider adding type hints to the items list.",
            confidence="high"
        )
    ]
    return {"findings": findings, "messages": [resp]}

def build_single_agent():
    g = StateGraph(SingleAgentState)
    g.add_node("fetch", sa_fetch)
    g.add_node("review", sa_review)
    g.add_edge(START, "fetch")
    g.add_edge("fetch", "review")
    g.add_edge("review", END)
    return g.compile()

(In dieser Demonstration sind die Ergebnisse fest codierte Ersatzwerte für die Art von Ausgabe, die ein solcher Reviewer typischerweise erzeugt; betrachten Sie daher den Vergleich als Illustration und nicht als Vergleichswert.)

So werden in etwa die ersten KI-Reviewer entwickelt: Man gibt den Diff ein und bittet um die Aufdeckung von Fehlern. Wenn das System gegen den Mock-PR geprüft wird, bemerkt der Agent den fehlenden Test und schlägt Typhinweise vor. Er überseht jedoch die unvalidierte Eingabe sowie das Risiko bei der Einführung.

Der Grund liegt im Prompt. Da das Modell aufgefordert wird, die Änderung zusammenzufassen und anschließend die Risiken aufzulisten, konzentriert es sich darauf, zu erklären, dass eine Rabattfunktion hinzugefügt wurde. Beim Erstellen der Ergebnisliste befindet es sich bereits im beschreibenden Modus, achtet nur auf die Oberflächensyntax und versucht nicht, etwas zu zerstören. Die Lösung besteht darin, die Syntheseaufgabe von der adversarialen Aufgabe zu trennen, was zwei Agenten bedeutet.

Die Übergabe: Verwenden Sie ein Schema statt Prosa

Der häufigste Fehler bei der Gestaltung mehrerer Agenten besteht darin, dass die Agenten miteinander in freier Form kommunizieren. Wenn Agent A drei Absätze schreibt und sie an Agent B weitergibt, muss B den Text analysieren, erraten, was wichtig ist, und auf einer unklaren Grundlage weiterarbeiten. So verschlechtert sich der Kontext.

Agenten sind Funktionen, und Funktionen sollten typisierte Objekte übergeben. Damit ein mehragentiges System zuverlässig funktioniert, muss das genaue Schema dessen definiert werden, was zwischen den Agenten ausgetauscht wird.

Im Review-Assistenten ist der erste Agent der Analyzer, dessen Aufgabe es ist, die Pull Request zu analysieren. Er muss einen ChangeModel erzeugen, ein Objekt, das ihn zwingt, spezifische Metadaten zur Änderung herauszufinden.

class ChangedInterface(BaseModel):
    file: str
    symbol: str
    change_type: Literal["added", "modified", "removed"]
    description: str

class ChangeModel(BaseModel):
    """Analyzer → Reviewer handoff. Structured, not prose."""
    purpose: str
    impacted_files: list[str]
    changed_interfaces: list[ChangedInterface]
    config_changes: list[str]
    migration_risk: bool
    assumptions: list[str]
    tests_touched: list[str]
    tests_likely_needed: list[str]

Jedes Feld dient dem nachgelagerten Agenten. Die Liste der geänderten Schnittstellen ermöglicht es dem zweiten Agenten, schnell herauszufinden, was wichtig ist. Die Liste der Konfigurationsänderungen hilft dem Analyzer dabei, Anwendungslogik von Bereitstellungslogik zu trennen.

Der assumptions-Array verdient besondere Aufmerksamkeit. Der Analyzer muss darlegen, was der Autor des Codes voraussetzt – beispielsweise dass discount_pct eine Zahl zwischen 0 und 1 ist. Damit wird nichts kritisiert, es werden lediglich Fakten dargestellt. Dieses strukturierte Objekt wird anschließend zur exakten Eingabe für den Reviewer.

Design zwei: Analyzer und Risk Reviewer

Das neue Zustandschema enthält den ChangeModel sowie ein boolesches conflict-Flag, das später entscheidet, ob ein Mensch eingeschaltet werden soll.

class TwoAgentState(TypedDict):
    pr_id: str
    messages: list
    diff: str
    change_model: ChangeModel | None
    findings: list[ReviewFinding]
    conflict: bool

Der Fetch-Node bleibt unverändert und holt den Diff sowie die Testabdeckung ab. Anschließend kommt der Analyzer-Node mit einer streng begrenzten Anweisung. Dem Modell wird ausdrücklich mitgeteilt, nach Fehlern nicht suchen zu sollen, sodass es sich ausschließlich auf das Verstehen konzentriert.

ANALYZER_PROMPT = """\
You are the Analyzer agent. Your ONLY job: understand the change.
Do NOT critique. Do NOT hunt for bugs. Just map what changed.
Produce a structured change model."""

def ta_analyzer(state: TwoAgentState) -> dict:
    """Analyzer: compress & clarify. Optimizes for coherence."""

    # We use LLM structured output to fill the ChangeModel.
    # For this demonstration, we simulate the accurate analysis.
    cm = ChangeModel(
        purpose="Add discount percentage support to invoice billing",
        impacted_files=["src/billing/invoice.py", "src/billing/api.py",
                        "config/feature_flags.yaml"],
        changed_interfaces=[
            ChangedInterface(file="src/billing/invoice.py",
                symbol="InvoiceService.calculate_total",
                change_type="modified",
                description="Added discount_pct param (default 0)"),
            ChangedInterface(file="src/billing/api.py",
                symbol="create_invoice",
                change_type="modified",
                description="Reads discount_pct from request, passes to service"),
        ],
        config_changes=["discount_billing flag added, 100% rollout"],
        migration_risk=False,
        assumptions=[
            "discount_pct is expected to be 0..1 (fraction, not percentage)",
            "No existing callers pass discount_pct yet",
            "Feature flag controls visibility, not the calculation",
        ],
        tests_touched=[],
        tests_likely_needed=[
            "test discount path in calculate_total",
            "test boundary: discount_pct = 0, 1, >1, <0",
            "test API validation of discount_pct input",
        ],
    )
    return {
        "change_model": cm,
        "messages": [AIMessage(content=f"Analyzer: change model built. "
                               f"{len(cm.changed_interfaces)} interfaces changed, "
                               f"{len(cm.assumptions)} assumptions made.")],
    }

Der Analyzer liest den Diff und protokolliert, was tatsächlich vorhanden ist: den neuen Parameter, das neue Feature-Flag sowie die implizite Annahme, dass der Rabatt zwischen 0 und 1 liegt.

Anschließend gelangt das Objekt zum Risikobewerter, dessen Anweisungen absichtlich feindlich gestaltet sind. Ihm wird gesagt, dem Analyzer misstrauen und nach Fehlern suchen zu sollen.

REVIEWER_PROMPT = """\
You are the Risk Reviewer. The Analyzer gave you a change model.
DISTRUST it. Your job: find what's missing, broken, or dangerous.
Challenge every assumption. Check for missing tests, regressions,
rollout risks, and security issues."""

def ta_reviewer(state: TwoAgentState) -> dict:
    """Reviewer: expand & challenge. Optimizes for skepticism."""
    cm = state["change_model"]
    findings: list[ReviewFinding] = []

    # The Reviewer iterates through the Analyzer's assumptions
    for a in cm.assumptions:
        if "0..1" in a:
            findings.append(ReviewFinding(
                severity="critical",
                category="security",
                file="src/billing/api.py",
                description="ASSUMPTION CHALLENGED: discount_pct comes from user input "
                    "(req.discount_pct) with NO validation. Values <0 or >1 break billing. "
                    "Negative discount = price increase beyond subtotal. "
                    "Value >1 = negative total.",
                confidence="high"))

    # The Reviewer checks the test mapping
    if not cm.tests_touched and cm.tests_likely_needed:
        findings.append(ReviewFinding(
            severity="high",
            category="missing_test",
            file="tests/test_billing.py",
            description=f"NO tests touched but {len(cm.tests_likely_needed)} needed: "
                + "; ".join(cm.tests_likely_needed),
            confidence="high"))

    # The Reviewer checks the config changes
    for cc in cm.config_changes:
        if "100%" in cc:
            findings.append(ReviewFinding(
                severity="high",
                category="rollout_risk",
                file="config/feature_flags.yaml",
                description="Feature flag at 100% from day one — no gradual rollout. "
                    "Combined with unvalidated discount input, this is a billing incident risk.",
                confidence="high"))

    # The Reviewer looks for edge cases in the logic flow
    findings.append(ReviewFinding(
        severity="medium",
        category="edge_case",
        file="src/billing/api.py",
        description="Feature flag controls visibility but calculate_total always applies "
            "discount. If flag is off but API still receives discount_pct, discount "
            "is silently applied.",
        confidence="medium"))

    # We determine if there is a conflict worth escalating
    conflict = any(f.severity in ("critical", "high") for f in findings)

    return {
        "findings": findings,
        "conflict": conflict,
        "messages": [AIMessage(content=f"Reviewer: {len(findings)} findings, "
                               f"conflict={conflict}")],
    }

Weil der Prüfer keine Ressourcen für die Erkennung der geänderten Dateien oder des Zwecks des Pull Requests aufwendet, führt dies dazu, dass der gesamte Kontext dazu genutzt wird, die Logik zu stören. Er geht davon aus, dass der Rabatt zwischen 0 und 1 liegen sollte, prüft den Diff, findet keinen Validierungscode und markiert somit einen Fehler der Eingabeverifikation mit hoher Schweregrad.

Das ist einer der wenigen Bereiche, in denen mehrere Agenten zusammenarbeiten müssen – einer erstellt das Ziel, der andere prüft es.

Arbitrage mit einer menschlichen Einmischung

Der Prüfer hat ernsthafte Mängel festgestellt, die dem Autor des Pull Requests entgangen sind. Ihre direkte Veröffentlichung auf GitHub wäre jedoch riskant. Falls der Prüfer falsche Informationen liefert, wird dies das Team verärgern. Eine sinnvolle Ergänzung in der Produktion wäre ein Merge-Node, der die festgestellten Mängel bewertet und entscheidet, ob eine menschliche Person eingreifen sollte.

def ta_merge(state: TwoAgentState) -> dict:
    """Merge findings. If conflict, surface for human review."""
    if state["conflict"]:
        return {
            "messages": [AIMessage(content=(
                "⚠ CONFLICT: Reviewer found critical/high issues. "
                "Routing to human review."
            ))],
        }
    return {
        "messages": [AIMessage(content="Findings merged. No escalation needed.")],
    }

def route_after_merge(state: TwoAgentState) -> str:
    return "escalate" if state["conflict"] else "emit"

Wenn die conflict-Flagge aktiv ist, wird der Ablauf an einen Eskalationsknoten weitergeleitet. Dort kommen bedingte Kanten, interrupt() sowie die Wiederaufnahme auf Basis von Command zum Einsatz.

from langgraph.types import Command, interrupt

def ta_escalate(state: TwoAgentState) -> Command:
    """Human-in-the-loop for conflicting findings."""

    # The interrupt function pauses execution and surfaces data to the caller
    decision = interrupt({
        "kind": "review_conflict",
        "pr_id": state["pr_id"],
        "change_model": state["change_model"].model_dump(),
        "findings": [f.model_dump() for f in state["findings"]],
        "prompt": "Review findings. Respond: "
                  '{"action":"accept_all"} or {"action":"override","drop_indices":[...]}',
    })

    # Execution resumes here when the human provides input
    action = decision.get("action", "accept_all")

    if action == "override":
        drop = set(decision.get("drop_indices", []))
        kept = [f for i, f in enumerate(state["findings"]) if i not in drop]

        # We use Command to update state and dynamically route to the next node
        return Command(update={"findings": kept}, goto="emit")

    return Command(goto="emit")

Durch den Aufruf von interrupt() wird die Ausführung pausiert. Der Graph serialisiert seinen aktuellen Zustand und speichert ihn über den konfigurierten Checkpointer – in einer lokalen Demo handelt es sich dabei um MemorySaver. In der Produktion würde man einen langlebigen Speichermechanismus unter Verwendung von Postgres verwenden, damit der gepausierte Zustand auch nach Neustarts des Servers erhalten bleibt.

Der Backend-Server kann anschließend eine Slack-Nachricht an einen Senior Engineer mit den strukturierten Erkenntnissen senden. Dieser liest die kritische Warnung bezüglich des unvalidierten Rabatts und drückt bei Zustimmung auf die Schaltfläche „Alle akzeptieren“. Wenn der webhook Ihren Server erreicht, setzt er die Ausführung fort, indem er dem Graph einen Command übermittelt, der die Daten zum Wiederaufnehmen enthält.

# How you resume the graph from your backend API
ta.invoke(Command(resume={"action": "accept_all"}), config)

Das Diagramm wird aktiv, verarbeitet die menschliche Entscheidung, passt die Ergebnisse bei Bedarf an und wechselt zum endgültigen Ausgabenode.

def ta_emit(state: TwoAgentState) -> dict:
    """Final output: formatted review."""
    lines = [f"=== Code Review: PR {state['pr_id']} ==="]
    if state["change_model"]:
        lines.append(f"Purpose: {state['change_model'].purpose}")
    lines.append(f"Findings ({len(state['findings'])}):")

    for i, f in enumerate(state["findings"]):
        lines.append(f"  [{f.severity}] ({f.category}) {f.file}")
        lines.append(f"    {f.description}")

    return {"messages": [AIMessage(content="\n".join(lines))]}

Verkabelung des Graphen

Die Knoten werden zu einem StateGraph zusammengefasst. Ein statischer Kante ist aus dem escalate-Knoten nicht erforderlich, da ta_escalate einen Command(goto="emit") zurückgibt und LangGraph dynamisch leitet.

from langgraph.checkpoint.memory import MemorySaver

def build_two_agent():
    g = StateGraph(TwoAgentState)

    g.add_node("fetch", ta_fetch)
    g.add_node("analyzer", ta_analyzer)
    g.add_node("reviewer", ta_reviewer)
    g.add_node("merge", ta_merge)
    g.add_node("escalate", ta_escalate)
    g.add_node("emit", ta_emit)

    g.add_edge(START, "fetch")
    g.add_edge("fetch", "analyzer")
    g.add_edge("analyzer", "reviewer")
    g.add_edge("reviewer", "merge")

    g.add_conditional_edges("merge", route_after_merge,
        {"escalate": "escalate", "emit": "emit"})

    g.add_edge("emit", END)

    # A checkpointer is required to use interrupt()
    # Use MemorySaver for local testing, Postgres for production
    return g.compile(checkpointer=MemorySaver())

Vergleich der beiden

Stellen Sie die beiden Reviewer in diesem PR nebeneinander – der Unterschied ist offensichtlich.

Der einzelne Reviewer weist zwei Probleme aus. Er empfiehlt die Hinzufügung von Unit-Tests und schlägt Typhinweise vor. Er verhält sich wie ein höflicher Junior-Entwickler, der sich auf die Syntax konzentriert.

Das Duo findet vier Probleme. Es stuft die unvalidierte Eingabe als kritischen Fehler in der Geschäftslogik ein. Es weist darauf hin, dass negative Rabatte die Preise erhöhen würden. Es betrachtet den sofortigen 100%-Rollout als Risiko für die Rechnungsstellung. Zudem identifiziert es einen Randfall, in dem der Rabatt trotz ausgeschalteter Feature-Flag stillschweigend angewendet wird.

Das Mehr-Agenten-Design garantiert nicht jedes Mal bessere Bewertungen. Hier hat es geholfen, weil die Aufteilung außergewöhnlich klar war. Das zweite Agent fügte echte Signale statt Rauschen hinzu und erkannte die Fehler in der Geschäftslogik, die die Produktion lahmlegen. Es funktionierte, weil die kognitive Last aufgeteilt wurde: Der Analyzer übernahm die umfangreiche Auswertung und die Kartenbildung der Schnittstellen, während der Reviewer eine saubere ChangeModel-Struktur erhielt und sich voll und ganz auf seine kritische Prüfung konzentrieren konnte.

Wann man keine Mehr-Agenten-Struktur verwenden sollte

Die Kombination aus zwei Agenten ist zwar leistungsstark, doch es besteht die Versuchung, jeden einzelnen Agenten als Teil eines Mehr-Agenten-Workflows umzuschreiben. Widerstehen Sie diesem Drang.

Zusätzliche Agenten verursachen Verzögerungen. Der Reviewer mit zwei Agenten benötigt etwa doppelt so lange, da seine LLM-Aufrufe sequenziell ablaufen, etwa doppelt so viele Token verbraucht wird und außerdem ein komplexeres Zustandsmodell sowie eine aufwändigere Übergabelogik erforderlich sind. Wenden Sie dieses Muster nur an, wenn die Aufgabe in eine der drei genannten Situationen passt.

Adversarisches Arbeiten. Wenn ein System sowohl Inhalte erstellen als auch kritisieren muss, sollten diese Aufgaben getrennt werden. Der Generator und der Discriminator stellen das am besten bewährte Mehr-Agenten-Design dar. Ein Agent schreibt den Code und ein anderer schreibt Tests, um ihn zu stören. Einer entwirft eine E-Mail und ein anderer überprüft sie auf Konformitätsprobleme.

Strenge Trennung der Tools. Ein Agent, der im öffentlichen Internet sucht, und ein Agent, der Datenbankabfragen ausführt, sollten keinen gemeinsamen „Verstand“ teilen. Ein Forscher soll öffentliche Daten sammeln und eine sauber aufbereitete Zusammenfassung an einen Ausführenden weitergeben. Dadurch entsteht eine klare Sicherheitsgrenze. Wenn eine Webseite eine Prompt-Injection enthält, ist nur der Forscher kompromittiert, während der Ausführende isoliert bleibt.

Asymmetrische Skalierung des Kontexts. Manchmal fließt eine enorme Menge an Daten in eine winzige Entscheidung. Eine Flotte billiger, schneller Worker kann 100 Protokolldateien parallel lesen, aus jeder die relevanten Fehlerzeilen extrahieren und eine kurze Zusammenfassung an einen einzigen, teuren Supervisor weiterleiten, der die endgültige Entscheidung trifft.

Falls eine Aufgabe nicht in eines dieser Szenarien passt, sollte man sich auf einen einzigen Agenten beschränken. Ein einzelner Agent mit einer gut formulierten Anweisung, strengen Einschränkungen bezüglich der verwendeten Tools sowie einem klaren Zustandsmodell ist immer überlegen gegenüber einer unübersichtlichen Mehr-Agenten-Konfiguration.