<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Giulio D'Erme</title>
    <description>The latest articles on DEV Community by Giulio D'Erme (@gde03).</description>
    <link>https://dev.to/gde03</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3940212%2F5c67ea11-57fb-4c0d-af21-86b03bfcda07.jpg</url>
      <title>DEV Community: Giulio D'Erme</title>
      <link>https://dev.to/gde03</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gde03"/>
    <language>en</language>
    <item>
      <title>The RAG benchmark that pays you for saying "I don't know", and what it exposed</title>
      <dc:creator>Giulio D'Erme</dc:creator>
      <pubDate>Sun, 09 Aug 2026 11:33:16 +0000</pubDate>
      <link>https://dev.to/gde03/the-rag-benchmark-that-pays-you-for-saying-i-dont-know-and-what-it-exposed-4411</link>
      <guid>https://dev.to/gde03/the-rag-benchmark-that-pays-you-for-saying-i-dont-know-and-what-it-exposed-4411</guid>
      <description>&lt;p&gt;Nine published systems. One of them refuses 2.3× more often than &lt;code&gt;gpt-4o&lt;/code&gt; and still finishes 4th on answer quality. It calls no LLM.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Part 5 of &lt;strong&gt;The Answerability Problem&lt;/strong&gt;. Parts 1 to 4 argued the field measures accuracy on questions that have answers. This one is about the benchmark that doesn't. Code: &lt;a href="https://github.com/GiulioDER/RE-call" rel="noopener noreferrer"&gt;RE-call&lt;/a&gt; (Apache 2.0).&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/IBM/mt-rag-benchmark" rel="noopener noreferrer"&gt;MTRAG&lt;/a&gt;&lt;/strong&gt; (IBM, TACL 2025) is 842 human-written multi-turn tasks with an official &lt;code&gt;gpt-4o-mini&lt;/code&gt; judge that pays a &lt;strong&gt;full 1.0 on every metric&lt;/strong&gt; for correctly saying &lt;em&gt;"I do not have that information."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Almost no other benchmark scores a refusal as anything but a miss. So this is the one board where the thing my system is built for is actually worth points.&lt;/p&gt;

&lt;p&gt;Here is what nine published systems do with that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nobody is collecting the free money
&lt;/h2&gt;

