Галоўная / Артыкулы

Калі два агенты лепшыя за аднаго: памочнік рэв’ю кода на LangGraph двума спосабамі

Будуем рэцэнзента кода як аднаго агента і як пару «аналізатар + ацэншчык рызык» у LangGraph і разбіраем тры выпадкі, калі шматагентнасць вартая свайго кошту.

Выдавец: REACTAPP.TOP14 хв чытання3219 слоў

Крыніцы: Multi-Agent Systems: When 2 Agents Beat 1 (and When They Don’t)

„Мульты-агент“ — гэта популярны тэрмін сёгодні. У дэманстрацыях паказваецца, як пяць персонажаў на базе ШЫ размовяюць у сімуляванай кімнаце, каб збіраць інфармацыю з веб-сайта і пісаць пастку на блог, пры чым у яных ёсць імены на кшталт Дзецярнік, Пісьменнік і Рэдактар, а таксама тэрмінал, у якім агенты спароўваюцься між сабою. Гэта выглядае вражаюча, але так не ствараюцца надзеяныя програмы.

Дадаўшы агентов, сама па сабе сістэма не становіцца розумней. Гэта прымножае можлівасці ўзбіяння. Возьмімо базавую арыметыку: агент, які правільна выкананае сваю задачу 90% часу, мае надзеянасць 0,90. Калі з’едыніць трохе такіх агентоў, верыгоднасці прымножаюцца, і базавая надзеянасць падыходзіць да апошніх 72%. Затрымка у выкананні задач падваіцца, вітраты на API зростаюць у тры разы, а канечны рэзультат не становіцца лепшым.

Запіт, на якій варта даты адказ, — гэта калі больш за аднаго агента насправды плакатыць за рэшэння. Хорашы спосаб раскінуць гэта — стварыць два асістэнты для перагляду коду на базе ШЫ. Першая версія — цэна звычайны пераглядач на аднаго агента. Другая дзеліць работу между Аналізатаром і Пераглядачам рызыка, выкарыстоўваючы функцыі LangGraph, такія як умовныя рэшткі, interrupt() і воззванні Command для перадачы задачы чалавеку, калі гэта неабходна. Порэванае паказвае, чаму аднаго агента не ўсвіядомляе серьёзнага браку ў рахунаванні, а чаму пара яго знаходзіць.

Чаму перагляд коду ўскладнены для аднаго запіту

Хорашы перагляд выклікае патрэбу ў двух аблічна разных способах мышлення.

Першы — гэта разумеўненне. Вы чытаеце разлікі, відображаеце функцыі, якія змяніліся, выявляеце, якія файлы пацверджаны, і выважваеце, што мела на увазе апошнія версія коду. Мета — стварыць ментальную мадэль новай функцыі.

Другій падчынны спосаб — адварсары. Вы шукаеце крайнія прыклады, відсутныя тэсты і логічныя прасліні, і намагаецеся зламаць код.

Калі папросіце адзін LLM зрабіць гэта ўсё ў аднам запіце, ён будзе страждзець. Падсумаванне і крытыка ў однай процэдурэ зазвычай спрыяюць цэлесообразнасі больш, чым сумневам. Калі мадэль выбірае певную версію таго, каму служыць код, яна вялікай меры не прыкладзае зусілляў да атакі на гэту версію, а зводзіцца да дробных зауважэнняў пра стыль, такіх як відсутнасць докстрынгаў чы правіл іменавання.

Конкрэтны прыклад гэта чытра паказвае. Уявіце сабе фальшывы запрос на падключэння да сістэмы білінгу, у яком автар дадаў функцыю зніжкі да калькулятора рахункоў. Гэтае ўсё, што атрымліваюць оба дызайны:

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

Калі прыгнуць праззе, можна пазіць калькольныя проблемы. Значэнне discount_pct прыходзіць безпосерэдна з вводу карыстальніка на роўні API і прабіваецца ў расчытанні без жадной перапрацавкі. Пользоватэль можа адправіць абяцковы знак для падвышэння цены або значэнне больш за 1, што прыводзіць да абяцковага загальнага рэзультата. Тэсты не пакрываюць гэты новы сцэнарый. А флаг функцыі налаштаваны так, каб яна была доступна 100% корыстувачаў адразу. Гэта сцэнарый цэнавання, які лёгка можа быць выкорыстаны для зловжывання. Як жа справляецца з гэтым стандартны аднаго-агента рэвізор?

Дызайн 1: рэвізор з аднаго агента

