The Lego Model: How Tool Calling Changes the Way We Build AI Applications

“What should I read next?” seems like a simple question. But answering it well requires knowing what the user is currently reading, what they’ve finished recently, and what’s already on their list. When I built QuietReads, a book tracking app with an AI assistant, I faced a choice: pre-load all this context on every request (expensive), build complex routing logic to fetch the right data (brittle), or find a different approach entirely.

In those post, I talk about why I chose this option, the architectural implications, and the tradeoffs involved. 

Conversational Interfaces Are Not Deterministic

A lot of mid-career technologists like me still think deterministically. We reach for decision trees, map out every probable permutation, and write test cases to cover each branch. This works well for forms and structured APIs where you control the inputs.

But QuietReads has a chat interface. Users might ask:

  • “What should I read next?”
  • “I’m in the mood for something like the last book I finished, but shorter”
  • “What were my thoughts on that dystopian novel from last month?”
Asking the QuietReads assistant to recommend some books

Each query requires different context. The first needs the user’s want-to-read list. The second needs their recently finished books plus some understanding of “shorter.” The third requires searching through their notes. I couldn’t predict which context any given question would need, and I didn’t want to fetch everything every time.

The traditional approach would be routing logic. For just the first query, you might write something like:

def get_context_for_recommendation(message, user_id):
    context = {}

    if contains_recommendation_intent(message):
        context['want_to_read'] = get_want_to_read_books(user_id)
        context['recently_finished'] = get_recently_finished(user_id)

        if mentions_specific_book(message):
            book = extract_book_reference(message)
            context['book_details'] = get_book_details(book)

    # ... and this continues for every intent type
    return context

This gets unwieldy fast. Each new question type requires new routing rules. The intent detection functions themselves need maintenance. And you’re constantly guessing what context the model will need.

LLMs allow us to use a different mental model. 

Think of LLMs as expert Lego assemblers. You provide a curated set of bricks (tools), an instruction manual (your system prompt), and let the assembler determine which bricks to use and in what order. You don’t hand them every brick in existence. You give them the right pieces for the task and clear guidance on when to use each one.

Tools as Building Blocks

In QuietReads, I define “context tools” that let the AI retrieve user data as needed:

CONTEXT_TOOLS = [
    {
        "name": "get_user_profile",
        "description": (
            "Get the user's name and reading preferences. Use this when you need to "
            "personalize your response or discuss their reading interests."
        ),
        "input_schema": {"type": "object", "properties": {}}
    },
    {
        "name": "get_want_to_read",
        "description": (
            "Get books on the user's want-to-read list. IMPORTANT: Always call this "
            "BEFORE recommending any books to avoid suggesting books they already have."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "limit": {"type": "integer", "description": "Max books to return (default: 10)"}
            }
        }
    }
]


Each tool has a clear description of when to use it. The model reads these descriptions and decides which tools to call based on the user’s question.

The Agentic Loop

When the model decides to use a tool, we handle that request, execute the tool, and feed the results back. This creates a loop (simplified code below):

async def execute(self, system, messages, tools, tool_handlers, max_tokens=2048):
    response = self.client.messages.create(
        model=self.model,
        max_tokens=max_tokens,
        system=system,
        messages=messages,
        tools=tools
    )

    while response.stop_reason == "tool_use":
        tool_results = []

        for block in response.content:
            if block.type == "tool_use":
                tool_name = block.name
                tool_input = block.input

                # Execute the tool and capture result
                result = await tool_handlers[tool_name](tool_input)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": result
                })

        # Feed results back and get next response
        messages.append({"role": "assistant", "content": response.content})
        messages.append({"role": "user", "content": tool_results})
        response = self.client.messages.create(...)

    return response

The model might call multiple tools, or call the same tool with different parameters, or decide it has enough context after the first call. 

The loop continues until the model has everything it needs to answer. In practice, you should also enforce a maximum iteration count as a guardrail against runaway loops or unexpectedly expensive queries. When the model requests multiple tools in a single response, you can execute them in parallel for performance gains.

