Python & AI Tutorials Logo
LangChain & LangGraph

12: Construindo Ferramentas para o Seu Agente

Na Parte IV, vamos construir um agente(agent) — uma IA que descobre o que fazer com a solicitação de um usuário e então faz, em vez de apenas responder a ela como um chatbot.

Aqui está a diferença na prática. Digamos que um usuário pergunte: "Por favor, cancele o pedido #12345." Um chatbot diria algo como: "Vá em Minha Página > Histórico de Pedidos e clique no botão 'Cancelar' para aquele pedido." A partir daí, cabe ao usuário seguir esses passos por conta própria. Um agente faz o trabalho — ele localiza o pedido, verifica se ele é elegível para cancelamento e o cancela. Ele toma uma ação.

O que torna isso possível são as ferramentas(tools): uma ferramenta que localiza um pedido, uma ferramenta que cancela um, uma ferramenta que envia um e-mail. Com ferramentas, um LLM deixa de estar limitado a gerar texto e começa a realmente realizar coisas.

Por favor, cancele o pedido #12345.

chama get_order(12345)

Pedido encontrado,
elegível para cancelamento

chama cancel_order(12345)

Cancelamento concluído

Seu pedido foi cancelado.

Usuário

Loop do Agente

Ferramenta de Busca de Pedido

Ferramenta de Cancelamento de Pedido

Vamos construir isso peça por peça ao longo da Parte IV: primeiro as ferramentas que o agente usará (este capítulo), depois conectando essas ferramentas a um LLM (Capítulo 13) e, por fim, o loop do agente que percorre o ciclo decidir → agir → observar (Capítulo 14).

Este capítulo aborda a primeira peça — definir ferramentas, conectá-las a dados reais e tratar erros com segurança.

12.1) Definindo Ferramentas com o Decorador @tool

12.1.1) Como Funciona uma Ferramenta?

Acabamos de ver um agente localizar e cancelar um pedido usando ferramentas. Antes de avançar, aqui está algo que vale a pena esclarecer: quando as pessoas dizem "o LLM usa uma ferramenta", parece que o LLM é quem a chama diretamente. Não é. O LLM nunca executa nada por conta própria — tudo o que ele faz é pedir que uma ferramenta seja chamada com determinados argumentos. A execução real acontece no nosso código.

Para que isso funcione, o LLM precisa saber quais ferramentas existem e quando cada uma se aplica. Por isso, toda ferramenta vem com três informações de metadados:

  • name — um identificador curto como get_order que o LLM usa para especificar qual ferramenta ele quer.
  • description — uma frase descrevendo o que a ferramenta faz e quando usá-la. É isso que o LLM lê para escolher a ferramenta certa para a tarefa.
  • Esquema de entrada — quais são os parâmetros da ferramenta: seus nomes, tipos e significado. O LLM precisa disso para preencher os argumentos corretamente.

Nada disso exige trabalho extra da sua parte. O name vem diretamente do nome da função, a description vem da sua docstring, e o esquema de entrada vem das dicas de tipo dos parâmetros. Tudo o que você precisa fazer é anexar o decorador @tool do LangChain.

12.1.2) Construindo a Sua Primeira Ferramenta

Vamos colocar isso em prática. Escreva uma função com dicas de tipo e uma docstring, depois decore-a com @tool.

python
from langchain.tools import tool
 
@tool
def get_weather(city: str) -> str:
    """Obtém o clima atual para uma determinada cidade."""
    return f"Está sempre ensolarado em {city}!"

Vamos verificar o que @tool gerou para nós.

python
print(get_weather.name)
# Saída: get_weather
 
print(get_weather.description)
# Saída: Obtém o clima atual para uma determinada cidade.
 
print(get_weather.args)
# Saída: {'city': {'title': 'City', 'type': 'string'}}

O nome da função get_weather tornou-se seu name, a docstring tornou-se sua description, e a dica de tipo city: str tornou-se seu esquema de entrada. Todas as três informações de metadados da seção anterior foram geradas automaticamente. É isso que o LLM usa para escolher uma ferramenta e preencher seus argumentos.

Uma vez anexado o @tool, a função torna-se um objeto de ferramenta do LangChain, o que significa que você não pode mais chamá-la como uma função normal — get_weather("Paris") não vai funcionar. Em vez disso, você a chama com .invoke(), o mesmo método de execução padrão que usamos para cadeias lá no Capítulo 6. Os argumentos entram como um dicionário:

python
result = get_weather.invoke({"city": "Paris"})
print(result)
# Saída: Está sempre ensolarado em Paris!

12.1.3) Personalizando name e description

Por padrão, o name vem do nome da função e a description vem da docstring. Você pode sobrescrever ambos.

Passe um nome como o primeiro argumento de @tool:

python
@tool("web_search")
def search(query: str) -> str:
    """Pesquisa informações na web."""
    return f"Resultados para: {query}"
 
print(search.name)
# Saída: web_search

Você também pode sobrescrever a descrição, usando o parâmetro description. Isso é útil quando você quer manter a docstring como uma nota para outros desenvolvedores enquanto dá ao LLM algo mais personalizado:

python
@tool("calculator", description="Realiza operações aritméticas. Use isto para qualquer problema matemático.")
def calc(expression: str) -> str:
    """Avalia uma string de expressão matemática."""
    return str(eval(expression))  # AVISO: eval() é inseguro. Nunca o use em produção.

Use snake_case para nomes de ferramentas — alguns provedores de LLM rejeitam nomes com espaços ou caracteres especiais.

12.1.4) Definindo um Esquema de Entrada com Pydantic

