<?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: Adedotun Akindele</title>
    <description>The latest articles on DEV Community by Adedotun Akindele (@dotkeyz96).</description>
    <link>https://dev.to/dotkeyz96</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%2F4036981%2F5cd7be22-a9ac-4fa5-9b26-e1898c22e42a.jpg</url>
      <title>DEV Community: Adedotun Akindele</title>
      <link>https://dev.to/dotkeyz96</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dotkeyz96"/>
    <language>en</language>
    <item>
      <title>I Built an AI That Forgets on Purpose</title>
      <dc:creator>Adedotun Akindele</dc:creator>
      <pubDate>Sun, 19 Jul 2026 22:28:43 +0000</pubDate>
      <link>https://dev.to/dotkeyz96/i-built-an-ai-that-forgets-on-purpose-1bf7</link>
      <guid>https://dev.to/dotkeyz96/i-built-an-ai-that-forgets-on-purpose-1bf7</guid>
      <description>&lt;p&gt;Every chat with an AI feels like a fresh start, doesn’t it? It doesn’t remember what you mentioned yesterday, can’t connect notes across sessions, and treats a casual "good morning" the same as a critical lab result. It’s a little frustrating, honestly.&lt;/p&gt;

&lt;p&gt;So I built a memory system where &lt;strong&gt;information has to earn its place&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Every message gets scored for importance the moment it arrives. If it turns out useful later, its lifespan extends. Once a night, a background job consolidates the valuable stuff into permanent summaries and facts, and lets the rest fade away.&lt;/p&gt;

&lt;p&gt;The name &lt;strong&gt;Àtúnbí&lt;/strong&gt; means &lt;em&gt;"reborn"&lt;/em&gt; in Yoruba. Raw memory comes and goes. The good stuff sticks around.&lt;/p&gt;

&lt;p&gt;I built this for the Qwen Cloud Hackathon, and tested it by tracking two diabetic patients — Ada and Kai — across visits, medication changes, and lab results. But it works for more than healthcare: swap patients for codebases, legal cases, research papers, or your own notes, and the core model stays exactly the same.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture: Two Paths, One Brain
&lt;/h2&gt;

&lt;p&gt;The system is split into two complementary rhythms to balance speed and depth: one for real-time responses, another for slow, nightly learning. This split is what makes long-term memory feel snappy, not clunky.&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%2Fiz6h0y6v32b62d9jisa2.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%2Fiz6h0y6v32b62d9jisa2.png" alt=" " width="800" height="1275"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Serving Path (Real-Time)
&lt;/h3&gt;

&lt;p&gt;This loop responds in under a second:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ingest&lt;/strong&gt;: Normalize every input (voice, PDFs, images, text) into a single text stream&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Process&lt;/strong&gt;: Embed, score, and extract entities from incoming messages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Working Memory&lt;/strong&gt;: Store raw messages with dynamic expiration timestamps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval&lt;/strong&gt;: Query all memory tiers to build context, then generate a response&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feedback loop&lt;/strong&gt;: Every time a memory is used, its lifespan extends — it earns its spot.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Learning Path (Nightly Dream Phase)
&lt;/h3&gt;

&lt;p&gt;Once every 24 hours, EventBridge triggers a separate job for slow, deep work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pulls all Working Memory entries older than 24 hours&lt;/li&gt;
&lt;li&gt;Clusters related messages into conversation summaries → saved to &lt;strong&gt;Episodic Memory&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Extracts standalone facts → saved to &lt;strong&gt;Semantic Memory&lt;/strong&gt; (contradictions mark old facts as &lt;code&gt;superseded&lt;/code&gt;, never deleted)&lt;/li&gt;
&lt;li&gt;Updates relationships in the &lt;strong&gt;Entity + Procedural Memory&lt;/strong&gt; graph&lt;/li&gt;
&lt;li&gt;Prunes low-score, unused entries — this is the intentional forgetting.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Five Memory Tiers
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Lifespan&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Working Memory&lt;/td&gt;
&lt;td&gt;Serving&lt;/td&gt;
&lt;td&gt;Raw messages + embeddings&lt;/td&gt;
&lt;td&gt;1h–168h, extended by use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Episodic Memory&lt;/td&gt;
&lt;td&gt;Learning&lt;/td&gt;
&lt;td&gt;Conversation summaries&lt;/td&gt;
&lt;td&gt;Permanent (unless updated)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic Memory&lt;/td&gt;
&lt;td&gt;Learning&lt;/td&gt;
&lt;td&gt;Verified standalone facts&lt;/td&gt;
&lt;td&gt;Permanent, full audit trail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Entity + Procedural&lt;/td&gt;
&lt;td&gt;Shared&lt;/td&gt;
&lt;td&gt;Relationship graph + learned patterns&lt;/td&gt;
&lt;td&gt;Evolves continuously&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Model Router: Why One LLM Isn’t Enough
&lt;/h2&gt;

