Первый простой инструмент

Урок 2 из 6 курса «Работа с инструментами Claude»: официальный курс Anthropic Academy (Антропик) на русском языке. Этот урок бесплатный.

Ваш первый простой инструмент

Ваш первый простой инструмент

В предыдущем уроке мы рассмотрели рабочий процесс использования инструментов. Пришло время приступить к реализации простого примера использования инструмента. Напомним, процесс использования инструментов состоит из 4 шагов:

  1. Предоставьте Claude инструменты и пользовательский prompt: (API запрос)

    • Определите набор инструментов, к которым Claude должен иметь доступ, включая их имена, описания и входные схемы.
    • Предоставьте пользовательский prompt, для ответа на который может потребоваться использование одного или нескольких из этих инструментов.
  2. Claude использует инструмент: (API ответ)

    • Claude оценивает пользовательский prompt и решает, поможет ли какой-либо из доступных инструментов ответить на запрос или выполнить задачу пользователя. Если да, он также решает, какой(ие) инструмент(ы) использовать и с каким(и) входным(и) данным(и).
    • Claude выводит правильно отформатированный запрос на использование инструмента.
    • API ответ будет содержать stop_reason со значением tool_use, указывая, что Claude хочет использовать внешний инструмент.
  3. Извлеките входные данные инструмента, запустите код и верните результаты: (API запрос)

    • На стороне клиента вам следует извлечь имя инструмента и входные данные из запроса на использование инструмента Claude.
    • Запустите фактический код инструмента на стороне клиента.
    • Верните результаты Claude, продолжив диалог новым пользовательским сообщением, содержащим блок контента tool_result.
  4. Claude использует результат работы инструмента для формулирования ответа: (API ответ)

    • Получив результаты работы инструмента, Claude использует эту информацию для формулирования окончательного ответа на исходный пользовательский prompt.

Мы начнем с простой демонстрации, которая требует "общения" с Claude всего один раз (не волнуйтесь, скоро мы перейдем к более интересным примерам!). Это означает, что мы пока не будем заниматься шагом 4. Мы попросим Claude ответить на вопрос, Claude запросит использование инструмента для ответа, а затем мы извлечем входные данные инструмента, запустим код и вернем полученное значение.

Современные большие языковые модели испытывают трудности с математическими операциями, что подтверждается следующим кодом.

Мы просим Claude "Умножить 1984135 на 9343116":

from anthropic import Anthropic
from dotenv import load_dotenv

load_dotenv()

client = Anthropic()

# Относительно простая математическая задача
response = client.messages.create(
    model="claude-3-haiku-20240307",
    messages=[{"role": "user", "content":"Multiply 1984135 by 9343116. Only respond with the result"}],
    max_tokens=400
)
print(response.content[0].text)
18555375560

Вероятно, при многократном запуске приведенного выше кода мы получим другой ответ, но это один из ответов Claude:

18593367726060

Фактический правильный ответ:

18538003464660

Claude ошибся всего лишь на 55364261400!

Использование инструментов спешит на помощь!

Claude не очень хорошо справляется со сложной математикой, поэтому давайте расширим его возможности, предоставив доступ к инструменту-калькулятору.

Вот простая схема, объясняющая процесс:

chickens_calculator.png

Первый шаг — определить фактическую функцию калькулятора и убедиться, что она работает независимо от Claude. Мы напишем ОЧЕНЬ простую функцию, которая ожидает три аргумента: * Операцию, например "add" (сложить) или "multiply" (умножить) * Два операнда

Вот базовая реализация:

def calculator(operation, operand1, operand2):
    if operation == "add":
        return operand1 + operand2
    elif operation == "subtract":
        return operand1 - operand2
    elif operation == "multiply":
        return operand1 * operand2
    elif operation == "divide":
        if operand2 == 0:
            raise ValueError("Cannot divide by zero.")
        return operand1 / operand2
    else:
        raise ValueError(f"Unsupported operation: {operation}")

Обратите внимание, что эта простая функция довольно ограничена в своей полезности, поскольку она может обрабатывать только простые выражения, такие как 234 + 213 или 3 * 9. Цель здесь — пройти процесс работы с инструментами на очень простом учебном примере.

Давайте протестируем нашу функцию и убедимся, что она работает.

calculator("add", 10, 3)
13
calculator("divide", 200, 25)
8.0

