Головна / Статті

Інженерія циклів: чотири вкладені цикли для самовдосконалюваних AI-агентів

Інженерія циклів для самовдосконалюваних AI-агентів: чотири вкладені цикли (ReAct, повторна спроба, опитувач черги, підйом по пагорбу) плюс суддя, метр бюджету та перевірка на зміни.

Видавець: REACTAPP.TOP32 хв читання7374 слів

Джерела: Loop Engineering: Building Self-Improving AI Agents with Four Nested Loops

Протягом приблизно двох років робота з моделлю означала чергування. Інженер пише запит, читає отриману відповідь та вручну додає наступний фрагмент контексту. Ця процедура має приховані витрати — своєрідний «податок на організацію», адже швидкість усього процесу обмежується темпом, з яким людина може переглядати інформацію. Щоб подолати це, потрібно змінити фокус: припинити керувати інструментом та почати проектувати систему, яка буде ним керувати.

Три ідеї формулюють цю зміну.

  • Вплив піднімається на вищий рівень. Роль зі створення точних інструкцій змінюється на проектування циклів, які повторюються, а зрештою — на роль керівника. Людина визначає мету та умови зупинки, а агент відповідає за кожен крок на шляху між ними.
  • Автономні ланцюги мають чітко визначену структуру. Вони знаходять завдання, передають їх підагентам, перевіряють результати за допомогою незалежних тестів та фіксують свій стан, що дозволяє уникнути участі людини у процесі сортування, виправлення та підтвердження результатів.
  • Проєктування цілей знаходиться на вершині. Тут внутрішні механізми ланцюга відходять на другий план. AlphaEvolve, який побив рекорди множення матриць, що існували з 1969 року, та Kiro від Amazon, який перетворює високорівневі цілі на вимоги та технічні специфікації, демонструють цю закономірність: якщо правильно сформулювати функцію оцінки, машина знаходить відповідь.

Щоб створити цикл навколо агента, потрібна чітка ментальна модель цього агента всередині нього. Тож логічний порядок розробки починається з простого працівника, який викликає інструменти, додається друга модель, яка оцінює його результати (щоб система не оцінювала власні роботи), обидві моделі під’єднуються у цикл, а отримані результати зберігаються на диску, адже моделі забувають, а репозиторій — ні. Після цього йдуть заходи захисту від маніпулювання метриками агентом, подібні до шахрайства, яке було виявлено під час тестування o3 від OpenAI за допомогою METR, потім метрика, яка відстежує витрати в середовищі, яке може бути багатим на токени або бідним на них, і нарешті перехід від скрипту з 70 рядків до чогось, що може функціонувати в реальних умовах.

Далі наведено функціональну реалізацію всієї цієї ідеї з використанням процесу схвалення страхових полісів як тестової платформи.

Чому це є проблемою архітектури

Перехід від моделі, яка відповідає на запитання, до агента, який надійно виконує дії, — це не завдання з написання запитів. Це проблема проектування. Більшість навчальних матеріалів показують одного агента, який використовує один інструмент. Реальні системи потребують стійкості до збоїв, самокорекції, обмежень на витрати, виявлення змін та постійного вдосконалення одночасно.

Підхід, який тут використовується, називається інженерією циклів: вкладаються кілька контрольних циклів, кожен з окремою функцією, щоб вся система працювала краще, ніж будь-який окремий цикл.

Страхування є зручною сферою для досліджень. Рішення приймаються з невеликого фіксованого набору варіантів (прийняти, змінити, відхилити, направити), правила є чіткими, а історичні дані надають приклади правильних та неправильних відповідей. Ті самі принципи застосовуються під час перегляду коду, аналізу документів, сортування запитів підтримки та обробки медичних записів.

Чотири цикли, один всередині іншого

Звичайне програмне забезпечення контролює складність за допомогою шарів абстракції. Інженерія циклів контролює ненадійність агента за допомогою шарів зворотного зв’язку:

┌──────────────────────────────────────────────────────┐
│  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)   │  │  │  │
│  │  │  └────────────────────────────────────┘  │  │  │
│  │  └──────────────────────────────────────────┘  │  │
│  └────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────┘

Кожен цикл має одну функцію, одну проблему, від якої він захищається, та один сигнал зворотного зв’язку, на який реагує. Важливо дотримуватися дисципліни, щоб проблеми не поширювалися між циклами.

Домен

Перш ніж розглядати архітектуру, ось дані. Заява на страхування — це простий запис:

{
  "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
}

Кожен із 20 зразкових додатків також має еталонну мітку:

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

Рішення є одним із чотирьох варіантів:

  • прийняти: стандартний ризик, схвалити як є
  • змінити: схвалити за певних умов або з додатковими зауваженнями
  • відхилити: ризик перевищує допустимий рівень
  • передати на розгляд: справу має переглянути старший експерт з оцінки ризиків

Невелика база знань містить п’ять прихованих правил оцінки ризиків, які стосуються зон повеней, історії заявок, кредитного рейтингу, максимальних обсягів страхування та обмежень, характерних для окремих штатів. Агент не отримує інформацію про ці правила у своїх інструкціях; йому потрібно знаходити та застосовувати їх через виклики інструментів. Для навчання використовується шістнадцять заявок, а чотири залишаються для тестування.

