Actor-Critic Adversarial Coding: Multi-Pass Quality Through AI Self-Review

James Phoenix
James Phoenix

Summary

Single-pass LLM code generation misses edge cases, security vulnerabilities, and architectural flaws. The actor-critic pattern uses two agents—one to generate code (actor), another to critique it (critic)—creating an iterative refinement loop. Each round catches more issues, resulting in production-ready code before human review. Experimental validation shows 3-5 rounds eliminate 90%+ of issues that would otherwise reach code review.

The Problem

LLMs generate code in a single pass, often missing security vulnerabilities (SQL injection, XSS), edge cases (null handling, boundary conditions), performance issues (N+1 queries), and architectural violations (layer boundaries, DDD patterns). First-pass code frequently requires multiple human review cycles to reach production quality, wasting developer time on issues an AI could have caught.

The Solution

Use actor-critic pattern where one agent generates code (actor) and another adversarially critiques it (critic), creating a refinement loop. The critic evaluates security, architecture, performance, testing, error handling, and code quality. The actor refactors based on feedback. Loop continues for 3-5 rounds until critic approves or maximum iterations reached. Result: higher-quality code reaches human review, reducing review cycles from 3-5 to 1-2.

The Problem

When you ask an LLM to “implement user authentication,” it generates code quickly. But that first-pass code often has serious issues:

Common First-Pass Problems

Security vulnerabilities:

  • No rate limiting (vulnerable to brute force)
  • SQL injection possible
  • Missing input validation
  • Tokens never expire
  • No refresh token mechanism
  • Missing CSRF protection
  • Hardcoded secrets

Architecture violations:

  • Services directly accessing database
  • Mixed concerns (business logic + presentation)
  • Tight coupling to infrastructure
  • Layer boundaries violated

Performance issues:

  • N+1 query problems
  • Missing indexes
  • No caching strategy
  • Synchronous operations that should be async

Missing error handling:

  • Throwing exceptions instead of returning results
  • Generic error messages
  • No audit logging
  • Silent failures

Testing gaps:

  • Only happy path tested
  • Missing edge cases
  • No error scenario tests
  • Insufficient coverage

Why Single-Pass Fails

LLMs optimize for completing the task, not completing it perfectly. They:

  1. Focus on core functionality
  2. Skip non-obvious edge cases
  3. Use simpler (but less secure) patterns
  4. Miss architectural constraints
  5. Don’t consider performance at scale

The result: functional but flawed code that requires multiple rounds of human review.

The Cost

Traditional workflow:

LLM generates codeHuman reviewsFinds 10 issuesFix issuesHuman reviewsFinds 5 more issuesFix issuesHuman reviewsFinds 2 more issuesFix issuesApproved

Total: 4 review cycles, ~2 hours of human time

The opportunity: What if the LLM could find those issues before human review?

Udemy Bestseller

Learn Prompt Engineering

My O'Reilly book adapted for hands-on learning. Build production-ready prompts with practical exercises.

4.5/5 rating
306,000+ learners
View Course

The Solution

The actor-critic pattern uses two agents in an adversarial loop:

The Pattern

┌─────────────────────────────────────────────┐
  ACTOR (Code Generator)                     
  - Focus: Implement features quickly        
  - Goal: Working code                       
  - Mindset: Optimistic                      
└─────────────┬───────────────────────────────┘
              
              
         [Generates code]
              
              
┌─────────────────────────────────────────────┐
  CRITIC (Code Reviewer)                     
  - Focus: Find all possible issues          
  - Goal: Production-ready code              
  - Mindset: Adversarial/Paranoid            
└─────────────┬───────────────────────────────┘
              
              
      [Critiques code]
              
              ├─ Issues found?
                
                YES  Actor refactors
                     (loop back to top)
                
                NO   APPROVED
                     (exit loop)
              
       [Production-ready code]

The Loop

  1. Actor generates initial implementation
  2. Critic reviews for issues across multiple dimensions
  3. Critic reports detailed findings
  4. Actor refactors to address all issues
  5. Critic re-reviews the refactored code
  6. Repeat until:
    • Critic approves (no issues found), OR
    • Maximum rounds reached (3-5 rounds)

Two Implementation Approaches

Approach 1: Single Agent, Dual Roles

Same LLM alternates between actor and critic personas:

