Урок 4. Разбор: саммари телефонного разговора

До этого мы разбирали приёмы по отдельности. Теперь соберём их в один рабочий промпт для типовой корпоративной задачи: саммари звонков в поддержку.

Легенда: компания Acme Corporation продаёт устройства для умного дома. Сотни звонков в день, и бизнесу нужно превратить эти разговоры в полезные структурированные данные — чтобы потом считать метрики качества поддержки.

Что усложняет задачу:

  • Звонки бывают короткие на пять реплик и длинные на двадцать.
  • Темы любые: от «не ловит Wi-Fi» до сложной поломки системы.
  • Саммари нужны в едином формате, иначе их потом не проанализировать.
  • В саммари нельзя тащить персональные данные клиента.

План — по лучшим практикам из предыдущих уроков: системный промпт задаёт роль, структура промпта помогает модели, инструкции описывают желаемый вывод, XML-теги размечают данные, отдельно прописываем краевые случаи и добавляем примеры.

Какие бывают транскрипты

Прежде чем писать промпт, посмотрим на данные. Вот короткий звонок:

call1 = """
Агент: Спасибо за звонок в поддержку Acme Smart Home. Меня зовут Алекс. Чем помочь?
Клиент: Здравствуйте, у меня не включается умная лампочка.
Агент: Понял. Вы пробовали сбросить лампочку?
Клиент: Нет. А как это сделать?
Агент: Выключите питание на 5 секунд и включите обратно — она сбросится.
Клиент: Хорошо, попробую. Спасибо!
Агент: Пожалуйста. Звоните, если что-то ещё понадобится.
"""

Звонок средней длины, который закончился решением (фрагмент сокращён — в оригинале агент ведёт клиента через калибровку термостата):

call2 = """
Агент: Поддержка Acme Smart Home, это Джейми. Чем могу помочь?
Клиент: Джейми, мой SmartTherm не держит заданную температуру. Стоит 72 °F, а дома
        гораздо теплее.
Агент: Сожалею. Давайте разбираться. SmartTherm подключён к Wi-Fi?
Клиент: Да, значок Wi-Fi горит на экране.
... [пропущено: калибровка через меню, клиент вводит реальную температуру в комнате] ...
Агент: Отлично. Нажмите «выбрать» для подтверждения. Перекалибровка займёт
        несколько минут, проверьте через час.
Клиент: Хорошо, спасибо за помощь.
"""

И длинный звонок без решения — агент проверил двери, окна, батарейку и в итоге перевёл клиента на техническую команду для полной диагностики.

Уже на трёх примерах видно, чем эти данные неудобны: длина скачет в разы, темы разные, часть звонков заканчивается решением, часть — эскалацией, часть требует follow-up. Промпт должен переваривать всё это одинаково ровно.

Наивная версия промпта

Начинаем, как всегда, с самого простого:

prompt = """
Просуммируй следующий транскрипт звонка в поддержку. Сфокусируйся на основной
проблеме, на том, как её решили, и на необходимых дальнейших действиях.

{transcript}
"""

Общую идею модель схватывает. Но у этой версии четыре дыры:

  • не задан формат вывода — саммари будут разной формы, и их не распарсить;
  • нет сценариев для «нестандартных» звонков (нерешённая проблема, мало информации);
  • нет ограничений по длине — саммари может расползтись на абзац на каждое поле;
  • нет запрета на персональные данные — имена и телефоны поедут в аналитику.

Дальше закрываем эти дыры по одной. Промптинг — итеративный процесс: начинаем с простого и наращиваем.

Системный промпт

Самое дешёвое улучшение — задать роль и тон один раз для всего взаимодействия:

system = """
Ты — опытный аналитик клиентской поддержки. Ты умеешь извлекать ключевую
информацию из транскриптов звонков и оформлять её в структурированном виде.
Твоя задача — анализировать транскрипты звонков и выдавать краткие точные
саммари, сохраняя профессиональный тон.
"""

Данные наверх, схема вниз

Основной промпт собираем по кусочкам. Первое правило: длинные документы — в начало промпта. Модель должна получить весь контекст до того, как услышит инструкции. И сразу размечаем транскрипт XML-тегом, чтобы было видно, где данные, а где текст промпта:

prompt_pt1 = """
Проанализируй следующий транскрипт звонка в поддержку и сгенерируй
JSON-саммари разговора:

<transcript>
[СЮДА ВСТАВЛЯЕТСЯ ТРАНСКРИПТ]
</transcript>
"""

Второе — заранее решить, как выглядит хороший вывод. Для последующего парсинга удобнее всего JSON. Что в нём должно быть по минимуму: статус (хватило ли информации на саммари), описание проблемы клиента, нужен ли follow-up, детали follow-up, как решили вопрос, список неоднозначностей.

