Function Calling и Tool Use в LLM-агентах: руководство по архитектуре и паттернам

Разбираем архитектуру Function Calling и Tool Use в LLM-агентах: от агентного цикла до паттернов маршрутизации и параллельных вызовов. Примеры на Python для OpenAI и Claude API, безопасность и оптимизация.

Введение: почему LLM без инструментов — это только половина дела

Давайте начистоту: большие языковые модели умеют работать с текстом просто блестяще. Но вот в чём загвоздка — они только с текстом и работают. Спросите у LLM текущий курс доллара, и она выдаст вам что-то правдоподобное, но вполне возможно устаревшее. Попросите выполнить SQL-запрос — получите красивый текст запроса, но не результат его выполнения. Отправить письмо? Нет, только текст письма.

Именно здесь на сцену выходят Function Calling и Tool Use — механизмы, которые превращают LLM из (пусть и гениального) генератора текста в полноценного агента, способного взаимодействовать с реальным миром.

В этом руководстве мы разберём архитектуру Function Calling от основ до продвинутых паттернов, сравним реализации в API OpenAI и Anthropic Claude, и — что самое ценное — создадим работающие примеры агентов на Python. Если вы хотите выйти за рамки простого промпт-инжиниринга и строить по-настоящему интерактивные AI-системы, вы в правильном месте.

Архитектура Function Calling: как это устроено под капотом

Основной принцип

Первое, что нужно понять (и это важно!): LLM не выполняет функции напрямую. Модель лишь генерирует структурированный JSON, в котором говорит: «Вызови вот эту функцию с такими-то параметрами». А дальше ваше приложение берёт этот JSON и выполняет реальную работу.

Честно говоря, это гениальное архитектурное решение. Модель отвечает за интеллект — понимание намерения пользователя и выбор нужного инструмента. А вы отвечаете за безопасность и контроль выполнения.

Весь процесс укладывается в пять шагов:

  1. Определение инструментов — вы описываете доступные функции в формате JSON Schema: имя, описание, параметры
  2. Запрос к модели — сообщение пользователя отправляется вместе с описанием доступных инструментов
  3. Решение модели — LLM анализирует запрос и решает, какой инструмент вызвать и с какими аргументами (или не вызывать вовсе)
  4. Выполнение функции — ваше приложение получает структурированный вызов и выполняет реальную функцию
  5. Финальный ответ — результат передаётся обратно модели, которая формирует человекочитаемый ответ

Классификация инструментов

Не все инструменты одинаковы с точки зрения рисков. Я обычно делю их на три категории:

  • Доступ к данным (Data Access) — чтение из баз данных, API, файловых систем. Самая безопасная категория — операции только на чтение, ничего не сломаете
  • Вычисления (Computation) — математические расчёты, преобразование данных. Тоже без побочных эффектов
  • Действия (Actions) — отправка сообщений, запись в базу, создание файлов. Вот тут нужно быть осторожнее всего

Для действий настоятельно рекомендуется паттерн «прочитай перед записью» — всегда проверяйте текущее состояние системы перед тем, как что-то менять. Поверьте, это избавит вас от многих неприятных сюрпризов.

Реализация в API OpenAI: практическое руководство

Определение функций

OpenAI использует параметр tools в Chat Completions API. Каждый инструмент описывается JSON-схемой. Начнём с классического примера — погодного агента:

from openai import OpenAI
import json

client = OpenAI()

# Определяем инструменты
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Получить текущую погоду для указанного города. Используйте эту функцию, когда пользователь спрашивает о погоде.",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "Название города, например: Москва, Санкт-Петербург"
                    },
                    "units": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "Единицы измерения температуры"
                    }
                },
                "required": ["city"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "search_database",
            "description": "Поиск информации о продуктах в базе данных по ключевым словам.",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "Поисковый запрос"
                    },
                    "category": {
                        "type": "string",
                        "description": "Категория для фильтрации результатов"
                    },
                    "limit": {
                        "type": "integer",
                        "description": "Максимальное количество результатов",
                        "default": 10
                    }
                },
                "required": ["query"]
            }
        }
    }
]