Цикл 1: агент ReAct

Захищає від: моделі, яка приймає рішення без доказів. Сигнал зворотного зв’язку: граф викликів інструментів, що вказує, чи агент ознайомився з усіма відповідними даними перед прийняттям рішення.

ReAct, скорочення від reason plus act, є базовою схемою. Агент розмірковує про те, що йому потрібно, викликає інструмент, читає результат, знову розмірковує та повторює цей процес, поки не прийме рішення:

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)

У LangGraph це перетворюється на граф станів із двома вузлами та умовною діагоналлю. Стан містить ID додатку, список повідомлень, поля для прийняття рішень, уроки, поточні витрати та лічильник кроків:

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

Одна деталь заслуговує уваги. Анотація add_messages змушує LangGraph додавати нові повідомлення до списку на кожному кроці замість того, щоб замінювати його, тож агент зберігає всю розмову протягом кожного виклику інструменту для одного додатку.

Будівельник бере системний запит та спільний метр витрат та прив’язує інструменти до моделі:

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

Усередині нього знаходяться вузол агента, вузол інструментів, функція маршрутизації та збірка графа:

    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()

Чотири інструменти

Агент отримує рівно чотири інструменти, що достатньо для виконання всіх необхідних завдань:

@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 лише читає дані. retrieve_rules виконує пошук за ключовими словами у фіксованому наборі правил, що змушує до прямого пошуку. fetch_risk_signal імітує отримання зовнішніх даних, таких як інформація від кредитних агентств чи карти повеней. draft_decision є кінцевою дією та має найвище значення. Вона змушує агента ухвалити конкретне рішення з обґрунтуванням та специфічними позначками, тож структурований результат формується безпосередньо з виклику інструменту, і нікому не потрібно аналізувати вільний текст.

Чому важлива межа кількості кроків

MAX_STEPS = 8 запобігає нескінченним ланцюгам викликів інструментів. Збентежена модель може двадцять разів викликати функцію пошуку правил для слова „повінь“, намагаючись знайти відповідь, якої немає, тим самим витрачаючи токени та час. Коли кількість кроків досягає цієї межі, стан отримує рішення ran_out_of_steps, яке може бути розпізнане та перепробоване у циклі 2.

Цикл 2: перевірка та повторна спроба

Захищає від: однієї неправильної відповіді, спричиненої помилковою логікою. Сигнал зворотного зв’язку: детерміністичний оцінювач is_correct(), без участі моделі.

Цикл 1 може зазнати невдачі. Агент може неправильно прочитати позначку, пропустити правило або досягти ліміту кроків. Цикл 2 виявляє ці проблеми та дає агенту ще одну спробу з цілеспрямованим зворотним зв’язком:

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

Кілька дизайнерських рішень варто детальніше пояснити.

Суддя є детерміністичним. Функція is_correct() читає файл історичних рішень та порівнює рядки. Вона ніколи не викликає модель. Використання другої моделі як судді додало б ще одне джерело ненадійності, а тут істина просто є істиною.

Перенесення справи — це завершений результат. Коли агент використовує команду refer, цикл 2 записує справу до файлу human_queue.json та повідомляє про успіх. Передача справи людині вважається правильним кінцевим станом, а не причиною для повторної спроби.

Зворотний зв’язок вказує на правильну відповідь. Замість фрази „спробуйте ще раз“ повідомлення повідомляє агенту, яка категорія рішення є правильною, та просить його ще раз переглянути фактори ризику. Це спрямовує наступну спробу на правильну відповідь, на відміну від сліпого повторного вибору даних, який може привести до помилок.

Історія передається далі. Додаткові повідомлення включають усю попередню розмову та відгуки, тож під час наступної роботи агент бачить усе, що вже намагався зробити, і менш схильний повторювати ту саму помилкову стратегію.

Цикл 3: збирач черги подій

Захищає від: тісного зв’язку між тими, хто створює роботу, та тими, хто її виконує. Сигнал відгуку: вміст файлу pending_applications.json, звичайного файлу, який виконує функцію черги.

Цей цикл є оркестратором. Він бере дані з черги, пропускає одну заявку через цикли 1 та 2, оновлює статус виконання та час від часу запускає цикл 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

Чому файл для черги

Черга — це файл у форматі JSON, а не потік Redis чи черга SQS, і це робиться навмисно, тому що для локальної розробки її легко перевіряти (відкрити файл та побачити, що знаходиться у очікуванні), легко відновлювати (перезапустити після збою, і черга залишиться недоторканою), а також вона є відокремленою (той, хто її заповнює, не потребує знань про споживача). У продакшн-системі був би використаний належний посередник повідомлень, а інтерфейсний контракт у вигляді списку ID не змінився б.

Виявлення змін

Перш ніж обробляти щось, Loop 3 перевіряє початкові дані:

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

Перевірка працює так:

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."
        )

Хеш SHA-256 файлу історичних рішень зберігається у стані виконання під час ініціалізації та перевіряється щоразу, коли починається цикл подій. Якщо хтось змінює оригінальні дані, щоб полегшити роботу агента, система відмовляється працювати. Це закриває приховану лазівку в системах самопокращення: найдешевший спосіб підвищити оцінку — це змінити сам механізм оцінювання.

