DEV Community

Wanderson Alves Rodrigues
Wanderson Alves Rodrigues

Posted on

Tool Calling dando mãos e olhos aos modelos de linguagem (LLMs)

Grandes Modelos de Linguagem (LLMs), como o GPT-4, o Llama 3, Claude 3 e DeepSeek, são modelos treinados com enormes quantidades de informações. Eles funcionam como “cérebros”, construídos a partir de uma vasta representação do conhecimento humano, capazes de escrever, raciocinar e sintetizar informações em um nível sem precedentes. No entanto, além de apenas gerar texto, um LLM pode identificar que uma ação precisa ser executada e, então, solicitar que essa ação seja realizada por uma função externa.

Um LLM, por padrão, não sabe que horas são. Ele não sabe como está o tempo lá fora. Ele não pode acessar as notícias de hoje, verificar o preço de uma ação ou fazer uma reserva no seu restaurante favorito. Seu conhecimento é estático, congelado no tempo no momento em que seu treinamento foi concluído.

É aqui que entra o Tool Calling (ou Function Calling), uma das capacidades mais transformadoras no campo da IA. Este é o mecanismo que finalmente dá "mãos e olhos" a esses cérebros, permitindo-lhes interagir com o mundo exterior.

A Analogia: O Cérebro na Sala Fechada

Imagine um gênio trancado em uma biblioteca. Ele leu todos os livros, mas não tem janelas, relógio ou telefone. Se você perguntar a ele sobre a história da Roma Antiga, ele lhe dará uma resposta brilhante. Se você perguntar: "Está chovendo em Paris agora?", ele só poderá dizer: "Com base nos padrões climáticos históricos descritos nos livros, Paris frequentemente tem chuvas..."

O Tool Calling é o equivalente a instalar um intercomunicador nessa sala.

Através desse intercomunicador, o gênio (o LLM) não pode sair, mas ele pode pedir a alguém do lado de fora (sua aplicação) para fazer algo por ele. Ele pode dizer: "Por favor, ligue para o serviço de meteorologia e pergunte o tempo em Paris."

Como Funciona o "Tool Calling"?

O processo não é mágico, trata-se de um fluxo de dados elegante e estruturado entre o LLM e o código da sua aplicação. Esse fluxo de chamadas de ferramentas nada mais é do que a execução de funções,classes, métodos, scripts ou serviços externos que expomos para que a IA realize algo em nosso nome.

Outro ponto bastante interessante, que muitas pessoas confundem, é que o LLM não executa diretamente a ferramenta, ele apenas tem conhecimento da sua existência. Quando uma conversa é direcionada para algo que uma ferramenta disponível pode resolver, a IA apenas envia uma solicitação para que essa ferramenta seja executada. Ou seja, quem de fato implementa e realiza a chamada da ferramenta somos nós, por meio do código da aplicação.

Vamos ao mão na massa.

A Definição (O Desenvolvedor Informa as Ferramentas)

Primeiro, nós, como desenvolvedores, temos que dizer ao LLM quais "ferramentas" ele tem à sua disposição.

Para esse primeiro passo, vamos criar a ferramenta para multiplicar dois números de ponto flutuante.

from langchain.chat_models import init_chat_model
from langchain.tools import BaseTool, tool
from langchain_core.messages import (
    AIMessage,
    BaseMessage,
    HumanMessage,
    SystemMessage,
    ToolMessage,
)
from rich import print
from dotenv import load_dotenv

load_dotenv()
Enter fullscreen mode Exit fullscreen mode
@tool()
def multiply(a: float, b: float) -> float:
    """Multipy a * b and return the result
    Args:
        a (float): The first number
        b (float): The second number
    Returns:
        float: The resulting foat of the equation a * b"""
    return a * b
Enter fullscreen mode Exit fullscreen mode

Depois que decoramos a função com @tool, ela se torna uma ferramenta que pode ser utilizada por LLMs. No entanto, a partir desse momento, ela deixa de ser apenas uma função simples e passa a ser uma instância da classe StructuredTool ou seja, uma ferramenta enriquecida com metadados e descrição, que o LLM pode compreender e utilizar de forma estruturada.Se imprimir a função multiply vamos ter algo como abaixo.

