Перейти к содержанию
Публикация AiManual

Единый API для tool use в Transformers: как упростили подключение инструментов к LLM

Разбираем, как единый API tool use в Hugging Face Transformers связывает JSON Schema, Python-функции, tool calls и tool responses. Показываем полный цикл для ча

Коротко

Что будет в материале

  1. 01

    Короткий ответ: зачем нужен единый API для tool use в Transformers

  2. 02

    Почему разные форматы вызовов инструментов создавали лишнюю работу

  3. 03

    Как описывать инструменты: JSON Schema или Python-функция

  4. 04

    Как проходят tool calls и tool responses через историю чата

Tool use позволяет языковой модели запрашивать действия приложения: поиск по базе, вызов внешнего API, вычисление, получение документа или проверку факта. Модель выбирает инструмент и формирует аргументы, а приложение проверяет запрос, запускает функцию и возвращает результат в историю чата.

Hugging Face Transformers дает общий сценарий для этой цепочки. Инструменты можно описывать через JSON Schema или Python-функции, передавать их вместе с сообщениями через chat template и сохранять tool calls и tool responses в едином контексте. Это уменьшает объем model-specific кода при работе с несколькими совместимыми моделями.

Граница возможностей остается четкой: Transformers не запускает внешнюю функцию вместо приложения, не выдает модели права доступа и не отменяет особенности конкретного checkpoint. Для Mistral, Cohere, NousResearch и Llama нужно отдельно проверять chat template, формат ответа и поддержку tool use. Единый интерфейс сокращает число различий, но не превращает разные семейства LLM в одинаковый runtime.

Коротко: общий API полезен как слой описания инструментов, подготовки сообщений и передачи результатов. Исполнительный код, валидация аргументов, безопасность, разбор сырого ответа модели и адаптация шаблона чата остаются задачами приложения.

Короткий ответ: зачем нужен единый API для tool use в Transformers

Обычная LLM возвращает текст. Модель с поддержкой tool use может вернуть структурированный запрос: вызвать инструмент с определенным именем и аргументами. Например, вместо ответа о погоде она просит приложение запустить get_weather для конкретного города.

Единый интерфейс Transformers связывает три операции:

  1. описание доступных инструментов;
  2. сериализацию инструментов и сообщений через chat template;
  3. передачу вызова модели и результата функции обратно в историю.

Для чат-бота, RAG-системы или агента это означает общий каркас цикла. Бизнес-логика может работать с внутренним представлением инструмента, а преобразование в формат конкретной модели остается на границе адаптера.

Что именно становится общим

У tool use есть три разных уровня. Их смешение часто создает неверные ожидания от библиотеки.

УровеньЧто делаетКто отвечает
Описание toolsХранит имя, назначение, параметры, типы и обязательные поляПриложение и Transformers
Chat templateПреобразует сообщения и описания инструментов в последовательность токенов нужного форматаТокенизатор и шаблон конкретного checkpoint
ИсполнениеПроверяет аргументы, вызывает Python-функцию или endpoint, обрабатывает ошибкуПриложение

На уровне библиотеки разработчик получает единый вход для списка инструментов. В актуальных сценариях Transformers этот список передают в процесс подготовки chat template через параметр tools, если выбранная модель и ее шаблон поддерживают такой режим. Шаблон сам определяет, как представить декларации модели.

Единый слой полезен и для истории. Приложение хранит сообщение пользователя, ответ assistant с tool call, сообщение tool с результатом и финальный ответ. Благодаря этому модель видит последовательность событий, а не отдельную строку, которую разработчик вручную добавил к prompt.

Что единый интерфейс не обещает

  • Все модели не начинают одинаково понимать один и тот же шаблон.
  • Названия ролей, специальные токены и маркеры вызова могут различаться.
  • Аргументы могут прийти в виде JSON-строки, объекта или фрагмента ответа, который придется извлечь отдельно.
  • Поддержка Python-функций и конкретных аннотаций зависит от версии Transformers и chat template.
  • Наличие поля tools в коде не гарантирует, что checkpoint корректно выберет инструмент.