&lt;p&gt;I used seven specialized Qwen models, split cleanly across the two paths. The biggest lesson: &lt;strong&gt;picking the right tool beats using one giant model for everything.&lt;/strong&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Serving Path Models (fast, frequent use)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;qwen3-asr-flash&lt;/code&gt;&lt;/strong&gt;: Audio transcription — handled medical terminology far better than expected&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;qwen3.5-omni-flash&lt;/code&gt;&lt;/strong&gt;: Vision/image description — great for context, &lt;em&gt;never&lt;/em&gt; trust it for exact numbers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;text-embedding-v4&lt;/code&gt;&lt;/strong&gt;: 1536-dim vectors for semantic search (8192 token support, rock solid)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;qwen-flash&lt;/code&gt;&lt;/strong&gt;: Real-time scorer — rates importance (0–1) and emotional valence (-1 to +1)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;qwen-plus-latest&lt;/code&gt;&lt;/strong&gt;: Workhorse — entity extraction, PDF parsing, final response generation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;qwen-turbo&lt;/code&gt;&lt;/strong&gt;: Binary relevance filter — keeps only retrieved memories that actually matter&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Learning Path Model (slow, daily use)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;qwen-max&lt;/code&gt;&lt;/strong&gt;: Heavy summarization and fact extraction for the Dream Phase — accurate, so used sparingly.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Bug That Hid For Three Days
&lt;/h2&gt;

&lt;p&gt;Most of my debugging time went here: three silent failures, each masking the next.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The demo&lt;/strong&gt;: A doctor uploads Ada’s blood work PDF showing her HbA1c is &lt;strong&gt;8.2%&lt;/strong&gt;. The system confidently replies &lt;em&gt;"7.1%"&lt;/em&gt; — and doubles down when questioned.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Vision Hallucination
&lt;/h3&gt;

&lt;p&gt;I first sent rendered PDF images to the vision model, which invented plausible-sounding but wrong numbers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt;: Extract raw text with &lt;code&gt;pymupdf&lt;/code&gt; first, then send that text to a text-only model. Text models don’t make up numbers from pixels.&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="c1"&gt;# Before
&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_render_pdf_pages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# After
&lt;/span&gt;&lt;span class="n"&gt;pdf_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_extract_pdf_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Poisoned Cache
&lt;/h3&gt;

&lt;p&gt;Fixed… but the system still said 7.1%. That first wrong answer had been stored in Working Memory as a "fact," and every query pulled the cached mistake instead of re-reading the file. I had to manually delete it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;workingmemory&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="k"&gt;ILIKE&lt;/span&gt; &lt;span class="s1"&gt;'%HbA1c%7.1%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(This is exactly why Working Memory expires quickly — bad entries usually flush themselves, but this one got retrieved so often it kept extending its stay!)&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Silent Truncation
&lt;/h3&gt;

&lt;p&gt;Fixed again… still wrong. Finally, I checked the database: the model output correct values, but my ingestion pipeline cut every chunk at 500 characters. The HbA1c row sat at position 600, so it never made it to disk.&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="c1"&gt;# The culprit
&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;source_label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;para&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Now: full documents stored whole; chat messages still chunked
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;source_type&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;source_label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;para&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;source_label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;para&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Takeaway&lt;/strong&gt;: Never trust just the chat response — check the source, model output, &lt;em&gt;and&lt;/em&gt; database.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deployment: Boring Is Beautiful
&lt;/h2&gt;

&lt;p&gt;Everything runs on Alibaba Cloud, kept simple and maintainable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ECS&lt;/strong&gt;: Single Docker container (FastAPI + Nginx) for the Serving Path, deployed via GitHub Actions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ApsaraDB RDS&lt;/strong&gt;: PostgreSQL + &lt;code&gt;pgvector&lt;/code&gt; holds all memory tiers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OSS&lt;/strong&gt;: Stores raw uploads from Ingest&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EventBridge&lt;/strong&gt;: Triggers the nightly Dream Phase cron, which scales to zero when done&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One &lt;code&gt;docker run&lt;/code&gt; command ties it all together; credentials live in GitHub Secrets. No Kubernetes, no over-engineering — just what works.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hard-Won Lessons (So You Save Time)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check your dependencies&lt;/strong&gt;: &lt;code&gt;pymupdf&lt;/code&gt; once missing from &lt;code&gt;requirements.txt&lt;/code&gt; crashed my deploy on startup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vision describes, text transcribes&lt;/strong&gt;: Need exact numbers from a document? Extract raw text first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One size doesn’t fit all&lt;/strong&gt;: Chat messages and lab reports are different — don’t force the same chunk size on both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test background jobs&lt;/strong&gt;: I polished the real-time pipeline but barely tested the nightly cron. Always verify the parts that run when you’re asleep.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;Àtúnbí. Reborn.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built for the Qwen Cloud Hackathon. &lt;a href="https://github.com/Dotkeyz/atunbi" rel="noopener noreferrer"&gt;View the code on GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;How do you approach long-term memory in LLM apps? What’s the trickiest bug you’ve chased? Let’s talk in the comments!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>hackathon</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