StructuredTool(
  name='multiply',
  description='Multipy a * b and return the result\n    Args:\n        a (float): The first number\n        b (float):
  The second number\n    Returns:\n        float: The resulting foat of the equation a * b',
  args_schema=<class 'langchain_core.utils.pydantic.multiply'>,
  func=<function multiply at 0x7532c41da2a0>
)
Enter fullscreen mode Exit fullscreen mode

Vamos agora fazer uma chamada ao modelo GPT-4o-mini: inicialmente sem ferramentas e, em seguida, com elas.

llm = init_chat_model("gpt-4o-mini", temperature=0)
system_message = SystemMessage(
    content=""" Você é um assistente de matemática prestativo. Você tem acesso a ferramentas. 
                Quando o usuário pedir algo, primeiro verifique se você possui uma ferramenta que resolva esse problema."""
)
human_message = HumanMessage("Oi eu sou Goku")
messages: list[BaseMessage] = [
    system_message,
    human_message,
]
result = llm.invoke(messages)
Enter fullscreen mode Exit fullscreen mode

A saída de result segue abaixo. Na mensagem de retorno AIMessage -> content, temos a resposta do modelo: 'Oi, Goku! Como posso ajudar você hoje? Se precisar de ajuda com matemática ou qualquer outra coisa, é só me avisar!'.

AIMessage(
    content='Oi, Goku! Como posso ajudar você hoje? Se precisar de ajuda com matemática ou qualquer outra coisa, é só me avisar!',
    additional_kwargs={'refusal': None},
    response_metadata={
        'token_usage': {
            'completion_tokens': 28,
            'prompt_tokens': 54,
            'total_tokens': 82,
            'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0},
            'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}
        },
        'model_provider': 'openai',
        'model_name': 'gpt-4o-mini-2024-07-18',
        'system_fingerprint': 'fp_560af6e559',
        'id': 'chatcmpl-CSuxFiARtuT2nfkJYzXlnQRg656Dl',
        'service_tier': 'default',
        'finish_reason': 'stop',
        'logprobs': None
    },
    id='lc_run--18e23d3f-ba97-4d4b-8dc4-8bc2a55a4d4b-0',
    usage_metadata={'input_tokens': 54, 'output_tokens': 28, 'total_tokens': 82, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}
)
Enter fullscreen mode Exit fullscreen mode

Vamos agora adicionar uma ferramenta à conversa e observar o resultado. O código completo segue abaixo:

from langchain.chat_models import init_chat_model
from langchain.tools import BaseTool, tool
from langchain_core.messages import (
    BaseMessage,
    HumanMessage,
    SystemMessage,
)
from rich import print

from dotenv import load_dotenv

load_dotenv()

@tool()
def multiply(a: float, b: float) -> float:
    """Multipy a * b and return the result
    Args:
        a (float): The first number
        b (float): The second number
    Returns:
        float: The resulting foat of the equation a * b"""
    return a * b


llm = init_chat_model("gpt-4o-mini", temperature=0)

system_message = SystemMessage(
    content=""" Você é um assistente de matemática prestativo. Você tem acesso a ferramentas. 
                Quando o usuário pedir algo, primeiro verifique se você possui uma ferramenta que resolva esse problema."""
)
human_message = HumanMessage("Oi eu sou Goku, quanto é 12.5 multiplicado por 3.4?")
messages: list[BaseMessage] = [
    system_message,
    human_message,
]
tools: list[BaseTool] = [
    multiply,
]

llm_with_tools = llm.bind_tools(tools)

if __name__ == "__main__":
    result = llm_with_tools.invoke(messages)
    print(result)

Enter fullscreen mode Exit fullscreen mode

