# Pairwise Self Verification Reasoning

> Verify solution quality through pairwise comparison rather than pointwise scoring. Implement topology coverage and Swiss refinement to allocate verification compute to uncertain pairs, improving calibration and reducing verification overhead.

- Skill: `adu2021/pairwise-self-verification-reasoning` (Agent Skill)
- Install (CLI): `npx skillmds@latest add adu2021/pairwise-self-verification-reasoning`
- Raw SKILL.md: https://api.skillmd.com/api/skills/adu2021/pairwise-self-verification-reasoning/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: adu2021 (https://skillmd.com/u/adu2021)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/adu2021/pairwise-self-verification-reasoning

---


# V_1: Pairwise Self-Verification for Parallel Reasoning

Pointwise solution verification (score each candidate independently) suffers from calibration collapse: models lack comparative context and assign arbitrary absolute scores. Pairwise verification (compare solutions head-to-head) provides superior discrimination by grounding judgments in relative comparisons. V_1 extends this insight with uncertainty-guided allocation: allocate verification compute to uncertain pairs using Bradley-Terry models.

The core innovation treats solution verification as a ranking problem with adaptive compute allocation. Pairs with similar quality scores yield high information gain and receive more verification passes; pairs with clear winners are skipped or verified once.

## Core Concept

V_1 implements two coordinated mechanisms:

1. **Pairwise Ranking**: Models compare any two solutions, assigning probabilistic judgments via Bradley-Terry model to provide calibrated confidence
2. **Uncertainty-Guided Allocation**: Use confidence magnitude as proxy for information gain; allocate extra compute passes to uncertain pairs

This combination achieves superior calibration while reducing total verification compute compared to pointwise approaches.

## Architecture Overview

- **Input**: k candidate solutions from generation models
- **Topology Generation**: Ensure all pairs covered (complete tournament graph or subset)
- **Swiss Refinement**: Iteratively allocate verification passes to uncertain pairs
- **Bradley-Terry Model**: Accumulate pairwise judgments into global ranking
- **Output**: Ranked solution list with confidence intervals

## Implementation Steps

**Step 1: Implement pairwise comparison oracle**

Create a verifier that compares two solutions and returns confidence-calibrated judgments.

```python
class PairwiseVerifier:
    """Compare solutions head-to-head with confidence scoring."""

    def __init__(self, verification_model):
        self.model = verification_model

    def compare(self, solution_a, solution_b, prompt):
        """
        Compare two solutions and return winner + confidence.
        Returns: (winner_idx ∈ {0, 1}, confidence ∈ [0.5, 1.0])
        """
        comparison_prompt = f"""
Question/Task: {prompt}

Solution A:
{solution_a}

Solution B:
{solution_b}

Which solution is better? Respond with: BETTER_A or BETTER_B, and confidence 0-100.
"""

        response = self.model.generate(comparison_prompt, max_tokens=50)

        # Parse response
        if 'BETTER_A' in response:
            winner = 0
        elif 'BETTER_B' in response:
            winner = 1
        else:
            winner = random.randint(0, 1)

        # Extract confidence (normalize to [0.5, 1.0])
        try:
            confidence_raw = int(''.join(filter(str.isdigit, response.split('confidence')[-1])))
            confidence = 0.5 + (confidence_raw / 100) * 0.5  # Map [0, 100] → [0.5, 1.0]
        except:
            confidence = 0.75  # Default if parsing fails

        return winner, confidence

    def batch_compare(self, solution_pairs, prompt):
        """Compare multiple pairs in parallel."""
        results = []
        for sol_a, sol_b in solution_pairs:
            winner, conf = self.compare(sol_a, sol_b, prompt)
            results.append((winner, conf))
        return results
```

**Step 2: Design comparison topology**

Organize comparisons to ensure coverage while minimizing total comparisons.

```python
class ComparisonTopology:
    """Manage comparison graph to ensure all solutions are ranked."""

    def __init__(self, num_solutions):
        self.num_solutions = num_solutions
        self.comparisons = []  # List of (idx_a, idx_b) pairs
        self.comparison_count = {}  # Track how many times each pair compared

    def round_robin_topology(self):
        """
        Round-robin: each solution compared with 2-3 others.
        Ensures coverage with ~2K comparisons per K solutions.
        """
        topology = []
        for i in range(self.num_solutions):
            # Compare solution i with next 2-3 solutions (circular)
            for offset in [1, 2]:
                j = (i + offset) % self.num_solutions
                if (i, j) not in topology and (j, i) not in topology:
                    topology.append((i, j))

        return topology

    def complete_tournament_topology(self):
        """
        Complete tournament: every pair compared once.
        Expensive (~K²/2 comparisons) but highest quality.
        Use for k ≤ 10.
        """
        topology = []
        for i in range(self.num_solutions):
            for j in range(i + 1, self.num_solutions):
                topology.append((i, j))
        return topology

    def adaptive_topology(self, previous_rankings):
        """
        Adaptive: focus on pairs with similar quality scores.
        Skip comparisons between clearly different solutions.
        """
        topology = []
        quality_scores = [r['score'] for r in previous_rankings]

        for i in range(self.num_solutions):
            for j in range(i + 1, self.num_solutions):
                score_diff = abs(quality_scores[i] - quality_scores[j])

                # Include pair if scores are close (uncertain rank)
                if score_diff < 0.3:  # Threshold
                    topology.append((i, j))

        return topology
```

