Багато рук, один перо: сім правил оркестрування AI-агентів
Сім правил для оркестрування агентів ШІ: коли простий робочий процес є кращим, чому у оркестратора немає інструментів, повернення типових підагентів та вказані умови зупинки.
Джерела: Many Hands, One Pen: Orchestrating Agents Without Losing the Plot
Уявіть Військово-морські сили США у 1942 році, які стикалися з проблемою координації, що багато говорить про сучасне програмне забезпечення. Військові кораблі оснащувалися радарами, сонарами, засобами радіоперехоплення та людьми-спостерігачами, і кожен звіт надходив на місток одночасно та в випадковому порядку. Капітан, який намагався вести бій, був затьмарений непропрацьованими сигналами.
Відповіддю Військово-морських сил стала кімната без вікон під назвою Центр бойової інформації. Кожен звіт від датчика позначався на єдиній спільній схемі за узгодженим форматом. Оператори, які читали показники приладів, нічого не керували; їхнім завданням було підтримувати точність схеми, тоді як один офіцер сам вирішував, яким буде наступний крок корабля. Інформація надходила у широкому обсязі, рішення звужувалися до однієї точки, а стандартний формат звітів дозволяв порівнювати записи між собою.
Системи з кількома агентами постійно знову та знову досліджують ту саму ситуацію. Етап, коли «додати ще одного агента» був рішенням усіх проблем, вже минув, і 2026 рік переважно пройшов під знаком вимірювання вартості цього рішення. Єдиний підхід, який залишився, — це той, який обрав Військово-морський флот: одночасне збирання інформації, її звітування у встановленому форматі, прийняття рішень в одному місці та чітке визначення критеріїв завершення роботи.
Корисна концепція: більшість проблем з кількома агентами насправді не є проблемами агентів. Це проблеми контексту та контролю потоку, прикидані під проблеми агентів. Зазвичай процес йде не так через те, що два агенти приймають несумісні рішення, які кожен з них не може бачити, через втрату значення інформації під час її передачі через межу, або тому, що ніхто не записав, як має виглядати завершення роботи.
Далі наведено сім практик, кожна з яких базується на помилках у системах, що вже використовувалися. Разом вони допомагають вам визначити, коли звичайний робочий процес кращий за агента, коли оркестратор повинен оплачувати свої токени, як розділяти роботу за межами контексту, чому оркестратору слід не втручатися у інструменти, як надати підагентам контракт повернення, який можна перевірити, та як навмисно завершити виконання, а не за бажанням моделі.
Використовуйте звичайні робочі процеси за замовчуванням; автономію потрібно здобувати
Що йде не так. Візьмемо процес повернення грошей із чотирма можливими результатами, кожен з яких вже регулюється письмовим правилом. Команда обгортає цей процес автономним циклом, шістьма інструментами та послідовністю цілей. Раптово тикет від вівторка починає працювати іншим шляхом, ніж тикет від понеділка, і кожен запуск коштує від десяти до п’ятдесяти разів більше, ніж за старим кодом. Точність залишається незмінною, оскільки жодна частина процесу не є настільки неоднозначною, щоб потребувала інтелектуальних алгоритмів.
Рішення. Спочатку намалюйте діаграму потоку. Усе, що можна спланувати заздалегідь, має потрапити до звичайного потоку керування, тож простий код оброблятиме кожну з можливих гілок. Викликайте модель лише на тому етапі, де вхідні дані справді піддаються тлумаченню. Агент має отримати право керування гілкою лише тоді, коли буде доведено, що її неможливо описати словами.
REFUND_LIMIT = 50.00
def llm_step(prompt: str) -> str:
"""Called only where the written rules run out."""
return model.complete(prompt).strip().lower()
def handle_refund(ticket):
if ticket.days_since_purchase > 30:
return deny(ticket, reason="outside window")
if ticket.amount <= REFUND_LIMIT:
return approve(ticket) # a rule, not a judgement call
intent = llm_step(
f"Classify as fraud, defect, or remorse:\n{ticket.body}"
)
if intent == "fraud":
return escalate(ticket, queue="risk")
if intent == "defect":
return approve(ticket)
return route_to_human(ticket)
Проблема. Ви можете потрапити у лабіринт вкладених умов, які тихо кодують правила оцінки, яких модель насправді не може застосувати. Коли ви пишете п’яту умову для вільного тексту, саме ця неоднозначність і є причиною існування моделі.
Пропустіть оркестратор, якщо проблема вже відома
Що йде не так. Чотириетапний процес створення звіту опиняється в циклі оркестратора з обмеженням у п’ятнадцять проходжень. Кожне проходження витрачає час на планування, щоб вирішити, що робити далі, і кожен такий крок приходить до вже відомої вам відповіді: отримати дані, витягнути інформацію, підсумувати її, сформатувати. Це означає п’ятнадцять кроків обробки по кілька тисяч токенів кожен, щоб знову знайти список, який можна було б ввести за п’ять рядків.
Рішення. Використовуйте швидке правило перед тим, як щось кодувати. Якщо повний список підзавдань відомий ще до початку виконання, виконуйте його за допомогою простого циклу з фіксованим розподілом та зберігайте токени для інших цілей. Оркестрація, заснована на моделях, підходить там, де сам розбір невідомий або має еволюціонувати на основі результатів попередніх кроків.
# Before: up to 15 planning calls to rediscover a fixed list
def run_orchestrated(doc):
state = {"doc": doc}
for _ in range(15):
decision = orchestrator.plan(state) # 1 LLM call per pass
if decision.action == "finalize":
break
state = WORKERS[decision.worker](state)
return state
# After: 0 planning calls, same four workers, same result
PIPELINE = [fetch, extract, summarize, format_report]
def run_static(doc):
state = {"doc": doc}
for step in PIPELINE: # 0 LLM calls here
state = step(state)
return state
Проблема. У цієї поради є інший бік. Система, яка щомісяця створює новий спеціальний випадок, або де результат другого кроку справді визначає, чи буде виконаний третій крок, здійснює справжнє планування. Видалення такого планувальника лише переміщує процес прийняття рішень у якийсь конфігураційний файл, за яким ніхто не буде доглядати.
Розділяйте агентів за контекстом, а не за посадою
Що йде не так. Версія багатоагентної системи у вигляді організаційної схеми виглядає охайною: планувальник, реалізатор, тестувальник, рецензент — робота над кожною функцією по черзі. На практиці кожна передача завдань супроводжується припущеннями, які ніхто не записав. Тестувальник перевіряє поведінку, якої реалізатор зовсім не мав на увазі, а рецензент заперечує проти вибору, зробленого планувальником навмисно. Ви платите у чотири рази більше через таку „деформацію“ в процесі роботи, тоді як один агент, який виконуватиме всі чотири завдання, впорався б краще.
Рішення. Розрізайте відповідно до ліній контексту. Перш ніж додавати другого агента, поставте дизайну одне запитання: чи може це підзавдання бути виконане в межах контекстного вікна, яке не має нічого спільного з вікном сусіднього завдання? Якщо відповідь «ні», обидва завдання належать до одного агента, і саме тому агент, який створює функцію, також повинен писати тести для неї.
from dataclasses import dataclass
@dataclass
class Subtask:
name: str
needs: set[str] # facts this subtask must read
produces: set[str] # facts this subtask decides
def should_split(a: Subtask, b: Subtask) -> bool:
"""Split only when neither side needs what the other decides."""
shared = (a.needs & b.produces) | (b.needs & a.produces)
return not shared
implement = Subtask("implement", {"spec"}, {"api_shape", "error_semantics"})
test = Subtask("test", {"spec", "api_shape", "error_semantics"}, {"cases"})
assert not should_split(implement, test) # one agent writes both
Проблема. Справді ізольовані підзавдання трапляються рідше, ніж може здатися з організаційної схеми, тому тест часто зазначає «один агент», і це правильна відповідь, а не брак уяви. Розділення завдань є корисним для роботи з широким спектром дій, наприклад, коли п’ятеро агентів читають п’ять незалежних джерел, оскільки рішення одного не впливає на потреби іншого.
Нехай оркестратор формує рішення та ніколи не викликає інструменти
Що йде не так. Оркестратор отримує інструмент пошуку для надзвичайних ситуацій. Незабаром він перестає розподіляти завдання та починає самостійно виконувати підзавдання. Необроблені результати роботи інструментів накопичуються у його вікні, тож його планування погіршується саме тоді, коли тривалість виконання зростає, а планування стає найважливішим. Також порушується принцип простежуваності, оскільки незрозуміло, який агент створив які результати.
Рішення. Жодні плани не повинні також завантажуватися, тому необхідно надати оркестратору жодних інструментів. Він формує лише одне компактне, типоване рішення: делегувати, перепланувати чи завершити, разом із працівником, якого потрібно використати, та текстом завдання. Околишній цикл читає це рішення, запускає вказаного працівника та зберігає результат у спільному стані, готовому до наступного рішення.
from typing import Literal
from pydantic import BaseModel
class OrchestratorDecision(BaseModel):
next_action: Literal["delegate", "replan", "finalize"]
target_worker: str | None = None
task_description: str | None = None
reasoning: str
def orchestrator_step(state):
d = decide(state) # model has zero tools attached
if d.next_action == "finalize":
return finalize(state, d.reasoning)
if d.next_action == "replan":
return state.reset_plan(d.reasoning)
worker = WORKERS[d.target_worker] # workers own every tool
result = worker.run(d.task_description)
return state.record(d.target_worker, result)
Проблема. Без інструментів оркестратор бачить лише те, що надсилають працівники. Якщо працівник надсилає лише короткий огляд, це залишає процес планування без інформації. Натомість слід надати можливість лише читання спільного стану замість прямого доступу до інструментів, а також забезпечити, щоб дані, які надсилають працівники, були достатньо структурованими для планування.
Змусіть кожного підагента дотримуватися узгодженого формату повернення даних
Що йде не так. Уявіть дослідницького підагента, який надсилає 2 500-слівний щоденник усіх спроб, включаючи невдалий шлях. Якщо таких працівників сім, то до вікна батьківського агента потрапляє близько 20 000 токенів ще до початку обробки даних. Делегування мало б захистити це вікно, але насправді дослідження оплачується двічі, а батьківський агент виконує інструкції ще гірше саме тоді, коли це найбільш важливо.
Рішення. Уявіть собі повернення даних як межу API, де кожен підагент має схему відповіді. Його заключне повідомлення — це JSON із іменованими полями: результати дослідження, джерела, незадані запитання та статус завершення. Перевірте його при отриманні та відхиліть все неправильно сформоване або занадто довге для повторної спроби, щоб воно ніколи не потрапило до батьківського елемента.
from pydantic import BaseModel, Field, ValidationError
class SubagentResult(BaseModel):
findings: list[str] = Field(max_length=5) # hard cap, one sentence each
sources: list[str]
open_questions: list[str] = []
completion_status: str # complete | partial | blocked
def parse_or_retry(worker, task, attempts=2):
for _ in range(attempts):
raw = worker.run(task, response_format=SubagentResult)
try:
return SubagentResult.model_validate_json(raw)
except ValidationError as err:
task = f"{task}\n\nRejected: {err}\nReturn only JSON in the schema."
raise RuntimeError(f"{worker.name} returned no valid result")
Проблема. Схеми встановлюють лише мінімум, і нічого більше. Робот все одно може помістити абзац довжиною 400 слів у один рядок з результатами та надати ідентичний „надмірний“ формат, прикидаючись JSON. Необхідно обмежити як кількість елементів, так і довжину кожного поля, а також вказати у тексті відмови назву поля, яке переповнилося, щоб спроба повторного виконання знала, що потрібно виправити.
Паралелізувати читання та спрямовувати записи через одного агента
Що йде не так. Два агента працюють над однією функцією одночасно, і кожен повідомляє про успіх. Один намалював фон у стилі Super Mario, а інший — спрайт Flappy Bird, причому обидва пройшли тести, написані для їхніх власних уявлень про дизайн. Об’єднання не може вирішити вибір, про існування якого не знали жодні з агентів, тому один або обидва результати відкидаються.
Рішення. Читання може відбуватися паралельно; написання — ні. Обмежте кожного працівника списком інструментів лише для читання, що дозволить етапу збору даних розширюватися настільки, наскільки це необхідно, і ніколи не надавайте працівнику жодних інструментів для запису. Потім результати обробляються в одному безумовному виклику, який отримує результати всіх працівників разом.
import asyncio
READ_TOOLS = ["search_repo", "read_file", "fetch_docs"]
async def gather_context(subtasks):
workers = [Agent(name=t.name, tools=READ_TOOLS) for t in subtasks]
return await asyncio.gather(
*(w.run(t.prompt) for w, t in zip(workers, subtasks))
)
async def build_feature(spec, subtasks):
findings = await gather_context(subtasks) # wide, parallel, read-only
writer = Agent(name="writer", tools=["write_file", "apply_patch"])
return await writer.run(spec, context=findings) # one writer, one pass
Проблема. Цей єдиний записувач є вузьким місцем та мусить одночасно обробляти результати всіх працівників, тож ви зіткнетеся з обмеженням кількості токенів задовго до досягнення логічного ліміту. Якщо результат справді розділяється чітко, як у випадку трьох файлів, які не мають спільного інтерфейсу, призначте кожному файлу власного записувача та дотримуйтесь однакових правил у кожному з них.
Запишіть умови завершення запису
Що йде не так. Запуск завершується, помилок немає, але робота залишається незавершеною. Аналіз 1 600 слідів багатьох агентів у семи фреймворках показав, що 12,4% невдач спричинені тим, що агенти не знають умов свого зупинення, ще 6,2% — передчасним завершенням. Тож майже кожна п’ята невдача проявляється у чіткому завершенні з напівзавершеним результатом.
Рішення. Перетворіть кінцеву мету на щось конкретне, а не на просте відчуття. Для кожного завдання створіть чіткий перелік кроків для його виконання, а також встановіть позначку про схвалення, яка буде оцінюватися за первісною метою, на єдиному шляху до кінцевого стану. Згідно з тим самим дослідженням, використання механізму перевірки підвищило ефективність ChatDev на 9,4%, а контроль за виконанням високорівневих цілей — на 15,6%.
CHECKLIST = [
"every requested section exists",
"each claim cites a retrieved source",
"open questions are listed, or explicitly none",
]
def verify_complete(objective, checklist, output) -> bool:
for item in checklist:
verdict = judge(f"Objective: {objective}\nCheck: {item}\n\n{output}")
if not verdict.passed:
log.info("termination blocked by: %s", item)
return False
return judge(f"Does this satisfy the objective?\n{objective}\n\n{output}").passed
def finish(state):
if verify_complete(state.objective, CHECKLIST, state.draft):
return state.done()
return state.keep_working(reason="checklist not satisfied")
Проблема. Якщо дозволити моделі, яка створює контент, самій оцінювати свій чек-лист, вона просто схвалить свою роботу. Краще використовувати механічні критерії, де це можливо — наприклад, наявність файлу, заповнення поля чи правильне формування посилання. Блокування, яке ніколи не знімається, також є видом збою, тому обмежте кількість спроб та видаєте помилку, коли вони закінчуються.
З чого почати
Якщо ви збираєтесь змінити лише одну річ, оберіть найдешевший варіант: описайте письмово, що означає завершення роботи. Чек-лист та крок перевірки перед досягненням кінцевого стану можна створити за один день, він підійде як для одного агента, так і для цілої групи, та усуне цілу категорію прихованих збоїв. Зробіть це завданням цього тижня.
Далі йде процес видалення. Перевірте свій оркестратор та протестуйте, чи можна звести його функції до фіксованого списку та функції агрегації. Якщо так, видаліть його – це зменшить витрати на токени та затримки, а результати залишаться стабільними. Оскільки його легко скасувати, це є безпечним першим кроком.
Відкладіть використання складніших інструментів, доки це не почне завдавати проблем. Формат повернення даних у вигляді тексту є ефективним при використанні п’яти та більше працівників, а список дозволених дій типу «тільки для читання» має значення, коли два агенти справді працюють одночасно. Винятком є оркестратор без додаткових інструментів: спочатку він майже безкоштовний, але пізніше його додавання стає складним.
Системи, які залишаться ефективними у 2026 році, не матимуть найбільшої кількості агентів. У них з’єднання між агентами є найменшими та найбільш чітко визначеними елементами конструкції. Компактніша структура, міцніші умови співпраці та простіші механізми управління.
Пов’язана література
- Агенти паралельного кодування з git Worktrees, диригент та Antigravity 2.0 — Як координуючий агент, git Worktrees для кожного агента та треки, засновані на завданнях, дозволяють створити десяток функцій SRE приблизно за п’ять годин, а також уроки щодо запитань HITL та правил ефективного об’єднання коду.
- Агентний підхід RAG з Agno Teams: переписування запитів, цикл критики та Qdrant — Координуюча команда переписує запити, повторює процес пошуку за допомогою критика та надає відповіді лише з схваленого контексту, використовуючи гібридний пошук типу dense, BM25 та ColBERT у Qdrant.