<?xml version="1.0" encoding="UTF-8"?>
<rss  xmlns:atom="http://www.w3.org/2005/Atom" 
      xmlns:media="http://search.yahoo.com/mrss/" 
      xmlns:content="http://purl.org/rss/1.0/modules/content/" 
      xmlns:dc="http://purl.org/dc/elements/1.1/" 
      version="2.0">
<channel>
<title>The Agile Monkeys Lab</title>
<link>https://labs.theagilemonkeys.com/</link>
<atom:link href="https://labs.theagilemonkeys.com/index.xml" rel="self" type="application/rss+xml"/>
<description>Engineering notes from The Agile Monkeys on agentic engineering and the AI SDLC: what we build with coding agents, how we measure it, what we learned.</description>
<image>
<url>https://labs.theagilemonkeys.com/og-image.png</url>
<title>The Agile Monkeys Lab</title>
<link>https://labs.theagilemonkeys.com/</link>
<height>76</height>
<width>144</width>
</image>
<generator>quarto-1.11.9</generator>
<lastBuildDate>Thu, 29 Oct 2026 00:00:00 GMT</lastBuildDate>
<item>
  <title>Agentic Engineering: Be a Supreme Whisperer</title>
  <dc:creator>Álvaro López</dc:creator>
  <link>https://labs.theagilemonkeys.com/posts/agentic-engineering-be-a-supreme-whisperer/</link>
  <description><![CDATA[ 




<p><a href="cover-16x9.jpg" class="lightbox" data-gallery="quarto-lightbox-gallery-1"><img src="https://labs.theagilemonkeys.com/posts/agentic-engineering-be-a-supreme-whisperer/cover-16x9.jpg" class="img-fluid" style="width:100.0%" alt="A cowboy holding a microphone leans toward a mechanical horse in a futuristic Western town."></a></p>
<p><strong>Getting more out of Codex or Claude Code starts with the work around them.</strong></p>
<p>Much of the effort happens around the agent: gathering context, handing out small tasks, explaining the next step, and checking progress. Those handoffs take attention, even when the agent is doing useful work.</p>
<p>At The Agile Monkeys, we have been reorganizing our work around three habits that help us delegate more complete work and spend less energy coordinating it:</p>
<p><strong>Be ambitious. Be a whisperer. Be lazy.</strong></p>
<p>Here is how to put them to work, with a practice to take away from each section.</p>
<section id="be-ambitious-give-it-a-complete-goal" class="level2">
<h2 class="anchored" data-anchor-id="be-ambitious-give-it-a-complete-goal">Be ambitious: give it a complete goal</h2>
<p>Think back to a few recent requests. Could the agent have handled the work between them too?</p>
<p>We increasingly delegate a <strong>complete outcome</strong>: where to investigate, what to deliver, how to check it, and when to involve us. That gives the agent room to <strong>explore, plan, implement, and verify</strong>.</p>
<p>For a feature, an illustrative brief might be:</p>
<blockquote class="blockquote">
<p>Make notification preferences work end to end. Inspect the code and propose a plan. After our approval, implement it, run the relevant checks, and test the flow in the development environment using the browser. Bring us a reviewable PR with the results. Ask us about missing product decisions; leave merging and production deployment for review.</p>
</blockquote>
<p>We have used this approach for changes spanning several services. Once we approve the plan, the agent can keep going through implementation and testing. We return when there is something worth reviewing or a decision it needs us to make.</p>
<p>Even substantial requests can go further: include the initial investigation, or the verification that usually happens afterward.</p>
<p><strong>Give it the outcome, the definition of done, and clear review points.</strong></p>
</section>
<section id="be-a-whisperer-think-out-loud-to-the-agent" class="level2">
<h2 class="anchored" data-anchor-id="be-a-whisperer-think-out-loud-to-the-agent">Be a whisperer: think out loud to the agent</h2>
<p><strong>Askomity push.</strong></p>
<p>That is what the transcription produced when one of us said, in Spanish, <em>haz commit y push</em>: roughly, “do a commit and push.” The agent understood the intention and responded about committing and pushing the changes.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><a href="voice-to-brief.png" class="lightbox" data-gallery="quarto-lightbox-gallery-2" title="An illustrative billing request: false starts become a concrete goal, scope, and checks."><img src="https://labs.theagilemonkeys.com/posts/agentic-engineering-be-a-supreme-whisperer/voice-to-brief.png" class="img-fluid figure-img" style="width:100.0%" alt="An invented spoken request about billing changes direction several times. The agent organizes it into monthly usage billing, over-limit usage only, unchanged subscription plans, and a charge preview, test mode and reviewable PR."></a></p>
<figcaption>An illustrative billing request: false starts become a concrete goal, scope, and checks.</figcaption>
</figure>
</div>
<p>When preparing a presentation, we used voice to work through the structure, reshape slides, and rehearse the delivery. The agent turned that rambling into presenter notes and reminders of what to demonstrate.</p>
<p>We still write or edit exact wording when needed. For exploration, <strong>the conversation can start before the idea is fully organized</strong>.</p>
<p>Try a minute of thinking out loud, then ask the agent to clarify the goal and any open questions.</p>
<p><strong>Start talking while thinking. Let the agent help find the structure.</strong></p>
</section>
<section id="be-lazy-let-it-find-the-context" class="level2">
<h2 class="anchored" data-anchor-id="be-lazy-let-it-find-the-context">Be lazy: let it find the context</h2>
<p>A request arrives in Slack. Before work starts, there is a related issue to find, a document to read, and last week’s discussion to recover.</p>
<p>That preparation costs attention. <strong>Copying and pasting is work.</strong></p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><a href="connected-context.png" class="lightbox" data-gallery="quarto-lightbox-gallery-3" title="Connected sources give the agent somewhere to investigate; the goal determines which context matters."><img src="https://labs.theagilemonkeys.com/posts/agentic-engineering-be-a-supreme-whisperer/connected-context.png" class="img-fluid figure-img" style="width:100.0%" alt="Email, chat, meetings, documents, code and logs feed an agent that selects the relevant decisions, constraints and evidence for the current goal."></a></p>
<figcaption>Connected sources give the agent somewhere to investigate; the goal determines which context matters.</figcaption>
</figure>
</div>
<p>Connect the sources the work needs, with access appropriate to the task. We can then point to a conversation and let the agent investigate related information, including what we might have forgotten.</p>
<p>Distinguish <strong>what the agent can access</strong> from <strong>what it needs for this task</strong>. Keep the working context focused with concise tool results, especially under tighter model or token budgets.</p>
<p><strong>Point to the sources. Let the agent do the gathering and connecting.</strong></p>
</section>
<section id="turn-those-habits-into-a-daily-loop" class="level2">
<h2 class="anchored" data-anchor-id="turn-those-habits-into-a-daily-loop">Turn those habits into a daily loop</h2>
<p>Here is one way we put them together:</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><a href="daily-agent-loop.png" class="lightbox" data-gallery="quarto-lightbox-gallery-4" title="Priorities guide the goals; human review feeds completed work back into the document."><img src="https://labs.theagilemonkeys.com/posts/agentic-engineering-be-a-supreme-whisperer/daily-agent-loop.png" class="img-fluid figure-img" style="width:100.0%" alt="Six numbered stages form a cycle around a central priorities document: collect signals, prioritize, prepare goals, let agents advance, review results and update progress. Human review is highlighted in yellow; the priorities document is marked with a yellow edge."></a></p>
<figcaption>Priorities guide the goals; human review feeds completed work back into the document.</figcaption>
</figure>
</div>
<p><strong>Email review.</strong> A scheduled task brings us the important messages at the start of the day. Initially, we checked the inbox too and corrected what it missed or included unnecessarily. Those corrections refined the criteria; we now rely much less on manual scanning.</p>
<p>These reviews now do more than filter relevant mail: they sometimes catch important messages we would have missed. One example was a certificate-expiry notice that could have left several of our internal services unavailable.</p>
<p><strong>Priorities.</strong> A separate ongoing workflow reviews the connected sources and updates our priorities document in Notion. It can connect a chat request with a meeting tomorrow and surface what needs preparation today.</p>
<p><strong>Parallel work.</strong> From those priorities, we launch independent goals: investigate this, prepare that proposal, start this implementation. We review the results, and completed work feeds back into the priorities.</p>
<p>Parallel work also means more conversations in an agent workspace (Codex, Claude Code, or another tool). Keeping active work visible takes some organization. One arrangement we use is:</p>
<ul>
<li><strong>Permanent:</strong> recurring routines and scheduled tasks, such as the daily email review.</li>
<li><strong>Pinned:</strong> the work we are actively following.</li>
<li><strong>Key projects:</strong> projects we return to frequently.</li>
</ul>
<p>Organization is personal; project folders may be enough. The useful test is whether <strong>work needing attention is easy to see and find</strong>.</p>
<p><strong>Keep the priorities, delegated work, and completed results connected.</strong></p>
</section>
<section id="make-each-correction-useful-next-time" class="level2">
<h2 class="anchored" data-anchor-id="make-each-correction-useful-next-time">Make each correction useful next time</h2>
<p>Useful knowledge can disappear into old chats. A <strong>knowledge workspace</strong> with an index, current work, project folders, and reusable instructions gives later sessions something to pick up.</p>
<p><strong>Make upkeep part of the workflow, rather than another thing to remember.</strong> A skill or instructions in an agent’s file, such as <code>AGENTS.md</code> or <code>CLAUDE.md</code>, can make recording decisions, corrections, and remaining work part of the task.</p>
<p>Some of us use a <strong>scheduled task</strong> to review sessions that are largely finished, extract the useful lessons, and update our personal or project knowledge base. PR review feedback can feed the same workspace.</p>
<p>A manual <strong>session review</strong> is still useful at the end of a task:</p>
<blockquote class="blockquote">
<p>What did we have to explain repeatedly? Propose updates to the reusable instructions, and save the decisions and remaining work so we can resume later.</p>
</blockquote>
<p>Keep task-specific details in the relevant project notes, and review changes to reusable rules so a one-off correction does not become a blanket instruction. The lesson lives in files and examples that later runs actually read.</p>
<p><strong>Save the lesson where future runs can read it—and make saving it routine.</strong></p>
</section>
<section id="make-the-result-easier-to-understand" class="level2">
<h2 class="anchored" data-anchor-id="make-the-result-easier-to-understand">Make the result easier to understand</h2>
<p>Getting an agent to finish is only part of the job. A finished result is only useful if it is easy to understand and review.</p>
<p><strong>Ask for a format that makes the next decision easier.</strong> A diagram can clarify service relationships. An HTML report can make a proposal easier to navigate. An interactive artifact can turn an explanation into something to explore.</p>
<p>We can save the explanation styles that work well as reusable instructions, including the diagrams and examples that make a result easier to follow.</p>
<p>Then question the result:</p>
<blockquote class="blockquote">
<p>Why this approach? Which assumption does it depend on? Show us how you checked that it works.</p>
</blockquote>
<p>For the billing example, an illustrative answer could look like this:</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><a href="decision-explained.png" class="lightbox" data-gallery="quarto-lightbox-gallery-5" title="A decision becomes easier to review when its assumption, reasoning, and checks are visible."><img src="https://labs.theagilemonkeys.com/posts/agentic-engineering-be-a-supreme-whisperer/decision-explained.png" class="img-fluid figure-img" style="width:100.0%" alt="An invented billing example links an assumption that charges can run after checkout to a decision to use a background billing job, keeping checkout fast and allowing retries. The proposed checks simulate a payment timeout and retry the same billing period to check responsiveness and prevent duplicate charges."></a></p>
<figcaption>A decision becomes easier to review when its assumption, reasoning, and checks are visible.</figcaption>
</figure>
</div>
<p>Now there is something specific to challenge: does that assumption hold, and do the checks support the decision?</p>
<p><strong>Delegate some of the work of making the result reviewable.</strong></p>
</section>
<section id="protect-attention-as-parallel-work-grows" class="level2">
<h2 class="anchored" data-anchor-id="protect-attention-as-parallel-work-grows">Protect attention as parallel work grows</h2>
<p>More agents can advance independently. <strong>Our capacity to switch between them still has a limit.</strong></p>
<p>Prepare each goal so it can advance until there is a reviewable result, a blocker, or a decision that needs human input. Workstreams touching the same services may need coordination rather than simultaneous execution.</p>
<p>As coordination grows, a director agent with clear roles, session names, and documented state is one pattern to explore.</p>
<p>The environment matters too. Scripts that return check results and relevant errors can keep huge build logs out of the conversation. Different models may suit planning and execution, depending on the budget.</p>
<p>Keep asking: <strong>is this reducing the attention each task needs, or giving us more things to monitor?</strong></p>
</section>
<section id="pick-one-practice-and-try-it-this-week" class="level2">
<h2 class="anchored" data-anchor-id="pick-one-practice-and-try-it-this-week">Pick one practice and try it this week</h2>
<p>Pick one new practice: think out loud, delegate a complete goal, connect a source, automate a daily review, or ask for a visual explanation.</p>
<p>Use it on a real task. Notice <strong>how often it needs intervention, what has to be repeated, and how much effort review takes</strong>. Adjust and try again; if it does not fit, choose another practice.</p>
<p>These habits reinforce one another. Connected context makes a spoken brief easier to act on. Clear goals let work advance further. Saved feedback reduces repeated corrections. Readable results make it easier to pick work up again.</p>
<p><strong>One practice. One real task. Give it a proper try.</strong></p>
<p>Keep building on what works. There is a title to work toward:</p>
<p><strong>SWAT: Supreme Whisperer of All Time.</strong></p>


<hr>
<p class="hub-topic"><a class="share-btn" href="https://hub.theagilemonkeys.com/topics/agentic-engineering?utm_source=lab&amp;utm_medium=referral&amp;utm_campaign=tamhub-2026-10&amp;utm_content=topic-link">Topic on tamHub: Agentic engineering →</a></p>
</section>

 ]]></description>
  <category>agentic engineering</category>
  <category>AI agents</category>
  <category>productivity</category>
  <guid>https://labs.theagilemonkeys.com/posts/agentic-engineering-be-a-supreme-whisperer/</guid>
  <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
  <media:content url="https://labs.theagilemonkeys.com/posts/agentic-engineering-be-a-supreme-whisperer/og-image.png" medium="image" type="image/png" height="76" width="144"/>
</item>
<item>
  <title>Our job was never translating requests into code</title>
  <link>https://labs.theagilemonkeys.com/posts/our-job-was-never-translating-requests-into-code/</link>
  <description><![CDATA[ 




<p>When people ask what engineers are for now that AI writes the code, we think the question comes from a misunderstanding of what an engineer does. A lot of weight has always been put on the code.</p>
<p>It made sense for a while. Code took so long and was so complex that the job looked like translation. The CEO says “I want this”, and you think: fine, how do I turn that into code that does it? So it really does look as if that is all the engineer did.</p>
<p><strong>But that was never the role.</strong> The role of an engineer was to have an environment and know how to solve problems in it with whatever was available. If code is what you have, you solve it with code.</p>
<p>Often engineers did everything they could to avoid writing it: take this library, that one, wire them together, done. No new code, and the problem is solved.</p>
<p>That is still the job. Here is what it looks like on a normal day, and in one example from our work on AIFindr.</p>
<section id="an-agent-sets-the-days-priorities" class="level2">
<h2 class="anchored" data-anchor-id="an-agent-sets-the-days-priorities">An agent sets the day’s priorities</h2>
<p>One of us works with Codex and Claude Code side by side, with a lot of things running in parallel, and starts every day with two agent sessions. One reviews all the email. The other is connected to Slack, email, Notion, practically everything, and its job is to say what the priorities are.</p>
<p>There is a Notion document with all of them. The agent goes through it: this one was due today, this one has already moved forward. An urgent Slack message about something that needs doing becomes a new priority and a new task.</p>
<p>The agent manages those priorities now.</p>
</section>
<section id="our-attention-caps-how-many-tasks-run-in-parallel" class="level2">
<h2 class="anchored" data-anchor-id="our-attention-caps-how-many-tasks-run-in-parallel">Our attention caps how many tasks run in parallel</h2>
<p>With the list ready, everything that can run in parallel gets launched, which is usually most of it. The first wave is easy: go and research this, look at what is happening here, find out why this is not working. That is four or five tasks, more or less.</p>
<p>Then come the implementations that have already been running for several days. Review what was done the day before, decide where to go next, read what results came back.</p>
<p>Altogether, <strong>five, six, seven tasks is what one person can sustain</strong> in this setup. More could be launched. But as they finish, each one needs attention, and someone has to get involved and see where it went. How many agents you can run depends on how much your own mind can process.</p>
</section>
<section id="an-example-the-agents-built-the-filter-we-supplied-the-constraint" class="level2">
<h2 class="anchored" data-anchor-id="an-example-the-agents-built-the-filter-we-supplied-the-constraint">An example: the agents built the filter, we supplied the constraint</h2>
<p>On AIFindr, people were typing their personal data into the chat, and we needed to clean the PII out of each message before it reached the main model. Our first attempt put a third-party model in front of the main one. It worked, but it was very slow: the user waited too long before seeing an answer. And the raw PII still had to leave our infrastructure, something a major Latin American bank we work with would not accept.</p>
<p>Optimization is always a rabbit hole, one you can go into and not come out of for a month. So we sent coding agents down it instead. They researched open-weight models that could run <strong>inside our own infrastructure</strong>, a setup complicated enough that the engineer who led this would have spent two months learning it. The agents did it in an hour or less.</p>
<p>They tried several models and found one that filtered the personal data out of users’ queries in <strong>100 to 200 milliseconds</strong>. It matched the remote model on the evaluation set we had built first. From idea to a working service took practically no time.</p>
<p>The first version ran on a GPU. It was fast and private, and <strong>it came to about EUR 1,340 a month for that one feature</strong>.</p>
<p>That is where the engineer has to step in: you have this constraint, that is too expensive, look for alternatives. We gave an agent a stricter goal: keep the privacy, the quality and the speed, and make it sensible to run. It looped through hypotheses, testing each one against our evaluations and benchmarks, until it found an answer we had barely considered: no GPU at all. Before deploying, we used the previous months’ usage data, requests per second included, to load-test exactly the setup we would run. It now runs on small ARM CPU tasks for <strong>about EUR 120 a month across all our environments, 91% less</strong>. The full story is in our post on <a href="https://labs.theagilemonkeys.com/posts/aifindr-privacy-private-fast-affordable/">AIFindr Privacy</a>.</p>
<p>The agents did the research, the setup, the load tests and even the search for a cheaper architecture. Their first working answer was too expensive to run, and the constraint that ruled it out was ours. So was the evaluation set that defined what good enough meant. <strong>The AI can do practically anything, but not everything is actually right for your problem.</strong> That judgment, and knowing how to guide the AI, is our job now.</p>
</section>
<section id="the-bottleneck-moved-to-the-ticket" class="level2">
<h2 class="anchored" data-anchor-id="the-bottleneck-moved-to-the-ticket">The bottleneck moved to the ticket</h2>
<p>When a ticket does not land in one shot, we see a bit of both: sometimes the miss is in the code, sometimes in the ticket. But getting the code right on the first try keeps getting easier. Getting all the context into the ticket does not, especially when part of it has not been figured out yet. That is why the engineer still reviews the result and decides whether it actually does what we need.</p>
<p>With <a href="https://facility.theagilemonkeys.com/">Facility</a> we automated the path from ticket to production. Now we are working on the step before it: taking what is in people’s heads, meetings and chats, and turning it into tickets with enough context for more PRs to land in one shot.</p>
</section>
<section id="the-problems-got-bigger" class="level2">
<h2 class="anchored" data-anchor-id="the-problems-got-bigger">The problems got bigger</h2>
<p><em>So what is AI?</em> Right now it writes very good code. We would call it a tool, except it is obviously much more than that. It is one more thing we have available, and as engineers what we can do now is understand it, learn to use it well and know it well enough to solve problems with it.</p>
<p>We are not going to spend our time painting a button blue anymore. We are going to be working on problems closer to curing a disease. The problems have grown in complexity, and what we have to do is take a tool that powerful and, with an engineer’s mind, apply it correctly to the problem and find the solution.</p>
<p>The next time an optimization looks like a month-long rabbit hole, send the agents down it first. Keep the constraint for yourself.</p>
<p>This piece is one of a pair. The other, <a href="https://labs.theagilemonkeys.com/posts/engineers-finally-have-time-for-the-problems-nobody-could-prioritize/">Engineers finally have time for the problems nobody could prioritize</a>, is about what to invest in: skills, our own tooling and how we learn. This one is about how the work gets done.</p>


<hr>
<p class="hub-topic"><a class="share-btn" href="https://hub.theagilemonkeys.com/topics/agentic-engineering?utm_source=lab&amp;utm_medium=referral&amp;utm_campaign=tamhub-2026-10&amp;utm_content=topic-link">Topic on tamHub: Agentic engineering →</a></p>
</section>

 ]]></description>
  <category>AI agents</category>
  <category>software engineering</category>
  <guid>https://labs.theagilemonkeys.com/posts/our-job-was-never-translating-requests-into-code/</guid>
  <pubDate>Mon, 26 Oct 2026 00:00:00 GMT</pubDate>
  <media:content url="https://labs.theagilemonkeys.com/posts/our-job-was-never-translating-requests-into-code/og-image.png" medium="image" type="image/png" height="76" width="144"/>
</item>
<item>
  <title>Engineers finally have time for the problems nobody could prioritize</title>
  <link>https://labs.theagilemonkeys.com/posts/engineers-finally-have-time-for-the-problems-nobody-could-prioritize/</link>
  <description><![CDATA[ 




<p>Every engineering team has a list of problems it knows about and never gets to: a latency issue, a PII problem, the internal tool that would make everyone faster. For years the reason was the same. There was no time, because someone had to write the code.</p>
<p>The skills an engineer needs are different now, and the change starts with what drops out. Our opinion is that <strong>the time code used to take now belongs to that list</strong>, and the engineers who stand out will be the ones who spend it there.</p>
<p>This piece is one of a pair. The other, <a href="https://labs.theagilemonkeys.com/posts/our-job-was-never-translating-requests-into-code/">Our job was never translating requests into code</a>, is about how the work gets done day to day. This one is about what to invest in.</p>
<section id="writing-and-reviewing-code-are-losing-value" class="level2">
<h2 class="anchored" data-anchor-id="writing-and-reviewing-code-are-losing-value">Writing and reviewing code are losing value</h2>
<p>The clearest skill to lose importance is writing code, in every sense, including the ability to review it. Once a system can hand you the output, the environment, the artifact, whatever it is, and you can check it directly from a functional point of view, code review stops being where the value is.</p>
<p>The same goes for defining an architecture graphically. Claude is going to do that for you too.</p>
<p>New skills appear, starting with the obvious one: these AI systems have to be learned, and learning them matters. But the bigger shift is in skills that were always important and rarely got the time.</p>
</section>
<section id="the-time-code-used-to-take-now-goes-to-engineering-problems" class="level2">
<h2 class="anchored" data-anchor-id="the-time-code-used-to-take-now-goes-to-engineering-problems">The time code used to take now goes to engineering problems</h2>
<p>Before, some things could not be prioritized. Latency problems, or PII problems, could take a long time, and prioritizing them was a serious problem. So they waited.</p>
<p>Now they get prioritized. They ask for time, attention and a share of problem solving as an engineer that they rarely got before. One of those PII problems, and what solving it took, is in <a href="https://labs.theagilemonkeys.com/posts/our-job-was-never-translating-requests-into-code/">Our job was never translating requests into code</a>.</p>
<p>That has a consequence for engineers who were never very fluent at that kind of problem solving: now they have to be. If you want to stand out, or just be useful, those are the situations where an engineer can be far more useful than before.</p>
</section>
<section id="the-internal-tooling-that-used-to-be-a-time-sink-now-pays-off" class="level2">
<h2 class="anchored" data-anchor-id="the-internal-tooling-that-used-to-be-a-time-sink-now-pays-off">The internal tooling that used to be a time sink now pays off</h2>
<p>Our team has a lot of people who treat development almost as a hobby. For years the same thing kept happening: everyone wanted the perfect development environment for their own team. People would spend time on “I’ll build this library, this tool”, and it was always a time sink. It never really sped anyone up, and the day-to-day work ate it.</p>
<p><strong>AI gives us a huge capacity to build, and we have not managed to exploit it fully yet.</strong> Seeing both at once changes the calculation. What used to be a time sink, building a tool like that, now makes a lot of sense for a company like ours. It is how we ended up building <a href="https://facility.theagilemonkeys.com/">Facility</a>, large and complex tooling for working with AI that works very well, and reached effectiveness in a short time.</p>
<p>We think any company should start considering what used to be the time sink nobody touched: spending time on its own software development lifecycle. That is what will actually speed it up, and quickly.</p>
<p>There is one condition. You can only do it if you have the tool and you understand the rules of the game.</p>
</section>
<section id="the-rules-are-learned-from-people-who-have-hit-the-problems" class="level2">
<h2 class="anchored" data-anchor-id="the-rules-are-learned-from-people-who-have-hit-the-problems">The rules are learned from people who have hit the problems</h2>
<p>There is too much hype. You try to stay optimistic, but a lot of it is easy to question unless it comes from very reliable sources, and if you let everything you see online carry you, you end up overwhelmed. It happens to all of us.</p>
<p>Consuming content is an important part of learning. But the people we end up learning from are the ones who have actually faced the problems. What they keep running into is that things are not that easy. That is good news. It means there is a lot to learn, and it means you do not have to try everything, because not everything is useful.</p>
<p>That is why being in the same room with people who are really doing the work matters so much to us. <strong>It is what separates being overwhelmed from being grounded</strong>, focused on what actually works for you.</p>
</section>
<section id="bigger-systems-carry-more-responsibility" class="level2">
<h2 class="anchored" data-anchor-id="bigger-systems-carry-more-responsibility">Bigger systems carry more responsibility</h2>
<p>We are going to build more, and bigger. Complexity will grow, and so will the responsibility and the impact software has on people.</p>
<p>That is where engineers stay necessary: engineers who can keep control of the AI, make it evolve within the domains they work in, and deliver value with it. Someone has to maintain and improve those systems so they keep serving the people who depend on them.</p>
<p>The job will be more complex and will need different skills. There is an opportunity now to adapt and do bigger, better things with what we have.</p>
<p>Take the list of problems your team knows about and never schedules. Pick one, and give it the time the code used to take.</p>


<hr>
<p class="hub-topic"><a class="share-btn" href="https://hub.theagilemonkeys.com/topics/agentic-engineering?utm_source=lab&amp;utm_medium=referral&amp;utm_campaign=tamhub-2026-10&amp;utm_content=topic-link">Topic on tamHub: Agentic engineering →</a></p>
</section>

 ]]></description>
  <category>AI strategy</category>
  <category>software engineering</category>
  <guid>https://labs.theagilemonkeys.com/posts/engineers-finally-have-time-for-the-problems-nobody-could-prioritize/</guid>
  <pubDate>Fri, 09 Oct 2026 00:00:00 GMT</pubDate>
  <media:content url="https://labs.theagilemonkeys.com/posts/engineers-finally-have-time-for-the-problems-nobody-could-prioritize/og-image.png" medium="image" type="image/png" height="76" width="144"/>