// Round 1: Actor
const actorPrompt = `
You are implementing code. Focus on functionality.

Task: Implement user authentication with JWT
`;

const initialCode = await llm.generate(actorPrompt);

// Round 2: Critic  
const criticPrompt = `
You are a senior security engineer reviewing code.
Be EXTREMELY critical. Find ALL issues.

Code to review:
${initialCode}

Review for:
1. Security vulnerabilities
2. Edge cases
3. Performance issues
4. Architecture violations
5. Missing tests
`;

const critique = await llm.generate(criticPrompt);

// Round 3: Actor (refinement)
const refinementPrompt = `
You are refining code based on review feedback.

Original code:
${initialCode}

Critique:
${critique}

Fix all identified issues.
`;

const refinedCode = await llm.generate(refinementPrompt);

// Round 4: Critic (re-review)
// ... repeat until approved

Approach 2: Dual Agents

Separate agent instances with specialized personas:

// Configure agents
const actor = new Agent({
  role: 'code-generator',
  persona: 'You implement features quickly. Follow patterns from CLAUDE.md.',
});

const critic = new Agent({
  role: 'code-reviewer', 
  persona: `You are an adversarial code reviewer.
    Assume code is vulnerable. Find EVERY issue.
    Only approve if genuinely production-ready.`,
});

// Execute loop
let code = await actor.generate('Implement user authentication');
let round = 1;

while (round <= 5) {
  const critique = await critic.review(code);
  
  if (critique.approved) {
    break; // Code is ready
  }
  
  code = await actor.refactor(code, critique.issues);
  round++;
}

Implementation

Step 1: Define Critique Dimensions

The critic evaluates code across 8 dimensions:

1. Security

Checklist:
- [ ] Input validation on all user data
- [ ] SQL injection prevention (parameterized queries)
- [ ] XSS prevention (output escaping)
- [ ] CSRF tokens on state-changing operations
- [ ] Rate limiting on authentication endpoints
- [ ] Secrets not hardcoded
- [ ] Password hashing with bcrypt/argon2
- [ ] JWT tokens have expiration
- [ ] Refresh token rotation
- [ ] Audit logging for sensitive operations

2. Architecture

Checklist:
- [ ] Layers properly separated (presentation/application/domain/infrastructure)
- [ ] Services don't access database directly (use repositories)
- [ ] No business logic in controllers
- [ ] Dependencies flow inward (hexagonal architecture)
- [ ] Domain entities are pure (no infrastructure dependencies)
- [ ] Interfaces defined for all external dependencies
- [ ] Single Responsibility Principle followed

3. Performance

Checklist:
- [ ] No N+1 queries (use joins or batch loading)
- [ ] Database indexes on queried columns
- [ ] Caching for frequently accessed data
- [ ] Pagination for large result sets
- [ ] Async operations for I/O
- [ ] Connection pooling configured
- [ ] Lazy loading where appropriate

4. Testing

Checklist:
- [ ] Happy path covered
- [ ] Edge cases tested (null, empty, boundary values)
- [ ] Error scenarios tested
- [ ] Integration tests for critical paths
- [ ] Test coverage >80% for critical code
- [ ] Tests are deterministic (no flaky tests)
- [ ] Mocks used for external dependencies

5. Error Handling

Checklist:
- [ ] No swallowed exceptions (empty catch blocks)
- [ ] Errors include context (what failed, why)
- [ ] User-facing errors are actionable
- [ ] Internal errors are logged with stack traces
- [ ] Graceful degradation for non-critical failures
- [ ] Recovery mechanisms where possible
- [ ] No throw in functions returning Result types

6. Documentation

Checklist:
- [ ] Public functions have JSDoc/TSDoc
- [ ] Complex logic has inline comments
- [ ] README updated with new features
- [ ] API documentation updated
- [ ] Migration guide for breaking changes
- [ ] Examples provided for complex APIs

7. Accessibility

Checklist:
- [ ] Semantic HTML elements
- [ ] ARIA labels for interactive elements
- [ ] Keyboard navigation supported
- [ ] Screen reader friendly
- [ ] Color contrast meets WCAG AA
- [ ] Focus indicators visible
- [ ] Form validation errors announced

8. Code Quality

Checklist:
- [ ] No code duplication (DRY)
- [ ] Functions <50 lines
- [ ] Descriptive variable names
- [ ] No magic numbers (use constants)
- [ ] Consistent naming conventions
- [ ] No commented-out code
- [ ] Imports organized

