Чому агенти, що працюють довго, потребують машини станів: ролі лідера, працівника та перевірячого.
Чотири причини, чому цикл одного агента зазнає невдачі під час виконання тривалих завдань, та як команда з лідера, працівника та перевірячого, керована детерміністичним двигуном, їх виправляє, а також коли його можна пропустити.
Джерела: MiniMax Agent Team Best Practice: An AI Agent Is a While Loop. A Reliable One Is a State Machine.
Перехід від чат-бота до агента вимагає циклу while, а не наукового прориву. Це правда, і саме тут зупиняються більшість пояснень та починаються проблеми у практичній реалізації. Більш корисним є запитання про те, чому цей цикл тихо зламується під час виконання завдань, які тривають довше десяти хвилин, та яка структура може його відновити. MiniMax опублікував два детальні матеріали саме на цю тему: навчальний посібник Mini-Agent з відкритим кодом, який описує базовий цикл, та технічний звіт їхньої команди Agent Team, у якому розглядається, що зламалося при масштабуванні, та яка система виконання була створена у відповідь. Обидва матеріали є основою для подальших досліджень, і ця схема працює з будь-якою технологічною стеком.
Один цикл — від початку до кінця
Якщо спростити агента, залишиться чотири компоненти: модель, список повідомлень у реальному часі, реєстр інструментів та цикл їх обробки. Принцип роботи Mini-Agent полягає у наступному: спочатку перевіряється, чи не наблизився обсяг історії до максимального ліміту, і якщо так — він скорочується. Потім до моделі надсилаються повідомлення та визначення інструментів. Якщо модель не просить жодних інструментів, цикл завершується. Інакше виконуються запитані інструменти, їхні результати додаються до повідомлень, і процес повторюється. Ось повна версія для будь-якого кінцевого пункту, сумісного з OpenAI, з двома реальними інструментами та функцією підсумовування:
# naive_loop.py -- a single-agent tool loop,
# Mini-Agent style: summarize if needed ->
# llm.generate(messages, tools) -> execute
# tool_calls -> append results -> repeat.
import json
import subprocess
from openai import OpenAI
client = OpenAI()
MODEL = "gpt-4o-mini"
MAX_HISTORY = 40 # crude context budget
def read_file(path: str) -> str:
with open(path) as f:
return f.read()[:4000]
def run_cmd(cmd: str) -> str:
p = subprocess.run(
cmd, shell=True, timeout=30,
capture_output=True, text=True)
out = p.stdout + p.stderr
return f"exit={p.returncode}\n{out[-2000:]}"
TOOLS = {"read_file": read_file,
"run_cmd": run_cmd}
def schema(name, desc, arg):
return {
"type": "function",
"function": {
"name": name,
"description": desc,
"parameters": {
"type": "object",
"properties": {
arg: {"type": "string"},
},
"required": [arg],
},
},
}
SCHEMAS = [
schema("read_file",
"Read a local text file.", "path"),
schema("run_cmd",
"Run a shell command.", "cmd"),
]
def summarize(msgs):
# Mini-Agent compresses old turns with an
# LLM summary call when the context nears
# its limit; hard truncation is the v0.
if len(msgs) <= MAX_HISTORY:
return msgs
return [msgs[0]] + msgs[-MAX_HISTORY:]
def run(task: str, max_steps: int = 20):
msgs = [{"role": "user", "content": task}]
for _ in range(max_steps):
msgs = summarize(msgs)
r = client.chat.completions.create(
model=MODEL,
messages=msgs,
tools=SCHEMAS,
)
msg = r.choices[0].message
msgs.append(msg)
if not msg.tool_calls:
return msg.content # model says done
for call in msg.tool_calls:
fn = TOOLS[call.function.name]
args = json.loads(
call.function.arguments)
try:
out = fn(**args)
except Exception as e:
out = f"tool error: {e}"
msgs.append({
"role": "tool",
"tool_call_id": call.id,
"content": out,
})
return "hit max_steps -- gave up"
if __name__ == "__main__":
print(run("Count the .py files here, then "
"summarize naive_loop.py."))
У цьому циклі є два вихідні моменти, і обидва мають значення. Успіх визначається самим моделлю через відсутність подальших викликів інструментів. Безпека забезпечується простим лічильником – обмеженням кількості кроків. Саме ця асиметрія, коли лише модель вирішує, коли робота завершена, є тим фактором, який першим дає збій.
Коли один цикл не витримує довгих завдань
Якщо запустити той самий цикл на двогодинному завданні замість двомінутного, починаються проблеми. Команда MiniMax описала ті проблеми, з якими вони постійно стикалися. Жодна з них не є багом. Це властивості окремого контексту, тому більший таймфрейм чи кращий запит не вирішують жодної з цих проблем.
Спочатку з’являється тривога через контекст: агент зупиняється, коли ніхто його про це не просив. Після виконання семи підзавдань він закінчує три з них та повідомляє, що виконав завдання 1, 2 та 3, питаючи, чи продовжувати. Моделі мають нечітке уявлення про те, коли може закінчитися тривале завдання, а пряме навчання цього уявлення є надзвичайно дорогим як з фінансової, так і з алгоритмічної точки зору. Звичайний цикл передає рішення про зупинку найменш надійній його частині.
Далі відбувається деградація: агент стає все менш ефективним у міру збільшення обсягу контексту. На п’ятій хвилині він працює чітко, а на шістдесят п’ятій — поводиться як розсіяний стажер. Одна помилка, наприклад, коли дослідницьке завдання перетворюється на маркетинговий текст, псує все, що відбувається далі, оскільки кожне наступне покоління формується на основі цієї «забрудненої» історії.
По-третє, ніхто не перевіряє роботу, оскільки суддя водночас є учасником. Одинокий агент, який створює кінцевий результат, оцінює сам себе саме в тому контексті, де виникли його помилки. Це не нечесність — це перевірка сцени, яку сам модель щойно побудувала.
По-четверте, це мовчання. Інтерфейси у стилі чату змушують користувачів очікувати підтвердження протягом кількох секунд, навіть для завдання тривалістю годину, але цикл, прихований у виклику моделі, не може повідомити, що він отримав запит та повідомить про результат. Згідно з даними MiniMax, запитання „Чому мій агент не відповідає?“ отримало більше відгуків від користувачів, ніж будь-що інше.
Рішення: три ролі та детерміністичний двигун
MiniMax відповідає з урахуванням структури. Три типи ролей координуються звичайним детерміністичним кодом, а не розумним запитом, і цей код називається Team Engine. Один цикл життя завдання — це одна Сесія, причому двигун проходить кожну Сесію через три стани: стан створення, стан перевірки та стан завершення, знову активуючи виконавця щоразу, коли перевірка провалюється.
- Лідер розбиває запит користувача на структурований план, вибирає, які частини виконуються одночасно, відповідає користувачеві протягом кількох секунд, поки решта виконується поза його увагою, а потім об’єднує результати.
- Робітники беруть на себе підзавдання, кожне з яких виконується у окремому контексті з власними інструментами, пам’яттю та необхідним форматом вихідних даних. Це розділення має значення, оскільки кожен робітник бачить лише одне підзавдання, а не кілька годин накопичених даних.
Ядро двигуна достатньо мале, щоб його можна було записати. Ця версія не потребує API-ключа, оскільки штучні ролі замінюють виклики LLM та можуть змінюватися без впливу на машину станів:
# team_engine.py -- the loop promoted to a
# state machine. One task lifecycle is one
# Session; deterministic code owns every
# transition: producing -> verifying -> done.
from dataclasses import dataclass, field
PRODUCING = "producing"
VERIFYING = "verifying"
DONE = "done"
FAILED = "failed"
@dataclass
class Session:
goal: str
state: str = PRODUCING
attempts: int = 0
artifact: str = ""
reviews: list = field(default_factory=list)
def run_session(s, worker, verifier,
max_retries: int = 3):
while s.state not in (DONE, FAILED):
if s.state == PRODUCING:
s.attempts += 1
s.artifact = worker(
s.goal, s.reviews)
s.state = VERIFYING
elif s.state == VERIFYING:
ok, report = verifier(
s.goal, s.artifact)
s.reviews.append(report)
if ok:
s.state = DONE
elif s.attempts >= max_retries:
s.state = FAILED
else:
# Reject: wake the worker with
# the review log in context.
s.state = PRODUCING
return s
def run_team(goals, worker, verifier):
# Leader's job: split the goal, run the
# sessions (a thread pool in real use),
# then merge N artifacts into 1 result.
done, failed = [], []
for g in goals:
s = run_session(Session(g),
worker, verifier)
if s.state == DONE:
done.append(s.artifact)
else:
failed.append(s.goal)
return done, failed
if __name__ == "__main__":
# Stub roles so the engine runs anywhere;
# swap in LLM calls for the real thing.
def worker(goal, reviews):
v = len(reviews) + 1
return f"draft v{v}: {goal}"
def verifier(goal, artifact):
ok = "v2" in artifact
return ok, f"review {artifact!r}: {ok}"
done, failed = run_team(
["intro", "benchmarks", "faq"],
worker, verifier)
print("delivered:", done)
print("failed:", failed)
Якщо його запустити, перша версія кожної сесії буде відхилена, працівник завантажиться з журналом перевірки у своєму контексті, а друга версія пройде. Якщо порівняти це з простим циклом, видно кілька відмінностей.
- Зупинка: однократний цикл закінчується, коли модель припиняє виконувати запити до інструментів (fuzzy) або досягає ліміту кроків, тоді як двигун використовує чіткі стани «завершено» та «невдача» з лімітом спроб, визначеним у коді.
- Перевірка: у циклі той самий контекст, який створив роботу, її перевіряє, тоді як команда використовує адверсарійного перевірювача у окремому контексті, заснованому на командах.
- Паралелізм: цикл — це один послідовний контекст, тоді як команда запускає працівників у паралельних сесіях, причому лідер об’єднує багато з них в одну.
- Стабільність: цикл зберігає список повідомлень до моменту узагальнення, тоді як команда зберігає стан сесії, записи перевірок, результати роботи та пам’ять кожного агента.
Лідер також не обмежується одноразовими викликами, які створюють підзавдання та повертають текст. За принципом роботи MiniMax він отримує актуальний статус двигуна та може надсилати додаткові запити у сеанс, який перебуває у процесі обробки чи перевірки, тож існує як проактивне надсилання інформації, так і запити за потребою. Це також допомагає вирішити проблему мовчання: Лідер підтверджує отримання запиту протягом кількох секунд, а користувач потім отримує звіти на контрольних точках, таких як «розпочато», «заблоковано», «необхідне рішення» та «завершено».
Функціонування перевірювача
Проблема самооцінки зникає лише тоді, коли вердикт походить ззовні будь-якої моделі. Для коду MiniMax оточує його виконання спеціальним механізмом. Робота відбувається на гілках, у пісочницях; зміни представлені у вигляді різниць, тести можна повторювати, перегляди залишають записи, а невдачі можна переграти знову. Найважливіше твердження: умова зупинки має бути пов’язана з чимось детермінованим та спостережуваним ззовні, таким як тести, інструменти перевірки коду та процес збірки, а ніколи з відчуттям моделі про завершення роботи.
# grounded_verifier.py -- evidence over vibes.
# The verdict comes from exit codes and logs,
# never from the model's self-assessment.
import subprocess
import sys
PY = sys.executable
# pytest exit codes: 0 = all passed,
# 1 = failures, 5 = no tests collected
# (acceptable for a docs-only change).
PYTEST_OK = (0, 5)
CHECKS = [
("diff", ["git", "diff", "--stat"], (0,)),
("build", [PY, "-m", "compileall",
"-q", "."], (0,)),
("tests", [PY, "-m", "pytest", "-q",
"--maxfail", "1"], PYTEST_OK),
]
def run_check(name, cmd, ok_codes, cwd):
p = subprocess.run(
cmd, cwd=cwd,
capture_output=True, text=True)
tail = (p.stdout + p.stderr)[-2000:]
return {
"check": name,
"cmd": " ".join(cmd),
"exit_code": p.returncode,
"ok": p.returncode in ok_codes,
"output_tail": tail,
}
def verify(repo: str):
evidence = []
for name, cmd, ok_codes in CHECKS:
e = run_check(name, cmd, ok_codes, repo)
evidence.append(e)
if not e["ok"]:
# Reject with logs attached: the
# worker retries against real
# errors, not "please improve".
return False, evidence
return True, evidence
if __name__ == "__main__":
ok, ev = verify(".")
for e in ev:
print(f"{e['check']:>6} exit "
f"{e['exit_code']} ok={e['ok']}")
print("verdict:", "pass" if ok else "fail")
Список доказів водночас є записом перевірки, частиною повторно відтворюваного, прозорого сліду, який відрізняє те, що можна доставити, від того, що є лише ймовірним. MiniMax ще більше дробить процес перевірки. Тестувальник створює зовнішні докази шляхом виконання тестів, стиснення журналів помилок та додавання мінімального прикладу їх відтворення. Оглядач ставить інше запитання — не про те, чи пройшов тест, а про те, чи слід змінити код саме так, аналізуючи межі абстракції, нові залежності, розширені права доступу та конфіденційну інформацію, що потрапляє до журналів. Оглядачі можуть працювати паралельно з фахівцями в таких галузях, як загальні питання, безпека та конкретні домени.
Скільки коштує команда та коли її можна пропустити
Команда — це не безкоштовний паралелізм. Три види витрат з’являються вже тоді, коли агенти починають співпрацювати, і більший контекстний вікно не усуває жодної з них.
Витрати на передачу: однакову інформацію доводиться переорганізовувати між агентами, тож сторінки досліджень перетворюються на короткий опис завдання для автора, який потім використовується для перевірки формату. Рішенням є передача читабельних файлів із короткими резюме через відповідні шляхи, замість прямого передавання великих об’ємів контексту.
Витрати на спільне використання: кожен глобально спільний токен оплачується кожним працівником у кожному раунді. Ефективна структура забезпечує приватність пам’яті для кожного агента, дозволяє їм взаємодіяти через інтерупції через командний рядок та пропонує одну спільну дошку, яку агенти читають лише тоді, коли це необхідно.
Третіми є витрати на агрегацію. Збір десяти часткових чернеток — це просто. Об’єднання їх у один файл із відповідними фактами, посиланнями та тоном є найскладнішим завданням для керівника, і для цього потрібен крок об’єднання, а не додаткові працівники.
Консенсус також має свою ціну. Вільні дискусії між агентами можуть спожити в кілька разів більше токенів, ніж потрібно для ізольованої самокорекції, і все одно не покращити точність. Цінність полягає у структурі: обмежені сесії, чіткі артефакти та можливість відхилення результатів верифікатором.
Пропустіть командну роботу, якщо завдання невелике, локальне та дешеве у перевірці, адже один цикл коштує менше. Використовуйте команду тоді, коли робота може бути чітко розділена, помилки мають великі наслідки та незалежна перевірка може надати докази того, що модель не може обдурити себе. Хороший критерій: додавайте роль лише тоді, коли вона охоплює сценарій збою, якого не охоплює жодна інша роль; інакше додаткові витрати на співпрацю не принесуть жодної користі.
Що будувати спочатку
Почніть з найпростішої машини станів, якій можна довіряти. Кожна сесія має мету, результат, журнал перевірок та ліміт спроб, причому кожен перехід чітко описаний. Розмістіть верифікатор після механізму, відповіді якого надходять від тестів, перевірок коду, процесів компіляції чи подібних автоматизованих перевірок. Паралельні працівники додаються пізніше: паралелізм — це оптимізація, тоді як межі коректності є частиною архітектури. Залиште крок об’єднання даних від лідера видимим, тримайте результати передачі даних у доступному для перевірки вигляді та фіксуйте причини завершення кожної сесії.
Те, що виходить, — це не автономний розум. Це контрольований робочий процес: моделі пропонують варіанти, інструменти виконують роботу, а звичайний код визначає, чи завершено завдання. Спочатку створіть простий цикл вручну, перш ніж використовувати фреймворк. Очікуйте, що тривалі завдання можуть пошкодити його структуру через тривогу, відхилення, самооцінку та мовчання, що не можна виправити жодними запитами. Розглядайте системи з кількома агентами як середовище виконання з планом керування станом, подіями, дозволами, можливістю спостереження та відновлюваними елементами, а не як рольову гру в рамках запиту. Також перед створенням команди передбачіть кошти на передачу обов’язків, обмін інформацією та її агрегацію, і ніколи не створюйте команду через друкарську помилку.
Пов’язана література
- Практичні нотатки: Claude Code Agent Teams: Практичний посібник зі створення команди — Детальний огляд Практичних нотаток: Claude Code Agent Teams: Практичний посібник зі створення команди: контракти, перевірки та місця для вставки коду для команд, які використовують цю модель.
- Чому журнали діяльності агента не задовольнять вимогам статті 12: записи рішень, причинність та походження — Трекування — це не засіб для забезпечення відповідальності. Як записувати рішення агента у вигляді стійких об’єктів із причинними зв’язками, ланцюговим походженням та детерміністичними правилами політики за допомогою Semantica.