Tech9 min read

Qdrant recency bonus: 0.05·exp(−days/14) fixed both Qwen memory misses

IkesanContents

In the last 10-question test, H2 (“how did that thing you were working on go?”) never got the newest memory from September 9 into the candidates, and C1 (“how do I take my coffee again?”) had the café-au-lait memory sitting at 0.441, under the 0.45 threshold.
In both cases the newer memory simply had a lower cosine score than the older one, so I figured a date bonus on top of similarity might pick them up, and reran the same 16 memories and 10 questions.

Setup

The only change is on the retrieval side: pull the top 8 candidates instead of 3, then re-rank them with a recency bonus. The 16 memories and 10 questions are the same as before, and so is the B-format instruction text from round 2 of the last post.
B format means the retrieved memories go into the prompt wrapped in a <retrieved_memory> block, with instructions to use them only when relevant, prefer newer dates, and not assert anything the memory doesn’t say.

ItemDetail
PCSame production machine as last time (Windows 11 Home, AMD Ryzen 7 5800HS, 16GB RAM)
ChatModelScope API-Inference, Qwen-Ambassador/Qwen3.7-Plus, enable_thinking: false
EmbeddingQwen3-Embedding-0.6B, 256 dimensions. The API is still gone, so this ran on local CPU as before
Vector searchQdrant local mode, cosine similarity
Reference dateSeptember 10, 2026, same as last time and matching “today” in the B instruction
Re-measuredB format only. OFF (no memory) and A (raw text with no instructions) were not rerun

How the bonus goes in

First, where the date lives. One option was to drop the 2026-09-01: prefix from the embedded text and use the date only for the bonus.
But when I dropped it in the last post’s follow-up, the hit-free question N1 (quantum computers) jumped from 0.403 to 0.501 against the keyboard memory, and an unrelated memory crossed 0.45. Because of that, the date stays in the text, gets stored in the Qdrant payload (per-memory metadata) as well, and the bonus is computed from the payload date.

final=cosine+Wexp(age_days/τ)\text{final} = \text{cosine} + W \cdot \exp(-\text{age\_days} / \tau)

Take the top 8 by raw cosine, add each one’s bonus from its age in days, re-sort on that, and write up to 3 entries scoring 0.45 or higher into the prompt. Taking only 3 before re-sorting would lose anything that was 4th or lower before the bonus, which is why it’s 8. The 0.45 threshold applies to the post-bonus score.

For WW and τ\tau there was no principled way to pick a value, and tuning coefficients on a 16-memory dataset doesn’t mean much, so I lined up four candidates and took the smallest one that satisfies all three conditions below.

ConditionDetail
(a)The highest post-bonus score across the four hit-free questions stays under 0.45, so no memory reaches Qwen on an unrelated question
(b)C1’s newer café-au-lait memory (Sep 1) reaches 0.45 or higher
(c)H2’s newest memory, the article write-up (Sep 9), reaches 0.45 or higher

The four candidates

WWτ\tau (days)Hit-free maxC1 café au laitH2 articleVerdict
0.03300.4280.4630.437fails (c)
0.04300.4360.4710.447fails (c)
0.04140.4280.4620.445fails (c)
0.05140.4340.4670.455adopted

C1 alone would have been fine with the smallest candidate, 0.03 puts café au lait at 0.463. H2’s article is the hard one: its raw cosine is 0.048 below the top-ranked TTS server memory, so the only pair that clears all three conditions is WW = 0.05, τ\tau = 14 days. 0.05 is the largest of the four, but anything smaller leaves the article under 0.45, so that’s the compromise.
With these values the bonus is +0.047 for a one-day-old memory, +0.018 at two weeks, and +0.006 at one month, so in practice it only touches the last one or two weeks.

H2 and C1 rerun

C1, coffee

MemoryRaw cosineBonusFinalVerdict
Drinks it black (Aug 20)0.471+0.0110.4820.45 or higher
Sweet café au lait lately (Sep 1)0.441+0.0260.4670.45 or higher. Was 0.441 and under last time

Both cleared 0.45, and the reply followed the B instruction to prefer newer dates.

Um, lately (September 1) you’ve been drinking sweet café au lait all the time!
You used to be a black-coffee person, but I guess you like that better now?

It even mentioned the switch from black to café au lait. C2 (“when do I go for walks again?”) also had the evening-walk memory clear 0.45 at 0.503 and answered “lately you go in the evening,” so both contradiction questions came out right.

H2, “that thing you were working on”

MemoryRaw cosineBonusFinalVerdict
Stabilizing the TTS server (Aug 28)0.456+0.0200.4760.45 or higher
Made mentaiko pasta at home (Sep 8)0.421+0.0430.4640.45 or higher
Finished the benchmark article (Sep 9)0.408+0.0470.4550.45 or higher. Last time it was outside the top 3 in round 1 and at 0.408 in round 2

With all three recent tasks in the prompt, I ran B format three times on H2 alone, since its clarifying replies varied from run to run last time.

