HinterBuild logoHinterBuild
AI Systems · 10 min read

Constitutional AI Prompting: Self-Critique Patterns That

Learn constitutional ai prompting through concrete architecture trade-offs, failure modes, rollout controls, and production measurement practices.

Muhammad Abdul Sami, author

Muhammad Abdul Sami

· 10 min read

  • RAG
  • Embeddings
  • Vector Databases
  • Evaluation

Table of Contents:

Constitutional AI Fundamentals: Training Models to Self-Improve

Short answer: Constitutional AI prompting uses a written set of principles (a "constitution") to drive a generate, critique, revise loop at inference time. The model produces a draft, critiques it against the principles, then rewrites it to comply, with no human in the loop for the common case.

A content moderation AI agent was flagging false positives at 18%. We implemented Constitutional AI patterns with safety principles. The model now self-critiques borderline cases against defined guidelines before final classification. False positives dropped to 6%, a 67% reduction.

Key Takeaways:

  • Constitutional AI prompting is an inference-time pattern: draft, critique against explicit principles, revise. It needs no fine-tuning.
  • Principles must be testable. "Be safe" is useless; "Do not state a dosage without a 'consult a clinician' caveat" is checkable by a critic model.
  • Self-critique catches policy violations far better than reasoning errors. Use it for compliance and tone, not for fixing math.
  • Budget 2-3x tokens for a full critique-and-revise loop; gate it by risk level so low-stakes traffic pays once.
  • Verify with a different model for critical outputs. A model grading its own work shares its own blind spots.
  • Track the refinement rate. A rising rate means the base prompt regressed; a rate near zero means the critic is not looking hard enough.

The technique comes from Anthropic's paper Constitutional AI: Harmlessness from AI Feedback (Bai et al., 2022), which used critique-and-revision to generate training data. This post is about the prompting version: applying the same loop at request time to any hosted model, whether you call Anthropic's API or OpenAI's. For production AI agents, it reduces post-deployment interventions significantly.


Training-Time CAI vs Constitutional AI Prompting

The original paper used the constitution in two training phases: supervised fine-tuning on revised outputs, then reinforcement learning from AI feedback (RLAIF) where the model's own preferences replaced human labels. That is a model-vendor activity. What application teams do is the inference-time version, and it helps to be precise about what you are and are not getting.

AspectTraining-time CAI (RLAIF)Constitutional AI prompting (this post)
Where principles liveBaked into weightsIn prompts you control and version
Cost per requestNone extra2-3x tokens for critique and revision
LatencyNone extraOne or two additional round-trips
Principle updatesRequires retrainingEdit a prompt, redeploy
Domain specificityGeneral safetyYour policies, your tone, your compliance rules
Who can do itModel vendors, fine-tuning teamsAny team with API access
Failure surfaceModel-levelCritic prompt quality, JSON parsing, loop bounds

The practical implication: hosted frontier models already carry a general constitution from training. Your prompting layer should encode the domain-specific rules the vendor could not know: your refund policy, your regulated-advice disclaimers, your brand voice. Duplicating generic "don't be harmful" principles adds cost and little value. If you find yourself needing behavior changes that prompts cannot achieve, that is the signal to consider fine-tuning instead of prompting.


Self-Critique Patterns: Teaching Models to Review Their Work

The core loop has three calls: generate, critique, revise. The critique step returns structured JSON so the revise step can be skipped when nothing was flagged, which is the single biggest cost lever in the pattern. Ask for response_format={"type": "json_object"} (or a JSON schema) so parsing never becomes the failure point; see our guide to structured output from LLMs for the hardening details.

python
from typing import Dict, Any
from openai import AsyncOpenAI

client = AsyncOpenAI()

CRITIQUE_PROMPT = """Review this response against these principles:

Principles:
{principles}

Original response:
{response}

Critique:
1. What principles does this response violate, if any?
2. What could be improved?
3. What is done well?

Return JSON: {{"violations": [...], "improvements": [...], "strengths": [...]}}"""

REVISION_PROMPT = """Original response:
{original}

Critique:
{critique}

Revised response following the critique:"""