Следующий шаг — определить наш инструмент и сообщить о нем Claude. При определении инструмента мы следуем очень специфическому формату. Каждое определение инструмента включает:

  • name: Имя инструмента. Должно соответствовать регулярному выражению ^[a-zA-Z0-9_-]{1,64}$.
  • description: Подробное текстовое описание того, что делает инструмент, когда его следует использовать и как он себя ведет.
  • input_schema: Объект JSON Schema, определяющий ожидаемые параметры для инструмента.

Не знакомы с JSON Schema? Узнайте больше здесь.

Вот простой пример для гипотетического инструмента:

{
  "name": "send_email",
  "description": "Sends an email to the specified recipient with the given subject and body.",
  "input_schema": {
    "type": "object",
    "properties": {
      "to": {
        "type": "string",
        "description": "The email address of the recipient"
      },
      "subject": {
        "type": "string",
        "description": "The subject line of the email"
      },
      "body": {
        "type": "string",
        "description": "The content of the email message"
      }
    },
    "required": ["to", "subject", "body"]
  }
}

Этот инструмент, названный send_email, ожидает следующие входные данные: * to, который является строкой и обязателен * subject, который является строкой и обязателен * body, который является строкой и обязателен

Вот еще одно определение инструмента для инструмента под названием search_product:

{
  "name": "search_product",
  "description": "Search for a product by name or keyword and return its current price and availability.",
  "input_schema": {
    "type": "object",
    "properties": {
      "query": {
        "type": "string",
        "description": "The product name or search keyword, e.g. 'iPhone 13 Pro' or 'wireless headphones'"
      },
      "category": {
        "type": "string",
        "enum": ["electronics", "clothing", "home", "toys", "sports"],
        "description": "The product category to narrow down the search results"
      },
      "max_price": {
        "type": "number",
        "description": "The maximum price of the product, used to filter the search results"
      }
    },
    "required": ["query"]
  }
}

Этот инструмент имеет 3 входных параметра: * Обязательную строку query, представляющую название продукта или ключевое слово для поиска * Необязательную строку category, которая должна быть одним из предопределенных значений для сужения поиска. Обратите внимание на "enum" в определении. * Необязательное число max_price для фильтрации результатов ниже определенной ценовой отметки

Определение нашего инструмента-калькулятора

Давайте определим соответствующий инструмент для нашей функции калькулятора, которую мы написали ранее. Мы знаем, что функция калькулятора имеет 3 обязательных аргумента: * operation - который может быть только "add" (сложить), "subtract" (вычесть), "multiply" (умножить) или "divide" (разделить) * operand1, который должен быть числом * operand2, который также должен быть числом

Вот определение инструмента:

calculator_tool = {
    "name": "calculator",
    "description": "A simple calculator that performs basic arithmetic operations.",
    "input_schema": {
        "type": "object",
        "properties": {
            "operation": {
                "type": "string",
                "enum": ["add", "subtract", "multiply", "divide"],
                "description": "The arithmetic operation to perform."
            },
            "operand1": {
                "type": "number",
                "description": "The first operand."
            },
            "operand2": {
                "type": "number",
                "description": "The second operand."
            }
        },
        "required": ["operation", "operand1", "operand2"]
    }
}

Упражнение

Давайте попрактикуемся в написании правильно отформатированного определения инструмента, используя следующую функцию в качестве примера:

def inventory_lookup(product_name, max_results):
    return "this function doesn't do anything"
    #Вам не нужно трогать это или что-либо с этим делать!

Эту гипотетическую функцию inventory_lookup следует вызывать следующим образом:

inventory_lookup("AA batteries", 4)

inventory_lookup("birthday candle", 10)

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


Предоставляем Claude наш инструмент

Теперь вернемся к нашей функции калькулятора, которую мы написали ранее. В данный момент Claude ничего не знает об инструменте-калькуляторе! Это просто небольшой словарь Python. При отправке запроса Claude мы можем передать список инструментов, чтобы "сообщить" Claude о них. Давайте попробуем сейчас:

response = client.messages.create(
    model="claude-3-haiku-20240307",
    messages=[{"role": "user", "content": "Multiply 1984135 by 9343116. Only respond with the result"}],
    max_tokens=300,
    # Сообщаем Claude о нашем инструменте
    tools=[calculator_tool]
)

Далее давайте посмотрим на ответ, который Claude нам возвращает:

response
ToolsBetaMessage(id='msg_01UfKwdmEsgTh99wfpgW4NJ7', content=[ToolUseBlock(id='toolu_015wQ7Wipo589yT9B3YTwjF1', input={'operand1': 1984135, 'operand2': 9343116, 'operation': 'multiply'}, name='calculator', type='tool_use')], model='claude-3-haiku-20240307', role='assistant', stop_reason='tool_use', stop_sequence=None, type='message', usage=Usage(input_tokens=420, output_tokens=93))
ToolsBetaMessage(id='msg_01UfKwdmEsgTh99wfpgW4NJ7', content=[ToolUseBlock(id='toolu_015wQ7Wipo589yT9B3YTwjF1', input={'operand1': 1984135, 'operand2': 9343116, 'operation': 'multiply'}, name='calculator', type='tool_use')], model='claude-3-haiku-20240307', role='assistant', stop_reason='tool_use', stop_sequence=None, type='message', usage=Usage(input_tokens=420, output_tokens=93))

Вы могли заметить, что наш ответ выглядит несколько иначе, чем обычно! В частности, вместо обычного Message мы теперь получаем ToolsMessage.

Кроме того, мы можем проверить response.stop_reason и увидеть, что Claude остановился, потому что решил, что пришло время использовать инструмент:

response.stop_reason
'tool_use'

response.content содержит список, включающий ToolUseBlock, который сам содержит информацию об имени инструмента и входных данных:

response.content
[ToolUseBlock(id='toolu_015wQ7Wipo589yT9B3YTwjF1', input={'operand1': 1984135, 'operand2': 9343116, 'operation': 'multiply'}, name='calculator', type='tool_use')]
tool_name = response.content[0].name
tool_inputs = response.content[0].input

print("The Tool Name Claude Wants To Call:", tool_name)
print("The Inputs Claude Wants To Call It With:", tool_inputs)
The Tool Name Claude Wants To Call: calculator
The Inputs Claude Wants To Call It With: {'operand1': 1984135, 'operand2': 9343116, 'operation': 'multiply'}

Следующий шаг — просто взять имя инструмента и входные данные, предоставленные Claude, и использовать их для фактического вызова функции калькулятора, которую мы написали ранее. Тогда у нас будет окончательный ответ!

operation = tool_inputs["operation"]
operand1 = tool_inputs["operand1"]
operand2 = tool_inputs["operand2"]

result = calculator(operation, operand1, operand2)
print("RESULT IS", result)
RESULT IS 18538003464660

Мы получили правильный ответ 18538003464660!!! Вместо того чтобы полагаться на Claude в правильности математических вычислений, мы просто задаем ему вопрос и предоставляем доступ к инструменту, который он может использовать при необходимости.

Важное замечание

Если мы спросим Claude о чем-то, что не требует использования инструмента, в данном случае о чем-то, что не имеет отношения к математике или расчетам, мы, вероятно, хотим, чтобы он ответил как обычно. Claude обычно так и делает, но иногда он очень стремится использовать свои инструменты!

Вот пример, когда Claude иногда пытается использовать калькулятор, хотя это не имеет смысла. Давайте посмотрим, что произойдет, если мы спросим Claude: "Какого цвета изумруды?"

response = client.messages.create(
    model="claude-3-haiku-20240307",
    messages=[{"role": "user", "content":"What color are emeralds?"}],
    max_tokens=400,
    tools=[calculator_tool]
)
response
ToolsBetaMessage(id='msg_01Dj82HdyrxGJpi8XVtqEYvs', content=[ToolUseBlock(id='toolu_01Xo7x3dV1FVoBSGntHNAX4Q', input={'operand1': 0, 'operand2': 0, 'operation': 'add'}, name='calculator', type='tool_use')], model='claude-3-haiku-20240307', role='assistant', stop_reason='tool_use', stop_sequence=None, type='message', usage=Usage(input_tokens=409, output_tokens=89))

Claude дает нам такой ответ:

ToolsBetaMessage(id='msg_01Dj82HdyrxGJpi8XVtqEYvs', content=[ToolUseBlock(id='toolu_01Xo7x3dV1FVoBSGntHNAX4Q', input={'operand1': 0, 'operand2': 0, 'operation': 'add'}, name='calculator', type='tool_use')], model='claude-3-haiku-20240307', role='assistant', stop_reason='tool_use', stop_sequence=None, type='message', usage=Usage(input_tokens=409, output_tokens=89))

Claude хочет, чтобы мы вызвали инструмент-калькулятор? Очень простое решение — скорректировать наш prompt или добавить системный prompt, который гласит примерно следующее: У вас есть доступ к инструментам, но используйте их только при необходимости. Если инструмент не требуется, отвечайте как обычно:

response = client.messages.create(
    model="claude-3-haiku-20240307",
    system="You have access to tools, but only use them when necessary.  If a tool is not required, respond as normal",
    messages=[{"role": "user", "content":"What color are emeralds?"}],
    max_tokens=400,
    tools=[calculator_tool]
)
response
ToolsBetaMessage(id='msg_01YRRfnUUhP1u5ojr9iWZGGu', content=[TextBlock(text='Emeralds are green in color.', type='text')], model='claude-3-haiku-20240307', role='assistant', stop_reason='end_turn', stop_sequence=None, type='message', usage=Usage(input_tokens=434, output_tokens=12))

Теперь Claude отвечает соответствующим контентом и не пытается "втиснуть" использование инструмента, когда это не имеет смысла. Вот новый ответ, который мы получаем:

'Emeralds are green in color.'

Мы также видим, что stop_reason теперь end_turn вместо tool_use.

response.stop_reason
'end_turn'

Собираем всё воедино

def calculator(operation, operand1, operand2):
    if operation == "add":
        return operand1 + operand2
    elif operation == "subtract":
        return operand1 - operand2
    elif operation == "multiply":
        return operand1 * operand2
    elif operation == "divide":
        if operand2 == 0:
            raise ValueError("Cannot divide by zero.")
        return operand1 / operand2
    else:
        raise ValueError(f"Unsupported operation: {operation}")


calculator_tool = {
    "name": "calculator",
    "description": "A simple calculator that performs basic arithmetic operations.",
    "input_schema": {
        "type": "object",
        "properties": {
            "operation": {
                "type": "string",
                "enum": ["add", "subtract", "multiply", "divide"],
                "description": "The arithmetic operation to perform.",
            },
            "operand1": {"type": "number", "description": "The first operand."},
            "operand2": {"type": "number", "description": "The second operand."},
        },
        "required": ["operation", "operand1", "operand2"],
    },
}


def prompt_claude(prompt):
    messages = [{"role": "user", "content": prompt}]
    response = client.messages.create(
        model="claude-3-haiku-20240307",
        system="You have access to tools, but only use them when necessary. If a tool is not required, respond as normal",
        messages=messages,
        max_tokens=500,
        tools=[calculator_tool],
    )

    if response.stop_reason == "tool_use":
        tool_use = response.content[-1]
        tool_name = tool_use.name
        tool_input = tool_use.input

        if tool_name == "calculator":
            print("Claude wants to use the calculator tool")
            operation = tool_input["operation"]
            operand1 = tool_input["operand1"]
            operand2 = tool_input["operand2"]

            try:
                result = calculator(operation, operand1, operand2)
                print("Calculation result is:", result)
            except ValueError as e:
                print(f"Error: {str(e)}")

    elif response.stop_reason == "end_turn":
        print("Claude didn't want to use a tool")
        print("Claude responded with:")
        print(response.content[0].text)
prompt_claude("I had 23 chickens but 2 flew away.  How many are left?")
Claude want to use the calculator tool
Calculation result is:  21
prompt_claude("What is 201 times 2")
Claude want to use the calculator tool
Calculation result is:  402
prompt_claude("Write me a haiku about the ocean")
Claude didn't want to use a tool
Claude responded with: 
Here is a haiku about the ocean:

Vast blue expanse shines,
Waves crash upon sandy shores,
Ocean's soothing song.

Упражнение

Ваша задача — помочь создать ассистента для исследований с использованием Claude. Пользователь может ввести тему, которую он хочет исследовать, и получить список ссылок на статьи Википедии, сохраненных в файл Markdown для последующего чтения. Мы могли бы попробовать напрямую попросить Claude сгенерировать список URL-адресов статей, но Claude ненадежен в работе с URL-адресами и может галлюцинировать их. Кроме того, легальные статьи могли быть перемещены на новый URL-адрес после даты отсечения обучения Claude. Вместо этого мы будем использовать инструмент, который подключается к реальному Wikipedia API, чтобы это работало!

Мы предоставим Claude доступ к инструменту, который принимает список возможных заголовков статей Википедии, сгенерированных Claude, но которые могли быть сгаллюцинированы. Мы можем использовать этот инструмент для поиска в Википедии фактических заголовков статей и URL-адресов Википедии, чтобы убедиться, что окончательный список состоит из статей, которые действительно существуют. Затем мы сохраним эти URL-адреса статей в файл Markdown для последующего чтения.

Мы предоставили вам две вспомогательные функции:

import wikipedia
def generate_wikipedia_reading_list(research_topic, article_titles):
    wikipedia_articles = []
    for t in article_titles:
        results = wikipedia.search(t)
        try:
            page = wikipedia.page(results[0])
            title = page.title
            url = page.url
            wikipedia_articles.append({"title": title, "url": url})
        except:
            continue
    add_to_research_reading_file(wikipedia_articles, research_topic)

def add_to_research_reading_file(articles, topic):
    with open("output/research_reading.md", "a", encoding="utf-8") as file:
        file.write(f"## {topic} \n")
        for article in articles:
            title = article["title"]
            url = article["url"]
            file.write(f"* [{title}]({url}) \n")
        file.write(f"\n\n")

Первая функция, generate_wikipedia_reading_list, ожидает получения темы исследования, такой как "История Гавайев" или "Пираты по всему миру", и списка потенциальных названий статей Википедии, которые мы попросим Claude сгенерировать. Функция использует пакет wikipedia для поиска соответствующих РЕАЛЬНЫХ страниц Википедии и создает список словарей, содержащих заголовок и URL-адрес статьи.

Затем она вызывает add_to_research_reading_file, передавая список данных статей Википедии и общую тему исследования. Эта функция просто добавляет ссылки Markdown на каждую статью Википедии в файл output/research_reading.md. Имя файла пока жестко закодировано, и функция предполагает, что он существует. Он существует в этом репозитории, но вам потребуется создать его самостоятельно, если вы работаете в другом месте.

Идея заключается в том, что мы заставим Claude "вызвать" generate_wikipedia_reading_list со списком потенциальных заголовков статей, которые могут быть или не быть реальными. Claude может передать следующий список заголовков статей, некоторые из которых являются реальными статьями Википедии, а некоторые нет:

["Piracy", "Famous Pirate Ships", "Golden Age Of Piracy", "List of Pirates", "Pirates and Parrots", "Piracy in the 21st Century"]

Функция generate_wikipedia_reading_list просматривает каждый из этих заголовков статей и собирает реальные заголовки статей и соответствующие URL-адреса для любых статей Википедии, которые действительно существуют. Затем она вызывает add_to_research_reading_file для записи этого контента в файл Markdown для последующего использования.

Конечная цель

Ваша задача — реализовать функцию под названием get_research_help, которая принимает тему исследования и желаемое количество статей. Эта функция должна использовать Claude для фактической генерации списка возможных статей Википедии и вызова функции generate_wikipedia_reading_list, описанной выше. Вот несколько примеров вызовов функции:

get_research_help("Pirates Across The World", 7)

get_research_help("History of Hawaii", 3)

get_research_help("are animals conscious?", 3)

После этих 3 вызовов функций наш выходной файл research_reading.md выглядит следующим образом (проверьте его сами в output/research_reading.md):

research_reading.png

Для достижения этой цели вам потребуется выполнить следующее:

  • Напишите определение инструмента для функции generate_wikipedia_reading_list
  • Реализуйте функцию get_research_help
    • Напишите prompt для Claude, сообщающий ему, что вам нужна помощь в сборе информации по конкретной теме и сколько заголовков статей вы хотите, чтобы он сгенерировал
    • Сообщите Claude о доступном ему инструменте
    • Отправьте свой запрос Claude
    • Проверьте, вызвал ли Claude инструмент. Если да, вам нужно будет передать сгенерированные им заголовки статей и тему в функцию generate_wikipedia_reading_list, которую мы вам предоставили. Эта функция соберет реальные ссылки на статьи Википедии, а затем вызовет add_to_research_reading_file для записи ссылок в output/research_reading.md
    • Откройте output/research_reading.md, чтобы проверить, сработало ли это!
Стартовый код
# Вот ваш стартовый код!
import wikipedia
def generate_wikipedia_reading_list(research_topic, article_titles):
    wikipedia_articles = []
    for t in article_titles:
        results = wikipedia.search(t)
        try:
            page = wikipedia.page(results[0])
            title = page.title
            url = page.url
            wikipedia_articles.append({"title": title, "url": url})
        except:
            continue
    add_to_research_reading_file(wikipedia_articles, research_topic)

def add_to_research_reading_file(articles, topic):
    with open("output/research_reading.md", "a", encoding="utf-8") as file:
        file.write(f"## {topic} \n")
        for article in articles:
            title = article["title"]
            url = article["url"]
            file.write(f"* [{title}]({url}) \n")
        file.write(f"\n\n")
        
def get_research_help(topic, num_articles=3):
   #Реализуйте эту функцию! 
   pass

Полезные гиды