Пропуск проти зупинки

Конструкція try/except у Циклі 3 чітко визначає межі. Коли вичерпується бюджет, все зупиняється. Будь-яка інша виняткова ситуація фіксується, проблемна програма видаляється з черги, а обробляється наступна. Таким чином одна несправна програма не може заблокувати весь процес, тоді як вичерпання коштів є фатальним за принципом проектування, адже система не повинна продовжувати роботу, коли немає чого витрачати.

Цикл 4: підйом на пагорб

Захищає від: фіксованого запиту, який ніколи не навчається на основі досвіду. Сигнал зворотного зв’язку: точність навчання разом із вердиктом аудиту.

Кожні вісім рішень (HILL_CLIMB_EVERY = 8), Луп 3 передає обов’язки Лупу 4, який виконує цикл роздумів, мутацій, оцінки та фільтрації для автоматичного вдосконалення системного запиту:

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

Замисліться

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()

Інструкція про те, щоб не запам’ятовувати конкретні ідентифікатори додатків чи деталі заявників, має велике значення. Без неї модель могла б створювати запити на кшталт „для APP-003 завжди відхиляти“, які ідеально працюватимуть на даних навчання, але повністю зазнають невдачі у випадках, яких вона не бачила. Новий запит має узагальнювати на основі закономірностей у невдачах, а не окремих випадків.

Оцінка

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

Кожен запит кандидата проходить через цикл 1 у всіх 16 додатків для навчання. Це дорого — 16 запусків моделі за раунд, що дає щонайменше 80 запусків протягом п’яти раундів, але сигнал є чесним: запит має працювати з усіма даними навчання, а не лише з окремим зразком.

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

Перш ніж кандидата буде прийнято, мають бути дотримані обидві умови. По-перше, score > best_score — тобто його результат має бути суворо кращим за найкращий наразі. Рівні результати відхиляються, що запобігає зміщенню запиту. По-друге, аудитор має повернути VERDICT: CLEAN, підтверджуючи, що кандидат не містить жодних заздалегідь встановлених відповідей.

Аудитор запиту

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

Це схема використання LLM як судді, але обмежена одним бінарним структурним запитанням: чи містить запит конкретні ID чи запам’ятовані відповіді? Саме у таких випадках судді у вигляді моделей є надійними. Аудитор ніколи не оцінює якість, стиль чи правильність — він лише шукає певну закономірність.

Пам’ять уроків

Окрім алгоритму підйому по схилу, система зберігає список lessons, який залишається актуальним між кожними запусками. Якщо додаток все одно зазнає невдачі після кожної спроби, він викликає функцію 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()

Кожен урок потім додається до кожного наступного запуску Loop 1 через 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}"

Це навчання з кількома прикладами без градієнтного спуску. Система вчиться на помилках під час роботи, збираючи стиснені знання у вигляді правил природної мови. Функція у стилі протоколу CL-BENCH перевіряє, чи справді ці уроки допомагають:

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

Контроль бюджету за допомогою лічильника витрат

Кожен виклик моделі проходить через один спільний CostMeter, який передається від Loop 4 до Loop 3, потім до Loop 2 та нарешті до Loop 1, тож усі витрати накопичуються в одному місці:

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

Ключовий момент — це порядок дій: перевіряйте бюджет перед тим, як додавати витрати. Таким чином помилка виникатиме відразу при досягненні ліміту, а не після того, як вже станеться перевитрата. BudgetExhaustedError передається вгору по ієрархії циклів, і цикл 3 знову його піднімає, замість того щоб приховати, тож вичерпаний бюджет завжди призводить до завершення всього процесу.

Стан виконання з ідемпотентною налаштуванням

run_state.json фіксує все, що сталося: оброблені заявки, списки успішних та невдалих випробувань, накопичені уроки, рішення, прийняті з моменту останнього „підйому“, та контрольна сума від судді. Його ініціалізація є захисною:

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

Тут проявляються три звички стійкості. Читання з кодуванням utf-8-sig безшумно видаляє мітки порядку байтів у Windows. Порожній або пошкоджений файл повертається до стандартного стану замість того, щоб завершитися помилкою. Крім того, контрольна сума судді перераховується щоразу під час запуску, і їй ніколи не довіряють на основі попереднього запуску.

Три способи його запуску

Існує три способи використання, і кожен з них задіює лише частину стеку.

У одиночному режимі запускається лише Цикл 1, що зручно для дебаггінгу одного додатку та для перегляду того, які інструменти використовує агент та чому:

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

Режим циклу подій виконує цикли 1, 2 та 3 для кожного очікуючого застосунку та може запустити цикл 4 після кожних восьми рішень. Прапорець --once спорожнює чергу та завершує роботу замість безкінечного опитування:

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

Режим покращення запускає Loop 4 безпосередньо, з п’ятьма раундами підйому на пагорби у частині даних для тренування. Потім він оцінює найкращий запит, знайдений на основі відокремленої частини даних для тестування, та виводить отриманий приріст:

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

Одна заявка — від початку до кінця

Це повний потік даних для однієї заявки під час її обробки в системі:

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

