Инженерия циклов: четыре вложенных цикла для самосовершенствующихся ИИ-агентов
Инженерия циклов для самосовершенствующихся ИИ-агентов: четыре вложенных цикла (ReAct, повторная попытка, опрос очереди, метод подъема по склону) плюс судья, метр бюджета и проверка на нарушения.
Источники: Loop Engineering: Building Self-Improving AI Agents with Four Nested Loops
Примерно в течение двух лет работа с моделью подразумевала поочередную деятельность: инженер писал запрос, читал полученный ответ и вручную добавлял следующий фрагмент контекста. Такая процедура сопровождалась скрытыми затратами, своего рода «налогом на организацию», поскольку скорость всего процесса определялась тем, насколько быстро может просмотреть информацию человек. Чтобы преодолеть это ограничение, необходимо изменить подход: перестать использовать инструмент напрямую и начать разрабатывать систему, которая будет им управлять.
Три идеи помогают осмыслить это изменение.
- Влияние перемещается на более высокий уровень. Роль смещается от составления точных инструкций к проектированию циклов, которые их повторяют, а в конечном итоге — к роли руководителя. Человек задаёт цель и условия остановки, а агент отвечает за каждый шаг на пути между ними.
- Автономные циклы имеют узнаваемую структуру. Они находят задачи, передают их подагентам, проверяют результаты с помощью независимых тестов и фиксируют свое состояние, что позволяет человеку не участвовать в процессе сортировки, исправления и проверки.
- Проектирование целей находится на вершине. Здесь внутренние механизмы цикла отходят на второй план. AlphaEvolve, превзошедший рекорды по множению матриц, действовавшие с 1969 года, и Kiro от Amazon, преобразующий высокоуровневые цели в требования и спецификации, демонстрируют эту закономерность: если правильно сформулировать функцию оценки, машина найдет решение.
Чтобы спроектировать цикл для работы с агентом, необходимо иметь четкую мысленную модель самого агента. Поэтому логичный порядок разработки начинается с простого рабочего элемента, выполняющего вызовы инструментов, затем добавляется вторая модель, оценивающая его результаты (чтобы система не оценивала собственную работу), после чего обе модели соединяются в цикл, а полученные результаты записываются на диск, поскольку модели забывают, а хранилище — нет. Затем следуют меры защиты от манипуляций агента с метриками, подобные случаи обмана, зафиксированные во время тестирования о3 от 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
Это схема с использованием ЯИ как судьи, но ограниченная одним бинарным структурным вопросом: содержит ли запрос конкретные идентификаторы или запомненные ответы? Именно в таких случаях судейство модели является надежным. Аудитор никогда не оценивает качество, стиль или точность — он просто ищет определенный шаблон.
Память уроков
Помимо алгоритма подъема по склону, система сохраняет список 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, который передается от Цикла 4 к Циклу 3, затем к Циклу 2 и, наконец, к Циклу 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
Режим улучшения запускает цикл 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, создаваемый блокнотом 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 стандартный вывод буферизуется, если не указано иное. Если пропустить параметр flush=True, строка «обработка 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 устраняет повторяющиеся ошибки путем переписывания самого запроса на основе накопившихся неудач.
Соберите их вместе, добавьте детерминированный оценщик, защитите всё с помощью контроля бюджета и обнаружения изменений, и система со временем значительно улучшится без участия человека.
Что вы получаете
Применяйте эту конструкцию с осторожностью. Самое серьезное предупреждение из обсуждений циклов агентов — это концепция «когнитивной капитуляции» Адди Османи: передача полного контроля циклу и прекращение анализа его результатов. Преимущества при этом остаются реальными и измеримыми.
- Самосовершенствование без участия человека. Метод подъема по горе позволил повысить точность обучения с 44% до 75% без ручной работы с запросами, причем система самостоятельно извлекла правильные правила оценки из образцов неудач.
- Изоляция сбоев между слоями. Один неисправный приложение не приводит к сбою всей системы. Механизм Loop 3 фиксирует ошибки отдельных приложений, записывает их и продолжает работу.
BudgetExhaustedError— единственное исключение, которое передаётся на верхние уровни, и это сделано намеренно. - Прогресс, который можно измерять и аудитировать. Функция
measure_gain()вычисляет конкретное значение, учитывая разницу между системами с состоянием и без него в отдельной группе данных. Улучшения можно представить заинтересованным сторонам в виде процентов и денежной суммы. - Защищённая от подделок оценка. Хэш-сумма SHA-256 исходных данных предотвращает наиболее распространённую скрытую проблему самосовершенствующихся систем — изменение ответов для улучшения результатов. Если хэш-суммы не совпадают, процесс не запускается.
- Знания, накапливающиеся со временем. Уроки, извлеченные из неудач, сохраняются между запусками, поэтому каждый новый запуск цикла начинается с более глубоким пониманием ситуации, чем предыдущий, и не требует повторного изучения основ.
- Производитель и потребитель остаются разделенными. Благодаря очереди файлов тот элемент, который формирует список задач, находится отдельно от процесса их обработки. Очередь можно приостановить, возобновить, просмотреть или перепроиграть, при этом код агента остается нетронутым.
- Передача задачи человеку является приоритетным состоянием. Решения, принятые с помощью функции
refer, сохраняются в файлеhuman_queue.jsonи считаются принятыми. Цикл не пытается повторно обработать задачу, которую должен решить человек. - Повторные попытки направленны. Обратная связь после неудачи указывает на причину ошибки, например: «Вы сказали „изменить“, но правильный ответ — „обратиться к“, пересмотреть факторы риска». Это позволяет быстрее достичь результата, чем случайная пересборка данных.
Каковы затраты
- Количество токенов быстро растёт. Пять раундов алгоритма подъёма на холм за 16 запусков обучения требуют как минимум 80 запусков цикла 1, плюс операции анализа и аудита. Один запуск с параметром
--mode improveпотребовал 1,83 доллара из бюджета в 2,00 доллара. Исчерпание бюджета — не редкость; это обычный исход такого запуска. - Задержки действительно существуют. Обработка одного приложения в полном режиме может занять от 30 до 60 секунд, а 16 приложений в режиме цикла событий — несколько минут. Это подходит для пакетной обработки, но не подходит для интерактивного использования.
- Недетерминизм скрывает сбои. В цикле 1 используется динамическая модель, поэтому один и тот же запрос в одном и том же приложении может привести к разным последовательностям действий инструментов. Сбой может не воспроизводиться, а для получения значимых показателей точности требуется несколько итераций оценки.
- Настройка требует значительных усилий. Перед первой итерацией необходимы состояние выполнения, очередь задач, история принятых решений с хэш-суммами, файл с оптимальными запросами и список уроков. Это в разы больше инфраструктуры, чем один вызов
agent.invoke(). - Проблема чрезмерной адаптации никогда не исчезает. Без механизма аудита метод горного подъема в конечном итоге приводит к созданию запроса, который запоминает ответы, полученные во время обучения, что позволяет добиваться идеальных результатов на тестах по обучающим данным, но почти случайных результатов на реальных тестах. Аудит — единственное, что предотвращает самосовершенствование за счет самообмана.
- Когнитивная пассивность представляет собой реальную причину сбоев. Чем надежнее становится цикл, тем реже кто-либо читает его результаты, и когда вы перестаете это делать, вы теряете возможность заметить, что цикл начал выходить из строя по-новому. Циклы, находящиеся без присмотра, требуют внешнего мониторинга, поскольку они не могут сами проверять свою семантическую корректность.
- Отладка затрагивает всю структуру программы. Неправильное решение может возникнуть из-за ошибочного отражения данных в Цикле 4, пропущенного правила оценки в Цикле 2, инструмента, не вернувшего никаких данных в Цикле 1, или поврежденного состояния выполнения в Цикле 3. Выявление причины сбоя в четырех вложенных циклах сложнее, чем в одной функции.
Стоит ли вообще создавать циклы?
Анализ схем циклической обработки дает четкие рекомендации: выполняйте в рамках цикла задачи, которые являются повторяющимися, поддающимися проверке и имеющими высокую ценность, с встроенным инструментом проверки, а для всего остального просто сделайте запрос один раз. Всё сводится к двум вопросам.
- Должна ли работа выполняться повторно или требует множества шагов, которые никто не может предсказать? Если достаточен один запрос, цикл добавляет лишние затраты и не создает никакой архитектуры.
- Может ли агент сам определить, завершил ли он работу? Без теста, ожидаемого формата или эталонных ответов цикл никогда не заканчивается, и он продолжает работать бесконечно.
Ответы на оба вопроса должны быть «да». Если хотя бы один ответ — «нет», пропустите использование цикла.
Подходящие случаи
Повторяющаяся работа большого объема. Шестнадцать заявок на страховку, 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 tries, a budget or a time limit, always, with no exceptions. Never assume the model will stop on its own. Grassroots setups like a shell while :; do ... done loop contain nothing on the model side that ends them, so they run until somebody kills the process, and OpenAI's SDK ships max_turns because experience showed models don't reliably end themselves. This codebase has three stops at three levels: MAX_STEPS = 8 in Loop 1 prevents endless tool chains, max_retries = 3 in Loop 2 prevents endless retries, and BUDGET_USD = 2.00 through CostMeter is the ultimate backstop. At the step cap Loop 1 returns ran_out_of_steps, when retries run out Loop 2 returns passed=False, and when the budget is gone the error propagates up and halts everything.
3. Good tools
Действия, доступные агенту, должны быть надежными, четко описанными и иметь четко определенные границы. Сам цикл является простым, а работа выполняется с помощью инструментов. По рекомендациям Anthropic наборы инструментов и их документация должны разрабатываться тщательно, поскольку нечеткое описание приводит к нечетким запросам, а слишком мощный инструмент — к необратимым ошибкам. Здесь имеются четыре инструмента, каждый из которых выполняет одну задачу: чистый поиск информации без побочных эффектов, поиск по ключевым словам в статической базе знаний, имитация загрузки внешних данных и структурированное действие в терминале, требующее соблюдения заданной схемы (Literal["accept", "modify", "decline", "refer"]), указания обоснования и использования явных флагов. Без последнего элемента агент возвращает текст, требующий сложной обработки.
4. Память
Сохраняйте историю, но кратко её обобщайте, чтобы контекст не становился слишком объёмным, а для диалогов длиной более одного разговора записывайте состояние на диск. Инструмент Anthropic’s long-running-agent harness позволяет агенту читать предыдущее состояние из 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, где вывод в стандартный поток буферизуется. Линии решений содержат решение, статус и время выполнения в одной строке для каждого приложения. Линии подтверждения указывают на то, что извлечение данных произошло.
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
Цикл событий также отслеживает исчерпание бюджета во время процесса поиска оптимального решения отдельно от исчерпания бюджета во время обработки, поэтому исчерпание ресурсов посреди цикла самосовершенствования не приводит к потере остальных элементов очереди. Цикл работает более стабильно: пропускается дорогостоящий шаг совершенствования, а обработка продолжается.
Чек-лист перед тем, как оставить его работать самостоятельно
Прежде чем доверять циклу работу без присмотра, убедитесь в следующем:
- Цель может быть проверена машиной (здесь с помощью функции
is_correct()по сравнению с истинными данными). - На каждом уровне предусмотрено жесткое ограничение (
MAX_STEPS,max_retries,BUDGET_USD). - Инструменты четко определены и задокументированы (четыре инструмента, по одному заданию на каждый).
- Состояние сохраняется после перезапуска (
run_state.jsonвместе с списком уроков). - Механизм проверки отделен от генератора (мидлвэр рубрик плюс детерминистичный судья).
- Сложные задачи имеют косвенный или явный план (четырехэтапная последовательность, указанная в запросе).
- Ключевые этапы фиксируются и отслеживаются (линии прогресса
[loop3]). - С самого первого вызова устанавливается и контролируется бюджет (один
CostMeter, общий для всех циклов).
Если пропустить хотя бы один из этих элементов, цикл потерпит неудачу в реальных условиях — не сразу, но обязательно в конечном итоге. Большинство сбоев в производстве сводятся к отсутствию шага проверки, условия остановки или сохраненного состояния. Сначала устраните эти проблемы, затем настройте всё остальное.
Связанная литература
- Параллельные агенты LangGraph для рассмотрения pull-запросов с помощью webhook FastAPI — Как webhook, очередь Redis и четыре агента LangGraph превращают pull-запрос в структурированный отзыв ИИ с оценкой примерно за 30 секунд, при этом ngrok используется для локальных тестов.
- Создание агента поддержки LEPA с использованием LangGraph — Реализация узлов SupportState, intake и classify, условных рёбер, а также маркеров START/END, что позволяет запросу на поддержку в школе уточнить информацию или перейти к соответствующим документам.