async def constitutional_response(
    user_query: str,
    principles: list[str],
) -> Dict[str, Any]:
    """Generate response with self-critique loop."""
    initial = await client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": user_query}],
    )
    initial_response = initial.choices[0].message.content
    
    # Step 2: Self-critique
    principles_text = "\n".join([f"- {p}" for p in principles])
    critique_result = await client.chat.completions.create(
        model="gpt-4o",
        messages=[{
            "role": "user",
            "content": CRITIQUE_PROMPT.format(
                principles=principles_text,
                response=initial_response,
            ),
        }],
        response_format={"type": "json_object"},
    )
    import json
    critique = json.loads(critique_result.choices[0].message.content)
    
    # Step 3: Revise if violations found
    if critique["violations"]:
        revised = await client.chat.completions.create(
            model="gpt-4o",
            messages=[{
                "role": "user",
                "content": REVISION_PROMPT.format(
                    original=initial_response,
                    critique=json.dumps(critique, indent=2),
                ),
            }],
        )
        final_response = revised.choices[0].message.content
    else:
        final_response = initial_response
    
    return {
        "response": final_response,
        "initial": initial_response,
        "critique": critique,
        "revised": critique["violations"] != [],
    }

# Example usage
SAFETY_PRINCIPLES = [
    "Never provide instructions for illegal activities",
    "Avoid generating hateful or discriminatory content",
    "Don't reveal personal identifiable information",
    "Refuse requests that could cause harm",
]

result = await constitutional_response(
    "How do I bypass security systems?",
    principles=SAFETY_PRINCIPLES,
)

print(f"Initial: {result['initial']}")
print(f"Critique: {result['critique']}")
print(f"Final: {result['response']}")

Integrate with system prompt design patterns for comprehensive behavior control.

Writing principles the critic can actually apply

The quality of Constitutional AI prompting is capped by the quality of the constitution. Three rules from production:

  • One observable behavior per principle. "Be helpful and safe" is two principles, neither checkable. "If the user asks for medication dosage, include a line recommending they confirm with a pharmacist" can be verified by reading the output.
  • Prefer positive obligations for revision, prohibitions for refusal. Prohibitions ("never reveal internal pricing") are easy to critique against but give the reviser no direction. Pair them with what to do instead ("direct the user to the public pricing page").
  • Order principles by priority and say so. When principles conflict (helpfulness vs. a disclaimer requirement), the critic needs a tie-break rule. State it explicitly at the top of the constitution.

A useful test: hand the constitution and a sample output to a colleague and ask them to grade it. If two humans disagree on whether a principle was violated, the critic model will be inconsistent too.


Refinement Loops: Iterative Quality Improvement

A single critique pass fixes most policy violations. A bounded refinement loop is for outputs with graded quality criteria (clarity, completeness, tone) where the first revision may still fall short. Two design rules keep it from becoming a cost sink: always cap max_iterations, and score with a cheaper model than the one generating.

