AI Architecture Pattern 04: Evaluator Optimizer

Pattern 03 proved the format was valid. This pattern adds an independent judge that checks whether the content is actually correct β€” catching a hallucinated refund timeline before it reaches the customer, with a correction hint that makes the rewrite targeted rather than blind.

The answer was valid. It was also wrong.

Pattern 03 gave us schema enforcement β€” every response field is correctly typed, enum values are constrained during generation, a schema violation surfaces immediately with the exact field and reason. That was the right problem to solve.

But it doesn’t catch this:

“Your refund of Β£49.99 will be processed within 3-5 business days and credited back to your card immediately.”

That answer is well-typed. The status field is a valid enum. The JSON is parseable. The refund policy says 5-10 business days. “Immediately” contradicts it. And nothing in Pattern 03 checks whether the answer is true β€” only whether it’s well-formed.

Mike waited. The refund didn’t arrive on Friday. He called support. He was angry. He filed a complaint.

The complaint wasn’t about format. It was about a confident, wrong answer that reached him without any check on its content.

[!NOTE] This post is part of a continuing 16-pattern series: a payment-support assistant at a fictional bank, starting simple and accumulating exactly the complexity a real one would, in the order a real one would need it. Read the series overview or clone the repo and run python -m launcher.main from the repo root to browse the whole series locally.

Why This Pattern Exists

The previous three patterns built a grounded, contract-enforced, schema-valid assistant. Each one fixed a real gap:

  1. Pattern 01 β€” the model answers from its own training data (wrong). Fix: retrieve trusted policy and transaction context first.
  2. Pattern 02 β€” a second team sends different field names, the prompt silently receives bad data. Fix: a validated schema contract per caller.
  3. Pattern 03 β€” the model generates a free-text status that doesn’t match the routing queue’s enum. Fix: constrained decoding enforces the exact value during generation.

What’s left: the model could still produce a schema-valid, well-grounded-sounding answer that contradicts the policy it was given. Pattern 03 can’t catch it β€” it checks shape, not content.

Pattern 04’s job: add an independent model call that judges the draft against the policy context, catches the contradiction, and supplies a correction hint before the answer is delivered.

The Pipeline

Three Converse calls, not one:

Customer Question + Policy Context + Transaction
        β”‚
        β–Ό
[1] Generate ─────────────────── Converse call #1
        β”‚  draft answer
        β–Ό
[2] Judge ───────────────────── Converse call #2
        β”‚  {verdict, confidence, reason}
        β–Ό
[3] Gate ─────────── PASS β†’ deliver
        β”‚ FAIL
        β–Ό
[4] Rewrite (with judge reason) ─ Converse call #3
        β”‚
        β–Ό
[5] Deliver + Audit Trail

The key property: the judge is always a separate call. If the same call generated and judged the draft, it would defend its own output β€” a documented failure mode in LLM-as-a-judge research. Separation is what makes the judge independent.

The correction hint: the judge returns a reason string β€” “Draft states refund will arrive by Friday β€” contradicts the 5-10 business day policy.” That reason is injected into the rewrite call’s system prompt. A rewrite that knows what was wrong is meaningfully better than a blind retry. This is the key architectural property the inline judge has that Bedrock Guardrails contextual grounding doesn’t: Guardrails returns block or allow with no reason field.

What the Demo Shows

Four scenarios, each teaching a different outcome:

ScenarioWhat the judge catchesVerdict
Refund timeline“by Friday” contradicts 5-10 business day policyFAIL β†’ rewrite
Chargeback dispute“provisional credit immediately” β€” no policy basisFAIL β†’ rewrite
Pending paymentCorrectly cites 1-3 business day clearing β€” groundedPASS
Ambiguous questionVague promise “get your money back” β€” implicit commitmentFAIL (low confidence) β†’ rewrite

The demo’s left panel shows the generator’s original draft. The right panel shows the judge’s verdict, confidence score, reason, and the final answer Mike actually received. The “β†Ί Regenerated” pill appears when a rewrite happened.

Design Decision: Why Not Bedrock Model Evaluation?

The ROADMAP.md entry for this pattern said “Bedrock Model Evaluation (LLM-as-a-Judge)” β€” that’s what I started with. Step 2 research changed the answer.

Bedrock Model Evaluation is a managed batch service. You submit a CreateEvaluationJob, it runs offline, and the results land in S3. It’s excellent for auditing a corpus of historical responses β€” testing a new model version, weekly quality sampling, regulatory reporting. But it cannot block delivery in real time. An evaluation job can’t gate a single response that’s about to go to Mike right now.