Уроки зі створення

1. Тримайте оракула окремо від агента. Суддя (is_correct, evaluate_prompt) не має спільного коду з агентом (run_loop1). Суддя читає статичні файли, а агент викликає модель. Дозвольте моделі оцінювати власний вихід, і ви отримаєте агента, який ніколи не помиляється.

2. Робіть оцінку детермінованою. Правильність виконання завдання, точність навчання та точність тестування є детермінованими для однакових вхідних даних. Єдина випадковість полягає у відповідях моделі, тому будь-який результат можна відтворити на основі запиту та насіння.

3. Використовуйте інструмент для остаточної відповіді для структурованого виведення. Інструмент draft_decision краще підходить для обробки прозового тексту. Його схема визначає словниковий запас (accept | modify | decline | refer), обов’язкові поля (flags, rationale, recommended_premium_adj) та їхні типи, тому агент змушений бути чітким у своїх діях.

4. Захист від шахрайства є обов’язковим у системах самосвоєвдосконалення. Як тільки система може редагувати власні інструкції, вона починає шукати швидкі шляхи, а найочевидніший з них — це запам’ятовування відповідей з процесу навчання. Функція audit_prompt() автоматично виявляє це. Без неї метод «підйому по пагорбу» неминуче призводить до запитів із майже ідеальною точністю під час навчання та майже випадковою точністю під час тестування.

5. Мітки порядку байтів та кодування є помилками у продакшені. Якщо ви розробляєте на Windows або отримуєте файли від користувачів Windows, обов’язково використовуйте encoding="utf-8-sig" під час кожного читання файлу. Мітка BOM, створена програмою Notepad у Windows, спричиняє помилку json.JSONDecodeError: Unexpected UTF-8 BOM, яку важко діагностувати, оскільки файл виглядає абсолютно нормально в редакторі.

6. Перевіряйте бюджет перед тим, як витрачати гроші, а не після.

# 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

Порядок має значення, коли кілька викликів моделі відбуваються один за одним. Спочатку додайте їх, а потім перевірте — бюджет може бути перевищений на суму, еквівалентну кільком викликам, ще до того, як з’явиться помилка.

7. Очищуйте лінії прогресу у довгих циклах.

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

У Windows stdout зберігається у буфері, якщо не вказано інше. Якщо пропустити параметр flush=True, рядок „processing APP-009“ з’явиться після повернення виклику API, що скасовує сенс індикатора прогресу. Завжди очищуйте буфер рядків, які передують блокуючим операціям вводу-виводу.

8. Тримайте бюджет для алгоритму пошуку оптимуму окремо від бюджету циклу подій. Виснаження бюджету під час використання цього алгоритму має бути виявлене безпосередньо на місці виклику та не дозволяти цьому призвести до зупинки циклу подій. Після того, як вісім оцінок додатків використають більшу частину бюджету у розмірі 2 доларів, у алгоритмі залишається мало можливостей. Цикл подій спеціально фіксує помилку BudgetExhaustedError, що виникає під час роботи алгоритму, та продовжує обробку решти додатків:

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

Застосування цього шаблону в інших ситуаціях

Дизайн з чотирма циклами не прив’язаний до страхування. Щоб адаптувати його, замініть ці елементи:

  • Інструменти циклу 1. Тут: пошук, правила, сигнал, прийняття рішення. У вас: інструменти отримання даних та виконання дій для вашої сфери.
  • Актуальні дані. Тут: файл історичних рішень. У вас: ваш власний номенклатурний набір даних.
  • Оцінювач. Тут: порівняння рядків. У вас: тест на коректність, специфічний для вашої задачі.
  • Уроки. Тут: правила схвалення заявок. У вас: шаблони помилок з вашої сфери.
  • Запит для пошуку оптимального рішення. Тут: запит системи схвалення заявок. У вас: запит системи вашого агента.
  • Бюджет. Тут: 2,00 долара. У вас: сума, яка має коштувати кожен запуск.

Є одна сувора умова: суддя має бути детермінованим та незалежним від агента. Якщо для вашої задачі неможливо визначити тест на детерміновану коректність, система не зможе надійно покращувати себе.

Спробуємо

Налаштування та чотири команди:

# 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

Результати пошуку на схилах виглядають так:

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%

Початковий промпт досягає 44% під час навчання. Через п’ять раундів переможний промпт отримує 75% у тестовому наборі, що є прирістом на 25 пунктів порівняно з автоматичною оптимізацією промптів без ручної їхньої розробки.

Що дають ці цикли

Інженерія циклів — це не фреймворк. Це спосіб розділення надійності агента на шари, кожен з яких має окрему функцію та механізм зворотного зв’язку. Чотири цикли вирішують чотири окремі моди збою:

  • Цикл 1 забезпечує ґрунтування рішень: агент повинен ознайомитися з доказами перед прийняттям рішення.
  • Цикл 2 вирішує проблеми одноразових збоїв: один неправильний шлях міркувань не повинен постійно заважати роботі програми.
  • Цикл 3 забезпечує продуктивність та стійкість: одна несправна програма не повинна блокувати всю систему.
  • Цикл 4 усуває постійні помилки шляхом переписування самого запиту на основі накопичених невдач.