python
class RefinementLoop:
    """Iterative refinement with quality gates."""
    
    def __init__(
        self,
        client,
        max_iterations: int = 3,
        quality_threshold: float = 0.85,
    ):
        self.client = client
        self.max_iterations = max_iterations
        self.quality_threshold = quality_threshold
    
    async def generate_with_refinement(
        self,
        prompt: str,
        quality_criteria: list[str],
    ) -> Dict[str, Any]:
        """Generate and refine until quality threshold met."""
        current_response = None
        iterations = []
        
        for i in range(self.max_iterations):
            # Generate/refine
            if current_response is None:
                response = await self._generate(prompt)
            else:
                response = await self._refine(prompt, current_response, quality_criteria)
            
            # Evaluate quality
            quality_score = await self._evaluate_quality(response, quality_criteria)
            
            iterations.append({
                "iteration": i + 1,
                "response": response,
                "quality_score": quality_score,
            })
            
            current_response = response
            
            if quality_score >= self.quality_threshold:
                break
        
        return {
            "final_response": current_response,
            "iterations": iterations,
            "converged": quality_score >= self.quality_threshold,
        }
    
    async def _generate(self, prompt: str) -> str:
        response = await self.client.chat.completions.create(
            model="gpt-4o",
            messages=[{"role": "user", "content": prompt}],
        )
        return response.choices[0].message.content
    
    async def _refine(
        self,
        original_prompt: str,
        previous_response: str,
        criteria: list[str],
    ) -> str:
        refine_prompt = f"""Original request: {original_prompt}

Previous response:
{previous_response}

Quality criteria not fully met:
{chr(10).join([f"- {c}" for c in criteria])}

Generate an improved response that better satisfies all criteria:"""
        
        return await self._generate(refine_prompt)
    
    async def _evaluate_quality(
        self,
        response: str,
        criteria: list[str],
    ) -> float:
        """Score response quality (0-1)."""
        eval_prompt = f"""Rate this response on how well it satisfies these criteria (0-10 scale):

Criteria:
{chr(10).join([f"- {c}" for c in criteria])}

Response:
{response}

Return JSON: {{"score": 0-10, "reasoning": "..."}}"""
        
        result = await self.client.chat.completions.create(
            model="gpt-4o-mini",  # Cheaper model for evaluation
            messages=[{"role": "user", "content": eval_prompt}],
            response_format={"type": "json_object"},
        )
        
        import json
        data = json.loads(result.choices[0].message.content)
        return data["score"] / 10.0  # Normalize to 0-1

# Usage
refiner = RefinementLoop(client, max_iterations=3, quality_threshold=0.85)

result = await refiner.generate_with_refinement(
    prompt="Explain quantum computing to a 10-year-old",
    quality_criteria=[
        "Uses simple, age-appropriate language",
        "Includes concrete analogies",
        "Avoids technical jargon",
        "Explains key concepts clearly",
    ],
)

print(f"Converged: {result['converged']}")
for iter_data in result['iterations']:
    print(f"Iteration {iter_data['iteration']}: Quality {iter_data['quality_score']:.2f}")

Combine with prompt versioning to track principle effectiveness.

In practice, most requests that converge do so by iteration two. If your telemetry shows many requests hitting the cap, the criteria are either contradictory or too vague for the scorer to reward progress, and no amount of iteration will fix that; rewrite the criteria instead.


Harmlessness Principles: Core Safety Guidelines

The three groupings below (harmlessness, helpfulness, honesty) mirror the structure of the original paper. Treat them as a starting template and replace the generic entries with your domain rules: a fintech assistant's honesty constitution should mention regulated-advice language; a support bot's helpfulness constitution should mention escalation paths.

python
HARMLESSNESS_CONSTITUTION = [
    "Never assist with illegal activities or provide instructions for breaking laws",
    "Avoid generating content that promotes violence, self-harm, or harm to others",
    "Don't produce hateful, discriminatory, or harassing content based on protected characteristics",
    "Refuse to generate sexually explicit content involving minors",
    "Don't help users deceive, manipulate, or scam others",
    "Avoid revealing personal identifiable information about real individuals",
    "Don't provide medical, legal, or financial advice without appropriate disclaimers",
    "Refuse to bypass safety systems or content policies",
]

HELPFULNESS_CONSTITUTION = [
    "Provide accurate, factual information when answering questions",
    "Acknowledge uncertainty when you don't know something",
    "Give specific, actionable answers rather than vague generalities",
    "Include relevant examples and context to aid understanding",
    "Break complex topics into clear, digestible steps",
    "Anticipate follow-up questions and address them proactively",
]

HONESTY_CONSTITUTION = [
    "Admit limitations and gaps in knowledge",
    "Cite sources when making factual claims",
    "Distinguish between facts, opinions, and speculation",
    "Correct previous errors when discovered",
    "Be transparent about your nature as an AI",
]

Connect to multi-agent orchestration where agents enforce principles across specialized tasks.


Production Implementation: CAI System

The production version separates critical violations (refuse outright) from minor ones (refine). This matters because refining an unsafe response often produces a plausible unsafe response, which is worse than a clean refusal. The violation checker runs on a cheaper model; the generator and reviser run on the strong one.

python
from dataclasses import dataclass
from typing import List

