Build GenAI Solutions with Microsoft Foundry
Microsoft Foundry is the central platform for building generative AI solutions in Azure. This topic covers how Foundry is organised, how to implement Prompt Flow pipelines, how to build a RAG solution that grounds your AI in real data, and how to evaluate your models before deployment.
This topic uses from Part 1: endpoint, API key, managed identity, resource group, environment variable, os.environ.get(), content filters, and prompt shields. New concepts below are all introduced before they appear in code.
Part 2 terms — introduced before any code
AI that creates new content — text, images, audio, code — rather than just analysing or classifying existing content. Large language models (LLMs) like GPT-4o are generative AI. They predict the most likely next token given a prompt.
An AI model trained on massive amounts of text data. It learns patterns in language and can generate coherent, contextually relevant text. GPT-4o, GPT-3.5-turbo are LLMs. They do not "know" facts — they predict likely text based on patterns they learned during training.
The basic unit of text that an LLM processes. A token is roughly 4 characters or ¾ of a word. "Hello world" = ~2 tokens. A typical sentence ≈ 15–20 tokens. LLMs have a maximum context window (maximum total tokens for input + output). Costs are billed per token.
The maximum number of tokens an LLM can process in one API call — both the input (your prompt + conversation history) and the output (the model's response). GPT-4o supports up to 128,000 tokens. Exceeding the context window causes an error.
The text you send to an LLM to get a response. A prompt can be a question, an instruction, a piece of text to summarise, code to fix, or any combination. The quality of your prompt directly determines the quality of the model's output.
When an LLM confidently generates factually incorrect or completely made-up information. LLMs don't "check facts" — they predict likely text. RAG (Retrieval-Augmented Generation) is the primary technique for reducing hallucination by grounding the model in real source data.
The practice of providing an LLM with specific, verified information as part of the prompt so its response is based on that data rather than its training knowledge. A grounded model is less likely to hallucinate because it has real facts to reference.
A pattern where relevant information is retrieved from a data store (e.g. Azure AI Search index) and added to the prompt before sending it to the LLM. The model generates a response grounded in the retrieved documents rather than relying purely on its training data.
A numerical representation of text as a list of numbers (a vector). Similar pieces of text produce similar vectors. Embeddings allow you to find semantically similar content — e.g. "car accident" would have a similar embedding to "vehicle collision" even though the words differ.
A database optimised for storing and searching embeddings (vectors). When you query a vector store with a question, it finds the stored documents whose embeddings are closest to the question's embedding — this is semantic search. Azure AI Search supports vector indexes.
The process of splitting large documents into smaller pieces (chunks) before embedding them. Necessary because LLMs have context windows. A chunk is typically 256–1,024 tokens. The right chunk size balances context (bigger chunks = more context) vs precision (smaller chunks = more targeted retrieval).
A Microsoft Foundry feature for building multi-step AI pipelines visually. A flow connects nodes — LLM calls, Python code, search queries, prompts — into a sequence. You test, evaluate, and deploy flows as endpoints. Think of it as a flowchart where each box does something with AI.
A reusable prompt structure with placeholder variables. For example:
"Summarise the following document in {{language}}: {{document}}".
At runtime, the variables are filled in with actual values.
Foundry uses Jinja2 syntax (double curly braces) for templates.
A quality score measuring whether the model's response is factually supported by the source documents provided. A response is grounded if every claim in it can be traced to the provided context. Foundry's evaluation tool measures this automatically.
The collection of AI models available in Microsoft Foundry. Includes Azure OpenAI models (GPT-4o, DALL-E), open-source models from Hugging Face (Mistral, Llama), and Microsoft's own models (Phi-3). You browse, compare, and deploy models directly from the catalog.
How Foundry is organised for GenAI work
From Part 1 Topic 1 you know Foundry has three levels: Hub → Project → Services. Now let's go deeper into what each level means for building GenAI solutions.
The Hub is the university building — shared facilities:
power, network, storage, compute, and security. All labs in the building
share these.
A Project is a specific research lab inside the building —
it has its own experiments, data, and models. One building can have many labs.
Researchers from the same lab share access. Researchers from different labs
can be kept separate.
Connections are the cables and pipes from the building's
utilities into your lab — connections to Azure AI Search, Azure OpenAI,
Blob Storage, etc. You configure these once at the Hub level and all
Projects can use them.
What lives where in Foundry
| Level | What you configure here | Who manages it |
|---|---|---|
| Hub | Storage account, compute clusters, network settings (VNet/private endpoint), shared connections to Azure OpenAI and AI Search, RBAC for the whole hub | Platform admins / IT team |
| Project | Model deployments, prompt flows, evaluations, datasets, fine-tuning jobs, API endpoints for your specific application | AI developers / data scientists |
| Connection | Named references to external services — "my-openai-connection" pointing to an Azure OpenAI resource, "my-search-connection" pointing to Azure AI Search | Set up once by admins, used by developers in flows |
Creating a Hub and Project — portal walkthrough
Sign in with your Azure account. This is the Microsoft Foundry portal — separate from portal.azure.com (the general Azure portal).
Click + New Hub → Name it → Select your subscription and resource group → Choose a region → Click Create. Azure creates the hub with a default storage account and key vault.
Inside your Hub → Click + New Project → Name it → Create. The project is your workspace for building your GenAI solution.
Hub settings → Connections → + New Connection → Select Azure OpenAI → Choose your Azure OpenAI resource → Save. Now all projects in this hub can use this connection.
Your project → Models + endpoints → + Deploy model → Choose from Model Catalog (e.g. gpt-4o) → Set deployment name → Choose deployment type → Deploy.
Choosing the right model for your use case
The Model Catalog in Foundry contains hundreds of models. The exam tests whether you can choose the right model for a given scenario. These are the key model families you need to know:
| Model / Family | Provider | Best for | Choose when |
|---|---|---|---|
gpt-4o |
OpenAI (via Azure) | Chat, complex reasoning, multimodal (text + images) | High quality needed, budget allows, or images are part of input |
gpt-35-turbo |
OpenAI (via Azure) | Chat, summarisation, classification | Cost-effective, high volume, simpler tasks |
text-embedding-ada-002 |
OpenAI (via Azure) | Converting text to vectors for semantic search | Any RAG pipeline, vector search, or similarity task |
dall-e-3 |
OpenAI (via Azure) | Generating images from text prompts | Any image generation requirement |
Phi-3 |
Microsoft | Small, efficient language model for edge/device deployment | Low-latency edge scenarios, lower compute requirements |
Mistral / Llama |
Mistral AI / Meta | Open-source alternatives to GPT models | Open-source preference, specific licensing requirements |
"Company needs to process uploaded images and describe what is in them."
→ gpt-4o (multimodal — understands images)
"Company needs to implement semantic search over 500,000 documents."
→ text-embedding-ada-002 (create embeddings for vector search)
"Company needs to generate product photos from text descriptions."
→ dall-e-3 (image generation)
"Company needs a low-cost high-volume customer service chatbot."
→ gpt-35-turbo (most cost-effective for chat)
Building multi-step AI pipelines
A single API call to an LLM is often not enough for real-world AI applications. You typically need to: retrieve information, format a prompt, call the model, process the output, and maybe call the model again. Prompt Flow lets you build these multi-step pipelines visually and in code.
Think of Prompt Flow as a factory assembly line. Raw material (user question) comes in at one end. It passes through multiple stations — Station 1 searches your database for relevant documents, Station 2 formats those documents into a prompt, Station 3 sends the prompt to GPT-4o, Station 4 checks the response for safety — and a finished product (answer) comes out the other end. Each station is a node in your flow.
Types of nodes in a Prompt Flow
Calls an LLM (e.g. GPT-4o) with a prompt template. Returns the model's text response. This is the core node in most flows.
Runs custom Python code — format data, call external APIs, parse JSON, apply business logic. Gives you full flexibility between LLM calls.
Queries Azure AI Search to retrieve relevant documents based on a user's question. Used as the "retrieval" step in RAG patterns.
A Jinja2 template that formats inputs into a prompt string. Takes variables from previous nodes and combines them into the prompt you'll send to the LLM.
Flow types you need to know
| Flow Type | What it does | Use case |
|---|---|---|
| Standard Flow | A linear sequence of nodes — runs once from start to finish | Single-turn Q&A, document summarisation |
| Chat Flow | A flow designed for multi-turn conversations — maintains conversation history | Chatbots, virtual assistants |
| Evaluation Flow | A special flow that assesses the quality of another flow's outputs using metrics | Testing groundedness, relevance, coherence |
Prompt templates — Jinja2 syntax
Foundry uses Jinja2 for prompt templates. You put variable names
inside double curly braces: {{variable_name}}.
At runtime Foundry replaces these with actual values from previous nodes.
# This is a Jinja2 prompt template (not Python code — it's a template string)
# Used inside a Foundry Prompt Template node
system:
You are a helpful customer service assistant for Contoso Bank.
Answer questions only based on the provided context.
If the answer is not in the context, say "I don't have that information."
user:
Context from our knowledge base:
# {{context}} is a Jinja2 variable — replaced at runtime with retrieved documents
{{context}}
Customer question:
# {{user_question}} is replaced with the actual question the user asked
{{user_question}}
# At runtime this becomes:
# system: You are a helpful customer service assistant...
# user: Context from our knowledge base:
# [retrieved document text here]
# Customer question: What is the overdraft fee?
Grounding your model in real data
RAG (Retrieval-Augmented Generation) is the most important pattern in enterprise AI. Instead of relying on the LLM's training knowledge (which may be outdated or simply wrong), you retrieve relevant documents from your own data and include them in the prompt. The model answers based on your data.
Imagine asking GPT-4o "What is our company's refund policy?"
The model has never seen your internal policy document — it will either
admit it doesn't know, or worse, hallucinate a plausible-sounding answer.
With RAG: your refund policy document is stored in a vector index.
When the user asks the question, the system retrieves the relevant
paragraph from the policy document and includes it in the prompt.
The model reads the actual policy and answers accurately.
RAG = retrieval + generation. Retrieval finds the facts. Generation
articulates them in natural language.
The 5-step RAG pipeline
Load your source documents (PDFs, Word files, web pages) and split them into chunks. Each chunk is a small piece of text — typically 256 to 1,024 tokens. Chunks that are too small lose context. Chunks that are too large may exceed the model's context window when multiple are retrieved.
Send each chunk to the embedding model (text-embedding-ada-002).
It converts the text into a list of numbers (a vector) that represents
its meaning. Store both the original text and its vector in Azure AI Search
(which supports vector indexes).
When a user asks a question, send that question to the same embedding model. This converts the question into a vector in the same "semantic space" as your document chunks.
Query Azure AI Search with the question's vector. It finds the chunks whose vectors are most similar (closest in vector space) to the question. These are the most semantically relevant documents. Return the top K results (e.g. top 3 chunks).
Combine the retrieved chunks and the user's question into a prompt: "Using only the following documents, answer this question: [question]. Documents: [chunk 1, chunk 2, chunk 3]." Send to GPT-4o. The model generates an answer based on the actual documents.
Python — Building a simple RAG call
New Python concept introduced here: dictionary.
A dictionary stores key-value pairs — like a lookup table.
{"key": "value"}. Explained fully in the code comments.
Install: pip install openai azure-search-documents azure-identity
import os
from openai import AzureOpenAI # Azure OpenAI client
from azure.search.documents import SearchClient # Azure AI Search client
from azure.core.credentials import AzureKeyCredential
# ── Step 1: Set up Azure OpenAI client ────────────────────────────────
# AzureOpenAI connects to YOUR Azure OpenAI resource (not public OpenAI)
openai_client = AzureOpenAI(
azure_endpoint = os.environ.get("AZURE_OPENAI_ENDPOINT"), # Your Azure OpenAI endpoint
api_key = os.environ.get("AZURE_OPENAI_KEY"), # Your Azure OpenAI key
api_version = "2024-02-01" # API version — always specify this
)
# ── Step 2: Set up Azure AI Search client ─────────────────────────────
search_client = SearchClient(
endpoint = os.environ.get("SEARCH_ENDPOINT"), # Your search endpoint (search.windows.net)
index_name = "documents-index", # The index where your chunks are stored
credential = AzureKeyCredential(os.environ.get("SEARCH_KEY"))
)
# ── Step 3: Embed the user's question ─────────────────────────────────
user_question = "What is the company's refund policy for digital products?"
# Call the embedding model to convert the question into a vector (list of numbers)
# embeddings.create() is a function on the client
# model= specifies the deployment name of your embedding model
# input= is the text to embed
embedding_response = openai_client.embeddings.create(
model = "text-embedding-ada-002", # Your embedding model deployment name
input = user_question
)
# .data[0].embedding extracts the vector (list of numbers) from the response
# .data is a list of results (we sent one input, so data[0] is our result)
# .embedding is the actual list of ~1536 floating point numbers
question_vector = embedding_response.data[0].embedding
# ── Step 4: Search for the most relevant document chunks ──────────────
# New concept: dictionary — a collection of key-value pairs in curly braces {}
# Like a real dictionary: "word" (key) → "definition" (value)
# Here: "vector" is the key, question_vector is the value
vector_query = {
"vector": question_vector, # The embedding of the user's question
"fields": "content_vector", # Which field in the index stores the document vectors
"k": 3 # Return the top 3 most similar chunks
}
# search() queries the Azure AI Search index using the vector
results = search_client.search(
search_text = "", # Empty string = vector-only search (no keyword search)
vector_queries = [vector_query], # Pass our vector query
select = ["content"] # Only return the "content" field from each chunk
)
# Build a single string from all retrieved chunks
# For loop joins all chunk texts with two newlines between them
context_text = "\n\n".join([result["content"] for result in results])
# ── Step 5: Send the grounded prompt to the LLM ───────────────────────
# New concept: list of dictionaries — a list where each item is a dictionary
# The messages array tells the model the conversation so far
# Each message is a dictionary with "role" and "content" keys
messages = [
{
"role": "system", # "system" sets the model's behaviour and persona
"content": "You are a helpful assistant. Answer questions using ONLY the provided context. "
"If the answer is not in the context, say you don't know."
},
{
"role": "user", # "user" is the human's message
"content": f"Context:\n{context_text}\n\nQuestion: {user_question}"
# f-string (from Python) inserts variables: context_text and user_question
# The model sees the retrieved documents AND the question in one message
}
]
# chat.completions.create() sends the conversation to GPT-4o and returns a response
# model= is the deployment name of your GPT-4o model (not "gpt-4o" literally)
response = openai_client.chat.completions.create(
model = "my-gpt4o-deployment", # Your deployment name from Foundry
messages = messages # The conversation history
)
# Extract the model's reply from the response
# .choices is a list of possible responses (usually 1)
# .choices[0] is the first (and usually only) response
# .message.content is the actual text the model generated
answer = response.choices[0].message.content
print(f"Answer: {answer}")
Measuring the quality of your GenAI solution
Before deploying a GenAI solution to users, you need to measure how well it actually performs. Microsoft Foundry includes built-in evaluation tools that automatically score your model's outputs against quality metrics.
The 5 key evaluation metrics
Is the response factually supported by the provided source documents? Score 1–5. High score = the model answered from the context, not from hallucination. Most important for RAG solutions.
Does the response actually answer the user's question? Score 1–5. A response that is factually correct but doesn't address the question has low relevance.
Is the response logically consistent and well-structured? Score 1–5. A coherent response flows naturally and doesn't contradict itself.
Is the language natural, grammatically correct, and easy to read? Score 1–5. High fluency = professional-quality prose.
How similar is the generated answer to a reference (gold standard) answer? Uses semantic similarity. Used when you have known correct answers to compare against.
How to run an evaluation in Foundry
Create a file with test questions and (optionally) expected answers. Format: JSONL with one question per line. Foundry runs your flow against each question.
Your project → Evaluation → + New evaluation → Select your flow or model deployment → Upload test dataset.
Choose which metrics to compute: Groundedness, Relevance, Coherence, Fluency, Similarity. You can select all or just the ones relevant to your use case.
Foundry runs the flow against all test questions and scores each response. Results show per-question scores and averages. Identify which questions score poorly and improve the flow.
"Model answers are correct but based on made-up facts not in the documents."
→ Low Groundedness
"Model gives accurate information but doesn't actually answer the user's question."
→ Low Relevance
"Model responses are factually correct and relevant but hard to read."
→ Low Fluency or Coherence
Calling your deployed flow from a Python application
Once your Prompt Flow is deployed as an endpoint in Foundry, you call it
from your Python application just like any other REST endpoint.
The Foundry SDK makes this straightforward.
Install: pip install azure-ai-projects azure-identity
import os
from azure.ai.projects import AIProjectClient # Foundry project client
from azure.identity import DefaultAzureCredential # Managed identity auth (from Part 1)
# ── Create the Foundry project client ────────────────────────────────
# Connection string format: region.api.azureml.ms;subscription-id;resource-group;workspace-name
# Found in: ai.azure.com → your project → Overview → Connection string
connection_string = os.environ.get("AZURE_AI_PROJECT_CONNECTION_STRING")
# DefaultAzureCredential automatically uses managed identity when running in Azure
# (no credentials to store — from Part 1 Topic 3)
credential = DefaultAzureCredential()
# AIProjectClient connects to your specific Foundry project
project_client = AIProjectClient.from_connection_string(
conn_str = connection_string,
credential = credential
)
# ── Use a connection to get an OpenAI client ──────────────────────────
# Foundry manages the connection — you don't need to hardcode the endpoint or key
# get_azure_openai_client() returns a pre-configured OpenAI client using the
# connection you set up in your Foundry hub (Connection → Azure OpenAI)
openai_client = project_client.inference.get_azure_openai_client(
api_version = "2024-02-01"
)
# ── Call your model deployment ────────────────────────────────────────
# Now use the client exactly as you would with direct Azure OpenAI
response = openai_client.chat.completions.create(
model = "my-gpt4o-deployment", # Your deployment name
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What is Azure AI Foundry?"}
]
)
print(response.choices[0].message.content)
What this topic is specifically tested on
1. RAG reduces hallucination by grounding — Any scenario
about "reducing incorrect answers" or "basing responses on company data"
→ RAG pattern.
2. Embedding model = text-embedding-ada-002 — The vectorisation
step in RAG always uses this model. Know it by name.
3. Groundedness is the most important RAG metric — Measures
whether responses are based on retrieved documents (not made-up).
4. Prompt Flow node types — LLM node (calls the model),
Python node (custom code), Search node (retrieves documents),
Prompt Template node (formats the prompt). Know each one's purpose.
5. Hub vs Project — Shared infrastructure (storage, connections,
network) lives at Hub level. AI work (flows, models, evaluations) lives at
Project level. Exam scenario: "a company wants all teams to share the same
Azure OpenAI connection" → configure the connection at Hub level.
Test your understanding
5 questions — select an answer to see the explanation immediately.
Which architectural pattern should be implemented?
B is correct. RAG is the correct pattern for this use case — enterprise Q&A where answers must be grounded in specific source documents. Documents are chunked, embedded, and stored in a vector index. At query time, the most relevant chunks are retrieved and included in the prompt, so the model's answer is traceable to actual documents. Fine-tuning (option A) teaches the model writing style/patterns but doesn't make it "know" specific documents — it still hallucinations. Option C is impossible — 200,000 documents vastly exceeds any context window. Option D provides keyword search but not semantically grounded AI answers.