Implementing Function Calling (Tool Use) for AI Assistant in a Mobile Application
Function Calling is a mechanism where the model doesn't try to answer "what's the weather tomorrow" itself, but returns structured JSON describing what needs calling: {"name": "get_weather", "arguments": {"city": "Minsk", "date": "tomorrow"}}. App executes call, returns result, model forms final answer. OpenAI calls it tools, Anthropic—tool_use, Google—function_calling.
Where It Really Breaks on Mobile
Most common problem—poorly described JSON Schema for tools. Model chooses tool based on description and parameter schema. If schema is vague ("pass what's needed"), model either doesn't call tool, or passes parameters in wrong type. Concrete case: amount field described as string instead of number—model sends "150", deserializer expects Double, app crashes with JsonDataCorruptedException. Gson and Moshi don't convert string to number silently by default.
Second bottleneck—parallel tool calls. GPT-4 and Claude 3 can return multiple tool_calls in single response. Processing sequentially—user waits. On Android correct—async/await via coroutines (async { } + awaitAll()), on iOS—async let or TaskGroup. Important: all results must return to model in single messages[] step with role: "tool" for each call—OpenAI requires this, else 400 Invalid request.
Third problem—infinite call loop. If tool returned error, model sometimes tries calling it again with same parameters. Limit iterations (5–10 usually enough) and pass error explicitly in response content—helps model switch strategy.
ToolDispatcher Architecture
// Android—tool dispatcher
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"}"""
}
}
Each handler returns String (JSON string of result). Model gets text, not object—fundamental. No need serialize complex structures; key data JSON sufficient.
Tool descriptions must be maximally concrete:
{
"name": "search_products",
"description": "Searches products in catalog by name or category. Use when user asks about specific product or wants to see assortment.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query in user's language"},
"category": {"type": "string", "enum": ["electronics", "clothing", "food"]},
"limit": {"type": "integer", "default": 10, "maximum": 50}
},
"required": ["query"]
}
}
description field affects whether model calls tool. "Search"—bad description. "Searches products when user names specific product"—model understands application context.
Dialog State Management on Client
Function Calling requires storing full message history: user → assistant (with tool_calls) → tool (result) → assistant (final answer). On mobile—correct data model for Message:
// iOS
enum MessageRole { case user, assistant, tool }
struct Message: Codable {
let role: MessageRole
let content: String?
let toolCalls: [ToolCall]? // only for role == .assistant
let toolCallId: String? // only for role == .tool
let name: String? // tool name for role == .tool
}
Save whole chain in @State / ViewModel. If trimming for token economy, trim only early user/assistant pairs, but never trim incomplete tool call cycle—model gets context error.
Stages and Timeline
Analyze business logic and describe tools → implement ToolDispatcher and JSON Schema → integrate into dialog cycle → handle parallel calls → test edge cases (unknown tool, API error, timeout) → monitor.
Function Calling for 3–5 tools: 2–3 weeks. With extended logic, parallel calls, complex state management—4–6 weeks.







