Почему ваши агенты нестабильны: промпт — плохой исполнительный слой
Валидация, retry, маршрутизация живут в промпте — агент работает в 72% случаев. Выношу execution logic в код — 97%. Разбираю, что не должно быть в промпте.
У меня был агент на Claude Sonnet. Обработка клиентских заявок: классифицировать, извлечь данные, проверить в базе, ответить. Системный промпт — 2 800 токенов. Роль, порядок шагов, условия ветвления, формат ответа, retry, валидация, ограничения. Всё в одном тексте.
Стабильность: 72%.
Каждый третий запрос — сбой. Пропущенный шаг, сломанный JSON, бесконечный retry, неправильный маршрут. Три недели я переписывал промпт: уточнял формулировки, добавлял примеры, капслочил важное. Довёл до 81%. Потолок. Дальше промпт не тянул.
За один вечер вынес управление выполнением в Python. Граф шагов в LangGraph, Pydantic-валидация, retry в коде. Та же модель, те же данные — 97%.
Разница — не в промпте. А в том, кому поручено управление выполнением.
Промпт — рекомендация, не контракт
Промпт — текст на естественном языке. LLM интерпретирует его вероятностно. «Сначала классифицируй, потом извлеки, потом проверь» — для модели это не императивная программа, а рекомендация с весами.
В 82% запусков — следует порядку. В 13% — пропускает шаг или переставляет. В 5% — импровизирует. Не потому что модель плохая — потому что естественный язык не имеет гарантий исполнения.
# Код: 100% гарантия порядка
step_1()
step_2()
step_3()
# Промпт: ~82% вероятность порядка
"Сначала выполни шаг 1, затем шаг 2, затем шаг 3." 18% ненадёжности — свойство среды, не баг промпта. LLM оптимизирует на «полезный ответ», а не на «точное следование инструкции».
Пять вещей, которые нельзя доверять промпту
1. Порядок выполнения
Промпт описывает последовательность словами. LLM решает, следовать ли. Граф в коде — гарантирует:
from langgraph.graph import StateGraph, START, END
graph = StateGraph(AgentState)
graph.add_edge(START, "classify")
graph.add_edge("classify", "extract")
graph.add_edge("extract", "validate")
graph.add_edge("validate", "check_db")
graph.add_edge("check_db", "respond")
graph.add_edge("respond", END)Агент физически не может пропустить validate. Переходы детерминированные. LLM работает внутри шага — но не контролирует переходы.
2. Валидация результатов
Промпт: «Убедись, что сумма — число, а дата в формате YYYY-MM-DD.»
LLM, который сгалюцинировал число, не поймает свою галлюцинацию при «проверке». Он уверен в ответе. Реальный случай: агент извлёк «15 000» как 150000. Промпт говорил проверить — модель «проверила» и подтвердила. Две заявки с десятикратно завышенной суммой ушли в работу.
from pydantic import BaseModel, field_validator
class ExtractedData(BaseModel):
amount: float
currency: str
date: date
@field_validator("amount")
@classmethod
def must_be_reasonable(cls, v):
if v <= 0 or v > 10_000_000:
raise ValueError(f"Amount {v} outside valid range")
return round(v, 2)Pydantic ловит 100% нарушений. Не «обычно» — каждый раз. При ошибке — retry с конкретным сообщением: модель получает обратную связь от кода, не от себя.
3. Retry-логика
Промпт: «Если ошибка — попробуй ещё раз с другими параметрами.» Реальность: LLM повторяет с теми же параметрами (формально «другими» — переставил пробел). Или входит в бесконечный цикл: промпт не задал лимит. Один запрос — 38 вызовов за 4 минуты. $12.
async def with_retry(fn, state, max_retries=3):
for attempt in range(max_retries + 1):
result = await fn(state)
if result.get("success"):
return result
state["error"] = result.get("error")
state["attempt"] = attempt + 1
await asyncio.sleep(2 ** attempt)
return {"failed": True, "error": state.get("error")}Три попытки. Backoff. Таймаут. Fallback на человека. Десять строк — и бесконечных циклов больше нет.
4. Маршрутизация с приоритетами
Промпт: «Жалобу — в качество. Срочное — эскалируй. Вопрос — в FAQ.» Заявка — одновременно жалоба, срочная и содержит вопрос. Промпт не описал приоритеты. LLM выбрал один маршрут, потерял два.
def route(state: AgentState) -> str:
pri = state["classification"]["priority"]
cat = state["classification"]["category"]
if pri >= 4: return "escalate"
if cat == "complaint": return "quality_team"
if cat == "question": return "faq"
return "standard" Детерминированно. Приоритет — явный. 20 строк unit-тестов покрывают все комбинации. Промпт нельзя unit-тестировать.
5. Управление состоянием
Промпт: «Помни, что клиент уже верифицирован. Не проверяй повторно.» На длинной цепочке из 15 tool calls системный промпт — далеко от текущей позиции. Правила теряют вес. Модель «забывает».
class AgentState(TypedDict):
client_verified: bool
retry_count: int
completed_steps: list[str]
async def check_client(state):
if state["client_verified"]:
return state # Детерминированный пропуск
result = await db.verify(state["client_id"])
state["client_verified"] = result.verified
return stateBool — не «помнишь ли ты». Typed dict хранит состояние точно.
Что остаётся в промпте
После выноса execution logic промпт сжимается с 2 800 до 300-400 токенов.
Было (один промпт на всё):
Ты — агент обработки заявок. 6 инструментов.
Порядок: ... [500 слов]
При ошибке: ... [300 слов]
Формат: ... [200 слов]
Ограничения: ... [400 слов]Стало (промпт для одного шага):
Ты — специалист по классификации клиентских заявок.
Категория: complaint, question, claim, feedback, billing.
Приоритет: 1 (низкий) — 5 (критический).
JSON: {"category": "...", "priority": N, "reasoning": "..."}Одна задача. Короткий промпт. LLM справляется на 95%+, а не на 72% — потому что задача проще.
Правило разделения
Если инструкцию можно выразить как if/else, цикл или Pydantic-схему — ей не место в промпте.
В промпте (LLM думает): понять суть заявки, извлечь сущности из текста, оценить тональность, сгенерировать ответ, принять нестандартное решение.
В коде (код выполняет): порядок шагов, валидация результатов, retry с backoff, маршрутизация, управление состоянием.
Результат
Стабильность: промпт = execution — 72% → 81% (потолок). Код = execution — 97%.
Промпт: 2 800 токенов → 300-400 на шаг.
Отладка edge case: 2-4 часа → 15-30 минут.
Добавить шаг: 1-2 часа → 30 минут.
Unit-тесты: невозможно → полное покрытие.
25 процентных пунктов. Без смены модели, без fine-tuning. Одно архитектурное решение: перестать просить LLM управлять выполнением.
Промпт — для мышления. Код — для выполнения. Каждый раз, когда в промпте пишете «сначала X, потом Y, если ошибка — повтори» — вы просите поэта работать диспетчером. Он справится. Иногда. Диспетчер — всегда.