Strona główna / Artykuły

Inżynieria pętli: Cztery pętle nawarstwione dla agentów AI samodoskonalących się

Inżynieria pętli dla agentów AI samodoskonalących się: cztery pętle nawarstwione (ReAct, retry, queue poller, hill climbing) plus sędzia, miernik budżetu oraz kontrola modyfikacji.

Wydawca: REACTAPP.TOP32 min czytania7374 słów

Źródła: Loop Engineering: Building Self-Improving AI Agents with Four Nested Loops

Przez około dwa lata praca z modelem oznaczała zmianę się w zadaniach. Inżynier pisze prompt, czyta odpowiedź i ręcznie dostarcza kolejny fragment kontekstu. Ta rutyna wiąże się z ukrytymi kosztami, rodzajem opłaty za koordynację, ponieważ szybkość całego procesu jest ograniczona tempem, z jakim człowiek może dokonać oceny. Aby to przezwyciężyć, konieczna jest zmiana priorytetów: trzeba przestać używać narzędzia i zacząć projektować system, który je obsługuje.

Trzy koncepcje określają tę zmianę.

  • Wpływ przenosi się na wyższy poziom. Rola zmienia się z tworzenia dokładnych instrukcji na projektowanie pętli powtarzalnych, a ostatecznie na coś na kształt stanowiska kierownika. Człowiek określa cel i warunek zatrzymania, natomiast agent odpowiada za każdy krok na tej drodze.
  • Autonomiczne pętle mają rozpoznawalną strukturę anatomiczną. Samodzielnie znajdują zadania, przekazują je podagentom, sprawdzają wyniki za pomocą niezależnych testów oraz zapisują swój stan, co pozwala uniknąć udziału człowieka w cyklu triażu, naprawy i weryfikacji.
  • Inżynieria celów znajduje się na szczycie. W tym etapie wewnętrzne mechanizmy działania pętli tracą na znaczeniu. AlphaEvolve, który pobili rekordy szybkości mnożenia macierzy obowiązujące od 1969 roku, oraz Kiro firmy Amazon, który przekształca cele wysokiego poziomu na wymagania i specyfikacje, ilustrują ten wzorzec: jeśli funkcja oceny jest dobrze skonstruowana, maszyna sama znajduje odpowiedź.

Aby zaprojektować pętlę wokół agenta, potrzebny jest dokładny model mentalny tego agenta. Dlatego rozsądna kolejność budowy zaczyna się od prostego pracownika wykonującego wywołania narzędzi, następnie dodaje się drugi model, który ocenia jego wyniki (aby system przestał oceniać własne prace domowe), łączy oba w pętlę i zapisuje uzyskane wyniki na dysku – modele bowiem zapominają, natomiast repozytorium nie. Po tym przychodzą mechanizmy obronne przed manipulowaniem metryką przez agenta, podobne do oszustw zaobserwowanych podczas testowania o3 firmy OpenAI przez METR, następnie wskaźnik śledzący koszt w środowisku, które może być bogate lub ubogie w tokeny, a na końcu ewolucja od skryptu składającego się z 70 linii do rozwiązania zdolnego do funkcjonowania w warunkach produkcyjnych.

Poniżej znajduje się działająca implementacja całej tej koncepcji, wykorzystująca proces oceny ubezpieczeń jako pole testowe.

Dlaczego to jest problem architektury

Przejście od modelu, który odpowiada na pytania, do agenta, który niezawodnie podejmuje działania, to nie ćwiczenie z pisania promptów. To problem projektowy. Większość tutoriali pokazuje jeden agent korzystający z jednego narzędzia. Prawdziwe systemy wymagają tolerancji na błędy, samokorekcji, ograniczeń wydatków, wykrywania ingerencji oraz ciągłego doskonalenia – wszystkiego naraz.

Podejście to nazywa się inżynierią pętli: układa się kilka pętli sterujących, z których każda ma określone zadanie, dzięki czemu cały system funkcjonuje lepiej niż pojedyncza pętla.

Ubezpieczenia to odpowiednia dziedzina do tego celu. Decyzje pochodzą z małego, ustalonego zestawu (zaakceptować, zmodyfikować, odrzucić, skierować), zasady są jasne, a dane historyczne dostarczają poprawnych i błędnych odpowiedzi. Te same wzorce można zastosować przy przeglądaniu kodu, analizie dokumentów, triażu zgłoszeń oraz przetwarzaniu kart medycznych.

Cztery pętli, jedna wewnątrz drugiej

Zwykłe oprogramowanie kontroluje złożoność za pomocą warstw abstrakcji. Inżynieria pętli radzi sobie z niestabilnością agenta poprzez warstwy informacji zwrotnej:

┌──────────────────────────────────────────────────────┐
│  Loop 4 — Hill Climbing (self-improvement over time) │
│  ┌────────────────────────────────────────────────┐  │
│  │  Loop 3 — Event Queue Poller (orchestration)   │  │
│  │  ┌──────────────────────────────────────────┐  │  │
│  │  │  Loop 2 — Verification & Retry           │  │  │
│  │  │  ┌────────────────────────────────────┐  │  │  │
│  │  │  │  Loop 1 — ReAct Agent              │  │  │  │
│  │  │  │  (think → act → observe → think)   │  │  │  │
│  │  │  └────────────────────────────────────┘  │  │  │
│  │  └──────────────────────────────────────────┘  │  │
│  └────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────┘

Każda pętla ma jedną funkcję, jeden rodzaj awarii, przed którym ma zadanie ją chronić, oraz jeden sygnał zwrotny, na który reaguje. Ważna jest dyscyplina polegająca na unikaniu przenikania się problemów między poszczególnymi pętlami.

Domena

Zanim przejdziemy do architektury, oto dane. Wniosek ubezpieczeniowy to prosty rekord:

{
  "id": "APP-001",
  "applicant_age": 34,
  "state": "FL",
  "property_type": "residential",
  "coverage_amount": 450000,
  "claim_history_5yr": 1,
  "credit_score_tier": "good",
  "flood_zone": true,
  "has_flood_rider": false,
  "business_use": false
}

Każda z 20 aplikacji przykładowych ma również etykietę odpowiadającą prawdzie:

"APP-001": {
  "split": "train",
  "expected_decision": "modify",
  "expected_flags": ["flood_rider_required", "windstorm_exclusion"]
}

Decyzja należy do jednej z czterech kategorii:

  • przyjąć: standardowe ryzyko, zatwierdzić w obecnej formie
  • zmodyfikować: zatwierdzić pod warunkami lub z dodatkowymi zastrzeżeniami
  • odmówić: ryzyko przekracza dopuszczalny próg
  • przekazać: musi to przejrzeć starszy ekspert ubezpieczeniowy

Niewielka baza wiedzy zawiera pięć ukrytych zasad ubezpieczeniowych dotyczących stref powodziowych, historii roszczeń, oceny kredytowej, górnych granic ochrony oraz ograniczeń specyficznych dla poszczególnych stanów. Agent nie otrzymuje informacji o nich w swoich instrukcjach – musi je znaleźć i zastosować za pomocą odpowiednich narzędzi. Szesnaście aplikacji stanowi zestaw treningowy, a cztery są zachowane do celów testowania.

Pętla 1: agent ReAct

Obrona przed: modelem, który podejmuje decyzje bez żadnych dowodów. Sygnał zwrotny: graf wywołań narzędzi, który wskazuje, czy agent sprawdził wszystko, co istotne, przed podjęciem decyzji.

ReAct, skrót od reason plus act, to podstawowy wzorzec działania. Agent zastanawia się nad tym, czego potrzebuje, wywołuje narzędzie, czyta wynik, ponownie się zastanawia i powtarza ten proces, aż podejmie ostateczną decyzję:

System Prompt
     │
     ▼
[Think] → What information do I need?
     │
     ▼
[Act]   → Call tool (lookup_application, retrieve_rules, ...)
     │
     ▼
[Observe] → Tool returns result
     │
     ▼
[Think] → What does this mean? What next?
     │
    ... (repeat until decision is made)
     │
     ▼
[Act]   → draft_decision (terminal tool call)

W LangGraph staje się to grafem stanowym składającym się z dwóch węzłów i krawędzi warunkowej. Stan zawiera ID aplikacji, listę wiadomości, pola decyzyjne, lekcje, aktualną kwotę wydanych środków oraz licznik kroków:

