Home » Building AI Assistants » Multi-Turn Conversations

How to Handle Multi-Turn Conversations in AI

Multi-turn conversation handling is the ability of an AI assistant to maintain coherent understanding across a sequence of exchanges. This means resolving references to previous messages ("change that to blue"), tracking entities introduced earlier in the conversation, managing topic switches without losing context, maintaining state for multi-step tasks, and recovering gracefully when the conversation goes off track. Good multi-turn handling makes the difference between an assistant that feels like a continuous partner and one that feels like a series of disconnected interactions.

Before You Start

You need a working assistant with conversation history management. This guide builds on the history infrastructure described in How to Add Conversation History to an AI Assistant. If your assistant only handles single-turn interactions (each message is independent), start there first. Multi-turn handling is about how you use and manage that history to maintain coherent conversational state.

Step-by-Step Setup

Step 1: Structure conversation state.
Beyond raw message history, maintain a structured state object that tracks what the conversation has established. This state gives the model explicit information about the current context rather than requiring it to infer everything from the message log. Key state fields include: the current topic (what the user is asking about), active entities (things that have been mentioned and might be referenced again), pending tasks (multi-step operations in progress), and established facts (decisions and preferences stated during this conversation).
# Example: structured conversation state class ConversationState: def __init__(self, conversation_id): self.conversation_id = conversation_id self.current_topic = None self.active_entities = {} # name -> description self.pending_tasks = [] # in-progress multi-step operations self.established_facts = [] # facts stated this session self.topic_history = [] # previous topics for back-reference def set_topic(self, topic): if self.current_topic: self.topic_history.append(self.current_topic) self.current_topic = topic def add_entity(self, name, description): self.active_entities[name] = description def to_context_string(self): parts = [] if self.current_topic: parts.append(f"Current topic: {self.current_topic}") if self.active_entities: entities = ", ".join(self.active_entities.keys()) parts.append(f"Active entities: {entities}") if self.pending_tasks: tasks = "; ".join(self.pending_tasks) parts.append(f"Pending tasks: {tasks}") if self.established_facts: facts = "; ".join(self.established_facts[-5:]) parts.append(f"Established facts: {facts}") return "\n".join(parts)
Step 2: Handle references and pronouns.
Users naturally use pronouns and references that depend on conversational context. "Can you make it bigger?" requires knowing what "it" refers to. "Use the same approach as before" requires knowing what was discussed earlier. Language models handle this well when the relevant context is within the context window, but they lose track when the referent was mentioned many turns ago or was part of a tool result that has been truncated.

The solution is ensuring that entities introduced in conversation remain accessible. When the user mentions a specific file, database table, API endpoint, or concept, add it to the active entities in conversation state. When you trim older messages from the context, check whether any active entities were introduced in the trimmed messages and, if so, preserve them in the state summary that replaces those messages. This way, a reference to "the migration we discussed" can be resolved even if the original discussion has been summarized.

Step 3: Manage topic transitions.
Users switch topics naturally within conversations. A developer might ask about deployment, then ask a billing question, then return to deployment with new information. The assistant needs to handle these transitions without confusing context between topics and without losing the earlier topic when the user returns to it.

Track the current topic and maintain a topic history stack. When the user switches topics, push the current topic to history and set the new one. When the user appears to return to a previous topic (referencing something discussed earlier, using entities from a previous topic), pop the relevant topic from history and restore it as current. Include the current topic in the context sent to the model so it can frame its responses appropriately.

Persistent memory helps with topic management because important facts from each topic are stored independently of the conversation flow. If the user discussed deployment settings earlier in the conversation, those settings are in memory and can be retrieved when the user returns to the deployment topic, regardless of how many unrelated messages occurred in between.

Step 4: Track task state across turns.
Multi-step tasks require explicit state tracking because the model cannot reliably maintain complex state through conversation context alone. If the user asks the assistant to set up a new project (which involves creating a repository, configuring CI, setting up the database, and deploying a staging environment), the assistant needs to track which steps have been completed, which are in progress, which are pending, and what information has been gathered for upcoming steps.
# Example: task state tracking class TaskTracker: def __init__(self): self.tasks = {} def create_task(self, task_id, steps): self.tasks[task_id] = { "steps": [{"name": s, "status": "pending", "result": None} for s in steps], "current_step": 0 } def complete_step(self, task_id, result=None): task = self.tasks[task_id] step = task["steps"][task["current_step"]] step["status"] = "completed" step["result"] = result task["current_step"] += 1 def get_status_summary(self, task_id): task = self.tasks[task_id] completed = [s for s in task["steps"] if s["status"] == "completed"] pending = [s for s in task["steps"] if s["status"] == "pending"] current = task["current_step"] summary = f"Task progress: {len(completed)}/{len(task['steps'])} steps done.\n" if current < len(task["steps"]): summary += f"Next: {task['steps'][current]['name']}" return summary

Include the task status summary in the model's context for each turn. This gives the model an explicit record of where the task stands, which is more reliable than asking it to infer progress from the conversation history. When a task spans multiple sessions, store the task state in persistent memory so it can be resumed later.

Step 5: Handle conversation repair.
Conversations go wrong. The model misunderstands a request, the user provides ambiguous instructions, or the context gets confused after several topic switches. Conversation repair is the ability to detect these problems and recover from them without requiring the user to start over.

Detecting misunderstandings relies on user signals: explicit corrections ("No, I meant the other file"), repetition of a request (the user asks the same thing again, indicating the first answer was wrong), and escalating frustration (shorter messages, more direct language). When the assistant detects a potential misunderstanding, it should acknowledge the confusion, ask a clarifying question, and re-ground itself in the corrected context rather than doubling down on the wrong interpretation.

When context becomes confused (the model mixes up entities from different topics, references the wrong previous action, or loses track of a multi-step task), a deliberate context reset helps. The assistant can summarize its current understanding of the situation and ask the user to confirm or correct it. This is more honest and effective than continuing with confused context and producing increasingly wrong responses.

Give your assistant persistent context that survives topic switches, session breaks, and conversation repair. Adaptive Recall stores the important facts from every conversation so multi-turn handling has a reliable foundation.

Get Started Free