Skip to content
jimmyjames177414Public

About

Measure which words your prompt actually needs. Learned stopwords and anti-stopwords, by real ablation rather than proxy scores.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

jimmyjames177414

stopless

Measure which parts of a prompt your model actually needs, by deleting them and scoring what breaks.

CI Python License

In 2020 I compiled one of the most-copied "complete English stopword lists" on GitHub: 11,017 characters of words to throw away, posted as a comment on a stopwords gist that has drawn 64 forks and 78 comments since 2010, and cited in published research since. Lists like that delete not, no, without, unless and except first. Plausibly the highest-leverage tokens in the whole prompt. Stopless is me asking whether that list was ever a good idea for an LLM, and measuring the answer.

Stopless calls a word that a classical list would delete but that your model measurably needs an anti-stopword. Finding them is an intersection you can compute: elements whose removal demonstrably hurts, and which a named stopword list would have thrown away on sight.

Show me it working

No API key and no network. The demo replays recorded responses, and runs from a fresh clone:

git clone https://github.com/jimmyjames177414/stopless && cd stopless

uv run stopless analyze examples/negation_qa/prompts.jsonl \
  --cassette examples/negation_qa/cassette.json \
  --words "not,without,no,never,only,please,actually,generally" \
  --classes article --repeats 5

stopless finding anti-stopwords

⚠️ Everything in this block is synthetic

The corpus is 120 invented yes/no questions about invented nouns (examples/negation_qa/build_corpus.py generates it). The responses were recorded from Stopless's own bundled MockProvider: a deterministic toy model, not a language model. The effect sizes are large because that toy model is negation-sensitive by construction. This output demonstrates that the measurement pipeline works end to end. It says nothing whatsoever about how any real model behaves.

  element                  effect  95% CI                tok Δ     n
  ----------------------------------------------------------------

ANTI-STOPWORDS  (on the reference stopword list, but removal hurts)
  no                       -95.0pp  [-100.0, -90.0]         ~-1    12
  not                      -94.5pp  [-98.0, -90.5]          ~-1    40

MUST KEEP  (removal hurts; not on the reference stopword list)
  never                    -96.7pp  [-100.0, -91.7]         ~-2    12
  without                  -94.2pp  [-98.3, -89.2]          ~-2    24

UNDERPOWERED / TASK-DEPENDENT  (widen the run before believing anything)
  only                      -5.0pp  [-15.0, +0.0]           ~-1     4
  actually                  +0.0pp  [-3.3, +3.3]            ~-3    30
  please                    +0.7pp  [-2.7, +4.7]            ~-3    30
  generally                 +0.7pp  [-2.0, +3.3]            ~-4    30

LOW INFORMATION  (small effect, bounded)
  the                       +0.3pp  [-2.0, +3.0]            ~-1    60
  a                         +0.5pp  [-1.2, +2.2]            ~-4   120

  not present in this corpus: an

  model mock-negation-qa:v1 · 120 items · 5 repeats · 2,410 model calls · median observed MDE 5.7pp
  control accuracy 97.5% · scorer exact_match:v1 · ablation delete · scope affected · stochastic
  95% percentile bootstrap over items (10,000 resamples) · p-values sign-flip permutation, Holm across 10 element(s)
  anti-stopword band = must-keep ∩ en_nltk (179 words) · prompt tokens 83,645 (estimated)
  replayed from cassette.json (recorded by examples/negation_qa/build_corpus.py (stopless 0.1.0); responses came from a synthetic/mock provider)

The call count is a cold cache. Run it a second time and the last block reads 0 model calls, because responses are cached under ~/.cache/stopless/; the bands, effects and intervals are unchanged.

Four things in that table are worth pointing at.

not and no are anti-stopwords; without and never are not. The measurement is identical: all four are catastrophic to remove. The difference is that not and no are on NLTK's English stopword list and without and never are not, so only the first pair can reach the anti-stopword band at all. The band is an intersection with a named list, and the report names it. A larger list (scikit-learn's 318-word set, stopwords-iso) would move without across. See data/reference_stopwords/README.md.

only is not reported as safe, even though its true effect here is zero. It appears in four items. Four items cannot distinguish "no effect" from "we could not see one", so it lands in task-dependent and the run tells you to widen it.

Nothing is reported as safe-to-remove. safe-to-remove means the whole confidence interval sits inside ±1pp, and 120 items at this noise level cannot certify that. The filler words land one band down in low-information. That is the tool working: it will not tell you something is free until it can show you it is free.

