Reliability

Handling Ambiguous or Bad Model Output

⏱ 16 min

Course 5’s error-handling lesson covers what happens when a call fails outright, a WP_Error, a network timeout, a missing key. This lesson covers a different problem: the call succeeds, the model returns something, and that something doesn’t hold up against what your feature actually needs, a missing field, a category that isn’t in your allowed list, a title that’s empty. That’s not a network failure, it’s bad content wearing the shape of a successful response, and it needs its own handling: validate, then retry with a clarification if it’s worth retrying, then fall back safely if it still isn’t right.

What you'll learn in this lesson
Validating structured output
Checking a decoded response against your schema in PHP, not trusting the request alone.
Retry-with-clarification
Re-prompting with a specific note about what was wrong, not just repeating the same call.
Capping retries
Why an unbounded retry loop is a cost and reliability risk of its own.
Safe fallback behavior
What to do, and never do, once retries are exhausted.
Prerequisites

Lesson 3’s bulk-tagging JSON example and Course 5’s is_wp_error() error-handling pattern. This lesson builds directly on the schema from Lesson 3.

Step 1: A schema request lowers bad output, it doesn’t remove it

Lesson 3’s as_json_response( $schema ) call constrains what the model is asked to return. It doesn’t guarantee every response satisfies every constraint on every call, providers vary in how strictly they enforce a schema, and a model can still return valid JSON that’s missing a required field or contains a value outside your intended set. The validation step is where you check for that, in your own PHP, after the decode, every time.

// File: my-plugin/class-bulk-tagger.php
function validate_tag_response( array $data, array $allowed_categories ): bool {
	if ( empty( $data['title'] ) || ! is_string( $data['title'] ) ) {
		return false;
	}
	if ( empty( $data['tags'] ) || ! is_array( $data['tags'] ) ) {
		return false;
	}
	if (
		empty( $data['category'] )
		|| ! in_array( $data['category'], $allowed_categories, true )
	) {
		return false;
	}
	return true;
}

Step 2: Retrying with a clarification, not a repeat

Calling the exact same prompt again after a validation failure rarely fixes anything, you’re re-rolling the same dice, not correcting a mistake. A retry that actually helps tells the model specifically what was wrong with its last answer:

// File: my-plugin/class-bulk-tagger.php
function retry_with_clarification(
	string $post_content,
	array $schema,
	array $data,
	array $allowed_categories
) {
	$problem = empty( $data['category'] ) || ! in_array( $data['category'], $allowed_categories, true )
		? 'Your last response was missing a valid category. category must be exactly ' .
		  'one of: ' . implode( ', ', $allowed_categories ) . '.'
		: 'Your last response was missing a required field.';

	$raw = wp_ai_client_prompt()
		->with_text( "Post content:\n" . $post_content )
		->using_system_instruction(
			'Suggest a concise title, up to five relevant tags, and the single ' .
			'best-fit category. ' . $problem
		)
		->as_json_response( $schema )
		->using_temperature( 0.2 )
		->generate_text();

	return json_decode( $raw, true );
}
The validate, retry, fallback flow
1
Validate
Decode the response and check every field your feature depends on, not just that decoding succeeded.
2
Retry with clarification
If it failed, re-prompt once or twice with a specific note about exactly what was wrong.
3
Fall back safely
If it still fails, do not guess, log it, flag it for review, and leave existing data untouched.

Step 3: Capping the retry count

A retry loop with no limit is its own reliability risk, every retry is a full additional API call, with its own cost and its own chance of a different kind of failure. Two attempts total, the original call plus one retry, is a reasonable default for most WordPress content features, more than that rarely improves the odds enough to justify the added cost and latency:

// File: my-plugin/class-bulk-tagger.php
function tag_post_with_retry( int $post_id ): ?array {
	$post_content = wp_strip_all_tags( get_post_field( 'post_content', $post_id ) );
	$categories   = wp_list_pluck( get_categories( array( 'hide_empty' => false ) ), 'name' );
	$schema       = build_tag_schema( $categories ); // Same schema shape as Lesson 3.

	$raw  = wp_ai_client_prompt()
		->with_text( "Post content:\n" . $post_content )
		->using_system_instruction( 'Suggest a concise title, up to five relevant tags, and the single best-fit category.' )
		->as_json_response( $schema )
		->using_temperature( 0.2 )
		->generate_text();

	$data = json_decode( $raw, true );

	if ( ! is_array( $data ) || ! validate_tag_response( $data, $categories ) ) {
		$data = retry_with_clarification( $post_content, $schema, (array) $data, $categories );
	}

	if ( ! is_array( $data ) || ! validate_tag_response( $data, $categories ) ) {
		return null; // Step 4 handles this.
	}

	return $data;
}

Step 4: Falling back safely once retries are exhausted

What happens when tag_post_with_retry() returns null matters as much as the retry logic itself. The safe default is to change nothing and make the failure visible, not to guess at a value and write it anyway:

// File: my-plugin/class-bulk-tagger.php
$result = tag_post_with_retry( $post_id );

if ( null === $result ) {
	update_post_meta( $post_id, '_ai_tagging_needs_review', true );
	error_log( "AI bulk-tagging failed validation twice for post {$post_id}." );
	return; // Leave the post's existing title and terms untouched.
}

wp_update_post( array( 'ID' => $post_id, 'post_title' => sanitize_text_field( $result['title'] ) ) );
wp_set_post_terms( $post_id, $result['tags'], 'post_tag' );
wp_set_post_terms( $post_id, array( $result['category'] ), 'category' );

Flagging the post with _ai_tagging_needs_review gives a human editor a real signal to follow up on, rather than the post silently keeping stale data with no indication anything went wrong.

Retrying with the identical prompt rarely helps

If your retry logic just calls the exact same builder chain again with no added clarification, you’re relying on randomness to fix a problem that’s often systematic, a schema issue, a category that no longer exists, a post with almost no content to work from. Always add something specific to the retry prompt about what failed, or skip the retry and go straight to a safe fallback.

Test it: force a validation failure

wp-env run cli wp eval '
// Simulate a response missing category to exercise the fallback path.
$fake_response = array( "title" => "Example Post", "tags" => array( "example" ) );
$categories    = array( "Tutorials", "News", "Reviews" );

var_dump( validate_tag_response( $fake_response, $categories ) );
'

You should see bool(false), confirming validation correctly rejects a response missing category before your code ever tries to write it to the database.

Recap

A schema request reduces bad structured output, it doesn’t eliminate it, so validate every decoded response against the fields your feature actually needs before trusting it. When validation fails, one retry with a specific clarification about what was wrong is far more useful than repeating the identical call, and a hard cap on retry attempts keeps a bad response from turning into an unbounded cost. When retries are exhausted, fall back to leaving existing data untouched and flagging the item for review, never to guessing.

Resources & further reading

← Guardrails: Constraining Model Behavior Storing Reusable Prompt Templates as WordPress Options →