Агентный цикл обработки

А вот это, пожалуй, самая важная часть. Агентный цикл (agentic loop) — это паттерн, при котором модель может совершать несколько вызовов инструментов подряд, пока не соберёт достаточно информации для финального ответа. По сути, это то, что делает агента агентом:

def run_agent(user_message: str, tools: list) -> str:
    messages = [
        {"role": "system", "content": "Вы — полезный ассистент с доступом к инструментам."},
        {"role": "user", "content": user_message}
    ]

    while True:
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=tools,
            tool_choice="auto"
        )

        message = response.choices[0].message

        # Если модель решила не вызывать инструменты — возвращаем ответ
        if not message.tool_calls:
            return message.content

        # Добавляем ответ модели в историю
        messages.append(message)

        # Обрабатываем каждый вызов инструмента
        for tool_call in message.tool_calls:
            function_name = tool_call.function.name
            arguments = json.loads(tool_call.function.arguments)

            # Выполняем функцию
            result = execute_function(function_name, arguments)

            # Добавляем результат в историю
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": json.dumps(result, ensure_ascii=False)
            })

def execute_function(name: str, args: dict) -> dict:
    """Маршрутизатор вызовов функций."""
    functions = {
        "get_weather": get_weather_data,
        "search_database": search_products,
    }
    if name in functions:
        return functions[name](**args)
    return {"error": f"Неизвестная функция: {name}"}

Строгий режим (Strict Mode)

Для production-систем стоит обратить внимание на строгий режим Function Calling в OpenAI. Он гарантирует, что модель генерирует аргументы, точно соответствующие вашей JSON-схеме. Никаких сюрпризов в виде неожиданных полей или пропущенных обязательных параметров:

tools = [
    {
        "type": "function",
        "function": {
            "name": "create_order",
            "strict": True,  # Включаем строгий режим
            "parameters": {
                "type": "object",
                "properties": {
                    "product_id": {"type": "string"},
                    "quantity": {"type": "integer"},
                    "shipping_address": {
                        "type": "object",
                        "properties": {
                            "street": {"type": "string"},
                            "city": {"type": "string"},
                            "postal_code": {"type": "string"}
                        },
                        "required": ["street", "city", "postal_code"],
                        "additionalProperties": False
                    }
                },
                "required": ["product_id", "quantity", "shipping_address"],
                "additionalProperties": False
            }
        }
    }
]

Реализация в API Anthropic Claude: Tool Use

Особенности подхода Claude

Anthropic пошли немного другим путём и используют термин «Tool Use». Концептуально всё похоже на OpenAI, но есть свои нюансы — в частности, структуры tool_use и tool_result в Messages API. Давайте посмотрим на примере финансового агента:

import anthropic
import json

client = anthropic.Anthropic()

# Определяем инструменты для Claude
tools = [
    {
        "name": "get_stock_price",
        "description": "Получает текущую цену акций по тикеру. Используйте, когда пользователь спрашивает о стоимости акций компании.",
        "input_schema": {
            "type": "object",
            "properties": {
                "ticker": {
                    "type": "string",
                    "description": "Биржевой тикер, например: AAPL, GOOGL, MSFT"
                },
                "exchange": {
                    "type": "string",
                    "description": "Биржа: NYSE, NASDAQ, MOEX",
                    "enum": ["NYSE", "NASDAQ", "MOEX"]
                }
            },
            "required": ["ticker"]
        }
    },
    {
        "name": "calculate_portfolio_return",
        "description": "Рассчитывает доходность портфеля за указанный период.",
        "input_schema": {
            "type": "object",
            "properties": {
                "holdings": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "ticker": {"type": "string"},
                            "shares": {"type": "number"},
                            "purchase_price": {"type": "number"}
                        },
                        "required": ["ticker", "shares", "purchase_price"]
                    },
                    "description": "Список позиций в портфеле"
                },
                "period_days": {
                    "type": "integer",
                    "description": "Период в днях для расчёта доходности"
                }
            },
            "required": ["holdings", "period_days"]
        }
    }
]

