From Software Engineer to AI Engineer - Part 3: Giving it a hand A developer's blog series on transitioning from software engineering to AI engineering demonstrates how to give large language models 'hands' through tool use. The latest installment explains that models are like brains without bodies and shows how to define tools as Python functions, using a payment refund cost calculator as an example. The post details the mechanics of tool catalogs, structured outputs, and how tool results become part of the model's context for further reasoning. As we have seen, models are conceptually quite simple: a list of input messages goes in and the model predicts the next token repeatedly to form the output message. It can't reach the internet or your database or Slack. And it cannot reliably do arithmetic or cringe example incoming tell you the number of r's in 'strawberry' - remember from part 1 that it never sees the letters, only token IDs. In that sense, the model is like a brain without a body. It can reason through a problem, describe the steps to take, even recite all of Shakespeare. But without a body, the model cannot perform any action. This is where tools come in, and making them is very much like normal software engineering. Tools are just functions or methods in a programming language. How tool use works under the hood depends on the model, but let's walk through it conceptually. It starts with a catalog of tools that the model can use, often a json list. In the API you pass this catalog as a separate tools parameter and the provider injects it for you: { "name": "multiply", "description": "Multiplies two numbers and returns the result.", "parameters": { "type": "object", "properties": { "a": { "type": "number", "description": "The first number." }, "b": { "type": "number", "description": "The second number." } }, "required": "a", "b" } }, ..more tools.. If the model decides that it wants to use a tool, its output message will be a structured message like we saw in article 2: { "name": "multiply", "arguments": { "a": 24, "b": 57 } } Your job as an AI engineer is to take such outputs and pass them to Python functions that actually perform the action. Then you respond with a structured message: Tool result multiply : { "result": 1368 } The tool result is now part of the model's context window and can, therefore, be used for further reasoning. So how did the model learn to do this? The model was trained on large volumes of tool-use transcripts, so requesting a tool and reading back a result is a pattern it has seen many times. Your tool catalog is the variable part, the runtime configuration if you will. Let's now look at a real example. Payment operations requires deterministic arithmetic, and models are not reliable calculators. Create app/tools.py and an empty app/ init .py so we can import our tools . We will create a tool that calculates refund costs. python from langchain core.tools import tool @tool def calculate refund cost original charge eur: float, refund amount eur: float, processing fee pct: float, processing fee fixed eur: float, refund admin fee eur: float = 0.25, - dict: """Calculate what refunding a payment actually costs the merchant. Use this whenever the user gives you a the original charge amount and b the payment method's fee structure percentage plus fixed fee per transaction . Do not guess the fee structure yourself if the user hasn't provided it. Processing fees paid on the original charge are NOT returned by the processor when you refund, and most processors charge a small admin fee per refund on top. The returned total cost of refund eur represents the merchant's total out-of-pocket cost after the refund is processed: refunded amount + non-refundable original processing fee + refund admin fee. Limitations: - Assumes the original payment processing fee is fully retained by the processor after a refund. Some processors, payment methods, or regions may have different refund fee policies. - Does not account for currency conversion costs, exchange-rate changes, taxes, accounting impacts, subscription adjustments, or other business-specific costs. Args: original charge eur: The amount of the original charge. refund amount eur: The amount being refunded full or partial . processing fee pct: Percentage fee on the original charge, e.g. 1.8 for 1.8%. processing fee fixed eur: Fixed fee on the original charge. refund admin fee eur: Processor's per-refund admin fee, default €0.25. Returns: A dictionary containing: - refund amount eur: The amount sent back to the customer. - processing fee eur: The original payment processing fee that the merchant still pays after issuing the refund. - total cost of refund eur: The merchant's total out-of-pocket cost. This includes the money returned to the customer and the processing fees. """ processing fee = original charge eur processing fee pct / 100 + processing fee fixed eur total = refund amount eur + refund admin fee eur + processing fee return { "refund amount eur": round refund amount eur, 2 , "processing fee eur": round processing fee, 2 , "total cost of refund eur": round total, 2 , } Docstrings have always been important guidance for programmers deciding which function to use, and they now fulfill the same role for models choosing between tools. And models actually read the docs, so docstrings are arguably more important than ever. Three tips for writing tool docstrings: Say when, not just what. "Use this whenever the user gives you a ... and b ..." - that is routing logic helping the model choose between tools. A docstring that only describes the computation leaves the when to chance. Set boundaries. "Do not guess the fee structure yourself" might be the most crucial logic in the whole tool definition. Hallucinating the fee structure will impact the calculation heavily. Another example is to explicitly tell the model not to underestimate the costs even if the user keeps asking for a better answer. Assistants without such boundaries have made real headlines: a car dealership's chatbot was talked into "agreeing" to sell a Chevy Tahoe for one dollar, and Air Canada was held liable in court for a refund policy its chatbot invented. Boundaries matter. Be frank about limitations. Every piece of logic has assumptions. For example, our simple refund tool assumes euros and does not take into account cross-border payments with multiple currencies. Making these limitations explicit is useful because it prevents the model from over-trusting the calculation. These limitations are often passed in the output to the user as well. Let's try our new calculate refund cost tool. Create 03 tool manual.py , which imports the tool we just wrote and binds it to the model using bind tools : python from dotenv import load dotenv from langchain.chat models import init chat model from app.tools import calculate refund cost import pprint load dotenv model = init chat model "anthropic:claude-sonnet-5" model with tools = model.bind tools calculate refund cost question = "Customer paid €480 on a European consumer card 1.8% + €0.25 . " "Full refund - what does it cost us?" msg = model with tools.invoke question pprint.pp msg And you'll see something like I truncated longer texts for your reading convenience : bash $ python3 03 tool manual.py AIMessage content= ... , tool calls= { "name": "calculate refund cost", "args": { "original charge eur": 480, "refund amount eur": 480, "processing fee pct": 1.8, "processing fee fixed eur": 0.25, }, "id": "toolu 011GofEYU4Vz1...", "type": "tool call", } , invalid tool calls= , usage metadata={ "input tokens": 1259, "output tokens": 224, "total tokens": 1483, "input token details": { ... }, }, The output is not an answer to your question. It's a tool request. Your application logic should check if tool calls is set, invoke the tools, and then pass the results back to the model to resume. Note that tool calls is a list: a model can request several tools in one turn, so always loop over it instead of grabbing the first entry. The snippet below, 03 tool manual 2.py , does two rounds with the model. First, we pass it the question and the model decides to respond with a tool request. Then, we respond with the tool results and the model gives us the final answer. python from dotenv import load dotenv from langchain.chat models import init chat model from langchain core.messages import ToolMessage from app.tools import calculate refund cost load dotenv model = init chat model "anthropic:claude-sonnet-5" model with tools = model.bind tools calculate refund cost question = "Customer paid €480 on a European consumer card 1.8% + €0.25 . " "Full refund - what does it cost us?" msg = model with tools.invoke question print msg tool map = { "calculate refund cost": calculate refund cost } tool messages = for tool call in msg.tool calls: retrieve function by name tool = tool map tool call "name" call the function result = tool.invoke tool call "args" store result tool messages.append ToolMessage content=str result , tool call id=tool call "id" give all messages as input, including the tool messages next msg = model with tools.invoke {"role": "user", "content": question}, msg, tool messages print next msg.content Pro tip: if a tool raises, don't let the exception escape. Catch it and send the error text back as the ToolMessage content. The model can then fix its arguments or explain the problem to the user, whereas an unhandled exception just kills the turn. And the result: bash $ python3 03 tool manual 2.py Here's the breakdown for a full refund on the €480 charge European consumer card, 1.8% + €0.25 : | Item | Amount | |-------------------------------------------|--------:| | Refunded to customer | €480.00 | | Original processing fee non-refundable | €8.89 | | Refund admin fee | €0.25 | | Total out-of-pocket cost | €489.14 | So issuing the full refund actually costs you €489.14 : - €480.00 returned to the customer - €8.89 original processing fee not refunded - €0.25 refund processing/admin fee Our calculate refund cost tool is a simple example, but I hope you see how powerful this tool usage can be. You can make tools to search on the internet, to select data from your database, you could even let it send out emails or write to a queue. Anything you could do with normal Python code can now be integrated with the model. Tool usage is the integration point between these two worlds. The next two articles dive into advanced tool categories: knowledge retrieval via RAG and API integration via MCP. You might have heard about these three-letter acronyms and we'll learn what they actually mean.