Dlaczego agenci działające przez długi czas potrzebują maszyny stanów: role lidera, pracownika i weryfikatora
Cztery sposoby, w jakie pętla pojedynczego agenta zawodzi przy długich zadaniach, oraz jak zespół składający się z lidera, pracownika i weryfikatora, napędzany deterministycznym silnikiem, je naprawia, plus kiedy należy go pominąć.
Źródła: MiniMax Agent Team Best Practice: An AI Agent Is a While Loop. A Reliable One Is a State Machine.
Przejście od chatbota do agenta wymaga pętli while, a nie przełomu w badaniach. To prawda, i właśnie tutaj kończą się większość wyjaśnień, a zaczynają się największe trudności w produkcji. Bardziej użyteczne pytanie brzmi, dlaczego ta pętla cicho przestaje funkcjonować przy zadaniach trwających dłużej niż dziesięć minut oraz jaka struktura może ją naprawić. MiniMax opublikowało dwa szczere materiały na ten temat: otwarty tutorial Mini-Agent, który przedstawia podstawową pętlę, oraz raport techniczny ich zespołu Agent Team, opisujący problemy występujące w skali dużych systemów oraz rozwiązania wprowadzone w odpowiedzi na nie. Obie te publikacje stanowią tu fundament, a ten wzorzec można zastosować w dowolnym stacku.
Jedna pętla – od początku do końca
Gdy pozbawimy agenta wszystkich zbędnych elementów, pozostaną cztery komponenty: model, aktualna lista wiadomości, rejestr narzędzi oraz pętla obsługująca je. Przykład Mini-Agenta funkcjonuje w ten sposób: przy każdym przetworzeniu najpierw sprawdza się, czy historia jest bliska maksymalnej długości dozwolonej w kontekście, a jeśli tak, to ją skraca. Następnie wysyła wiadomości oraz definicje narzędzi do modelu. Jeśli model nie wymaga żadnych narzędzi, pętla kończy swoją pracę. W przeciwnym razie uruchamia żądane narzędzia, dodaje ich wyniki do wiadomości i powtarza proces. Oto pełna wersja przeznaczona dla dowolnego endpointu kompatybilnego z OpenAI, zawierająca dwa rzeczywiste narzędzia oraz mechanizm streszczania:
# 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."))
Pętla ma dwa wyjścia, a oba mają znaczenie. Sukcesem jest ocena samego modelu, wyrażona brakiem dalszych wywołań narzędzi. Bezpieczeństwo to prosty licznik – limit liczby kroków. Ta asymetria, przy której to model sam decyduje, kiedy praca jest zakończona, jest właśnie tym, co ustępuje pierwsze.
Gdzie pojedyncza pętla zawodzi przy długich zadaniach
Uruchom tę samą pętlę przy zadaniu trwającym dwie godziny zamiast dwie minuty, a problemy zaczynają się pojawiać. Zespół MiniMax spisał te, z którymi ciągle się spotykali. Żaden z nich nie jest błędem. Są to właściwości danego kontekstu, więc większy okno czasowe czy lepszy prompt nie rozwiązują żadnego z nich.
Najpierw pojawia się lęk związany z kontekstem: agent przestaje działać, mimo że nikt go o to nie prosił. Po wykonaniu siedmiu podzadań kończy trzy z nich i informuje, że ukończył zadania nr 1, 2 i 3, pytając, czy ma kontynuować. Modele mają niejasną ocenę co do tego, kiedy długie zadanie może się zakończyć, a bezpośrednie szkolenie tej zdolności jest niezwykle kosztowne pod względem finansowym i wymaga dużego wysiłku algorytmicznego. Zwykły pętla przekazuje decyzję o zatrzymaniu działania najmniej niezawodnej części systemu.
Następnie następuje degradacja: agent staje się coraz mniej sprytny w miarę wzrostu objętości kontekstu. Po piątej minucie jego działanie jest precyzyjne, natomiast po sześćdziesiątej piątej zachowuje się jak rozproszony stażysta. Jeden błąd, np. gdy zadanie badawcze przeradza się w tekst marketingowy, psuje wszystko, co następuje później, ponieważ każda kolejna generacja opiera się na zanieczyszczonej historii.
Po trzecie, nic nie sprawdza pracy, ponieważ sędzia jest jednocześnie uczestnikiem. Samotny agent, który tworzy ostateczny wynik, ocenia sam siebie w tym samym kontekście, w którym popełnił błędy. To nie jest nieuczciwość – to po prostu sprawdzanie scenariusza, który sam model właśnie stworzył.
Po czwarte jest milczenie. Interfejsy w stylu czatu sprawiają, że użytkownicy oczekują potwierdzenia w ciągu kilku sekund, nawet przy zadaniu trwającym godzinę, ale pętla ukryta w wywołaniu modelu nie może poinformować, że otrzymała żądanie i zamierza o tym donieść. Zgodnie z danymi MiniMax, pytanie „dlaczego mój agent nie odpowiada” przyciągnęło najwięcej opinii od użytkowników ze wszystkich innych.
Rozwiązanie: trzy role i silnik deterministyczny
MiniMax odpowiada w sposób uporządkowany. Trzy rodzaje ról są koordynowane przez zwykły, deterministyczny kod, a nie przez sprytny prompt; ten kod nazywany jest Team Engine. Jeden cykl życia zadania to jedna sesja, a silnik przechodzi każdą sesję przez trzy stany: tworzenie, weryfikację i zakończenie, ponownie uruchamiając twórcę w przypadku nieudanej weryfikacji.
- Lider dzieli prośbę użytkownika na uporządkowany plan, decyduje, które elementy mają być wykonywane równolegle, odpowiada użytkownikowi w ciągu kilku sekund, podczas gdy reszta działa poza zasięgiem wzroku, a na koniec łączy wyniki.
- Pracownicy zajmują się podzadaniami, z których każde odbywa się w oddzielnym kontekście, wyposażonym we własne narzędzia, pamięć oraz wymagany format wyjściowy. Ta separacja jest ważna, ponieważ pracownik widzi zawsze tylko jedno podzadanie, a nigdy dwie godziny nagromadzonych błędów.
- Weryfikatorzy przekształcają „jest gotowe” na „może zostać dostarczone”. Ta relacja jest celowo konfrontacyjna. Pracownik, który kończy zadanie, uruchamia weryfikatora, a weryfikator, który odrzuca zadanie, restartuje pracownika – podobnie jak w przypadku działu R&D i QA, gdzie jakość powstaje w wyniku kolejnych iteracji, a nie dzięki mikrozarządzaniu przez człowieka.
Jądro silnika jest na tyle małe, że można je zapisać. Ta wersja nie wymaga klucza API, ponieważ role stubów zastępują wywołania LLM i można je wymieniać bez ingerencji w maszynę stanów:
# 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)
Uruchom to – pierwsza wersja każdej sesji zostanie odrzucona, Worker obudzi się z dziennikiem przeglądów w swoim kontekście, a druga wersja zostanie przyjęta. Umieść to obok prostego pętla i widać kilka różnic.
- Zatrzymywanie: pojedyncza pętla kończy się, gdy model przestaje wysyłać żądania narzędzi (fuzzy) lub osiąga limit kroków, natomiast silnik używa wyraźnych stanów „done” i „failed” z budżetem prób przechowywanym w kodzie.
- Weryfikacja: w pętli to ten sam kontekst, który wygenerował pracę, ją sprawdza, podczas gdy zespół używa wrogiego weryfikatora w oddzielnym kontekście, opartego na poleceniach.
- Paralelizm: pętla to jeden sekwencyjny kontekst, natomiast zespół uruchamia Workerów w równoległych sesjach, przy czym lider łączy wiele z nich w jedną.
- Trwałość: pętla przechowuje listę wiadomości aż do stworzenia streszczenia, natomiast zespół przechowuje stan sesji, zapisy przeglądów, artefakty oraz pamięć poszczególnych agentów.
Lider nie jest również ograniczony do jednorazowych wywołań, które tworzą podzadanie i zwracają tekst. W projekcie MiniMax odczytuje aktualny stan silnika i może wysyłać dodatkowe polecenia do sesji, która jest w trakcie przetwarzania lub weryfikacji, dzięki czemu istnieje zarówno proaktywne wysyłanie danych, jak i zapytania na żądanie. To rozwiązanie radzi sobie również z problemem milczenia: lider potwierdza działanie w ciągu kilku sekund, a użytkownik otrzymuje następnie informacje w określonych punktach, takie jak „rozpoczęto”, „zablokowano”, „potrzebna decyzja” czy „zakończono”.
Ugruntowanie weryfikatora
Błąd samodzielnej oceny znika tylko wtedy, gdy werdykt pochodzi spoza każdego modelu. W przypadku kodu MiniMax otacza jego wykonywanie specjalnym mechanizmem. Praca odbywa się na gałęziach, jest uruchamiana w środowiskach testowych, edycje są reprezentowane jako różnice, testy można powtarzać, przeglądy pozostawiają zapisy, a niepowodzenia można ponownie wykonać. Najważniejsze stwierdzenie: warunek zatrzymania musi być powiązany z czymś deterministycznym i widocznym z zewnątrz, takim jak testy, narzędzia do sprawdzania kodu i proces budowania oprogramowania, a nigdy z subiektywnym odczuciem modelu, że pracę zakończył.
# 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")
Lista dowodów pełni jednocześnie funkcję zapisu przeglądu, stanowiąc część śledzenia możliwego do odtworzenia i kontrolowanego, które odróżnia to, co rzeczywiście dostarczalne, od tego, co jedynie prawdopodobne. MiniMax dodatkowo dzieli rolę osoby przeglądającej. Tester tworzy dodatkowe dowody poprzez wykonywanie testów, kompresowanie dzienników błędów oraz dodanie minimalnego przykładu ich odtworzenia. Przeglądający zadaje inne pytanie – nie o to, czy kod przeszedł testy, ale czy powinien zostać zmodyfikowany w taki sposób, biorąc pod uwagę granice abstrakcji, nowe zależności, rozszerzone uprawnienia oraz dane poufne przedostające się do dzienników. Przeglądający mogą pracować równolegle z specjalistami w takich dziedzinach jak ogólne aspekty, bezpieczeństwo i konkretny obszar aplikacji.
Ile kosztuje zespół i kiedy go pominąć
Zespół to nie jest darmowy paralelizm. Trzy koszty pojawiają się już w momencie współpracy agentów, a większe okno kontekstowe nie eliminuje żadnego z nich.
Koszt przenoszenia: te same informacje muszą być ponownie uporządkowane pomiędzy agentami, więc strony badawcze stają się streszczeniem dla autora, które z kolei służy jako dane wejściowe do sprawdzenia formatu. Rozwiązaniem jest przekazywanie plików czytelnych z krótkimi streszczeniami, a nie surowych zapisów kontekstu.
Koszt dzielenia się: za każdy globalnie udostępniany token płaci każdy pracownik w każdej rundzie. Praktyczna struktura zapewnia, że pamięć pozostaje prywatna dla każdego agenta, umożliwia im wzajemną komunikację poprzez przerwy w interfejsie CLI oraz oferuje jedną wspólną tablicę, do której agenty zaglądają tylko wtedy, gdy tego potrzebują.
Trzecim kosztem jest agregacja. Zbieranie dziesięciu częściowych wersji jest proste. Połączenie ich w jeden plik zawierający odpowiednie fakty, cytaty i ton jest najtrudniejszym zadaniem Lidera i wymaga kroku łączenia, a nie dodatkowych pracowników.
Konsensus również ma swoją cenę. Debata swobodna pomiędzy agentami może zużyć kilka razy więcej zasobów niż samodzielna korekta i mimo to nie poprawić dokładności. Wartość leży w strukturze: ograniczone sesje, wyraźne elementy strukturalne oraz możliwość odrzucenia przez weryfikatora.
Pomijaj zespół, gdy zadanie jest małe, lokalne i tanie w sprawdzeniu, ponieważ jeden cykl kosztuje mniej. Używaj zespołu, gdy praca daje się łatwo podzielić, błędy są kosztowne, a niezależna weryfikacja może dostarczyć dowodów na to, że model nie może oszukać samego siebie. Dobry test: dodaj rolę tylko wtedy, gdy obejmuje ona sposób awarii, którego nie obejmuje żadna inna rola; w przeciwnym razie dodatkowe koszty współpracy nic nie przynoszą.
Co zbudować najpierw
Zacznij od najprostszej maszyny stanowej, której możesz zaufać. Każda sesja ma określony cel, artefakt, rejestr przeglądów oraz limit prób ponownych, a każda transycja jest dokładnie opisana. Umieść narzędzie weryfikacyjne za mechanizmem, którego odpowiedzi pochodzą z testów, narzędzi do sprawdzania kodu, procesów budowania lub podobnych automatycznych kontrol. Pracownicy równolegli zostaną dodani później: równoległość to optymalizacja, natomiast granice poprawności stanowią element architektury. Zachowaj widoczny krok łączenia danych przez lidera, utrzymuj artefakty przekazywane między procesami w dostępie do inspekcji oraz zapisuj powody zakończenia każdej sesji.
To, co powstaje, to nie autonomiczny umysł. To kontrolowany proces pracy: modele proponują rozwiązania, narzędzia je wdrażają, a zwykły kod decyduje, czy zadanie zostało ukończone. Zbuduj najpierw ręcznie prosty cykl, zanim zaczniesz używać frameworka. Oczekuj, że długotrwałe zadania mogą uszkodzić jego strukturę z powodu lęku, dewiacji, samodzielnej oceny i milczenia – czego żadne polecenia nie mogą naprawić. Traktuj podejście wielu agentów jako środowisko działania z płaszczyzną sterowania stanem, zdarzeniami, uprawnieniami, możliwościami obserwacji oraz artefaktami, które można przywrócić, a nie jako grę ról w ramach polecenia. Przygotuj się również na przekazywanie zadań, dzielenie się informacjami i agregację danych przed utworzeniem zespołu, i nigdy nie twórz zespołu z powodu błędu pisowni.
Literatura pokrewna
- Praktyczne notatki: Claude Code Agent Teams: Praktyczny przewodnik po budowie zespołu — Szczegółowy opis Praktycznych notatek: Claude Code Agent Teams: Praktyczny przewodnik po budowie zespołu: umowy, sprawdzania oraz miejsca na kod do wstawienia dla zespołów wdrażających ten wzorzec.
- Dlaczego logi agenta nie spełnią wymogów Artykułu 12: Zapisy decyzji, przyczynowość i pochodzenie — Ślady nie stanowią dowodu odpowiedzialności. Jak rejestrować decyzje agenta jako trwałe obiekty z powiązaniami przyczynowymi, łańcuchami haszów określającymi pochodzenie oraz deterministycznymi regułami polityki, wykorzystując Semantica.