Every number carries an interval, an N, and a token delta. A deletion effect without those is not a finding, it is a vibe.

Ablate an agent instruction file

The same machinery, pointed at a CLAUDE.md / AGENTS.md / system prompt. Each line, bullet, heading and section is deleted in turn and scored against a real task suite:

uv run stopless instructions examples/instruction_ablation/AGENTS.example.md \
  --suite examples/instruction_ablation/tasks.jsonl \
  --cassette examples/instruction_ablation/cassette.json \
  --repeats 5

Synthetic, again: an invented instruction file, a 40-item invented suite, and the same deterministic toy model. It reads three of its own instructions and ignores the rest, which is exactly what makes the ground truth checkable.

MUST KEEP  (removal hurts; not on the reference stopword list)
  line[3] '- Reply with a single word and nothing…'       -97.0pp  [-99.0, -94.5]         ~-13    40
  line[4] '- Write the answer in lowercase.'              -97.0pp  [-99.0, -94.5]         ~-11    40
  section[0] '# Agent instructions: closed-question a…'   -97.0pp  [-99.0, -94.5]        ~-177    40
  section[1] '## Output format - Reply with a single…'    -97.0pp  [-99.0, -94.5]         ~-47    40
  line[8] '- If the question contains a negation,…'       -47.5pp  [-63.0, -31.5]         ~-20    40
  section[2] '## Reasoning - Read the context before…'    -47.0pp  [-62.5, -31.5]         ~-55    40

UNDERPOWERED / TASK-DEPENDENT  (widen the run before believing anything)
  line[0] '# Agent instructions: closed-question a…'       -0.5pp  [-4.5, +3.5]           ~-15    40
  line[6] '## Reasoning'                                   -0.5pp  [-4.0, +3.0]            ~-5    40
  line[12] '- Never apologise for giving a short an…'      -0.5pp  [-4.0, +3.0]           ~-15    40
  line[5] '- Do not add punctuation, and do not re…'       +1.0pp  [-2.0, +4.0]           ~-17    40
  line[9] '- Prefer the supplied context over your…'       +1.5pp  [-1.5, +4.5]           ~-17    40
  line[11] '- Be concise and professional.'                +1.5pp  [-1.0, +4.0]            ~-9    40
  line[1] 'You are answering closed questions abou…'       +2.0pp  [-0.5, +4.5]           ~-19    40
  line[2] '## Output format'                               +2.0pp  [-0.5, +5.0]            ~-6    40
  line[10] '## Tone'                                       +2.0pp  [+0.0, +4.5]            ~-3    40
  line[13] '- Thank the user when they supply extra…'      +2.0pp  [-0.5, +5.0]           ~-14    40
  line[7] '- Read the context before reading the q…'       +2.5pp  [+0.0, +5.0]           ~-13    40
  section[3] '## Tone - Be concise and professional.…'     +2.5pp  [+0.5, +4.5]           ~-41    40

  model mock-negation-qa:v1 · 40 items · 5 repeats · 3,800 model calls · median observed MDE 3.9pp
  control accuracy 97.0% · scorer exact_match:v1 · ablation delete · scope affected · stochastic

section[0] is the H1, so its section is the whole file: deleting the entire instruction file costs 97 points, which bounds what the file is worth in total. Two formatting bullets account for all of it. The ## Tone section costs nothing.

Size the run before you spend money

uv run stopless power --items 120 --repeats 5 --baseline 0.975
  POWER: what a run this size could detect

  items                 120
  repeats per arm       5
  assumed accuracy      97.5%
  assumed pairing rho   0.5
  alpha / power         0.05 / 0.8

  minimum detectable effect   1.79pp

  An effect smaller than that will not be distinguishable from zero at this N.
  Scaling: the MDE falls with the square root of items, so detecting half the
  effect costs four times the run.

  items needed for a given effect (repeats=5, accuracy=97.5%):
    detect  1.0pp   →  383 items
    detect  2.0pp   →  96 items
    detect  5.0pp   →  16 items
    detect 10.0pp   →  4 items

Telling you up front that 20 items cannot detect a 2% effect is more useful than any single measurement.

Install

Nothing here is published to a package index. Run it straight from the repository:

uvx --from git+https://github.com/jimmyjames177414/stopless stopless --help

Python 3.10+, and one runtime dependency (PyYAML) so that a cold start stays fast.