**Step 3: Implement Bradley-Terry model for ranking**

Accumulate pairwise judgments into calibrated global ranking.

```python
import numpy as np

class BradleyTerryRanker:
    """
    Bradley-Terry model: models pairwise comparison outcomes.
    P(A beats B) = λ_A / (λ_A + λ_B), where λ_i is strength parameter.
    """

    def __init__(self, num_solutions):
        self.num_solutions = num_solutions
        self.strengths = np.ones(num_solutions)  # Initial uniform strengths
        self.win_counts = np.zeros(num_solutions)  # Wins per solution
        self.comparison_counts = np.zeros((num_solutions, num_solutions))

    def update_from_comparison(self, winner_idx, loser_idx, confidence):
        """Update strength parameters from a single comparison."""
        self.win_counts[winner_idx] += confidence

        # Update comparison matrix for information tracking
        self.comparison_counts[winner_idx][loser_idx] += 1
        self.comparison_counts[loser_idx][winner_idx] += 1

    def fit_strengths(self, num_iterations=10):
        """
        EM-style fitting: iteratively estimate strength parameters.
        λ_i ∝ (wins_i) / (expected_matchups_i)
        """
        for iteration in range(num_iterations):
            # E-step: expected wins
            new_strengths = np.zeros(self.num_solutions)

            for i in range(self.num_solutions):
                expected_wins = 0.0

                for j in range(self.num_solutions):
                    if i != j:
                        # Probability i beats j under current model
                        p_ij = self.strengths[i] / (self.strengths[i] + self.strengths[j])

                        # Update with observed comparison
                        if self.comparison_counts[i][j] > 0:
                            expected_wins += (self.win_counts[i] * p_ij)

                new_strengths[i] = expected_wins + 1e-8  # Avoid zero

            # Normalize
            self.strengths = new_strengths / new_strengths.sum() * len(new_strengths)

    def get_ranking(self):
        """Return solutions ranked by fitted strength parameters."""
        indices = np.argsort(-self.strengths)  # Descending order
        scores = self.strengths[indices]

        return [
            {'solution_idx': idx, 'score': float(scores[rank])}
            for rank, idx in enumerate(indices)
        ]

    def get_uncertainty(self, idx_a, idx_b):
        """
        Estimate uncertainty for a specific pair.
        High uncertainty when strengths are similar.
        """
        strength_diff = abs(self.strengths[idx_a] - self.strengths[idx_b])
        strength_sum = self.strengths[idx_a] + self.strengths[idx_b]

        # Normalize difference to [0, 1]
        uncertainty = 1.0 - (strength_diff / (strength_sum + 1e-8))

        return uncertainty
```

**Step 4: Swiss refinement for adaptive allocation**

Iteratively identify uncertain pairs and allocate more verification passes.

```python
def swiss_refinement(solutions, verifier, prompt, max_comparison_budget=100):
    """
    Swiss-system refinement: iteratively identify uncertain pairs,
    allocate extra comparison passes to them.
    """
    k = len(solutions)
    ranker = BradleyTerryRanker(k)

    # Initial topology: round-robin to establish baseline ranking
    topology = ComparisonTopology(k)
    initial_pairs = topology.round_robin_topology()

    comparison_budget_used = 0

    for pair_idx, (i, j) in enumerate(initial_pairs):
        if comparison_budget_used >= max_comparison_budget:
            break

        winner, confidence = verifier.compare(solutions[i], solutions[j], prompt)
        ranker.update_from_comparison(winner, 1 - winner, confidence)
        comparison_budget_used += 1

    # Fit initial ranking
    ranker.fit_strengths(num_iterations=5)

    # Refinement phase: allocate extra comparisons to uncertain pairs
    for refinement_round in range(3):
        # Find most uncertain pairs
        uncertain_pairs = []

        for i in range(k):
            for j in range(i + 1, k):
                uncertainty = ranker.get_uncertainty(i, j)
                uncertain_pairs.append((uncertainty, i, j))

        # Sort by uncertainty (descending)
        uncertain_pairs.sort(reverse=True)

        # Compare top uncertain pairs
        for uncertainty, i, j in uncertain_pairs[:5]:  # Top 5 uncertain
            if comparison_budget_used >= max_comparison_budget:
                break

            winner, confidence = verifier.compare(solutions[i], solutions[j], prompt)
            ranker.update_from_comparison(winner, 1 - winner, confidence)
            comparison_budget_used += 1

        # Refit
        ranker.fit_strengths(num_iterations=3)

    return ranker.get_ranking(), comparison_budget_used
```