Спачатку — набор адзінакоў агента: адзін інструмент для атрымання разніцы, адзін — для ачынення файлоў, адзін — для пошуку сімвалаў і адзін — для адналежнасці пра тое, якія тэсты існуюць.

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]

Далей — дзяржава. TypedDict дапамага пад падтрымкуючы структуру з LangGraph, і ён зберагае ID PR, історыю паведамленняў, неапдэтыраны разлік і спіс выявленых проблем. Модэль Pydantic опісвае кожную з выявленых проблем і прымушвае модэль класіфікаваць ўсё, што ён знаходзіць, за дапамою фіксаваных літэралайнаў, што значна спрыяе пазнейшай апрацоўцы.

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)

Далей іду вузлы. Граф складаецца з простай лініі: вузел запошчання, які завантажае кантэкст, а пасля — вузел перагляду, які выкананяе запіт.

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

(У гэтай дэманстрацыі рэзультаты ўоснованы на жорсткая прыкладзенае мастацтва для падобных выходных дадзеных, якія зазвычай дае такі адзінак, таму спрыяйце гэтыя парадынкі як ілюстрацыю, а не як крытэрый.)

Сапраўды, такім чынам ствараюцца большасць адзінакоў першага пасэгу на базе AI: падаецца разніца, прасіцца пра багі. Калі ўсё пераглядаецца па адносу да фальшывага PR, агент зазначае відсутнае тэставанне і пропануе паведамленні пра тыпы. Але ён не бачыць неверыфікованых дадзеных і рызыка, якія можу выйсці па выкананні.

Прычына крыўціцца ў запіце. Калі модэлю прасяць падсумаваць змяну, а потым перыясніць рызыкі, ён насвятляе свою увагу на тым, што была дадана функцыя зніжкі. Калі ён прыходзіць да часткі з рэзультатамі, ён вялікай меры ужо належыць да опісавага стылю, фокусіруючыся на вялікім сінтаксі і не намагаючыся чагось зламаць. Рашэннем ёсць раздзеленыя заведаменні сінтэзы і адварсарскага аналізу, што значыць два агенты.

Перадача: выкорыстоўваць схему, а не прозу

Найбольшая памылка ў дзеянні сістэм з калькольнікамі — калі калькольнікі могу разгаварыцца адзін з іншым у вільным тэксце. Калі калькольнік А запіша тры параграфы і перадае іх калькольніку Б, той павінен аналізаваць тэкст, здагадвацца, што ўажліва, і будаваць працу на нечыстай, неяснай базе. Самэй так падупадае кантэкст.

Калькольнікі — гэта функцыі, і функцыі павінны перадаваць об’екты з адзінаковым тыпам. Ёнколі сістэма з калькольнікамі мае быць надзеямна, неабходна ўзгадка аб точным шыматку таго, што перадаецца між німі.

У асістэнце для адзору першы калькольнік — гэта Аналізатор, і яго задача — аналізаваць PR. Ён павінен стварыць ChangeModel — об’ект, які змушвае яго выявіць конкрэтныя метаданы пра змяну.

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]

Кожна галерэя служыць агенту, які працуе далей. Спіс змененых інтэрфейсаў дапамагае другаму агенту быстра з’ясаваць, што мае значэння. Спіс змян у налаштаваннях дазволяе Аналізатору аддзельна выдзеляць логіку прыемлень ад логікі развёртвання.

Масэвы assumptions заслуговае на ўвагу. Аналізатор павінен чытальна паказаць, чаго, на думку автара коду, можна спакоўвацца, напрыклад, што discount_pct — гэта дроб межу 0 і 1. Гэта не ўскладненне чаго-небудзь; гэта проста фіксацыя фактов. Гэты структураваны об’ект стае точным вхідным дадзенням для Пераглядача.

Дзялённе 2: Аналізатор і Пераглядач рызыка

Новая схема стану мае ChangeModel плюс булевы флаг conflict, які пазней вяршыць рашэнне, чы розглядаць справу з адамам.

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

Узел fetch застаецца тым жа, як і раней, і збірае даны пра разніцы і ступень пакрыці тэстамі. Далей ідзе узел Analyzer з дужа строгім запітам. Модэлю чыстаючы сказваецца не шукаць багі, таму яна сфокусаваецца выключна на розумеенні.

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

Аналізатор чытае разлік і фіксуе тое, што там насправды ёсць: новы параметр, новы флаг функцыі, а таксама неявна прыпуск, што зніжка знаходзіцца межа 0 і 1.

