Article

+ Updated

The ReAct Framework Explained - An AI Reasoning-and-Acting Paradigm from Principles to Practice

ReAct is an AI prompting paradigm/framework proposed by Google researchers in 2022 that combines Reasoning and Acting.

Before this, zero-shot, few-shot, and even CoT prompting patterns all relied purely on the prompt to answer directly, or on the examples the prompt provides to reason from. These approaches have some limitations; for example, they can't obtain real external information in real time and then think further, reason, and reach a conclusion on that basis. If you aren't yet familiar with the basic prompting content mentioned above, I recommend reading # Introduction to Prompt Engineering: A Comprehensive Look at Zero-Shot and Few-Shot Prompting

ReAct prompts LLMs to generate verbal reasoning traces and actions for a task. This lets the system perform dynamic reasoning to create, maintain, and adjust action plans, while also supporting interaction with external environments (for example, Wikipedia) to incorporate additional information into the reasoning.


Workflow

Each cycle of ReAct consists of three stages:

Thought → Action → Observation

PLAINTEXT

复制

Question: What's the weather in Beijing today?

Thought: I need to look up the real-time weather, so I should call the weather tool.
Action: search("Beijing weather today")
Observation: The result shows Beijing at 22°C and sunny.

Thought: I've got the answer and can reply to the user now.
Answer: It's sunny in Beijing today, around 22°C.

+

Pasted image 20260505103819

Hands-On Practice

1. Initializing the Model Client

PLAINTEXT

复制

"""
A real ReAct Agent
- LLM: DeepSeek-V4-Pro (thinking enabled)
- Search tool: Tavily (real web search)
- Calculation tool: Python eval (local execution)
Flow: Think → Act → Observe → Think → ... → Final answer
"""
import requests
import json
import os

from openai import OpenAI
from tavily import TavilyClient

