Определение и интеграция функций-инструментов

На прошлой странице мы научились, как сохранять и восстанавливать состояние агента — его память. Это позволило нашему ИИ-агенту помнить предыдущие взаимодействия и вести более осмысленный диалог.

Теперь пришло время наделить агента действием.

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

Для этого используются инструменты (tools) — функции, которые агент может вызывать по решению LLM, когда это необходимо для выполнения задачи.


Что такое инструмент в MCP?

Инструмент (tool) — это функция на сервере, которая может быть вызвана LLM в процессе выполнения запроса.

Чтобы LLM мог её использовать, он должен понимать, что делает инструмент, какие у него параметры и что он возвращает.

Для этого инструмент описывается в специальном формате — JSON-схеме, доступной через эндпоинт /capabilities.

Когда LLM видит, что для решения задачи нужно выполнить действие (например, получить погоду), он формирует вызов инструмента и отправляет его на MCP-сервер.

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

🔍 Важно: Инструменты — это не просто API-вызовы. Это структурированные возможности, которые LLM может осознанно выбирать, как человек выбирает отвёртку или молоток в зависимости от задачи.


Формат описания инструмента

Каждый инструмент описывается объектом в формате JSON. Вот обязательные поля:

ПолеОписание
nameУникальное имя инструмента (латиница, без пробелов)
descriptionПонятное описание того, что делает инструмент (LLM читает это!)
parametersJSON-схема параметров, которую понимает LLM

Пример описания инструмента для получения погоды:

{
  "name": "getWeather",
  "description": "Возвращает текущую погоду в указанном городе",
  "parameters": {
    "type": "object",
    "properties": {
      "city": {
        "type": "string",
        "description": "Название города, например, Москва"
      }
    },
    "required": ["city"]
  }
}

⚠️ Ошибка, которую часто допускают:
Неточное описание description — например, "получает погоду".
Это недостаточно! LLM может не понять, нужен ли город, в каком формате и обязателен ли он.

Правильно: "Возвращает текущую погоду в указанном городе. Параметр city — обязательный, строка, например 'Санкт-Петербург'."


Реализация функции-инструмента на TypeScript

Теперь создадим саму функцию. Она должна:

  • Принимать параметры в формате, соответствующем схеме
  • Быть асинхронной (используем async)
  • Выполнять действие (например, запрос к API)
  • Возвращать результат в стандартизированном формате
import axios from 'axios';

interface GetWeatherParams {
  city: string;
}

// Асинхронная функция-инструмент
export const getWeather = async (params: GetWeatherParams) => {
  try {
    // Валидация параметров
    if (!params.city || typeof params.city !== 'string') {
      return {
        result: null,
        error: 'Параметр "city" обязателен и должен быть строкой'
      };
    }

    // Вызов внешнего API
    const response = await axios.get('https://api.openweathermap.org/data/2.5/weather', {
      params: {
        q: params.city,
        appid: process.env.OPENWEATHER_API_KEY,
        lang: 'ru',
        units: 'metric'
      }
    });

    const weather = response.data;
    return {
      result: {
        city: weather.name,
        temp: weather.main.temp,
        description: weather.weather[0].description
      },
      error: null
    };
  } catch (error: any) {
    return {
      result: null,
      error: `Не удалось получить погоду: ${error.message}`
    };
  }
};

💡 Зачем нужна обёртка с result и error?
Такой формат позволяет MCP-серверу предсказуемо обрабатывать ответы:

  • Если error не null — LLM получит сообщение об ошибке и сможет попробовать снова или сообщить пользователю.
  • Если result не null — LLM продолжит работу с данными.

Регистрация инструмента в MCP-сервере

Чтобы LLM узнал о нашем инструменте, нужно добавить его описание в ответ эндпоинта /capabilities.

app.get('/capabilities', (req, res) => {
  res.json({
    tools: [
      {
        name: "getWeather",
        description: "Возвращает текущую погоду в указанном городе",
        parameters: {
          type: "object",
          properties: {
            city: { type: "string", description: "Название города" }
          },
          required: ["city"]
        }
      }
    ]
  });
});

Теперь LLM "знает" об этом инструменте и может решить, когда его вызвать.


Обработка вызова инструмента: эндпоинт /tool

Когда LLM решает использовать инструмент, он отправляет запрос на /tool с телом:

{
  "tool": "getWeather",
  "parameters": {
    "city": "Екатеринбург"
  }
}

Наш сервер должен:

  1. Получить запрос
  2. Найти нужную функцию по имени
  3. Вызвать её с параметрами
  4. Вернуть результат
app.post('/tool', async (req, res) => {
  const { tool, parameters } = req.body;

  // Проверка, что инструмент существует
  if (tool !== 'getWeather') {
    return res.status(404).json({
      error: `Инструмент "${tool}" не найден`
    });
  }

  try {
    // Вызов функции
    const result = await getWeather(parameters);
    res.json(result);
  } catch (error) {
    res.status(500).json({
      result: null,
      error: 'Внутренняя ошибка сервера при вызове инструмента'
    });
  }
});

🔐 Безопасность: В реальном проекте используйте реестр инструментов (например, const tools = { getWeather, getTime, ... }) и проверяйте имя через in или hasOwnProperty.


Практический пример: интеграция от начала до конца

Представим, пользователь спрашивает:
"Какая погода в Казани? Нужно ли брать зонт?"

  1. LLM анализирует запрос → понимает, что нужно получить погоду
  2. Находит зарегистрированный инструмент getWeather
  3. Формирует вызов:
    { "tool": "getWeather", "parameters": { "city": "Казань" } }
    
  4. Отправляет на /tool
  5. Сервер вызывает функцию, получает данные
  6. Возвращает:
    {
      "result": {
        "city": "Казань",
        "temp": 14,
        "description": "небольшой дождь"
      },
      "error": null
    }
    
  7. LLM получает результат и отвечает:
    "В Казани 14°C и небольшой дождь. Возьмите зонт."

Упражнение: создайте свой первый инструмент

Добавьте в свой MCP-сервер новый инструмент — getTime, который возвращает текущее время в заданном часовом поясе.

Шаги:

  1. Опишите инструмент в /capabilities:
    • name: getTime
    • description: "Возвращает текущее время в указанном часовом поясе, например 'Europe/Moscow'"
    • parameters: объект с обязательным полем timezone (строка)
  2. Создайте асинхронную функцию getTime(params):
    • Используйте Intl.DateTimeFormat для получения времени
    • Возвращайте объект { result: { time: "15:30", timezone: "Europe/Moscow" }, error: null }
    • Обработайте ошибку, если часовой пояс не валиден
  3. Добавьте обработку в /tool

Проверьте: отправьте POST-запрос на /tool с телом:

{ "tool": "getTime", "parameters": { "timezone": "Asia/Novosibirsk" } }

Подводим итоги

Сегодня мы:

  • Познакомились с понятием инструмента (tool) — функции, которую может вызвать LLM
  • Изучили формат описания инструмента в JSON для /capabilities
  • Реализовали асинхронную функцию с валидацией и обработкой ошибок
  • Настроили обработку вызова через эндпоинт /tool
  • Вернули результат в стандартизированном формате: { result, error }

Теперь ваш агент может не только думать — он может действовать.

А в следующей теме мы расширим эти навыки: создадим инструменты для работы с базой данных, генерации PDF и вызова внутренних API.

Вы уже видите, как из простых кирпичиков строится мощный ИИ-агент?

Давайте продолжим — впереди ещё больше возможностей. 🚀