L’interface est localisée ; les présentations et documents des Skills sont traduits automatiquement par IA dans votre langue. Le code, les commandes, chemins et termes techniques restent en version originale, et le document de l’auteur fait foi.
Explorer les Skills
Développement · SKILL.md

rank-gpt

rank-gpt

LLM-as-reranker patterns. RankGPT listwise / pairwise / pointwise prompting, sliding window for long candidate lists, cost-latency vs Cohere Rerank, calibration tricks, structured output for rank lists. Code uses the Anthropic SDK. USE WHEN: user mentions "LLM reranker", "RankGPT", "listwise reranking", "pairwise reranking", "pointwise reranking", "GPT rer…

Independent GitHub snapshot · Automated classification · Original documentation governs

Documentation de la Skill

GitHub

name: rank-gpt description: | LLM-as-reranker patterns. RankGPT listwise / pairwise / pointwise prompting, sliding window for long candidate lists, cost-latency vs Cohere Rerank, calibration tricks, structured output for rank lists. Code uses the Anthropic SDK.

USE WHEN: user mentions "LLM reranker", "RankGPT", "listwise reranking", "pairwise reranking", "pointwise reranking", "GPT reranker", "Claude reranker"

DO NOT USE FOR: standard cross-encoder reranking - use rag/reranking; fine-tuning cross-encoders - use retrieval/cross-encoder-training; ColBERT - use retrieval/colbert-retrieval allowed-tools: Read, Grep, Glob, Write, Edit

RankGPT: LLM-as-Reranker

Why and When

LLM rerankers win when:

  • The query contains instructions ("prefer recent docs", "exclude deprecated APIs").
  • The corpus is too niche for pretrained rerankers (internal jargon, proprietary ontology).
  • The system needs explanations alongside rankings.
  • You have very few labeled examples — an LLM needs zero.

They lose on:

  • Latency: 300-2000 ms vs 50-300 ms for cross-encoders.
  • Cost: $3-30 per 1k queries vs <$2 for Cohere Rerank.
  • Throughput: rate limits cap QPS without batching.

Three Prompting Styles

StylePrompt shapeCostQuality
PointwiseScore each doc independently (0-10)O(n) calls, cheapestMid
PairwiseAsk which of two docs is betterO(n log n) calls, priciestHigh but slow
ListwiseRank the whole batch in one callOne call per windowBest quality-to-cost

Listwise is the RankGPT default.

Listwise Reranking with Claude

# pip install anthropic
from anthropic import Anthropic
import json, re

client = Anthropic()
MODEL = "claude-haiku-4-5-20250929"  # cheap, fast; upgrade to sonnet for hard queries

LISTWISE_PROMPT = """You rank passages by relevance to a user query.

Query: {query}

Passages (each starts with its index in brackets):
{passages}

Return a JSON object with a single field "ranking": an array of passage indexes
ordered from most to least relevant. Include every index exactly once.
No other text, no explanations."""

def format_passages(docs: list[str]) -> str:
    return "\n\n".join(f"[{i}] {d}" for i, d in enumerate(docs))

def listwise_rank(query: str, docs: list[str]) -> list[int]:
    msg = client.messages.create(
        model=MODEL,
        max_tokens=1024,
        messages=[{
            "role": "user",
            "content": LISTWISE_PROMPT.format(query=query, passages=format_passages(docs)),
        }],
    )
    text = msg.content[0].text
    match = re.search(r"\{[\s\S]*\}", text)
    data = json.loads(match.group(0))
    ranking = data["ranking"]
    # Safety: fill missing indexes at the end
    seen = set(ranking)
    missing = [i for i in range(len(docs)) if i not in seen]
    return ranking + missing

Sliding Window for Long Candidate Lists

Claude handles > 100 passages in one prompt but prompt length grows quickly. The RankGPT paper uses a sliding window:

[0..19] rank -> keep top 10
[10..29] rank (seeded with previous top 10) -> keep top 10
[20..39] ...
def sliding_rerank(query: str, docs: list[str], window: int = 20,
                   step: int = 10, keep: int = 10) -> list[int]:
    n = len(docs)
    if n <= window:
        return listwise_rank(query, docs)[:keep]

    # Start from the end so the best candidates bubble to position 0
    order = list(range(n))
    start = n - window
    while start >= 0:
        end = start + window
        slice_docs = [docs[i] for i in order[start:end]]
        local = listwise_rank(query, slice_docs)
        order[start:end] = [order[start + idx] for idx in local]
        start -= step
    return order[:keep]

End-to-start scan is the trick: the strongest document reaches the front of the list as windows slide back.

Pairwise (Highest Quality, Most Expensive)

Pairwise calls scale as O(n log n) with merge-sort style comparisons:

PAIRWISE_PROMPT = """Given the query, which passage is more relevant? Answer with "A" or "B" only.

Query: {query}

Passage A:
{a}

Passage B:
{b}

Answer:"""

def pairwise_compare(query: str, a: str, b: str) -> int:
    msg = client.messages.create(
        model=MODEL,
        max_tokens=5,
        messages=[{"role": "user",
                   "content": PAIRWISE_PROMPT.format(query=query, a=a, b=b)}],
    )
    return -1 if msg.content[0].text.strip().upper().startswith("A") else 1

import functools
def merge_sort_rerank(query: str, docs: list[str]) -> list[int]:
    idx = list(range(len(docs)))
    def cmp(i, j):
        return pairwise_compare(query, docs[i], docs[j])
    return sorted(idx, key=functools.cmp_to_key(cmp))

Avoid naive bubble-sort (O(n^2) calls). Use merge-sort or tournament bracket.

Pointwise (Cheapest, Most Parallel)

import asyncio
from anthropic import AsyncAnthropic
aclient = AsyncAnthropic()