As principais diferenças entre uma chamada sem ferramentas ou seja, uma conversa simples e uma conversa com ferramentas estão na adição da função multiply, que representa nossa ferramenta responsável por executar uma ação específica no caso a multiplicação de dois pontos flutuantes, e na criação da lista de ferramentas disponíveis. Além disso, a mensagem agora é diferente da primeira chamada “Oi, eu sou o Goku! Quanto é 12.5 multiplicado por 3.4?”. Com base nessa solicitação, o LLM identifica a intenção do usuário e sugere a ferramenta mais adequada para resolver a tarefa, neste caso, a função responsável por realizar a multiplicação.

Nesse exemplo, temos apenas uma ferramenta, mas poderíamos incluir várias, cada uma com sua função e finalidade. No entanto, é importante ter cuidado: quanto maior o número de ferramentas disponíveis, maior a complexidade do processo de decisão do modelo. Isso pode levar a comportamentos inesperados ou alucinações, especialmente se as ferramentas tiverem propósitos semelhantes ou se o contexto da solicitação não for claro.

Portanto, ao adicionar múltiplas ferramentas, é recomendável definir descrições claras e propósitos bem delimitados para cada uma delas, garantindo que a LLM saiba quando e como utilizá-las corretamente.

@tool()
def multiply(a: float, b: float) -> float:
    """Multipy a * b and return the result
    Args:
        a (float): The first number
        b (float): The second number
    Returns:
        float: The resulting foat of the equation a * b"""
    return a * b
Enter fullscreen mode Exit fullscreen mode
tools: list[BaseTool] = [
    multiply,
]
llm_with_tools = llm.bind_tools(tools)
Enter fullscreen mode Exit fullscreen mode

Após realizar a chamada ao LLM com a ferramenta configurada, observamos a seguinte saída gerada pelo modelo:

AIMessage(
    content='',
    additional_kwargs={'refusal': None},
    response_metadata={
        'token_usage': {
            'completion_tokens': 21,
            'prompt_tokens': 144,
            'total_tokens': 165,
            'completion_tokens_details': {
                'accepted_prediction_tokens': 0,
                'audio_tokens': 0,
                'reasoning_tokens': 0,
                'rejected_prediction_tokens': 0
            },
            'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}
        },
        'model_provider': 'openai',
        'model_name': 'gpt-4o-mini-2024-07-18',
        'system_fingerprint': 'fp_560af6e559',
        'id': 'chatcmpl-CTyJBbjWwJJ4ehSSIMqmXzxFIhEXn',
        'service_tier': 'default',
        'finish_reason': 'tool_calls',
        'logprobs': None
    },
    id='lc_run--961d6734-1a03-4c3a-afe5-2d0a9a34861c-0',
    tool_calls=[
        {
            'name': 'multiply',
            'args': {'a': 12.5, 'b': 3.4},
            'id': 'call_CezDcbeqTBbMvVMp8WMSNZD8',
            'type': 'tool_call'
        }
    ],
    usage_metadata={
        'input_tokens': 144,
        'output_tokens': 21,
        'total_tokens': 165,
        'input_token_details': {'audio': 0, 'cache_read': 0},
        'output_token_details': {'audio': 0, 'reasoning': 0}
    }
)
Enter fullscreen mode Exit fullscreen mode

A parte mais importante da resposta é a tag tool_calls, pois é nela que o LLM indica qual ou quais ferramentas são mais adequadas para atender à solicitação do usuário.

Dentro dessa estrutura, encontramos o nome da função selecionada 'name': 'multiply', os argumentos utilizados 'args': {'a': 12.5, 'b': 3.4} e um identificador único da chamada 'id': 'call_CezDcbeqTBbMvVMp8WMSNZD8'.

Esse identificador é útil para rastrear a execução da ferramenta ou correlacionar respostas quando há múltiplas chamadas dentro da mesma interação. Em resumo, o bloco tool_calls representa o momento em que o modelo decide invocar uma ação externa para gerar uma resposta mais precisa.

    tool_calls=[
        {
            'name': 'multiply',
            'args': {'a': 12.5, 'b': 3.4},
            'id': 'call_CezDcbeqTBbMvVMp8WMSNZD8',
            'type': 'tool_call'
        }
    ],
Enter fullscreen mode Exit fullscreen mode

