Проблема: нечёткое описание — непредсказуемый агент

Ты пишешь инструмент для поиска документов. Модель должна передать строку запроса и, опционально, количество результатов. Казалось бы, всё просто. Но посмотри, что происходит при разном качестве описания:

❌ Размытое описание
"input_schema": {
  "properties": {
    "query": {"type": "string"},
    "n": {"type": "number"}
  }
}
Модель не знает: n — это что? Целое или дробное? Лимит или смещение? Обязательное? Генерирует наугад: n: 1.5, n: 0, иногда вообще пропускает.
✅ Точное описание
"input_schema": {
  "properties": {
    "query": {
      "type": "string",
      "description": "Поисковый запрос"
    },
    "limit": {
      "type": "integer",
      "description": "Макс. число результатов (1–20)",
      "minimum": 1, "maximum": 20
    }
  },
  "required": ["query"]
}
Модель знает всё: query обязателен, limit — целое от 1 до 20, необязательное. Генерирует стабильно и правильно.

JSON Schema — это стандарт, который решает эту проблему. Он даёт модели точный контракт: какие параметры существуют, какого они типа, какие значения допустимы.

Теория: что такое JSON Schema

JSON Schema — это формальный язык для описания структуры JSON-данных. Стандарт разработан IETF (черновик Draft-07 — наиболее широко поддерживаемая версия, которую используют Anthropic, OpenAI и другие LLM-провайдеры). Он позволяет однозначно описать: какие поля есть в объекте, какого они типа, какие ограничения на значения, что обязательно.

В контексте tool calling твой input_schema — это JSON Schema для объекта аргументов. Когда модель генерирует вызов инструмента, она обязана соответствовать этой схеме. Это как типы в TypeScript, только для runtime-данных, которые LLM передаёт в функцию.

Анатомия полного описания инструмента

Посмотрим на структуру целиком. Каждая часть выполняет свою роль — и ни одну нельзя игнорировать:

100%
колёсико — масштаб  ·  зажать и тянуть — перемещение
ОПИСАНИЕ ИНСТРУМЕНТА (tool) "name": "get_weather" "description": "Возвращает погоду в указанном городе..." INPUT_SCHEMA · JSON Schema "type": "object" PROPERTIES · параметры функции "city" type: "string" description: "..." "units" type: "string" enum: ["celsius","fahrenheit"] Каждый параметр: тип + описание + ограничения "required": ["city"] ← обязательные параметры "additionalProperties": false ← запрет лишних полей name — идентификатор для роутера description — промпт для LLM input_schema — JSON Schema объекта properties — параметры функции тип · описание · ограничения required — без них вызов невозможен
ℹ️ Всегда указывай "type": "object"

Корневой элемент input_schema всегда должен быть объектом ("type": "object"). LLM-провайдеры ожидают именно это. Даже если у инструмента один параметр — он всё равно обёртывается в объект.

Примитивные типы: string, number, integer, boolean

JSON Schema поддерживает шесть примитивных типов. Четыре из них используются в инструментах агентов практически всегда. Выбор правильного типа важен: если написать "number" вместо "integer", модель может передать 2.5 вместо 2 — и твоя функция упадёт с ошибкой типа.

"string"
Текстовая строка. Самый частый тип — запросы, имена, коды, URL.
"query": "курс доллара"
"integer"
Целое число. Используй для лимитов, ID, номеров страниц, количеств.
"limit": 10
"number"
Число с плавающей точкой. Для координат, цен, рейтингов, весов.
"latitude": 55.7522
"boolean"
Булево значение. Для флагов: включить/выключить, с учётом/без.
"include_drafts": true

К примитивным типам можно добавлять ограничения-валидаторы. Они не только защищают твой код — они дают модели явный контракт, в каком диапазоне генерировать значения:

python
properties = {
    # string: ограничение длины
    "query": {
        "type": "string",
        "description": "Поисковый запрос пользователя",
        "minLength": 1,
        "maxLength": 500
    },

    # integer: ограничение диапазона
    "limit": {
        "type": "integer",
        "description": "Максимальное количество результатов (1–50)",
        "minimum": 1,
        "maximum": 50
    },

    # number: координаты
    "latitude": {
        "type": "number",
        "description": "Широта в градусах (от -90 до 90)",
        "minimum": -90,
        "maximum": 90
    },

    # boolean: флаг
    "include_archived": {
        "type": "boolean",
        "description": "Включить архивные документы в результаты поиска. По умолчанию false."
    }
}

enum: ограничение допустимых значений

Когда параметр может принимать только конкретный набор значений — используй enum. Это один из самых полезных инструментов для надёжных агентов: вместо того чтобы надеяться, что модель угадает правильный формат строки, ты явно перечисляешь все допустимые варианты.

