Урок 4 закінчився попередженням. Агент тепер шукає, пам’ятає і планує. Але кожна нова здатність зробила промт довшим. Подивіться на промт сьогодні. Він містить:
- правила білінгу;
- політику рефандів;
- поведінку export;
- API-ліміти;
- тон відповіді;
- правила ескалації.
Це 3 екрани тексту. Тепер приходить тікет T-3001: «Upgraded my plan yesterday, there is a duplicate charge on my card, and export gives an empty file». Тікет вимагає 2 експертизи одночасно:
- білінгову: подвійний чардж після апгрейда означає, що стара підписка ще не закрила свій інвойс;
- технічну: порожній export означає проблему з фільтром статусів або з чергою.
1 агент уміє обидві речі. Але кожен новий домен у промті робить агента трохи гіршим у всіх інших доменах. Застереження про рефанди з’являються у відповіді про export. Технічні деталі потрапляють у відповідь про гроші.
У цей момент продакт-менеджер пропонує: «Створимо команду агентів. Один агент для білінгу, один для техніки, і головний агент над ними. Як у людей». Архітектори часто реагують на цю фразу негативно. Але ідея не погана. Погане буває тільки виконання.
У цьому уроці ми зберемо оркестратора і спеціалістів. Потім ми зламаємо цю команду 4 способами. Найважливіше: ми виміряємо вартість команди і її реальну користь. Числа покажуть результат.
Терміни
Тема multi-agent має нечіткі поняття. Тому спочатку визначимо терміни.
- workflow - фіксована послідовність кроків. Workflow надійний і дешевий, але не адаптується.
- Агент - процес, який адаптується. Агент сам вирішує наступний крок на основі спостережень.
- Multi-agent система - не «рій розумів», а конкретна конструкція: 1 оркестратор, 2 або більше спеціалістів і чіткий контракт між ними.
Головна реальна цінність multi-agent Ця цінність не в тому, що 2 агенти розумніші за 1. Оркестратор і спеціаліст використовують ту саму модель. Система не отримала більше інтелекту. Система отримала ізоляцію контексту. Правила білінгу більше не губляться серед технічних правил. Промт кожного агента займає пів екрана. Кожен агент має тільки свій корпус. Інші обіцянки multi-agent зводяться до ізоляції, до прав доступу або до театру. Ви побачите це в числах.
Контракт рівня мінімальний. Далі кожен пункт покаже свою користь:
Спеціаліст: той самий цикл, вузький світ
Перша добра новина уроку: нова архітектура не потрібна. Спеціаліст - це агент з Уроків 1-2 з вужчим світом. search_kb з Уроку 3 отримує корпус як параметр:
TECH_KB- відомі статті з Уроку 3: kb-101 про порожній export, kb-102 про чергу, kb-105 про ліміти;BILLING_KB- kb-103 про інвойси з Уроку 3 і нові kb-201 і kb-202 про чарджі і рефанди.
Спеціаліст має 3 частини: промт, корпус, інструменти:
@dataclass
class Specialist:
name: str
prompt: str
corpus: dict[str, str]
tools: tuple[str, ...]
SPECIALISTS = {
"billing": Specialist("billing", BILLING_PROMPT, BILLING_KB,
("search_kb", "get_subscription", "get_invoices")),
"tech": Specialist("tech", TECH_PROMPT, TECH_KB,
("search_kb", "check_system", "read_logs")),
}
def can_use(agent: str, tool: str) -> bool:
# межі доступу з Уроку 1 стають архітектурою: least privilege по агентах
return agent in SPECIALISTS and tool in SPECIALISTS[agent].tools
Подивіться на can_use. В Уроці 1 межі доступу захищали дані від тікета. Тепер межі доступу визначають форму системи. Billing-агент фізично не має технічних інструментів. Tech-агент не бачить інвойсів. Це
least privilege
для агентів. Це друга реальна причина для multi-agent. Ми повернемося до неї пізніше:
Промт спеціаліста тепер займає пів екрана, і весь текст стосується задачі. Ось промт білінгового агента. Порівняйте його з 3 екранами генераліста:
BILLING_PROMPT = (
"You are the billing specialist. Scope: charges, refunds, invoices, proration, "
"subscription state. Work only from your own knowledge base and billing tools, "
"product behavior is not your scope. Quote amounts and invoice ids exactly as "
"the tools return them, never promise refund timelines shorter than the "
"policy, escalate charges older than 90 days to a human. " + REPORT_RULES
)
Звіт, а не діалог
Тепер питання, від якого залежить уся конструкція: що спеціаліст повертає оркестратору? Перша ідея - повернути діалог. Оркестратор прочитає міркування спеціаліста і перекаже їх далі. Не робіть так. У розділі Break ви побачите причину на реальному тікеті. Правильна відповідь - структурований звіт. Факти зі звіту потрапляють на спільну дошку:
@dataclass
class Fact:
key: str
value: str
source: str # хто знайшов і де: "billing:kb-201"
@dataclass
class Report:
specialist: str
facts: list[Fact]
recommendation: str
confidence: float
@dataclass
class Board:
facts: dict[str, Fact] = field(default_factory=dict)
def merge(self, report: Report) -> int:
new = [f for f in report.facts if f.key not in self.facts]
self.facts.update({f.key: f for f in new})
return len(new)
def pretty(self) -> str:
return "; ".join(f"{f.key}={f.value}" for f in self.facts.values())
Це той самий словник фактів з Уроку 2. Але тепер команда має спільний словник:
blackboard
. Кожен факт має
provenance
: хто знайшов факт і в якому документі. Також merge повертає кількість нових фактів. Це важлива деталь. Пізніше це число стане детектором прогресу для всієї команди:
Усередині спеціаліст має відомий цикл choose-observe-extract. Але цикл працює тільки зі своїми інструментами і своїм корпусом. Назовні спеціаліст повертає звіт, а не діалог:
def run_specialist(spec: Specialist, task: str, meter: CostMeter) -> Report:
# той самий цикл, що в Уроках 1-2, тільки вузький. Назовні - звіт, не діалог.
facts: list[Fact] = []
history = ""
for tool in spec.tools:
obs = observe(spec.name, tool, task)
meter.add(spec.prompt + task + history, obs) # choose
meter.add(spec.prompt + obs, "facts") # extract
history += "\n" + obs
if tool == "search_kb":
facts = [Fact(cid, spec.corpus[cid], f"{spec.name}:{cid}")
for cid in search_kb(spec.corpus, task)]
if not facts:
return Report(spec.name, [], "out of my scope, return to orchestrator", 0.2)
meter.add(spec.prompt + task + history, str(facts)) # зібрати звіт
return Report(spec.name, facts, f"apply {facts[0].key}", 0.9)
Задача поза корпусом не є винятком або галюцинацією. Це правильний звіт з порожніми фактами і низьким confidence. Запам’ятайте цей рядок. Він скоро буде корисним.
delegate: 1 новий інструмент, 0 нової архітектури
Друга добра новина. Оркестратор - не нова сутність. Це планувальник з Уроку 2 з новим інструментом delegate(name, task) поруч з lookup-інструментами. Вхід - задача, вихід - спостереження. Ви добре знаєте цей контракт інструмента. Усередині інструмента є межі. Межі знову є в коді, а не в промті:
@dataclass
class Budget:
max_delegations: int = 4
max_no_progress: int = 2
delegations: int = 0
no_progress: int = 0
def delegate(name: str, task: str, board: Board, budget: Budget, meter: CostMeter) -> str:
# інструмент планувальника з Уроку 2; межі знову в коді, а не в промті
if budget.delegations >= budget.max_delegations:
return "denied by policy: delegation budget exhausted, escalate"
if budget.no_progress >= budget.max_no_progress:
return "denied by policy: two delegations without new facts, escalate"
budget.delegations += 1
# незалежність: спеціаліст бачить задачу, а не чужі висновки з дошки
report = run_specialist(SPECIALISTS[name], task, meter)
new = board.merge(report)
budget.no_progress = 0 if new else budget.no_progress + 1
return (f"{report.specialist}: +{new} facts, confidence "
f"{report.confidence}, {report.recommendation}")
Тут ви бачите відомі механізми:
- бюджет кроків з Уроку 2 став бюджетом делегацій;
- лічильник прогресу рахує нові факти на дошці, а не виклики;
- відмова - звичайне спостереження.
Після відмови оркестратор повинен вибрати інший шлях, найчастіше ескалацію. Цикл оркестратора:
def route_fanout(ticket: str) -> list[str]:
# v1: "хай глянуть обидва" - і за кожен погляд платиш (тріаж - у triage_llm)
return list(SPECIALISTS)
def run_multi(ticket_id: str, router=route_fanout):
# оркестратор - сам планувальник з Уроку 2: choose і extract щокроку
ticket = TICKETS[ticket_id]
board, budget, meter = Board(), Budget(), CostMeter()
history = ""
meter.add(ORCH_PROMPT + ticket, "plan")
contribs = []
for name in router(ticket):
before = len(board.facts)
ctx = ORCH_PROMPT + ticket + history + board.pretty()
meter.add(ctx, f"delegate({name})") # choose
obs = delegate(name, ticket, board, budget, meter)
meter.add(ORCH_PROMPT + obs, "facts") # extract звіту
history += "\n" + obs
contribs.append((name, len(board.facts) - before))
meter.add(ORCH_PROMPT + ticket + history + board.pretty(), "final decision")
return board, meter, contribs
Роутер v1 - це fan-out : «нехай обидва спеціалісти подивляться». Перша чернетка будь-якої multi-agent системи працює так. Зараз ви побачите вартість цієї чернетки.
Виміряйте, а потім довіряйте команді
Multi-agent architecture коштує в 8 разів дорожче.
скарга з office hoursКоманда готова, тікети надходять, демо виглядає розумно. Оркестратор делегує, спеціалісти звітують, дошка заповнюється. Більшість команд зупиняється саме тут, бо система працює. Ми не зупиняємося. Кожен LLM-виклик системи має лічильник:
@dataclass
class CostMeter:
llm_calls: int = 0
tokens: int = 0
def add(self, prompt: str, output: str) -> None:
# 1 токен ~ 4 символи: груба оцінка, але для порівняння архітектур чесна
self.llm_calls += 1
self.tokens += (len(prompt) + len(output)) // 4
Поруч з multi є run_single. Це агент з Уроку 4: 1 промт, спільний корпус, той самий лічильник. Це важливо. Порівнюйте конфігурації тільки на 1 золотому наборі і тільки 1 методом вимірювання. GOLD містить 3 тікети і факти, які потрібні для повного рішення. Успіх означає, що агент зібрав усі потрібні факти:
def evaluate(run_fn) -> tuple[float, float, float]:
ok = calls = tokens = 0
for tid, want in GOLD.items():
board, meter, _ = run_fn(tid)
ok += want <= set(board.facts)
calls += meter.llm_calls
tokens += meter.tokens
return ok / len(GOLD), calls / len(GOLD), tokens / len(GOLD)
Запускаємо обидві конфігурації:
=== Success і вартість: single проти multi на одному GOLD ===
конфігурація success llm/тікет токени/тікет x single
single 1.00 7.0 517 1.0x
multi fan-out 1.00 19.3 4086 7.9x
Прочитайте цю таблицю 2 рази. Точність однакова. Вартість у 7.9 раза більша. Людина з епіграфа не перебільшила, вона тільки округлила.
Множник має 3 джерела:
- кожен спеціаліст виконує свій повний цикл choose-extract;
- оркестратор платить за план, за кожен delegate і за кінцеве рішення;
- кожен рівень додає свій промт у кожен виклик.
Це agent theater , патерн F05 з нашого каталогу. Команда агентів створює відчуття інтелекту. Але число на золотому наборі не змінилося навіть на 1 відсоток.
Демо також показує детальніший аналіз: хто реально приніс факти:
=== Чи додав другий спеціаліст хоч один факт (fan-out) ===
T-3001 billing +1 tech +1
T-3002 billing +1 tech +0
T-3003 billing +0 tech +1
Тікет T-3001 має 2 домени, і обидва спеціалісти корисні. Саме для таких тікетів ми будували команду. Але тікети T-3002 і T-3003 мають 1 домен. На цих тікетах другий спеціаліст виконує повний платний цикл і додає 0 фактів.
Це луна-метрика: частка тікетів, на яких другий спеціаліст реально додав факти. У типовій підтримці більшість тікетів має 1 домен. Якщо луна-метрика низька, fan-out платить за театр на кожному другому тікеті. Повторні спроби збільшують цю вартість ще швидше (F07).
Перше реальне зменшення вартості - делегувати тільки потрібним спеціалістам. У повному коді це triage_llm. Оркестратор 1 викликом зі structured output ділить тікет на підзадачі. Потім оркестратор викликає тільки потрібних спеціалістів. Точність та сама, множник зменшується.
Ламаємо: 3 способи зламати команду
Вартість була першою проблемою. Тепер зламаємо саму механіку. Ми зробимо це вручну, з журналами.
Злам 1: ping-pong делегація. Тікет T-3005: «Our legal team needs a GDPR data processing addendum signed». Цей тікет не стосується нашої команди. Це не білінг і не технічний домен. Подивіться, що робить наївний оркестратор без бюджетів:
=== Ping-pong делегація: тікет поза обома доменами ===
delegate(billing) -> billing: +0 facts, confidence 0.2, out of my scope, return to orchestrator
delegate(tech) -> tech: +0 facts, confidence 0.2, out of my scope, return to orchestrator
delegate(billing) -> denied by policy: two delegations without new facts, escalate
delegate(tech) -> denied by policy: two delegations without new facts, escalate
витрачено даремно: 12 LLM-викликів, ~2437 токенів
Billing відповідає «не мій домен». Tech відповідає «не мій домен». Без запобіжника оркестратор делегує знову і сподівається на інший результат.
Ви знаєте цю проблему. Це нескінченний цикл з Уроку 1, але на рівень вище. Там модель повторювала виклик інструмента. Тут оркестратор повторює виклик цілого агента. Рішення те саме, і воно вже є в коді delegate: бюджет делегацій і прогрес за фактами. Після 2 делегацій без нових фактів на дошці третя делегація не запускає спеціаліста. Третя делегація повертає «escalate». Агенти використали 12 викликів, але цикл зупинився. Його зупинив код, а не промт.
Злам 2: зіпсований телефон. Повернімося до T-3001. Уявіть систему без звіту і без дошки. Оркестратор переказує результат своїми словами. Так працюють наївні multi-agent системи, де агенти передають сирий текст. Повний код містить навмисний антиприклад:
def relay_naive(report: Report) -> str:
# АНТИПРИКЛАД: переказ без дошки - рекомендація доїжджає, факти ні
return f"{report.specialist} says: {report.recommendation}"
=== Зіпсутий телефон: переказ проти дошки ===
наївний переказ: billing says: apply kb-201
дошка фактів з provenance:
kb-201 <- billing:kb-201: Duplicate charge after a plan upgrade: the old subscription stays active until its invoice closes. Refund the smaller charge and cancel the stale subscription, otherwise the customer is billed twice again next cycle.
Подивіться на різницю. Переказ передав рекомендацію: «застосуй kb-201». Але факт містить критичну другу частину. Рефанду недостатньо. Агент також повинен скасувати стару підписку. Інакше в наступному циклі оплати система знову спише гроші 2 рази.
Оркестратор з переказом зробить рефанд і закриє тікет. Через 1 місяць оркестратор отримає той самий тікет, і клієнт буде ще більш роздратований. Факт загубився на межі між агентами. Модель не винна. Ви самі створили зіпсований телефон.
Злам 3: луна-камера. Ідея: для надійності покажемо другому спеціалісту висновок першого, щоб другий перевірив. На папері це розумно. На практиці це дуже погано. Якщо контекст моделі містить впевнений чужий висновок, модель схильна погодитися. Фраза «billing вважає, що це kb-201» майже гарантує відповідь «згоден, kb-201». 2 агенти, 2 платні виклики, 1 думка і 0 перевірки.
Тому в delegate спеціаліст отримує тільки задачу. Рядок «Never forward one specialist’s conclusions to another» в ORCH_PROMPT - тільки додатковий захист. Основна межа вже є в коді.
Ще 1 злам не вимагає сценарію: спеціаліст сам пробує використати чужі інструменти. У наївній системі це залежить від промту. У нашій системі це залежить від 1 if:
=== Межі доступу: спеціаліст проситься в чужі інструменти ===
billing -> read_logs: denied by policy: read_logs is outside the billing scope
tech -> get_invoices: denied by policy: get_invoices is outside the tech scope
Коли multi-agent справді виправданий
Після всіх зламів подивимося на матрицю рішення. Є 4 причини, які виправдовують вартість, і 1 причина, яка не виправдовує:
Розглянемо кожну причину.
- Ізоляція контексту. Корпус і правила спеціаліста не вміщаються в спільний промт без втрати якості. Це наш сьогоднішній випадок.
- Різні права доступу. Агенти вимагають різних привілеїв. В Уроці 1 це був guardrail, а тут це архітектура. Якщо атакер контролює 1 агента, він не отримує інструменти іншого агента.
- Паралелізм. Незалежні підзадачі можуть працювати одночасно. Це перевага в латентності. Не подавайте цю перевагу як перевагу в якості.
- Незалежні перспективи. Задача вимагає перевірки, а не луни. Це найскладніша причина. Урок 7 повністю присвячений цій темі.
Усі інші причини - театр. Приклади: «сучасний стек», «так робить індустрія», «у демо виглядало розумно».
Той самий оркестратор в інших доменах
Скелет «оркестратор + спеціалісти + дошка + бюджети» повністю працює в інших доменах. Перевіримо це на 3 відомих агентах.
On-call агент. Диспетчер інциденту і вузькі runbook-агенти: 1 для баз даних, 1 для мережі, 1 для деплоїв. Головна перевага тут - права, а не контекст. Діагностика тільки на читання є в одних агентів. restart_service має окремий агент, і цей агент вимагає approval людини з Уроку 2. Матриця «агент × інструмент», яку ви сьогодні написали, у продакшні стає політикою безпеки інциденту.
Аналітик. SQL-агент пише запит. Незалежний верифікатор перевіряє результат. Ключове слово - «незалежний». Верифікатор бачить питання і дані, але не бачить SQL і міркування першого агента. Якщо ви покажете верифікатору запит, верифікатор «перевірить» запит так само, як спеціаліст у луна-камері: погодиться.
Кодовий агент. Планувальник, виконавець, рев’юер. Рев’юер бачить тільки diff і прогін тестів, а не пояснення виконавця. Це незалежна перспектива в найчистішій формі. Ця трійка з’єднує 2 наступні уроки. Зараз ви зрозумієте чому.
Каталог: що закрито, що відкрито
- Великий промт генераліста - закрито через ізоляцію контексту. Спеціаліст має пів екрана правил, свій корпус і 3 інструменти.
- Цикли делегацій - закрито через бюджет делегацій і прогрес за фактами в
delegate. Це той самий нескінченний цикл з Уроку 1 і те саме рішення, але на рівень вище. - Зіпсований телефон - закрито через структурований звіт і дошку з provenance. Дошка передає факт точно і з підписом автора.
- Луна-камера - закрито через незалежні запити. Спеціаліст бачить задачу, а не чужі висновки.
- Вихід за межі scope - закрито через least privilege у коді.
can_useповертає відмову як спостереження. Жоден промт не змінює цю відмову. - Agent theater - код не закриває цю проблему. Єдиний захист - вимірювати single проти multi на 1 золотому наборі. Сьогодні числа показали ту саму точність і 7.9x токенів. Якщо ви не маєте своєї версії цієї таблиці, ваша система за замовчуванням є театром.
- Незгода спеціалістів - відкрито. Billing може сказати одне, а tech - інше. Дошка показує різницю, але система не має арбітра.
Еволюція вимог
Вимоги v1
- Тріаж і делегування підзадач
- Структурований звіт спеціаліста
- 1 кінцеве рішення
- Спільний бюджет на тікет
Вимоги v2 (після Break)
- Бюджет делегацій і прогрес за фактами в коді delegate. Відмова - це спостереження
- Дошка фактів з provenance замість переказу своїми словами
- Незалежні запити: чужі висновки не потрапляють у контекст спеціаліста
- Least privilege у коді: матриця агент x інструмент, denied by policy
- Тріаж замість fan-out: делегуйте тільки потрібним спеціалістам
- Порівняння з single на золотому наборі до релізу: multi повинен довести перевагу числом, а не відчуттям
Завантажте повний код
1 файл містить:
- корпуси за доменами;
- спеціалістів з least privilege;
- дошку фактів;
- delegate з бюджетами;
- single проти multi з лічильником вартості;
- усі сценарії Break.
Код працює офлайн без LLM-викликів. Ключ API потрібен тільки для LLM-тріажу тікета на підзадачі в demo.
Завантажити lesson-5-multi-agent.py
pip install anthropic
export ANTHROPIC_API_KEY=sk-...
python lesson-5-multi-agent.py
Команда працює, її вартість виміряна, межі знову є в коді. Але подивіться ще раз на останній пункт каталогу. Він відкритий навмисно. Якщо 2 агенти мають різні оцінки, сьогодні останнє слово має людина. У підтримці «правильно» означає те, що підтвердив клієнт або старший співробітник зміни. Це працює, але повільно, дорого і без масштабування.
Тепер найцікавіше. Є домен, у якому задача сама має арбітра. Цей арбітр не втомлюється і відповідає за секунди. Це код. Тести або зелені, або червоні. Гарні слова моделі не змінять результат тестів.
Наступний урок змінює домен і будує кодового агента. Репозиторій - це стан, а цикл «зміна, тести» працює в пісочниці. Там ви побачите найгірше відкриття курсу. Агент з чесним арбітром дуже швидко вчиться обманювати арбітра: агент змінює тести замість коду.



