dual-use constraints

Dual-Use Constraints: Eliminating Drift in LLM Systems

When you are building real systems that rely on LLM output, you eventually face a maintenance problem: the rules you put in the prompt drift out of sync with the rules you use to validate the output. This post documents the dual-use constraints pattern, its technique, and how it works to reduce drift by using the same constraints for both purposes.


The Problem

Most LLM systems have two separate places where rules are defined:

1. The Prompt 

You are a helpful NPC in a fantasy game. You must NOT reveal any secrets about the main quest. You must NOT break character. You must NOT use modern slang.

2. The Validator 

if (output.Contains("secret")) 
return ValidationResult.Fail("Revealed secrets");

if (output.Contains("quest"))
return ValidationResult.Fail("Mentioned quest");

if (output.Contains("dude") || output.Contains("bro"))
return ValidationResult.Fail("Modern slang");

Notice anything? The prompt says “secrets about the main quest” but the validator checks for “secret” and “quest” separately. The prompt says “modern slang” but the validator only checks for “dude” or “bro.”

These have already begun to drift.

In a production codebase, the prompt lives in one file, the validator lives in another, and they’re maintained by different people at different times. Six months later, someone adds a new rule to the prompt but forgets something and doesn’t update the validator. Or they update the validator’s patterns but the prompt still tells the LLM the old ruleset.

The result: your LLM produces output that passes validation but violates the spirit of your rules. Or worse, output that follows the prompt but fails validation for reasons the LLM was never told about.


The Dual-Use Constraints Pattern

The solution is straightforward: use the same object for both purposes.

public class Constraint { 
  public string Id { get; set; }
public string Description { get; set; }
public string? PromptInjection { get; set; }
public List<string> ValidationPatterns { get; set; }
public ConstraintType Type { get; set; } // Requirement or Prohibition
}

A single Constraint object contains: – What to tell the LLM (PromptInjection) – What to check in the output (ValidationPatterns)

When you modify the constraint, both the prompt and the validation change together. There’s no opportunity to drift because there’s only one source of truth.


Implementation

Here’s how constraints are created:

var noSecrets = Constraint.Prohibition( 
id: "no_secrets",
description: "Cannot reveal secrets",
promptInjection: "You must NOT reveal any secrets about the main quest.",
patterns: new[] { "secret", "classified", "hidden truth" }
);

var stayInCharacter = Constraint.Prohibition(
id: "no_modern_slang",
description: "Must stay in character",
promptInjection: "You must NOT use modern slang or break character.",
patterns: new[] { "dude", "bro", "like totally", "no cap" }
);

These constraints are collected into a ConstraintSet:

ConstraintSet constraints = new();
constraints.Add(noSecrets);
constraints.Add(stayInCharacter);

The same ConstraintSet is both used for prompt generation and validation:

// For prompt injection 
var promptSection = constraints.ToPromptInjection();
// Output:
// "You must NOT:
// - You must NOT reveal any secrets about the main quest.
// - You must NOT use modern slang or break character."


// For validation
// Returns list of violated constraints with matched patterns
var violations = validator.CheckProhibitions(llmOutput, constraints);

Why This Matters

Consider what happens when you need to add a new rule: “NPCs cannot mention other players by name.”

Without dual-use constraints:

  1. Update the prompt template (file A)
  2. Update the validator (file B)
  3. Update the unit tests for both
  4. Hope you didn’t miss anything

With dual-use constraints: 

  1. Add one constraint object
  2. Done
constraints.Add(Constraint.Prohibition( 
id: "no_player_names",
description: "Cannot mention other players",
promptInjection: "You must NOT mention other players by name.",
patterns: playerNames.ToArray() // Dynamic list from game state )
);

The prompt automatically includes the new rule. The validator automatically checks for it. Tests that use the constraint set automatically cover both paths.


Dynamic Constraints

This pattern becomes more powerful when constraints are generated at runtime.

public ConstraintSet BuildConstraints(NpcContext context) 
{ var constraints = new ConstraintSet(); // Static constraints constraints.Add(_noModernSlang); constraints.Add(_noMetaText); // Dynamic: NPC-specific knowledge boundaries foreach (var secret in context.ForbiddenKnowledge) { constraints.Add(Constraint.Prohibition( id: $"no_knowledge_{secret.Id}", description: $"Cannot reveal {secret.Name}", promptInjection: $"You do not know about {secret.Name}.", patterns: secret.KeyPhrases.ToArray() )); } // Dynamic: Current conversation context if (context.PlayerIsDisguised) { constraints.Add(Constraint.Prohibition( id: "no_recognize_player", description: "Cannot recognize disguised player", promptInjection: "You do not recognize this person.", patterns: new[] { context.PlayerName, "recognize", "it's you" } )); } return constraints; }

The prompt and validation stay synchronized regardless of how complex the constraint logic becomes. The NPC’s forbidden knowledge list can change, players can put on disguises, and the system adapts without any risk of prompt invalidation or mismatch.


Validation Results

When validation fails, you get actionable information:

public class ValidationViolation {  
public Constraint Constraint { get; set; }
public string MatchedPattern { get; set; }
public string Context { get; set; } // Surrounding text
}

This allows us to:

  • Retry with escalation: Add the violated constraint to a “stricter” prompt section.
  • Log information: Track which constraints fail most often.
  • Debug state: See exactly which pattern matched and where.

Trade-offs

Prompt injection text vs validation patterns: Sometimes what you tell the LLM differs from what you can reliably detect. “Don’t be rude” is a reasonable prompt instruction but a difficult validation pattern. For these cases, the PromptInjection and ValidationPatterns can differ within the same constraint. The point is they’re still co-located and reviewed together.

Pattern limitations: Simple string matching catches obvious violations but misses paraphrasing. The LLM might say “the hidden information” instead of “secret.” This is a validation problem in general, not specific to this pattern. You can extend ValidationPatterns to support regex or semantic similarity as needed.

Constraint explosion: Dynamic constraint generation can produce large constraint sets. The prompt injection method should handle this gracefully (truncation, summarization, or priority-based selection).


Summary

Dual-use constraints eliminate prompt/validation drift by enforcing a single source of truth. One object defines both what the LLM is told and what’s checked in the output.

The implementation is straightforward:

– PromptInjection: What goes into the prompt

– ValidationPatterns: What’s checked in the output

– Both fields live on the same object

When you modify a rule, both the prompt and validation change together. There’s no synchronization to maintain because there’s nothing to synchronize.

The pattern is implemented in LlamaBrain‘s Constraint.cs and ConstraintSet.cs. Results are reproducible.

Leave a Reply

Your email address will not be published. Required fields are marked *

 

This site uses Akismet to reduce spam. Learn how your comment data is processed.