Constrained Decoding › Regex Constraints

Michael BrenndoerferUpdated July 31, 20255 min read
Language AI HandbookAll chapters
Part 28 of 67GPT Architecture
Chapter 206Constrained Decoding › Regex Constraints
Reading section 6 of 13 · Regex Constraints · 5 minSection 6 of 13 · 5 min

Book contents

Section 6 of 13 · 5 min read

Constrained Decoding › Regex ConstraintsOne section · Sections in this chapter6 / 13 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Regular expressions provide a flexible way to constrain generation when you need pattern matching without full grammar complexity. Common use cases include:

  • Phone numbers: \d{3}-\d{3}-\d{4}
  • Email addresses: [a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}
  • Dates: \d{4}-\d{2}-\d{2}
  • Custom identifiers: [A-Z]{2}\d{6}

Regex to DFA Conversion

Regular expressions can be converted to deterministic finite automata (DFAs), which then guide generation exactly like grammar-based FSMs.

In[17]:
Code
import re


class RegexConstraint:
    """
    Constrain generation to match a regular expression.

    Uses a simplified simulation approach for demonstration.
    Real implementations compile to DFA for efficiency.
    """

    def __init__(self, pattern: str):
        self.pattern = pattern
        self.regex = re.compile(f"^{pattern}$")
        self.partial_regex = re.compile(f"^{pattern[:]}")

    def get_valid_tokens(
        self, current: str, vocabulary: list[str]
    ) -> list[str]:
        """
        Find tokens that could extend current string toward a match.

        Args:
            current: String generated so far
            vocabulary: List of possible next tokens

        Returns:
            List of valid next tokens
        """
        valid = []
        for token in vocabulary:
            candidate = current + token
            # Check if candidate could be prefix of a valid match
            if self._could_match(candidate):
                valid.append(token)
        return valid

    def _could_match(self, text: str) -> bool:
        """Check if text could be extended to match the pattern."""
        # Simplified check: see if any extension could work
        # Real implementations use DFA state tracking
        for extension in ["", "a", "0", "@", ".", "-"]:
            test = text + extension * 20  # Pad with potential matches
            if self.regex.match(test):
                return True
        # Also check if text itself is a valid prefix
        # by trying the pattern with .* appended conceptually
        try:
            partial = re.compile(f"^{re.escape(text)}")
            # If we can find any completion that matches, text is valid
            return True  # Simplified - assume prefixes are valid
        except re.error:
            return False
Out[18]:
Console
Regex constraint: \d{3}-\d{3}-\d{4}

Valid next characters at each step:

(empty)              → valid: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-']
"5"                  → valid: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-']
"55"                 → valid: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-']
"555"                → valid: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-']
"555-"               → valid: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-']
"555-12"             → valid: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-']
"555-123-"           → valid: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-']
"555-123-456"        → valid: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-']

The regex constraint ensures that partial outputs always remain on a path toward completing the pattern. Once you've generated "555-", only digits are valid for the next three positions, followed by a mandatory hyphen, then four more digits.

Out[19]:
Visualization
Bar chart of 12 positions in a phone number showing valid character counts: 10 (blue) for digit positions and 1 (orange) for hyphen positions at positions 4 and 8, with a pattern annotation.
Regex constraint for phone number generation. At each position, the constraint specifies exactly which characters are valid based on the pattern.

Combining Regex with Semantic Guidance

A powerful technique combines regex constraints with the model's semantic knowledge. The regex ensures format correctness while the model contributes meaningful content.

In[20]:
Code
import numpy as np


def generate_with_regex_constraint(
    model_probs: dict[str, float],
    current: str,
    regex_pattern: str,
    vocabulary: list[str],
) -> str:
    """
    Sample next token using both model preferences and regex constraints.

    Args:
        model_probs: Model's probability distribution over tokens
        current: Generated string so far
        regex_pattern: Pattern the final output must match
        vocabulary: List of possible tokens

    Returns:
        Selected token
    """
    constraint = RegexConstraint(regex_pattern)
    valid_tokens = constraint.get_valid_tokens(current, vocabulary)

    if not valid_tokens:
        raise ValueError(f"No valid continuation from '{current}'")

    # Filter model probs to valid tokens only
    filtered_probs = {t: model_probs.get(t, 0) for t in valid_tokens}
    total = sum(filtered_probs.values())

    if total == 0:
        # Uniform over valid tokens if model gives no probability
        return np.random.choice(valid_tokens)

    # Normalize and sample
    normalized = {t: p / total for t, p in filtered_probs.items()}
    tokens, probs = zip(*normalized.items())
    return np.random.choice(tokens, p=probs)
Out[21]:
Console
Model digit preferences (from training data):
  '5': 15%
  '1': 12%
  '7': 11%
  '2': 10%
  '6': 10%

Generating phone number with constraint + model semantics:

Generated: 063582472021

Note: Digits reflect model preferences, hyphens are structurally required.