Поэтому общий API нужно воспринимать как контракт между приложением и слоем подготовки запроса. Надежность всей цепочки определяется еще качеством модели, шаблоном чата, парсером, валидатором и исполнителем.

Почему разные форматы вызовов инструментов создавали лишнюю работу

Один и тот же инструмент можно представить несколькими способами. В декларации ему нужны имя, описание и параметры. В prompt эти данные могут попасть как JSON, XML-подобная разметка или специальная последовательность токенов. В ответе модели вызов может быть отдельным объектом, строкой с JSON или текстом между служебными маркерами.

Один инструмент, несколько представлений

Предположим, приложение умеет получать документ по идентификатору. Смысл операции не меняется, но модельный контракт может отличаться в четырех местах:

  • имя функции может находиться в поле name или внутри объекта function;
  • аргументы могут быть JSON-объектом или строкой, которую нужно декодировать;
  • результат может передаваться через роль tool либо через специальное сообщение шаблона;
  • связь между вызовом и ответом может использовать идентификатор call или имя инструмента.

На уровне приложения это одна операция: get_document(document_id). На уровне модели четыре различия способны затронуть prompt, сериализацию, парсинг и историю сообщений.

ЭтапОбщий смыслГде появляется различие
ДекларацияСообщить модели, какой инструмент доступенФорма schema и способ передачи в chat template
ВызовПолучить имя и аргументыМаркеры, поля объекта, JSON или текст
ОтветПередать результат функцииРоль сообщения, идентификатор и сериализация результата

Где появлялся model-specific код

До появления общего слоя разработчик часто держал отдельные ветки для каждого семейства или шаблона:

  1. собрать список инструментов в нужном формате;
  2. вставить декларации в prompt;
  3. выбрать правильный chat template и служебные маркеры;
  4. найти вызов в сыром ответе модели;
  5. декодировать аргументы и проверить их типы;
  6. создать сообщение с результатом;
  7. запустить повторную генерацию после tool response.

Такие ветки быстро начинают расходиться. Исправление парсера для одной модели не попадает в другой путь. Логи разных адаптеров получают разную структуру. Регрессионный тест приходится дублировать, а ошибка в одном формате может проявиться только на конкретном checkpoint.

Почему ручные адаптеры плохо масштабируются

Проблема ручных адаптеров растет вместе с числом моделей и инструментов. Добавление нового tool затрагивает схему, реестр, валидатор и обработчик. Добавление нового checkpoint затрагивает шаблон, генерацию, парсер и историю.

В агентной цепочке каждый новый шаг увеличивает объем контекста. Если приложение сохраняет лишний служебный текст, повторные ответы и сырые фрагменты парсинга, модель получает больше токенов, а отладка становится сложнее. Единый внутренний формат помогает отделить данные вызова от транспортного представления и не тащить model-specific детали по всему проекту.

Задача унификации здесь практическая: держать один контракт инструмента и одну бизнес-логику, а различия моделей локализовать в подготовке сообщений и разборе ответа.

Как описывать инструменты: JSON Schema или Python-функция

Описание инструмента задает контракт между тремя участниками: моделью, приложением и исполнителем функции. В нем должны совпадать имя операции, назначение, типы аргументов и фактическая логика обработчика.

JSON Schema как явный контракт

JSON Schema подходит, когда инструменты приходят из конфигурации, базы данных или отдельного сервиса. Разработчик получает полный контроль над декларацией и может хранить ее независимо от Python-кода.

{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "Получает текущие погодные данные для города",
    "parameters": {
      "type": "object",
      "properties": {
        "city": {
          "type": "string",
          "description": "Название города"
        },
        "units": {
          "type": "string",
          "enum": ["celsius", "fahrenheit"]
        }
      },
      "required": ["city"]
    }
  }
}

Это иллюстративная форма декларации. Точную оболочку schema нужно сверить с требованиями версии Transformers и шаблона выбранной модели. Самые полезные части контракта почти всегда одинаковы по смыслу:

  • короткое уникальное имя;
  • описание, объясняющее назначение и границы операции;
  • объект параметров;
  • типы и понятные описания полей;
  • список обязательных аргументов;
  • перечисления для ограниченного набора значений.

