Strona główna / Artykuły

Gdy dwóch agentów pokonuje jednego: Copilot do przeglądania kodu LangGraph, dostępny w obu kierunkach

Stwórz jednego recenzenta kodu jako pojedynczego agenta, a następnie jako analityka wraz z recenzentem ryzyka w LangGraph, a potem dowiedz się o trzech przypadkach, w których użycie wielu agentów jest opłacalne.

Wydawca: REACTAPP.TOP14 min czytania3219 słów

Źródła: Multi-Agent Systems: When 2 Agents Beat 1 (and When They Don’t)

„Multi-agent” to słowo modowe tego czasu. Dema pokazują pięć postaci AI rozmawiających w symulowanej sali w celu przeszukiwania strony i napisania posta na blogu – mają one nazwy takie jak Badacz, Pisarz i Redaktor – oraz terminal pełen agentów, które ze sobą dyskutują. Wygląda to imponująco, ale tak nie tworzy się niezawodne oprogramowanie.

Dodawanie agentów samo w sobie nie czyni systemu mądrzejszym. Zwiększa natomiast liczby możliwych błędów. Weźmy prostą arytmetykę: jeden agent, który prawidłowo wykonywa swoją pracę w 90% przypadków, ma niezawodność 0,90. Gdy połączymy trzy takie agenty, prawdopodobieństwa się mnożą, więc podstawowa niezawodność spada do około 72%. Opóźnienia się podwajają, koszty korzystania z API potrójniają, a ostateczny wynik nie jest lepszy.

Pytanie, na które warto odpowiedzieć, brzmi: kiedy więcej niż jeden agent faktycznie rozwiązuje problem. Dobrym sposobem na ustalenie tego jest stworzenie dwukrotnie asystenta do przeglądania kodu opartego na sztucznej inteligencji. Pierwsza wersja to konwencjonalny recenzent działający samodzielnie. Druga dzieli pracę między analitykiem a recenzentem ryzyka, wykorzystując funkcje LangGraph takie jak warunkowe krawędzie, interrupt() oraz możliwość wznowienia pracy na podstawie Command, aby w razie potrzeby zaangażować człowieka. Porównanie to pokazuje, dlaczego pojedynczy agent przeocza poważną usterkę w fakturowaniu, a dlaczego para ją wykrywa.

Dlaczego przeglądanie kodu jest trudne przy jednym poleceniu

Dobry przegląd wymaga dwóch zupełnie różnych sposobów myślenia.

Pierwszym z nich jest zrozumienie. Czytamy różnice między wersjami kodu, identyfikujemy zmienione funkcje, określamy, które pliki są dotknięte, i próbujemy odgadnąć intencję autora. Celem jest stworzenie mentalnego modelu nowej funkcjonalności.

Drugi podejście jest oparte na konfrontacji. Szukasz przypadków granicznych, brakujących testów i luk w logice, a także próbujesz złamać kod.

Jeśli poprosisz jeden model językowy o wykonywanie obu zadań w jednym poleceniu, będzie miał z tym trudności. Streszczanie i krytykowanie w tej samej fazie zwykle faworyzuje spójność kosztem wątpliwości. Gdy model ustali wersję opisującą to, co robi kod, inwestuje mniej wysiłku w podważanie tej wersji i zamiast tego skupia się na drobnych kwestiach stylistycznych, takich jak brak dokumentacji lub nieprawidłowe zasady nazywania.

Konkretny przykład to ilustruje. Wyobraź sobie fałszywy wniosek o pull request dotyczący systemu fakturowania, w którym autor dodaje funkcję zniżek do kalkulatora faktur. Oto różnice, które otrzymują oba projekty:

--- 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

Przy bliższym przyjrzeniu się widać kilka poważnych problemów. Wartość discount_pct trafia bezpośrednio z danych wprowadzonych przez użytkownika na warstwę API i jest używana w obliczeniach bez żadnej weryfikacji. Użytkownik mógłby wysłać ujemną wartość zniżki, aby podnieść cenę, lub wartość większą niż 1, aby uzyskać ujemną kwotę łączną. Testy nie obejmują tego nowego scenariusza. Ponadto flaga funkcji jest ustawiona tak, aby dotrzeć do 100% użytkowników natychmiast. To ścieżka ustalania cen bardzo podatna na nadużycia. Jak radzi sobie z tym typowy recenzent pracujący samodzielnie?

Projekt jeden: recenzent pracujący samodzielnie

Najpierw przychodzi zestaw narzędzi agenta: jedno narzędzie do pobierania różnic, jedno do otwierania plików, jedno do wyszukiwania symboli oraz jedno do informowania o dostępnych testach.

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]

Następnie stan. TypedDict zapewnia kompatybilność z LangGraph i przechowuje ID PR, historię wiadomości, surowy diff oraz listę stwierdzonych problemów. Model Pydantic opisuje każde z tych problemów i zmusza model do klasyfikacji znalezionych elementów za pomocą ustalonych wartości literowych, co znacznie ułatwia późniejszą obróbkę.

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)

