Мы интегрируем Function Calling (Tool Use) в мобильные приложения — это механизм, при котором AI-модель не пытается сама ответить на вопрос «какая погода завтра», а возвращает структурированный JSON с описанием того, что нужно вызвать: {"name": "get_weather", "arguments": {"city": "Минск", "date": "tomorrow"}}. Приложение выполняет вызов, передаёт результат обратно, и модель формирует финальный ответ. У OpenAI это tools, у Anthropic — tool_use, у Google — function_calling. Наш опыт 5+ лет и 30+ успешных проектов гарантирует надёжную интеграцию. Закажите консультацию для оценки вашего сценария.
Где реально ломается на мобильном
Самая частая проблема — неправильно описанные JSON Schema для инструментов. Модель выбирает инструмент на основе описания и схемы параметров. Если схема размыта («передай что нужно»), модель либо не вызывает инструмент вовсе, либо передаёт параметры в неправильном типе. Конкретный кейс: поле amount описано как string вместо number — модель передаёт "150", десериализатор ожидает Double, приложение крашится с JsonDataCorruptedException. Gson и Moshi по умолчанию не конвертируют строку в число молча.
Второе узкое место — параллельные вызовы инструментов. GPT-4 и Claude 3 могут вернуть несколько tool_calls в одном ответе. Если обрабатывать их последовательно, пользователь ждёт. На Android правильно — async/await через корутины (async { } + awaitAll()), на iOS — async let или TaskGroup. И важно: все результаты нужно вернуть модели в одном messages[] шаге с role: "tool" для каждого вызова — OpenAI требует именно это, иначе 400 Invalid request.
Третья проблема — бесконечный цикл вызовов. Если инструмент вернул ошибку, модель иногда пытается вызвать его снова с теми же параметрами. Ограничивайте количество итераций (обычно 5–10 достаточно) и передавайте ошибку явно в content ответа инструмента — это помогает модели переключиться на другую стратегию.
Как избежать бесконечного цикла вызовов?
Установите лимит итераций (например, 8) и передавайте ошибку в content ответа инструмента. Модель, получив сообщение об ошибке, изменит стратегию. Также полезно добавить флаг в схему инструмента, чтобы модель не вызывала его повторно без изменения параметров.
Что делать при параллельных вызовах?
Используйте асинхронное выполнение. На Android — корутины с async и awaitAll(), на iOS — TaskGroup. Все результаты собирайте в массив и отправляйте одним сообщением с role: "tool". Это снижает задержку и соответствует требованиям API. Сравнение: правильно реализованное асинхронное выполнение сокращает время ответа в 3 раза по сравнению с последовательной обработкой.
Архитектура ToolDispatcher
// Android — диспетчер инструментов class ToolDispatcher { private val tools = mapOf<String, suspend (JsonObject) -> String>( "get_weather" to ::handleGetWeather, "search_flights" to ::handleSearchFlights, "book_hotel" to ::handleBookHotel ) suspend fun dispatch(toolName: String, args: JsonObject): String { return tools[toolName]?.invoke(args) ?: """{"error": "unknown tool: $toolName"}""" } } Каждый обработчик возвращает String (JSON-строку результата). Модель получает текст, не объект — это принципиально. Не нужно сериализовывать сложные структуры; достаточно понятного JSON с ключевыми данными.
Описание инструментов должно быть максимально конкретным:
{ "name": "search_products", "description": "Ищет товары в каталоге по названию или категории. Используй когда пользователь спрашивает о конкретном товаре или хочет посмотреть ассортимент.", "parameters": { "type": "object", "properties": { "query": {"type": "string", "description": "Поисковый запрос на языке пользователя"}, "category": {"type": "string", "enum": ["electronics", "clothing", "food"]}, "limit": {"type": "integer", "default": 10, "maximum": 50} }, "required": ["query"] } } Поле description влияет на то, вызовет ли модель инструмент. «Поиск» — плохое описание. «Ищет товары в каталоге, когда пользователь называет конкретный продукт» — модель понимает контекст применения.
Управление состоянием диалога на клиенте
Function Calling требует хранить полную историю сообщений: user → assistant (с tool_calls) → tool (результат) → assistant (финальный ответ). На мобильном это означает правильную модель данных для Message:
// iOS enum MessageRole { case user, assistant, tool } struct Message: Codable { let role: MessageRole let content: String? let toolCalls: [ToolCall]? // только для role == .assistant let toolCallId: String? // только для role == .tool let name: String? // имя инструмента для role == .tool } Сохраняйте всю цепочку в @State / ViewModel. Если обрезать историю для экономии токенов, режьте только ранние user/assistant пары, но никогда не режьте незавершённый цикл вызова инструмента — модель получит ошибку контекста.
Сравнение провайдеров Function Calling
| Провайдер | Механизм | Формат описания | Параллельные вызовы |
|---|---|---|---|
| OpenAI | tools |
JSON Schema | Да |
| Anthropic | tool_use |
JSON Schema | Да (Claude 3+) |
function_calling |
JSON Schema | Да (Gemini) |
Все три провайдера используют JSON Schema ( Wikipedia: JSON Schema ) для описания параметров. Различия в имени поля и формате ответа, но архитектура диспетчера универсальна.
Что входит в работу
| Этап | Длительность | Результат |
|---|---|---|
| Анализ и описание инструментов | 3–5 дней | JSON Schema для каждого инструмента |
| Реализация ToolDispatcher | 5–7 дней | Код диспетчера с обработчиками |
| Интеграция в диалоговый цикл | 3–4 дня | Полная цепочка вызовов |
| Обработка параллельных вызовов | 2–3 дня | Асинхронная реализация |
| Тестирование граничных случаев | 5–7 дней | Набор тестов (неизвестный инструмент, ошибка API, таймаут) |
| Мониторинг и документация | 2–3 дня | Логирование и инструкция по эксплуатации |
Интеграция Function Calling для 3–5 инструментов — 2–3 недели. С расширенной логикой, параллельными вызовами и сложным управлением состоянием — 4–6 недель. Закажите консультацию для точной оценки вашего проекта. Получите рабочий прототип уже через 10 дней.







