Как работает 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:
Как работает 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 об этом слое ничего не знает —
для неё это обычный инструмент.
Детальное сравнение по ключевым критериям
Теперь сравним по конкретным критериям:
| Критерий | 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 и через MCP — и почувствуем разницу в сложности и гибкости.
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.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
Алгоритм принятия решения
Конкретный алгоритм выбора — задай себе эти вопросы последовательно:
Типичные ошибки при выборе
datetime.now().isoformat() в MCP-сервер —
это 200 строк инфраструктуры ради 1 строки логики.
Накладные расходы на subprocess, протокол и subprocess management
несоразмерны задаче.
read_file, send_email
и calculate_roi. Первые два — переиспользуемые инфраструктурные.
Последний — специфичен для этого продукта. Менять версию сервера из-за
бизнес-логики обновляет и инфраструктурные инструменты у всех клиентов.
Шпаргалка
- 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 инструмента не меняется, меняется где живёт код
Практика
get_weather(city: str) → str
двумя способами: как function calling и как MCP-сервер.
Напиши тест, который вызывает оба и сравнивает задержку на 100 итерациях.
На сколько MCP медленнее? При каком количестве tool calls в одной сессии
разница перестаёт быть значимой?
analyze_code(code: str) → dict, который возвращает метрики:
количество функций, классов, строк, цикломатическую сложность.
Задача агенту: «Найди все Python-файлы в директории, проанализируй каждый
и сохрани отчёт с топ-5 самых сложных файлов».