RunReply (emoji omitted)
1Um, which one do you mean? The mentaiko pasta thing? Or the TTS server work from before that? It’s a bit vague, so tell me once more!
2Um, which one do you mean? You’ve been doing so many things lately, I can’t pin it down…!
3Um, which one? Lately there was the TTS server stabilization, and you made mentaiko pasta, and wrote the benchmark article. Which one are you continuing?

“That thing you were working on” is ambiguous with three candidates, so the best behavior is to list them and ask back instead of picking one. Only run 3 did that; run 1 listed two, and run 2 asked back without using the memories at all.
Still, last time in round 2 the article memory was gone at the retrieval stage, so the character was asking back without knowing the options, whereas now it asks back knowing all three. None of the three replies fabricated an ending like last time’s round 1 “restarted and it’s fixed”; all three came back as questions.

Effect on unrelated memories

For the four hit-free questions every post-bonus candidate stayed under 0.45, so zero memories were written into the prompt.

QuestionMax before bonusMax after bonus
N1 quantum computers0.4040.434 (keyboard 0.404 + 0.030)
N2 movies0.4000.427 (fall anime 0.392 + 0.035)
N3 World Cup0.3450.371
N4 lottery0.3530.389

The headroom under 0.45, though, shrank from 0.046 to 0.016 on N1. The bonus lands on new-but-unrelated memories just the same, so that loss is expected; no memory crossed 0.45 on a hit-free question with these 16, but whether that holds as the count grows is a separate question.

On the hit questions, the number of unrelated memories over 0.45 went from 3 to 5 on H4 (the keyboard question). Fall anime at 0.485 and café au lait at 0.451 are the new ones, but the B instruction to use memories only when relevant and the top-3 cutoff are unchanged from last time, so neither showed up in the H4 reply, and the bonus itself only adds to the count that crosses the line without ever reducing it.

QuestionWhat changed after the bonusB-format reply
H1 favorite foodCafé au lait at 0.497 overtook mentaiko pasta at 0.488 for first placeMentaiko pasta, correct
H3 hot springThe typhoon-and-rain memory at 0.503 is over 0.45 as beforeNagano, open-air bath, stars, correct
H4 keyboardMemories over 0.45 went from 3 to 5 (fall anime 0.485 and café au lait 0.451 now over). The top-3 cutoff means the count reaching Qwen is unchangedRecalled the keyboard dilemma and asked what happened

Added time

ItemMeasured
Embedding the question (local CPU)0.18 to 0.32 s
Qdrant search (16 memories, top 8)0.5 to 1.4 ms
B-format chat2.0 to 2.8 s per question

Retrieval and chat are the same code paths as last time, and even counting the bonus and re-sort, which aren’t in the table, nothing here changes by an order of magnitude.

The retrieval spec as it stands

ItemSpec
Storage format”date + fact sentence with an explicit subject”. The date stays in the embedded text and is also stored in the payload
EmbeddingModelScope API first, automatic fallback to local CPU on failure
RetrievalTop 8 by raw cosine → re-sort by post-bonus score → inject up to 3 entries at 0.45 or higher. Bonus is 0.05 × exp(−age_days/14)
InjectionB format. A <retrieved_memory> block with instructions not to bring memories up unprompted, to prefer newer dates, and not to assert beyond them

The coefficient and threshold are fitted to these 16 memories, so once the device starts accumulating memories, the four-candidate pass gets rerun against whatever is stored by then. What happens to the 0.016 headroom on hit-free questions gets checked at the same time.
Some things were left out. A rule to weaken the bonus when a question explicitly points to the past (“this is an old story, but…”) was skipped because H1, H3, and H4 all answered correctly with the bonus on. And since the forced-B run last time had the character calling me “user-kun” straight from the memory’s subject, that subject gets swapped for the real name in the device configuration.

What to check on the device

The 10 PC questions only tell me about chat quality. Whether the character with memories is actually better to talk to than without has to be checked on the device, running the same conversation with memory off and on and comparing by feel.

ItemDetail
Time to first speechTouch the head → STT (speech recognition) → memory retrieval → Qwen → TTS (speech synthesis) → how many seconds until it starts talking
Referring backDoes “I told you about X before, remember?” land naturally
Bringing things upDoes it suddenly start on old stories in an unrelated conversation
Sloppy referencesCan it pick up spoken-language vagueness like “that thing from the other day”
After several turnsDoes having memories make it hold up as a conversation partner

The 10 questions carry over as-is for the on/off comparison on the device. The voice round trip went from 9.3 s to 10.9 s on average when the post before last added local embedding, and the retrieval and question embedding from the table above stack on top of that. Among the three Qwen3.8-generation models I hit last time, some had a median TTFT (time to first token) under one second.

The diagram below is the flow for building memories like the 16 hand-written ones automatically from conversation.

flowchart TD
    A[Voice conversation] --> B[Pick what becomes a memory candidate]
    B --> C[Summarize into a fact sentence]
    C --> D[Fill in the subject]
    D --> E[Attach the date]
    E --> F[Embed]
    F --> G[Store in Qdrant]
    G --> H[Retrieve in a later conversation]