Без enum модель может написать "Celsius", "C", "celsius" или "по Цельсию" — и ни один из этих вариантов не совпадёт с тем, что ожидает твоя функция. С enum выбор строго ограничен.

python
properties = {
    # enum для строк — единицы измерения
    "units": {
        "type": "string",
        "description": "Единицы температуры: celsius (°C) или fahrenheit (°F)",
        "enum": ["celsius", "fahrenheit"]
    },

    # enum для строк — направление сортировки
    "sort_order": {
        "type": "string",
        "description": "Порядок сортировки результатов",
        "enum": ["asc", "desc"]
    },

    # enum для чисел — размер страницы из фиксированного набора
    "page_size": {
        "type": "integer",
        "description": "Количество результатов на странице",
        "enum": [10, 25, 50, 100]
    },

    # enum с одним значением — фиксированный параметр (редко, но бывает)
    "version": {
        "type": "string",
        "enum": ["v2"]
    }
}
💡 enum работает для любого типа

enum можно применить к string, integer, number и даже к смешанным спискам. Самый частый случай — строки с фиксированным набором значений: коды языков, статусы, единицы измерения, направления.

Массивы: список значений

Когда параметр — это несколько значений одного типа, используй "type": "array". Обязательно указывай items — схему каждого элемента массива. Без items модель не знает что должно быть внутри.

Типичные применения массивов в агентах: список тегов для поиска, несколько ID для пакетного запроса, набор полей для выборки из базы, список URL для обработки.

python
properties = {
    # Простой массив строк — теги
    "tags": {
        "type": "array",
        "description": "Список тегов для фильтрации документов. Минимум 1, максимум 10.",
        "items": {
            "type": "string",
            "description": "Тег в формате snake_case, например 'machine_learning'"
        },
        "minItems": 1,
        "maxItems": 10
    },

    # Массив целых чисел — ID записей
    "document_ids": {
        "type": "array",
        "description": "Список ID документов для пакетного получения",
        "items": {
            "type": "integer",
            "minimum": 1
        }
    },

    # Массив строк из фиксированного набора — поля выборки
    "fields": {
        "type": "array",
        "description": "Список полей для включения в ответ. По умолчанию — все поля.",
        "items": {
            "type": "string",
            "enum": ["title", "content", "author", "created_at", "tags"]
        }
    }
}

Вложенные объекты: группировка параметров

Когда несколько параметров логически связаны — объедини их во вложенный объект. Это улучшает читаемость схемы и помогает модели понять, что эти поля принадлежат одной концепции. Например, координаты (широта + долгота), диапазон дат (от + до), параметры пагинации (страница + размер).

python
properties = {
    "query": {
        "type": "string",
        "description": "Поисковый запрос"
    },

    # Вложенный объект — диапазон дат
    "date_range": {
        "type": "object",
        "description": "Фильтрация по дате создания. Оба поля необязательны.",
        "properties": {
            "from": {
                "type": "string",
                "description": "Начало диапазона в формате ISO 8601: '2024-01-01'"
            },
            "to": {
                "type": "string",
                "description": "Конец диапазона в формате ISO 8601: '2024-12-31'"
            }
        },
        # required внутри вложенного объекта — тоже работает
        # здесь оба поля необязательны, поэтому required отсутствует
    },

    # Вложенный объект — координаты
    "location": {
        "type": "object",
        "description": "Географические координаты точки поиска",
        "properties": {
            "lat": {
                "type": "number",
                "description": "Широта (-90..90)",
                "minimum": -90, "maximum": 90
            },
            "lon": {
                "type": "number",
                "description": "Долгота (-180..180)",
                "minimum": -180, "maximum": 180
            }
        },
        "required": ["lat", "lon"]
    }
}

# Полный инструмент с вложенными объектами:
tool = {
    "name": "search_documents",
    "description": "Поиск документов по запросу с фильтрацией по дате и геолокации.",
    "input_schema": {
        "type": "object",
        "properties": properties,
        "required": ["query"],
        "additionalProperties": False  # запрещаем лишние поля
    }
}
⚠️ Глубокая вложенность усложняет prompt

Глубже двух уровней вложенности (объект внутри объекта) — сигнал, что стоит упростить схему или разбить на несколько инструментов. Чем сложнее схема, тем выше вероятность, что модель ошибётся в структуре аргументов.

required и optional: что обязательно

Поле required — массив имён параметров, без которых вызов невозможен. Если параметр не в required — он опциональный, и модель может его не передавать.

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

❌ Все параметры обязательны
"required": [
  "query", "limit",
  "sort_order", "include_archived"
]
Модель вынуждена заполнять все поля всегда. Будет угадывать значения для параметров, которые пользователь не указал — например, подставит случайный sort_order.
✅ Только действительно обязательное
"required": ["query"]
// limit: опционально, дефолт в коде
// sort_order: опционально
// include_archived: опционально
Модель передаёт только то, что реально нужно по контексту запроса. Дефолтные значения задаются в твоём коде, не в схеме.

