$ sudo teach IT

Агент силён ровно настолько, насколько хороши его инструменты. Модель не видит код вашей функции: она знает только имя, описание и список аргументов. Если описание расплывчатое, агент вызовет не тот инструмент или передаст в него ерунду. Если инструмент упадёт с исключением, весь запуск агента прервётся, а пользователь ничего не получит.

В этом уроке вы научитесь оформлять функции как полноценные инструменты LangChain: с понятным именем, описанием, проверкой аргументов и аккуратной обработкой ошибок. Это навык, который пригодится в каждом агенте, от бота поддержки до помощника аналитика.

Что уже умеем

В прошлом уроке вы подключили модель через LangChain, разобрали типы сообщений и собрали первого агента через create_agent, передав ему обычную функцию Python. Из урока 1.6 вы помните, что модель получает JSON-схему функции и возвращает имя и аргументы вызова. Сегодня посмотрим, как управлять этой схемой и что делать, если вызов прошёл неудачно.

Что вы узнаете в рамках статьи

  • как декоратор @tool превращает функцию в инструмент и что он берёт из её кода;
  • как задать имя и описание инструмента, чтобы модель выбирала его правильно;
  • как описать аргументы через аннотации типов и схему Pydantic;
  • как возвращать ошибки так, чтобы агент мог исправиться;
  • почему результат инструмента должен быть коротким.

Ключевые термины

  • Инструмент (tool) — функция с именем, описанием и схемой аргументов, которую агент может вызвать.
  • Декоратор @tool — способ превратить функцию Python в инструмент LangChain.
  • Схема аргументов — описание параметров инструмента: имена, типы, значения по умолчанию, пояснения.
  • args_schema — параметр @tool, в который передаётся модель Pydantic со схемой аргументов.
  • Результат инструмента — текст, который возвращается модели в ToolMessage и попадает в контекст.

Декоратор @tool: что видит модель

Импортируйте декоратор из langchain.tools и поставьте его над функцией. LangChain возьмёт имя функции как имя инструмента, строку документации как описание, а аннотации типов как схему аргументов. Аннотации обязательны: без них LangChain не сможет построить схему.

from langchain.tools import tool

@tool
def search_orders(customer: str, limit: int = 5) -> str:
    """Ищет последние заказы клиента в базе магазина.

    Args:
        customer: имя или email клиента
        limit: сколько последних заказов вернуть
    """
    return f"Найдено 2 заказа клиента {customer}: №1042 (доставлен), №1077 (в пути)"

print(search_orders.name)
print(search_orders.description)
print(search_orders.args)

Что увидим в консоли: имя search_orders, текст описания и словарь аргументов, где у customer тип string, а у limit тип integer и значение по умолчанию 5. Именно это, а не тело функции, уйдёт модели. Поэтому строка документации — не комментарий для коллег, а инструкция для агента.

Инструмент можно вызвать и вручную, чтобы проверить его без модели: search_orders.invoke({"customer": "anna@mail.ru"}). Это удобно при отладке: вы сразу видите, что вернёт функция.

Имя и описание решают, какой инструмент выберет агент

Когда инструментов несколько, модель выбирает между ними по описаниям. Хорошее описание отвечает на три вопроса: что делает инструмент, когда его использовать и когда не использовать. Имя должно быть глаголом с объектом: search_orders, cancel_order, а не tool1 или helper.

Если имя функции в коде неудобно менять, его можно задать в декораторе, как и описание:

@tool("calculator", description=(
    "Вычисляет арифметическое выражение, например '12 * 9 / 5 + 32'. "
    "Используй для любых расчётов вместо подсчёта в уме."
))
def calc(expression: str) -> str:
    allowed = set("0123456789+-*/(). ")
    if not set(expression) <= allowed:
        return "Ошибка: допустимы только цифры, скобки и знаки + - * /"
    return str(eval(expression))

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

Помните, что описания всех инструментов отправляются модели при каждом обращении. Десять инструментов по 100 токенов — это 1000 входных токенов на каждый шаг агента. Пишите описания точно, но без лишней воды.

Аргументы: типы, пояснения и схема Pydantic

Модель заполняет аргументы сама, поэтому чем строже схема, тем меньше ошибок. Для простых случаев хватает аннотаций и раздела Args в документации. Когда аргументов больше или у них есть ограничения, опишите их моделью Pydantic, как вы делали в уроке 1.5, и передайте её в args_schema.

from typing import Literal
from pydantic import BaseModel, Field

