Как работает function calling: инструменты внутри запроса

Function calling (или tool use) — это механизм, встроенный в API языковой модели. Ты передаёшь описания инструментов прямо в теле API-запроса. Модель решает, вызвать ли инструмент, и возвращает структурированный блок с именем функции и аргументами. Твой код исполняет функцию и отправляет результат обратно. Всё происходит в рамках одного приложения.

┌──────────────────────────────────────────────────────────────┐
│                      Твоё приложение                         │
│                                                              │
│  ┌─────────────┐    API request       ┌──────────────────┐  │
│  │             │  (messages + tools)  │                  │  │
│  │  Твой код   │ ──────────────────▶  │   Claude API     │  │
│  │             │                      │   (LLM)          │  │
│  │  tool_use   │ ◀────────────────── │                  │  │
│  │  block      │  response            └──────────────────┘  │
│  │             │                                            │
│  │  execute()  │  ← функция вызывается прямо здесь         │
│  │             │                                            │
│  │  tool_result│  API request (+ tool_result)               │
│  │             │ ──────────────────▶  LLM продолжает        │
│  └─────────────┘                                            │
└──────────────────────────────────────────────────────────────┘

Всё внутри одного процесса. Никаких внешних протоколов.

Ключевые свойства function calling:

Function Calling — что это значит на практике
Инструменты определяются в коде приложения и передаются в каждом запросе
Выполнение инструмента — просто вызов функции Python/JS внутри того же процесса
Никакого дополнительного транспорта: нет stdio, нет HTTP, нет subprocess
Список инструментов фиксирован на момент запроса (статическое объявление)
Инструменты тесно связаны с приложением — нельзя переиспользовать в другом проекте без копирования кода
Одна кодовая база: логика агента, инструменты и бизнес-логика в одном месте
MCP — что это значит на практике
Инструменты живут в отдельном процессе (MCP-сервере), клиент открывает к нему соединение
Выполнение инструмента — JSON-RPC запрос через stdio или HTTP+SSE к другому процессу
Протокол: initialize → list_tools → call_tool — фиксированный контракт
Список инструментов обнаруживается динамически через tools/list при подключении
Один MCP-сервер подключается к любому MCP-совместимому хосту (Claude Desktop, LangChain, свой агент)
Чёткая граница: клиентская часть (агент) и серверная (инструменты) разделены

Как работает MCP: инструменты за пределами приложения

MCP добавляет ещё один слой между приложением и исполнением инструмента. Вместо прямого вызова функции — JSON-RPC запрос к внешнему процессу. Этот процесс — MCP-сервер — управляет инструментами независимо.

┌─────────────────────────────┐         ┌──────────────────────────┐
│       Твоё приложение       │         │      MCP-сервер           │
│                             │  stdio  │  (отдельный процесс)     │
│  ┌──────────┐  ┌─────────┐  │ или SSE │                          │
│  │ LLM /    │  │   MCP   │  │ ◀─────▶ │  Tool Registry           │
│  │ Claude   │  │ Client  │  │ JSON-   │  ├── read_file()         │
│  │   API    │  │         │  │  RPC    │  ├── query_db()          │
│  └──────────┘  └─────────┘  │         │  └── http_get()          │
│       ↑             ↓       │         │                          │
│    tool_use    call_tool     │         │  Изолированный процесс:  │
│    response    request       │         │  свои зависимости,       │
│                             │         │  своя конфигурация,      │
└─────────────────────────────┘         │  своя команда            │
                                        └──────────────────────────┘

Один MCP-сервер → любой MCP-хост (Claude Desktop, VS Code, твой агент, …)

Важный нюанс: когда агент вызывает инструмент через MCP, модель по-прежнему использует стандартный механизм tool use API. MCP-клиент на стороне приложения перехватывает запрос, превращает его в JSON-RPC вызов к серверу и возвращает результат как tool_result. LLM об этом слое ничего не знает — для неё это обычный инструмент.

Детальное сравнение по ключевым критериям

100%
колёсико — масштаб  ·  зажать и тянуть — перемещение
FUNCTION CALLING MCP Одно приложение LLM Claude API tool_use block Agent Code Python / JS tool_result TOOLS (встроены в приложение) search_web() run_sql() read_file() прямой вызов функции — нет overhead тесно связаны с приложением API Request messages: [{...}] tools: [ {name: "search_web", inputSchema: ...}, {name: "run_sql", ...} ] ← объявляется в каждом запросе Твоё приложение LLM tool_use block MCP Client ClientSession stdio / HTTP+SSE JSON-RPC 2.0 MCP Server (отдельный процесс) initialize / list_tools tools/call dispatcher TOOL REGISTRY search_web() run_sql() read_file() свои зависимости свои логи, своя команда переиспользуется везде Claude Desktop · VS Code LangChain · твой агент overhead: ~1–5 ms (stdio)

