Урок 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>
"""
Куда в промпте лучше поместить длинный транскрипт?
Место подумать и теги вывода
Последний кусок первой версии — просим модель сначала подумать вслух, а потом выдать 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.
Зачем оборачивать 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> — ровно в том виде, в каком мы ждём ответ от модели. Разнообразие примеров тут важнее их количества: три непохожих случая учат лучше, чем пять однотипных.
Почему звонок «ошиблись номером» нельзя просто просуммировать как обычный?
Финальный промпт: структура
Итоговый промпт длинный, но устроен он просто:
Проанализируй следующий транскрипт звонка в поддержку и сгенерируй
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 — Добавить поле в схему
Продуктовая команда просит различать звонки, которые агент решил сам, и звонки, переданные дальше. Как добавить это в промпт, не сломав остальное?
Решение упражненияСначала попробуйте сами — потом сверьтесь
Правку нужно внести в трёх местах сразу, иначе модель начнёт плавать:
- В описание JSON-структуры — добавить поле с допустимыми значениями:
"resolvedBy": "AGENT" | "ESCALATED"
- В инструкции — одну строку о том, что считать эскалацией: «если разговор закончился передачей клиента другой команде, ставь ESCALATED».
- В примеры — в первом примере
"resolvedBy": "AGENT", во втором (где вопрос ушёл в техкоманду)"resolvedBy": "ESCALATED".
Пример INSUFFICIENT_DATA не трогаем: там по-прежнему возвращается только статус. Правило простое: схема, инструкция и примеры всегда меняются вместе. Обновили одно из трёх — получили противоречивый промпт.
Упражнение 4.2 — Модель додумывает
В саммари звонка про SmartLock, где клиент так и не назвал модель устройства, появляется строка «клиент использует последнюю модель SmartLock». Откуда она берётся и что править?
Решение упражненияСначала попробуйте сами — потом сверьтесь
Источник — реплика самого клиента: «давайте просто считать, что это последняя модель». Модель приняла предположение за факт и перенесла его в саммари.
Лечится двумя добавками в инструкции:
- Опирайся только на то, что прямо сказано в транскрипте.
Не додумывай детали, которых там нет.
- Всё, что осталось неподтверждённым, помещай в ambiguities,
а не в поля summary.
И — обязательно — в примерах должен быть случай, где неподтверждённая деталь уехала именно в ambiguities. В финальном промпте такой пример уже есть: там в список попало «конкретный текст ошибки клиент не назвал». Инструкция задаёт правило, пример показывает, как оно выглядит на выходе.
Что запомнить
- Сложный промпт собирается слоями: роль → данные → инструкции → формат → краевые случаи → примеры → место подумать.
- Длинный документ — в начало промпта, размеченный XML-тегом.
- Заданная JSON-схема превращает текстовый ответ в данные, с которыми можно работать программно.
<thinking>даёт модели место рассудить,<json>— отделяет результат для парсинга.- Краевые случаи проектируются явно: отдельный статус вроде
INSUFFICIENT_DATAлучше, чем правдоподобное саммари мусорного звонка. - Примеры должны покрывать разные ситуации, а не повторять одну.
- Проверка «глазами на пяти примерах» — не оценка качества. Продакшн начинается с эвалов.
Дочитали и сделали упражнения? Зафиксируйте прогресс — отметка сохранится в вашем браузере.