LLM App Development #6: Connecting External Functions with Tool Calling

5 min read

Through Part 5, Claude returned text or structured data as its “answer.” But Claude on its own does not know today’s weather and cannot look into our database. It can only answer from what it was trained on. In this post we cover tool calling, which lets Claude call functions we define and connect to the outside world.

What tool calling is #

The flow goes like this. We tell Claude the list of tools (functions) it can use, along with a description of each. While answering, if Claude decides it needs a tool, instead of running the function itself it requests “call the get_weather tool with city=Seoul.” We receive that request, our code actually runs the function, and we return the result to Claude. Claude then produces a final answer from that result.

The key is that Claude does not run the function itself. Claude only expresses the intent “I want to call this tool with these arguments”; execution is up to us.

Defining and calling a tool #

A tool is defined with a name, a description, and an input schema. The input schema uses the same JSON schema format we saw in Part 5.

tool_define.py
import anthropic

client = anthropic.Anthropic()

tools = [
    {
        "name": "get_weather",
        "description": "Get the current weather for a city.",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "City name"},
            },
            "required": ["city"],
        },
    }
]

def get_weather(city: str) -> str:
    # In practice, call a weather API. Here we return a sample value.
    return f"The current weather in {city} is sunny, 22 degrees."

messages = [{"role": "user", "content": "What's the weather in Seoul?"}]

response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    tools=tools,
    messages=messages,
)

print(response.stop_reason)  # tool_use

The description is not just a description. Claude reads it to decide when to use this tool. So it is important to write clearly not only what the tool does but also when to use it.

Since we asked about the weather, Claude does not answer directly but tries to call the tool. At this point stop_reason comes back as tool_use, and the response contains a tool_use block.

Returning the result and finishing #

The tool_use block contains the tool name (name), arguments (input), and an identifier (id). We run the function with those arguments and return the result as a tool_result. We must match tool_use_id to indicate which call the result is for.

tool_loop.py
while response.stop_reason == "tool_use":
    # First add the assistant response containing the tool call to the history
    messages.append({"role": "assistant", "content": response.content})

    tool_results = []
    for block in response.content:
        if block.type == "tool_use":
            if block.name == "get_weather":
                result = get_weather(**block.input)
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": result,
            })

    # Tool results go back in the user role
    messages.append({"role": "user", "content": tool_results})

    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        tools=tools,
        messages=messages,
    )

print(next(b.text for b in response.content if b.type == "text"))

There is a reason it is wrapped in a while loop. Claude may not stop after using a tool once. It might check the weather and then call another tool. So while stop_reason is tool_use, we keep looping until Claude stops calling tools and finishes naturally (end_turn).

One easy thing to miss is that you must put the assistant response containing the tool call back into the history (messages.append). Only then does Claude remember which tool it called and connect it to the result.

Simplifying with the tool runner #

Instead of writing this loop by hand every time, the SDK’s tool runner handles calling, executing, and looping automatically. Define a function with the @beta_tool decorator and the input schema is generated from the function signature.

tool_runner.py
from anthropic import beta_tool

@beta_tool
def get_weather(city: str) -> str:
    """Get the current weather for a city.

    Args:
        city: City name.
    """
    return f"The current weather in {city} is sunny, 22 degrees."

runner = client.beta.messages.tool_runner(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    tools=[get_weather],
    messages=[{"role": "user", "content": "What's the weather in Seoul?"}],
)

for message in runner:
    print(message)
Note
The tool runner is currently a beta feature. When you need to control the loop yourself — for example to confirm with the user before running a tool, or to log calls — use the manual loop above. Otherwise, the tool runner is convenient.

Where people commonly trip up #

  • Not matching tool_use_id — The tool_use_id on a tool_result must exactly equal the id of the tool_use block it answers. If you call several tools, match each one.
  • Not putting the assistant response in the history — If you do not put the response containing the tool call back into messages, on the next call Claude cannot connect its call to the result and you get an error.
  • A poor description — If the description is vague, Claude calls the tool at the wrong time or not at all. Write clearly what the tool does and when to use it.

Wrapping up #

In this post we covered tool calling, which connects Claude to external functions.

  • When we define tools, Claude expresses its intent to call one as a tool_use, and we do the execution.
  • We return the execution result as a tool_result matched to the tool_use_id, and loop while stop_reason is tool_use.
  • The tool runner can handle this loop automatically.

Now Claude can use external functions. In the next post, “LLM App Development #7: Embeddings and Vector Search,” we will change direction and cover embeddings, which turn our documents into a form Claude can search. It is the preparation step that leads into RAG in the following part.

X