Схема сообщает модели допустимую форму вызова. Она не заменяет серверную проверку. Аргумент от LLM считается недоверенным вводом, даже если модель сама сформировала его по декларации.

Python-функция как более удобное описание

Если инструмент уже существует в приложении, удобнее передать Python-функцию. Имя, сигнатура, аннотации типов и docstring могут использоваться для построения схемы. Такой способ уменьшает дублирование между обработчиком и декларацией.

def get_weather(city: str, units: str = 'celsius') -> dict:
    '''Получает текущие погодные данные для города.

    Args:
        city: Название города.
        units: Единицы измерения, celsius или fahrenheit.
    '''
    return weather_client.fetch(city=city, units=units)

В Transformers Python-функции передают в список tools в тех версиях и для тех chat template, где такой вход поддерживается. Автоматическое извлечение схемы не делает любую сигнатуру совместимой. Нужно проверить обработку значений по умолчанию, объединений типов, перечислений, вложенных объектов и описаний в docstring.

Для небольшого проекта функция удобнее: код обработчика и его контракт находятся рядом. Для большой системы JSON Schema обычно дает более предсказуемый контроль, особенно когда инструменты доступны разным сервисам или языкам программирования.

Что проверять до передачи схемы модели

  • Имя в schema точно совпадает с ключом в реестре исполнителей.
  • Обязательные поля действительно требуются функции.
  • Типы соответствуют реальному обработчику, а не только подсказке для модели.
  • Значения по умолчанию не создают опасное поведение.
  • Даты, часовые пояса, идентификаторы и перечисления описаны однозначно.
  • Описание объясняет, когда инструмент нужно вызывать, а когда достаточно ответа без него.
  • Инструмент дает модели минимально необходимые права.
  • Сервер повторно проверяет значения перед обращением к базе, файлам или внешнему API.

Как проходят tool calls и tool responses через историю чата

Рабочий цикл состоит из пяти шагов: пользователь формулирует задачу, приложение передает tools, модель возвращает запрос на действие, функция отрабатывает, результат возвращается в историю и запускается следующая генерация.

Первый запрос: сообщение пользователя и список tools

Приложение создает массив сообщений и список доступных инструментов. Затем токенизатор применяет chat template. На этом этапе шаблон превращает роли, текст, декларации tools и флаг начала ответа assistant в последовательность, которую ожидает checkpoint.

Список инструментов лучше формировать для каждой операции отдельно. Если передать модели десятки похожих функций, она получает больше вариантов выбора и чаще ошибается в маршрутизации. Реестр приложения может быть большим, а набор tools в конкретном запросе, ограниченным задачей пользователя.

messages = [
    {
        'role': 'user',
        'content': 'Узнай погоду в Казани и сообщи температуру.'
    }
]

tools = [weather_schema]

model_input = build_model_input(
    messages=messages,
    tools=tools,
    add_generation_prompt=True
)

Функция build_model_input здесь обозначает слой, который вызывает токенизатор и его chat template. Названия параметров, формат возвращаемого объекта и необходимость передавать тензоры зависят от конкретного кода инференса.

Ответ модели с запросом на действие

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

{
  "name": "get_weather",
  "arguments": {
    "city": "Казань",
    "units": "celsius"
  }
}

Это нормализованное внутреннее представление, а не универсальный сырой ответ всех моделей. В реальном ответе поле arguments может оказаться JSON-строкой. Иногда перед объектом вызова присутствует текст, служебный маркер или несколько вызовов подряд. Парсер должен учитывать формат конкретного chat template.

До запуска функции приложение проверяет три вещи: имя входит в разрешенный реестр, JSON разбирается без ошибки, аргументы проходят схему и бизнес-правила.

Возврат результата и продолжение генерации

После выполнения приложение добавляет в историю сообщение assistant с исходным вызовом и сообщение tool с результатом. Это позволяет модели связать результат с собственным запросом и сформировать финальный ответ.

messages.append({
    'role': 'assistant',
    'tool_calls': [call]
})

result = execute_tool(call)