</item>
<item>
  <title>Case Study: Rewriting What Parsing Can Do</title>
  <link>https://labs.theagilemonkeys.com/posts/reading-a-mortgage-statement-without-getting-it-wrong/</link>
  <description><![CDATA[ 




<section id="where-we-started" class="level2">
<h2 class="anchored" data-anchor-id="where-we-started">Where We Started</h2>
<p><a href="https://www.themomentisnau.com/">nau!</a>’s mission fits in one sentence: help people in Spain find the best mortgage deal, and flag the moment switching lenders is actually worth the paperwork. To do that, nau! needs to know exactly what someone is paying right now: outstanding balance, rate, margin, term, monthly payment, and who they’re paying it to. All of that sits in a mortgage statement the user already has on their phone.</p>
<p>Reading that statement automatically was always the plan. Nobody should have to retype numbers they’re already holding in their hand.</p>
</section>
<section id="the-problem-forty-banks-zero-patience" class="level2">
<h2 class="anchored" data-anchor-id="the-problem-forty-banks-zero-patience">The problem: Forty Banks, Zero Patience</h2>
<p>Here’s the catch. Every bank writes its statement differently and calls the same number three different things. On top of that, most people don’t hand over a clean PDF. They upload a photo taken on the kitchen table, or a screenshot from their banking app, and both come out messier than any clean document would.</p>
<p>When the reading failed, the app fell back to asking people to type the numbers in by hand, right in the middle of signing up for something they hadn’t even tried yet. A lot of them just stopped there.</p>
<p>There’s no soft failure mode here, either. Get a number wrong, and you’re not looking at a typo. You’re handing someone bad financial advice.</p>
</section>
<section id="our-solution-split-the-job-trust-the-answer" class="level2">
<h2 class="anchored" data-anchor-id="our-solution-split-the-job-trust-the-answer">Our solution: Split the Job, Trust the Answer</h2>
<p>We redesigned nau!’s reading pipeline on Claude and rebuilt it around two simple steps.</p>
<p>The first step answers a single question: which bank issued the statement. The second step reads the actual numbers, using a template built for that specific bank. Each template spells out what shows up on that bank’s statement, what each field is called, and roughly where it sits on the page. There are about forty of these, one per lender, plus a generic fallback for anything that doesn’t match.</p>
<p>We split the work this way because asking one pass to do both jobs is asking too much of it. It would have to figure out which bank’s statement it’s looking at while also hunting for numbers whose names depend on that answer. Once the bank is known, the second step already knows exactly what it’s looking for.</p>
<p>The reason there are forty separate templates, instead of one that covers every bank, comes down to precision. A template built for one bank can be exact: this field is called this, and it sits here. A template meant to cover every bank can only say “look for something that means outstanding balance,” and that vagueness is exactly where most misreadings came from. Before the redesign, 29% of statements were falling through to that generic template, and those were the readings the system trusted the least.</p>
<p>Both steps also require the model to answer in a fixed format: a form with named fields it has to fill in, nothing else. An empty field is something the code can handle. A paragraph of explanation sitting where a number should be is what used to break things downstream.</p>
<p>Three checks stand between a reading and the database. Is this even a mortgage statement? (Over sixty days, the system turned away twenty-one files that had nothing to do with a mortgage.) Does the bank the model guessed match what the IBAN itself says? (If not, the reading gets thrown out and redone.) And then there’s the user: every reading is saved next to whatever they later correct by hand, so the team always knows exactly what to fix next. Nothing irreversible happens without the user confirming it first.</p>
</section>
<section id="the-real-impact-reading-with-a-little-bit-of-judgment" class="level2">
<h2 class="anchored" data-anchor-id="the-real-impact-reading-with-a-little-bit-of-judgment">The real impact: Reading With a Little Bit of Judgment</h2>
<p>Here’s the part that actually makes this different, and it’s not really about reading at all. It’s parsing. Parsing itself isn’t new, it’s been around for years. What’s new is how fast and how easy it’s gotten.</p>
<p>The old way meant a couple of developers and a few weeks to hardcode every field for every bank format. Once that code existed, it was rigid. A bank tweaks its layout, moves a field, changes a label, and the whole thing breaks until someone goes back in and rewrites it.</p>
<p>This version was built in under a week by someone with no technical background, and it kept getting better with input directly from nau!’s own team, no dev cycle required.</p>
<p>The real difference is that the model behind it can make small decisions on its own. It doesn’t need every single field hardcoded to work. If a bank makes a small change, a slightly different layout, a relabeled field, the agent can still adapt and read it correctly. Traditional code doesn’t have that option. If the statement didn’t match exactly, that was it: a developer had to go back in and rewrite it. Here, a small variation just doesn’t break anything.</p>
<p>There’s also a second agent running quietly every night, going through every statement the system has seen and getting a little sharper each time. Fifteen nights in, it had read 210 mortgage statements on its own and built a fingerprint catalog covering 21 different banks, with no one checking its work and not a single mistake.</p>
<p>We designed three levels of autonomy for this agent and deliberately started it at the most cautious one: observe and accumulate. It doesn’t write anything a client will ever see. Since these statements carry someone’s name, ID number, and IBAN, the catalog it builds is limited to a fixed set of fields, verified against the database, not just whatever it decided to ask for in a prompt. We’d rather an error cost us an empty table than have someone find it in their own mortgage.</p>
<p>That’s the real shift here. We used to save the documents themselves, because that was the only way for the system to learn. Now we save what it learned, and we can delete the documents.</p>
</section>
<section id="the-numbers-dont-lie" class="level2">
<h2 class="anchored" data-anchor-id="the-numbers-dont-lie">The Numbers Don’t Lie</h2>
<p>The biggest gain showed up on the hardest input: a photo taken on a phone. Before the redesign, the system picked the wrong bank template for roughly half of those. Right after we shipped the fix, that number dropped to under a third, and it keeps shrinking as the system teaches itself a little more with every statement it reads. Outright failures, where the reading breaks and nothing comes back at all, now sit at 1.2%, twelve out of every thousand statements. Before we rebuilt it, that number couldn’t even be measured, because those failures weren’t being logged in the first place. Today, the system has read 1,873 statements for 1,008 users.</p>
</section>
<section id="in-our-clients-own-words" class="level2">
<h2 class="anchored" data-anchor-id="in-our-clients-own-words">In our client’s own words</h2>
<p>Automating this one step didn’t just cut down on manual data entry, it took away the single biggest point where people were dropping out of signup, and gave the nau! team the confidence to keep building faster on top of it.</p>
<blockquote class="blockquote">
<p>“This agent has been working alone for fifteen nights now: it’s read 210 mortgage statements and built a fingerprint catalog of 21 banks, with no one checking its work and not a single mistake. (…) The hardest part wasn’t deciding what it could do. It was deciding what we wouldn’t let it do.(…) The real shift is we used to save documents because that was the only way to learn. Now we save what we learned, and we can delete the documents, increasing our customer’s data safety”</p>
<p>Sergio Carbajal, CEO of nau!</p>
</blockquote>
<p>There’s more to build here, and we’re glad to be building it with the team at nau!.</p>


</section>

 ]]></description>
  <category>LLM</category>
  <category>document parsing</category>
  <category>production</category>
  <category>agents</category>
  <category>customer story</category>
  <guid>https://labs.theagilemonkeys.com/posts/reading-a-mortgage-statement-without-getting-it-wrong/</guid>
  <pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate>
  <media:content url="https://labs.theagilemonkeys.com/posts/reading-a-mortgage-statement-without-getting-it-wrong/og-image.png" medium="image" type="image/png" height="76" width="144"/>
</item>
<item>
  <title>AIFindr Privacy: How We Made Private AI Fast, Then Made It Affordable</title>
  <link>https://labs.theagilemonkeys.com/posts/aifindr-privacy-private-fast-affordable/</link>
  <description><![CDATA[ 




<p>PII redaction has an awkward property: it has to happen before almost everything else.</p>
<p>Before an agent retrieves a document, calls a tool, or generates its first word, the user’s message must be checked for names, email addresses, account numbers, and other sensitive data. If that check is slow, the user waits before the useful work even begins. If it sends the original text elsewhere, the privacy boundary is already compromised.</p>
<p>For us, solving those two problems led to two very different breakthroughs. The first used an open-weight model on a GPU to make redaction private and much faster. The second began when we priced that architecture and realized we could not justify running it.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><a href="journey.svg" class="lightbox" data-gallery="quarto-lightbox-gallery-1" title="The AIFindr Privacy journey: remote baseline, GPU breakthrough, and CPU breakthrough"><img src="https://labs.theagilemonkeys.com/posts/aifindr-privacy-private-fast-affordable/journey.svg" class="img-fluid quarto-figure quarto-figure-center figure-img" style="width:100.0%" alt="The AIFindr Privacy journey: remote baseline, GPU breakthrough, and CPU breakthrough"></a></p>
</figure>
</div>
<figcaption>The AIFindr Privacy journey: remote baseline, GPU breakthrough, and CPU breakthrough</figcaption>
</figure>
</div>
<section id="a-privacy-layer-that-still-crossed-the-boundary" class="level2">
<h2 class="anchored" data-anchor-id="a-privacy-layer-that-still-crossed-the-boundary">A privacy layer that still crossed the boundary</h2>
<p>Our initial PII provider used GPT-5.4 Nano with low reasoning. Functionally, it worked well. The workflow and our downstream analytics received redacted text instead of the original message.</p>
<p>That was useful containment, but <strong>the original PII still had to leave our infrastructure</strong>: <a href="https://www.aifindr.ai/">AIFindr</a> (The Agile Monkeys’ platform for building reliable enterprise AI agents) had to send it to a third-party model for redaction.</p>
<p>A major Latin American bank made the requirement explicit: original PII should not leave our infrastructure. At the same time, the remote redaction call was adding roughly 0.8-1.2 seconds before the agent workflow could start. The client felt that delay directly: it degraded the experience before the first useful word could even appear.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><a href="privacy-boundary.png" class="lightbox" data-gallery="quarto-lightbox-gallery-2" title="The original redaction flow protected downstream systems, but raw PII still crossed the infrastructure boundary"><img src="https://labs.theagilemonkeys.com/posts/aifindr-privacy-private-fast-affordable/privacy-boundary.png" class="img-fluid quarto-figure quarto-figure-center figure-img" style="width:100.0%" alt="The original redaction flow protected downstream systems, but raw PII still crossed the infrastructure boundary"></a></p>
</figure>
</div>
<figcaption>The original redaction flow protected downstream systems, but raw PII still crossed the infrastructure boundary</figcaption>
</figure>
</div>
<p><em>Why not just use regular expressions?</em> They work well for predictable formats, but PII is often ambiguous and context-dependent. Formats also vary across countries and user input, so adding more rules quickly becomes brittle and risks creating false positives.</p>
<p>So we decided to investigate open-weight models that could run fully inside our infrastructure. To compare them responsibly, we first needed a strong evaluation dataset: standard PII examples plus difficult cases with ambiguous values, malformed formats, and false-positive traps. It had to tell us whether sensitive data remained visible, whether harmless content disappeared, and how long redaction took. Quality came before speed or cost.</p>
</section>
<section id="breakthrough-one-an-open-weight-model-on-our-gpu" class="level2">
<h2 class="anchored" data-anchor-id="breakthrough-one-an-open-weight-model-on-our-gpu">Breakthrough one: an open-weight model on our GPU</h2>
<p>We created AIFindr Privacy as a separate internal service that the backend could call for redaction. The service ran the open-weight model on a GPU inside our infrastructure.</p>
<p>The first engine used OpenAI’s open-weight <code>openai/privacy-filter</code>. It is a purpose-built classifier, not a generative LLM. We combined its detections with deterministic structural safeguards and span-resolution logic, then exposed redacted text and non-sensitive detection telemetry.</p>
<p>On an NVIDIA T4 inside our network, the result felt like a genuine step change.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><a href="latency-quality.svg" class="lightbox" data-gallery="quarto-lightbox-gallery-3" title="Latency, evaluation quality, and privacy for the external baseline and the GPU service"><img src="https://labs.theagilemonkeys.com/posts/aifindr-privacy-private-fast-affordable/latency-quality.svg" class="img-fluid quarto-figure quarto-figure-center figure-img" style="width:100.0%" alt="Latency, evaluation quality, and privacy for the external baseline and the GPU service"></a></p>
</figure>
</div>
<figcaption>Latency, evaluation quality, and privacy for the external baseline and the GPU service</figcaption>
</figure>
</div>
<p>The result was a <strong>4.9x speed-up in median PII-stage latency</strong>, while both approaches reached a <strong>96.6% evaluation pass rate</strong>. Most importantly, it was a win on both fronts: <strong>raw PII stayed inside our infrastructure</strong>, and the workflow began much sooner without giving up quality.</p>
<p>There were serving lessons hiding inside the win. A GPU does not automatically make concurrent HTTP requests parallel. We had to instrument queue time, configure microbatching explicitly, add backpressure, and distinguish model time from end-to-end latency.</p>
<p>Conclusion: we had achieved both goals. Redaction was much faster, and the original message stayed entirely inside our infrastructure. Everything looked solved.</p>
<p><strong>Or so we thought.</strong></p>
</section>
<section id="then-we-calculated-the-bill" class="level2">
<h2 class="anchored" data-anchor-id="then-we-calculated-the-bill">Then we calculated the bill</h2>
<p>The original deployment design kept one <code>g4dn.xlarge</code> GPU instance active in development, one in staging, and two in production for availability and deployments.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><a href="gpu-topology.png" class="lightbox" data-gallery="quarto-lightbox-gallery-4" title="The original topology required four always-on GPUs across development, staging, and production"><img src="https://labs.theagilemonkeys.com/posts/aifindr-privacy-private-fast-affordable/gpu-topology.png" class="img-fluid quarto-figure quarto-figure-center figure-img" style="width:100.0%" alt="The original topology required four always-on GPUs across development, staging, and production"></a></p>
</figure>
</div>
<figcaption>The original topology required four always-on GPUs across development, staging, and production</figcaption>
</figure>
</div>
<p>That was approximately <strong>EUR 1,343.76 per month</strong>, or <strong>EUR 16,125 per year</strong>, in compute for AIFindr Privacy alone. That bill covered only PII redaction, not the rest of AIFindr’s infrastructure.</p>
<p>We had an approach that solved privacy and latency exceptionally well, but <strong>its cost made the architecture economically unsustainable</strong>.</p>
<p>This was not a model failure. It was a system-design failure. We could have chosen a slightly cheaper GPU and reduced the bill a little, but not enough: the architecture would still have been far too expensive.</p>
<p>So we went back to the drawing board.</p>
</section>
<section id="we-gave-an-agent-the-goal.-then-we-let-the-loop-run" class="level2">
<h2 class="anchored" data-anchor-id="we-gave-an-agent-the-goal.-then-we-let-the-loop-run">We gave an agent the goal. Then we let the loop run</h2>
<p>The new objective was stricter than “find cheaper hardware”:</p>
<blockquote class="blockquote">
<p>Preserve privacy, evaluation quality, and a clearly faster experience, while making the service economically sensible to operate.</p>
</blockquote>
<p>We decomposed the GPU prototype into variables we could test independently: model artifact, numeric representation, runtime, CPU architecture, task size, batching, traffic shape, and deployment topology.</p>
<p>We used coding agents to make that breadth practical. We gave an agent a clear goal and ran it in an autonomous loop with scoped access to the repository and AWS. For each hypothesis, it researched the official artifacts, prepared a temporary deployment, ran evaluations and benchmarks, saved the evidence, removed the temporary infrastructure, and returned to the loop with the next hypothesis. It stopped when an approach met the goal.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><a href="agent-research-loop.png" class="lightbox" data-gallery="quarto-lightbox-gallery-5" title="The autonomous, goal-driven loop used for the second investigation"><img src="https://labs.theagilemonkeys.com/posts/aifindr-privacy-private-fast-affordable/agent-research-loop.png" class="img-fluid quarto-figure quarto-figure-center figure-img" style="width:100.0%" alt="The autonomous, goal-driven loop used for the second investigation"></a></p>
</figure>
</div>
<figcaption>The autonomous, goal-driven loop used for the second investigation</figcaption>
</figure>
</div>
<p>That loop began pointing toward an answer we had barely considered. <em>What if the GPU was not needed at all?</em></p>
</section>
<section id="breakthrough-two-the-surprising-answer-was-cpu" class="level2">
<h2 class="anchored" data-anchor-id="breakthrough-two-the-surprising-answer-was-cpu">Breakthrough two: the surprising answer was CPU</h2>
<p>It sounded wrong at first. Running the model on CPU felt like the route back to slow inference. The measurements told a different story.</p>
<p>That was possible because we were serving a small, task-specific classifier rather than a generative LLM, making an optimized CPU runtime a realistic option.</p>
<p>OpenAI had already published several ONNX variants of the same model. We tested the official artifacts without changing the weights ourselves. The smaller Q4 variants introduced new quality regressions, so we rejected them. The official INT8 artifact, <code>model_quantized</code>, passed.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><a href="runtime-gate.svg" class="lightbox" data-gallery="quarto-lightbox-gallery-6" title="The quality-first selection funnel for the official ONNX artifacts"><img src="https://labs.theagilemonkeys.com/posts/aifindr-privacy-private-fast-affordable/runtime-gate.svg" class="img-fluid quarto-figure quarto-figure-center figure-img" style="width:100.0%" alt="The quality-first selection funnel for the official ONNX artifacts"></a></p>
</figure>
</div>
<figcaption>The quality-first selection funnel for the official ONNX artifacts</figcaption>
</figure>
</div>
<p>INT8 preserved every baseline redaction we checked and reduced runtime memory to about <strong>1.6 GiB</strong>, which meant a <strong>4 GiB task was enough</strong>.</p>
<p>ARM64 changed the performance result too. On direct inference, a 2-vCPU ARM task reached a median of <strong>115 ms</strong>, compared with <strong>144 ms</strong> for the tested T4 path. Quality held, and AIFindr Privacy even outperformed GPT-5.4 Nano on the harder evaluation set (<strong>76% vs.&nbsp;69% normalized pass rate, with half as many false negatives</strong>).</p>
<p><em>Could a CPU service really handle production traffic?</em></p>
<p>We used several months of AIFindr production analytics to reconstruct real peaks across all projects, then load-tested the exact topology we intended to deploy:</p>
<blockquote class="blockquote">
<p>Two ARM64 Fargate tasks, each with 1 vCPU and 4 GiB, behind an internal load balancer.</p>
</blockquote>
<p>The answer was yes. The pair comfortably handled loads above the peaks we had observed while keeping latency within our target and producing no HTTP errors. CPU was not merely viable in a benchmark; it was enough for our real production demand. Two tasks also provided availability during failures and deployments, with a clear horizontal scaling path as traffic grows.</p>
</section>
<section id="from-eur-1344-to-eur-119-a-month" class="level2">
<h2 class="anchored" data-anchor-id="from-eur-1344-to-eur-119-a-month">From EUR 1,344 to EUR 119 a month</h2>
<p>The production pair costs approximately <strong>EUR 59.55 per month</strong>. DEV and STG add approximately EUR 29.77 each, bringing the complete three-environment topology to <strong>EUR 119.09 per month</strong>. Compared with the original EUR 1,343.76 GPU design, that is a <strong>91.1 percent reduction</strong>.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><a href="cost-collapse.svg" class="lightbox" data-gallery="quarto-lightbox-gallery-7" title="The CPU design reduced the estimated AIFindr Privacy infrastructure bill by 91.1 percent"><img src="https://labs.theagilemonkeys.com/posts/aifindr-privacy-private-fast-affordable/cost-collapse.svg" class="img-fluid quarto-figure quarto-figure-center figure-img" style="width:100.0%" alt="The CPU design reduced the estimated AIFindr Privacy infrastructure bill by 91.1 percent"></a></p>
</figure>
</div>
<figcaption>The CPU design reduced the estimated AIFindr Privacy infrastructure bill by 91.1 percent</figcaption>
</figure>
</div>
<p>There was still one inconvenient fact to acknowledge: at today’s AIFindr traffic, GPT-5.4 Nano remains cheaper on direct inference spend. Being honest about that matters. We had dramatically reduced the cost of private inference, but had not yet crossed the pure-cost line against a usage-priced API.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><a href="break-even-relative.svg" class="lightbox" data-gallery="quarto-lightbox-gallery-8" title="Estimated monthly cost against redaction volume, expressed relative to current traffic"><img src="https://labs.theagilemonkeys.com/posts/aifindr-privacy-private-fast-affordable/break-even-relative.svg" class="img-fluid quarto-figure quarto-figure-center figure-img" style="width:100.0%" alt="Estimated monthly cost against redaction volume, expressed relative to current traffic"></a></p>
</figure>
</div>
<figcaption>Estimated monthly cost against redaction volume, expressed relative to current traffic</figcaption>
</figure>
</div>
<p>The curves cross at roughly <strong>4.1 times our July 2026 redaction baseline</strong>. That month’s projected traffic was approximately <strong>65 percent above June</strong>, making the crossing point a plausible horizon rather than a distant thought.</p>
<p>But cost was never the only ledger. Even before that crossing point, raw PII stays inside our infrastructure and redaction is much faster. Those were the two outcomes our clients actually asked for. Lower cost at scale would simply become the third win.</p>
</section>
<section id="what-we-actually-gained" class="level2">
<h2 class="anchored" data-anchor-id="what-we-actually-gained">What we actually gained</h2>
<p>AIFindr Privacy ended with three concrete results: raw PII stays inside our infrastructure, redaction is substantially faster, and the selected design is 91.1 percent cheaper than the original GPU topology.</p>
<p>The model at its center is a classifier, not an LLM. That is part of the lesson: <strong>small, task-specific open-weight models can remove latency, cost, and data boundary problems from narrow parts of a larger agent workflow.</strong></p>
<p>More importantly, <strong>this is no longer an experiment</strong>. It is an open-weight model running in production and <strong>a reusable way to investigate the next improvement</strong>: set the goal, give an agent bounded autonomy, evaluate every hypothesis, and follow the evidence.</p>


</section>

 ]]></description>
  <category>AI</category>
  <category>privacy</category>
  <category>open-weight models</category>
  <category>production</category>
  <category>benchmarking</category>
  <guid>https://labs.theagilemonkeys.com/posts/aifindr-privacy-private-fast-affordable/</guid>
  <pubDate>Thu, 20 Aug 2026 00:00:00 GMT</pubDate>
  <media:content url="https://labs.theagilemonkeys.com/posts/aifindr-privacy-private-fast-affordable/og-image.png" medium="image" type="image/png" height="76" width="144"/>
</item>
<item>
  <title>Our Take on AI SDLC ROI in Regulated Industries</title>
  <link>https://labs.theagilemonkeys.com/posts/ai-sdlc-roi-regulated-industries-enterprise-legible-to-agents/</link>
  <description><![CDATA[ 




<p>CTOs negotiating with AI SDLC providers should assume one thing: <strong>the baseline agent capability</strong> is already strong enough for serious enterprise work. The ROI question has shifted.</p>
<p>For regulated companies, the winning factor will be the <strong>agent operating environment</strong>: domain context, governance, restrictions, test strategy, evidence, and feedback loops that agents can actually use.</p>
<p>Provider selection still matters. Security, cost, data residency, support, and integration depth are real concerns. But they are <strong>no longer the main source of advantage</strong>.</p>
<p>Our opinion is straightforward: <strong>AI SDLC ROI in regulated industries will come from making the enterprise legible to agents.</strong></p>
<section id="the-stack-is-becoming-the-surface" class="level2">
<h2 class="anchored" data-anchor-id="the-stack-is-becoming-the-surface">The stack is becoming the surface</h2>
<p>The leading AI SDLC products are converging around the same pattern: understand the task, inspect the codebase, plan, modify files, run tools, test, review, and submit work.</p>
<p>Claude Code is already positioned as a <strong>command center for multi-agent coding</strong>. Codex follows the same direction from OpenAI’s side. GitHub Copilot, Cursor, Atlassian Rovo, and others are moving along the same curve. The interfaces vary. The strategic pattern is becoming clear.</p>
</section>
<section id="the-real-bottleneck-is-the-enterprise-context" class="level2">
<h2 class="anchored" data-anchor-id="the-real-bottleneck-is-the-enterprise-context">The real bottleneck is the enterprise context</h2>
<p>Most regulated enterprises have enormous amounts of knowledge. They rarely have that knowledge <strong>in a form an agent can use reliably</strong>. The relevant context is scattered across repositories, Jira tickets, architecture documents, Confluence pages, Microsoft Teams threads, policy PDFs, risk registers, incident reviews, security exceptions, vendor assessments, and the heads of senior engineers.</p>
<p><strong>Agents cannot reliably use knowledge that is scattered, stale, contradictory, or unowned.</strong> That creates the practical failure mode we see most often: the agent is intelligent, yet behaves like a newcomer.</p>
<p>It does not know which payment flow is sensitive. It does not know which customer field is regulated. It does not know which dependency is banned. It does not know which architecture decision came from a past incident. It does not know which tests matter, which reviewer owns the domain, or which evidence an auditor will expect.</p>
<p>A serious AI SDLC environment needs a managed domain context:</p>
<ul>
<li>System ownership.</li>
<li>Data classes and regulated flows.</li>
<li>Architecture rules.</li>
<li>Mandatory controls.</li>
<li>Approved patterns.</li>
<li>Trusted tests.</li>
<li>Human approval thresholds.</li>
<li>Policy exceptions and expiry dates.</li>
<li>Evidence required before merge or release.</li>
</ul>
<p><em>This is engineering infrastructure.</em></p>
</section>
<section id="unlocking-the-data-requires-data-engineering-effort" class="level2">
<h2 class="anchored" data-anchor-id="unlocking-the-data-requires-data-engineering-effort">Unlocking the data requires data engineering effort</h2>
<p>Agent performance does not depend only on model quality. <strong>The harness around the agent shapes the outcome</strong>: which tools it can call, how data is retrieved, how results are ranked, and which sources are treated as authoritative.</p>
<p>Naively connecting Codex, Claude Code, or any other agent to every internal data source will not create a strong AI SDLC system. It will create <strong>a noisy context</strong>. The work is to build the data layer agents need: classify, normalize, index, filter, rank, deduplicate, resolve ownership, preserve lineage, and expose the right source at the right moment.</p>
<p>Regulated companies also need to separate <strong>working context</strong> from <strong>validated context</strong>.</p>
<p>Meeting transcripts, chat messages, draft documents, and ticket comments can help an agent understand current intent. They <strong>should not carry the same authority</strong> as ERP records, production systems of record, approved policies, signed contracts, validated requirements, or released architecture decisions.</p>
<p>A human-in-the-loop process can promote working context into validated context when needed. That promotion should leave evidence: who approved it, when, for which scope, and when it should expire.</p>
<p>For precision and cost, the long-term answer is usually some form of indexed knowledge layer: part structured data, part unstructured knowledge, part temporal memory. Call it a knowledge graph, a context graph, or an enterprise memory layer. <em>The name matters less than the behavior.</em> It should help agents answer the most frequent questions quickly, cite the source of truth, avoid stale or low-authority information, and keep LLM context manageable when the enterprise has too much data to fit into a prompt. Agents can always go back to the original source when the task requires it. The context layer decides what they should see first.</p>
</section>
<section id="skills-are-becoming-the-new-runbooks" class="level2">
<h2 class="anchored" data-anchor-id="skills-are-becoming-the-new-runbooks">Skills are becoming the new runbooks</h2>
<p>Skills make this direction concrete. They package instructions, resources, and optional scripts so an agent can follow a workflow consistently instead of relying on whatever an engineer remembers to say in chat.</p>
<p>For regulated AI SDLC, that matters. A good skill can capture the sources to inspect, the tools that are allowed, the actions that are forbidden, the tests that must run, the evidence that must be attached, and the risk threshold that requires escalation. It turns fragile, repeated guidance into <strong>a versioned operating procedure</strong>.</p>
<p>The best organizations will turn their <strong>senior engineering judgment</strong> into reusable agent runbooks: small enough to maintain, specific enough to guide real work, and explicit enough to audit.</p>
</section>
<section id="governance-has-to-live-inside-the-workflow" class="level2">
<h2 class="anchored" data-anchor-id="governance-has-to-live-inside-the-workflow">Governance has to live inside the workflow</h2>
<p>In regulated industries, governance cannot remain <strong>a policy document or a final review step</strong>. Agents can read files, run commands, call tools, open PRs, inspect logs, and propose changes. Governance has to live inside that workflow. As it happens with human employees, agents must be properly isolated by design, with a strong permissions system that makes it physically impossible for an agent to access the wrong data. This maps naturally to HIPAA safeguards, FDA Computer Software Assurance, and the NIST AI Risk Management Framework.</p>
<p>For AI SDLC, that means:</p>
<ul>
<li>Agent identity and access control.</li>
<li>Tool permissions by task and risk level.</li>
<li>Data boundaries and secrets protection.</li>
<li>Branch protections and merge policies.</li>
<li>Mandatory tests and security scans.</li>
<li>Human approval gates.</li>
<li>Audit logs for prompts, tool calls, models, and decisions.</li>
<li>Evidence capture for regulated workflows.</li>
<li>Incident review when agent-generated work fails.</li>
</ul>
<p><strong>Instructions guide behavior. Controls enforce behavior.</strong></p>
</section>
<section id="measure-roi-where-it-matters" class="level2">
<h2 class="anchored" data-anchor-id="measure-roi-where-it-matters">Measure ROI where it matters</h2>
<p>Activity metrics are dangerous. <strong>More generated code can mean more review burden, more rework, and more risk.</strong></p>
<p>AI SDLC redefines how you measure ROI. We’ve seen other metrics being more useful on this context:</p>
<ul>
<li>First-pass PR acceptance rate.</li>
<li>Reviewer minutes per accepted change.</li>
<li>Issue-to-merge lead time.</li>
<li>Defect escape rate after agent-authored changes.</li>
<li>Security findings by severity.</li>
<li>Policy violations caught before human review.</li>
<li>Percentage of agent changes backed by required evidence.</li>
<li>Time to produce audit or release documentation.</li>
<li>Repeated review comments converted into reusable skills or rules.</li>
</ul>
<p>And those are just examples. The way you measure depends on your own environment. AI can help to measure, too.</p>
<p>The best AI SDLC program should feel <strong>faster to engineering</strong> and <strong>safer to risk, compliance, and security</strong>.</p>
</section>
<section id="what-questions-ctos-should-answer" class="level2">
<h2 class="anchored" data-anchor-id="what-questions-ctos-should-answer">What questions CTOs should answer</h2>
<p>Before scaling AI SDLC, leadership should answer a small set of hard questions:</p>
<ol type="1">
<li><strong>Where does our domain knowledge live, and who owns it?</strong></li>
<li><strong>Which restrictions should agents learn, and which restrictions should agents be unable to bypass?</strong></li>
<li><strong>What does an acceptable AI-generated PR look like in our environment?</strong></li>
<li><strong>Which workflows can safely run with high autonomy?</strong></li>
<li><strong>How will we measure accepted work, rework, defects, policy violations, and audit readiness?</strong></li>
<li><strong>How will failed agent runs improve our skills, tests, rules, and context?</strong></li>
</ol>


<hr>
<p class="hub-topic"><a class="share-btn" href="https://hub.theagilemonkeys.com/topics/ai-sdlc?utm_source=lab&amp;utm_medium=referral&amp;utm_campaign=tamhub-2026-10&amp;utm_content=topic-link">Topic on tamHub: AI SDLC →</a></p>
</section>

 ]]></description>
  <category>AI agents</category>
  <category>AI SDLC</category>
  <category>governance</category>
  <guid>https://labs.theagilemonkeys.com/posts/ai-sdlc-roi-regulated-industries-enterprise-legible-to-agents/</guid>
  <pubDate>Tue, 09 Jun 2026 00:00:00 GMT</pubDate>
  <media:content url="https://labs.theagilemonkeys.com/posts/ai-sdlc-roi-regulated-industries-enterprise-legible-to-agents/og-image.png" medium="image" type="image/png" height="76" width="144"/>