class UnderwritingState(TypedDict):
    application_id: str
    messages: Annotated[list[AnyMessage], add_messages]  # reducer accumulates
    decision: str
    rationale: str
    flags: list[str]
    recommended_premium_adj: float
    lessons: list[str]
    spend: float
    _steps: int

Jeden szczegół zasługuje na uwagę. Adnotacja add_messages sprawia, że LangGraph dodaje elementy do listy wiadomości na każdym kroku zamiast ją zastępować, dzięki czemu agent przechowuje całą rozmowę podczas każdego wywołania narzędzia w ramach jednej aplikacji.

Budowniczy bierze prompt systemowy oraz wspólny licznik kosztów i łączy narzędzia z modelem:

def build_loop1(system_prompt: str, meter: CostMeter):
    model = ChatAnthropic(model="claude-sonnet-4-6").bind_tools(TOOLS)

Wewnątrz znajdują się węzeł agenta, węzeł narzędzi, funkcja routingu oraz zbiór grafu:

    def agent_node(state: UnderwritingState) -> dict:
        prompt = system_prompt
        if state.get("lessons"):
            prompt += "\n\nUnderwriting lessons:\n" + "\n".join(
                f"- {l}" for l in state["lessons"]
            )
        msgs = [SystemMessage(content=prompt)] + list(state["messages"])
        response = model.invoke(msgs)
        meter.record_from_message(response)          # track spend per call
        return {
            "messages": [response],
            "_steps": state.get("_steps", 0) + 1,
            "spend": meter.spent,
        }

    def tools_node(state: UnderwritingState) -> dict:
        last = state["messages"][-1]
        tool_results, updates = _dispatch_tools(last.tool_calls)
        return {"messages": tool_results, **updates}  # updates captures draft_decision output

    def should_continue(state) -> Literal["tools", "__end__"]:
        if state.get("_steps", 0) >= MAX_STEPS:   # hard cap at 8 steps
            return END
        if getattr(state["messages"][-1], "tool_calls", None):
            return "tools"
        return END

    g = StateGraph(UnderwritingState)
    g.add_node("agent", agent_node)
    g.add_node("tools", tools_node)
    g.set_entry_point("agent")
    g.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END})
    g.add_edge("tools", "agent")
    return g.compile()

Cztery narzędzia

Agent otrzymuje dokładnie cztery narzędzia, wystarczające, by obejmować wszystko, czego potrzebuje:

@tool
def lookup_application(app_id: str) -> dict:
    """Return the full application record for the given application ID."""
    # reads from applications.json — deterministic, no LLM call
    ...

@tool
def retrieve_rules(risk_factor: str) -> list[str]:
    """Return underwriting rules matching the given risk factor keyword.
    Use terms like: flood, claims, credit, coverage, state, business."""
    # keyword match against rules_kb.json - forces explicit rule lookup
    ...

@tool
def fetch_risk_signal(signal_type: str) -> dict:
    # simulated external data pull (credit bureaus, flood maps, etc.)
    ...

@tool
def draft_decision(
    decision: Literal["accept", "modify", "decline", "refer"],
    rationale: str,
    flags: list[str],
    recommended_premium_adj: float,
) -> dict:
    """Finalize the underwriting decision with structured output."""
    # terminal action - structured schema forces the agent to commit explicitly
    ...

lookup_application jedynie odczytuje dane. retrieve_rules przeprowadza wyszukiwanie według słów kluczowych w ustalonej kolekcji reguł, co wymusza wyraźne sprawdzenie. fetch_risk_signal symuluje pobieranie danych z zewnętrznych źródeł, takich jak biura kredytowe czy mapy powodziowe. draft_decision to ostateczna akcja i ma największe znaczenie – sprawia, że agent podejmuje zapisaną decyzję z uzasadnieniem i konkretnymi flagami, dzięki czemu ustrukturyzowany wynik pochodzi bezpośrednio z samego wywołania narzędzia, a nikt nie musi analizować tekstu niestrukturyzowanego.

Dlaczego istotna jest ograniczona liczba kroków

MAX_STEPS = 8 zapobiega nieskończonym łańcuchom wywołań narzędzi. Zdezorientowany model może dwadzieścia razy próbować wyszukać reguły dotyczące „powodzi”, szukając odpowiedzi, której tam nie ma, co powoduje marnotrawstwo tokenów i czasu. Gdy liczba kroków osiągnie to ograniczenie, stan otrzymuje decyzję ran_out_of_steps, którą Loop 2 może rozpoznać i spróbować ponownie.

Pętla 2: weryfikacja i ponowna próba

Ochrona przed: jedną błędną odpowiedzią wynikającą z błędnego toku rozumowania. Sygnał informacyjny: deterministyczny oceniający, is_correct(), bez udziału modelu.

Pętla 1 może zawieść. Agent może błędnie odczytać flagę, pominąć zasadę lub osiągnąć limit kroków. Pętla 2 wykrywa te błędy i daje agencie kolejną szansę wraz z precyzyjnymi informacjami zwrotnymi:

def run_loop2(
    app_id: str,
    system_prompt: str,
    meter: CostMeter,
    max_retries: int = 3,
) -> tuple[dict, bool]:
    state = run_loop1(app_id, system_prompt, meter)
    for attempt in range(max_retries):
        decision = state.get("decision", "ran_out_of_steps")
        if decision == "refer":
            _write_human_queue(app_id, state)   # human-in-the-loop path
            return state, True
        if is_correct(decision, app_id):         # deterministic check
            return state, True
        if attempt >= max_retries - 1:
            return state, False                  # exhausted retries
        # construct targeted feedback for next attempt
        expected = load_historical_decisions()[app_id]["expected_decision"]
        feedback = HumanMessage(content=(
            f"Your decision was '{decision}' but this application requires '{expected}'. "
            f"Review the risk factors carefully and call draft_decision again."
        ))
        extra_messages = list(state.get("messages", [])) + [feedback]
        state = run_loop1(app_id, system_prompt, meter, extra_messages=extra_messages)
    return state, False

Kilka decyzji projektowych warto wyjaśnić dokładniej.

Sędzia jest deterministyczny. Funkcja is_correct() odczytuje plik z historycznymi decyzjami i porównuje ciągi znaków. Nigdy nie wywołuje modelu. Użycie drugiego modelu jako sędziego dodałoby drugie źródło niestabilności, a tutaj prawda obiektywna po prostu jest prawdą obiektywną.

Odwołanie to załatwiony wynik. Gdy agent wpisze refer, pętla 2 zapisuje sprawę do pliku human_queue.json i zgłasza sukces. Przekazanie sprawy osobie traktowane jest jako ważny stan końcowy, a nie jako coś, co należy spróbować ponownie.

Informacja zwrotna wskazuje cel. Zamiast „spróbuj ponownie”, komunikat podaje agentowi, która kategoria decyzji jest prawidłowa, i prosi go o ponowną ocenę czynników ryzyka. To kieruje kolejną próbę ku właściwej odpowiedzi, podczas gdy ślepe ponowne losowanie mogłoby doprowadzić w błąd.

Historia jest przenoszona dalej. Dodatkowe wiadomości zawierają całą wcześniejszą rozmowę wraz z informacjami zwrotnymi, dzięki czemu podczas kolejnego uruchomienia agent widzi wszystko, co już próbował, i jest mniej skłonny do powtarzania tego samego błędnego podejścia.

Pętla 3: monitor kolejki zdarzeń

Ochrona przed: ścisłym powiązaniem pomiędzy osobą, która tworzy zadania, a osobą, która je wykonywa. Sygnał informacji zwrotnej: zawartość pliku pending_applications.json, zwykłego pliku pełniącego rolę kolejki.

Ta pętla pełni rolę koordynatora. Pobiera elementy z kolejki oczekujących, prowadzi jedno z zadań przez pętle 1 i 2, aktualizuje stan wykonywania i od czasu do czasu uruchamia pętlę 4:

