Guides
Detection guide

LLM Monitoring: How to Detect Behavior Drift in Production

By Sazabi Team

Last verified on Nov 8, 2026.

An LLM feature can break without a deploy. The model behind an alias changes, a prompt gets edited, retrieval starts returning different documents or users send a new kind of input. The code is the same, so no exception fires. What changes is the output: it gets longer, shorter, more cautious or less well formed.

Drift is a statistical problem. A single odd answer is noise. The signal is a rate moving: parse failures going from 0.1% to 4%, refusals doubling or median output tokens jumping 40%. You catch it by logging a few cheap fields on every call and watching their rates by model and prompt version.

What the evidence looks like

Log one record per model call. This example has the fields that matter:

{
  "ts": "2026-11-08T11:20:41Z",
  "feature": "invoice-extraction",
  "model": "example-model-2026-10",
  "prompt_version": "v14",
  "input_tokens": 2410,
  "output_tokens": 1024,
  "stop_reason": "max_tokens",
  "parse_ok": false,
  "validation_error": "missing field: total",
  "latency_ms": 5120
}

The max_tokens stop reason with a failed parse is a classic pair. The response was cut off before the JSON closed. Anthropic's stop reason reference lists the values for its API, including end_turn, max_tokens, refusal and tool_use. OpenTelemetry's GenAI span conventions define a finish reasons attribute for generation spans, though they are still at Development status.

How to detect it

  1. Tag every call with the model ID and a prompt version you control. If you cannot tell which prompt produced an output, you cannot attribute a change.
  2. Compute daily or hourly rates per feature, model and prompt version: parse failure rate, refusal rate, truncation rate (stop reason max_tokens or similar), median and p95 output tokens and tool call rate.
  3. Compare each window with a reference window, such as the trailing 7 days at the same hour.
WITH cur AS (
  SELECT feature, model, prompt_version,
         count(*) AS n,
         avg((NOT parse_ok)::int) AS parse_fail,
         avg((stop_reason = 'refusal')::int) AS refusal,
         percentile_cont(0.5) WITHIN GROUP (ORDER BY output_tokens) AS p50_out
  FROM llm_calls
  WHERE ts > now() - interval '1 hour'
  GROUP BY 1, 2, 3
), ref AS (
  SELECT feature, model, prompt_version,
         avg((NOT parse_ok)::int) AS parse_fail,
         avg((stop_reason = 'refusal')::int) AS refusal,
         percentile_cont(0.5) WITHIN GROUP (ORDER BY output_tokens) AS p50_out
  FROM llm_calls
  WHERE ts BETWEEN now() - interval '8 days' AND now() - interval '1 day'
  GROUP BY 1, 2, 3
)
SELECT cur.*, ref.parse_fail AS ref_parse_fail, ref.p50_out AS ref_p50_out
FROM cur LEFT JOIN ref USING (feature, model, prompt_version)
WHERE cur.n >= 200
  AND (ref.feature IS NULL
       OR cur.parse_fail > greatest(0.01, 3 * ref.parse_fail)
       OR cur.p50_out > 1.4 * ref.p50_out);

The ref.feature IS NULL condition also surfaces a brand-new model or prompt version with no reference at all, which is worth a look.

For quality that logs cannot see, add a small fixed evaluation set. Replay the same 50 to 200 inputs on a schedule, score the outputs with checks you trust and chart the score by model and prompt version. A moving score with unchanged inputs is drift by definition.

Thresholds and pitfalls

Use a relative rule with an absolute floor. Three times the reference rate is meaningless when the reference is 0.01%, so also require the rate to exceed something like 1%. Require a minimum count per window, since 200 calls in an hour gives a far steadier rate than 20.

Know the usual causes before you alert, because most "drift" is self-inflicted:

  • A prompt edit or a new tool definition. Compare by prompt version before blaming the provider.
  • A moved alias. Anthropic documents that aliases for earlier models point to the latest dated snapshot, while a model ID is a pinned version. Pin an exact ID where your provider offers one.
  • Input mix. A new customer with long documents raises token use and truncation without any model change. Segment by customer or input size.
  • Retrieval changes. A re-indexed corpus changes what the model sees.
  • Weekly cycles. Business-hours traffic differs from weekend traffic, so match the hour and weekday in the reference window.

What to do when it fires

Find which dimension moved: model, prompt version, feature or customer. If it is a model or alias, pin the previous ID and replay your evaluation set on both. If it is a prompt, roll it back. For truncation, raise the output limit or shorten the schema. For parse failures, tighten validation and add a retry with the error message fed back.

Each model ID also has its own deprecation and retirement schedule, so put the retirement date for every ID you use on a calendar. A forced migration is drift with a deadline.

Related guides: LLM spend spikes covers the cost side of token growth and rogue AI agents covers tool misuse. The Langfuse and LangSmith pages describe how Sazabi connects to those tools, and the OpenRouter page covers sending generation traces over OpenTelemetry.

With Sazabi

Sazabi detects this today with no setup: the Sazabi agent automatically sets up sensors for your project and maintains them over time. LLM Behavior Drift Alerts: An Issue When Model Output Quietly Changes covers what it catches.

Frequently Asked Questions

What is LLM drift?

It is a change in how a model-backed feature behaves without a code change you intended. Typical signs are longer or shorter outputs, more refusals, more malformed structured output and different token use.

Does a pinned model version stop drift?

It removes one cause. Anthropic documents that a model ID refers to a pinned version, while aliases for earlier models can move to a new snapshot. Prompts, retrieved data and user input can still change behavior.

What should I log for every LLM call?

Log the model ID, prompt version, input and output token counts, stop or finish reason, latency, whether output parsing succeeded and any validation error. Avoid storing full prompts unless you have a retention plan.

How do I set a threshold for parse failures?

Use a relative rule and an absolute floor together, such as three times the trailing rate and above 1%. Add a minimum count per window so a few calls cannot trigger it.

Sources

  1. Model IDs identify pinned versions; aliases for earlier models point to the latest dated snapshot; each ID has its own deprecation and retirement schedule (accessed Nov 8, 2026)
  2. Stop reason values including end_turn, max_tokens, refusal, tool_use and model_context_window_exceeded; how truncation shows up (accessed Nov 8, 2026)
  3. OpenTelemetry GenAI span conventions record finish reasons on a generation; the conventions are at Development status (accessed Nov 8, 2026)