We integrate Function Calling (Tool Use) into mobile apps — a mechanism where the AI model does not try to answer the question 'what's the weather tomorrow' on its own, but returns a structured JSON describing what needs to be called: {"name": "get_weather", "arguments": {"city": "Minsk", "date": "tomorrow"}}. The app executes the call, passes the result back, and the model generates the final answer. For OpenAI it's tools, for Anthropic — tool_use, for Google — function_calling. Our 5+ years of experience and 30+ successful projects ensure reliable integration. Request a consultation to evaluate your scenario.
Where it actually breaks on mobile
The most common problem is incorrectly described JSON Schema for tools. The model selects a tool based on the description and parameter schema. If the schema is vague ('pass what's needed'), the model either doesn't call the tool at all or passes parameters in the wrong type. Concrete case: the amount field described as string instead of number — the model passes "150", the deserializer expects Double, the app crashes with JsonDataCorruptedException. Gson and Moshi by default do not silently convert strings to numbers.
The second bottleneck is parallel tool calls. GPT-4 and Claude 3 can return multiple tool_calls in one response. If processed sequentially, the user waits. The right approach on Android is async/await via coroutines (async { } + awaitAll()), on iOS — async let or TaskGroup. And crucially: all results must be returned to the model in one messages[] step with role: "tool" for each call — OpenAI requires exactly that, otherwise 400 Invalid request.
The third problem is an infinite call loop. If the tool returns an error, the model sometimes tries to call it again with the same parameters. Limit the number of iterations (usually 5–10 is sufficient) and explicitly pass the error in the content of the tool response — this helps the model switch to another strategy.
How to avoid infinite call loops?
Set an iteration limit (e.g., 8) and pass the error in the content of the tool response. The model, upon receiving the error message, will change its strategy. Also useful to add a flag in the tool schema to prevent the model from calling it again without parameter changes.
What to do with parallel calls?
Use asynchronous execution. On Android — coroutines with async and awaitAll(), on iOS — TaskGroup. Collect all results in an array and send in one message with role: "tool". This reduces latency and meets API requirements. Comparison: properly implemented async execution reduces response time by 3 times compared to sequential processing.
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 a String (JSON string of the result). The model gets text, not an object — this is fundamental. No need to serialize complex structures; enough with a clear JSON containing key data.
Tool descriptions should be as specific as possible:
{ "name": "search_products", "description": "Searches products in catalog by name or category. Use when the user asks about a specific product or wants to browse the 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"] } } The description field influences whether the model calls the tool. 'Search' is a poor description. 'Searches products in catalog when the user names a specific product' — the model understands the context of use.
Dialog State Management on Client
Function Calling requires storing the full message history: user → assistant (with tool_calls) → tool (result) → assistant (final answer). On mobile this means a proper 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 the entire chain in @State / ViewModel. If you trim history to save tokens, only cut early user/assistant pairs, but never cut an incomplete tool call cycle — the model will get a context error.
Provider Comparison for Function Calling
| Provider | Mechanism | Description Format | Parallel Calls |
|---|---|---|---|
| OpenAI | tools |
JSON Schema | Yes |
| Anthropic | tool_use |
JSON Schema | Yes (Claude 3+) |
function_calling |
JSON Schema | Yes (Gemini) |
All three providers use JSON Schema ( Wikipedia: JSON Schema ) to describe parameters. Differences are in field names and response format, but the dispatcher architecture is universal.
What's Included in the Work
| Stage | Duration | Result |
|---|---|---|
| Analysis and tool description | 3–5 days | JSON Schema for each tool |
| ToolDispatcher implementation | 5–7 days | Dispatcher code with handlers |
| Integration into dialog loop | 3–4 days | Complete call chain |
| Parallel call handling | 2–3 days | Async implementation |
| Edge case testing | 5–7 days | Test suite (unknown tool, API error, timeout) |
| Monitoring and documentation | 2–3 days | Logging and operation manual |
Integration of Function Calling for 3–5 tools — 2–3 weeks. With extended logic, parallel calls, and complex state management — 4–6 weeks. Request a consultation for an accurate estimate for your project. Get a working prototype within 10 days.







