Pattern 6: Reflection
Build iterative generate-critique-refine loops using Microsoft Agent Framework's group chat orchestration. Learn the reflection pattern with RoundRobinGroupChatManager in C#.
Overview
Learning Objectives
By the end of this tutorial, you will be able to:
- Understand the Reflection pattern and when to use it
- Learn Microsoft Agent Framework’s group chat orchestration
- Implement custom termination logic with
RoundRobinGroupChatManager - Build a two-agent feedback loop for iterative improvement
Prerequisites
- .NET 8.0 SDK or later installed
- An LLM provider configured (see Setting Up the Agent Environment)
- Basic familiarity with C# async/await patterns
- Completed Pattern 1: Prompt Chaining (recommended)
What is Reflection?
Reflection is an agentic pattern where output quality is improved through iterative feedback loops. Instead of producing content once, a generator agent creates output while a critic agent evaluates it against quality criteria, feeding back improvements until standards are met.
flowchart TD
A[User Input] --> B[Generator Agent]
B --> C[Critic Agent]
C --> D{Approved?}
D -->|No| E[Feedback]
E --> B
D -->|Yes| F[Final Output]
This mirrors how humans review work: write a draft, get feedback, revise, repeat until satisfied. The pattern automates this cycle using specialized agents.
When to Use Reflection
Use reflection when:
- Quality matters more than speed (proposals, reports, creative content)
- Clear evaluation criteria exist (you can define what “good” looks like)
- Iterative improvement is feasible (content can be revised)
- Human-like review processes would add value
Avoid reflection when:
- Speed is critical (real-time responses)
- Output is objective/factual (no subjective quality to improve)
- Single-pass generation is sufficient
- Cost is a primary concern (reflection multiplies API calls)
Real-world examples:
| Use Case | Generator | Critic | Termination Criteria |
|---|---|---|---|
| Startup Pitch | Pitch Writer | VC Investor | Investor approval |
| Code Review | Code Generator | Senior Developer | All issues addressed |
| Essay Writing | Writer | Editor | Quality score >= 8/10 |
| Legal Documents | Drafter | Compliance Officer | No compliance issues |
Understanding Group Chat Orchestration
Microsoft Agent Framework provides RoundRobinGroupChatManager for orchestrating multi-agent conversations. This is the foundation for implementing the reflection pattern.
What is RoundRobinGroupChatManager?
RoundRobinGroupChatManager is a built-in orchestrator that:
- Manages turn-taking: Agents take turns in a round-robin fashion
- Maintains conversation history: Each agent sees all previous messages
- Handles termination: Checks after each turn whether to continue or stop
- Abstracts complexity: You focus on agent logic, not orchestration
How Group Chats Work
flowchart TD
A[User Input] --> B[GroupChatManager]
B --> C[Select Next Agent]
C --> D[Agent Processes History]
D --> E[Add Response to History]
E --> F{Should Terminate?}
F -->|No| C
F -->|Yes| G[Return Output]
The manager cycles through agents, giving each a chance to respond to the full conversation history. After each response, it checks termination conditions.
Key Components
| Component | Purpose |
|---|---|
AgentWorkflowBuilder.CreateGroupChatBuilderWith() | Factory for creating group chat workflows |
AddParticipants() | Registers agents in the conversation |
MaximumIterationCount | Safety limit to prevent infinite loops |
ShouldTerminateAsync() | Override to implement custom termination logic |
Step-by-Step Implementation
We’ll build a Startup Pitch Perfector where a PitchWriter agent creates pitches and a VCCritic agent evaluates them until approved.
Step 1: Clone the Patterns Repository
git clone https://github.com/dotnetagents/patterns.git
cd patterns/06-reflection/src/Reflection
Step 2: Configure Your LLM Provider
Set up environment variables for your provider. See Setting Up the Agent Environment for details.
For Azure OpenAI:
export AZURE_OPENAI_API_KEY=your-key
export AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
Step 3: Create the Generator Agent (PitchWriter)
The generator creates the initial output and revises based on feedback:
private const string PitchWriterPrompt = """
You are an expert startup pitch writer. Given a startup idea, create a
compelling pitch that covers: Problem, Solution, Market Size, Business Model,
Traction, Competition, Team, and The Ask.
When given feedback from investors, thoughtfully address their concerns
while maintaining a compelling narrative. Focus on making the pitch stronger
with each revision.
Structure your pitch with clear sections and make it persuasive yet realistic.
""";
var writerClient = ChatClientFactory.Create(config.Provider, config.WriterModel);
var writer = new ChatClientAgent(
writerClient,
PitchWriterPrompt,
"PitchWriter", // Agent name (appears in AuthorName)
"Pitch writer agent" // Description
);
Key points:
- Clear role definition: The agent knows it’s a pitch writer
- Revision instructions: Explicitly told to address feedback
- Structured output: Required sections ensure completeness
Step 4: Create the Critic Agent (VCCritic)
The critic evaluates output and provides a verdict:
private const string VCCriticPrompt = """
You are a skeptical VC partner evaluating startup pitches. Your job is to
find weaknesses and ask tough questions. Evaluate the pitch on:
- Problem clarity and market pain
- Solution-market fit
- TAM/SAM/SOM credibility
- Competitive differentiation
- Business model viability
- Traction and validation
- Team execution ability
Provide specific, actionable feedback. Be constructive but rigorous.
IMPORTANT: At the end of your critique, you MUST include one of these verdicts:
- "APPROVED" - if the pitch is strong enough for an investor meeting
- "NEEDS_REVISION" - if the pitch needs more work
Only approve if the pitch genuinely addresses the core investor concerns.
""";
var criticClient = ChatClientFactory.Create(config.Provider, config.CriticModel);
var critic = new ChatClientAgent(
criticClient,
VCCriticPrompt,
"VCCritic",
"VC critic agent"
);
Key points:
- Clear evaluation criteria: Specific dimensions to assess
- Actionable feedback: Not just “bad” but “here’s what to fix”
- Explicit verdict: APPROVED or NEEDS_REVISION for termination logic
Step 5: Implement Custom Termination Logic
Extend RoundRobinGroupChatManager to terminate when the critic approves:
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
public class ApprovalBasedGroupChatManager : RoundRobinGroupChatManager
{
private readonly string _approverName;
public ApprovalBasedGroupChatManager(
IReadOnlyList<AIAgent> agents,
string approverName)
: base(agents)
{
_approverName = approverName;
}
protected override ValueTask<bool> ShouldTerminateAsync(
IReadOnlyList<ChatMessage> history,
CancellationToken cancellationToken = default)
{
var lastMessage = history.LastOrDefault();
// Only the approver can terminate the conversation
bool shouldTerminate = lastMessage?.AuthorName == _approverName &&
lastMessage.Text.Contains("APPROVED", StringComparison.OrdinalIgnoreCase) &&
!lastMessage.Text.Contains("NOT APPROVED", StringComparison.OrdinalIgnoreCase) &&
!lastMessage.Text.Contains("NEEDS_REVISION", StringComparison.OrdinalIgnoreCase);
if (shouldTerminate)
{
Console.WriteLine($"\n[APPROVED after {history.Count} messages]");
}
return ValueTask.FromResult(shouldTerminate);
}
}
Key points:
- Approver check: Only the designated agent can approve
- Keyword matching: Looks for “APPROVED” in the message
- Negative exclusion: Ignores “NOT APPROVED” and “NEEDS_REVISION”
- Message count tracking: Logs how many turns it took
Step 6: Build the Group Chat Workflow
Assemble everything using AgentWorkflowBuilder:
public record ReflectionPipelineConfig
{
public required string Provider { get; init; }
public required string WriterModel { get; init; }
public required string CriticModel { get; init; }
public int MaxIterations { get; init; } = 3;
}
public static (Workflow, Dictionary<string, string>) Create(ReflectionPipelineConfig config)
{
var writerClient = ChatClientFactory.Create(config.Provider, config.WriterModel);
var criticClient = ChatClientFactory.Create(config.Provider, config.CriticModel);
var writer = new ChatClientAgent(writerClient, PitchWriterPrompt, "PitchWriter", "Pitch writer agent");
var critic = new ChatClientAgent(criticClient, VCCriticPrompt, "VCCritic", "VC critic agent");
// Create workflow with custom approval-based termination
var workflow = AgentWorkflowBuilder
.CreateGroupChatBuilderWith(agents =>
new ApprovalBasedGroupChatManager(agents, "VCCritic")
{
MaximumIterationCount = config.MaxIterations * 2 // 2 turns per iteration
})
.AddParticipants(writer, critic)
.Build();
var agentModels = new Dictionary<string, string>
{
["PitchWriter"] = config.WriterModel,
["VCCritic"] = config.CriticModel
};
return (workflow, agentModels);
}
Key points:
- Factory function:
CreateGroupChatBuilderWith()takes a function that creates your custom manager - MaximumIterationCount: Set to
iterations * 2because each iteration has 2 turns (writer + critic) - AddParticipants: Order matters - first agent speaks first
Step 7: Run the Workflow
var (workflow, agentModels) = ReflectionPipeline.Create(
new ReflectionPipelineConfig
{
Provider = "azure",
WriterModel = "gpt-4.1",
CriticModel = "gpt-4.1",
MaxIterations = 3
}
);
var startupIdea = """
An AI-powered platform connecting travelers with sustainable Alpine
experiences in Switzerland. Features eco-certified accommodations,
local mountain guides, and carbon-neutral transport options.
""";
var content = await WorkflowRunner.RunAsync(workflow, startupIdea);
Console.WriteLine(content);
Deep Dive: RoundRobin Orchestration
Let’s trace through exactly what happens when the workflow runs.
Message Flow Sequence
sequenceDiagram participant U as User participant M as GroupChatManager participant W as PitchWriter participant C as VCCritic U->>M: Startup idea M->>W: Process (history: user input) W->>M: Initial pitch Note over M: Check ShouldTerminate → false M->>C: Process (history: input + pitch) C->>M: Feedback + NEEDS_REVISION Note over M: Check ShouldTerminate → false M->>W: Process (history: all messages) W->>M: Revised pitch Note over M: Check ShouldTerminate → false M->>C: Process (history: all messages) C->>M: APPROVED Note over M: Check ShouldTerminate → true M->>U: Return final pitch
Step-by-Step Breakdown
- User input arrives as the initial
ChatMessage - GroupChatManager selects PitchWriter (first participant)
- PitchWriter processes the conversation history (just user input)
- Response added to history with
AuthorName = "PitchWriter" - ShouldTerminateAsync() called → returns
false(not approved yet) - GroupChatManager selects VCCritic (next in round-robin)
- VCCritic processes full history (user input + pitch)
- Response added with
AuthorName = "VCCritic" - If VCCritic says “APPROVED” → ShouldTerminateAsync returns
true, workflow ends - If “NEEDS_REVISION” → PitchWriter revises, loop continues
Message History Structure
Each agent receives the full conversation history:
// history is IReadOnlyList<ChatMessage>
// Each message contains:
public class ChatMessage
{
public ChatRole Role { get; } // User, Assistant, System
public string Text { get; } // The message content
public string? AuthorName { get; } // Which agent wrote it
}
The AuthorName property is crucial - it lets agents distinguish who said what, and lets termination logic check which agent provided the verdict.
Why RoundRobin Works for Reflection
| Benefit | How RoundRobin Enables It |
|---|---|
| Natural dialogue | Turn-taking mimics human review |
| Full context | Each agent sees complete history |
| Specific feedback | Critic can reference exact issues |
| Addressed improvements | Generator sees what to fix |
Three Approaches Compared
The reflection pattern can be implemented in three ways, each with trade-offs:
1. Single-Shot (Baseline)
One agent, no reflection:
var pitchWriter = new AgentExecutor(new AgentConfig
{
Name = "SingleShotPitchWriter",
Provider = provider,
Model = model,
Instructions = """
You are an expert startup pitch writer. Given a startup idea, create a
compelling pitch that covers all the essential elements investors look for.
Structure your pitch with these sections:
1. **Problem**: What pain point are you solving?
2. **Solution**: How does your product/service solve this problem?
3. **Market Size**: TAM/SAM/SOM - be specific with numbers.
4. **Business Model**: How will you make money?
5. **Traction**: Any validation, users, or revenue?
6. **Competition**: Who else is in this space? What's your moat?
7. **Team**: Why is this the right team?
8. **The Ask**: What funding are you seeking?
"""
});
var workflow = new WorkflowBuilder(pitchWriter).Build();
Pros: Fast, cheap, simple Cons: No quality improvement, single perspective
2. Self-Reflection (Single Agent)
One agent with internal critique phases:
private const string SelfReflectionPrompt = """
You are an expert startup pitch writer with strong self-critique abilities.
You will go through three phases:
PHASE 1 - INITIAL DRAFT:
Create a compelling startup pitch covering: Problem, Solution, Market Size,
Business Model, Traction, Competition, Team, and The Ask.
PHASE 2 - SELF-CRITIQUE:
After creating the draft, put on your "skeptical VC" hat and critically evaluate
your own pitch. Consider: Is the problem compelling? Is the market size credible?
Is the business model viable? What are the weaknesses?
PHASE 3 - REVISION:
Based on your self-critique, revise and strengthen the pitch. Address the
weaknesses you identified while maintaining a compelling narrative.
Output ONLY the final revised pitch after completing all three phases internally.
""";
Pros: Single API call, implicit reflection Cons: Limited critique depth, no true dialogue
3. Two-Agent (RoundRobin)
Generator + Critic in group chat:
var workflow = AgentWorkflowBuilder
.CreateGroupChatBuilderWith(agents =>
new ApprovalBasedGroupChatManager(agents, "VCCritic")
{
MaximumIterationCount = config.MaxIterations * 2
})
.AddParticipants(writer, critic)
.Build();
Pros: True dialogue, specialized agents, flexible termination Cons: Higher cost, more complexity, longer latency
Comparison Table
| Approach | API Calls | Quality | Complexity | Use When |
|---|---|---|---|---|
| Single-Shot | 1 | Baseline | Low | Speed matters, quick drafts |
| Self-Reflection | 1 | Moderate | Low | Budget-conscious, simple tasks |
| Two-Agent | 2-6+ | Highest | Medium | Quality matters, clear criteria |
Alternative Termination Strategies
Beyond approval-based termination, you can implement other strategies:
Score-Based Termination
Terminate when quality score exceeds threshold:
protected override ValueTask<bool> ShouldTerminateAsync(
IReadOnlyList<ChatMessage> history,
CancellationToken cancellationToken = default)
{
var lastMessage = history.LastOrDefault();
// Look for "Score: X/10" in critic's message
if (lastMessage?.AuthorName == _criticName)
{
var match = Regex.Match(lastMessage.Text, @"Score:\s*(\d+)/10");
if (match.Success && int.TryParse(match.Groups[1].Value, out var score))
{
return ValueTask.FromResult(score >= 8); // Terminate if 8+
}
}
return ValueTask.FromResult(false);
}
Convergence-Based Termination
Terminate when feedback becomes minimal:
protected override ValueTask<bool> ShouldTerminateAsync(
IReadOnlyList<ChatMessage> history,
CancellationToken cancellationToken = default)
{
var lastMessage = history.LastOrDefault();
if (lastMessage?.AuthorName == _criticName)
{
// If feedback is short, consider it approval
var feedbackLength = lastMessage.Text.Length;
var hasNoIssues = lastMessage.Text.Contains("no major issues",
StringComparison.OrdinalIgnoreCase);
return ValueTask.FromResult(feedbackLength < 200 || hasNoIssues);
}
return ValueTask.FromResult(false);
}
Multi-Criteria Termination
Combine multiple conditions:
protected override ValueTask<bool> ShouldTerminateAsync(
IReadOnlyList<ChatMessage> history,
CancellationToken cancellationToken = default)
{
// Must have at least 2 iterations
if (history.Count < 4) return ValueTask.FromResult(false);
var lastMessage = history.LastOrDefault();
if (lastMessage?.AuthorName != _criticName)
return ValueTask.FromResult(false);
// Check multiple approval signals
bool hasApproval = lastMessage.Text.Contains("APPROVED",
StringComparison.OrdinalIgnoreCase);
bool hasHighScore = Regex.IsMatch(lastMessage.Text, @"Score:\s*[89]|10/10");
bool hasRecommendation = lastMessage.Text.Contains("ready for presentation",
StringComparison.OrdinalIgnoreCase);
return ValueTask.FromResult(hasApproval || hasHighScore || hasRecommendation);
}
Common Pitfalls
1. No Termination Condition
Problem: Conversation loops forever, burning tokens.
Solution: Always set MaximumIterationCount as a safety limit:
new ApprovalBasedGroupChatManager(agents, "VCCritic")
{
MaximumIterationCount = 6 // Hard stop after 6 turns
}
2. Vague Approval Criteria
Problem: Critic never says “APPROVED” because prompt doesn’t require it.
Solution: Make the verdict requirement explicit in the prompt:
// Bad: "Let me know if it's good"
// Good:
"""
IMPORTANT: At the end of your critique, you MUST include one of:
- "APPROVED" - if ready for investors
- "NEEDS_REVISION" - if more work needed
"""
3. Too Many Iterations
Problem: Diminishing returns after 2-3 rounds.
Solution: Set reasonable limits and monitor quality improvement:
public int MaxIterations { get; init; } = 3; // Usually 2-3 is optimal
4. Wrong AuthorName Check
Problem: Termination logic never triggers because name doesn’t match.
Solution: Ensure agent names are consistent:
// When creating agent:
var critic = new ChatClientAgent(..., "VCCritic", ...);
// When checking termination:
bool shouldTerminate = lastMessage?.AuthorName == "VCCritic" // Must match exactly
Running the Example
cd patterns/06-reflection/src/Reflection
# Interactive mode
dotnet run -- --provider azure --model gpt-4.1
# List available benchmarks
dotnet run -- --list-benchmarks
# Run all benchmarks
dotnet run -- --benchmark
Summary
What You Learned
- The Reflection pattern improves output through iterative feedback loops
RoundRobinGroupChatManagerorchestrates multi-agent conversations- Custom termination logic controls when the loop ends
- Three approaches exist: single-shot, self-reflection, and two-agent
Key Insights
- RoundRobin orchestration handles turn-taking and history management automatically
- Custom termination lets you define domain-specific approval criteria
- Two-agent reflection produces the highest quality but at higher cost
- MaximumIterationCount is essential to prevent infinite loops
Try It Yourself
- Change the domain: Replace startup pitch with code review or essay writing
- Add a third agent: Introduce an editor after the critic approves
- Implement score-based termination: Use numeric quality scores instead of keywords
- Compare models: Try different models for generator vs critic
Next Steps
- Pattern 5: Parallelization - Run multiple agents concurrently
- Pattern 3: Routing - Route requests to specialized agents
- Pattern 2: Tool Use - Give agents access to external functions
Resources
Found this helpful?
Comments