{
  "summary": {
    "customerIssue": "Краткое описание основной проблемы или причины звонка",
    "resolution": "Как вопрос решили, если решили",
    "followUpRequired": true/false,
    "followUpDetails": "Описание дальнейших действий или null, если не требуются"
  },
  "status": "COMPLETE",
  "ambiguities": ["Неясные или размытые моменты разговора, либо пустой массив"]
}

Теперь оформляем это как часть промпта — вместе с инструкциями и ограничениями:

prompt_pt2 = """
Инструкции:
1. Внимательно прочитай транскрипт.
2. Проанализируй его, сфокусировавшись на основной проблеме, решении и
   необходимых дальнейших действиях.
3. Сгенерируй JSON-объект, описывающий ключевые аспекты разговора в
   заданной структуре.

Важные правила:
- Конфиденциальность: не включай персональные данные клиента — имена,
  телефоны, адреса почты.
- Лимит символов: каждое текстовое поле — не длиннее 100 символов.
- Сохраняй профессиональный тон.

Формат вывода:
<json>
{ ...структура выше... }
</json>
"""
Квиз 1

Куда в промпте лучше поместить длинный транскрипт?

Место подумать и теги вывода

Последний кусок первой версии — просим модель сначала подумать вслух, а потом выдать JSON, и разводим то и другое по разным тегам:

prompt_pt3 = """
Прежде чем генерировать JSON, проанализируй транскрипт внутри тегов
<thinking>. Опиши основную проблему, решение, необходимость follow-up
и любые неоднозначности.
Затем выдай JSON внутри тегов <json>.
"""

Зачем это нужно. Блок <thinking> заставляет модель разложить рассуждение до того, как она начнёт заполнять поля — анализ получается аккуратнее. Плюс мы (и любой ревьюер) видим ход мысли: это спасает при отладке и контроле качества.

А отделение анализа от результата даёт чисто инженерную выгоду: JSON лежит внутри <json>, и его можно вытащить одной регуляркой, не разбирая остальной текст ответа.

import re

json_content = re.search(r'<json>(.*?)</json>', response.content[0].text, re.DOTALL)
if json_content:
    print(json_content.group(1).strip())
else:
    print("В ответе нет блока с JSON.")

На обычных звонках эта версия работает хорошо. Даже на «мутном» транскрипте — где клиент не помнит модель замка, путает его с термостатом и убегает по делам, не оставив телефона — модель честно заполняет ambiguities.

Квиз 2

Зачем оборачивать JSON в отдельные теги, если он и так последний в ответе?

Краевые случаи: где промпт ломается

Все транскрипты выше были нормальными разговорами. В реальности прилетает другое: звонки с обрывом связи, языковой барьер, неразборчивая запись, клиент, который сразу бросил трубку. Такие звонки суммировать не нужно вообще — мы измеряем эффективность поддержки, и мусорные саммари перекосят метрики.

wrong_number_call = """
Агент: Поддержка Acme Smart Home, Лиза. Чем помочь?
Клиент: Это техподдержка?
Агент: Да, техподдержка устройств Acme Smart Home. С чем помочь?
Клиент: Извините, ошибся номером.
Агент: Ничего страшного. Хорошего дня.
"""

garbled_call = """
Агент: Спасибо за звонок в поддержку Acme Smart Home. Это Алекс. Чем помочь?
Клиент: [неразборчиво]
Агент: Алло? Вы на связи?
"""

Прогоняем такие звонки через текущий промпт — и получаем полноценные саммари. В полях появляется вот такое:

"customerIssue": "Клиент говорил на языке, которого агент не понимает (испанский)."
"customerIssue": "Непонятно из-за неразборчивого голоса клиента"
"customerIssue": "Клиент ошибся номером техподдержки"

Формально модель права. Но для аналитики это мусор: он попадёт в общую статистику как обычные обращения.

Стратегия: флаг вместо саммари

Варианта два: помечать такие звонки как несуммируемые (и отправлять на ручной разбор) или заводить отдельные категории («технические сложности», «языковой барьер»). Для простоты выбираем первый — просим модель в таких случаях возвращать только:

{
  "status": "INSUFFICIENT_DATA"
}

Для этого добавляем в промпт критерии:

Критерии недостаточных данных:
   Если выполняется хотя бы одно из условий:
   a) в транскрипте меньше 5 обменов репликами
   b) проблема клиента неясна
   c) запись неразборчива, обрывается или мешает языковой барьер
   Тогда верни ТОЛЬКО такой JSON:
   {
     "status": "INSUFFICIENT_DATA"
   }

И примеры

Инструкции задают правило, примеры показывают его в действии. В финальный промпт кладём блок <examples> с тремя разными парами вход-выход:

  • полный разговор без follow-up — status: COMPLETE, ambiguities: [];
  • полный разговор с эскалацией — followUpRequired: true и непустой список неоднозначностей;
  • несуммируемый разговор — только {"status": "INSUFFICIENT_DATA"}.

Каждый пример идёт целиком: транскрипт в <transcript>, рассуждение в <thinking>, ответ в <json> — ровно в том виде, в каком мы ждём ответ от модели. Разнообразие примеров тут важнее их количества: три непохожих случая учат лучше, чем пять однотипных.