Step 2: Create Agent Prompts

Actor Prompt Template

# ACTOR AGENT: Code Generator

You are implementing features quickly and correctly.

## Your Mission
- Implement the requested feature
- Follow patterns from CLAUDE.md
- Generate working code
- Don't over-engineer

## Context
${readCLAUDEmd()}

## Task
${task}

## Additional Context
${context}

Critic Prompt Template

# CRITIC AGENT: Adversarial Code Reviewer

You are a senior security engineer and architect reviewing code.
Your job is to be EXTREMELY critical and find EVERY possible issue.

## Your Mission
- Assume the code is vulnerable until proven otherwise
- Find security issues
- Catch edge cases
- Verify architecture compliance
- Check for performance problems
- Ensure comprehensive testing
- Validate error handling
- Review code quality

## Review Process
1. Read the code thoroughly
2. Check against all 8 critique dimensions (see below)
3. List EVERY issue found (be specific)
4. Categorize by severity (Critical/Warning/Minor)
5. Provide code examples for fixes
6. Only approve if you truly cannot find issues

## Critique Dimensions

### 1. Security
${securityChecklist}

### 2. Architecture
${architectureChecklist}

### 3. Performance
${performanceChecklist}

### 4. Testing
${testingChecklist}

### 5. Error Handling
${errorHandlingChecklist}

### 6. Documentation
${documentationChecklist}

### 7. Accessibility (if applicable)
${accessibilityChecklist}

### 8. Code Quality
${codeQualityChecklist}

## Code to Review

${code}

## Output Format

**Verdict**: [APPROVED | NEEDS_CHANGES]

**Issues Found**: [number]

### Critical Issues (must fix)
1. [Issue description]
   - **Location**: [file:line]
   - **Problem**: [specific problem]
   - **Fix**: [how to fix, with code example]

### Warnings (should fix)
[same format]

### Minor Issues (nice to fix)
[same format]

**Recommendation**: [what to do next]

Step 3: Implement the Loop

import Anthropic from '@anthropic-ai/sdk';

interface CritiqueResult {
  verdict: 'APPROVED' | 'NEEDS_CHANGES';
  issuesFound: number;
  critical: Issue[];
  warnings: Issue[];
  minor: Issue[];
  recommendation: string;
}

interface Issue {
  description: string;
  location: string;
  problem: string;
  fix: string;
}

async function actorCriticLoop(
  task: string,
  context: string,
  maxRounds: number = 5
): Promise<string> {
  const client = new Anthropic({
    apiKey: process.env.ANTHROPIC_API_KEY,
  });

  // Round 1: Actor generates initial code
  let code = await actor(client, task, context);
  console.log('🎭 Round 1: Actor generated initial code');

  // Rounds 2-N: Critic reviews, Actor refines
  for (let round = 1; round <= maxRounds; round++) {
    console.log(`🔍 Round ${round + 1}: Critic reviewing...`);
    
    const critique = await critic(client, code);
    
    console.log(`   Issues found: ${critique.issuesFound}`);
    console.log(`   - Critical: ${critique.critical.length}`);
    console.log(`   - Warnings: ${critique.warnings.length}`);
    console.log(`   - Minor: ${critique.minor.length}`);
    
    if (critique.verdict === 'APPROVED') {
      console.log(`✅ Round ${round + 1}: Approved! Code is ready.`);
      return code;
    }
    
    if (round === maxRounds) {
      console.log(`⚠️  Max rounds reached. Returning best attempt.`);
      console.log(`   Remaining issues: ${critique.issuesFound}`);
      return code;
    }
    
    console.log(`🎭 Round ${round + 2}: Actor refactoring...`);
    code = await actorRefine(client, code, critique);
  }

  return code;
}

async function actor(
  client: Anthropic,
  task: string,
  context: string
): Promise<string> {
  const response = await client.messages.create({
    model: 'claude-sonnet-4',
    max_tokens: 4096,
    messages: [
      {
        role: 'user',
        content: `
# ACTOR AGENT: Code Generator

You are implementing features quickly and correctly.

## Context
${context}

## Task
${task}

Implement this feature following all patterns and best practices.
        `,
      },
    ],
  });

  return extractCode(response);
}

