I was building a customer support automation tool last month when I hit a wall. I had a working LLM call that could answer questions, but turning that into an actual agent—something that could remember context, call tools, and handle multi-step tasks—meant writing a mountain of glue code. I was manually managing conversation history, parsing function calls, and stitching together workflows. It was fragile, tedious, and frankly not what I wanted to spend my time on.
That's when I stumbled onto Microsoft Agent Framework (MAF). I was skeptical—another framework, another abstraction layer to learn—but the progressive tutorial structure caught my eye. It promised to build an agent one concept at a time, which is exactly how I like to learn. So I carved out an afternoon and dug in. Here's what I found, where I stumbled, and what actually worked.
The Setup: Getting the Foundation Right
MAF supports Python, C#, and Go. I went with Python since that's my daily driver. The Go version is still in public preview and is missing some features like declarative agents and RAG, so keep that in mind if Go is your thing.
First, I created a fresh virtual environment and installed the core package:
python -m venv maf-env
source maf-env/bin/activate
pip install microsoft-agent-framework
You'll also need an Azure OpenAI endpoint or an OpenAI API key. I set mine up as environment variables:
export AZURE_OPENAI_ENDPOINT="https://your-endpoint.openai.azure.com/"
export AZURE_OPENAI_API_KEY="your-key-here"
I actually tried using a standard OpenAI key first without the Azure prefix, and the framework threw a confusing authentication error. Turns out the default configuration assumes Azure OpenAI. If you're using standard OpenAI, you need to explicitly configure the client. That was my first "gotcha"—worth knowing upfront.
Step 1: Your First Agent (The "Hello World")
The first step is refreshingly simple: create an agent, invoke it, and stream the response. No tools, no memory, just a straight conversation.
from agent_framework import Agent
agent = Agent(
name="my-first-agent",
instructions="You are a helpful assistant that explains technical concepts simply."
)
response = await agent.invoke("What is a vector database?")
print(response)
That's it. A few lines of code and you get a streamed response. I appreciated that the tutorial started here instead of throwing a fully-featured agent at me on page one. It let me verify my environment was working before things got complicated.
One surprise: the invoke method is async. If you're running this in a regular Python script instead of a Jupyter notebook, you'll need asyncio.run(). The docs mention this, but I missed it on my first pass and spent ten minutes wondering why my script just hung without output.
Step 2: Adding Tools (Where It Gets Interesting)
A chatbot that just talks isn't really an agent. The next step teaches you how to give your agent function tools it can call. This is where MAF started saving me real time.
from agent_framework import Agent, tool
@tool
def get_order_status(order_id: str) -> str:
"""Look up the status of a customer order by its ID."""
# In reality, this would hit a database or API
return f"Order {order_id} is currently shipping and arrives in 2 days."
agent = Agent(
name="support-agent",
instructions="You are a customer support agent. Help customers with their orders.",
tools=[get_order_status]
)
response = await agent.invoke("Where is my order #12345?")
When I ran this, the agent automatically recognized it needed to call the tool, executed get_order_status, and incorporated the result into its response. No manual parsing of function call JSON, no routing logic. The framework handles the entire tool-calling loop.
I made a mistake here that cost me an hour: I forgot to add a docstring to my tool function. Without the docstring, the agent had no idea when to call the function, so it just guessed or ignored it. The docstring is how MAF generates the tool description for the LLM. Treat it like API documentation—be clear and specific.
Step 3: Multi-Turn Conversations
Once my agent could use tools, I needed it to handle back-and-forth conversations. Step 3 introduces sessions for maintaining conversation state.
from agent_framework import Agent, Session
agent = Agent(
name="support-agent",
instructions="You are a customer support agent.",
tools=[get_order_status]
)
session = Session(agent)
# First message
await session.invoke("Check on order #12345")
# Follow-up — the agent remembers the context
response = await session.invoke("When will it arrive?")
print(response) # Correctly references order #12345 without me repeating it
Before MAF, I was manually appending messages to a list and passing the entire history back to the LLM on every call. Sessions abstract all of that away. The framework manages the message history, tool call results, and assistant responses behind the scenes.
The key insight: a Session object is stateful, so if you're building a web service, you need one session per user conversation. Don't try to share a single session across multiple users—I tried that during testing and got some very confused responses.
Step 4: Memory & Persistence
Sessions handle short-term conversation memory, but what about persistent context that should be available across sessions? Step 4 covers context providers.
from agent_framework import Agent, ContextProvider
class UserProfileProvider(ContextProvider):
async def get_context(self, user_id: str) -> str:
# Fetch from database in production
return f"User is a premium member since 2022. Preferred language: English."
agent = Agent(
name="support-agent",
instructions="You are a customer support agent. Personalize responses based on user context.",
context_providers=[UserProfileProvider()]
)
Context providers inject information into the agent's system prompt at invocation time. This is perfect for user profiles, account details, or any data the agent should always "know" about the current user.
I initially tried shoving everything into the instructions parameter, which got messy fast. Context providers keep the base instructions clean while dynamically injecting relevant data. It's a separation of concerns that makes your agents much more maintainable.
Step 5 & 6: Workflows and the Agent Harness
This is where MAF really differentiates itself. Step 5 introduces deterministic workflows for composing multi-step processes, and Step 6 adds the "harness" agent that can plan and track complex tasks.
Workflows let you chain agents together in a defined sequence—Agent A processes input, passes results to Agent B, and so on. The harness agent takes this further by acting as an orchestrator that can break down complex requests, delegate to specialized agents, and track progress.
I haven't built anything production-ready with the harness yet, but I experimented with a simple research workflow: one agent finds information, another summarizes it, and a third formats the output. The workflow syntax is clean:
from agent_framework import Workflow, Step
workflow = Workflow(
steps=[
Step(agent=research_agent, name="research"),
Step(agent=summary_agent, name="summarize"),
Step(agent=format_agent, name="format")
]
)
result = await workflow.invoke("Explain quantum computing for a blog post")
The output from each step automatically feeds into the next. You can also add conditional logic and branching, though I haven't needed that yet.
Step 7: Hosting Your Agent
The final step covers exposing your agent via hosting infrastructure. This is where you go from a local script to something users can actually interact with. The framework provides integration with Azure hosting options, making it straightforward to deploy your agent as an API endpoint.
I deployed a test agent to Azure Container Apps in about 20 minutes following the tutorial. The hosting setup was smoother than I expected, though you'll need some familiarity with Azure infrastructure.
Honest Assessment and Practical Tips
After spending a weekend with MAF, here's my take:
What I liked:
- The progressive tutorial structure is genuinely excellent. Each step builds on the last without overwhelming you.
- Tool calling "just works" once you set it up correctly. The automatic loop handling saved me significant code.
- Sessions and context providers solve real problems cleanly.
Limitations to be aware of:
- The framework is still evolving. I hit a few rough edges, particularly around error messages that weren't helpful.
- The Go version is missing significant features compared to Python and C#.
- Documentation outside the core tutorial is sparse. When I wanted to customize tool call behavior, I ended up reading source code.
- If you're not on Azure, setup requires extra configuration. The default assumptions are Azure-centric.
Tips from my experience:
- Always add detailed docstrings to your tool functions. This is non-negotiable.
- Use
asyncio.run()if you're running outside Jupyter. - One session per user conversation—don't share state.
- Start with Step 1 and work through sequentially. Skipping ahead will confuse you.
- Keep your agent instructions focused and specific. Vague instructions produce vague behavior.
MAF isn't the only agent framework out there, and for simple use cases it might be overkill. But if you're building something that needs tools, conversation memory, and multi-step workflows—and especially if you're already in the Azure ecosystem—it's worth a serious look. The progressive approach means you can adopt it incrementally without a massive upfront investment.