messages.append({
    'role': 'tool',
    'name': call['function']['name'],
    'content': json.dumps(result, ensure_ascii=False)
})

final_input = build_model_input(
    messages=messages,
    tools=tools,
    add_generation_prompt=True
)

В примере используется внутренний формат, где у вызова есть вложенный объект function. Конкретный шаблон может требовать другую структуру, роль или идентификатор. Связь между call и response нужно сохранять, если ее поддерживает выбранная модель.

Результат инструмента лучше возвращать в компактном структурированном виде. Сырые HTML-страницы, stack trace и большие документы увеличивают контекст и усложняют следующую генерацию. Для RAG полезнее вернуть найденные фрагменты, идентификаторы документов и короткие метаданные, чем весь ответ поискового сервиса.

Что делать с ошибкой инструмента

Ошибка функции тоже должна иметь понятный контракт. Приложение выбирает один из четырех сценариев:

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

Модели не нужен внутренний stack trace с путями к файлам, токенами доступа и служебными данными. Для нее достаточно кода ошибки, безопасного описания причины и признака, можно ли повторить операцию.

Как единый Transformers tools API соотносится с Mistral, Cohere, NousResearch и Llama

Сравнивать нужно конкретные checkpoint, а не только названия семейств. У одной линейки могут существовать базовая, instruct- и chat-версии с разными шаблонами. Производная модель может использовать собственную разметку даже при похожей архитектуре.

Общий тестовый сценарий для разных семейств

Для честной проверки Mistral, Cohere, NousResearch и Llama используйте один инструмент, одну схему и одну задачу. Например, функцию поиска документа с обязательным параметром query.

  1. Передайте одинаковую декларацию инструмента.
  2. Отправьте одинаковое пользовательское сообщение.
  3. Проверьте, выбрала ли модель правильное имя.
  4. Проверьте тип, полноту и значения аргументов.
  5. Добавьте tool response в историю.
  6. Запросите финальный ответ после результата.
  7. Повторите тест с ошибкой инструмента и пустым результатом.

Метрика совместимости здесь состоит не из одного факта вызова. Нужны корректные аргументы, правильный порядок сообщений, стабильное продолжение после результата и понятное поведение при отказе.

Что остается зависимым от конкретной модели

  • Mistral. Проверьте chat template токенизатора и способ передачи tools для выбранной instruct-модели. Не переносите шаблон от одного checkpoint к другому без проверки.
  • Cohere. Уточните режим диалога, поддерживаемые роли и форму вызова для конкретной версии модели. Название семейства не описывает весь транспортный контракт.
  • NousResearch. Многие модели этого издателя основаны на других семействах. Сначала определите базовую модель и откройте фактический шаблон из конфигурации checkpoint.
  • Llama. Сравните базовую и instruct-версию. Для tool use нужен шаблон, который умеет передавать декларации и возвращать структурированный вызов, а не произвольный текст.

Для каждого семейства нужно отдельно проверить специальные токены, роли сообщений, сериализацию arguments, поддержку нескольких вызовов, потоковую генерацию и реакцию на некорректный JSON. Эти различия не исчезают из-за общего списка tools в коде приложения.

Минимальный чек-лист совместимости

  • Версия Transformers согласована с кодом инференса.
  • В конфигурации токенизатора есть подходящий chat template.
  • Шаблон принимает JSON Schema или Python-функции в выбранном режиме.
  • Модель стабильно возвращает имя инструмента и аргументы.
  • Парсер обрабатывает JSON-строку, объект, лишний текст и несколько вызовов.
  • История с assistant tool call и tool response снова принимается моделью.
  • Локальный inference-сервер не меняет роли и специальные поля.
  • Есть тесты для успешного вызова, неверных аргументов, тайм-аута и пустого результата.

Перед сменой модели полезно оценить не только качество текста. В чек-листе оценки новых AI-моделей отдельно разобраны контекст, API, MCP, скорость, VRAM и другие параметры, которые влияют на выбор рабочего стека.

Практическая интеграция: от функции приложения до рабочего чат-бота