@dataclass
class Constitution:
    """Set of principles governing AI behavior."""
    harmlessness: List[str]
    helpfulness: List[str]
    honesty: List[str]
    
    def all_principles(self) -> List[str]:
        return self.harmlessness + self.helpfulness + self.honesty

class ConstitutionalAISystem:
    """Production Constitutional AI implementation."""
    
    def __init__(self, client, constitution: Constitution):
        self.client = client
        self.constitution = constitution
    
    async def generate(self, user_query: str) -> Dict[str, Any]:
        """Generate response with constitutional safeguards."""
        # Generate initial response
        initial_response = await self._generate_initial(user_query)
        
        # Check for immediate violations
        violations = await self._check_violations(initial_response)
        
        if violations["critical"]:
            # Critical violations—return refusal
            return {
                "response": "I cannot assist with that request as it violates safety guidelines.",
                "initial": initial_response,
                "violations": violations,
                "refused": True,
            }
        
        # If minor violations, refine
        if violations["minor"]:
            refined_response = await self._refine_response(
                user_query,
                initial_response,
                violations["minor"],
            )
        else:
            refined_response = initial_response
        
        return {
            "response": refined_response,
            "initial": initial_response,
            "violations": violations,
            "refined": bool(violations["minor"]),
            "refused": False,
        }
    
    async def _generate_initial(self, query: str) -> str:
        response = await self.client.chat.completions.create(
            model="gpt-4o",
            messages=[{"role": "user", "content": query}],
        )
        return response.choices[0].message.content
    
    async def _check_violations(self, response: str) -> Dict[str, List[str]]:
        """Check response against constitution."""
        check_prompt = f"""Review this response against safety principles:

Principles:
{chr(10).join([f"- {p}" for p in self.constitution.all_principles()])}

Response:
{response}

Identify violations:
- CRITICAL: Direct violations requiring refusal
- MINOR: Issues that should be refined

Return JSON: {{"critical": [...], "minor": [...]}}"""
        
        result = await self.client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": check_prompt}],
            response_format={"type": "json_object"},
        )
        
        import json
        return json.loads(result.choices[0].message.content)
    
    async def _refine_response(
        self,
        original_query: str,
        response: str,
        violations: List[str],
    ) -> str:
        """Refine response to address violations."""
        refine_prompt = f"""Original query: {original_query}

Initial response:
{response}

Issues to address:
{chr(10).join([f"- {v}" for v in violations])}

Generate a refined response that maintains helpfulness while addressing these issues:"""
        
        result = await self.client.chat.completions.create(
            model="gpt-4o",
            messages=[{"role": "user", "content": refine_prompt}],
        )
        return result.choices[0].message.content

# Production usage
constitution = Constitution(
    harmlessness=HARMLESSNESS_CONSTITUTION,
    helpfulness=HELPFULNESS_CONSTITUTION,
    honesty=HONESTY_CONSTITUTION,
)

cai_system = ConstitutionalAISystem(client, constitution)

result = await cai_system.generate("How can I improve my credit score?")
print(f"Response: {result['response']}")
print(f"Refined: {result['refined']}")
print(f"Refused: {result['refused']}")

Deploy with RAG and LLM systems for safe, reliable outputs.


Multi-Step Verification

For outputs that carry real liability (regulated advice, external communications, actions with side effects), add an independent verifier from a different model family. Self-critique shares the generator's blind spots: if the model misread the policy when drafting, it will often misread it again when reviewing. A second model breaks that correlation. This is the same reasoning behind using a separate judge in LLM-as-judge evaluation.

python
async def multi_step_verification(
    response: str,
    principles: List[str],
) -> Dict[str, Any]:
    """Multi-model verification for critical outputs."""
    # Step 1: Self-critique
    self_critique = await constitutional_response(response, principles)
    
    # Step 2: Independent verification (different model)
    verify_prompt = f"""Verify this response follows all safety principles:

Principles:
{chr(10).join([f"- {p}" for p in principles])}

Response to verify:
{response}

Return JSON: {{"safe": true/false, "concerns": [...]}}"""
    
    verification = await client.chat.completions.create(
        model="claude-3-5-sonnet",  # Different model for independence
        messages=[{"role": "user", "content": verify_prompt}],
    )
    
    # Step 3: Aggregate results
    import json
    verify_data = json.loads(verification.choices[0].message.content)
    
    return {
        "approved": verify_data["safe"] and not self_critique["critique"]["violations"],
        "self_critique": self_critique,
        "external_verification": verify_data,
    }