Зберігайте їх разом, додайте детермінованого оцінювача, захистіть усе за допомогою контролю бюджету та виявлення змін, і система з часом значно покращуватиметься без участі людини.

Що ви отримуєте

Приймайте цю концепцію з увагою. Найсуворішим застереженням з численних досліджень щодо циклів агентів є концепція „когнітивної капітуляції“ Адді Османі: надання циклу повного контролю та припинення аналізу його результатів. Переваги все одно залишаються реальними та вимірюваними.

  1. Самовдосконалення без участі людини. Метод підйому по схилу підвищив точність навчання з 44% до 75% без ручної роботи з запитами, а система самостійно витягла правильні правила оцінки зі шаблонів невдач.
  2. Ізоляція несправностей між шарами. Одна несправна програма не призводить до зупинки всієї системи. Механізм Loop 3 фіксує помилки кожної окремої програми, записує їх та продовжує роботу. BudgetExhaustedError — єдина виняткова ситуація, яка передається всередину системи, і це робиться навмисно.
  3. Прогрес, який можна виміряти та перевірити. Функція measure_gain() надає конкретні показники, порівнюючи роботу системи зі станом з мітками та без них у відокремленій групі даних. Покращення можна представити зацікавленим сторонам у вигляді відсотка та суми в доларах.
  4. Оцінка, стійка до змін. Хеш-сума SHA-256 фактичних даних запобігає найпоширенішій проблемі самопокращувальних систем — коли хтось змінює відповіді, щоб покращити результати. Якщо хеш-суми не збігаються, процес не запускається.
  5. Знання, які накопичуються. Уроки, отримані внаслідок невдач, зберігаються між запусками, тому кожен новий запуск циклу 1 починається ефективніше, ніж попередній, і не потребує повторного навчання з нуля.
  6. Виробник та споживач залишаються роз’єднаними. Завдяки черзі файлів те, що створює список завдань, є окремим від процесу обробки. Чергу можна призупинити, відновити, переглянути чи перепрограти, при цьому код агента залишається недоторканим.
  7. Людське втручання є пріоритетним станом. Рішення, отримані за допомогою refer, зберігаються у human_queue.json та вважаються прийнятими. Цикл не намагається знову виконати те, що має вирішити людина.
  8. Повторні спроби є цілеспрямованими. Інформація після невдачі вказує, що пішло не так, наприклад: «Ви сказали „змінити“, але правильною відповіддю є „звернутися“, перегляньте фактори ризику». Це дозволяє досягти результату швидше, ніж випадкове перепідбирання даних.

Яка це коштує

  1. Кількість токенів швидко зростає. П’ять раундів алгоритму підйому по схилу протягом 16 процесів навчання вимагають щонайменше 80 запусків етапу Loop 1, а також операцій аналізу та аудиту. Один запуск з параметром --mode improve спожив 1,83 долара з бюджету у 2,00 доларів. Вичерпання бюджету — це не винятковий випадок; це звичайний результат завершення процесу.
  2. Затримка є реальною. Обробка одного застосунку за всіма етапами може зайняти від 30 до 60 секунд, а 16 застосунків у режимі event-loop — кілька хвилин. Це підходить для пакетної обробки, але не підходить для інтерактивного використання.
  3. Недетермінізм приховує невдачі. Луп 1 використовує живу модель, тому один і той самий запит у одному й тому ж додатку може призвести до різних послідовностей дій інструментів. Невдача може не повторитися, а для отримання значущих показників точності потрібно кілька спроб оцінки.
  4. Налаштування є складними. Перед першою спробою потрібні стан виконання, черга запитів, історичні рішення з контрольними сумами, файл з найкращими запитами та список уроків. Це в рази більше інфраструктури, ніж один виклик agent.invoke().
  5. Переобучення ніколи не зникає. Без механізму аудиту процес підйому по схилу зрештою призводить до створення запиту, який запам’ятовує відповіді з процесу навчання, досягаючи ідеальних результатів на тестах навчання та майже випадкових результатів на тестах у реальних умовах. Аудит — це єдине, що стримує самопокращення від самообману.
  6. Когнітивна здача — це справжній спосіб збою. Чим надійнішим стає цикл, тим менше хтось читає його результати, і коли ви перестаєте читати, ви перестаєте помічати, що цикл почав збоїти по-новому. Цикли, які не контролюються, потребують зовнішнього моніторингу, адже цикл не може самостійно перевіряти свою семантичну коректність.
  7. Виправлення помилок охоплює весь стек. Неправильне рішення може виникнути через неправильне відображення в Циклі 4, пропущене правило оцінювання в Циклі 2, інструмент, який не повернув жодних даних у Циклі 1, або пошкоджений стан виконання в Циклі 3. Відстеження збою через чотири вкладені цикли складніше, ніж відстеження збою в одній функції.

Чи варто взагалі створювати цикли?

Огляд схем публічних циклів надає чіткі рекомендації: використовуйте цикли для завдань, які є повторюваними, підлягають перевірці та мають високу цінність, з вбудованою системою перевірки, а для всього іншого просто зробіть запит один раз. Усе зводиться до двох запитань.

  1. Чи є робота повторюваною, чи потребує багатьох кроків, які ніхто не може передбачити? Якщо достатньо одного запиту, цикл додає зайві витрати та не забезпечує ефективної архітектури.
  2. Чи може агент сам визначити, чи завершив він роботу? Без тесту, очікуваного формату чи зразків відповідей цикл ніколи не закінчується та продовжує працювати без кінця.

