Один и тот же tool для вызова функций моделью приходится описывать заново под каждый фреймворк: tool() в AI SDK, registerTool в MCP SDK, defineTool в Genkit, tool в LangChain. Логика функции при этом не меняется, меняется только обёртка. Рабочее решение - хранить описание инструмента как обычный объект с полями name, description, inputSchema, outputSchema и execute, а под конкретный фреймворк писать небольшой адаптер.
Основу такого объекта закрывают две спецификации. Standard Schema отвечает за валидацию через свойство ~standard, её поддерживают Zod, Valibot, ArkType и другие библиотеки. Standard JSON Schema отвечает за генерацию JSON Schema под нужный диалект: draft-2020-12 для OpenAI и Anthropic, openapi-3.0 для Gemini. Поверх них предложен интерфейс StandardToolV0 - только тип без рантайма плюс необязательная эталонная реализация в пакете standard-tool.
Оговорка, которую стоит держать в голове сразу: пока другие проекты не создают и не читают такие объекты, StandardToolV0 остаётся ещё одним конкурирующим форматом с одним мейнтейнером. Авторы Zod, Valibot и ArkType в его поддержке не участвуют. Ниже разберём, что даёт подход, как устроены обе спецификации, чем отличаются обёртки фреймворков и в каких случаях выгоднее подождать.
Проблема: один инструмент и четыре обёртки
Сценарий, который повторяется в командах: первая версия инструментов написана на tool() из AI SDK. В проекте появляется MCP-сервер, и инструменты переписывают под registerTool. Соседняя команда работает на Genkit, и те же инструменты пишут в третий раз, через defineTool. Код внутри функций не меняется, но каждую версию приходится оформлять заново (разбор на Хабре).
Что такое инструмент для LLM на самом деле
Инструмент (tool) для LLM - это функция и всё, что модели нужно знать, чтобы её вызвать: имя, описание и схема аргументов. Модели этого достаточно, чтобы решить, когда и как вызвать инструмент. Этого же хватает, чтобы сгенерировать документацию, построить форму ввода или добавить команду в CLI (описание подхода).
Отсюда вывод: фреймворк нужен для оркестрации вызова, но не для хранения сути инструмента. Суть - несколько полей метаданных плюс функция.
Почему обёртки привязаны к фреймворкам
Каждая обёртка - это функция или метод конкретного пакета. tool() берётся из пакета ai; defineTool - метод экземпляра Genkit; registerTool - метод McpServer из MCP SDK. Функции инструментов остаются прежними, меняется только обёртка.
Дальше начинается проблема библиотек. Библиотеке, которая хочет поставлять инструменты, приходится выбрать один фреймворк, и его устанавливает каждый, кто её использует. Хотите дать пользователям инструменты сразу для AI SDK, MCP и Genkit - придётся тянуть три зависимости или выпускать три пакета. Похожую задачу решали в Hugging Face Transformers, где сводили разные форматы вызовов к одному интерфейсу: подробности в материале про единый API tool use.
Идея: инструмент как обычный объект
Без фреймворка инструмент описывает сам себя: код, имя, описание и схемы входа и выхода. Если оформить его обычным объектом, он останется частью вашего кода. Для переноса в другой фреймворк нужен только небольшой адаптер, переписывать инструмент не придётся.
Поля name, description, inputSchema, outputSchema, execute
- name - идентификатор инструмента, по нему модель и код понимают, что именно вызывается.
- description - текст для модели: что инструмент делает и когда его уместно использовать.
- inputSchema - схема аргументов, которые принимает функция.
- outputSchema - схема результата, который функция возвращает.
- execute - сама функция.
Минимальный объект выглядит так:
const weatherTool = {
name: 'get_weather',
description: 'Возвращает текущую погоду в городе',
inputSchema: z.object({ city: z.string() }),
outputSchema: z.object({ tempC: z.number(), summary: z.string() }),
execute: async ({ city }) => {
return await fetchWeather(city);
},
};
Тут нет ни одного импорта из фреймворка. Схемы описаны через Zod, но с тем же успехом подойдёт Valibot или ArkType.
StandardToolV0: только тип, без рантайма
StandardToolV0 оформляет эту идею в интерфейс. Это только тип без рантайма: не фреймворк и не библиотека, а контракт, который можно скопировать к себе. Готовая реализация лежит в пакете standard-tool, и она необязательная, эталонная.
Смысл разделения простой. Интерфейс описывает форму объекта. Всё, что связано с работой схем, отдано двум спецификациям: Standard Schema и Standard JSON Schema.
Standard Schema: валидация без привязки к библиотеке
Standard Schema - это TypeScript-интерфейс для библиотек валидации, его разработали авторы Zod, Valibot и ArkType. Код, который принимает Standard Schema, работает со схемой из любой библиотеки с поддержкой спецификации, и отдельный адаптер под каждую библиотеку не нужен.
Как работает свойство ~standard
Весь интерфейс - одно свойство ~standard. Этого достаточно, чтобы принимать схемы из разных библиотек без адаптеров.
interface StandardSchemaV1<Input = unknown, Output = Input> {
readonly '~standard': {
readonly version: 1;
readonly vendor: string;
readonly validate: (
value: unknown,
) => Result<Output> | Promise<Result<Output>>;
readonly types?: {
readonly input: Input;
readonly output: Output;
};
};
}
Вызов валидации одинаков для любой библиотеки:
const result = await schema['~standard'].validate(data);
if (result.issues) {
throw new Error(result.issues.map((issue) => issue.message).join('; '));
}
const value = result.value;
Спецификация состоит только из типов. В пакете @standard-schema/spec нет рантайм-кода, и библиотека может скопировать интерфейс к себе, а не зависеть от пакета. Это снимает риск, что лишняя зависимость сломает сборку.
Кто уже поддерживает Standard Schema
Спецификацию реализуют больше 30 библиотек, среди них Zod, Valibot, ArkType, yup и joi. Принимают её больше 60 проектов, в том числе tRPC, TanStack Form и Router, Hono, Elysia, oRPC и React Hook Form (исходный материал). Для формата, который существует ради совместимости, это сильный показатель: он уже в продакшене, а не только в документации.
Standard JSON Schema: генерация схемы под нужный диалект
Валидация - только половина того, что инструменту нужно от схем. Вторая половина - JSON Schema: прежде чем вызвать инструмент, модель должна получить описание аргументов в формате, который понимает провайдер.
Почему диалекты различаются
Провайдеры ожидают разные диалекты JSON Schema. OpenAI и Anthropic работают с draft-2020-12, Gemini - с openapi-3.0. Одна и та же схема аргументов должна уметь генерироваться в разных форматах, иначе под каждого провайдера придётся держать отдельный генератор.
Standard JSON Schema закрывает именно генерацию: схема описывается один раз, а на выходе получается представление под нужный диалект.
Как это связано с inputSchema и outputSchema
Поля inputSchema и outputSchema в объекте инструмента получают двойную роль. Через Standard Schema они валидируют данные на стороне кода. Через Standard JSON Schema из них генерируется описание для модели под конкретный диалект. Разделение ответственности получается чистым: валидация остаётся в коде, генерация описания уходит адаптеру.
Сравнение: AI SDK, Mastra, Genkit, LangChain и MCP SDK
Самое заметное различие между фреймворками - точка входа, через которую описывается инструмент.
| Фреймворк | Как описывается инструмент |
|---|---|
| AI SDK | Функция tool() из пакета ai |
| MCP SDK | Метод registerTool у экземпляра McpServer |
| Genkit | Метод defineTool у экземпляра Genkit |
| LangChain | Собственная обёртка tool |
| Mastra | Собственная обёртка, детали зависят от версии |
Точные имена полей, способ задания схем и наличие выходной схемы зависят от версии, поэтому перед миграцией сверяйтесь с актуальной документацией выбранного фреймворка. Общее ядро у всех одно: имя, описание, схема аргументов и функция-обработчик.
Что общего и что различается
По смыслу все обёртки передают модели одно и то же: имя инструмента, описание и схему аргументов. Совпадают name, description, inputSchema и execute. Различия начинаются в деталях: способ задания схем, наличие outputSchema, формат регистрации. Именно эти расхождения заставляют писать адаптер: поля совпадают по смыслу, но не по форме.
Почему единый формат всё ещё не стандарт
Каждый фреймворк развивается отдельно, со своими приоритетами и сроками, и договариваться об общем контракте никто не обязан. Пока StandardToolV0 никем, кроме автора, не читается и не создаётся, он остаётся ещё одним форматом в общем списке.
Адаптеры: как подключить StandardToolV0 к провайдерам
Адаптер берёт объект с полями name, description, inputSchema, outputSchema и execute и преобразует его в то, что ожидает фреймворк или провайдер. Для OpenAI и Anthropic нужен draft-2020-12, для Gemini - openapi-3.0, поэтому генерацию схемы удобно держать на стороне адаптера, а не внутри инструмента. Когда инструментов становится много, появляется отдельная задача - их фильтрация перед отправкой в модель; практический пример отбора разбирается в статье про tool-prune и экономию промпт-токенов. В продакшене инструменты часто живут за шлюзом: кейс AvioBook на Amazon Bedrock AgentCore показывает, как MCP-инструменты выносят в Gateway и изолируют данные.
Адаптер для AI SDK
Задача адаптера - принять StandardToolV0 и вернуть результат tool() из пакета ai, передав описание и схему аргументов в формате, который ожидает AI SDK:
import { tool } from 'ai';
// Схематично: имена полей зависят от версии AI SDK
function toAiSdk(t) {
return tool({
description: t.description,
schema: t.inputSchema,
execute: t.execute,
});
}
Имена полей у tool() менялись между версиями, поэтому перед копированием сверьтесь с документацией своей версии. Логика адаптера от этого не меняется: он просто раскладывает поля объекта по местам.
Адаптер для MCP SDK
// server - экземпляр McpServer
function registerStdTool(server, t) {
server.registerTool(
t.name,
{
description: t.description,
inputSchema: t.inputSchema,
},
t.execute,
);
}
Поля name, description и inputSchema маппятся напрямую. outputSchema добавляется, если ваш клиент её читает; для базового сценария вызова инструмента достаточно входной схемы.
Адаптер для Genkit
// ai - экземпляр Genkit
function toGenkit(ai, t) {
return ai.defineTool(
{
name: t.name,
description: t.description,
inputSchema: t.inputSchema,
outputSchema: t.outputSchema,
},
t.execute,
);
}
execute передаётся как есть: функции инструмента всё равно, кто её вызвал. Перед переводом всего набора инструментов проверьте один адаптер на одном инструменте, чтобы поймать расхождения в сигнатурах сразу.
Ограничения: почему StandardToolV0 пока не стандарт
Главное ограничение проговаривается прямо: пока другие проекты не создают и не читают такие объекты, StandardToolV0 остаётся ещё одним конкурирующим форматом с одним мейнтейнером без поддержки авторов Zod, Valibot и ArkType.
Один мейнтейнер и отсутствие поддержки авторов схем
У формата нет гарантии развития и нет обязательств у фреймворков. Даже если Zod, Valibot и ArkType поддерживают Standard Schema, к самому StandardToolV0 они отношения не имеют: это надстройка, а не часть спецификации валидации. Пока нет второй и третьей независимой реализации со стороны других команд, статус остаётся экспериментальным.
Когда подход оправдан, а когда проще подождать
Подход окупается в двух ситуациях. Первая - вы пишете библиотеку инструментов и не хотите привязывать её к одному фреймворку, заставляя пользователей ставить лишнюю зависимость. Вторая - у вас несколько проектов на разных фреймворках, и дублирование описаний уже мешает.
Подождать разумнее, если вы работаете в одном фреймворке и миграции не планируете: лишний слой абстракции добавит кода без выгоды. Промежуточный вариант - описать инструменты обычным объектом и написать один адаптер под свой фреймворк, не завязываясь на имя StandardToolV0. Тогда при появлении отраслевого стандарта вы поменяете адаптер, а не сами инструменты. Такой объект по сути API-контракт инструмента, и логика здесь та же, что и с другими контрактами: проектирование важнее кода.
Итог: что делать с инструментами сегодня
Опишите инструмент обычным объектом с полями name, description, inputSchema, outputSchema и execute. Это убирает дублирование уже сейчас, даже если вы не называете объект StandardToolV0. Валидацию отдайте Standard Schema: одна реализация будет работать и с Zod, и с Valibot, и с ArkType. Генерацию JSON Schema под провайдера закройте отдельным слоем адаптера: draft-2020-12 для OpenAI и Anthropic, openapi-3.0 для Gemini.
StandardToolV0 попробовать можно, но с открытыми глазами: это контракт с одним мейнтейнером, а не отраслевой стандарт. Оцените, сколько фреймворков вы поддерживаете. Если один, выносить описание в отдельный объект стоит ради чистоты кода. Если два и больше или вы поставляете инструменты наружу, вынос описания окупается сразу.
Отдельно про бытовую часть: если для зарубежных AI-провайдеров нужна оплата подписок, а локальная карта не подходит, иногда выручает виртуальная банковская карта зарубежного банка с пополнением в рублях через СБП. Биллинг не относится к архитектуре инструментов, но всплывает ровно тогда, когда код уже готов.