def run_event_loop(
    system_prompt: str,
    meter: CostMeter,
    pending_path: Path = DEFAULT_PENDING,
    state_path: Path = DEFAULT_STATE,
    once: bool = False,
) -> None:
    run_state = load_run_state(state_path)
    verify_judge_checksum(run_state.get("judge_checksum", ""))   # tamper check
    total = len(json.loads(pending_path.read_text(encoding="utf-8-sig"))
                if pending_path.exists() else [])
    processed = 0
    while True:
        pending = (json.loads(pending_path.read_text(encoding="utf-8-sig"))
                   if pending_path.exists() else [])
        if not pending:
            if once:
                print("[loop3] queue empty - done")
                break
            print("[loop3] queue empty - sleeping...")
            time.sleep(POLL_INTERVAL)
            continue
        app_id = pending[0]
        processed += 1
        print(f"[loop3] {processed}/{total}  {app_id} ...", flush=True)
        try:
            result_state, passed = run_loop2(app_id, prompt_with_lessons, meter)
        except BudgetExhaustedError:
            raise                                # budget exhaustion is fatal
        except Exception as exc:
            print(f"[loop3] {app_id} ERROR: {exc!r} - skipping")
            # remove from queue and continue - one bad app cannot block the rest
            remaining = [x for x in pending if x != app_id]
            pending_path.write_text(json.dumps(remaining), encoding="utf-8")
            continue
        # ... update state, distill lesson, trigger hill-climbing

Dlaczego plik z kolejką?

Kolejka to celowo plik JSON, a nie strumień Redis czy kolejka SQS, ponieważ w rozwoju lokalnym jest łatwa do sprawdzenia (można otworzyć plik i zobaczyć, co jest w oczekiwaniu), łatwa do wznowienia (po awarii wystarczy ją ponownie uruchomić, a kolejka pozostanie nienaruszona) oraz odseparowana (to, co ją wypełnia, nie musi znać szczegółów o odbiorcy). W systemie produkcyjnym zastosowano by odpowiedniego brokera wiadomości, a umowa interfejsu, czyli lista identyfikatorów, nie uległaby zmianie.

Detekcja modyfikacji

Zanim cokolwiek przetworzy, Loop 3 weryfikuje prawdziwe dane:

verify_judge_checksum(run_state.get("judge_checksum", ""))

Kontrola działa w następujący sposób:

def compute_judge_checksum() -> str:
    content = _HIST.read_bytes()           # historical_decisions.json
    return hashlib.sha256(content).hexdigest()


def verify_judge_checksum(expected: str) -> None:
    actual = compute_judge_checksum()
    if actual != expected:
        raise RuntimeError(
            f"Judge checksum mismatch - historical_decisions.json was tampered with."
        )

Hasz SHA-256 pliku z decyzjami historycznymi jest przechowywany w stanie uruchomienia podczas inicjalizacji i sprawdzany na nowo za każdym razem, gdy uruchamia się pętla zdarzeń. Jeśli ktoś zmieni prawdziwe dane, aby faworyzować agenta, system odmawia dalszego działania. To zamyka cichą lukę w systemach samodoskonalących się: najtańszym sposobem na podniesienie oceny jest zmiana mechanizmu oceny.

Pomijanie versus awaria

Struktura try/except w Pętli 3 wyznacza wyraźną granicę. Wyczerpanie budżetu powoduje ponowne zatrzymanie wszystkiego. Każda inna wyjątkowa sytuacja jest zapisywana, aplikacja powodująca problem jest usuwana z kolejki, a następna jest przetwarzana. Dlatego jedna nieprawidłowa aplikacja nie może zablokować całego procesu, natomiast wyczerpanie środków jest z góry przewidziane jako katastrofalne, ponieważ system nie powinien funkcjonować, gdy nie ma już nic do wydania.

Pętla 4: wspinaczka po wzgórzu

Obrona przed: sztywnym promptem, który nigdy nie uczy się na podstawie doświadczeń. Sygnał informacyjny: dokładność treningu wraz z werdyktem audytu.

Każda ósma decyzja (HILL_CLIMB_EVERY = 8) powoduje, że Pętla 3 przekazuje kontrolę Pętli 4, która uruchamia cykl refleksji, mutacji, oceny i decydowania w celu automatycznego ulepszenia promptu systemu:

seed prompt
     │
     ▼
evaluate on training set → score, failures
     │
     ▼
┌────────────────────────────────┐
│  for each round:               │
│                                │
│  reflect(current, failures)    │  ← LLM analyzes failure patterns
│       │                        │
│       ▼                        │
│  candidate_prompt              │
│       │                        │
│       ▼                        │
│  evaluate(candidate, train)    │  ← full eval run
│       │                        │
│       ▼                        │
│  audit_prompt(candidate)       │  ← anti-cheating check
│       │                        │
│       ▼                        │
│  if score > best AND clean:    │
│      best = candidate          │  ← keep
│  else:                         │
│      discard                   │  ← revert
└────────────────────────────────┘
     │
     ▼
write best_prompt.txt

Rozważaj

REFLECT_TEMPLATE = """You are improving the system prompt of an insurance underwriting agent.

CURRENT PROMPT:
{prompt}
The agent got these decisions WRONG on training applications:
{failures}
Look for PATTERNS in the failures. Infer GENERAL underwriting rules that would fix them.
Do NOT memorize specific application IDs or applicant details.
Write an improved system prompt. Reply with ONLY the new prompt text."""


def reflect(current_prompt: str, failures: list[dict]) -> str:
    client = anthropic.Anthropic()
    failure_text = "\n".join(
        f"- APP {f['app_id']}: agent said '{f['got']}', expected '{f['expected']}'. "
        f"Application data: {f['application']}"
        for f in failures
    )
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1500,
        messages=[{"role": "user", "content": REFLECT_TEMPLATE.format(
            prompt=current_prompt,
            failures=failure_text,
        )}],
    )
    return response.content[0].text.strip()

Zalecenie nieprzypominania sobie konkretnych identyfikatorów aplikacji lub szczegółów aplikanta ma duże znaczenie. Bez niego model mógłby tworzyć instrukcje w stylu „dla APP-003 zawsze odrzucaj”, które doskonale sprawdzałyby się na danych treningowych, ale całkowicie zawodziłyby w przypadkach, których nie widział. Nowa instrukcja musi generalizować na podstawie wzorców występujących w niepowodzeniach, a nie pojedynczych przypadków.

Ocena

def evaluate_prompt(system_prompt: str, split: str = "train", meter=None):
    tasks = load_tasks(split)
    failures = []
    for task in tasks:
        state = run_loop1(task["id"], system_prompt, meter)
        decision = state.get("decision", "ran_out_of_steps")
        if not is_correct(decision, task["id"]):
            failures.append({
                "app_id": task["id"],
                "got": decision,
                "expected": task["expected_decision"],
                "application": task["application"],
            })
    score = (len(tasks) - len(failures)) / len(tasks)
    return score, failures

Każdy prompt kandydat jest przetwarzany w Pętli 1 we wszystkich 16 aplikacjach treningowych. Jest to kosztowne – 16 uruchomień modelu na rundę, a co najmniej 80 w ciągu pięciu rund – ale sygnał jest jednoznaczny: prompt musi działać we wszystkich przypadkach z całej dystrybucji treningowej, a nie tylko na jednym przykładzie.

Gate

def improve(seed_prompt: str, lessons: list[str], meter: CostMeter, rounds: int = 5) -> str:
    best = with_lessons(seed_prompt, lessons)
    best_score, failures = evaluate_prompt(best, split="train", meter=meter)
    for r in range(1, rounds + 1):
        candidate = reflect(best, failures)
        score, cand_failures = evaluate_prompt(candidate, split="train", meter=meter)
        verdict = audit_prompt(candidate)        # anti-cheat check
        if score > best_score and verdict == "VERDICT: CLEAN":
            best, best_score, failures = candidate, score, cand_failures
            BEST_PROMPT_PATH.write_text(best, encoding="utf-8")
            print(f"round {r}: {score:.0%} KEPT  ({len(cand_failures)} failures)")
        else:
            reason = "cheating" if verdict != "VERDICT: CLEAN" else f"{score:.0%} <= {best_score:.0%}"
            print(f"round {r}: {score:.0%} REVERTED ({reason})")
    return best

Aby kandydat został przyjęty, muszą zostać spełnione obie warunki. Po pierwsze, score > best_score – musi on wyraźnie przewyższyć obecny rekord. Remisy są odrzucane, co zapobiega dewiacjom w zapytaniach. Po drugie, audytor musi zwrócić wartość VERDICT: CLEAN, potwierdzającą, że kandydat nie zawiera ustalonych z góry odpowiedzi.