Відповіді на обидва запитання мають бути «так». Якщо хоча б одна відповідь — «ні», пропустіть використання циклу.

Підходящі випадки

Повторювана робота великого обсягу. Шістнадцять заявок на страхування, 500 файлів коду, 1 000 заявок на підтримку. Цикл окупає свою вартість тоді, коли завдання виконується багато разів, а не один раз.

Існує детерміністична перевірка готовності. Це можуть бути еталонні мітки, набір тестів, валідатор схеми чи критерії оцінки у вигляді правил «пройшло/не пройшло». Суддя ніколи не повинен використовувати модель, адже суддя, заснований на моделі, приносить у процес власну ненадійність.

Агент може перевірити власну роботу. SDK Agent від Anthropic вважає цей крок найбільш недооціненим. Функція is_correct() забезпечує це, а у кодувальному агенті — набір тестів. Якщо агент не може перевірити свій результат, цикл не закривається.

Мітковані дані чи повторно використовувані критерії успіху. Для методу підйому по схилу потрібні навчальні та тестові дані з відомими відповідями. Без них можна аналізувати невдачі, але неможливо визначити, чи допоміг цей аналіз.

Ніхто не стежить за кожним запуском. Уявіть собі пакети підтвердження заявок, які обробляються протягом ночі, заплановані аудити чи потоки документів, де немає людини, яка б працювала безпосередньо в циклі, і саме така конструкція призначена для роботи. Якщо хтось може негайно переглянути кожен результат, пряме запитування є швидшим рішенням.

Витрати на кожен запуск оцінки є прийнятними. Одна ітерація алгоритму підйому на „пагорб“ коштує так само, як і повний процес обробки кожної тренувальної заявки. Якщо це занадто дорого, то та сама ситуація із Loop 4, тож використовуйте його рідше або з меншим набором даних для тренування.

Вам потрібні цифри. Функція measure_gain() існує тому, що твердження „система покращилась“ має бути кількісно оцінене. Якщо зацікавлені сторони хочуть конкретних цифр, інженерія циклів їх надає. Якщо достатньо лише твердження „виглядає краще“, це є зайвим.

Погана адаптація

Одне запитання вирішує це завдання. Переформатування CSV-файлу, отримання дати з документа, узагальнення абзацу — у всіх цих випадках кількість кроків є передбачуваною, тому достатньо написати скрипт або одноразове запитання. Використання циклів у таких ситуаціях — це зайва складність для простої задачі.

„Готово“ — це справа смаку. Запит типу „Зробіть цей електронний лист більш професійним“ не має чіткого критерію завершення, а використання другої моделі для оцінки естетики лише збільшує невизначеність, не вирішуючи проблеми. Спочатку потрібно чітко визначити цілі — наприклад, обсяг до 100 слів, професійний тон, відсутність вигуків — тільки після цього можна перевірити результат.

Дії є незворотними без перевірки. Надсилання електронних листів, об’єднання запитів на зміни, видалення записів, переказ грошей — усе це може мати незворотні наслідки. Необхідно суворо контролювати права доступу та вимагати паузи перед будь-яким видаленням, надсиланням чи оплатою. Цикли, які дозволяють виконувати незворотні дії без контролю, є причиною майбутніх проблем.

Бюджет не може покрити процес оцінки. Якщо сума у 2,00 долара є обмеженою, процеси, пов’язані з оптимізацією, споживають її ще до обробки всіх заявок на навчання. Система вирішувала цю проблему за допомогою BudgetExhaustedError, але для ситуацій з обмеженим бюджетом реальним рішенням є використання меншого набору даних для навчання, меншої кількості ітерацій або відкладення етапу Loop 4 до моменту, коли можна буде оплатити його.

Немає підтримки постійності стану. Якщо ви не можете зберігати стан виконання, уроки та чергу на диск чи в базу даних, ви не зможете продовжити роботу після збою, накопичувати уроки між сеансами чи захиститися від вичерпання бюджету під час виконання. Середовища без стану потребують іншої архітектури.

Ви все ще досліджуєте. Одне викликання агента за допомогою хороших інструментів показує, де саме агент робить помилку, тоді як вкладені цикли вказують лише на проблеми всієї системи. Спочатку дослідіть окремий агент, а шари додавайте лише після того, як ознайомитеся з його власними способами збою.

Останнє правило від Райса Саллівана попереджає про два крайнощі: цикли "tokenmaxxing", які працюють лише для збільшення витрат, та наївні однорядкові формулювання типу "promptchud". Цикл має сенс, коли він відтворює справжні проблеми продукту в реалістичних умовах тестування, а втрачає свою доцільність, коли виглядає лише вражаюче.

Вісім характеристик ефективного циклу

