close

DEV Community

Cover image for Loop Engineering: How To Stop The "You're Absolutely Right" Sycophancy
Gábor Mészáros Subscriber for Reporails

Posted on • Edited on • Originally published at reporails.com

Loop Engineering: How To Stop The "You're Absolutely Right" Sycophancy

"You're absolutely right!" The agent says it before it has looked at anything. You point out a bug, it tells you you're right. You suggest the opposite of what you asked for a minute ago, it tells you you're right again. That reflexive agreement, the little affirmation stapled to the front of every reply, is one of the most complained-about tics coding agents have right now, and once you notice it you cannot stop noticing it.

The obvious fix is to tell it to stop. Add a line to your instructions: do not open with placation, 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 single turn, read again on the turns the agent was going to agree politely and the turns it was going to do real work, paid for whether or not it was ever about to placate. So the real question the title asks is how you actually stop it, and the answer turns out not to be an instruction at all.

Notice something about that line, though. 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 you might add: 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, the kind you can pin to a file path and the kind you cannot, because they are about behavior rather than location. Which kind a rule is decides where it can live and what it costs to keep it there. That split is what this piece is about.

A decision split: a rule on the surface branches on whether it is about a location or a behavior; a location rule pins to its file path and loads only there, a behavior rule has no path and moves to a check.

Both kinds pile up regardless. Your CLAUDE.md started at nine lines, and six months later it is three hundred. The no-placation rule joins the mock-import rule from the last two pieces, the payments-module invariant, the frontend spacing scale, the deploy checklist, the API-versioning policy, and a paragraph on how to phrase migration names. This morning you asked the agent to fix a typo in the README, and every one of those three hundred lines rode into its context alongside the request. None of them had anything to do with the README.

The series opener named the loop that runs generate, check, steer, retry, stop, and posed three questions about the parts it turns on. Two pieces answered the first two: the check that decides good enough, stop, and the gate that refuses a bad write before it lands. This one takes the third question the opener posed, the part underneath both of the others: the context surface. It is the next arm on the bench, with more still to come. Every check and every gate is fed by rules the agent carries as loaded text, and that text is paid for on every turn whether it applies to the turn or not. So the question the opener left last is a cost question. What does an instruction actually cost to keep loaded, and what changes when you load it only where it applies. The answer runs straight through the split above: one kind of rule you can teach the harness to load only where it belongs, and the other kind, the placation rule among them, you handle a different way entirely.

What the surface is here

The surface is everything the model sees before it reads your request. The system prompt, the tool definitions, whatever documents got retrieved, and the instruction files (CLAUDE.md, AGENTS.md, and their nested siblings) that ride along on every turn. Prompt engineering tuned the request. Context engineering curated this surface. The loop reuses it: the surface is the state the loop carries between iterations, re-sent each time the model generates.

The agent loop drawn as a cycle: the context surface feeds the model, the model generates a reply, block-placation.sh checks it, a placated reply routes through steer and retry back to the model, a clean reply reaches continue; the context surface node is marked as the part this piece takes apart.

"Re-sent each time" is the property this piece turns on. A conversation is not cumulative from the model's side. Each turn, the whole surface is assembled and handed over again from the top. The three-hundred-line instruction file is not loaded once at the start of the session and remembered. It is loaded on turn one, and again on turn two, and again on the turn where you were editing a README. Whatever sits on the surface is paid for at every one of those turns, in full.

A row of turns along a timeline; the same block of instruction text is re-stamped into the context window at every turn, with a README-edit turn highlighted where none of the loaded rules apply but all of them are still paid for.

What loaded costs, part one: tokens

Put arithmetic on it, as a worked illustration and not a measurement. Say the instruction file has grown to a few thousand tokens, which three hundred lines of prose comfortably will. A working session runs dozens of turns. At forty turns, the file is assembled into the context window forty times. If two thirds of it is rules that only matter in one corner of the codebase, you have paid to keep a few thousand tokens of mostly-irrelevant text in the window on every one of those forty turns, and two thirds of that spend bought nothing on the turn it rode along on.

The numbers are illustrative; the shape is not. An always-loaded instruction costs its length times the number of turns it survives, because the window is rebuilt every turn, so a rule you write once and never delete is one you pay for thousands of times over a session, whether or not any given turn had a use for it.

What loaded costs, part two: attention

Tokens are the measurable half. The other half is what the extra text does to the model reading past it.

