Using AI-assisted software development is a game changer. No doubt. But as I continue to use it, I feel my intuition and problem solving abilities start to wane with each prompt and completion. And while I don’t think I’ve forgotten things, it’s more of the fact that I’m getting rusty and need to build back up those problem solving muscles.
To help me through this, I’m working through the How to Solve It with Code course from Jeremy Howard and Answer.ai. It’s based on the How to Solve It mathematics teaching methodology popularized by George Polya in the 20th century. In essence, it’s a framework for breaking down complex problems into digestible and testable chunks and iterating through the process until we get to a correct answer. It helps take overwhelming problems and make them manageable for someone like me that might have been out of practice or just a bit rough around the edges. I’ve been a manager for close to the last 6 years and recently transitioned back to being an individual contributor. So this How to Solve It course is a real nice way for me to warm back up my problem solving muscles and get out of the strategy and higher level meetings that I’ve been involved with.
Let me walk through one example from the course that really drove this home for me. Instead of just using an AI library’s built-in tool loop, I built one from scratch. The process of breaking it down step-by-step helped me understand not just what was happening, but why.
One of the first problems in the course was figuring out how to create a method for an AI to use tools and answer prompts in an agentic way. The course introduces us to a library called claudette that has this built in, but part of the homework was to build this from scratch. I’ve used these tool calling loops in the past through AI-assisted development, but never took the time to write one myself. I’m going to write how this works from my current understanding and then show the complete method using the claudette library for simple things like sending the prompt to Anthropic. Everything else should be built from basic building blocks.
First, we need to instantiate the client using an Anthropic model and pass in an initial prompt and our available tools. Tools are nothing more than deterministic functions such as adding or multiplying two numbers together:
c = Client(model)
r = c(pr, tools=tools)
After that, we’ll want to setup our chat history to keep track of our interactions with the model. This history consists of messages or objects of each send and response type. We have roles of ‘user’ for the user sending the message and ‘assistant’ for the response from the model itself. As we iterate through our loop, we’ll add to this object.
h = [
{'role' : 'user', 'content': pr},
{'role' : 'assistant', 'content' : r.content }
]
Next, we can start our loop. Each response, r has a stop_reason. The stop_reason helps us understand if we need to continue to loop through our interaction with the model. If the stop reason is tool_use, then it’s the model saying “Hey, I stopped because I need to get a result of the tool.” We’ll also collect our tool use blocks from the model in a new list called tubs, short for tool use blocks. A tool use block is a response from the model that says “Hey! I saw that I need to use a tool that I have available to me. So here’s what I think. I identified that we have a tool named ‘multer’ that multiplies two numbers together and I noticed we have two numbers that we need to multiply. I’ll give you the name of the tool and a dictionary containing the parameters we need to pass. I want you to run this through your method and give me the result back as part of your next message to me.”
A tool use response might look like: ToolUseBlock(id='toolu_01BG79yAK8z9TbRTXzPzgBqZ', input={'a': 42427928, 'b': 548749}, name='multer', type='tool_use')
while r.stop_reason == 'tool_use':
tubs = [item for item in r.content if item.type == 'tool_use']
For each tub, we’ll use the tool it wants to use and the paramaters it wants to pass and collect that in a new list of tool result content objects named trcs = []
# Create a list to store all our tool result content objects
trcs = []
# Loop through each tool use block the model requested
for tub in tubs:
We can extract the name of the tool, the id, and the parameters from our tub object and use them to get a deterministic result that we can save in a trc object:
trc = {
"type": "tool_result",
"tool_use_id" : tub.id, # Links this result to the tool request
"content" : str(globals()[tub.name](**tub.input)) # The actual result
}
trcs.append(trc)
Once we have our Tool Result Content, we can append that back to the history object we created earlier to include a new message after the initial response that says “I ran the tools requested, and here are the results”
h.append({'role' : 'user', 'content' : trcs})
r = c(h, tools=tools)
We’ll then finally append our result back from the model to our history object and restart the loop. If the latest result from the model has a stop_reason of tool_use then we’ll repeat the process. If not, we’ll return our final result for the initial prompt we passed in.
The final function might look something like this:
def do_tool_loop(pr, tools, model):
"""
A function that implements a tool loop for interacting with an LLM.
Args:
pr: The prompt/question we're asking the model
tools: List of functions the model can use
model: The model version to use
"""
# This sets up our communication to Anthropic using the Claudette library
c = Client(model)
# Here we're calling our client and passing our prompt and tools array,
# letting the model know what tools we have available to use
r = c(pr, tools=tools)
# We're setting up our chat history dictionary that consists of messages or objects
# of each send and response type. Here we have roles of 'user' for the user sending
# a message and 'assistant' for the response object from the model.
# We'll build on top of this as we progress in our loop.
h = [
{'role' : 'user', 'content': pr},
{'role' : 'assistant', 'content' : r.content }
]
# Each response has a stop_reason. This stop reason helps us understand if we need
# to continue to loop through the interaction with the model. If the stop reason is
# 'tool_use', the model is basically saying "I stopped because I need the result of
# a tool before I can continue."
while r.stop_reason == "tool_use":
# Filter through r.content to find all items where type == 'tool_use'
# This handles cases where the model might request multiple tools at once
tubs = [item for item in r.content if item.type == 'tool_use']
# Create a list to store all our tool result content objects
trcs = []
# Loop through each tool use block the model requested
for tub in tubs:
# The Tool Result Content is an object that we can pass back to the model.
# It gets the result of the requested tool. We use globals()[tub.name](**tub.input)
# as a way to basically say "run the function with this name (e.g. multer())
# and spread out the parameters we got from our input object (in this case a and b)."
# The tool_use_id matches this result back to the specific tool request.
trc = {
"type": "tool_result",
"tool_use_id" : tub.id, # Links this result to the tool request
"content" : str(globals()[tub.name](**tub.input)) # The actual result
}
trcs.append(trc)
# We can update our history to now include a new message after the initial response
# from the model that says "I ran the tool(s) you wanted me to and here are the values."
h.append({'role' : 'user', 'content' : trcs})
# We'll pass the entire history object including the latest tool run(s) back to the model
r = c(h, tools=tools)
# We take the response from the model and add that back to our history object.
# If the response contains a stop reason of 'tool_use', we'll repeat the entire loop.
# If not, we'll stop our loop and display the last result.
h.append({'role' : 'assistant', 'content' : r.content})
# Display and return the final response when we're done
display(r)
return r
So this is not a very complicated function, but it did require a lot of thinking to break down and solve. I’ve used AI-assisted functions like this previously, but by taking the time to fully understand what’s happening behind the magic I can better understand what’s happening and debug if something goes wrong.
I’m still not 100% convinced we need to understand things at this level of detail all of the time. But doing so with my current levels of managerial rust built up has been both challenging and ultimately rewarding.
I believe that using tools like Claude Code and Codex CLI are the future of software development. But I also believe that we need to keep our problem solving muscles sharp or we’ll become too reliant on AI and quickly slip into a WALL-E like future where we’re too reliant on machines to do the thinking for us.