</item>
<item>
  <title>The brand that survives AI</title>
  <dc:creator>The Agile Monkeys</dc:creator>
  <link>https://labs.theagilemonkeys.com/posts/brand-that-survives-ai/</link>
  <description><![CDATA[ 




<p>Any team already using generative AI to produce work knows the symptom. The first draft is acceptable. The second drifts a little. By the fifth, you don’t recognize it. Nobody decided to change the brand, but the brand has changed anyway, one degree per cycle, until one day you open a landing page shipped last week and can’t tell what company it belongs to.</p>
<p>Call it identity drift. It’s what happens when AI produces the work but the brand still lives where only humans can read it.</p>
<section id="where-the-brand-lives" class="level2">
<h2 class="anchored" data-anchor-id="where-the-brand-lives">Where the brand lives</h2>
<p>Your brand probably lives in good shape. A Figma library with Variables and components. A brand portal on the web. A Notion page the team bookmarks. A senior designer who reviews the final pass. None of that is broken. None of it is what the AI reads.</p>
<p>Brand portals are optimized for human consumption: visual, browseable, narrative. The AI needs the same source in a different shape. AI didn’t create that gap. It made it visible. And now it produces against the gap at scale.</p>
</section>
<section id="move-the-brand-to-where-the-ai-reads" class="level2">
<h2 class="anchored" data-anchor-id="move-the-brand-to-where-the-ai-reads">Move the brand to where the AI reads</h2>
<p>If the AI produces, the brand has to live where the AI reads. Plain text, structured, in a single source, accessible to any tool your team opens tomorrow. Not as a replacement for your Figma library or your brand portal. As the layer the AI can actually read.</p>
<p>In April 2026 Google published the <a href="https://stitch.withgoogle.com/docs/design-md/specification">DESIGN.md spec</a> under Apache 2.0: an open format that combines YAML for exact tokens with markdown for rationale, intent, and use cases. Any LLM reads it natively. So do agentic builders that follow the spec. So does any code editor with file access. It’s the brand turned into something machines can parse and humans can keep editing with a commit.</p>
<p>That’s the foundation we built The Agile Monkeys Brand Hub on.</p>
</section>
<section id="the-system" class="level2">
<h2 class="anchored" data-anchor-id="the-system">The system</h2>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><a href="brand-hub-system-loop.png" class="lightbox" data-gallery="quarto-lightbox-gallery-1" title="The Brand Hub loop: a single DESIGN.md source feeds generators and evaluators across the team’s AI tools."><img src="https://labs.theagilemonkeys.com/posts/brand-that-survives-ai/brand-hub-system-loop.png" class="img-fluid figure-img" alt="The Brand Hub loop: a single DESIGN.md source feeds generators and evaluators across the team’s AI tools."></a></p>
<figcaption>The Brand Hub loop: a single DESIGN.md source feeds generators and evaluators across the team’s AI tools.</figcaption>
</figure>
</div>
<p>A single source: the <code>DESIGN.md</code> in a private repo. A hub that distributes it alongside SVG logos and ready-to-paste prompts. The tools your team uses for generative work read from the hub: agentic builders like Lovable, v0 or Bolt, AI code editors like Cursor, CLI agents like Codex or Claude Code. They produce a first pass. Humans iterate on it according to what the piece needs. Evaluators check that the iteration didn’t pull it off-brand. The loop closes.</p>
<p>Two kinds of prompts live in the hub: generators that produce, evaluators that judge. They never combine. It’s the same rule that keeps author and reviewer apart in any healthy process. Both are loadable, so any tool that reads markdown picks up the right one when it starts a project. That’s what turns a brand portal into something machines can operate.</p>
</section>
<section id="the-loop-that-matters-apply-correct-learn" class="level2">
<h2 class="anchored" data-anchor-id="the-loop-that-matters-apply-correct-learn">The loop that matters: apply, correct, learn</h2>
<p>Most teams stop at produce, validate, publish. The interesting loop runs one step further: produce, validate, learn.</p>
<p>The evaluator doesn’t block publication. It informs. Each gap comes with cause and consequence, and three open paths for the user: correct, publish with a notification to the brand guardian, or reevaluate whether the piece is in the right lane. When the user picks correct, the evaluator hands over a ready-to-paste prompt with the fixes. It never edits the piece itself. That keeps producer and judge separate by mechanism, not just by principle. The system’s authority lives in what it offers. Forbidding is not how it works.</p>
<p>If the evaluator keeps flagging the same gap across different pieces, that’s the system asking for adjustment. The pieces are doing their job. They’re surfacing where the source has gone stale. Every use becomes signal. Each piece that passes validates the system. Each piece that breaks refines it.</p>
</section>
<section id="updating-the-system-means-editing-a-file" class="level2">
<h2 class="anchored" data-anchor-id="updating-the-system-means-editing-a-file">Updating the system means editing a file</h2>
<p>A new decision (a component, a rule, a tone) opens a PR to the <code>DESIGN.md</code> from GitHub’s web UI. No terminal, no Git CLI, accessible to non-technical people. Automation handles what’s technical. Human guardians judge whether the change makes sense for the brand.</p>
<p>Once merged, the change is live. New pieces consume the new version automatically. Nothing else needs rebuilding. The source changed, and everything that reads it keeps reading it.</p>
</section>
<section id="the-window" class="level2">
<h2 class="anchored" data-anchor-id="the-window">The window</h2>
<p>The first draft is already on-brand, and AI speed stops fighting brand quality because the brand is already inside the prompt. The system also gets smarter with use: evaluations don’t only improve pieces, they move the system. The brand doesn’t petrify. It grows because coherence lives in the file, and the file is editable.</p>
<p>A brand only humans can read doesn’t survive AI. Your team already uses generative tools to produce. The open question is whether your brand can survive that production without you watching every piece.</p>
<p>We built this for ourselves, and it’s already producing. Brands hold their shape over the next two years if their guidelines are where the AI can read them. Otherwise the AI writes the brand.</p>


</section>

 ]]></description>
  <category>AI strategy</category>
  <category>brand</category>
  <category>design systems</category>
  <guid>https://labs.theagilemonkeys.com/posts/brand-that-survives-ai/</guid>
  <pubDate>Tue, 02 Jun 2026 00:00:00 GMT</pubDate>
  <media:content url="https://labs.theagilemonkeys.com/posts/brand-that-survives-ai/og-image.png" medium="image" type="image/png" height="76" width="144"/>
</item>
<item>
  <title>RAG Retrieval Evaluation in the Ingestion Pipeline: What We Learned</title>
  <link>https://labs.theagilemonkeys.com/posts/what-we-learned-moving-retrieval-evals-into-the-ingestion-pipeline/</link>
  <description><![CDATA[ 




<p>We ran into this while working on a knowledge-heavy product with overlapping content, frequent updates, and plenty of opportunities for the system to sound confidently wrong. The symptom was painfully familiar: we updated the knowledge base, and the answer still failed.</p>
<p>That kind of failure gets blamed on the model, or on “RAG quality” in the abstract. But the operational lesson was sharper. If a content change can degrade retrieval, then ingestion is part of the serving path. And if ingestion can break retrieval, that’s where the evaluation needs to run.</p>
<section id="the-real-failure-mode-wasnt-the-model-got-confused" class="level2">
<h2 class="anchored" data-anchor-id="the-real-failure-mode-wasnt-the-model-got-confused">The real failure mode wasn’t “the model got confused”</h2>
<p>Our first lesson was that classic single-pass RAG was too weak for a corpus with heavy overlap and weak boundaries.</p>
<p>Different entities shared similar names while content overlapped semantically across contexts that had nothing to do with each other. Freshness made things worse: old chunks could stick around and remain plausible long after the underlying source had changed. A single retrieval pass would return something that sounded right while actually being wrong.</p>
<p>That last problem is the dangerous one. <strong>Retrieval failures in this kind of corpus tend to look plausible</strong>. The system finds a chunk adjacent to the truth, close enough to generate a confident-sounding answer but far enough to mislead anyone who trusts it.</p>
<p>In practice, that pushed us toward the worst possible coping strategy: one-off prompt tweaks for specific failure cases. A well-placed system instruction can rescue a demo, but it doesn’t make the retrieval layer any better at separating lookalike content. Over time, those patches multiply and become their own maintenance burden.</p>
<p>Once we framed the problem that way, the work that actually mattered turned out to be lower in the stack than we’d expected: metadata boundaries, chunk navigation, retrieval strategy, and verification loops.</p>
</section>
<section id="if-the-knowledge-changes-at-ingestion-time-evaluate-it-there" class="level2">
<h2 class="anchored" data-anchor-id="if-the-knowledge-changes-at-ingestion-time-evaluate-it-there">If the knowledge changes at ingestion time, evaluate it there</h2>
<p>The key shift was simple: run retrieval checks when content enters the system, rather than waiting for a human to notice a bad answer downstream.</p>
<p>That means treating ingestion as more than a data plumbing step. Ingestion is where retrieval quality can regress, because that’s where you decide how files get parsed, how content gets chunked, which metadata survives the process, and whether canonical identifiers and freshness signals make it through intact. If any of those decisions change, answer quality can change with them.</p>
<p>So we started embedding an <strong>evaluation step directly into the automated ingestion scripts</strong>, with the goal of verifying retrieval quality and raising alerts when something degrades.</p>
<p>The flow looks roughly like this:</p>
<div class="cell" data-layout-align="default">
<div class="cell-output-display">
<div>
<p></p><figure class="figure"><p></p>
<div>

<pre class="mermaid mermaid-js">graph TD
  A["Source Fetch"] --&gt; B["Parse / Normalize"]
  B --&gt; C["Chunk"]
  C --&gt; D["Enrich with Canonical Metadata"]
  D --&gt; E["Index"]
  E --&gt; F["Run Retrieval Eval Set"]
  F --&gt; G["Compare Against Baseline"]
  G --&gt; H["Alert or Block on Regression"]
</pre>
</div>
<p></p></figure><p></p>
</div>
</div>
</div>
<p>This does two useful things:</p>
<ol type="1">
<li>It moves <strong>quality detection closer to the change that caused it</strong>. You’re no longer waiting for a user to stumble into a regression three hours later.</li>
<li>It turns <strong>retrieval quality into something that can participate in release logic</strong>: if this ingestion run makes retrieval materially worse on known cases, it shouldn’t pass quietly.</li>
</ol>
<p>We increasingly think this is the missing piece in a lot of RAG systems. Teams evaluate prompts, inspect traces after incidents, and review final answers. But they rarely evaluate the exact moment where the knowledge base changed shape.</p>
</section>
<section id="why-we-stopped-trusting-one-retrieval-pass" class="level2">
<h2 class="anchored" data-anchor-id="why-we-stopped-trusting-one-retrieval-pass">Why we stopped trusting one retrieval pass</h2>
<p>Moving evaluation earlier only helps if the retrieval stack itself surfaces ambiguity instead of hiding it.</p>
<p>For us, that meant <strong>replacing the single retrieval call with a more agentic pattern</strong>. We added hybrid search to avoid depending on one similarity mechanism, pagination so the system could look beyond the first page of results, and document-level drill-down so the agent could zoom into a promising source rather than guessing from a fragment. On the data side, we invested in strong metadata boundaries to prevent overlapping content from bleeding across contexts, and we preserved canonical IDs from the source whenever possible so disambiguation could rely on something sturdier than naming luck.</p>
<p>The contrast is instructive. A one-shot RAG call asks “what seems relevant right now?” and commits to whatever comes back. A search-plus-drill-down workflow lets the agent ask a follow-up: “what evidence belongs to this specific context, and what should I inspect next?” In domains where content overlaps heavily and naming is messy, that second question is the one that keeps the system from acting like a very confident intern with three browser tabs open.</p>
</section>
<section id="keeping-the-safety-path-and-the-experiment-path-separate" class="level2">
<h2 class="anchored" data-anchor-id="keeping-the-safety-path-and-the-experiment-path-separate">Keeping the safety path and the experiment path separate</h2>
<p>The other lesson was organizational rather than technical.</p>
<p>Once we started taking ingestion-time evaluation seriously, we realized we needed two distinct loops. The first is a <strong>safety loop</strong> for production ingestion: did content ingest cleanly, did retrieval regress on the evaluation set, and should we alert or block promotion? The second is an <strong>experiment loop</strong> for retrieval hypotheses: would different chunking work better, does a new file processing approach improve things, and which ideas are worth graduating into production?</p>
<p>These loops are related, but they shouldn’t be the same system. We started treating the experiment loop as its own harness rather than letting every idea leak directly into ingestion. That makes the work less glamorous but far more usable. You get a place to test retrieval hypotheses systematically instead of rediscovering the same failure mode document by document, demo by demo.</p>
<p>It also forces a healthy question that’s easy to avoid: what evaluation set and alert thresholds are strong enough that newly ingested content is safe to trust? That lives squarely in the boring-but-critical territory where production reliability actually gets built.</p>
</section>
<section id="what-wed-do-differently-from-day-one" class="level2">
<h2 class="anchored" data-anchor-id="what-wed-do-differently-from-day-one">What we’d do differently from day one</h2>
<p>If we were starting this system over, we’d be more opinionated about three things. We’d treat ingestion as part of retrieval quality from the start, rather than wiring in evaluation after the first few regressions forced our hand. Metadata boundaries and canonical identifiers would come before prompt-level fixes, because those fixes multiply fast and age poorly. And the agent would get real search and drill-down tools from day one instead of hoping a single retrieval pass could hold up against a corpus full of overlapping entities.</p>
</section>
<section id="key-takeaway" class="level2">
<h2 class="anchored" data-anchor-id="key-takeaway">Key takeaway</h2>
<p>If your retrieval QA starts when a user opens the chat, most regressions have already happened upstream during ingestion. Put evaluation there, add alerting there, and treat metadata quality as part of the product rather than cleanup work you’ll get to later.</p>


</section>

 ]]></description>
  <category>RAG</category>
  <category>evaluation</category>
  <category>data pipelines</category>
  <guid>https://labs.theagilemonkeys.com/posts/what-we-learned-moving-retrieval-evals-into-the-ingestion-pipeline/</guid>
  <pubDate>Thu, 19 Mar 2026 00:00:00 GMT</pubDate>
  <media:content url="https://labs.theagilemonkeys.com/posts/what-we-learned-moving-retrieval-evals-into-the-ingestion-pipeline/og-image.png" medium="image" type="image/png" height="76" width="144"/>
</item>
<item>
  <title>AI Agent Platform Architecture: What One Real Agent Taught Us</title>
  <link>https://labs.theagilemonkeys.com/posts/what-one-real-agent-taught-us-about-the-platform-we-actually-need/</link>
  <description><![CDATA[ 




<p>Most teams building agents still start in the wrong place.</p>
<p>They sketch the platform first: agent registry, orchestration layer, eval dashboards, memory, governance, runtime, beautiful control plane. It feels strategic, but it hides the question that actually matters: can one agent do useful work against real company data, with a failure rate people will tolerate, and a review cost the team can actually live with?</p>
<p>We took a different path. We skipped the platform, shipped one real agent, and let that agent surface the stack we actually needed.</p>
<p>Something awkward happens fast when you do that. The missing pieces usually aren’t the ones the platform demos advertise.</p>
<p>In our case, one production-oriented internal content workflow forced a sharper realization. A platform that can tell you token spend is still incomplete if it can’t tell you whether the agent created any value, whether reviewer feedback improved the next run, or what the agent actually did in a user’s name.</p>
<p>That changed our view of what the second layer of an agent platform should be.</p>
<section id="the-platform-first-trap-is-still-a-trap" class="level2">
<h2 class="anchored" data-anchor-id="the-platform-first-trap-is-still-a-trap">The platform-first trap is still a trap</h2>
<p>The first useful agent immediately exposes realities that architecture diagrams politely ignore.</p>
<p>For us, the bar was concrete: produce publishable content on a weekly cadence. Something a human reviewer could actually ship, not just find “surprisingly good.”</p>
<p>That bar made the real blockers obvious. Source access was uneven, and retrieval quality varied enough between runs that we couldn’t predict whether a given draft would be usable. Feedback loops depended on manual effort that didn’t scale. Review, which we’d initially assumed was temporary scaffolding, turned out to be a permanent part of the system. And the hardest problem wasn’t generation at all — it was earning enough operational trust that people would actually use the output.</p>
<p>A control plane on top of those problems would have been premature. A very organized way to scale fragile behavior.</p>
<p>One real agent gives you a better roadmap than a month of platform speculation, because it forces contact with failure modes instead of abstractions.</p>
</section>
<section id="cost-dashboards-arent-roi" class="level2">
<h2 class="anchored" data-anchor-id="cost-dashboards-arent-roi">Cost dashboards aren’t ROI</h2>
<p>Once we started looking at agent platforms more closely, a pattern jumped out.</p>
<p>In the products we reviewed — including Salesforce Agentforce, Microsoft Copilot Studio, Notion Custom Agents, Dust, and CrewAI — cost tracking was common. Model spend is visible, scary, and easy to graph, so it makes sense as a starting metric.</p>
<p>The interesting question for a real agent, though, is whether it reduced work, increased throughput, or improved a decision enough to justify existing. Those questions require mission-specific metrics.</p>
<p>For a content workflow agent, the dashboard we actually wanted looked like this:</p>
<div class="cell" data-layout-align="default">
<div class="cell-output-display">
<div>
<p></p><figure class="figure"><p></p>
<div>

<pre class="mermaid mermaid-js">graph TD
  M["Mission: Produce publishable technical articles"]

  M --&gt; C["Cost"]
  M --&gt; O["Outcomes"]
  M --&gt; L["Learning"]
  M --&gt; R["Risk"]

  C --&gt; C1["Model/tool spend per run"]

  O --&gt; O1["Drafts generated"]
  O --&gt; O2["Drafts approved for publication"]
  O --&gt; O3["Time to publish"]
  O --&gt; O4["Edit rate / reviewer effort"]

  L --&gt; L1["Feedback captured"]
  L --&gt; L2["Lessons learned written"]
  L --&gt; L3["Repeated mistakes retired"]

  R --&gt; R1["Messages/posts sent"]
  R --&gt; R2["Actions awaiting approval"]
  R --&gt; R3["Write operations by channel"]
</pre>
</div>
<p></p></figure><p></p>
</div>
</div>
</div>
<p>That’s a very different platform surface from “token usage over time.”</p>
<p>A platform that only tracks spend optimizes for cheaper wrong answers. Useful for finance, but nowhere near enough for operations. We increasingly think every serious agent needs a mission-specific ROI dashboard rather than a generic activity panel. Otherwise teams end up arguing about vibes, with a cost chart on the side pretending to be evidence.</p>
</section>
<section id="feedback-cant-live-in-one-persons-settings-page" class="level2">
<h2 class="anchored" data-anchor-id="feedback-cant-live-in-one-persons-settings-page">Feedback can’t live in one person’s settings page</h2>
<p>The second gap was how platforms handle improvement.</p>
<p>A lot of current tooling assumes a workflow where one person creates the agent, opens settings, rewrites the prompt, and tests again. That works for demos. It breaks down the moment a second person has an opinion.</p>
<p>In real work, feedback is distributed. One reviewer catches tone problems, another flags a missing source, and a third keeps noticing the same factual error the agent was already corrected for. If all of that learning stays trapped in the creator’s prompt window, the team isn’t improving the agent — they’re generating a queue of editorial cleanup.</p>
<p>We hit that directly. Written feedback wasn’t reliably changing later behavior, so the agent could repeat the same mistake with fresh confidence. One of the least charming traits an agent can have.</p>
<p>Our response was to move toward a simpler, more operational loop: let reviewers give feedback in the flow of work, let the agent produce a better revision from that feedback, and when corrections repeat, turn them into durable lessons learned that persist across runs.</p>
<p>Even our first implementation made the real requirement clear. Improvement has to work across the whole team and compound over time, because individual prompt tweaks just don’t accumulate. The early version also surfaced its own limitations quickly — feedback handling only worked on newly generated pieces, which is exactly the kind of boundary you only notice once the workflow is real.</p>
<p>That’s why we now treat feedback capture as platform-level work.</p>
</section>
<section id="governance-starts-with-action-visibility" class="level2">
<h2 class="anchored" data-anchor-id="governance-starts-with-action-visibility">Governance starts with action visibility</h2>
<p>The third lesson was even more operational.</p>
<p>During testing of a Slack-connected workflow, we noticed messages had been sent without the user realizing they’d already gone out. In an internal setting that’s merely annoying, but in a customer-facing workflow it’s the kind of thing that ends pilot programs.</p>
<p>This is where a lot of “agent governance” talk becomes too abstract. Before you design exotic policy engines, there’s a simpler question worth answering: can the user see exactly what the agent did, where it did it, and whether it acted with explicit approval? Without that visibility, you have permissions plus optimism — which isn’t the same thing as governance.</p>
<p>The minimum useful layer is boring in the best possible way: a clear action log, explicit approval points for risky writes, visible traceability for messages sent on a user’s behalf, and easy ways to reduce permissions when trust hasn’t been earned yet. If using the agent makes people want to revoke write access, that’s worth treating as product feedback rather than user hesitation.</p>
<p>We now think action visibility is one of the first platform primitives worth building, especially for anything that writes into shared systems.</p>
</section>
<section id="what-one-real-agent-taught-us-to-build-next" class="level2">
<h2 class="anchored" data-anchor-id="what-one-real-agent-taught-us-to-build-next">What one real agent taught us to build next</h2>
<p>After all of this, the platform shape that feels justified is much smaller and much sharper than the original control-plane fantasy.</p>
<p>We’d start by making it clear what the agent can read and which missing source would make it obviously wrong. Then we’d add mission-specific outcome tracking, because measuring useful work is the only way to get past “it seems good” as an evaluation strategy. From there, the priorities are a durable review loop where feedback actually improves future runs rather than evaporating after one correction, and action logs with approval controls for anything writing into Slack, docs, tickets, or external systems. Only after those pieces were earning their keep would we invest in broader abstractions like registry, orchestration, and generalized runtime.</p>
<p>The sequence looks like this:</p>
<div class="cell" data-layout-align="default">
<div class="cell-output-display">
<div>
<p></p><figure class="figure"><p></p>
<div>

<pre class="mermaid mermaid-js">graph TD
  A["Pick one real agent"] --&gt; B["Define operational success metric"]
  B --&gt; C["Connect the sources it actually needs"]
  C --&gt; D["Instrument outcomes and review effort"]
  D --&gt; E["Make feedback durable"]
  E --&gt; F["Log risky actions and approvals"]
  F --&gt; G["Abstract common pieces into a platform"]
</pre>
</div>
<p></p></figure><p></p>
</div>
</div>
</div>
<p>Less glamorous than “let’s build the agent platform.” Also much harder to fake.</p>
</section>
<section id="key-takeaway" class="level2">
<h2 class="anchored" data-anchor-id="key-takeaway">Key takeaway</h2>
<p>If you’re building agents, don’t stop at source connectors and spend dashboards. One real workflow will quickly show you what the platform is actually missing: outcome tracking, durable team feedback, and action visibility. Build those before you fall in love with orchestration diagrams.</p>


</section>

 ]]></description>
  <category>AI agents</category>
  <category>architecture</category>
  <category>AI strategy</category>
  <guid>https://labs.theagilemonkeys.com/posts/what-one-real-agent-taught-us-about-the-platform-we-actually-need/</guid>
  <pubDate>Thu, 19 Mar 2026 00:00:00 GMT</pubDate>
  <media:content url="https://labs.theagilemonkeys.com/posts/what-one-real-agent-taught-us-about-the-platform-we-actually-need/og-image.png" medium="image" type="image/png" height="76" width="144"/>