Дефолтные значения не описываются в JSON Schema — их обрабатывает твоя функция:

python
def search_documents(
    query: str,                   # обязательный
    limit: int = 10,              # опциональный — дефолт в Python
    sort_order: str = "desc",     # опциональный
    include_archived: bool = False # опциональный
) -> list[dict]:
    """Функция сама управляет дефолтами — схема только про обязательность."""
    ...

# Схема при этом:
"input_schema": {
    "type": "object",
    "properties": {
        "query":            {"type": "string",  "description": "..."},
        "limit":            {"type": "integer", "description": "...", "minimum": 1, "maximum": 100},
        "sort_order":       {"type": "string",  "description": "...", "enum": ["asc", "desc"]},
        "include_archived": {"type": "boolean", "description": "..."}
    },
    "required": ["query"]   # только query обязателен
}

Описания параметров: инструкция, не документация

Поле description каждого параметра — это промпт. Модель читает его в момент принятия решения о значении аргумента. Хорошее описание устраняет неоднозначность и предотвращает ошибки.

❌ Бесполезные описания
"city": "Город"
"date": "Дата"
"format": "Формат"
Не добавляют информации. Модель не знает: «Москва» или «Moscow»? Какой формат даты? Что значит «формат»?
✅ Чёткие, конкретные описания
"city": "Название города
  на русском языке,
  например 'Москва'"
"date": "Дата в ISO 8601:
  '2024-03-15'"
"format": "Формат ответа:
  'json' или 'text'"
Явно указан формат, примеры значений, ожидаемый язык. Модель генерирует предсказуемо.

Что включать в хорошее описание параметра:

Формат
Укажи ожидаемый формат: ISO 8601, snake_case, трёхбуквенный код. Особенно важно для строк с нестандартным форматом.
Пример
Конкретный пример лучше любого описания: "например, 'machine_learning'", "например, 'USD'". Один пример снимает 80% неоднозначности.
Диапазон
Если параметр числовой — укажи диапазон в описании плюс minimum/maximum в схеме. Двойная защита: и для модели, и для валидатора.
Язык/кодировка
Для текстовых параметров уточни язык: "на русском языке", "на английском, в нижнем регистре". Без этого модель угадывает.

Готовые паттерны для реальных инструментов

Ниже — три полных описания инструментов, которые покрывают 80% случаев в реальных агентах. Используй их как шаблоны.

python
{
    "name": "search_web",
    "description": (
        "Выполняет поиск в интернете и возвращает список релевантных результатов. "
        "Используй когда нужна актуальная информация, которой нет в твоих данных: "
        "новости, текущие цены, свежие исследования, события."
    ),
    "input_schema": {
        "type": "object",
        "properties": {
            "query": {
                "type": "string",
                "description": "Поисковый запрос на языке, наиболее релевантном для темы. Формулируй кратко и конкретно.",
                "minLength": 2,
                "maxLength": 300
            },
            "num_results": {
                "type": "integer",
                "description": "Количество результатов (1–10). По умолчанию 5.",
                "minimum": 1,
                "maximum": 10
            },
            "language": {
                "type": "string",
                "description": "Предпочтительный язык результатов: 'ru' — русский, 'en' — английский.",
                "enum": ["ru", "en"]
            }
        },
        "required": ["query"],
        "additionalProperties": False
    }
}

Паттерн: запрос к базе данных

python
{
    "name": "query_database",
    "description": (
        "Выполняет фильтрацию записей в базе данных клиентов. "
        "Используй для поиска клиентов по имени, статусу или дате регистрации."
    ),
    "input_schema": {
        "type": "object",
        "properties": {
            "filters": {
                "type": "object",
                "description": "Условия фильтрации. Все поля необязательны, но хотя бы одно должно быть указано.",
                "properties": {
                    "name_contains": {
                        "type": "string",
                        "description": "Фильтр по имени: возвращает записи, содержащие эту подстроку"
                    },
                    "status": {
                        "type": "string",
                        "description": "Статус клиента",
                        "enum": ["active", "inactive", "pending"]
                    },
                    "registered_after": {
                        "type": "string",
                        "description": "Дата регистрации, после которой искать. Формат: 'YYYY-MM-DD'"
                    }
                }
            },
            "limit": {
                "type": "integer",
                "description": "Максимальное количество записей в ответе (1–100). По умолчанию 20.",
                "minimum": 1,
                "maximum": 100
            },
            "fields": {
                "type": "array",
                "description": "Список полей для включения в ответ. Если не указан — возвращаются все поля.",
                "items": {
                    "type": "string",
                    "enum": ["id", "name", "email", "status", "created_at", "last_login"]
                }
            }
        },
        "required": ["filters"],
        "additionalProperties": False
    }
}