Agora vem o pulo do gato: não é a LLM que executa a função, e sim o seu código. Todas as ferramentas são identificadas a partir do contexto da conversa, e o modelo apenas sugere qual(is) delas devem ser utilizadas.

Cabe ao seu código desenvolvido por você executar efetivamente as funções com os parâmetros indicados pela LLM. Isso permite que várias ferramentas sejam acionadas conforme necessário, enriquecendo a conversa com informações adicionais, sem que o modelo precise realizar cálculos ou ações externas por conta própria.

É bastante interessante entender como isso funciona nos bastidores. Quando você utiliza frameworks como LangGraph, CrewAI, Microsoft Agent Framework, Semantic Kernel, entre outros, a execução dessas funções fica abstraída, facilitando a integração e a automação, sem que você precise lidar com todos os detalhes de implementação manualmente. Vamos ver agora como podemos executar nossa ferramenta.

from langchain.chat_models import init_chat_model
from langchain.tools import BaseTool, tool
from langchain_core.messages import (
    BaseMessage,
    HumanMessage,
    SystemMessage,
    AIMessage,
    ToolMessage,
)
from rich import print

from dotenv import load_dotenv

load_dotenv()


@tool()
def multiply(a: float, b: float) -> float:
    """Multipy a * b and return the result
    Args:
        a (float): The first number
        b (float): The second number
    Returns:
        float: The resulting foat of the equation a * b"""
    return a * b


llm = init_chat_model("gpt-4o-mini", temperature=0)
system_message = SystemMessage(
    content=""" Você é um assistente de matemática prestativo. Você tem acesso a ferramentas. 
                Quando o usuário pedir algo, primeiro verifique se você possui uma ferramenta que resolva esse problema."""
)
human_message = HumanMessage("Oi eu sou Goku, quanto é 12.5 multiplicado por 3.4?")
messages: list[BaseMessage] = [
    system_message,
    human_message,
]
tools: list[BaseTool] = [
    multiply,
]
tools_by_name = {tool.name: tool for tool in tools}

llm_with_tools = llm.bind_tools(tools)


def run_tool(response):
    if isinstance(response, AIMessage) and getattr(response, "tool_calls", None):
        call = response.tool_calls[-1]
        name, args, id_ = call["name"], call["args"], call["id"]

        try:
            content = tools_by_name[name].invoke(args)
            status = "success"
        except (KeyError, IndexError, TypeError, ValueError) as error:
            content = f"Erro: Ferramenta '{name}' não encontrada.{error}"
            status = "error"
        tool_message = ToolMessage(content=content, tool_call_id=id_, status=status)
        messages.append(tool_message)
        llm_response = llm_with_tools.invoke(messages)
        messages.append(llm_response)
        print(messages)


if __name__ == "__main__":
    llm_response = llm_with_tools.invoke(messages)
    messages.append(llm_response)
    run_tool(llm_response)
Enter fullscreen mode Exit fullscreen mode

O que tem de novidades nesse código:

  • tools_by_name = {tool.name: tool for tool in tools}: Nesse ponto, estamos pegando todos os nomes das ferramentas (funções) que foram passadas para a LLM e criando um dicionário que mapeia cada nome para o respectivo objeto da ferramenta. Isso permite acessar rapidamente qualquer ferramenta pelo seu nome quando a LLM indicar que ela deve ser utilizada.
  • run_tool: Executando ferramentas sugeridas pela LLM a função run_tool é responsável por executar de fato a função indicada pela LLM para realizar a ação desejada. Ela recebe a resposta da LLM, verifica se há chamadas de ferramentas (tool_calls) e, se houver, executa a ferramenta correspondente e registra a resposta.

Vamos analisar o trecho de código linha a linha e entender o que ele faz:

def run_tool(response):
    if isinstance(response, AIMessage) and getattr(response, "tool_calls", None):
        call = response.tool_calls[-1]
        name, args, id_ = call["name"], call["args"], call["id"]

        try:
            content = tools_by_name[name].invoke(args)
            status = "success"
        except (KeyError, IndexError, TypeError, ValueError) as error:
            content = f"Erro: Ferramenta '{name}' não encontrada.{error}"
            status = "error"
        tool_message = ToolMessage(content=content, tool_call_id=id_, status=status)
        messages.append(tool_message)
        llm_response = llm_with_tools.invoke(messages)
        messages.append(llm_response)
        print(messages)