The State of AI Instruction Quality pointed a deterministic analyzer at 28,721 repositories and found the median instruction file carries 50 content items and 12 actual directives. The other 38 items are headings, restated context, and structure the model is free to ignore. Every turn, the model has to find the twelve rules that bind inside the fifty items that are loaded, and the thirty-eight that do not bind are still text it reads on the way to the work. Adding a rule that does not apply to the current task costs its tokens and widens the pile the relevant rules are hiding in.

There is a measured effect under that pile. A vaguely-phrased instruction is followed less reliably as more unrelated instructions load beside it on the same turn: the same rule that the model largely honors when it is one of a few gets superficially acknowledged and then ignored once it is one of many, and the reporails corpus measures that decline rising with the number of competing topics. The rule did not change. The company it was loaded with did. The lever it points at is fewer rules loaded at once, with the relevant one present on the turn it applies rather than on every turn.

This is the surface reporails reads. It points the same deterministic analysis at your own instruction files and tells you, with measured evidence, which instructions couple to behavior and which are text the model can ignore, so you can see what you are actually paying to keep loaded instead of guessing at it. It does not load your context or trim it at runtime. It measures the surface you wrote down and reports where the spend is not buying compliance. Knowing which of your fifty items are the twelve that bind is the precondition for the next move, which is deciding where each of them should live.

The rules you can pin to a path

Start with the first kind, the rules that have a path. For a located rule the move is to load it only on the turns it applies to instead of on every turn, and the mechanism is older than agents: a thin index at the top that routes to depth, and depth that loads on match.

Claude Code ships exactly this affordance, and it is worth being concrete about it because the build below leans on it. A CLAUDE.md at the project root loads at the start of every session, so it is the always-on surface. A CLAUDE.md placed inside a subdirectory does not: the harness loads it on demand, when the agent reads or edits a file in that subtree, and not before (per the Claude Code memory docs). A rule that only governs src/payments/ can live in src/payments/CLAUDE.md, and it reaches the model on the turns the agent works in src/payments/, on those turns only. The root file keeps the handful of rules that apply everywhere. Everything path-specific moves down to the path it belongs to.

That subdirectory file is the simplest form of path-scoped loading. The frontmatter-scoped form is more precise, and it does not need a file parked in the directory at all. Claude Code also reads .claude/rules/*.md, and a rule file carrying a paths: glob in its frontmatter loads only for matching files. The docs state it plainly: "Path-scoped rules trigger when Claude reads files matching the pattern, not on every tool use." Tag a rule paths: ["src/**/*.py"] and it rides the surface when the agent touches Python under src/ and is absent otherwise.