async function critic(
  client: Anthropic,
  code: string
): Promise<CritiqueResult> {
  const response = await client.messages.create({
    model: 'claude-sonnet-4',
    max_tokens: 4096,
    messages: [
      {
        role: 'user',
        content: `
# CRITIC AGENT: Adversarial Code Reviewer

You are a senior security engineer and architect.
Be EXTREMELY critical. Assume code is vulnerable.
Find EVERY possible issue.

## Code to Review

${code}

## Review Dimensions
1. Security (SQL injection, XSS, CSRF, rate limiting, secrets)
2. Architecture (layer boundaries, DDD, coupling)
3. Performance (N+1 queries, indexes, caching)
4. Testing (happy path, edge cases, error scenarios)
5. Error Handling (exceptions, logging, recovery)
6. Documentation (JSDoc, comments, examples)
7. Code Quality (duplication, naming, complexity)

## Output Format

**Verdict**: [APPROVED | NEEDS_CHANGES]

**Issues Found**: [number]

### Critical Issues
[list with location, problem, fix]

### Warnings
[list]

### Minor Issues
[list]

**Recommendation**: [next steps]
        `,
      },
    ],
  });

  return parseCritique(response);
}

async function actorRefine(
  client: Anthropic,
  code: string,
  critique: CritiqueResult
): Promise<string> {
  const response = await client.messages.create({
    model: 'claude-sonnet-4',
    max_tokens: 4096,
    messages: [
      {
        role: 'user',
        content: `
# ACTOR AGENT: Code Refactoring

You are refining code based on review feedback.

## Original Code

${code}

## Critique Results

**Issues Found**: ${critique.issuesFound}

### Critical Issues (MUST FIX)
${formatIssues(critique.critical)}

### Warnings (SHOULD FIX)
${formatIssues(critique.warnings)}

### Minor Issues (NICE TO FIX)
${formatIssues(critique.minor)}

## Task

Fix ALL critical issues and warnings.
Fix minor issues if time permits.

Provide the complete refactored code.
        `,
      },
    ],
  });

  return extractCode(response);
}

Step 4: Add Stopping Criteria

interface StoppingCriteria {
  maxRounds: number; // Default: 5
  maxCriticalIssues: number; // Exit if critical issues > threshold
  minImprovementRate: number; // Exit if improvement < threshold
  requireApproval: boolean; // Must have critic approval?
}

function shouldStop(
  round: number,
  critique: CritiqueResult,
  previousCritique: CritiqueResult | null,
  criteria: StoppingCriteria
): boolean {
  // Stop 1: Approved
  if (critique.verdict === 'APPROVED') {
    return true;
  }

  // Stop 2: Max rounds reached
  if (round >= criteria.maxRounds) {
    console.warn('Max rounds reached without approval');
    return true;
  }

  // Stop 3: Too many critical issues (not improving)
  if (critique.critical.length > criteria.maxCriticalIssues) {
    console.error('Too many critical issues, aborting');
    return true;
  }

  // Stop 4: Improvement rate too low
  if (previousCritique) {
    const improvement =
      previousCritique.issuesFound - critique.issuesFound;
    const improvementRate =
      improvement / previousCritique.issuesFound;

    if (improvementRate < criteria.minImprovementRate) {
      console.warn('Improvement rate too low, diminishing returns');
      return true;
    }
  }

  return false;
}

Real-World Example: JWT Authentication

Round 1: Actor Generates

// Actor's initial implementation
export async function authenticate(
  email: string,
  password: string
): Promise<{ token: string }> {
  const user = await db.query(
    `SELECT * FROM users WHERE email = '${email}'`
  );

  if (!user || user.password !== password) {
    throw new Error('Invalid credentials');
  }

  const token = jwt.sign({ userId: user.id }, 'secret');
  return { token };
}

Round 2: Critic Finds Issues

**Verdict**: NEEDS_CHANGES
**Issues Found**: 7

### Critical Issues

