Evaluation Loop
A sequential pipeline runs once and delivers results. An evaluation loop runs iteratively — a critic reviews the output, and if it doesn’t pass, the drafter revises and tries again. The loop continues until quality is met or a maximum number of rounds is reached.
The Pattern
DraftAgent → produces output ↓CriticAgent → reviews it (structured output: pass/revise + feedback) ↓ "pass"? → done "revise"? → feed feedback back to DraftAgent → repeatThe critic returns structured output — a typed hash with verdict, feedback, and score. This makes the loop condition a simple string comparison instead of text parsing.
Why Structured Output for the Critic?
# Without schema — you'd have to parse natural languagereview = critic.ask("Review this").content# => "I think it needs more examples and the explanation is unclear"# How do you know if it "passed"?
# With schema — predictable, typed datareview = critic.ask("Review this").content# => { "verdict" => "revise", "feedback" => "Add one example", "score" => 5 }review["verdict"] == "pass" # => true/false, reliableYour Task
Open evaluation_loop.rb. DraftAgent, CriticAgent, and ReviewDecision schema are already defined. Your job is to implement the evaluation loop:
- Ask
DraftAgentfor an initial draft onTOPIC - Pass the draft to
CriticAgentfor review — it returns structured output - Check the verdict: if
"revise", send the feedback back toDraftAgentfor a revision - Loop up to
MAX_ROUNDStimes - Print each round showing the draft, verdict, score, and feedback
MAX_ROUNDS = 3
drafter = DraftAgent.newcritic = CriticAgent.new
# Initial draftdraft = drafter.ask("Write an explanation of: #{TOPIC}").contentputs "Round 1 draft:\n#{draft}\n"
MAX_ROUNDS.times do |round| review = critic.ask("Review this technical explanation:\n\n#{draft}").content
puts "Review — verdict: #{review["verdict"]}, score: #{review["score"]}/10" puts "Feedback: #{review["feedback"]}" puts
break if review["verdict"] == "pass"
# Ask the drafter to revise — it remembers the original draft draft = drafter.ask( "Please revise based on this feedback: #{review["feedback"]}" ).content puts "Round #{round + 2} draft:\n#{draft}\n"endRun it:
$ ruby agentic_workflows/evaluation_loop.rbWatch the draft improve across rounds as the critic’s feedback is incorporated.
Why Not Just Run It Once?
A single pass gives you whatever quality the model generates on the first try. The evaluation loop gives you consistent minimum quality:
- Set your bar: “score >= 7 and includes a code example”
- The loop guarantees you never ship below that bar
- Usually 1-2 revisions are enough; rarely need 3+
Convergence and Limits
Always set a MAX_ROUNDS. Without it, a disagreeable critic could loop forever:
MAX_ROUNDS = 3 # Cost and time boundedrounds_taken = 0
loop do break if review["verdict"] == "pass" || rounds_taken >= MAX_ROUNDS # ... rounds_taken += 1end
if rounds_taken >= MAX_ROUNDS puts "Warning: max rounds reached — using best available draft"endAdjusting the Quality Bar
Change what “pass” means by updating the critic’s instructions:
class CriticAgent < RubyLLM::Agent instructions <<~PROMPT Return "pass" if ALL of these are true: 1. Technically accurate 2. Includes a working code example 3. Under 200 words 4. Score >= 8 Otherwise return "revise" with specific, actionable feedback. PROMPTendTighter criteria → more revisions → higher quality → higher cost. Tune based on your use case.
- Preparing Ruby runtime