The same primitive ships across every major agent tool, each with its own frontmatter key. Cursor scopes a .cursor/rules/*.mdc file with a globs: field, auto-attaching the rule "when a matching file is in context." GitHub Copilot scopes a .github/instructions/*.instructions.md file with an applyTo: glob, applying it to "requests made in the context of files that match a specified path." Three vendors, one idea: attach a rule to a file pattern, and pay for the rule only when a matching file is in play.

Reporails canonizes that cross-vendor mapping and checks for it. Its path-scope-declared rule (CORE:S:0038) verifies that a path-scoped instruction file actually declares the scope it governs, and the Claude paths, Cursor globs, and Copilot applyTo equivalence is the exact thing the rule holds constant across agents. The full public rule set lives at reporails.com/rules.

That is the whole idea. The always-on surface carries what is always relevant, and a rule that is relevant only sometimes loads only then.

This scales past a handful of nested files. The steering setup I work in runs its whole rule corpus this way: each rule is subscribed to the transition it governs, and its full body loads only when that transition fires. The system can report what actually loaded, and on a working session the report runs lopsided: a single steering rule loaded its full body, a handful more stayed one-line pointers, and a dozen-plus subscribed rules never loaded at all, because nothing in the session's work touched their triggers. That is the shape you are after: the rules that applied were in context when they applied, and the rules that did not apply cost a pointer or nothing, instead of a full body on every turn. One caveat the same receipt discipline demands stating: a pointer delivered is not a rule followed, so a rule that must hold every time cannot lean on being loaded only when it happens to apply. It also gets a deterministic floor, the cheapest place of all to keep a rule, which the placation case below turns out to need. The build after that is one you can run yourself, without any of this machinery.

Build it: split the file that grew

Take the running example the series has used throughout. The no-mocks rule from the check and the gate pieces says production code under src/ must not import a mock library. In the three-hundred-line file, it sits in the always-on surface, loaded on every turn, including the turn you spent editing the README where it could not possibly apply.

The split that follows is itself something reporails measures. Its modular-file-organization rule (CORE:S:0010) checks whether an instruction surface distributes its rules across scoped files or piles them into one monolith, which is the exact shape a grown three-hundred-line CLAUDE.md has taken. The build below is what passing that check looks like on your own repo.

Start by sorting the file by scope, not by topic. For each rule, ask one question: does this apply to every turn, or only to turns that touch a particular path or task? The no-mocks rule applies only when the agent is writing Python under src/. The commit-message convention applies only when the agent is composing a commit. The frontend spacing scale applies only under web/ or theme/. The "which test runner" note genuinely does apply everywhere, so it stays.

Then move each rule to where its scope is. The root CLAUDE.md shrinks to the always-relevant minimum:

# Project instructions (root, always loaded)
- Test runner is `pytest`; never edit files under `generated/`.
- Commit style and per-area rules load from the CLAUDE.md nearest
  the file you are editing.
Enter fullscreen mode Exit fullscreen mode

The no-mocks rule moves down to src/CLAUDE.md, beside the code it governs:

# src/ instructions (loaded when the agent works under src/)
- Production code uses a real dependency or a constructor-injected fake;
  do not import a mock library. no-mocks.sh and the PreToolUse gate
  both enforce this; see the earlier pieces in this series.
Enter fullscreen mode Exit fullscreen mode

Now the README turn loads the root file and nothing under src/, because the agent never opened anything under src/. The no-mocks rule, the payments invariant, the frontend scale: none of them ride along, because none of their paths were in play. When the agent does start editing src/payments/charge.py, src/CLAUDE.md loads on that turn, carrying the no-mocks rule to exactly the moment it applies. It is present on every turn it could bind and absent on every turn it could not, and the always-on surface dropped from three hundred lines to a handful.

One discipline keeps the split honest: the nested src/CLAUDE.md extends the root and stays consistent with it. The root names the test runner and points to the nearest file; src/ adds the no-mocks constraint on top and overrides nothing above it. Reporails checks exactly this as child-nested-instructions (CORE:S:0011): a nested instruction file has to build on its parent and hold consistent with what the parent already says, so a subtree file that quietly reverses a root rule is a defect the check catches.

Two turns compared: on any turn the root CLAUDE.md loads; a branch on whether the turn touches src/ shows the src rules staying unloaded when it does not, and src/CLAUDE.md loading for that turn only when it does.

Progressive disclosure changes when the rule loads while leaving what the rule says untouched. The strictness is intact; only the standing cost falls. The check and the gate from the earlier pieces still sit beside the rule, unaltered, refusing to pay for it on the turns it has nothing to say.

The rule with no path

Every mechanism in the last two sections scopes a rule by path: the nested CLAUDE.md, the paths: glob, Cursor's globs:, Copilot's applyTo:. They all answer one question, which file is the agent touching, and load the rule when the answer matches. That works because the no-mocks rule is tied to a location. It is about Python under src/, and src/ is a path you can name.

The placation rule from the opening has no such path. "Do not open with placation" is not about where the agent is working. It is about what the agent is about to say, on any turn, in any file or in no file at all. There is no glob for it. You cannot write paths: or applyTo: for a behavior, because no file pattern predicts a behavior, so the whole path-based toolkit has nothing to offer this rule. It cannot be scoped the way a location rule can. If it stays on the surface, it stays loaded on every turn.

This is where the title's question gets its answer. The rule leaves the surface a different way: not to a narrower path, but to a check.

"Never open with placation, never tell the user they are right before you have checked" is a rule whose violations are rare and mechanically recognizable: the tell is a short list of opener phrases. That makes it a poor fit for loaded text and a good fit for a check. Instead of a stronger instruction paid on every turn, write a deterministic Stop-hook check: a scan that runs when the agent finishes a message, matches the banned openers in the outgoing text, and fires only when one actually appears. On every turn where the placation does not happen, the rule costs nothing, because it is not on the surface at all. It is present as enforcement and absent as loaded text. You did not load a bigger rule. You moved the rule off the surface.

Concretely, here are both forms of the same rule. The steering form is the loaded line, the request that rides every turn, in CLAUDE.md:

# Voice (loaded every turn)
- Open with the substance, not placation. Affirm the user only after you have checked.
Enter fullscreen mode Exit fullscreen mode

The enforcement form is a check you write yourself, a dozen lines of bash, and it closes the loop on the agent's own output. A Stop hook runs when the agent finishes its turn, and Claude Code hands it the final reply as last_assistant_message on stdin (per the Claude Code hooks reference). The hook checks the reply, steers the model when it placates, lets it retry, and stops when the reply comes back clean or once it has already been steered. Save it as .claude/hooks/block-placation.sh:

#!/usr/bin/env bash
# block-placation.sh: one arm of the loop, run as a Claude Code Stop hook.
# check the reply, steer the model when it placates, let it retry, stop when it comes back clean or once steered.
payload=$(cat)                                                 # the Stop event arrives as JSON on stdin
reply=$(jq -r '.last_assistant_message // ""' <<<"$payload")  # the final assistant text of the turn
steered=$(jq -r '.stop_hook_active // false' <<<"$payload")   # true when this turn is already a re-steer

# check: does the reply open with placation?
if ! grep -qiE '^[[:space:]>*_-]*(you.?re +(absolutely +|completely +|so +)?right|great question|absolutely[[:punct:]])' <<<"$reply"; then
    exit 0    # clean, so stop and let the turn finish
fi

# stop arm: if the last turn was already a re-steer and it still opens this way, let it through instead of bouncing forever
[ "$steered" = "true" ] && exit 0

# steer: block the stop and hand back a correction, so the model regenerates the reply (the retry)
echo "Revise before finishing: this reply opens with an affirmation before checking anything. Drop the opening phrase and lead with the substance." >&2
exit 2
Enter fullscreen mode Exit fullscreen mode

Wire it as a Stop hook in .claude/settings.json:

{
  "hooks": {
    "Stop": [
      { "hooks": [ { "type": "command", "command": ".claude/hooks/block-placation.sh" } ] }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Same rule, two forms, and it is the steering-versus-enforcement split the gate piece drew, now aimed at a behavior instead of a file write. The loaded line asks the model not to placate and is paid on every turn. The hook does not ask. It reads what the agent produced (the check), and on a clean reply it exits and lets the turn stop, so it costs nothing on every turn the placation does not happen because it is not on the surface at all. On the turn a reply opens with you're absolutely right, the hook refuses the stop and hands back a correction (the steer), and the model regenerates the reply (the retry). The loop then terminates one of two ways: the new reply comes back clean, so the hook lets it stop, or it opened placating a second time, in which case the stop_hook_active guard lets it through rather than bounce forever. Claude Code also caps continuations with an 8-consecutive-continuation backstop, so even a hook that forgot its own stop arm cannot loop indefinitely, but the point of a well-built loop is that it names its own stop condition instead of leaning on the platform's backstop. It is the same generate → check → steer → retry → stop shape the opener drew, now closing on a behavior.

Two panels of the same placation rule: on the left it is a CLAUDE.md line loaded on every turn (the steering form); on the right the line is gone from the file and the rule lives in a block-placation.sh Stop hook that fires only when a reply opens with a placation phrase (the enforcement form).

I run this exact check against my own coding agent's replies, a deterministic scan that fires when a banned opener shows up in what the agent just wrote. In one working session it caught six replies that led with you're right before the agent had looked at anything, each one flagged at the boundary instead of sliding past. A real catch on real output, reproducible, and the banned phrase never had to ride on the context surface for the scan to find it. The rule earned its keep by matching the violation, not by being read on every turn it did not apply.

The gate piece built a hook to refuse a bad write, so the boundary is worth drawing. That piece was about a channel that can say no where a prompt can only ask. This one is about cost. The check is a deterministic scan of the text the agent produced, the same shape as the diff-scan the check piece built, and its context cost is zero on every clean turn. A rule you can express as a check on the output is a rule you never have to keep loaded to enforce.

That splits two axes that are easy to run together. Path-scoping decides when a location-tied rule loads, and it is the right tool for every rule that has a path. It says nothing about whether a rule that did load actually moved the behavior, which is the other axis, and the one that decides whether a behavior rule was worth writing at all. Measuring which instructions on your surface couple to behavior, and which are text the model reads and ignores, is what reporails does: it reads the surface you wrote down and reports the coupling, the axis a path glob cannot reach. The two do not compete. Path-scoping trims what loads; the coupling read tells you whether what loaded earned its slot.

Which rule loads where

The sort has a failure mode in each direction, the same shape the gate piece found for steering versus refusing.

Push a genuinely global rule down into a subtree and it goes missing on the turns it was supposed to cover. "Never edit generated/" scoped to src/ does not reach the agent when it is about to edit a generated file under web/, which is one of the turns you wrote it for. A must-hold-everywhere rule belongs on the always-on surface, and the cost of keeping it there is the cost you should pay, because its scope really is every turn.

Hoist a narrow rule up into the root and you are back where you started. It loads on every turn, taxes every turn, and pads the pile the global rules hide in. The payments-module invariant on the always-on surface is billed on the README turn and the frontend turn for nothing.

The heuristic is the scope question made explicit. A rule loads at the level whose every access it governs. Applies to the whole repo, root file. Applies to one package, that package's file. Applies to one task the agent performs occasionally, a file or skill that loads on that task's description match rather than on every turn. The test is not how important the rule is. A critical rule with a narrow scope still loads narrowly; importance decides whether you also back it with a gate, which the gate piece covers, not where the rule loads.

The loop only checks what you wrote down

Three arms of the loop taken apart so far, and the loop is not out of arms. The check decides good enough, stop, and a misfiring check is a broken instrument before it is an absent signal. The gate refuses a bad write before it lands, and a rule that must hold every time belongs in the channel that can say no. The surface is what feeds both, and every rule on it is loaded text you pay for on every turn it survives, so a rule that applies only sometimes should load only then, and a rule you can turn into a check on the output does not need to sit on the surface at all.

What ties them together is the thing the series keeps returning to. The loop only ever checks, gates, and steers on what you wrote down. The check runs the rule you encoded; the gate refuses on the pattern you specified; the surface carries the instructions you authored and hands them to the model turn after turn. None of it reaches past the text you put there. That is why the text is worth engineering. What it says decides whether the loop catches the right thing, and where it loads decides what every turn costs to run. Load the whole surface every turn and you pay for all of it on turns that needed almost none of it. Load each rule where it applies and the loop still holds, on the turns that matter, for a fraction of the standing cost.

There is an arm this series has kept naming in passing and has not yet taken apart: the steer. Between one attempt and the next, the loop carries something back to the model, the check's own output, rephrased into the next instruction. What it carries back is a choice, and a careless one poisons everything downstream of it. Feed back the wrong signal and the next check judges the next attempt against the wrong thing, so the loop converges, confidently, on an answer no one asked for. What the loop should hand back between tries, and what breaks when it hands back too much or the wrong part, is the next arm to measure.


I work on Reporails, deterministic diagnostics for the instruction files, rules, and prompts that steer coding agents. It reads the steering surface and tells you, with measured evidence, which instructions couple to behavior and which are text the model is free to ignore. It does not load or trim your context; it measures the surface you wrote down, so you know what you are paying to keep in front of the model.

Top comments (9)

Collapse
 
jugeni profile image
Mike Czerwinski

The generalizable move here isn't "add a hook," it's that a behavioral rule is really a locational rule wearing a disguise, as long as it has a fixed point to check at. "Don't open with praise" is checkable at message-start the same way "no mock imports" is checkable at a file path. Both get cheap once you find the fixed point.

Where does this stop working though? Something like "don't be condescending" doesn't have an obvious fixed point, it's distributed across the whole response, not anchored to an opening line. Have you run into rules that resisted this move, or does everything behavioral eventually reduce to a checkable location if you look hard enough?

Collapse
 
skillselion profile image
Skillselion

The location vs behavior split maps cleanly onto what I see in skill install data (I work on a skills directory, so I stare at this all day): the fastest-growing skills right now are exactly packaged behavior rules (force-minimal-diffs, no-AI-prose-patterns, token diets). People are voting with installs to move behavior rules out of the always-loaded CLAUDE.md and into things that load on demand or run as checks, for the exact context-tax reason you describe.

One boundary case I keep hitting: rules that are behavioral but domain-scoped. "Frontend spacing scale" is a behavior rule, no single file path owns it, yet it only matters when the agent touches UI. Does your framework treat scoped-behavior as a third category, or does it collapse into "check" once you can write a linter for it?

Collapse
 
cleverhoods profile image
Gábor Mészáros Reporails

Yep, the install-data point is the thesis in miniature: people are paying the context tax knowingly and moving behavior rules off the always-loaded surface.

On the boundary case, I don't think scoped-behavior is a third category. Location and checkability are two orthogonal axes, and "frontend spacing scale" scores on both. It has a loose path-scope, since it only fires when the agent touches web/, *.css, or *.tsx, so the domain is the glob: attach it with globs: / applyTo: and it loads on UI turns only, the same move as a location rule. And it's deterministically checkable, so stylelint enforces the 4px grid outside the model, exactly like you said.

So it doesn't become a third thing, it lands on both axes at once. The linter is the enforcement the model can't wish an off-grid value past. The loaded rule then shrinks to a one-line pointer that just tells the model the constraint exists, worth keeping even with stylelint in place, because a model that knows the rule fights it less on the retries. The only bucket with nowhere to go but a Stop-hook check on the output is pathless-and-uncheckable, like the placation rule.

Collapse
 
seven7763 profile image
Seven

"You're absolutely right" is the symptom; unbounded tool loops are the disease.

What has helped me is treating sycophancy as a harness problem: require a concrete artifact change (diff, test result, failing assertion) before the loop can continue, and add an explicit "I could be wrong — here is the evidence" exit. Without that, the model optimizes for agreeable text instead of progress.

Collapse
 
cleverhoods profile image
Gábor Mészáros Reporails

I read your point as landing on a different arm than this piece does, and that's the productive part. This piece is about what a loaded instruction costs. The continuation condition you're describing, gate the loop on a real artifact change, is a couple of arms over, in the check and the stop.

Requiring a concrete artifact change does kill the pure sycophancy case: the model can't clear the gate with agreeable text, it has to produce a diff or a test result, and most loops don't even ask for that much.

Note: the artifact gate is itself gameable. "Produce a change that clears the check" and "make progress on the goal" are not the same requirement, and a model under pressure takes the cheaper one. Hand it a failing test and "make it pass," and it can edit the assertion to match the bug. A real diff, a green check, the gate satisfied, the goal gone. So the artifact you gate on has to be one the agent can't rewrite to clear, and the signal you feed back between tries has to carry the goal, not just "make it pass." I just wrote that specific failure up:
dev.to/reporails/loop-engineering-...

The evidence-exit is the other half, and the part almost nobody builds. A loop that only knows "continue until the check clears" will stop on a check it gamed. One that can say "I could be wrong, here is what I checked" is a separate arm from the check, and you're right that without it the model optimizes for the agreeable ending instead of the correct one.

Collapse
 
kartik-nvjk profile image
Kartik N V J K

The location-rule vs behavior-rule split is a really clean framing, and it matches something I've been running into: the behavior rules don't belong in CLAUDE.md at all, they belong as a post-generation check that either passes or rewrites. Pushing sycophancy suppression into a lint step (regex the opener, refuse the turn if it starts with placation) means the context stays clean and the rule becomes deterministic instead of probabilistic. Have you tried gating with an actual assertion rather than a prompt line?

Collapse
 
cleverhoods profile image
Gábor Mészáros Reporails • Edited

Yes, and running it for a while taught me it is less assertion instead of the prompt line than assertion plus a compose-time version of the same rule, doing two different jobs.

The assertion is the Stop check you describe: regex the opener, refuse the turn (exit 2) with a correction, let the model regenerate. It is deterministic, it fires on the actual output, and it is free on every turn the placation does not happen because it is off the context surface. That is the build the piece lands on.

Where the second layer comes in: the assertion fires after the text is generated. In a streamed setup the user has already seen the opener by the time the Stop check refuses it, and it only ever fires on the pattern you wrote down. So I also keep a compose-time form of the rule that shapes the draft before it emits. That half is probabilistic and it slips exactly when the context window is full, which is when its needed the most. The assertion is the backstop that cannot be argued out of firing; the compose-time rule is what tries to make the draft clean in the first place. Neither alone was enough for me.

Two things the assertion form teaches that a prompt line hides. One, the pattern is a moving target: "you're absolutely right" is easy, but "great point", "fair enough", "that makes sense" walk past a naive opener list, so the regex is something you tune against real transcripts rather than set once. A missed match is a gap you can see and close, where a probabilistic rule just fails silently. Two, the ceiling: the check catches the tell, not the agreeableness. A model that learns the opener is banned can still be sycophantic two sentences in. You get determinism on exactly the part you can express as a pattern, and the rest stays prompt-and-pray.

Collapse
 
hemanth_from_2050 profile image
Hemanth

good one

Some comments may only be visible to logged-in visitors. Sign in to view all comments.