The inline judge is plain application code: two sequential Converse calls in the same request lifecycle. No managed service required. Simpler, cheaper per-request, and actually capable of doing the job.

Inline judge (built)Bedrock Model EvaluationBedrock Guardrails
Per-request gatingβœ…βŒ batch onlyβœ…
Reason for verdictβœ…βœ… (in report)❌
Rewrite with correction hintβœ…βŒβŒ
Synchronousβœ…βŒβœ…
Use forReal-time gateOffline auditManaged blocking

The right pick depends on what problem you’re solving. This pattern solves per-request quality gating with a targeted rewrite. That’s Option A.

The Audit Trail

Every response from run_evaluator_loop() includes a full audit trail:

{
  "answer": "...",         # final delivered answer
  "evaluation": {          # judge verdict
    "verdict": "fail",
    "confidence": 0.15,
    "reason": "Draft states refund will arrive 'by Friday'..."
  },
  "audit": [
    {"stage": "generate",  "output": "<original draft>"},
    {"stage": "evaluate",  "verdict": "fail", "confidence": 0.15, "reason": "..."},
    {"stage": "rewrite",   "output": "<corrected answer>"},
  ]
}

For a payment-support system, this is the compliance record: proof that the draft was caught before delivery, what specifically was wrong, and what was actually sent to Mike. In a dispute, that audit trail is evidence.

On AWS

Replace the local mock client with a real boto3.client("bedrock-runtime", region_name="us-east-1"). The converse() call shape is identical β€” the mock was built to mirror the real API exactly. Both the generator and judge use amazon.nova-micro-v1:0: $0.035/1M input + $0.14/1M output tokens.

A full loop β€” generate + judge + rewrite β€” uses roughly 800 tokens. That’s ~$0.00004 per request. At 10,000 requests a day, the judge costs about $0.40/day. For the protection it provides against wrong-but-confident answers in a financial context, that’s effectively free.

Line itemCost
Local (today)$0 β€” mock client mirrors the real Converse API shapes
AWS, demo scale~$0–$1/month β€” a few dozen requests costs fractions of a cent
Generator call β€” Nova Micro~$0.000035/1K input + $0.00014/1K output β€” same default as Patterns 01–03
Judge call β€” Nova MicroSame rate; judge output is typically shorter than the generator’s
Rewrite call β€” Nova MicroSame rate, only incurred when the judge fails β€” not every request
Judge upgrade to Nova Pro (stricter reasoning)Higher rate β€” one modelId change, no other code change
LambdaAlways Free β€” 1M requests/month
DynamoDB (audit log)Always Free β€” 25GB/month
API GatewayFree 12 months, then ~$1/million calls

What I Learned

The biggest correction in building this pattern: Bedrock Model Evaluation is not the inline judge. The ROADMAP described it as the LLM-as-a-Judge service, which it technically is β€” but only in an offline batch context. Discovering that forced me to read the actual Converse API docs more carefully and arrive at the cleaner, simpler, more honest answer: two Converse calls, synchronous, no managed service needed.

The second correction: Bedrock Guardrails contextual grounding is explicitly documented as not supporting conversational QA. I checked before assuming, found the limitation documented clearly, and that’s why it’s Option C (not built) in the design comparison rather than a surprise during implementation.

More Than One Way to Build This

Is there an open-source tool that does what this pattern just built? The closest real match is promptfoo, a CI/testing tool that lets one Bedrock model grade another call’s output using a rubric β€” genuinely the same “one model judges another” idea, verified straight from promptfoo’s own docs. But it’s a test-suite construct, YAML config in a CI pipeline, not something a live request path calls synchronously.

Here’s the honest gap worth naming directly: no AWS-published resource β€” not a blog post, not a workshop, not a samples repo β€” describes this pattern’s actual shape: one synchronous, per-request, two-Bedrock-call generate-then-judge-then-rewrite loop. Everything AWS publishes in this space, including RAGAS integration, is batch or offline. So this design isn’t following an established AWS pattern β€” it’s a genuine, distinct architectural choice, worth saying plainly rather than implying prior art that doesn’t exist.

The Wider Evaluator Field: 9 Real Approaches, We Build 1

Option A/B/C above is the design decision β€” inline judge vs. the two managed alternatives. Underneath that sits a wider field of ways to structure the judge itself, worth an honest map even though this pattern only builds the simplest one.