Цяпер об’ект пераходзіць да спеціяліста з аналізу рызыка, якому намеравана ўскладніць задачу. Йому кажуць не даваць доверы Аналізатору і шукаць недолікі.

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

Пакалькі ўтваральнік адзін не выкарыстоўвае токены для выявлення таго, якія файлы зменіліся чым служыць PR, таму весь яго кантэкст спрыяе нарушэнню логікі. Ён спачатку прыймае зачынак, што зніжка мае быць ад 0 да 1, пераглядае разлік, не знаходзіць коду для верыфікацыі і пазначае баг высокай сер’ёзнасці ў верыфікацыі вхідных дадзеных.

Гэта адна з некалькіх сітуацыяў, дзе калькі агентаў працуюць разам: адні ствараюць цэль, а іншые працуюць над ёю.

Арбітраж з удзелам чалавека

Утваральнік адзін выявіў сер’ёзныя проблемы, якія автар PR працягнуў не пазначыць. Аднак публікацыя іх безпосерэдна на GitHub будзе рызыкаванаю. Калі утваральнік адзін выдумвае, гэта раздражае команду. Розумным рашэннем ў працоўным сераўеры ёсць вузол злучэння, які ацэніць выявленыя проблемы і выявіць, чы рэшаць пытанне трэба за дапамогою чалавека.

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"

Калі флаг conflict ў стане true, поток прайходзіць да вузла эскаліяцыі. Сярод іншага, там выкарыстоўваюцца умовныя рэшты, функцыя interrupt() і воззванні для продажы працы на аднойчыні на адваротах, базаваных на 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")

Выклік interrupt() зупіняе выканання. Граф выконвае серыялізацыю свайго тэкушчага стану і зберагае яго праз настаўлены чэкпойнтар, якім у локальнай дэмаверсіі ёст MemorySaver. У прымэтнай сістэме вы бы вікорысталі надзеяны сэйвер, падтрымваны Postgres, каб зупінены стан застаўся пасля перзапуску сервера.

Пасля чаго ваш бэкенд можа апрацаваць структураваныя даны і аправіць поведамленне ў Slack да старшагінжынера. Яны чытаюць крытычныя паведамленні пра неверыфікованыя зніжкі, і калі пагадаюцца, натыкаюць на кнопку „Прыняць усе“. Калі вебхук досягае вашага сервера, ён продовжвае выканання, перэдаючы графу Command, які несе даны для продажу працы.

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

Граф працюе, обрабоцоўвае рашэнне чалавека, практычна скорачвае рэзультаты, якія ўтварыліся, і пераходзіць да фінальнага вузла выдачы.

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

Падчыранне графа

Узлы саўершаны ў StateGraph. Не патрабуецца статычная грань з узла escalate, таму ўжо ta_escalate вяртае Command(goto="emit"), а LangGraph роюе дынамічна.

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

Порэшчанне двух падходаў

Калі размясціць двух аналізаваўшыхх адразу ўжо ў гэтым PR, разлік будзе явны.

Адзіні агент выявіў два прыбліжэння. Ён рэкамендуе дадаць тэсты на елементы і пропануе вікорыстоўваць падказкі тыпаў. Ён паводзіцца як ввічлівы младшы распрацоўшчык, які сфокусаваны на сынтаксі.

Пара выявіла чатыры прыбліжэння. Яны пазначылі неверыфікованы вхідны даны як крытычныя недагодзі бізнес-логікі. Яны зазначылі, што негатыўныя зніжкі могу падняць цены. Яны пазначылі мглевае застосаванне 100% зніжкі як рызык для білінгу. А таксама выявілі крайній кейс, калі зніжка застосоўваецца непазорна, нават калі флаг функцыі выключаны.

Дыяграма з мнагімі агентамі не гарантуе лепшых адзінакоў ў кожны раз. Яна памагла тут, таму што разбіўка задач была нечым выключна чыстая. Другі агент даў рэальны сігнал, а не шум, выявіўшы бягункі бізнес-логікі, якія выклікаюць зупінку працы сістэмы. Гэта запрацавало, таму што канцэптуальная навантажэння была раздзелена: Аналізатор віконваў складную працу з чытання і картавання інтэрфейсаў, а Адзінакавач отрымаў чысты ChangeModel і мог аддацца сваім перакананням.

Калі не варта вікорыстоўваць мнагія агенты

Пара агентаў ўзятае ў роўнасці з моцным інструментам, і можа выклікваць спрагу перапісаць кожнага агента як частку мнагія-агентнага працэсу. Не паддаюцеся такой спраге.