</item>
<item>
  <title>LLM-Powered Codebase Analysis: Technical Due Diligence Before Software Estimation</title>
  <link>https://labs.theagilemonkeys.com/posts/how-we-use-llms-to-map-unfamiliar-codebases-before-committing-to-an-estimate/</link>
  <description><![CDATA[ 




<p>When a mature system lands on your desk with the instruction “just add this one capability,” the most dangerous thing you can do is start coding too early.</p>
<p>In older systems, the estimate is rarely about code volume. It’s about unknowns: hidden assumptions, undocumented flows, infrastructure that lives outside the repo, security controls that only exist in somebody’s head. The gap between what you think the system does and what it actually does is where estimates blow up.</p>
<p>We’ve been using LLMs for a very specific part of that problem: technical due diligence before implementation. The model reads an unfamiliar codebase fast enough that we can answer the question that actually matters: should we integrate with what already exists, extend it, or rebuild the capability from scratch?</p>
<p>The useful shift has been simple: let the LLM produce a map of the codebase, then make humans confirm that map before anyone commits to an estimate.</p>
<section id="most-implementation-estimates-are-really-estimates-of-hidden-system-behavior" class="level2">
<h2 class="anchored" data-anchor-id="most-implementation-estimates-are-really-estimates-of-hidden-system-behavior">Most “implementation estimates” are really estimates of hidden system behavior</h2>
<p>On greenfield work, scope is usually visible. On mature systems, it often isn’t.</p>
<p>A request that sounds small can sit on top of a surprising amount of invisible machinery. Routing rules exist but aren’t documented. Background jobs only become obvious once you trace them end to end. Authorization assumptions hide inside service boundaries. Operational controls live outside the application layer entirely. Downstream notifications, retries, and failure handling never came up in the original request.</p>
<p>This is where teams get trapped. Somebody hears “we need feature X,” imagines a clean standalone implementation, and estimates accordingly. Two days later, the real work shows up: “Actually it has to fit the existing intake path, reuse the current workflows, preserve existing security controls, and behave consistently with the rest of the platform.”</p>
<p>At that point, you’re no longer estimating implementation. You’re paying interest on unknowns.</p>
<p>We’ve found LLMs are unusually good at compressing that discovery phase.</p>
</section>
<section id="start-by-asking-for-a-flow-map" class="level2">
<h2 class="anchored" data-anchor-id="start-by-asking-for-a-flow-map">Start by asking for a flow map</h2>
<p>The first thing we want from the model is a map of what exists, not a recommendation for what to build.</p>
<p>A good prompt is closer to technical archaeology than brainstorming:</p>
<pre class="text"><code>Read this codebase and map the end-to-end flow for capability X. Cite the files, routes, handlers, jobs, and config involved. Separate clearly between: 1) behavior directly supported by source code 2) likely behavior inferred from naming or structure 3) important unknowns that require human confirmation</code></pre>
<blockquote class="blockquote">
<p>💡 <strong>A note on prompts:</strong> These are starting points, not recipes. Every codebase has its own idioms, and you’ll need to adjust wording, scope, and follow-up questions to get useful output. If you copy these verbatim and get mediocre results, iterate on them before writing off the approach.</p>
</blockquote>
<p>That three-way distinction matters. Without it, the model blends observation and invention into one very confident paragraph. You get a map you can’t trust.</p>
<p>What we want instead is a reviewable pipeline:</p>
<div class="cell" data-layout-align="default">
<div class="cell-output-display">
<div>
<p></p><figure class="figure"><p></p>
<div>

<pre class="mermaid mermaid-js">graph LR
  A["New request"] --&gt; B["Entry point"]
  B --&gt; C["Routing / orchestration"]
  C --&gt; D["Validation &amp; authorization"]
  D --&gt; E["Storage / persistence"]
  E --&gt; F["Downstream processing"]
  F --&gt; G["Notifications / side effects"]
  G --&gt; H["Error handling / retries"]
</pre>
</div>
<p></p></figure><p></p>
</div>
</div>
</div>
<p>That artifact makes the existing system discussable. Vague “I think it works like this” turns into something the team can actually review. And it immediately exposes where the request assumes something exists that doesn’t, or ignores something that does.</p>
<p>We’ve had good results cross-checking the same mapping task with more than one model, then comparing outputs. Disagreement between models is informative: if two produce different maps from the same repo, humans should slow down and look closer.</p>
</section>
<section id="use-the-flow-map-to-generate-the-questions-that-change-the-estimate" class="level2">
<h2 class="anchored" data-anchor-id="use-the-flow-map-to-generate-the-questions-that-change-the-estimate">Use the flow map to generate the questions that change the estimate</h2>
<p>Once we have a draft map, the next step is structured questioning.</p>
<p>This is where the LLM earns its keep as a question generator for missing work. We ask something like:</p>
<pre class="text"><code>Given this observed flow, what security, operational, and scope questions must be answered before deciding whether to integrate, extend, or rebuild? Group them by risk area and keep each question specific.</code></pre>
<p>The checklist that comes back is rarely publishable as-is, but it’s a strong starting point. We rewrite it in our own words and send it for confirm-or-correct review.</p>
<p>The categories that repeatedly change the estimate are consistent enough to be worth listing:</p>
<ul>
<li><strong>Authentication and trust boundaries.</strong> Where does this input come from? Where is identity established? What parts of the trust chain live in the application versus external infrastructure?</li>
<li><strong>Authorization and tenancy.</strong> Who can trigger this flow, how is access mapped to the right account or tenant, and what happens when identity is known but permission isn’t?</li>
<li><strong>Validation and content safety.</strong> Are we validating structurally, semantically, or both? Are there hidden rules around formats, payload sizes, or attachment types? Where do malformed inputs go?</li>
<li><strong>Abuse prevention and backpressure.</strong> Where is throttling enforced? What happens under spikes, replayed requests, or noisy neighbors? Are retries idempotent, bounded, and observable?</li>
<li><strong>Downstream side effects.</strong> Which jobs, notifications, webhooks, or derived artifacts depend on this path? What must happen synchronously versus eventually? What constitutes partial success?</li>
</ul>
<p>This is the point where a supposedly small feature reveals itself as either a straightforward extension, an integration into a partially built capability, or a genuine architectural mismatch that justifies a rebuild.</p>
<p>Without the checklist, teams discover that late. With it, they discover it while decisions are still cheap.</p>
</section>
<section id="llms-are-surprisingly-good-at-spotting-scope-drift" class="level2">
<h2 class="anchored" data-anchor-id="llms-are-surprisingly-good-at-spotting-scope-drift">LLMs are surprisingly good at spotting scope drift</h2>
<p>One of the more valuable patterns we’ve found is using the model as a contradiction engine.</p>
<p>Once the existing flow is mapped, we give the model the observed architecture (supported by code and config) alongside the newly requested behavior (as described by stakeholders). Then we ask it to highlight tension:</p>
<pre class="text"><code>Compare the requested change with the current implementation. What assumptions would break if we implement it the proposed way? Which parts look like an extension of the current design, and which parts imply a different architecture?</code></pre>
<p>This works well because scope drift enters projects in harmless-looking language. “Can we make it work through this other path too?” “Can we move this responsibility to another component?” “Can we simplify by bypassing the existing workflow?” Those sentences sound small. A model that has already built a repo-level map can surface the consequences quickly: where trust boundaries shift, where orchestration gets duplicated, where isolation breaks, where failure handling becomes inconsistent, or where the operational burden exceeds anything the original estimate accounted for.</p>
<p>We treat that output as a sharp draft of the conversation humans need to have, not as a conclusion in itself.</p>
</section>
<section id="the-guardrails-that-keep-this-useful" class="level2">
<h2 class="anchored" data-anchor-id="the-guardrails-that-keep-this-useful">The guardrails that keep this useful</h2>
<p>This approach only works if the team is disciplined about how the output is used. Our operating rules:</p>
<ul>
<li>Every claim should trace back to source: files, handlers, config, jobs, tests, or logs. If the model can’t point to evidence, the claim stays provisional.</li>
<li>Use confirm-or-correct review. The goal is helping maintainers validate or falsify the map quickly, not producing a document that looks authoritative on its own.</li>
<li>Separate observed behavior from desired behavior. Mature systems are full of “it should do this” stories. That’s not the same as “it does this today.”</li>
<li>Treat infrastructure as a first-class unknown. A repo rarely contains the whole truth. Critical behavior may live in gateways, queues, policies, or provider configuration.</li>
<li>Pull security and operations questions earlier, not later. The model shouldn’t replace security review. It should make sure it happens before anyone commits to a plan.</li>
</ul>
<p>This makes the LLM less magical and more useful. It becomes a force multiplier for engineering judgment.</p>
</section>
<section id="key-takeaway" class="level2">
<h2 class="anchored" data-anchor-id="key-takeaway">Key takeaway</h2>
<p>The best use we’ve found for LLMs in “integrate vs rebuild” decisions is reducing ambiguity before planning begins. If the model can help you produce a reviewable end-to-end flow map, a concrete checklist of hidden security and operational assumptions, and a clear view of where the new request contradicts the current design, you’ve already shortened the riskiest part of the project.</p>
<p>Before your team commits to the next “small” change in a mature system, try letting the LLM read the codebase first. Force it to cite its work. Make humans confirm the map. Then decide whether to integrate, extend, or rebuild.</p>


</section>

 ]]></description>
  <category>LLM</category>
  <category>architecture</category>
  <guid>https://labs.theagilemonkeys.com/posts/how-we-use-llms-to-map-unfamiliar-codebases-before-committing-to-an-estimate/</guid>
  <pubDate>Thu, 19 Mar 2026 00:00:00 GMT</pubDate>
  <media:content url="https://labs.theagilemonkeys.com/posts/how-we-use-llms-to-map-unfamiliar-codebases-before-committing-to-an-estimate/og-image.png" medium="image" type="image/png" height="76" width="144"/>
</item>
<item>
  <title>Reducing LLM Cost and Latency in Production AI Agents: A Case Study</title>
  <dc:creator>Juan Sagasti</dc:creator>
  <link>https://labs.theagilemonkeys.com/posts/the-fastest-ai-agents-send-less-text/</link>
  <description><![CDATA[ 




<p>At <a href="https://www.aifindr.ai/">AI Findr</a>, we help eCommerce brands turn traditional search experiences into assisted shopping journeys and personal shopper-style conversations. Because AI Findr is a SaaS product serving many different businesses, a core capability of the platform is letting each business connect <strong>almost any product-search API</strong> to our agent workflow.</p>
<p>That flexibility accelerated adoption, but it also created a recurring problem in some tenant flows. Different stores returned different payload shapes, many product-search tool responses included far more fields than the model actually needed to understand the user’s intent, and some LLM responses still carried too much UI-oriented structure instead of a minimal rendering contract. As that context piled up, <strong>token spend</strong> and <strong>latency</strong> went up with it.</p>
<p>In one production path, the same query dropped from <strong>34,302 to 10,100 total tokens</strong> and from <strong>28.16s to 6.76s</strong> after we redesigned the workflow around normalized product contracts, minimal output schemas, context whitelisting, and ID-based product hydration.</p>
<p>This post breaks down those architecture decisions and why they improved both unit economics and user experience.</p>
<section id="from-flexible-integrations-to-production-constraints" class="level2">
<h2 class="anchored" data-anchor-id="from-flexible-integrations-to-production-constraints">From Flexible Integrations to Production Constraints</h2>
<p>As a SaaS product, we are not integrating one clean product catalog. We need to support many different businesses, each with its own search API, payload shape, and data quality quirks. Unless those differences are normalized early, integration flexibility turns into token overhead, latency, and frontend fragility.</p>
</section>
<section id="where-latency-was-really-coming-from" class="level2">
<h2 class="anchored" data-anchor-id="where-latency-was-really-coming-from">Where Latency Was Really Coming From</h2>
<p>The bottleneck was not just the model. It was everything we let flow through it.</p>
<p>In some tenant flows, the LLM was getting too much product data, output schemas that were too broad, and response structures that were too close to UI rendering.</p>
<p>The model was doing too much. More context in, more structure out, more latency and cost.</p>
</section>
<section id="the-four-optimizations" class="level2">
<h2 class="anchored" data-anchor-id="the-four-optimizations">The Four Optimizations</h2>
<section id="post-process-raw-search-tool-responses" class="level3">
<h3 class="anchored" data-anchor-id="post-process-raw-search-tool-responses">1. Post-Process Raw Search Tool Responses</h3>
<p>The first problem was the raw search response itself. Large client payloads can quickly flood an agent’s <strong>context window</strong> and make it slow, expensive, or unusable.</p>
<p>We solved that with configurable <strong>Go-template post-processing</strong> per client: clean the raw response, keep only the fields we actually need, and apply the compact <strong>TOON</strong> format before passing anything downstream.</p>
</section>
<section id="keep-structured-output-schemas-minimal" class="level3">
<h3 class="anchored" data-anchor-id="keep-structured-output-schemas-minimal">2. Keep Structured Output Schemas Minimal</h3>
<p>We were already using <strong>Structured Outputs</strong> from the OpenAI SDK, so format reliability was not the main issue. The issue was that one shared output schema had become too large as the product grew.</p>
<p>We fixed that by adding client-configurable output modes and defaulting to the smallest possible schema. In many cases, the model only needs to return <code>text_response</code>, which makes completions cheaper and faster. That matters even more here because <strong>output tokens cost 8x more than uncached input tokens</strong> at the pricing used in this article.</p>
</section>
<section id="send-only-the-field-subset-the-agent-needs" class="level3">
<h3 class="anchored" data-anchor-id="send-only-the-field-subset-the-agent-needs">3. Send Only the Field Subset the Agent Needs</h3>
<p>Even after post-processing, the LLM still did not need the full payload. Fields like image URLs are expensive in tokens and not useful for reasoning.</p>
<p>So we sent only a <strong>whitelisted subset</strong> into model context, while the frontend received the full post-processed payload via <strong>SSE</strong> to hydrate rich product details.</p>
</section>
<section id="replace-a-heavy-ui-dsl-with-a-lightweight-selector-tag" class="level3">
<h3 class="anchored" data-anchor-id="replace-a-heavy-ui-dsl-with-a-lightweight-selector-tag">4. Replace a Heavy UI DSL with a Lightweight Selector Tag</h3>
<p>We used to make the model generate full product card structures. That was too expensive for the LLM and too fragile for the UI.</p>
<p>We replaced that with a much simpler selector tag:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode xml code-with-copy"><code class="sourceCode xml"><span id="cb1-1">&lt;<span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">PS</span>&gt;[id1,id2,id3]&lt;/<span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">PS</span>&gt;</span></code></pre></div></div>
<p>The model now returns only <strong>product IDs</strong>, and the frontend hydrates those cards using product data it already received via SSE. That gives the LLM a much smaller contract to produce and gives the UI a much more reliable one to render.</p>
</section>
</section>
<section id="a-simplified-view-of-the-flow" class="level2">
<h2 class="anchored" data-anchor-id="a-simplified-view-of-the-flow">A Simplified View of the Flow</h2>
<p>After those four changes, the architecture becomes much simpler: the model only sees a compact contract, and the frontend renders from deterministic product memory.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><a href="architecture-flow-overview.svg" class="lightbox" data-gallery="quarto-lightbox-gallery-1" title="Architecture flow from raw payloads to frontend hydration"><img src="https://labs.theagilemonkeys.com/posts/the-fastest-ai-agents-send-less-text/architecture-flow-overview.svg" class="img-fluid quarto-figure quarto-figure-center figure-img" style="width:100.0%" alt="Architecture flow from raw payloads to frontend hydration"></a></p>
</figure>
</div>
<figcaption>Architecture flow from raw payloads to frontend hydration</figcaption>
</figure>
</div>
</section>
<section id="business-impact-and-trade-offs" class="level2">
<h2 class="anchored" data-anchor-id="business-impact-and-trade-offs">Business Impact and Trade-Offs</h2>
<p>The business impact was straightforward: faster answers, lower cost per conversation, and a more consistent product experience. Once the model stopped carrying oversized payloads and rendering-heavy structures, the UX became more responsive and the unit economics became easier to control.</p>
<p>The trade-off is that more responsibility moves into application logic. You need stable product IDs and clear hydration rules across stores. In practice, that is usually a good trade, because deterministic application logic is easier to debug and scale than pushing more structure through the model.</p>
<p>If you only measure three things, measure <strong>input tokens per turn</strong>, <strong>end-to-end latency (especially median and P95)</strong>, and <strong>cost per conversation</strong>.</p>
</section>
<section id="before-vs-after-same-query" class="level2">
<h2 class="anchored" data-anchor-id="before-vs-after-same-query">Before vs After: Same Query</h2>
<p>Using the same query in both versions of the flow:</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Metric</th>
<th>Before</th>
<th>After</th>
<th>Delta</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Input tokens</td>
<td>31,970</td>
<td>9,705</td>
<td>-69.6%</td>
</tr>
<tr class="even">
<td>Output tokens</td>
<td>2,332</td>
<td>395</td>
<td>-83.1%</td>
</tr>
<tr class="odd">
<td>Total tokens</td>
<td>34,302</td>
<td>10,100</td>
<td>-70.6%</td>
</tr>
<tr class="even">
<td>End-to-end latency</td>
<td>28.16s</td>
<td>6.76s</td>
<td>-76.0% (4.17x faster)</td>
</tr>
</tbody>
</table>
<p>This is a <strong>single-query reference point</strong>, but it clearly shows the direction and magnitude of impact.</p>
</section>
<section id="what-this-looks-like-at-300000-queries" class="level2">
<h2 class="anchored" data-anchor-id="what-this-looks-like-at-300000-queries">What This Looks Like at 300,000 Queries</h2>
<p>If this query were representative of <strong>300,000 queries</strong>, the operational impact would be:</p>
<table class="caption-top table">
<colgroup>
<col style="width: 25%">
<col style="width: 25%">
<col style="width: 25%">
<col style="width: 25%">
</colgroup>
<thead>
<tr class="header">
<th>Metric</th>
<th>Before</th>
<th>After</th>
<th>Savings</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Total token volume</td>
<td>10.291B</td>
<td>3.030B</td>
<td>7.261B fewer tokens</td>
</tr>
<tr class="even">
<td>Cumulative user wait time</td>
<td>97.8 days</td>
<td>23.5 days</td>
<td>74.3 days less waiting</td>
</tr>
</tbody>
</table>
<p>And using <strong>GPT-5.1 pricing as of March 4, 2026</strong> (<code>$1.25 / 1M</code> input, <code>$0.125 / 1M</code> cached input, <code>$10 / 1M</code> output), the cost impact would be:</p>
<table class="caption-top table">
<colgroup>
<col style="width: 25%">
<col style="width: 25%">
<col style="width: 25%">
<col style="width: 25%">
</colgroup>
<thead>
<tr class="header">
<th>Scenario</th>
<th>Before</th>
<th>After</th>
<th>Savings</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>No cached input tokens (0%)</td>
<td>$18,984.75 ($0.0633/query)</td>
<td>$4,824.38 ($0.0161/query)</td>
<td>$14,160.38 (-74.6%)</td>
</tr>
<tr class="even">
<td>20% cached input tokens</td>
<td>$16,826.78 ($0.0561/query)</td>
<td>$4,169.29 ($0.0139/query)</td>
<td>$12,657.49 (-75.2%)</td>
</tr>
</tbody>
</table>
<p>This is a <strong>directional estimate</strong>, not a measured production average.</p>
</section>
<section id="final-takeaway" class="level2">
<h2 class="anchored" data-anchor-id="final-takeaway">Final Takeaway</h2>
<p>The less context an agent has to process, the faster and cheaper it will be. That sounds obvious, but it is easy to lose that discipline as a product grows.</p>
<p>More tenants, more integrations, and more product requirements usually mean more fields, broader outputs, and more rendering logic creeping into the model path. Unless those boundaries stay tight, inefficiency becomes the default.</p>
<p>In practice, the biggest gains often come from better contracts, not bigger models. <em>Optimize what goes around the model, not just the prompt inside it.</em></p>
<p>Start by auditing one high-traffic tool-output path and measuring token, latency, and cost deltas before expanding.</p>


</section>

 ]]></description>
  <category>AI agents</category>
  <category>LLM</category>
  <category>production</category>
  <category>architecture</category>
  <guid>https://labs.theagilemonkeys.com/posts/the-fastest-ai-agents-send-less-text/</guid>
  <pubDate>Wed, 04 Mar 2026 00:00:00 GMT</pubDate>
  <media:content url="https://labs.theagilemonkeys.com/posts/the-fastest-ai-agents-send-less-text/og-image.png" medium="image" type="image/png" height="76" width="144"/>
