Реализация Function Calling (Tool Use) для AI-ассистента в мобильном приложении
Function Calling — это механизм, при котором модель не пытается сама ответить на вопрос «какая погода завтра», а возвращает структурированный JSON с описанием того, что нужно вызвать: {"name": "get_weather", "arguments": {"city": "Минск", "date": "tomorrow"}}. Приложение выполняет вызов, передаёт результат обратно, и модель формирует финальный ответ. У OpenAI это tools, у Anthropic — tool_use, у Google — function_calling.
Где реально ломается на мобильном
Самая частая проблема — неправильно описанные 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 ответа инструмента — это помогает модели переключиться на другую стратегию.
Архитектура 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 пары, но никогда не режьте незавершённый цикл вызова инструмента — модель получит ошибку контекста.
Этапы и сроки
Анализ бизнес-логики и описание инструментов → реализация ToolDispatcher и JSON Schema → интеграция в диалоговый цикл → обработка параллельных вызовов → тестирование граничных случаев (неизвестный инструмент, ошибка API, таймаут) → мониторинг.
Интеграция Function Calling для 3–5 инструментов: 2–3 недели. С расширенной логикой, параллельными вызовами и сложным управлением состоянием — 4–6 недель.







