Python & AI Tutorials Logo
LangChain & LangGraph

12: Создание инструментов для вашего агента

В части IV мы создадим агент(agent) — ИИ, который выясняет, что нужно сделать с запросом пользователя, и затем делает это, а не просто отвечает на него, как чат-бот.

Вот в чём разница на практике. Допустим, пользователь спрашивает: «Пожалуйста, отмените заказ #12345». Чат-бот ответил бы что-то вроде: «Перейдите в Моя страница > История заказов и нажмите кнопку „Отменить“ для этого заказа». Дальше выполнить эти шаги самому должен пользователь. Агент же сам выполняет работу — он находит заказ, проверяет, подлежит ли он отмене, и отменяет его. Он совершает действие.

Возможным это делают инструменты(tools): инструмент, который находит заказ, инструмент, который отменяет его, инструмент, который отправляет электронное письмо. С инструментами LLM перестаёт ограничиваться генерацией текста и начинает реально выполнять задачи.

Пожалуйста, отмените заказ #12345.

вызывает get_order(12345)

Заказ найден,
подлежит отмене

вызывает cancel_order(12345)

Отмена завершена

Ваш заказ был отменён.

Пользователь

Цикл агента

Инструмент поиска заказа

Инструмент отмены заказа

Мы будем строить это шаг за шагом на протяжении части IV: сначала инструменты, которые будет использовать агент (эта глава), затем подключение этих инструментов к LLM (глава 13) и, наконец, цикл агента, который проходит через решить → действовать → наблюдать (глава 14).

Эта глава охватывает первую часть — определение инструментов, подключение их к реальным данным и безопасную обработку ошибок.

12.1) Определение инструментов с помощью декоратора @tool

12.1.1) Как работает инструмент?

Мы только что видели, как агент находил и отменял заказ с помощью инструментов. Прежде чем двигаться дальше, стоит прояснить один момент: когда говорят, что «LLM использует инструмент», звучит так, будто LLM сам вызывает его напрямую. Это не так. LLM никогда ничего не выполняет сам — всё, что он делает, это просит вызвать инструмент с определёнными аргументами. Само выполнение происходит в нашем коде.

Чтобы это работало, LLM должен знать, какие инструменты существуют и когда применяется каждый из них. Поэтому каждый инструмент сопровождается тремя элементами метаданных:

  • name — короткий идентификатор, такой как get_order, который LLM использует, чтобы указать, какой инструмент ему нужен.
  • description — предложение, описывающее, что делает инструмент и когда его использовать. Именно это читает LLM, чтобы выбрать подходящий инструмент для задачи.
  • Схема входных данных — каковы параметры инструмента: их имена, типы и значение. LLM это нужно, чтобы правильно заполнить аргументы.

Ничто из этого не требует от вас дополнительной работы. name берётся прямо из имени функции, description — из её docstring, а схема входных данных — из аннотаций типов параметров. Всё, что вам нужно сделать, — это прикрепить декоратор @tool из LangChain.

12.1.2) Создание вашего первого инструмента

Давайте применим это на практике. Напишите функцию с аннотациями типов и docstring, затем украсьте её декоратором @tool.

python
from langchain.tools import tool
 
@tool
def get_weather(city: str) -> str:
    """Получить текущую погоду для заданного города."""
    return f"В городе {city} всегда солнечно!"

Давайте проверим, что @tool сгенерировал для нас.

python
print(get_weather.name)
# Вывод: get_weather
 
print(get_weather.description)
# Вывод: Получить текущую погоду для заданного города.
 
print(get_weather.args)
# Вывод: {'city': {'title': 'City', 'type': 'string'}}

Имя функции get_weather стало её name, docstring стал её description, а аннотация типа city: str стала её схемой входных данных. Все три элемента метаданных из предыдущего раздела были сгенерированы автоматически. Именно это LLM использует, чтобы выбрать инструмент и заполнить его аргументы.

После того как @tool прикреплён, функция становится объектом инструмента LangChain, а значит, вы больше не можете вызывать её как обычную функцию — get_weather("Paris") не сработает. Вместо этого вы вызываете её с помощью .invoke() — того же стандартного метода выполнения, который мы использовали для цепочек ещё в главе 6. Аргументы передаются в виде словаря:

python
result = get_weather.invoke({"city": "Paris"})
print(result)
# Вывод: В городе Paris всегда солнечно!

12.1.3) Настройка name и description

По умолчанию name берётся из имени функции, а description — из docstring. Вы можете переопределить оба.

Передайте имя в качестве первого аргумента в @tool:

python
@tool("web_search")
def search(query: str) -> str:
    """Поиск информации в интернете."""
    return f"Результаты для: {query}"
 
print(search.name)
# Вывод: web_search

Вы также можете переопределить описание с помощью параметра description. Это полезно, когда вы хотите оставить docstring в качестве заметки для других разработчиков, давая LLM при этом что-то более подходящее:

python
@tool("calculator", description="Выполняет арифметические операции. Используйте это для любой математической задачи.")
def calc(expression: str) -> str:
    """Вычисляет строку с математическим выражением."""
    return str(eval(expression))  # ПРЕДУПРЕЖДЕНИЕ: eval() небезопасен. Никогда не используйте его в продакшене.