</item>
<item>
  <title>Legacy Modernization with AI: From 229 Vulnerabilities to Zero</title>
  <dc:creator>Atamán Vega Vega at The Agile Monkeys</dc:creator>
  <link>https://labs.theagilemonkeys.com/posts/from-229-vulnerabilities-to-zero-ai-legacy-modernisation/</link>
  <description><![CDATA[ 




<section id="the-starting-point-a-ticking-clock" class="level2">
<h2 class="anchored" data-anchor-id="the-starting-point-a-ticking-clock">The Starting Point: A Ticking Clock</h2>
<p>Every organisation that has been building software for more than a decade carries the same quiet burden: somewhere in production, there is an application that works perfectly well for the business, but is held together by components that the rest of the world has long since moved on from.</p>
<p>Our application — a critical enterprise system serving hundreds of thousands of users — was exactly that. It had been running reliably for years, quietly doing its job. But under the hood, it was running on frameworks and libraries that were not just old: they were <em>exposing the organisation to real security risk.</em></p>
<p>When we ran our first automated security scan using the OWASP Dependency-Check tool, the report came back with <strong>more than 229 reported vulnerabilities</strong>. Seeing that number on the screen is a gut-punch. It feels like a building inspection that has just condemned your house.</p>
<p>The question was: what do we do about it?</p>
<hr>
</section>
<section id="why-you-cant-just-update-everything" class="level2">
<h2 class="anchored" data-anchor-id="why-you-cant-just-update-everything">Why You Can’t Just “Update Everything”</h2>
<p>The instinctive response to a vulnerability report is to update everything to the latest version. Simple, right?</p>
<p>In practice, this is rarely simple — and for a mature enterprise application, it can be nearly impossible without effectively rewriting the system.</p>
<p>Our application had already been partially modernised over the years. It was running <strong>Spring 5</strong> and <strong>Spring Security 5</strong>, with a commercial support contract covering the harder-to-maintain legacy dependencies. That commercial arrangement had kept the system compliant, but it was not a permanent solution.</p>
<p>The application’s entire request-handling architecture, page navigation logic, and form processing were built around Struts. Spring was the backbone of dependency injection, database access, and transaction management — essentially the skeleton of the entire system.</p>
<p>The modern, secure versions of both ecosystems had moved on dramatically:</p>
<ul>
<li><strong>Spring 6</strong> requires the <strong>Jakarta EE</strong> namespace — an industry-wide rename of the <code>javax.*</code> package family to <code>jakarta.*</code> — and had dropped support for Java versions below 17</li>
<li><strong>Spring 6</strong> had <strong>completely removed</strong> its built-in Struts integration module (<code>spring-struts</code>), which existed precisely to wire Spring beans into Struts actions</li>
<li><strong>Struts 1.2.9</strong> was written against the old <code>javax.*</code> namespace and was never updated for the <code>jakarta.*</code> world</li>
</ul>
<p>In plain terms: the two frameworks we depended on had evolved in ways that made them fundamentally <strong>incompatible with each other</strong>. You could upgrade one or the other, but not both — and you needed both.</p>
<p>This is the kind of problem that, in the past, would have meant a multi-year, multi-million dollar “big bang” rewrite. Write everything from scratch in modern technology. Hope nothing gets missed. Pray the business logic gets reproduced faithfully.</p>
<p>We took a different approach.</p>
<hr>
</section>
<section id="bringing-ai-into-the-room" class="level2">
<h2 class="anchored" data-anchor-id="bringing-ai-into-the-room">Bringing AI Into the Room</h2>
<p>We engaged AI as an active participant in the modernisation process — not just as an autocomplete tool, but as a technical collaborator that could hold the entire codebase in context, reason about complex dependency chains, and propose solutions to problems that had no obvious off-the-shelf answer.</p>
<p>The first task we gave it was deceptively simple: <em>understand the problem and tell us what is real</em>.</p>
<hr>
</section>
<section id="the-art-of-the-false-positive" class="level2">
<h2 class="anchored" data-anchor-id="the-art-of-the-false-positive">The Art of the False Positive</h2>
<p>Here is something that security reports almost never tell you clearly: <strong>not every vulnerability listed is actually a vulnerability in your application.</strong></p>
<p>Automated tools work by scanning the names and version numbers of the libraries you use, then matching them against a global database of known vulnerabilities (the NVD — National Vulnerability Database). This matching process is imprecise. The tools are designed to over-report, because missing a real vulnerability is far worse than flagging a false one.</p>
<p>But for a team trying to act on those findings, false positives are a serious problem. They dilute attention, waste engineering time, and — most dangerously — can lead to a kind of learned helplessness where the team starts dismissing reports entirely.</p>
<p>AI was extraordinarily effective at distinguishing real threats from noise. Here are the categories of false positives it identified:</p>
<section id="name-collisions-when-your-code-looks-like-someone-elses" class="level3">
<h3 class="anchored" data-anchor-id="name-collisions-when-your-code-looks-like-someone-elses">1. Name Collisions: When Your Code Looks Like Someone Else’s</h3>
<p>Several of our internal project modules carried names that happened to contain common English words — words that also appeared in the names of completely unrelated third-party products flagged in the vulnerability database. The scanner matched on the word overlap and reported CVEs that had nothing to do with our code.</p>
<p>These were private, internal components that shared nothing — not code, not architecture, not even a category — with the affected products. AI identified this pattern immediately, articulated precisely why each match was incorrect, and generated the suppression rules needed to eliminate the noise.</p>
</section>
<section id="struts-2.x-cves-applied-to-struts-1.x" class="level3">
<h3 class="anchored" data-anchor-id="struts-2.x-cves-applied-to-struts-1.x">2. Struts 2.x CVEs Applied to Struts 1.x</h3>
<p>This was one of the largest categories of false positives — and one of the most technically subtle.</p>
<p>Apache Struts has two completely separate product lines: <strong>Struts 1.x</strong> (our framework) and <strong>Struts 2.x</strong> (a completely different framework, built on different architecture, with different security characteristics). The two share a name and a brand, but they are not the same software.</p>
<p>Struts 2.x has been the source of some of the most serious enterprise security vulnerabilities of the past decade — including the infamous <strong>CVE-2017-5638</strong>, which was used in the Equifax breach. The attack vectors in Struts 2.x exploits rely on <strong>OGNL (Object-Graph Navigation Language)</strong>, a powerful expression evaluation engine that is a core part of Struts 2’s architecture.</p>
<p><strong>Struts 1.x does not use OGNL.</strong> It never did.</p>
<p>Yet the vulnerability scanner was flagging our Struts 1.2.9 installation with CVEs like <strong>CVE-2024-53677</strong>, <strong>CVE-2023-50164</strong>, <strong>CVE-2023-34149</strong>, and <strong>CVE-2025-25015</strong> — all of which are Struts 2.x OGNL injection or file upload vulnerabilities that have zero technical relevance to Struts 1.x.</p>
<p>AI was able to understand this architectural distinction, explain it clearly, and document exactly why each of these CVEs was inapplicable. This wasn’t just a cleanup exercise — it was a <em>genuine security analysis</em> that required understanding how both frameworks work internally.</p>
</section>
<section id="vulnerabilities-already-fixed-in-our-version" class="level3">
<h3 class="anchored" data-anchor-id="vulnerabilities-already-fixed-in-our-version">3. Vulnerabilities Already Fixed in Our Version</h3>
<p>The vulnerability scanner was also flagging CVEs against several libraries we use — specifically, vulnerabilities that had been patched and fixed many versions before the version we were actually running.</p>
<p>In more than one case, a CVE that was fixed in an older release was still being reported against our significantly newer version, because the scanner’s CPE (Common Platform Enumeration) matching was broad enough to catch our library by name regardless of version. Our code was never affected.</p>
<p>AI identified the pattern, cross-referenced each CVE advisory against the library changelogs to confirm the fix version, and produced the appropriate suppressions with a clear, auditable justification.</p>
</section>
<section id="mitigated-vulnerabilities-context-is-everything" class="level3">
<h3 class="anchored" data-anchor-id="mitigated-vulnerabilities-context-is-everything">4. Mitigated Vulnerabilities: Context Is Everything</h3>
<p>Some vulnerabilities are real — but the way your application uses the affected library means the attack vector simply cannot be reached.</p>
<p>Our application uses a third-party library for PDF generation. There are known vulnerabilities in the version we use — related to infinite loops and null pointer exceptions when processing <em>malicious, attacker-crafted PDF files</em>.</p>
<p>The key word is <em>processing</em>. Our application <strong>generates</strong> PDFs from trusted server-side data. It does not accept PDFs from users and parse them through iText. The attack vector requires an attacker to supply a malicious PDF for the application to read — which never happens in our workflow.</p>
<p>AI understood this distinction, documented the mitigation context, and correctly classified these as acceptable residual risks rather than actionable vulnerabilities.</p>
<hr>
</section>
</section>
<section id="a-structured-attack-triage-before-tools" class="level2">
<h2 class="anchored" data-anchor-id="a-structured-attack-triage-before-tools">A Structured Attack: Triage Before Tools</h2>
<p>With the false positives cleared away, the team had a much cleaner picture of the real work ahead. But even after stripping out the noise, a meaningful backlog of genuine issues remained — and attempting to fix everything at once is a reliable path to paralysis.</p>
<p>The next step was to <strong>classify the remaining vulnerabilities into three tiers</strong>, based on the complexity of the fix required:</p>
<p><strong>Easy:</strong> Simple version bumps. A newer release of the same library already contained the fix, the update was backwards-compatible, and changing a version number was all that was needed. These could be resolved quickly and in bulk.</p>
<p><strong>Medium:</strong> Updates that required care. The fix existed, but the newer version involved API changes, configuration adjustments, or testing to confirm compatibility with the rest of the stack. These needed individual attention but had clear, documented upgrade paths.</p>
<p><strong>Difficult:</strong> Cases where no straightforward fix existed — where upgrading one component would break another, or where the required version was fundamentally incompatible with a core framework already in the application.</p>
<p>This tiered approach transformed the work from a single overwhelming task into a structured campaign. The team started with the easy wins, clearing a large portion of the backlog quickly and building momentum. Medium-complexity fixes followed, each one narrowing the gap further. By the time the team arrived at the genuinely difficult cases, the list was short, the context was clear, and the focus was sharp.</p>
<p>The hardest case — the one that required the most creative engineering — was the incompatibility between Spring 6 and Struts 1.x.</p>
<hr>
</section>
<section id="the-incompatibility-problem-building-a-bridge" class="level2">
<h2 class="anchored" data-anchor-id="the-incompatibility-problem-building-a-bridge">The Incompatibility Problem: Building a Bridge</h2>
<p>After eliminating the false positives, a core set of <em>genuine</em> modernisation challenges remained. The biggest: how do you upgrade to Spring 6 when your entire UI layer depends on a framework (Struts 1.x) that Spring 6 refuses to support?</p>
<p>This is where AI moved from analysis into <em>invention</em>.</p>
<section id="the-problem-in-plain-language" class="level3">
<h3 class="anchored" data-anchor-id="the-problem-in-plain-language">The Problem in Plain Language</h3>
<p>Think of it like a building with two systems that need to talk to each other: the plumbing (Spring) and the electrical wiring (Struts). You’re upgrading the plumbing to a completely new standard. But the new plumbing standard’s connectors are a different shape — and the electrical system was designed for the old connectors, and its manufacturer stopped making it decades ago.</p>
<p>You have three options:</p>
<ol type="1">
<li>Replace both systems simultaneously (the “big bang” rewrite — high risk, very expensive)</li>
<li>Don’t upgrade (accept the security debt — not acceptable)</li>
<li><strong>Build an adapter</strong> that speaks both languages</li>
</ol>
<p>AI designed and implemented <strong>Option 3</strong>.</p>
</section>
<section id="the-compatibility-layer" class="level3">
<h3 class="anchored" data-anchor-id="the-compatibility-layer">The Compatibility Layer</h3>
<p>The solution was a purpose-built compatibility package — a set of classes that sit between the old Struts world and the new Spring/Jakarta world, translating between them at runtime.</p>
<p>The technical challenge has two dimensions:</p>
<p><strong>Dimension 1: The namespace divide.</strong> Spring 6 (and modern Tomcat) uses the <code>jakarta.*</code> package namespace. Struts 1.2.9 was written against the <code>javax.*</code> namespace. These represent the same concepts under different names — but Java treats them as entirely different types. An object of type <code>javax.*</code> is simply not the same as its <code>jakarta.*</code> counterpart at runtime, even though they describe identical things.</p>
<p><strong>Dimension 2: The missing bridge.</strong> Spring used to ship a dedicated integration module (<code>spring-struts</code>) that handled the wiring between Spring-managed beans and Struts actions. Spring 6 dropped it entirely. There was no replacement. The bridge simply didn’t exist anymore.</p>
<p>The compatibility layer AI built solves both problems:</p>
<ul>
<li>A new family of classes mirroring the Struts action interfaces was created — written against the <strong>Jakarta</strong> namespace, matching the signatures of the Struts originals as closely as possible</li>
<li>A custom proxy component was written to replace the removed Spring integration. This proxy acts as the connective tissue: Struts invokes it using the old <code>javax.*</code> types, it looks up the correct Spring-managed action bean, translates the request and response objects across the namespace boundary, executes the action, and translates the result back</li>
</ul>
<p>The beauty of this approach is that the existing application code — hundreds of Action classes, thousands of lines of business logic — required only <strong>import statement changes</strong>. The logic itself was untouched. The compat layer absorbed all the complexity so the application code didn’t have to.</p>
<p>This is what a well-designed compatibility layer should do: make the hard thing invisible.</p>
<hr>
</section>
</section>
<section id="the-result-229-vulnerabilities-0" class="level2">
<h2 class="anchored" data-anchor-id="the-result-229-vulnerabilities-0">The Result: 229+ Vulnerabilities → 0</h2>
<p>After completing the modernisation:</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th></th>
<th>Before</th>
<th>After</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>Reported vulnerabilities</strong></td>
<td>229+</td>
<td><strong>0</strong></td>
</tr>
<tr class="even">
<td><strong>Spring Framework version</strong></td>
<td>5.x</td>
<td><strong>6.x</strong></td>
</tr>
<tr class="odd">
<td><strong>Spring Security version</strong></td>
<td>5.x</td>
<td><strong>6.x</strong></td>
</tr>
<tr class="even">
<td><strong>Java version</strong></td>
<td>Java 17</td>
<td><strong>Java 21</strong></td>
</tr>
<tr class="odd">
<td><strong>Servlet namespace</strong></td>
<td><code>javax.*</code></td>
<td><strong><code>jakarta.*</code></strong></td>
</tr>
<tr class="even">
<td><strong>Struts integration</strong></td>
<td>Removed in Spring 6</td>
<td>Custom compat layer</td>
</tr>
<tr class="odd">
<td><strong>False positives documented</strong></td>
<td>0</td>
<td>30+ (with full audit trail)</td>
</tr>
<tr class="even">
<td><strong>Business logic rewritten</strong></td>
<td>—</td>
<td><strong>Zero lines</strong></td>
</tr>
</tbody>
</table>
<p>The last row is the one that matters most to the business. The application behaves identically to users. No features were removed. No business logic was altered. The security posture was transformed without any of the risk that a rewrite would have introduced.</p>
<hr>
</section>
<section id="what-this-tells-us-about-ai-assisted-modernisation" class="level2">
<h2 class="anchored" data-anchor-id="what-this-tells-us-about-ai-assisted-modernisation">What This Tells Us About AI-Assisted Modernisation</h2>
<p>This project illustrates something important about where AI adds genuine value in software engineering — and it isn’t just about writing code faster.</p>
<p><strong>The hardest problems in legacy modernisation are not coding problems.</strong> They are <em>reasoning</em> problems:</p>
<ul>
<li>Is this vulnerability real, or is it a false match?</li>
<li>Does this CVE apply to the way we actually use this library?</li>
<li>Is there a way to satisfy two incompatible requirements simultaneously?</li>
<li>What is the minimum change that achieves the security goal without breaking the application?</li>
</ul>
<p>These questions require holding a large amount of context simultaneously, understanding the semantics of security advisories, and designing solutions that work within tight constraints. They are exactly the kind of problems where AI currently provides the most leverage.</p>
<p>The compat layer is a particularly good example. A human engineer could have designed it — but it would have required deep expertise in three different technology stacks (Struts internals, Spring internals, and the <code>javax</code>→<code>jakarta</code> migration) <em>and</em> the creative insight to see that a thin translation layer was the right architecture. AI could draw on all of that knowledge simultaneously and propose a coherent design quickly.</p>
<p><strong>False positive analysis</strong> is another. Working through 229+ reported vulnerabilities one by one, understanding the semantics of each CVE, matching it against the actual usage patterns in the codebase, and writing precise suppression rules with documented justifications — this is tedious, high-skill work. AI can do it faster and more consistently than a human team under time pressure.</p>
<hr>
</section>
<section id="the-takeaway" class="level2">
<h2 class="anchored" data-anchor-id="the-takeaway">The Takeaway</h2>
<p>Legacy modernisation projects have a reputation for being long, expensive, and risky. That reputation is largely deserved — when they are approached as “big bang” rewrites.</p>
<p>A better approach is surgical: understand precisely what needs to change, distinguish real risk from noise, build bridges where direct replacement is impossible, and preserve everything that works.</p>
<p>AI doesn’t replace the engineers who understand the system. But it amplifies what they can do — turning a project that might have taken quarters into one that takes weeks, and turning a vulnerability report that looked like a condemned building into a system that is genuinely, verifiably secure.</p>
<p><strong>Zero vulnerabilities. Zero lines of business logic rewritten. One very large problem, solved.</strong></p>
<hr>
<p><em>Written February 2026.</em></p>


</section>

 ]]></description>
  <category>legacy modernization</category>
  <category>LLM</category>
  <guid>https://labs.theagilemonkeys.com/posts/from-229-vulnerabilities-to-zero-ai-legacy-modernisation/</guid>
  <pubDate>Mon, 02 Mar 2026 00:00:00 GMT</pubDate>
  <media:content url="https://labs.theagilemonkeys.com/posts/from-229-vulnerabilities-to-zero-ai-legacy-modernisation/og-image.png" medium="image" type="image/png" height="76" width="144"/>
</item>
<item>
  <title>How to Build Products Out of AI Agents (Instead of Adding AI to Them)</title>
  <link>https://labs.theagilemonkeys.com/posts/stop-using-ai-in-your-product-start-building-your-product-out-of-agents/</link>
  <description><![CDATA[ 




<p>Most product builders are still thinking about AI the old way: <strong>use AI to help you build features</strong> (generate code, write copy, speed up support). That is useful, but it is not the real shift.</p>
<p>The shift is this:</p>
<p><strong>Build your product features by compounding AI agents.</strong> Not “AI inside a feature,” but “features made of agents.”</p>
<p>When you do this properly, you stop treating AI as a helper and start treating it as a <strong>core system primitive</strong>, like a database, a queue, or an API layer. The result is simple: <strong>more value shipped faster</strong>, and often with a step-change in what your product can do.</p>
<p>And here is the reality: it is hard to appreciate the leverage until you try it <em>the right way</em>. When you integrate agents correctly, the power becomes obvious.</p>
<p>So what does “the right way” actually mean?</p>
<hr>
<section id="the-new-feature-building-paradigm" class="level2">
<h2 class="anchored" data-anchor-id="the-new-feature-building-paradigm">The New Feature-Building Paradigm</h2>
<p>In the agent-native mindset, every new feature begins with a different set of questions:</p>
<ul>
<li><strong>What tools do I need to provide so that an agent can do this?</strong></li>
<li><strong>What should the agent reason about vs.&nbsp;what must be deterministic and controlled via tools?</strong></li>
<li><strong>What prompt (instructions + constraints) will consistently guide it to the right behavior?</strong></li>
<li><strong>How do I evaluate it to verify it does what it is supposed to do?</strong></li>
</ul>
<p>This changes how you design software.</p>
<p>Instead of writing rigid flows for every edge case, you build <strong>capable agents with</strong> <strong>a set of tools</strong>, <strong>a prompt contract</strong>, <strong>an evaluation harness</strong>, and <strong>a loop for autonomy and improvement</strong>. That is the new stack.</p>
<hr>
</section>
<section id="tools-are-the-product-surface-area-for-agents" class="level2">
<h2 class="anchored" data-anchor-id="tools-are-the-product-surface-area-for-agents">1) Tools Are the Product Surface Area for Agents</h2>
<p>If agents are going to run your features, they need two things: the ability to <strong>get the right information</strong>, and the ability to <strong>act on your system</strong> safely.</p>
<p>That is what tools are: <strong>controlled interfaces to your product.</strong></p>
<section id="the-core-principle" class="level3">
<h3 class="anchored" data-anchor-id="the-core-principle">The Core Principle</h3>
<p>Agent gives you <strong>flexibility and adaptability</strong>. Tools give you <strong>determinism, safety, and control</strong>.</p>
<p>If you want reliable agent behavior, you do not “hope the model will do the right thing.” You give it tools that <em>make the right thing the easiest thing</em>.</p>
</section>
<section id="practical-tips-for-tools" class="level3">
<h3 class="anchored" data-anchor-id="practical-tips-for-tools">Practical Tips for Tools</h3>
<ul>
<li><strong>Make tools small and composable.</strong> A tool should do one thing well (e.g., <code>get_user_profile</code>, <code>create_invoice</code>, <code>refund_payment</code>) rather than being a giant “do everything” endpoint.</li>
<li><strong>Design tools as contracts.</strong> Strong schemas, clear input validation, meaningful error messages, and idempotency where needed.</li>
<li><strong>Separate “read tools” and “write tools.”</strong> Reading is usually safe; writing should be gated, logged, and permissioned.</li>
<li><strong>Build guardrails into tools, not prompts.</strong> If something must never happen (e.g., deleting customer data), enforce it in the tool layer.</li>
<li><strong>Return structured outputs.</strong> Tools should return data the agent can reason over. JSON-like structures are better than free text.</li>
</ul>
<p>If tools are weak, the agent becomes either dangerously unconstrained or constantly blocked and useless.</p>
<p>Your tools are how the agent becomes a real product component.</p>
<hr>
</section>
</section>
<section id="prompting-still-matters-even-more-than-before" class="level2">
<h2 class="anchored" data-anchor-id="prompting-still-matters-even-more-than-before">2) Prompting Still Matters: Even More Than Before</h2>
<p>Yes, prompting is still key. Arguably <strong>more</strong> key now.</p>
<p>As models get better at following instructions, the prompt becomes less “a suggestion” and more like <strong>a specification</strong>. It determines whether the behavior is mediocre and inconsistent or robust and repeatable.</p>
<section id="practical-tips-for-prompts" class="level3">
<h3 class="anchored" data-anchor-id="practical-tips-for-prompts">Practical Tips for Prompts</h3>
<p>There are tons of guides for this on the internet, but here are a few important tips related to agents and tools:</p>
<ul>
<li><strong>Write prompts as operating manuals.</strong> Role, goals, constraints, what “good” looks like, what “bad” looks like.</li>
<li><strong>Define decision rules.</strong> Example: “If confidence &lt; X, ask for clarification. If tool returns error Y, retry once, then escalate.”</li>
<li><strong>Force structure.</strong> Define a clear output format, separate decision and execution steps, and go for a tool-first behavior.</li>
<li><strong>Make the tool policy explicit.</strong> When to call tools, when not to, how to handle failures.</li>
<li><strong>Include real examples.</strong> Few-shot examples for edge cases can dramatically stabilize behavior.</li>
</ul>
<hr>
</section>
</section>
<section id="evaluation-is-the-backbone-and-it-must-be-integrated-into-the-product" class="level2">
<h2 class="anchored" data-anchor-id="evaluation-is-the-backbone-and-it-must-be-integrated-into-the-product">3) Evaluation Is the Backbone, and It Must Be Integrated into the Product</h2>
<p>This is the part most teams under-invest in, and it is <strong>the reason many “agent features” remain demos</strong>.</p>
<p><strong>Evaluation is not a separate phase. It is part of the system.</strong> If agents are performing product work, then evaluations must be automated, continuously running, deeply integrated, and ideally improved with agents themselves.</p>
<section id="what-deeply-integrated-evaluation-means" class="level3">
<h3 class="anchored" data-anchor-id="what-deeply-integrated-evaluation-means">What “Deeply Integrated Evaluation” Means</h3>
<p>Deeply integrated evaluation means every major agent workflow ships with a <strong>test harness</strong> that captures real traces (including <strong>tool calls</strong> and <strong>outcomes</strong>) and turns them into <strong>regression tests</strong> you can run whenever you change prompts, tools, or models. It also means you measure what actually matters in production, like <strong>quality</strong>, <strong>safety</strong>, <strong>latency</strong>, <strong>cost</strong>, and <strong>user satisfaction</strong>, and you have clear failure routing (retry, fallback, escalation, or human review) instead of hand-wavy “it looked fine.”</p>
<p>Just as important, the evaluation layer needs access to the same underlying data the agent had when it produced an answer, so an evaluator can verify the response is <strong>grounded</strong> rather than merely plausible. A great way to do this is to leverage <strong>agents as evaluators</strong>, but keep them tightly scoped: give them clear, narrow instructions and dedicated verification tools to check cited sources, cross-reference the answer against your ingested data, and flag anything unsupported or inconsistent.</p>
</section>
<section id="practical-tips-for-evaluation" class="level3">
<h3 class="anchored" data-anchor-id="practical-tips-for-evaluation">Practical Tips for Evaluation</h3>
<ul>
<li><strong>Evaluate in production, both manually and automatically.</strong> Support a manual “evaluate this run” trigger for any conversation or workflow, and continuously sample live runs as agents work (random plus risk-based sampling) to run evaluations in the background.</li>
<li><strong>Turn failures into a regression dataset.</strong> Any run that fails evaluation should be saved, together with the exact context the agent had (retrieved chunks, tool outputs, inputs, actions), into an evaluation dataset that you can replay later as <strong>regression evaluations</strong>.</li>
<li><strong>Close the loop with evaluator-driven change proposals (human-approved).</strong> Give the evaluator dedicated tools to propose concrete fixes, such as prompt edits, improved retrieval settings, or ingestion/data corrections, and store those proposals in a review queue for a human to approve. Once approved, re-run the regression suite to confirm the change actually makes the failing cases pass.</li>
</ul>
<p>Evaluation is what turns agents from “cool” into “production.”</p>
<hr>
</section>
</section>
<section id="toward-autonomy-less-human-in-the-loop-by-design" class="level2">
<h2 class="anchored" data-anchor-id="toward-autonomy-less-human-in-the-loop-by-design">Toward Autonomy: Less “Human in the Loop” by Design</h2>
<p>The goal is not to guide agents constantly. The goal is to make them <strong>autonomous</strong>: self-validate what they do, self-correct when something goes wrong, self-test changes, and automatically deploy or publish when confidence is high.</p>
<p>In other words, <strong>keep humans in the loop only where it matters</strong>: reserve manual review for critical or high-risk actions, and let the rest run automatically. That is how agent-native products evolve faster, scale further, and deliver more value as autonomy increases.</p>
<p>Autonomy does not come from a bigger model. It comes from the triad:</p>
<p><em>Tools + prompting + integrated evaluation</em></p>
<p><strong>That is the formula.</strong></p>
<hr>
</section>
<section id="the-real-takeaway" class="level2">
<h2 class="anchored" data-anchor-id="the-real-takeaway">The Real Takeaway</h2>
<p>If you are still thinking “How can AI help me build my product faster?” you are already behind.</p>
<p>The new question is:</p>
<p><strong>How can I build my product with agents as first-class components, designed for autonomy, determinism, and continuous evaluation?</strong></p>
<p>You will only really understand the leverage once you build at least one feature this way end-to-end.</p>
<p>And once you do, it becomes hard to go back.</p>


</section>

 ]]></description>
  <category>AI agents</category>
  <category>AI strategy</category>
  <category>evaluation</category>
  <guid>https://labs.theagilemonkeys.com/posts/stop-using-ai-in-your-product-start-building-your-product-out-of-agents/</guid>
  <pubDate>Thu, 26 Feb 2026 00:00:00 GMT</pubDate>
  <media:content url="https://labs.theagilemonkeys.com/posts/stop-using-ai-in-your-product-start-building-your-product-out-of-agents/og-image.png" medium="image" type="image/png" height="76" width="144"/>