Теперь сравним по конкретным критериям:

Критерий Function Calling MCP
Развёртывание Часть приложения, один процесс Отдельный процесс, независимый деплой
Задержка Минимальная — прямой вызов функции +1–5 ms stdio, +10–50 ms HTTP+SSE
Переиспользование Только в рамках одного приложения Любой MCP-совместимый хост
Обнаружение инструментов Статическое — объявляется в коде Динамическое — tools/list при подключении
Изоляция Нет — один процесс, общая память Да — отдельный процесс, своё окружение
Отладка Проще — всё в одном месте, один стектрейс Сложнее — два процесса, разные логи
Командная модель Одна команда владеет всем Разные команды владеют разными серверами
Версионирование Вместе с приложением Независимо — сервер v2 без изменений клиента
Состояние (state) Внутри функций (или глобальное) В процессе сервера (соединение с БД, сессия браузера)
Зависимости Общие с приложением (конфликты версий) Изолированные — сервер устанавливает свои
Порог входа Меньше — 10 строк кода для первого инструмента Больше — протокол, транспорт, subprocess
Поддержка IDE Стандартный Python/JS tooling MCP Inspector, Claude Desktop, VS Code MCP

Когда выбирать function calling

Function calling — правильный выбор, когда инструменты являются частью бизнес-логики приложения и не предназначены для переиспользования вне него.

Выбирай function calling, если:
Пишешь одно приложение, одна команда, один LLM-провайдер
Инструменты тесно завязаны на бизнес-логику (domain-specific)
Важна минимальная задержка — каждая миллисекунда на счету
Прототипирование: хочешь проверить идею за час
Инструменты уже реализованы как функции в том же проекте
Нет требования работать с Claude Desktop или другими хостами
Нужна полная трассировка в одном месте для дебага
Выбирай MCP, если:
Инструменты нужны в нескольких приложениях или у нескольких команд
Хочешь подключить готовый сервер (filesystem, postgres, github)
Инструмент держит долгое состояние: соединение с БД, браузерную сессию
Инструменты на другом языке программирования или у другой команды
Нужно работать в Claude Desktop или VS Code Copilot
Хочешь независимо версионировать и деплоить инструменты
Инструменты требуют изолированных зависимостей (конфликт версий)
Задержка реже критична, чем кажется. Накладные расходы MCP через stdio — 1–5 ms на вызов. При типичном агентском цикле с несколькими tool calls это 5–25 ms суммарно. Сравни с 500–3000 ms на сам LLM-запрос. В большинстве агентских задач разница незаметна. Оптимизируй задержку только там, где это измеримо критично.

Одна задача, два подхода: код рядом

Пример: инструмент поиска в базе данных. Посмотрим на реализацию через function calling и через MCP — и почувствуем разницу в сложности и гибкости.

Function Calling
import sqlite3
import anthropic

client = anthropic.Anthropic()

# Инструмент — просто функция в коде
def search_products(query: str, limit: int = 10) -> str:
    conn = sqlite3.connect("shop.db")
    rows = conn.execute(
        "SELECT name, price FROM products "
        "WHERE name LIKE ? LIMIT ?",
        (f"%{query}%", limit)
    ).fetchall()
    conn.close()
    return str(rows)

# Описание передаётся в каждом запросе
tools = [{
    "name": "search_products",
    "description": "Поиск товаров по названию",
    "input_schema": {
        "type": "object",
        "properties": {
            "query": {"type": "string"},
            "limit": {"type": "integer", "default": 10}
        },
        "required": ["query"]
    }
}]

resp = client.messages.create(
    model="claude-opus-4-6",
    max_tokens=1024,
    tools=tools,
    messages=[{"role": "user",
               "content": "Найди ноутбуки до 50000р"}]
)

# Выполняем, если модель решила вызвать
if resp.stop_reason == "tool_use":
    for block in resp.content:
        if block.type == "tool_use":
            result = search_products(**block.input)
            # Отправляем результат обратно
            ...
MCP Server
# === mcp_server.py (отдельный процесс) ===
import asyncio
import sqlite3
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types

server = Server("shop-tools")