Everywhere below, stopless … means either of two things that work: the uvx --from git+https://github.com/jimmyjames177414/stopless stopless … form above, or uv run stopless … inside a clone. The demo commands are written the second way because they read files from examples/, which ships in the source tree rather than the wheel.

Run it against a real model, in two commands

Any OpenAI-compatible /v1/chat/completions endpoint works: Ollama, vLLM, LM Studio, llama.cpp's server, OpenRouter, Together, Groq, DeepSeek, OpenAI. Provider selection is a URL, never a code change. A contributor with Ollama and no credit card can run everything in this repo.

ollama pull qwen3:8b

stopless analyze your_prompts.jsonl \
  --model qwen3:8b --base-url http://localhost:11434/v1 \
  --classes negation,hedging,filler,article \
  --scorer exact_match --repeats 5 --out results/

Your corpus is JSONL, one object per line:

{"id": "qa.0001", "prompt": "…", "expected": "no"}

Always dry-run first. It prints the exact number of model calls and an estimated token count before anything is sent:

stopless analyze your_prompts.jsonl --model qwen3:8b --classes negation --dry-run

--max-calls N is a hard stop. Responses are cached under ~/.cache/stopless/, so a re-run with a different scorer costs nothing.

What comes out

A CXS v0.1 results directory (see schemas/):

results/<experiment_id>/
  manifest.json          # what was run, on what, with which seed and determinism claim
  interventions.json     # every arm, including the mandatory noop control
  trials.jsonl           # one raw execution per line, raw, never aggregated
  outcomes.jsonl         # scores, separate from trials so a run can be re-scored for free
  report.json            # aggregates and statistics
  report.md              # the human-readable version
  signal-map.json        # ← the durable artifact

A results directory is written once, when a run completes. There is no resume: a run stopped by --max-calls or a crash produces no results directory, though its responses are in the cache so re-running does not pay for them twice.

signal-map.json is the point. It is a committable, diffable statement of for this model, on this task, these elements are safe to delete and these will bite you. It carries its own caveats, so it cannot be quoted without them. Diff it across model releases.

stopless report results/<id>/ re-renders any finished run without calling anything.

How it works

  1. Select. An element is a word (lexical:not), a lexical class expanded into its members (--classes negation), or a structural unit (structural:line[3]).
  2. Ablate. Delete it from every prompt, then repair the damage: whitespace, orphaned punctuation, sentence-initial capitals. Unrepaired text measures "can the model cope with malformed input?", which is a different question. --ablation-mode mask swaps in a neutral placeholder instead, to partly control for length.
  3. Run the grid. items × arms × repeats, always including a noop control, cached and budgeted.
  4. Score. exact_match, contains, numeric_tolerance, json_field, regex, or your own script.
  5. Measure. Paired difference per item, 95% percentile bootstrap resampling items, sign-flip permutation p-values, Holm correction, and an MDE so a null result is interpretable.
  6. Classify. From the interval, never the point estimate.

The bands

Band Rule
safe-to-remove the whole interval sits inside ±1pp
low-information the whole interval sits inside ±3pp, but is wider than ±1pp
task-dependent the interval spans both "matters" and "does not matter", so the run cannot say
must-keep the whole interval sits below −3pp: removal measurably hurts
anti-stopword must-keep and the element is on the reference stopword list
removal-helps the whole interval sits above +3pp: the model did better without it

A band is never assigned from a point estimate. The type system enforces it: the classifier accepts an object that cannot be constructed without both interval bounds. So does a test that hands it a −18pp midpoint inside a [-40, +4] interval and asserts the answer is task-dependent, not must-keep.

A zero-width bootstrap interval gets special treatment. Four items that all scored identically produce [0.0, 0.0], which read literally would certify safe-to-remove on four observations. No observed variation is not the same as no uncertainty, so the band is decided from the rule-of-three bound (±300/n pp) instead.

Prior art

Stopless is not the first tool to ablate parts of a prompt, and it does not claim to be.

Prior work How it scores importance Its goal
ContextCite (MIT, NeurIPS 2024) real ablation plus a linear surrogate over random ablations attribute one generated statement to context sources, per query
LLMLingua / LLMLingua-2 (Microsoft) proxy: small-LM perplexity, information entropy, a GPT-4-distilled token classifier produce a cheaper working prompt
Selective Context proxy: self-information compress
TokenSHAP, AttriBoT token-level attribution against model output or generation probability explain a generation
LLM-Microscope removes punctuation, stopwords and articles; reports the effect on MMLU and BABILong a research finding
NLTK / spaCy / scikit-learn / stopwords-iso none: fixed, hand-made, model-agnostic IR term filtering
Stopless real ablation, scored on task outcome, aggregated over a corpus a reusable per-model, per-task element vocabulary, and anti-stopword discovery