Następnie przychodzą węzły. Graf ma prostą strukturę: węzeł pobierania, który ładuje kontekst, a potem węzeł przeglądania, który uruchamia polecenie.

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()

(W tym przykładzie wyniki są sztywno zdefiniowanymi zamiennikami tego rodzaju wyjścia, które zazwyczaj generuje taki recenzent, więc traktuj to porównanie jako ilustrację, a nie punkt odniesienia.)

Mniej więcej w ten sposób tworzone są pierwsze wersje recenzentów AI: podaje się różnice i prosi o zlokalizowanie błędów. Po przetestowaniu na symulowanym PR agent zauważa brakujący test i sugeruje wskazówki typowe. Przeocza jednak niezweryfikowane dane wejściowe oraz ryzyko wprowadzenia zmian.

Powód leży w instrukcjach. Gdy model ma podsumować zmianę, a następnie wymienić ryzyka, skupia się na wyjaśnieniu, że dodano funkcję zniżek. Zanim zacznie przygotowywać listę ustaleń, jest już w trybie opisowym, skupiając się na powierzchownej składni i nie próbując niczego zepsuć. Rozwiązaniem jest oddzielenie zadania syntezy od zadania adwersarialnego, co oznacza użycie dwóch agentów.

Przekazanie obowiązków: użyj schematu, a nie prozy

Najczęstszym błędem w projektowaniu systemów wielu agentów jest pozwalanie im na komunikację ze sobą w formie zwykłego tekstu. Jeśli agent A napisa trzy akapity i przekaże je agentowi B, ten musi przeanalizować tekst, odgadnąć, co jest ważne, i budować dalej na niejasnej podstawie. W ten sposób kontekst ulega degradacji.

Agenci to funkcje, a funkcje powinny przekazywać obiekty o określonym typie. Aby system wielu agentów był niezawodny, należy zdefiniować dokładną strukturę tego, co przemieszcza się pomiędzy nimi.

W asyście do przeglądania pierwszym agentem jest Analizator, a jego zadaniem jest mapowanie PR. Musi on wygenerować obiekt ChangeModel, który zmusza go do wyodrębnienia konkretnych metadanych dotyczących zmiany.

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]

Każde pole służy agencie poniżej w łańcuchu przetwarzania. Lista zmienionych interfejsów umożliwia drugiemu agencie szybkie sprawdzenie tego, co jest istotne. Lista zmian konfiguracji pozwala Analyzerowi oddzielić logikę aplikacji od logiki implementacji.

Tablica assumptions wymaga szczególnej uwagi. Analyzer musi jasno określić, co założał autor kodu, na przykład że discount_pct to ułamek w przedziale od 0 do 1. Nie krytykuje niczego – po prostu przedstawia fakty. Ten ustrukturyzowany obiekt staje się dokładnym materiałem wejściowym dla Reviewera.

Projekt dwa: Analyzer i Risk Reviewer

Nowy schemat stanu zawiera ChangeModel wraz z flagą booleanową conflict, która później decyduje o konieczności przekazania sprawy do osoby.

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

Węzeł fetch pozostaje niezmieniony i pobiera informacje o różnicach oraz stopniu pokrycia testami. Następny jest węzeł Analyzer, który ma ściśle określone instrukcje. Modelowi wyraźnie mówi się, aby nie szukał błędów, więc koncentruje się wyłącznie na zrozumieniu tekstu.

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.")],
    }

Analizator czyta różnicę i rejestruje to, co faktycznie się tam znajduje: nowy parametr, nowy flaga funkcji oraz domniemanie, że zniżka mieści się w przedziale od 0 do 1.

Następnie obiekt trafia do osoby odpowiedzialnej za ocenę ryzyka, której instrukcje są celowo wrogie. Poleca się jej nie ufać Analizatorowi i szukać wad.

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}")],
    }

Ponieważ osoba przeglądająca nie marnuje zasobów na ustalanie, które pliki się zmieniły lub w jakim celu została wysłana prośba o integrację, cały kontekst wpływa negatywnie na logikę systemu. Przyjmuje ona założenie, że zniżka powinna mieć wartość od 0 do 1, sprawdza różnice pomiędzy wersjami, nie znajduje żadnego kodu walidującego i sygnalizuje błąd walidacji danych o wysokim stopniu poważności.

To jeden z nielicznych przypadków, gdy kilka agentów współpracuje ze sobą. Jeden tworzy cel, a drugi próbuje go osiągnąć.

Arbitraż z udziałem człowieka

Osoba przeglądająca wykryła poważne problemy, których autor prośby o integrację nie zauważył. Jednak publikowanie ich bezpośrednio na GitHubie byłoby ryzykowne. Jeśli osoba przeglądająca myli się, to zdenerwuje zespół. Rozsądnym rozwiązaniem w produkcji jest użycie węzła łączącego, który oceni te problemy i zdecyduje, czy potrzebny jest arbitraż przez człowieka.

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"

Gdy flaga conflict ma wartość true, przepływ kieruje się do węzła eskalacji. Wtedy właśnie przydają się krawędzie warunkowe, funkcja interrupt() oraz możliwość wznowienia działania na podstawie Command.

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")

