# Truncated Step Level Retrieval Reasoning

> Improve credit assignment in retrieval-augmented reasoning by truncating trajectories at single decision points. Generate k samples sharing a common prefix, differing only at the next step to isolate variation and reduce gradient variance by T-fold on T-step tasks.

- Skill: `adu2021/truncated-step-level-retrieval-reasoning` (Agent Skill)
- Install (CLI): `npx skillmds@latest add adu2021/truncated-step-level-retrieval-reasoning`
- Raw SKILL.md: https://api.skillmd.com/api/skills/adu2021/truncated-step-level-retrieval-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/truncated-step-level-retrieval-reasoning

---


# Truncated Step-Level Sampling for Retrieval-Augmented Reasoning

Standard reinforcement learning for retrieval-augmented generation (RAG) tasks suffers from poor credit assignment: high-variance gradient estimates make it hard to distinguish which search query or reasoning step improved final outcomes. Truncated step-level sampling fixes this by generating trajectories that share a prefix and diverge only at a single decision point. This isolation dramatically reduces variance while enabling per-step reward supervision through dense LLM-as-Judge scoring.

The core innovation treats each reasoning step as an isolated decision problem. By fixing all prior steps, the method achieves T-fold variance reduction on T-step tasks while maintaining gradient accuracy through reward decomposition.

## Core Concept

Truncated sampling operates through two coordinated mechanisms:

1. **Prefix-Sharing Trajectories**: Generate k candidate continuations from identical prefixes, isolating variation to a single step. This eliminates confounding from earlier decisions.

2. **Dense Step-Level Rewards**: Score each step independently via LLM judge on three dimensions:
   - Reasoning quality (is the thinking sound?)
   - Query quality (is the search formulation effective?)
   - Answer correctness (does the final answer match?)

These two components combine to provide stable, interpretable learning signals.

## Architecture Overview

- **Input**: Multi-hop retrieval task (question requiring multiple document lookups)
- **Prefix Generation**: Execute steps 1 to t-1 deterministically or sample once, reuse across batch
- **Branching**: Generate k different step-t candidates from shared prefix
- **Evaluation**: LLM judges each step t independently on three dimensions
- **Reward Aggregation**: Combine three reward signals (reasoning + query + answer)
- **Policy Update**: Gradient step on isolated step with reduced variance
- **Output**: Improved retrieval and reasoning policy

## Implementation Steps

**Step 1: Structure multi-hop reasoning task with explicit steps**

Define the reasoning process to enable step-level isolation.

```python
def decompose_rag_task(question):
    """
    Decompose RAG into explicit steps: search → retrieve → reason → search → answer.
    Each step is atomic and independently scorable.
    """
    steps = [
        {'step': 0, 'action': 'formulate_initial_query', 'state': question},
        {'step': 1, 'action': 'retrieve_documents', 'state': None},
        {'step': 2, 'action': 'synthesize_reasoning', 'state': None},
        {'step': 3, 'action': 'formulate_followup_query', 'state': None},
        {'step': 4, 'action': 'retrieve_evidence', 'state': None},
        {'step': 5, 'action': 'generate_final_answer', 'state': None}
    ]

    return steps

# Example: "Who was the first person to reach the South Pole?"
# Step 0: User question
# Step 1: Search "first person South Pole" → retrieve docs about Amundsen, Scott
# Step 2: Synthesize: "Roald Amundsen reached it in 1911"
# Step 3: Formulate follow-up: "Roald Amundsen nationality" (verify credibility)
# Step 4: Retrieve clarification docs
# Step 5: Generate final answer with sources
```

**Step 2: Generate prefix and sample k step-t candidates**

Execute deterministically up to step t-1, then generate k diverse continuations for step t.

