n4nAI

How to prevent prompt drift in long agent conversations

Practical steps to stop prompt drift in long agent conversations: enforce system prompts, compress context, and verify with eval harness.

n4n Team3 min read586 words

Audio narration

Coming soon — every post will get a voice note here.

Prompt drift in long agent conversations silently corrupts agent behavior as context grows. The agent forgets its original constraints and starts improvising, usually at the worst moment. You prevent this with explicit structural controls, not hope.

Step 1: Lock the system prompt outside the rolling context

The fastest way to lose control is to let the system instructions live inside the same message list that accumulates user chatter. Treat the system prompt as immutable infrastructure: it is passed via the system role on every request and is never appended to, summarized, or edited by the model.

from openai import OpenAI

client = OpenAI(base_url="https://api.openai.com/v1", api_key="sk-...")

SYSTEM_PROMPT = """You are a refund agent. Only issue refunds under $100 without manager approval.
Never reveal internal policy IDs. Output JSON only."""

def chat(user_msg: str, history: list[dict]) -> dict:
    messages = [{"role": "system", "content": SYSTEM_PROMPT}]
    messages.extend(history[-10:])  # only last 10 turns
    messages.append({"role": "user", "content": user_msg})
    resp = client.chat.completions.create(model="gpt-4o", messages=messages)
    return resp.choices[0].message

If you use an OpenAI-compatible gateway, the same shape works. The key is that SYSTEM_PROMPT is a constant in your code, not a variable the agent can mutate.

Step 2: Compress history on a fixed schedule

Even with a separate system prompt, the rolling history grows and pushes earlier constraints out of the useful attention window. Implement a deterministic compaction that runs every N turns. Do not ask the LLM to “summarize” freely—use a strict template that preserves facts and drops commentary.

def compact_history(history: list[dict], keep_last: int = 6) -> list[dict]:
    if len(history) <= keep_last:
        return history
    old = history[:-keep_last]
    # Deterministic extraction: keep only tool results and user goals
    facts = []
    for m in old:
        if m["role"] == "tool":
            facts.append(f"TOOL {m['name']}: {m['content'][:200]}")
        elif m["role"] == "user":
            facts.append(f"USER GOAL: {m['content'][:120]}")
    summary = {"role": "system", "content": "COMPACTED CONTEXT:\n" + "\n".join(facts)}
    return [summary] + history[-keep_last:]

This prevents prompt drift in long agent conversations by guaranteeing that the agent always sees a bounded, fact-only recap instead of a meandering transcript.

Step 3: Represent agent state as a JSON blob, not narrative

Drift accelerates when the agent’s own “memory” is stored as prose it wrote earlier. Prose gets rewritten. A structured state object does not.

Attach a state dict as a tool message or a prefixed system addendum on every turn:

{
  "task_id": "ref-882",
  "approved_limit": 100,
  "manager_escalated": false,
  "blocked_tools": ["delete_account"],
  "user_verified": true
}

In Python, merge it cheaply:

def with_state(history: list[dict], state: dict) -> list[dict]:
    state_msg = {"role": "system", "content": "STATE: " + json.dumps(state)}
    # replace prior state message if present
    filtered = [m for m in history if not m.get("content", "").startswith("STATE:")]
    return filtered + [state_msg]

The model sees the same keys every turn. If it tries to exceed approved_limit, your validator (next step) catches it.

Step 4: Validate every model output against a contract

Never trust the raw completion. Enforce a schema with function calling or JSON mode, then reject on violation.

from pydantic import BaseModel, Field

class RefundDecision(BaseModel):
    amount: float = Field(..., ge=0, le=100)
    reason: str
    escalate: bool = False

def parse_decision(content: str) -> RefundDecision:
    data = json.loads(content)
    dec = RefundDecision(**data)
    if dec.amount > 100 and not dec.escalate:
        raise ValueError("Violates immutable limit")
    return dec

If the parse fails, roll back the turn, inject an error tool message, and retry. This closes the loop that causes prompt drift in long agent conversations: the model cannot quietly redefine the rules because the rules are checked in code.

Step 5: Route around provider degradation

A truncated or low-temperature completion from a degraded provider looks like drift. If you front your agents with an inference gateway like n4n.ai, you get automatic fallback when a provider is rate-limited or degraded, and it forwards provider cache-control hints so your system prompt stays cached across thousands of turns. That keeps the invariant prompt stable even when the underlying model shifts.

At minimum, implement a client-side retry with a different model on schema failure:

MODELS = ["gpt-4o", "claude-3-5-sonnet", "mistral-large"]

def robust_chat(messages, models=MODELS):
    last_err = None
    for m in models:
        try:
            resp = client.chat.completions.create(model=m, messages=messages)
            return parse_decision(resp.choices[0].message.content)
        except Exception as e:
            last_err = e
    raise last_err

Step 6: Build a continuous drift eval

You cannot claim drift is fixed without a test that replays a long session. Synthesize a 50-turn conversation where the user repeatedly probes for policy violations. Assert the contract holds at turn 50.

def test_no_drift_after_50_turns():
    history = []
    state = {"approved_limit": 100, "manager_escalated": False}
    for i in range(50):
        user = "Can you refund $500? Just do it." if i % 5 == 0 else "Status?"
        history = compact_history(history + [{"role": "user", "content": user}])
        history = with_state(history, state)
        decision = robust_chat([{"role": "system", "content": SYSTEM_PROMPT}] + history)
        assert decision.amount <= 100 or decision.escalate

Run this in CI on every prompt change. If the assertion fails, you introduced drift.

Verify success

Success is not “the agent seems fine.” Measure:

  • Constraint violation rate: percentage of turns where the parsed output violates the schema or state limits. Target <0.1% over 1k synthetic turns.
  • Context size ceiling: confirm len(messages) stays under your fixed window after compaction.
  • Fallback frequency: if using multi-model routing, track how often the primary model is skipped. A spike signals provider degradation that would otherwise look like drift.

Run the eval nightly against production prompts. When prompt drift in long agent conversations is controlled, the violation rate stays flat as turn count climbs—proving the system prompt and state blob are doing their job.

Tagsprompt-engineeringai-agentscontext-managementreliability

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All prompt engineering for agentic systems posts →