Audytor zapytań

AUDIT_SYSTEM = (
    "You are a security auditor for AI systems. "
    "Check whether the prompt contains any hardcoded application IDs (e.g. APP-001) "
    "or hardcoded decisions for specific applications. "
    "These would constitute cheating — memorizing answers instead of learning rules. "
    "Reply with exactly one of:\n"
    "VERDICT: CLEAN\n"
    "VERDICT: CHEATING\n"
    "Followed by a one-sentence explanation."
)


def audit_prompt(evolved_prompt: str) -> str:
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=200,
        system=AUDIT_SYSTEM,
        messages=[{"role": "user", "content": f"PROMPT TO AUDIT:\n{evolved_prompt}"}],
    )
    return response.content[0].text.strip().split("\n")[0]   # first line only

To jest konfiguracja z LLM jako sędzią, ale ograniczona do jednego binarnego pytania strukturalnego: czy prompt zawiera konkretne ID lub zapamiętane odpowiedzi? To właśnie taki rodzaj klasyfikacji, w którym modele-sędziowie są niezawodne. Audytor nigdy nie ocenia jakości, stylu ani poprawności – szuka jedynie określonego wzorca.

Pamięć lekcji

Oprócz metody wspinaczki po wzgórzach system przechowuje listę lessons, która pozostaje niezmieniona pomiędzy kolejnymi uruchomieniami. Gdy aplikacja nadal zawodzi po każdej próbie, wywołuje funkcję distill_lesson():

DISTILL_PROMPT = """An insurance underwriting agent made a wrong decision.

Application details: {application}
Agent decided: {got}
Correct decision: {expected}
Write ONE short, general underwriting rule that would prevent this mistake.
Do NOT reference this specific application ID or any applicant names/details.
Reply with only the rule, as a single sentence."""


def distill_lesson(application: dict, got: str, expected: str) -> str:
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=150,
        messages=[{"role": "user", "content": DISTILL_PROMPT.format(...)}],
    )
    return response.content[0].text.strip()

Każda lekcja jest następnie dodawana do każdego późniejszego uruchomienia Loop 1 za pomocą with_lessons():

def with_lessons(base_prompt: str, lessons: list[str]) -> str:
    if not lessons:
        return base_prompt
    lessons_text = "\n".join(f"- {l}" for l in lessons)
    return base_prompt + f"\n\nUnderwriting lessons learned:\n{lessons_text}"

To jest uczenie się z kilkoma przykładami bez spadku gradientu. System uczy się na podstawie błędów w trakcie działania, gromadząc skompresowaną wiedzę w postaci reguł w języku naturalnym. Funkcja w stylu protokołu CL-BENCH sprawdza, czy te lekcje rzeczywiście pomagają:

def measure_gain(base_prompt: str, lessons: list[str], split: str = "test", meter=None) -> float:
    stateless_score, _ = evaluate_prompt(base_prompt, split=split, meter=meter)
    augmented = with_lessons(base_prompt, lessons)
    stateful_score, _ = evaluate_prompt(augmented, split=split, meter=meter)
    return stateful_score - stateless_score   # positive = lessons help

Kontrola budżetu za pomocą licznika kosztów

Każde wezwanie modelu przechodzi przez jeden wspólny CostMeter, przekazywany z Loop 4 do Loop 3, potem do Loop 2 i wreszcie do Loop 1, dzięki czemu wszystkie wydatki gromadzą się w jednym miejscu:

BUDGET_USD = 2.00
INPUT_PRICE_PER_TOKEN  = 3.00  / 1_000_000
OUTPUT_PRICE_PER_TOKEN = 15.00 / 1_000_000


class CostMeter:
    def record(self, input_tokens: int, output_tokens: int) -> None:
        if self.spent >= self._budget:              # check BEFORE adding
            raise BudgetExhaustedError(
                f"Budget ${self._budget:.2f} exhausted at ${self.spent:.4f}"
            )
        self.spent += input_tokens * INPUT_PRICE_PER_TOKEN \
                    + output_tokens * OUTPUT_PRICE_PER_TOKEN

Kluczowe jest właściwe ustalenie kolejności działań: sprawdź budżet przed dodaniem kosztu. Dzięki temu błąd zostanie wykryty natychmiast po osiągnięciu limitu, a nie dopiero po fakcie przekroczenia wydatków. BudgetExhaustedError jest przekazywany w górę po hierarchii pętli, a pętla 3 ponownie go rzuca zamiast go ignorować, dzięki czemu wyczerpany budżet zawsze kończy całe uruchomienie.

Stan uruchomienia z idempotentną konfiguracją

run_state.json zapisuje wszystko, co się wydarzyło: przetworzone aplikacje, listy z powodzeniem i niepowodzeniem, zgromadzone wnioski, decyzje podjęte od ostatniego „wzniesienia” oraz checksum sędziego. Jego inicjalizacja ma charakter obronny:

def _init_run_state() -> dict:
    path = _STATE_DIR / "run_state.json"
    state = {}
    if path.exists():
        try:
            content = path.read_text(encoding="utf-8-sig").strip()  # handles Windows BOM
            if content:
                state = json.loads(content)
        except (json.JSONDecodeError, UnicodeDecodeError):
            pass                                 # corrupt file → start fresh
    if not state:
        state = dict(_DEFAULT_RUN_STATE)
    state["judge_checksum"] = compute_judge_checksum()   # always refresh
    path.write_text(json.dumps(state, indent=2), encoding="utf-8")
    return state

Tutaj widoczne są trzy nawyki odpornościowe. Czytanie z użyciem utf-8-sig w tle usuwa znaczniki porządku bajtów systemu Windows. Pusty lub uszkodzony plik powraca do stanu domyślnego zamiast się zatrzymać. Ponadto kontrolna suma hash jest obliczana na nowo przy każdym uruchomieniu, a nie polega się na wartościach z poprzedniego wykonywania.

Trzy sposoby uruchomienia

Istnieją trzy sposoby dostępu, a każdy z nich wykorzystuje tylko część zasobów.

Tryb pojedynczy uruchamia sam Loop 1, co jest przydatne podczas debugowania jednej aplikacji oraz do obserwacji, jakie narzędzia wykorzystuje agent i dlaczego:

python -m src.main --mode single --app-id APP-001

Tryb pętli zdarzeń uruchamia pętle 1, 2 i 3 dla każdego oczekującego zapytania aplikacji i może uruchomić pętlę 4 po każdych ośmiu decyzjach. Flaga --once opróżnia kolejkę i zamyka proces zamiast kontynuować bez końca pomiary:

python -m src.main --mode event-loop --once

Tryb doskonalenia uruchamia bezpośrednio Pętlę 4, z pięcioma rundami wspinaczki po wzgórzach w części treningowej. Następnie ocenia najlepszy prompt znaleziony na podstawie części testowej i wyświetla uzyskany przyrost:

python -m src.main --mode improve --rounds 5

Jedna aplikacja, od początku do końca

Oto pełny przepływ danych dla pojedynczej aplikacji w systemie:

pending_applications.json
          │
          │  [Loop 3 reads APP-009]
          ▼
    run_loop2("APP-009", prompt, meter)
          │
          │  [Loop 2 calls Loop 1]
          ▼
    run_loop1("APP-009", prompt, meter)
          │
          │  [LangGraph StateGraph]
          ▼
    agent_node → ChatAnthropic.invoke([SystemMsg, HumanMsg])
          │
          │  response: tool_call(lookup_application, {"app_id": "APP-009"})
          ▼
    tools_node → lookup_application.invoke({"app_id": "APP-009"})
          │
          │  returns: {id: APP-009, state: TX, coverage: 1200000, ...}
          ▼
    agent_node → ChatAnthropic.invoke([..., ToolMessage])
          │
          │  response: tool_call(retrieve_rules, {"risk_factor": "coverage"})
          ▼
    tools_node → retrieve_rules.invoke({"risk_factor": "coverage"})
          │
          │  returns: ["Coverage > $1M requires mandatory refer. Flag: high_coverage"]
          ▼
    agent_node → ChatAnthropic.invoke([..., ToolMessage])
          │
          │  response: tool_call(draft_decision, {decision: "refer", ...})
          ▼
    tools_node → draft_decision.invoke({...})
          │
          │  updates state: {decision: "refer", flags: ["high_coverage"], ...}
          ▼
    should_continue → END (no more tool calls)
          │
          ▼
    Loop 2: decision == "refer" → write human_queue.json → return (state, True)
          │
          ▼
    Loop 3: passed=True → update run_state.json → remove APP-009 from queue
          │
          │  [decisions_since_last_improvement becomes 9 >= 8]
          ▼
    Loop 4: improve(seed_prompt, lessons, meter, rounds=5)
          │
          │  reflect → evaluate → audit → gate → write best_prompt.txt
          ▼
    Loop 3: continue with APP-010 using new best_prompt