Wywołanie interrupt() powoduje pauzę w wykonywaniu. Graf serializuje swój obecny stan i przechowuje go za pomocą skonfigurowanego wskaźnika stanu, który w lokalnej demonstracji to MemorySaver. W środowisku produkcyjnym należałoby użyć trwałego mechanizmu przechowywania wspieranego przez Postgres, aby stan w stanie pauzy przetrwał restart serwera.

Następnie twoje backend może wysłać wiadomość do Slacka do doświadczonego inżyniera z ustrukturyzowanymi wynikami. Ten przeczyta krytyczne ostrzeżenie dotyczące niezweryfikowanego rabatu i, jeśli się zgodzi, naciśnie przycisk „Accept All”. Gdy webhook dotrze do twojego serwera, ten kontynuuje działanie, przekazując grafowi Command zawierający dane do wznowienia pracy.

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

Graf się budzi, przetwarza decyzję człowieka, w razie potrzeby koryguje wyniki i przechodzi do końcowego węzła emisji.

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))]}

Podłączanie grafu

Węzły są łączone w StateGraph. Nie jest potrzebny żaden statyczny krawędź wychodzący z węzła escalate, ponieważ ta_escalate zwraca Command(goto="emit"), a LangGraph kieruje ruch dynamicznie.

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())

Porównanie obu

Umieść obu recenzentów obok siebie w tym PR – różnica jest oczywista.

Jedyny recenzent wskazuje dwa problemy. Zaleca dodanie testów jednostkowych i proponuje wskaźniki typów. Zachowuje się jak uprzejmy junior, który skupia się na składni.

Para recenzentów znajduje cztery problemy. Zaznacza niezweryfikowane dane jako krytyczną wadę logiki biznesowej. Zwraca uwagę, że ujemne zniżki podniosłyby ceny. Oznacza natychmiastowe wdrożenie na 100% jako ryzyko dla procesu fakturowania. Ponadto identyfikuje przypadek krawędziowy, w którym zniżka jest stosowana bez żadnych komunikatów, nawet gdy flaga funkcji jest wyłączona.

Projektowanie z udziałem wielu agentów nie gwarantuje zawsze lepszych ocen. W tym przypadku okazało się pomocne, ponieważ dekompozycja była niezwykle przejrzysta. Drugi agent dostarczał rzeczywistych informacji, a nie szumu, wykrywając błędy w logice biznesowej, które powodują awarie w produkcji. Działało to skutecznie, ponieważ obciążenie poznawcze zostało rozłożone – analizator zajmował się intensywnym przetwarzaniem danych i mapowaniem interfejsów, natomiast recenzent otrzymywał uporządkowany obiekt ChangeModel i mógł skupić się na krytycznym spojrzeniu na rozwiązanie.

Kiedy nie należy używać wielu agentów

Para agentów jest potężna, więc może pojawić się pokusa przepisania każdego z nich jako części procesu wieloagentowego. Trzeba temu oprzeć się.

Dodatkowi agenci powodują opóźnienia. Proces recenzji z udziałem dwóch agentów trwa mniej więcej dwa razy dłużej, ponieważ ich wywołania modeli językowych są sekwencyjne, generują mniej więcej dwa razy więcej kosztów w postaci tokenów, a także wymagają bardziej złożonego schematu stanów i logiki przekazywania danych. Używaj tego podejścia tylko wtedy, gdy zadanie pasuje do jednej z trzech określonych sytuacji.

Praca w modelu adwersarialnym. Jeśli system musi zarówno tworzyć, jak i krytykować treść, należy rozdzielić te role. Najlepiej sprawdzonym rozwiązaniem jest projekt oparty na kilku agentach: jeden generuje kod, a drugi pisze testy w celu jego złamania; jeden opracowuje e-mail, a drugi sprawdza go pod kątem problemów z zgodnością.

Ścisła izolacja narzędzi. Agent, który przeszukuje publiczną sieć, oraz agent, który wykonywa zapytania do bazy danych, nie powinny dzielić się tym samym „mózgiem”. Niech badacz zbiera dane publiczne i przekazuje sprostowaną ich wersję wykonawcy. To tworzy solidną granicę bezpieczeństwa. Jeśli strona internetowa zawiera atak typu prompt injection, tylko badacz zostaje skompromitowany, natomiast wykonawca pozostaje izolowany.

Niesymetryczne skalowanie kontekstu. Czasami ogromna ilość danych służy do podjęcia niewielkiej decyzji. Zespół tanich, szybkich pracowników może jednocześnie czytać 100 plików logów, z których każdy wyodrębnia odpowiednie linie z błędami i przekazuje krótkie podsumowanie pojedynczemu, drogocennemu nadzorcy, który podejmuje ostateczną decyzję.

Jeśli zadanie nie pasuje do żadnej z tych sytuacji, należy pozostać przy jednym agencie. Jeden agent z dobrze opracowanym poleceniem, ścisłymi ograniczeniami narzędziowymi oraz uporządkowanym schematem stanu zawsze będzie lepszy od chaotycznego układu wielu agentów.