</item>
<item>
  <title>Vector Search for Fashion E-commerce: Custom Embedding Lens Without Fine-Tuning</title>
  <dc:creator>Melina Hernández and David Godoy at The Agile Monkeys</dc:creator>
  <link>https://labs.theagilemonkeys.com/posts/summer-embedding-lens-fashion-ecommerce/</link>
  <description><![CDATA[ 




<p>Imagine if your product-search engine could sense the change of seasons the same way a shopper does, or know about memorable moments, instantly swapping wool coats for sundresses the moment summer hits, or spotlighting perfect gift ideas in the run-up to Mother’s Day. That’s the promise of Embedding Lens, a plug-and-play “spectacles” layer that slips over any static embedding model and teaches it new tricks without costly reprocessing.</p>
<section id="what-static-re-indexing-cant-fix" class="level2">
<h2 class="anchored" data-anchor-id="what-static-re-indexing-cant-fix">What Static Re-indexing Can’t Fix 🎯</h2>
<p>The typical search pipeline already reindexes every new or edited SKU overnight, so the catalog itself is always up to date. What <em>isn’t</em> fresh are the <strong>context signals,</strong> those short-lived moments when customers want a different flavour of “relevance” without any change in product data.</p>
<table class="caption-top table">
<colgroup>
<col style="width: 25%">
<col style="width: 25%">
<col style="width: 25%">
<col style="width: 25%">
</colgroup>
<thead>
<tr class="header">
<th>Context moment</th>
<th>User query</th>
<th>User wish</th>
<th>Why the encoder can’t keep up</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>Early summer</strong></td>
<td>bag</td>
<td>Raffia, canvas, brights</td>
<td>Embeddings learned on season-agnostic data.</td>
</tr>
<tr class="even">
<td><strong>Christmas gifts</strong></td>
<td>mini bag</td>
<td>Sparkly, party-ready</td>
<td>Model still ranks everyday leather first.</td>
</tr>
<tr class="odd">
<td><strong>Mother’s Day</strong></td>
<td>scarf</td>
<td>Floral, gift-boxed</td>
<td>Needs a temporary <em>giftable</em> bias.</td>
</tr>
</tbody>
</table>
<p>Re-training or re-ranking for <strong>every</strong> event is overkill, slow, GPU-hungry, and risk-heavy in prod.</p>
<p>We need a <strong>surgical dial</strong> that we can turn on/off per query, per segment, or per campaign without touching product information (description or embeddings).</p>
</section>
<section id="our-angle-the-embedding-lens" class="level2">
<h2 class="anchored" data-anchor-id="our-angle-the-embedding-lens">Our Angle: the <em>Embedding Lens</em> 👓</h2>
<section id="whats-an-embedding-lens" class="level3">
<h3 class="anchored" data-anchor-id="whats-an-embedding-lens">What’s an Embedding Lens?</h3>
<p>A tiny linear map T we multiply with the <strong>query embedding</strong> right before the nearest-neighbor lookup:</p>
<p><img src="https://latex.codecogs.com/png.latex?%5Ctilde%7Bq%7D%20=%20%5Ctext%7Bnorm%7D((I%20+%20UV%5E%7B%5Ctop%7D)%20q);%20%5Cquad%20U,%20V%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bd%20%5Ctimes%20r%7D,%20%5C,%20r%20%5Cll%20d."></p>
<ul>
<li>In plain English: <em>“Nudge the query toward specific criteria in the embedding space.”</em></li>
<li>Zero changes to product information, zero re-indexing.</li>
</ul>
<p><strong>Label once, Use Forever:</strong> We only need to label a product set to train the lens. Once the lens understands the target concept, it can be applied to new products without requiring modifications to the catalog.</p>
</section>
<section id="why-start-with-a-summer-lens" class="level3">
<h3 class="anchored" data-anchor-id="why-start-with-a-summer-lens">Why start with a <em>Summer</em> lens?</h3>
<ul>
<li><strong>Seasonality is an easy win</strong> - When the weather changes, so does what “relevant” looks like; stakeholders feel the impact immediately.</li>
<li><strong>Seasonality is complex enough</strong> - To determine a product is suitable for summer requires taking into account many aspects of the product not just a linear relation to a particular characteristic.</li>
<li><strong>Instant visual sanity-check</strong> - “raffia tote &gt; leather tote” is obvious at first glance, letting us iterate quickly before tackling subtler segments.</li>
<li><strong>Perfect sandbox for the bigger goal</strong> - If a single lens can nudge search toward beach-ready styles, the same trick can later target <em>gifting</em>, <em>vegan</em>, or <em>city-specific</em> intent without adding any additional information to products.</li>
</ul>
</section>
</section>
<section id="how-we-built-it" class="level2">
<h2 class="anchored" data-anchor-id="how-we-built-it">How We Built It 🔧</h2>
<section id="mind-set-start-small-validate-early-and-then-scale" class="level3">
<h3 class="anchored" data-anchor-id="mind-set-start-small-validate-early-and-then-scale">Mind-set: Start small, validate early and then scale</h3>
<section id="idea-spark" class="level4">
<h4 class="anchored" data-anchor-id="idea-spark"><strong>Idea spark</strong></h4>
<p>The initial thought was modest: “What if we steer only the search vector instead of updating every product embedding?” A quick literature sweep (adapters, prompt-tuning, linear query shifts) confirmed the maths was already out there, waiting to be applied.</p>
</section>
<section id="the-shortest-way-to-validate-the-red-lens" class="level4">
<h4 class="anchored" data-anchor-id="the-shortest-way-to-validate-the-red-lens"><strong>The shortest way to validate (The “Red” Lens)</strong></h4>
<p>To test the waters, we took a frozen snapshot of the full catalogue and trained a lens on roughly 300 triplets (red versus non-red), small enough to run on a laptop’s CPU. A handful of sample queries (“bag”, “sneakers”, “dress”) instantly floated scarlet items to the top 3: plenty of signal, zero re-indexing.</p>
</section>
<section id="testing-the-summer-waters" class="level4">
<h4 class="anchored" data-anchor-id="testing-the-summer-waters"><strong>Testing the “summer” waters</strong></h4>
<p>When the business team identified seasonality as a significant challenge, we revised the pipeline. We replaced the previous red-not-red dataset with a <strong>summer label</strong> generated using a large language model (LLM) and rebuilt the triplet file based on the current catalog. <strong>All other elements, including the data format and training code, stayed the same.</strong></p>
</section>
<section id="understanding-summer-vibes-lab-scale-validation" class="level4">
<h4 class="anchored" data-anchor-id="understanding-summer-vibes-lab-scale-validation"><strong>Understanding Summer Vibes (Lab-scale validation)</strong></h4>
<p>After validating our hypothesis with a small model, we aimed to scale up and determine if it could be applied to a production-grade system. To achieve this, we created an equivalent lab environment suited for testing and validation.</p>
<p>Utilizing the complete product set and real query logs, we transitioned to using a single rented GPU (from Runpod) and trained an image-and-text-based summer lens model. We then measured performance using <strong>Precision@k, Summer-Precision@k, and latency</strong> metrics (in W+B).</p>
<p>Over several iterative cycles, spread across spare evenings and quiet mornings, we tuned hyperparameters until the semantic gains were consistent and the added latency stayed within acceptable limits for production.</p>
</section>
</section>
<section id="the-recipe-crafting-a-consistent-training-routine-for-a-beach-ready-model" class="level3">
<h3 class="anchored" data-anchor-id="the-recipe-crafting-a-consistent-training-routine-for-a-beach-ready-model">The Recipe: Crafting a Consistent Training Routine for a Beach-Ready Model</h3>
<section id="data-preparation" class="level4">
<h4 class="anchored" data-anchor-id="data-preparation"><strong>Data preparation</strong></h4>
<p>Before an AI model can effectively adapt embeddings for semantic search, it needs a well-structured and informative training dataset. In this phase, we assemble aligned pairs of user queries and products, along with their respective vector embeddings and a supervision signal that guides the learning process.</p>
<p>Our target schema captures the key components for this adaptation process:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode json code-with-copy"><code class="sourceCode json"><span id="cb1-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb1-2">  <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"query"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"search text query"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb1-3">  <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"query_embedding"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"embedding representation of the query"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb1-4">  <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"product_id"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"unique identifier of the product"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb1-5">  <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"product_embedding"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"embedding representation of the product"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb1-6">  <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"len_score"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"adjustment parameter indicating the desired similarity level"</span></span>
<span id="cb1-7"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">}</span></span></code></pre></div></div>
<p>Each row represents a training example linking a natural language query to a candidate product. The <code>len_score</code> field encodes how closely the embeddings should align in semantic space, enabling the model to learn a fine-tuned transformation that reflects domain-specific relevance.</p>
<p>Let’s break down the different components and illustrate how the data is collected and transformed to fit the target schema.</p>
<section id="queries" class="level5 underlineHeader">
<h5 class="underlineHeader anchored" data-anchor-id="queries"><em>1. Queries</em></h5>
<p>The <code>query</code> field is sourced from real user behavior in production analytics. We focus on the most commonly used and semantically rich queries issued by users in the existing system. These high-frequency queries help the model learn patterns that generalize well to real-world search behavior.</p>
</section>
<section id="products" class="level5 underlineHeader">
<h5 class="underlineHeader anchored" data-anchor-id="products"><em>2. Products</em></h5>
<p>Each <code>product_id</code> maps to a product entry retrieved from the production database. We ensure the product metadata, including titles, descriptions, and image references, is up to date and consistent with what users see in the current search experience.</p>
</section>
<section id="embeddings" class="level5 underlineHeader">
<h5 class="underlineHeader anchored" data-anchor-id="embeddings"><em>3. Embeddings</em></h5>
<p>The <code>query_embedding</code> and <code>product_embedding</code> fields are generated using the same embedding strategy deployed in production to maintain consistency within the semantic space.</p>
<ul>
<li><strong>Product Embedding</strong> = <code>CLIP LAION</code> (on product images) + <code>text-embedding-3-large</code> (on product description)</li>
<li><strong>Query Embedding</strong> = <code>CLIP LAION</code> (on query text) + <code>text-embedding-3-large</code> (on query text)</li>
</ul>
<p>By concatenating multimodal embeddings from both vision (CLIP) and text (OpenAI), we create a joint semantic space that captures visual style and textual meaning, essential for fashion and lifestyle domains where style matters as much as substance.</p>
</section>
<section id="len-score" class="level5 underlineHeader">
<h5 class="underlineHeader anchored" data-anchor-id="len-score"><em>4. Len Score</em></h5>
<p>The <code>len_score</code> is a synthetic supervision signal indicating how semantically close a product should be to the query in the adapted embedding space. It is generated using a custom large language model (LLM) classifier trained to recognize alignment with seasonal style cues (e.g., “summer vibes”, “beachwear”, “light fabrics”).</p>
<p>To scale this process efficiently, we use <strong>OpenAI Batches</strong>, allowing us to label thousands of products at once. Then, we rank each product based on its summer score and semantic similarity to each query. This enables the creation of a rich, style-aware training dataset without manual labeling bottlenecks.</p>
<div class="callout callout-style-default callout-tip callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
<span class="screen-reader-only">Tip</span>Protecting Semantic Integrity
</div>
</div>
<div class="callout-body-container callout-body">
<p>While adapting embeddings for nuanced relevance, <strong>it’s critical not to break the core semantic integrity of the search space</strong>. For example, a high-scoring summer “bikini swimsuit” should never appear at the top of results for a query like “bags”, no matter how stylistically aligned.</p>
<p>To safeguard against this, we introduced a <strong>similarity gate</strong>: A function that, if a product is fundamentally unsuitable for a given query category, the <code>len_score</code> is forcibly set to 0, turning that pair into a <strong>contrastive negative</strong> during training. This ensures that adaptation enhances style relevance without sacrificing semantic precision.</p>
</div>
</div>
</section>
<section id="final-dataset" class="level5 underlineHeader">
<h5 class="underlineHeader anchored" data-anchor-id="final-dataset"><em>Final dataset</em></h5>
<p>To support training and evaluation at scale, we compiled a robust and diverse dataset using a combination of production analytics, embedding-based filtering, and random sampling.</p>
<div class="shortHeader">
<p><strong>Queries:</strong></p>
<ul>
<li><strong>1,300 high-frequency search queries</strong>, extracted from real user behavior.</li>
</ul>
</div>
<div class="shortHeader">
<p><strong>Products per Query:</strong></p>
<ul>
<li><strong>500 top-ranked products</strong> based on <strong>baseline cosine similarity</strong> using the current (unadapted) embedding space.</li>
<li><strong>500 randomly selected products</strong> from the remainder of the 16,000-product catalog to introduce semantic diversity and harder negatives.</li>
</ul>
</div>
<p><strong>Total: ~1.3 million query-product pairs</strong></p>
<div class="shortHeader">
<p><strong>Data Split:</strong></p>
<ul>
<li><strong>60% Training Set</strong> – used to fit the transformation model.</li>
<li><strong>40% Evaluation Set</strong> – used to measure generalization and assess model performance against held-out queries and products.</li>
</ul>
</div>
<p>By combining both relevant and irrelevant candidates per query, this helps the model learn fine-grained distinctions in style relevance.</p>
<p>This structured and automated data preparation pipeline ensures that our adaptation model is trained on relevant, high-quality signals that are grounded in production semantics and enhanced with domain-specific relevance cues. In the next section, we will explore how this dataset is utilized to train a lightweight projection module, which subtly adjusts embeddings to better align with user expectations in seasonal and style-driven contexts.</p>
</section>
</section>
<section id="training-pipeline" class="level4">
<h4 class="anchored" data-anchor-id="training-pipeline"><strong>Training pipeline</strong></h4>
<p>With our data prepped and aligned, the next step is training the model to learn a fine-grained transformation that shifts query embeddings just enough to reflect user intent and seasonal trends—without breaking the underlying semantic alignment.</p>
<section id="model-architecture" class="level5 underlineHeader">
<h5 class="underlineHeader anchored" data-anchor-id="model-architecture"><em>Model Architecture</em></h5>
<p>At the heart of our system is a lightweight neural network that learns to apply a small but meaningful <em>delta</em> to the original query embedding:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb2-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">class</span> EmbeddingTransformer(nn.Module):</span>
<span id="cb2-2">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__init__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, dim, hidden_dim):</span>
<span id="cb2-3">        <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">super</span>().<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__init__</span>()</span>
<span id="cb2-4">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.transform <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> nn.Sequential(</span>
<span id="cb2-5">            nn.Linear(dim, hidden_dim),</span>
<span id="cb2-6">            nn.ReLU(),</span>
<span id="cb2-7">            nn.Dropout(<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.1</span>),</span>
<span id="cb2-8">            nn.Linear(hidden_dim, dim)</span>
<span id="cb2-9">        )</span>
<span id="cb2-10"></span>
<span id="cb2-11">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> forward(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, x):</span>
<span id="cb2-12">        delta <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.transform(x)</span>
<span id="cb2-13">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> delta</span></code></pre></div></div>
<p>This architecture takes an input query embedding of dimension <strong>4352</strong> (concatenated CLIP and text embeddings) and learns a residual-style projection. The output delta is added back to the original embedding and normalized, forming the <strong>adapted query vector</strong> used for training.</p>
</section>
<section id="loss-function-cosine-similarity-regression" class="level5 underlineHeader">
<h5 class="underlineHeader anchored" data-anchor-id="loss-function-cosine-similarity-regression"><em>Loss Function: Cosine Similarity Regression</em></h5>
<p>To align the adapted query vector with the target product embedding, we use a <strong>cosine similarity regression loss</strong>, scaled from <code>[-1, 1]</code> to <code>[0, 1]</code> and compared to the <code>len_score</code> target via Mean Square Error (MSE):</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb3-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> _cosine_similarity_regression_loss(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, q, p, target):</span>
<span id="cb3-2">    p_normalized <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> F.normalize(p, dim<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>)</span>
<span id="cb3-3">    cos_sim <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (F.cosine_similarity(q, p_normalized, dim<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span></span>
<span id="cb3-4">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> F.mse_loss(cos_sim, target)</span></code></pre></div></div>
<p>This lets the model learn <strong>how similar</strong> a product should be to a given query, not just a binary match, but a nuanced similarity score that reflects stylistic intent.</p>
</section>
<section id="training-loop-and-monitoring" class="level5 underlineHeader">
<h5 class="underlineHeader anchored" data-anchor-id="training-loop-and-monitoring"><em>Training Loop and Monitoring</em></h5>
<p>We train the model using PyTorch and the Adam optimizer, with all metrics logged via <strong>Weights &amp; Biases (W&amp;B)</strong> for real-time monitoring.</p>
<p>To streamline training, manage hardware resources (in Runpod) more efficiently, and gain more granular control over the training process, we updated the loop to use the <strong>Hugging Face Accelerate</strong> framework.</p>
<p>This allows us to:</p>
<ul>
<li>Efficiently scale across devices (CPU/GPU)</li>
<li>Better manage memory usage for large embeddings</li>
<li>Easily swap data strategies during experimentation</li>
</ul>
<p>Here’s how it works:</p>
<ul>
<li>Load query-product pairs from the training dataset (<code>jsonl</code> format).</li>
<li>Each epoch processes query-product pairs in batches:
<ul>
<li>Generate the delta and add it to the original query embedding.</li>
<li>Normalize the new query vector.</li>
<li>Compute cosine similarity with the product embedding.</li>
<li>Back propagate using MSE loss between similarity and <code>len_score</code>.</li>
</ul></li>
<li>Log training loss per step and average loss per epoch to W&amp;B.</li>
<li>Save model checkpoints after each epoch with timestamped filenames.</li>
<li>Upload final metrics including loss, elapsed time, and model path to W&amp;B at the end of training.</li>
</ul>
<p>This setup enables:</p>
<ul>
<li>Fast experimentation with consistent model saving and tracking.</li>
<li>Simple restart or resume of training from checkpoints.</li>
<li>Easy inspection of model behavior and convergence across epochs.</li>
</ul>
</section>
</section>
<section id="lens-usage" class="level4">
<h4 class="anchored" data-anchor-id="lens-usage"><strong>Lens Usage</strong></h4>
<p>Once the <strong>Summer Lens</strong> model is trained, it can be applied in real time to adapt incoming query embeddings, subtly shifting their position in the semantic space to favor seasonal or stylistic intent without losing category relevance.</p>
<p>The process is lightweight and fast, making it suitable for production environments:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb4-1">query_tensor <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> torch.tensor(query_embedding, dtype<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>torch.float32).reshape(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>)</span>
<span id="cb4-2"></span>
<span id="cb4-3">delta <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.model(query_tensor)</span>
<span id="cb4-4">adapted_query_embedding <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> F.normalize(query_tensor <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> delta, dim<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>)</span></code></pre></div></div>
<p>The adapted embedding is then utilized in the vector search pipeline just like the original one, enhancing the relevance of results for queries influenced by seasonality and style.</p>
<p>This “lens” functions as a lightweight, plug-and-play, differentiable filter over the original query, adjusting its representation to better align with what users are truly seeking, particularly in areas where intent may change over time, trends, or context.</p>
<div class="callout callout-style-default callout-tip callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
<span class="screen-reader-only">Tip</span>Need a different trade‑off?
</div>
</div>
<div class="callout-body-container callout-body">
<p>When you want to maintain more of the original query’s meaning while still incorporating seasonal or stylistic adjustments, <strong>consider blending the raw and adapted embeddings</strong>.</p>
<p>You can do this by adjusting the blend factor α (where q_final = (1‑α)·q_raw + α·q_lens) to achieve the desired level of seasonal bias without needing to retrain or re-index:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb5-1">alpha <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.7</span> <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Blending factor</span></span>
<span id="cb5-2"></span>
<span id="cb5-3">weighted_query <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> F.normalize((<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>alpha) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> query_emb <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> alpha <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> adapted_query_emb, dim<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>)</span></code></pre></div></div>
<p>This gives you finer control over how much influence the lens has, enabling <strong>gradual adaptation</strong> instead of full transformation, especially useful for sensitive or high-precision search scenarios.</p>
</div>
</div>
</section>
<section id="evaluation-validation-pipeline" class="level4">
<h4 class="anchored" data-anchor-id="evaluation-validation-pipeline"><strong>Evaluation &amp; Validation pipeline</strong></h4>
<p>Our eyes and intuition indicated that our approach was effective, but <strong>we needed formal validation</strong> to confirm its efficacy for implementation in a production environment. The best part is that if this technique is validated, <strong>we can incorporate it into our toolkit for future projects</strong>, which will save time and money by allowing us to bypass unnecessary fine-tuning loops while still achieving excellent results.</p>
<p>We promised ourselves two things before giving this as validated:</p>
<ol type="1">
<li><strong>Keep the score-card tiny.</strong> One number for general relevance, one for summer vibes.</li>
<li><strong>Automate the whole pass.</strong> Hit <em>Enter</em>, come back to a W&amp;B dashboard.</li>
</ol>
<section id="what-we-measure" class="level5 underlineHeader">
<h5 class="underlineHeader anchored" data-anchor-id="what-we-measure"><em>What we measure</em></h5>
<table class="caption-top table">
<colgroup>
<col style="width: 33%">
<col style="width: 33%">
<col style="width: 33%">
</colgroup>
<thead>
<tr class="header">
<th><strong>Metric</strong></th>
<th><strong>Why we care</strong></th>
<th><strong>Threshold we locked</strong></th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>Precision @ k</strong></td>
<td>“Of the first <em>k</em> results, how many are simply <em>relevant</em>?”</td>
<td>Count how many of the top-k items cross the same relevance score the baseline already meets (we freeze that cut-off at <strong>0.67</strong>).</td>
</tr>
<tr class="even">
<td><strong>Summer-Precision @ k</strong></td>
<td>“Of those <em>k</em>, how many really scream <em>summer</em>?”</td>
<td>Ask an LLM to rate each product 0-1 on “summer vibe”. Anything ≥ <strong>0.70</strong> counts as a beach-hit and bumps the score</td>
</tr>
</tbody>
</table>
<p>Both metrics are averaged over the evaluation split of <strong>hold-out production queries</strong> that have not been included in the training loop.</p>
</section>
<section id="how-the-evaluation-works" class="level5 underlineHeader">
<h5 class="underlineHeader anchored" data-anchor-id="how-the-evaluation-works"><em>How the evaluation works</em></h5>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb6-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> seasonal_lens.common.metrics <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> (</span>
<span id="cb6-2">    load_search_results,        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># jsonl → {query: [ids]}</span></span>
<span id="cb6-3">    load_scores_from_jsonl,     <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># jsonl → {query: {id: score}}</span></span>
<span id="cb6-4">    precision_at_k</span>
<span id="cb6-5">)</span>
<span id="cb6-6"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> numpy <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> np</span>
<span id="cb6-7"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> pathlib <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> Path</span>
<span id="cb6-8"></span>
<span id="cb6-9"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># --- 1. Load artefacts ----------------------------------------------------</span></span>
<span id="cb6-10">variant   <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> load_search_results(Path(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"results/lens.jsonl"</span>))</span>
<span id="cb6-11">relevance <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> load_scores_from_jsonl(Path(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"labels/relevance.jsonl"</span>)) <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># cosine</span></span>
<span id="cb6-12">summer    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> load_scores_from_jsonl(Path(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"labels/summer.jsonl"</span>))    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 0-1 summer-score</span></span>
<span id="cb6-13"></span>
<span id="cb6-14"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># --- 2. Compute metrics ---------------------------------------------------</span></span>
<span id="cb6-15"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> mean_prec(results, labels, thr, k<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>):</span>
<span id="cb6-16">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> np.mean([</span>
<span id="cb6-17">        precision_at_k(</span>
<span id="cb6-18">            [labels[q].get(pid, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.0</span>) <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> pid <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> ranks],</span>
<span id="cb6-19">            k<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>k,</span>
<span id="cb6-20">            threshold<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>thr</span>
<span id="cb6-21">        )</span>
<span id="cb6-22">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> q, ranks <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> results.items()</span>
<span id="cb6-23">    ])</span>
<span id="cb6-24"></span>
<span id="cb6-25">prec5        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> mean_prec(variant, relevance, thr<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.67</span>, k<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>)</span>
<span id="cb6-26">summer_prec5 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> mean_prec(variant, summer,    thr<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.70</span>, k<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>)</span>
<span id="cb6-27"></span>
<span id="cb6-28"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"Precision@5        : </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>prec5<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:.3f}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)</span>
<span id="cb6-29"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"Summer-Precision@5 : </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>summer_prec5<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:.3f}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)</span></code></pre></div></div>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb7-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> precision_at_k(scores, k, thr):</span>
<span id="cb7-2">    hits <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">sum</span>(s <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;=</span> thr <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> s <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> scores[:k])</span>
<span id="cb7-3">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> hits <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> k</span></code></pre></div></div>
<ol type="1">
<li>Generates a <strong>baseline</strong> ranking straight from relevance labels</li>
<li>Loads the <strong>lens</strong> runs</li>
<li>Computes Precision @ k and Summer-P @ k for both the baseline and the lens</li>
<li>Logs results to W&amp;B</li>
</ol>
</section>
</section>
</section>
</section>
<section id="the-numbers-decoding-the-results" class="level2">
<h2 class="anchored" data-anchor-id="the-numbers-decoding-the-results">The numbers: Decoding the Results 📊</h2>
<p>After training and evaluation, the results confirmed the value of embedding adaptation through the Summer Lens. The best-performing setup used a <strong>50/50 blend</strong> of the original and adapted query vectors, successfully capturing the <strong>seasonal relevance boost</strong> without sacrificing the <strong>baseline precision</strong>.</p>
<p>Even better, performance remained production-ready: added <strong>latency averaged just 1.04 ms per query</strong>, making the lens suitable for real-time search pipelines with no noticeable overhead.</p>
<section id="best-performing-setup-results" class="level3">
<h3 class="anchored" data-anchor-id="best-performing-setup-results">Best-performing setup results</h3>
<ol type="1">
<li>Summer catalog boost: <strong>+138,4%</strong></li>
<li>Overall relevance impact: <strong>-11,71%</strong></li>
</ol>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><a href="metrics-comparison.webp" class="lightbox" data-gallery="quarto-lightbox-gallery-1" title="Metrics comparison between the baseline model and the applied lens model"><img src="https://labs.theagilemonkeys.com/posts/summer-embedding-lens-fashion-ecommerce/metrics-comparison.webp" class="img-fluid figure-img" alt="Metrics comparison between the baseline model and the applied lens model"></a></p>
<figcaption>Metrics comparison between the baseline model and the applied lens model</figcaption>
</figure>
</div>
<section id="results-snapshot" class="level4 underlineHeader">
<h4 class="underlineHeader anchored" data-anchor-id="results-snapshot"><em>Results snapshot</em></h4>
<table class="caption-top table">
<thead>
<tr class="header">
<th><strong>Metric</strong></th>
<th><strong>Baseline</strong></th>
<th><strong>Lens 100%</strong></th>
<th><strong>Lens 50% blend</strong></th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Precision @ 10</td>
<td>0.96</td>
<td>0.58</td>
<td>0.86</td>
</tr>
<tr class="even">
<td>Summer-P @ 10</td>
<td>0.35</td>
<td>0.95</td>
<td>0.86</td>
</tr>
<tr class="odd">
<td>Extra Latency (ms)</td>
<td>-</td>
<td>1.04</td>
<td>1.04</td>
</tr>
</tbody>
</table>
</section>
</section>
</section>
<section id="what-we-learned-tips" class="level2">
<h2 class="anchored" data-anchor-id="what-we-learned-tips">What We Learned (Tips) 🎓</h2>
<ol type="1">
<li><strong>Cheap synthetic data is gold.</strong> OpenAI batch prompts gave us realistic, summer scores for all the catalog items (11k) in less than a day.</li>
<li><strong>Screenshots trump spreadsheets.</strong> Putting results side‑by‑side makes the shift instantly clear to business stakeholders, far better than dropping a table of numbers, so every evaluation run now auto‑exports that comparison image.</li>
<li><strong>Trust, but eyeball.</strong> When we build the <em>len_score</em> (relevance + summer vibe), the weightings can accidentally tip the scale, everything looks “very summer” <em>or</em> “hardly summer” at all. A quick histogram of those combined scores shows how much variety we really have on the summer axis, so we can rebalance triplets before training and avoid teaching the lens a one‑note view of the catalogue</li>
<li><strong>Blending is a secret weapon.</strong> That extra knob α lets us decide <em>how much</em> lens power to apply, α = 0.5 hit the sweet spot and saved days of hyper‑param tinkering and code churn.</li>
<li><strong>Version everything.</strong> We store triplet files and LLM scores on Hugging Face Datasets; rolling back a bad run is just changing a dataset tag.</li>
</ol>
</section>
<section id="conclusions" class="level2">
<h2 class="anchored" data-anchor-id="conclusions">Conclusions 🎯</h2>
<p>The Embedding Lens proves that contextual search can be made both agile and production‑ready. By adding a lightweight, toggleable layer to our existing embedding pipeline, we can deliver seasonally or thematically tailored results without re‑indexing, retraining the base model, or adding operational complexity, while preserving production‑grade performance and simple deployments at scale.</p>
<p><strong>Key takeaways:</strong></p>
<ul>
<li><strong>On‑demand adaptation:</strong> Activate or retire themed search experiences in seconds, no catalog reprocessing required.</li>
<li><strong>Faster data prep:</strong> LLM‑generated synthetic data cuts human labeling effort, enabling rapid rollout of new lenses aligned to business needs.</li>
<li><strong>Rapid, low‑cost iteration:</strong> The lens architecture trains economically, supporting quick prototype-test-refine cycles.</li>
<li><strong>Production‑safe latency:</strong> ~<strong>1.04 ms</strong> added per query, effectively negligible for user experience.</li>
<li><strong>Risk‑mitigated rollout:</strong> Straightforward A/B testing lets us trial multiple themed searches across segments without infrastructure changes.</li>
<li><strong>Built for extensibility:</strong> Supports lens composition (e.g., “summer+party”), multi‑aspect context (weather, location), and behavior‑adaptive lenses, without retraining the foundational embedding model.</li>
</ul>
<p>Overall, the embedding lens approach advances our search architecture toward a more responsive, context‑aware system that scales operationally and economically, making continuous innovation practical in production.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><a href="monkey_close.png" class="lightbox" data-gallery="quarto-lightbox-gallery-2"><img src="https://labs.theagilemonkeys.com/posts/summer-embedding-lens-fashion-ecommerce/monkey_close.png" class="img-fluid quarto-figure quarto-figure-center figure-img" style="width:70.0%"></a></p>
</figure>
</div>


</section>

 ]]></description>
  <category>RAG</category>
  <category>e-commerce</category>
  <category>LLM</category>
  <guid>https://labs.theagilemonkeys.com/posts/summer-embedding-lens-fashion-ecommerce/</guid>
  <pubDate>Wed, 11 Feb 2026 00:00:00 GMT</pubDate>
  <media:content url="https://labs.theagilemonkeys.com/posts/summer-embedding-lens-fashion-ecommerce/og-image.png" medium="image" type="image/png" height="76" width="144"/>
</item>
<item>
  <title>AI Document Parsing in Production: Benchmarking GPT, Claude, AWS and Azure</title>
  <link>https://labs.theagilemonkeys.com/posts/state-of-ai-document-parsing/</link>
  <description><![CDATA[ 




<p>We ran 64 controlled experiments across five parsing providers to learn which ones are ready for production workflows that combine IDs, vital records, and tax forms. The harness, sample documents, and evaluation scripts live in the public repository <a href="https://github.com/theam/document-data-extraction-benchmark">document-data-extraction-benchmark</a> so teams can reproduce and extend these results.</p>
<section id="at-a-glance" class="level2">
<h2 class="anchored" data-anchor-id="at-a-glance">At a glance</h2>
<ul>
<li>Scope: 5 providers, 6 document types, images and PDFs</li>
<li>Metrics: accuracy, average latency, and cost</li>
<li>Runs: 64 experiments using a reproducible harness</li>
<li>Default pick: GPT-5.1 for accuracy, cost, and latency balance</li>
<li>When to specialize: AWS or Azure for passport-heavy ID workflows</li>
</ul>
</section>
<section id="quick-recommendations" class="level2">
<h2 class="anchored" data-anchor-id="quick-recommendations">Quick recommendations</h2>
<ul>
<li>General pipelines: GPT-5.1 as the default; consider Claude Sonnet 4.5 only when a 1M context window is required</li>
<li>IDs (passports): AWS Textract or Azure ID model; GPT-5.1 is the strongest LLM choice</li>
<li>Defer: Google DocAI unless you are investing in custom processors</li>
<li>QA: Plan human review on roughly 30 percent of outputs</li>
</ul>
</section>
<section id="executive-summary" class="level2">
<h2 class="anchored" data-anchor-id="executive-summary">Executive summary</h2>
<ul>
<li>Top performers: GPT-5.1 leads with 73.2 percent accuracy, production-ready average latency (~12.3 seconds), and very low cost (~$0.00054 per document). Claude Sonnet 4.5 matches accuracy but is slower and 53x more expensive.</li>
<li>OCR specialty: AWS Textract and Azure excel on passports and other government IDs thanks to specialized models, but fall short on general documents where LLMs dominate.</li>
<li>Cost value: GPT-5.1 delivers near state-of-the-art accuracy at almost the lowest cost.</li>
<li>Not production ready: Google DocAI underperformed and would require substantial custom development.</li>
<li>Testing and reproducibility: All benchmarks were run with the open harness, document samples, and evaluation scripts in <a href="https://github.com/theam/document-data-extraction-benchmark">document-data-extraction-benchmark</a>.</li>
</ul>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
<span class="screen-reader-only">Note</span>Context and limitations
</div>
</div>
<div class="callout-body-container callout-body">
<ul>
<li>Results are directional from 64 tests across 6 document types, mostly U.S. documents.</li>
<li>Configurations were largely out of the box; accuracy may shift with tuning, different samples, or provider updates.</li>
<li>Use these rankings to shortlist providers. Re-run your top three documents per workflow with tuned prompts or model settings before finalizing.</li>
</ul>
</div>
</div>
</section>
<section id="why-we-ran-this-evaluation" class="level2">
<h2 class="anchored" data-anchor-id="why-we-ran-this-evaluation">Why we ran this evaluation</h2>
<p>Operational workflows depend on accurate extraction of structured data from IDs, vital records, and tax forms. Accuracy, speed, and reliability directly affect automation quality and cost. We tested five providers on eight documents to see which vendor delivers the best real-world performance today.</p>
</section>
<section id="setup-overview" class="level2">
<h2 class="anchored" data-anchor-id="setup-overview">Setup overview</h2>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Dimension</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Providers</td>
<td>GPT-5.1, Claude Sonnet 4.5, AWS Textract, Azure Document Intelligence, Google DocAI</td>
</tr>
<tr class="even">
<td>Document types</td>
<td>Passports, driver licenses, birth certificates, marriage certificates, W-2, 1040</td>
</tr>
<tr class="odd">
<td>Formats</td>
<td>Images and PDFs</td>
</tr>
<tr class="even">
<td>Metrics</td>
<td>Accuracy, average latency, and cost</td>
</tr>
<tr class="odd">
<td>Reproducibility</td>
<td>Harness plus scripts in repository <a href="https://github.com/theam/document-data-extraction-benchmark">document-data-extraction-benchmark</a></td>
</tr>
</tbody>
</table>
</section>
<section id="key-findings" class="level2">
<h2 class="anchored" data-anchor-id="key-findings">Key findings</h2>
<ol type="1">
<li>LLM vision models outperform classic OCR on general documents. GPT-5.1 and Claude Sonnet 4.5 reach 70 to 75 percent accuracy on certificates and tax forms where OCR lags at 0 to 40 percent.</li>
<li>Specialized OCR wins for narrow ID use cases. AWS Textract and Azure hit around 90 percent accuracy on passports due to ID-trained models; their accuracy drops sharply on certificates and tax forms.</li>
<li>GPT-5.1 is the composite leader. It ties for highest accuracy (73.2 percent) with Claude Sonnet 4.5 while being faster on average (~12.3 seconds vs ~16.3 seconds) and far cheaper (~$0.00054 vs ~$0.0286 per document).</li>
<li>Providers with major gaps. Azure is strong on passports but inconsistent elsewhere without configuration. Google DocAI showed the lowest accuracy and needs custom processor development.</li>
</ol>
</section>
<section id="provider-snapshots" class="level2">
<h2 class="anchored" data-anchor-id="provider-snapshots">Provider snapshots</h2>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
<span class="screen-reader-only">Note</span>GPT-5.1 - default leader and budget choice
</div>
</div>
<div class="callout-body-container callout-body">
<ul>
<li>Accuracy: 73.2 percent</li>
<li>Avg latency: ~12.3 seconds</li>
<li>Cost: ~$0.00054 per document</li>
<li>Best overall balance of accuracy, cost, and speed.</li>
</ul>
</div>
</div>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
<span class="screen-reader-only">Note</span>AWS Textract
</div>
</div>
<div class="callout-body-container callout-body">
<ul>
<li>Accuracy: ~50 percent overall, ~90 percent on passports</li>
<li>Avg latency: ~7.7 seconds</li>
<li>Excels on passports and standardized ID formats.</li>
</ul>
</div>
</div>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
<span class="screen-reader-only">Note</span>Claude Sonnet 4.5 - accuracy tie at premium cost
</div>
</div>
<div class="callout-body-container callout-body">
<ul>
<li>Accuracy: 73.2 percent</li>
<li>Avg latency: ~16.3 seconds</li>
<li>Cost: ~$0.0286 per document</li>
<li>Use when the 1M context window is mandatory and budget is secondary.</li>
</ul>
</div>
</div>
<div class="callout callout-style-default callout-warning callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
<span class="screen-reader-only">Warning</span>Google DocAI
</div>
</div>
<div class="callout-body-container callout-body">
<ul>
<li>Accuracy: ~22 percent</li>
<li>Avg latency: ~6.8 seconds</li>
<li>Requires custom processors for acceptable accuracy; not recommended out of the box.</li>
</ul>
</div>
</div>
</section>
<section id="performance-by-document-type" class="level2">
<h2 class="anchored" data-anchor-id="performance-by-document-type">Performance by document type</h2>
<ul>
<li>Passports: OCR tools (AWS Textract, Azure) dominate passports with MRZ-trained models at roughly 90 percent accuracy. GPT-5.1 is the best LLM here (~80 percent), closing most of the gap while staying far cheaper.</li>
<li>Driver licenses: All providers struggle; layout variability is the main issue and no model clearly dominates.</li>
<li>Birth and marriage certificates: GPT-5.1 and Claude Sonnet 4.5 reach 75 to 90 percent accuracy. OCR models often remain below 40 percent.</li>
<li>Tax forms (W-2 and 1040): LLMs show strong semantic understanding, delivering around 80 to 100 percent accuracy on many fields. Claude Sonnet 4.5, GPT-5.1, and Textract cluster around ~91.7 percent on W-2 and ~80 percent on 1040.</li>
</ul>
</section>
<section id="when-to-choose-each-provider" class="level2">
<h2 class="anchored" data-anchor-id="when-to-choose-each-provider">When to choose each provider</h2>
<ul>
<li>Choose GPT-5.1 if you want the best composite score (73.2 percent accuracy, ~12.3 second average latency, ~$0.00054 per document) across mixed document types with a single default.</li>
<li>Choose Claude Sonnet 4.5 if you need a 1M token context window for long or complex documents and can accept slower latency and much higher cost for the same accuracy.</li>
<li>Use AWS Textract or Azure only if you have ID-heavy workflows (especially passports) and need maximum ID accuracy at the expense of general-document performance and higher page-based pricing.</li>
<li>Avoid Google DocAI for now; low accuracy outweighs its speed advantage unless you plan significant custom processor work.</li>
</ul>
</section>
<section id="strategic-recommendations" class="level2">
<h2 class="anchored" data-anchor-id="strategic-recommendations">Strategic recommendations</h2>
<ol type="1">
<li>Adopt GPT-5.1 as the default provider for general-purpose document extraction across mixed document types.</li>
<li>Use AWS Textract or Azure ID models for passport-heavy or government ID workflows that demand near 90 percent ID accuracy.</li>
<li>Reserve Claude Sonnet 4.5 for scenarios where the 1M context window is a hard requirement.</li>
<li>Budget for human review on roughly 30 percent of outputs given the current accuracy ceiling of about 73 percent.</li>
</ol>


</section>

 ]]></description>
  <category>LLM</category>
  <category>evaluation</category>
  <category>production</category>
  <guid>https://labs.theagilemonkeys.com/posts/state-of-ai-document-parsing/</guid>
  <pubDate>Thu, 11 Dec 2025 00:00:00 GMT</pubDate>
  <media:content url="https://labs.theagilemonkeys.com/posts/state-of-ai-document-parsing/og-image.png" medium="image" type="image/png" height="76" width="144"/>
</item>
<item>
  <title>AI-Powered E2E Testing: 70% Reduction in Test Execution Time with Stagehand</title>
  <link>https://labs.theagilemonkeys.com/posts/testing-in-the-era-of-ai/</link>
  <description><![CDATA[ 




<p>The arrival of AI agents has radically transformed how we interact with software. However, there is a massive gap between “playing around” with an agent in a chat interface and orchestrating a robust solution in a production environment.</p>
<p>Recently, I set out to experiment with how these advancements could facilitate the automation of End-to-End (E2E) tests. My goal was simple: reduce the friction of manual testing without sacrificing reliability. What started with simple tests in ChatGPT evolved into a scalable technical architecture based on <strong>Stagehand</strong>, <strong>Browserbase</strong>, and <strong>Gemini 2.5 Flash</strong>.</p>
<p>Below, I detail the technical evolution of this implementation, the obstacles encountered, and how we managed to reduce testing times by 70%.</p>
<hr>
<section id="phase-1-the-limitation-of-agent-mode-chatgpt" class="level2">
<h2 class="anchored" data-anchor-id="phase-1-the-limitation-of-agent-mode-chatgpt">Phase 1: The Limitation of “Agent Mode” (ChatGPT)</h2>
<p>My first approach was to use ChatGPT’s Agent Mode. The premise is seductive: describe a user flow in natural language and watch the AI navigate, click, and validate results.</p>
<p>The technology works, the results are surprising, and the agent’s capability borders on the magical. For prototyping quick flows, it is an exceptional tool. However, when attempting to translate this into an engineering environment, the limitations became evident:</p>
<ul>
<li><strong>Lack of CI/CD Integration:</strong> The execution lives isolated within the chat. There is no API to trigger these tests automatically after a Pull Request.</li>
<li><strong>Collaboration and Versioning:</strong> Relying on manually copy-pasting prompts eliminates any real automation benefit. Without code, there is no version control (Git), no execution history, and team collaboration becomes chaotic.</li>
<li><strong>Human Dependency:</strong> In flows requiring authentication or credential management, the agent often halts, waiting for human intervention.</li>
<li><strong>Non-deterministic Environment:</strong> Since you don’t control the browser or the execution context, reproducing a specific error for troubleshooting is practically impossible.</li>
</ul>
<p><strong>Conclusion:</strong> Chat is ideal for assisting, iterating, and validating. Code is what allows us to scale, automate, and monitor.</p>
<hr>
</section>
<section id="phase-2-programmatic-architecture-with-stagehand" class="level2">
<h2 class="anchored" data-anchor-id="phase-2-programmatic-architecture-with-stagehand">Phase 2: Programmatic Architecture with Stagehand</h2>
<p>To address the scalability and operational issues, I migrated to <strong>Stagehand</strong>. This tool abstracts away the complexity of “human navigation” but lives within our code (TypeScript).</p>
<p>The brilliance of Stagehand is that it doesn’t try to reinvent the wheel; instead, it introduces intention-based interaction primitives:</p>
<ul>
<li><strong><code>act()</code> (The Executor):</strong> Replaces the traditional click/type. You don’t tell it <em>where</em> to click (selector); you tell it <em>what</em> you want to achieve.
<ul>
<li><em>Ex:</em> <code>await stagehand.act("Log in with the saved credentials");</code></li>
</ul></li>
<li><strong><code>extract()</code> (The Analyst):</strong> Converts unstructured HTML into typed JSON objects.
<ul>
<li><em>Ex:</em> “Extract the price and stock of this product.”</li>
</ul></li>
<li><strong><code>eval()</code> (The Judge):</strong> The evolution of assertions. It evaluates logical conditions based on visual and contextual information.
<ul>
<li><em>Ex:</em> <code>const isError = await stagehand.eval("Is there a wrong password error?");</code></li>
</ul></li>
<li><strong><code>observe()</code> (The Eyes):</strong> Allows the script to understand what interactive elements exist in the current state before making a decision.</li>
</ul>
<p>With these primitives, we shift from “giving instructions to the DOM” to describing behaviors.</p>
<hr>
</section>
<section id="the-technical-architecture-browserbase-and-gemini" class="level2">
<h2 class="anchored" data-anchor-id="the-technical-architecture-browserbase-and-gemini">The Technical Architecture: Browserbase and Gemini</h2>
<p>While Stagehand is versatile and can run locally or in headless mode on GitHub Actions, basic execution often falls short for a demanding production environment. Validating on a standard runner works but lacks the deep observability necessary for a modern QA pipeline.</p>
<p>That is where we consolidated the technical architecture:</p>
<section id="infrastructure-and-persistence-browserbase" class="level3">
<h3 class="anchored" data-anchor-id="infrastructure-and-persistence-browserbase">1. Infrastructure and Persistence (Browserbase)</h3>
<p>Delegating browser execution to <strong>Browserbase</strong> solved two critical problems:</p>
<ul>
<li><strong>Context Management (Persistent Sessions):</strong> One of the biggest bottlenecks in E2E is the time lost on repetitive logins. Leveraging the <em>Contexts</em> functionality, we can authenticate once, save the state (cookies, storage), and reuse it. This allows us to run tests in parallel with different roles without repeating authentication in every case.</li>
<li><strong>Advanced Observability:</strong> Traditional text logs (<code>Error: element not found</code>) are insufficient for understanding visual glitches or intermediate states. Browserbase enables Session Replay and execution across multiple viewports, allowing us to debug failures by seeing exactly what happened on the screen during each run.</li>
</ul>
</section>
<section id="model-optimization-gemini-2.5-flash" class="level3">
<h3 class="anchored" data-anchor-id="model-optimization-gemini-2.5-flash">2. Model Optimization (Gemini 2.5 Flash)</h3>
<p>Initially, tests were conducted with heavier models like GPT-4. Although accuracy was high, the latency per instruction penalized the total suite time.</p>
<p>Following Stagehand community benchmarks comparing cost, accuracy, and speed, we migrated the inference engine to <strong>Gemini 2.5 Flash</strong>. The results were immediate:</p>
<ul>
<li><strong>Latency Reduction:</strong> Execution time per test dropped from ~30 seconds to ~15 seconds.</li>
<li><strong>Cost Efficiency:</strong> Being a model optimized for high frequency, the operational cost decreased drastically, allowing continuous runs on every commit without compromising the budget.</li>
</ul>
<hr>
</section>
</section>
<section id="technical-comparison-imperative-vs.-declarative" class="level2">
<h2 class="anchored" data-anchor-id="technical-comparison-imperative-vs.-declarative">Technical Comparison: Imperative vs.&nbsp;Declarative</h2>
<p>The deepest change isn’t just the tools, but the paradigm. Until now, UI testing was written in imperative code, which is very brittle in the face of frontend changes. Stagehand, with its AI integration, allows us to define tests based on intentions.</p>
<p><strong>Traditional Approach (CSS/XPath Selectors):</strong> Requires knowing the specific name of identifiers. It is fragile: if a developer changes a CSS class, the test breaks.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode typescript code-with-copy"><code class="sourceCode typescript"><span id="cb1-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">// If the ID changes or the button moves, the test fails</span></span>
<span id="cb1-2"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">await</span> page<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">type</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'#username-field'</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">process</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">env</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">USER</span>)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb1-3"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">await</span> page<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">click</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'.submit-btn-wrapper &gt; button'</span>)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb1-4"></span>
<span id="cb1-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">// Cryptic logs if it fails</span></span>
<span id="cb1-6"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">await</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">expect</span>(page<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">locator</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'.dashboard-header'</span>))<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">toBeVisible</span>()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span></code></pre></div></div>
<p><strong>Modern Approach (Stagehand + AI):</strong> More resilient and readable. The AI interprets the interface as a human would, regardless of changes in the underlying code.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode typescript code-with-copy"><code class="sourceCode typescript"><span id="cb2-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">// The instruction describes the intention, the AI solves the execution</span></span>
<span id="cb2-2"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">await</span> stagehand<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">act</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Log in with standard user credentials"</span>)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb2-3"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">await</span> stagehand<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">act</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Navigate to the configuration panel"</span>)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb2-4"></span>
<span id="cb2-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">// Semantic validation</span></span>
<span id="cb2-6"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">const</span> isVisible <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">await</span> stagehand<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">eval</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Verify that the sales chart is visible"</span>)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb2-7"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> (<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!</span>isVisible) <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">throw</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">new</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">Error</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"The chart did not load"</span>)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span></code></pre></div></div>
<p>Instead of tying ourselves to a selector name, we tie ourselves to the behavior expected by the user.</p>
<hr>
</section>
<section id="use-cases-and-conclusion" class="level2">
<h2 class="anchored" data-anchor-id="use-cases-and-conclusion">Use Cases and Conclusion</h2>
<p>This architecture is not limited to QA. The ability to navigate the web intelligently opens doors to broader automations:</p>
<ul>
<li><strong>RPA (Robotic Process Automation):</strong> Automating tedious administrative tasks, such as monthly invoice downloads or routine dashboard updates.</li>
<li><strong>Synthetic Monitoring:</strong> Periodic validation of critical flows in production from different geolocations and devices.</li>
<li><strong>Resilient Data Scraping:</strong> Extracting data from highly dynamic sites where traditional scrapers, based on rigid selectors, frequently fail.</li>
</ul>
<p>The era of maintaining manual CSS selectors is coming to an end. The era of designing behaviors and letting AI execute has just begun.</p>


</section>

 ]]></description>
  <category>AI agents</category>
  <category>testing</category>
  <guid>https://labs.theagilemonkeys.com/posts/testing-in-the-era-of-ai/</guid>
  <pubDate>Mon, 24 Nov 2025 00:00:00 GMT</pubDate>
  <media:content url="https://labs.theagilemonkeys.com/posts/testing-in-the-era-of-ai/og-image.png" medium="image" type="image/png" height="76" width="144"/>
