Building Autonomous Agents in Azure: A Tool-First Approach
StackMindset Team
Mon Feb 09 2026
The biggest shift in Gen AI right now is moving from Chatbots (Passively answering) to Agents (Actively doing). An Agent uses an LLM as a reasoning engine to decide which tools to call.
But deploying agents to production is tricky. Loops can get stuck. State is lost. Tools fail.
Here is our battle-tested architecture for deploying AI Agents on Azure.
1. The Core: Function Calling with OpenAI
Azure OpenAI models (GPT-4o, GPT-3.5-Turbo) support Function Calling. You define a JSON schema for your tools, and the model returns structured JSON arguments instead of text.
The Agent Loop
- User: “Check the status of order #12345.”
- LLM: Thought: I need the
check_order_statustool. -> Action:{"order_id": "12345"} - System: Executes
check_order_status("12345")-> Returns “Shipped” - LLM: Observation: Order is shipped. -> Response: “Your order #12345 has shipped!“
2. Defining Tools in Python
We encapsulate business logic as Pydantic models (for validation).
from langchain.tools import tool
from pydantic import BaseModel, Field
class CheckOrderInput(BaseModel):
order_id: str = Field(description="The unique order ID (e.g., #12345)")
@tool("check_order_status", args_schema=CheckOrderInput)
def check_order_status(order_id: str) -> str:
"""Check the shipping status of an order."""
# Simulate API call
return f"Order {order_id} is SHIPPED via UPS."
3. Hosting Agents on Azure Durable Functions
An agent conversation can last minutes or hours. HTTP timeouts kill standard Azure Functions.
Use Durable Functions (Orchestrator Pattern):
- Stateful: Remembers conversation history automatically.
- Resilient: If the function crashes, it replays from last checkpoint.
- Async: Can wait for long-running tool execution (e.g., “Generate Report”).
Architecture Pattern
import azure.durable_functions as df
def orchestrator_function(context: df.DurableOrchestrationContext):
history = context.get_input()
user_message = history[-1]
# Call LLM via Activity Function
agent_response = yield context.call_activity("CallLLM", history)
if agent_response.get("tool_calls"):
# Parallel Tool Execution (Fan-out)
tasks = []
for tool_call in agent_response["tool_calls"]:
tasks.append(context.call_activity(tool_call["name"], tool_call["args"]))
tool_outputs = yield context.task_all(tasks)
# Append outputs to history and loop back (recursion)
history.append({"role": "tool", "content": tool_outputs})
context.continue_as_new(history)
return agent_response["content"]
4. Human-in-the-Loop (Approval)
Crucial for enterprise agents. Before taking a high-risk action (e.g., “Refund User”), the agent must pause.
Durable Functions Implementation:
if tool_name == "refund_user":
yield context.wait_for_external_event("ManagerApproval")
The agent literally pauses execution for days until a manager clicks “Approve” in a dashboard.
5. Security Guardrails
Agents are unpredictable.
- Never allow
drop tabletool. Read-only tools by default. - Limit loops: Set
max_iterations=5to prevent infinite loops burning tokens. - System Prompt: Always instruct the agent what it cannot do.
Conclusion
Building a production agent combines DevOps (State management, Retries) with Prompt Engineering. By using Azure Durable Functions, we solve the hardest part of agent development: reliability and long-running state.
Written by StackMindset
We build autonomous agents and robust CI/CD pipelines to help developers ship better software, faster.