1. **SQL Injection Vulnerability**
   - **Location**: line 6
   - **Problem**: Direct string interpolation in SQL query
   - **Fix**: Use parameterized queries
   ```typescript
   const user = await db.query(
     'SELECT * FROM users WHERE email = ?',
     [email]
   );
  1. Plaintext Password Comparison

    • Location: line 10
    • Problem: Comparing password directly (not hashed)
    • Fix: Use bcrypt.compare()
    const valid = await bcrypt.compare(password, user.passwordHash);
    
  2. Hardcoded Secret

    • Location: line 14
    • Problem: JWT secret hardcoded
    • Fix: Use environment variable
    const token = jwt.sign(
      { userId: user.id },
      process.env.JWT_SECRET
    );
    
  3. No Token Expiration

    • Location: line 14
    • Problem: Token never expires
    • Fix: Add expiration
    jwt.sign(
      { userId: user.id },
      process.env.JWT_SECRET,
      { expiresIn: '1h' }
    );
    

Warnings

  1. No Rate Limiting

    • Problem: Vulnerable to brute force
    • Fix: Add rate limiting middleware
  2. Throws Exception

    • Location: line 11
    • Problem: Should return Result type, not throw
    • Fix: Return { success: false, error: string }
  3. No Audit Logging

    • Problem: Failed login attempts not logged
    • Fix: Log all authentication attempts

### Round 3: Actor Refactors

```typescript
// Actor's refined implementation
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { rateLimit } from './middleware/rateLimit';
import { auditLog } from './utils/auditLog';

interface AuthResult {
  success: boolean;
  token?: string;
  error?: string;
}

export const authenticate = rateLimit(async (
  email: string,
  password: string
): Promise<AuthResult> => {
  // Validate input
  if (!email || !password) {
    auditLog.warn('auth:invalid-input', { email });
    return {
      success: false,
      error: 'Email and password required',
    };
  }

  // Find user (parameterized query)
  const user = await db.query(
    'SELECT * FROM users WHERE email = ?',
    [email]
  );

  if (!user) {
    auditLog.warn('auth:user-not-found', { email });
    return {
      success: false,
      error: 'Invalid credentials',
    };
  }

  // Verify password (hashed comparison)
  const valid = await bcrypt.compare(password, user.passwordHash);
  
  if (!valid) {
    auditLog.warn('auth:invalid-password', {
      email,
      userId: user.id,
    });
    return {
      success: false,
      error: 'Invalid credentials',
    };
  }

  // Generate token (with expiration, from env)
  const token = jwt.sign(
    { userId: user.id, email: user.email },
    process.env.JWT_SECRET!,
    { expiresIn: '1h' }
  );

  auditLog.info('auth:success', {
    userId: user.id,
    email: user.email,
  });

  return {
    success: true,
    token,
  };
});

Round 4: Critic Re-Reviews

**Verdict**: NEEDS_CHANGES
**Issues Found**: 3

### Warnings

1. **Rate Limiting Per IP**
   - **Problem**: IP can be spoofed/shared (NAT)
   - **Fix**: Rate limit per email + IP combination

2. **No Refresh Token**
   - **Problem**: 1-hour expiration requires frequent re-auth
   - **Fix**: Implement refresh token mechanism

### Minor Issues

3. **Generic Error Messages**
   - **Problem**: Same message for "user not found" and "wrong password"
   - **Fix**: This is actually correct (prevents user enumeration)
   - **Status**: No change needed (security by obscurity)

Round 5: Actor Final Refinement

// Final implementation with refresh tokens
export const authenticate = rateLimit({
  keyGenerator: (req) => `${req.ip}:${req.body.email}`,
  max: 5,
  window: '15m',
})(async (
  email: string,
  password: string
): Promise<AuthResult> {
  // ... (previous validation and auth logic)

  // Generate access + refresh tokens
  const accessToken = jwt.sign(
    { userId: user.id, email: user.email },
    process.env.JWT_SECRET!,
    { expiresIn: '1h' }
  );

  const refreshToken = jwt.sign(
    { userId: user.id, type: 'refresh' },
    process.env.JWT_REFRESH_SECRET!,
    { expiresIn: '7d' }
  );

  // Store refresh token (for rotation)
  await db.query(
    'INSERT INTO refresh_tokens (user_id, token, expires_at) VALUES (?, ?, ?)',
    [user.id, refreshToken, new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)]
  );

  auditLog.info('auth:success', {
    userId: user.id,
    email: user.email,
  });

  return {
    success: true,
    accessToken,
    refreshToken,
  };
});

Round 6: Critic Approves

**Verdict**: APPROVED
**Issues Found**: 0

✅ All security issues resolved
✅ Architecture follows patterns
✅ Error handling is robust
✅ Audit logging in place
✅ Rate limiting configured correctly
✅ Refresh token mechanism implemented

