●COWORK — Claude Cowork expands to web and mobile with remote sessions, synced files, and a shared Chat and Cowork home across devices●M365 — Claude Cowork adds Microsoft 365 write tools to draft and send email, manage calendars, and update OneDrive and SharePoint files●MCP — Admins can provision MCP connectors org-wide through their identity provider, starting with Okta, with access granted on first login●CODE — Claude Code fixes SessionStart hook streaming in headless sessions so remote workers are not idle-reaped mid-hook●GOV — Claude Code and Claude Cowork are in public beta in Claude for Government Desktop on a FedRAMP High authorized environment●LIMITS — Claude Code weekly usage limits are increased by 50% through July 13●COWORK — Claude Cowork expands to web and mobile with remote sessions, synced files, and a shared Chat and Cowork home across devices●M365 — Claude Cowork adds Microsoft 365 write tools to draft and send email, manage calendars, and update OneDrive and SharePoint files●MCP — Admins can provision MCP connectors org-wide through their identity provider, starting with Okta, with access granted on first login●CODE — Claude Code fixes SessionStart hook streaming in headless sessions so remote workers are not idle-reaped mid-hook●GOV — Claude Code and Claude Cowork are in public beta in Claude for Government Desktop on a FedRAMP High authorized environment●LIMITS — Claude Code weekly usage limits are increased by 50% through July 13
When the RAG Started Being Confidently Wrong — Field Notes on Measuring Retrieval Misses With Groundedness
In a Claude API RAG, the answers stay fluent while the facts drift. Often the cause is a silent recall decay on the retrieval side, missing the document that holds the answer. Field notes on measuring groundedness and retrieval hit rate and walking the system back, with working code and real numbers.
I asked our support RAG, "How many days is the refund window?" Claude answered smoothly: "Within 14 days." The actual policy was 30. The answer was fluent, cited its sources, and radiated confidence — which is exactly what made my stomach drop.
The model wasn't broken. Retrieval was. It had failed to fetch the document holding the answer, yet generation carried on, and Claude assembled a plausible falsehood from the unrelated fragments it happened to have.
These are field notes on a silent recall decay — a retrieval miss — in a Claude API RAG I ran in production: how I surfaced it with a groundedness number and walked it back in stages. This is not about building a RAG. It is about how to notice, and fix, when retrieval quietly degrades after you've built it. I'll leave the build steps to pieces like prompt caching design for the Claude API and focus here on measurement.
Generation never stops, so decay stays invisible
The awkward thing about RAG is that generation continues even when retrieval fails. With zero relevant documents, or a few unrelated fragments hovering near the threshold, Claude still returns something that reads well. The API throws nothing. The logs are a wall of 200s.
The decay I hit didn't arrive one morning — it crept. I added documents to the knowledge base, tweaked chunking, swapped the embedding model for a newer version. Every change was well-intentioned. And each time, the document containing the answer got quietly pushed out of the top-K.
The trouble is that none of this shows up in how the answer looks. The prose stays fluent — the confidence, if anything, grows. Only a vague "it's felt off lately" from users arrives, and late. So I needed to measure whether retrieval was actually grabbing the answer, separately from generation.
Doubt it as two numbers
Treating "the RAG is drifting" as one lump makes the cause impossible to isolate. I split the problem in two and gave each its own number.
Metric
What it measures
What a low value means
Retrieval hit rate
Share of queries where the answer-bearing doc lands in the top K
Retrieval (embeddings, chunks, threshold) is broken
Groundedness
Share of answers actually supported by the retrieved docs
Retrieval works but generation ignores the docs
If retrieval hit rate is low, the cause is the retrieval pipeline. If hit rate is high but groundedness is low, the cause is the generation prompt or how the context is packed. Just being able to make that cut ended the blind trial-and-error.
The first time I measured, retrieval hit rate was 68% and groundedness 61%. One in three times, the document with the answer never even reached Claude. Seeing the numbers is what finally made the problem's location click.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦How to split a fluent-but-wrong RAG into two numbers — groundedness and retrieval hit rate — to tell a retrieval failure from a generation failure
✦A working implementation that puts Claude in the grader's seat to score whether an answer is actually grounded in the retrieved docs, run daily
✦The staged fixes — chunk bloat, embedding-model swaps, threshold drift — that walked groundedness from 61% back to 94%
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Put Claude in the grader's seat to measure groundedness
Grading groundedness by hand doesn't last. So I stood up Claude as a grader in a call separate from generation, judging whether the answer is supported by the retrieved documents. The grading prompt is short, and the verdict comes back structured.
// eval/groundedness.tsimport Anthropic from '@anthropic-ai/sdk';const client = new Anthropic();type Verdict = { grounded: boolean; supported_claims: number; unsupported_claims: number };export async function scoreGroundedness( answer: string, retrievedDocs: string[],): Promise<Verdict> { const context = retrievedDocs.map((d, i) => `[doc ${i + 1}]\n${d}`).join('\n\n'); const res = await client.messages.create({ model: 'claude-sonnet-5', max_tokens: 300, // Grading is a separate process from generation. Do not fill gaps with outside knowledge. system: 'Judge, using only the given documents, whether each claim in the answer is supported. ' + 'Claims absent from the documents count as unsupported. Return JSON only.', messages: [ { role: 'user', content: `# Retrieved documents\n${context}\n\n# Answer\n${answer}\n\n` + 'Reply as {"grounded": boolean, "supported_claims": number, "unsupported_claims": number}.', }, ], }); const text = res.content[0].type === 'text' ? res.content[0].text : '{}'; return JSON.parse(text.slice(text.indexOf('{'), text.lastIndexOf('}') + 1));}
The key is telling the grader not to fill gaps with outside knowledge. Leave that ambiguous and Claude will defend the answer with its own general knowledge, and groundedness reads too lenient. Taking the verdict as JSON — down to counts of supported and unsupported claims — lets you later trace which answers drifted, and by how much.
For retrieval hit rate, I fixed a dataset of about 120 items — each a question paired with the ID of the document that holds its answer — and mechanically counted whether the correct ID appeared in the results. No special machinery. It just rides along in the daily batch.
Make groundedness one daily number
End the measurement as a one-off investigation and the decay quietly returns. I fixed groundedness and retrieval hit rate as two numbers I check on the dashboard each morning. As an indie developer handing daily work across several sites to a model, I find instruments like these are what let me keep the overnight runs going without worry. I hand even AdMob-revenue support copy to the model, so when answer correctness breaks, it hits real operations directly.
Following the numbers, I worked down from the top causes.
Point
Retrieval hit rate
Groundedness
Action taken
Baseline
68%
61%
— (assess current state)
Week 2
82%
74%
Re-split bloated chunks to ~512 tokens
Week 4
91%
88%
Recalibrated similarity threshold, top-K 4→8
Week 6
93%
94%
Told generation not to state ungrounded claims
The biggest lever was re-chunking. With every document I added, long heading-sized chunks had accumulated, each holding several topics at once. Their embeddings averaged out into vectors that were vaguely near every question and decisive for none. Just cutting to around 512 tokens raised the retrieval hit rate by 14 points.
Threshold drift is easy to miss too. When I swapped the embedding model for a new version, I reused the old cosine-similarity threshold as-is. The score distributions differ between versions, so the cutoff effectively tightened and started rejecting the answer document.
// retrieve.ts — calibrate the threshold from the distribution (don't reuse a fixed value)export function calibrateThreshold(scores: number[], keepRatio = 0.3): number { const sorted = [...scores].sort((a, b) => b - a); const idx = Math.floor(sorted.length * keepRatio); return sorted[idx] ?? 0; // the position that keeps the top 30% becomes the dynamic threshold}
Don't reuse a fixed threshold after an embedding-model update. That's the kind of lesson that only sticks once it has stung you.
Bake "if it isn't grounded, don't answer" into generation
Fixing retrieval brings groundedness up a little behind the hit rate. The last push was on the generation side: stop Claude from patching holes with general knowledge when the retrieved docs don't hold the answer. In the prompt, make it say "I don't know" outright when there's no support.
// system prompt in generate.ts (excerpt)const system = `You answer only from the retrieved documents.- State only claims that the documents support.- If the documents do not contain the answer, do not guess; say "the provided materials contain no relevant statement."- End each claim with the [doc n] it relied on.`;
Requiring a [doc n] on each claim stabilizes the groundedness grading and makes answers easier for users to verify. A RAG that can honestly say "I don't know" is far more trustworthy than one that lies fluently. I've come to prefer the honesty of drawing the boundary correctly over the fluency of filling the gap.
The recovery order on one page
If I met the same situation again, this is the sequence I'd follow.
Don't trust how the answer looks. Treat fluency and correctness as separate things.
Doubt it as "retrieval hit rate" and "groundedness." Isolate retrieval-side vs generation-side first.
Put Claude in the grader's seat and make groundedness one daily number.
Work down from chunk bloat, threshold drift, and embedding updates.
Calibrate the threshold from the distribution. Don't reuse a fixed value after an update.
Bake "if it isn't grounded, don't answer" into generation, and require a [doc n] on each claim.
A RAG isn't done when you build it; it earns trust only when you keep measuring whether it still grabs the answer. There's nothing flashy about it, but this quiet instrument is what steadily illuminates the decay hidden behind fluent prose. I hope it helps in your own build. Thank you for reading.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.