Lekcje z budowy

1. Trzymaj orakula oddzielone od agenta. Sędzia (is_correct, evaluate_prompt) nie dzieli się żadnym kodem z agentem (run_loop1). Sędzia czyta pliki statyczne, a agent wywołuje model. Pozwól modelowi oceniać własny wynik – w ten sposób otrzymasz osobę, która zawsze zgadza się bez błędów.

2. Uczynij ocenę deterministyczną. Poprawność działania aplikacji, dokładność treningu oraz dokładność testów są deterministyczne dla tych samych danych wejściowych. Jedyną elementem losowości są odpowiedzi modelu, więc każdą ocenę można odtworzyć na podstawie promptu i seedu.

3. Użyj narzędzia do generowania ostatecznej odpowiedzi w celu uzyskania ustrukturyzowanego wyniku. Narzędzie draft_decision jest skuteczniejsze od analizy tekstu prozowego. Jego schemat określa słownictwo (accept | modify | decline | refer), wymagane pola (flags, rationale, recommended_premium_adj) oraz ich typy, dzięki czemu agent musi postępować w sposób jednoznaczny.

4. Zabezpieczenia przed oszustwem nie są opcjonalne w systemach samodoskonalących się. Gdy system może edytować własne instrukcje, znajdzie sposoby na obejście zasad, a najoczywistszym z nich jest zapamiętywanie odpowiedzi z treningu. Funkcja audit_prompt() automatycznie wykrywa takie zachowania. Bez niej metoda „wspinaczki po wzgórzach” zawsze daje prompty o niemal perfekcyjnej dokładności w treningu, ale niemal losowej dokładności w testach.

5. Znaki określające kolejność bajtów oraz kodowania to błędy produkcyjne. Jeśli pracujesz na systemie Windows lub otrzymujesz pliki od użytkowników Windows, konieczne jest użycie encoding="utf-8-sig" przy każdym odczytywaniu pliku. Znak BOM utworzony przez Notatnik w Windows powoduje błąd json.JSONDecodeError: Unexpected UTF-8 BOM, którego trudno zdiagnozować, ponieważ plik wygląda w edytorze zupełnie normalnie.

6. Sprawdź budżet przed wydaniem pieniędzy, a nie po nim.

# WRONG: allows one overspend before raising
self.spent += cost
if self.spent > self._budget:
    raise BudgetExhaustedError(...)
# CORRECT: raises before the overspend registers
if self.spent >= self._budget:
    raise BudgetExhaustedError(...)
self.spent += cost

Kolejność ma znaczenie, gdy kilka wywołań modelu następuje szybko po sobie. Najpierw dodaj dane, a potem sprawdź – budżet może zostać przekroczony już po kilku takich wywołaniach, zanim pojawi się błąd.

7. Oczyść linie postępu w długich pętlach.

print(f"[loop3] {processed}/{total}  {app_id} ...", flush=True)

W systemie Windows standardowy wyjście jest buforowane, chyba że zaznaczono inaczej. Jeśli pominie się flush=True, linia „processing APP-009” pojawi się po zakończeniu wywołania API, co unieważnia sens wskaźnika postępu. Zawsze należy wyczyścić bufor z linii przed blokującymi operacjami wejścia/wyjścia.

8. Trzymaj budżet używany w metodzie hill-climbing oddzielnie od budżetu pętli zdarzeń. Wyczerpanie budżetu podczas tej metody musi zostać wykryte w miejscu tego wywołania i nie może doprowadzić do awarii pętli zdarzeń. Po tym, jak osiem ocen aplikacji zużyje większość budżetu w wysokości 2 dolarów, metodzie hill-climbing pozostaje niewiele zasobów. Pętla zdarzeń wykrywa specyficzny błąd BudgetExhaustedError pochodzący z tej metody i kontynuuje przetwarzanie pozostałych aplikacji:

try:
    system_prompt = run_hill_climbing(system_prompt, lessons, meter)
    run_state["decisions_since_last_improvement"] = 0
except BudgetExhaustedError:
    print(f"[loop3] hill-climbing skipped — budget exhausted at ${meter.spent:.4f}")
    run_state["decisions_since_last_improvement"] = 0

Zastosowanie wzorca w innych miejscach

Projekt oparty na czterech pętlach nie jest związany wyłącznie z ubezpieczeniami. Aby go dostosować, zamień te elementy:

  • Narzędzia pętli 1. Tutaj: wyszukiwanie, reguły, sygnał, decyzja. U Ciebie: narzędzia do pobierania danych i podejmowania działań w Twojej dziedzinie.
  • Prawda podstawowa. Tutaj: plik z historycznymi decyzjami. U Ciebie: Twój własny ztaggowany zbiór danych.
  • Sędzia. Tutaj: porównanie ciągów znaków. U Ciebie: test poprawności specyficzny dla Twojego zadań.
  • Lekcje. Tutaj: reguły underwritingu. U Ciebie: wzorce błędów z Twojej dziedziny.
  • Wskazówka do wspinaczki po wzgórzu. Tutaj: wskazówka dla systemu underwritingu. U Ciebie: wskazówka dla systemu Twojego agenta.
  • Budżet. Tutaj: 2,00 dolara. U Ciebie: kwota, jaką powinien kosztować każdy przeprowadzony proces.

Jedno ograniczenie jest bezwzględne: sędzia musi być deterministyczny i niezależny od agenta. Jeśli nie można zdefiniować testu poprawności deterministycznej dla danego zadania, system nie może w sposób niezawodny się ulepszać.

Próba w praktyce

Ustawienia oraz cztery polecenia:

# 1. Install
cd underwriting_loop
pip install -e ".[dev]"
export ANTHROPIC_API_KEY=sk-ant-...
# 2. Run a single application (debug mode - cheapest)
python -m src.main --mode single --app-id APP-001
# 3. Drain the pending queue with event loop
python -m src.main --mode event-loop --once
# 4. Run standalone hill-climbing (5 rounds)
python -m src.main --mode improve --rounds 5
# 5. Run the full offline test suite (no API key needed)
pytest tests/ -v

Wynik wspinaczki po wzgórzach wygląda tak:

seed score: 44%  (9 failures)
round 1: 56% KEPT  (7 failures)
round 2: 56% REVERTED (score 56% <= best 56%)
round 3: 69% KEPT  (5 failures)
round 4: 75% KEPT  (4 failures)
round 5: 69% REVERTED (score 69% <= best 75%)
Final test evaluation...
spend:              $1.8342
test score:         75%
gain (vs baseline): +25%

Prompt początkowy osiąga 44% wyniku podczas treningu. Pięć rund później zwycięski prompt uzyskuje 75% wyniku w zestawie testowym, co stanowi skok o 25 punktów w porównaniu z automatycznym udoskonalaniem promptu bez ręcznej jego konstrukcji.

Co dają te pętle

Inżynieria pętli nie jest ramą programową. Jest to sposób dzielenia niezawodności agenta na warstwy, z których każda ma jedną odpowiedzialność i jeden mechanizm zwrotnego informowania. Cztery pętle odpowiadają na cztery odrębne rodzaje awarii:

  • Pętla 1 zajmuje się uzasadnieniami: agent musi skonsultować się z dowodami przed podjęciem decyzji.
  • Pętla 2 radzi sobie z awariami w pojedynczych próbach: jeden błędny tok rozumowania nie powinien trwale sparaliżować aplikacji.
  • Pętla 3 zajmuje się wydajnością i odpornością: jedna błędna aplikacja nie powinna blokować całego procesu.
  • Pętla 4 radzi sobie z powtarzającymi się błędami poprzez przepisanie samego promptu na podstawie zgromadzonych niepowodzeń.