Архитектуру удобно разделить на четыре слоя: реестр инструментов, подготовка запроса, исполнитель и обработчик ответа модели. Такой разрез одинаково подходит для чат-бота, агента, RAG-поиска и вызова внешнего API.

Реестр инструментов и маршрутизация вызовов

Реестр связывает имя, видимое модели, с функцией приложения. В него попадают только разрешенные операции.

TOOLS = {
    'get_weather': get_weather,
    'search_documents': search_documents
}

SCHEMAS = [weather_schema, search_schema]

def route_call(call):
    name = call['name']
    if name not in TOOLS:
        raise ValueError('Unknown tool')

    arguments = call['arguments']
    if isinstance(arguments, str):
        arguments = json.loads(arguments)

    validate_arguments(name, arguments)
    return TOOLS[name](**arguments)

Функция validate_arguments должна проверять схему и ограничения предметной области. Например, строка с идентификатором документа может соответствовать JSON Schema, но все равно не иметь доступа к документам текущего пользователя.

Маршрутизатор не должен выполнять неизвестное имя, молча игнорировать лишние поля или передавать аргументы в shell-команду. Неожиданный вызов нужно записать в лог и завершить контролируемой ошибкой.

Безопасное выполнение аргументов модели

LLM формирует предложение для приложения. Доверенным вводом оно не становится. Перед вызовом внешнего API или функции проверьте:

  • права текущего пользователя;
  • допустимые значения и диапазоны;
  • размер строк, списков и вложенных объектов;
  • тайм-аут и число повторов;
  • лимиты запросов и стоимость операции;
  • доступ к файлам, базе данных и сетевым адресам;
  • отсутствие секретов в аргументах и логах.

Для опасных действий нужен дополнительный барьер: подтверждение пользователя, режим только для чтения или отдельный сервис с ограниченными правами. Transformers отвечает за передачу контекста и форматирование взаимодействия. Политика доступа находится рядом с исполнителем.

Практический разбор этой границы есть в статье про harness engineering для AI-агентов: стабильность цепочки зависит от контроля состояния, инструментов, данных и ошибок.

Как этот слой используется в RAG и агентных сценариях

В RAG tool use может разделить поиск и генерацию. Модель выбирает инструмент поиска, приложение получает документы, фильтрует их по правам и возвращает компактный результат в историю. После этого LLM формирует ответ с учетом найденного контекста.

Типичные инструменты для такой системы:

  • search_documents, поиск по индексу;
  • get_document, получение разрешенного фрагмента;
  • query_database, выполнение ограниченного запроса;
  • calculate, точное вычисление вне LLM;
  • check_fact, обращение к внутреннему справочнику;
  • call_service, запрос к рабочему API.

В AI-агенте тот же цикл может повторяться несколько раз. Оркестратор хранит состояние, ограничивает число шагов и решает, когда остановить модель. Подробная схема такого рантайма разобрана в материале о создании AI-агента на Python.

MCP можно подключить на уровне окружающей инфраструктуры. MCP-сервер предоставляет инструменты, а клиент или оркестратор получает их описание и связывает с модельным циклом. Transformers может занимать место модельного слоя, но подключение MCP-сервера, управление правами и жизненным циклом сессии остаются задачами приложения.

Логирование и повторяемость результата

Для каждой цепочки сохраняйте отдельные события:

  1. исходные сообщения;
  2. список переданных tools;
  3. сырой ответ модели;
  4. нормализованный tool call;
  5. результат функции или безопасное описание ошибки;
  6. финальный ответ.

Такая запись помогает определить место сбоя. Ошибка может находиться в схеме, chat template, генерации, парсере, валидаторе, правах доступа или внешнем API.

Секреты, персональные данные и полные документы не должны автоматически попадать в логи. Для регрессионного теста лучше использовать обезличенные фикстуры и фиксированные ответы инструментов. Тогда смена checkpoint не смешивается с нестабильностью внешнего сервиса.

Где унификация действительно сокращает код, а где ручная работа остается

Общий API дает максимальный эффект в повторяющемся каркасе. Он помогает вынести регистрацию инструментов, передачу описаний, историю сообщений и базовый цикл вызова в один слой.

Что можно вынести в общий слой

