Notes from the long road of learning

[
[
[

]
]
]

When most people start using LLMs, they build a chatbot.

You type something.

The model replies.

You ask again.

It replies again.

At first, this feels impressive. It can explain code, summarize notes, rewrite emails, and answer questions in a way that feels smart and useful.

But after some time, one limitation becomes very obvious.

A chatbot can talk, but it cannot actually do anything.

If I ask, “What is the weather in Bangalore right now?”, a normal chatbot may still answer. But unless it has access to a tool or some external system, it is only guessing. It does not actually know the current weather. It cannot check an API. It cannot read a file from my machine. It cannot query a database on its own. Anthropic’s tool-use flow is built around this exact limitation: Claude can return a tool_use request, your application runs the tool, and then you send back a tool_result.

That is where the move from chatbot to agent begins.

A Chatbot Only Knows the Conversation

A simple chatbot works inside the boundary of the conversation. It knows the prompt, the previous messages, and whatever knowledge is already inside the model.

That works well for many tasks:

  • Explaining concepts
  • Generating code
  • Rewriting text
  • Brainstorming ideas
  • Summarizing documents

But it breaks the moment the answer depends on something external:

  • What is the temperature outside?
  • What did that email say?
  • Does that file exist on my system?
  • What does my database store right now?
  • What is the result of this API call?

A pure chatbot cannot do any of these things. This is the wall that people keep hitting.

An Agent Does One More Thing

The difference is surprisingly simple. An agent adds only one ability on top of chat:

The model can ask your application to run a tool, and your application sends the result back.

That is the whole idea. Everything else, multi-step planning, web browsing, RAG, autonomous loops, is built on top of this single concept.

Here is what the flow actually looks like:

  • You give the model a list of tools it can use.
  • The user asks something.
  • The model decides it needs a tool.
  • Your code runs the tool.
  • The model receives the result and gives a better answer.

Let us look at some real code.

A Working Example in Python

Let us use the Anthropic API with a simple get_weather tool. This keeps the example small enough to read end to end, but real enough to feel what is happening.

Step 1: Install and set up

pip install anthropic python-dotenv
echo "ANTHROPIC_API_KEY=your_key_here" > .env

Now load the key and create the client:

from dotenv import load_dotenv
from anthropic import Anthropic
load_dotenv()
client = Anthropic()
MODEL = "claude-sonnet-4-5"

Step 2: Define a tool

A tool is two things: a schema that the model sees, and a function that your code actually runs.

import json
def get_weather(city: str, unit: str = "celsius") -> dict:
fake_data = {
"bangalore": {"temp": 29, "condition": "Partly cloudy"},
"delhi": {"temp": 36, "condition": "Hot"},
"mumbai": {"temp": 31, "condition": "Humid"},
}
key = city.strip().lower()
if key not in fake_data:
raise ValueError(f"Weather data not found for '{city}'")
return {
"city": city,
"temperature": fake_data[key]["temp"],
"unit": unit,
"condition": fake_data[key]["condition"],
}
tools = [
{
"name": "get_weather",
"description": "Get the current weather for a given city",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "Name of the city, e.g. Bangalore"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit"
}
},
"required": ["city"]
}
}
]

No framework. No router. Just a dict and a function.

Step 3: The agent loop

This is the part where a chatbot becomes an agent. The model asks for a tool, you run it, and send the result back.

messages = [
{"role": "user", "content": "What is the weather in Bangalore right now?"}
]
while True:
response = client.messages.create(
model=MODEL,
max_tokens=500,
tools=tools,
messages=messages
)
if response.stop_reason != "tool_use":
print(response.content[0].text)
break
messages.append({"role": "assistant", "content": response.content})
tool_use_block = next(b for b in response.content if b.type == "tool_use")
try:
result = get_weather(**tool_use_block.input)
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": tool_use_block.id,
"content": json.dumps(result)
}]
})
except Exception as e:
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": tool_use_block.id,
"content": str(e),
"is_error": True
}]
})

Read this slowly. The model asks for a tool. Your code runs it. You send the result back with a tool_result. And if something goes wrong, you set is_error: True. That last bit is important and almost nobody talks about it.

Why Error Handling Matters

Real tools fail. Cities do not exist. APIs return stale data. Files are missing. The model does not know any of this unless you tell it.

If the user asks for weather in Atlantis:

messages = [
{"role": "user", "content": "What is the weather in Atlantis?"}
]

Your function raises an exception. Instead of quietly failing, you send this back:

messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": tool_use_block.id,
"content": "Weather data not found for 'Atlantis'",
"is_error": True
}]
})

Now the model can say something honest. It might ask the user to check the spelling. It might suggest nearby cities. It might simply tell the user it could not find the data. That feels real. That is why is_error matters.

Why BYOK Makes This Easier to Learn

A lot of agent platforms hide this loop behind visuals, nodes, edges, and configuration files. You can ship something without ever seeing what is happening. That is fine until something breaks.

When you use your own API key and write the loop yourself, you see every move clearly:

  • You send messages and tool schemas.
  • The model returns either text or a tool_use request.
  • You run the tool.
  • You send back a tool_result.
  • Repeat until the model stops asking and returns an answer.

Four moves. That is the whole game.

The Real Mental Shift

For me, the moment this clicked was not about the code. It was about what the model was allowed to do.

A chatbot only replies. An agent can ask for missing information before it replies.

That is a different kind of system. The model does not need to pretend it already knows everything. It can pause. It can request a tool. It can get real data. And then it can give a proper answer.

Chatbot: text in, text out.

Agent: text in, tool use when needed, better output.

What to Build Next

Once this makes sense, try these in order:

  • Add a second tool like get_time and watch the model pick between them.
  • Add read_file and ask it to read a file from your folder.
  • Deliberately break a tool and handle it with is_error.
  • Point it at a real API and replace the fake weather function.

You will, within one afternoon, have something that feels like an actual agent. With your own key. In one file. No magic.

Final Thought

The move from chatbot to agent is not about using a bigger framework or a fancier architecture. It is about one very simple idea:

The model can ask. You can answer. The loop closes.

Once you see it, you cannot unsee it. Now go build something that does more than talk.

Leave a comment