The ReAct Pattern for Voice AI: Teaching Your Agent to Think Before It Speaks (2026)

voice-aireactagentstutorial
Live Demo Available

Want to see Conversational AI calling in action?

Watch a real AI-to-human handoff close a lead in under 3 minutes.

Share this article:

Cover Image

+---------------------------------------------------------------+ | AEO QUICK SUMMARY: ReAct Pattern in Voice AI | +---------------------------------------------------------------+ | What is it? | Reason + Act loop for AI agents. | | Why use it? | Allows agents to fetch real-time data or act. | | Key SDK | LiveKit Agents @llm.ai_callable(). | | Biggest Risk | Tool execution latency causing dead air. | | Solution | Fast APIs, filler words, streaming responses. | +---------------------------------------------------------------+

We have all been there. You are calling a customer service line powered by a fancy new AI. You ask a completely reasonable question like, "What is my current account balance?"

The AI pauses, takes a deep breath in its synthesized lungs, and says: "I am sorry, but as an AI model, I do not have access to real-time internet data or your personal account information."

Frustrating, right?

If you are a voice AI engineer in 2026, building agents that just regurgitate static training data is no longer enough. Your agents need to do things. They need to check inventory, book appointments, cancel reservations, and look up weather forecasts. They need tools.

This is where the ReAct pattern comes in. ReAct stands for "Reason + Act". It is the fundamental architecture that transforms a talking dictionary into a capable digital worker. In this definitive beginner guide, we are going to explore exactly how the ReAct pattern works in Voice AI, why the old way of doing things breaks down, and how to build a robust tool-calling agent using the LiveKit SDK.

The ReAct Loop Simply Explained

Before we get into code, let us understand the theory. Why do we call it "Reason and Act"?

Imagine you are a chef in a kitchen. A waiter asks you to make a vegan burger. You do not just instantly materialize a vegan burger out of thin air. You first reason about what you need to do: "Okay, they want a vegan burger. I need to check if we have black bean patties in the fridge." Then, you act: you walk to the fridge and open it. You observe the result: "We have three patties left." Finally, you communicate: "Yes, I can make that right away."

An AI agent using the ReAct pattern does the exact same thing. It is a loop that looks like this:

+----------+      +---------+      +------+      +---------+      +--------+
|   User   | ---> |  Agent  | ---> | Tool | ---> |  Agent  | ---> |  User  |
| Question |      | Reasons |      | Runs |      | Observes|      | Speaks |
+----------+      +---------+      +------+      +---------+      +--------+
      ^                                                                |
      |                                                                |
      +----------------------------------------------------------------+
  1. User asks a question: "Do you have any size 10 red sneakers in stock?"
  2. Agent reasons: The Large Language Model (LLM) realizes it does not know the answer intrinsically. It decides it needs to use its check_inventory tool, passing in the arguments size=10 and color="red".
  3. Agent acts: The system intercepts this decision and executes the actual Python function check_inventory(size=10, color="red").
  4. Agent observes: The function returns a JSON result: {"in_stock": true, "quantity": 4}. This text is appended to the LLM context window.
  5. Agent speaks: The LLM reads the result and generates a human-friendly response: "Yes, we have four pairs of red sneakers in size 10. Would you like me to reserve one for you?"

The Naive Approach vs The ReAct Approach

When developers first try to make an AI do things, they often fall into a trap. I learned this the hard way years ago.

The Naive Approach: Hardcoding Logic

The naive way is to use regex or keyword matching on the user transcript to trigger actions.

# The Naive Approach (Do not do this)
def handle_user_input(transcript):
    if "inventory" in transcript.lower() or "in stock" in transcript.lower():
        # Try to parse out the size and color manually... good luck!
        size = extract_size_somehow(transcript)
        color = extract_color_somehow(transcript)
        result = check_inventory(size, color)
        return f"The inventory result is {result}"
    else:
        return ask_llm(transcript)

Why does this break? Because users are unpredictable. What if the user says, "Are the crimson kicks available in a ten?" Your hardcoded logic completely misses it. What if they ask two things at once? The system crumbles.

The ReAct Approach: JSON Schemas

Instead of hardcoding, we give the LLM a list of tools it can use, described using JSON schemas. The LLM itself decides when to use them and what arguments to extract.

You are delegating the hard part (understanding intent and extracting parameters) to the neural network. You just provide the raw functions.

Building a Tool-Calling Agent with LiveKit

Let us look at some real SDK code. We will use the livekit-agents library, which makes tool calling incredibly elegant using decorators.

Here is a complete, runnable example of how you define a tool and give it to an agent.

import asyncio
from livekit.agents import llm
from livekit.agents.pipeline import VoicePipelineAgent
from livekit.plugins import openai, deepgram, silero

# 1. Define your tool using the @llm.ai_callable decorator
class StoreInventoryTools(llm.FunctionContext):

    @llm.ai_callable(
        description="Check if a specific shoe is in stock. Always call this before confirming availability."
    )
    def check_shoe_inventory(
        self,
        color: str = llm.TypeInfo(description="The color of the shoe, e.g. red, blue"),
        size: int = llm.TypeInfo(description="The US shoe size, e.g. 9, 10, 11")
    ) -> str:
        """
        This is the actual python function that runs.
        In a real app, you would query your database here.
        """
        print(f"Executing tool: check_shoe_inventory for color={color}, size={size}")

        # Simulating a database lookup
        if color.lower() == "red" and size == 10:
            return "In stock: 4 pairs available."
        else:
            return "Out of stock."