@server.list_tools()
async def list_tools():
    return [types.Tool(
        name="search_products",
        description="Поиск товаров по названию",
        inputSchema={
            "type": "object",
            "properties": {
                "query": {"type": "string"},
                "limit": {"type": "integer",
                          "default": 10}
            },
            "required": ["query"]
        }
    )]

@server.call_tool()
async def call_tool(name, arguments):
    if name == "search_products":
        conn = sqlite3.connect("shop.db")
        rows = conn.execute(
            "SELECT name, price FROM products "
            "WHERE name LIKE ? LIMIT ?",
            (f"%{arguments['query']}%",
             arguments.get("limit", 10))
        ).fetchall()
        conn.close()
        return [types.TextContent(
            type="text", text=str(rows)
        )]

async def main():
    async with stdio_server() as (r, w):
        await server.run(
            r, w,
            server.create_initialization_options()
        )

asyncio.run(main())

Function calling — меньше кода, нет протокола. MCP-сервер — больше кода, но теперь search_products работает из Claude Desktop, VS Code и любого другого MCP-хоста без изменений.

Гибридный подход: MCP + function calling вместе

Оба подхода не противоречат друг другу и часто используются одновременно. Типичный сценарий: агент использует MCP для доступа к инфраструктуре (файлы, БД, внешние API), а через function calling реализует domain-специфичную логику, которую не нужно переиспользовать.

"""
Гибридный агент:
- MCP для инфраструктурных инструментов (файлы, github)
- Function calling для бизнес-логики (форматирование отчёта, валидация)
"""
import anthropic
from mcp import ClientSession
from mcp.client.stdio import stdio_client

# Бизнес-инструменты — inline через function calling
BUSINESS_TOOLS = [
    {
        "name": "format_report",
        "description": "Форматирует данные в Markdown-отчёт по шаблону компании",
        "input_schema": {
            "type": "object",
            "properties": {
                "title":   {"type": "string"},
                "data":    {"type": "string"},
                "section": {"type": "string", "enum": ["weekly", "monthly", "quarterly"]}
            },
            "required": ["title", "data", "section"]
        }
    }
]

def format_report(title: str, data: str, section: str) -> str:
    # Логика форматирования специфична для этого приложения
    return f"# {title}\n\n**Период:** {section}\n\n{data}"


async def run_hybrid_agent(task: str):
    # Подключаемся к MCP для инфраструктурных инструментов
    async with stdio_client(
        command="python", args=["mcp_server.py"]
    ) as (read, write):
        async with ClientSession(read, write) as mcp_session:
            await mcp_session.initialize()

            # Получаем MCP-инструменты динамически
            mcp_tools_result = await mcp_session.list_tools()
            mcp_tools = [
                {
                    "name": t.name,
                    "description": t.description,
                    "input_schema": t.inputSchema
                }
                for t in mcp_tools_result.tools
            ]

            # Объединяем: MCP + business tools
            all_tools = mcp_tools + BUSINESS_TOOLS

            client = anthropic.Anthropic()
            messages = [{"role": "user", "content": task}]

            while True:
                resp = client.messages.create(
                    model="claude-opus-4-6",
                    max_tokens=4096,
                    tools=all_tools,
                    messages=messages,
                )
                messages.append({"role": "assistant", "content": resp.content})

                if resp.stop_reason != "tool_use":
                    break

                tool_results = []
                for block in resp.content:
                    if block.type != "tool_use":
                        continue

                    if block.name == "format_report":
                        # Бизнес-инструмент — вызываем локально
                        result = format_report(**block.input)
                    else:
                        # MCP-инструмент — вызываем через протокол
                        mcp_result = await mcp_session.call_tool(
                            block.name, block.input
                        )
                        result = mcp_result.content[0].text

                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": result,
                    })

                messages.append({"role": "user", "content": tool_results})

            return resp.content[0].text
Правило разделения в гибридном подходе: MCP — для инструментов, которые будут переиспользованы или держат внешнее состояние. Function calling — для логики, специфичной для этого приложения и этой команды. Граница проходит по вопросу: «Нужен ли этот инструмент кому-то ещё кроме этого приложения?»

Алгоритм принятия решения

Конкретный алгоритм выбора — задай себе эти вопросы последовательно:

Алгоритм выбора: MCP или function calling
1. Нужно ли этот инструмент использовать из Claude Desktop, VS Code или другого MCP-хоста?
Да
→ MCP (других вариантов нет)
Нет
Переходи к вопросу 2
2. Будет ли этот инструмент нужен более чем в одном приложении или команде?
Да
→ MCP (переиспользование)
Нет / не знаю
Переходи к вопросу 3
3. Инструмент держит постоянное состояние (соединение с БД, браузерная сессия, кэш)?
Да
→ MCP (state живёт в процессе)
Нет
Переходи к вопросу 4
4. Инструмент разрабатывает другая команда или он на другом языке программирования?
Да
→ MCP (чёткий контракт через протокол)
Нет
Переходи к вопросу 5
5. Ты на стадии прототипа, или инструмент специфичен только для этой бизнес-логики?
Да
→ Function Calling (проще, быстрее)
Нет
→ Начни с FC, рефактори в MCP при первом переиспользовании
Рефакторинг FC → MCP — это нормально. Начинать с function calling и мигрировать в MCP — распространённая практика. API инструмента (имя, параметры) остаётся тем же. Меняется только то, где живёт код: внутри приложения → в отдельном процессе. Тесты на уровне инструмента переиспользуются без изменений.

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

MCP для всего — включая однострочные утилиты
Завернуть datetime.now().isoformat() в MCP-сервер — это 200 строк инфраструктуры ради 1 строки логики. Накладные расходы на subprocess, протокол и subprocess management несоразмерны задаче.
Исправление: MCP — для инструментов с реальным состоянием, зависимостями или потребностью в переиспользовании. Простые утилиты — в function calling.
Function calling для всего — и потом переписывать
Команда пишет 15 инструментов в function calling. Потом оказывается, что нужно подключить их в три разных приложения. Код копируется, расходится, каждое приложение чинит баги отдельно.
Исправление: задай вопрос «нужен ли этот инструмент где-то ещё?» на этапе проектирования. Если есть хоть один шанс — делай MCP сразу.
Смешивать инфраструктурные и бизнес-инструменты в одном MCP-сервере
В одном сервере живут read_file, send_email и calculate_roi. Первые два — переиспользуемые инфраструктурные. Последний — специфичен для этого продукта. Менять версию сервера из-за бизнес-логики обновляет и инфраструктурные инструменты у всех клиентов.
Исправление: инфраструктура — отдельный MCP-сервер, бизнес-логика — function calling или отдельный сервер с явным версионированием.
Оптимизировать задержку MCP там, где она не важна
Разработчик переписывает MCP-сервер на function calling, сэкономив 3 ms на вызов. При этом LLM-запрос занимает 1200 ms. Суммарное ускорение агентского цикла — 0.2%.
Исправление: меряй реальную задержку системы end-to-end. MCP добавляет значимый overhead только в системах реального времени с latency < 100 ms.

Шпаргалка

MCP vs Function Calling — краткая выжимка
  • Function calling: инструменты в теле запроса → прямой вызов функции → результат в tool_result
  • MCP: отдельный процесс → JSON-RPC через stdio/SSE → tool registry → результат через протокол
  • Задержка: FC = 0 ms overhead; MCP stdio = 1–5 ms; MCP HTTP = 10–50 ms; LLM = 500–3000 ms
  • Переиспользование: FC — только в одном приложении; MCP — любой MCP-хост
  • Выбирай FC: прототип, domain-specific логика, одна команда, низкая задержка критична
  • Выбирай MCP: Claude Desktop, переиспользование, другая команда, долгое состояние, изолированные зависимости
  • Гибрид: MCP для инфраструктуры (файлы, БД, API), FC для бизнес-логики — работают вместе
  • Вопрос выбора: «Нужен ли этот инструмент кому-то ещё?» — если да, MCP
  • Миграция FC → MCP: нормальная практика; API инструмента не меняется, меняется где живёт код

Практика

Задание 1. Возьми любой рабочий агент с 3–5 инструментами через function calling. Пройди по алгоритму выбора для каждого инструмента. Какие инструменты остались бы как FC? Какие стоит вынести в MCP? Напиши список с обоснованием для каждого инструмента (1–2 предложения).
Задание 2. Реализуй инструмент get_weather(city: str) → str двумя способами: как function calling и как MCP-сервер. Напиши тест, который вызывает оба и сравнивает задержку на 100 итерациях. На сколько MCP медленнее? При каком количестве tool calls в одной сессии разница перестаёт быть значимой?
Задание 3 (продвинутый). Реализуй гибридный агент: MCP-сервер предоставляет инструменты для работы с файловой системой, а через function calling реализован инструмент analyze_code(code: str) → dict, который возвращает метрики: количество функций, классов, строк, цикломатическую сложность. Задача агенту: «Найди все Python-файлы в директории, проанализируй каждый и сохрани отчёт с топ-5 самых сложных файлов».