snakereach

Writing · RAG

Why your chatbot quotes the cookie banner

A support bot goes live. Someone asks how to reset their password. It replies with something like:

an answer nobody wanted
We use cookies to improve your experience. Accept all · Manage
preferences. For account help see Support › Account › Password.
Skip to main content.

The instinct is to blame the model, and the next move is usually a longer system prompt telling it not to do that. It will not help, because the model is behaving correctly. It was asked to answer from the retrieved context, and the retrieved context was a cookie banner.

How the banner got in

Trace it backwards and it is always the same path.

Someone crawled the docs site. Extraction took the whole <body>, so every page began with the consent notice, the skip link, the header and the sidebar. That text was chunked along with everything else. The first chunk of every page is now mostly furniture, and there are eight hundred pages of it.

Then someone asked a question. The retriever compared the query against every chunk, and those eight hundred near-identical furniture chunks were all a little bit similar to everything, because they contain words like account, support, preferences and help in a page that is nominally about the product.

Why boilerplate is unusually good at winning

This is the part that surprises people. Boilerplate does not just take up room; it actively competes well.

  • It is short and dense. Navigation is nouns. A chunk of pure nouns from your product's domain embeds close to a lot of queries about that domain.
  • It repeats. Eight hundred similar chunks get eight hundred chances to be in the top five. A single good paragraph gets one.
  • It looks like an index. “Support › Account › Password” contains the query's exact words in the right order, without containing a single instruction.

So the failure is not random noise leaking through. It is a systematic bias toward the least informative text on the site.

How to confirm it in five minutes

Before changing anything, verify the diagnosis. Two checks settle it.

Print what you retrieved. Not the answer, the chunks. If the top five contain menus, you are done diagnosing. Most teams debugging this have never actually looked at the retrieved text.

Hash the first chunk of every document. If a large number of documents share a first chunk, that chunk is your header, and every one of those documents is contributing nothing.

the check
from collections import Counter
import hashlib

first = Counter(
    hashlib.sha1(doc.chunks[0].encode()).hexdigest()
    for doc in corpus
)
print(first.most_common(3))
# [('4f2a…', 812), ('9b7c…', 44), ('1d03…', 7)]
#   812 documents begin with identical text. That is the sidebar.

The fixes that do not work

Each of these gets tried, and each treats a symptom.

Prompting harder. “Ignore navigation elements.” The model cannot ignore what it was given and still answer from context, and you have spent tokens asking.

A bigger embedding model. It will embed the cookie banner more accurately.

A reranker. This one genuinely helps, which is the problem: it hides the issue well enough that nobody fixes it, and you now pay for a second model pass on every query to discard text you should never have stored.

Stopword filtering. “Accept all cookies” is not made of stopwords.

The fix that does

Do not ingest it. Extraction should identify the content region and discard the chrome before anything downstream sees it, which is a different operation from stripping tags.

Two things follow immediately, and both are worth having on their own:

  • Chunks become distinct. Deduplication starts working, because two pages that differ only in content no longer share three hundred identical words.
  • Headings become usable split points. Once the structure is preserved rather than flattened, you can chunk on it instead of on character count.

RAG quality starts at extraction goes through the rest of that pipeline.

A cheap regression test

Once it is fixed, keep it fixed. Sites get redesigned and extraction quietly degrades, usually without anything failing.

Keep a handful of URLs and their expected content length. If a page that reliably produced 4,000 characters starts producing 400, or 40,000, something changed. That test costs almost nothing and catches the class of bug that otherwise surfaces as “the bot got worse and nobody knows when”.

snakereach turns any URL into the shape described here: markdown first, structure preserved, with per-stage timings. Try it on a page of your own, or read what comes back.

More writing