This code handles the happy path. Production implementations need additional safeguards: error handling when tools fail or timeout, validation of tool inputs before execution, and graceful handling when the model hallucinates a tool name that doesn’t exist.

Client and Server-side Tools

QuietReads uses two categories of tools. Client-side tools are functions I implement: when the model calls get_want_to_read, my code queries the database and returns formatted results.

Server-side tools are capabilities the AI provider offers (like the web_search tool used below). 

I enable web search so the assistant can look up recent book releases or author news. Anthropic’s infrastructure handles the search; I just control when and how it’s available:

tools:
  web_search:
    type: "web_search_20250305"
    name: "web_search"
    server_side: true
    enabled: true
    config:
      max_uses: 5  # Limit searches per request

With some prompt engineering, the model can integrate custom database queries with real-time web searches, producing responses that feel coherent to the user. 

The system prompt guides how the model synthesizes information from different sources, when to cite web results versus personal reading history, and how to maintain a consistent voice across tool-augmented responses.

Architectural Implications

It is important to recognize where determinism matters and where it doesn’t. Each tool is a testable piece of code. I can unit test get_want_to_read in isolation, verify it returns the right data, and trust it to behave consistently. What I can’t fully predict is which tools the model will call or in what order. In my work, I have found that even cheaper models like the Haiku family of models do a decent job at tool use.

This separation has practical implications. Tool descriptions are instructions the model uses to decide when to call each tool. Writing clear, specific descriptions is as important as the implementation itself. Instead of pre-loading everything a user might need, I provide minimal context upfront and let the model request more, keeping initial requests fast and reducing token costs.

And while the model chooses its tools, I still control the boundaries. QuietReads runs input guardrails before messages reach the assistant and can validate outputs before returning them to the user.

The Tradeoffs

Tool calling introduces real costs that you should weigh against your specific requirements.

  • Predictability. With static context, you know exactly what data the model sees on every request. With tool calling, the model decides what to retrieve. This makes cost and performance harder to predict. A simple question might resolve in one API call; a complex one might trigger four tool calls and five round-trips.
  • Prompt caching. Static context can benefit significantly from prompt caching, where repeated system prompts are stored and reused. Dynamic tool results change with each request, which can reduce or eliminate caching benefits. Depending on your usage patterns, this could meaningfully impact both latency and cost.
  • Quality assurance. Unit testing individual tools is straightforward, but testing the system end-to-end becomes harder. The model might call tools in unexpected combinations, or skip tools you expected it to use. Comprehensive evaluations become essential because tool calling adds non-determinism to the critical path. I’ll write more about evaluation strategies in a future post.
  • Refactoring risk. IDE tooling can automatically update function signatures across a codebase. Tool definitions live in JSON objects that describe behavior and parameters in natural language. If you change a tool’s behavior or modify its parameters, automated refactoring won’t catch the JSON definitions, and the mismatch may not surface until production. LLM-based coding agents like Claude Code handle this well, and adding tool-specific checks to code review agents helps catch these issues.
  • Latency. Each iteration of the agentic loop requires a round-trip to the API. For QuietReads, this is acceptable. For applications where response time is critical, the additional latency may be a dealbreaker.

That said, tool calling offers real advantages beyond flexibility. Token costs can decrease because the model only retrieves data it actually needs. Direct tool calls avoid the protocol overhead of intermediary layers like MCP servers. And tools create a clean separation of concerns: database migrations, API upgrades, or new data providers can happen without touching the prompt.

This pattern fits QuietReads: read-only tools, flexible latency, and context costs that exceed API overhead. Applications with side effects, strict latency, or predictable context needs may want different approaches.

Navigating a Mindset Shift

Building with tool calling requires accepting the risk of non-deterministic code execution. You cannot predict every code path. Instead of mapping out decision trees, you’re designing capabilities and constraints. You’re giving the model a well-stocked toolbox and clear guidance, then trusting it to assemble the right response.

