DEV Community

Cover image for I Told My LLM Critic to Be Adversarial. It Started Blocking Plans for Being 'Not Thorough Enough.'
Debashish Ghosal
Debashish Ghosal

Posted on

I Told My LLM Critic to Be Adversarial. It Started Blocking Plans for Being 'Not Thorough Enough.'

This is article 2 in a series about building PlannerCritic, an open-source engine where one LLM writes a plan and a second LLM reviews it. Article 1 is here — it covers the 157-goal field test and what it found. This one is about a specific bug that taught me something about LLM judgment.

The critic was supposed to catch unsafe plans. Instead it blocked plans for being incomplete. The fix was a frozenset.


What Happened

I gave the critic this system prompt:

You are an adversarial plan reviewer.
Enter fullscreen mode Exit fullscreen mode

Six words. Seemed fine at the time.

After running 16 strict goals, every single one escalated. Not because the plans were unsafe. Because the critic was blocking on completeness concerns — "this plan could also cover edge case X" — instead of concrete defects.

The annoying part: the critic was technically doing its job. I told it to be adversarial. So it found something to object to in every plan. The problem was I didn't specify what kind of adversarial.

The product intent for strict mode was: "no concrete safety, ordering, or rollback violations." The critic was enforcing: "no reviewer concerns whatsoever." Those are different contracts, and I didn't notice until 16 goals had failed for the wrong reason.

The Real Examples

Here is what the critic flagged as a blocker before the fix:

A plan for cross-account VPC peering got blocked because the critic said the rollback plan "could also consider" DNS failback scenarios. That is a completeness suggestion, not a safety defect. The plan had rollback. It just didn't cover every hypothetical.

A plan for embedding index migration got blocked because the critic flagged a generic "risk" finding about potential latency during cutover. That is risk commentary, not a structural defect.

Both of these should have been warnings. The engine should have approved the plan with acknowledged risk. Instead, it escalated to a human.

The Fix

Two parts.

Prompt change: Replaced "adversarial plan reviewer" with "plan reviewer" and added explicit severity rules:

SEVERITY RULES — blocker is reserved ONLY for concrete, plan-local defects
that make the plan unsafe to execute:
  blocker = unsafe_sequencing, weak_rollback, unverified_dependencies, feasibility
  warning = risk, missing_steps
  info = minor observations
Do NOT escalate completeness or thoroughness concerns to blocker.
Enter fullscreen mode Exit fullscreen mode

Code guardrail:

_BLOCKER_ELIGIBLE_FAMILIES = frozenset({
    "unsafe_sequencing",
    "weak_rollback",
    "unverified_dependencies",
    "feasibility",
})

if severity == Severity.BLOCKER and item.heuristic_family not in _BLOCKER_ELIGIBLE_FAMILIES:
    severity = Severity.WARNING
Enter fullscreen mode Exit fullscreen mode

Even if the LLM returns blocker for a risk or missing_steps finding, the code downgrades it before it enters the findings list.

What Changed After the Fix

Before: every strict goal failed because the critic was blocking on advisory concerns.

After: zero advisory findings appeared as blockers across 92 post-fix runs. All 132 blockers that fired belonged to concrete defect families or deterministic gates.

The balanced goals still approved. The strict goals still escalated. But the reason changed. They escalated because the plans had real structural defects, not because the critic was being thorough.

Why the Prompt Alone Wasn't Enough

I tried fixing it with prompt engineering first. I added "only block on concrete defects" to the system prompt. The critic still escalated completeness concerns to blocker about 30% of the time.

That's the thing about LLMs. They don't have a stable concept of "this is bad enough to stop." It depends on the model, the temperature, and how the plan is worded. You can ask nicely. The model will still disagree with you when it feels like being thorough.

The code guardrail is deterministic. It does not depend on the LLM behaving correctly. That is why it works.

The prompt is helpful. The guardrail is the contract.

What I'd Do Differently

Start with the severity taxonomy, not the persona. Define what counts as a blocker in code. Write the prompt to match. Test with real data. The unit tests passed for me because they used hand-crafted inputs. The field test caught the bug because it used a real LLM producing real plans.

The research backs this up. The self-correction literature shows LLMs are unreliable at evaluating their own output — the "self-correction blind spot" affects ~64.5% of models (arXiv 2507.02778). If the model can't reliably evaluate correctness, it can't be trusted to decide what is fatal.


Article 2 of 5 in the PlannerCritic series.

Series: Article 1: "I Ran 157 Agent Plans Against a Real LLM" · Article 3: "The Planner Made the Same 3 Mistakes" · Article 4: "The Field Test Found 10 Issues" · Article 5: "I Tried to Prompt-Inject My Own Engine"

Links:

Top comments (1)

Collapse
 
peterbuildssecure profile image
Peter

This is a good separation between probabilistic classification and deterministic consequence. The model can propose a finding, but code decides whether that finding is allowed to stop execution.

One failure mode I’d test next is label migration. The allowlist prevents missing_steps from becoming a blocker, but the model may describe the same concern as unsafe_sequencing, which is blocker-eligible. The guardrail still trusts the model to choose the family correctly.

I’d keep the raw finding and normalized family in the evaluation data, then build boundary cases that differ by one fact: optional step versus required dependency, possible latency versus unsafe ordering, rollback improvement versus no viable rollback. A confusion matrix around those boundaries would show whether the guard removed the bad decision or merely moved it into classification.

For the highest-impact cases, a deterministic invariant such as “all irreversible steps have a verified predecessor and rollback condition” is stronger than accepting any model-selected blocker label.