```python
def generate_truncated_batch(model, question, num_candidates=4, target_step=2):
    """
    Generate batch of trajectories sharing prefix, differing only at target_step.

    Example with target_step=2:
    - Execute steps 0, 1 once (deterministic or single sample)
    - Generate k candidates for step 2
    - Execute steps 3+ identically for all k candidates
    """
    steps = decompose_rag_task(question)

    # Phase 1: Execute steps before target_step deterministically
    prefix_state = question
    for step_idx in range(target_step):
        step = steps[step_idx]
        if step_idx == 0:
            prefix_state = generate_initial_query(model, question)
        elif step_idx == 1:
            documents = retrieve_documents(prefix_state)
            prefix_state = documents
        # ... continue deterministically

    # Phase 2: Generate k candidates for step target_step
    candidates = []
    for _ in range(num_candidates):
        candidate_action = model.generate(
            f"Given context: {prefix_state}, generate next reasoning step:",
            max_tokens=150,
            temperature=0.7,
            top_p=0.9
        )
        candidates.append(candidate_action)

    # Phase 3: Continuation after target_step (reuse for all candidates)
    trajectories = []
    for candidate in candidates:
        # Forward pass through remaining steps
        trajectory = {
            'prefix': prefix_state,
            'step_t_action': candidate,
            'target_step': target_step,
            'full_trajectory': None
        }

        # Execute remaining steps using this candidate
        current_state = candidate
        for step_idx in range(target_step + 1, len(steps)):
            current_state = execute_step(model, steps[step_idx], current_state)

        trajectory['full_trajectory'] = current_state
        trajectories.append(trajectory)

    return trajectories
```

**Step 3: Compute per-step LLM-as-Judge rewards**

Score each step independently on reasoning quality, query quality, and answer correctness.

```python
def compute_three_part_reward(trajectory, question, reference_answer,
                             judge_model):
    """
    Decompose reward into three scoreable components:
    1. Reasoning quality: Is the thinking sound?
    2. Query quality: Is the search formulation likely to retrieve relevant docs?
    3. Answer correctness: Does final answer match reference?
    """
    target_step = trajectory['target_step']
    step_action = trajectory['step_t_action']
    full_trajectory = trajectory['full_trajectory']

    # Component 1: Reasoning quality (LLM judge)
    reasoning_prompt = f"""
Evaluate the reasoning quality of this step:
Question: {question}
Prefix context: {trajectory['prefix'][:100]}
Reasoning step: {step_action}

Rate on scale 0-1: Is the logic sound? Does it make progress toward answer?
"""

    reasoning_score = judge_model.score(
        reasoning_prompt,
        return_numeric=True  # 0-1 score
    )

    # Component 2: Query quality (relevance of search formulation)
    if 'query' in step_action.lower() or 'search' in step_action.lower():
        query_prompt = f"""
Evaluate query formulation quality:
Original question: {question}
Formulated query: {step_action}

Rate 0-1: Will this retrieve relevant documents?
"""

        query_score = judge_model.score(query_prompt, return_numeric=True)
    else:
        query_score = 0.5  # Neutral if not a query step

    # Component 3: Answer correctness (binary: correct/incorrect)
    final_answer = full_trajectory.split('\n')[-1]  # Last line of trajectory
    is_correct = final_answer.lower() in reference_answer.lower()
    answer_score = 1.0 if is_correct else 0.0

    # Combine components (equal weighting)
    total_reward = (reasoning_score + query_score + answer_score) / 3.0

    return {
        'reasoning_score': reasoning_score,
        'query_score': query_score,
        'answer_score': answer_score,
        'total_reward': total_reward
    }

# Evaluate batch
batch = generate_truncated_batch(model, question, num_candidates=4, target_step=2)
rewards = [compute_three_part_reward(traj, question, reference_answer, judge)
           for traj in batch]
```

**Step 4: Compute policy gradients with variance reduction**

Calculate gradients using isolated step rewards and prove T-fold variance reduction.

```python
def compute_pg_loss_truncated(model, batch, rewards, target_step):
    """
    Policy gradient loss with variance reduction from truncation.

    Key insight: By fixing prefix and only varying step t,
    var(gradient) is reduced by ~T-fold relative to full-trajectory sampling.
    """
    total_loss = 0.0

    for trajectory, reward in zip(batch, rewards):
        # Get log probability of the step-t action under current policy
        step_action = trajectory['step_t_action']
        prefix_context = trajectory['prefix']

        logits = model.forward(
            f"Context: {prefix_context}\nAction:",
            output_logits=True
        )

        # Log probability of sampled action
        action_logprob = compute_logprob(logits, step_action, model.tokenizer)

        # Policy gradient: maximize log-prob of high-reward actions
        # Loss = -logprob * reward (negative for gradient descent)
        step_loss = -action_logprob * reward['total_reward']

        total_loss += step_loss

    return total_loss / len(batch)

def analyze_variance_reduction(trajectories_full, trajectories_truncated):
    """
    Empirically verify T-fold variance reduction.
    trajectories_full: full-trajectory samples from baseline
    trajectories_truncated: truncated samples from this method
    """
    # Compute gradient variance
    grads_full = [compute_gradient(traj) for traj in trajectories_full]
    grads_truncated = [compute_gradient(traj) for traj in trajectories_truncated]

    var_full = np.var([g.norm().item() for g in grads_full])
    var_truncated = np.var([g.norm().item() for g in grads_truncated])

    # Expected T-fold reduction
    T = 5  # Assume 5-step tasks
    expected_ratio = T
    observed_ratio = var_full / var_truncated

    print(f"Variance reduction: {observed_ratio:.1f}x (expected ~{expected_ratio}x)")
    return observed_ratio
```