Цикли зазвичай зупиняються негарними способами. Вони ніколи не припиняються, не виснажують бюджет та не повертають непотрібні дані під маркуванням „готово“. Аналіз публічних конфігурацій циклів показав, що в кожному надійно працюючому циклі є вісім характеристик, кожна з яких відповідає певному дизайнерському рішенню в цьому кодовому базисі.

1. Перевірювана мета

Визначте, що означає „готово“, щоб машина могла це перевірити, а не для того, щоб людина почувалася задоволеною. Фраза „Зробити правильне рішення щодо страхування“ є неперевірюваною. Функція is_correct(decision, app_id) порівнює поточне рішення з історичними прикладами – або вони збігаються, або ні. Без перевірюваної мети цикл ніколи не дізнається, коли зупинитися, і буде передавати вважані правильними, але насправді хибні результати як готову роботу. Мета є бінарним пропозиційним виразом, а не описом якості. Розділення на навчальні та тестові дані гарантує, що модель буде застосовувати знання не лише на основі запам’ятованого.

2. Жорстке зупинення

Max завжди намагається дотримуватися бюджету чи часових обмежень, без жодних винятків. Ніколи не припускайте, що модель зупиниться сама по собі. Структури на базі команди shell у вигляді циклу while :; do ... done не містять на боці моделі нічого, що могло б їх зупинити, тому вони працюють, доки хтось не зупинить процес, а SDK від OpenAI передбачає параметр max_turns, оскільки досвід показав, що моделі не завжди зупиняються самостійно. У цій базі коду є три рівні зупинки: MAX_STEPS = 8 у Циклі 1 запобігає нескінченним послідовностям дій, max_retries = 3 у Циклі 2 — нескінченним спробам, а BUDGET_USD = 2.00 через CostMeter є остаточним захистом. При досягненні ліміту кроків Цикл 1 повертає значення ran_out_of_steps, коли закінчуються спроби — Цикл 2 повертає passed=False, а коли закінчується бюджет, помилка поширюється вгору та зупиняє все.

3. Хороші інструменти

Дії, які доступні агенту, мають бути надійними, чітко описаними та максимально обмеженими за обсягом. Сам цикл є простим, а робота виконується за допомогою інструментів. Anthropic рекомендує ретельно проектувати набори інструментів та їхню документацію, адже нечіткий опис призводить до нечітких запитів, а занадто потужний інструмент — до незворотних помилок. Тут є чотири інструменти, кожен з яких виконує одну функцію: чисте отримання інформації без побічних ефектів, пошук за ключовими словами у статичній базі знань, імітація отримання зовнішніх даних та структурована дія в терміналі, яка змушує дотримуватися заданої схеми (Literal["accept", "modify", "decline", "refer"]), разом із обґрунтуванням та чіткими позначками. Без останнього елемента агент повертає текст, який потребує складного аналізу.

4. Пам’ять

Зберігайте історію, але узагальнюйте її, щоб контекст не ставав надто об’ємним, а для ситуацій, які тривають довше однієї розмови, зберігайте стан у диску. Механізм довготривалого виконання агентів у Anthropic дозволяє агенту читати попередній стан з git та файлів прогресу, робити поступові кроки та залишати середовище чистим для наступної ітерації. Чисте вікно контексту не зберігається після перезапуску, збоїв чи досягнення меж об’єму. Тут працюють два механізми в різних часових масштабах. Під час однієї роботи функція add_messages у LangGraph накопичує всю розмову з інструментом для одного застосування. Між роботами список lessons у файлі run_state.json накопичує узагальнені правила з усіх застосувань та всіх перезапусків. Функція distill_lesson() виконує крок стиснення: вона зберігає одне узагальнюване правило, а не сухий запис про збій, що дозволяє тримати список коротким та контекст, який вводиться, керованим.

5. Окремий перевірювач

Створювати, оцінювати, виправляти, повторювати — творець ніколи не оцінює власну роботу. SDK Anthropic вважає перевірку вашої роботи найменш оцінюваним етапом, проте ця перевірка має бути незалежною від процесу створення. Агент, який оцінює себе сам, перетворюється на слухняного виконавця, а надання другому агенту лише завдання з оцінювання запобігає цьому. У цій базі коду є три незалежні рівні перевірки:

  • Перевірка за рубрикою (структура). RubricMiddleware перевіряє дев’ять правил на основі обробленого результату за допомогою чистих булевих умов та без використання моделі.
  • Перевірка суддею (правильність). is_correct() порівнює відповідь із збереженою міткою за допомогою точного порівняння рядків.
  • Перевірка аудитором (боротьба з читерством). audit_prompt() шукає запам’ятовані відповіді за допомогою бінарної класифікації моделі, а не оцінки якості.

Критерії оцінювання та суддя є абсолютно детермінованими. Під час аудиту використовується модель, але лише для бінарного структурного запитання (“Чи містить це ідентифікатори додатків?”), у категорії, де судді-моделі є найнадійнішими.

6. План для складних завдань

Для великих, багатоетапних завдань нехай агент напише план перед виконанням дій, а для невеликих — пропустить цей крок. Дослідження Anthropic щодо агентів, які працюють протягом тривалого часу, показали, що робота над однією функцією за раз є критично важливою, оскільки цикл, який має виправити все одразу, працює неефективно, тоді як робота з однією зміною за проходження етапу призводить до результату. У цьому кодовому базі запит формулює чотири послідовні кроки використання інструментів, що функціонує як вбудований план: отримати дані, отримати правила, отримати зовнішні сигнали, ухвалити рішення. Така послідовність запобігає тому, щоб агент одразу ухвалював рішення без аналізу доказів, а ліміт у вісім кроків розрахований так, щоб включати всю послідовність плюс одну спробу повторення.

