Потоковая передача ответов
При создании чат-приложений с Claude возникает серьёзная проблема с пользовательским опытом: генерация ответов может занимать 10-30 секунд, оставляя пользователей смотреть на индикатор загрузки. Решение — потоковая передача ответов, которая позволяет пользователям видеть текст, появляющийся по частям по мере его генерации Claude, создавая гораздо более отзывчивое ощущение.
Проблема со стандартными ответами
В типичной настройке чата ваш сервер отправляет сообщение пользователя Claude и ждёт полного ответа, прежде чем отправить что-либо обратно клиенту. Это создаёт неудобную задержку, при которой пользователи не получают обратной связи о том, что что-то происходит.
Как работает потоковая передача
При включённой потоковой передаче Claude немедленно отправляет первоначальный ответ, указывающий на то, что он получил ваш запрос и начинает генерировать текст. Затем вы получаете серию событий, каждое из которых содержит небольшую часть общего ответа.
Ваш сервер может пересылать эти текстовые фрагменты вашему клиентскому приложению по мере их поступления, позволяя пользователям видеть, как ответ формируется слово за словом. Все эти события являются частью одного запроса к Claude.
Понимание событий потока
Когда вы включаете потоковую передачу, Claude отправляет несколько типов событий:
- message_start — отправляется новое сообщение
- content_block_start — начало нового блока, содержащего текст, использование инструментов или другой контент
- content_block_delta — фрагменты фактически сгенерированного текста
- content_block_stop — текущий блок контента завершён
- message_stop — текущее сообщение завершено
- message_delta — конец информации о текущем сообщении
События content_block_delta содержат фактически сгенерированный текст, который вы захотите отобразить пользователям.
Базовая реализация потоковой передачи
Чтобы включить потоковую передачу, добавьте stream=True в ваш вызов messages.create:
messages = []
add_user_message(messages, "Write a 1 sentence description of a fake database")
stream = client.messages.create(
model=model,
max_tokens=1000,
messages=messages,
stream=True
)
for event in stream:
print(event)
Упрощённая потоковая передача текста
Вместо ручного анализа событий вы можете использовать упрощённый интерфейс потоковой передачи SDK, который извлекает только текстовое содержимое:
with client.messages.stream(
model=model,
max_tokens=1000,
messages=messages
) as stream:
for text in stream.text_stream:
print(text, end="")
Этот подход автоматически отфильтровывает всё, кроме фактического текстового содержимого, что обычно и требуется для отображения ответов пользователям.
Получение полного сообщения
Хотя потоковая передача отдельных фрагментов отлично подходит для пользовательского опыта, вам часто требуется полное сообщение для хранения или дальнейшей обработки. После завершения потоковой передачи вы можете получить собранное окончательное сообщение:
with client.messages.stream(
model=model,
max_tokens=1000,
messages=messages
) as stream:
for text in stream.text_stream:
# Send each chunk to your client
pass
# Get the complete message for database storage
final_message = stream.get_final_message()
Это даёт вам лучшее из двух миров: потоковую передачу в реальном времени для пользователей и полный объект сообщения для вашей прикладной логики.
Структурированные данные
Когда вам нужно, чтобы Claude генерировал структурированные данные, такие как JSON, код Python или маркированные списки, вы часто сталкиваетесь с распространённой проблемой: Claude стремится быть полезным и добавлять пояснительный текст вокруг вашего контента. Хотя это обычно хорошо, иногда вам нужны только необработанные данные без чего-либо ещё.
Рассмотрим создание веб-приложения, которое генерирует правила AWS EventBridge. Пользователи вводят описание, нажимают «сгенерировать» и ожидают увидеть чистый JSON, который они могут немедленно скопировать и использовать. Если Claude возвращает JSON, обёрнутый в блоки кода markdown с пояснительным текстом, пользователи не могут просто скопировать весь ответ — им приходится вручную выбирать только часть JSON.
Проблема с ответами по умолчанию
По умолчанию, когда вы просите Claude сгенерировать JSON, вы можете получить что-то вроде этого:
```json
{
"source": ["aws.ec2"],
"detail-type": ["EC2 Instance State-change Notification"],
"detail": {
"state": ["running"]
}
}
```
Это правило фиксирует изменения состояния экземпляров EC2, когда экземпляры начинают работать.
JSON корректен, но он обёрнут в форматирование markdown и включает пояснительный текст. Для веб-приложения, где пользователям нужно скопировать необработанный JSON, это создаёт затруднения в пользовательском опыте.
Решение: Предзаполнение сообщения ассистента + Стоп-последовательности
Вы можете объединить предзаполнение сообщения ассистента со стоп-последовательностями, чтобы получить именно тот контент, который вам нужен. Вот как это работает:
messages = []
add_user_message(messages, "Generate a very short event bridge rule as json")
add_assistant_message(messages, "```json")
text = chat(messages, stop_sequences=["```"])
Эта техника работает следующим образом:
- Сообщение пользователя указывает Claude, что генерировать
- Предзаполненное сообщение ассистента заставляет Claude думать, что он уже начал блок кода markdown
- Claude продолжает, записывая только содержимое JSON
- Когда Claude пытается закрыть блок кода с помощью
```, стоп-последовательность немедленно завершает генерацию
Результатом является чистый JSON без дополнительного форматирования:
{
"source": ["aws.ec2"],
"detail-type": ["EC2 Instance State-change Notification"],
"detail": {
"state": ["running"]
}
}
Обработка ответа
Вы можете заметить несколько лишних символов новой строки в ответе. Их легко обработать:
text.strip()
Эта техника не ограничивается генерацией JSON. Используйте её всякий раз, когда вам нужны структурированные данные без комментариев:
- Фрагменты кода Python
- Любой форматированный контент, где вам нужно только содержимое, а не пояснения
Ключ в том, чтобы определить, во что Claude естественным образом хочет обернуть ваш контент, а затем использовать это в качестве предзаполнения и стоп-последовательности. Для кода это обычно блоки кода markdown. Для списков это могут быть другие маркеры форматирования.
Этот подход даёт вам точный контроль над форматом вывода Claude, значительно упрощая интеграцию сгенерированного ИИ контента в приложения, где чистые, структурированные данные имеют решающее значение.
Оценка промптов
При работе с Claude написание хорошего промпта — это только начало. Для создания надёжных AI-приложений необходимо понимать две критически важные концепции: промпт-инжиниринг и оценка промптов. Промпт-инжиниринг предоставляет методы для написания лучших промптов, в то время как оценка промптов помогает измерить, насколько хорошо эти промпты на самом деле работают.
Промпт-инжиниринг против оценки промптов
Промпт-инжиниринг — это ваш набор инструментов для создания эффективных промптов. Он включает такие методы, как:
- Многократное промптирование
- Структурирование с помощью XML-тегов
- Многие другие лучшие практики
Эти методы помогают Claude точно понять, что вы запрашиваете и как вы хотите, чтобы он ответил.
Оценка промптов использует другой подход. Вместо того чтобы фокусироваться на том, как писать промпты, она направлена на измерение их эффективности с помощью автоматизированного тестирования. Вы можете:
- Тестировать на соответствие ожидаемым ответам
- Сравнивать различные версии одного и того же промпта
- Проверять результаты на наличие ошибок
Три пути после написания промпта
После того как вы составили промпт, обычно перед вами стоят три варианта дальнейших действий:
- Вариант 1: Протестировать промпт один раз и решить, что он достаточно хорош. Это несёт значительный риск сбоя в продакшене, когда пользователи предоставляют неожиданные входные данные.
- Вариант 2: Протестировать промпт несколько раз и подкорректировать его для обработки одного-двух крайних случаев. Хотя это лучше, чем вариант 1, пользователи часто будут предоставлять очень неожиданные выходные данные, которые вы не учли.
- Вариант 3: Пропустить промпт через конвейер оценки, чтобы оценить его, а затем итерировать промпт на основе объективных метрик. Этот подход требует больше работы и затрат, но даёт гораздо больше уверенности в надёжности вашего промпта.
Почему большинство инженеров попадают в ловушки тестирования
Варианты 1 и 2 — это распространённые ловушки, в которые попадают все инженеры, включая меня. Естественно написать промпт для серьёзного приложения и не протестировать его достаточно тщательно. Мы склонны недооценивать, сколько крайних случаев встретят реальные пользователи.
Реальность такова, что когда вы развёртываете промпт в продакшене, пользователи будут взаимодействовать с ним способами, которые вы никогда не предвидели. То, что казалось надёжным промптом во время вашего ограниченного тестирования, может быстро сломаться при столкновении со всем разнообразием реальных входных данных.
Подход "сначала оценка"
Вариант 3 представляет собой более систематический подход к разработке промптов. Пропуская ваш промпт через конвейер оценки, вы получаете объективные метрики его производительности по более широкому диапазону тестовых случаев. Этот подход, основанный на данных, позволяет вам:
- Выявлять слабые места до того, как они станут проблемами в продакшене
- Объективно сравнивать различные версии промптов
- Итерировать с уверенностью, основываясь на измеримых улучшениях
- Создавать более надёжные AI-приложения
Хотя этот подход требует больших первоначальных инвестиций времени и инфраструктуры тестирования, он окупается надёжностью и устойчивостью вашего конечного приложения. Цель состоит в том, чтобы выявлять проблемы на этапе разработки, а не после того, как с ними столкнутся ваши пользователи.
Типичный рабочий процесс оценки
Типичный рабочий процесс оценки промптов состоит из пяти ключевых шагов, которые помогают систематически улучшать ваши промпты с помощью объективных измерений. Хотя существует множество различных способов построения этих рабочих процессов и доступны различные открытые и платные инструменты, понимание основного процесса поможет вам начать с малого и масштабироваться по мере необходимости.
Шаг 1: Составьте промпт
Начните с написания первоначального промпта, который вы хотите улучшить. Для этого примера мы будем использовать простой промпт:
prompt = f"""
Please answer the user's question:
{question}
"""
Этот базовый промпт послужит нашей отправной точкой для тестирования и улучшения.
Шаг 2: Создайте набор данных для оценки
Ваш набор данных для оценки содержит примеры входных данных, которые представляют типы вопросов или запросов, которые ваш промпт будет обрабатывать в продакшене. Набор данных должен включать вопросы, которые будут интерполированы в ваш шаблон промпта.
Для этого примера наш набор данных включает три вопроса:
- "Как приготовить овсянку?"
- "Как далеко находится Луна?"
В реальных оценках у вас могут быть десятки, сотни или даже тысячи записей. Вы можете собрать эти наборы данных вручную или использовать Claude для их генерации.
Шаг 3: Пропустите через Claude
Возьмите каждый вопрос из вашего набора данных и объедините его с вашим шаблоном промпта, чтобы создать полные промпты. Затем отправьте каждый из них в Claude для получения ответов.
Например, первый вопрос становится:
Пожалуйста, ответьте на вопрос пользователя:
What's 2+2?
Claude может ответить "2 + 2 = 4" на математический вопрос, предоставить инструкции по приготовлению овсянки на второй вопрос и указать расстояние до Луны на третий.
Шаг 4: Пропустите через оценщика
Оценщик оценивает качество ответов Claude, анализируя как исходный вопрос, так и ответ Claude. Этот шаг обеспечивает объективную оценку, обычно по шкале от 1 до 10, где 10 представляет собой идеальный ответ, а более низкие баллы указывают на возможность улучшения.
В нашем примере оценщик может присвоить:
- Математический вопрос: 10 (идеальный ответ)
- Вопрос об овсянке: 4 (требует улучшения)
- Вопрос о Луне: 9 (очень хороший ответ)
Средний балл по всем вопросам даёт вам объективное измерение: (10 + 4 + 9) ÷ 3 = 7.66
Шаг 5: Измените промпт и повторите
Теперь, когда у вас есть базовый балл, вы можете изменить свой промпт и запустить весь процесс снова, чтобы увидеть, улучшают ли ваши изменения производительность.
Например, вы можете добавить больше указаний в свой промпт:
prompt = f"""
Please answer the user's question:
{question}
Answer the question with ample detail
"""
После прохождения этого улучшенного промпта через тот же процесс оценки вы можете получить более высокий средний балл 8.7, что указывает на то, что дополнительная инструкция помогла Claude предоставить лучшие ответы.
Ключевое преимущество этого рабочего процесса — получение объективных измерений производительности промптов. Вы можете:
- Сравнивать различные версии промптов численно
- Использовать версию с лучшим баллом
- Продолжать итерировать, чтобы найти ещё лучшие подходы
Этот систематический подход устраняет догадки из промпт-инжиниринга и даёт вам уверенность в том, что ваши изменения на самом деле являются улучшениями, а не просто различными вариациями.
Генерация тестовых наборов данных
Создание пользовательского рабочего процесса оценки промптов начинается с разработки надежного промпта, а затем генерации тестовых данных для проверки его эффективности. Давайте рассмотрим настройку системы оценки для промпта, который помогает пользователям писать код, специфичный для AWS.
Постановка цели
Наш промпт должен помогать пользователям в написании трех конкретных типов вывода для сценариев использования AWS:
- Файлы конфигурации JSON
- Регулярные выражения
- Код Python
Ключевое требование заключается в том, что когда пользователь запрашивает помощь с задачей, мы возвращаем чистый вывод в одном из этих форматов без каких-либо дополнительных объяснений, заголовков или нижних колонтитулов.
Вот наш начальный промпт (версия 1):
prompt = f"""
Please provide a solution to the following task:
{task}
"""
Создание набора данных для оценки
Набор данных для оценки содержит входные данные, которые мы будем подавать в наш промпт. Для каждой комбинации промпта и входных данных мы будем запускать промпт и анализировать результаты.
Наш набор данных будет представлять собой массив объектов JSON, где каждый объект содержит свойство "task", описывающее, что мы хотим, чтобы Claude выполнил. Мы можем создать этот набор данных вручную или сгенерировать его автоматически с помощью Claude.
Поскольку мы генерируем тестовые данные, это прекрасная возможность использовать более быструю модель, такую как Haiku, вместо полной модели Claude.
Генерация тестовых данных с помощью кода
Давайте создадим функцию, которая автоматически генерирует наш тестовый набор данных. Сначала нам понадобятся вспомогательные функции для работы с Claude:
def add_user_message(messages, text):
user_message = {"role": "user", "content": text}
messages.append(user_message)
def add_assistant_message(messages, text):
assistant_message = {"role": "assistant", "content": text}
messages.append(assistant_message)
def chat(messages, system=None, temperature=1.0, stop_sequences=[]):
params = {
"model": model,
"max_tokens": 1000,
"messages": messages,
"temperature": temperature
}
if system:
params["system"] = system
if stop_sequences:
params["stop_sequences"] = stop_sequences
response = client.messages.create(**params)
return response.content[0].text
Теперь мы создадим нашу функцию генерации набора данных:
def generate_dataset():
prompt = """
Generate an evaluation dataset for a prompt evaluation. The dataset will be used to evaluate prompts that generate Python, JSON, or Regex specifically for AWS-related tasks. Generate an array of JSON objects, each representing task that requires Python, JSON, or a Regex to complete.
Example output:
```json
[
{
"task": "Описание задачи",
},
...additional
]
```
* Focus on tasks that can be solved by writing a single Python function, a single JSON object, or a single regex
* Focus on tasks that do not require writing much code
Please generate 3 objects.
"""
Чтобы правильно разобрать ответ JSON, мы будем использовать предварительное заполнение и стоп-последовательности:
messages = []
add_user_message(messages, prompt)
add_assistant_message(messages, "```json")
text = chat(messages, stop_sequences=["```"])
return json.loads(text)
Тестирование генерации набора данных
Давайте запустим нашу функцию и посмотрим, какие тестовые случаи мы получим:
dataset = generate_dataset()
print(dataset)
Это должно вернуть три различных тестовых случая, охватывающих наши целевые выводы — функции Python, конфигурации JSON и регулярные выражения для задач, специфичных для AWS.
Сохранение набора данных
Как только у нас будет наш набор данных, мы сохраним его в файл, чтобы мы могли легко загрузить его позже во время оценки:
with open('dataset.json', 'w') as f:
json.dump(dataset, f, indent=2)
Файл в той же директории, что и ваш ноутбук, содержащий ваш список задач, готовых к оценке промптов.
С этой основой у вас теперь есть систематический способ генерации тестовых данных для оценки того, насколько хорошо ваши промпты справляются с различными типами задач кодирования, связанных с AWS.
Запуск оценки
Теперь, когда наш набор данных для оценки готов, пришло время построить основной конвейер оценки. Это включает в себя взятие каждого тестового случая, объединение его с нашим промптом, подачу его в Claude, а затем оценку результатов.
Процесс оценки следует четкому рабочему процессу: мы берем наш набор данных тестовых случаев, объединяем каждый из них с нашим шаблоном промпта, отправляем его в Claude для обработки, а затем оцениваем вывод с помощью системы оценки.
Создание основных функций
Конвейер оценки состоит из трех основных функций, каждая из которых имеет определенную ответственность. Начнем с самой простой — функции, которая обрабатывает отдельные промпты.
Функция run_prompt
Эта функция принимает тестовый случай и объединяет его с нашим шаблоном промпта:
def run_prompt(test_case):
"""Объединяет промпт и входные данные тестового случая, затем возвращает результат"""
prompt = f"""
Please solve the following task:
{test_case["task"]}
"""
messages = []
add_user_message(messages, prompt)
output = chat(messages)
return output
Сейчас мы оставляем промпт чрезвычайно простым. Мы не включаем никаких инструкций по форматированию, поэтому Claude, вероятно, вернет более многословный вывод, чем нам нужно. Мы уточним это позже, по мере итерации над нашим дизайном промпта.
Функция run_test_case
Эта функция организует выполнение одного тестового случая и оценку результата:
def run_test_case(test_case):
"""Вызывает run_prompt, затем оценивает результат"""
output = run_prompt(test_case)
# TODO - Grading
score = 10
return {
"output": output,
"test_case": test_case,
"score": score
}
Пока мы используем жестко заданную оценку 10. Логика оценки — это то, чему мы уделим значительное время в следующих разделах, но этот заполнитель позволяет нам протестировать общий конвейер.
Функция run_eval
Эта функция координирует весь процесс оценки:
def run_eval(dataset):
"""Загружает набор данных и вызывает run_test_case для каждого случая"""
results = []
for test_case in dataset:
result = run_test_case(test_case)
results.append(result)
return results
Эта функция обрабатывает каждый тестовый случай в нашем наборе данных и собирает все результаты в один список.
Запуск оценки
Чтобы выполнить наш конвейер оценки, мы загружаем наш набор данных и пропускаем его через наши функции:
with open("dataset.json", "r") as f:
dataset = json.load(f)
results = run_eval(dataset)
При первом запуске ожидайте, что это займет некоторое время — даже с Claude Haiku, обработка полного набора данных может занять около 30 секунд. Методы оптимизации мы рассмотрим позже.
Изучение результатов
Оценка возвращает структурированный массив JSON, где каждый объект представляет результат одного тестового случая:
print(json.dumps(results, indent=2))
Каждый результат содержит три ключевых элемента информации:
- output — полный ответ от Claude
- test_case — исходный тестовый случай, который был обработан
- score — оценка (в настоящее время жестко задана)
Как вы можете видеть в выводе, Claude генерирует довольно многословные ответы, поскольку мы еще не предоставили конкретных инструкций по форматированию. Это именно та проблема, которую мы будем решать по мере уточнения наших промптов.
Что мы достигли
На данный момент мы успешно построили основной конвейер оценки. Мы можем взять наш набор данных, обработать его через Claude и собрать структурированные результаты. Основной недостающий элемент — это система оценки — эта жестко заданная оценка 10 должна быть заменена фактической логикой оценки.
Этот конвейер представляет собой основу большинства систем оценки ИИ. Хотя это может показаться простым, вы только что построили большую часть того, что на самом деле делает конвейер оценки. Сложность заключается в деталях — лучшие промпты, сложная оценка и оптимизация производительности.
Далее мы углубимся в критически важную тему оценщиков, которые превратят наши жестко заданные оценки в значимые оценки производительности Claude.