Quando uma ferramenta recebe vários parâmetros, ou quando você quer descrever cada um individualmente, defina o esquema de entrada com um modelo Pydantic em vez disso. Este é o mesmo BaseModel e Field que usamos para saída estruturada lá no Capítulo 7.

python
from pydantic import BaseModel, Field
from langchain.tools import tool
 
class WeatherInput(BaseModel):
    """Entrada para consultas de clima."""
    location: str = Field(description="Nome da cidade (ex.: Seul, Tóquio)")
    units: str = Field(default="celsius", description="Unidade de temperatura (celsius ou fahrenheit)")
 
@tool(args_schema=WeatherInput)
def get_weather_detailed(location: str, units: str = "celsius") -> str:
    """Obtém o clima atual com uma unidade de temperatura escolhida."""
    temp = 22 if units == "celsius" else 72
    return f"Clima atual em {location}: {temp} graus {units[0].upper()}"

O que quer que você escreva em Field(description=...) torna-se parte do esquema de entrada que o LLM lê, então ele sabe exatamente o que cada parâmetro significa. Na maioria das vezes, dicas de tipo e uma docstring clara são tudo o que você precisa — recorra a args_schema apenas quando precisar daquele nível extra de detalhe por parâmetro.

12.2) Tratando Erros de Ferramentas

No mundo real, as ferramentas podem falhar — uma conexão de banco de dados cai, ou aparece uma entrada que você não havia planejado. Nesta seção, vamos tratar esses erros dentro da própria ferramenta, para que o agente possa responder de forma sensata em vez de parar completamente. Primeiro, vamos configurar as funções das quais nossas ferramentas vão depender.

12.2.1) Configuração: Funções de Busca de Produtos

python
# product_service.py
 
PRODUCTS = {
    1: {"name": "Wireless Mouse", "price": 29.99, "stock": 120},
    2: {"name": "Mechanical Keyboard", "price": 89.99, "stock": 0},
    3: {"name": "USB-C Hub", "price": 45.50, "stock": 35},
    4: {"name": "Laptop Stand", "price": 39.00, "stock": 8},
}
 
def fetch_product(product_id: int) -> dict:
    """Busca informações de produto por ID."""
    product = PRODUCTS.get(product_id)
    if product is None:
        raise ValueError(f"Produto com ID {product_id} não encontrado.")
    return product
 
def fetch_stock(product_id: int) -> int:
    """Retorna a quantidade em estoque de um produto."""
    product = PRODUCTS.get(product_id)
    if product is None:
        raise ValueError(f"Produto com ID {product_id} não encontrado.")
    return product["stock"]

Ambas as funções levantam um ValueError quando recebem um ID de produto que não existe.

12.2.2) Tratando Erros em uma Ferramenta

Vamos encapsular fetch_product em uma ferramenta get_product.

python
from langchain.tools import tool
from product_service import fetch_product
 
@tool
def get_product(product_id: int) -> str:
    """Busca um produto pelo seu ID. Retorna seu nome, preço e nível de estoque."""
    product = fetch_product(product_id)
    return f"Produto {product_id}: {product['name']} — ${product['price']:.2f}, {product['stock']} em estoque."

Com um ID válido, ela funciona como esperado.

python
print(get_product.invoke({"product_id": 1}))
# Saída: Produto 1: Wireless Mouse — $29.99, 120 em estoque.

Mas passe um ID que não existe, e fetch_product levanta um ValueError que nada captura — a execução do agente para ali mesmo.

python
print(get_product.invoke({"product_id": 99}))
# ValueError: Produto com ID 99 não encontrado.

A correção é simples: capture a exceção dentro da ferramenta e retorne uma string que o LLM possa entender, em vez de deixá-la se propagar. Sucesso ou falha, a ferramenta sempre retorna uma string, e o LLM usa essa string para decidir o que fazer em seguida.

python
from langchain.tools import tool
from product_service import fetch_product
 
@tool
def get_product(product_id: int) -> str:
    """Busca um produto pelo seu ID. Retorna seu nome, preço e nível de estoque."""
    try:
        product = fetch_product(product_id)
        return f"Produto {product_id}: {product['name']} — ${product['price']:.2f}, {product['stock']} em estoque."
    except ValueError as e:
        return f"Erro: {e}"
    except Exception as e:
        return f"Erro inesperado ao buscar o produto {product_id}: {e}"
python
print(get_product.invoke({"product_id": 1}))
# Saída: Produto 1: Wireless Mouse — $29.99, 120 em estoque.
 
print(get_product.invoke({"product_id": 99}))
# Saída: Erro: Produto com ID 99 não encontrado.

Um ID inexistente não levanta mais uma exceção — em vez disso, retorna uma mensagem de erro que o LLM pode entender.

Vamos aplicar o mesmo padrão a check_stock:

python
from product_service import fetch_stock
 
@tool
def check_stock(product_id: int) -> str:
    """Verifica se um produto está atualmente em estoque."""
    try:
        stock = fetch_stock(product_id)
        if stock > 0:
            return f"{stock} unidades disponíveis."
        return "Fora de estoque."
    except ValueError as e:
        return f"Erro: {e}"
    except Exception as e:
        return f"Erro inesperado ao verificar o estoque do produto {product_id}: {e}"

Ferramentas construídas dessa forma podem ser testadas diretamente com .invoke(). Certifique-se de que uma ferramenta funciona corretamente por conta própria antes de conectá-la a um LLM. Caso contrário, quando algo der errado dentro do agente mais tarde, você não conseguirá saber se a ferramenta está quebrada ou se o modelo apenas fez uma chamada ruim.