AI Engineering & LLMs (50 Qs)•Part 3: Agentic Workflows, Function Calling & MCP
The ReAct Pattern (Reason + Act): Thought-Action-Observation Loop & Production Trade-Offs
1. The ReAct (Reasoning + Acting) Paradigm
Introduced by Yao et al. (Google Research / Princeton), ReAct interleaves reasoning traces ("Thoughts") with task-specific actions ("Tool Calls") and real-world feedback ("Observations"):
vbnet
User Goal / Prompt
│
▼
┌─────────────────────────────────┐
│ Thought: Formulate hypothesis & │
│ next action step │
└────────────────┬────────────────┘
│
▼
┌─────────────────────────────────┐
│ Action: Call tool (Search, SQL, │
│ Calculator, API) │
└────────────────┬────────────────┘
│
▼
┌─────────────────────────────────┐
│ Observation: Receive real-world │
│ tool output / error │
└────────────────┬────────────────┘
│
▼
Is goal achieved or max steps reached?
├─────────► YES ──► Final Answer to User
└─────────► NO ──► Loop back to Thought
2. Failure Modes & Mitigations in Production ReAct Agents
| Failure Mode | Manifestation | Production Mitigation |
|---|---|---|
| Action Parsing Failure | The model outputs invalid JSON or malformed tool call syntax. | Use native constrained JSON decoding (Grammars) instead of regex string parsing. |
| Infinite Feedback Loop | The model repeats the exact same failing tool call with identical arguments. | Cycle detection in memory state: terminate loop if action signature repeats 3x. |
| Context Window Bloat | Each tool observation adds 1,000+ tokens, rapidly exhausting context limits. | Summarize or truncate large observation outputs before appending to the scratchpad. |
| Cascading Error Propagation | An early hallucinated thought causes subsequent actions to drift off-track. | Implement intermediate reflection or verification steps before irreversible writes. |
3. Production ReAct Execution Engine in Python
python
from typing import Dict, Any, Callable
class ReActAgent:
def __init__(self, llm_client, tools: Dict[str, Callable]):
self.llm = llm_client
self.tools = tools
self.max_steps = 5
async def run(self, query: str) -> str:
scratchpad = f"User Query: {query}\n"
for step in range(self.max_steps):
prompt = f"{scratchpad}\nThink step-by-step. Output in format:\nThought: <reasoning>\nAction: <tool_name>[<args>] OR Final Answer: <response>"
response = await self.llm.generate(prompt)
scratchpad += f"\n{response}"
if "Final Answer:" in response:
return response.split("Final Answer:")[1].strip()
# Parse action and execute tool
action, args = self._parse_action(response)
if action in self.tools:
try:
observation = await self.tools[action](args)
except Exception as err:
observation = f"Error executing {action}: {str(err)}"
else:
observation = f"Tool {action} does not exist."
scratchpad += f"\nObservation: {observation}"
return "Agent reached maximum step limit without resolving query."