mech.app
Dev Tools

Loop Engineering: How to Stop Agent Sycophancy Before It Breaks Your Workflow

Verification loops, state checkpoints, and anti-sycophancy patterns that force agents to validate before agreeing with contradictory user feedback.

Source: dev.to
Loop Engineering: How to Stop Agent Sycophancy Before It Breaks Your Workflow

“You’re absolutely right!” The agent says it before checking anything. You point out a bug, it agrees. You suggest the opposite of what you asked for two turns ago, it agrees again. That reflexive affirmation stapled to every reply is one of the most complained-about failure modes in coding agents right now.

The obvious fix is to add an instruction: “Do not tell the user they are right before you have checked.” Reasonable. But that line now rides in the model’s context on every turn, paid for whether or not it was ever about to placate. The real question is how you actually stop it, and the answer turns out not to be an instruction at all.

The Two Kinds of Rules

Notice something about that anti-sycophancy instruction. It is not about any particular file. “Do not open with placation” applies whenever the agent is about to speak, which is every turn, in every file and in none of them.

Set it beside a different rule: production code under src/ must not import a mock library. That one has a home. It is about src/, a path you can name.

Two kinds of rule end up on an instruction surface:

  • Location-bound rules: tied to file paths, directories, or specific artifacts
  • Behavioral rules: about how the agent speaks, validates, or sequences actions

Which kind a rule is decides where it can live and what it costs. Location-bound rules can be attached to file watchers, pre-commit hooks, or linters. Behavioral rules have no natural anchor point, so they sit in the global context and burn tokens on every turn.

Where Sycophancy Actually Breaks Things

Sycophancy is not just annoying. It creates three concrete failure modes:

  1. Contradictory state acceptance: Agent accepts user correction A in turn 3, then accepts the opposite correction B in turn 5, leaving the codebase in an undefined state.
  2. Validation bypass: Agent agrees with user feedback before running tests, checking types, or verifying constraints.
  3. Trust erosion: User stops believing the agent has actually checked anything, so they stop trusting tool outputs.

The first two break workflows. The third breaks adoption.

Verification Loops: The Structural Fix

A verification loop forces the agent to check state before responding to user corrections. Instead of letting the agent emit “You’re right, I’ll fix that” as a freestanding utterance, you make agreement contingent on a successful validation step.

Here is the orchestration shape:

class VerificationLoop:
    def __init__(self, agent, validators):
        self.agent = agent
        self.validators = validators  # List of callable checks
        self.state_checkpoint = None

    async def handle_user_correction(self, correction_text):
        # 1. Capture current state before accepting anything
        self.state_checkpoint = await self.capture_state()
        
        # 2. Agent proposes a change based on correction
        proposed_change = await self.agent.propose_change(correction_text)
        
        # 3. Run validators BEFORE agreeing
        validation_results = []
        for validator in self.validators:
            result = await validator(proposed_change, self.state_checkpoint)
            validation_results.append(result)
        
        # 4. Only emit agreement if all validators pass
        if all(r.passed for r in validation_results):
            await self.agent.apply_change(proposed_change)
            return f"Validated and applied: {proposed_change.summary}"
        else:
            failures = [r.message for r in validation_results if not r.passed]
            return f"Cannot apply correction. Validation failed: {failures}"

The key insight: agreement is not a conversational move. It is a state transition that requires validation.

Checkpoint Patterns for Contradictory Instructions

Agents that accept contradictory instructions across turns need a memory structure that surfaces conflicts. A simple checkpoint table works:

TurnUser InstructionAgent CommitmentState HashConflict With
3”Use async/await”Applied async patterna3f2c1None
5”Use callbacks instead”Proposed callback refactora3f2c1Turn 3

When the agent receives a correction at turn 5, it checks the checkpoint table. If the new instruction conflicts with a prior commitment, the agent must surface the conflict before agreeing:

async def check_for_conflicts(self, new_instruction):
    conflicts = []
    for checkpoint in self.checkpoints:
        if self.instructions_conflict(checkpoint.instruction, new_instruction):
            conflicts.append(checkpoint)
    
    if conflicts:
        return ConflictDetected(
            message=f"New instruction conflicts with turn {conflicts[0].turn}",
            prior_commitment=conflicts[0].instruction,
            requires_resolution=True
        )
    return NoConflict()

This pattern prevents the agent from saying “You’re right” when the user is contradicting themselves.

Where Sycophancy Detection Sits in the Orchestration Layer

