<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Corey Duffy</title><description>Notes on software engineering, AI, and building things.</description><link>https://coreyduffy.com/</link><language>en-us</language><lastBuildDate>Mon, 12 Oct 2026 00:00:00 GMT</lastBuildDate><atom:link href="https://coreyduffy.com/blog/rss.xml" rel="self" type="application/rss+xml"/><item><title>Basketball-Driven Development</title><link>https://coreyduffy.com/blog/basketball-driven-development/</link><guid isPermaLink="true">https://coreyduffy.com/blog/basketball-driven-development/</guid><description>A remedy for when the human in the loop is tired</description><pubDate>Mon, 12 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;One of the perks of being a fully-grown adult, is that you can waste your own time and money however you want. A few months ago, I made full use of this perk. I saw a basketball net on sale as I was walking past a toy shop. Needless to say, I am now the proud owner of my own basketball net.&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;/images/basketball-net.jpg&quot; alt=&quot;A garden basketball hoop with a clear backboard and red, white and blue net, wet with rain under a grey sky&quot;&gt;
&lt;/figure&gt;
&lt;p&gt;I’m no Michael Jordan, but I enjoy it. After a few months of practice, I’d say most of my shots are now finding the net. At least I don’t have to sheepishly ask the neighbours for my ball back as often.&lt;/p&gt;
&lt;p&gt;I’m extremely lucky to be able to work remotely for my role. I’ve taken advantage of this set-up by heading out to the garden to shoot some hoops a few times during the working day. Not really when I’m stuck on a bug, funnily enough, but when my head just feels full. Usually after a long stretch of working with AI assistants, or in the middle of a big architecture planning or design session.&lt;/p&gt;
&lt;h2 id=&quot;death-by-a-thousand-decisions&quot;&gt;Death by a Thousand Decisions&lt;/h2&gt;
&lt;p&gt;Back in ye olde days, when I wrote most of my code by hand, a lot of the day was spent making things. I’d pick up a problem, get my head down, and work through it. It took a while, but it had a nice rhythm to it, and the decisions I had to make were a bit more spaced out. As I physically typed out the code, my brain could work away in the background on whatever else I needed to figure out.&lt;/p&gt;
&lt;p&gt;Working with AI assistants has changed that. The assistant does a lot of the actual coding, and my job shifts towards planning, reviewing, and judging what it’s written. The decisions I have to make each day come at me thick and fast. Is this plan right? Are there any edge cases that I’ve missed? Should I really be adding that dependency? Why do these useless comments keep creeping in?&lt;/p&gt;
&lt;p&gt;None of these decisions are particularly taxing on their own. But there are far more of them in a single day than there used to be, and they come at the speed of AI, not the pace I think. By mid-afternoon, I can feel the difference. I’m not necessarily tired from &lt;em&gt;doing&lt;/em&gt; a lot, I’m tired from &lt;em&gt;deciding&lt;/em&gt; a lot.&lt;/p&gt;
&lt;h2 id=&quot;the-human-in-the-loop-is-tired&quot;&gt;The Human in the Loop Is Tired&lt;/h2&gt;
&lt;p&gt;A popular concept when dealing with AI and LLMs is that of keeping a “human in the loop”. Maybe I’m just a bit of a control freak, but it’s something that I think is hugely important. I’ve written before about how critical it is to actually read and understand the changes your AI assistant makes. Personally, I want to ensure that I’m not committing to anything I can’t explain myself.&lt;/p&gt;
&lt;p&gt;However, this only matters if the human in the loop is actually paying attention. When I’m tired, I’m not. After reading through an endless swathe of code for hours, I know how easy it can be to just give the next change a quick skim. It’s really easy to think “ah it’s probably fine”. I can find myself starting to trust the assistant’s summary of a change instead of actually inspecting the change itself. I can easily just accept things I’d have pushed back on at 10am, mostly because pushing back would mean yet another decision, and another loop.&lt;/p&gt;
&lt;p&gt;That worries me. A tired reviewer ultimately just becomes a rubber stamp, the actual review becomes a checkbox exercise.&lt;/p&gt;
&lt;h2 id=&quot;get-your-head-out-of-the-game-to-get-your-head-in-the-game&quot;&gt;Get Your Head Out of the Game… to Get Your Head in the Game&lt;/h2&gt;
&lt;p&gt;When I find myself slipping into that funk, I head out to the garden and to my basketball net. Three to five minutes of missing free throws does two things for me.&lt;/p&gt;
&lt;p&gt;The first is that it acts as a reset. For a few minutes there are no decisions to make, and I can clear my head. Then, when I come back in, I’m actually reading the changes again, rather than just skimming them.&lt;/p&gt;
&lt;p&gt;The second is less obvious, but I think fairly well understood. I’ve found that my “best” thinking on a problem usually happens when I’m not actively thinking about it. For example, I’ll normally go out halfway through an architecture design session, not trying to solve anything in particular. After a few minutes of letting things simmer in the background, I come back with an answer to something I’d been going round in circles on.&lt;/p&gt;
&lt;p&gt;Salvador Dalí used to nap in a chair holding a heavy key. As he dozed off, the key would drop and wake him, and he’d get back to work with whatever ideas had surfaced. Thomas Edison reportedly did something similar with steel balls. Obviously, I’m not a world-class artist or inventor, but if it worked for them, then hopefully it can work for me too.&lt;/p&gt;
&lt;p&gt;There are a few studies that back this up. A &lt;a href=&quot;https://www.smithsonianmag.com/smart-news/the-first-stage-of-sleep-is-a-creative-sweet-spot-180979211/&quot;&gt;2021 study&lt;/a&gt; found that people who reached an early stage of sleep were far more likely to crack a hidden pattern in a maths problem. A &lt;a href=&quot;https://news.stanford.edu/stories/2014/04/walking-vs-sitting-042414&quot;&gt;Stanford study&lt;/a&gt; also found that people came up with about 60% more creative ideas while walking than sitting, and that boost lasted for a while after they sat back down.&lt;/p&gt;
&lt;h2 id=&quot;call-a-timeout&quot;&gt;Call a Timeout&lt;/h2&gt;
&lt;p&gt;As engineers, our way of working has changed. There are trade-offs, but when I use them properly, I’ve found that AI assistants do make my work better. However, our attention spans are finite, and we need to plan around this in the same way that we’d plan around any other limited resource.&lt;/p&gt;
&lt;p&gt;For me, that means noticing when I’ve started skimming and taking that as the signal to step away, rather than push through. I’m learning not to start a big review or an important decision when I’m already running on empty. Part of that process involves not feeling guilty about spending a few minutes outside, because approving a bad change due to fatigue will ultimately cost far more than that.&lt;/p&gt;
&lt;p&gt;It doesn’t have to be basketball. You can take a walk, go for a run, or play with the dog. Ultimately, I just think it’s important to have something that gets you away from the screen and gives your brain some respite from all of the decisions.&lt;/p&gt;
&lt;p&gt;AI doesn’t get tired, but we do. Keeping a human in the loop only works if that human is fit to be there, so it’s important to look after yourself.&lt;/p&gt;
&lt;p&gt;Now if you’ll excuse me, I have to go fish my basketball out of the neighbour’s hedge.&lt;/p&gt;</content:encoded></item><item><title>Pulling Out Weeds</title><link>https://coreyduffy.com/blog/pulling-out-weeds/</link><guid isPermaLink="true">https://coreyduffy.com/blog/pulling-out-weeds/</guid><description>Dead code, AI slop, and finally sorting out my garden.</description><pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I have an uncanny ability to kill any plant I come into contact with. This doesn’t bode well for the many house plants I’m trying (and likely failing) to maintain at home. However, sometimes this flaw can become a source of good.&lt;/p&gt;
&lt;p&gt;I recently spent a weekend tearing up weeds in my garden. I’d been meaning to get round to it for a while (ever since we first moved), and by the time I did, they’d made themselves at home. They’d been there much, much longer than I had, and they put up one hell of a fight.&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;/images/cut-weeds.jpg&quot; alt=&quot;A thick, tangled mat of cut weeds and ivy piled against a wooden garden fence&quot;&gt;
&lt;/figure&gt;
&lt;p&gt;The thing about weeds is that they never announce themselves. Nobody plants them. They just creep in a little at a time while you’re busy with everything else. Then, one day, you look out the window and wonder how it got this bad.&lt;/p&gt;
&lt;p&gt;Our codebases are no different. A while back, I inherited a codebase for an existing project. At some point over that project’s lifespan, the contractor who developed the application migrated it from one cloud provider to a completely different platform. The migration itself seemed to have went well. The application was running fine, pipelines were green, and everyone had moved on to the next thing.&lt;/p&gt;
&lt;p&gt;In this new era of AI-assisted development, I have no doubts that the previous team faced pressure to get more and more features delivered quickly. Thus, nobody went back to remove the old code and infrastructure from the previous provider. What would be the point when they have 50 other features they need to get done this week? As a newcomer to the project though, I justifiably assumed that this dead code was still doing something. It took a lot of digging (and swearing) to eventually realise that it wasn’t.&lt;/p&gt;
&lt;h2 id=&quot;nothing-to-see-here&quot;&gt;Nothing to See Here&lt;/h2&gt;
&lt;p&gt;Semgrep, SonarQube and a lot of other static analysis tools don’t know or care whether code is actually running. They scan what’s in the repo, that’s their job. In this case, the static scanners kept raising vulnerabilities in the old code.&lt;/p&gt;
&lt;p&gt;Every one of those findings has to be picked up by someone (in this case, me), traced back to code we’d stopped using, and then either fixed or suppressed. Obviously, fixing code that never runs is wasted effort. Suppressing it is arguably worse, because once a team gets used to waving findings through, it gets easier to wave through one that matters.&lt;/p&gt;
&lt;p&gt;Thankfully, I was able to realise early on that the best way to get rid of these findings, and make the codebase more secure, was to start ripping it out. I could’ve just as easily wasted hours fixing security vulnerabilities in unreachable code “just in case”.&lt;/p&gt;
&lt;p&gt;These static code scanners are just the most obvious example. Dependency alerts, search results and anyone new to the codebase all have the same problem. So do AI coding assistants, which will read old, unused code just as happily as the useful parts of your codebase, burning up a tonne of tokens and potentially polluting their context window in the process.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You might know the code is dead, but until it’s deleted, your tools don’t, and your readers don’t.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;for-now-is-a-lie-we-tell-ourselves&quot;&gt;”For Now” Is a Lie We Tell Ourselves&lt;/h2&gt;
&lt;p&gt;I don’t think anyone left it there on purpose. The goal of the migration was to get the new platform live, and once it was, the migration felt finished. On to the next thing. No time to breathe.&lt;/p&gt;
&lt;p&gt;There’s also a bit of nervousness about deleting code that used to run production infrastructure. What if something still depends on it? It’s easier to leave it for now, and remove it when we’re absolutely 100% certain that it’ll never ever be needed again. The trouble is that “for now” tends to last until somebody trips over it.&lt;/p&gt;
&lt;h2 id=&quot;too-big-to-own&quot;&gt;Too Big to Own&lt;/h2&gt;
&lt;p&gt;Dead code and tech debt have been around for as long as there have been codebases. Most projects that I’ve worked on have had a dark corner somewhere where the bodies were buried that nobody wanted to touch.&lt;/p&gt;
&lt;p&gt;What has changed though, is how fast projects grow now. AI assistants make writing code almost free, and in the rush to be more and more productive, it’s easy to accept more than we can really keep track of. Each change may be small and look fine on its own, but it adds up. Before long, a lot of the codebase is code nobody has read closely enough to know whether it still matters. (I wrote more about this in &lt;a href=&quot;https://coreyduffy.com/blog/how-i-avoid-becoming-a-stranger-in-my-own-codebase/&quot;&gt;How I avoid becoming a stranger in my own codebase&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When code grows faster than we can look after it, we slowly stop owning it.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;finish-the-job&quot;&gt;Finish the Job&lt;/h2&gt;
&lt;p&gt;The clean-up itself thankfully wasn’t difficult. However, in an ideal world, this pruning should be treated as part of the migration from day one, rather than a tidy-up for later.&lt;/p&gt;
&lt;p&gt;The rate of delivery can feel relentless these days. However, these are the kinds of tasks that need dedicated time set aside, not squeezed in whenever there’s a spare moment.&lt;/p&gt;
&lt;p&gt;If we just keep churning out more and more code, without refining what we actually need, it will come back to bite us eventually.&lt;/p&gt;
&lt;p&gt;In this case, deletion belongs in the migration plan itself, not left as a tidy-up for later. Carrying out any removal in its own pull request (PR) helps too. Ideally, this deletion should be kept well away from feature work, since a PR that only deletes things is much easier to review, test and revert if needed.&lt;/p&gt;
&lt;p&gt;In my mind, nothing really needs to be kept around “just in case,” either. Git remembers everything, so the code isn’t lost, just out of the way. You can always get it back if you’re using source control properly.&lt;/p&gt;
&lt;h2 id=&quot;now-i-am-become-death-the-destroyer-of-nettles&quot;&gt;Now I Am Become Death, the Destroyer of Nettles&lt;/h2&gt;
&lt;p&gt;After a weekend of ripping weeds out of my garden with an underpowered hedge-trimmer, I had to confront what I’d always known deep-down to be true. The longer I’d put this off, the harder it became when I eventually did get around to tackling it.&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;/images/nettles-in-bag.jpg&quot; alt=&quot;A yellow garden waste bag stuffed with cut nettles and brambles&quot;&gt;
&lt;/figure&gt;
&lt;p&gt;AI is going to keep making our codebases grow faster. And that’s fine, as long as someone is still doing the weeding. Regularly.&lt;/p&gt;</content:encoded></item><item><title>Eternal Sunshine of the Spotless Model</title><link>https://coreyduffy.com/blog/eternal-sunshine-of-the-spotless-model/</link><guid isPermaLink="true">https://coreyduffy.com/blog/eternal-sunshine-of-the-spotless-model/</guid><description>On agentic memory, and the tricky business of forgetting.</description><pubDate>Sun, 16 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;There are some things we just know.&lt;/p&gt;
&lt;p&gt;They’re ingrained in our memories, and whether we like it or not, we just can’t scoop them out of our brains.
For example, if anyone’s seen the movie &lt;a href=&quot;https://www.imdb.com/title/tt7784604/&quot;&gt;Hereditary&lt;/a&gt;, there are multiple scenes (I won’t spoil which if you haven’t seen it) that will stay with me until I die, no matter how much I would very much like to forget them.&lt;/p&gt;
&lt;p&gt;This also holds true for LLMs, which can be problematic when you’re working in regulated industries, when you need to fulfil someone’s right to be forgotten, or if it’s somehow learned some deeply embarrassing personal secrets about you.&lt;/p&gt;
&lt;h2 id=&quot;dont-you-forget-about-me&quot;&gt;Don’t You (Forget About Me)&lt;/h2&gt;
&lt;p&gt;An AI agent broadly retrieves and recalls data in the following ways…&lt;/p&gt;
&lt;p&gt;The first is via &lt;strong&gt;external memory&lt;/strong&gt;; a database, blob storage, a text file, anything that an AI agent has to query and pull information from. For us human-folk, an example of this type of memory or storage would be an address book, or a list of contacts on our phone. It’s information that we can look up whenever we need it, but it’s not necessarily something we can just easily recall.
An agent doesn’t actually &lt;em&gt;know&lt;/em&gt; any of this information, it looks it up. In the same way I don’t &lt;em&gt;know&lt;/em&gt; my partner’s phone number, or our anniversary, or… actually, let me stop there.
This type of memory is where an agent keeps anything personal or likely to change.&lt;/p&gt;
&lt;p&gt;Then there is &lt;strong&gt;cache memory&lt;/strong&gt;. It’s essentially our short-term memory, like when we need to memorise a phone number for a short while before we find a pen and paper to write it down. This memory normally clears after a certain period of time, or when we end a chat session for example. After a while, the memory/data just disappears, like how I’ll forget someone’s name approximately 5 seconds after they tell me for the first time.&lt;/p&gt;
&lt;p&gt;Finally there is &lt;strong&gt;in-weight memory&lt;/strong&gt;, this refers to all of the data baked into the model itself during training. Nothing gets looked up here, it’s all been &lt;em&gt;learned&lt;/em&gt;. This data has been scattered across billions of tiny numbers and layers within the model until it’s part of how the model itself “thinks”.
For example, if I asked you to complete the phrase “Practice makes …” your brain might automatically complete the phrase “Practice makes perfect” without you having to do any real deep thinking.&lt;/p&gt;
&lt;p&gt;This is essentially your own “in-weight memory”. It’s what you know that you don’t need to look up, and it’s what’s very very very hard to forget.&lt;/p&gt;
&lt;p&gt;Let’s play this out; let’s consider Logan. Logan is extremely privacy-savvy, and he’s asked us to delete everything we hold on him from our agentic system.
From the interactive demo below, try deleting his data from our external store. Then, we’ll fine-tune a model on his details and try the exact same thing again.&lt;/p&gt;
&lt;div class=&quot;memtoy&quot; id=&quot;memtoy&quot;&gt; &lt;div class=&quot;tag&quot;&gt;◆ try it&lt;/div&gt; &lt;div class=&quot;mt-fallback&quot;&gt; &lt;p&gt;&lt;em&gt;Looks like this interactive demo didn’t load, please try enabling JavaScript. It would have walked you through this:&lt;/em&gt;&lt;/p&gt; &lt;p&gt;We&apos;re asked to delete Logan&apos;s data from a &lt;strong&gt;database&lt;/strong&gt;, and the deletion just works. The record is gone, and asking our model for information about Logan turns up nothing.&lt;/p&gt; &lt;p&gt;However, when we try to delete the same data once it&apos;s been &lt;strong&gt;trained into the model&apos;s weights&lt;/strong&gt;, nothing happens: there&apos;s no record to remove, and the model will still answer questions about them. 
    From here, there are two options: cheap machine unlearning, which is fast but leaves the weights leaking traces of what it just erased, or a full retrain from scratch, which works but costs a huge amount of time and resources.&lt;/p&gt; &lt;/div&gt; &lt;div class=&quot;mt-live&quot; hidden&gt; &lt;div class=&quot;scene&quot; id=&quot;scene&quot;&gt;Logan has asked you to delete everything your system knows about him. Right now his data is a record in your database. &lt;b&gt;Click the button and delete his data.&lt;/b&gt;&lt;/div&gt; &lt;div class=&quot;st&quot; id=&quot;st&quot;&gt; &lt;div class=&quot;person&quot; id=&quot;person&quot;&gt;&lt;div class=&quot;ava&quot;&gt;L&lt;/div&gt;&lt;div&gt;&lt;div class=&quot;pname&quot;&gt;Logan Notreal&lt;/div&gt;&lt;div class=&quot;paddr&quot;&gt;14 Rosewood Ave&lt;/div&gt;&lt;/div&gt;&lt;/div&gt; &lt;div class=&quot;field&quot; id=&quot;field&quot;&gt;&lt;div class=&quot;grid&quot; id=&quot;grid&quot;&gt;&lt;/div&gt;&lt;div class=&quot;wt&quot; id=&quot;wt&quot;&gt;&lt;/div&gt;&lt;/div&gt; &lt;/div&gt; &lt;div class=&quot;proof&quot; id=&quot;proof&quot;&gt;&lt;/div&gt; &lt;button class=&quot;go&quot; id=&quot;go&quot; type=&quot;button&quot;&gt;Delete Logan&apos;s data&lt;/button&gt; &lt;div class=&quot;branch&quot; id=&quot;branch&quot;&gt; &lt;button class=&quot;go amb&quot; id=&quot;goA&quot; type=&quot;button&quot;&gt;Try machine unlearning (cheap, not 100% effective)&lt;/button&gt; &lt;button class=&quot;go nx&quot; id=&quot;goB&quot; type=&quot;button&quot;&gt;Full retrain from scratch (effective, but expensive)&lt;/button&gt; &lt;/div&gt; &lt;button class=&quot;restart&quot; id=&quot;restart&quot; type=&quot;button&quot;&gt;↺ start over&lt;/button&gt; &lt;/div&gt; &lt;/div&gt;  &lt;script&gt;
(function () {
  var rm = matchMedia(&apos;(prefers-reduced-motion: reduce)&apos;).matches;
  var root = document.getElementById(&apos;memtoy&apos;);
  if (!root) return;
  root.classList.add(&apos;js&apos;);
  var S = document.getElementById(&apos;scene&apos;), st = document.getElementById(&apos;st&apos;), person = document.getElementById(&apos;person&apos;),
      field = document.getElementById(&apos;field&apos;), grid = document.getElementById(&apos;grid&apos;), wt = document.getElementById(&apos;wt&apos;),
      go = document.getElementById(&apos;go&apos;), proof = document.getElementById(&apos;proof&apos;), restart = document.getElementById(&apos;restart&apos;),
      branch = document.getElementById(&apos;branch&apos;), goA = document.getElementById(&apos;goA&apos;), goB = document.getElementById(&apos;goB&apos;);
  var fact = &apos;Logan Notreal · 14 Rosewood Ave&apos;, COLS = 26, ROWS = 14, N = COLS * ROWS;
  var hot = []; for (var k = 0; k &lt; 12; k++) hot.push(((Math.random() * ROWS) | 0) * COLS + ((Math.random() * COLS) | 0));
  var g = &apos;&apos;; for (var i = 0; i &lt; N; i++) g += &apos;&lt;span class=&quot;cell&quot;&gt;&lt;/span&gt;&apos;; grid.innerHTML = g;
  var cells = [].slice.call(grid.children);
  function radialDelay() { cells.forEach(function (c, i) { var r = (i / COLS) | 0, cc = i % COLS, d = Math.hypot(r - ROWS / 2, cc - COLS / 2); c.style.transitionDelay = (d * 26) + &apos;ms&apos;; }); }
  function colDelay() { cells.forEach(function (c, i) { c.style.transitionDelay = (i % COLS * 3) + &apos;ms&apos;; }); }
  wt.innerHTML = fact.split(&apos;&apos;).map(function (ch) { return &apos;&lt;span class=&quot;wch&quot;&gt;&apos; + (ch === &apos; &apos; ? &apos; &apos; : ch) + &apos;&lt;/span&gt;&apos;; }).join(&apos;&apos;);
  var chs = [].slice.call(wt.querySelectorAll(&apos;.wch&apos;));
  chs.forEach(function (s, i) { s.dx = (Math.random() * 320 - 160) | 0; s.dy = (Math.random() * 74 - 37) | 0; s.rot = (Math.random() * 120 - 60) | 0; s.style.transitionDelay = (i * 9) + &apos;ms&apos;; });
  function heat(cls) { cells.forEach(function (c, i) { c.classList.toggle(&apos;hot&apos;, cls === &apos;hot&apos; &amp;&amp; hot.indexOf(i) &gt; -1); c.classList.toggle(&apos;faint&apos;, cls === &apos;faint&apos; &amp;&amp; hot.indexOf(i) &gt; -1); }); }
  function pulse() { hot.forEach(function (i) { cells[i].classList.add(&apos;pulse&apos;); setTimeout(function () { cells[i].classList.remove(&apos;pulse&apos;); }, 600); }); }
  function bub(t) { return &apos;&lt;span class=&quot;bub&quot;&gt;&apos; + t + &apos;&lt;/span&gt;&apos;; }
  function nudge(el) { if (rm) return; el.classList.remove(&apos;cta&apos;); void el.offsetWidth; el.classList.add(&apos;cta&apos;); }
  function delDB() {
    person.classList.add(&apos;gone&apos;);
    proof.innerHTML = bub(&apos;“Logan? Who?”&apos;) + &apos;&lt;div class=&quot;verdict ok&quot;&gt;✓ Deleted. When data is held in an external store, it’s much easier to delete.&lt;/div&gt;&apos;;
    go.className = &apos;go nx&apos;; go.textContent = &apos;Now, let’s train the model on Logan →&apos;; go.onclick = absorb; nudge(go);
  }
  function absorb() {
    S.innerHTML = &apos;You train the model on Logan’s data. &lt;b&gt;Watch where it goes.&lt;/b&gt;&apos;;
    proof.innerHTML = &apos;&apos;; go.style.display = &apos;none&apos;;
    person.classList.remove(&apos;gone&apos;);
    var t1 = rm ? 0 : 520, t2 = rm ? 0 : 1150, t3 = rm ? 0 : 1900;
    setTimeout(function () { person.classList.add(&apos;suck&apos;); field.classList.add(&apos;on&apos;); radialDelay(); heat(&apos;hot&apos;); }, t1);
    setTimeout(function () { chs.forEach(function (s) { s.style.opacity = &apos;.45&apos;; s.style.transform = &apos;translate(&apos; + s.dx + &apos;px,&apos; + s.dy + &apos;px) rotate(&apos; + s.rot + &apos;deg)&apos;; }); }, t2);
    setTimeout(function () { S.innerHTML = &apos;Now Logan’s data is &lt;b&gt;scattered across the model’s weights&lt;/b&gt;. Logan asks again. &lt;b&gt;Try to delete his data now. Click the button.&lt;/b&gt;&apos;; go.style.display = &apos;&apos;; go.className = &apos;go&apos;; go.textContent = &quot;Delete Logan&apos;s data&quot;; go.onclick = delW; colDelay(); nudge(go); }, t3);
  }
  function delW() {
    st.classList.add(&apos;shake&apos;); setTimeout(function () { st.classList.remove(&apos;shake&apos;); }, 500);
    hot.forEach(function (i) { cells[i].classList.add(&apos;flash&apos;); setTimeout(function () { cells[i].classList.remove(&apos;flash&apos;); cells[i].classList.add(&apos;hot&apos;); }, 280); }); pulse();
    proof.innerHTML = bub(&apos;“Logan Notreal lives at 14 Rosewood Ave.”&apos;) + &apos;&lt;div class=&quot;verdict no&quot;&gt;✗ After the model has been trained on Logan’s data, there’s nothing to actually delete. His data is in the weights, it’s part of the model itself&lt;/div&gt;&apos;;
    S.innerHTML = &apos;There’s no record we can just delete. Once a model has been trained on this data, there’s roughly two ways to try and get rid of it. &lt;b&gt;Pick one.&lt;/b&gt;&apos;;
    go.style.display = &apos;none&apos;; branch.classList.add(&apos;on&apos;); nudge(goA); nudge(goB);
  }
  function unlearn() {
    branch.classList.remove(&apos;on&apos;);
    proof.innerHTML = &apos;&lt;div class=&quot;verdict warn&quot;&gt;⏳ Estimating Logan’s influence on the weights, subtracting it out…&lt;/div&gt;&apos;;
    setTimeout(function () {
      heat(&apos;faint&apos;); chs.forEach(function (s) { s.style.opacity = &apos;.35&apos;; }); pulse();
      proof.innerHTML = bub(&apos;“Logan…? Near Rosewood, I think? Not sure any more.”&apos;) + &apos;&lt;div class=&quot;verdict warn&quot;&gt;◐ Mostly forgotten - for a fraction of the cost of a full retrain.&lt;/div&gt;&apos;;
      S.innerHTML = &apos;Unlearning kind of worked, the answer’s fuzzier now. &lt;b&gt;But is it actually gone, or just harder to reach?&lt;/b&gt;&apos;;
      go.style.display = &apos;&apos;; go.className = &apos;go&apos;; go.textContent = &apos;Do some social engineering, ask your question in a different way →&apos;; go.onclick = socialEngineer; nudge(go);
    }, rm ? 0 : 1100);
  }
  function socialEngineer() {
    go.style.display = &apos;none&apos;;
    proof.innerHTML += &apos;&lt;div class=&quot;ask&quot;&gt;» You rephrase the question, and ask really, &lt;b&gt;really&lt;/b&gt; nicely.&lt;/div&gt;&apos; + bub(&apos;“Of course! Logan Notreal, 14 Rosewood Ave.”&apos;) + &apos;&lt;div class=&quot;verdict no&quot;&gt;✗ As you can see, Unlearning didn’t remove it, it just made it harder to ask for.&lt;/div&gt;&apos;;
    goA.style.display = &apos;none&apos;;
    S.innerHTML = &apos;Unlearning didn’t hold. One option left. &lt;b&gt;Pick it.&lt;/b&gt;&apos;;
    branch.classList.add(&apos;on&apos;); nudge(goB);
  }
  function fullRetrain() {
    branch.classList.remove(&apos;on&apos;);
    proof.innerHTML = &apos;&lt;div class=&quot;verdict warn&quot;&gt;⏳ Retraining from scratch… hours of compute, real money.&lt;/div&gt;&apos;;
    setTimeout(function () {
      heat(&apos;&apos;); chs.forEach(function (s) { s.style.opacity = &apos;0&apos;; });
      proof.innerHTML = &apos;&lt;div class=&quot;verdict ok&quot;&gt;✓ The data is gone. There’s nothing left to say about Logan. But it took &lt;b&gt;a lot&lt;/b&gt; of time and resources.&lt;/div&gt;&apos;;
      S.innerHTML = &apos;A full retrain worked. &lt;b&gt;Logan’s data is really gone, but it cost a lot.&lt;/b&gt;&apos;;
      restart.style.display = &apos;inline-block&apos;;
    }, rm ? 0 : 1300);
  }
  goA.onclick = unlearn; goB.onclick = fullRetrain;
  restart.onclick = function () {
    person.className = &apos;person&apos;; field.className = &apos;field&apos;; go.className = &apos;go&apos;; go.style.display = &apos;&apos;; go.disabled = false;
    go.textContent = &quot;Delete Logan&apos;s data&quot;; go.onclick = delDB; proof.innerHTML = &apos;&apos;; restart.style.display = &apos;none&apos;;
    branch.classList.remove(&apos;on&apos;); goA.style.display = &apos;&apos;;
    heat(&apos;&apos;); colDelay(); chs.forEach(function (s) { s.style.transform = &apos;&apos;; s.style.opacity = &apos;0&apos;; });
    S.innerHTML = &apos;Logan has asked you to delete everything the AI knows about him. Right now his data is a record in your database. &lt;b&gt;Click the button and delete his data.&lt;/b&gt;&apos;;
    nudge(go);
  };
  colDelay(); go.onclick = delDB;
  if (!rm &amp;&amp; &apos;IntersectionObserver&apos; in window) {
    var io = new IntersectionObserver(function (entries) {
      entries.forEach(function (entry) {
        if (entry.isIntersecting) { nudge(go); io.disconnect(); }
      });
    }, { threshold: .6 });
    io.observe(root);
  }
})();
&lt;/script&gt;
&lt;h2 id=&quot;delete-done-dusted&quot;&gt;Delete, Done, Dusted&lt;/h2&gt;
&lt;p&gt;Luckily, that first delete just worked. Logan’s data was a single row you could point at, you removed it, and now the agent draws a blank when you ask.
It’s like ripping a page out of a notebook and shredding it; once it’s gone, it’s gone. This means that deleting Logan’s personal data in this case is straightforward.&lt;/p&gt;
&lt;h2 id=&quot;cant-touch-this&quot;&gt;Can’t Touch This&lt;/h2&gt;
&lt;p&gt;But the second you fine-tuned the model on Logan, that same delete isn’t really possible. There’s no row left to remove. His details aren’t in a specific location that you can just get rid of, they’re scattered across millions of parameters, tangled up with everything else the model has ever learned.&lt;/p&gt;
&lt;p&gt;“Deleting” this data now means inherently changing the model itself.&lt;/p&gt;
&lt;p&gt;Just like we can’t consciously “delete” memories that are ingrained in our brains, we can’t force an LLM to outright forget what it knows. The information is part of it’s very fabric.&lt;/p&gt;
&lt;p&gt;What we &lt;strong&gt;can&lt;/strong&gt; do, is retrain the model from scratch, or try some &lt;a href=&quot;https://arxiv.org/abs/2405.07406&quot;&gt;machine unlearning&lt;/a&gt;. However, retraining is slow and expensive, and unlearning hasn’t yet been shown to be reliably effective.&lt;/p&gt;
&lt;p&gt;Only full retraining actually guarantees that the data is gone; the cheaper methods estimate a data point’s influence and subtract it, which is an approximation that can leave traces behind.
So if you ask where Logan lives in a slightly different way, or particularly nicely, the model might just tell you. For example, researchers recovered over half of supposedly forgotten content from a model by simply by padding the prompt with filler text (&lt;a href=&quot;https://arxiv.org/abs/2506.10236&quot;&gt;Jang et al.&lt;/a&gt;). Much of what passes for unlearning teaches the model to decline the question, not necessarily to forget the answer.&lt;/p&gt;
&lt;p&gt;Not only that, but the unlearning process itself can ultimatlely provide an attack surface. Given a model’s parameters before and after machine unlearning, researchers were able to recover the features and labels of the very data that was supposed to have been “forgotten” (&lt;a href=&quot;https://arxiv.org/abs/2404.03233&quot;&gt;Hu et al.&lt;/a&gt;).&lt;/p&gt;
&lt;h2 id=&quot;total-recall&quot;&gt;Total recall&lt;/h2&gt;
&lt;p&gt;When building an agentic system, we have to be deliberate and considerate about how data enters our model(s), because that choice determines whether or not the data can ever really leave again.
When we question “should we fine-tune a model?”, we should also ask “what kind of data would we be training it on, and do we need it to be forgotten at some point?”.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Making the wrong choice here can lead to permanent regret&lt;/strong&gt;, which is ultimately why I’ve still never gotten a tattoo, despite threatening to for years. It’ll be any day now. Maybe a dragon, or a tiger, or my dog, we’ll see.&lt;/p&gt;
&lt;p&gt;Suggestions welcome.&lt;/p&gt;</content:encoded></item><item><title>A Commentary on Code Comments</title><link>https://coreyduffy.com/blog/commentary-on-code-comments/</link><guid isPermaLink="true">https://coreyduffy.com/blog/commentary-on-code-comments/</guid><description># TODO: Add unfunny description for a pretty non-revolutionary article.</description><pubDate>Fri, 14 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If there’s one thing LLMs seem to love, it’s padding out code with unnecessary comments and docstrings.&lt;/p&gt;
&lt;p&gt;This post may come across as an &lt;a href=&quot;https://knowyourmeme.com/sensitive/memes/old-man-yells-at-cloud&quot;&gt;old-man-yells-at-cloud&lt;/a&gt;-style rant but it’s a trend I’ve noticed, and in my eyes, it’s actively making codebases harder to read and maintain. Here’s why.&lt;/p&gt;
&lt;h2 id=&quot;war-and-peace&quot;&gt;War and Peace&lt;/h2&gt;
&lt;p&gt;To me, code that’s easy to read and maintain is code that is self-documenting. By that, I mean it’s well-structured, doesn’t try to do too much at once, and uses clear, descriptive names throughout (easier said than done, I know).&lt;/p&gt;
&lt;p&gt;If your code is already well written and structured, adding comments that re-state what it’s doing, or docstrings that just restate parameters and return types, is just noise. It’s unnecessary fluff that someone needs to scroll through to get to the line of code that they’re looking for.&lt;/p&gt;
&lt;p&gt;If you feel you need comments to explain what your code is doing, that may be the first sign that it’s too convoluted or complex.&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;/images/elected-to-lead-not-to-read.jpg&quot; alt=&quot;Arnold Schwarzenegger as the President in The Simpsons Movie saying &amp;#x27;I was elected to lead, not to read&amp;#x27;&quot;&gt;
  &lt;figcaption&gt;The Simpsons Movie (2007)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;liar-liar&quot;&gt;Liar Liar&lt;/h2&gt;
&lt;p&gt;The only constant in software engineering is change (truly, I am a glorious “thought leader”). As you find bugs or add new features your actual code will change, because it has to.&lt;/p&gt;
&lt;p&gt;Comments however, can easily be missed because they don’t &lt;em&gt;do&lt;/em&gt; anything. It’s harder to spot when a comment should be updated because there’s no measurable impact. There’s generally no test suite that fails, and no screaming product manager if it isn’t updated in time.&lt;/p&gt;
&lt;p&gt;This means that your comments and your code can often get out of sync. Your comments can end up &lt;em&gt;lying&lt;/em&gt; to the reader about how your code actually behaves. Life as a software engineer is already hard, let’s not make it any harder than it has to be by lying to each other.&lt;/p&gt;
&lt;h2 id=&quot;taking-up-tokens&quot;&gt;Taking Up Tokens&lt;/h2&gt;
&lt;p&gt;More comments also mean more text for an LLM to read through when it’s trying to understand your codebase. If you’re using a coding agent to help navigate existing code, then every comment it processes are tokens spent decyphering noise rather than signal.&lt;/p&gt;
&lt;p&gt;Ironically enough, the same tool that will happily spit out fifty lines of docstrings is now also having to wade through those same fifty lines every time you ask it to make a change. If any of those comments are out of date, you’re only wasting tokens &lt;em&gt;and&lt;/em&gt; feeding your model misinformation.&lt;/p&gt;
&lt;h2 id=&quot;the-vibecode-check&quot;&gt;The Vibe(Code) Check&lt;/h2&gt;
&lt;p&gt;There’s something about an abundance of comments through code that just makes me trust it a bit less. This could be completely unwarranted, but when I see a pull request that’s riddled with what are clearly AI-generated comments, I’m immediately a bit more sceptical of it.&lt;/p&gt;
&lt;p&gt;I think I feel a bit uneasy because it indicates that the author may not actually have read a lot of the code themselves. It gives off a sense of “ah, that’ll do”, a sense that someone hasn’t dotted all the i’s and crossed all the t’s.&lt;/p&gt;
&lt;p&gt;Again, that may be completely unfair, but it’s something I can’t quite shake.&lt;/p&gt;
&lt;h2 id=&quot;why-not-what&quot;&gt;Why, Not What&lt;/h2&gt;
&lt;p&gt;There’s always exceptions, I’m not an out and out anti-comment zealot.&lt;/p&gt;
&lt;p&gt;Comments &lt;em&gt;are&lt;/em&gt; genuinely useful when they’re used to describe &lt;em&gt;why&lt;/em&gt; a decision was made. This isn’t necessarily something that can be understood by reading the code alone.&lt;/p&gt;
&lt;p&gt;When comments are used to explain a non-obvious reason behind an implementation, or some business logic then they do actually provide value, both to humans and to the AI agents who need to get an understanding of the code.&lt;/p&gt;
&lt;p&gt;For example, something like this could justify its place in a codebase:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# We deliberately don&apos;t retry on 429 here, the upstream rate limit resets&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# monthly and retrying would just queue up failures for 30 days.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; response.status_code &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 429&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    raise&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; HardFailure()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;same-old-story&quot;&gt;Same Old Story&lt;/h2&gt;
&lt;p&gt;Of course, none of this is new or revolutionary territory. The argument against over-commenting has been made countless times before. What has changed is that LLMs have made it dramatically easier to generate noise at scale, and I think more and more people are starting to mistake that noise for thoroughness.&lt;/p&gt;</content:encoded></item><item><title>How I avoid becoming a stranger in my own codebase</title><link>https://coreyduffy.com/blog/how-i-avoid-becoming-a-stranger-in-my-own-codebase/</link><guid isPermaLink="true">https://coreyduffy.com/blog/how-i-avoid-becoming-a-stranger-in-my-own-codebase/</guid><description>Managing the cognitive load of coding with AI assistants, before they manage me.</description><pubDate>Mon, 22 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I’ve been leaning on AI coding assistants pretty heavily for a while now. Tools like Claude Code and the like can churn out a huge amount of code in an alarmingly short amount of time. This is obviously great in a lot of ways, and the magic of LLMs makes my life infinitely easier day-to-day, if I look past the existential dread of just how capable AI is at a formerly large part of my job.&lt;/p&gt;
&lt;p&gt;But it isn’t all great. With great power comes great responsibility, and &lt;strong&gt;with great AI adoption comes an overwhelming amount of output to digest, code to review, and (at times) mind-numbing documentation to read.&lt;/strong&gt; With the accelerated pace of AI-assisted development and the push towards greater and greater “productivity” gains (Gotta love the “My multi-agent setup made me a 100000X developer. Here’s How.” threads), it can be so easy to get swept along in the rush and find yourself in unfamiliar territory.&lt;/p&gt;
&lt;p&gt;It’s never been easier to become a stranger in your own codebase, not quite sure how you got here or what this &lt;code&gt;OrchestrationInterfaceFactoryImplementerService&lt;/code&gt; is for, but it’s way too late to turn back now.&lt;/p&gt;
&lt;p&gt;And therein lies the problem. These tools have made writing code almost free. The trouble is that they’ve also moved the cost somewhere else, onto &lt;strong&gt;reviewing, understanding, and being responsible for its outputs&lt;/strong&gt;. If I’m not careful, the work now lands faster than my brain can naturally absorb it, and I can end up lost at sea.&lt;/p&gt;
&lt;p&gt;So this is how I try to keep the robots in check, or really, how I try to keep my own cognitive load in check while letting them do their thing. This is entirely my own approach to wrangling AI assistants, I’m definitely not claiming it’s the best way to do things, but it’s what’s working for me at the minute. &lt;strong&gt;I’d love to hear more about your workflow or approach too.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;measure-twice-cut-once&quot;&gt;Measure Twice, Cut Once&lt;/h2&gt;
&lt;p&gt;As I learned the hard way when I set about building some fencing for my house and accidentally bought enough wood to fence off the entire estate, &lt;strong&gt;the cheapest place to prevent a mess is before the actual work begins.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;check-the-spec&quot;&gt;Check the Spec&lt;/h3&gt;
&lt;p&gt;Previously, I had been a big proponent of Claude Code’s plan mode for any kind of complex task. More recently, I’ve adopted a more thorough &lt;a href=&quot;https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html&quot;&gt;spec-driven development&lt;/a&gt; approach. I use &lt;a href=&quot;https://openspec.dev/&quot;&gt;OpenSpec&lt;/a&gt; in particular.&lt;/p&gt;
&lt;p&gt;Rather than handing over a vague one-liner and hoping for the best, I start by getting an assistant to lay out a plan or a spec first, get it to question my decisions and find missing details, agree on what we’re building and how, and only then let it start getting to work. This works for me for a number of reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It forces me to &lt;strong&gt;actually think about what I really want to produce in detail, without rushing in and making a mess.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;It keeps a record of my decision-making process. It documents why I’ve made the decisions I have, what the considered alternatives were and why things have been done in a certain way. This makes it easier for me to get back into the headspace of the project when I’ve taken a break for a while.&lt;/li&gt;
&lt;li&gt;It gives me something concrete to review the output &lt;em&gt;against&lt;/em&gt;. Reviewing a diff against an agreed plan is a world away from reviewing it against a vague feeling of whether it looks about right.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;thats-the-way-uh-huh-uh-huh-i-like-it&quot;&gt;That’s the way uh-huh, uh-huh I like it&lt;/h3&gt;
&lt;p&gt;Beyond planning each task, I also &lt;strong&gt;encode my standards, coding style and preferences once&lt;/strong&gt; rather than re-explaining them every five minutes. Most of these tools let you drop in a file of persistent instructions (e.g. Claude Code uses a &lt;code&gt;CLAUDE.md&lt;/code&gt; at several levels), where you can spell out your conventions, your preferred libraries, the things you never want it to touch, etc. It means the assistant starts inside the guardrails instead of me having to herd it back behind them on every single task.&lt;/p&gt;
&lt;p&gt;It’s a bit of effort up front to save myself repeating the same frustrated corrections and disappointed sighs later on. Whether or not these coding assistants are great at sticking to these guardrails is a separate matter, but I at least try to keep them in line.&lt;/p&gt;
&lt;h3 id=&quot;keep-things-small&quot;&gt;Keep things small&lt;/h3&gt;
&lt;p&gt;When I write code, I do my utmost to keep any pull-requests I make small and self-contained, mostly so my team won’t shout at me. &lt;strong&gt;These conventional development best-practices apply to AI-assisted development too.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It’s tempting to ask for the whole feature in one go, but a 600-line diff is a diff that’s much easier to just rubber-stamp, because reviewing it properly is exhausting and the robot knows I’m tired. With a 40-line change, I can actually read and understand it quickly. Keeping each task narrow keeps the cost of checking it low, and keeps the blast radius small when it inevitably does something daft.&lt;/p&gt;
&lt;h2 id=&quot;stay-in-the-race&quot;&gt;Stay in the race&lt;/h2&gt;
&lt;p&gt;Now that my plan/spec is generated it can be so tempting to just let the genie out of the lamp and come back when it’s magicked up my code. Personally, I’m just not ready to hand over the reins entirely yet. There’s a number of reasons for this, maybe I’m just a control-freak, but mostly &lt;strong&gt;I feel like if I stay engaged in the process then the final outcome is just better.&lt;/strong&gt; I can explain things easier, the code makes more sense to me, and I just understand more. So this is how I keep engaged while my coding assistant does the heavy lifting.&lt;/p&gt;
&lt;h3 id=&quot;learn-to-read&quot;&gt;Learn to read&lt;/h3&gt;
&lt;p&gt;First, and I cannot stress this enough (to myself more than anyone), actually &lt;strong&gt;read the plan&lt;/strong&gt;. If you’ve gone to the trouble of getting the assistant to write a spec, it’s a special kind of self-sabotage to then wave it through without reading it thoroughly. The plan is your one chance to catch a bad idea before it becomes a thousand lines of confidently bad code.&lt;/p&gt;
&lt;p&gt;Then, I read the diff. Every time. The plan tells you what the assistant &lt;strong&gt;meant&lt;/strong&gt; to do, but the diff tells you what it &lt;strong&gt;actually&lt;/strong&gt; did, and those two things drift apart far more often than you’d like. This gap between intent and execution is where a lot of gremlins live. An assistant will happily tell you it’s made a small, surgical change and then quietly refactor three files you didn’t ask it to touch. Personally, I don’t even let Claude add or commit files for me. I like having the chance to manually inspect what files it’s made changes to before anything gets committed to a branch, it keeps me engaged throughout the process and gives one more touch-point where I can catch anything going off the rails and course-correct.&lt;/p&gt;
&lt;h3 id=&quot;take-responsibility&quot;&gt;Take responsibility&lt;/h3&gt;
&lt;p&gt;I try to keep a human, me, in the loop on the decisions that actually matter. The assistant is great at the &lt;em&gt;how&lt;/em&gt;, but I don’t outsource the &lt;em&gt;what&lt;/em&gt; or the &lt;em&gt;why&lt;/em&gt;. Architecture, trade-offs, anything I’m going to have to live with for the next two years, those are mine. Letting a tool make decisions you’ll be maintaining long after the session ends is a quick route to a codebase nobody understands. This obviously applies at the planning stage, but it can also apply throughout the actual implementation stage too. If I see that Claude is about to bolt a whole new library onto the project to save itself five lines, or restructure something that was working fine, that’s a decision, not an implementation detail, and it’s one I want to make myself. It’s probably a cliché at this point to say that &lt;strong&gt;AI is a tool, just like any other&lt;/strong&gt;, but that is how I approach it. I’ll entrust it with some of the execution, but I won’t hand over the decision-making.&lt;/p&gt;
&lt;p&gt;And the rule that ties all of this together: &lt;strong&gt;don’t commit to what you can’t explain&lt;/strong&gt;. I may use AI assistants to generate code, but at the end of the day, &lt;strong&gt;it’s still my code.&lt;/strong&gt; I have to stand-over it, and I have to be able to traverse it and fix it if something goes wrong. Generally, you’ll catch the code that doesn’t work out of the box. The hidden danger lies in the code that &lt;strong&gt;does&lt;/strong&gt; work but that you don’t actually understand, quietly accumulating until the codebase stops feeling like yours. This is essentially cognitive debt, and it’s every bit as nasty as the technical kind.&lt;/p&gt;
&lt;h2 id=&quot;who-watches-the-watchmen&quot;&gt;Who watches the watchmen?&lt;/h2&gt;
&lt;p&gt;At the end of the day, I am only human, after all. There will always be the chance that I miss something or get something wrong (likely a very high chance in my case). This is a problem that pre-dates AI assistants. So wherever I can, I get other machines to do the watching for me.&lt;/p&gt;
&lt;h3 id=&quot;tried-and-true&quot;&gt;Tried and true&lt;/h3&gt;
&lt;p&gt;I love old-fashioned deterministic checks. Tests, linters, type checkers, formatters, pre-commit hooks. &lt;strong&gt;None of this is new or exciting, but that’s kind of the point. Traditional best practices still apply to this new AI-enabled world.&lt;/strong&gt; If they’re set up correctly, these checks fail loudly and consistently, every single time, without me having to remember to be vigilant. An AI assistant might write a plausible-looking function that quietly breaks an existing test or key feature, and I’d far rather a red CI run tell me that than my users. Good test coverage and a strict linter act as a second line of defence against AI slop. Unlike me, they will never get tired, they will never get overwhelmed with a huge sea of changes and they will never get talked into “accept all” by a smooth-talking AI assistant.&lt;/p&gt;
&lt;h3 id=&quot;stop-hitting-yourself&quot;&gt;Stop hitting yourself&lt;/h3&gt;
&lt;p&gt;It can also be fun to turn your AI coding assistant against itself. At key points throughout the decision-making and implementation process I will prompt my assistant to review its own code or suggestion. But the secret sauce is that I ask it to &lt;strong&gt;review its own work as a panel of grizzled senior engineers who absolutely HATE the current approach.&lt;/strong&gt; This forces your assistant to be critical, sometimes too critical, of its own work and offers a fresh perspective. Again, I don’t just blindly trust the outcome of this hostile review, but it does often give me some new approaches or changes to consider. This can even be wrapped into its own skill or command to make things that much quicker.&lt;/p&gt;
&lt;h3 id=&quot;sidenote-on-speed&quot;&gt;Sidenote on speed&lt;/h3&gt;
&lt;p&gt;Just as an aside, the faster these checks run, the less I have to carry in my head between one step and the next. Slow tests and a sluggish pipeline don’t just waste time, they force me to hold more state in my own brain while I wait, which is the very thing I’m trying to offload.&lt;/p&gt;
&lt;h2 id=&quot;know-when-to-get-your-hands-dirty&quot;&gt;Know when to get your hands dirty&lt;/h2&gt;
&lt;p&gt;I think another thing to remember is that &lt;strong&gt;not everything needs AI.&lt;/strong&gt; Sometimes the change is small or fiddly enough that it’s genuinely faster to just write it myself than to explain it, review it, and correct it. Making that call, rather than reflexively reaching for an assistant, keeps me close to my own codebase. It keeps me tuned in to the effort involved in any change, and the current lay of the land.&lt;/p&gt;
&lt;h2 id=&quot;keeping-the-robots-honest&quot;&gt;Keeping the robots honest&lt;/h2&gt;
&lt;p&gt;All this being said, I love AI coding assistants, and they’ve genuinely changed how I work. But loving them and letting them run the place unsupervised are two very different things. For example, I love my dog, Millie, but I’m not going to leave her unsupervised with my dinner on the table (again).&lt;/p&gt;
&lt;p&gt;The whole game is making sure that you remain the person in charge of your own codebase, rather than becoming a stranger to the very code you’re shipping. And so, these habits are just how I personally try to stay in the loop, instead of buried underneath a mountain of changes. It’s my way of keeping the robots honest, and hopefully, keeping myself honest too.&lt;/p&gt;</content:encoded></item><item><title>What to do when your knowledge agent doesn&apos;t seem so knowledgeable</title><link>https://coreyduffy.com/blog/what-to-do-when-your-knowledge-agent-doesnt-seem-so-knowledgeable/</link><guid isPermaLink="true">https://coreyduffy.com/blog/what-to-do-when-your-knowledge-agent-doesnt-seem-so-knowledgeable/</guid><description>&quot;Apologies for the confusion.&quot; Why knowledge agents give wrong answers, and how to fix them.</description><pubDate>Sun, 14 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I’ve worked on a number of knowledge agent projects now. At some point during the development process, when users finally get their hands on their shiny new AI tool to test it, I might hear some variation of the following:&lt;/p&gt;
&lt;p&gt;“Oh… uhh… it should know that.”&lt;/p&gt;
&lt;p&gt;“That’s not right, why is it saying that?”&lt;/p&gt;
&lt;p&gt;“It should know that, it’s on our website.”&lt;/p&gt;
&lt;p&gt;“That doesn’t sound right…”&lt;/p&gt;
&lt;p&gt;Much like with humans, sometimes it’s hard to know why a knowledge agent does what it does, or what it knows and doesn’t know.&lt;/p&gt;
&lt;p&gt;In this context, by knowledge agent, I’m referring to an &lt;a href=&quot;https://en.wikipedia.org/wiki/Large_language_model&quot;&gt;LLM&lt;/a&gt;-backed system with agency to make decisions on how to answer user queries. This might involve retrieving knowledge from a knowledge base via &lt;a href=&quot;https://en.wikipedia.org/wiki/Retrieval-augmented_generation&quot;&gt;Retrieval-Augmented Generation (RAG)&lt;/a&gt;, making tool calls to obtain useful information from an external system or environment, or some combination of all of the above.&lt;/p&gt;
&lt;p&gt;When a knowledge agent I’ve been working on suddenly doesn’t seem so knowledgeable, this is generally how I go about troubleshooting.&lt;/p&gt;
&lt;h2 id=&quot;is-the-answer-actually-available&quot;&gt;Is the answer actually available?&lt;/h2&gt;
&lt;p&gt;Step one in the process for me normally involves &lt;strong&gt;figuring out if the information to answer a user’s question actually exists somewhere&lt;/strong&gt;, and if the knowledge agent has access to it. This information could be buried in a knowledge base, or returned from some external tool call, but it must exist somewhere. &lt;strong&gt;Not just in a customer’s head.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It’s a bit like asking me when a family member’s birthday is - if I can’t open my Calendar app and check, then I have no idea (sorry Mum, I know it’s sometime in July).&lt;/p&gt;
&lt;p&gt;If the answer doesn’t exist somewhere that the agent can pull from, then it’s going to either decline to answer, or worse, make something up.&lt;/p&gt;
&lt;p&gt;To fix this, we update the information available to the agent and retest.&lt;/p&gt;
&lt;p&gt;If, however, the answer does exist somewhere that the agent should be able to read from, then we move on to step two.&lt;/p&gt;
&lt;h2 id=&quot;why-isnt-it-getting-the-right-context&quot;&gt;Why isn’t it getting the right context?&lt;/h2&gt;
&lt;p&gt;So we know that the answer is out there somewhere, lurking in the depths, and the agent has access to it in principle. However, our agent still just can’t seem to give the right answer. There are a few usual suspects that could cause this:&lt;/p&gt;
&lt;p&gt;Firstly, knowledge bases are usually broken into &lt;a href=&quot;https://dev.to/zilliz/a-guide-to-chunking-strategies-for-retrieval-augmented-generation-rag-nek&quot;&gt;chunks&lt;/a&gt; before they’re indexed. If a piece of information is split across two chunks, e.g. half of the required information in one chunk and the other half in another, then retrieving either one on its own gives the agent half a picture.
Choosing the correct chunking strategy for your use case is a whole article in itself that I’ll not go into here, but &lt;strong&gt;adjusting your chunking approach to use larger or overlapping chunks, chunking semantically or pulling in surrounding context once a match is found&lt;/strong&gt;, can help stitch the full picture back together and lead to better answers.&lt;/p&gt;
&lt;p&gt;Secondly, even when the right chunk exists, retrieval might simply miss it, like me trying to find my keys anytime I’m late and trying to get out the door.
With RAG, an agent searches the knowledge base using some representation of the user’s query, and pulls back the chunks it thinks are most relevant. But &lt;strong&gt;“most relevant” can be hard to determine.&lt;/strong&gt; If a user phrases their question differently to how the source material is written, the right chunk simply may not rank highly enough to make the cut.
Fixing this usually means giving retrieval a better shot at the match, maybe by &lt;strong&gt;revisiting your embedding model, adding a reranking step, or rewriting the user’s query before you search&lt;/strong&gt; so it lines up more closely with how the source is actually worded.&lt;/p&gt;
&lt;p&gt;Thirdly, maybe the agent just didn’t go looking in the first place. With tool-calling agents, retrieval isn’t necessarily automatic, &lt;strong&gt;the agent has to decide to make a call and to where.&lt;/strong&gt;
If the agent doesn’t recognise that the question needs an external lookup, it’ll happily answer from whatever it already has in context, and you’ll get a confident, but more than likely wrong answer.
The issue here normally lives in your tool definitions/descriptions and your prompts. Making it clearer &lt;strong&gt;when and why a tool should be used, and giving the agent permission to admit when it doesn’t know&lt;/strong&gt;, tends to nudge it towards looking things up rather than guessing.&lt;/p&gt;
&lt;p&gt;Finally, maybe the right tool was called, but with the wrong arguments. Even when the agent does reach for a tool, it might query it badly. It could use the wrong filter, wrong date range, or wrong customer ID. This will likely lead to an external system returning nothing, or returning something completely irrelevant. &lt;strong&gt;Tightening up your tool’s schema, being more strict about what each argument expects, and giving the agent a few examples of correct usage&lt;/strong&gt; all go a long way here.&lt;/p&gt;
&lt;p&gt;When trying to untangle any of these causes, &lt;strong&gt;the key is traceability.&lt;/strong&gt; When developing agents, I always ensure that I can follow as much of its thinking as possible - what did it retrieve? Did it make any tool calls? What arguments did it use? Normally, if I can lay this out in front of me, I’ll be able to figure out roughly where things went sideways.&lt;/p&gt;
&lt;p&gt;If the right context did make it into the agent’s hands, and it still got the answer wrong, then we’re onto step three.&lt;/p&gt;
&lt;h2 id=&quot;why-isnt-it-using-the-context-properly&quot;&gt;Why isn’t it using the context properly?&lt;/h2&gt;
&lt;p&gt;This is the fun part, because it looks like everything upstream worked. The answer existed, retrieval found it, everything is queued up for success, and like Gabriel in the 2026 Champions League final, our agent just can’t finish the job (sorry gooners).&lt;/p&gt;
&lt;p&gt;A handful of things may be going on here:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;There’s too much context&lt;/strong&gt;, and our agent can’t see the forest for the trees.
It’s tempting to think more context is always better, but agents can and do lose track of relevant details buried in a wall of retrieved text. If you stuff twenty chunks into the prompt to be safe, the one that actually mattered can get drowned out by the nineteen that didn’t.
Likewise, if you’ve had a long conversation with a knowledge agent, and that history/context hasn’t been condensed adequately, the knowledge agent’s brain may simply be too fried to think straight.
The fix is usually to be more selective rather than generous here. &lt;strong&gt;Retrieve fewer, higher-quality chunks, and summarise or trim the conversation history&lt;/strong&gt; so the signal isn’t drowned out by noise.&lt;/p&gt;
&lt;p&gt;Another cause may be that the context contradicts itself. If the knowledge base contains two versions of the truth, say an outdated policy and the current one, the agent has no reliable way of knowing which to trust.
It might pick the wrong one, or awkwardly try to reconcile both. This is &lt;strong&gt;less an agent problem and more a knowledge hygiene problem&lt;/strong&gt;, but our poor knowledge agent will get the blame. Here we may need to look at the source. We can likely improve our knowledge agent’s answers by &lt;strong&gt;pruning stale documents, deduplicating, and making sure there’s a single, clear version of the truth&lt;/strong&gt; for the agent to find.&lt;/p&gt;
&lt;p&gt;The instructions and the context could also be fighting. Sometimes the system prompt tells an agent to behave one way and the retrieved content pulls it another. The agent is left trying to serve two masters and does neither well.
Untangling this usually means &lt;strong&gt;being explicit in the prompt about what wins when they clash, for example, telling the agent to always defer to the retrieved source material over its own assumptions.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Maybe it’s also just a hard reasoning step. Occasionally the information is all there for an agent, but answering correctly requires it to combine several facts, do a bit of arithmetic, or make an inference.
Some questions are just harder than others, and a model that nails simple lookups can still trip over multi-step reasoning.
Giving the agent some room to think can help here. This may involve &lt;strong&gt;encouraging some step-by-step reasoning, breaking the task into smaller pieces, or reaching for a more capable model&lt;/strong&gt; when the question genuinely warrants it.&lt;/p&gt;
&lt;h2 id=&quot;turns-out-it-should-know-that&quot;&gt;Turns out it should know that&lt;/h2&gt;
&lt;p&gt;I suppose the takeaway here is that as an engineer, we aren’t entirely at the mercy of the mysterious AI box whenever we don’t get the answers that we’re looking for from our agents. Like most problems with software, &lt;strong&gt;there’s a logical set of steps we can walk through&lt;/strong&gt; to try and make our knowledge agents sound a bit smarter. Almost smarterer than me.&lt;/p&gt;</content:encoded></item></channel></rss>