Układaj je jedno na drugim, dodaj deterministycznego sędziego, chronij wszystko za pomocą kontroli budżetu i wykrywania modyfikacji, a system z czasem znacznie się poprawi bez żadnej interwencji ludzkiej.

Co zyskujesz

Przyjmij tę architekturę z otwartymi oczami. Największą ostrożnością wynikającą z szerszych rozważań na temat pętli agentów jest „poddanie się kognitywne” Addy Osmani: przekazanie pętli pełnej kontroli i zaprzestanie analizy jej wyników. Korzyści są nadal rzeczywiste i mierzalne.

  1. Samoudoskonalanie bez udziału człowieka. Metoda wspinaczki po wzgórzach podniosła dokładność szkolenia z 44% do 75% bez konieczności ręcznej edycji promptów, a system samodzielnie wyodrębnił odpowiednie zasady oceny na podstawie wzorców niepowodzeń.
  2. Izolacja błędów pomiędzy warstwami. Jeden uszkodzony aplikacja nie powoduje przerwy w całym procesie. Pętla 3 łapie błędy poszczególnych aplikacji, loguje je i kontynuuje działanie. BudgetExhaustedError jest jedynym wyjątkiem, który dociera na najwyższy poziom, i jest to celowe rozwiązanie.
  3. Postęp, który można zmierzyć i sprawdzić. Funkcja measure_gain() dostarcza konkretnych danych – porównuje wyniki systemów z pamięcią stanu z tymi bez niej na wybranym zbiorze danych. Ulepszenia można przedstawić zainteresowanym stronom w postaci procentu oraz kosztu w dolarach.
  4. Ocena odporna na modyfikacje. Suma kontrolna SHA-256 dla prawdziwych danych zapobiega najczęstemu, niewidocznemu błędowi w systemach samodoskonalących się – mianowicie modyfikowaniu odpowiedzi w celu lepszego wyglądu wyników. Jeśli sumy kontrolne się nie zgadzają, nic się nie uruchamia.
  5. Wiedza, która się gromadzi. Lekcje wyciągnięte z niepowodzeń pozostają dostępne przy kolejnych uruchomieniach, dzięki czemu każde nowe wezwanie Loop 1 rozpoczyna się mądrzej niż poprzednie i nie musi uczyć się od nowa.
  6. Producent i konsument pozostają odseparowani. Dzięki kolejce plików to, co tworzy listę z zadań do wykonania, jest oddzielone od procesu oceny. Kolejkę można zawiesić, wznowić, przeanalizować lub odtworzyć, podczas gdy kod agenta pozostaje nietknięty.
  7. Eskalacja do człowieka to priorytetowy stan. Decyzje typu refer trafiają do pliku human_queue.json i są uznawane za zatwierdzone. Pętla nie próbuje ponownie rozwiązać problemu, który powinien rozstrzygnąć człowiek.
  8. Próby ponawiane są celowe. Informacja po nieudanej próbie wskazuje, co poszło nie tak, na przykład „powiedziałeś ‘zmień’, ale prawidłową odpowiedzią jest ‘skorzystaj z odniesienia’, sprawdź ponownie czynniki ryzyka”. Dzięki temu postępy są szybsze niż przy losowym ponownym próbkowaniu.

Jakie to kosztuje

  1. Tokeny mnożą się szybko. Pięć rund metodą wspinaczki po wzgórzu podczas 16 aplikacji treningowych wymaga co najmniej 80 rund Loop 1, plus operacje refleksji i audytu. Jedna runda z flagą --mode improve pochłonęła 1,83 dolara z budżetu wynoszącego 2,00 dolara. Przekroczenie budżetu nie jest przypadkiem wyjątkowym – to normalny scenariusz zakończenia procesu.
  2. Opóźnienia są rzeczywiste. Przetworzenie jednej aplikacji przez cały stack może zająć od 30 do 60 sekund, a 16 aplikacji w trybie event-loop – kilka minut. To wystarcza do pracy zbiorczej, ale nie nadaje się do zastosowań interaktywnych.
  3. Niedeterminizm ukrywa błędy. Pętla 1 wykorzystuje żywy model, więc ten sam prompt w tej samej aplikacji może doprowadzić do różnych sekwencji narzędzi. Błąd może się nie powtórzyć, a wiarygodne wartości dokładności wymagają kilku prób oceny.
  4. Przygotowanie jest skomplikowane. Przed pierwszą próbą potrzebny jest stan wykonywania, kolejka z zadaniami, historyczne decyzje wraz z checksumami, plik z najlepszymi promptami oraz lista lekcji. To o rzęd wielkości więcej infrastruktury niż pojedyncze wywołanie agent.invoke().
  5. Przeładowanie modelu nigdy nie znika. Bez mechanizmu audytu metoda wspinaczki po wzgórzach ostatecznie generuje prompt, który zapamiętuje odpowiedzi z treningu, osiągając perfekcyjne wyniki na treningu, ale wyniki bliskie przypadkowości na testach. Audyt jest jedyną barierą między samodoskonaleniem a oszukiwaniem samego siebie.
  6. Kognitywna rezygnacja to prawdziwy sposób awarii. Im bardziej niezawodny staje się pętla, tym mniej ktoś czyta jej wyniki, a gdy przestajemy czytać, przestajemy zauważać, że pętla zaczęła awariować w nowy sposób. Pętle pozostawione bez nadzoru wymagają zewnętrznego monitorowania, ponieważ pętla sama nie może sprawdzić swojej poprawności semantycznej.
  7. Debugowanie obejmuje całą strukturę programu. Błędna decyzja może wynikać z błędnej refleksji w Pętli 4, pominięcia reguły w Pętli 2, narzędzia, które nie zwróciło żadnych danych w Pętli 1, lub uszkodzonego stanu działania w Pętli 3. Śledzenie awarii w czterech nawarstwionych pętlach jest trudniejsze niż śledzenie jej w pojedynczej funkcji.

Czy w ogóle warto budować pętle?

Przegląd konfiguracji pętli publicznych daje jasne wskazówki: wybieraj zadania pętlowe, które są powtarzalne, poddawalne ocenie i o wysokiej wartości, z wbudowanym sprawdzaczem, a we wszystkich innych przypadkach wystarczy zadać prośbę tylko raz. Sprowadza się to do dwóch pytań.

  1. Czy praca się powtarza, czy wymaga wielu kroków, których nikt nie może przewidzieć? Jeśli wystarczy jedna prośba, pętla dodaje zbędny obciążenie i nie zapewnia żadnej architektury.
  2. Czy agent może sam ocenić, czy skończył? Bez testu, określonego formatu lub wzorcowych odpowiedzi nic nie zakończy pętli, a ona będzie się błądzić.

Oba odpowiedzi muszą być twierdzące. Jeśli choć jedna będzie przecząca, pomiń pętlę.

Odpowiednie zastosowania

Prace powtarzalne o dużym obrocie. Szesnaście wniosków ubezpieczeniowych, 500 plików kodu, 1000 ticketi obsługi klienta. Pętla zwraca koszt swojej konfiguracji, gdy zadanie jest wykonywane wiele razy, a nie tylko raz.

Istnieje deterministyczna metoda weryfikacji wyniku. Etykiety odpowiadające prawdzie, zestaw testów, walidator schematu lub lista binarnych zasad „przejdź/nie przejdź”. Sędzia nie powinien korzystać z modelu, ponieważ sędzia oparty na modelu wprowadza własną niepewność.

Agent może sprawdzić własną pracę. SDK Agent firmy Anthropic określa to jako najmniej doceniany krok. Funkcja is_correct() służy do tego celu, a w agencie programistycznym zadanie to pełni zestaw testów. Jeśli agent nie może sprawdzić swojego wyniku, pętla nie zostaje zamknięta.

Dane z etykietami lub wielokrotnie używalne kryteria sukcesu. Metoda wspinaczki po wzgórzu wymaga podziału danych na treningowe i testowe z znanymi odpowiedziami. Bez tego można analizować niepowodzenia, ale nie da się stwierdzić, czy ta analiza była pomocna.

Nikt nie obserwuje każdego przebiegu. Pomyśl o partiach akceptacji wniosków przetwarzanych przez noc, zaplanowanych audytach lub procesach przetwarzania dokumentów, w których nikt nie znajduje się wewnątrz wewnętrznego pętli, a taka konstrukcja ma właśnie tak działać. Jeśli ktoś może natychmiast przejrzeć każdy wynik, bezpośrednie instrukcje są szybsze.