Enter fullscreen mode Exit fullscreen mode
    if isinstance(response, AIMessage) and getattr(response, "tool_calls", None):
Enter fullscreen mode Exit fullscreen mode

Verifica duas coisas:

  • Se response é uma instância da classe AIMessage.
  • Se response possui um atributo chamado tool_calls (usando getattr para evitar erro se não existir).
  • Ou seja, só prossegue se a mensagem veio do LLM e contém chamadas para ferramentas.
   call = response.tool_calls[-1]
Enter fullscreen mode Exit fullscreen mode
  • Pega a última chamada de ferramenta presente na lista tool_calls.
  • Cada call é esperado ser um dicionário com informações sobre a ferramenta a ser executada.
    name, args, id_ = call["name"], call["args"], call["id"]
Enter fullscreen mode Exit fullscreen mode

Extrai três informações da chamada da ferramenta:

  • name: nome da ferramenta a ser chamada.
  • args: argumentos que a ferramenta precisa.
  • id_: identificador da chamada (usado para rastrear qual mensagem gerou a ação).
        try:
            content = tools_by_name[name].invoke(args)
            status = "success"
Enter fullscreen mode Exit fullscreen mode

Tenta executar a ferramenta:

  • tools_by_name é um dicionário que mapeia nomes de ferramentas para objetos/funções.

  • .invoke(args) executa a ferramenta com os argumentos fornecidos.

  • Se funcionar, define status como "success" e salva a saída em content.

        except (KeyError, IndexError, TypeError, ValueError) as error:
            content = f"Erro: Ferramenta '{name}' não encontrada.{error}"
            status = "error"
Enter fullscreen mode Exit fullscreen mode

Se houver algum erro ao chamar a ferramenta (ex: nome não existe, argumentos incorretos), captura a exceção e:

  • Define content com uma mensagem de erro.

  • Marca status como "error".

        tool_message = ToolMessage(content=content, tool_call_id=id_, status=status)
Enter fullscreen mode Exit fullscreen mode

Cria uma mensagem do tipo ToolMessage que representa a execução da ferramenta:

  • content: resultado ou mensagem de erro.

  • tool_call_id: liga a mensagem ao ID da chamada da ferramenta original.

  • status: sucesso ou erro.

        messages.append(tool_message)
Enter fullscreen mode Exit fullscreen mode

Adiciona a mensagem da ferramenta à lista messages, que é o histórico da conversa.

        llm_response = llm_with_tools.invoke(messages)
Enter fullscreen mode Exit fullscreen mode

Envia o histórico atualizado (messages) de volta para o LLM, agora incluindo a resposta da ferramenta.

A saída após a execução da chamada com ferramentas é:

[
    SystemMessage(
        content=' Você é um assistente de matemática prestativo. Você tem acesso a ferramentas. \n                Quando o usuário pedir algo, primeiro verifique se você possui uma ferramenta 
que resolva esse problema.',
        additional_kwargs={},
        response_metadata={}
    ),
    HumanMessage(content='Oi eu sou Goku, quanto é 12.5 multiplicado por 3.4?', additional_kwargs={}, response_metadata={}),
    AIMessage(
        content='',
        additional_kwargs={'refusal': None},
        response_metadata={
            'token_usage': {
                'completion_tokens': 21,
                'prompt_tokens': 144,
                'total_tokens': 165,
                'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0},
                'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}
            },
            'model_provider': 'openai',
            'model_name': 'gpt-4o-mini-2024-07-18',
            'system_fingerprint': 'fp_560af6e559',
            'id': 'chatcmpl-CTzPwOcFISxRtpXMgNEVDTDacTsio',
            'service_tier': 'default',
            'finish_reason': 'tool_calls',
            'logprobs': None
        },
        id='lc_run--f5058b1e-863d-47af-a444-963f3252a01c-0',
        tool_calls=[{'name': 'multiply', 'args': {'a': 12.5, 'b': 3.4}, 'id': 'call_boELMe3XRPYIlgCW5XlJaQzI', 'type': 'tool_call'}],
        usage_metadata={
            'input_tokens': 144,
            'output_tokens': 21,
            'total_tokens': 165,
            'input_token_details': {'audio': 0, 'cache_read': 0},
            'output_token_details': {'audio': 0, 'reasoning': 0}
        }
    ),
    ToolMessage(content='42.5', tool_call_id='call_boELMe3XRPYIlgCW5XlJaQzI'),
    AIMessage(
        content='Oi Goku! O resultado de 12.5 multiplicado por 3.4 é 42.5.',
        additional_kwargs={'refusal': None},
        response_metadata={
            'token_usage': {
                'completion_tokens': 25,
                'prompt_tokens': 175,
                'total_tokens': 200,
                'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0},
                'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}
            },
            'model_provider': 'openai',
            'model_name': 'gpt-4o-mini-2024-07-18',
            'system_fingerprint': 'fp_560af6e559',
            'id': 'chatcmpl-CTzPxR7YftzqdNVl3UIKezdPJ6sAz',
            'service_tier': 'default',
            'finish_reason': 'stop',
            'logprobs': None
        },
        id='lc_run--57a0afb7-aaa3-4a71-986a-c42f167d35d0-0',
        usage_metadata={
            'input_tokens': 175,
            'output_tokens': 25,
            'total_tokens': 200,
            'input_token_details': {'audio': 0, 'cache_read': 0},
            'output_token_details': {'audio': 0, 'reasoning': 0}
        }
    )
]
Enter fullscreen mode Exit fullscreen mode

O fluxo de mensagens:

a - SystemMessage

SystemMessage(
    content='Você é um assistente de matemática prestativo. Você tem acesso a ferramentas. Quando o usuário pedir algo, primeiro verifique se você possui uma ferramenta que resolva esse problema.'
)
Enter fullscreen mode Exit fullscreen mode
  • Define o contexto e instruções do assistente.
  • Indica que o modelo pode usar ferramentas externas para responder.
  • Funciona como a “base” do diálogo, informando como o assistente deve se comportar.

b - HumanMessage

HumanMessage(content='Oi eu sou Goku, quanto é 12.5 multiplicado por 3.4?')
Enter fullscreen mode Exit fullscreen mode
  • Representa a pergunta do usuário.
  • Nesse caso, o usuário quer calcular uma multiplicação.

c - AIMessage (primeira resposta)

AIMessage(
    content='',
    tool_calls=[{'name': 'multiply', 'args': {'a': 12.5, 'b': 3.4}, 'id': 'call_boELMe3XRPYIlgCW5XlJaQzI'}],
    response_metadata={'finish_reason': 'tool_calls'}
)
Enter fullscreen mode Exit fullscreen mode
  • O modelo não respondeu diretamente ao usuário ainda (content='').
  • Identificou que uma ferramenta deve ser usada: multiply.
  • Preencheu tool_calls com:

    • name: nome da ferramenta a ser chamada.
    • args: argumentos necessários para a execução.
    • id: identificador da chamada.

d - ToolMessage

ToolMessage(content='42.5', tool_call_id='call_boELMe3XRPYIlgCW5XlJaQzI')
Enter fullscreen mode Exit fullscreen mode
  • Contém o resultado da execução da ferramenta.
  • Aqui, a multiplicação foi realizada pelo código (run_tool) e retornou 42.5.
  • Associado à chamada específica pelo tool_call_id.

e - AIMessage (resposta final)

AIMessage(
    content='Oi Goku! O resultado de 12.5 multiplicado por 3.4 é 42.5.',
    response_metadata={'finish_reason': 'stop'}
)
Enter fullscreen mode Exit fullscreen mode

O código completo: Github

Referências:

An introduction to function calling and tool use

What is tool calling?

Tool calling

Tools & Function Calling in LLMs and AI Agents: A Hands-On Guide

Tool Calling for LLMs: A Detailed Tutorial

Introdução ao Microsoft Agent Framework

Top comments (0)