# ─────────────────────────────────────────
# 1. Initialize the client
#    It's recommended to read the key from an environment variable to avoid leaking it by hard-coding
#    Run in the terminal:
#      export DEEPSEEK_API_KEY=sk-xxx
#      export TAVILY_API_KEY=tvly-xxx
# ─────────────────────────────────────────
client = OpenAI(
    api_key=os.environ.get("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com",
)
tavily_client = TavilyClient(api_key=os.environ.get("TAVILY_API_KEY"))

MODEL = "deepseek-v4-pro"

If you find environment variables a hassle, you can also temporarily hard-code the key information directly in the code.


2. Implementing the Tools

This mainly implements the tool functions the agent needs to call during execution, such as web search and math calculation.

PLAINTEXT

复制

# ─────────────────────────────────────────
# 2. Tool implementation
# ─────────────────────────────────────────
def search(query: str) -> str:
    """
    Tavily real web search
    Returns the title + summary + source of the top 3 results
    """
    response = tavily_client.search(
        query=query,
        max_results=3,
        search_depth="basic",  # basic saves tokens; advanced goes deeper
    )
    results = []
    for r in response.get("results", []):
        results.append(f"[{r['title']}]\n{r['content']}\nSource: {r['url']}")
    return "\n\n".join(results) if results else "No relevant results found"


def calculator(expression: str) -> str:
    """Local calculator, executes Python math expressions"""
    try:
        result = eval(expression)
        return f"Result: {result}"
    except Exception as e:
        return f"Calculation error: {e}"

def get_current_time() -> str:
    """
    Returns the current date and time
    """
    from datetime import datetime

    return f"Current time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"


def get_exchange_rate(base: str, target: str) -> str:
    """Real-time exchange rate lookup"""
    url = f"https://open.er-api.com/v6/latest/{base.upper()}"
    response = requests.get(url)
    data = response.json()

    rate = data["rates"].get(target.upper())

    if rate:
        return f"1 {base.upper()} = {rate} {target.upper()}"

    return f"No exchange rate found for {target}"

# Tool registry: name → function
TOOLS = {
    "search": search,
    "calculator": calculator,
    "get_current_time": get_current_time,
    "get_exchange_rate": get_exchange_rate,
}

3. Defining the Tool Schema

The Tool Schema is defined so the model knows which tools it can call.

PLAINTEXT

复制

# ─────────────────────────────────────────
# 3. Tool Schema (tells the model which tools are available)
# ─────────────────────────────────────────
TOOL_SCHEMAS = [
    {
        "type": "function",
        "function": {
            "name": "search",
            "description": "Search the web for real-time information, suited to looking up news, weather, people, events, the latest data, and so on",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "Search keywords, as specific as possible, such as \"2025 latest AI model rankings\"",
                    }
                },
                "required": ["query"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "calculator",
            "description": "Perform a math calculation. Input a valid Python math expression",
            "parameters": {
                "type": "object",
                "properties": {
                    "expression": {
                        "type": "string",
                        "description": "Math expression, such as '3 * (12 + 5)' or '2 ** 10'",
                    }
                },
                "required": ["expression"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "get_current_time",
            "description": "Get the current date and time, returned in year-month-day, hour:minute:second format",
            "parameters": {
                "type": "object",
                "properties": {},  # No parameters, left empty
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "get_exchange_rate",
            "description": "Query the real-time exchange rate between two currencies, such as USD to CNY",
            "parameters": {
                "type": "object",
                "properties": {
                    "base": {
                        "type": "string",
                        "description": "Base currency code, such as USD, CNY, EUR",
                    },
                    "target": {
                        "type": "string",
                        "description": "Target currency code, such as CNY, USD, JPY",
                    },
                },
                "required": ["base", "target"],
            },
        },
    },
]

4. Wrapping the Model Call

  • Always add the thinking parameter, with stream=False

  • Streaming must be turned off inside the ReAct loop, otherwise tool_calls can't be parsed properly

PLAINTEXT

复制

# ─────────────────────────────────────────
# 4. Wrap the DeepSeek call
# ─────────────────────────────────────────
def call_model(messages: list) -> object:
    return client.chat.completions.create(
        model=MODEL,
        messages=messages,
        tools=TOOL_SCHEMAS,
        tool_choice="auto",
        reasoning_effort="high",
        extra_body={"thinking": {"type": "enabled"}},
        stream=False,
    )

5. The ReAct Main Loop

PLAINTEXT

复制

# ─────────────────────────────────────────
# 5. ReAct main loop
# ─────────────────────────────────────────
def run_agent(user_question: str, max_steps: int = 6):
    print(f"\n{'=' * 55}")
    print(f"Question: {user_question}")
    print(f"{'=' * 55}")

    # Conversation history, appended each round — this is the Agent's short-term memory
    messages = [
        {
            "role": "system",
            "content": (
                "Before answering, first judge whether a tool is needed to get information. "
                "When needed, call a tool and, after getting the result, give the final answer by synthesizing it. "
                "If the question can be answered directly, there is no need to call a tool. "
                "Please answer in English."
            ),
        },
        {"role": "user", "content": user_question},
    ]

    for step in range(max_steps):
        print(f"\n── Step {step + 1} ──────────────────────────────")

        # ① THINK: call the model and let it decide the next step
        response = call_model(messages)
        msg = response.choices[0].message

        # If thinking is enabled, print the model's reasoning process (optional, for debugging)
        if hasattr(msg, "reasoning_content") and msg.reasoning_content:
            preview = msg.reasoning_content[:150].replace("\n", " ")
            print(f"[THINK]   {preview}...")

        if msg.tool_calls:
            # ② ACT: the model decides to call a tool
            messages.append(msg)  # Save the model's decision into the history

            for tool_call in msg.tool_calls:
                fn_name = tool_call.function.name
                fn_args = json.loads(tool_call.function.arguments)

                print(f"[ACT]     Tool: {fn_name}")
                print(f"          Arguments: {fn_args}")

                # ③ OBSERVE: run the tool and get the real result
                if fn_name in TOOLS:
                    result = TOOLS[fn_name](**fn_args)
                else:
                    result = f"Tool {fn_name} does not exist"

                # Only print the first 200 characters to avoid flooding the screen
                preview = result[:200] + "..." if len(result) > 200 else result
                print(f"[OBSERVE] {preview}")

                # Append the tool result to the history; the model will see it in the next round
                messages.append(
                    {"role": "tool", "tool_call_id": tool_call.id, "content": result}
                )

        else:
            # The model thinks it has enough information and gives the final answer directly
            print(f"\n[Final answer]\n{msg.content}")
            return msg.content

    print("\n[Maximum steps reached, forcibly ended]")
    return "Reached the maximum number of reasoning steps without completing the task."



6. Test Run

PLAINTEXT

复制

# ─────────────────────────────────────────
# 6. Test run
# ─────────────────────────────────────────
if __name__ == "__main__":
    # Test 1: needs a real search
    # run_agent("What's the weather like today in Zhijin County, Bijie City, Guizhou Province?")

    # Test 2: search + calculation combined
    # run_agent("What's Tesla's stock price today? If I buy 100 shares, how many US dollars would I need?")

    # Test 3: answer directly, no tool needed
    # run_agent("Explain in one sentence what a ReAct Agent is")

    # Test 4: current time
    # run_agent("Current Beijing time")

    # Test 5: exchange rate test
    run_agent("How many Chinese yuan can 1000 US dollars get right now?")

Sample Run Results (Partial)

Pasted image 20260505115634

+

Pasted image 20260505115715

+

Pasted image 20260505115729

+

Copyright

Copyright of this article belongs to its author or rights holder. When reposting or quoting, please follow the applicable license terms and keep the link to this article.

Link to this article: https://xuyi.dev/en/2026-05-05-kpewur