Дадатковыя агенты прыносяць затрымкі. Адзінакаванне за дапамою двух агентаў займае прыблізна удвое больш часу, таму што ўсі ўклеканні LLM выкананыя ў парадку, гэта коштае прыблізна удвое больш токенав, а таксама выклікае болей складную схему станаў і логіку перадачы задач. Выкорыстоўвайце такі падход толькі тады, калі задача падходзіць пад адну з трох сітуацый.

Адверсарыя работа. Калі сістэма должна як ствараць, так і адгукавацься да контэнту, трэба раздзеліць гэтыя ролі. Найэфектывнейшым дызайнам з многімі агентамі ёсць вікорыстанне генератора і дыскрымінатора: адні выконвае код, а другі пішчае тэсты, каб яго зламаць; адні складае электронны ліст, а другі пераканальвае яго на наявнасць проблем з адпаведнасцю.

Строгая ізоляцыя інструментаў. Агент, які шукае інфармацыю на публічным вебе, і агент, які выконвае запыты да базы дадзеных, не должны дэліць адной і той жа логікай. Нехай дзеячы саберае публічныя даны і перадае ўпорадкованы ўзвод дзеячу, які яго выканае. Гэта стварае жорсткія межы безпекі. Калі на веб-сторанцы ёсць інжэкцыя запыту, парадузеным будзе толькі дзеячы, а дзеячы застанецца ізольаваным.

Асіметрычнае масштабаванне кантэксту. Інодзе вельмі веліка колькасць дадзэнняў вплывае на незначныя рашэнні. Група дзешавых, быстрых працоўнікаў можа чытаць 100 файлаў лога паралельна, кожны з яных выкарыстоўваець актуальныя радкі з адзінакамі і перадае короткія падсумкі аднаму дорогаму керуючаму працоўніку, які прыме рашэнні.

Калі задача не падходзіць ні да однага з гэтых варыянтав, лепш застацца з аднім агентам. Адзін агент з добра сконструаваным запітам, строгімі абмежэннямі інструментаў і чыстым схемам стану завжды будзе лепшы за хаотычную структуру з калькама агентаў.

Супаўзеўныя матэрыялы

  • Claude Code Agent Teams: Калі і як перэйсці на кальку агентаў — Калі аднаго агента достатнь, чатыры прычыны для дадаўання ўсё больш агентаў, шаблоны оркестрацыі, стылі камунікацыі, восьма способаў абвалення і выбор між варыянтамі з прыроднымі функціямі і самастайнымі налаштаваннямі для команд Claude Code.
  • Стварэнне мульты-агента-рутэра з LangGraph і агентам-надзірнікамі — Як раздзеліць аднаго перавантажанага агента на спецыялістаў за дапамогою агента-надзірніка ў LangGraph, выкарыстоўваючы готавыя дапаможнікі і ручна структуру StateGraph, а таксама прыметы дызайну станаў і падступы, якія можуць спрычыніць проблемы.
  • Як навучыць сістэму рэкамендацый, дзе людзі хочуць жыць: гео-ембеддынгі — Як платформа з нерухомасю заменіла первісныя коордынаты шыроты і далготы на навучаныя гео-ембеддынгі H3 у сістэме рэкамендацый з двума „вышкамі“, а таксама паказала рэзультаты працы без падключэння да інтэрнэту і рэзультаты тэстаў тыпу A/B.
  • Як сабагенты і сістэмы з калькольнікамі агентаў дзеляць працу і калектары — проста адпаведнае поясненне сабагентаў у працэ паўстанавлення сістэм з калькольнікамі агентаў, дызайну оркестратора і працавіка, чатырох топалогій, калектароў за токены і калі аднаму агенту лепей выбірацца.
  • Стварэнне двух локальных агентаў LLM для дыскусіі: падставы, фальшывы кансэнсус, багі схемы — эксперымент Ollama з агентамі, якія разлічаюцца па доказах, а не па персанажах, які раскрывае падставы NLI, баг фальшывага кансэнсуса, перакананні проты атак і обавязковыя поля.
  • Паспорт GCA: Контракт задачы, які відновляе просторава логіка ў VLM-ах — Як агент з геаметрычнымыя обмежэннямі віддзеляе формалізацыю задачы ад геаметрычнай екзекуцыі, чаму ёму не трэба трэнінгу, і як ён перамагае сільныя базовыя рашэнні на тэстах.