ContextCite is the closest prior art. The honest distinction is one of question, not technique: ContextCite answers "which source produced this sentence, for this one query?" Stopless answers "across 200 prompts, which recurring elements can I delete from prompts of this kind, for this model, and which will bite me?" Per-query attribution versus a corpus-level reusable vocabulary.

The suspicion that stopword removal is not free predates LLMs entirely: classical NLP was already comparing it against downstream task accuracy, e.g. Comparative analysis of effect of stopwords removal on sentiment classification (IEEE). Stopless is that question asked of a model rather than a classifier, and answered per model and per task rather than once.

Negation fragility in LLMs is a long-established finding and is not ours. Removing stopwords and measuring the effect has been published. What we could not find published is the combination: word- and phrase-level ablation scored against a real task metric, aggregated into a durable artifact, and applied to agent instruction files. Full sourcing, including the claims we are forbidden from making, is in NOVELTY.md.

Statistical methodology follows Miller, Adding Error Bars to Evals (arXiv:2411.00640). Cited, not claimed.

Honest limitations

  • Deletion changes length, and length itself changes behaviour. --ablation-mode mask holds word count roughly constant, but not token count and certainly not attention mass. The report prints the token delta beside every effect so the confound is visible. It is reported, not solved.
  • Elements interact and v0.1 does not measure that. Removing not and without together is not the sum of their separate effects. Every number here is a single-element effect.
  • Results do not transfer. They are specific to one model, one task, one corpus, and one scorer. The tool refuses to imply otherwise, and signal-map.json records all four.
  • The demo corpus is synthetic and the demo model is a toy. Findings about real models require real runs, which cost real money.
  • Scoring is exact-match style. Open-ended generation needs an LLM judge, which v0.1 deliberately does not ship. A judge is itself sensitive to the words being ablated, so a judged ablation measures the judge and the subject at the same time.
  • anti-stopword depends on which reference list you compare against, and different published lists disagree about without, except and unless.
  • The bundled NLTK stopword list was transcribed by hand, not downloaded. data/reference_stopwords/README.md gives you a one-line command to diff it against your own copy, and asks you to open an issue if it differs.
  • Structural selectors are regexes, not a parser. Sentence splitting knows about a dozen abbreviations and nothing more.
  • stopless power's a priori MDE rests on an assumed within-item correlation (rho, default 0.5). Raising it shrinks the MDE, so it is the assumption to be sceptical of. The observed MDE reported after a run does not depend on it.

Not built yet

Wanted, and tracked as help-wanted issues:

  • an LLM-judge scorer, with the confound controls it needs to mean anything
  • automatic element discovery (n-gram mining): v0.1 only ablates elements you declare
  • interaction effects between elements
  • cross-model consistency reports: is not safe on model A but not on model B?
  • local HuggingFace model support without an HTTP server

Per SHARED-FOUNDATION D10, none of these ships as a stub. A stub that looks implemented is worse than an admitted gap.

Contributing

The easiest useful contribution is a data file, not code: data/lexical_classes/*.yaml are plain YAML word lists, deliberately outside src/, so adding a class or a language never means reading Python. See CONTRIBUTING.md.

git clone https://github.com/jimmyjames177414/stopless && cd stopless
uv venv && uv pip install -e ".[dev]"
uv run pytest -m "not live"     # 348 tests, no API key, no network
uv run ruff check . && uv run ruff format --check .
uv run mypy --strict src/

Interoperability

Stopless writes CXS v0.1, a file format, not a library. It imports no sibling project and no sibling project imports it. tests/test_cxs_conformance.py validates real output from a real run against the vendored schemas, because a schema nobody validates against is a wish.

Read next

Licence

Apache-2.0.


jimmyjames177414

@jimmyjames177414 · Apache-2.0

One of nine open-source tools for measuring what context and tools actually do to AI systems:
stopless · stopbench · mincontext · validwhile · errorbars
assumptionledger · toolsweep · knowwhen · inconclusive

About

Measure which words your prompt actually needs. Learned stopwords and anti-stopwords, by real ablation rather than proxy scores.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages