Проблема: нечёткое описание — непредсказуемый агент
Ты пишешь инструмент для поиска документов. Модель должна передать строку запроса и, опционально, количество результатов. Казалось бы, всё просто. Но посмотри, что происходит при разном качестве описания:
"properties": {
"query": {"type": "string"},
"n": {"type": "number"}
}
}
n — это что? Целое или дробное? Лимит или смещение? Обязательное? Генерирует наугад: n: 1.5, n: 0, иногда вообще пропускает."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 передаёт в функцию.
Анатомия полного описания инструмента
Посмотрим на структуру целиком. Каждая часть выполняет свою роль — и ни одну нельзя игнорировать:
"type": "object"Корневой элемент input_schema всегда должен быть объектом ("type": "object"). LLM-провайдеры ожидают именно это. Даже если у инструмента один параметр — он всё равно обёртывается в объект.
Примитивные типы: string, number, integer, boolean
JSON Schema поддерживает шесть примитивных типов. Четыре из них используются в инструментах агентов практически всегда. Выбор правильного типа важен: если написать "number" вместо "integer", модель может передать 2.5 вместо 2 — и твоя функция упадёт с ошибкой типа.
К примитивным типам можно добавлять ограничения-валидаторы. Они не только защищают твой код — они дают модели явный контракт, в каком диапазоне генерировать значения:
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 выбор строго ограничен.
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 можно применить к string, integer, number и даже к смешанным спискам. Самый частый случай — строки с фиксированным набором значений: коды языков, статусы, единицы измерения, направления.
Массивы: список значений
Когда параметр — это несколько значений одного типа, используй "type": "array". Обязательно указывай items — схему каждого элемента массива. Без items модель не знает что должно быть внутри.
Типичные применения массивов в агентах: список тегов для поиска, несколько ID для пакетного запроса, набор полей для выборки из базы, список URL для обработки.
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"]
}
}
}
Вложенные объекты: группировка параметров
Когда несколько параметров логически связаны — объедини их во вложенный объект. Это улучшает читаемость схемы и помогает модели понять, что эти поля принадлежат одной концепции. Например, координаты (широта + долгота), диапазон дат (от + до), параметры пагинации (страница + размер).
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 # запрещаем лишние поля
}
}
Глубже двух уровней вложенности (объект внутри объекта) — сигнал, что стоит упростить схему или разбить на несколько инструментов. Чем сложнее схема, тем выше вероятность, что модель ошибётся в структуре аргументов.
required и optional: что обязательно
Поле required — массив имён параметров, без которых вызов невозможен. Если параметр не в required — он опциональный, и модель может его не передавать.
Правило выбора простое: обязательными делай только те параметры, без которых функция не может работать в принципе. Всё остальное — опционально. Это даёт модели гибкость и снижает вероятность ошибок.
"query", "limit",
"sort_order", "include_archived"
]
sort_order.// limit: опционально, дефолт в коде
// sort_order: опционально
// include_archived: опционально
Дефолтные значения не описываются в JSON Schema — их обрабатывает твоя функция:
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 каждого параметра — это промпт. Модель читает его в момент принятия решения о значении аргумента. Хорошее описание устраняет неоднозначность и предотвращает ошибки.
"date": "Дата"
"format": "Формат"
на русском языке,
например 'Москва'"
"date": "Дата в ISO 8601:
'2024-03-15'"
"format": "Формат ответа:
'json' или 'text'"
Что включать в хорошее описание параметра:
minimum/maximum в схеме. Двойная защита: и для модели, и для валидатора.Готовые паттерны для реальных инструментов
Ниже — три полных описания инструментов, которые покрывают 80% случаев в реальных агентах. Используй их как шаблоны.
Паттерн: поисковый инструмент
{
"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
}
}
Паттерн: запрос к базе данных
{
"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
}
}
Паттерн: инструмент-действие (создание/изменение)
{
"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
}
}
Типичные ошибки
"q": {"type": "string"},
"n": {"type": "number"},
"f": {"type": "string"}
}
"query": {
"type": "string",
"description": "Поисковый запрос"
},
"limit": {
"type": "integer",
"description": "Число результатов"
}
}
"type": "number"
}
5.0 или 2.5. Если функция ожидает int — TypeError в рантайме."type": "integer",
"minimum": 1,
"maximum": 100
}
"query", "limit",
"sort", "lang", "page"
]
// 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
Практическое задание
-
Инструмент для работы с заметками. Опиши JSON Schema для инструмента
manage_note, который умеет создавать, обновлять и удалять заметки. Параметры:action(одно из трёх значений через enum),note_id(обязателен для update и delete, необязателен для create),title(строка, макс. 200 символов),content(строка, макс. 10 000 символов),tags(массив строк, макс. 5 тегов). Подумай: что обязательно, а что — нет? -
Валидация описаний. Возьми инструмент из предыдущего урока (
get_exchange_rate) и намеренно сделай плохое описание: без description у параметров, короткие непонятные имена. Запусти агента с обоими вариантами и сравни стабильность аргументов. Сколько раз из 10 попыток модель передала правильные аргументы? -
Инструмент с вложенным объектом. Опиши инструмент
send_emailс параметрами:to(email-адрес),subject(строка),body(строка),attachments(массив объектов с полямиfilenameиcontent_type),options(вложенный объект сpriorityиread_receipt). Реализуй функцию-заглушку и проверь, что агент правильно заполняет все поля при запросе «отправь письмо Ивану с темой Отчёт».
Теперь ты умеешь описывать любые инструменты через JSON Schema. Следующая статья — Parallel tool calling: как модель вызывает несколько инструментов за один запрос, как обработать все результаты и зачем это ускоряет агента.