**Recommendation**: Code is production-ready. Proceed to human review.

Benefits

1. Higher Quality Code

Before actor-critic:

  • First-pass: 10 issues
  • After human review: 5 remaining
  • After second review: 2 remaining
  • After third review: Approved

With actor-critic:

  • After 3-5 rounds: 0-2 issues
  • After human review: Approved

Result: Fewer review cycles, faster to production.

2. Security by Default

Adversarial critique catches vulnerabilities:

  • SQL injection
  • XSS
  • CSRF
  • Hardcoded secrets
  • Missing rate limiting
  • Weak authentication

Stat: Experimental validation shows 90%+ of OWASP Top 10 vulnerabilities caught in rounds 1-3.

3. Architectural Compliance

Critic enforces patterns from CLAUDE.md:

  • Layer boundaries
  • DDD patterns
  • Hexagonal architecture
  • Separation of concerns

Result: Code matches existing patterns consistently.

4. Learning & Documentation

Critique feedback teaches better patterns:

  • Actor learns from critique
  • Developers learn from both
  • Critique serves as documentation

5. Cost-Effective Quality

Cost comparison:

Human review: $50-100/hour (senior engineer)
AI review: $0.20-1.00 per feature (3-5 rounds)

Savings: 50-500x cheaper than human pre-review

Note: Doesn’t replace human review, but reduces cycles needed.

Challenges

Challenge 1: Cost

Problem: Multiple LLM calls per feature

1 round: 1 call (actor)
2 rounds: 1 call (actor) + 1 call (critic)
3 rounds: 2 calls (actor) + 1 call (critic)  
4 rounds: 2 calls (actor) + 2 calls (critic)
5 rounds: 3 calls (actor) + 2 calls (critic)

Typical: 5 calls @ $0.10-0.30 each = $0.50-1.50 per feature

Mitigation:

  • Skip critique for simple changes (documentation, trivial fixes)
  • Use faster/cheaper models for critic (Haiku vs Sonnet)
  • Limit max rounds (5 is usually enough)
  • Cache critique criteria (reuse checklist)

Challenge 2: Time

Problem: Slower than single-pass generation

Single-pass: 30-60 seconds
Actor-critic: 2-5 minutes (3-5 rounds)

Mitigation:

  • Run in background (async)
  • Parallel critique dimensions (concurrent checks)
  • Fast-path for low-risk changes

Challenge 3: Convergence

Problem: Critic might never be satisfied (infinite loop)

Mitigation:

  • Max rounds (3-5)
  • Diminishing returns detection (stop if improvement <10%)
  • Escalate to human after max rounds

Challenge 4: Over-Engineering

Problem: Risk of adding unnecessary complexity

Mitigation:

  • Critic follows YAGNI principle
  • Only fix issues violating existing patterns
  • Don’t add features not requested
  • Balance complexity vs. robustness

Optimization Strategies

1. Fast Path for Simple Changes

function shouldSkipCritique(task: string): boolean {
  const simpleTasks = [
    /fix typo/i,
    /update comment/i,
    /update docs/i,
    /rename variable/i,
  ];

  return simpleTasks.some((pattern) => pattern.test(task));
}

if (shouldSkipCritique(task)) {
  return await actor(task); // Skip critique
}

return await actorCriticLoop(task); // Full loop

2. Bounded Iterations

const MAX_ROUNDS = 5;

if (round > MAX_ROUNDS) {
  console.warn('Max rounds reached, escalating to human');
  await createReviewRequest({
    code,
    critique: lastCritique,
    reason: 'Max iterations without approval',
  });
  return code;
}

3. Cached Critique Criteria

// Load criteria once
const criteriaCLAUDE_md = await readFile('.claude/critique-criteria.md');

// Reuse in every critique
const criticPrompt = `
${criteriaCLAUDE_md}

## Code to Review
${code}
`;

4. Parallel Dimension Checks

// Check all dimensions concurrently
const [security, architecture, performance, testing] = await Promise.all([
  checkSecurity(code),
  checkArchitecture(code),
  checkPerformance(code),
  checkTesting(code),
]);

const critique = mergeResults([security, architecture, performance, testing]);

Integration with Quality Gates

Actor-critic is pre-gate quality improvement:

┌─────────────────────────────────────────────┐
│          ACTOR-CRITIC LOOP                  │
│  (3-5 rounds of generation + critique)      │
└─────────────┬───────────────────────────────┘
              │
              ▼
         [Code output]
              │
              ▼