Агентный цикл для Claude

Агентный цикл для Claude выглядит чуть иначе. Главное отличие — Claude использует блоки контента (content blocks) разных типов, и нужно правильно обрабатывать каждый из них:

def run_claude_agent(user_message: str, tools: list) -> str:
    messages = [{"role": "user", "content": user_message}]

    while True:
        response = client.messages.create(
            model="claude-sonnet-4-5-20250929",
            max_tokens=4096,
            system="Вы — финансовый аналитик с доступом к биржевым данным.",
            tools=tools,
            messages=messages
        )

        # Проверяем, нужно ли выполнять инструменты
        if response.stop_reason == "end_turn":
            # Извлекаем текстовый ответ
            for block in response.content:
                if block.type == "text":
                    return block.text

        # Добавляем ответ ассистента в историю
        messages.append({"role": "assistant", "content": response.content})

        # Обрабатываем вызовы инструментов
        tool_results = []
        for block in response.content:
            if block.type == "tool_use":
                result = execute_tool(block.name, block.input)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": json.dumps(result, ensure_ascii=False)
                })

        # Передаём результаты обратно модели
        messages.append({"role": "user", "content": tool_results})

def execute_tool(name: str, input_data: dict) -> dict:
    """Выполняет инструмент и возвращает результат."""
    if name == "get_stock_price":
        return fetch_stock_price(**input_data)
    elif name == "calculate_portfolio_return":
        return calc_return(**input_data)
    return {"error": f"Неизвестный инструмент: {name}"}

Structured Outputs в Claude

С 2025 года Anthropic поддерживает строгие структурированные выходы. Это особенно полезно в агентных workflow, где нужна и надёжность вызовов инструментов, и предсказуемость формата ответов. Комбинация строгого Tool Use с Structured Outputs — это, по моему опыту, одна из самых недооценённых возможностей:

# Строгий Tool Use: валидация параметров вызова инструментов
response = client.messages.create(
    model="claude-sonnet-4-5-20250929",
    max_tokens=1024,
    tools=tools,
    messages=messages,
    headers={
        "anthropic-beta": "advanced-tool-use-2025-11-20"
    }
)

Паттерны проектирования агентов с инструментами

Итак, мы разобрались с базовой механикой. Теперь поговорим о том, как правильно организовать взаимодействие агента с инструментами. За последний год сложилось несколько проверенных паттернов.

1. Паттерн «Маршрутизатор» (Router Pattern)

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

router_tools = [
    {
        "name": "handle_billing",
        "description": "Обработка вопросов по биллингу, оплате, счетам и подпискам",
        "input_schema": {
            "type": "object",
            "properties": {
                "customer_id": {"type": "string"},
                "issue_type": {
                    "type": "string",
                    "enum": ["payment_failed", "refund_request", "plan_change", "invoice_question"]
                },
                "details": {"type": "string"}
            },
            "required": ["customer_id", "issue_type"]
        }
    },
    {
        "name": "handle_technical",
        "description": "Обработка технических вопросов, ошибок и проблем с продуктом",
        "input_schema": {
            "type": "object",
            "properties": {
                "customer_id": {"type": "string"},
                "error_code": {"type": "string"},
                "description": {"type": "string"}
            },
            "required": ["customer_id", "description"]
        }
    },
    {
        "name": "escalate_to_human",
        "description": "Передать запрос живому оператору, если проблема слишком сложна или клиент явно просит об этом",
        "input_schema": {
            "type": "object",
            "properties": {
                "customer_id": {"type": "string"},
                "reason": {"type": "string"},
                "priority": {
                    "type": "string",
                    "enum": ["low", "medium", "high", "critical"]
                }
            },
            "required": ["customer_id", "reason", "priority"]
        }
    }
]

2. Паттерн «Цепочка инструментов» (Tool Chaining)

Этот паттерн — настоящая рабочая лошадка для сложных задач. Модель последовательно вызывает несколько инструментов, где результат одного становится входом для следующего. Представьте себе конвейер: запросить данные → посчитать статистику → построить график → написать отчёт:

# Пример: агент для анализа данных
# Шаг 1: Модель вызывает query_database для получения данных
# Шаг 2: Используя полученные данные, вызывает calculate_statistics
# Шаг 3: На основе статистики вызывает generate_chart
# Шаг 4: Формирует итоговый отчёт с текстом и визуализациями

analysis_tools = [
    {
        "name": "query_database",
        "description": "Выполняет SQL-запрос к аналитической базе данных и возвращает результаты.",
        "input_schema": {
            "type": "object",
            "properties": {
                "sql": {"type": "string", "description": "SQL-запрос для выполнения"},
                "database": {"type": "string", "enum": ["analytics", "warehouse", "production_readonly"]}
            },
            "required": ["sql", "database"]
        }
    },
    {
        "name": "calculate_statistics",
        "description": "Выполняет статистические расчёты над числовым набором данных.",
        "input_schema": {
            "type": "object",
            "properties": {
                "data": {
                    "type": "array",
                    "items": {"type": "number"},
                    "description": "Массив числовых значений для анализа"
                },
                "operations": {
                    "type": "array",
                    "items": {
                        "type": "string",
                        "enum": ["mean", "median", "std_dev", "percentile_95", "trend"]
                    }
                }
            },
            "required": ["data", "operations"]
        }
    },
    {
        "name": "generate_chart",
        "description": "Создаёт визуализацию данных в указанном формате.",
        "input_schema": {
            "type": "object",
            "properties": {
                "chart_type": {"type": "string", "enum": ["line", "bar", "pie", "scatter"]},
                "title": {"type": "string"},
                "data_points": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "label": {"type": "string"},
                            "value": {"type": "number"}
                        }
                    }
                }
            },
            "required": ["chart_type", "data_points"]
        }
    }
]

3. Паттерн «Параллельные вызовы» (Parallel Tool Calls)

Зачем ждать, если можно делать несколько дел одновременно? Современные модели умеют запрашивать выполнение нескольких инструментов параллельно. Типичный пример: пользователь спрашивает «Сравни погоду в Москве и Лондоне» — и модель отправляет два запроса сразу.

# Модель может вернуть несколько tool_calls одновременно
# Например, при запросе "Сравни погоду в Москве и Лондоне":
# tool_call_1: get_weather(city="Москва")
# tool_call_2: get_weather(city="Лондон")
# Оба вызова выполняются параллельно

import asyncio

async def execute_parallel_tools(tool_calls: list) -> list:
    """Параллельное выполнение независимых инструментов."""
    tasks = []
    for call in tool_calls:
        task = asyncio.create_task(
            async_execute_function(call.function.name,
                                   json.loads(call.function.arguments))
        )
        tasks.append((call.id, task))

    results = []
    for call_id, task in tasks:
        result = await task
        results.append({
            "role": "tool",
            "tool_call_id": call_id,
            "content": json.dumps(result, ensure_ascii=False)
        })
    return results

4. Паттерн «Самоконтроль» (Self-Validation)

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

