# Dspy React

> Build tool-using agents with dspy.ReAct - the Reasoning-Action-Observation loop for interleaved thinking and action. Use when the task requires calling external tools or APIs to gather information - multi-step tool use with reasoning, like searching databases, calling APIs, or combining multiple data sources. Common scenarios - building agents that search the web and synthesize results, multi-step information gathering from APIs, chatbots that look up data before answering, question answering that requires external knowledge, or any task needing interleaved reasoning and action. Related - ai-taking-actions, ai-searching-docs, dspy-codeact, dspy-tools, dspy-mcp. Also used for dspy.ReAct, ReAct agent pattern, reasoning and acting loop, tool-using agent in DSPy, search then answer pattern, agent with tools, multi-step tool use, interleave thinking and acting, API-calling agent, agent that reasons about tool outputs, when to use ReAct vs CodeAct, build intelligent agent with DSPy.

- Skill: `lebsral/dspy-react` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add lebsral/dspy-react`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lebsral/dspy-react/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: lebsral (https://skillmd.com/u/lebsral)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lebsral/dspy-react

---


# Build Tool-Using Agents with dspy.ReAct

Guide the user through building agents that reason step-by-step and call tools to accomplish tasks. `dspy.ReAct` implements the Reasoning-Action-Observation loop -- the agent thinks about what to do, calls a tool, observes the result, and repeats until it has an answer.

## Gather context first

Before generating code, ask:

1. **What tools does the agent need?** Describe what each tool does and what data source it connects to (e.g., "a web search tool that calls the Serper API", "a database lookup for user accounts by email").
2. **How many steps might the agent need per query?** One or two lookups (simple) or multi-step research chains (complex)? This determines `max_iters`.
3. **What are the agent's inputs?** Just a question, or also context like user ID, session state, or a system prompt?
4. **What outputs do you need?** A plain answer field, or structured fields like `confidence`, `sources`, or `category`?

## What is ReAct

`dspy.ReAct` implements the Reasoning-Action-Observation loop as an optimizable module. The agent reasons about what to do, calls a tool, observes the result, and repeats until it has enough information to answer. DSPy handles the loop mechanics and prompt construction.

## When to use ReAct

| Use ReAct when... | Use something else when... |
|--------------------|----------------------------|
| The agent needs to call external tools (search, APIs, databases) | You just need input -> output with no tools (`dspy.ChainOfThought`) |
| Multi-step reasoning with real-world data | The task is purely computational / code-heavy (`dspy.CodeAct`) |
| You want the agent to decide which tools to call and in what order | You have a fixed pipeline of steps (`dspy.Module` with sub-modules) |
| You need an interpretable trace of reasoning + actions | You need agents coordinating with each other (see `/ai-coordinating-agents`) |

## Defining tools

Tools are Python functions with type hints and docstrings. DSPy uses the function signature and docstring to tell the agent what each tool does and how to call it.

```python
def search(query: str) -> str:
    """Search the web for information about a topic."""
    # Your search implementation here
    return "search results..."

def calculate(expression: str) -> float:
    """Evaluate a mathematical expression and return the result."""
    return eval(expression)  # use a safe evaluator in production

def get_weather(city: str) -> str:
    """Get the current weather for a city."""
    # Your weather API call here
    return f"72°F and sunny in {city}"
```

**Tool requirements:**
- **Type hints** on all parameters and the return type -- DSPy uses these to generate the tool schema
- **Docstring** explaining what the tool does -- the agent reads this to decide when to use it
- **Return a string** (or something that converts to string) -- the result becomes the Observation

Keep tools focused on one thing. A `search` tool should search, not search-and-summarize.

## Basic ReAct agent

```python
import dspy

lm = dspy.LM("openai/gpt-4o-mini")  # or "anthropic/claude-sonnet-4-5-20250929", etc.
dspy.configure(lm=lm)

def search(query: str) -> str:
    """Search for information about a topic."""
    return "DSPy is a framework for programming language models."

agent = dspy.ReAct("question -> answer", tools=[search])
result = agent(question="What is DSPy?")
print(result.answer)
```

That's it. The agent will:
1. Read the question
2. Decide whether to call `search`
3. Use the search result to formulate an answer

## Constructor parameters

```python
dspy.ReAct(
    signature,      # str | Signature -- required, defines inputs/outputs
    tools,          # list[Callable | dspy.Tool] -- required, available tools
    max_iters=20,   # int -- max reasoning-action cycles
)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `signature` | `str \| type[Signature]` | required | Input/output contract (inline or class-based) |
| `tools` | `list[Callable \| dspy.Tool]` | required | Functions the agent can call. DSPy wraps plain functions automatically |
| `max_iters` | `int` | `20` | Max Thought-Action-Observation cycles before forcing an answer |

## The max_iters parameter

