Skip to content
Back to skills

Composition Rl Prompts

ASecurity

Convert easy, high-accuracy training prompts into harder compositional problems by sequentially chaining multiple prompts together. Use Composition-RL to maintain effective learning signals during RL training when many prompts achieve near-perfect accuracy, enabling curriculum learning through progressive compositional depths.

  • 6 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 9, 2026
testingpythongo

Works with

  • cli

Security analysis

A100/100

Scanned September 9, 2026

npx -y skills add ADu2021/skillXiv --skill composition-rl-prompts --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Composition Rl Prompts?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Composition Rl Prompts
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/adu2021-composition-rl-prompts/badge)](https://www.skillsdirectory.com/skills/adu2021-composition-rl-prompts)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: composition-rl-prompts
title: "Composition-RL: Compose Your Verifiable Prompts for RL of LLMs"
version: 0.0.2
engine: skillxiv-v0.0.2-claude-opus-4.6
license: MIT
url: "https://arxiv.org/abs/2602.12036"
keywords: [Reinforcement Learning, Prompt Composition, Curriculum Learning, Problem Difficulty, Verifiable Reasoning]
description: "Convert easy, high-accuracy training prompts into harder compositional problems by sequentially chaining multiple prompts together. Use Composition-RL to maintain effective learning signals during RL training when many prompts achieve near-perfect accuracy, enabling curriculum learning through progressive compositional depths."
---

# Composition-RL: Compose Your Verifiable Prompts for RL of LLMs

## Problem Context

During reinforcement learning of language models on mathematical and reasoning tasks, many prompts become too easy—models achieve 100% accuracy, making these prompts uninformative for further learning. This creates a plateau where gradient signals vanish. Standard curriculum learning solutions are expensive, requiring new data collection or external scoring. Composition-RL addresses this by algorithmically combining existing easy prompts into harder compositional ones without new annotation.

## Core Concept

Composition-RL automatically merges K existing prompts into a single compositional prompt that requires solving all K sub-problems sequentially. A two-prompt composition works as follows:

1. Extract the answer from prompt 1 and create a symbolic definition
2. Modify prompt 2 to use a variable instead of a concrete value
3. Link the two by replacing the variable with the symbolic value from prompt 1

The key insight is that solving the composed problem requires solving each sub-problem in sequence, creating a multiplicative difficulty increase without manual curation.

## Architecture Overview

- **Prompt parsing**: Extract structure and numerical values from existing prompts
- **Sequential linking**: Chain solutions from earlier prompts into later prompts
- **Difficulty scaling**: Compose progressively (depth 2, then depth 3)
- **Validation**: Verify composed prompts are solvable and distinct from originals
- **Curriculum stages**: Train on original → depth-2 compositions → depth-3 compositions

## Implementation

### Step 1: Parse problem structure and extract answers

Identify prompt templates and answer patterns.

```python
import re
from typing import Dict, List, Tuple, Optional
from dataclasses import dataclass

@dataclass
class ParsedProblem:
    """Extracted problem structure."""
    original_text: str
    template_pattern: str
    variables: Dict[str, str]
    answer_value: Optional[str]
    answer_type: str  # 'numeric', 'symbolic', 'boolean'

class ProblemStructureParser:
    """Parse mathematical problem structure for composition."""

    def __init__(self):
        self.numeric_pattern = r'[-+]?\d+(?:\.\d+)?'
        self.variable_pattern = r'[a-zA-Z_]\w*'

    def parse_problem(self, problem_text: str) -> ParsedProblem:
        """
        Extract structure from a problem.
        Assumes verifiable problems with clear answers.
        """
        # Simple extraction: assume last sentence/line is answer
        lines = problem_text.strip().split('\n')

        # Find answer (last line or line with specific markers)
        answer_line = None
        answer_value = None
        for line in reversed(lines):
            if 'Answer:' in line or 'answer:' in line or '=' in line:
                answer_line = line
                answer_match = re.search(r'=\s*(.+?)(?:\s*[.,]|$)', line)
                if answer_match:
                    answer_value = answer_match.group(1).strip()
                break

        if answer_value is None:
            answer_value = lines[-1].strip()

        # Determine answer type
        if re.match(r'[-+]?\d+(?:\.\d+)?$', answer_value):
            answer_type = 'numeric'
        else:
            answer_type = 'symbolic'

        return ParsedProblem(
            original_text=problem_text,
            template_pattern='\n'.join(lines[:-1]),  # Template without answer
            variables={},
            answer_value=answer_value,
            answer_type=answer_type
        )

    def extract_numerical_values(self, problem_text: str) -> List[Tuple[str, str]]:
        """Extract all numerical values and their context."""
        matches = []
        for match in re.finditer(self.numeric_pattern, problem_text):
            value = match.group(0)
            context_start = max(0, match.start() - 20)
            context_end = min(len(problem_text), match.end() + 20)
            context = problem_text[context_start:context_end]
            matches.append((value, context))
        return matches
```

### Step 2: Compose two prompts sequentially

Link answer from first prompt into second prompt.

```python
class PromptComposer:
    """Compose multiple prompts into single difficult problem."""

    def __init__(self, parser: ProblemStructureParser):
        self.parser = parser

    def compose_two_prompts(
        self,
        prompt_1: str,
        prompt_2: str,
        variable_name: str = "X"
    ) -> Tuple[str, str]:
        """
        Compose two prompts into one.

        Strategy:
        1. Parse prompt_1, extract answer as value V
        2. In prompt_2, replace a numerical constant with variable
        3. Create composed prompt that solves p1, then uses answer in p2

        Args:
            prompt_1: First problem
            prompt_2: Second problem to modify
            variable_name: Name for the variable linking problems

        Returns:
            (composed_prompt, expected_answer)
        """
        # Parse both problems
        parsed_1 = self.parser.parse_problem(prompt_1)
        parsed_2 = self.parser.parse_problem(prompt_2)

        answer_1 = parsed_1.answer_value

        # Find a numerical value in prompt_2 to replace
        numerical_values = self.parser.extract_numerical_values(prompt_2)

        if not numerical_values:
            # No numeric value to replace, use direct substitution
            # Modify template of prompt_2 to reference the variable
            modified_prompt_2 = prompt_2.replace(
                parsed_2.answer_value,
                ""  # Will be determined by solving prompt_1
            )
        else:
            # Replace first numerical value with variable
            value_to_replace, _ = numerical_values[0]

            # Create modified prompt_2
            modified_prompt_2 = prompt_2.replace(
                value_to_replace,
                f"{variable_name} (obtained from the first problem)",
                count=1
            )

        # Compose into single problem
        composed_prompt = f"""Solve the following two-part problem:

Part 1: {prompt_1.strip()}

Part 2: {modified_prompt_2.strip()}

Let {variable_name} be the answer to Part 1. Use this value in Part 2 to find the final answer.

What is the final answer?"""

        # Composed answer requires solving both
        return composed_prompt, parsed_2.answer_value  # Final answer from part 2

    def compose_k_prompts(
        self,
        prompts: List[str],
        max_depth: int = 3
    ) -> List[Tuple[str, str]]:
        """
        Compose K prompts with progressive chaining.

        For 3 prompts: (p1, p2, p3) -> p1's answer feeds p2, p2's answer feeds p3
        """
        composed = []

        # Generate compositions of depth 2, 3, ..., min(max_depth, len(prompts))
        for depth in range(2, min(max_depth + 1, len(prompts) + 1)):
            subset = prompts[:depth]

            # Progressive chaining
            current_prompt = subset[0]
            answers_chain = [self.parser.parse_problem(subset[0]).answer_value]

            for i in range(1, len(subset)):
                next_prompt = subset[i]
                current_prompt, final_answer = self.compose_two_prompts(
                    current_prompt, next_prompt,
                    variable_name=f"X{i}"
                )
                answers_chain.append(final_answer)

            composed.append((current_prompt, answers_chain[-1]))

        return composed
```

### Step 3: Validate composed prompts

Ensure composed problems are solvable and distinct.

```python
class CompositionValidator:
    """Validate composed prompts for training."""

    def __init__(self, verifier_model):
        """
        Args:
            verifier_model: Model that verifies if answer is correct.
                          Returns True if correct, False otherwise.
        """
        self.verifier_model = verifier_model

    def validate_composition(
        self,
        composed_prompt: str,
        expected_answer: str,
        original_prompts: List[str]
    ) -> Dict[str, bool]:
        """
        Validate that composed prompt is solvable.

        Returns:
            Dict with keys: 'is_valid', 'is_distinct', 'solvable'
        """
        # Check 1: Is it structurally distinct from originals?
        is_distinct = all(
            composed_prompt.strip() != orig.strip()
            for orig in original_prompts
        )

        # Check 2: Does the composed prompt appear solvable?
        # (Quick check: answer_format is consistent)
        answer_format_valid = isinstance(expected_answer, str)

        # Check 3: Verify with model (sample generation)
        solvable = self._test_solvability(composed_prompt, expected_answer)

        return {
            'is_valid': is_distinct and answer_format_valid,
            'is_distinct': is_distinct,
            'solvable': solvable
        }

    def _test_solvability(
        self,
        prompt: str,
        expected_answer: str,
        num_samples: int = 3
    ) -> bool:
        """
        Quick test: can model generate the expected answer?
        """
        for _ in range(num_samples):
            response = self.verifier_model.generate(
                prompt, max_tokens=500, temperature=0.7
            )

            # Check if response contains expected answer
            if self._answer_matches(response, expected_answer):
                return True

        return False

    @staticmethod
    def _answer_matches(response: str, expected: str) -> bool:
        """Check if response contains expected answer."""
        response_clean = re.sub(r'[^a-zA-Z0-9.-]', '', response.lower())
        expected_clean = re.sub(r'[^a-zA-Z0-9.-]', '', expected.lower())
        return expected_clean in response_clean
```

### Step 4: Build curriculum with progressive composition

Progressively introduce compositional difficulties.

```python
class CompositionCurriculum:
    """Manage curriculum learning through progressive composition."""

    def __init__(
        self,
        original_prompts: List[str],
        composer: PromptComposer,
        validator: CompositionValidator,
        max_composition_depth: int = 3
    ):
        self.original_prompts = original_prompts
        self.composer = composer
        self.validator = validator
        self.max_composition_depth = max_composition_depth

        self.curriculum_stages = []
        self._build_curriculum()

    def _build_curriculum(self):
        """Generate curriculum stages."""

        # Stage 1: Original prompts
        stage_1 = [
            (prompt, self.composer.parser.parse_problem(prompt).answer_value)
            for prompt in self.original_prompts
        ]
        self.curriculum_stages.append(('depth_1', stage_1))

        # Stages 2+: Progressive compositions
        for depth in range(2, self.max_composition_depth + 1):
            composed_prompts = []

            # Sample subsets and compose
            for i in range(0, len(self.original_prompts), depth):
                subset = self.original_prompts[i:i + depth]
                if len(subset) < depth:
                    continue

                compositions = self.composer.compose_k_prompts(subset, max_depth=depth)

                for comp_prompt, answer in compositions:
                    # Validate
                    validation = self.validator.validate_composition(
                        comp_prompt, answer, subset
                    )

                    if validation['is_valid']:
                        composed_prompts.append((comp_prompt, answer))

            if composed_prompts:
                self.curriculum_stages.append((f'depth_{depth}', composed_prompts))

    def get_stage(self, stage_index: int) -> Tuple[str, List[Tuple[str, str]]]:
        """Get curriculum stage."""
        return self.curriculum_stages[min(stage_index, len(self.curriculum_stages) - 1)]

    def get_all_stages(self) -> List[Tuple[str, List[Tuple[str, str]]]]:
        """Return all curriculum stages."""
        return self.curriculum_stages
```

### Step 5: Training with curriculum

Train using progressively harder compositions.

```python
def train_with_composition_curriculum(
    model,
    original_prompts: List[str],
    verifier,
    optimizer,
    num_epochs: int = 3,
    curriculum_stages: int = 3,
    group_size: int = 8,
    device: str = 'cuda'
):
    """
    Train LLM with composition-based curriculum.

    Stages: original → depth-2 compositions → depth-3 compositions
    """
    parser = ProblemStructureParser()
    composer = PromptComposer(parser)
    validator = CompositionValidator(model)

    curriculum = CompositionCurriculum(
        original_prompts, composer, validator,
        max_composition_depth=curriculum_stages
    )

    for epoch in range(num_epochs):
        # Get current curriculum stage
        stage_idx = min(epoch // max(1, num_epochs // curriculum_stages), curriculum_stages - 1)
        stage_name, stage_prompts = curriculum.get_stage(stage_idx)

        print(f"Epoch {epoch + 1}: Curriculum stage {stage_name} "
              f"({len(stage_prompts)} problems)")

        total_loss = 0.0
        num_batches = 0

        # Training on current stage
        for batch_idx in range(0, len(stage_prompts), group_size):
            batch_prompts = stage_prompts[batch_idx:batch_idx + group_size]
            batch_size = len(batch_prompts)

            # Generate responses
            responses = []
            log_probs_list = []

            for prompt, expected_answer in batch_prompts:
                response, log_prob = model.generate_with_logprobs(
                    prompt, max_tokens=500
                )
                responses.append(response)
                log_probs_list.append(log_prob)

            log_probs = torch.stack(log_probs_list).to(device)

            # Verify answers
            rewards = torch.tensor([
                float(verifier(r)) for r in responses
            ], dtype=torch.float32, device=device)

            # GRPO loss
            log_prob_ratio = log_probs - log_probs.detach()
            ratio = torch.exp(log_prob_ratio)

            # Group relative baseline
            group_mean_reward = rewards.mean()
            relative_rewards = rewards - group_mean_reward

            clipped_ratio = torch.clamp(ratio, 0.5, 2.0)
            loss = -torch.min(
                log_prob_ratio * relative_rewards.unsqueeze(-1),
                torch.log(clipped_ratio) * relative_rewards.unsqueeze(-1)
            ).mean()

            optimizer.zero_grad()
            loss.backward()
            torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
            optimizer.step()

            total_loss += loss.item()
            num_batches += 1

        avg_loss = total_loss / num_batches
        avg_reward = rewards.mean().item()
        print(f"  Loss: {avg_loss:.4f}, Avg Reward: {avg_reward:.4f}")

    return model
```

## Practical Guidance

**When to use**: RL training on mathematical, reasoning, or verification tasks where many prompts reach near-perfect accuracy during training

**Hyperparameters**:
- **max_composition_depth**: 2-4 (deeper compositions harder but may become unsolvable)
- **curriculum_stages**: 2-3 (original → depth-2 → depth-3)
- **group_size**: 4-8 (GRPO grouping)
- **epoch_per_stage**: Spend 1-2 epochs per curriculum stage

**Key advantages**:
- Creates curriculum without new data collection
- Maintains learning signals when originals become easy
- Progressive difficulty scaling
- Works with existing verifiable prompt sets

**Common pitfalls**:
- Compositions becoming too hard (no correct solutions)
- Answer extraction failing on varied formats
- Not validating composed prompts before training
- Curriculum transitions too abrupt

**Scaling**: Composition is O(K²) for K prompts, but typically run offline. Validation can be parallelized.

## Reference

Paper: https://arxiv.org/abs/2602.12036
Related work: Curriculum learning, data augmentation, compositional generalization
Benchmarks: MATH, AIME, arithmetic reasoning
Code concepts: Sequential linking, problem parsing, curriculum management

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…