#ApproachStatus
1Single inline judge (binary grounding gate)Built here
2Multi-criteria inline judge (grounding + tone + policy compliance, scored separately)Documented β€” same Converse API, add a structured-output schema (Pattern 03) to the judge call
3Chain-of-thought judge (judge reasons in writing before scoring, for auditability)Documented β€” more tokens/latency, but a compliance team can read the reasoning, not just the verdict
4Bedrock Model Evaluation (batch)Named alternative above (Option B)
5Bedrock Guardrails contextual groundingNamed alternative above (Option C)
6Self-reflection / same-model judge (generator appends its own self-check)Documented β€” cheaper, but a documented confirmation-bias risk; not for compliance-driven domains
7RAGAS (open-source RAG-quality metrics)Different architecture β€” Pattern 13’s territory; AWS does publish a real Bedrock Agents + RAGAS integration, but it’s framed as batch evaluation over collected traces, not this pattern’s synchronous loop
8G-Eval / a non-Bedrock model as judgeNon-AWS β€” real technique (Liu et al., 2023), but sending customer data to an external judge model is its own compliance problem in this domain
9Human-in-the-loop review (flagged responses go to a person)Different architecture β€” Pattern 10’s territory; Amazon A2I is the natural AWS service here but is being shut down 2026-07-30, so any new build needs an alternative review queue

Why single inline judge and not multi-criteria or chain-of-thought: this pattern’s job is teaching the core mechanic β€” an independent second call gates delivery β€” not building the most sophisticated judge possible. Multi-criteria and chain-of-thought are real, natural next steps once the basic gate exists, not competitors to it.