`max_iters` controls how many Thought-Action-Observation cycles the agent can take before it must produce an answer:

```python
# Simple lookup -- 1-2 tool calls usually enough
agent = dspy.ReAct("question -> answer", tools=[search], max_iters=3)

# Complex research -- may need many tool calls
agent = dspy.ReAct("question -> answer", tools=[search, lookup], max_iters=8)
```

Guidelines:
- **Default is 20** -- usually fine for most tasks
- **Set it lower** (2-5) for simple lookups where one or two tool calls suffice
- **Keep it at 20** for complex multi-step research tasks
- If the agent hits `max_iters` without finishing, it returns its best answer so far

## Multi-tool agents

Give the agent multiple tools and it decides which to use and when:

```python
import dspy

def search(query: str) -> str:
    """Search the web for general information."""
    return "search results..."

def lookup_user(email: str) -> str:
    """Look up a user account by email address."""
    return '{"name": "Alice", "plan": "pro", "status": "active"}'

def check_order(order_id: str) -> str:
    """Check the status of an order by its ID."""
    return '{"order_id": "12345", "status": "shipped", "eta": "March 20"}'

agent = dspy.ReAct(
    "question -> answer",
    tools=[search, lookup_user, check_order],
    max_iters=5,
)

# The agent picks the right tool based on the question
result = agent(question="What's the status of order 12345?")
print(result.answer)  # Uses check_order

result = agent(question="What plan is alice@example.com on?")
print(result.answer)  # Uses lookup_user
```

The agent can also chain tools -- call `lookup_user` first, then use the result to call `check_order`.

## Wrapping ReAct in a custom module

For production use, wrap `dspy.ReAct` inside a `dspy.Module` to add pre-processing, context, or post-processing:

```python
class SupportAgent(dspy.Module):
    def __init__(self):
        self.agent = dspy.ReAct(
            "question, context -> answer",
            tools=[search, lookup_user, check_order],
            max_iters=6,
        )

    def forward(self, question):
        context = (
            "You are a customer support agent. "
            "Use lookup_user for account questions, "
            "check_order for order questions, "
            "and search for general questions."
        )
        return self.agent(question=question, context=context)

def support_reward(args, pred):
    if len(pred.answer) > 20:
        return 1.0
    return 0.0  # Response too short — not detailed enough

validated_support = dspy.Refine(
    module=SupportAgent(),
    N=3,
    reward_fn=support_reward,
    threshold=1.0,
)
```

This pattern lets you:
- Pass extra context or instructions to the agent
- Add reward-based quality constraints with `dspy.Refine`
- Optimize the agent with DSPy optimizers (they tune the inner ReAct module)
- Save and load the optimized state

## Using class-based signatures

For agents with typed inputs and outputs, use a class-based signature:

```python
from typing import Literal

class ResearchTask(dspy.Signature):
    """Research a topic and provide a comprehensive answer with sources."""
    question: str = dspy.InputField(desc="The research question")
    answer: str = dspy.OutputField(desc="A thorough answer to the question")
    confidence: Literal["high", "medium", "low"] = dspy.OutputField(
        desc="Confidence level based on the sources found"
    )

agent = dspy.ReAct(ResearchTask, tools=[search], max_iters=5)
result = agent(question="What are the main features of DSPy?")
print(result.answer)
print(result.confidence)
```

## ReAct vs CodeAct

Both are agent modules, but they act differently:

| | ReAct | CodeAct |
|---|-------|---------|
| **How it acts** | Calls tools by name with arguments | Writes and executes Python code |
| **Best for** | API calls, database lookups, search | Data manipulation, calculations, file I/O |
| **Interpretability** | Clear tool call trace | Full code trace |
| **Tool style** | Function calls | Python expressions |
| **Use when** | You have specific tools to call | The task is better solved by writing code |

```python
# ReAct -- calls tools
agent = dspy.ReAct("question -> answer", tools=[search, calculate])

# CodeAct -- writes code
agent = dspy.CodeAct("question -> answer", tools=[search, calculate])
```

If you're unsure, start with ReAct. Switch to CodeAct if the agent needs to do math, string manipulation, or data transformations between tool calls.

> **Upcoming in DSPy 3.3.0 (beta):** `ReActV2` will add native tool calling via the model's built-in tool-calling API. Not yet stable -- audit against DSPy 3.2.1 for production use.

## Error handling

Tools can fail. Handle errors inside your tools so the agent gets a useful message instead of a crash:

```python
import requests

def search(query: str) -> str:
    """Search the web for information."""
    try:
        response = requests.get(
            "https://api.example.com/search",
            params={"q": query},
            timeout=5,
        )
        response.raise_for_status()
        return response.json()["results"]
    except requests.Timeout:
        return "Error: Search timed out. Try a simpler query."
    except requests.HTTPError as e:
        return f"Error: Search failed with status {e.response.status_code}."
    except Exception as e:
        return f"Error: {str(e)}"
```