**Step 5: Integration with generation and training**

Combine parallel generation with verification and optional training.

```python
def generate_and_verify(generator, verifier, prompt, num_candidates=8):
    """
    Generate multiple solutions in parallel, then rank via pairwise verification.
    """
    # Parallel generation (can be distributed)
    solutions = [
        generator.generate(prompt, temperature=0.7)
        for _ in range(num_candidates)
    ]

    # Pairwise verification with Swiss refinement
    ranking, budget_used = swiss_refinement(
        solutions,
        verifier,
        prompt,
        max_comparison_budget=30
    )

    return {
        'ranking': ranking,
        'solutions': [solutions[r['solution_idx']] for r in ranking],
        'verifications': budget_used,
        'efficiency': (num_candidates * (num_candidates - 1) / 2) / budget_used
    }

def train_with_pairwise_ranking(generator, verifier, train_prompts,
                               num_iterations=1000):
    """
    Train generator using pairwise verification feedback.
    PairRL: co-evolve generation and verification capabilities.
    """
    optimizer = torch.optim.AdamW(generator.parameters(), lr=1e-4)

    for iteration in range(num_iterations):
        prompt = random.choice(train_prompts)

        # Generate candidates
        solutions = [
            generator.generate(prompt, temperature=0.7)
            for _ in range(4)
        ]

        # Verify ranking
        ranking, _ = swiss_refinement(solutions, verifier, prompt,
                                     max_comparison_budget=10)

        # Policy gradient: encourage generating top-ranked solution
        top_solution = ranking[0]['solution_idx']
        logprob_top = generator.compute_logprob(prompt, solutions[top_solution])

        # Bottom-ranked solution (negative example)
        bottom_solution = ranking[-1]['solution_idx']
        logprob_bottom = generator.compute_logprob(prompt, solutions[bottom_solution])

        # Loss: margin between top and bottom
        loss = -logprob_top + logprob_bottom

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        if (iteration + 1) % 100 == 0:
            print(f"Iteration {iteration + 1}: Loss = {loss.item():.4f}")

    return generator
```

**Step 6: Evaluation on reasoning tasks**

Benchmark on tasks with multiple valid solutions requiring careful discrimination.

```python
def evaluate_verification_quality(verifier, solution_pairs, ground_truth_rankings):
    """
    Evaluate: does pairwise verification produce correct rankings?
    """
    correct_predictions = 0
    total_comparisons = 0

    for (sol_a, sol_b), (correct_winner, _) in zip(solution_pairs, ground_truth_rankings):
        predicted_winner, confidence = verifier.compare(sol_a, sol_b, "")

        if predicted_winner == correct_winner:
            correct_predictions += 1

        total_comparisons += 1

    accuracy = correct_predictions / total_comparisons
    print(f"Pairwise verification accuracy: {accuracy * 100:.1f}%")

    return accuracy
```

## Practical Guidance

**Hyperparameter Selection:**
- **Uncertainty threshold for Swiss**: 0.2-0.4. Higher = more refinement passes, higher quality but more compute.
- **Max comparison budget**: 20-50 comparisons for k=5-10 solutions. Roughly 3-5x per solution.
- **Bradley-Terry fitting iterations**: 3-10. More iterations = better fit; diminishing returns beyond 5.
- **Top-k allocation**: In Swiss refinement, compare top 5-10 uncertain pairs; higher allocates more to edge cases.

**When to Use:**
- Multi-solution reasoning tasks where quality differences are subtle
- Scenarios where pointwise verification fails to discriminate
- Settings where verification compute is limited (Swiss refinement optimizes allocation)
- Problems requiring calibrated confidence estimates

**When NOT to Use:**
- Single solution generation (no ranking needed)
- Tasks with obvious best solution (pairwise offers no benefit)
- Real-time systems requiring single-pass verification
- Settings with unreliable comparison oracle

**Common Pitfalls:**
- **Non-transitivity**: If verifier is inconsistent, Bradley-Terry may cycle (A beats B, B beats C, C beats A). Detect and retrain verifier.
- **Sparse comparison graph**: Too few comparisons may leave solutions unranked. Ensure complete or near-complete coverage.
- **Confidence calibration**: If verifier always returns confidence 0.99, uncertainty estimation fails. Validate confidence distribution.
- **Timeout in Swiss refinement**: If many pairs are equally uncertain, refinement loop may run long. Add hard iteration limit.

## Reference

arXiv: https://arxiv.org/abs/2603.04304

