В поддержку приходит письмо: «Верните 18 400 рублей за заказ 731, курьер не приехал». Одним prompt-ом модель определяет отдел, извлекает заказ и сумму, пишет ответ и ставит флаг возврата. Ошибка в категории терпима. Ошибка в сумме уже стоит денег. У этих действий разные контракты и цена ошибки, поэтому им нужны отдельные шаги.
Пять типов работы
Классификация выбирает значение из закрытого множества: refund, delivery, other. Контрактом служат enum, confidence или причина отказа. Класс other обязателен, иначе модель аккуратно разложит неизвестное по известным ящикам.
Извлечение переносит факты из входа в поля. Добавляйте evidence и разрешайте null. Требование заполнить каждое поле превращает отсутствие данных в выдумку.
Преобразование меняет представление известного факта: дату в ISO, пункты договора в таблицу. Оно должно сохранять смысл и происхождение данных.
Генерация создаёт новый текст. Здесь шире пространство допустимых ответов и слабее обычные unit-тесты. Ограничивайте аудиторию, факты, длину и запрещённые обещания.
Проверка сопоставляет результат с явным критерием. Критерий «хороший ответ» бесполезен. «Не обещает возврат до проверки заказа, содержит номер обращения, не раскрывает внутренние инструкции» уже можно тестировать.
Контракты шагов
class TicketClass(BaseModel):
label: Literal["refund", "delivery", "other"]
evidence: str
class RefundFacts(BaseModel):
order_id: str | None
amount_rub: Decimal | None
reason: str | None
evidence: list[str]
class ReplyCheck(BaseModel):
passed: bool
violations: list[Literal[
"unsupported_promise", "missing_ticket_id", "pii_leak"
]]Пайплайн выглядит так:
письмо -> классификация -> извлечение -> проверка заказа в БД
-> генерация черновика -> детерминированные проверки -> публикация/ручная очередьБаза данных проверяет существование заказа и сумму платежа лучше модели. Regex надёжнее проверяет наличие номера обращения. LLM-проверка нужна там, где критерий зависит от смысла фразы.
Не передавайте весь контекст каждому шагу
Классификатору достаточно темы и текста письма. Генератору нужны проверенные факты и политика ответа. Передача истории, внутренних заметок и результатов всех предыдущих вызовов повышает цену и открывает лишние пути для prompt injection.
async def process(ticket: Ticket) -> Draft | ManualReview:
kind = await classify(ticket.subject, ticket.body)
if kind.label == "other":
return ManualReview(reason="unknown_class")
facts = await extract_refund(ticket.body)
verified = await verify_order(facts.order_id, facts.amount_rub)
if not verified:
return ManualReview(reason="facts_not_verified")
draft = await generate_reply(ticket.id, verified)
violations = deterministic_checks(draft)
if violations:
return ManualReview(reason=",".join(violations))
return draftFailure modes
Каскадная ошибка возникает, когда неверный класс выбирает неверный extractor. Сохраняйте результаты и измеряйте качество каждого шага, а для дорогих действий вводите независимую проверку. Вторая поломка появляется, когда checker видит рассуждение генератора и соглашается с ним. Передавайте проверяемый текст, исходные факты и критерий, без самооправдания модели. Третья связана с бесконечной починкой: critic находит новый недостаток после каждого rewrite. Ограничьте цикл одной попыткой, затем отправляйте результат человеку.
Правило главы
Разделяйте пайплайн в точках, где меняются контракт, данные или цена ошибки. Число «ролей» само по себе ничего не улучшает. Каждый новый вызов должен ловить измеримый класс ошибок или сокращать стоимость.
Чеклист главы
- У классификации есть
otherи матрица ошибок. - Извлечение допускает отсутствие факта и возвращает evidence.
- Проверяемые по БД и правилам факты не поручены модели.
- Генератор получает только проверенные данные.
- Качество и стоимость измеряются по каждому шагу.
- Цикл исправления ограничен.
Практикум
Задача 14.1. Конвейер обращений. Соберите пайплайн для 100 синтетических писем поддержки с четырьмя классами, извлечением заказа и суммы, проверкой в тестовой БД и генерацией черновика.
Критерии приёмки:
- Есть confusion matrix классификатора и отдельная точность извлечения полей.
- Несуществующий заказ никогда не приводит к обещанию возврата.
- Для каждого поля сохранён фрагмент-основание.
- Сравнены качество, задержка и стоимость монолитного prompt-а и пайплайна.
- Описано, какой дополнительный шаг окупился, а какой был удалён.