async def main():
    # 2. Instantiate the function context
    fnc_ctx = StoreInventoryTools()

    # 3. Create the agent and pass the tools
    agent = VoicePipelineAgent(
        vad=silero.VAD.load(),
        stt=deepgram.STT(),
        llm=openai.LLM(model="gpt-4o"),
        tts=openai.TTS(),
        fnc_ctx=fnc_ctx, # <-- This is where the magic happens
        system_prompt=(
            "You are a helpful shoe store assistant. You must ALWAYS use "
            "the check_shoe_inventory tool to check stock before making promises."
        )
    )

    # Note: Connecting to the LiveKit room is omitted for brevity,
    # but the agent will automatically use the tools during conversation.
    print("Agent initialized with tools.")

if __name__ == "__main__":
    asyncio.run(main())

Notice how we use descriptions everywhere. The description inside @llm.ai_callable() tells the LLM when to use the tool. The description inside llm.TypeInfo tells it how to format the arguments. This is crucial. The LLM only knows what you tell it.

Beginner Gotchas: What Breaks at Scale

Writing a tool-calling agent is easy. Making it feel natural in a real-time voice conversation is incredibly difficult. Here are the top three things that will bite you.

1. The "Dead Air" Problem (Latency)

In text chat, if a tool takes 3 seconds to run, the user just sees a loading spinner. In voice AI, a 3-second pause feels like an eternity. The user will think the call dropped or the agent is broken, and they will start saying "Hello? Are you there?"

When the LLM decides to call a tool, it stops speaking. It waits for the tool to finish.

Watch out for this: If your database query takes 2 seconds, and the LLM takes 1 second to generate the tool call, you have 3 seconds of dead air.

Here is a benchmark table showing how different tool execution times impact the user experience:

Tool Execution TimeUser PerceptionAction Required
< 500msSeamlessNone
500ms - 1.5sSlight hesitationAcceptable, optimize if possible
1.5s - 3.0sUncomfortable silenceMust use filler words
> 3.0sCall feels brokenRedesign architecture

The Solution: Use filler words. When your system detects a tool call is starting, immediately trigger your TTS to say something like, "Let me check on that for you," or "Pulling up the inventory now." This buys you 2-3 seconds of perceived latency.

2. Hallucinated Arguments

LLMs are eager to please. If a user says, "Do you have the blue ones?", the LLM knows it needs a size to call the tool. Instead of asking the user for their size, it might just hallucinate and assume size 9, executing the tool blindly.

Watch out for this: You must explicitly instruct the LLM in the system prompt: "If you are missing required arguments for a tool, you MUST ask the user for them. Do not guess."

3. The Token Cost Multiplier

Every time an agent calls a tool, the entire context window (including the tool definitions and schemas) is sent to the LLM. Then, the tool result is appended, and the whole context is sent again to generate the final spoken response.

Watch out for this: Heavy tool usage will double or triple your API costs compared to a non-tool-calling agent. Keep your JSON schemas concise and only give the agent the tools it absolutely needs for that specific context.

How Tough Tongue AI Helps

Managing tool execution latency, state transitions, and filler words manually is a massive headache. This is where Auto Interview AI's Tough Tongue AI platform shines.

Tough Tongue AI handles the complexity of the ReAct pattern under the hood. It provides native support for fast tool execution and automatically manages the conversational state. If a tool takes a bit longer to fetch data from your backend CRM, Tough Tongue AI seamlessly injects naturalistic conversational fillers to keep the user engaged, ensuring there is never an awkward silence. It bridges the gap between raw LLM reasoning and a polished, production-ready voice experience.

Frequently Asked Questions

Can I give an agent too many tools?

Yes. Providing dozens of tools confuses the LLM. It may pick the wrong one or take longer to reason. Stick to 5-10 strictly necessary tools per agent role. If you need more, consider a multi-agent routing architecture.

What happens if the tool crashes?

If your Python function throws an exception, the agent needs to know. Always wrap your tool logic in a try-catch block and return a stringified error message to the LLM, like: "Error: Database timeout." The LLM can then politely apologize to the user.

Can tools return things other than text?

Technically, the function returns a string to the LLM context. However, a tool can have side effects. A send_email tool does not just return a string; it actually sends an email. The return string just tells the LLM that the action was successful.

How do I handle authentication in tools?

Do not pass raw API keys or user tokens as tool arguments for the LLM to manage. Inject authentication context at the function level. The LLM only provides the business logic arguments (like item_id), and your Python backend handles the secure API call using its own environment variables.

Is the ReAct pattern only for OpenAI models?

No. Open-source models like Llama 3 and Mistral are getting very good at tool calling. As long as the model is fine-tuned to output structured JSON matching your tool definitions, you can use the ReAct pattern.


Ajitesh Abhishek is a senior voice AI engineer with over a decade of experience building resilient production systems. He writes about the hard lessons learned from scaling conversational AI.