7. Логування

Записуйте кожну думку, дію та результат, щоб під час сеансу виправлення помилок о 3 годині ночі було що читати. Якщо певний крок не записаний, з точки зору виправлення помилок він не відбувся. Помилки в вкладених циклах майже неможливо діагностувати без логів: який цикл, яка програма, яка спроба, який інструмент повернув неправильні дані? Для цього існують три рівні логування. Лінії прогресу на кшталт [loop3] 3/16 APP-009 ... оновлюються, що є обов’язковим у Windows, де stdout є буферизованим. Лінії рішень містять рішення, статус та час виконання в одному рядку для кожної програми. Лінії з результатами підтверджують, що вилучення даних відбулося.

8. Розуміння витрат

Цикли швидко спалюють токени, тому почніть з невеликих обсягів та обмежень, а потім поступово розширюйте їх, стежачи за першими кількома запусками, перш ніж залишати цикл без нагляду. Одна ітерація проходження через 16 тренувальних завдань, включаючи п’ять раундів, аналіз та перевірки, коштує майже 2,00 доларів. Лише після того, як ви дізнаєтесь вартість кожної ітерації, ви зможете встановити бюджет та припинити роботу. CostMeter відстежує витрати з точністю до чотирьох знаків після коми та викликає BudgetExhaustedError ще до того, як відбудеться перевитрата:

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

Цикл подій також фіксує вичерпання бюджету під час проходження складних етапів окремо від вичерпання під час обробки даних, тому вичерпання ресурсів посеред циклу самовдосконалення не призводить до втрати решти елементів черги. Цикл працює без проблем: пропускає дорогий крок вдосконалення та продовжує обробку.

Перелік перевірок перед тим, як залишити його працювати самостійно

Перш ніж довірити циклу роботу без нагляду, переконайтеся у наступному:

  1. Мета є підтверджуваною машиною (тут — is_correct() у порівнянні з істинними даними).
  2. На кожному рівні є жорстке обмеження (MAX_STEPS, max_retries, BUDGET_USD).
  3. Інструменти мають чіткі обмеження та документацію (чотири інструменти, по одній завданню кожен).
  4. Стан системи зберігається після перезапуску (run_state.json разом із списком уроків).
  5. Механізм перевірки є окремим від генератора (мідлвейр з критеріями оцінки та детерміністичний суддя).
  6. Складні завдання мають неявний або явний план (четырьохкрокова послідовність, зазначена у запиті).
  7. Ключові кроки фіксуються та відстежуються (лінії прогресу [loop3]).
  8. Визначається бюджет, який відстежується з першого виклику (один CostMeter, який використовується у всіх циклах).

Якщо пропустити хоча б один з цих етапів, цикл зазнає невдачі у продакшені — не завжди відразу, але обов’язково зрештою. Більшість проблем у продакшені пов’язані з відсутністю кроку перевірки, умови зупинки чи збереженого стану. Спочатку вирішіть ці проблеми, а потім налаштовуйте все інше.

Пов’язана література

  • Playwright та TypeScript без знань TypeScript: інженер, шар ШІ та чотири етапи — Як інженер з тестування з досвідом роботи з Selenium та C# створив фреймворк Playwright на TypeScript із використанням ШІ: структура, правила архітектури, необхідні навички, хуки та чотириетапний процес обробки замовлень.
  • Інженерія циклів для агентів ШІ: п’ять частин, чотири форми та створення в LangGraph — Як навмисно спроєктувати цикл, який працює за агентом: його п’ять частин, чотири поширені форми, агент для досліджень із функціями перевірки та повторної спроби в LangGraph, механізми захисту від збоїв та ситуації, коли не варто використовувати цикл.
  • Агенти, які самовдосконалюються: одужання, навчання та контрольовані зміни — архітектура для агентів, які відновлюються під час виконання, навчаються під час кількох запусків та змінюються лише через перевірені версії, з використанням LangGraph, MongoDB та Phoenix.
  • Від циклів агентів до графів робочих процесів: типовий стан, брами доказів та обмежене відновлення — чому агенти, що працюють довго, не підходять для одного циклу, та як чіткі вузли, ребра, типові артефакти, перевірені переходи та дозволи для кожного вузла роблять їх надійними.
  • Опис циклів, які виконує AI-агент у мобільному додатку — Чотири запитання, на які мусить відповісти кожен цикл агента, шість вкладених циклів від дотику до обробки даних та п’ять способів, якими перевірка може показати завершення роботи без виявлення проблем.
  • Інженерія циклів: створення циклів агента з валідаторами та правилами зупинки — Практичний посібник зі створення чотирьох послідовних циклів агента: основи ReAct, системи оцінювання за шкалою, механізми маршрутизації LangGraph, контракти циклів, тригери подій, алгоритми подолання перешкод та захисні механізми від збоїв.