**Step 5: RL training loop with step-level updates**

Integrate truncated sampling into standard policy gradient training.

```python
def train_rag_with_truncated_sampling(model, judge_model, train_tasks,
                                     num_steps=10000, batch_size=32):
    """
    Train retrieval-augmented reasoning with truncated step-level sampling.
    """
    optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)

    for step in range(num_steps):
        # Sample tasks from training set
        task_batch = random.sample(train_tasks, batch_size)

        total_loss = 0.0

        for question, reference_answer in task_batch:
            # Randomly select which step to optimize
            target_step = random.randint(1, 4)

            # Generate truncated batch (k candidates for step target_step)
            trajectories = generate_truncated_batch(
                model,
                question,
                num_candidates=4,
                target_step=target_step
            )

            # Compute rewards
            rewards = [
                compute_three_part_reward(traj, question, reference_answer, judge_model)
                for traj in trajectories
            ]

            # Compute loss
            loss = compute_pg_loss_truncated(model, trajectories, rewards, target_step)

            total_loss += loss

        # Gradient step
        optimizer.zero_grad()
        (total_loss / len(task_batch)).backward()
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
        optimizer.step()

        if (step + 1) % 100 == 0:
            print(f"Step {step + 1}: Loss = {(total_loss / len(task_batch)).item():.4f}")

    return model
```

**Step 6: Evaluation on multi-hop QA benchmarks**

Benchmark on tasks requiring multiple retrieval steps (TREC, HotpotQA, etc.).

```python
def evaluate_on_multihop_qa(model, eval_tasks):
    """
    Evaluate on multi-hop QA: questions requiring 2-4 retrieval steps.
    """
    correct = 0
    total = 0

    for question, reference_answer, num_hops in eval_tasks:
        # Execute full trajectory
        trajectory = execute_full_rag(model, question, max_steps=num_hops)

        # Extract final answer
        final_answer = trajectory.split('\n')[-1]

        # Check correctness
        if final_answer.lower() in reference_answer.lower():
            correct += 1

        total += 1

    accuracy = correct / total
    print(f"Multi-hop QA accuracy: {accuracy * 100:.1f}%")

    return accuracy
```

## Practical Guidance

**Hyperparameter Selection:**
- **Number of candidates k**: 2-4. More candidates = better coverage; diminishing returns beyond 4.
- **Target step selection**: Sample uniformly from [1, T] for balanced training, or focus on early steps for faster learning.
- **Reward weights**: Equal [1/3, 1/3, 1/3] standard; can emphasize answer_score for final tuning.
- **Judge model temperature**: 0.5-0.7 for stable scoring; lower = more consistent, higher = more nuanced.

**When to Use:**
- Multi-hop retrieval tasks (2-5 steps) where single-step methods fail
- Scenarios where every reasoning step affects downstream success
- Settings with access to LLM judge for step-level scoring
- Tasks where variance reduction significantly improves convergence

**When NOT to Use:**
- Single-hop retrieval (no multi-step credit assignment benefit)
- Tasks with very long trajectories (T > 10); variance reduction diminishes
- Real-time systems where multiple forward passes are expensive
- Scenarios without reliable reference answers for reward feedback

**Common Pitfalls:**
- **Inconsistent judge scoring**: If judge is non-deterministic, rewards are noisy. Use deterministic judge or averaging over multiple runs.
- **Step isolation breaks dependencies**: Some steps may be impossible without specific prefix context. Validate that sampled candidates are valid.
- **Reward signal too sparse**: If most samples have zero answer_score, provide auxiliary rewards (query quality, reasoning soundness) to maintain signal.
- **Target step bias**: If always sampling early steps, later steps never get optimized. Ensure uniform or curriculum-based step selection.

## Reference

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