You control what tools exist, what data they access, what the model knows about when to use them, and what guardrails prevent misuse. The model handles the dynamic orchestration that would otherwise require hundreds of lines of if/else chains.

For those of us who’ve spent years thinking in flowcharts, this shift takes practice. But once it clicks, you start asking different questions: not “what are all the paths a user might take?” but “what capabilities does the model need, and how do I describe when to use them?”

Do agentic coding tools make us less productive?

Are agentic coding tools like Cursor making us less productive?

An interesting study from METR (HT The Pragmatic Engineer newsletter) found that use of LLM-assisted coding tools like Cursor made some experienced developers *less* productive than before. The catch? Most of these developers had used Cursor for less than 50 hours.

I’ve been vibe coding a webapp as my summer project using Claude Code and Cursor, and this finding resonates with my experience.

Here’s what I think is happening:

➡️ IDEs are incredibly complex tools with steep learning curves
Fifty hours isn’t nearly enough time to develop the muscle memory needed for effective coding with any new IDE. I’ve stuck with IntelliJ for years precisely because of this learning curve investment, and even now when I’m not evaluating Cursor, I use Claude Code with IntelliJ.

➡️ AI tools are black boxes in ways that aren’t obvious
You might assume Cursor simply passes what you type to the underlying LLM, but these tools use code search, context compaction, and LLM routing behind the scenes. What you type gets significantly modified before reaching the AI, making it nearly impossible to develop a mental model of what’s happening. Cursor updates far more frequently than any IDE I’ve used, and those “optimizations” can change behavior without warning.

➡️ Agentic workflows break the flow state
It’s easy to get distracted when your AI agent is off deciding what file to change next. By the time the agent is done, I’ve often forgotten what I was working on. I honestly don’t understand how people running multiple instances of Claude Code stay productive.

📝 My experience so far
The magic moments when Claude Code one-shots a complex solution are incredible. But like I discovered while switching from Replit’s authentication to Supabase, it can be a grind. The auth switch seemed to work perfectly at first, but broke other parts of my app in subtle ways. Later, migrating the database to Supabase was a disaster – I forgot to update my CLAUDE.md file with the new libraries, so Claude Code got confused and burned through tokens trying to figure out my setup. Technically user error, but exactly the kind of mistake you only learn to avoid through experience.

I expect my productivity will improve significantly as I develop better workflows around these tools. Until then, vibe coding feels like being a new parent – periods of genuine frustration interspersed with moments of sheer amazement.

Book Review – A Philosophy of Software Design by John Ousterhout

“A Philosophy of Software Design” by John Ousterhout is a short and thought-provoking book about practical software development.

Key Concept

The book starts with a bold claim – the most critical job of a software engineer is to reduce and manage complexity.

Mr. Ousterhout defines complexity as “anything related to the structure of a software system that makes it hard to understand and modify the system.”

This definition serves as a motivating principle for the book. The author explores where complexity comes from and how to reduce it in a series of short chapters, which often include real-world code examples.

My well-thumbed copy of the book

Summary

The book starts with identifying the symptoms of complexity:

  1. The difficulty in making seemingly simple changes to a system.
  2. Increasing cognitive load – i.e., a developer’s ability to understand a system’s behavior.
  3. The presence of “Unknown unknowns” – undocumented and non-obvious behavior.

Mr. Ousterhout states that there are two leading causes of complexity in a software system:

  1. Dependencies – A given piece of code cannot be understood or modified in isolation
  2. Obscurity – When vital information is not apparent. Obscurity arises due to a need for more consistency in how the code is written and missing documentation.

To reduce complexity, a developer must focus not only on writing correct code (“Tactical Programming”) but also invest time to produce clean designs, and effective comments and fix problems as they arise (“Strategic Programming”).

The book provides several actionable approaches to reducing complexity.