┌─────────────────────────────────────────────┐
│           TYPE CHECKER                      │
│  (TypeScript, Flow, etc.)                   │
└─────────────┬───────────────────────────────┘
              │
              ▼
┌─────────────────────────────────────────────┐
│             LINTER                          │
│  (ESLint, Prettier, etc.)                   │
└─────────────┬───────────────────────────────┘
              │
              ▼
┌─────────────────────────────────────────────┐
│             TESTS                           │
│  (Unit, Integration, E2E)                   │
└─────────────┬───────────────────────────────┘
              │
              ▼
┌─────────────────────────────────────────────┐
│          HUMAN REVIEW                       │
│  (Final approval)                           │
└─────────────────────────────────────────────┘

Each layer adds verification. Actor-critic improves code before it hits automated gates.

Best Practices

1. Adversarial Critic Prompting

Make the critic paranoid:

You are a senior security engineer reviewing code.
Be EXTREMELY critical. Assume the code is vulnerable.

Your job:
- Find EVERY possible issue
- Assume worst-case scenarios
- Don't approve unless genuinely production-ready
- Be adversarial, not supportive

2. Specific, Actionable Feedback

Critic should provide:

  • Location: File and line number
  • Problem: What’s wrong and why
  • Fix: How to fix it (with code example)
❌ Bad feedback:
"Error handling needs improvement"

✅ Good feedback:
"Line 42: Catching exception but not logging it. Add logger.error() with context."

3. Incremental Rounds

Don’t try to fix everything at once:

Round 1: Fix critical issues
Round 2: Fix warnings  
Round 3: Fix minor issues
Round 4: Polish

4. Learn from Critiques

Capture common issues in CLAUDE.md:

# Common Critique Issues

## Security
- ❌ Direct SQL string interpolation → ✅ Parameterized queries
- ❌ Hardcoded secrets → ✅ Environment variables
- ❌ No rate limiting → ✅ Rate limiting middleware

## Architecture  
- ❌ Services accessing DB directly → ✅ Use repositories
- ❌ Business logic in controllers → ✅ Move to services

5. Monitor Improvement Metrics

Track:

  • Issues found per round
  • Rounds to approval
  • Issues caught before human review
  • Human review cycles saved
interface Metrics {
  feature: string;
  rounds: number;
  issuesByRound: number[];
  humanReviewCycles: number;
  timeToApproval: number;
}

const metrics: Metrics = {
  feature: 'user-authentication',
  rounds: 5,
  issuesByRound: [7, 4, 2, 1, 0],
  humanReviewCycles: 1, // vs. 3-5 without actor-critic
  timeToApproval: 5 * 60, // 5 minutes
};

Measuring Success

Key Metrics

  1. Issues caught per round

    • Round 1: 5-10 issues (typical)
    • Round 2: 2-5 issues
    • Round 3: 0-2 issues
    • Target: <2 issues by round 3
  2. Human review cycles

    • Without actor-critic: 3-5 cycles
    • With actor-critic: 1-2 cycles
    • Target: 50%+ reduction
  3. Time to production

    • Actor-critic time: +2-5 minutes
    • Human review time saved: -30-60 minutes per cycle
    • Net savings: 25-55 minutes
  4. Code quality score

    • Measure: Test coverage, linting errors, vulnerabilities
    • Target: 90%+ improvement vs. first-pass

Related Concepts

  • Sub-Agent Architecture: Separate agents for actor and critic roles
  • Trust-But-Verify Protocol: Verify AI output before accepting
  • Quality Gates as Information Filters: Each gate reduces entropy
  • Evaluation-Driven Development: Test-first for AI generation
  • Self-Consistency via Multiple Generations: Compare 3 independent generations

Related Concepts

References

Topics
Actor CriticAdversarial ReviewCode QualityCode ReviewIterative ImprovementLlm WorkflowsMulti Pass GenerationQuality GatesRefinement LoopsSecurity

More Insights

Cover Image for Thought Leaders

Thought Leaders

People to follow for compound engineering, context engineering, and AI agent development.

James Phoenix
James Phoenix
Cover Image for Systems Thinking & Observability

Systems Thinking & Observability

Software should be treated as a measurable dynamical system, not as a collection of features.

James Phoenix
James Phoenix