</item>
<item>
  <title>AI Evaluation in Production: Why Evals Are a Mindset, Not a Tool</title>
  <link>https://labs.theagilemonkeys.com/posts/why-your-team-is-stuck-on-ai-evals/</link>
  <description><![CDATA[ 




<p>Over the last few years, we’ve been deeply invested in understanding how to evaluate the performance of our AI systems, especially our search and chat experiences. Along the way, we’ve built several prototypes, dashboards, and tools, each one teaching us something new. But despite all the progress, we noticed a pattern: we kept building interfaces before fully understanding what <em>evaluating</em> really means.</p>
<p>At some point, we realized that evaluation isn’t about fancy UIs or sophisticated platforms. <strong>Evals is not a UI, or a dashboard, or a platform — it’s about data.</strong> It’s about collecting, transforming, and interpreting data to understand, at any moment, how your systems are performing and how they’re improving over time.</p>
<section id="what-really-matters-in-evaluations" class="level2">
<h2 class="anchored" data-anchor-id="what-really-matters-in-evaluations">What Really Matters in Evaluations</h2>
<p>⚙️ <strong>Process data at scale.</strong><br>
We generate millions of events every month from our search and chat systems. The first challenge is building reliable data transformation pipelines — in our case, we lean on virtually unbounded cloud databases and lambdas to collect and process events efficiently, but there are many ways to do this. The key is ensuring the data can flow, transform, and aggregate without friction.</p>
<p>📏 <strong>Define the right metrics.</strong><br>
Decide what you want to measure and why. Sometimes it’s simple metrics (like response times or number of results returned), and sometimes it’s more complex (like LLM-based verdicts or semantic accuracy scores). These definitions often come more from product and business needs than from engineering, but they determine whether you have your product under control or not.</p>
<p>📊 <strong>Visualize results clearly.</strong><br>
Each metric should be visible and easy to interpret — whether in a table, a chart, or a trend graph. The goal is clarity, not complexity.</p>
<p>🧩 <strong>Evals are a source of very nice reference datasets.</strong><br>
Evaluations are an efficient way to label data at scale, and making a human-curated selection of this data produces high-quality datasets that you can use to benchmark new versions of your system before they go live, and to fine-tune or train better models over time (increasing quality and performance and reducing cost).</p>
</section>
<section id="borrowing-the-mindset-from-product-analytics" class="level2">
<h2 class="anchored" data-anchor-id="borrowing-the-mindset-from-product-analytics">Borrowing the Mindset from Product Analytics</h2>
<p>In many ways, building AI evals isn’t so different from tracking product KPIs. The main difference is that much of our data is unstructured — often text — and requires LLMs to interpret. But the principles are the same: focus on <strong>data, metrics, and insights</strong>, not on reinventing analytics tooling.</p>
<p>That’s why we now rely on existing analytics platforms and standard cloud services to process and visualize events and evaluation results. That way we spend most of our time where it truly matters — defining metrics, building robust pipelines, and making decisions based on real data.</p>
<p>By fostering a culture of continuous evaluation — one built on clear metrics and real datasets — we’re not just measuring quality. We’re creating the foundations to improve it every single day.</p>
<p>And that’s the real goal of AI evals: not building another tool, but building understanding.</p>


</section>

 ]]></description>
  <category>evaluation</category>
  <category>AI strategy</category>
  <category>production</category>
  <guid>https://labs.theagilemonkeys.com/posts/why-your-team-is-stuck-on-ai-evals/</guid>
  <pubDate>Mon, 17 Nov 2025 00:00:00 GMT</pubDate>
  <media:content url="https://labs.theagilemonkeys.com/posts/why-your-team-is-stuck-on-ai-evals/og-image.png" medium="image" type="image/png" height="76" width="144"/>
</item>
<item>
  <title>AGI Is a Decade Away: Why Autonomous Agents and Practical AI Engineering Start Now</title>
  <link>https://labs.theagilemonkeys.com/posts/agi-decade-away-real-work-starts-now/</link>
  <description><![CDATA[ 




<p>This article summarizes the main themes and insights that sparked our internal debate after listening to <a href="https://www.dwarkesh.com/p/andrej-karpathy">Andrej Karpathy’s recent conversation with Dwarkesh Patel</a>. It captures the points that resonated most with our team — what we found inspiring, debatable, or worth exploring further — and how these ideas connect to our day-to-day work building AI-powered products.</p>
<section id="the-cognitive-core-and-externalized-memory" class="level2">
<h2 class="anchored" data-anchor-id="the-cognitive-core-and-externalized-memory">The “Cognitive Core” and Externalized Memory</h2>
<p>Karpathy’s central idea is to separate a model’s <strong>reasoning</strong> from its <strong>memory</strong>. Instead of storing vast knowledge in weights, he suggests keeping a compact “cognitive core” focused on reasoning and problem-solving, while retrieving facts and context from external systems. This underscores the growing importance of information retrieval and echoes our belief that GenAI should be viewed as an enabler for processing and understanding unstructured data—not as a database. The real opportunity lies in building powerful retrieval systems that amplify smarter reasoning models, especially when working with private or proprietary data that isn’t publicly available.</p>
</section>
<section id="the-decade-of-agents-not-the-year" class="level2">
<h2 class="anchored" data-anchor-id="the-decade-of-agents-not-the-year">The Decade of Agents — Not the Year</h2>
<p>Karpathy predicts that this will be the <em>decade</em> of agents, not the year. Building robust autonomous systems still requires advances in continual learning, multimodal reasoning, and reliable computer use. Our team agreed: it’s more realistic to focus on <strong>assistive agents with bounded scope and autonomy</strong>, capable of executing tasks under human supervision, than to chase the mirage of full autonomy.</p>
</section>
<section id="why-software-is-ais-natural-beachhead" class="level2">
<h2 class="anchored" data-anchor-id="why-software-is-ais-natural-beachhead">Why Software Is AI’s Natural Beachhead</h2>
<p>As discussed in our session, software development is uniquely suited for AI integration because <strong>outcomes are measurable and reversible</strong>. Code either compiles or fails, tests can verify correctness, and metrics quantify performance. This quantifiability — as Andrej noted during the discussion — explains why AI-assisted coding has advanced faster than creative or legal applications. Software is the perfect playground for measurable, iterative improvement. What other domains share these measurable and reversible qualities? Use cases where outputs can be deterministically validated, tested, and audited may represent the next wave of near‑term opportunities for applied AI.</p>
</section>
<section id="smaller-models-bigger-leverage" class="level2">
<h2 class="anchored" data-anchor-id="smaller-models-bigger-leverage">Smaller Models, Bigger Leverage</h2>
<p>We were struck by Karpathy’s emphasis on the potential of <strong>small, specialized models</strong>. Many tasks don’t require billion-parameter LLMs; a smaller model combined with retrieval and rules can achieve similar quality at a fraction of the cost. Our team highlighted how this approach can significantly reduce infrastructure costs and enhance performance—particularly in production environments where token usage scales fast and response speed directly shapes perceived product quality. We’ve been applying this principle by fine‑tuning smaller models to handle tasks once reserved for the largest ones. The key is data: start with a powerful model to generate high‑quality examples, curate real usage data, and iteratively refine the smaller model until it meets or surpasses an acceptable quality threshold.</p>
</section>
<section id="the-value-of-continuous-evaluation" class="level2">
<h2 class="anchored" data-anchor-id="the-value-of-continuous-evaluation">The Value of Continuous Evaluation</h2>
<p>Another recurring theme was the importance of <strong>evals as a strategic process</strong>, not just QA. Teams that continuously transform user data and incidents into automated evaluations will build faster, safer, and more reliable systems. Static, dataset‑driven evaluations remain invaluable—they serve as the foundational complement to unit tests before deploying an AI‑first application. At the same time, our team is heavily investing in live evaluations that continuously benchmark real‑world usage. These systems help us respond to unexpected usage patterns and serve as a rich source of new, high‑quality samples to refine and expand our static evaluation datasets over time.</p>
</section>
<section id="where-the-debate-remains-open" class="level2">
<h2 class="anchored" data-anchor-id="where-the-debate-remains-open">Where the Debate Remains Open</h2>
<p>We discussed several open questions:</p>
<ul>
<li>How small can a cognitive core realistically be before quality collapses?</li>
<li>When does it make sense to fine-tune or build custom models instead of relying on well-prompted general ones?</li>
<li>How do we design memory layers that combine persistent user context, transient session data, and structured retrieval systems?</li>
</ul>
</section>
<section id="closing-thoughts" class="level2">
<h2 class="anchored" data-anchor-id="closing-thoughts">Closing Thoughts</h2>
<p>Karpathy’s conversation was a refreshing reminder that <strong>progress in AI is as much about engineering discipline as about model scale</strong>. As a team, we found inspiration in his pragmatism: simplify architectures, externalize memory, prioritize measurable outcomes, and view agents as tools for augmentation, not autonomy. The future of AI is exciting, and this kind of discussions remind us that we’re just starting this journey.</p>
<hr>
<p><em>These reflections were compiled from an internal discussion by The Agile Monkeys team following Andrej Karpathy’s interview with Dwarkesh Patel (October 2025). The recording and full transcript are available at </em><a href="https://www.dwarkesh.com/p/andrej-karpathy"><strong>dwarkesh.com/p/andrej-karpathy</strong></a><em>.</em></p>


</section>

 ]]></description>
  <category>AI agents</category>
  <category>AI strategy</category>
  <category>RAG</category>
  <guid>https://labs.theagilemonkeys.com/posts/agi-decade-away-real-work-starts-now/</guid>
  <pubDate>Thu, 13 Nov 2025 00:00:00 GMT</pubDate>
  <media:content url="https://labs.theagilemonkeys.com/posts/agi-decade-away-real-work-starts-now/og-image.png" medium="image" type="image/png" height="76" width="144"/>