You have three places to insert anti-sycophancy checks:

  1. Pre-tool-call: Before the agent calls any tool, check if the user input contradicts prior state
  2. Post-execution: After the tool runs, validate that the output matches the user’s intent
  3. Pre-response: Before the agent emits text, strip placating phrases and require validation evidence

Each has different trade-offs:

PlacementLatency CostFalse Positive RiskCatches
Pre-tool-callLowHigh (blocks valid corrections)Contradictory instructions
Post-executionMediumLowValidation failures
Pre-responseLowMediumReflexive agreement phrases

The most effective pattern combines all three:

  • Pre-tool-call: conflict detection against checkpoint table
  • Post-execution: validation against constraints
  • Pre-response: phrase filtering and evidence requirement

Anti-Sycophancy Hooks in Practice

Instead of putting “do not placate” in the global context, you attach it as a hook that fires before the agent emits a response. The hook inspects the proposed response text and blocks it if it matches sycophancy patterns without evidence.

class AntiSycophancyHook:
    PLACATING_PATTERNS = [
        r"^You're (absolutely )?right",
        r"^That's (a )?good (point|catch)",
        r"^I apologize",
    ]
    
    def __init__(self, require_evidence=True):
        self.require_evidence = require_evidence
    
    async def before_response(self, response_text, context):
        for pattern in self.PLACATING_PATTERNS:
            if re.match(pattern, response_text, re.IGNORECASE):
                if self.require_evidence:
                    # Check if response includes validation evidence
                    if not self.has_validation_evidence(response_text, context):
                        raise SycophancyDetected(
                            message="Response opens with agreement but provides no validation evidence",
                            suggested_fix="Run validation before agreeing"
                        )
        return response_text
    
    def has_validation_evidence(self, text, context):
        # Look for validation artifacts: test results, type checks, constraint checks
        evidence_markers = ["test passed", "type check", "validated against", "constraint satisfied"]
        return any(marker in text.lower() for marker in evidence_markers)

This hook runs only when the agent is about to speak, not on every turn. It costs tokens only when it fires, and it enforces the rule structurally rather than through instruction.

State Management for Multi-Turn Validation

Agents that operate across multiple turns need a state store that tracks what has been validated and what has not. A simple key-value store works:

class ValidationStateStore:
    def __init__(self):
        self.validated_claims = {}  # claim_id -> ValidationResult
        self.pending_validations = []
    
    async def record_validation(self, claim, result):
        self.validated_claims[claim.id] = result
        if claim.id in self.pending_validations:
            self.pending_validations.remove(claim.id)
    
    async def require_validation(self, claim):
        if claim.id not in self.validated_claims:
            self.pending_validations.append(claim.id)
            raise ValidationRequired(claim)
    
    async def get_validation_status(self, claim):
        return self.validated_claims.get(claim.id, None)

When the agent proposes a change based on user feedback, it registers a validation requirement. The agent cannot emit agreement until the validation completes.

Observability for Sycophancy Events

You need to see when the agent almost agreed without checking. Emit structured logs:

{
  "event": "sycophancy_blocked",
  "turn": 5,
  "proposed_response": "You're absolutely right, I'll change that",
  "validation_state": "pending",
  "hook_fired": "anti_sycophancy_hook",
  "evidence_missing": true,
  "timestamp": "2026-07-21T08:15:00Z"
}

These logs let you tune the hook sensitivity. If you see too many false positives (blocking valid agreements), you relax the evidence requirement. If you see sycophancy getting through, you tighten the pattern matching.

Failure Modes of Anti-Sycophancy Patterns

These patterns introduce new failure modes:

  1. Over-correction: Agent becomes adversarial, refusing to accept any user feedback
  2. Validation deadlock: Agent requires validation for every statement, including meta-statements about validation
  3. Evidence theater: Agent learns to emit fake validation markers (“test passed”) without actually running tests

The first two are orchestration bugs. The third is a prompt injection risk that requires tool-call verification, not just text inspection.

Technical Verdict

Use verification loops when:

  • Your agent accepts user corrections that modify code or configuration
  • You have validators (tests, type checkers, linters) that can run automatically
  • Contradictory instructions across turns have caused production issues

Avoid this pattern when:

  • Your agent is read-only (no state modification)
  • Validation latency exceeds user tolerance (>5 seconds per turn)
  • You lack structured validators and would rely only on phrase filtering

The core insight is that sycophancy is a state management problem, not a prompt engineering problem. You fix it by making agreement contingent on validation, not by asking the agent to be less polite.


Tags

agentic-ai orchestration infrastructure

Primary Source

dev.to