Some highlights:

  • Modular design can help encapsulate complexity, freeing developers to focus on one problem at a time. It is more important for a module to have a simple interface than a simple implementation.
  • Prevent information leakage between modules and write specialized code that implements specific features (once!).
  • Functions (or modules) should be deep – and developers should prioritize sound design over writing short and easy-to-read functions.
  • Consider multiple options when faced with a design decision. Exploring non-obvious solutions before implementing them could result in more performant and less complex code.
  • Writing comments should be part of the design process, and developers should use comments to describe things that are not obvious from the code.

The book concludes with a discussion of trends in software development, including agile development, test-driven development, and object-oriented programming.

Conclusion

“A Philosophy of Software Design” is an opinionated and focused book. It provides a clear view of the challenges of writing good code, which I found valuable.

Mr. Ousterhout provides actionable advice for novice and experienced developers by focusing on code, comments, and modules.

However, the book is also relatively low-level. The book contains little discussion around system design, distributed systems, or effective communication (outside of good code and effective comments).

While books such as “The Pragmatic Programmer” provide a more rounded approach to software engineering, I admire that Mr. Ousterhout sticks to the core concepts in his book.

Natural Language Processing Made Easy with GPT-3

Natural Language Processing or NLP is a catch-all term for making sense of unstructured text-like data. Google search recommendations, chatbots, and grammar checkers are all forms of NLP.
This is a field with many years of research. But, for the last 5-7 years, machine learning has reigned supreme. 

Five years ago, machine learning approaches to NLP were labor intensive. Success meant having access to large amounts of clean and labeled training data that would train ML models. A text summarization model would be pretty different from one that did sentiment analysis. 

The development of large language models or LLMs has revolutionized this field. Models like GPT-3 are a general-purpose tools that can be used to do several different tasks with very little training.

To show GPT-3 in action, I built a tiny slack bot that asks some questions and uses GPT-3 to generate actions. The video below is a demo of the bot and also an explanation of how to prompt GPT-3 to do NLP tasks.

George Saunders on Feedback

Feedback is an integral part of working in a team and managing people.

Code reviews, architectural reviews, 1-1s, and Sprint Retrospectives, are all situations that involve giving (and receiving) feedback as software engineers, product managers, and engineering managers. Yet, giving critical feedback can be a difficult and stressful experience. So how best to navigate these potentially adversarial situations?

George Saunders is one of my favorite contemporary writers. He has an excellent Substack called Story Club. In this week’s post, Saunders talks about giving feedback to other writers. While his advice is in the context of a writers workshop, I found it quite applicable to my work.

Saunders advices us to give specific yet kind feedback:

.. as we learn to analyze and diagnose with increased specificity and precision, the potential for hurt feelings diminishes, because we are offering specific, actionable ways (easy ways, often, ways that excite the writer, once she’s made aware of them) to make the story better. And who doesn’t want some of that?

George Saunders

Giving constructive or critical feedback is integral to working as a software engineer. Yet, these conversations can become challenging. 

One might be tempted not to say anything or speak in the most generic and broad terms to avoid offense. Instead, as Saunders suggests, the focus should be on giving thoughtful, specific, precise, and actionable feedback:

In this [giving feedback], we indicate that we are on the writer’s side, we are rooting for her and are glad to have found these small but definite ways to make her story better. There’s no snark, no competition, no dismissiveness, nothing negative or accusatory about it; just the feeling that we, her readers, are coming together with her, the writer, by way of craft. We’re all on the same team, the team of art.

George Saunders

Not much more to add is there?

On crypto outages

What to do when your decentralized, scalable, performant blockchain turns out to be not so scalable, sort-of-centralized and not so performant? 

Crypto’s selling point is robustness that is built on decentralization. No single points of failures should mean no downtime right? Right?

Turns out, crypto’s weaknesses are same as those of other, more mundane technologies. Bad code, bad actors and the fact that building scalable, distributed (and decentralized) systems is hard!

Solana, a Layer 1 blockchain, suffered a long outage over the weekend. This happened when the crypto markets are melting down.. 

Solana is supposed to be the answer to Ethereum’s performance and scalability issues. And yet, Solana has been plagued by performance issues and outages over the last few months. 

