Многоходовые диалоги с использованием инструментов
При разработке приложений, использующих возможности больших языковых моделей (LLM), таких как Claude, часто возникает необходимость в интеграции различных внешних инструментов. Эти инструменты позволяют Claude выполнять действия, выходящие за рамки его собственных знаний, например, получать актуальные данные, выполнять вычисления или взаимодействовать с другими системами. Однако иногда для ответа на один, казалось бы, простой вопрос пользователя Claude может потребоваться последовательный вызов нескольких инструментов.
Представьте сценарий: пользователь спрашивает: "Какой день будет через 103 дня от сегодняшней даты?". Чтобы ответить на этот вопрос, Claude не может просто "знать" текущую дату или выполнить сложное календарное вычисление. Ему потребуется выполнить два отдельных шага:
- Сначала получить текущую дату, используя один инструмент (например,
get_current_datetime). - Затем, имея текущую дату, добавить к ней 103 дня, используя другой инструмент (например,
add_duration_to_datetime).
Такой подход создает шаблон многоходового диалога, где Claude делает несколько запросов к инструментам, прежде чем предоставить окончательный ответ. Ваше приложение должно быть спроектировано таким образом, чтобы автоматически обрабатывать эту последовательность действий, обеспечивая бесшовное взаимодействие для конечного пользователя.
Многоходовый шаблон использования инструментов
Чтобы лучше понять, как это работает, давайте рассмотрим пошаговую последовательность событий, когда Claude нуждается в нескольких инструментах для выполнения задачи:
- Пользователь задает вопрос: "Какой день будет через 103 дня от сегодняшней даты?"
- Claude анализирует запрос: Модель понимает, что ей нужна текущая дата. Она генерирует блок использования инструмента, запрашивая вызов функции
get_current_datetime. - Ваш сервер обрабатывает запрос: Ваше приложение перехватывает этот запрос инструмента, вызывает соответствующую функцию на вашей стороне (например, обращается к системным часам) и возвращает результат (например, "2023-10-26").
- Claude продолжает обработку: Получив текущую дату, Claude осознает, что теперь ему нужно выполнить второе действие — добавить 103 дня. Он генерирует новый блок использования инструмента, запрашивая вызов функции
add_duration_to_datetimeс параметрами "2023-10-26" и "103 дня". - Ваш сервер обрабатывает второй запрос: Ваше приложение снова перехватывает запрос, вызывает функцию
add_duration_to_datetimeи возвращает результат (например, "2024-02-06"). - Claude формирует окончательный ответ: Теперь у Claude есть вся необходимая информация. Он формулирует и предоставляет окончательный ответ пользователю: "Через 103 дня от сегодняшней даты будет 6 февраля 2024 года."
Этот процесс происходит "за кулисами", и для пользователя он выглядит как единый, мгновенный ответ, хотя на самом деле Claude и ваше приложение выполнили несколько итераций взаимодействия.
Создание цикла диалога
Для эффективной обработки многоходовых шаблонов использования инструментов необходимо реализовать цикл диалога, который будет продолжаться до тех пор, пока Claude не перестанет запрашивать инструменты и не предоставит окончательный ответ. Этот цикл является сердцем вашей логики взаимодействия с LLM.
Концептуально, такой цикл можно представить следующим образом:
def run_conversation(messages):
while True:
# Отправляем текущий набор сообщений Claude и получаем ответ
response = chat(messages)
# Добавляем ответ Claude в историю диалога
add_assistant_message(messages, response) # Или add_user_message, если ответ содержит tool_results
# Проверяем, содержит ли ответ Claude запрос на использование инструмента
if not response_is_asking_for_a_tool(response):
# Если нет, Claude предоставил окончательный ответ, выходим из цикла
break
# Если да, выполняем запрошенные инструменты
tool_result_blocks = run_tools(response)
# Добавляем результаты выполнения инструментов обратно в историю диалога
add_tool_result_message(messages, tool_result_blocks) # Это будет сообщением от "пользователя" для Claude
return messages # Возвращаем полную историю диалога
В этом псевдокоде функция chat(messages) отправляет историю диалога Claude и получает его ответ. Функция response_is_asking_for_a_tool(response) проверяет, содержит ли ответ Claude блок tool_use. Если да, то run_tools(response) выполняет эти инструменты, а их результаты добавляются обратно в историю диалога как новое сообщение, которое Claude затем обрабатывает на следующей итерации цикла. Этот процесс повторяется до тех пор, пока Claude не сможет дать окончательный ответ без вызова дополнительных инструментов.
Рефакторинг вспомогательных функций
Прежде чем реализовать полноценный цикл диалога, необходимо обновить вспомогательные функции, чтобы они могли корректно обрабатывать различные типы сообщений и блоков, которые могут поступать от Claude или отправляться ему. В контексте работы с инструментами сообщения становятся более сложными, чем просто текстовые строки.
Обновление обработчиков сообщений
Традиционные функции, такие как add_user_message и add_assistant_message, часто были разработаны для работы с простыми текстовыми строками. Однако ответы Claude, особенно при использовании инструментов, могут содержать не только текст, но и блоки tool_use или tool_result. Чтобы обеспечить гибкость, эти функции должны уметь принимать различные форматы:
- Простую строку (для обычного текстового сообщения).
- Список блоков (например,
[{"type": "text", "text": "Привет"}, {"type": "tool_use", ...}]). - Полный объект сообщения (например, объект
Messageиз SDK Anthropic).
Пример обновленной функции add_user_message:
from anthropic.types import Message
def add_user_message(messages, message):
user_message = {
"role": "user",
"content": message.content if isinstance(message, Message) else message
}
messages.append(user_message)
Эта функция теперь позволяет передавать в качестве message либо строку, либо список блоков, либо полный объект Message. Если передан объект Message, она извлекает его содержимое (message.content); в противном случае она использует переданное значение как есть. Это значительно повышает гибкость и упрощает добавление как пользовательских запросов, так и результатов выполнения инструментов в историю диалога.
Обновление функции чата
Ваша основная функция, которая взаимодействует с API Claude (например, chat), также требует модификации. Ей необходимо:
- Принимать список доступных инструментов (
tools), которые Claude может использовать. - Возвращать полный объект сообщения (
Message), а не только извлеченный текст. Это критически важно, поскольку полный объект содержит информацию о запросах инструментов.
Пример обновленной функции chat:
def chat(messages, system=None, temperature=1.0, stop_sequences=[], tools=None):
params = {
"model": model, # Предполагается, что 'model' определена глобально или передается
"max_tokens": 1000,
"messages": messages,
"temperature": temperature,
"stop_sequences": stop_sequences,
}
if tools:
params["tools"] = tools # Добавляем список инструментов, если он предоставлен
if system:
params["system"] = system # Добавляем системный промпт, если он предоставлен
message = client.messages.create(**params) # Вызываем API и получаем полный объект Message
return message
Ключевые изменения здесь — это добавление параметра tools, который передается в API Claude, и то, что функция теперь возвращает полный объект Message. Этот объект может содержать различные типы блоков, включая tool_use, которые необходимы для реализации многоходового диалога.
Извлечение текста из сообщений
Поскольку функция chat теперь возвращает полные объекты Message, а не просто текст, вам понадобится вспомогательная функция для извлечения только текстового содержимого, когда это необходимо (например, для отображения окончательного ответа пользователю).
def text_from_message(message):
return "\n".join(
[block.text for block in message.content if block.type == "text"]
)
Эта функция просматривает все блоки в содержимом сообщения (message.content), находит те, у которых type равен "text", и объединяет их текстовое содержимое в одну строку. Это полезно, когда вы хотите показать пользователю только читаемый текст, игнорируя внутренние блоки tool_use или tool_result.
Почему эти изменения важны
Эти шаги по рефакторингу подготавливают ваш код к реалиям диалогов с поддержкой инструментов, делая его более надежным и гибким:
- Множественные блоки в одном сообщении: Ответы Claude могут содержать как текстовые блоки, так и блоки использования инструментов (
tool_use) одновременно. Ваша система должна уметь обрабатывать эту сложность. - Гибкая обработка сообщений: Ваши функции теперь могут работать с различными форматами сообщений (строки, списки блоков, полные объекты
Message), что упрощает интеграцию и управление диалогом. - Полное сохранение сообщений: Вы сохраняете всю информацию, предоставленную Claude, а не только текстовые части. Это критически важно для анализа и выполнения запросов инструментов.
- Поддержка списка инструментов: Ваша функция
chatтеперь может корректно получать и использовать список доступных инструментов, позволяя Claude выбирать наиболее подходящий для текущей задачи.
С этими фундаментальными изменениями вы готовы реализовать полноценный цикл диалога, который автоматически обрабатывает множественные вызовы инструментов. Это создает бесшовный опыт, где Claude может использовать любые необходимые инструменты для полного и точного ответа на вопросы пользователей, даже если для этого требуется несколько шагов.