Квиз 3

Почему звонок «ошиблись номером» нельзя просто просуммировать как обычный?

Финальный промпт: структура

Итоговый промпт длинный, но устроен он просто:

Проанализируй следующий транскрипт звонка в поддержку и сгенерируй
JSON-саммари разговора:

<transcript>
[СЮДА ВСТАВЛЯЕТСЯ ТРАНСКРИПТ]
</transcript>

<instructions>
- Общие инструкции и правила
- Описание формата JSON
- Критерии недостаточных данных (краевые случаи)
<examples>
разнообразные примеры вход/выход
</examples>
</instructions>

Прежде чем генерировать JSON, проанализируй транскрипт в тегах <thinking>.
Опиши основную проблему, решение, необходимость follow-up и неоднозначности.
Затем выдай JSON в тегах <json>.

Плюс системный промпт, который задаёт роль и тон. Вызов выглядит так:

response = client.messages.create(
    model="claude-sonnet-5",
    system=system,
    max_tokens=4096,
    messages=[
        {"role": "user", "content": final_prompt}
    ]
)

Проверяем на всём наборе: обычные звонки дают ровные саммари, «мутный» звонок — непустой ambiguities, неразборчивый / оборванный / с языковым барьером — INSUFFICIENT_DATA. Даже на строке "бла бла бла" и на пустой строке промпт отрабатывает как надо.

Важно: этот промпт ещё не готов к продакшену. Мы проверили его глазами на горстке примеров — это не репрезентативно для живого потока звонков. Чтобы довести прототип до надёжного решения, нужна нормальная система оценки с количественными метриками. Именно эвалы отделяют «многообещающий черновик» от продакшн-качества.

Упражнения

Упражнение 4.1 — Добавить поле в схему

Продуктовая команда просит различать звонки, которые агент решил сам, и звонки, переданные дальше. Как добавить это в промпт, не сломав остальное?

Решение упражненияСначала попробуйте сами — потом сверьтесь

Правку нужно внести в трёх местах сразу, иначе модель начнёт плавать:

  1. В описание JSON-структуры — добавить поле с допустимыми значениями:
"resolvedBy": "AGENT" | "ESCALATED"
  1. В инструкции — одну строку о том, что считать эскалацией: «если разговор закончился передачей клиента другой команде, ставь ESCALATED».
  2. В примеры — в первом примере "resolvedBy": "AGENT", во втором (где вопрос ушёл в техкоманду) "resolvedBy": "ESCALATED".

Пример INSUFFICIENT_DATA не трогаем: там по-прежнему возвращается только статус. Правило простое: схема, инструкция и примеры всегда меняются вместе. Обновили одно из трёх — получили противоречивый промпт.

Упражнение 4.2 — Модель додумывает

В саммари звонка про SmartLock, где клиент так и не назвал модель устройства, появляется строка «клиент использует последнюю модель SmartLock». Откуда она берётся и что править?

Решение упражненияСначала попробуйте сами — потом сверьтесь

Источник — реплика самого клиента: «давайте просто считать, что это последняя модель». Модель приняла предположение за факт и перенесла его в саммари.

Лечится двумя добавками в инструкции:

- Опирайся только на то, что прямо сказано в транскрипте.
  Не додумывай детали, которых там нет.
- Всё, что осталось неподтверждённым, помещай в ambiguities,
  а не в поля summary.

И — обязательно — в примерах должен быть случай, где неподтверждённая деталь уехала именно в ambiguities. В финальном промпте такой пример уже есть: там в список попало «конкретный текст ошибки клиент не назвал». Инструкция задаёт правило, пример показывает, как оно выглядит на выходе.

Что запомнить

  • Сложный промпт собирается слоями: роль → данные → инструкции → формат → краевые случаи → примеры → место подумать.
  • Длинный документ — в начало промпта, размеченный XML-тегом.
  • Заданная JSON-схема превращает текстовый ответ в данные, с которыми можно работать программно.
  • <thinking> даёт модели место рассудить, <json> — отделяет результат для парсинга.
  • Краевые случаи проектируются явно: отдельный статус вроде INSUFFICIENT_DATA лучше, чем правдоподобное саммари мусорного звонка.
  • Примеры должны покрывать разные ситуации, а не повторять одну.
  • Проверка «глазами на пяти примерах» — не оценка качества. Продакшн начинается с эвалов.

Дочитали и сделали упражнения? Зафиксируйте прогресс — отметка сохранится в вашем браузере.

Перевод и адаптация урока «Call Summarizing Prompt Walkthrough» курса Real World Prompting © Anthropic, лицензия CC BY-NC 4.0. Перевод: Дарья Воронкина (@aishipuchka). Материал изменён: переведён на русский, транскрипты сокращены, названия моделей актуализированы, добавлены квизы и упражнения. Используется некоммерчески.