← Back to Patterns

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#.

Pattern 6: Reflection

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

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 CaseGeneratorCriticTermination Criteria
Startup PitchPitch WriterVC InvestorInvestor approval
Code ReviewCode GeneratorSenior DeveloperAll issues addressed
Essay WritingWriterEditorQuality score >= 8/10
Legal DocumentsDrafterCompliance OfficerNo 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

ComponentPurpose
AgentWorkflowBuilder.CreateGroupChatBuilderWith()Factory for creating group chat workflows
AddParticipants()Registers agents in the conversation
MaximumIterationCountSafety 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 * 2 because 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

  1. User input arrives as the initial ChatMessage
  2. GroupChatManager selects PitchWriter (first participant)
  3. PitchWriter processes the conversation history (just user input)
  4. Response added to history with AuthorName = "PitchWriter"
  5. ShouldTerminateAsync() called → returns false (not approved yet)
  6. GroupChatManager selects VCCritic (next in round-robin)
  7. VCCritic processes full history (user input + pitch)
  8. Response added with AuthorName = "VCCritic"
  9. If VCCritic says “APPROVED” → ShouldTerminateAsync returns true, workflow ends
  10. 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

BenefitHow RoundRobin Enables It
Natural dialogueTurn-taking mimics human review
Full contextEach agent sees complete history
Specific feedbackCritic can reference exact issues
Addressed improvementsGenerator 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

ApproachAPI CallsQualityComplexityUse When
Single-Shot1BaselineLowSpeed matters, quick drafts
Self-Reflection1ModerateLowBudget-conscious, simple tasks
Two-Agent2-6+HighestMediumQuality 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
  • RoundRobinGroupChatManager orchestrates 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

  1. Change the domain: Replace startup pitch with code review or essay writing
  2. Add a third agent: Introduce an editor after the critic approves
  3. Implement score-based termination: Use numeric quality scores instead of keywords
  4. Compare models: Try different models for generator vs critic

Next Steps

Resources

Comments