</item>
<item>
  <title>How to Migrate LLMs Safely in Production: GPT-4o to GPT-4.1 Case Study</title>
  <dc:creator>Tomás de las Casas, Jose Manuel Martinez, Nayara Rodríguez, Borja García at The Agile Monkeys</dc:creator>
  <link>https://labs.theagilemonkeys.com/posts/safe-migration-from-gpt4o_to_gpt41-model/</link>
  <description><![CDATA[ 




<p><strong>TL;DR</strong>: Our evaluation showed that migrating from OpenAI’s GPT-4o to GPT-4.1 would deliver equal or better performance in semantic understanding and classification quality, without negatively impacting user experience.</p>
<hr>
<section id="introduction" class="level2">
<h2 class="anchored" data-anchor-id="introduction">Introduction</h2>
<p>When OpenAI releases updates to its foundational models, engineering teams face a critical decision: <em>when</em> and <em>how</em> should we upgrade our production systems? While the <a href="https://openai.com/index/gpt-4-1/">GPT-4.1</a> model promises improvements in language understanding, inference speed, latency, and cost, switching models without proper evaluation can introduce regressions in business-critical tasks.</p>
<p>To address this challenge, we designed a <strong>multi-layer evaluation protocol</strong> that would give us confidence in our migration decision while minimizing risks to our production e-commerce application.</p>
<p>In our specific use case, we use LLMs to extract intent and business-specific features from user queries made in an e-commerce site search engine.</p>
<hr>
</section>
<section id="our-evaluation-strategy" class="level2">
<h2 class="anchored" data-anchor-id="our-evaluation-strategy">Our Evaluation Strategy</h2>
<p>We structured our migration process around <strong>two complementary evaluation tracks</strong>:</p>
<section id="prompt-level-evaluations" class="level3">
<h3 class="anchored" data-anchor-id="prompt-level-evaluations">Prompt-Level Evaluations</h3>
<p>This track focuses on understanding how different models and prompts perform in isolation.</p>
<p>For this evaluation, we created a dataset containing 165 carefully crafted queries with their expected outputs. We then tested multiple prompt versions across both GPT-4o and GPT-4.1 to understand how each model responds to different instructions.</p>
<p>As we know exactly what the model should return for each query in our dataset, we can measure alignment objectively.</p>
</section>
<section id="application-level-evaluations" class="level3">
<h3 class="anchored" data-anchor-id="application-level-evaluations">Application-Level Evaluations</h3>
<p>This track analyzes the real-world impact by examining the final set of products returned by our e-commerce application when using different models and prompts.</p>
<p>Since we’re testing the complete system, we needed to create a golden dataset based on our actual product catalog. We ran real-world queries through our application pipeline using multiple prompt versions, then measured and compared the overall accuracy of different model and prompt combinations.</p>
<hr>
</section>
</section>
<section id="what-we-tested" class="level2">
<h2 class="anchored" data-anchor-id="what-we-tested">What We Tested</h2>
<p>Our evaluation included:</p>
<p><strong>Prompt Versions:</strong></p>
<ul>
<li><strong>v25</strong>: Our current production prompt used with GPT-4o</li>
<li><strong>v26</strong>: A new prompt designed specifically for GPT-4.1, featuring significantly reduced input tokens by removing tasks that could be handled reliably with code instead of the LLM</li>
<li><strong>v27</strong>: A variation of v26 that reintroduces our domain-specific glossary to help the LLM better understand our e-commerce terminology</li>
</ul>
<p><strong>Models:</strong> GPT-4o and GPT-4.1</p>
<p><strong>Evaluation Methods:</strong></p>
<ul>
<li><strong>Prompt-level</strong>: Raw model responses analyzed through a dedicated REST endpoint, with outputs compared against expected classification and translation results</li>
<li><strong>Application-level</strong>: Complete system testing using our golden dataset, measuring accuracy on a 0-1 scale by comparing returned product IDs against expected results</li>
</ul>
<hr>
</section>
<section id="results-and-discussion" class="level2">
<h2 class="anchored" data-anchor-id="results-and-discussion">Results and Discussion</h2>
<section id="prompt-level-evaluation-results" class="level3">
<h3 class="anchored" data-anchor-id="prompt-level-evaluation-results">Prompt-level evaluation results</h3>
<p>Our isolated prompt testing revealed promising results:</p>
<div id="fc8fbf3b" class="cell" data-execution_count="1">
<div class="cell-output cell-output-display cell-output-markdown" data-execution_count="1">
<table class="caption-top table table-sm table-striped small">
<colgroup>
<col style="width: 26%">
<col style="width: 28%">
<col style="width: 18%">
<col style="width: 26%">
</colgroup>
<thead>
<tr class="header">
<th style="text-align: left;">Prompt Version</th>
<th style="text-align: left;">Model</th>
<th style="text-align: left;">Pass Rate</th>
<th style="text-align: left;">Execution Time</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td style="text-align: left;">v25</td>
<td style="text-align: left;">gpt-4.1-2025-04-14</td>
<td style="text-align: left;">81.82%</td>
<td style="text-align: left;">14.26 seconds</td>
</tr>
<tr class="even">
<td style="text-align: left;">v25</td>
<td style="text-align: left;">gpt-4o</td>
<td style="text-align: left;">85.86%</td>
<td style="text-align: left;">15.98 seconds</td>
</tr>
<tr class="odd">
<td style="text-align: left;">v26</td>
<td style="text-align: left;">gpt-4.1-2025-04-14</td>
<td style="text-align: left;">93.33%</td>
<td style="text-align: left;">15.24 seconds</td>
</tr>
<tr class="even">
<td style="text-align: left;">v26</td>
<td style="text-align: left;">gpt-4o</td>
<td style="text-align: left;">91.72%</td>
<td style="text-align: left;">15.24 seconds</td>
</tr>
<tr class="odd">
<td style="text-align: left;">v27</td>
<td style="text-align: left;">gpt-4.1-2025-04-14</td>
<td style="text-align: left;">89.70%</td>
<td style="text-align: left;">14.74 seconds</td>
</tr>
<tr class="even">
<td style="text-align: left;">v27</td>
<td style="text-align: left;">gpt-4o</td>
<td style="text-align: left;">89.29%</td>
<td style="text-align: left;">17.57 seconds</td>
</tr>
</tbody>
</table>
</div>
</div>
<p><strong>Key findings:</strong></p>
<ul>
<li>GPT-4.1 matched or exceeded GPT-4o performance across all three prompt versions</li>
<li>Both v26 and v27 showed improved pass rates compared to the baseline</li>
<li>GPT-4.1 consistently achieved higher pass rates than GPT-4o for the new prompt versions</li>
</ul>
</section>
<section id="application-level-evaluation-results" class="level3">
<h3 class="anchored" data-anchor-id="application-level-evaluation-results">Application-level evaluation results</h3>
<p>When we tested the complete system using our golden dataset, the results were even more encouraging. We measured <strong>semantic classification accuracy</strong> by comparing actual outputs against our expected results.</p>
<div id="91d47b94" class="cell" data-execution_count="2">
<div class="cell-output cell-output-display cell-output-markdown" data-execution_count="2">
<table class="caption-top table table-sm table-striped small">
<thead>
<tr class="header">
<th style="text-align: left;">Prompt Version</th>
<th style="text-align: right;">Mean Score</th>
<th style="text-align: right;">Median Score</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td style="text-align: left;">v25</td>
<td style="text-align: right;">0.807</td>
<td style="text-align: right;">0.933</td>
</tr>
<tr class="even">
<td style="text-align: left;">v26</td>
<td style="text-align: right;">0.873</td>
<td style="text-align: right;">0.965</td>
</tr>
<tr class="odd">
<td style="text-align: left;">v27</td>
<td style="text-align: right;">0.873</td>
<td style="text-align: right;">0.965</td>
</tr>
</tbody>
</table>
</div>
</div>
<p><strong>Key findings:</strong></p>
<ul>
<li>The newer prompt versions (v26 and v27) significantly outperformed our baseline v25 prompt.</li>
<li>Importantly, v27 matched v26’s performance while providing better domain-specific understanding, giving us confidence in our prompt optimization approach.</li>
</ul>
<hr>
</section>
</section>
<section id="conclusion" class="level2">
<h2 class="anchored" data-anchor-id="conclusion">Conclusion</h2>
<p>Our comprehensive evaluation demonstrated that <strong>GPT-4.1 with prompt v27</strong> outperforms our current <strong>GPT-4o with v25</strong> setup, both in isolated testing and real-world application scenarios:</p>
<ul>
<li>Application accuracy improved from ~80.6% (v25) to ~87.3% (v27) on our golden dataset</li>
<li>GPT-4.1 achieved an impressive <strong>91.72% pass rate</strong> on our evaluation dataset</li>
</ul>
<p>This evaluation gave us the confidence to proceed with the migration, knowing we’re not just maintaining performance but actually improving it while benefiting from GPT-4.1’s enhanced speed and cost efficiency.</p>
<hr>
</section>
<section id="experimental-section" class="level2">
<h2 class="anchored" data-anchor-id="experimental-section">Experimental Section</h2>
<section id="raw-evaluation-data" class="level3">
<h3 class="anchored" data-anchor-id="raw-evaluation-data">Raw Evaluation Data</h3>
<p>For full transparency and reproducibility, all our evaluation data is available:</p>
<p><strong>Comparison Scores:</strong></p>
<ul>
<li><a href="data/application_level_evaluation_results.csv"><code>Application Level Evaluations</code></a></li>
<li><a href="data/prompt_level_evaluation_summary.csv"><code>Prompt Level Evaluations</code></a></li>
<li><a href="data/prompt_level_evaluation_full_summary.csv"><code>Prompt Level Evaluations Full Summary</code></a></li>
</ul>
<p><strong>Prompt Level Evaluations API Test Responses:</strong></p>
<ul>
<li><a href="data/first_run.zip"><code>First Run Results</code></a></li>
<li><a href="data/second_run.zip"><code>Second Run Results</code></a></li>
<li><a href="data/third_run.zip"><code>Third Run Results</code></a></li>
</ul>
<hr>
</section>
</section>
<section id="acknowledgments" class="level2">
<h2 class="anchored" data-anchor-id="acknowledgments">Acknowledgments</h2>
<p>Special thanks to the engineering and data teams at The Agile Monkeys for their meticulous work on evaluation design and implementation. Their rigorous approach made this confident migration possible.</p>


</section>

 ]]></description>
  <category>LLM</category>
  <category>evaluation</category>
  <category>production</category>
  <guid>https://labs.theagilemonkeys.com/posts/safe-migration-from-gpt4o_to_gpt41-model/</guid>
  <pubDate>Thu, 24 Jul 2025 00:00:00 GMT</pubDate>
  <media:content url="https://labs.theagilemonkeys.com/posts/safe-migration-from-gpt4o_to_gpt41-model/og-image.png" medium="image" type="image/png" height="76" width="144"/>
</item>
<item>
  <title>Improving Vector Search Recall by Fusing Multi-Modal Embeddings</title>
  <link>https://labs.theagilemonkeys.com/posts/improving-vector-search-recall-fusing-embeddings/</link>
  <description><![CDATA[ 




<p>In fashion e-commerce, neither image-only nor text-only embeddings capture the full spectrum of cues hidden in brand marketing language and product photography. We show that <strong>early fusion by concatenating a CLIP image vector with a large‑language‑model text vector lifts full‑ranking nDCG from 0.738 → 0.842 —</strong>a <strong>+14 %</strong> relative gain over the image baseline and <strong>+3 %</strong> over a strong text baseline—while a naïve weighted‑sum late‑fusion yields a smaller  +9 % lift. The recipe adds two lines of Python and no extra training, yet leaves ample headroom for supervised fine‑tuning and learned fusion.</p>
<section id="motivation" class="level2">
<h2 class="anchored" data-anchor-id="motivation">1&nbsp;Motivation</h2>
<p>A shopper types <strong>“oversized off‑white tee with crew neck”</strong>.</p>
<p>The catalogue, however, says:</p>
<ul>
<li>Colour: <em>stone</em> — not <em>off‑white</em></li>
<li>Style notes: <em>loose fit</em>, <em>boyfriend cut</em></li>
<li>Image: crew neck visible, but not part of the product information.</li>
</ul>
<p>Keyword search collapses; a single embedding model still misses half the picture.</p>
<p>The pragmatic question:</p>
<p><em>How far can we get by blending two complementary off‑the‑shelf encoders before investing in task‑specific training?</em></p>
</section>
<section id="experimental-setup" class="level2">
<h2 class="anchored" data-anchor-id="experimental-setup">2&nbsp;Experimental Set‑up</h2>
<table class="caption-top table">
<colgroup>
<col style="width: 50%">
<col style="width: 50%">
</colgroup>
<thead>
<tr class="header">
<th>Component</th>
<th>Specification</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>Catalogue</strong></td>
<td>proprietary fashion dataset, ≈ 10k SKUs</td>
</tr>
<tr class="even">
<td><strong>Image encoder</strong></td>
<td><code>laion‑CLIP‑ViT‑bigG‑14‑laion2B‑39B‑b160k</code></td>
</tr>
<tr class="odd">
<td><strong>Text encoder</strong></td>
<td><code>text‑embedding‑3‑large</code> over the concatenation of: • <code>name</code> • <code>description</code> • <code>product_type</code> • <code>primary_color</code> • <code>secondary_color</code></td>
</tr>
<tr class="even">
<td><strong>Similarity metric</strong></td>
<td>Brute-force cosine similarity (exact search)</td>
</tr>
<tr class="odd">
<td><strong>Evaluation</strong></td>
<td>nDCG over the <em>full</em> ranked list</td>
</tr>
<tr class="even">
<td><strong>Queries &amp; labels</strong></td>
<td>mixture of synthetic and expert‑verified relevance judgments</td>
</tr>
<tr class="odd">
<td><strong>Hardware</strong></td>
<td>Single workstation GPU (RTX 4090) for embedding; CPU for search</td>
</tr>
</tbody>
</table>
</section>
<section id="fusion-strategies" class="level2">
<h2 class="anchored" data-anchor-id="fusion-strategies">3&nbsp;Fusion Strategies</h2>
<section id="early-fusion-concatenation" class="level3">
<h3 class="anchored" data-anchor-id="early-fusion-concatenation">3.1&nbsp;Early Fusion (Concatenation)</h3>
<p>Early fusion <em>joins</em> the two embedding spaces end‑to‑end. If the image encoder lives in <img src="https://latex.codecogs.com/png.latex?%5Cmathbb%7BR%7D%5E%7Bd_1%7D"> and the text encoder in <img src="https://latex.codecogs.com/png.latex?%5Cmathbb%7BR%7D%5E%7Bd_2%7D">, the concatenated vector <img src="https://latex.codecogs.com/png.latex?z%20=%20%5Balpha%5C,%20%5Chat%20x,%5C,%20eta%5C,%20%5Chat%20y%5D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bd_1+d_2%7D"> is searched with plain cosine similarity.</p>
<p><strong>Why does this help?</strong></p>
<ul>
<li><strong>Complementary neighborhoods</strong> – an item that is close in <em>either</em> modality rises in the joint rank.</li>
<li><strong>Additive kernel view</strong>: cosine in the fused space is exactly <img src="https://latex.codecogs.com/png.latex?K%20=%20alpha%5E%7B2%7DK_%7B%20ext%7Bimg%7D%7D%20+%20eta%5E%7B2%7DK_%7B%20ext%7Btext%7D%7D"> the better kernel dominates automatically.</li>
<li><strong>Capacity bump</strong> – doubling dimensionality roughly doubles the linear decision surface, separating confounding categories such as <em>colour</em> versus <em>cut</em>.</li>
<li><strong>Noise averaging</strong> – distances coming from partly independent sub‑spaces reduce variance, lowering false negatives at fixed <code>k</code>.</li>
</ul>
<p>Amazingly, a default <img src="https://latex.codecogs.com/png.latex?alpha=eta=1"> already delivers most of the benefit.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> numpy <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> np</span>
<span id="cb1-2"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> faiss</span>
<span id="cb1-3"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> clip, torch, openai</span>
<span id="cb1-4"></span>
<span id="cb1-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># --- 1. encode ---------------------------------------------------------------</span></span>
<span id="cb1-6">device <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"cuda"</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> torch.cuda.is_available() <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"cpu"</span></span>
<span id="cb1-7"></span>
<span id="cb1-8"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Image embedding</span></span>
<span id="cb1-9">clip_model, clip_preprocess <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> clip.load(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"ViT-bigG-14"</span>, device<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>device)</span>
<span id="cb1-10"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> encode_image(path):</span>
<span id="cb1-11">    img <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> clip_preprocess(PIL.Image.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">open</span>(path)).unsqueeze(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>).to(device)</span>
<span id="cb1-12">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">with</span> torch.no_grad():</span>
<span id="cb1-13">        vec <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> clip_model.encode_image(img).squeeze().cpu().numpy()</span>
<span id="cb1-14">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> vec <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> np.linalg.norm(vec)</span>
<span id="cb1-15"></span>
<span id="cb1-16"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Text embedding (OpenAI)</span></span>
<span id="cb1-17"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> encode_text(text):</span>
<span id="cb1-18">    resp <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> openai.embeddings.create(</span>
<span id="cb1-19">        model<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"text-embedding-3-large"</span>,</span>
<span id="cb1-20">        <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">input</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>text,</span>
<span id="cb1-21">    )</span>
<span id="cb1-22">    vec <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.array(response.data[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>].embedding, dtype<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>np.float32)</span>
<span id="cb1-23">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> vec <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> np.linalg.norm(vec)</span>
<span id="cb1-24"></span>
<span id="cb1-25"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># --- 2. early fusion ---------------------------------------------------------</span></span>
<span id="cb1-26"></span>
<span id="cb1-27"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> concat_fusion(img_vec, txt_vec, alpha<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.0</span>, beta<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.0</span>):</span>
<span id="cb1-28">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> np.concatenate([alpha <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> img_vec, beta <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> txt_vec]).astype(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"float32"</span>)</span>
<span id="cb1-29"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Example</span></span>
<span id="cb1-30">img_vec <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> encode_image(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"sku_123.jpg"</span>)</span>
<span id="cb1-31">txt_fields <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [name, description, product_type, primary_color, secondary_color]</span>
<span id="cb1-32">txt_vec <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> encode_text(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" "</span>.join(txt_fields))</span>
<span id="cb1-33">z <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> concat_fusion(img_vec, txt_vec)</span></code></pre></div></div>
<p><em>Add to FAISS</em></p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb2-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># cosine because vectors are L2‑normalised</span></span>
<span id="cb2-2">index <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> faiss.IndexFlatIP(z.size)</span>
<span id="cb2-3"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># add all catalogue vectors</span></span>
<span id="cb2-4">index.add(np.stack([z, ...]))</span>
<span id="cb2-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># retrieve top‑50</span></span>
<span id="cb2-6">D, I <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> index.search(query_vecs, k<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">50</span>)</span></code></pre></div></div>
<p><strong>Engineering cost:</strong> two extra lines after the normalisation step.</p>
</section>
<section id="late-fusion-weighted-sum" class="level3">
<h3 class="anchored" data-anchor-id="late-fusion-weighted-sum">3.2&nbsp;Late Fusion (Weighted Sum)</h3>
<p>Late fusion keeps the dimensionality unchanged: the two vectors are <em>blended</em> in place.</p>
<p><img src="https://latex.codecogs.com/png.latex?z_%5Clambda%20=%20%5Clambda%5C,%5Chat%20x%20+%20(1-%5Clambda)%5C,%5Chat%20y%20,%5Cquad%20z_%5Clambda%5Cin%5Cmathbb%7BR%7D%5E%7Bd%7D."></p>
<p>The idea resides in taking advantage of the two-tower architecture of CLIP and fusing both the image characteristics and the product information characteristics using the same model.</p>
<p>Intuitively, this draws a straight line between the image and text points. If their nearest neighbors differ, the midpoint can end up <em>between</em> both relevant clusters, diluting precision.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb3-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> weighted_sum(img_vec, txt_vec, lam<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.5</span>):</span>
<span id="cb3-2">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> (lam <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> img_vec <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> (<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> lam) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> txt_vec).astype(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"float32"</span>)</span>
<span id="cb3-3"></span>
<span id="cb3-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># simple λ grid‑search on a held‑out set</span></span>
<span id="cb3-5">lams <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.linspace(<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.1</span>, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.9</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">9</span>)</span>
<span id="cb3-6">best_lam, best_score <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>, <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>np.inf</span>
<span id="cb3-7"></span>
<span id="cb3-8"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> lam <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> lams:</span>
<span id="cb3-9">    fused <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.stack([weighted_sum(iv, tv, lam) <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> iv, tv <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">zip</span>(img_vecs, txt_vecs)])</span>
<span id="cb3-10">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># user‑provided evaluation routine</span></span>
<span id="cb3-11">    score <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> evaluate_nDCG(fused) </span>
<span id="cb3-12">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> score <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> best_score:</span>
<span id="cb3-13">        best_lam, best_score <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> lam, score</span>
<span id="cb3-14"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"best λ = </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>best_lam<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:.2f}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)</span></code></pre></div></div>
<p>A light grid search usually nudges nDCG a couple of points above the naïve <code>λ = 0.5</code> setting, but in our catalogue <strong>still trails concatenation.</strong></p>
</section>
</section>
<section id="results" class="level2">
<h2 class="anchored" data-anchor-id="results">4&nbsp;Results</h2>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Method</th>
<th>nDCG (full)</th>
<th>Relative Δ vs.&nbsp;CLIP</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>CLIP image only</td>
<td><strong>0.738</strong></td>
<td>—</td>
</tr>
<tr class="even">
<td>text‑embedding‑3‑large only</td>
<td><strong>0.819</strong></td>
<td><strong>+11 %</strong></td>
</tr>
<tr class="odd">
<td>Weighted sum (λ ≈ 0.5)</td>
<td><strong>0.803</strong></td>
<td><strong>+9 %</strong></td>
</tr>
<tr class="even">
<td><strong>Concatenation (α = β = 1)</strong></td>
<td><strong>0.842</strong></td>
<td><strong>+14 %</strong></td>
</tr>
</tbody>
</table>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><a href="chart.png" class="lightbox" data-gallery="quarto-lightbox-gallery-1" title="Comparison diagram between the different embedding configurations"><img src="https://labs.theagilemonkeys.com/posts/improving-vector-search-recall-fusing-embeddings/chart.png" class="img-fluid figure-img" alt="Comparison diagram between the different embedding configurations"></a></p>
<figcaption>Comparison diagram between the different embedding configurations</figcaption>
</figure>
</div>
<p>Concatenation delivers the highest lift at the lowest implementation complexity.</p>
<section id="why-a-14-retrieval-quality-gain-matters-for-the-business" class="level3">
<h3 class="anchored" data-anchor-id="why-a-14-retrieval-quality-gain-matters-for-the-business"><strong>4.1 Why a +14 % retrieval-quality gain matters for the business</strong></h3>
<ul>
<li><strong>More relevant products surface earlier.</strong> nDCG measures how much of the ideal ranking you achieve. Moving from 0.738 to 0.842 means the search engine now recovers <strong>84 % of the best-possible ranking signal instead of 74 %</strong>—a 40 % reduction in “missed relevance”.</li>
<li><strong>Fewer customer drop-offs.</strong> When desired items appear near the top, users need fewer scrolls or query reformulations, reducing frustration and the likelihood of abandoning the session.</li>
<li><strong>Stronger merchandising leverage.</strong> Brand-specific colour names (<em>stone</em>, <em>ivory</em>) and visual cues (neckline, pattern) are matched more reliably, so manual synonym lists and pinning rules can be trimmed back.</li>
<li><strong>Scalable benefit.</strong> Because the fusion method adds no latency-critical computation at query time, the quality gain scales to peak traffic without extra infrastructure cost.</li>
</ul>
<p><strong>Takeaway:</strong> the +14 % relative nDCG boost tells you that search relevance has improved materially; the exact impact on revenue or engagement will depend on your funnel metrics and should be validated through a controlled A/B test.</p>
</section>
</section>
<section id="next-steps" class="level2">
<h2 class="anchored" data-anchor-id="next-steps">5 Next Steps</h2>
<ul>
<li><strong>Domain-specific fine-tuning:</strong> Gather a modest set of (query, positive, negative) triplets from your production logs or a small annotation sprint. Apply LoRA adapters (or similar lightweight methods) to the CLIP image tower while freezing the text encoder. Even three epochs on ≈ 1–2 % of the catalogue often yield an extra single-digit nDCG lift, enough to justify the few GPU-hours required.</li>
<li><strong>Adaptive weighting:</strong> Replace the fixed α, β scaling factors with a two-layer MLP that predicts modality weights from the query text alone (e.g., longer colour-heavy queries lean toward the image slice). Train the MLP with pairwise ranking loss against your triplets. Early sandbox tests show +1–2 % nDCG over constant weights with negligible runtime cost.</li>
<li><strong>End-to-end dual-tower:</strong> Train the image and text encoders together from scratch (or from CLIP-like checkpoints) with a shared projection head so that their <em>concatenation</em> is directly optimised for your ranking loss. The result stays ANN-friendly while baking domain semantics into both towers. Expect the biggest payoff once you control tens of thousands of labelled pairs.</li>
<li><strong>Memory optimisations:</strong> At 3 840-D (CLIP + text-embeddings-3-large) you’re storing ~15 kB per item uncompressed. Orthogonal Product Quantisation or a 95 %-variance PCA cut reduces storage by an order of magnitude and slashes RAM/IO, typically at &lt; 1 % quality loss. Combine with HNSW-PQ for sub-30 ms latency on million-scale catalogues without a GPU.</li>
</ul>
<hr>
</section>
<section id="conclusion" class="level2">
<h2 class="anchored" data-anchor-id="conclusion">6&nbsp;Conclusion</h2>
<p>Early fusion by simple vector concatenation yields a <strong>+15 %</strong> retrieval boost in fashion e‑commerce with almost no engineering effort. Late‑fusion weighted sums help, but less so; supervised fusion and fine‑tuning remain promising avenues once the “trivial concatenation” gain is banked.</p>
</section>
<section id="references" class="level2">
<h2 class="anchored" data-anchor-id="references">References</h2>
<ol type="1">
<li>Sivic, J. &amp; Zisserman, A. <em>Video Google: A Text Retrieval Approach to Object Matching</em>. ICCV&nbsp;2003.</li>
<li>Lanckriet, G. et al.&nbsp;<em>Learning the Kernel Matrix with Semidefinite Programming</em>. JMLR&nbsp;2004.</li>
<li>Gönen, M. &amp; Alpaydın, E. <em>Multiple Kernel Learning Algorithms</em>. JMLR&nbsp;2011.</li>
<li>Radford, A. et al.&nbsp;<em>CLIP: Contrastive Language–Image Pre‑training</em>. 2021.</li>
<li>OpenAI. <em>text‑embedding‑3 models</em>. 2024</li>
<li>Zhu, X., Huang, S.-W., Ding, H., Yang, J., Chen, K., Zhou, T., et al.&nbsp;(2024). B<em>ringing Multimodality to Amazon Visual Search System</em>. arXiv preprint arXiv:2412.13364.</li>
<li>Superlinked Research Team. (2024, February 21). <em>Retrieval from Image and Text Modalities.</em> VectorHub by Superlinked.</li>
</ol>


</section>

 ]]></description>
  <category>RAG</category>
  <category>e-commerce</category>
  <guid>https://labs.theagilemonkeys.com/posts/improving-vector-search-recall-fusing-embeddings/</guid>
  <pubDate>Thu, 24 Jul 2025 00:00:00 GMT</pubDate>
  <media:content url="https://labs.theagilemonkeys.com/posts/improving-vector-search-recall-fusing-embeddings/og-image.png" medium="image" type="image/png" height="76" width="144"/>
</item>
</channel>
</rss>