Koszt każdego przebiegu oceny jest akceptowalny. Jeden cykl optymalizacji jest wyceniany jak pełne przetworzenie każdego zastosowania treningowego. Jeśli to jest nie do przyjęcia, to samo dotyczy Loop 4, więc należy go uruchamiać rzadziej lub na mniejszym zbiorze danych treningowych.

Potrzebujesz liczb. Funkcja measure_gain() istnieje, ponieważ stwierdzenie „system się poprawił” musi zostać skwantyfikowane. Jeśli interesariusze chcą konkretnych danych, inżynieria pętli je dostarcza. Jeśli wystarczy stwierdzenie „wygląda lepiej”, to jest to nadmiarowość.

Słabe dopasowania

Jeden prompt wystarczy do tego. Przekształcanie pliku CSV, wydobycie daty z dokumentu, streszczanie akapitu. W tych przypadkach liczba kroków jest przewidywalna, więc napisz skrypt lub jeden prompt. Użycie pętli w takich sytuacjach to zbędna złożoność dla prostego problemu.

„Gotowe” to kwestia subiektywnego gustu. Zadanie typu „Uczynij ten e-mail bardziej profesjonalnym” nie ma mechanizmu weryfikacji binarnej, a dodanie drugiego modelu do oceny estetyki wprowadza dodatkową zmienność bez rozwiązania problemu. Najpierw określ jasno cel – na przykład mniej niż 100 słów, profesjonalny ton, brak wykrzykników – dopiero potem można to sprawdzić.

Działania są nieodwracalne bez sprawdzenia. Wysyłanie e-maili, łączenie prośb o zmiany, usuwanie rekordów, przekazywanie pieniędzy. Ściśle kontroluj uprawnienia i wymagaj pauzy przed usunięciem, wysłaniem lub opłaceniem czegokolwiek. Pętle umożliwiające wykonywanie nieodwracalnych działań bez żadnych kontrol są źródłem przyszłych problemów.

Budżet nie może pokryć kosztów oceny. Jeśli 2,00 dolarów to mała kwota, procesy związane z optymalizacją zużywają ją, zanim zostaną przetworzone wszystkie aplikacje szkoleniowe. System radził sobie z tym za pomocą błędu BudgetExhaustedError, ale w przypadku ograniczonego budżetu prawdziwym rozwiązaniem jest mniejszy zbiór danych do szkolenia, mniej rund lub brak możliwości wykonania etapu 4, dopóki nie będzie można za to zapłacić.

Brak trwałości stanu. Jeśli nie można zapisać stanu działania, lekcji oraz kolejki na dysku lub w bazie danych, nie można kontynuować pracy po awarii, gromadzić lekcji między sesjami ani chronić się przed wyczerpaniem budżetu w trakcie działania. Środowiska bez stanu wymagają innej architektury.

Nadal eksplorujesz. Jedno wezwanie do agenta przy użyciu odpowiednich narzędzi pokazuje, gdzie agent popełnia błędy, podczas gdy pętle nawarstwione pokazują jedynie, gdzie zawodzi cały system. Najpierw sprawdź pojedynczego agenta, a warstwy dodawaj dopiero wtedy, gdy poznasz jego własne sposoby awarii.

Ostatnia zasada pochodzi od Rhysa Sullivana, który ostrzega przed dwoma skrajnościami: pętlami „tokenmaxxing”, które działają wyłącznie po to, by zwiększyć wydatki, oraz prostymi, naiwnymi zdaniami typu „promptchud”. Pętla ma sens, gdy reprodukuje rzeczywiste błędy produktu w realistycznych warunkach testowych, a traci na znaczeniu, gdy wygląda jedynie imponująco.

Osiem cech skutecznej pętli

Pętle zazwyczaj przestają działać w mało atrakcyjny sposób. Nigdy się nie zatrzymują, wyczerpują budżet lub zwracają bezużyteczne wyniki oznaczone jako ukończone. Przegląd publicznych konfiguracji pętli wykazał, że w każdej pętli, która działa niezawodnie, występuje osiem cech, a każda z nich odpowiada decyzji projektowej w tym kodzie.

1. Cel poddający się weryfikacji

Zdefiniuj pojęcie „ukończone”, aby maszyna mogła je sprawdzić, a nie po to, by człowiek miał satysfakcję. „Wydać dobrą decyzję o akceptacji” nie daje możliwości weryfikacji. Funkcja is_correct(decision, app_id) porównuje decyzję z wcześniejszymi przykładami – albo się zgadza, albo nie. Bez celu poddającego się weryfikacji pętla nigdy nie wie, kiedy przestać, i będzie zwracać pewne siebie, dopracowane, lecz błędne wyniki jako ukończoną pracę. Cel musi być predykatem binarnym, a nie opisem jakości. Podział na dane treningowe i testowe zapewnia, że rozwiązanie będzie ogólne, a nie oparte wyłącznie na memorowaniu przeszłych przypadków.

2. Jasne zatrzymanie

Max zawsze próbuje przestrzegać budżetu lub limitu czasu, bez żadnych wyjątków. Nigdy nie zakładaj, że model sam się zatrzyma. Konfiguracje typu „grassroots”, takie jak pętla while :; do ... done, nie zawierają nic po stronie modelu, co mogłoby je zakończyć, więc działają aż do chwili, gdy ktoś zakończy proces. SDK OpenAI zawiera parametr max_turns, ponieważ doświadczenie pokazało, że modele nie zawsze samodzielnie się zatrzymują. Ten kod posiada trzy mechanizmy zatrzymywania na trzech poziomach: MAX_STEPS = 8 w Pętli 1 zapobiega nieskończonym łańcuchom operacji, max_retries = 3 w Pętli 2 zapobiega nieskończonym próbom, a BUDGET_USD = 2.00 poprzez CostMeter stanowi ostateczną barierę bezpieczeństwa. Po osiągnięciu limitu kroków Pętla 1 zwraca wartość ran_out_of_steps, gdy skończą się próby – Pętla 2 zwraca passed=False, a gdy wyczerpie się budżet, błąd jest przekazywany dalej i zatrzymuje całość.

3. Dobre narzędzia

Działania dostępne dla agenta muszą być niezawodne, jasno opisane i mieć precyzyjnie określony zakres. Sam cykl jest prosty, a praca odbywa się w narzędziach. Zgodnie z zaleceniami Anthropic, zestawy narzędzi oraz ich dokumentacja muszą być starannie zaprojektowane, ponieważ niejasny opis prowadzi do niejasnych wywołań, a zbyt potężne narzędzie powoduje błędy nieodwracalne. Tutaj znajdują się cztery narzędzia, z których każde pełni jedną funkcję: czyste pobieranie danych bez efektów ubocznych, wyszukiwanie według słów kluczowych w statycznej bazie wiedzy, symulowane pobieranie danych z zewnętrznych źródeł oraz strukturyzowana akcja w terminalu, która wymusza stosowanie określonego schematu (Literal["accept", "modify", "decline", "refer"]), wraz z uzasadnieniem i wyraźnymi flagami. Bez tego ostatniego elementu agent zwraca tekst, który wymaga delikatnego analizowania.

4. Pamięć

Zachowaj historię, ale streszcz ją, aby kontekst nie był zbyt obszerny, a w przypadku rozmów trwających dłużej niż jedna, zapisz stan na dysku. Narzędzie Anthropic’s long-running-agent harness umożliwia agentowi odczytywanie wcześniejszego stanu z plików git oraz plików postępów, realizowanie postępów krok po kroku i pozostawianie środowiska w czystym stanie przed kolejną iteracją. Surowe okno kontekstowe nie przetrwa restartów, awarii ani ograniczeń rozmiarowych. Tutaj działają dwa mechanizmy w różnych skalach czasowych. W ramach jednego uruchomienia funkcja add_messages z LangGraph gromadzi całą rozmowę z narzędziem dla danej aplikacji. Pomiędzy uruchomieniami lista lessons w pliku run_state.json gromadzi streszczone zasady z wszystkich aplikacji i wszystkich restartów. Funkcja distill_lesson() służy do kompresji: przechowuje pojedynczą, uogólnialną zasadę, a nie surowy zapis błędu, dzięki czemu lista pozostaje krótka, a wprowadzany kontekst łatwy do zarządzania.