&lt;p&gt;Correct refusals on the 55 unanswerable tasks. Same judge, same tasks, every system:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;system&lt;/th&gt;
&lt;th&gt;correct refusals&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;llama-3.1-8b&lt;/td&gt;
&lt;td&gt;18/55&lt;/td&gt;
&lt;td&gt;32.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;🔹 &lt;code&gt;gpt-4o&lt;/code&gt; + RE-call&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16/55&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;29.1%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;llama-3.1-70b&lt;/td&gt;
&lt;td&gt;16/55&lt;/td&gt;
&lt;td&gt;29.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;gpt-4o-mini&lt;/td&gt;
&lt;td&gt;13/55&lt;/td&gt;
&lt;td&gt;23.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;gpt-4o &lt;em&gt;(benchmark's own retrieval)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;7/55&lt;/td&gt;
&lt;td&gt;12.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;llama-3.1-405b&lt;/td&gt;
&lt;td&gt;3/55&lt;/td&gt;
&lt;td&gt;5.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;qwen-2.5-72b&lt;/td&gt;
&lt;td&gt;1/55&lt;/td&gt;
&lt;td&gt;1.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;mixtral-8x22b&lt;/td&gt;
&lt;td&gt;0/55&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Two rows in that table read the same document set with the same model.&lt;/strong&gt; Row 6 is &lt;code&gt;gpt-4o&lt;/code&gt; on the benchmark's own retrieval. Row 2 is &lt;code&gt;gpt-4o&lt;/code&gt; on RE-call's. Same reader, same prompt, same judge, only the contexts differ, and the refusal rate goes from 7 to 16 out of 55.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then read the rank order. It is close to inverted against model size.&lt;/strong&gt; The 8B model refuses best. The 405B model refuses 5.5% of the time. One model refuses nothing at all. Scale does not buy this, and on this evidence it costs it.&lt;/p&gt;

&lt;p&gt;RE-call is &lt;strong&gt;second of ten&lt;/strong&gt;, tied exactly with &lt;code&gt;llama-3.1-70b&lt;/code&gt; at 16 of 55, and it refuses &lt;strong&gt;2.3× more often than &lt;code&gt;gpt-4o&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;16× more often than &lt;code&gt;qwen-2.5-72b&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The usual objection, answered
&lt;/h2&gt;

&lt;p&gt;Abstention is cheap if you don't care about answering. So here is what it cost, on the same 842 tasks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;system&lt;/th&gt;
&lt;th&gt;answer quality&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;llama-3.1-405b&lt;/td&gt;
&lt;td&gt;0.5691&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;qwen-2.5-72b&lt;/td&gt;
&lt;td&gt;0.5625&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;gpt-4o &lt;em&gt;(benchmark's own retrieval)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;0.5591&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;🔹 &lt;code&gt;gpt-4o&lt;/code&gt; + RE-call&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.5527&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;c4ai-command-r-plus&lt;/td&gt;
&lt;td&gt;0.5502&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;RE-call is the only system in that top four that is also top two on abstention.&lt;/strong&gt; The three above it refuse 5.5%, 1.8% and 12.7%. The gap to &lt;code&gt;gpt-4o&lt;/code&gt; is &lt;strong&gt;0.0064&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is the whole claim, and it is a narrow one: not "best", but &lt;strong&gt;the only one on the board that is near the top of both columns at once&lt;/strong&gt;. Everyone else picked a column.&lt;/p&gt;

&lt;p&gt;Same two rows again, 3 and 4, same reader. Swapping the benchmark's own retrieval for RE-call's costs &lt;strong&gt;0.0064&lt;/strong&gt; on answer quality and more than doubles what the system correctly refuses. That is the trade, priced.&lt;/p&gt;

&lt;p&gt;And against the benchmark's own retrieval directly, identical generator, prompt and judge, only the contexts swapped, RE-call is ahead: &lt;strong&gt;0.5527 against 0.5516&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Small. Positive under both prompts I tested. I am not going to inflate it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Every row above is scored by MTRAG's official &lt;code&gt;gpt-4o-mini-2024-07-18&lt;/code&gt; judge, hard-coded by the benchmark and applied identically to my rows and theirs. The reader on my rows is &lt;code&gt;gpt-4o&lt;/code&gt;, the same model as the row I am compared against.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One engine, whatever you can afford
&lt;/h2&gt;

&lt;p&gt;The other half of this. The same pipeline runs at both ends of the price range, and every rung is a named flag with a measured price, not a tier you buy.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;configuration&lt;/th&gt;
&lt;th&gt;nDCG@5&lt;/th&gt;
&lt;th&gt;cost&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;🔹 SPLADE learned sparse &lt;em&gt;(the free default)&lt;/em&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.3573&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;local, $0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ Voyage rerank &lt;em&gt;(one flag)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;0.4342&lt;/td&gt;
&lt;td&gt;paid API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;A 48% relative span between two flags&lt;/strong&gt;, measured on 777 judged queries.&lt;/p&gt;

&lt;p&gt;The reranker is worth &lt;strong&gt;+0.0769 nDCG@5&lt;/strong&gt; and is &lt;strong&gt;worse on 162 of the 777&lt;/strong&gt;. That is exactly why it is off by default. I would rather ship a flag with two published numbers than a default with a marketing claim.&lt;/p&gt;

&lt;p&gt;Everything dashed and amber below is opt-in and switched on by name. The solid path is what runs if you change nothing, and it costs &lt;strong&gt;$0 at any scale&lt;/strong&gt;, because there is no LLM anywhere in it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3t7fmf1sxpq24pd7k1qq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3t7fmf1sxpq24pd7k1qq.png" alt="The RE-call pipeline. Write path: memo with supersession and validity frontmatter, chunk, embed locally with no API call. Query path: embed the query. Both meet in one PostgreSQL and pgvector database holding vectors and full text. Dense cosine and sparse full-text retrieval feed Reciprocal Rank Fusion, with an optional cross-encoder rerank shown dashed and amber. Then a gap check against a calibrated threshold, then the trust layer enforcing supersession, validity and confidence, fed by a calibration fitted per embedder and corpus. Output is a verdict with confidence and provenance, or ABSTAIN with a reason." width="800" height="1355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Writing a memory is a local embedding.&lt;/strong&gt; No LLM call, so the marginal cost of a memory is $0 and stays $0 at any scale, on any model, forever. There is no upstream pricing change that can alter that.&lt;/p&gt;

&lt;p&gt;Air-gapped and fully local, or cloud embedder and reranker when a jargon-heavy corpus makes it worth paying. Same engine, same guarantee at every setting: superseded or expired memories are demoted rather than served, and the confidence threshold is &lt;strong&gt;fitted per embedder and corpus at install time, never a shipped constant.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The number I nearly published
&lt;/h2&gt;

&lt;p&gt;One thing before you trust any of the above.&lt;/p&gt;

&lt;p&gt;An earlier version of this run had RE-call &lt;strong&gt;beating every baseline, including llama-3.1-405b&lt;/strong&gt;. It was an artifact and it was one edit away from going out.&lt;/p&gt;

&lt;p&gt;The official scorer reads the answerability label in lower case. The release files ship it capitalised. So the conditioning that pays 1.0 for a correct refusal silently never ran on my data: 2,526 lookups, &lt;strong&gt;zero label matches&lt;/strong&gt;. My metrics were raw, the published baselines' were conditioned, and I was comparing two different quantities in exactly the place where my system is strongest.&lt;/p&gt;

&lt;p&gt;Both sides were individually correct. &lt;strong&gt;The error lived entirely in the join.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two signals should have caught it earlier and I explained both away. I had even written the memo describing the two-key problem, that same day, and never connected it. Having the fact is not the same as applying it.&lt;/p&gt;

&lt;p&gt;Corrected, the honest reading is: RE-call at &lt;strong&gt;0.5527 against gpt-4o's 0.5591, so −0.0064&lt;/strong&gt;. Fourth, not first. The abstention result survives intact, because it was measured by the official judge rather than by the string-matching detector that had earlier inflated it from 29% to 43.6%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The anomaly you can explain is still an anomaly, and the one that flatters you is the one to chase hardest.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  So which rung is yours
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Take the free default&lt;/strong&gt; if the marginal cost of a memory has to be zero, your data cannot leave, or you need it to work offline. 0.3573, local, $0, no API key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add the reranker&lt;/strong&gt; when a human is waiting. One second is invisible next to the generator call that follows it, and it is the largest single gain in the project. Check it on your corpus first: it lost on 162 of 777 queries here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use something else&lt;/strong&gt; if abstention is worth nothing to you and you only need the top of one column. Three systems beat me on answer quality. All three of them will confidently answer a question whose answer is not in the corpus, between 87% and 98% of the time.&lt;/p&gt;

&lt;p&gt;That is the trade. A system that invents a plausible answer and one that says nothing are not two points on one scale.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Every number, the six generation runs behind them, the correction in full, and the scoring bug I reported upstream: &lt;strong&gt;&lt;a href="https://github.com/GiulioDER/RE-call/blob/master/docs/MTRAG_BENCHMARK.md" rel="noopener noreferrer"&gt;docs/MTRAG_BENCHMARK.md&lt;/a&gt;&lt;/strong&gt;. &lt;a href="https://github.com/GiulioDER/RE-call" rel="noopener noreferrer"&gt;RE-call&lt;/a&gt; is Apache 2.0, &lt;code&gt;pip install recall-rag&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rag</category>
      <category>ai</category>
      <category>opensource</category>
      <category>benchmarking</category>
    </item>
    <item>
      <title>Make your AI assistant predict the result before it runs it</title>
      <dc:creator>Giulio D'Erme</dc:creator>
      <pubDate>Sun, 02 Aug 2026 14:20:27 +0000</pubDate>
      <link>https://dev.to/gde03/make-your-ai-assistant-predict-the-result-before-it-runs-it-2abo</link>
      <guid>https://dev.to/gde03/make-your-ai-assistant-predict-the-result-before-it-runs-it-2abo</guid>
      <description>&lt;p&gt;&lt;em&gt;Less technical than my usual posts. Nothing to install, nothing to pay for, and it works the same whether you're using an AI assistant for code, spreadsheets, research or writing.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Here is the shape of the problem I want to describe. See if you recognise it.&lt;/p&gt;

&lt;p&gt;You ask your assistant to do something that produces a result. It does. A number, a summary, a status, an answer comes back. It's plausible. You ask why, and you get a clear, confident explanation that makes complete sense of it. So you believe it, you write it down, and you build your next three decisions on top of it.&lt;/p&gt;

&lt;p&gt;A week later you find out the whole thing was wrong. Not wrong in an obvious way. Wrong because the tool was measuring something you didn't ask about, or reading a file that was empty, or answering a slightly different question than the one in your head.&lt;/p&gt;

&lt;p&gt;Notice what didn't happen there. Nothing was invented. No fake citation, no made-up fact, none of the failures people mean when they say "hallucination". The failure was structural: &lt;strong&gt;nobody committed to what the answer should be while it was still possible to be wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The habit
&lt;/h2&gt;

&lt;p&gt;Before your assistant runs anything that produces a result, make it answer two questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. What do you expect the result to be, and why?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. How would you know if this were broken?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the whole technique. Two sentences before the work instead of a paragraph of explanation after it. It works on the free tier. It works on any model. It costs a few seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the first question works
&lt;/h2&gt;

&lt;p&gt;This isn't about "thinking harder", and it isn't a motivational trick. It changes the kind of problem your assistant is solving.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explaining a result you've already seen is an open-ended task.&lt;/strong&gt; There are a huge number of stories that fit any given outcome, and a language model is exceptionally good at finding one of them. That's not a flaw, it's the thing it's built to do. The problem is that a fluent explanation tells you nothing about whether the result is correct. It reads identical either way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Predicting a result you haven't seen is a closed task.&lt;/strong&gt; Everything the assistant believes about your data, your files and your request has to collapse into one statement that can turn out to be wrong. And when the real output disagrees with the prediction, you get a signal that could not have been manufactured after the fact, because the prediction is already sitting there on the screen.&lt;/p&gt;

&lt;p&gt;That gap between expected and actual is the entire product. It's cheap to produce and impossible to fake in hindsight.&lt;/p&gt;

&lt;p&gt;There's a bonus effect, and it's the reason results feel steadier once you do this. Predicting first pins down the standard of success &lt;strong&gt;before&lt;/strong&gt; the data arrives. Most of the wobble between one run and the next isn't the model changing its mind, it's the standard quietly moving to fit whatever showed up.&lt;/p&gt;

&lt;p&gt;Science has a name for this and an entire apparatus around it: pre-registration. Nobody needed to invent it for AI. We just forgot to apply it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the second question matters more
&lt;/h2&gt;

&lt;p&gt;I learned this one the expensive way.&lt;/p&gt;

&lt;p&gt;I had a system meant to answer time-sensitive questions: which of these documents was true &lt;em&gt;as of&lt;/em&gt; a particular date. It scored badly. I had predicted it would score badly, because the task is genuinely hard. Prediction matched result. I nodded, wrote the number down, and moved on.&lt;/p&gt;

&lt;p&gt;The prediction was right and the measurement was worthless.&lt;/p&gt;

&lt;p&gt;None of the documents had dates attached to them. Not one, out of four hundred and twenty. The system was being asked a question about time using material that had no time in it. When I fixed that, with exactly the same documents, the score went from 0.19 to 0.98.&lt;/p&gt;

&lt;p&gt;I want to be careful with that number, because it's the kind of number people quote badly, including the person who produced it. &lt;strong&gt;That is not an improvement.&lt;/strong&gt; Nothing got smarter. That's what it looks like from the outside when a broken instrument gets repaired, and if I'd published it as a win I'd have been telling a lie with correct arithmetic in it.&lt;/p&gt;

&lt;p&gt;Here's the general rule, and it's the part almost everyone skips:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Predicting the answer will not tell you the question was never asked.&lt;/strong&gt; If the setup is broken, your prediction and your result can be wrong in the same direction, agree with each other perfectly, and confirm nothing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Which is exactly what the second question is for. "How would I know if this were broken?" forces something different out of the assistant: not a guess at the answer, but a statement about the machinery, one that has to hold true &lt;em&gt;whatever the answer turns out to be&lt;/em&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Expectation: "the score will be low, this is a hard problem."&lt;/li&gt;
&lt;li&gt;Broken-check: "if the documents don't have dates on them, the question isn't being asked at all."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first one was right and taught me nothing. The second one would have gone off on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Done is not the same as proved
&lt;/h2&gt;

&lt;p&gt;That second question generalises into something I now say out loud several times a week.&lt;/p&gt;

&lt;p&gt;When a task reports success, what you have learned is that a task reported success. You have not learned that a file was written, that the file has anything in it, that the numbers cover the dates you asked for, or that the check you requested was actually performed.&lt;/p&gt;

&lt;p&gt;My favourite example is small and stupid. I once wrote a rule-checker to catch a specific kind of mistake in my own work. It ran clean for weeks. It ran clean because of a settings mismatch that meant the rule I cared about was never switched on. The checker was structurally incapable of finding anything. Nothing was broken, nothing errored, everything was green, and the green meant nothing at all.&lt;/p&gt;

&lt;p&gt;The first test I wrote to fix that read the settings file to confirm the settings file. It passed. Two separate reviews walked straight past it. The thing that finally caught anything was running the checker against a deliberately bad example and watching it complain.&lt;/p&gt;

&lt;p&gt;So: ask for the output, not the status. Show me the rows. Show me the file. Show me it catching one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anything that's supposed to warn you, make it warn you once
&lt;/h2&gt;

&lt;p&gt;Same idea, pointed somewhere useful.&lt;/p&gt;

&lt;p&gt;A safety net you have never seen catch anything is not a safety net. It's a guess in the shape of one. This applies to far more than code: the alert that's supposed to email you when a number goes out of range, the filter that's supposed to catch the bad rows, the check that's supposed to stop the bad export.&lt;/p&gt;

&lt;p&gt;So test it deliberately. Feed it the bad thing on purpose, once, and watch it fire.&lt;/p&gt;

&lt;p&gt;I ran that exercise across a system I'd been maintaining for months. It found twelve warnings that could never have gone off under any circumstances. One of them was the reason a cost had been quietly running for about two months before anyone noticed. Every one of those twelve had been read and approved by someone, including me. Reading them was never going to work, because they &lt;em&gt;read correctly&lt;/em&gt;. That's precisely what makes them dangerous.&lt;/p&gt;

&lt;p&gt;Two things I got wrong before I got them right, and they generalise:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Breaking it on purpose only proves whatever you actually broke.&lt;/strong&gt; I once "confirmed" that two checks happened in the right order by deleting one of them entirely. All that proved was that a check existed. The ordering, which was the actual thing I cared about, stayed untested.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A check that can't pass is as useless as one that can't fail.&lt;/strong&gt; I hit five in a row where the thing also refused to go green on a known-good input, and each one had been introduced while fixing the previous one. Identical behaviour on the good case and the bad case means it's measuring nothing, in either direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Look it up, don't remember it
&lt;/h2&gt;

&lt;p&gt;One more, cheaper than everything above. When the assistant needs a fact it could go and check, make it go and check. A confident wrong value is worse than an admitted blank, because a blank is honest and gets dealt with, while a wrong value slides silently into everything downstream.&lt;/p&gt;

&lt;p&gt;And the part that took me embarrassingly long to accept: &lt;strong&gt;"it isn't there" is a complete answer.&lt;/strong&gt; It does not need to be improved into something that looks more like information.&lt;/p&gt;

&lt;h2&gt;
  
  
  The version you can paste in right now
&lt;/h2&gt;

&lt;p&gt;All of it collapses into five lines. Put them in your assistant's instructions file, your saved prompt, your project settings, wherever your tool keeps standing rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; Before running anything that produces a result: say what you expect
  and why, in one or two sentences. Then run it.
&lt;span class="p"&gt;-&lt;/span&gt; Also say how I'd know if this were broken, and check that first.
&lt;span class="p"&gt;-&lt;/span&gt; "Done" is not a result. Show me the output: the file, the rows, the value.
&lt;span class="p"&gt;-&lt;/span&gt; Anything meant to warn me gets tested once by making it warn me.
&lt;span class="p"&gt;-&lt;/span&gt; Look facts up instead of recalling them. "Not there" is a valid answer.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If five lines is four too many, keep the second one. It's the one that has paid for itself the most times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can you actually prove this helps?
&lt;/h2&gt;

&lt;p&gt;Not the way I'd like to. I can't show you that it produces fewer invented facts, because I haven't run that experiment and I'm not going to claim a result I don't have.&lt;/p&gt;

&lt;p&gt;But that isn't really the thing that hurts, is it. What hurts is the week you spend building on top of an answer that was wrong from the start. And &lt;strong&gt;that one is measurable, by you, this month, for free.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Keep a tally. Every time you have to walk something back, throw work away, or redo a decision because the result underneath it turned out to be wrong, put a mark down. One line, one date, one sentence on what the wrong thing was.&lt;/p&gt;

&lt;p&gt;Do that for two weeks as you work now. Then add the two questions and do it for two more.&lt;/p&gt;

&lt;p&gt;You're not counting how often the assistant sounds right, which is close to always and tells you nothing. You're counting how often it &lt;em&gt;stayed&lt;/em&gt; right long enough to build on. That's the number that maps onto your actual time, and unlike a benchmark score it's yours, it's specific to your work, and nobody can argue with it.&lt;/p&gt;

&lt;p&gt;My honest position: I have a mechanism I find convincing and an incident log where the disasters cluster hard on the occasions when nobody committed to anything in advance. That's evidence. It isn't a measurement, and I'd be doing the exact thing this post argues against if I dressed it up as one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it doesn't help
&lt;/h2&gt;

&lt;p&gt;Three limits, because I'd rather say them than have them said back to me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It needs something checkable.&lt;/strong&gt; If there's no observable outcome, "predict the result" turns into a confident-sounding preamble that flatters whatever comes next. When there's nothing to be wrong about, skip it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A confident prediction can pull you toward it.&lt;/strong&gt; Say a number out loud beforehand and there's pressure, on a model as much as on a person, to read the result as agreeing with it. The fix is to predict the &lt;strong&gt;observable&lt;/strong&gt;, not the conclusion. "There should be roughly 400 rows, all dated after June" pulls much less than "this should confirm my theory."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It slows you down slightly.&lt;/strong&gt; A few seconds per task. You get them back the first time it catches something, and you'll know exactly when that happens, because the prediction will be sitting right there next to the result, disagreeing with it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you try the five lines, I'd like to know which ones earn their place and which are cargo cult. I'm fairly sure at least one of them is.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>discuss</category>
      <category>beginners</category>
    </item>
    <item>
      <title>The memory layer that never calls an LLM: what that buys, and what it costs</title>
      <dc:creator>Giulio D'Erme</dc:creator>
      <pubDate>Thu, 30 Jul 2026 20:43:23 +0000</pubDate>
      <link>https://dev.to/gde03/the-memory-layer-that-never-calls-an-llm-what-that-buys-and-what-it-costs-12ch</link>
      <guid>https://dev.to/gde03/the-memory-layer-that-never-calls-an-llm-what-that-buys-and-what-it-costs-12ch</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 4 of **The Answerability Problem&lt;/em&gt;&lt;em&gt;, and the one that isn't about abstention. &lt;a href="https://dev.to/gde03/the-ai-memory-benchmark-everyone-quotes-forbids-saying-i-dont-know-o1n"&gt;Parts 1–3&lt;/a&gt; argued that the field measures the wrong half and that my own system hits a wall on the right half. This part is the ledger: what RE-call is, what it loses at, and why you might still want the thing that loses. Code: &lt;a href="https://github.com/GiulioDER/RE-call" rel="noopener noreferrer"&gt;RE-call&lt;/a&gt; (MIT).&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Three parts of "here is what doesn't work" earn one part of "here is what this is for."&lt;/p&gt;

&lt;p&gt;So, plainly: &lt;strong&gt;on BEAM's 1M-token bucket, Mem0 scores better than RE-call on categories I care about.&lt;/strong&gt; I'm not going to bury that, and I'm not going to explain it away. I'm going to tell you exactly why it happens, what that accuracy costs, and let you decide which side of the trade you want, because for a lot of people the answer is not the obvious one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mechanism, in one sentence
&lt;/h2&gt;

&lt;p&gt;Mem0 calls an LLM when you &lt;em&gt;write&lt;/em&gt; a memory, and stores the distilled result. RE-call calls no LLM ever, and stores your raw turns.&lt;/p&gt;

&lt;p&gt;That single decision explains everything downstream: the wins, the losses, the bill, and where your data goes.&lt;/p&gt;

&lt;p&gt;The clearest place to see it is &lt;code&gt;temporal_reasoning&lt;/code&gt;, my second-worst BEAM category: &lt;strong&gt;0.408 against Mem0's 0.567.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One disclosure before that number goes anywhere, because it changes what it means: &lt;strong&gt;that cell was measured with reranking off.&lt;/strong&gt; The BEAM harness takes a &lt;code&gt;--reranker&lt;/code&gt; flag and defaults it to &lt;code&gt;none&lt;/code&gt;, and the run didn't pass it. Reranking is the largest retrieval gain in this project. &lt;a href="https://dev.to/gde03/relevance-is-not-answerability-six-signals-and-none-of-them-beat-plain-cosine-2f14"&gt;Part 2&lt;/a&gt; measures it at hit@5 0.671 → 0.777 on LOCOMO, improving &lt;em&gt;every&lt;/em&gt; category including the multi-hop floor I'd predicted it wouldn't touch. So 0.408 is my &lt;strong&gt;shipped default&lt;/strong&gt;, not my best configuration, and the reranked cell is &lt;strong&gt;unmeasured&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I'm not going to tell you which way that would go. The last time I predicted a category wouldn't benefit from reranking I was wrong, and guessing here would be the same error with a bigger number attached.&lt;/p&gt;

&lt;p&gt;What I &lt;em&gt;can&lt;/em&gt; say is that reranking is unlikely to be the whole story, because the diagnosis isn't a ranking failure. Of seven badly-lost questions only one had empty retrieval; five were answered confidently and wrongly. They are all the same shape, "how many days between A and B", and in the five it answered, my system used the wrong &lt;em&gt;instance&lt;/em&gt; of a date, while getting the arithmetic on those wrong dates right every single time. That is what makes this a selection problem and not a reasoning one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;gold&lt;/th&gt;
&lt;th&gt;our answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;25 Mar → 1 Apr = 7 days&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;14 days&lt;/strong&gt;, using the &lt;em&gt;updated&lt;/em&gt; deadline of 15 Apr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25 Mar → 10 Apr = 16 days&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;26 days&lt;/strong&gt;, using a &lt;em&gt;different&lt;/em&gt; viewing on 15 Mar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15 Feb → 20 Feb = 5 days&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0 days&lt;/strong&gt;, using 10 Jan, the date the deadline was &lt;em&gt;set&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Mem0 gets these right because its stored memory is one distilled line, &lt;em&gt;"Sprint 1 deadline: February 15, 2024"&lt;/em&gt;. Mine is the same date scattered across many raw turns in different roles: when it was set, when it was revised, when someone mentioned it in passing.&lt;/p&gt;

&lt;p&gt;&lt;del&gt;&lt;strong&gt;This is the one category where LLM distillation at ingest is genuinely the better architecture&lt;/strong&gt;, and no retrieval-side change I can afford replicates it. It's recorded in the repo as a known limit, not an open task.&lt;/del&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Correction, added after publication.&lt;/strong&gt; That paragraph claimed more than I had measured, and a reader's comment exposed it. The cell was measured with my own temporal layer structurally unable to fire, which is not the same as having tried it, and "recency is falsified" rules out one family of fix, not all of them. The obvious successor is unsound too, for a reason I did not expect: a validity window records when a turn was &lt;em&gt;said&lt;/em&gt;, not when the event &lt;em&gt;happened&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So I enumerated the seven questions instead of describing them; they are a fixture in the repo now. Of the five my system answered, the mechanisms are &lt;strong&gt;two&lt;/strong&gt; wrong instances of a similar event, &lt;strong&gt;one&lt;/strong&gt; genuine revision, &lt;strong&gt;one&lt;/strong&gt; field &lt;em&gt;value&lt;/em&gt; confused with the time it was &lt;em&gt;asserted&lt;/em&gt;, and &lt;strong&gt;one&lt;/strong&gt; event time confused with mention time. Supersession therefore reaches &lt;strong&gt;one of five&lt;/strong&gt;: four problems wearing one category name, none of them solved. Stated as narrowly as it was made, that is a hand reading of five answers from the run artifact alone, and five items is a list, not a rate.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So: they're better at this, for a real reason. Whether it is cheaply fixable is now an open question rather than a closed one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What that accuracy costs
&lt;/h2&gt;

&lt;p&gt;Here's the other side of the same decision, measured on the identical benchmark workload:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;RE-call&lt;/th&gt;
&lt;th&gt;Mem0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LLM calls to build the memory&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;272&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tokens&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2.6 M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$7.29&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ingest wall clock&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;67 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;288 s (&lt;strong&gt;~4.3× slower&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;where your documents go&lt;/td&gt;
&lt;td&gt;your Postgres&lt;/td&gt;
&lt;td&gt;an LLM provider, once per memory written&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That $7.29 is for &lt;em&gt;one&lt;/em&gt; benchmark's memory. It is not a subscription. It is a per-memory marginal cost that scales with everything you ever write. RE-call's write path calls no model, so its marginal cost is &lt;strong&gt;$0 at any scale, on any model, forever&lt;/strong&gt;. There is no pricing change upstream that can alter that number.&lt;/p&gt;

&lt;p&gt;The ingest gap is the same fact wearing a stopwatch: an extraction call per session is a network round-trip per session.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Retrieve latency, 77 ms against 104 ms, I report as **directional only&lt;/em&gt;&lt;em&gt;. The repeated-query bootstrap CI is optimistic and the two backends differ, so I won't lean on it.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that isn't a number
&lt;/h2&gt;

&lt;p&gt;Your data never leaves your infrastructure. Local embeddings, the PostgreSQL you already run, and it works &lt;strong&gt;offline&lt;/strong&gt;: on an air-gapped box, in a privacy-bound environment, under a DPA that doesn't have room for another subprocessor.&lt;/p&gt;

&lt;p&gt;A memory layer that calls an LLM per write cannot offer that, structurally. Not because anyone is careless, but because the architecture requires sending the content out to distil it.&lt;/p&gt;

&lt;p&gt;If you're a solo developer, that's a cost story. If you're a company holding customer conversations, it's a procurement story, and it's usually the one that decides the question before accuracy is ever discussed.&lt;/p&gt;

&lt;p&gt;The cloud embedder is available as a &lt;strong&gt;measured option&lt;/strong&gt; rather than a default, and the repo prices it honestly: it wins on 16 of 17 held-out corpora, median +0.059 hit@5, and it means every document and every query leaves your machine, at 246 ms p50 against 45 ms local. You get the numbers and you choose. That's the pattern for everything here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everything is a switch, and both settings are measured
&lt;/h2&gt;

&lt;p&gt;This is the part I undersold for five articles.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Embedder&lt;/strong&gt;: a hashing model that needs no download, &lt;code&gt;bge-small&lt;/code&gt;, &lt;code&gt;bge-large&lt;/code&gt;, Voyage, or anything OpenAI-compatible. Measured across 17 corpora, with the rule for when paying is worth it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reranking&lt;/strong&gt;: off by default, one flag on. It's the largest retrieval gain in the project (&lt;strong&gt;hit@5 0.671 → 0.777&lt;/strong&gt;) and it costs ~1,050 ms/query, about 4× wall clock. Both halves are published, and the decision is yours: answering a human, ~1 s is invisible next to the LLM call that follows; serving high-volume automated retrieval, it dominates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entailment abstention&lt;/strong&gt;: off by default, and &lt;a href="https://dev.to/gde03/relevance-is-not-answerability-six-signals-and-none-of-them-beat-plain-cosine-2f14"&gt;Part 2&lt;/a&gt; is 2,000 words on why it doesn't rescue the hard case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The whole stack&lt;/strong&gt;: Postgres and pgvector. No separate vector database, no queue, no second store to keep consistent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd rather ship a flag with two measured settings than a default with a marketing claim.&lt;/p&gt;

&lt;h2&gt;
  
  
  And the risk that doesn't show up in an accuracy column
&lt;/h2&gt;

&lt;p&gt;BEAM has an &lt;code&gt;abstention&lt;/code&gt; category, questions whose correct answer is "that isn't in here." I scored &lt;strong&gt;Mem0's own published answers&lt;/strong&gt; on it, with &lt;strong&gt;Mem0's own judge&lt;/strong&gt;. n=70:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mem0 did&lt;/th&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;mean score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;abstained&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.974&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;answered anyway&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.016&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The category is near-perfectly binary, and it is testing exactly one thing: does the system invent an answer when the evidence isn't there. &lt;strong&gt;It invents one 46% of the time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One real example: asked about user feedback that was never recorded, it answered &lt;em&gt;"User testing showed a positive response: the dynamic language switching feature achieved a 90% satisfaction rate."&lt;/em&gt; The corpus does contain &lt;em&gt;"achieving a 90% satisfaction rate is a strong start"&lt;/em&gt;, the &lt;strong&gt;assistant&lt;/strong&gt; speculating, which retrieval surfaced and the answerer read as fact.&lt;/p&gt;

&lt;p&gt;I want to be precise about what I'm claiming, because this is where it would be easy to overreach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Their published score is &lt;strong&gt;honest&lt;/strong&gt;. I reproduced their BEAM cell to &lt;strong&gt;0.0005&lt;/strong&gt; (0.6414 against 0.6409). Nothing is being faked.&lt;/li&gt;
&lt;li&gt;The benchmark is &lt;strong&gt;not&lt;/strong&gt; backfitted to them. A benchmark tuned to flatter a vendor would not expose a 46% fabrication rate on that vendor.&lt;/li&gt;
&lt;li&gt;And my own number here is not a triumph. On the same category my shipped policy abstains correctly &lt;strong&gt;23.3%&lt;/strong&gt; of the time at a &lt;strong&gt;9.3%&lt;/strong&gt; false-abstain cost, measured on 30 unanswerable and 270 answerable questions across conversations 0–14, at &lt;strong&gt;$0&lt;/strong&gt;, because that probe needs no LLM. Same configuration caveat as above: reranker off. I tested four stricter policies and &lt;strong&gt;every one of them nets worse&lt;/strong&gt; on BEAM's 9:1 answerable-to-unanswerable mix. The shipped policy is already the best of the five, which is the opposite of what I predicted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So neither of us solves this. The difference is the shape of the failure: a system that fabricates a plausible satisfaction rate is a different kind of liability from one that returns nothing. Depending on what you're building, "slightly less accurate" and "confidently invents a statistic" are not two points on one scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things wrong with the benchmark itself
&lt;/h2&gt;

&lt;p&gt;Worth saying because they cut against my own story as much as anyone's.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BEAM's unanswerable questions score &lt;em&gt;higher&lt;/em&gt; than its answerable ones&lt;/strong&gt;: median top-1 cosine &lt;strong&gt;0.676&lt;/strong&gt; against &lt;strong&gt;0.641&lt;/strong&gt;. They're adversarially constructed, and a lexical-coverage signal sharing no mathematics with cosine inverts in the same direction. So &lt;strong&gt;every BEAM figure, theirs and mine, is an upper bound on difficulty, not an estimate of deployed behaviour.&lt;/strong&gt; On an ordinary corpus, where unanswerable means genuinely absent, plain cosine separates at AUC 0.780.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The scoring is 9:1 against withholding.&lt;/strong&gt; Abstention is ~10% of BEAM, so a policy that abstains more gains on 30 questions and loses on 270. That is not a complaint about the benchmark. Its abstention category is well built. It means &lt;strong&gt;an abstention claim cannot be made through BEAM's aggregate&lt;/strong&gt;, no matter how good the policy gets. The field needs a metric that prices a false answer against a withheld one, and this isn't it.&lt;/p&gt;

&lt;p&gt;Two smaller corrections that arm produced: BEAM's harness pins &lt;strong&gt;gpt-5&lt;/strong&gt; as answerer and judge (not gpt-4o), and the published &lt;strong&gt;64.1 is a mean rubric-nugget score, not a pass rate&lt;/strong&gt;: the pass rate for the same run is 70.14%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I'm not reporting: a paired BEAM aggregate.&lt;/strong&gt; I have per-category cells and the abstention probe; I do not have a like-for-like total, so there isn't one in the repo and there isn't one here. An earlier draft of this project did fuse two unrelated BEAM numbers into a headline that read well and did not exist. Once burned.&lt;/p&gt;

&lt;h2&gt;
  
  
  So which one should you use?
&lt;/h2&gt;

&lt;p&gt;Genuinely depends, and I'll say it against my own interest:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use the LLM-distilling architecture&lt;/strong&gt; if your questions are heavy on temporal and multi-hop reasoning over dense material, a strong reader is doing the answering, per-memory cost is not a constraint, and sending content to a model provider is fine for you. It is better at that, measurably, and I've shown you the category where it beats me and why.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use this one&lt;/strong&gt; if the marginal cost of a memory has to be zero, your data can't leave, you need it to work offline, you want the retrieval path to be inspectable and switchable rather than a model's opinion, or if a system that confidently invents a satisfaction rate is a worse outcome for you than one that says nothing.&lt;/p&gt;

&lt;p&gt;That's the trade. It isn't "we're better." It's that an LLM in the write path buys a better representation and charges you money, latency, and your data for it, and for a large number of real deployments that's the wrong purchase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the series ends up
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.to/gde03/the-ai-memory-benchmark-everyone-quotes-forbids-saying-i-dont-know-o1n"&gt;Parts 1–3&lt;/a&gt; said the field measures accuracy on questions that have answers, that the standard harness for the most-quoted benchmark &lt;em&gt;forbids&lt;/em&gt; abstaining, that when I built the missing metric my own system scored zero, and finally that the two public benchmarks disagree because each samples one point of a hidden axis, which turns a yes/no question into a coordinate.&lt;/p&gt;

&lt;p&gt;This part says the rest of it: that the same design which loses &lt;code&gt;temporal_reasoning&lt;/code&gt; is the one that costs $0, ingests 4.3× faster, and never sends a document anywhere, and that the incumbent's better aggregate comes with a 46% fabrication rate on the questions where the honest answer is silence.&lt;/p&gt;

&lt;p&gt;Both of those are true at once. Publishing only the flattering half is the thing this whole series is against.&lt;/p&gt;

&lt;h2&gt;
  
  
  For completeness: the paired contest
&lt;/h2&gt;

&lt;p&gt;On LOCOMO, same questions, same generator, same judge, only the memory differs. Paired McNemar over per-question outcomes, n=1,540:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;generator&lt;/th&gt;
&lt;th&gt;judge&lt;/th&gt;
&lt;th&gt;RE-call&lt;/th&gt;
&lt;th&gt;Mem0&lt;/th&gt;
&lt;th&gt;paired p&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gpt-4o-mini&lt;/td&gt;
&lt;td&gt;gpt-4o-mini&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.416&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.378&lt;/td&gt;
&lt;td&gt;0.0059&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gpt-4o-mini&lt;/td&gt;
&lt;td&gt;gpt-4o&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.466&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.412&lt;/td&gt;
&lt;td&gt;0.00018&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gpt-4o&lt;/td&gt;
&lt;td&gt;gpt-4o&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.484&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.444&lt;/td&gt;
&lt;td&gt;0.0065&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Holm–Bonferroni across all five cells run (largest adjusted p = 0.012), and it holds on the 1,369 questions where both judges agree (0.440 vs 0.399, p = 0.006).&lt;/p&gt;

&lt;p&gt;Two things I'll say louder than the result. &lt;strong&gt;The lead is a property of the reader, not a universal fact&lt;/strong&gt;. The margin shrinks as the generator strengthens and &lt;em&gt;reverses on Claude Sonnet&lt;/em&gt; (0.565 vs 0.608, n=584, a generator run after pre-registration and labelled as such). And &lt;strong&gt;note the absolute numbers&lt;/strong&gt;: 0.416–0.484, nothing like the 92.5 in the headlines, because a paired protocol with a strict judge and a matched retrieval budget measures something the leniency stack in &lt;a href="https://dev.to/gde03/the-ai-memory-benchmark-everyone-quotes-forbids-saying-i-dont-know-o1n"&gt;Part 1&lt;/a&gt; does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  One more thing, and it's the reason any of this is checkable
&lt;/h2&gt;

&lt;p&gt;Everything above is a number I produced about my own system. You have no way to recompute it without buying inference.&lt;/p&gt;

&lt;p&gt;That bothered me enough to build the fix: a benchmark where &lt;strong&gt;every number is recomputable by a hostile party for $0, with no model in the loop&lt;/strong&gt;: no judge, no paid generator, git as the database, and disputing a result is a pull request. It has RE-call on its board with an unflattering score, and a deliberately fabricated submission committed as a fixture because it passes five of the six mechanical checks.&lt;/p&gt;

&lt;p&gt;That's its own post, coming next, and it's the one I'd most like you to act on.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Every figure: &lt;a href="https://github.com/GiulioDER/RE-call/blob/master/results/FINDINGS.md" rel="noopener noreferrer"&gt;&lt;code&gt;results/FINDINGS.md&lt;/code&gt;&lt;/a&gt; §9d–§9p, &lt;a href="https://github.com/GiulioDER/RE-call/blob/master/results/RESULTS.md" rel="noopener noreferrer"&gt;&lt;code&gt;results/RESULTS.md&lt;/code&gt;&lt;/a&gt; §9–§11. &lt;a href="https://github.com/GiulioDER/RE-call" rel="noopener noreferrer"&gt;RE-call&lt;/a&gt; is MIT, &lt;code&gt;pip install recall-rag&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>opensource</category>
      <category>discuss</category>
    </item>
    <item>
      <title>“Does your agent know what it doesn’t know?” has no answer. It has a coordinate.</title>
      <dc:creator>Giulio D'Erme</dc:creator>
      <pubDate>Thu, 30 Jul 2026 20:33:21 +0000</pubDate>
      <link>https://dev.to/gde03/does-your-agent-know-what-it-doesnt-know-has-no-answer-it-has-a-coordinate-1p4f</link>
      <guid>https://dev.to/gde03/does-your-agent-know-what-it-doesnt-know-has-no-answer-it-has-a-coordinate-1p4f</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 3 of **The Answerability Problem&lt;/em&gt;&lt;em&gt;. &lt;a href="https://dev.to/gde03/the-ai-memory-benchmark-everyone-quotes-forbids-saying-i-dont-know-o1n"&gt;Part 1&lt;/a&gt; showed the standard harness excluding the questions that test refusal, and my own system scoring 0.000 on them. &lt;a href="https://dev.to/gde03/relevance-is-not-answerability-six-signals-and-none-of-them-beat-plain-cosine-2f14"&gt;Part 2&lt;/a&gt; measured six candidate signals and found none that beat plain cosine. This part explains why both were asking a question that has no answer. Code and every number: &lt;a href="https://github.com/GiulioDER/RE-call" rel="noopener noreferrer"&gt;RE-call&lt;/a&gt; (MIT).&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Two public benchmarks disagree about whether memory systems can abstain.&lt;/p&gt;

&lt;p&gt;On LOCOMO's adversarial category my system scores &lt;strong&gt;0.000&lt;/strong&gt; out of 446, and every lever I ship reaches a usable catch rate only by refusing 60% of legitimate questions. On BEAM's abstention category the same class of system does something, and the incumbent's own published answers abstain correctly 38 times out of 70.&lt;/p&gt;

&lt;p&gt;For weeks I treated that as two data points about one capability and tried to reconcile them. It is not two data points. It is two samples of a hidden variable, each reported as a scalar.&lt;/p&gt;

&lt;p&gt;Name the variable and the disagreement dissolves.&lt;/p&gt;

&lt;h2&gt;
  
  
  The variable is how far the question sits from what the corpus contains
&lt;/h2&gt;

&lt;p&gt;Ask a conversation log what Caroline realised, when it was Melanie who realised something and Caroline is right there discussing something else. The answer is absent by inches. Now ask the same corpus about restaurant reservations, when it contains none at all. Also absent, by a mile.&lt;/p&gt;

&lt;p&gt;Both are "unanswerable." Reporting one number for both is like reporting one number for "can it see" without saying how far away you put the object.&lt;/p&gt;

&lt;p&gt;So we built a benchmark whose x-axis is that distance. Questions are generated at controlled &lt;strong&gt;excision distances&lt;/strong&gt; from their own gold evidence: at &lt;code&gt;r=0.00&lt;/code&gt; only the single supporting turn is removed, at &lt;code&gt;r=1.00&lt;/code&gt; the whole surrounding topic is gone, with rungs in between. The document-id lists are frozen into the released manifest, so a system under test never computes its own distances. It just answers, and the harness already knows where each question sits.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result is a curve, and the shape is the finding
&lt;/h2&gt;

&lt;p&gt;Discrimination between answerable and unanswerable questions, by rung. AUC, n=200 per rung:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;rung&lt;/th&gt;
&lt;th&gt;&lt;code&gt;r=0.00&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;r=0.25&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;r=0.50&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;r=0.75&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;r=1.00&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AUC&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.567&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.784&lt;/td&gt;
&lt;td&gt;0.841&lt;/td&gt;
&lt;td&gt;0.921&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.968&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At the far rung it is nearly perfect. At the near rung it is &lt;strong&gt;barely above chance&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So "does your memory know when it doesn't know?" has no answer. The honest reply is a question back: &lt;em&gt;how far away?&lt;/em&gt; A usable gate needs roughly 0.90, and this system crosses that between &lt;code&gt;r=0.50&lt;/code&gt; and &lt;code&gt;r=0.75&lt;/code&gt;, which is to say only once &lt;strong&gt;half to three-quarters of the topic has been deleted&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That curve also explains the benchmark disagreement. LOCOMO's adversarials are built by misattribution, right event and wrong speaker, so they sit near &lt;code&gt;r=0.00&lt;/code&gt;. BEAM's are adversarially constructed too and sit near zero as well; its unanswerable questions actually score &lt;em&gt;higher&lt;/em&gt; similarity than its answerable ones. Corpora where abstention appears to "work", bounded technical documents where the miss is a different subject entirely, sit at the far end. Nobody was wrong. Everybody was reporting one point and calling it a capability.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mechanism, and it is not a threshold problem
&lt;/h2&gt;

&lt;p&gt;Here is the part I did not expect, and the reason I no longer think this is fixable by tuning.&lt;/p&gt;

&lt;p&gt;Take each answerable question, delete its gold evidence, and measure how far the top retrieval score moves. Paired, same question, n=200. Mean change: &lt;strong&gt;−0.0111&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The mean is not the story. The distribution is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;130 of 200 questions: exactly zero change.&lt;/strong&gt; Deleting the answer moved the score not at all.&lt;/li&gt;
&lt;li&gt;64 decreased.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6 increased.&lt;/strong&gt; Removing the evidence made the corpus look &lt;em&gt;more&lt;/em&gt; relevant.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For roughly two thirds of these questions the top hit was never the gold evidence to begin with. The number a threshold gates on was not reading the answer. It was reading something else nearby, and it went on reading it after the answer was gone.&lt;/p&gt;

&lt;p&gt;You cannot threshold your way out of that. A threshold is a monotone rule on a score, and the score is not a measurement of the thing you want to decide about. That is why &lt;a href="https://dev.to/gde03/relevance-is-not-answerability-six-signals-and-none-of-them-beat-plain-cosine-2f14"&gt;Part 2's&lt;/a&gt; six signals clustered and all failed: they were six ways of reading the same wrong quantity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then we checked whether any of it was an artifact
&lt;/h2&gt;

&lt;p&gt;A curve measured on one embedder is a property of that embedder until shown otherwise. So the whole thing was re-run on &lt;code&gt;gte-base&lt;/code&gt;: different model family, 768 dimensions against 384, and a cosine range that does not overlap the first one at all (0.7620–0.9332 against 0.4945–0.8238).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;rung&lt;/th&gt;
&lt;th&gt;&lt;code&gt;r=0.00&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;r=0.25&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;r=0.50&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;r=0.75&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;r=1.00&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;bge-small, 384d&lt;/td&gt;
&lt;td&gt;0.567&lt;/td&gt;
&lt;td&gt;0.784&lt;/td&gt;
&lt;td&gt;0.841&lt;/td&gt;
&lt;td&gt;0.921&lt;/td&gt;
&lt;td&gt;0.968&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gte-base, 768d&lt;/td&gt;
&lt;td&gt;0.570&lt;/td&gt;
&lt;td&gt;0.794&lt;/td&gt;
&lt;td&gt;0.841&lt;/td&gt;
&lt;td&gt;0.925&lt;/td&gt;
&lt;td&gt;0.976&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Within 0.010 at every rung, identical at &lt;code&gt;r=0.50&lt;/code&gt;, and the 0.90 crossing falls in the same interval on both. The &lt;em&gt;shape&lt;/em&gt; survives a change of family, size and scale. The &lt;strong&gt;magnitudes do not&lt;/strong&gt;: the within-unanswerable deltas run about 2.1× smaller on &lt;code&gt;gte-base&lt;/code&gt;. That is exactly why the predictions were registered on sign, monotonicity and ordering rather than on size. Had the pre-registration said "reproduces −0.1100", a real effect would now be reading as a failure.&lt;/p&gt;

&lt;p&gt;One detail I checked rather than reported: both arms have 173 of 200 questions monotone, and that identical count is coincidence. The two sets are not the same questions, overlapping on 156. What is substantive is that &lt;strong&gt;90% of the questions monotone under one embedder are monotone under the other&lt;/strong&gt;, which is agreement per question rather than in aggregate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two admissions, because they are the useful part
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The first version of this benchmark passed, and the pass was garbage.&lt;/strong&gt; v1 reported a perfect +1.000 discrimination across the board, which I came close to publishing. At the largest excision the removal took out the entire conversation, and the slice under test &lt;em&gt;was&lt;/em&gt; that conversation, so the index was empty. It scored a perfect ability to detect that nothing was there, on nothing being there. Every non-empty contrast in that arm was flat, at most +0.003.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And I predicted the wrong shape.&lt;/strong&gt; My registered prediction was a step: some distance at which the system flips from confident to uncertain. It is a gradient, smooth and monotone, and the shipped binary abstention decision is blind to every bit of it.&lt;/p&gt;

&lt;p&gt;A third admission belongs to the pre-registration rather than to me, and it is the honest reading of the whole arm. Before running, it recorded: &lt;em&gt;if the gradient holds, the most likely reading is mundane, since any dense retriever's top-1 similarity falls as you delete the relevant documents.&lt;/em&gt; That is what happened. The mechanism is almost certainly generic to dense retrieval rather than a property of my system. The interesting content is not that the gradient exists. It is that &lt;strong&gt;a graded signal sits underneath a binary decision that cannot express it&lt;/strong&gt;, and that at the boundary itself the signal is worth almost nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The guard that cannot fire
&lt;/h2&gt;

&lt;p&gt;While measuring the above, the shipped abstention floor turned out to be inert.&lt;/p&gt;

&lt;p&gt;The default constant is a cosine of 0.50. On &lt;code&gt;bge-small&lt;/code&gt; the observed scores span [0.4945, 0.8238], so &lt;strong&gt;1 response in 1200&lt;/strong&gt; falls below it. On &lt;code&gt;gte-base&lt;/code&gt; the entire distribution sits above 0.76, and the answer is &lt;strong&gt;0 of 1200&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The guard my library ships, whose job is to withhold an answer, could not fire on either embedder. Not "fired rarely". Could not fire, because the constant sits below the whole distribution it is compared against.&lt;/p&gt;

&lt;p&gt;That is the fifth instance of this pattern I have found in a month, and the first one located in the shipped library rather than in a benchmark harness. It is embedder-specific in both directions: on a paid API embedder the same constant does the opposite, starving 23.3% of legitimate queries. One number, three behaviours, depending on a model choice it knows nothing about.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not establish
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not a quality comparison.&lt;/strong&gt; Nothing here says one embedder retrieves better than the other. Only the shape of the distance response was measured.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two families is not "embedder-independent".&lt;/strong&gt; It raises confidence, and says nothing about API embedders, where the same constant fails in the opposite direction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not a single-variable change.&lt;/strong&gt; Family, size and dimensionality moved together, so the agreement cannot be attributed to any one of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same corpus, no judge.&lt;/strong&gt; A shared-corpus artifact would reproduce here rather than be caught, and answer &lt;em&gt;correctness&lt;/em&gt; is unmeasured throughout.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I would like argued with
&lt;/h2&gt;

&lt;p&gt;The claim I am least sure of is the strong one: that &lt;strong&gt;abstention is not a property a memory system has, but a function of distance that has to be measured against an axis&lt;/strong&gt;, and that any single-number abstention score is therefore a category error, including the ones I published earlier in this series.&lt;/p&gt;

&lt;p&gt;If that is right, "does it know what it doesn't know?" should stop being asked, and every benchmark reporting one abstention number should report a curve instead.&lt;/p&gt;

&lt;p&gt;If it is wrong, the cheapest way to show me is a signal that separates at &lt;code&gt;r=0.00&lt;/code&gt;, where mine sits at 0.567 on two independent embedder families. The manifest is frozen and public, the harness is one command, and it costs nothing to run.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/gde03/the-memory-layer-that-never-calls-an-llm-what-that-buys-and-what-it-costs-12ch"&gt;Part 4&lt;/a&gt; is the ledger: what this architecture buys, what it costs, and where it loses on purpose.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The ladder, its pre-registrations and every artifact: &lt;a href="https://github.com/GiulioDER/RE-call" rel="noopener noreferrer"&gt;RE-call&lt;/a&gt;, &lt;code&gt;benchmarks/ladder/&lt;/code&gt; and &lt;code&gt;results/ladder/&lt;/code&gt;. Both verdicts, including the retracted v1, are committed rather than summarised.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>llm</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Relevance is not answerability: six signals, and none of them beat plain cosine</title>
      <dc:creator>Giulio D'Erme</dc:creator>
      <pubDate>Thu, 30 Jul 2026 20:31:08 +0000</pubDate>
      <link>https://dev.to/gde03/relevance-is-not-answerability-six-signals-and-none-of-them-beat-plain-cosine-2f14</link>
      <guid>https://dev.to/gde03/relevance-is-not-answerability-six-signals-and-none-of-them-beat-plain-cosine-2f14</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 2 of **The Answerability Problem&lt;/em&gt;&lt;em&gt;. &lt;a href="https://dev.to/gde03/the-ai-memory-benchmark-everyone-quotes-forbids-saying-i-dont-know-o1n"&gt;Part 1&lt;/a&gt; showed the standard harness excluding the questions that test refusal, my own system scoring 0.000, and every lever I ship failing. It asked whether any cheap signal can separate "relevant" from "answerable." This is the bake-off. Code: &lt;a href="https://github.com/GiulioDER/RE-call" rel="noopener noreferrer"&gt;RE-call&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;LongMemEval is the other public benchmark in this space, and unlike LOCOMO it names abstention as a first-class question type. 500 questions over a synthetic user's chat history: 470 answerable, &lt;strong&gt;30 whose answer was never mentioned&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;My retrieval does well on it. The right session is in the returned set &lt;strong&gt;97%&lt;/strong&gt; of the time.&lt;/p&gt;

&lt;p&gt;Then the trust layer refuses to answer &lt;strong&gt;48% of those&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Sit with the shape of that. The system finds the answer, holds it, and declines to use it, in nearly half of the cases where it succeeded. As a product that is worse than having no abstention layer at all: you have paid for retrieval quality and then thrown it away at the last gate.&lt;/p&gt;

&lt;p&gt;And it gets stranger. False-abstain &lt;strong&gt;rises as the problem gets easier&lt;/strong&gt;: 0.328 → 0.409 → 0.481 as I narrow the haystack, while the fitted threshold &lt;em&gt;falls&lt;/em&gt; (0.752 → 0.723 → 0.713). A smaller haystack means a better top hit means higher cosines all round, and every one of those higher cosines belongs to an unanswerable question too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The obvious diagnosis, tested first, and wrong
&lt;/h2&gt;

&lt;p&gt;Every engineer's first instinct here, mine included, is &lt;em&gt;the threshold is in the wrong place&lt;/em&gt;. So I measured the two distributions directly. Top-1 cosine over all 500 questions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;answerable (n=470)&lt;/th&gt;
&lt;th&gt;unanswerable (n=30)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;q05 / q25&lt;/td&gt;
&lt;td&gt;0.612 / 0.671&lt;/td&gt;
&lt;td&gt;n/a / 0.620&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;median&lt;/td&gt;
&lt;td&gt;0.723&lt;/td&gt;
&lt;td&gt;0.647&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;q75 / max&lt;/td&gt;
&lt;td&gt;0.774 / 0.938&lt;/td&gt;
&lt;td&gt;0.689 / 0.811&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;AUC 0.753.&lt;/strong&gt; The unanswerable range sits almost entirely &lt;em&gt;inside&lt;/em&gt; the answerable range.&lt;/p&gt;

&lt;p&gt;There is no threshold. The best one obtainable on these very samples scores balanced error &lt;strong&gt;0.285&lt;/strong&gt; against the shipped rule's &lt;strong&gt;0.305&lt;/strong&gt;, a 0.02 improvement, and that 0.285 is an &lt;em&gt;in-sample&lt;/em&gt; ceiling, so held-out the gap is smaller still. If you want false-abstain down to a shippable 0.05, you pay about &lt;strong&gt;0.78 false-confidence&lt;/strong&gt; for it.&lt;/p&gt;

&lt;p&gt;Recalibration was ruled out by measurement, not by argument. That distinction matters to me: "I thought about it and it wouldn't work" is how you end up wrong the way I was in Part 1.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six signals, one measurement
&lt;/h2&gt;

&lt;p&gt;If the threshold isn't the problem and the &lt;em&gt;signal&lt;/em&gt; is, then swap the signal. I measured six candidates on the same 500 questions and the same haystacks, differing in nothing but which quantity the gate reads:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;signal&lt;/th&gt;
&lt;th&gt;family&lt;/th&gt;
&lt;th&gt;AUC&lt;/th&gt;
&lt;th&gt;95% CI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;dense_top1&lt;/code&gt; &lt;em&gt;(shipped)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;relevance, bi-encoder&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.753&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;[0.680, 0.826]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rerank_top1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;relevance, cross-encoder&lt;/td&gt;
&lt;td&gt;0.742&lt;/td&gt;
&lt;td&gt;[0.666, 0.818]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hybrid_top1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;relevance, RRF&lt;/td&gt;
&lt;td&gt;0.739&lt;/td&gt;
&lt;td&gt;[0.663, 0.815]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;entail_max&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;answerability, QNLI&lt;/td&gt;
&lt;td&gt;0.648&lt;/td&gt;
&lt;td&gt;[0.557, 0.739]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;margin_1_5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;distributional&lt;/td&gt;
&lt;td&gt;0.579&lt;/td&gt;
&lt;td&gt;[0.479, 0.679]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ratio_1_5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;distributional&lt;/td&gt;
&lt;td&gt;0.545&lt;/td&gt;
&lt;td&gt;[0.442, 0.648]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Nothing beat the signal already shipping.&lt;/strong&gt; And the two informative rows are not the top one.&lt;/p&gt;

&lt;h3&gt;
  
  
  The cross-encoder is the finding
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;rerank_top1&lt;/code&gt; is a cross-encoder. It does not embed the query and the document separately and compare vectors. It reads them &lt;strong&gt;jointly&lt;/strong&gt;, with full attention between them, which is exactly the architecture you'd reach for if you wanted a model that could tell "this passage answers this question" from "this passage is about this topic."&lt;/p&gt;

&lt;p&gt;It ranks superbly. Below, it delivers the single largest retrieval gain I've measured; on this benchmark it reaches hit@5 &lt;strong&gt;0.970&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And it scores a topically-related session that does &lt;em&gt;not&lt;/em&gt; contain the answer just as highly as one that does.&lt;/p&gt;

&lt;p&gt;That's the result that reorganised my mental model. The cross-encoder isn't failing because it's small or badly trained. It's failing because it was trained for &lt;strong&gt;relevance&lt;/strong&gt;, and it is &lt;em&gt;excellent&lt;/em&gt; at relevance, and relevance is a different quantity from answerability. A session where the user discusses their job search at length is maximally relevant to "what company did the user say they were joining?" whether or not they ever named the company.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Relevance asks &lt;em&gt;is this about the same thing?&lt;/em&gt; Answerability asks &lt;em&gt;is the specific fact in here?&lt;/em&gt; Nothing in a relevance objective ever requires a model to learn the second one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  And the answerability model came in below plain cosine
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;entail_max&lt;/code&gt; is the QNLI judge, the one built-in that's actually trained on "does this answer this question," the one that came out of &lt;a href="https://dev.to/gde03/retrieval-augmented-self-recall-what-the-comments-taught-me-re-call-v03-42c1"&gt;reader comments on the last series&lt;/a&gt; and that I was pleased to ship.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;0.648.&lt;/strong&gt; Below cosine. At its own untuned boundary it scores false-confidence &lt;strong&gt;0.533&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And here's the part that makes it a real finding instead of a bad day: months earlier, on a completely different corpus the judge had never seen, I measured its residual near-miss false-confidence at &lt;strong&gt;0.50&lt;/strong&gt;. The bound transferred &lt;em&gt;exactly&lt;/em&gt;. It was never good enough for this workload; it was simply never tested against a workload that made that visible.&lt;/p&gt;

&lt;p&gt;Stacking it behind a lowered gate doesn't rescue it either:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;configuration&lt;/th&gt;
&lt;th&gt;false-abstain&lt;/th&gt;
&lt;th&gt;false-confident&lt;/th&gt;
&lt;th&gt;balanced&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;shipped cosine @0.713&lt;/td&gt;
&lt;td&gt;0.443&lt;/td&gt;
&lt;td&gt;0.167&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.305&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;judge alone @0.5&lt;/td&gt;
&lt;td&gt;0.321&lt;/td&gt;
&lt;td&gt;0.533&lt;/td&gt;
&lt;td&gt;0.427&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gate 0.600 + judge&lt;/td&gt;
&lt;td&gt;0.332&lt;/td&gt;
&lt;td&gt;0.433&lt;/td&gt;
&lt;td&gt;0.383&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gate 0.650 + judge&lt;/td&gt;
&lt;td&gt;0.381&lt;/td&gt;
&lt;td&gt;0.233&lt;/td&gt;
&lt;td&gt;0.307&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The best stack ties the plain threshold and costs you a model pass per query.&lt;/p&gt;

&lt;h2&gt;
  
  
  The error bar that nearly buried all of it
&lt;/h2&gt;

&lt;p&gt;Now the methodological part, which is the one I'd actually want to read.&lt;/p&gt;

&lt;p&gt;There are &lt;strong&gt;30&lt;/strong&gt; unanswerable questions. That's the benchmark's own class size, not a sampling choice, and I can't re-run it larger. Small-n results deserve suspicion, so I put an interval on the AUC.&lt;/p&gt;

&lt;p&gt;The first version used &lt;code&gt;sqrt(A(1-A)/n_min)&lt;/code&gt;, roughly &lt;strong&gt;0.08&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's wrong, and wrong in the direction that feels responsible. It uses only the smaller class and throws away the 470-sample one entirely, and it is not the standard error of an AUC in the first place. The Hanley &amp;amp; McNeil (1982) estimator, which accounts for both classes, gives &lt;strong&gt;0.037&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;My error bar was &lt;strong&gt;2.1× too wide.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The consequence wasn't cosmetic. At ±0.08, the 0.90 usability bar falls back &lt;em&gt;inside&lt;/em&gt; the interval of the best signal, and the finding degrades from "measured exclusion" to "inconclusive, needs more data." I would have published a shrug. With the correct estimator, the best signal's interval tops out at &lt;strong&gt;0.826&lt;/strong&gt; and the bar sits &lt;strong&gt;outside&lt;/strong&gt; it.&lt;/p&gt;

&lt;p&gt;That's the difference between &lt;em&gt;"I couldn't tell"&lt;/em&gt; and &lt;em&gt;"none of these signals is good enough, and here's the proof."&lt;/em&gt; Same data. The entire finding lived in the variance estimator.&lt;/p&gt;

&lt;p&gt;Two things came out of that, and both ship:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The estimator is now a library function, &lt;code&gt;recall.calibration.separability_interval&lt;/code&gt;, pinned by a test, so the published table and the library's own runtime certification read from &lt;strong&gt;one&lt;/strong&gt; implementation and cannot drift apart.&lt;/li&gt;
&lt;li&gt;Certification tests the interval's &lt;strong&gt;lower bound&lt;/strong&gt;, not the point estimate. At the 20-samples-per-class minimum the module accepts, a measured AUC of 0.95 carries a lower bound of 0.879: it clears the bar on the point and &lt;em&gt;has not established it&lt;/em&gt;. Certifying on the point would readmit, through small-sample noise, exactly the silent failure this whole exercise exists to expose.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm equally clear on what n=30 does &lt;strong&gt;not&lt;/strong&gt; buy me. The three relevance signals at 0.74–0.75 are &lt;strong&gt;not distinguishable from one another&lt;/strong&gt;. Their intervals overlap almost entirely, the ordering among them is noise, and no claim here rests on it. Anyone who reads that table as "dense beats rerank for abstention" is reading further than the data goes. The only conclusion the sample supports is the one I'm drawing: &lt;strong&gt;none of them reaches the ~0.90 a usable gate needs.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  So what is the actual shape of the problem?
&lt;/h2&gt;

&lt;p&gt;Three structurally different relevance signals cluster at 0.74–0.75. The one model in the lineup trained on answerability lands below all of them. Two distributional signals land below that. A stronger judge, tested separately, moves along the curve without lifting it.&lt;/p&gt;

&lt;p&gt;The conclusion I'd have drawn a week ago is the tempting one: &lt;em&gt;answerability is not recoverable from the retrieval geometry at all.&lt;/em&gt; I'm not drawing it, because I ran the control that tests it and it came back the other way.&lt;/p&gt;

&lt;p&gt;Build an &lt;strong&gt;ordinary&lt;/strong&gt; unanswerable set instead of an adversarial one, mechanically and with no labelling. Take a 787-document corpus, index 657 of them, hold 120 out, and use each document's own summary line as a query. Queries for indexed documents are answerable; queries for held-out documents are unanswerable because the document is genuinely absent. On that set, plain cosine separates at &lt;strong&gt;AUC 0.780&lt;/strong&gt;, and you can abstain on half the unanswerable questions for a &lt;strong&gt;13.7%&lt;/strong&gt; false-abstain cost. That is not a usable gate by the 0.90 standard, but it is nothing like the coin flip the benchmarks produce.&lt;/p&gt;

&lt;p&gt;So the honest statement is narrower and more useful:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Similarity separates answerable from unanswerable when the unanswerable question is about something genuinely absent. It fails, and inverts, when the unanswerable question is &lt;em&gt;constructed&lt;/em&gt; to be adjacent.&lt;/strong&gt; LOCOMO's cat5, LongMemEval's unanswerable class and BEAM's abstention split are all the second kind, by design.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The inversion is the part worth staring at. On BEAM the unanswerable questions score &lt;strong&gt;higher&lt;/strong&gt; than the answerable ones, and a lexical-coverage signal sharing no mathematics with cosine inverts in the &lt;em&gt;same&lt;/em&gt; direction (0.741 unanswerable against 0.717 answerable). Two unrelated signals failing identically points at the questions, not the retriever.&lt;/p&gt;

&lt;p&gt;Which reframes what those benchmarks measure. &lt;strong&gt;They are upper bounds on difficulty, not estimates of deployed behaviour&lt;/strong&gt;, and reporting an adversarial abstention score as if it described production is its own kind of over-claim, one I was two paragraphs away from making.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/gde03/does-your-agent-know-what-it-doesnt-know-has-no-answer-it-has-a-coordinate-1p4f"&gt;Part 3&lt;/a&gt; is where that boundary gets located properly, on a continuous axis and with a coordinate, and where the real defect turns out to live: not in the signal, and not in the threshold, but in what the score reads.&lt;/p&gt;

&lt;h3&gt;
  
  
  The counterweight, folded in
&lt;/h3&gt;

&lt;p&gt;The obvious objection is that these signals are all weak because &lt;em&gt;retrieval&lt;/em&gt; is weak. It isn't, and I can price that precisely: turning on the cross-encoder reranker moves hit@5 from &lt;strong&gt;0.671 to 0.777&lt;/strong&gt; at n=1,536, roughly twice the largest embedder effect in this project, with intervals disjoint from baseline through k=10. Three checks say the gain is real: hit@20 barely moves (0.855 → 0.870), as reordering a fixed pool requires; the gain decays with depth exactly as the mechanism predicts (+0.155 at k=1 → +0.016 at k=20); and an unrelated cross-encoder with 12× the parameters lands &lt;em&gt;within noise&lt;/em&gt; at 6.3× the cost, so the effect belongs to &lt;strong&gt;reranking&lt;/strong&gt;, not to a model.&lt;/p&gt;

&lt;p&gt;Abstention across all three rerank arms: &lt;strong&gt;0.00, 0.00, 0.00.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The best retrieval work in the project moved honesty by nothing. Reranking reorders what retrieval returned; it never touches whether what came back contains an answer. &lt;strong&gt;Your retrieval-quality metric and your honesty metric are not correlated, and improving the first will feel like improving the second.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/gde03/does-your-agent-know-what-it-doesnt-know-has-no-answer-it-has-a-coordinate-1p4f"&gt;Part 3&lt;/a&gt; is where this stops being a wall and becomes a measurement: the two public benchmarks disagree about abstention for a reason, and naming it turns a yes/no question into a coordinate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question for the comments:&lt;/strong&gt; if you've built a production RAG gate that decides &lt;em&gt;"the answer isn't in what I retrieved"&lt;/em&gt;, what does it read, and &lt;strong&gt;have you measured it on unanswerable questions that aren't adversarial?&lt;/strong&gt; That distinction turned out to be the whole ballgame here, and I nearly published the stronger claim without it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Full tables, intervals, and the six-signal comparison: &lt;a href="https://github.com/GiulioDER/RE-call/blob/master/results/FINDINGS.md" rel="noopener noreferrer"&gt;&lt;code&gt;FINDINGS.md&lt;/code&gt; §10b&lt;/a&gt;, &lt;a href="https://github.com/GiulioDER/RE-call/blob/master/results/RESULTS.md" rel="noopener noreferrer"&gt;&lt;code&gt;RESULTS.md&lt;/code&gt; §8&lt;/a&gt;. The LongMemEval arm is flagged in-repo as the weakest evidence in the file, and why: it was measured pre-fix, so its retrieval rows are a dense-only lower bound. The abstention conclusion does not depend on that.&lt;/em&gt; Next: &lt;strong&gt;Part 3, "Does your agent know what it doesn't know?" has no answer. It has a coordinate.&lt;/strong&gt;*&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>machinelearning</category>
      <category>discuss</category>
    </item>
    <item>
      <title>The AI-memory benchmark everyone quotes forbids saying “I don't know”</title>
      <dc:creator>Giulio D'Erme</dc:creator>
      <pubDate>Thu, 30 Jul 2026 20:14:27 +0000</pubDate>
      <link>https://dev.to/gde03/the-ai-memory-benchmark-everyone-quotes-forbids-saying-i-dont-know-o1n</link>
      <guid>https://dev.to/gde03/the-ai-memory-benchmark-everyone-quotes-forbids-saying-i-dont-know-o1n</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 1 of **The Answerability Problem&lt;/em&gt;&lt;em&gt;. A follow-on from &lt;a href="https://dev.to/gde03/retrieval-augmented-self-recall-the-rag-problem-nobody-talks-about-2n0n"&gt;Retrieval-Augmented Self-Recall&lt;/a&gt;. That series argued agent memory needs calibrated abstention. This one is what happened when I actually tried to measure it. Code and every number: &lt;a href="https://github.com/GiulioDER/RE-call" rel="noopener noreferrer"&gt;RE-call&lt;/a&gt; (MIT).&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;There's a benchmark war going on in AI memory, and it's a healthy one.&lt;/p&gt;

&lt;p&gt;Mem0's paper (arXiv:2504.19413, Table 2) reports an LLM-as-judge score of &lt;strong&gt;J=66.88&lt;/strong&gt; for itself against &lt;strong&gt;65.99&lt;/strong&gt; for a competitor, Zep. Zep published a rebuttal disputing the methodology and the scores attributed to it. Mem0's later announcement of a new algorithm puts it at &lt;strong&gt;92.5&lt;/strong&gt; on LOCOMO and &lt;strong&gt;94.4&lt;/strong&gt; on LongMemEval. People argued about all of it. That's how you know the numbers matter to someone.&lt;/p&gt;

&lt;p&gt;But both sides are fighting over the same axis: &lt;strong&gt;of the questions the memory can answer, how many does it get right?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;LOCOMO has five question categories. Four of them are that axis. The fifth is 446 questions, &lt;strong&gt;22.5% of the benchmark&lt;/strong&gt;, that look answerable and aren't. They name a real person and a real topic from the conversation, then ask about something that person never said. Right event, wrong speaker. The correct answer is &lt;em&gt;"that isn't in here."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I wanted to know how the incumbents score on those 446. So I went and read the harness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two lines of code
&lt;/h2&gt;

&lt;p&gt;Mem0's published benchmark harness (&lt;code&gt;mem0ai/memory-benchmarks&lt;/code&gt;) contains this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;CATEGORIES_TO_EVALUATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Category 5, all 446 adversarial questions, is excluded from scoring entirely.&lt;/p&gt;

&lt;p&gt;And the answer prompt contains this instruction:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;NEVER say "not specified" … COMMIT AND ANSWER&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So it isn't that abstention goes unmeasured. The harness removes the questions that would test it, and then instructs the model not to do it on the ones that remain.&lt;/p&gt;

&lt;p&gt;I want to be careful here, because there's a cheap version of this observation and I'm not making it. Excluding cat5 is a &lt;em&gt;defensible&lt;/em&gt; choice if what you're measuring is answer quality: a refusal scores zero on an answerable question either way, and mixing the classes muddies the metric. The "COMMIT AND ANSWER" instruction reduces variance from a model that hedges. Neither line is a scandal.&lt;/p&gt;

&lt;p&gt;What they are, together, is a &lt;strong&gt;scope&lt;/strong&gt;. The number that comes out the other end is answer accuracy on questions that have answers, and it is structurally incapable of telling you anything about what the system does when the answer isn't there. That's fine, as long as everyone reads it that way. In practice it gets read as "how good is this memory."&lt;/p&gt;

&lt;h2&gt;
  
  
  The leniency stack underneath
&lt;/h2&gt;

&lt;p&gt;Once I was in the harness I kept reading, and the abstention exclusion turns out to be the &lt;em&gt;fourth&lt;/em&gt;-most interesting thing in it. The judge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;marks an answer &lt;strong&gt;CORRECT&lt;/strong&gt; if it matches &lt;strong&gt;one&lt;/strong&gt; item out of an N-item gold list;&lt;/li&gt;
&lt;li&gt;tolerates &lt;strong&gt;±14 days&lt;/strong&gt; on dates and &lt;strong&gt;±50%&lt;/strong&gt; on durations;&lt;/li&gt;
&lt;li&gt;is instructed to use the evidence &lt;strong&gt;"only to ACCEPT answers, never to reject them."&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;and hands the reader up to &lt;strong&gt;200 memories per question&lt;/strong&gt;, roughly 7k tokens, with no retrieval budget.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An independent audit (&lt;code&gt;dial481/locomo-audit&lt;/code&gt;) measured that judge &lt;strong&gt;over-accepting 62.8% of deliberately wrong answers&lt;/strong&gt;. The same audit found LOCOMO's answer key is partly corrupted, which caps the honest achievable ceiling somewhere near &lt;strong&gt;93.6&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Sit with that for a second. A published &lt;strong&gt;92.5&lt;/strong&gt; is not "nearly perfect." It is &lt;em&gt;at the ceiling of a benchmark whose ceiling is a data-quality artifact&lt;/em&gt;, produced by a judge that accepts nearly two thirds of answers known to be wrong, on the 77.5% of questions that were kept.&lt;/p&gt;

&lt;p&gt;I am not claiming anyone cheated. I'm claiming the number is load-bearing for a conclusion it cannot support, and that this is true of every published figure on this benchmark, including any I might publish.&lt;/p&gt;

&lt;h2&gt;
  
  
  What would a metric that sees abstention even look like?
&lt;/h2&gt;

&lt;p&gt;Here's the part that took me longest, and it's the part I'd most like pushback on.&lt;/p&gt;

&lt;p&gt;The obvious metric is "how often does it correctly refuse?", which I will call &lt;strong&gt;adversarial-abstain&lt;/strong&gt;. It is completely useless on its own, because I can score &lt;strong&gt;1.00&lt;/strong&gt; on it in one line: refuse everything. A memory that always says "I don't know" is perfectly honest and perfectly worthless.&lt;/p&gt;

&lt;p&gt;The mirror metric is &lt;strong&gt;false-abstain&lt;/strong&gt;: how often it refuses a question it could have answered. Also gameable on its own, in the opposite direction: never abstain and you score a perfect 0.000. Which, as it happens, is exactly what "COMMIT AND ANSWER" instructs.&lt;/p&gt;

&lt;p&gt;So neither column means anything alone. The quantity that can't be gamed by a degenerate policy is the &lt;strong&gt;difference&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;discrimination = adversarial-abstain − false-abstain&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Refuse everything: 1.00 − 1.00 = 0. Refuse nothing: 0.00 − 0.00 = 0. You only score above zero by &lt;em&gt;telling the two classes apart&lt;/em&gt;, which is the actual capability in question. It's the same shape as a balanced-accuracy or Youden's J, and I make no claim to having invented it; the claim is that nobody in this space is reporting it, and it is cheap to report.&lt;/p&gt;

&lt;p&gt;So I built the harness. It's a couple of hundred lines, it runs on the public LOCOMO file, and it ships in the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sLO&lt;/span&gt; https://raw.githubusercontent.com/snap-research/locomo/main/data/locomo10.json
python &lt;span class="nt"&gt;-m&lt;/span&gt; recall.eval.locomo_abstention &lt;span class="nt"&gt;--data&lt;/span&gt; locomo10.json &lt;span class="nt"&gt;--answerable-sample&lt;/span&gt; 40
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I ran it against my own system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero
&lt;/h2&gt;

&lt;p&gt;RE-call, default configuration, on all 446 adversarial questions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Adversarial abstain ↑&lt;/th&gt;
&lt;th&gt;Answerable false-abstain ↓&lt;/th&gt;
&lt;th&gt;discrimination&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;default&lt;/td&gt;
&lt;td&gt;0.000 [0.00, 0.01]&lt;/td&gt;
&lt;td&gt;0.000 [0.00, 0.01]&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.000&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Zero out of 446. Not "worse than I hoped". It is &lt;strong&gt;the exact score of a system with no abstention mechanism at all&lt;/strong&gt;, achieved by a system whose entire published thesis is calibrated abstention. I have spent months writing about this. I have a trust layer, a calibrated cosine threshold, and an entailment judge, and on the hardest public instance of the problem they collectively did nothing.&lt;/p&gt;

&lt;p&gt;I'd planned a different article. The draft that sat in this folder for two weeks was called &lt;em&gt;"The one question every AI-memory benchmark skips"&lt;/em&gt; and its argument was: they don't measure this, I do, here's my score. I killed it, because the honest version of the finding is not that I found a gap in the field. It's that &lt;strong&gt;I found a gap in the field and then fell straight into it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is a much more interesting result, and it's what the rest of the series is about.&lt;/p&gt;

&lt;h3&gt;
  
  
  And every lever I ship fails the same way
&lt;/h3&gt;

&lt;p&gt;Before going further, the short version of what I tried. Four modes, each measured &lt;em&gt;against its cost to answerable questions&lt;/em&gt;, because a system that abstains on everything scores 1.00 and is useless:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Adversarial abstain ↑&lt;/th&gt;
&lt;th&gt;Answerable false-abstain ↓&lt;/th&gt;
&lt;th&gt;discrimination&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;default&lt;/td&gt;
&lt;td&gt;0.000&lt;/td&gt;
&lt;td&gt;0.000&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.000&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;calibrated &lt;em&gt;(in-sample, an upper bound)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;0.574&lt;/td&gt;
&lt;td&gt;0.420&lt;/td&gt;
&lt;td&gt;0.154&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;entailment judge&lt;/td&gt;
&lt;td&gt;0.347&lt;/td&gt;
&lt;td&gt;0.290&lt;/td&gt;
&lt;td&gt;0.057&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;both&lt;/td&gt;
&lt;td&gt;0.796&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.603&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.193&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The best absolute catch is the worst trade: it refuses &lt;strong&gt;60.3% of the questions your users actually wanted answered&lt;/strong&gt;. There is no row here anyone ships.&lt;/p&gt;

&lt;p&gt;And when I later fixed two real retrieval bugs, discrimination went from 0.157 to &lt;strong&gt;0.154&lt;/strong&gt;, which is unchanged. Both columns rose together. &lt;em&gt;A retrieval improvement cannot improve an answerability judgement, because a better-retrieved on-topic-but-wrong passage scores **higher&lt;/em&gt;&lt;em&gt;, not lower.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the series goes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Part 2&lt;/strong&gt;: six candidate signals measured head to head, including a cross-encoder that reads query and document jointly and still can't tell. Relevance is not answerability. Plus the largest retrieval gain I've measured (+0.106 hit@5) moving abstention by exactly nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 3&lt;/strong&gt;: the payoff. why this benchmark and BEAM &lt;em&gt;disagree&lt;/em&gt;, and the hidden variable that dissolves it. Discrimination isn't a property, it's a curve, and I can now tell you the coordinate where it crosses usable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 4&lt;/strong&gt;: the trade this design implies. what a memory layer that never calls an LLM costs you, and what it saves.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I'm writing it this way
&lt;/h2&gt;

&lt;p&gt;The last time I published a design in this space, the comments were better than the post. Not&lt;br&gt;
vague encouragement, but people naming the failure mode precisely and describing what would fix it.&lt;br&gt;
I turned those into experiments and shipped what survived. One of them produced a real improvement&lt;br&gt;
that &lt;strong&gt;Part 2 then shows landing below plain cosine&lt;/strong&gt; on this workload, which is exactly why it was&lt;br&gt;
worth publishing: a design you keep in a drawer accumulates conviction, a design you publish&lt;br&gt;
accumulates objections, and objections are the cheapest high-quality signal available to anyone&lt;br&gt;
building this stuff.&lt;/p&gt;

&lt;p&gt;So if you build or evaluate retrieval-backed agents, the comment section is the point of this,&lt;br&gt;
not a formality. Tell me where the measurement is wrong.&lt;/p&gt;

&lt;p&gt;One thing worth saying because it cuts against my own framing: the field is already moving. Mem0's&lt;br&gt;
newer &lt;strong&gt;BEAM&lt;/strong&gt; benchmark ships an abstention category. Whatever you make of the rest of it, someone&lt;br&gt;
over there reached the same conclusion I did and did something about it. I'd rather be early to a&lt;br&gt;
fixed problem than right about a permanent one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question
&lt;/h2&gt;

&lt;p&gt;If you build or evaluate retrieval-backed agents, I'd genuinely like to know:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do you measure what your system does when the answer isn't in the corpus, and if you do, what number do you report?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not "does it hallucinate less." A number, on a fixed set of questions you know are unanswerable, alongside the cost in refused-but-answerable questions. If you have one, I want to see it. If you tried and it came out like mine, that's more useful still.&lt;/p&gt;

&lt;p&gt;And if you think &lt;code&gt;discrimination&lt;/code&gt; is the wrong summary statistic, say so. I would rather find that out in a comment section than after building four more articles on top of it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Every figure here is measured and reproducible: &lt;a href="https://github.com/GiulioDER/RE-call/blob/master/results/RESULTS.md" rel="noopener noreferrer"&gt;&lt;code&gt;results/RESULTS.md&lt;/code&gt; §7b&lt;/a&gt; and &lt;a href="https://github.com/GiulioDER/RE-call/blob/master/results/FINDINGS.md" rel="noopener noreferrer"&gt;&lt;code&gt;results/FINDINGS.md&lt;/code&gt; §9b&lt;/a&gt;. The harness is in the repo. Next: *&lt;/em&gt;&lt;a href="https://dev.to/gde03/relevance-is-not-answerability-six-signals-and-none-of-them-beat-plain-cosine-2f14"&gt;Part 2, Relevance is not answerability: six signals, and none of them beat plain cosine&lt;/a&gt;.***&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>opensource</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Retrieval-Augmented Self-Recall — What the Comments Taught Me (RE-call v0.3)</title>
      <dc:creator>Giulio D'Erme</dc:creator>
      <pubDate>Sat, 18 Jul 2026 12:07:45 +0000</pubDate>
      <link>https://dev.to/gde03/retrieval-augmented-self-recall-what-the-comments-taught-me-re-call-v03-42c1</link>
      <guid>https://dev.to/gde03/retrieval-augmented-self-recall-what-the-comments-taught-me-re-call-v03-42c1</guid>
      <description>&lt;p&gt;&lt;em&gt;A follow-up to &lt;a href="https://dev.to/gde03/retrieval-augmented-self-recall-the-rag-problem-nobody-talks-about-2n0n"&gt;Part 1: the self-recall thesis&lt;/a&gt; — the series runs through &lt;a href="https://dev.to/gde03/retrieval-augmented-self-recall-part-6-the-fine-tune-that-did-nothing-and-shipping-it-as-an-mcp-240m"&gt;Part 6&lt;/a&gt;. Code: &lt;a href="https://github.com/GiulioDER/RE-call" rel="noopener noreferrer"&gt;RE-call&lt;/a&gt; — everything below is measured and reproducible (&lt;code&gt;make eval&lt;/code&gt;), full study in &lt;a href="https://github.com/GiulioDER/RE-call/blob/master/docs/ENTAILMENT_SUPERSESSION_STUDY.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/ENTAILMENT_SUPERSESSION_STUDY.md&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I published a thesis post about agent memory and got five comments that were better than the post.&lt;/p&gt;

&lt;p&gt;Two of them didn't just critique the design — they described, precisely, why it would fail and what would fix it. So I did the only reasonable thing: I turned both into experiments, ran them on the same eval harness the series is built on, and shipped what survived. That's &lt;strong&gt;RE-call v0.3&lt;/strong&gt;, and this post is the receipt.&lt;/p&gt;

&lt;p&gt;I want to be explicit about why I'm writing it this way. The point of publishing this series was never broadcast — it was &lt;strong&gt;error-correction&lt;/strong&gt;. A design you keep in a drawer accumulates conviction; a design you publish accumulates &lt;em&gt;objections&lt;/em&gt;, and objections are the cheapest high-quality signal you will ever get. The comment section of Part 1 did more for this codebase than any week of solo iteration. This post exists to pay that back with the thing commenters almost never receive: evidence that someone listened, measured, and changed the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comment 1: "A similarity score is not a confidence score"
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.to/vinimabreu"&gt;Vinicius Pereira&lt;/a&gt; put it in one line I've been quoting since:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Proximity is a candidate; entailment is the evidence.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;His argument: the near-misses that hurt most are &lt;strong&gt;high-similarity and wrong&lt;/strong&gt; — memos semantically adjacent to the query that don't answer it. A threshold-based &lt;code&gt;gap_warning&lt;/code&gt; (Part 3, Part 5) waves them straight through &lt;em&gt;by construction&lt;/em&gt;, because their similarity clears any threshold you could calibrate. The abstention signal cannot be the retriever's own score. You need a separate check that the retrieved memo actually &lt;strong&gt;entails&lt;/strong&gt; an answer.&lt;/p&gt;

&lt;p&gt;He was right, and measurably so. I built a held-out challenge set of 10 near-miss queries — each names a strongly on-topic memo that does &lt;em&gt;not&lt;/em&gt; contain the asked-for fact ("how much did the cache reduce &lt;strong&gt;memory usage&lt;/strong&gt;" against a memo that measures &lt;em&gt;latency&lt;/em&gt;). Baseline, with the calibrated threshold from Part 5 doing its best:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Embedder&lt;/th&gt;
&lt;th&gt;Near-miss FCR @ calibrated threshold&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hashing-64&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bge-small&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;voyage-3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.40&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The threshold that scores a perfect 0.00 on far-gap queries passes 40–100% of near-misses. There is no threshold to fix. The distractor's cosine is &lt;em&gt;genuinely high&lt;/em&gt; — that's what makes it a near-miss.&lt;/p&gt;

&lt;p&gt;So v0.3 adds an opt-in &lt;strong&gt;entailment stage&lt;/strong&gt;: a small QNLI cross-encoder ("does this sentence answer this question?") judges the trusted hits, and a hit that doesn't entail the query is demoted to a new verdict, &lt;code&gt;not_entailed&lt;/code&gt;. The key property is exactly the one Vinicius predicted: it emits a &lt;strong&gt;decision at the judge's own trained boundary&lt;/strong&gt;, not another score — so there is no per-embedder constant left to recalibrate. And that transfer claim held: the &lt;em&gt;identical&lt;/em&gt; judge, zero tuning, on every embedder:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Embedder&lt;/th&gt;
&lt;th&gt;Near-miss FCR: threshold → +entailment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hashing-64&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.00 → &lt;strong&gt;0.60&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bge-small&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.80 → &lt;strong&gt;0.50&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;voyage-3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.40 → 0.40&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Where the comment needed a refinement — which is the point of measuring
&lt;/h3&gt;

&lt;p&gt;The ablation was the honest surprise. Running the judge &lt;em&gt;alone&lt;/em&gt;, without the threshold, &lt;strong&gt;degrades far-gap detection&lt;/strong&gt; (gap FCR 0.00 → 0.40 on both semantic embedders): fed nearest-noise from a topic the corpus doesn't cover, the QNLI model sometimes calls it an answer. So entailment does not &lt;em&gt;replace&lt;/em&gt; the calibrated threshold — the two guard &lt;strong&gt;different failure classes&lt;/strong&gt; and must be stacked. Threshold catches far gaps; judge catches near-misses.&lt;/p&gt;

&lt;p&gt;And the costs are real, and published: ~0.1–1.0 s of judge time per query on CPU, one legitimately answerable query wrongly rejected on both semantic embedders (its gold memo answers by &lt;em&gt;negation&lt;/em&gt; — "do we retry on 4xx?" → "we do &lt;strong&gt;not&lt;/strong&gt; retry" — and the judge reads that as not-answering), MRR on answerable queries dips 1.000 → 0.929. The residual near-miss FCR (0.40–0.60) is the judge's own quality bound — Part 5's law, one layer up: gap detection is bounded by the embedder, and &lt;strong&gt;abstention-by-entailment is bounded by the judge&lt;/strong&gt;. Ships OFF by default for exactly these reasons; you opt in with your eyes open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comment 2: "Supersession is a relation, not a property"
&lt;/h2&gt;

&lt;p&gt;The same comment carried a second thesis, on the guard I'd already confessed was weakest (freshness):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You are trying to infer a relation between two memos at read time, when both look valid in isolation. That inference is a losing game. Bind the truth when it is created.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And &lt;a href="https://dev.to/mateo_ruiz_6992b1fce47843"&gt;Mateo Ruiz&lt;/a&gt; had independently named the target shape:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Retrieval should return confidence + provenance + validity, not just relevance.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sentence is now, almost verbatim, how RE-call's trust layer describes itself. Every hit returns a verdict (&lt;code&gt;ok / superseded / expired / …&lt;/code&gt;), a calibrated confidence, and provenance; a memo declares &lt;code&gt;supersedes: old-memo.md&lt;/code&gt; in its frontmatter &lt;em&gt;at write time&lt;/em&gt;, and retrieval returns the current head of the chain instead of a resolved-but-still-embedded old decision.&lt;/p&gt;

&lt;p&gt;For v0.3 I added the experiment that closes the "why not just timestamps?" question — against the &lt;strong&gt;steelman&lt;/strong&gt;, not a strawman: "among the confidently-relevant hits, trust the newest", with the stale docs re-touched after their successors, the way any living corpus re-syncs constantly. Superseded-trust rate (how often the stale memo is handed back as the answer — lower is better):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Embedder&lt;/th&gt;
&lt;th&gt;Plain search&lt;/th&gt;
&lt;th&gt;Recency (steelman)&lt;/th&gt;
&lt;th&gt;Declared &lt;code&gt;supersedes:&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hashing-64&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.83&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.00&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bge-small&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.83&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.00&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;voyage-3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.00&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Look at the &lt;code&gt;bge-small&lt;/code&gt; row: the timestamp heuristic is &lt;em&gt;worse than plain relevance ranking&lt;/em&gt; — the tie-break actively promotes the freshly-re-synced stale memo in the one case where ranking had preferred the successor. A per-document timestamp cannot see a two-document relation, and making the timestamp "smarter" makes it more confidently wrong. The declared relation holds at 0.00 in the same runs.&lt;/p&gt;

&lt;p&gt;Vinicius also called the residual failure mode in advance: write-time binding is only as good as the author's discipline — a forgotten link is an orphan memo that looks valid forever. But, as I replied then: &lt;em&gt;impossible to infer&lt;/em&gt; becomes &lt;em&gt;possible to enforce&lt;/em&gt;. So v0.3 ships &lt;code&gt;recall lint&lt;/code&gt; — dangling &lt;code&gt;supersedes:&lt;/code&gt; references, cycles, ambiguous successors, versioned siblings with no declared edge, closures declared only in prose. No DB, exit 1 on errors, drops into CI in one line. (It paid for itself before it shipped: writing its tests uncovered a real parser bug where a scalar &lt;code&gt;[[wikilink]]&lt;/code&gt; was read as a YAML list, producing an edge that silently never resolved.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The experiment I still owe
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.to/nazar-boyko"&gt;Nazar Boyko&lt;/a&gt; asked, before Part 5 was even published, whether the gap threshold should be &lt;em&gt;relative&lt;/em&gt; — top hit versus the rest of the batch — rather than an absolute cutoff re-tuned per embedder. It's a good idea with a suspected hole (a spread-based check is blind to the single confident distractor — which is precisely the near-miss class above), but suspicion is not measurement. It's on the list, and the harness is now shaped to answer it.&lt;/p&gt;

&lt;p&gt;And &lt;a href="https://dev.to/hannune"&gt;Tae Kim&lt;/a&gt;'s point — a typed &lt;code&gt;coverage_check&lt;/code&gt; slot so the "no real match" signal can't be silently dropped — sharpened a design rule that now runs through the whole stack: &lt;strong&gt;the retriever computes the signal, the schema carries it.&lt;/strong&gt; Computed, it's a measurement; self-reported by the model, it's a declaration. Those fail very differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm actually arguing for
&lt;/h2&gt;

&lt;p&gt;Five people I've never met read a post about a niche RAG problem and, between them, produced: a falsifiable critique of my abstention mechanism, the correct architecture for supersession, a proposed alternative worth benchmarking, and a schema-design principle. Total cost to me: publishing something concrete enough to be wrong about, and taking the replies seriously enough to run them.&lt;/p&gt;

&lt;p&gt;That's the whole model. Not "content", not reach — &lt;strong&gt;working in public as a form of peer review&lt;/strong&gt;. The asymmetry is absurdly favorable: you contribute one design and get back the failure modes it would have taken you months to hit alone. The only price is that you have to be willing to write "I was wrong, here's the measurement" — which, in a series whose thesis is &lt;em&gt;calibrated honesty&lt;/em&gt;, is not a price at all. It's the product.&lt;/p&gt;

&lt;p&gt;So: thank you Vinicius, Mateo, Nazar, Tae — and &lt;a href="https://dev.to/amin_parva_ab01ff398fd341"&gt;Amin&lt;/a&gt;, whose memory-compaction angle (keep the gist graph, not every turn) is a different axis of the same problem and deserves its own experiment. v0.3 has your fingerprints on it.&lt;/p&gt;

&lt;p&gt;If you're reading this and see the next hole — the negation-blind judge, the owed relative-threshold benchmark, a stronger entailment model, something I haven't imagined — the comment section is open and the harness is public. Evidently, it works.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Code: &lt;a href="https://github.com/GiulioDER/RE-call" rel="noopener noreferrer"&gt;RE-call&lt;/a&gt; (MIT). The full v0.3 study with every table: &lt;a href="https://github.com/GiulioDER/RE-call/blob/master/docs/ENTAILMENT_SUPERSESSION_STUDY.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/ENTAILMENT_SUPERSESSION_STUDY.md&lt;/code&gt;&lt;/a&gt;. Series index: &lt;a href="https://dev.to/gde03/retrieval-augmented-self-recall-the-rag-problem-nobody-talks-about-2n0n"&gt;Part 1&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>Retrieval-Augmented Self-Recall — Part 6: The Fine-Tune That Did Nothing, and Shipping It as an MCP Server</title>
      <dc:creator>Giulio D'Erme</dc:creator>
      <pubDate>Sat, 18 Jul 2026 12:07:17 +0000</pubDate>
      <link>https://dev.to/gde03/retrieval-augmented-self-recall-part-6-the-fine-tune-that-did-nothing-and-shipping-it-as-an-mcp-240m</link>
      <guid>https://dev.to/gde03/retrieval-augmented-self-recall-part-6-the-fine-tune-that-did-nothing-and-shipping-it-as-an-mcp-240m</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 6 (finale) of Retrieval-Augmented Self-Recall. Code: &lt;a href="https://github.com/GiulioDER/RE-call" rel="noopener noreferrer"&gt;RE-call&lt;/a&gt;. &lt;a href="https://dev.to/gde03/retrieval-augmented-self-recall-part-5-the-gap-threshold-that-didnt-transfer-86a"&gt;Part 5: the gap threshold that didn't transfer&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I fine-tuned the embedder on my own domain expecting a win. I measured it properly, on held-out queries.&lt;/p&gt;

&lt;p&gt;The improvement was exactly zero. &lt;strong&gt;Δ+0.00 MRR. Δ+0.00 nDCG@10.&lt;/strong&gt; Not "small". Not "within noise". Zero.&lt;/p&gt;

&lt;p&gt;It's also the result I wanted, which takes some explaining. That's the first half of this post. The second half is how the whole engine ships, so an agent can actually use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fine-tune that did nothing
&lt;/h2&gt;

&lt;p&gt;After Part 5, the natural next question: if calibrating the &lt;em&gt;threshold&lt;/em&gt; helps, would a better &lt;em&gt;embedding&lt;/em&gt; help more? So I fine-tuned one on my domain.&lt;/p&gt;

&lt;p&gt;The setup: &lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt;, &lt;code&gt;OnlineContrastiveLoss&lt;/code&gt; on query/gold-chunk pairs, trained on the 14-document corpus. The result:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Test MRR&lt;/th&gt;
&lt;th&gt;Test nDCG@10&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Base&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ Fine-tuned&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Δ&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+0.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+0.00&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Zero lift. And that is the &lt;strong&gt;correct&lt;/strong&gt; outcome, not a failed experiment.&lt;/p&gt;

&lt;p&gt;Here's the reasoning, because it's the whole point. The base model &lt;em&gt;already&lt;/em&gt; scores a perfect MRR and nDCG@10 on this corpus. There is no headroom left to recover. The only ways to manufacture a "gain" from here would be dishonest ones: evaluate on the training set (and measure memorization, not retrieval), or artificially cripple the baseline so fine-tuning has something to fix. Reporting &lt;strong&gt;+0.00&lt;/strong&gt; is the honest read, and the honest read is that off-the-shelf embeddings already saturate this corpus.&lt;/p&gt;

&lt;p&gt;But the full result is more nuanced, and more useful. On a &lt;em&gt;harder&lt;/em&gt;, &lt;strong&gt;opaque-jargon&lt;/strong&gt; corpus — one where the base model genuinely struggles to map queries to the right chunks — the same fine-tuning gave &lt;strong&gt;+0.24 MRR&lt;/strong&gt;. So the real conclusion isn't "fine-tuning doesn't work." It's:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Fine-tuning helps when the base model doesn't already cover your vocabulary. When it does, you get nothing. Know which regime you're in &lt;em&gt;before&lt;/em&gt; you spend the GPU hours.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the value of a null result. "+0.00" told me my corpus was already well-covered by a general-purpose embedder — which saved me from a fine-tuning pipeline I didn't need, and told me exactly when I &lt;em&gt;would&lt;/em&gt; need one. Teams that reflexively bury negative results throw away findings like that and re-learn them the expensive way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shipping it: the MCP server
&lt;/h2&gt;

&lt;p&gt;An engine nobody can plug in is a paper. RE-call ships as &lt;strong&gt;&lt;code&gt;recall_mcp&lt;/code&gt;&lt;/strong&gt;, an MCP (Model Context Protocol) server over stdio, so Claude — Desktop, Code, or any MCP client — can query its own memory directly as a tool.&lt;/p&gt;

&lt;p&gt;That closes the loop with the applied series. There are three layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The human-editable memory&lt;/strong&gt; — the plain markdown files you curate by hand (the two-file memory system from the Claude Code series).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The retrieval engine&lt;/strong&gt; — RE-call: hybrid search on Postgres, plus the honesty guards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The MCP server&lt;/strong&gt; — how the agent reaches layer 2 at runtime, as a first-class tool.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And the design principle carries straight over from the applied series: &lt;strong&gt;the honesty signals ride inside the tool's structured output.&lt;/strong&gt; When the agent queries memory, the response isn't just a ranked list — every hit carries a trust &lt;em&gt;verdict&lt;/em&gt; (&lt;code&gt;ok / superseded / expired / …&lt;/code&gt;), a calibrated confidence, provenance, and validity, and the result carries &lt;code&gt;gap_warning&lt;/code&gt;, freshness, and an explicit &lt;code&gt;abstained&lt;/code&gt; + reason. The agent physically cannot get the answer without also getting "here's how much to trust it." Honesty isn't an advisory the model may ignore; it's baked into the shape of the response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the series lands
&lt;/h2&gt;

&lt;p&gt;Six parts ago I claimed self-recall is a &lt;em&gt;different&lt;/em&gt; RAG problem — one about &lt;strong&gt;calibrated abstention&lt;/strong&gt;, not ranking. Everything since served that one idea:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Architecture (Part 2)&lt;/strong&gt; — hybrid dense + sparse retrieval on nothing but Postgres, because agent memory doesn't need a dedicated vector DB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guards (Part 3)&lt;/strong&gt; — &lt;code&gt;gap_warning&lt;/code&gt;, freshness, and anti-re-litigation, the three things a &lt;em&gt;memory&lt;/em&gt; does that a &lt;em&gt;search index&lt;/em&gt; doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluation (Part 4)&lt;/strong&gt; — a false-confident rate measured alongside MRR, because the failure that matters is the one ranking metrics can't see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Findings (Parts 4–6)&lt;/strong&gt; — hybrid + rerank earns its cost only on weak embedders; a hard-coded abstention threshold is a silent landmine; and fine-tuning is regime-dependent, worth exactly nothing on a corpus your base model already covers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two of those findings are negative results. That's deliberate. In a domain about knowing your own limits, the honest nulls are the most valuable thing on the table — and everything here is public, reproducible, and covered by a 150-test suite whose 49 DB-touching tests run against real Postgres, so you don't have to take my word for any of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read it, run it, break it
&lt;/h2&gt;

&lt;p&gt;The whole engine is open source: &lt;strong&gt;&lt;a href="https://github.com/GiulioDER/RE-call" rel="noopener noreferrer"&gt;RE-call&lt;/a&gt;&lt;/strong&gt; (MIT). Clone it, point it at your own corpus, and check the calibration on &lt;em&gt;your&lt;/em&gt; embedder before you trust any threshold — including mine.&lt;/p&gt;

&lt;p&gt;And if you came here from the applied track, &lt;a href="https://dev.to/gde03/claude-code-beyond-the-prompt-i-run-a-live-automated-trading-system-on-it-heres-the-whole-setup-478l"&gt;Claude Code, Beyond the Prompt&lt;/a&gt; is where all of this gets used in anger: the memory an agent reads at the start of every session, backed by the engine you just read the internals of.&lt;/p&gt;

&lt;p&gt;Thanks for reading the whole way down. If you build on it — or find where I'm wrong — I want to hear about it.&lt;/p&gt;

&lt;p&gt;And that's not a rhetorical close: the comments on Part 1 already found where I was wrong, twice, and the fixes shipped as v0.3 — an entailment stage for the near-miss a threshold can't see, and write-time supersession that beats any timestamp. The receipts, with the commenters' names on them, are in &lt;a href="https://dev.to/gde03/retrieval-augmented-self-recall-what-the-comments-taught-me-re-call-v03-42c1"&gt;the series follow-up&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The finale of Retrieval-Augmented Self-Recall. Code: &lt;a href="https://github.com/GiulioDER/RE-call" rel="noopener noreferrer"&gt;RE-call&lt;/a&gt;. Building agent memory, or hiring people who do? This series is the long-form version of my answer.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>mcp</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Retrieval-Augmented Self-Recall — Part 5: The Gap Threshold That Didn't Transfer</title>
      <dc:creator>Giulio D'Erme</dc:creator>
      <pubDate>Sat, 18 Jul 2026 12:06:53 +0000</pubDate>
      <link>https://dev.to/gde03/retrieval-augmented-self-recall-part-5-the-gap-threshold-that-didnt-transfer-86a</link>
      <guid>https://dev.to/gde03/retrieval-augmented-self-recall-part-5-the-gap-threshold-that-didnt-transfer-86a</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 5 of Retrieval-Augmented Self-Recall. Code: &lt;a href="https://github.com/GiulioDER/RE-call" rel="noopener noreferrer"&gt;RE-call&lt;/a&gt;. &lt;a href="https://dev.to/gde03/retrieval-augmented-self-recall-part-4-benchmarking-retrieval-and-honesty-aj3"&gt;Part 4: the eval harness&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I shipped the &lt;code&gt;gap_warning&lt;/code&gt; guard from Part 3 with a sensible-looking default: if the best cosine similarity is &lt;strong&gt;below 0.50&lt;/strong&gt;, call it a probable gap and abstain. I tested it. It worked.&lt;/p&gt;

&lt;p&gt;Then I swapped the embedding model. &lt;strong&gt;Every question that should have been refused came back as a confident answer.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No error. No crash. No failed test. The guard was still there, still running, still reporting that everything was fine. It had just quietly stopped being a guard.&lt;/p&gt;

&lt;p&gt;Here's the mechanism, because it generalises well past my project, and there's a decent chance it's live in your RAG system right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cosine similarity is not calibrated across models
&lt;/h2&gt;

&lt;p&gt;The gap guard fires when &lt;code&gt;best_cosine &amp;lt; threshold&lt;/code&gt;. I used 0.50. The problem is that &lt;strong&gt;0.50 means completely different things to different embedders&lt;/strong&gt;, because each model lays out its vector space with its own geometry. Same number, different meaning.&lt;/p&gt;

&lt;p&gt;The harness measured the cosine distributions for answerable vs. unanswerable queries, per embedder. Look at what "similarity" actually ranges over:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Embedder&lt;/th&gt;
&lt;th&gt;Answerable cosine&lt;/th&gt;
&lt;th&gt;Unanswerable cosine&lt;/th&gt;
&lt;th&gt;Separable?&lt;/th&gt;
&lt;th&gt;FCR @ 0.50&lt;/th&gt;
&lt;th&gt;FCR @ calibrated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hashing-64&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.30 – 0.68&lt;/td&gt;
&lt;td&gt;0.35 – 0.53&lt;/td&gt;
&lt;td&gt;no — overlap&lt;/td&gt;
&lt;td&gt;0.20*&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bge-small&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.70 – 0.90&lt;/td&gt;
&lt;td&gt;0.51 – 0.64&lt;/td&gt;
&lt;td&gt;yes, at ~0.70&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.00&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;voyage-3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.53 – 0.70&lt;/td&gt;
&lt;td&gt;0.09 – 0.32&lt;/td&gt;
&lt;td&gt;yes, at ~0.50&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.00&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;* &lt;em&gt;misleadingly low: with overlapping distributions the 0.50 cut also wrongly flags answerable queries, and the error-minimizing threshold simply stops firing at all. No threshold works here.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Read across the rows and the whole story is there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;voyage-3&lt;/code&gt;:&lt;/strong&gt; unanswerable queries score 0.09–0.32, answerable score 0.53–0.70. A threshold of 0.50 lands cleanly in the gap between them. FCR is 0.00. My default worked — &lt;strong&gt;by accident.&lt;/strong&gt; Voyage's geometry just happens to put the boundary near 0.50.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;bge-small&lt;/code&gt;:&lt;/strong&gt; unanswerable queries score &lt;strong&gt;0.51–0.64&lt;/strong&gt; — &lt;em&gt;entirely above&lt;/em&gt; my 0.50 threshold. So the guard, which only fires &lt;em&gt;below&lt;/em&gt; 0.50, never fires on them at all. Result: FCR &lt;strong&gt;1.00&lt;/strong&gt;. Every unanswerable query was confidently answered. The guard was switched off, and nothing told me. Recalibrate the threshold to ~0.70 and FCR drops to &lt;strong&gt;0.00&lt;/strong&gt; — the distributions &lt;em&gt;are&lt;/em&gt; separable, I was just cutting in the wrong place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;hashing-64&lt;/code&gt;:&lt;/strong&gt; answerable (0.30–0.68) and unanswerable (0.35–0.53) &lt;strong&gt;overlap&lt;/strong&gt;. No threshold separates them, because the embedder is too weak to distinguish "relevant" from "vaguely near." The right lesson here isn't "pick a better threshold" — it's "this embedder can't support abstention at all."&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson: never ship a hard-coded abstention threshold
&lt;/h2&gt;

&lt;p&gt;A magic constant that "works" is the most dangerous kind of code, because it works right up until the context shifts — and then it fails &lt;strong&gt;silently&lt;/strong&gt;, which is the worst possible failure mode for a safety guard. My 0.50 wasn't a good threshold that I'd validated. It was a coincidence that held for one embedder and collapsed the moment I changed one.&lt;/p&gt;

&lt;p&gt;The fix is cheap and non-negotiable: &lt;strong&gt;calibrate the abstention threshold per embedding model, against a small labeled set.&lt;/strong&gt; Twenty-odd queries — a handful answerable, a handful not — is enough to see where the two distributions actually sit and cut between them. Do NOT ship a constant. The writeup says it in one line: &lt;em&gt;calibrate per embedding model against a small labeled set; do not ship a hard-coded constant.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this generalizes past RAG
&lt;/h2&gt;

&lt;p&gt;Any decision that thresholds a similarity score inherits this exact trap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;semantic caching ("is this query close enough to a cached one?")&lt;/li&gt;
&lt;li&gt;near-duplicate / dedup detection&lt;/li&gt;
&lt;li&gt;"is this document relevant enough to include?"&lt;/li&gt;
&lt;li&gt;clustering cutoffs, entity-matching thresholds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In every one of these, the threshold is a property of the &lt;strong&gt;(embedder, corpus) pair&lt;/strong&gt;, not a universal constant. Swap the model and your carefully-chosen number is now cutting in the wrong place — and unless you're measuring the failure explicitly, you won't know.&lt;/p&gt;

&lt;p&gt;Which is the meta-point, and the reason Part 4 mattered: &lt;strong&gt;this failure is invisible without the eval harness.&lt;/strong&gt; The system ranks well, returns plausible results, and lies on gaps. You only catch it by measuring a false-confident rate &lt;em&gt;per embedder&lt;/em&gt;. Ranking metrics would have shown me green the entire time.&lt;/p&gt;

&lt;p&gt;Two footnotes from after this was drafted. First: when I trailed this finding in Part 1, a commenter guessed the mechanism before the post existed — and proposed a &lt;em&gt;relative&lt;/em&gt; threshold (top hit versus the rest of the batch) instead of an absolute one. That experiment is still owed; my worry is that a spread-based check is blind to the single confident distractor. Second, and worse: there is a whole failure class &lt;strong&gt;no threshold can catch, by construction&lt;/strong&gt; — the near-miss that scores &lt;em&gt;high&lt;/em&gt;. That one needed a different kind of fix, and it's the subject of &lt;a href="https://dev.to/gde03/retrieval-augmented-self-recall-what-the-comments-taught-me-re-call-v03-42c1"&gt;the follow-up post&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next
&lt;/h2&gt;

&lt;p&gt;If a better &lt;em&gt;threshold&lt;/em&gt; helps, would a better &lt;em&gt;embedding&lt;/em&gt; help more? The intuitive next move is to fine-tune the embedder on my domain. I did. The result was &lt;strong&gt;zero&lt;/strong&gt; — and Part 6 is about why that was exactly the right outcome, and the one case where fine-tuning actually did move the needle.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part 5 of Retrieval-Augmented Self-Recall. Code: &lt;a href="https://github.com/GiulioDER/RE-call" rel="noopener noreferrer"&gt;RE-call&lt;/a&gt;. The "measure the failure that matters, not the one that flatters" discipline runs through &lt;a href="https://dev.to/gde03/claude-code-beyond-the-prompt-i-run-a-live-automated-trading-system-on-it-heres-the-whole-setup-478l"&gt;Claude Code, Beyond the Prompt&lt;/a&gt; too.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>machinelearning</category>
      <category>programming</category>
    </item>
    <item>
      <title>Retrieval-Augmented Self-Recall — Part 4: Benchmarking Retrieval *and* Honesty</title>
      <dc:creator>Giulio D'Erme</dc:creator>
      <pubDate>Sat, 18 Jul 2026 12:06:26 +0000</pubDate>
      <link>https://dev.to/gde03/retrieval-augmented-self-recall-part-4-benchmarking-retrieval-and-honesty-aj3</link>
      <guid>https://dev.to/gde03/retrieval-augmented-self-recall-part-4-benchmarking-retrieval-and-honesty-aj3</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 4 of Retrieval-Augmented Self-Recall. Code: &lt;a href="https://github.com/GiulioDER/RE-call" rel="noopener noreferrer"&gt;RE-call&lt;/a&gt;. &lt;a href="https://dev.to/gde03/retrieval-augmented-self-recall-part-3-teaching-rag-to-say-i-dont-know-28no"&gt;Part 3: the honesty guards&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;A standard RAG benchmark would have handed my retriever a perfect score on a day it was confidently answering questions it had no data for.&lt;/p&gt;

&lt;p&gt;That's not a bug in my system. It's a gap in what those benchmarks ask. Every one of them asks the same thing: &lt;em&gt;when there was an answer, did you rank it first?&lt;/em&gt; None of them ask the question that decides whether agent memory is safe: &lt;em&gt;when there was no answer, did you say so?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So RE-call ships its own harness. Here's how it works, and the first finding it produced.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test set: the unanswerable queries are the point
&lt;/h2&gt;

&lt;p&gt;The evaluation runs on &lt;strong&gt;14 answerable queries + 5 unanswerable queries&lt;/strong&gt; over a synthetic corpus.&lt;/p&gt;

&lt;p&gt;Those 5 unanswerable queries are the whole reason the harness exists. They're questions the corpus genuinely cannot answer, where the &lt;em&gt;correct&lt;/em&gt; behavior is to abstain — to fire &lt;code&gt;gap_warning&lt;/code&gt;, not to confidently return the nearest memo. Standard retrieval benchmarks are built entirely from answerable queries; they have no way to score "did it correctly say nothing?" This harness is built around that case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two families of metrics
&lt;/h2&gt;

&lt;p&gt;Because there are two jobs — rank well when there's an answer, abstain when there isn't — there are two families of metrics:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ranking quality&lt;/strong&gt; (for the answerable queries):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;precision@k, recall@k&lt;/li&gt;
&lt;li&gt;MRR (mean reciprocal rank)&lt;/li&gt;
&lt;li&gt;nDCG@10&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Guard quality&lt;/strong&gt; (for the unanswerable queries):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;False-confident rate (FCR)&lt;/strong&gt; — the fraction of unanswerable queries that the guard &lt;em&gt;failed to flag&lt;/em&gt;. High FCR means the system confidently answered questions it should have abstained on. This is the honesty metric, and it's the one almost nobody reports.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why you need both is the crux: &lt;strong&gt;a system can post excellent MRR and terrible FCR.&lt;/strong&gt; It ranks beautifully whenever an answer exists, and lies confidently whenever one doesn't. If you only look at ranking metrics — as most RAG evals do — that failure is completely invisible. FCR is what drags it into the light.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ablation: every embedder × every fusion stage
&lt;/h2&gt;

&lt;p&gt;The harness runs the full matrix: each &lt;strong&gt;embedder&lt;/strong&gt; (&lt;code&gt;HashingEmbedder&lt;/code&gt;, &lt;code&gt;bge-small&lt;/code&gt;, &lt;code&gt;voyage-3&lt;/code&gt;) crossed with each &lt;strong&gt;fusion configuration&lt;/strong&gt; (dense only → hybrid → hybrid + rerank). That's what lets you answer "which component actually earns its cost?" instead of cargo-culting a reranker into every pipeline.&lt;/p&gt;

&lt;p&gt;And it runs against the real thing: &lt;strong&gt;49 integration tests on a live &lt;code&gt;pgvector&lt;/code&gt; container&lt;/strong&gt; (of a 150-test suite), in CI, no mock database. The benchmark exercises the actual retrieval path, not a stand-in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 1: hybrid + rerank helps most exactly where you'd expect — and nowhere else
&lt;/h2&gt;

&lt;p&gt;Here's the ablation on the &lt;strong&gt;weak&lt;/strong&gt; (&lt;code&gt;hashing&lt;/code&gt;) embedder — quality climbs monotonically as you add stages:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;MRR&lt;/th&gt;
&lt;th&gt;nDCG@10&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dense only&lt;/td&gt;
&lt;td&gt;0.63&lt;/td&gt;
&lt;td&gt;0.72&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ sparse (hybrid)&lt;/td&gt;
&lt;td&gt;0.74&lt;/td&gt;
&lt;td&gt;0.80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ cross-encoder rerank&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.00&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Now the same pipeline on the &lt;strong&gt;strong&lt;/strong&gt; &lt;code&gt;bge-small&lt;/code&gt; embedder: dense retrieval &lt;em&gt;already&lt;/em&gt; achieves a perfect &lt;a href="mailto:nDCG@10"&gt;nDCG@10&lt;/a&gt;. Hybrid fusion and reranking add &lt;strong&gt;nothing&lt;/strong&gt; — there's no headroom left to recover.&lt;/p&gt;

&lt;p&gt;The conclusion, stated plainly in the writeup: &lt;strong&gt;hybrid + rerank buys the most on weaker embedders or harder corpora; on an easy corpus with a strong embedder it's redundant.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is a genuinely useful engineering result, because the reflex in RAG is to stack a reranker onto everything. This says: don't pay for stages your embedder has already made unnecessary. Measure first. A cross-encoder rerank on every query is real latency and real cost — and on a strong embedder over a well-covered corpus, you're buying zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rigor is the point
&lt;/h2&gt;

&lt;p&gt;None of these numbers come from an in-memory toy. They come from the same harness that runs in CI against real Postgres — with a dependency audit — every commit. The reason to trust the &lt;em&gt;findings&lt;/em&gt; is that the &lt;em&gt;measurement&lt;/em&gt; is reproducible. That's the whole pitch of this track: measure honestly, including the parts that make your work look less impressive.&lt;/p&gt;

&lt;p&gt;Which is a good segue, because the same ablation surfaced something that made a chunk of my &lt;code&gt;gap_warning&lt;/code&gt; design look &lt;strong&gt;worthless&lt;/strong&gt; on certain embedders. I did not see it coming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next
&lt;/h2&gt;

&lt;p&gt;Part 5 is the finding I keep leading with: I shipped a sensible-looking abstention threshold, switched embedders, and watched &lt;strong&gt;every query that should have been refused&lt;/strong&gt; sail through as a confident answer. Why a hard-coded similarity threshold is a landmine — and what to do instead.&lt;/p&gt;

&lt;p&gt;(The harness itself has kept growing since this was drafted — it now also scores declared-supersession versus timestamps, and a held-out "near-miss" challenge set that no threshold can catch by construction. Both came out of reader comments, and both are covered in &lt;a href="https://dev.to/gde03/retrieval-augmented-self-recall-what-the-comments-taught-me-re-call-v03-42c1"&gt;the follow-up post&lt;/a&gt;.)&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part 4 of Retrieval-Augmented Self-Recall. Code: &lt;a href="https://github.com/GiulioDER/RE-call" rel="noopener noreferrer"&gt;RE-call&lt;/a&gt;. The eval-first discipline here is the same one behind &lt;a href="https://dev.to/gde03/claude-code-beyond-the-prompt-i-run-a-live-automated-trading-system-on-it-heres-the-whole-setup-478l"&gt;Claude Code, Beyond the Prompt&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>testing</category>
      <category>programming</category>
    </item>
    <item>
      <title>Retrieval-Augmented Self-Recall — Part 3: Teaching RAG to Say \"I Don't Know\</title>
      <dc:creator>Giulio D'Erme</dc:creator>
      <pubDate>Sat, 18 Jul 2026 12:06:01 +0000</pubDate>
      <link>https://dev.to/gde03/retrieval-augmented-self-recall-part-3-teaching-rag-to-say-i-dont-know-28no</link>
      <guid>https://dev.to/gde03/retrieval-augmented-self-recall-part-3-teaching-rag-to-say-i-dont-know-28no</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 3 of Retrieval-Augmented Self-Recall. Code: &lt;a href="https://github.com/GiulioDER/RE-call" rel="noopener noreferrer"&gt;RE-call&lt;/a&gt;. &lt;a href="https://dev.to/gde03/retrieval-augmented-self-recall-part-2-hybrid-rag-on-nothing-but-postgres-2pge"&gt;Part 2: hybrid retrieval on Postgres&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Ask your agent &lt;em&gt;"have we tried this filter on this market before?"&lt;/em&gt; when the honest answer is &lt;em&gt;never&lt;/em&gt;. A ranking retriever hands back the three closest memos anyway — something about a different filter, on a different market — and the agent, looking at three confident results, concludes: yes, we've looked at this.&lt;/p&gt;

&lt;p&gt;It just made a decision on a hallucination. Nothing in the stack noticed. No error was raised, because from the retriever's point of view nothing went wrong: you asked for the nearest neighbours and it gave you the nearest neighbours.&lt;/p&gt;

&lt;p&gt;Everything in Part 2 made retrieval &lt;em&gt;good&lt;/em&gt;. Good ranking makes this failure &lt;strong&gt;worse&lt;/strong&gt;, not better — it returns confident noise faster. This post is about making retrieval &lt;em&gt;honest&lt;/em&gt;, which for agent memory is the part that actually decides whether you can trust it.&lt;/p&gt;

&lt;p&gt;So RE-call wraps retrieval in &lt;strong&gt;honesty guards&lt;/strong&gt; — this post covers the original three, each answering a question ranking metrics never ask. (The current repo has grown that table to six, and the growth story is &lt;a href="https://dev.to/gde03/retrieval-augmented-self-recall-what-the-comments-taught-me-re-call-v03-42c1"&gt;its own post&lt;/a&gt;: two of the new guards exist because readers of Part 1 pointed at exactly the weaknesses you're about to see me describe. I'll flag those spots as we go.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Guard 1: &lt;code&gt;gap_warning&lt;/code&gt; — "is the best match good enough to trust?"
&lt;/h2&gt;

&lt;p&gt;After retrieval, look at the best dense cosine similarity. If it falls &lt;strong&gt;below a calibrated threshold&lt;/strong&gt;, the top result isn't the answer — it's the least-bad noise. The system sets &lt;code&gt;gap_warning = true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The important design idea: this is a &lt;strong&gt;second-order signal&lt;/strong&gt;. Retrieval still returns its ranked list; the guard &lt;em&gt;annotates&lt;/em&gt; how much to trust it. That annotation is what lets the calling agent do something other than blindly act — it can abstain, ask a human to confirm, widen the search, or explicitly note "no prior memory on this" before proceeding.&lt;/p&gt;

&lt;p&gt;That single flag is the difference between an agent that says &lt;em&gt;"we've looked at this before"&lt;/em&gt; and one that says &lt;em&gt;"I have nothing relevant on this — treat it as new."&lt;/em&gt; In a system that makes decisions, that distinction is worth more than any ranking improvement.&lt;/p&gt;

&lt;p&gt;There's one buried landmine here: &lt;strong&gt;what threshold?&lt;/strong&gt; The obvious move is to pick something like 0.50 and move on. That obvious move is quietly, dangerously wrong — and it's the biggest finding in this series, so I'm giving it its own post (Part 5). For now, the load-bearing word is &lt;em&gt;calibrated&lt;/em&gt;: the threshold is fit to data, never hard-coded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guard 2: freshness — "is this memory still current?"
&lt;/h2&gt;

&lt;p&gt;Every memo has a timestamp. The freshness guard reports the &lt;strong&gt;age&lt;/strong&gt; of retrieved content and warns when it's stale relative to the re-index cadence (my corpus re-indexes daily, so "stale" has a concrete meaning).&lt;/p&gt;

&lt;p&gt;This one is specific to &lt;em&gt;memory&lt;/em&gt; in a way document QA rarely deals with. A documentation corpus is mostly static — last year's page is still roughly true. Agent memory is a moving target: a decision recorded in April may have been &lt;em&gt;reversed&lt;/em&gt; in June. Without a freshness signal, April-truth and June-truth are indistinguishable at retrieval time, and the agent will happily act on a superseded conclusion. Freshness lets it weight recency, or at least flag the risk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Honest update, because this section aged:&lt;/strong&gt; freshness turned out to be the weakest guard of the three, and a commenter on Part 1 put a finger on why — supersession is a &lt;em&gt;relation between two memos&lt;/em&gt;, and no per-document timestamp can see a relation. We later measured it: even a steelmanned "trust the newest relevant hit" heuristic still hands back the stale memory &lt;strong&gt;83–100% of the time&lt;/strong&gt;, while an explicitly declared &lt;code&gt;supersedes:&lt;/code&gt; link holds at &lt;strong&gt;0.00&lt;/strong&gt;. The fix (a trust layer that binds the relation at write time) and the measurement are in &lt;a href="https://dev.to/gde03/retrieval-augmented-self-recall-what-the-comments-taught-me-re-call-v03-42c1"&gt;the follow-up post&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guard 3: anti-re-litigation — "did we already settle this?"
&lt;/h2&gt;

&lt;p&gt;The most agent-specific guard of the three. Before the agent proposes an idea, it queries memory for &lt;strong&gt;closed decisions&lt;/strong&gt; on that topic — the "we tried X, it failed, here's why" memos — and the guard surfaces them.&lt;/p&gt;

&lt;p&gt;The failure it prevents is subtle and expensive: an agent re-proposing a dead idea because the memo that killed it three months ago didn't happen to rank in the top results for today's phrasing. Ranking-optimized retrieval is bad at this specifically, because a settled-decision memo is often &lt;em&gt;lexically&lt;/em&gt; distant from the fresh proposal even though it's the most decision-relevant document in the store.&lt;/p&gt;

&lt;p&gt;The implementation leans on structure: decision-type memos (closed hypotheses, postmortems) are typed, and a targeted retrieval path prioritizes them when the agent is in "propose" mode. Memory that can't defend its own past decisions is condemned to relive them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The unifying idea
&lt;/h2&gt;

&lt;p&gt;Retrieval answers one question: &lt;em&gt;what's closest?&lt;/em&gt; The guards answer the three that actually govern whether the agent should act:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Should you trust it?&lt;/strong&gt; (&lt;code&gt;gap_warning&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it still current?&lt;/strong&gt; (freshness)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Did we already decide this?&lt;/strong&gt; (anti-re-litigation)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the whole difference between a &lt;strong&gt;search index&lt;/strong&gt; and a &lt;strong&gt;memory&lt;/strong&gt;. A search index ranks. A memory knows its own limits.&lt;/p&gt;

&lt;p&gt;(Since this was drafted, the guard table grew: trust verdicts with declared supersession, an opt-in entailment judge for the high-similarity-but-wrong case a threshold can never catch, and a write-time lint for the supersession graph. All three exist because readers argued with this post's ancestors — that story, with measurements, is &lt;a href="https://dev.to/gde03/retrieval-augmented-self-recall-what-the-comments-taught-me-re-call-v03-42c1"&gt;the follow-up&lt;/a&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  One rule, or the guards are theater
&lt;/h2&gt;

&lt;p&gt;A guard only helps if its signal &lt;strong&gt;reaches the decision layer&lt;/strong&gt;. A &lt;code&gt;gap_warning&lt;/code&gt; that gets computed and then dropped before the agent sees it is worse than useless — it's false assurance that the system is careful when it isn't. So in RE-call the honesty signals ride &lt;em&gt;inside&lt;/em&gt; the retrieval result: you cannot get the answer without also getting "here's how much to trust it." (If you read the applied series, this is the same principle as making the &lt;em&gt;tool&lt;/em&gt; enforce the rule instead of trusting the prompt to.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Next
&lt;/h2&gt;

&lt;p&gt;The guards make claims: &lt;em&gt;this is a gap, this is stale.&lt;/em&gt; Claims demand measurement — and "how well does it know when it doesn't know?" is a metric that standard RAG benchmarks don't even have. Part 4 builds the eval harness that measures it, and delivers the first finding about which pipeline components actually earn their cost.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part 3 of Retrieval-Augmented Self-Recall. Code: &lt;a href="https://github.com/GiulioDER/RE-call" rel="noopener noreferrer"&gt;RE-call&lt;/a&gt;. This is the layer that makes &lt;a href="https://dev.to/gde03/claude-code-beyond-the-prompt-i-run-a-live-automated-trading-system-on-it-heres-the-whole-setup-478l"&gt;Claude Code, Beyond the Prompt&lt;/a&gt;'s memory trustworthy, not just searchable.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>llm</category>
      <category>programming</category>
    </item>
    <item>
      <title>Retrieval-Augmented Self-Recall — Part 2: Hybrid RAG on Nothing but Postgres</title>
      <dc:creator>Giulio D'Erme</dc:creator>
      <pubDate>Sat, 18 Jul 2026 12:05:37 +0000</pubDate>
      <link>https://dev.to/gde03/retrieval-augmented-self-recall-part-2-hybrid-rag-on-nothing-but-postgres-2pge</link>
      <guid>https://dev.to/gde03/retrieval-augmented-self-recall-part-2-hybrid-rag-on-nothing-but-postgres-2pge</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 2 of Retrieval-Augmented Self-Recall. Code: &lt;a href="https://github.com/GiulioDER/RE-call" rel="noopener noreferrer"&gt;RE-call&lt;/a&gt;. &lt;a href="https://dev.to/gde03/retrieval-augmented-self-recall-the-rag-problem-nobody-talks-about-2n0n"&gt;Part 1: the self-recall problem&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Say "vector search" and the reflex is a dedicated vector database: Pinecone, Weaviate, Qdrant. I didn't install one. &lt;a href="https://github.com/GiulioDER/RE-call" rel="noopener noreferrer"&gt;RE-call&lt;/a&gt; keeps the dense vectors, the full-text index, and the metadata you filter them by in a single Postgres.&lt;/p&gt;

&lt;p&gt;Not as a shortcut, and not out of allergy to new infrastructure. Because for agent memory a separate vector store is the wrong &lt;em&gt;shape&lt;/em&gt;, and it quietly costs you the one property this whole system depends on.&lt;/p&gt;

&lt;p&gt;Here's the argument, and the retrieval pipeline it buys you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not a dedicated vector DB
&lt;/h2&gt;

&lt;p&gt;Agent memory has three properties that make Postgres the natural fit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It's already relational.&lt;/strong&gt; Memos have timestamps, source types, tags, decision status. That's structured metadata you want to filter and join on — exactly what a relational database is for. A separate vector store means keeping two systems in sync and losing transactional consistency between the vectors and the metadata.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;pgvector&lt;/code&gt; gives you real vector search &lt;em&gt;inside&lt;/em&gt; Postgres.&lt;/strong&gt; Approximate-nearest-neighbor cosine search, in the same database as your rows. And Postgres already ships full-text search. So you get &lt;strong&gt;dense and sparse retrieval in one transactional store&lt;/strong&gt; — no sync layer, no second system to operate.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The scale doesn't justify the complexity.&lt;/strong&gt; My corpus is ~700 memos, about 5 MB, re-indexed daily. Even orders of magnitude larger, a read-mostly, latency-tolerant memory is nowhere near the regime where a distributed vector DB earns its operational cost. Reaching for one here is over-engineering.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One store, one source of truth, ops you already know. Now the pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The retrieval pipeline
&lt;/h2&gt;

&lt;p&gt;RE-call retrieves in up to four stages. The first two run in parallel; the last two refine.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Dense retrieval
&lt;/h3&gt;

&lt;p&gt;Embed the query, run a &lt;code&gt;pgvector&lt;/code&gt; cosine-similarity search, take the top-k. This is semantic matching — it finds memos that &lt;em&gt;mean&lt;/em&gt; the same thing as the query even with no shared words. Strong on concepts, weak on exact tokens.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Sparse retrieval
&lt;/h3&gt;

&lt;p&gt;Run a Postgres full-text search (&lt;code&gt;tsvector&lt;/code&gt;/&lt;code&gt;tsquery&lt;/code&gt;) over the same corpus. This is lexical matching — it nails exact terms: a specific error code, a ticker, a piece of domain jargon, a proper noun. Strong on precision, blind to paraphrase.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Fusion with RRF
&lt;/h3&gt;

&lt;p&gt;Now you have two ranked lists that disagree. You fuse them with &lt;strong&gt;Reciprocal Rank Fusion&lt;/strong&gt; (k=60):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;score(doc) = Σ  1 / (k + rank_in_list_i(doc))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each document's score is the sum, across both lists, of one over its rank (plus a constant &lt;code&gt;k&lt;/code&gt;). Documents that rank high in &lt;em&gt;either&lt;/em&gt; list bubble up; documents high in &lt;em&gt;both&lt;/em&gt; dominate.&lt;/p&gt;

&lt;p&gt;The reason RRF specifically: dense cosine scores and full-text scores &lt;strong&gt;aren't on the same scale&lt;/strong&gt; — you can't just add or average them without arbitrary normalization. RRF sidesteps that entirely by fusing on &lt;em&gt;rank&lt;/em&gt; instead of raw score. &lt;code&gt;k=60&lt;/code&gt; is the well-established default and it's robust; you rarely need to tune it.&lt;/p&gt;

&lt;p&gt;Why fuse at all? Because dense and sparse fail in &lt;strong&gt;opposite directions&lt;/strong&gt; — one misses exact tokens, the other misses meaning. Combining them gives you the concept-matching of embeddings &lt;em&gt;and&lt;/em&gt; the precision of keyword search. (If you read the applied series, this is the "hybrid beats either alone" lesson — here's the actual mechanism under it.)&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Cross-encoder rerank (optional)
&lt;/h3&gt;

&lt;p&gt;The fused shortlist can be reordered by a &lt;strong&gt;cross-encoder&lt;/strong&gt; (&lt;code&gt;ms-marco-MiniLM&lt;/code&gt;). Unlike the bi-encoder embeddings in stage 1 — which encode query and document &lt;em&gt;separately&lt;/em&gt; and compare vectors — a cross-encoder encodes the query and a candidate &lt;em&gt;together&lt;/em&gt; and scores their relevance jointly. It's meaningfully more accurate and meaningfully slower, so you only run it on the top handful of candidates, never the whole corpus.&lt;/p&gt;

&lt;p&gt;Whether stage 4 is worth its cost turns out to depend heavily on your embedder — which is the subject of Part 4's benchmark.&lt;/p&gt;

&lt;p&gt;One property of this stage matters more than I realized when I first drafted this: the cross-encoder &lt;em&gt;reorders&lt;/em&gt;, but what it emits is still a &lt;strong&gt;score&lt;/strong&gt; you end up thresholding somewhere downstream. That distinction — a score you tune versus a decision you can trust — comes back with force later in the series.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pluggable embedders
&lt;/h2&gt;

&lt;p&gt;RE-call treats the embedder as a swappable component, with three shipped:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;HashingEmbedder&lt;/code&gt;&lt;/strong&gt; — deterministic, offline, no model download. It exists so the test suite and CI can run vector retrieval with zero external dependencies. Weak, but reproducible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FastEmbed (&lt;code&gt;bge-small&lt;/code&gt;)&lt;/strong&gt; — a strong local model, no API calls. Good default for privacy or air-gapped runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voyage (&lt;code&gt;voyage-3&lt;/code&gt;)&lt;/strong&gt; — a cloud model, the strongest of the three.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pluggability isn't just tidiness. It lets you test deterministically offline, run locally for privacy, or call the cloud for maximum quality — same pipeline, different tradeoff. And it sets up the single most important finding in this series: &lt;strong&gt;the embedder you choose changes how you have to calibrate everything downstream&lt;/strong&gt; (Part 5). Hold that thought.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is "just Postgres" actually enough?
&lt;/h2&gt;

&lt;p&gt;Fair challenge, and I don't want to hand-wave it. The claim is backed by the harness, not vibes: of RE-call's &lt;strong&gt;150-test suite, 49 integration tests run against a real &lt;code&gt;pgvector&lt;/code&gt; container&lt;/strong&gt; — no mock database — in CI. The retrieval you just read about is exercised against the real engine on every commit.&lt;/p&gt;

&lt;p&gt;And to be honest about the boundary: when &lt;em&gt;would&lt;/em&gt; you want a dedicated vector DB? Billions of vectors, sub-10 ms p99 under heavy concurrent QPS, distributed sharding across nodes. Agent memory is none of those. It's small, read-mostly, and perfectly happy with tens-of-milliseconds retrieval. Match the tool to the regime — and this regime is Postgres-shaped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next
&lt;/h2&gt;

&lt;p&gt;Retrieval now returns the closest memos. But "closest" is not "relevant" — the closest match to a question with no answer is still just noise wearing a high similarity score. Part 3 is about the guards that let the system tell the difference: how RE-call learns to say &lt;em&gt;"I don't know."&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part 2 of Retrieval-Augmented Self-Recall. Code: &lt;a href="https://github.com/GiulioDER/RE-call" rel="noopener noreferrer"&gt;RE-call&lt;/a&gt;. If you came from &lt;a href="https://dev.to/gde03/claude-code-beyond-the-prompt-i-run-a-live-automated-trading-system-on-it-heres-the-whole-setup-478l"&gt;Claude Code, Beyond the Prompt&lt;/a&gt;, this is the retrieval layer under Part 5's semantic search.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>postgres</category>
      <category>database</category>
    </item>
  </channel>
</rss>
