Skip to main content
Lesson 11 of 14 · Function Calling with Grok

Multi-Turn Conversations with Tools

Lesson 11 of 14 Updated July 29, 2026 Developer
On this page
  1. Multi-turn conversations with function calling
  2. The max_turns parameter
  3. Client-side multi-turn loop
  4. Multi-turn context with the Responses API
  5. Multi-turn conversation patterns
  6. Memory management
  7. Key takeaways

Multi-turn conversations with function calling

In a real conversation, the model may need to call tools multiple times throughout the exchange. Each turn can involve new calls, results that trigger other calls, or requests for clarification from the user before using a tool.

The max_turns parameter

The max_turns parameter limits the number of assistant/tool turns server-side in a single request. It applies only to built-in tools (web_search, code_interpreter, etc.).

response = client.responses.create(
    model="grok-4.5",
    input="Analyze 2026 tech trends",
    tools=[{"type": "web_search"}],
    max_turns=5  # Maximum 5 web searches before responding
)

Important point: client-side tool invocations reset the counter. When you send back a function_call_output, a new max_turns allocation begins.

Client-side multi-turn loop

To manage conversations where the model calls tools multiple times, implement a loop:

import json

def multi_turn_chat(user_message, tools, max_iterations=10):
    messages = [{"role": "user", "content": user_message}]

    for i in range(max_iterations):
        response = client.chat.completions.create(
            model="grok-4.5",
            messages=messages,
            tools=tools
        )

        message = response.choices[0].message
        messages.append(message)

        # If no tool_call, the conversation is over
        if not message.tool_calls:
            return message.content

        # Execute all requested tools
        for tool_call in message.tool_calls:
            args = json.loads(tool_call.function.arguments)
            result = execute_function(tool_call.function.name, args)
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": json.dumps(result)
            })

    return "Maximum number of iterations reached"

The max_iterations variable is an essential safeguard to prevent infinite loops. In production, set a reasonable limit (5–10 turns).

Multi-turn context with the Responses API

The Responses API offers two mechanisms to maintain context:

1. Remote storage

# First message
r1 = client.responses.create(
    model="grok-4.5",
    input="What's the weather like in Paris?",
    tools=tools,
    store_messages=True
)

# Tool result
r2 = client.responses.create(
    model="grok-4.5",
    input=[{
        "type": "function_call_output",
        "call_id": r1.output[0].call_id,
        "output": {temp: 18}
    }],
    previous_response_id=r1.id,
    store_messages=True
)

# Follow-up question (context is preserved)
r3 = client.responses.create(
    model="grok-4.5",
    input="What about tomorrow?",
    previous_response_id=r2.id,
    tools=tools,
    store_messages=True
)

The advantage: you don’t have to manage the message history. xAI stores and retrieves it automatically via previous_response_id.

2. Client-side encrypted content

response = client.responses.create(
    model="grok-4.5",
    input="Search for flight prices to Nice",
    tools=tools,
    use_encrypted_content=True
)

With use_encrypted_content=True, you receive reasoning and tool outputs in encrypted form. You send them back as-is in subsequent requests, without being able to read them. This is useful when you don’t want data to pass in clear text through your server.

Multi-turn conversation patterns

Pattern: Search then action

Turn 1 : User → "Find a cheap Paris-Nice flight"
         Model → tool_call: search_flights(from="CDG", to="NCE")
         Client → result : [{flight1}, {flight2}, {flight3}]
Turn 2 : Model → "I found 3 flights. The cheapest is €89. Do you want to book it?"
         User → "Yes, book the €89 flight"
Turn 3 : Model → tool_call: book_flight(flight_id="FL123", passengers=1)

Pattern: Data aggregation

Turn 1 : Model → tool_call: get_sales(month="march")
Turn 2 : Model → tool_call: get_expenses(month="march")
Turn 3 : Model → tool_call: get_headcount(month="march")
Turn 4 : Model → "Here is the monthly report: sales +12%, expenses -3%, headcount stable."

Pattern: Progressive refinement

Turn 1 : Model → tool_call: search_products(query="running shoes")
         Result → 150 products
Turn 2 : Model → "I found 150 products. What size and budget?"
         User → "42, max €120"
Turn 3 : Model → tool_call: search_products(query="running shoes", size=42, max_price=120)
         Result → 8 products

Memory management

In long conversations, history—which includes every tool_call and every tool result—grows quickly, and with it the cost of each turn. Three strategies are available to you, ranging from manual to fully delegated. Periodic summarization condenses older turns into a paragraph injected into the system prompt: you keep the substance of past exchanges for a fraction of their token weight, at the cost of some detail loss. The sliding window is more blunt but simpler: keep only the last N turns, accepting that the model will forget the beginning of the conversation—suitable when older exchanges no longer have operational importance. Remote storage, finally, delegates the problem to xAI with store_messages=True: the history lives server-side, and your application carries only a reference. The right choice depends on what your application needs to remember, and for how long.

Key takeaways

  • max_turns limits server-side tool turns—client tools reset the counter
  • Implement a loop with max_iterations to prevent infinite loops
  • previous_response_id chains conversations without resending history
  • store_messages=True delegates context storage to xAI
  • use_encrypted_content=True protects data in transit
  • Manage memory in long conversations (summarization, sliding window)