Version: 1.0 Last Updated: 2025-01-01
This document provides a researcher-grade description of the evaluation methodology used by HaystackBench. It covers the mathematical formulations, implementation details, known biases, and guidance for interpreting results.
The Needle in a Haystack (NIAH) family of tests was introduced by Gregory Kamradt (2023) and subsequently expanded by researchers at BAAI (NeedleBench, 2024). HaystackBench implements and extends this test family.
| Symbol | Definition |
|---|---|
| Context length in tokens | |
| Needle depth as a proportion in |
|
| Number of needles in M-RT/M-RS tasks | |
| Haystack document | |
| The |
|
| Query prompt | |
| Model response | |
| Scoring function; returns score in |
Given a haystack
- Compute insertion token position:
$p = \lfloor d \cdot L \rfloor$ - Find the nearest paragraph boundary
$p^*$ to$p$ (to avoid mid-sentence injection) - Construct injected document:
where
- The final document is truncated or padded to exactly
$L$ tokens
Boundary Enforcement: Injection always occurs at paragraph boundaries (double newline \n\n) to avoid syntactically malformed sentences that would make needle location artificially obvious.
Definition: A single synthetic fact
Experimental Grid:
where
Needle Template:
The [attribute] of [fictional_entity] is [synthetic_value].
Entity names are drawn from a synthetic vocabulary of obviously fictional planet names, chemical compounds, and proper nouns (e.g., "Zyranthex-7", "Project Heliodrome") to prevent model knowledge contamination.
Query: "What is the [attribute] of [fictional_entity]? Provide only the value."
Scoring: $$s_{\text{S-RT}}(r, n) = \begin{cases} 1.0 & \text{if normalize}(v_n) \in \text{normalize}(r) \ 0.0 & \text{otherwise} \end{cases}$$
where
Output: 2D heatmap where cell
Aggregate Score:
Definition:
Depth Distribution Strategies:
| Strategy | Formula | Depth axis |
|---|---|---|
| Uniform |
|
Collapsed to single value† |
| Centered | Needles span |
Swept (base depth varies) |
| Random |
|
Swept (new sample each cell) |
| Bookends |
|
Collapsed to single value† |
† Depth-sweep deduplication: For uniform and bookends, needle placement is identical regardless of which depth_percent value is the "base". Running the full depth grid would repeat the same test. HaystackBench therefore collapses the depth axis to a single representative value (0.5) when num_needles > 1 and the distribution is uniform or bookends. This reduces cell count by a factor equal to len(depth_percents) for these configurations. The centered and random strategies are unaffected — centered uses base_depth as the cluster centre (so the depth sweep is meaningful), and random produces a different layout each call.
Scoring (Partial Credit):
Individual needle scores: check whether value
Aggregate:
Reported as a function of
Definition:
Needle Generation (Numeric):
For
The [attribute] of [entity_k] is [v_k].
Reasoning Types:
| Type | Query | Ground Truth |
|---|---|---|
| Sum | "What is the total [attribute] across all entities?" | |
| Max | "Which entity has the highest [attribute]?" | |
| Mean | "What is the average [attribute]?" | |
| Conditional | "What is the [attr] of entities where [condition]?" | Filtered subset |
| Chain | "Entity A gives half its value to Entity B. What does B have?" |
Scoring: $$s_{\text{M-RS}}(r, \text{gt}) = \begin{cases} 1.0 & \text{if parse_numeric}(r) = \text{gt} \pm \epsilon \ s_{\text{LLM-judge}}(r, \text{gt}) & \text{for non-numeric} \ 0.0 & \text{otherwise} \end{cases}$$
where
Definition: The document contains only relational facts (no haystack filler). The model must perform multi-hop reasoning over a graph of relationships to answer a query.
Graph Construction:
Let
- Nodes
$V$ are named entities (fictional persons) - Edges
$E$ are first-order relationships (parent-of, reports-to, precedes, etc.)
Structure Types:
| Structure | Description | Example Query |
|---|---|---|
| Chain | Linear sequence: |
"Who is the great-grandparent of Z?" |
| Binary Tree | Complete binary tree of depth |
"Who are the siblings of Y?" |
| DAG | Directed acyclic graph with multiple paths | "Who is the common ancestor of X and Y?" |
Relationship Document Format:
[Person_A] is the parent of [Person_B].
[Person_B] is the parent of [Person_C].
...
Relationships are shuffled randomly within the document to prevent trivial sequential scanning.
Hop Depth: For chain tasks, hop depth
Scoring: $$s_{\text{ATC}}(r, v^) = \begin{cases} 1.0 & \text{if normalize}(v^) \in \text{normalize}(r) \ 0.0 & \text{otherwise} \end{cases}$$
where
Why No Haystack Filler? In ATC, all text is signal — there is no irrelevant background text. Context length is controlled by the number of relationships in the document. This tests reasoning over long documents rather than retrieval within noisy text.
Definition: A specific pattern (needle) appears exactly
Needle: Short, distinctive phrase (e.g., "The secret password is ALPHA-7").
Scoring: $$s_{\text{COUNT}}(r, k) = \begin{cases} 1.0 & \text{if parse_int}(r) = k \ \max\left(0, 1 - \frac{|parse_int}(r) - k|}{k}\right) & \text{partial credit} \end{cases}$$
Definition:
Document Format:
...filler...
{key_17}: {value_17}
...filler...
{key_42}: {value_42}
...
Keys are UUID-like identifiers (e.g., kv_7a3f2b) to prevent semantic shortcuts.
Scoring: Exact match on the value string.
This task directly measures attention capacity — as
Definition: Two contradictory facts are inserted at different depths. The model's answer reveals which it "believes."
Example:
- Needle A (at depth
$d_A$ ): "The boiling point of Xenarium is 412 degrees." - Needle B (at depth
$d_B$ ): "The boiling point of Xenarium is 891 degrees." - Query: "What is the boiling point of Xenarium?"
Scoring:
- Score = 0 or 1 depending on which answer the model gives
- Recorded as: chose_first (primacy) or chose_last (recency) or chose_neither (refusal)
Bias Measurement:
This reveals fundamental positional biases in the model's attention mechanism.
Definition: The correct needle is surrounded by semantically similar but incorrect "distractor" needles. Tests retrieval precision.
Example:
- Correct: "The mass of planet Voranthix-9 is 7.42 stellar units."
- Distractor 1: "The mass of planet Voranthix-8 is 3.17 stellar units."
- Distractor 2: "The mass of planet Voranthix-10 is 9.61 stellar units."
- Query: "What is the mass of planet Voranthix-9?"
Scoring: Exact match — must retrieve the value for the specific entity queried.
Needles use a three-part template that avoids real-world knowledge:
The [attribute] of [entity] is [value].
Entity Vocabulary: Generated by combining:
- Syllable patterns: (2-4 syllables from a phoneme inventory)
- Disambiguators: numeric suffixes, Greek letters, cardinal directions
Examples: Zyranthex-7, Project Heliodrome, Compound Delta-Krylos, Station Omicron-IV
Attribute Vocabulary: Domain-specific but unfamiliar:
orbital period,resonance frequency,thermal coefficient,migration index
Value Vocabulary:
- Numeric: integer or float in a specified range
- String: short alphanumeric codes (e.g.,
QR-7749,BETA-PRIME)
Uniqueness Guarantee: For M-RT runs, all
Users provide a Jinja2 template:
The secret passcode for {{ entity }} is {{ code }}.
With variable generators:
entity:
type: wordlist
values: [Vault A, Chamber B, Section C]
code:
type: random_hex
length: 8Users can define their own needle/expected-answer pairs in the Settings UI. These are stored in a persistent library and can be selected per-experiment from the wizard.
When custom needles are selected:
- The full
(needle_text, expected_answer, question)tuples are embedded into the experiment config at creation time, making the experiment self-contained - If
num_needles > 1, the pool is cycled cyclically across cells (e.g., 2 custom needles used for a 3-needle cell: needle 0, needle 1, needle 0) - Each cell's starting position in the pool is seeded from
(experiment_id, model, trial), keeping the needle assignment consistent across (context_length × depth) for a given trial
Custom needles are suited for domain-specific evaluation (e.g., medical codes, legal citations, product IDs) where synthetic abstract needles may not reflect real retrieval difficulty.
Generated algorithmically from a graph structure:
for (parent, child) in G.edges():
yield f"{parent} is the parent of {child}."Entity names are generated to be:
- Clearly human names (to suggest a social/kinship graph)
- Phonetically distinct (to avoid confusion between similar names)
- From a closed set for a given experiment (no external knowledge needed)
The original Kamradt NIAH uses a corpus of Paul Graham essays as the background "haystack." This text:
- Is freely available
- Has uniform style (minimal variation in prose register)
- Contains rich vocabulary that could plausibly contain any synthetic fact
- Is well-studied as a benchmark baseline
Source: Public HTML from paulgraham.com, concatenated and cleaned.
Padding Strategy: When the required length exceeds available text, the corpus is repeated (with a separator) until the target length is reached.
Random Wikipedia articles provide domain-diverse haystacks. For reproducibility, experiments should specify:
- Language:
en - Random seed: ensures same articles are fetched across runs
- Article count: number of articles concatenated
Computer science and ML paper abstracts create a technical prose haystack appropriate for ML benchmarking.
For complete control, synthetic paragraphs can be generated. These are useful for:
- Domain-specific benchmarking (legal, medical)
- Ablation studies on haystack type
All token counts use tiktoken with the cl100k_base vocabulary (OpenAI's standard) as a universal reference. This is an approximation for non-OpenAI models but provides consistent cross-provider comparison.
For billing-accurate estimates, providers' native tokenizers are invoked when available.
normalize(v) = v.strip().lower()
score = 1.0 if normalize(expected) in normalize(response) else 0.0
Used for: S-RT, M-RT, KV retrieval, ATC
The expected value appears as a substring of the response (after normalization). More lenient than exact match.
Using sentence-transformers (all-MiniLM-L6-v2 by default):
where
Threshold:
A separate model (default: cheapest available, e.g., claude-3-5-haiku) evaluates correctness:
Judge Prompt:
You are a strict fact-checking judge.
Ground truth answer: {expected}
Model response: {response}
Does the model's response correctly contain the ground truth answer?
Answer with only "YES" or "NO" followed by a one-sentence explanation.
Parsed: "YES" → score = 1.0, "NO" → score = 0.0.
parse_numeric(response) → float
score = 1.0 if |parsed - expected| / max(|expected|, 1) < 0.01 else 0.0
| Task | Default Scorer | Notes |
|---|---|---|
| S-RT | auto | Auto-selects: numeric (1% relative tolerance) for float/int values, substring otherwise. Handles model rounding correctly. |
| M-RT | substring (per needle) | Applied to each needle independently |
| M-RS | numeric → LLM judge | Numeric first, fall back to LLM judge |
| ATC | exact | Entity names are unambiguous |
| Counting | numeric | Must parse integer from response |
| KV | exact | Values are designed to be exact |
| Conflicting | categorical | Parse which value was chosen |
| Distractor | exact | Must get precisely the right entity's value |
The auto method (used by S-RT) calls _parse_numeric() on the expected answer; if it parses as a float, the response is scored numerically with 1% relative tolerance. This correctly handles cases where a model responds with a rounded value (e.g., expected 732.57, response 733) or adds units (732.57 M⁻¹).
LLMs exhibit well-documented positional biases:
- Lost in the Middle (Liu et al., 2023): Models perform worse when relevant information is in the middle of a long context
- Recency Bias: Models tend to preferentially recall information from the end of the context
- Primacy Bias: Models sometimes preferentially recall the beginning
HaystackBench's heatmaps directly visualize these biases.
The Paul Graham haystack is public text that models may have seen during training. This could create "familiar text" effects where known passages make retrieval easier or harder. Using synthetic haystacks eliminates this concern.
Using tiktoken as a universal token counter introduces slight inaccuracies for non-OpenAI models (especially for non-English text). The context_length axis should be interpreted as "approximate."
If synthetic needle templates are published, future models trained on internet text could memorize them. Users running long-term evaluations should rotate templates.
With trials_per_cell=1, variance from model stochasticity (even at temperature=0, many providers use non-zero temperature by default) is not captured. For publication, use trials_per_cell≥3.
If a model fails a retrieval task, it may be due to:
- Failing to retrieve the needle (true context failure)
- Formatting errors (retrieved correctly but in wrong format)
- Instruction non-compliance
LLM judge scoring mitigates case 2 and 3 but adds cost.
Context Length (tokens) →
4K 8K 16K 32K 64K 128K
┌──────┬──────┬──────┬──────┬──────┬──────┐ 0% (top)
│ 1.00 │ 1.00 │ 1.00 │ 0.90 │ 0.80 │ 0.60 │
├──────┼──────┼──────┼──────┼──────┼──────┤ 20%
│ 1.00 │ 1.00 │ 0.90 │ 0.70 │ 0.50 │ 0.30 │
├──────┼──────┼──────┼──────┼──────┼──────┤ 40%
│ 1.00 │ 0.90 │ 0.70 │ 0.50 │ 0.30 │ 0.10 │ ← "Lost in the Middle"
├──────┼──────┼──────┼──────┼──────┼──────┤ 60%
│ 1.00 │ 1.00 │ 0.90 │ 0.70 │ 0.50 │ 0.30 │
├──────┼──────┼──────┼──────┼──────┼──────┤ 80%
│ 1.00 │ 1.00 │ 1.00 │ 0.90 │ 0.80 │ 0.70 │
└──────┴──────┴──────┴──────┴──────┴──────┘ 100% (bottom)
Patterns to look for:
| Pattern | Visual signature | Interpretation |
|---|---|---|
| "Lost in the Middle" | Dark horizontal band in center | Model struggles with mid-context information |
| "Context Cliff" | Green columns → red columns at some length | Hard cutoff in effective context use |
| "End Advantage" | Bottom rows brighter than top | Recency bias |
| "Diagonal Fade" | Score decreases moving right-down | Combined depth and length degradation |
| "Perfect Square" | All cells green | Model handles task at all settings |
| Score | Interpretation |
|---|---|
| 0.95–1.00 | Excellent: effectively uses full context |
| 0.80–0.95 | Good: minor degradation at extreme lengths/depths |
| 0.60–0.80 | Fair: noticeable degradation; specific weaknesses exist |
| 0.40–0.60 | Poor: significant context limitations |
| < 0.40 | Failing: claimed context length not functionally available |
The radar chart shows performance across all task types simultaneously. "Balanced" models show a roughly circular profile. Models optimized for one task type may show a "spike" pattern.
Similarities: Same basic S-RT concept; Paul Graham haystack; depth × context grid.
Differences:
- Kamradt: ad-hoc script, no M-RT/M-RS/ATC, limited provider support
- HaystackBench: full framework, 8 task types, web UI, comprehensive scoring, export formats
Similarities: Multi-needle variants; ATC; systematic evaluation.
Differences:
- NeedleBench: research codebase, no UI, limited provider abstraction
- HaystackBench: production-grade tool, extensible provider system, web UI, CLI
RULER includes: NIAH variants, variable tracking, common/rare word aggregation, and question answering.
Differences:
- RULER: academic research tool focused on specific failure modes
- HaystackBench: practitioner-focused, configurable, multi-provider, with UI
LongBench tests real-world long-document tasks (summarization, QA, etc.).
Differences: HaystackBench tests synthetic controlled tasks to isolate context utilization from other capabilities. The two tools are complementary.
For publication-quality results, ensure:
-
trials_per_cell ≥ 3to capture variance -
temperature=0.0for deterministic responses (where supported) - Record exact model version (not just model name)
- Record exact haystack source and version
- Record exact needle templates used
- Export full JSON results (not just summary scores)
- Record haystackbench version:
haystackbench --version - Include the experiment YAML config in supplementary materials
If you use HaystackBench in research, please cite:
@software{haystackbench2025,
title = {HaystackBench: A Comprehensive NIAH Evaluation Framework for Large Language Models},
year = {2025},
url = {https://github.com/marmutapp/needlehaystack},
note = {Open-source software}
}-
Kamradt, G. (2023). Needle In A Haystack - Pressure Testing LLMs. GitHub. https://github.com/gkamradt/LLMTest_NeedleInAHaystack
-
Li, M., et al. (2024). NeedleBench: Can LLMs Do Retrieval and Reasoning in 1 Million Context Window? BAAI. arXiv:2407.11963.
-
Hsieh, C., et al. (2024). RULER: What's the Real Context Size of Your LLM? arXiv:2404.06654.
-
Bai, Y., et al. (2023). LongBench: A Bilingual, Multitask Benchmark for Long Context Understanding. arXiv:2308.14508.
-
Liu, N., et al. (2023). Lost in the Middle: How Language Models Use Long Contexts. arXiv:2307.03172.