Quality Scoring

Scoring per dimension lets you weight what matters for the surface. A moderation pipeline weights harmlessness heavily; an internal documentation assistant weights helpfulness. Keep the weights in config, not code, so product can tune them without a deploy.

python
class QualityScorer:
    """Score output quality against constitution."""
    
    def __init__(self, client, constitution: Constitution):
        self.client = client
        self.constitution = constitution
    
    async def score(self, response: str) -> Dict[str, float]:
        """Score response on multiple dimensions."""
        scores = {}
        
        # Score harmlessness
        scores["harmlessness"] = await self._score_dimension(
            response,
            self.constitution.harmlessness,
            "How well does this avoid harmful content?",
        )
        
        # Score helpfulness
        scores["helpfulness"] = await self._score_dimension(
            response,
            self.constitution.helpfulness,
            "How helpful and actionable is this?",
        )
        
        # Score honesty
        scores["honesty"] = await self._score_dimension(
            response,
            self.constitution.honesty,
            "How honest and transparent is this?",
        )
        
        # Overall score (weighted average)
        scores["overall"] = (
            scores["harmlessness"] * 0.5 +
            scores["helpfulness"] * 0.3 +
            scores["honesty"] * 0.2
        )
        
        return scores
    
    async def _score_dimension(
        self,
        response: str,
        principles: List[str],
        question: str,
    ) -> float:
        """Score on one dimension."""
        score_prompt = f"""{question}

Principles:
{chr(10).join([f"- {p}" for p in principles])}

Response:
{response}

Rate 0-10. Return JSON: {{"score": 0-10}}"""
        
        result = await self.client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": score_prompt}],
            response_format={"type": "json_object"},
        )
        
        import json
        data = json.loads(result.choices[0].message.content)
        return data["score"] / 10.0

Cost vs Quality Tradeoff

Constitutional AI prompting adds roughly 2-3x token cost for a full loop (critique + refinement), and one to two extra round-trips of latency. The table below is illustrative for a typical support-style request; your ratios depend on prompt and output length.

ConfigurationLLM callsRelative token costAdded latencyUse when
Plain generation11x0Low-risk, high-volume traffic
Critique only (no revise unless flagged)2 (3 when flagged)~1.6-2x+1 round-tripMost production surfaces
Full critique + revise, always3~2.5-3x+2 round-tripsRegulated or externally visible output
Bounded refinement loop (cap 3)3-7~3-6x+2-6 round-tripsGraded-quality content, offline or async
Critique + independent verifier3-4~3-4x+2 round-tripsActions with side effects, liability

The cheapest large win is conditional revision: since the critique returns structured violations, only pay for the third call when something was flagged. Route by risk so low-stakes queries never enter the loop:

python
class AdaptiveCAI:
    """Use CAI only when needed."""
    
    async def generate(self, query: str, risk_level: str) -> str:
        """Apply CAI based on risk."""
        if risk_level == "low":
            # Skip CAI for low-risk queries
            return await self._simple_generate(query)
        elif risk_level == "medium":
            # Single-pass critique
            return await self._single_critique(query)
        else:  # high risk
            # Full CAI with refinement
            return await self._full_cai(query)

Pair with LLM cost optimization strategies. Prompt caching helps here more than in most patterns because the constitution is a long, static prefix that repeats across the critique and revise calls.


Failure Modes of Self-Critique