validation_tools = [
    {
        "name": "execute_action",
        "description": "Выполняет действие в системе.",
        "input_schema": {
            "type": "object",
            "properties": {
                "action": {"type": "string"},
                "parameters": {"type": "object"}
            },
            "required": ["action"]
        }
    },
    {
        "name": "verify_result",
        "description": "Проверяет результат ранее выполненного действия, чтобы убедиться в корректности.",
        "input_schema": {
            "type": "object",
            "properties": {
                "action_id": {"type": "string"},
                "expected_state": {"type": "object"}
            },
            "required": ["action_id", "expected_state"]
        }
    },
    {
        "name": "rollback_action",
        "description": "Откатывает ранее выполненное действие в случае обнаружения ошибки.",
        "input_schema": {
            "type": "object",
            "properties": {
                "action_id": {"type": "string"},
                "reason": {"type": "string"}
            },
            "required": ["action_id", "reason"]
        }
    }
]

Лучшие практики описания инструментов

Качество описаний решает всё

Вот что многие упускают: описания инструментов — это, по сути, промпт-инжиниринг. Чем точнее и понятнее вы опишете свои функции, тем лучше модель будет их использовать. Это не преувеличение — плохие описания буквально ломают поведение агента.

Ключевые рекомендации:

  • Будьте конкретны — описание должно точно объяснять, что делает функция, когда её использовать и каких результатов ожидать
  • Указывайте ограничения — если функция работает только с определёнными форматами или имеет лимиты, напишите об этом
  • Приводите примеры — для строковых параметров укажите примеры допустимых значений
  • Разделяйте обязательные и необязательные параметры — используйте поле required
  • Помните о токенах — описания инструментов включаются в каждый запрос к API. Будьте лаконичны, но информативны

Антипаттерны, которых стоит избегать

На практике встречается несколько типичных ошибок:

  • Слишком общие описания — «Выполняет действие» не даёт модели ни одной зацепки. Она просто не поймёт, когда использовать этот инструмент
  • Перегруженные функции — одна функция, которая делает слишком много, сбивает модель с толку. Лучше несколько специализированных инструментов
  • Параметры без описаний — каждый параметр должен иметь чёткое описание, иначе готовьтесь к некорректным значениям
  • Конфликтующие инструменты — два инструмента с похожими описаниями — верный путь к непредсказуемому поведению

Обработка ошибок и безопасность

Многоуровневая обработка ошибок

В production-среде без надёжной обработки ошибок далеко не уедешь. Вот пример, который покрывает основные сценарии — от валидации входных данных до санитизации выходных:

import logging
from typing import Any

logger = logging.getLogger(__name__)

class ToolExecutionError(Exception):
    """Ошибка выполнения инструмента."""
    def __init__(self, tool_name: str, message: str, retryable: bool = False):
        self.tool_name = tool_name
        self.retryable = retryable
        super().__init__(message)

def safe_execute_tool(name: str, args: dict, max_retries: int = 2) -> dict:
    """Безопасное выполнение инструмента с обработкой ошибок."""

    # 1. Валидация входных данных
    if not validate_input(name, args):
        return {
            "error": "Некорректные входные параметры",
            "details": f"Параметры для '{name}' не прошли валидацию"
        }

    # 2. Проверка прав доступа
    if not check_permissions(name, args):
        return {
            "error": "Недостаточно прав",
            "details": f"Отказано в доступе к инструменту '{name}'"
        }

    # 3. Выполнение с повторными попытками
    for attempt in range(max_retries + 1):
        try:
            result = execute_function(name, args)

            # 4. Санитизация выходных данных
            return sanitize_output(result)

        except ToolExecutionError as e:
            logger.warning(f"Ошибка инструмента {name} (попытка {attempt + 1}): {e}")
            if not e.retryable or attempt == max_retries:
                return {
                    "error": str(e),
                    "tool": name,
                    "suggestion": "Попробуйте изменить параметры запроса"
                }

        except Exception as e:
            logger.error(f"Непредвиденная ошибка {name}: {e}")
            return {
                "error": "Внутренняя ошибка сервиса",
                "tool": name
            }

def sanitize_output(result: Any) -> dict:
    """Удаляет чувствительные данные из результатов."""
    if isinstance(result, dict):
        sensitive_keys = {"password", "token", "secret", "api_key", "credit_card"}
        return {
            k: "***СКРЫТО***" if k.lower() in sensitive_keys else v
            for k, v in result.items()
        }
    return {"result": result}

Безопасность вызовов инструментов

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

  • Принцип наименьших привилегий — давайте агенту только те инструменты, которые нужны для конкретной задачи. Не больше
  • Валидация входных данных — всегда проверяйте параметры до выполнения. Модель может галлюцинировать, и это нормально — ваша задача это перехватить
  • Подтверждение опасных действий — удаление данных, финансовые операции, отправка сообщений — всё это должно требовать подтверждения пользователя
  • Аудит и логирование — записывайте каждый вызов инструмента. Это пригодится и для отладки, и для расследований
  • Rate limiting — ограничивайте частоту вызовов, чтобы агент не разорил ваш бюджет за один разговор

Продвинутые техники: Interleaved Thinking и динамическое управление

Interleaved Thinking

Одна из самых интересных возможностей современных моделей — «перемежающееся мышление». Суть в том, что модель может рассуждать между вызовами инструментов, а не просто механически выполнять один за другим. Это позволяет:

  • Анализировать промежуточные результаты перед следующим вызовом
  • Менять стратегию на лету, если данные оказались не такими, как ожидалось
  • Принимать более обоснованные решения о выборе следующего инструмента
  • Объяснять пользователю свою логику — делать процесс прозрачным

На практике это выглядит так: модель получает результат первого вызова, «думает» над ним (генерирует внутренний анализ), а затем решает — вызвать другой инструмент или уже можно давать финальный ответ.

Динамическое управление набором инструментов

В реальных системах вы вряд ли захотите давать всем пользователям доступ ко всем инструментам сразу. Набор может меняться в зависимости от роли пользователя, этапа диалога или контекста задачи:

class DynamicToolManager:
    """Управляет набором доступных инструментов на основе контекста."""

    def __init__(self):
        self.all_tools = {}
        self.tool_categories = {}

    def register_tool(self, tool_def: dict, category: str,
                      required_role: str = "user"):
        """Регистрирует инструмент с метаданными."""
        name = tool_def["name"]
        self.all_tools[name] = {
            "definition": tool_def,
            "category": category,
            "required_role": required_role,
            "call_count": 0
        }
        self.tool_categories.setdefault(category, []).append(name)

    def get_tools_for_context(self, user_role: str,
                               conversation_stage: str,
                               active_categories: list = None) -> list:
        """Возвращает инструменты, доступные в текущем контексте."""
        available = []
        for name, meta in self.all_tools.items():
            # Проверяем роль
            if not self._has_role(user_role, meta["required_role"]):
                continue
            # Проверяем категорию
            if active_categories and meta["category"] not in active_categories:
                continue
            available.append(meta["definition"])
        return available

    def _has_role(self, user_role: str, required_role: str) -> bool:
        role_hierarchy = {"admin": 3, "operator": 2, "user": 1, "guest": 0}
        return role_hierarchy.get(user_role, 0) >= role_hierarchy.get(required_role, 0)

# Использование
manager = DynamicToolManager()
manager.register_tool(billing_tool, category="billing", required_role="operator")
manager.register_tool(admin_tool, category="admin", required_role="admin")
manager.register_tool(search_tool, category="general", required_role="user")

# Получаем инструменты для текущего контекста
context_tools = manager.get_tools_for_context(
    user_role="operator",
    conversation_stage="resolution",
    active_categories=["billing", "general"]
)

Сравнение подходов: OpenAI vs Claude vs Open Source

Какую платформу выбрать? Зависит от ваших приоритетов. Вот краткое сравнение основных игроков.

OpenAI (GPT-4o, o3, o4-mini)

  • Формат: параметр tools в Chat Completions API
  • Строгий режим: полная поддержка через strict: true
  • Параллельные вызовы: нативная поддержка
  • Фишка: модели o-серии используют инструменты прямо внутри цепочки рассуждений, что заметно улучшает качество выбора
  • Structured Outputs: гарантированная валидность JSON

Anthropic Claude (Sonnet 4.5, Opus 4.6)

  • Формат: параметр tools в Messages API с input_schema
  • Строгий режим: через бета-заголовок advanced-tool-use
  • Параллельные вызовы: поддерживаются
  • Фишка: Extended Thinking позволяет модели «думать» между вызовами; есть Agent SDK для построения полноценных агентов
  • Бонус: встроенный веб-поиск и Computer Use

Open Source (LLaMA, Mistral, Qwen)

  • Формат: зависит от реализации, часто через специальные токены
  • Строгий режим: реализуется через библиотеки вроде Outlines или Instructor
  • Параллельные вызовы: поддержка пока ограничена
  • Главное преимущество: полный контроль, своя инфраструктура, никаких затрат на API
  • Инструменты развёртывания: vLLM, Ollama, TGI

Практический пример: полнофункциональный агент-ассистент

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

import anthropic
import json
from datetime import datetime, timezone

client = anthropic.Anthropic()

# Имитация базы данных задач
task_db = {}

def create_task(title: str, description: str, priority: str,
                assignee: str = None) -> dict:
    task_id = f"TASK-{len(task_db) + 1:04d}"
    task = {
        "id": task_id,
        "title": title,
        "description": description,
        "priority": priority,
        "assignee": assignee,
        "status": "open",
        "created_at": datetime.now(timezone.utc).isoformat()
    }
    task_db[task_id] = task
    return {"success": True, "task": task}

def list_tasks(status: str = None, assignee: str = None) -> dict:
    tasks = list(task_db.values())
    if status:
        tasks = [t for t in tasks if t["status"] == status]
    if assignee:
        tasks = [t for t in tasks if t["assignee"] == assignee]
    return {"tasks": tasks, "total": len(tasks)}

def update_task(task_id: str, status: str = None,
                priority: str = None) -> dict:
    if task_id not in task_db:
        return {"error": f"Задача {task_id} не найдена"}
    if status:
        task_db[task_id]["status"] = status
    if priority:
        task_db[task_id]["priority"] = priority
    return {"success": True, "task": task_db[task_id]}

# Определение инструментов
task_tools = [
    {
        "name": "create_task",
        "description": "Создаёт новую задачу в трекере. Используйте, когда пользователь хочет добавить новую задачу, баг или фичу.",
        "input_schema": {
            "type": "object",
            "properties": {
                "title": {"type": "string", "description": "Краткое название задачи"},
                "description": {"type": "string", "description": "Подробное описание"},
                "priority": {
                    "type": "string",
                    "enum": ["low", "medium", "high", "critical"],
                    "description": "Приоритет задачи"
                },
                "assignee": {"type": "string", "description": "Ответственный исполнитель"}
            },
            "required": ["title", "description", "priority"]
        }
    },
    {
        "name": "list_tasks",
        "description": "Показывает список задач с возможностью фильтрации по статусу и исполнителю.",
        "input_schema": {
            "type": "object",
            "properties": {
                "status": {
                    "type": "string",
                    "enum": ["open", "in_progress", "review", "done"],
                    "description": "Фильтр по статусу"
                },
                "assignee": {"type": "string", "description": "Фильтр по исполнителю"}
            }
        }
    },
    {
        "name": "update_task",
        "description": "Обновляет статус или приоритет существующей задачи.",
        "input_schema": {
            "type": "object",
            "properties": {
                "task_id": {"type": "string", "description": "Идентификатор задачи, например: TASK-0001"},
                "status": {
                    "type": "string",
                    "enum": ["open", "in_progress", "review", "done"]
                },
                "priority": {
                    "type": "string",
                    "enum": ["low", "medium", "high", "critical"]
                }
            },
            "required": ["task_id"]
        }
    }
]