POINTWISE_PROMPT = """Rate the relevance of the passage to the query from 0 to 10.
Return a JSON object: {{"score": <number>}}

Query: {query}

Passage: {passage}"""

async def point_score(query: str, passage: str) -> float:
    msg = await aclient.messages.create(
        model=MODEL, max_tokens=40,
        messages=[{"role": "user",
                   "content": POINTWISE_PROMPT.format(query=query, passage=passage)}],
    )
    try:
        return float(json.loads(re.search(r"\{[^}]*\}", msg.content[0].text).group(0))["score"])
    except Exception:
        return 0.0

async def pointwise_rerank(query: str, docs: list[str], top_n: int = 5) -> list[int]:
    scores = await asyncio.gather(*(point_score(query, d) for d in docs))
    return sorted(range(len(docs)), key=lambda i: scores[i], reverse=True)[:top_n]

Run calls concurrently with a semaphore to respect rate limits.

Structured Output for Robustness

Anthropic tool-use forces a JSON shape — no regex needed:

tools = [{
    "name": "submit_ranking",
    "description": "Submit the ranking of passage indexes from most to least relevant.",
    "input_schema": {
        "type": "object",
        "properties": {
            "ranking": {
                "type": "array",
                "items": {"type": "integer"},
                "description": "Passage indexes in descending relevance order",
            },
        },
        "required": ["ranking"],
    },
}]

def listwise_structured(query: str, docs: list[str]) -> list[int]:
    msg = client.messages.create(
        model=MODEL, max_tokens=1024, tools=tools, tool_choice={"type": "tool", "name": "submit_ranking"},
        messages=[{"role": "user",
                   "content": LISTWISE_PROMPT.format(query=query, passages=format_passages(docs))}],
    )
    for block in msg.content:
        if block.type == "tool_use" and block.name == "submit_ranking":
            return block.input["ranking"]
    raise ValueError("No ranking returned")

Always prefer tool-use over regex JSON extraction in production.

Calibration: Force Consistency

LLMs are sensitive to passage order inside the prompt. Two tricks:

  1. Shuffle input order each call; rerank yields a relative order, not absolute.
  2. Run two passes with different initial orders; fuse with RRF.
import random

def robust_listwise(query: str, docs: list[str], passes: int = 3) -> list[int]:
    from collections import defaultdict
    scores = defaultdict(float)
    for _ in range(passes):
        perm = list(range(len(docs)))
        random.shuffle(perm)
        shuffled = [docs[i] for i in perm]
        ranking = listwise_structured(query, shuffled)
        for rank, local_idx in enumerate(ranking):
            real_idx = perm[local_idx]
            scores[real_idx] += 1.0 / (60 + rank + 1)  # RRF
    return sorted(scores, key=scores.get, reverse=True)

Cost and Latency vs Cohere Rerank

RerankerCandidatesLatencyCost / 1k queries
Cohere Rerank v3.550100-300 ms~$2.00
Claude Haiku listwise20300-700 ms~$3-5
Claude Haiku listwise sliding (50)50600-1500 ms~$8-15
Claude Sonnet listwise20500-1200 ms~$15-25
Claude Haiku pointwise async50200-600 ms~$5-8
Claude Haiku pairwise merge-sort20800-2000 ms~$10-20

Pick Haiku listwise with sliding window as the default LLM-reranker. Upgrade to Sonnet only for hard queries.

Prompt Caching for Cost Control

The query + passages prompt has a large fixed header (instructions, passages rarely change across a user session). Cache it via Anthropic's prompt caching:

msg = client.messages.create(
    model=MODEL, max_tokens=1024,
    system=[
        {"type": "text", "text": "You rank passages by relevance...",
         "cache_control": {"type": "ephemeral"}},
    ],
    messages=[{"role": "user", "content": LISTWISE_PROMPT.format(...)}],
)

Cache hits cut cost ~90% and latency ~30-50% for repeat calls within 5 minutes.

When to Fall Back

Always wrap LLM reranking with a timeout and fallback:

import asyncio

async def rerank_with_fallback(query, docs, top_n=5, timeout_s=2.0):
    try:
        return await asyncio.wait_for(
            pointwise_rerank(query, docs, top_n), timeout=timeout_s
        )
    except (asyncio.TimeoutError, Exception):
        return list(range(top_n))  # retrieval order

LLM rate limits or transient errors must not break the user-facing query path.

Anti-Patterns

Anti-PatternFix
Using Sonnet or Opus for rerankingHaiku is 90% as good at 10% the cost
Listwise with 100 docs in one promptUse sliding window; attention degrades
Parsing JSON with regex aloneUse Anthropic tool-use for structured output
No timeout on reranker callHard timeout (1-2 s) with retrieval-order fallback
Shuffling order but not aggregating multiple passesRun 3 passes and RRF — single shuffled pass is still biased
LLM rerank on every query in a high-QPS pathCache by (query, candidate-set-hash); or rerank only low-confidence retrievals
Ignoring rate limitsRespect Tier 1-4 caps; batch async calls under a semaphore
Missing passage indexes in the outputAlways fill missing indexes at the tail before consuming

Production Checklist

  • Listwise + sliding window default; pairwise only for high-stakes queries
  • Claude Haiku as default; Sonnet as opt-in for hard queries
  • Tool-use for structured ranking output (no regex fallback)
  • Prompt caching enabled on system prompt
  • Hard timeout with retrieval-order fallback
  • Rate-limit-aware semaphore around async calls
  • Rerank quality A/B tested against Cohere Rerank monthly
  • Cost per query dashboard (input + cached + output tokens)
  • Multi-pass shuffle + RRF for stability on sensitive queries
  • Defensive missing-index handling in the parser
Skills enregistrées