КомпонентЧто можно сделать общимПрактический результат
РеестрЕдиная таблица разрешенных имен и обработчиковМодель не зависит от структуры Python-модулей
СхемыХранение JSON Schema и генерация из функцийМеньше повторяющихся деклараций
ИсторияОбщий внутренний формат user, assistant, toolОдна бизнес-логика для разных моделей
ЦиклВызов, проверка, выполнение, возврат результатаЕдиная обработка тайм-аутов и ошибок
ЛогиОдинаковые поля для сырого и нормализованного ответаПроще сравнивать checkpoint и искать сбои

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

Почему парсер ответа все еще нужен

Унифицированная передача tools не отменяет разбор фактического ответа модели. Парсер должен уметь:

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

Если модель иногда дописывает пояснение рядом с JSON, приложение не должно передавать весь ответ в json.loads. Нужен парсер, который знает формат выбранного шаблона и умеет отказать при неоднозначном результате. Молчаливое угадывание аргументов опаснее остановки цепочки.

Какие адаптеры нельзя удалять без проверки

Отдельный адаптер нужен там, где различаются chat template, роли, специальные токены, потоковая генерация, формат arguments или правила локального inference-сервера. Публичный интерфейс приложения при этом может оставаться общим:

response = model_adapter.generate(
    messages=messages,
    tools=schemas
)

calls = model_adapter.parse_tool_calls(response)

За этим интерфейсом один адаптер может использовать токенизатор Transformers, другой, собственный разбор служебных маркеров. Бизнес-логика видит одинаковый список вызовов и не знает о транспортных деталях.

Перед удалением старой ветки сравните ответы на успешном вызове, нескольких вызовах, ошибке аргументов и повторной генерации после tool response. Без этих тестов рефакторинг легко ломает редкий, но критичный сценарий.

Ограничения, проверка и выводы для прикладного проекта

Чек-лист перед запуском

  1. Зафиксируйте версию Transformers и формат запуска модели.
  2. Проверьте chat template конкретного checkpoint.
  3. Передайте один минимальный tool через JSON Schema.
  4. Проверьте передачу Python-функции, если этот режим нужен проекту.
  5. Сохраните сырой ответ и проверьте парсинг имени и arguments.
  6. Добавьте assistant tool call и tool response в историю.
  7. Проверьте повторную генерацию финального ответа.
  8. Протестируйте неверный JSON, неизвестный tool, тайм-аут и отказ в доступе.
  9. Сравните поведение локального inference-сервера и прямого запуска через Transformers.
  10. Добавьте регрессионные тесты с фиксированными ответами инструментов.

Если модель работает нестабильно, сначала изолируйте слои. Отправьте ей один tool, уберите внешнюю сеть, зафиксируйте ответ функции и проверьте цепочку по журналу. Это быстрее, чем сразу менять prompt или весь оркестратор.

Когда нужен собственный слой совместимости

Собственный слой оправдан, когда проект использует несколько моделей, локальные серверы или инструменты с разными правами. Внутри него стоит определить единый контракт:

  • нормализованное имя инструмента;
  • словарь аргументов;
  • тип результата;
  • связь call и response;
  • коды ошибок;
  • лимиты времени и повторов.

Model-specific преобразования держите на границе: при подготовке запроса и разборе ответа. Тогда смена Mistral, Cohere, NousResearch или Llama не требует переписывать RAG-поиск, контроль доступа и исполнители внешних API.

Главный вывод

Единый API для tool use в Hugging Face Transformers делает подключение инструментов последовательнее. Разработчик получает общий подход к описанию JSON Schema и Python-функций, передаче tools в chat template, сохранению tool calls и возврату tool responses в историю.

Сокращение кода проявляется в реестре, подготовке сообщений, цикле выполнения и логировании. Ручная работа остается в парсере ответа, проверке аргументов, безопасности, обработке ошибок и адаптации конкретного chat template.

Практическое правило: держите общий контракт tools внутри приложения, а модельные особенности прячьте в адаптере. Проверяйте каждый checkpoint отдельным тестовым сценарием, даже если его семейство уже знакомо.

Подписаться на канал