# Запуск агента
def main():
    print("Агент-менеджер задач запущен. Введите ваш запрос:")

    while True:
        user_input = input("\nВы: ")
        if user_input.lower() in ["выход", "exit", "quit"]:
            break

        response = run_claude_agent(user_input, task_tools)
        print(f"\nАгент: {response}")

if __name__ == "__main__":
    main()

Мониторинг и оптимизация производительности

Метрики, за которыми стоит следить

Запустить агента в продакшен — это полдела. Дальше нужно понимать, как он работает. Вот метрики, которые я рекомендую мониторить:

  • Точность выбора инструментов — как часто модель выбирает правильный инструмент
  • Точность параметров — насколько корректны передаваемые аргументы
  • Количество итераций — сколько циклов tool_call → result нужно для выполнения задачи (чем меньше — тем лучше)
  • Latency — общее время ответа с учётом вызовов инструментов
  • Стоимость токенов — сколько токенов уходит на описания инструментов и результаты
  • Частота ошибок — доля неудачных вызовов и, что важнее, их причины

Оптимизация расхода токенов

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

  • Контекстная загрузка — подключайте только те инструменты, которые нужны прямо сейчас, а не весь набор
  • Компактные описания — лаконичные, но точные. Каждое лишнее слово стоит денег
  • Кэширование контекста — используйте Prompt Caching (есть и у OpenAI, и у Anthropic) для снижения стоимости повторных вызовов
  • Группировка инструментов — если есть лимит на количество инструментов, объединяйте связанные функции

Тренды Function Calling в 2026 году

Экосистема развивается очень быстро. Вот что определяет ландшафт прямо сейчас.

Model Context Protocol (MCP)

Anthropic выпустили открытый стандарт MCP, который унифицирует подключение инструментов к любым LLM. Идея простая, но мощная: вы создаёте сервер инструментов один раз, и он работает с любой моделью, поддерживающей протокол. Никакой привязки к конкретному провайдеру.

Нативное мышление с инструментами

Модели нового поколения (o3, o4-mini, Claude с Extended Thinking) обучены рассуждать об инструментах внутри цепочки мышления. На практике это значит заметно более точный выбор инструментов и параметров, особенно в сложных многошаговых сценариях.

Агентные фреймворки

В 2026 году фреймворки для построения агентов наконец вышли из стадии «интересный эксперимент» в «рабочий инструмент»: LangChain и LangGraph для оркестрации, CrewAI для мультиагентных систем, Anthropic Agent SDK для экосистемы Claude. Они берут на себя всю рутину управления циклом инструментов, а вы фокусируетесь на бизнес-логике.

Безопасность и контроль

Чем автономнее агенты, тем важнее контроль. Индустрия активно внедряет Human-in-the-Loop для опасных действий, гранулярное управление правами, аудит всех операций. Принцип «минимальных привилегий» становится стандартом для AI-агентов — и это правильно.

Заключение

Function Calling и Tool Use — это то, что превращает языковую модель из умного собеседника в полноценного агента, который может делать реальные вещи. Получать данные, считать, отправлять, создавать.

Если вынести из этого руководства главное:

  • LLM не выполняет функции сама — она генерирует структурированные вызовы, а ваш код их обрабатывает
  • Качество описаний инструментов критически влияет на поведение агента
  • Агентный цикл — это основа для многошаговых задач
  • Безопасность нужно закладывать с первого дня: валидация, контроль прав, аудит
  • Паттерны маршрутизации, цепочек и параллельных вызовов покрывают большинство реальных сценариев
  • Оптимизация токенов на описания инструментов — это прямая экономия бюджета

Мой совет: начните с простого агента с 2-3 инструментами. Отладьте агентный цикл и обработку ошибок. А дальше масштабируйте, добавляя паттерны и возможности по мере того, как растут требования. Не пытайтесь построить идеальную систему сразу — лучше итерируйте.

Об авторе Editorial Team

Our team of expert writers and editors.