Draft, critique, revise - until a reviewer signs off or the budget runs out.
The analogy: a writer and an editor. The writer produces a draft. The editor marks it up. The writer revises. Two or three rounds of that beats one person trying to be brilliant on the first attempt.
Use it when quality is the bottleneck and you can say what good looks like: code that has to compile, a summary that must not exceed 200 words, SQL that must only read from three tables. Reflection is the pattern that turns "usually fine" into "checked".
alias ExAgent.Patterns.Reflection
{:ok, result} =
Reflection.run("Write a Postgres query for monthly active users.",
generator: coder,
critic: reviewer,
accept?: &String.contains?(&1, "APPROVED"),
max_rounds: 3
)
result.output # the accepted draft
result.rounds # how many revisions it tookThe critic needs a way to say yes
accept? decides when to stop. Ask the critic for a token you can match on and
keep the instruction blunt - "Reply APPROVED if it is correct, otherwise list
what is wrong". Without a stop condition the loop just burns max_rounds worth
of tokens every time.
Two guardrails, both deliberate
max_rounds defaults to 3 and is a hard ceiling: an LLM critic can always
find something to complain about, so an unbounded loop is a runaway bill.
When the ceiling is hit without approval you get {:max_rounds, result}, not
{:ok, result}. The last draft is still there - often it is good enough - but
you have to choose to use unapproved work rather than have it handed to you as
if a reviewer had passed it.
When not to use it
If you cannot describe the acceptance criteria, a critic will produce vague
praise and you will pay double for the same answer. If the risk is a wrong
fact rather than poor quality, prefer ExAgent.Patterns.Consensus - a critic
reading one draft is easy to talk into agreeing with it.
Summary
Types
Where to send a prompt: a provider struct for a stateless call, or a running agent when the step should remember the conversation.
Functions
Generates an answer to task, then critiques and revises it.
Types
@type result() :: %{ output: String.t(), rounds: non_neg_integer(), critiques: [String.t()], accepted?: boolean() }
@type target() :: struct() | GenServer.server()
Where to send a prompt: a provider struct for a stateless call, or a running agent when the step should remember the conversation.
Functions
@spec run(String.t(), reflection_opts()) :: {:ok, result()} | {:max_rounds, result()} | {:error, term()}
Generates an answer to task, then critiques and revises it.
Options
:generator(required) - provider or agent that drafts and revises:critic- provider or agent that reviews (default: the generator, which is cheaper but a weaker check - a model reviewing itself agrees with itself):accept?-(critique -> boolean)deciding when the draft is good (default: the critique contains"APPROVED"):max_rounds- hard ceiling on revisions (default:3):critic_prompt-(task, draft -> prompt)to override the review prompt:revise_prompt-(task, draft, critique -> prompt)to override the revision
Returns {:ok, result} once the critic accepts, {:max_rounds, result} if it
never does, or {:error, reason} if a call fails.