Придерживайтесь snake_case для имён инструментов — некоторые провайдеры LLM отклоняют имена с пробелами или специальными символами.

12.1.4) Определение схемы входных данных с помощью Pydantic

Когда инструмент принимает несколько параметров или вы хотите описать каждый из них по отдельности, определите схему входных данных с помощью модели Pydantic. Это те же BaseModel и Field, которые мы использовали для структурированного вывода ещё в главе 7.

python
from pydantic import BaseModel, Field
from langchain.tools import tool
 
class WeatherInput(BaseModel):
    """Входные данные для запросов о погоде."""
    location: str = Field(description="Название города (например, Seoul, Tokyo)")
    units: str = Field(default="celsius", description="Единица измерения температуры (celsius или fahrenheit)")
 
@tool(args_schema=WeatherInput)
def get_weather_detailed(location: str, units: str = "celsius") -> str:
    """Получить текущую погоду с выбранной единицей измерения температуры."""
    temp = 22 if units == "celsius" else 72
    return f"Текущая погода в {location}: {temp} градусов {units[0].upper()}"

Что бы вы ни написали в Field(description=...), это становится частью схемы входных данных, которую читает LLM, чтобы точно знать, что означает каждый параметр. В большинстве случаев аннотаций типов и понятного docstring достаточно — прибегайте к args_schema только тогда, когда вам нужен этот дополнительный уровень детализации по каждому параметру.

12.2) Обработка ошибок инструмента

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

12.2.1) Подготовка: функции поиска товаров

python
# product_service.py
 
PRODUCTS = {
    1: {"name": "Wireless Mouse", "price": 29.99, "stock": 120},
    2: {"name": "Mechanical Keyboard", "price": 89.99, "stock": 0},
    3: {"name": "USB-C Hub", "price": 45.50, "stock": 35},
    4: {"name": "Laptop Stand", "price": 39.00, "stock": 8},
}
 
def fetch_product(product_id: int) -> dict:
    """Найти информацию о товаре по ID."""
    product = PRODUCTS.get(product_id)
    if product is None:
        raise ValueError(f"Product with ID {product_id} not found.")
    return product
 
def fetch_stock(product_id: int) -> int:
    """Вернуть количество товара на складе."""
    product = PRODUCTS.get(product_id)
    if product is None:
        raise ValueError(f"Product with ID {product_id} not found.")
    return product["stock"]

Обе функции вызывают ValueError, когда им передаётся несуществующий ID товара.

12.2.2) Обработка ошибок в инструменте

Давайте обернём fetch_product в инструмент get_product.

python
from langchain.tools import tool
from product_service import fetch_product
 
@tool
def get_product(product_id: int) -> str:
    """Найти товар по его ID. Возвращает его название, цену и уровень запасов."""
    product = fetch_product(product_id)
    return f"Product {product_id}: {product['name']} — ${product['price']:.2f}, {product['stock']} in stock."

С корректным ID он работает как ожидается.

python
print(get_product.invoke({"product_id": 1}))
# Вывод: Product 1: Wireless Mouse — $29.99, 120 in stock.

Но передайте несуществующий ID, и fetch_product вызовет ValueError, который никто не перехватывает, — выполнение агента останавливается прямо там.

python
print(get_product.invoke({"product_id": 99}))
# ValueError: Product with ID 99 not found.

Решение простое: перехватите исключение внутри инструмента и верните строку, которую LLM сможет понять, вместо того чтобы дать ему распространиться. Успех или неудача — инструмент всегда возвращает строку, и LLM использует эту строку, чтобы решить, что делать дальше.

python
from langchain.tools import tool
from product_service import fetch_product
 
@tool
def get_product(product_id: int) -> str:
    """Найти товар по его ID. Возвращает его название, цену и уровень запасов."""
    try:
        product = fetch_product(product_id)
        return f"Product {product_id}: {product['name']} — ${product['price']:.2f}, {product['stock']} in stock."
    except ValueError as e:
        return f"Error: {e}"
    except Exception as e:
        return f"Unexpected error looking up product {product_id}: {e}"
python
print(get_product.invoke({"product_id": 1}))
# Вывод: Product 1: Wireless Mouse — $29.99, 120 in stock.
 
print(get_product.invoke({"product_id": 99}))
# Вывод: Error: Product with ID 99 not found.

Несуществующий ID больше не вызывает исключение — вместо этого он возвращает сообщение об ошибке, которое LLM может понять.

Применим тот же шаблон к check_stock:

python
from product_service import fetch_stock
 
@tool
def check_stock(product_id: int) -> str:
    """Проверить, есть ли товар сейчас в наличии."""
    try:
        stock = fetch_stock(product_id)
        if stock > 0:
            return f"{stock} units available."
        return "Out of stock."
    except ValueError as e:
        return f"Error: {e}"
    except Exception as e:
        return f"Unexpected error checking stock for product {product_id}: {e}"

Инструменты, построенные таким образом, можно тестировать напрямую с помощью .invoke(). Убедитесь, что инструмент работает корректно сам по себе, прежде чем подключать его к LLM. В противном случае, когда позже что-то пойдёт не так внутри агента, вы не сможете понять, сломан ли инструмент или модель просто приняла неудачное решение.