Паттерн: инструмент-действие (создание/изменение)

python
{
    "name": "create_task",
    "description": (
        "Создаёт новую задачу в трекере. "
        "Используй когда пользователь явно просит создать, добавить или поставить задачу."
    ),
    "input_schema": {
        "type": "object",
        "properties": {
            "title": {
                "type": "string",
                "description": "Заголовок задачи — краткое, конкретное описание действия. Максимум 100 символов.",
                "minLength": 3,
                "maxLength": 100
            },
            "description": {
                "type": "string",
                "description": "Подробное описание задачи. Необязательно, но рекомендуется для сложных задач.",
                "maxLength": 2000
            },
            "priority": {
                "type": "string",
                "description": "Приоритет задачи: low (низкий), medium (средний), high (высокий). По умолчанию medium.",
                "enum": ["low", "medium", "high"]
            },
            "due_date": {
                "type": "string",
                "description": "Срок выполнения в формате ISO 8601, например '2024-12-31'. Необязательно."
            },
            "assignee_id": {
                "type": "integer",
                "description": "ID пользователя, которому назначить задачу. Если не указан — задача без исполнителя.",
                "minimum": 1
            }
        },
        "required": ["title"],
        "additionalProperties": False
    }
}

Типичные ошибки

❌ Нет описания параметров
"properties": {
  "q": {"type": "string"},
  "n": {"type": "number"},
  "f": {"type": "string"}
}
Короткие имена без описаний. Модель не знает что это значит и генерирует случайные значения.
✅ Понятные имена и описания
"properties": {
  "query": {
    "type": "string",
    "description": "Поисковый запрос"
  },
  "limit": {
    "type": "integer",
    "description": "Число результатов"
  }
}
Полные имена, каждый параметр с описанием. Модель генерирует правильно и стабильно.
❌ number вместо integer
"limit": {
  "type": "number"
}
Модель может передать 5.0 или 2.5. Если функция ожидает int — TypeError в рантайме.
✅ integer для целых чисел
"limit": {
  "type": "integer",
  "minimum": 1,
  "maximum": 100
}
Модель гарантированно передаст целое число в нужном диапазоне.
❌ Всё в required
"required": [
  "query", "limit",
  "sort", "lang", "page"
]
Модель вынуждена заполнять всё всегда — будет угадывать значения для параметров, которые пользователь не указал.
✅ Required — только необходимое
"required": ["query"]

// limit: default=10 в коде
// sort: default="desc" в коде
// lang: default="ru" в коде
Только query — без него поиск невозможен. Остальное — дефолты в функции.

Шпаргалка

# Скелет любого инструмента
{
  "name": "tool_name",               ← snake_case, уникальное
  "description": "Когда, зачем, что принимает"← промпт для LLM
  "input_schema": {
    "type": "object",                 ← всегда object
    "properties": {
      "param1": { "type": "string", "description": "..." },
      "param2": { "type": "integer", "minimum": 1, "maximum": 100 },
      "param3": { "type": "string", "enum": ["a", "b", "c"] },
    },
    "required": ["param1"],             ← только необходимые
    "additionalProperties": false     ← запрет лишних полей
  }
}

# Типы и их ключевые валидаторы
string  → minLength, maxLength, enum, pattern
integer → minimum, maximum, enum
number  → minimum, maximum
boolean → нет специальных валидаторов
array   → items: {схема элемента}, minItems, maxItems
object  → properties, required, additionalProperties

Практическое задание

  1. Инструмент для работы с заметками. Опиши JSON Schema для инструмента manage_note, который умеет создавать, обновлять и удалять заметки. Параметры: action (одно из трёх значений через enum), note_id (обязателен для update и delete, необязателен для create), title (строка, макс. 200 символов), content (строка, макс. 10 000 символов), tags (массив строк, макс. 5 тегов). Подумай: что обязательно, а что — нет?
  2. Валидация описаний. Возьми инструмент из предыдущего урока (get_exchange_rate) и намеренно сделай плохое описание: без description у параметров, короткие непонятные имена. Запусти агента с обоими вариантами и сравни стабильность аргументов. Сколько раз из 10 попыток модель передала правильные аргументы?
  3. Инструмент с вложенным объектом. Опиши инструмент send_email с параметрами: to (email-адрес), subject (строка), body (строка), attachments (массив объектов с полями filename и content_type), options (вложенный объект с priority и read_receipt). Реализуй функцию-заглушку и проверь, что агент правильно заполняет все поля при запросе «отправь письмо Ивану с темой Отчёт».
Следующий шаг

Теперь ты умеешь описывать любые инструменты через JSON Schema. Следующая статья — Parallel tool calling: как модель вызывает несколько инструментов за один запрос, как обработать все результаты и зачем это ускоряет агента.