This weekend’s issue was caused by “program cache exhaustion” due to “excessive duplicate transactions”. Solana developers released an emergency patch to resolve this issue and begged every validator to upgrade.

Where there is code, there are bugs.. 

Welcome to the brave new world, where the problems are the same as the ones in the old world. They just cost you a lot of funny money.

On Roblox’s Outage

Roblox is one of the world’s biggest game platforms. With over fifty million daily users, it is a wildly popular platform to build and play games. 

In October last year, they had an outage where the entire platform was down for over 72 hours. This was all over the news at the time..

Today, Roblox published a post mortem about the incident. It is fascinating reading for anyone interested in distributed systems, DevOps, and Engineering (link below). I will write up a more detailed note in a couple of days.

Summary
– The outage was due to an issue in their service discovery infrastructure which is implemented in Consul
– Roblox is deployed on-premise(!!) on 18,000 servers which run 170,000 service instances
– These services rely on Consul (from HashiCorp) for service discovery and configuration
– An upgrade to Consul and the resulting switch to the way services interact with Consul lead to a cascading set of failures resulting in the outage

Some Initial Thoughts
– Distributed systems are hard, and the use of service-oriented architectures come with costs of coordination and service discovery
– Microservice architectures do not reduce complexity, just move it up a layer of abstraction
– The complexity of the modern software stack comes not just from your code, but also from your dependencies. 
– Leader election is one of the hardest problems in Computer Science 🙂 

On Modern Web Development

Tom MacWright wrote a much discussed article on the state of modern web development a couple of weeks ago.

He states that the current default of building a Web Application as a SPA using React, or something similar, in the frontend and an API on the backend is overkill.

This is both an opinionated survey on the current state of web development and an extremely contrarian take on current accepted wisdom on how to build modern web applications. It’s worth a read.


His premise is that we are adding additional levels of abstraction such as Virtual DOMs, Server Side Rendering, Bundle Splitting, etc. He even goes on to say that trying to generic, purely REST-ful APIs is a bit like tilting at windmills, since we end up having them tightly coupled with frontend code anyway.

I am sympathetic to this point of view — mainly because I am rapidly transiting to “grumpy old man” phase of my career, and I find modern web development workflow terrifying complex. I had to work on a React application a couple of years ago. It made me want to run back into the warm embrace of Java Swing development (oh beautiful Java Swing).

The pure beauty of Java Swing

But, I also want to note that software engineering is pretty fashion driven where we bounce around between too much and too little abstraction. Every generation has this battle — Java didn’t have enough features so we ended up with Java EE, Java EE was too bloated so we ended up with Spring, the Java language was too complex so we built Go. Go is missing Generics so we will go ahead and add them. The pendulum keeps swinging and we have a complexity crusade every 5–7 years or so. Unsurprisingly, it has been just about 5 years since React has become the defacto way of building web applications. Time for the backlash!

I am sure we are going to be back at folks writing pure HTML and FTP-ing to an Apache server day now 😉

4 Waves of AI – And why they matter

I can’t open a newspaper or visit my friendly local bookstore without coming across a think piece about why AI is a *BIG DEAL* and how it changes everything. The tone of most of the material that I have come across is aptly summed up in this classic xkcd panel.


Classic xkcd panel on AI

In January 2019, I read Kai-Fu Lee’s fantastic book “AI Super-Powers: China, Silicon Valley, and The New World Order.” Mr. Lee is a thoughtful, even-handed guide to what is going on in the field of Artificial Intelligence (specifically Machine Learning) and how it may impact our future. The book is also an eye-opening account of the Chinese startup eco-system — but perhaps more on that another day.

Early in the book, Mr. Lee talks about how the spread of AI is happening in four waves. These waves are:

  1. Internet AI
  2. Business AI
  3. Perception AI
  4. Autonomous AI

Let’s take quick a look at each of these waves.


Internet AI

We deal with Internet AI every time we shop online, scroll through our social media feeds or Google something. From AI Superpowers:

Internet AI is mainly about using AI algorithms as recommendation engines: systems that learn our personal preferences and then serve up content hand-picked for us.

Examples of Internet AI include online advertising optimization, personalized news feeds, and algorithmic content recommendation.


Business AI

Advances in machine learning have allowed businesses to take advantage of labeled, structured data that resides in data repositories and train algorithms to outperform humans on clearly defined optimization tasks. Some examples here include automated credit scoring, fraud detection, algorithmic trading, and supply chain optimization. While not the most exciting topic, in the short term, Business AI has the potential to have a significant impact in the way we work and more potently, what *types of work* make sense to automate.

Business AI is about optimising and generating value from structured data.

Business AI has the potential to make what were once stable professions like accountancy, insurance, and medicine obsolete in their current form. It also has the potential to generate vast and lucrative new opportunities. More on this later.


Perception AI

Perception AI is about the “Digitisation of the physical world.” It is about using real-world data captured from IoT devices, cameras, smartphones, and other devices to blur the lines between the online and offline worlds. We already see applications of facial recognition and machine translation technology enhance offline experiences such as shopping and travel as well as enrich experiences such as education.

Perception AI is about blurring the lines between the online and offline world

Augmented reality (AR) devices and applications increase merging of the offline and online world. Perception AI also has worrying implications around surveillance, privacy and data protection.


Autonomous AI

Autonomous AI represents the culmination of the three preceding waves of AI. What was once science fiction is slowly becoming mundane. Autonomous AI is about fusing the ability to optimize from extremely complex datasets and integrate them with powerful sensory abilities resulting in machines that can understand and shape the world around them.

Autonomous AI results in machines that can understand and shape the world around them.

We already see some limited applications of Autonomous AI in the fields of self-driving cars, automated factories and pollinators.


What does it all mean?

Ben Evans, a partner at the storied VC firm Andreessen Horowitz, talks a little about the implications of advances in AI in the November 2018 presentation “The End of the Beginning”. He says:

“Tech is building different kinds of businesses, and so will take different shares of that opportunity, but more importantly change what those industries look like.“

He says further that a combination of high internet penetration, changing consumer expectations and a general “unbundling” of supply chains are creating business models that in turn are enabled and accelerated by AI. The breaking apart of tightly coupled logistics supply chains is just one example of this phenomenon.

At my work with Jeavio’s portfolio companies, I can already see this in action. We support entrepreneurs who are working in diverse fields such as customer experience analytics, construction and high tech agriculture. In each of these various fields, we see applications of Business AI that have the potential to disrupt existing models and generate tremendous value.

In my previous career working in high-frequency algorithmic trading, I have seen technology disrupt financial markets. Advances in AI are now doing the same in a wide variety of fields.

While AI cannot by itself generate new business models, it is already a potent force multiplier, which when deployed effectively, can increase efficiency and help businesses capture more value. We may not worry about our Robot Overlords just yet; we should keep an eye on the disruption and opportunities presented by the four waves of AI.

Do you know your dependencies?

A contributor on GitHub finds an abandoned, but popular JS library and commits code that targets a Bitcoin wallet made by a particular company. Hundreds of other libraries use this library making this vulnerability affect thousands of applications since it is a transitive dependency.


Photo by Bryson Hammer on Unsplash

NPM (and npmjs.com) provide a valuable service in hosting JavaScript dependencies. By blindly upgrading to latest version of libraries, developers can open themselves to malicious attacks similar to those described below.

I would recommend developers understand how npm’s package lock mechanism works. This will ensure that your dependencies are reproducible and force the use of known and trusted modules instead of downloading the latest version.

This is not a problem just with the JavaScript eco-system. Python (via pip or conda) and Java (via maven & gradle) have similar issues. However my, totally subjective and un-scientific, observation is that JavaScript libraries tend to have way more dependencies (see the “left-pad” debacle for example)..

Ars Technica has a good write up about this particular issue: https://arstechnica.com/information-technology/2018/11/hacker-backdoors-widely-used-open-source-software-to-steal-bitcoin/