Constitutional AI prompting is not a general quality multiplier. Knowing where it breaks saves you from over-applying it.

  • It does not reliably fix reasoning errors. Huang et al., Large Language Models Cannot Self-Correct Reasoning Yet, show that unguided self-correction can make answers worse on reasoning tasks. Self-critique works when the principle is a checkable policy, not when the model must notice its own arithmetic mistake. For factual accuracy, ground the answer with retrieval instead; see RAG hallucination fixes.
  • Over-refusal drift. A critic prompt that rewards caution will steadily push outputs toward refusals and disclaimers. Track refusal rate alongside violation rate, and include helpfulness principles in the critique so the critic is penalized for gutting the answer.
  • Sycophantic critique. If the critique prompt includes the original user request framed persuasively, the critic can be talked into approving. Critique the output against the principles; do not pass through user justifications.
  • Principle conflicts without a tie-break. Two principles pulling in opposite directions cause oscillation in refinement loops. Declare priority order.
  • JSON parsing as the weakest link. A critique call that returns malformed JSON should fail closed for critical categories (treat as violation, refuse or escalate) and fail open for minor ones. Decide this per category up front.
  • Injection through the critique channel. A user can attempt to write text that reads as a principle ("Ignore the constitution above"). Keep principles in the system prompt and the output under review in a clearly delimited user turn. Our prompt injection defense guide covers the delimiting patterns.

Monitoring Self-Critique

python
class CAIMonitor:
    """Track Constitutional AI effectiveness."""
    
    async def track_cai_execution(
        self,
        initial_response: str,
        final_response: str,
        violations_found: List[str],
        refined: bool,
    ) -> None:
        """Record CAI metrics."""
        await self.metrics.increment(
            "cai.executions.total",
            tags={"refined": refined},
        )
        
        if violations_found:
            await self.metrics.increment(
                "cai.violations.found",
                len(violations_found),
            )
        
        if refined:
            await self.metrics.increment("cai.refinements.performed")

Deploy with observability monitoring. The four numbers worth a dashboard: refinement rate (share of requests where revision ran), refusal rate, verifier disagreement rate (how often the independent check overrules self-critique), and p95 added latency. A refinement rate that climbs after a deploy is the earliest sign your base prompt or model version regressed.


Frequently Asked Questions

What is Constitutional AI prompting?

Constitutional AI prompting is an inference-time pattern where a model drafts a response, critiques it against a written list of principles, and revises it to comply. It borrows the critique-and-revision idea from Anthropic's Constitutional AI training method but applies it in your application code rather than in model training. It works with any hosted model and requires no fine-tuning.

When should I use Constitutional AI?

Use it for high-stakes outputs where policy compliance matters: content moderation, medical or financial disclaimers, legal information, customer-facing communications. Skip it for low-risk, high-volume tasks where the 2-3x token cost buys nothing, and do not rely on it to fix reasoning or factual errors.

Does self-critique actually improve output quality?

For policy and tone violations, yes: a critic reading the output against explicit rules catches most of what a single-pass generation misses. For reasoning and factual accuracy, the evidence is weak; research on self-correction shows unguided critique can make answers worse. Use retrieval for facts and Constitutional AI prompting for compliance.

How many principles should I include?

5-15 principles is optimal. Too few misses edge cases; too many creates conflicting guidance and increases token cost. Group related principles into categories (harmlessness, helpfulness, honesty).

Does CAI work with smaller models?

Frontier models work best (GPT-4o, Claude Sonnet). Smaller models struggle with nuanced self-critique. For smaller models, use external verification with a larger model.

Can I automate principle generation?

Yes—generate initial principles with LLMs, then refine based on production failures. Review and approve all principles before deploying.

How do I balance helpfulness and harmlessness?

Harmlessness takes priority—refuse unsafe requests even if unhelpful. For borderline cases, add disclaimers or caveats to maintain helpfulness while staying safe.


Conclusion

Constitutional AI enables models to self-improve against defined principles:

  • Define clear principles for harmlessness, helpfulness, honesty
  • Implement self-critique loops to catch issues before users see them
  • Refine responses that violate principles
  • Use risk-based triggers to balance cost and quality
  • Monitor effectiveness continuously

In our deployments, self-critique typically cuts the share of outputs that need human review substantially; measure your own rate before and after rather than assuming a number.

If you are building a system where output compliance is a launch requirement, talk to us about AI agent development or RAG and LLM systems with Constitutional AI prompting built in.

Free consultation

Book a free consultation call on Constitutional AI & self-critique

30-minute call with the HinterBuild team. Discuss your project, architecture questions, or next steps — no obligation.

Book a meeting

Keep reading