A documented risk worth surfacing plainly: self-reflection (#6) β€” asking the generator to grade its own output in the same call β€” is the cheapest option on this list, and also the one this pattern’s whole architecture exists to avoid. A model defending its own output shows measurable confirmation bias in the research; that’s exactly why the judge here is always a separate call, even using the same underlying model.

When to use this pattern

[!TIP] Use an inline judge when:

  1. Compliance demands a per-request grounding check, not a periodic audit.
  2. A real-time response is required β€” the check has to happen before delivery, not after.
  3. A correction path is needed when the judge fails, not just a flag.
  4. The cost of a confident, wrong answer is high enough to justify a second model call.

When not to use this pattern alone

[!WARNING] Do not use this pattern by itself when:

  1. You need offline auditing across a corpus of historical responses, not a per-request gate β€” that’s a batch job (Bedrock Model Evaluation), not a synchronous judge on every request.
  2. It’s single-turn RAG grounding over one retrieved document β€” Bedrock Guardrails’ contextual grounding check is simpler, though it doesn’t support conversational QA and has no reason field for a rewrite loop.
  3. Sub-second latency matters more than a grounding check β€” two or three sequential model calls roughly doubles response time.

Who’s using this in the real world

Catching a confident, well-formed, wrong answer with a second independent model call is a real production technique, not a hypothetical.

Datadog’s own engineering blog describes a production LLM-as-a-judge system for detecting hallucinations in RAG applications β€” the same independent-scoring idea this pattern builds, running in production today. (datadoghq.com/blog/ai/llm-hallucination-detection)

Google DeepMind published FACTS Grounding, an official benchmark that checks whether a model’s answer is actually grounded in a source document β€” using multiple independent LLM judges, deliberately mixed model families so no judge just favors its own family’s output. (deepmind.google/blog/facts-grounding-a-new-benchmark-for-evaluating-the-factuality-of-large-language-models)

This isn’t just a house opinion, either. Peer-reviewed research measured the self-preference risk directly: Panickssery, Bowman & Feng found GPT-4 was 73.5% accurate at recognizing its own writing versus other models’ or humans’ β€” and, more importantly, that self-recognition ability correlates with self-preference bias. The better a model is at telling its own output apart, the more it favors that output when scoring, even when human judges rate it no better than the alternative. That’s the concrete, measured version of the reason this pattern never lets the generator grade its own draft. (Panickssery, Bowman & Feng, “LLM Evaluators Recognize and Favor Their Own Generations,” NeurIPS 2024, arXiv:2404.13076)


Further reading


What’s Next

Pattern 04 catches wrong answers before they reach the customer. It still only knows what it was given β€” if the policy context was stale or the transaction data was incomplete, both generator and judge are working from wrong information. Pattern 05: Tool Calling solves this: the model requests a real-time data lookup instead of reasoning over whatever context was passed in.

[!TIP] Star the GitHub repo and follow along with the series.


🎯 Interview Prep β€” Pattern 04

Scenario-based questions an interviewer would ask about this pattern. Try answering out loud before expanding each one. Full guide, all patterns: Interview Guide β†’

Q1 Mike got a wrong refund timeline from your AI assistant. What architectural problem does this represent, and how did Pattern 04 fix it?

The generator is producing and validating its own output in the same call β€” there’s no external check. Pattern 03 proved the format was valid; this pattern adds a second, independent model call that evaluates the draft against the policy it was given.

Root cause: a model cannot evaluate its own output from the outside. When the same call generates and judges, it defends its own answer rather than assessing it.

Fix: separate the judge. Two Converse calls: generator produces a draft, judge returns {"verdict": "pass"|"fail", "confidence": float, "reason": string}. If verdict is FAIL, the judge’s reason becomes a correction hint for a targeted rewrite before delivery.

Q2 What is the structural difference between an inline judge and Bedrock Model Evaluation?

Inline judge: synchronous, per-request, blocks delivery. Runs as two sequential Converse calls within the same request lifecycle. Returns a verdict before the answer reaches the user.

Bedrock Model Evaluation: batch-only. You submit a CreateEvaluationJob; it runs offline against a dataset and stores results in S3. It cannot block delivery in real time.

Use the inline judge for per-request quality gating. Use Bedrock Model Evaluation for periodic offline audits β€” testing a new model version, weekly quality sampling, measuring grounding accuracy across a corpus. In a mature system you use both.

Q3 Why can't Bedrock Guardrails contextual grounding replace the inline judge in this pattern?

Two reasons.

First, AWS explicitly documents that Guardrails contextual grounding does not support conversational QA and chatbot use cases β€” it’s designed for RAG document retrieval grounding, not open-ended Q&A.

Second, and more architecturally important: Guardrails returns block or allow with no reason field. No reason means no correction hint. No correction hint means any rewrite is a blind retry β€” you know something was wrong but not what. The inline judge returns a specific reason that is injected into the rewrite system prompt. That specificity is what makes the rewrite targeted rather than a coin flip.

Q4 The judge returned verdict=fail with confidence=0.19. What does that confidence score mean, and does it affect your decision?

Confidence is the judge’s certainty in its own verdict β€” not a quality score for the answer. 0.19 means the judge flagged something but isn’t highly certain it caught the right problem. A FAIL at 0.19 still triggers a rewrite.

In a production system, confidence enables tiering:

  • FAIL + high confidence β†’ auto-rewrite
  • FAIL + low confidence β†’ rewrite + flag for human review
  • PASS + high confidence β†’ deliver
  • PASS + low confidence β†’ deliver but log for audit

The confidence score makes the system’s uncertainty observable. Ignoring it flattens the decision to binary pass/fail and loses information that’s valuable for calibrating thresholds over time.

Q5 Your judge call adds latency. A product manager asks why you can't just validate on the client side. What do you say?

Client-side validation can check format β€” types, field presence, enum values. It cannot check whether an answer contradicts an internal policy that isn’t available to the client. The judge has access to the same policy context the generator used; the client doesn’t.

More importantly: a wrong-but-confident answer reaching the client is a delivered bad answer. The judge catches it before delivery, so the rewrite also happens before delivery. Post-delivery validation is incident management; pre-delivery validation is prevention.

The latency cost is two sequential Converse calls β€” typically 2–4 seconds total on Nova Micro. For a payment-support chatbot where a wrong answer creates a support complaint or a chargeback, that latency is the right trade.

Q6 The rewrite loop produced a second wrong answer. What's the safest architectural response?

Set a rewrite budget β€” one attempt in this pattern’s demo. After exhausting the budget:

  1. Return a fallback answer that acknowledges the question and routes to human support β€” never a third blind retry
  2. Log the full audit trail: original draft, judge verdict, failed rewrite β€” as a signal for model quality review
  3. Escalate to a human support queue with the audit trail attached so the agent has context

Never retry indefinitely. A bounded loop with a hard fallback path is the correct production design. The audit trail turns a failure into a data point for calibrating the judge’s thresholds.

Q7 Show me the Converse API call shape for the judge call.
response = client.converse(
    modelId="amazon.nova-micro-v1:0",
    system=[{"text": (
        "You are a grounding judge. Evaluate if the draft contradicts the policy. "
        'Respond with ONLY valid JSON: {"verdict": "pass"|"fail", "confidence": <float>, "reason": "<string>"}'
    )}],
    messages=[{"role": "user", "content": [{"text": (
        f"Policy:\n{context}\n\nQuestion: {question}\n\nDraft:\n{draft}"
    )}]}],
    inferenceConfig={"maxTokens": 256, "temperature": 0.0},
)
judge_text = response["output"]["message"]["content"][0]["text"]
verdict = json.loads(judge_text)

Key differences from the generator call: temperature=0.0 (deterministic verdict), maxTokens=256 (short JSON output), system prompt instructs JSON-only response with no preamble.

Q8 When would you choose Bedrock Model Evaluation over an inline judge?

When you need to audit quality across a large corpus of historical responses:

  • Testing a new model version before rollout β€” compare quality across hundreds of question types
  • Weekly quality audits on a sample of production calls
  • Measuring grounding accuracy across a domain corpus
  • Regulatory reporting that requires documented quality evidence at scale

Bedrock Model Evaluation runs managed batch jobs, stores results in S3, and supports custom evaluation metrics. You’d use it for offline QA, not per-request gating. In a mature system: inline judge for real-time gating, Model Evaluation for periodic audits and pre-rollout validation of new model versions.

Q9 The compliance team wants to audit every response. What does your architecture give them?

Every call to run_evaluator_loop() returns a full audit trail:

[
  {"stage": "generate",  "output": "<draft text>"},
  {"stage": "evaluate",  "verdict": "fail", "confidence": 0.15, "reason": "..."},
  {"stage": "rewrite",   "output": "<corrected text>"},   # only if fail
]

For every delivered answer, the compliance team can see: what the generator originally produced, what the judge decided and why (with confidence), and what the rewrite produced if the draft was rejected. That’s a per-response evidence chain β€” useful for dispute resolution, regulatory audits, and model quality review in financial services.

The audit trail can be written to DynamoDB or S3 per request. Each entry is timestamped and tied to the customer’s transaction ID.

Q10 This pattern doubles your model calls β€” generator plus judge, sometimes a rewrite on top. How do you scale this to high volume without the cost or latency becoming the actual bottleneck?

The naive read is “2-3x the calls, 2-3x the cost” β€” true per-request, but the scaling levers are different for each stage:

  • Judge call latency: it’s on the critical path (blocks delivery), so it needs to stay fast. Keep maxTokens small (this pattern uses 256) and temperature=0.0 β€” a short, deterministic judge call is the cheapest way to keep the added latency bounded as volume grows, rather than trying to parallelize it away (there’s nothing to parallelize; the judge needs the generator’s draft first).
  • Rewrite calls are the volume-sensitive part, not the judge. If your fail rate is 5%, rewrites are a small tail β€” but at 10k rpm that’s still 500 extra Converse calls/minute. Track the fail rate as a first-class metric; a rising fail rate at scale usually means the generator’s base prompt or the policy context drifted, not that you need more rewrite capacity.
  • Bounded rewrite budget matters more at scale, not less β€” one unbounded retry loop multiplied across thousands of concurrent requests is how a quality problem becomes a cost incident. The hard fallback-to-human path isn’t just a correctness safeguard, it’s a cost ceiling.
  • Offline Bedrock Model Evaluation absorbs load the inline judge shouldn’t carry β€” periodic corpus-wide audits belong in batch jobs against S3, not squeezed into the per-request path just because “we want more coverage.”

The real interview signal: this pattern’s cost scales with the fail rate, not the request rate β€” a well-tuned generator with a 2% fail rate at 10k rpm costs much closer to a single-call pattern than the “always pay for 2 calls” mental model suggests.

Q11 How do you explain the Evaluator Optimizer pattern to a non-technical stakeholder?

“Imagine your AI assistant writes a response to a customer. Before we send it, a second, independent AI reads it and checks: does anything in this response contradict our actual policy? If it does, that second AI tells the first what was wrong, and the first rewrites the response. The customer only ever sees the corrected version.

It’s like having a compliance checker read every draft before it goes out β€” except this one runs in milliseconds and tells the writer exactly what to fix. And every check is logged, so if a customer ever disputes what they were told, we have a record showing the draft, what the checker flagged, and what was actually sent.”

Technical summary: two sequential Bedrock Converse calls, synchronous, per-request. Generator β†’ judge β†’ (conditional) rewrite. Full audit trail included in every response.


Have questions or feedback? Drop a comment below or connect on LinkedIn.

πŸ’¬ Comments

← Back to all posts