5. Odrębny sprawdzacz

Generowanie, ocenianie, korygowanie, powtarzanie – twórca nigdy nie ocenia własnej pracy. SDK Anthropic określa weryfikację pracy jako najmniej doceniany krok, ale musi ona być niezależna od procesu generowania. Agent, który ocenia sam siebie, staje się osobą gotową na przytaknięcie, a powierzenie drugiemu agentowi wyłącznie zadania oceny zapobiega temu. Ten zestaw kodów zawiera trzy niezależne poziomy sprawdzania:

  • Sprawdzenie według rubryk (struktura). RubricMiddleware sprawdza dziewięć reguł na podstawie przetworzonych wyników za pomocą czystych predykatów logicznych, bez użycia modelu.
  • Sprawdzenie przez sędziego (poprawność). is_correct() porównuje odpowiedź z zapisaną etykietą przy użyciu dokładnego porównania ciągów znaków.
  • Sprawdzenie audytowe (zapobieganie oszustwom). audit_prompt() szuka zapamiętanych odpowiedzi za pomocą klasyfikacji modelu binarnego, a nie oceny jakości.

Kryteria oceny oraz sędzia są w pełni deterministyczni. Audyt wykorzystuje model, ale jedynie do pytania strukturalnego typu binarnego („czy zawiera to identyfikatory aplikacji?”), w kategorii tej sędziowie opierający się na modelu są najbardziej niezawodni.

6. Plan dla zadań złożonych

W przypadku dużych, wieloetapowych zadań agent powinien najpierw napisać plan przed podjęciem działań, natomiast przy zadaniami prostych można go pominąć. Badania Anthropic nad agentami działającymi przez długi czas wykazały, że kluczowe jest praca nad jedną cechą naraz, ponieważ pętla polecająca naprawę wszystkiego jednocześnie powoduje problemy, podczas gdy podejmowanie jednej zmiany na etapie daje lepsze wyniki. W tym kodzie prompt określa kolejno cztery kroki wykorzystywane narzędziami, co stanowi wbudowany plan: pozyskanie danych, pozyskanie zasad, odczyt sygnałów z zewnątrz oraz podjęcie decyzji. Taka kolejność zapobiega temu, by agent od razu podejmował decyzję bez sprawdzenia dowodów, a limit ośmiu kroków jest dostosowany tak, by uwzględnić całą sekwencję plus jedną próbę ponowną.

7. Rejestracja działań

Zapisuj każdą myśl, działanie i wynik, aby podczas sesji naprawy o 3 nad ranem mieć co czytać. Jeśli jakiś krok nie został zarejestrowany, to z punktu widzenia naprawy nie miał miejsca. Błędy w pętlach nawzajem zawartych są niemal niemożliwe do zdiagnozowania bez rejestrów: która pętla, która aplikacja, który próba, który narzędzie zwróciło błędne dane? Trzy poziomy rejestrowania pomagają w tym. Linie postępu, takie jak [loop3] 3/16 APP-009 ..., są aktualizowane, co jest niezbędne w systemie Windows, gdzie stdout jest buforowany. Linie decyzji zawierają decyzję, status oraz czas trwania działania w jednej linii na aplikację. Linie informacyjne potwierdzają, że wydobywanie danych nastąpiło.

8. Znajomość kosztów

Pętle szybko zużywają tokeny, więc zacznij od małych, ograniczonych skali, a następnie je rozszerzaj, obserwując pierwsze kilka prób przed pozostawieniem pętli bez nadzoru. Jedna tura wspinaczki po 16 zastosowaniach treningowych, obejmująca pięć rund plus refleksję i weryfikację, kosztuje prawie 2,00 dolara. Dopiero po poznaniu kosztu na jedną iterację możesz ustalić budżet i odejść. CostMeter śledzi wydatki z dokładnością do czterech miejsc po przecinku i wywołuje BudgetExhaustedError zanim nastąpi przekroczenie budżetu:

# The ordering matters:
if self.spent >= self._budget:          # check BEFORE adding
    raise BudgetExhaustedError(...)
self.spent += cost                       # add AFTER the check clears

Pętla zdarzeń również wykrywa wyczerpanie budżetu podczas przeszukiwania terenu oddzielnie od wyczerpania podczas przetwarzania, więc wyczerpanie zasobów w trakcie rundy samodoskonalenia nie niszczy reszty kolejki. Pętla reaguje na to w sposób uporządkowany: pomija kosztowny krok doskonalenia i kontynuuje przetwarzanie.

Lista kontrolna przed pozostawieniem jej bez nadzoru

Zanim zdecydujesz się pozostawić jakąkolwiek pętlę bez nadzoru, upewnij się co do następujących kwestii:

  1. Cel jest możliwy do weryfikacji przez maszynę (tutaj is_correct() w porównaniu z prawdą obiektywną).
  2. Na każdym poziomie istnieje sztywne ograniczenie (MAX_STEPS, max_retries, BUDGET_USD).
  3. Narzędzia są dobrze zdefiniowane i udokumentowane (cztery narzędzia, po jednym do każdego zadania).
  4. Stan aplikacji przetrwa restarty (run_state.json wraz z listą lekcji).
  5. Kontroler jest oddzielny od generatora (middleware do oceny wraz z deterministycznym sędzią).
  6. Zadania złożone mają plan ukryty lub jawny (sekwencja czterech kroków opisana w instrukcjach).
  7. Kluczowe kroki są rejestrowane i usuwane (linie postępu [loop3]).
  8. Ustala się budżet i monitoruje go od pierwszego wezwania (jeden CostMeter współdzielony we wszystkich pętlach).

Jeśli pominie się którykolwiek z tych elementów, pętla zawiedzie w środowisku produkcyjnym – nie zawsze od razu, ale na pewno w końcu. Większość awarii w środowisku produkcyjnym wynika z braku kroku weryfikacji, braku warunku zatrzymania lub braku zapisanego stanu. Najpierw należy to naprawić, a dopiero potem dostosować wszystko inne.

Literatura pokrewna

  • Playwright i TypeScript bez znajomości TypeScript: inżynier, warstwa AI i cztery bramy — Jak inżynier QA z doświadczeniem w Selenium i C# stworzył framework Playwright w TypeScript z wykorzystaniem AI: struktura, zasady architektury, umiejętności, elementy łączące oraz czterostopniowy proces obsługi zleceń.
  • Inżynieria pętli dla agentów AI: pięć części, cztery kształty i budowa w LangGraph — Jak celowo zaprojektować pętlę działającą za agentem: jej pięć części, cztery typowe kształty, agent badawczy do weryfikacji i ponawiania prób w LangGraph, mechanizmy ochronne przed błędami oraz sytuacje, gdy nie należy stosować pętli.
  • Samodoskonalące się agenty: naprawa, uczenie się i kontrolowane zmiany — Architektura agentów, które odzyskują funkcjonalność w trakcie działania, uczą się podczas kolejnych wykonywań i zmieniają się jedynie poprzez sprawdzone wersje, wykorzystując LangGraph, MongoDB i Phoenix.
  • Od pętli agentów do grafów procesów: skategoryzowany stan, bramy dowodowe i ograniczone odzyskiwanie funkcjonalności — Dlaczego agenty działające przez długi czas przekraczają możliwości pojedynczej pętli oraz w jaki sposób wyraźne węzły, krawędzie, skategoryzowane artefakty, zweryfikowane przejścia i uprawnienia dla poszczególnych węzłów sprawiają, że są one niezawodne.
  • Zapisywanie pętli, które uruchamia agent AI w aplikacji mobilnej — Cztery pytania, na które musi odpowiedzieć każda pętla agenta, sześć pętli nawarstwionych od kliknięcia do przetworzenia danych oraz pięć sposobów, w jaki kontrola może potwierdzić ukończenie zadania bez wykrycia żadnych błędów.
  • Inżynieria pętli: Budowanie pętli agenta za pomocą weryfikatorów i reguł zatrzymywania — Praktyczny przewodnik po tworzeniu czterech warstwowych pętli agenta: podstawy ReAct, narzędzia oceny według kryteriów, routowanie w LangGraph, umowy pętli, wyzwalacze zdarzeń, metoda wspinaczki po wzgórzu oraz zasady ochrony przed awariami.