Курс Anthropic: Tool use, часть 2 — полный цикл, tool_choice и чат-бот с несколькими инструментами
Перевод уроков 4–6 курса Tool use из anthropics/courses: как вернуть модели результат инструмента блоком tool_result, три режима tool_choice (auto, any, tool) и чат-бот поддержки интернет-магазина с четырьмя инструментами, системным промптом и тегами для ответа.
Вторая часть перевода курса Tool use из anthropics/courses. В первой части — что такое инструменты, первый инструмент и JSON через инструменты. Здесь цикл замыкается: результат инструмента возвращается модели, и модель пишет окончательный ответ. Затем — как управлять выбором инструмента и как собрать чат-бота поддержки, который сам выбирает из нескольких инструментов.
Как читать вместе с курсом
Заголовок раздела «Как читать вместе с курсом»- Когда. Урок 4 — практика к разделу «Tool calling» урока 8.3.1 «Агентный цикл и ReAct»: тот же цикл «модель просит — код выполняет — результат возвращается модели», но в формате Anthropic. Урок 6 — простой агент поддержки; к нему относятся раздел «Безопасность инструментов» урока 8.3.2 и guardrails из урока 8.4.3: бот из урока 6 отменяет чужие заказы без всякой проверки, и авторы прямо об этом предупреждают.
- Сколько времени. Два-три часа с упражнениями; чат-бота из урока 6 стоит запустить и «поломать» самому.
- Что можно пропустить. Раздел «Проблема Opus» в уроке 6 относится к модели 2024 года; современные модели
отдают размышления отдельным блоком
thinking. Идея (просить ответ пользователю в отдельных тегах) остаётся полезной для небольших локальных моделей.
Урок 4. Полный цикл работы с инструментом
Заголовок раздела «Урок 4. Полный цикл работы с инструментом»Цели урока
Заголовок раздела «Цели урока»- Понять весь цикл работы с инструментами.
- Писать правильно оформленные сообщения
tool_result. - Сделать чат-бота, который пользуется инструментом.
В этом уроке мы реализуем «полный» цикл из четырёх шагов, о котором говорили раньше. До сих пор Claude вызывал инструменты, но мы ни разу не отправляли ему следующий запрос с результатом инструмента. Напомним все четыре шага:
- Дать Claude инструменты и запрос пользователя (запрос к API): описать инструменты — имена, описания, схемы входных данных — и передать запрос, например «Сколько акций General Motors я могу купить на 500 долларов?».
- Claude вызывает инструмент (ответ API): Claude решает, какой инструмент нужен и с какими данными, и выдаёт
запрос на вызов;
stop_reasonв ответе равенtool_use. - Извлечь входные данные, запустить код и вернуть результат (запрос к API): достать имя инструмента и входные
данные, запустить код у себя и вернуть результат новым сообщением пользователя с блоком
tool_result. - Claude использует результат, чтобы ответить (ответ API).
В этом уроке пройдём весь процесс. Общая схема:
flowchart TD A["1. Запрос к API: инструменты + вопрос"] --> B["2. Ответ: stop_reason = tool_use, блок tool_use"] B --> C["3. Наш код: достаём имя и input, вызываем функцию"] C --> D["Запрос к API: вся история + сообщение user с блоком tool_result"] D --> E["4. Ответ: окончательный текст"]
Наш скрипт будет устроен так: пользователь задаёт вопрос; если для ответа нужны свежие сведения, Claude просит
вызвать инструмент get_article с поисковым запросом; мы находим статью Википедии и отправляем её текст обратно;
Claude отвечает, опираясь на статью.
Несколько настоящих диалогов, к которым мы стремимся:
Пользователь: Кто на 2024 год получил больше «Оскаров» — Кристофер Нолан или Бен Стиллер?
Claude хочет получить статью о Кристофере Нолане (вызов инструмента)
Claude хочет получить статью о Бене Стиллере (вызов инструмента)
Claude: На 2024 год режиссёр Кристофер Нолан получил две премии «Оскар» — за лучшую режиссуру и лучший фильм, за «Оппенгеймера» (2023). Актёр и комик Бен Стиллер пока не получил ни одного «Оскара», хотя у него есть другие награды, например «Эмми» и премии MTV. Так что прямой ответ: на 2024 год у Кристофера Нолана больше «Оскаров» (2), чем у Бена Стиллера, у которого их нет.
Ещё пример:
Пользователь: Кто выиграл турнир Masters 2024 года?
Claude хочет получить статью о 2024 Masters Tournament (вызов инструмента)
Claude: Турнир Masters 2024 года выиграл Скотти Шеффлер — это его вторая победа на Masters. Он опередил на четыре удара занявшего второе место Людвига Оберга, для которого это был дебют на Masters.
Если спросить Claude о том, на что он может ответить без инструмента, он должен ответить сразу, не обращаясь к поиску по Википедии:
Пользователь: Кто выиграл Masters в 2021 году?
Claude: Masters 2021 года выиграл Хидэки Мацуяма — первый японец, победивший на мэйджоре в мужском гольфе. Он завершил турнир с результатом 278 (−10) и получил зелёный пиджак в гольф-клубе Augusta National.
К концу процесса список messages выглядит так (в оригинале — схема):
[ {"role": "user", "content": "Кто выиграл турнир Masters 2024 года?"}, {"role": "assistant", "content": [TextBlock(...), ToolUseBlock(id="toolu_X", name="get_article", input={...})]}, {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_X", "content": "<текст статьи>"}]}, {"role": "assistant", "content": [TextBlock("Турнир выиграл Скотти Шеффлер...")]}]Главная новая тема урока — как правильно оформить сообщение пользователя с tool_result в ответ на сообщение
Claude с tool_use.
Строим полный цикл
Заголовок раздела «Строим полный цикл»1. Функция поиска по Википедии
Заголовок раздела «1. Функция поиска по Википедии»Прежде чем подключать Claude, напишем простую функцию поиска по Википедии. Она с помощью пакета wikipedia ищет
страницы по запросу. Для простоты берём название первой найденной страницы и загружаем её текст.
Примечание: эта простая функция предполагает, что статья найдётся. Чтобы не раздувать пример, в ней нет обработки ошибок — в реальной жизни так делать не стоит!
import wikipedia
def get_article(search_term): results = wikipedia.search(search_term) # список названий подходящих статей first_result = results[0] # берём первое page = wikipedia.page(first_result, auto_suggest=False) # загружаем страницу return page.content # весь текст статьиarticle = get_article("Superman")print(article[:500]) # статья очень длинная, печатаем только началоSuperman is a superhero who appears in American comic books published by DC Comics. The character was created by writer Jerry Siegel and artist Joe Shuster, and debuted in the comic book Action Comics #1 ...(Начало английской статьи о Супермене: супергерой комиксов DC, создан Джерри Сигелом и Джо Шустером, дебютировал в Action Comics #1 в 1938 году.)
article = get_article("Zendaya")print(article[:500]) # статья очень длинная, печатаем только началоZendaya Maree Stoermer Coleman (born September 1, 1996) is an American actress and singer. She has received various accolades, including two Primetime Emmy Awards and a Golden Globe Award. ...(Зендея — американская актриса и певица, обладательница двух «Эмми» и «Золотого глобуса».)
2. Описание инструмента
Заголовок раздела «2. Описание инструмента»Теперь опишем инструмент в формате JSON Schema. Описание очень простое: функция ждёт один аргумент — строку поискового запроса.
article_search_tool = { "name": "get_article", "description": "Инструмент для получения актуальной статьи Википедии.", "input_schema": { "type": "object", "properties": { "search_term": { "type": "string", "description": "Поисковый запрос, по которому ищется статья Википедии по названию" }, }, "required": ["search_term"] }}3. Даём Claude инструмент и запрос
Заголовок раздела «3. Даём Claude инструмент и запрос»Сообщим Claude, что у него есть поиск по Википедии, и зададим вопрос, на который он заведомо не ответит без инструмента: «Кто выиграл турнир Masters 2024 года?»
from claude_local import client, MODEL_NAME
messages = [{"role": "user", "content": "Кто выиграл турнир Masters 2024 года?"}]
response = client.messages.create( model=MODEL_NAME, # в оригинале — "claude-3-sonnet-20240229" messages=messages, max_tokens=1000, tools=[article_search_tool])4. Claude вызывает инструмент (ответ API)
Заголовок раздела «4. Claude вызывает инструмент (ответ API)»Посмотрим на ответ. Claude хочет воспользоваться инструментом!
response.content[TextBlock(text='Okay, let me see if I can find information about the winner of the 2024 Masters Tournament on Wikipedia:', type='text'), ToolUseBlock(id='toolu_01CWuduGjgSRfsYJUNC2wxi7', input={'search_term': '2024 Masters Tournament'}, name='get_article', type='tool_use')]В ответе Claude два блока:
TextBlockс текстом «Хорошо, посмотрю, найдётся ли в Википедии информация о победителе турнира Masters 2024 года:»;ToolUseBlock, который вызывает наш инструментget_articleсsearch_term="2024 Masters Tournament".
5. Извлекаем входные данные, запускаем код и возвращаем результат (запрос к API)
Заголовок раздела «5. Извлекаем входные данные, запускаем код и возвращаем результат (запрос к API)»Claude сообщил, что хочет вызвать инструмент. Пора действительно выполнить функцию И ответить Claude текстом найденной статьи.
Особенно внимательно нужно обновлять список messages.
Сначала добавим в messages последний ответ Claude:
messages.append({"role": "assistant", "content": response.content})messages[{'role': 'user', 'content': 'Кто выиграл турнир Masters 2024 года?'}, {'role': 'assistant', 'content': [TextBlock(text='Okay, let me see if I can find information about the winner of the 2024 Masters Tournament on Wikipedia:', type='text'), ToolUseBlock(id='toolu_01CWuduGjgSRfsYJUNC2wxi7', input={'search_term': '2024 Masters Tournament'}, name='get_article', type='tool_use')]}]Затем достанем инструмент и аргументы, которые хочет использовать Claude:
# Простой, но хрупкий способ получить данные о вызове инструмента:# мы просто берём последний блок ответа Claude.tool_use = response.content[-1]tool_name = tool_use.nametool_input = tool_use.inputprint("Имя инструмента:", tool_name)print("Входные данные:", tool_input)Имя инструмента: get_articleВходные данные: {'search_term': '2024 Masters Tournament'}Убедимся, что Claude вызывает именно ожидаемый get_article, и передадим придуманный Claude search_term в нашу
функцию get_article:
if tool_name == "get_article": search_term = tool_input["search_term"] wiki_result = get_article(search_term) print(f"Ищем в Википедии: {search_term}") print("ТЕКСТ СТРАНИЦЫ ВИКИПЕДИИ:") print(wiki_result[:500]) # печатаем только кусочек — статья длиннаяИщем в Википедии: 2024 Masters TournamentТЕКСТ СТРАНИЦЫ ВИКИПЕДИИ:The 2024 Masters Tournament was the 88th edition of the Masters Tournament and the first of the men's four major golf championships held in 2024. ... Scottie Scheffler won his second Masters and major, four strokes ahead of runner-up Ludvig Åberg, who was playing in his first major. ...Мы выполнили функцию, которую просил вызвать Claude. Теперь нужно отправить Claude данные со страницы Википедии.
Как мы знаем, когда Claude хочет вызвать инструмент, он отвечает с stop_reason, равным tool_use, и одним или
несколькими блоками tool_use, в которых есть:
id— уникальный идентификатор этого блока вызова. По нему потом сопоставляется результат;name— имя вызываемого инструмента;input— объект с входными данными для инструмента, соответствующий егоinput_schema.
Выполнив функцию инструмента, мы тоже должны ответить Claude в определённом формате. Чтобы продолжить диалог,
отправляем новое сообщение с ролью user, в котором лежит блок типа tool_result со следующими полями:
tool_use_id—idтого запроса на вызов, результатом которого является этот блок;content— результат инструмента: строка (например,"content": "15 градусов") или список вложенных блоков (например,"content": [{"type": "text", "text": "15 градусов"}]);is_error(необязательное) —true, если при выполнении инструмента произошла ошибка.
Пример правильно оформленного сообщения tool_result:
{ "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "Здесь результат настоящего вызова инструмента" } ]}Сделаем так для нашего примера с Википедией — соберём сообщение с результатом поиска, чтобы отправить его Claude:
tool_response = { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": tool_use.id, # тот же id, что в блоке tool_use "content": wiki_result } ]}# Обратите внимание на длинный текст статьи!tool_response{'role': 'user', 'content': [{'type': 'tool_result', 'tool_use_id': 'toolu_01CWuduGjgSRfsYJUNC2wxi7', 'content': "The 2024 Masters Tournament was the 88th edition of the Masters Tournament ... == Course == ... == Field == ......[обрезано]Добавим tool_response в список сообщений:
messages.append(tool_response)Теперь messages выглядит так:
- Пользователь: Кто выиграл турнир Masters 2024 года?
- Ассистент: Хочу вызвать инструмент
get_articleсsearch_term= «2024 Masters Tournament». - Пользователь: Вот результат инструмента — запрошенная тобой статья Википедии.
Обратите внимание: id из блока вызова совпадает с tool_use_id в следующем сообщении пользователя.
sequenceDiagram participant U as user participant A as assistant U->>A: Кто выиграл Masters 2024? A->>U: tool_use id=toolu_01CW... get_article U->>A: tool_result tool_use_id=toolu_01CW... текст статьи A->>U: Турнир выиграл Скотти Шеффлер
6. Claude использует результат, чтобы ответить (ответ API)
Заголовок раздела «6. Claude использует результат, чтобы ответить (ответ API)»Наконец, отправим Claude новый запрос с обновлённым списком messages:
follow_up_response = client.messages.create( model=MODEL_NAME, # в оригинале — "claude-3-sonnet-20240229" messages=messages, max_tokens=1000, tools=[article_search_tool] # инструменты передаём и в повторном запросе)follow_up_response.content[0].textОтвет в оригинале (перевод): «Судя по сводке из Википедии, турнир Masters 2024 года выиграл Скотти Шеффлер. В финальном раунде он прошёл с результатом 68 и закончил на 11 ударов ниже пара, на четыре удара впереди Людвига Оберга. Это второй титул Masters у Шеффлера, первый был в 2022 году. Итак, главное: победитель — Скотти Шеффлер (−11), второе место — Людвиг Оберг (−7). В статье подробно описан каждый раунд… Дайте знать, если нужна ещё какая-то информация!»
Теперь у Claude есть сведения, нужные для ответа на исходный вопрос. Мы прошли все четыре шага!
Улучшаем код
Заголовок раздела «Улучшаем код»Как минимум, весь код выше стоит собрать в функцию, чтобы попробовать её несколько раз:
def answer_question(question): messages = [{"role": "user", "content": question}]
response = client.messages.create( model=MODEL_NAME, # в оригинале — "claude-3-sonnet-20240229" messages=messages, max_tokens=1000, tools=[article_search_tool] )
if response.stop_reason == "tool_use": tool_use = response.content[-1] tool_name = tool_use.name tool_input = tool_use.input # Добавляем в messages вызов инструмента от Claude: messages.append({"role": "assistant", "content": response.content})
if tool_name == "get_article": search_term = tool_input["search_term"] print(f"Claude хочет получить статью: {search_term}") wiki_result = get_article(search_term) # загружаем статью Википедии # Собираем сообщение tool_result tool_response = { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": tool_use.id, "content": wiki_result } ] } messages.append(tool_response) # Отвечаем Claude response = client.messages.create( model=MODEL_NAME, messages=messages, max_tokens=1000, tools=[article_search_tool] ) print("Окончательный ответ Claude:") print(response.content[0].text)
else: print("Claude не вызвал наш инструмент") print(response.content[0].text)answer_question("Кто выиграл Гран-при Австралии Формулы-1 2024 года?")Вывод в оригинале (перевод): «Claude хочет получить статью: 2024 F1 Australian Grand Prix. Окончательный ответ: главное из статьи — Гран-при Австралии 2024 года выиграл Карлос Сайнс-младший (Ferrari), второе место у его напарника Шарля Леклера, третье — у Ландо Норриса (McLaren). Макс Ферстаппен (Red Bull) стартовал с поула, но рано сошёл из-за отказа тормозов, прервав серию из девяти побед подряд. Гонка установила рекорд посещаемости Альберт-парка — 452 055 зрителей за уик-энд».
answer_question("Кто играет главные роли в фильме «Претенденты» (Challengers)?")Вывод в оригинале (перевод): Claude запросил статью «Challengers (2023 film)» и ответил, что главные роли играют Зендея (Таши Дункан), Джош О’Коннор (Патрик Цвейг) и Майк Фейст (Арт Дональдсон), и перечислил актёров второго плана.
# Пример, где Claude НЕ должен нуждаться в инструменте:answer_question("Кто написал книгу «Жизнь Пи»?")Вывод в оригинале (перевод): «Claude хочет получить статью: Life of Pi. Окончательный ответ: судя по статье Википедии, книгу „Жизнь Пи“ написал канадский писатель Янн Мартел, она вышла в 2001 году, получила Букеровскую премию в 2002-м и разошлась тиражом более 10 миллионов экземпляров…»
Заметьте: чтобы ответить на последний вопрос, Claude вызвал Википедию, хотя и так знает ответ. «Жизнь Пи» вышла в 2001 году — задолго до даты среза обучающих данных Claude!
Улучшаем промпт
Заголовок раздела «Улучшаем промпт»Как мы видели в одном из прошлых уроков, Claude иногда слишком рвётся пользоваться инструментами. Проще всего исправить это системным промптом. Например, таким:
system_prompt = """ Пользователь задаст тебе вопрос. Если для ответа нужны данные, которых не было в твоём обучении, можешь воспользоваться инструментом get_article и получить текст свежей статьи Википедии по теме. Если можешь ответить без дополнительной информации — отвечай сам. Вызывай инструмент, только когда это нужно. """Обновим функцию: передадим этот системный промпт в оба вызова client.messages.create параметром
system=system_prompt. Остальной код не меняется:
def answer_question(question): system_prompt = """ Пользователь задаст тебе вопрос. Если для ответа нужны данные, которых не было в твоём обучении, можешь воспользоваться инструментом get_article и получить текст свежей статьи Википедии по теме. Если можешь ответить без дополнительной информации — отвечай сам. Вызывай инструмент, только когда это нужно. """
messages = [{"role": "user", "content": question}]
response = client.messages.create( model=MODEL_NAME, system=system_prompt, # новое messages=messages, max_tokens=1000, tools=[article_search_tool] )
if response.stop_reason == "tool_use": tool_use = response.content[-1] tool_name = tool_use.name tool_input = tool_use.input messages.append({"role": "assistant", "content": response.content})
if tool_name == "get_article": search_term = tool_input["search_term"] print(f"Claude хочет получить статью: {search_term}") wiki_result = get_article(search_term) tool_response = { "role": "user", "content": [ {"type": "tool_result", "tool_use_id": tool_use.id, "content": wiki_result} ] } messages.append(tool_response) response = client.messages.create( model=MODEL_NAME, system=system_prompt, # новое messages=messages, max_tokens=1000, tools=[article_search_tool] ) print("Окончательный ответ Claude:") print(response.content[0].text)
else: print("Claude не вызвал наш инструмент") print(response.content[0].text)Зададим тот же вопрос:
answer_question("Кто написал книгу «Жизнь Пи»?")Вывод в оригинале (перевод): «Claude не вызвал наш инструмент. Книгу „Жизнь Пи“ написал канадский писатель Янн Мартел. Она вышла в 2001 году и стала международным бестселлером… получила Букеровскую премию в 2002 году, а в 2012-м её экранизировал Энг Ли».
Сработало! Claude не стал пользоваться инструментом, когда он не нужен. Проверим, что инструмент по-прежнему вызывается для вопросов, где нужны свежие знания:
answer_question("Кто написал музыку к фильму «Претенденты»?")Вывод в оригинале (перевод): Claude запросил статью «Challengers (film)» и ответил, что музыку написали Трент Резнор и Аттикус Росс, приведя цитату из статьи, а затем ещё раз повторив вывод.
Работает в обоих случаях! Теперь сделаем ответ покороче. Хорошо, что Claude объясняет, КАК пришёл к ответу, но это многословно. Немного базового промпт-инжиниринга:
prompt = f""" Ответь на вопрос <question>Кто написал сценарий фильма «Бедные-несчастные»?</question> Когда сможешь ответить, сделай ответ как можно короче и заключи его в теги <answer> """Функция с новым промптом (меняются только первые строки — остальное как в предыдущей версии):
def answer_question(question): system_prompt = """ Пользователь задаст тебе вопрос. Если для ответа нужны данные, которых не было в твоём обучении, можешь воспользоваться инструментом get_article и получить текст свежей статьи Википедии по теме. Если можешь ответить без дополнительной информации — отвечай сам. Вызывай инструмент, только когда это нужно. """ prompt = f""" Ответь на вопрос <question>{question}</question> Когда сможешь ответить, сделай ответ как можно короче и заключи его в теги <answer> """ messages = [{"role": "user", "content": prompt}] # вместо голого question — промпт с тегами # ... дальше без изменений: запрос, проверка stop_reason, tool_result, повторный запрос, печатьanswer_question("Кто написал музыку к фильму «Претенденты»?")Claude хочет получить статью: Challengers (2023 film)Окончательный ответ Claude:<answer>Музыку к фильму «Претенденты» написали Трент Резнор и Аттикус Росс.</answer>answer_question("Сколько ног у осьминога?")Claude не вызвал наш инструмент<answer>У осьминога 8 ног.</answer>Гораздо лучше! Теперь Claude отвечает без лишних «размышлений» и объяснений, как он пришёл к ответу.
Упражнение
Заголовок раздела «Упражнение»Доработайте код, чтобы он выполнял такие требования:
- Claude может не хватить сведений из первой статьи, которую мы вернули, — этот случай мы пока не обрабатываем. Представьте вопрос: «Сколько „Оскаров“ у Кристофера Нолана? Больше ли их, чем „Эмми“ у Бена Стиллера?» Claude нужно найти статью о Нолане И статью о Стиллере, скорее всего одну за другой. Сейчас код этого не позволяет — добавьте такую возможность! Подсказка: используйте цикл!
- Извлекайте ответ из тегов
<answer></answer>, чтобы печатать только сам ответ. - Превратите всё это в чат-бота для командной строки, который снова и снова просит пользователя ввести вопрос и отвечает, пока пользователь не выйдет из программы. (В оригинале — скриншот такого диалога: пользователь спрашивает про «Оскары» Нолана и «Эмми» Стиллера, бот дважды обращается к Википедии и даёт короткий ответ.)
Набросок решения (от переводчика; в оригинале решения нет)
import re
def ask(question, max_steps=5): messages = [{"role": "user", "content": f""" Ответь на вопрос <question>{question}</question> Когда сможешь ответить, сделай ответ как можно короче и заключи его в теги <answer>"""}] for _ in range(max_steps): # цикл: модель может вызывать инструмент несколько раз response = client.messages.create(model=MODEL_NAME, system=system_prompt, messages=messages, max_tokens=1000, tools=[article_search_tool]) messages.append({"role": "assistant", "content": response.content}) if response.stop_reason != "tool_use": # инструментов больше не нужно — это ответ text = "".join(b.text for b in response.content if b.type == "text") m = re.search(r"<answer>(.*?)</answer>", text, re.DOTALL) return m.group(1).strip() if m else text results = [] for block in response.content: # модель может попросить несколько вызовов сразу if block.type == "tool_use" and block.name == "get_article": print(f"Ищу в Википедии: {block.input['search_term']}") results.append({"type": "tool_result", "tool_use_id": block.id, "content": get_article(block.input["search_term"])}) messages.append({"role": "user", "content": results}) return "Не удалось ответить за отведённое число шагов"
while True: q = input("\nВы (пустая строка — выход): ") if not q: break print("Бот:", ask(q))Урок 5. Выбор инструмента (tool_choice)
Заголовок раздела «Урок 5. Выбор инструмента (tool_choice)»Claude API поддерживает параметр tool_choice, который задаёт, как Claude должен вызывать инструменты. В этом
ноутбуке посмотрим, как он работает и когда его использовать.
У tool_choice три возможных значения:
auto— Claude сам решает, вызывать ли какой-то из инструментов;any— Claude обязан вызвать один из инструментов, но какой — выбирает сам;tool— Claude обязан вызвать конкретный инструмент.
flowchart LR Q["Запрос + инструменты"] --> A["auto: текст ИЛИ любой инструмент"] Q --> B["any: обязательно какой-то инструмент"] Q --> C["tool: обязательно названный инструмент"]
Разберём каждый режим. Начнём с импорта SDK:
from claude_local import client, MODEL_NAMEtool_choice = auto позволяет модели самой решать, пользоваться ли инструментами. Это поведение по умолчанию,
если параметр tool_choice вообще не передавать.
Для демонстрации дадим Claude фальшивый инструмент веб-поиска. Будем задавать вопросы: для одних нужен поиск, на другие Claude должен ответить сам.
Опишем инструмент web_search. Чтобы не усложнять пример, в интернете мы на самом деле ничего не ищем.
def web_search(topic): print(f"делаем вид, что ищем в интернете: {topic}")
web_search_tool = { "name": "web_search", "description": "Инструмент для получения актуальной информации по теме через поиск в интернете", "input_schema": { "type": "object", "properties": { "topic": { "type": "string", "description": "Тема, которую нужно найти в интернете" }, }, "required": ["topic"] }}Затем напишем функцию, которая принимает user_query и передаёт его Claude вместе с web_search_tool. Параметр
tool_choice ставим в auto:
tool_choice={"type": "auto"}Полная функция:
from datetime import date
def chat_with_web_search(user_query): messages = [{"role": "user", "content": user_query}]
system_prompt = f""" Отвечай на как можно больше вопросов, опираясь на свои знания. Ищи в интернете только то, на что не можешь уверенно ответить. Сегодня {date.today().strftime("%d.%m.%Y")}. Если тебе кажется, что вопрос касается будущего — того, что ещё не произошло, — воспользуйся поиском. """
response = client.messages.create( system=system_prompt, model=MODEL_NAME, # в оригинале — "claude-3-sonnet-20240229" messages=messages, max_tokens=1000, tool_choice={"type": "auto"}, tools=[web_search_tool] ) last_content_block = response.content[-1] if last_content_block.type == "text": print("Claude НЕ вызвал инструмент") print(f"Ассистент: {last_content_block.text}") elif last_content_block.type == "tool_use": print("Claude хочет воспользоваться инструментом") print(last_content_block)Начнём с вопроса, на который Claude должен ответить без инструмента:
chat_with_web_search("Какого цвета небо?")Claude НЕ вызвал инструментАссистент: Днём небо выглядит голубым. Атмосфера Земли рассеивает синий свет Солнца сильнее других цветов, поэтому небо кажется голубым.На вопрос «Какого цвета небо?» Claude не пользуется инструментом. Теперь спросим то, для чего нужен поиск:
chat_with_web_search("Кто выиграл Гран-при Майами 2024 года?")Claude хочет воспользоваться инструментомToolUseBlock(id='toolu_staging_018nwaaRebX33pHqoZZXDaSw', input={'topic': '2024 Miami Grand Prix winner'}, name='web_search', type='tool_use')На этот вопрос Claude вызывает веб-поиск! Ещё несколько примеров:
# Здесь инструмент Claude НЕ нужен:chat_with_web_search("Кто выиграл Супербоул в 2022 году?")Claude НЕ вызвал инструментАссистент: Супербоул LVI в 2022 году выиграли «Лос-Анджелес Рэмс», победив «Цинциннати Бенгалс» со счётом 23:20. Матч прошёл 13 февраля 2022 года на стадионе SoFi в Инглвуде, Калифорния.# Здесь Claude ДОЛЖЕН воспользоваться инструментом:chat_with_web_search("Кто выиграл Супербоул в 2024 году?")Claude хочет воспользоваться инструментомToolUseBlock(id='toolu_staging_016XPwcprHAgYJBtN7A3jLhb', input={'topic': '2024 Super Bowl winner'}, name='web_search', type='tool_use')Промпт важен!
Заголовок раздела «Промпт важен!»В режиме auto важно потратить время на подробный промпт. Claude часто слишком охотно вызывает инструменты, и
подробный промпт помогает ему понять, когда инструмент нужен, а когда нет. В примере выше мы дали в системном промпте
конкретные указания:
system_prompt = f""" Отвечай на как можно больше вопросов, опираясь на свои знания. Ищи в интернете только то, на что не можешь уверенно ответить. Сегодня {date.today().strftime("%d.%m.%Y")}. Если тебе кажется, что вопрос касается будущего — того, что ещё не произошло, — воспользуйся поиском."""Принудительный вызов конкретного инструмента
Заголовок раздела «Принудительный вызов конкретного инструмента»Через tool_choice можно заставить Claude вызвать определённый инструмент. В примере ниже два простых инструмента:
print_sentiment_scores— инструмент, который «обманом» заставляет Claude выдать хорошо структурированный JSON с анализом тональности (подробнее о приёме — в уроке 3 и в рецепте Extracting Structured JSON using Claude and Tool Use кукбука Anthropic);calculator— очень простой калькулятор, который складывает два числа.
tools = [ { "name": "print_sentiment_scores", "description": "Печатает оценки тональности твита или фрагмента текста.", "input_schema": { "type": "object", "properties": { "positive_score": {"type": "number", "description": "Оценка положительной тональности, от 0.0 до 1.0."}, "negative_score": {"type": "number", "description": "Оценка отрицательной тональности, от 0.0 до 1.0."}, "neutral_score": {"type": "number", "description": "Оценка нейтральной тональности, от 0.0 до 1.0."} }, "required": ["positive_score", "negative_score", "neutral_score"] } }, { "name": "calculator", "description": "Складывает два числа", "input_schema": { "type": "object", "properties": { "num1": {"type": "number", "description": "первое слагаемое"}, "num2": {"type": "number", "description": "второе слагаемое"}, }, "required": ["num1", "num2"] } }]Цель — функция analyze_tweet_sentiment, которая принимает твит и с помощью Claude печатает простой анализ его
тональности. В итоге мы «заставим» Claude вызывать print_sentiment_scores, но сначала посмотрим, что будет, если
не заставлять.
В первой, «плохой» версии функции даём Claude оба инструмента и для сравнения ставим tool_choice в auto:
tool_choice={"type": "auto"}Мы намеренно не даём Claude хорошего промпта, чтобы эффект принудительного вызова было легче увидеть.
def analyze_tweet_sentiment(query): response = client.messages.create( model=MODEL_NAME, # в оригинале — "claude-3-sonnet-20240229" max_tokens=4096, tools=tools, tool_choice={"type": "auto"}, messages=[{"role": "user", "content": query}] ) print(response)Вызовем функцию с твитом «Ничего себе, я только что приготовил самое потрясающее блюдо!»:
analyze_tweet_sentiment("Ничего себе, я только что приготовил самое потрясающее блюдо!")ToolsBetaMessage(id='msg_staging_01ApgXx7W7qsDugdaRWh6p21', content=[TextBlock(text="That's great to hear! I don't actually have the capability to assess sentiment from text, but it sounds like you're really excited ...", type='text')], model='claude-3-sonnet-20240229', role='assistant', stop_reason='end_turn', stop_sequence=None, type='message', usage=Usage(input_tokens=429, output_tokens=69))Claude не вызывает print_sentiment_scores, а сразу отвечает:
«Здорово! Вообще-то я не умею оценивать тональность текста, но, похоже, вы очень рады и гордитесь потрясающим блюдом, которое приготовили…»
Теперь представим такой твит: «Обожаю своих кошек! У меня было четыре, и я только что взял ещё двух! Угадайте, сколько у меня теперь?»
analyze_tweet_sentiment("Обожаю своих кошек! У меня было четыре, и я только что взял ещё двух! Угадайте, сколько у меня теперь?")ToolsBetaMessage(id='msg_staging_018gTrwrx6YwBR2jjhdPooVg', content=[TextBlock(text="That's wonderful that you love your cats and adopted two more! To figure out how many cats you have now, I can use the calculator tool:", type='text'), ToolUseBlock(id='toolu_staging_01RFker5oMQoY6jErz5prmZg', input={'num1': 4, 'num2': 2}, name='calculator', type='tool_use')], model='claude-3-sonnet-20240229', role='assistant', stop_reason='tool_use', stop_sequence=None, type='message', usage=Usage(input_tokens=442, output_tokens=101))Claude хочет вызвать калькулятор:
ToolUseBlock(id=‘toolu_staging_01RFker5oMQoY6jErz5prmZg’, input={‘num1’: 4, ‘num2’: 2}, name=‘calculator’, type=‘tool_use’)
Очевидно, эта реализация делает не то, что нам нужно (в основном потому, что мы её так и задумали).
Заставим Claude всегда вызывать print_sentiment_scores, изменив tool_choice:
tool_choice={"type": "tool", "name": "print_sentiment_scores"}Кроме type = tool, нужно указать имя конкретного инструмента.
def analyze_tweet_sentiment(query): response = client.messages.create( model=MODEL_NAME, # в оригинале — "claude-3-sonnet-20240229" max_tokens=4096, tools=tools, tool_choice={"type": "tool", "name": "print_sentiment_scores"}, messages=[{"role": "user", "content": query}] ) print(response)Теперь с теми же промптами Claude всегда будет вызывать print_sentiment_scores:
analyze_tweet_sentiment("Ничего себе, я только что приготовил самое потрясающее блюдо!")ToolsBetaMessage(id='msg_staging_018GtYk8Xvee3w8Eeh6pbgoq', content=[ToolUseBlock(id='toolu_staging_01FMRQ9pZniZqFUGQwTcFU4N', input={'positive_score': 0.9, 'negative_score': 0.0, 'neutral_score': 0.1}, name='print_sentiment_scores', type='tool_use')], model='claude-3-sonnet-20240229', role='assistant', stop_reason='tool_use', stop_sequence=None, type='message', usage=Usage(input_tokens=527, output_tokens=79))Claude вызывает print_sentiment_scores с оценками 0.9 / 0.0 / 0.1. Даже если попытаться сбить его «математическим»
твитом, он всё равно вызывает print_sentiment_scores:
analyze_tweet_sentiment("Обожаю своих кошек! У меня было четыре, и я только что взял ещё двух! Угадайте, сколько у меня теперь?")ToolsBetaMessage(id='msg_staging_01RACamfrHdpvLxWaNwDfZEF', content=[ToolUseBlock(id='toolu_staging_01Wb6ZKSwKvqVSKLDAte9cKU', input={'positive_score': 0.8, 'negative_score': 0.0, 'neutral_score': 0.2}, name='print_sentiment_scores', type='tool_use')], model='claude-3-sonnet-20240229', role='assistant', stop_reason='tool_use', stop_sequence=None, type='message', usage=Usage(input_tokens=540, output_tokens=79))Даже когда мы заставляем Claude вызывать print_sentiment_scores, базовый промпт-инжиниринг всё равно нужен, чтобы
дать Claude контекст задачи:
def analyze_tweet_sentiment(query):
prompt = f""" Проанализируй тональность этого твита: <tweet>{query}</tweet> """
response = client.messages.create( model=MODEL_NAME, # в оригинале — "claude-3-sonnet-20240229" max_tokens=4096, tools=tools, tool_choice={"type": "auto"}, # так в оригинале; по смыслу раздела здесь должен быть {"type": "tool", "name": "print_sentiment_scores"} messages=[{"role": "user", "content": prompt}] ) print(response)Последнее значение tool_choice — any. Оно говорит Claude: «Ты обязан вызвать инструмент, но какой — выбирай
сам». Представим, что мы делаем SMS-чат-бота на Claude. Единственный способ для бота «общаться» с пользователем —
SMS-сообщения.
В примере ниже — очень простой SMS-помощник с двумя инструментами:
send_text_to_user— отправляет пользователю SMS;get_customer_info— ищет данные клиента по имени пользователя.
Идея в том, что бот всегда вызывает один из этих инструментов и никогда не отвечает простым текстом. В любой
ситуации Claude должен либо отправить SMS, либо вызвать get_customer_info, чтобы узнать больше о клиенте. Чтобы
это гарантировать, ставим tool_choice в any:
tool_choice={"type": "any"}def send_text_to_user(text): # Отправляет пользователю SMS. # Для простоты просто печатаем текст: print(f"ОТПРАВЛЕНО SMS: {text}")
def get_customer_info(username): return { "username": username, "email": f"{username}@email.com", "purchases": [ {"id": 1, "product": "компьютерная мышь"}, {"id": 2, "product": "защитное стекло для экрана"}, {"id": 3, "product": "USB-кабель для зарядки"}, ] }
tools = [ { "name": "send_text_to_user", "description": "Отправляет пользователю SMS", "input_schema": { "type": "object", "properties": { "text": {"type": "string", "description": "Текст, который нужно отправить пользователю в SMS"}, }, "required": ["text"] } }, { "name": "get_customer_info", "description": "Получает информацию о клиенте по его имени пользователя. В ответе — email, имя пользователя и прошлые покупки. Вызывай этот инструмент только после того, как пользователь сообщил своё имя пользователя", "input_schema": { "type": "object", "properties": { "username": {"type": "string", "description": "Имя пользователя (логин) этого клиента."}, }, "required": ["username"] } },]
system_prompt = """Всё твоё общение с пользователем идёт через SMS.Вызывай инструменты, только когда у тебя достаточно сведений, чтобы вызвать их правильно.Не вызывай get_customer_info, пока пользователь не сообщил своё имя пользователя. Это важно.Если не знаешь имени пользователя, просто спроси его."""
def sms_chatbot(user_message): messages = [{"role": "user", "content": user_message}]
response = client.messages.create( system=system_prompt, model=MODEL_NAME, # в оригинале — "claude-3-sonnet-20240229" max_tokens=4096, tools=tools, tool_choice={"type": "any"}, # обязательно какой-нибудь инструмент messages=messages ) if response.stop_reason == "tool_use": last_content_block = response.content[-1] if last_content_block.type == 'tool_use': tool_name = last_content_block.name tool_inputs = last_content_block.input print(f"=======Claude хочет вызвать инструмент {tool_name}=======") if tool_name == "send_text_to_user": send_text_to_user(tool_inputs["text"]) elif tool_name == "get_customer_info": print(get_customer_info(tool_inputs["username"])) else: print("Ой, такого инструмента нет!")
else: print("Инструмент не вызван. Такого быть не должно!")Начнём с простого:
sms_chatbot("Привет! Как дела?")=======Claude хочет вызвать инструмент send_text_to_user=======ОТПРАВЛЕНО SMS: Здравствуйте! У меня всё хорошо, спасибо. Чем могу помочь?Claude отвечает, вызывая send_text_to_user. Теперь спросим что-нибудь посложнее:
sms_chatbot("Мне нужна помощь с поиском заказа")=======Claude хочет вызвать инструмент send_text_to_user=======ОТПРАВЛЕНО SMS: Здравствуйте! Чтобы найти ваш заказ, мне сначала нужно ваше имя пользователя. Подскажите его, пожалуйста.Claude отправляет SMS с просьбой назвать имя пользователя. Посмотрим, что будет, если назвать его сразу:
sms_chatbot("Мне нужна помощь с поиском заказа. Моё имя пользователя — jenny76")=======Claude хочет вызвать инструмент get_customer_info======={'username': 'jenny76', 'email': 'jenny76@email.com', 'purchases': [{'id': 1, 'product': 'компьютерная мышь'}, {'id': 2, 'product': 'защитное стекло для экрана'}, {'id': 3, 'product': 'USB-кабель для зарядки'}]}Claude вызывает get_customer_info, как мы и хотели!
Даже на бессмысленное сообщение Claude всё равно вызовет один из инструментов:
sms_chatbot("askdj aksjdh asjkdbhas kjdhas 1+1 ajsdh")=======Claude хочет вызвать инструмент send_text_to_user=======ОТПРАВЛЕНО SMS: Боюсь, я не понял ваш запрос. Не могли бы вы переформулировать, с чем вам нужна помощь?Урок 6. Чат-бот с несколькими инструментами
Заголовок раздела «Урок 6. Чат-бот с несколькими инструментами»Поднимем работу с инструментами на новый уровень! Дадим Claude набор инструментов, из которых он будет выбирать. Цель — простой чат-бот поддержки вымышленной компании электроники TechNova. У него будут простые инструменты:
get_user— найти пользователя по email, имени пользователя или номеру телефона;get_order_by_id— найти заказ по номеру;get_customer_orders— найти все заказы клиента;cancel_order— отменить заказ с указанным номером.
Чат-бот будет очень ограниченным, но на нём видны ключевые моменты работы с несколькими инструментами.
(В оригинале — два скриншота диалога. В первом пользователь просит отменить заказ, бот спрашивает email, находит
клиента, его заказы и отменяет нужный. Во втором то же самое, но каждый вызов инструмента дополнительно печатается
строкой вида ====Claude wants to use the get_user tool====, чтобы было видно, что происходит «за кулисами».)
Поток информации для одного сообщения в чате:
flowchart TD U["Сообщение пользователя"] --> C["Claude + 4 инструмента"] C -->|"stop_reason = tool_use"| P["process_tool_call: метод FakeDatabase"] P -->|"tool_result"| C C -->|"end_turn"| R["Ответ пользователю"]
Наша фальшивая база данных
Заголовок раздела «Наша фальшивая база данных»Прежде чем подключать Claude, опишем очень простой класс FakeDatabase с вымышленными клиентами и заказами и
методами для работы с ними. В реальности чат-бот был бы подключён к одной или нескольким настоящим базам данных.
class FakeDatabase: def __init__(self): # Вымышленные клиенты self.customers = [ {"id": "1213210", "name": "John Doe", "email": "john@gmail.com", "phone": "123-456-7890", "username": "johndoe"}, {"id": "2837622", "name": "Priya Patel", "email": "priya@candy.com", "phone": "987-654-3210", "username": "priya123"}, {"id": "3924156", "name": "Liam Nguyen", "email": "lnguyen@yahoo.com", "phone": "555-123-4567", "username": "liamn"}, {"id": "4782901", "name": "Aaliyah Davis", "email": "aaliyahd@hotmail.com", "phone": "111-222-3333", "username": "adavis"}, {"id": "5190753", "name": "Hiroshi Nakamura", "email": "hiroshi@gmail.com", "phone": "444-555-6666", "username": "hiroshin"}, {"id": "6824095", "name": "Fatima Ahmed", "email": "fatimaa@outlook.com", "phone": "777-888-9999", "username": "fatimaahmed"}, {"id": "7135680", "name": "Alejandro Rodriguez", "email": "arodriguez@protonmail.com", "phone": "222-333-4444", "username": "alexr"}, {"id": "8259147", "name": "Megan Anderson", "email": "megana@gmail.com", "phone": "666-777-8888", "username": "manderson"}, {"id": "9603481", "name": "Kwame Osei", "email": "kwameo@yahoo.com", "phone": "999-000-1111", "username": "kwameo"}, {"id": "1057426", "name": "Mei Lin", "email": "meilin@gmail.com", "phone": "333-444-5555", "username": "mlin"} ]
# Вымышленные заказы; статусы: Shipped — отправлен, Processing — в обработке, Delivered — доставлен self.orders = [ {"id": "24601", "customer_id": "1213210", "product": "Беспроводные наушники", "quantity": 1, "price": 79.99, "status": "Shipped"}, {"id": "13579", "customer_id": "1213210", "product": "Чехол для смартфона", "quantity": 2, "price": 19.99, "status": "Processing"}, {"id": "97531", "customer_id": "2837622", "product": "Bluetooth-колонка", "quantity": 1, "price": "49.99", "status": "Shipped"}, {"id": "86420", "customer_id": "3924156", "product": "Фитнес-браслет", "quantity": 1, "price": 129.99, "status": "Delivered"}, {"id": "54321", "customer_id": "4782901", "product": "Чехол для ноутбука", "quantity": 3, "price": 24.99, "status": "Shipped"}, {"id": "19283", "customer_id": "5190753", "product": "Беспроводная мышь", "quantity": 1, "price": 34.99, "status": "Processing"}, {"id": "74651", "customer_id": "6824095", "product": "Игровая клавиатура", "quantity": 1, "price": 89.99, "status": "Delivered"}, {"id": "30298", "customer_id": "7135680", "product": "Портативный аккумулятор", "quantity": 2, "price": 29.99, "status": "Shipped"}, {"id": "47652", "customer_id": "8259147", "product": "Умные часы", "quantity": 1, "price": 199.99, "status": "Processing"}, {"id": "61984", "customer_id": "9603481", "product": "Наушники с шумоподавлением", "quantity": 1, "price": 149.99, "status": "Shipped"}, {"id": "58243", "customer_id": "1057426", "product": "Беспроводные вкладыши", "quantity": 2, "price": 99.99, "status": "Delivered"}, {"id": "90357", "customer_id": "1213210", "product": "Чехол для смартфона", "quantity": 1, "price": 19.99, "status": "Shipped"}, {"id": "28164", "customer_id": "2837622", "product": "Беспроводные наушники", "quantity": 2, "price": 79.99, "status": "Processing"} ]
def get_user(self, key, value): # Поиск пользователя по одному из трёх полей if key in {"email", "phone", "username"}: for customer in self.customers: if customer[key] == value: return customer return f"Не найден пользователь с {key} = {value}" else: raise ValueError(f"Недопустимый ключ: {key}")
return None
def get_order_by_id(self, order_id): for order in self.orders: if order["id"] == order_id: return order return None
def get_customer_orders(self, customer_id): return [order for order in self.orders if order["customer_id"] == customer_id]
def cancel_order(self, order_id): order = self.get_order_by_id(order_id) if order: if order["status"] == "Processing": # отменить можно только заказ «в обработке» order["status"] = "Cancelled" return "Заказ отменён" else: return "Заказ уже отправлен. Отменить его нельзя." return "Такой заказ не найден!"Создадим экземпляр FakeDatabase:
db = FakeDatabase()Проверим, что метод get_user работает как задумано. Он ждёт key — одно из значений "email", "username",
"phone" — и соответствующее value, по которому выполняет простой поиск и (надеемся) возвращает нужного
пользователя.
db.get_user("email", "john@gmail.com"){'id': '1213210', 'name': 'John Doe', 'email': 'john@gmail.com', 'phone': '123-456-7890', 'username': 'johndoe'}db.get_user("username", "adavis"){'id': '4782901', 'name': 'Aaliyah Davis', 'email': 'aaliyahd@hotmail.com', 'phone': '111-222-3333', 'username': 'adavis'}db.get_user("phone", "666-777-8888"){'id': '8259147', 'name': 'Megan Anderson', 'email': 'megana@gmail.com', 'phone': '666-777-8888', 'username': 'manderson'}Список всех заказов клиента можно получить через get_customer_orders, передав ID клиента:
db.get_customer_orders("1213210")[{'id': '24601', 'customer_id': '1213210', 'product': 'Беспроводные наушники', 'quantity': 1, 'price': 79.99, 'status': 'Shipped'}, {'id': '13579', 'customer_id': '1213210', 'product': 'Чехол для смартфона', 'quantity': 2, 'price': 19.99, 'status': 'Processing'}, {'id': '90357', 'customer_id': '1213210', 'product': 'Чехол для смартфона', 'quantity': 1, 'price': 19.99, 'status': 'Shipped'}]db.get_customer_orders("9603481")[{'id': '61984', 'customer_id': '9603481', 'product': 'Наушники с шумоподавлением', 'quantity': 1, 'price': 149.99, 'status': 'Shipped'}]Если номер заказа известен, заказ можно найти напрямую:
db.get_order_by_id('24601'){'id': '24601', 'customer_id': '1213210', 'product': 'Беспроводные наушники', 'quantity': 1, 'price': 79.99, 'status': 'Shipped'}Наконец, метод cancel_order отменяет заказ. Отменить можно только заказ в статусе «в обработке» (Processing):
уже отправленный заказ отменить нельзя! Метод ждёт order_id заказа, который нужно отменить.
# Найдём заказ в статусе «в обработке»:db.get_order_by_id("47652"){'id': '47652', 'customer_id': '8259147', 'product': 'Умные часы', 'quantity': 1, 'price': 199.99, 'status': 'Processing'}# Теперь отменим его!db.cancel_order("47652")'Заказ отменён'# Его статус теперь должен быть "Cancelled"db.get_order_by_id("47652"){'id': '47652', 'customer_id': '8259147', 'product': 'Умные часы', 'quantity': 1, 'price': 199.99, 'status': 'Cancelled'}Пишем инструменты
Заголовок раздела «Пишем инструменты»Мы проверили (очень) простую фальшивую базу, теперь напишем JSON-схемы инструментов. Начнём с самого простого — для
метода get_order_by_id:
tool1 = { "name": "get_order_by_id", "description": "Возвращает подробности заказа по его номеру: номер заказа, название товара, количество, цену и статус заказа.", "input_schema": { "type": "object", "properties": { "order_id": { "type": "string", "description": "Уникальный идентификатор заказа." } }, "required": ["order_id"] }}Как всегда, есть имя, описание и описание входных данных. Здесь вход один — обязательный order_id.
Теперь инструмент сложнее — для метода get_user. Напомним, у метода два аргумента:
key— одна из строк"email","username","phone";value— то, по чему ищем (сам email, номер телефона или имя пользователя).
Описание инструмента:
tool2 = { "name": "get_user", "description": "Ищет пользователя по email, телефону или имени пользователя.", "input_schema": { "type": "object", "properties": { "key": { "type": "string", "enum": ["email", "phone", "username"], "description": "Поле, по которому ищется пользователь (email, phone или username)." }, "value": { "type": "string", "description": "Значение, которое должно совпасть с указанным полем." } }, "required": ["key", "value"] }}Обратите особое внимание, как допустимые варианты key задаются через enum в схеме.
Осталось ещё два инструмента, но чтобы сэкономить время, вот сразу полный готовый список. (Можете сначала попробовать описать их сами — в качестве упражнения!)
tools = [ { "name": "get_user", "description": "Ищет пользователя по email, телефону или имени пользователя.", "input_schema": { "type": "object", "properties": { "key": { "type": "string", "enum": ["email", "phone", "username"], "description": "Поле, по которому ищется пользователь (email, phone или username)." }, "value": { "type": "string", "description": "Значение, которое должно совпасть с указанным полем." } }, "required": ["key", "value"] } }, { "name": "get_order_by_id", "description": "Возвращает подробности заказа по его номеру: номер заказа, название товара, количество, цену и статус заказа.", "input_schema": { "type": "object", "properties": { "order_id": { "type": "string", "description": "Уникальный идентификатор заказа." } }, "required": ["order_id"] } }, { "name": "get_customer_orders", "description": "Возвращает список заказов пользователя по его customer id.", "input_schema": { "type": "object", "properties": { "customer_id": { "type": "string", "description": "customer_id этого пользователя" } }, "required": ["customer_id"] } }, { "name": "cancel_order", "description": "Отменяет заказ по указанному order_id. Отменить можно только заказы в статусе 'processing'", "input_schema": { "type": "object", "properties": { "order_id": { "type": "string", "description": "order_id нужного заказа" } }, "required": ["order_id"] } }]Передаём инструменты Claude
Заголовок раздела «Передаём инструменты Claude»Дальше нужно сделать несколько вещей:
- Рассказать Claude об инструментах и отправить ему сообщение пользователя.
- Обработать ответ Claude:
- если Claude не хочет вызывать инструмент — показать его ответ пользователю и спросить следующее сообщение;
- если хочет — проверить, что он вызвал один из подходящих инструментов, выполнить функцию (например,
get_userилиcancel_order) и отправить Claude результат.
В итоге мы хотим скрипт для командной строки — интерактивного чат-бота, работающего в цикле. Но для простоты сначала напишем код линейно, а в конце соберём интерактивный скрипт.
Начнём с функции, которая превращает запрос Claude на вызов инструмента в настоящий вызов метода db:
def process_tool_call(tool_name, tool_input): if tool_name == "get_user": return db.get_user(tool_input["key"], tool_input["value"]) elif tool_name == "get_order_by_id": return db.get_order_by_id(tool_input["order_id"]) elif tool_name == "get_customer_orders": return db.get_customer_orders(tool_input["customer_id"]) elif tool_name == "cancel_order": return db.cancel_order(tool_input["order_id"])Начнём с простой демонстрации: Claude сам выбирает нужный инструмент из нескольких. Спросим: «Можете найти мои заказы? Мой email — john@gmail.com».
import jsonfrom claude_local import client, MODEL_NAME # в оригинале — anthropic.Client() и MODEL_NAME = "claude-3-sonnet-20240229"
messages = [{"role": "user", "content": "Можете найти мои заказы? Мой email — john@gmail.com"}]
# Отправляем запрос Clauderesponse = client.messages.create( model=MODEL_NAME, max_tokens=4096, tools=tools, messages=messages)print(response)ToolsBetaMessage(id='msg_0112Ab7iKF2gWeAcAd2XWaBb', content=[TextBlock(text='Sure, let me look up your orders based on your email. To get the user details by email:', type='text'), ToolUseBlock(id='toolu_01CNTeufcesL1sUP2jnwT8TA', input={'key': 'email', 'value': 'john@gmail.com'}, name='get_user', type='tool_use')], model='claude-3-sonnet-20240229', role='assistant', stop_reason='tool_use', stop_sequence=None, type='message', usage=Usage(input_tokens=574, output_tokens=96))Claude хочет вызвать get_user («Конечно, найду ваши заказы по email. Чтобы получить данные пользователя по
email:»).
Теперь напишем базовую логику: обновить список сообщений, вызвать нужный инструмент и снова обновить список сообщений результатом.
# Добавляем в messages ответ Claudemessages.append( {"role": "assistant", "content": response.content})
# Если Claude остановился, потому что хочет вызвать инструмент:if response.stop_reason == "tool_use": tool_use = response.content[-1] # наивный подход: считаем, что за раз вызывается один инструмент tool_name = tool_use.name tool_input = tool_use.input print(f"Claude хочет вызвать инструмент {tool_name}") print("Входные данные инструмента:") print(json.dumps(tool_input, indent=2))
# Действительно выполняем инструмент на нашей базе tool_result = process_tool_call(tool_name, tool_input)
print("\nРезультат инструмента:") print(json.dumps(tool_result, ensure_ascii=False, indent=2))
# Добавляем сообщение с tool_result: messages.append( { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": tool_use.id, "content": str(tool_result), # результат передаём строкой } ], }, )else: # Если Claude НЕ хочет вызывать инструмент, просто печатаем текстовый ответ print("\nПоддержка TechNova: " + f"{response.content[0].text}")Claude хочет вызвать инструмент get_userВходные данные инструмента:{ "key": "email", "value": "john@gmail.com"}
Результат инструмента:{ "id": "1213210", "name": "John Doe", "email": "john@gmail.com", "phone": "123-456-7890", "username": "johndoe"}(В оригинале в первом print пропущена буква f, и выводится буквально {tool_name}; здесь опечатка
исправлена.)
Список сообщений теперь такой:
messages[{'role': 'user', 'content': 'Можете найти мои заказы? Мой email — john@gmail.com'}, {'role': 'assistant', 'content': [TextBlock(text='Sure, let me look up your orders based on your email. To get the user details by email:', type='text'), ToolUseBlock(id='toolu_01CNTeufcesL1sUP2jnwT8TA', input={'key': 'email', 'value': 'john@gmail.com'}, name='get_user', type='tool_use')]}, {'role': 'user', 'content': [{'type': 'tool_result', 'tool_use_id': 'toolu_01CNTeufcesL1sUP2jnwT8TA', 'content': "{'id': '1213210', 'name': 'John Doe', 'email': 'john@gmail.com', 'phone': '123-456-7890', 'username': 'johndoe'}"}]}]Отправим Claude второй запрос с обновлённым списком:
response2 = client.messages.create( model=MODEL_NAME, max_tokens=4096, tools=tools, messages=messages)response2ToolsBetaMessage(id='msg_011Fru6wiViEExPXg7ANQkfH', content=[TextBlock(text='Now that I have your customer ID 1213210, I can retrieve your order history:', type='text'), ToolUseBlock(id='toolu_011eHUmUgCZXwr7swzL1LE6y', input={'customer_id': '1213210'}, name='get_customer_orders', type='tool_use')], model='claude-3-sonnet-20240229', role='assistant', stop_reason='tool_use', stop_sequence=None, type='message', usage=Usage(input_tokens=733, output_tokens=80))Получив результат get_user с ID пользователя, Claude хочет вызвать get_customer_orders, чтобы найти заказы этого
клиента («Теперь, когда у меня есть ваш ID клиента 1213210, я могу получить историю заказов:»). Он выбирает
подходящий инструмент. Примечание: здесь много мест, где Claude может ошибиться, но для начала неплохо!
Интерактивный скрипт
Заголовок раздела «Интерактивный скрипт»С таким ботом гораздо удобнее общаться через интерактивный скрипт в командной строке. Вот весь код выше, собранный в
функцию, которая запускает чат в цикле (лучше запускать его отдельным скриптом из командной строки). Класс
FakeDatabase, список tools и process_tool_call — те же, что выше; в оригинале они повторены в скрипте целиком.
import jsonfrom claude_local import client, MODEL_NAME # в оригинале — "claude-3-sonnet-20240229" (или "claude-3-opus-20240229")
# ... class FakeDatabase, tools и process_tool_call — как выше ...
db = FakeDatabase()
def simple_chat(): user_message = input("\nПользователь: ") messages = [{"role": "user", "content": user_message}] while True: # Если последнее сообщение от ассистента — спрашиваем у пользователя следующее if messages[-1].get("role") == "assistant": user_message = input("\nПользователь: ") messages.append({"role": "user", "content": user_message})
# Отправляем запрос Claude response = client.messages.create( model=MODEL_NAME, max_tokens=4096, tools=tools, messages=messages ) # Добавляем в messages ответ Claude messages.append( {"role": "assistant", "content": response.content} )
# Если Claude остановился, потому что хочет вызвать инструмент: if response.stop_reason == "tool_use": tool_use = response.content[-1] # наивный подход: считаем, что за раз вызывается один инструмент tool_name = tool_use.name tool_input = tool_use.input print(f"======Claude хочет вызвать инструмент {tool_name}======")
# Действительно выполняем инструмент на нашей базе tool_result = process_tool_call(tool_name, tool_input)
# Добавляем сообщение с tool_result # (последнее сообщение теперь от user, поэтому на следующем круге цикл сразу отправит его Claude): messages.append( { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": tool_use.id, "content": str(tool_result), } ], }, ) else: # Если Claude НЕ хочет вызывать инструмент, просто печатаем текстовый ответ print("\nПоддержка TechNova: " + f"{response.content[0].text}")
# Запускаем чат!# simple_chat()(В оригинале — скриншот диалога с этой версией: бот вызывает правильные инструменты, но в ответах рассказывает пользователю, какие инструменты собирается вызвать.)
Бот вызывает нужные инструменты, когда это требуется, но сами ответы далеки от идеала. Вряд ли мы хотим, чтобы бот для клиентов рассказывал, какие инструменты он вызывает!
Улучшаем промпт
Заголовок раздела «Улучшаем промпт»Опыт общения можно улучшить хорошим системным промптом. Первым делом стоит дать Claude контекст: он работает ассистентом службы поддержки TechNova.
Начало системного промпта:
system_prompt = """Ты — чат-бот службы поддержки интернет-магазина TechNova.Твоя задача — помогать пользователям находить их учётную запись и заказы и отменять заказы.Отвечай доброжелательно и коротко."""Не забудьте передавать этот промпт в параметре system при запросах к Claude!
(Скриншот из оригинала: с этим промптом ассистент знает, что он бот поддержки TechNova, и больше не рассказывает пользователю об инструментах.) Возможно, стоит ещё и явно запретить Claude упоминать инструменты.
Примечание: строки вида =======Claude wants to use the cancel_order_tool======= — это логирование, которое мы
добавили в скрипт, а не ответ Claude!
Если поиграть со скриптом подольше, обнаружатся и другие проблемы. Одна очень очевидная и очень неприятная видна в
таком диалоге (в оригинале — скриншот): пользователь просит помочь отменить заказ и говорит, что не знает номера
заказа. Claude должен был бы переспросить email, телефон или имя пользователя, чтобы найти клиента, а затем его
заказы. Вместо этого Claude сразу вызывает get_user, ничего не зная о пользователе: он выдумал адрес почты,
попробовал его — и, конечно, никого не нашёл.
Чтобы этого не было, дополним системный промпт примерно так:
system_prompt = """Ты — чат-бот службы поддержки интернет-магазина TechNova.Твоя задача — помогать пользователям находить их учётную запись и заказы и отменять заказы.Отвечай доброжелательно и коротко.У тебя есть набор инструментов, но пользуйся ими, только когда это нужно.Если тебе не хватает сведений, чтобы правильно вызвать инструмент, задай пользователю уточняющие вопросы и получи нужные данные.Не вызывай никакие инструменты, пока не получил от пользователя нужные данные."""(Скриншот из оригинала: с обновлённым промптом бот сначала спрашивает email и только потом ищет заказы.)
Гораздо лучше!
Проблема, характерная для Opus
Заголовок раздела «Проблема, характерная для Opus»При работе с инструментами Opus часто выводит свои размышления в тегах <thinking> или <reflection>, прежде чем
ответить пользователю (в оригинале — скриншот).
Такие размышления обычно улучшают результат, но для пользователя это ужасно: показывать ему ход мысли мы не хотим! Проще всего явно попросить модель выводить ответ для пользователя в определённых XML-тегах. Обновим системный промпт:
system_prompt = """Ты — чат-бот службы поддержки интернет-магазина TechNova.Твоя задача — помогать пользователям находить их учётную запись и заказы и отменять заказы.Отвечай доброжелательно и коротко.У тебя есть набор инструментов, но пользуйся ими, только когда это нужно.Если тебе не хватает сведений, чтобы правильно вызвать инструмент, задай пользователю уточняющие вопросы и получи нужные данные.Не вызывай никакие инструменты, пока не получил от пользователя нужные данные.
На каждом шаге разговора сначала обдумай ответ.Затем напиши ответ для пользователя.Важно: весь текст, обращённый к пользователю, помещай в XML-теги <reply></reply>, чтобы его было легко извлечь."""(Скриншот из оригинала: теперь ответ пользователю обёрнут в теги <reply>.)
Осталось извлечь содержимое между тегами и показывать пользователю только его. Вот обновлённая функция, которая именно это и делает:
import re
def extract_reply(text): pattern = r'<reply>(.*?)</reply>' # всё между <reply> и </reply> match = re.search(pattern, text, re.DOTALL) # DOTALL: точка совпадает и с переводом строки if match: return match.group(1) else: return None
def simple_chat(): system_prompt = """...последняя версия системного промпта выше...""" user_message = input("\nПользователь: ") messages = [{"role": "user", "content": user_message}] while True: if messages[-1].get("role") == "assistant": user_message = input("\nПользователь: ") messages.append({"role": "user", "content": user_message})
response = client.messages.create( model=MODEL_NAME, system=system_prompt, # новое: системный промпт max_tokens=4096, tools=tools, messages=messages ) messages.append({"role": "assistant", "content": response.content})
if response.stop_reason == "tool_use": tool_use = response.content[-1] # наивный подход: один инструмент за раз tool_name = tool_use.name tool_input = tool_use.input print(f"======Claude хочет вызвать инструмент {tool_name}======") tool_result = process_tool_call(tool_name, tool_input) messages.append( { "role": "user", "content": [ {"type": "tool_result", "tool_use_id": tool_use.id, "content": str(tool_result)} ], }, ) else: # новое: показываем пользователю только текст из <reply> model_reply = extract_reply(response.content[0].text) print("\nПоддержка TechNova: " + f"{model_reply}")
# Запускаем чат!# simple_chat()(Скриншот из оригинала: размышления скрыты, пользователь видит только ответ.)
Финальная версия
Заголовок раздела «Финальная версия»(В оригинале — скриншот длинного диалога с версией скрипта, которая раскрашивает вывод: пользователь находит свои заказы по email, пытается отменить уже отправленный заказ, получает отказ и отменяет заказ, который ещё в обработке.)
Заключительные замечания
Заголовок раздела «Заключительные замечания»Это учебная демонстрация, которая иллюстрирует работу с инструментами. Этот скрипт и промпт НЕ готовы к продакшену. Ассистент говорит вещи вроде «Номер заказа можно найти в письме с подтверждением заказа», не зная, правда ли это. В реальности нужно (как минимум) дать Claude много сведений о вашей компании. Нужно тщательно тестировать бота и убедиться, что он хорошо ведёт себя во всех ситуациях. И, наверное, плохая идея — позволять кому угодно так легко отменять заказы! Имея доступ к чужим email, имени пользователя или телефону, легко отменить заказы многим людям. В реальности понадобится какая-то аутентификация. Короче говоря: это демонстрация, а не полноценная реализация чат-бота!
Упражнение
Заголовок раздела «Упражнение»- Добавьте ассистенту (и классу
FakeDatabase) возможность менять email и номер телефона пользователя. - Опишите новый инструмент
get_user_info, который объединяетget_userиget_customer_orders: принимает email, телефон или имя пользователя и сразу возвращает данные пользователя вместе с историей заказов. - Добавьте обработку ошибок и проверку входных данных!
- Подключите бота к НАСТОЯЩЕЙ базе данных вместо класса
FakeDatabase.
Что дальше
Заголовок раздела «Что дальше»- Урок 8.3.2 «Инструменты и MCP»: как вынести такие инструменты в MCP-сервер, чтобы ими мог пользоваться любой агент.
- Конспект статей Anthropic «Building effective agents» и «Writing effective tools for agents»: когда хватает простого цикла из этого урока, а когда нужен агент, и как писать инструменты, которыми модель пользуется правильно.
- Рецепт из Claude Cookbooks «Агент поддержки клиентов» — тот же бот, оформленный в кукбуке Anthropic.
Словарик
Заголовок раздела «Словарик»| EN | RU |
|---|---|
| tool result | результат инструмента |
tool_use_id | идентификатор вызова, к которому относится результат |
tool_choice: auto / any / tool | выбор инструмента: сам решает / обязательно какой-то / обязательно этот |
| system prompt | системный промпт |
| customer support chatbot | чат-бот поддержки клиентов |
| authentication | аутентификация, проверка личности |
| input validation | проверка входных данных |