class DeliveryInput(BaseModel):
    city: str = Field(description="Город доставки, например 'Казань'")
    weight_kg: float = Field(gt=0, le=30, description="Вес посылки в кг, от 0 до 30")
    speed: Literal["standard", "express"] = Field(
        default="standard", description="Тип доставки"
    )

@tool(args_schema=DeliveryInput)
def delivery_cost(city: str, weight_kg: float, speed: str = "standard") -> str:
    """Рассчитывает стоимость доставки посылки по России в рублях."""
    price = 300 + 50 * weight_kg
    if speed == "express":
        price *= 1.5
    return f"Доставка в {city}: {price:.0f} руб."

Что увидим: на вопрос «Сколько стоит срочно отправить 4 кг в Казань?» агент вызовет delivery_cost с {'city': 'Казань', 'weight_kg': 4, 'speed': 'express'} и ответит «750 руб.». Literal ограничил скорость двумя значениями, а Field(gt=0, le=30) не даст передать отрицательный или слишком большой вес. Если модель всё же пришлёт аргументы, которые не проходят схему, LangChain вернёт ей сообщение об ошибке валидации, и модель сможет повторить вызов с исправленными данными.

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

Схема проверяет форму аргументов, но не их смысл. Заказа с таким номером может не быть, внешний сервис может не ответить. Если функция бросит исключение, оно по умолчанию прервёт запуск агента. Поэтому ожидаемые ошибки лучше перехватывать внутри инструмента и возвращать понятный текст: модель прочитает его в ToolMessage и решит, что делать дальше.

ORDERS = {1042: "доставлен", 1077: "в пути"}

@tool
def order_status(order_id: int) -> str:
    """Возвращает статус заказа по его номеру."""
    if order_id not in ORDERS:
        known = ", ".join(str(n) for n in ORDERS)
        return f"Ошибка: заказ {order_id} не найден. Существующие номера: {known}"
    return f"Заказ {order_id}: {ORDERS[order_id]}"

Что увидим: на вопрос «Где мой заказ 1043?» агент получит текст ошибки и ответит пользователю, что такого заказа нет, уточнив номер. Сообщение об ошибке пишется для модели: что пошло не так и как это исправить. Не возвращайте трассировку стека — она тратит токены и не помогает.

Результат инструмента — это тоже контекст

Всё, что вернул инструмент, попадает в историю и отправляется модели на каждом следующем шаге. Если API вернул JSON на 3000 токенов, а агенту нужен один статус, вы платите за эти 3000 токенов многократно. Выбирайте из ответа только нужные поля и форматируйте их коротким текстом:

@tool
def get_customer(email: str) -> str:
    """Возвращает имя, тариф и баланс клиента по email."""
    data = fake_crm_lookup(email)  # большой словарь из CRM
    return f"{data['name']}, тариф {data['plan']}, баланс {data['balance']} руб."

Что увидим: модель получит одну строку вместо огромного словаря, ответит так же точно, но дешевле и быстрее.

Частые ошибки

  • Оставить функцию без документации или аннотаций. Без строки документации у инструмента не будет описания, без аннотаций — схемы. Модель будет угадывать, когда и как его вызывать.
  • Давать похожие описания разным инструментам. Если search_orders и find_orders описаны одинаково, модель будет выбирать случайно. Описания должны явно разделять зоны ответственности.
  • Бросать исключения на ожидаемые ситуации. «Заказ не найден» — не авария, а нормальный ответ. Верните текст ошибки, и агент продолжит работу.
  • Возвращать сырые данные целиком. Огромный результат раздувает контекст на всех следующих шагах. Возвращайте только то, что нужно для ответа.

Наиболее частые вопросы

  • Можно ли передавать в агента функции без @tool? — Да, create_agent примет и обычную функцию, но @tool позволяет задать имя, описание и схему аргументов и проверить инструмент через .invoke.
  • На каком языке писать описания? — Модель понимает оба языка. Пишите на языке, на котором вам проще сформулировать точно, и держите стиль единым для всех инструментов.
  • Сколько инструментов давать агенту? — Столько, сколько реально нужно. Каждый лишний инструмент добавляет токены к каждому шагу и повышает шанс неверного выбора.
  • Что вернуть, если инструмент выполнил действие без данных? — Короткое подтверждение, например «Письмо отправлено на anna@mail.ru». Модели важно знать, что действие удалось.

Итоги

Поздравляем, теперь ваши инструменты говорят с агентом на понятном ему языке! Вы знаете, что модель видит только имя, описание и схему, умеете ограничивать аргументы через Pydantic, превращать ошибки в подсказки для модели и экономить контекст коротким результатом. Это фундамент надёжного агента.

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

На следующий урок