When a tool returns an error string, the agent sees it as an Observation and can decide to retry with different arguments, try a different tool, or give a partial answer.

For module-level error handling, wrap the agent call:

```python
class SafeAgent(dspy.Module):
    def __init__(self):
        self.agent = dspy.ReAct("question -> answer", tools=[search], max_iters=5)
        self.fallback = dspy.ChainOfThought("question -> answer")

    def forward(self, question):
        try:
            return self.agent(question=question)
        except Exception:
            # Fall back to answering without tools
            return self.fallback(question=question)
```

## Optimizing ReAct agents

ReAct agents are optimizable like any DSPy module. The optimizer tunes the reasoning prompts so the agent makes better tool-calling decisions:

```python
def answer_metric(example, prediction, trace=None):
    return prediction.answer.strip().lower() == example.answer.strip().lower()

# BootstrapFewShot for quick optimization
optimizer = dspy.BootstrapFewShot(metric=answer_metric, max_bootstrapped_demos=4)
optimized_agent = optimizer.compile(agent, trainset=trainset)

# MIPROv2 for better prompt optimization
optimizer = dspy.MIPROv2(metric=answer_metric, auto="medium")
optimized_agent = optimizer.compile(agent, trainset=trainset)

# Save and load
optimized_agent.save("optimized_agent.json")
```

Verify improvement: run `dspy.Evaluate` on a held-out devset before and after optimization. On tool-calling tasks with clear right/wrong answers (exact-match metrics), `BootstrapFewShot` typically gains 8-15 percentage points by teaching the agent better tool-selection patterns through examples; `MIPROv2` can add a further 5-12 points by optimizing the reasoning instructions.

## Debugging

Inspect what the agent is doing:

```python
# See the last few LM calls (thoughts, tool calls, observations)
dspy.inspect_history(n=5)

# Print the module structure
print(agent)
```

`inspect_history` shows you the full Thought-Action-Observation trace, which is invaluable for understanding why the agent called certain tools or gave a wrong answer.

## Gotchas

1. **Claude sets `max_iters=5` but the default is 20.** Claude habitually passes `max_iters=5` which cuts off complex multi-step tasks too early. The actual default is 20. Only lower it when you want to constrain simple tasks (2-3 for lookups). For complex research, the default of 20 is appropriate.
2. **Claude writes tools that raise exceptions instead of returning error strings.** When a tool raises an exception, the ReAct loop crashes -- the agent cannot recover. When a tool returns an error string (e.g., `"Error: user not found with email alice@example.com"`), the agent sees it as an Observation and can retry with different arguments or switch tools. Always wrap tool logic in try/except and return descriptive error strings, never raise.
3. **Claude adds `reasoning` or `trajectory` as signature fields.** DSPy injects these fields internally for the Thought-Action-Observation trace. Adding them to your signature causes field conflicts and duplicate output. Never declare `reasoning`, `trajectory`, `next_thought`, `next_tool_name`, or `next_tool_args` in a ReAct signature -- only declare actual input and output fields like `question` and `answer`.
4. **Claude writes tool docstrings like code comments, not like agent instructions.** The agent reads each docstring to decide when and how to call that tool. `"Wraps the requests library to call the search API"` gives the agent no guidance. Write action-oriented docstrings: `"Search for information about a topic. Use when you need current facts, definitions, or data not in your training."` Keep docstrings to 1-2 sentences -- they appear in every iteration prompt, so verbose docstrings inflate token usage linearly with `max_iters`.
5. **Claude ignores the `trajectory` in the return value.** `ReAct` returns a `dspy.Prediction` with a `.trajectory` dict containing the full Thought-Action-Observation trace. Access `result.trajectory` to log or debug the agent's reasoning path. Claude often discards this and only uses `result.answer`.

## Additional resources

- [dspy.ReAct API docs](https://dspy.ai/api/modules/ReAct/)
- For API details, see [reference.md](reference.md)
- For worked examples, see [examples.md](examples.md)

## Cross-references

> Install any skill: `npx skills add lebsral/DSPy-Programming-not-prompting-LMs-skills --skill <name>`

- **Defining tools** in detail -- see `/dspy-tools`
- **MCP tools** for wiring external MCP servers into ReAct agents -- see `/dspy-mcp`
- **CodeAct** for code-based agents -- see `/dspy-codeact`
- **Building custom modules** to wrap ReAct -- see `/dspy-modules`
- **Action-taking AI** from a problem-first perspective -- see `/ai-taking-actions`
- **Multi-agent coordination** -- see `/ai-coordinating-agents`
- **Install `/ai-do` if you do not have it** — it routes any AI problem to the right skill and is the fastest way to work: `npx skills add lebsral/DSPy-Programming-not-prompting-LMs-skills --skill ai-do`

