Skip to content
This page has been auto-translated and may contain errors.View in English

Uso de ferramentas

docs.scrimba.com

Uso de ferramentas (frequentemente chamado de function calling) é como um modelo de linguagem alcança além de sua própria caixa de texto: você entrega uma lista de funções que ele pode pedir para executar, e ele decide quando usá-las. O modelo nunca executa nada por si só, apenas solicita uma chamada, e seu código faz o trabalho.

Um modelo por si só está fechado em uma caixa. Ele não pode verificar o clima de hoje, procurar um usuário em seu banco de dados ou enviar um email. Ele apenas prevê texto do que aprendeu, congelado no momento do treinamento, a mesma imagem de como os LLMs funcionam. Uso de ferramentas é como você deixa alcançar fora da caixa, e é a base em que agentes são construídos.

A palavra para lembrar é "pedir". O modelo diz qual função quer e com quais argumentos. Seu código executa e retorna o resultado. Você permanece no controle o tempo todo.

Um modelo apenas produz texto, então por si só ele não pode buscar dados ao vivo, tocar seu banco de dados ou disparar uma ação. Uso de ferramentas fecha essa lacuna: você descreve um conjunto de funções, o modelo emite uma solicitação estruturada para chamar uma, seu código a executa, e você retorna o resultado. O modelo orquestra, seu código executa.

Este capítulo é o mecanismo subjacente a cada agente que você construirá. Acerte o loop, o esquema da ferramenta e o caminho de erro aqui, e agentes tornam-se esse mesmo padrão executado repetidamente.

Uso de ferramentas é a costura onde um gerador de texto probabilístico encontra seu código determinístico, e a maioria da dor de produção vive bem nessa costura. O modelo prevê uma chamada estruturada, seu código a executa, e o resultado é alimentado de volta como mais contexto. Tudo que torna isso difícil, validando argumentos não confiáveis, idempotência, viagens extras, chamadas alucinadas, observabilidade, sai dessa única transferência.

Nada aqui é matemática nova. É a disciplina de engenharia em torno de deixar um sistema estocástico acionar efeitos colaterais reais em seus sistemas, e é o substrato em que todo o capítulo agentes repousa.

O loop

Uso de ferramentas é um idas e vindas, não uma única chamada:

  1. Você envia a mensagem do usuário mais uma lista de ferramentas que o modelo pode usar.
  2. O modelo responde de uma de duas formas: uma resposta normal ou uma solicitação para chamar uma ferramenta, com os argumentos que quer.
  3. Se pediu uma ferramenta, seu código executa essa função e envia o resultado de volta ao modelo.
  4. O modelo usa o resultado para escrever sua resposta final.

Esse é o padrão completo. O modelo pede, seu código executa. Os passos 2 e 3 podem se repetir se o modelo precisar de várias ferramentas, que é o germe de agentes alguns capítulos adiante.

JunoO loop Uso de ferramentas é um loop: você envia uma mensagem mais uma lista de ferramentas, o modelo ou responde ou pede para chamar uma ferramenta com argumentos, seu código executa a função, e você envia o resultado de volta para o modelo usar. O modelo nunca executa código, ele apenas solicita chamadas, então você permanece no controle. Repita os passos do meio e você tem os primórdios de um agente.

O loop tem quatro tempos: envie a mensagem mais a lista de ferramentas, o modelo responde ou solicita uma chamada, seu código executa a função, o resultado volta, o modelo continua. A peça que os iniciantes frequentemente perdem é que raramente é uma rodada. O modelo pode solicitar uma ferramenta, ler seu resultado, e depois solicitar outra ferramenta com base no que viu, e assim por diante.

Então a forma real é um loop multi-turn: continue chamando o modelo e executando as ferramentas que ele pede até que uma resposta volte sem chamadas de ferramentas nela. Essa resposta final sem ferramentas é a resposta para o usuário.

python
messages = [{"role": "user", "content": user_text}]

while True:
    response = client.chat.completions.create(
        model=MODEL, messages=messages, tools=tools,
    )
    msg = response.choices[0].message
    messages.append(msg)              # registre o que o modelo disse

    if not msg.tool_calls:            # nenhuma ferramenta desejada: esta é a resposta
        return msg.content

    for call in msg.tool_calls:       # execute cada chamada solicitada, não apenas a primeira
        args = json.loads(call.function.arguments)
        result = tool_impls[call.function.name](args)
        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": json.dumps(result),
        })

A condição do loop é a questão de segurança. Um modelo que continua pedindo ferramentas nunca sai por conta própria, então coloque um limite nas iterações em produção e pare com um erro claro se você atingir o limite. O loop termina quando o modelo retorna uma resposta sem chamadas de ferramentas.

JunoO loop O loop é enviar ferramentas, o modelo responde ou pede uma chamada, você a executa, você alimenta o resultado de volta, repita. Aqui está o movimento: ele continua até o modelo retornar uma resposta sem chamadas de ferramentas, então escreva como um loop real, não uma declaração if. E coloque um limite nas iterações, porque um modelo que continua pedindo ferramentas vai felizmente girar para sempre às suas custas.

O loop é mecanicamente pequeno: chame o modelo com ferramentas, execute o que ele pedir, anexe resultados, repita até que ele retorne uma resposta sem chamadas de ferramentas. A parte interessante é que cada iteração é uma viagem de ida e volta completa do modelo, e viagens de ida e volta são onde sua latência e orçamento de token vão.

Cada volta reenvia todo o histórico de mensagens crescente, então uma tarefa que precisa de cinco chamadas de ferramentas paga pelo prompt cinco vezes, mais a entrada cresce a cada volta conforme os resultados se acumulam. Os controles:

  • Minimize o número de viagens de ida e volta dando ao modelo ferramentas que fazem mais por chamada.
  • Mantenha os resultados das ferramentas compactos para que o histórico reenviado não inche.
  • Execute chamadas de ferramentas independentes simultaneamente em vez de sequencialmente para que a latência wall-clock rastreie a chamada mais lenta, não sua soma.
python
MAX_TURNS = 8

for turn in range(MAX_TURNS):
    response = client.chat.completions.create(
        model=MODEL, messages=messages, tools=tools,
    )
    msg = response.choices[0].message
    messages.append(msg)

    if not msg.tool_calls:
        return msg.content

    # o modelo pode solicitar várias chamadas em um turn; execute-as, e continue
    for call in msg.tool_calls:
        messages.append(run_tool(call))   # validado, registrado, envolvido em erro

raise RuntimeError("tool loop did not converge within MAX_TURNS")

O limite rígido não é opcional. Sem ele, um modelo confuso pode fazer loop indefinidamente, e o modo de falha é uma solicitação lenta e cara em vez de um erro limpo. Coloque um limite, e quando você atingir, falhe alto e registre o rastreamento para você ver o que o modelo estava perseguindo.

JunoO loop O loop é trivial de escrever e caro de executar: cada volta é uma chamada de modelo completa sobre todo o histórico reenviado, então cinco chamadas de ferramentas significa pagar pelo prompt cinco vezes. Mantenha resultados compactos, execute chamadas independentes simultaneamente, e prefira poucas ferramentas mais robustas em vez de muitas conversas. E coloque um limite rígido nas voltas, porque no dia em que um modelo decide fazer loop para sempre, você quer um erro limpo em seus logs, não uma conta de cinco dígitos.

O que realmente está acontecendo

Uso de ferramentas pode parecer que o modelo ganhou novos poderes, e não ganhou. Internamente, o modelo faz a única coisa que sempre faz: prever texto. Nada mudou sobre a caixa em que vive.

Quando você inclui definições de ferramentas na solicitação, você está adicionando-as ao contexto a partir do qual o modelo prevê. O modelo foi treinado em exemplos onde, dadas ferramentas como essas, a continuação correta é às vezes uma mensagem especialmente formatada que significa "chame get_weather com a cidade São Paulo" em vez de uma frase normal. Então quando sua pergunta torna uma ferramenta útil, a saída mais provável é essa mensagem de chamada de ferramenta estruturada, e a API a superficializa para você como tool_calls.

O modelo não alcançou e executou nada. Ele previu que uma chamada de ferramenta é o movimento certo e escreveu uma solicitação para uma, em um formato que seu código sabe ler.

Então você, não o modelo, executa a função. Você pega o resultado real e o coloca de volta nas mensagens como mais contexto, e o modelo prevê sua resposta final a partir desse contexto ampliado. Toda a funcionalidade são duas previsões ordinárias com seu código fazendo o trabalho real no meio.

Você é a ponte entre a caixa de texto do modelo e o mundo real. Mantenha essa imagem e o resto do capítulo, agentes inclusos, para de parecer misterioso: cada ação que uma IA toma é uma solicitação prevista que seu código escolhe executar.

JunoO que realmente está acontecendo Uso de ferramentas é ainda pura previsão: o modelo foi treinado para que, dadas definições de ferramentas em seu contexto, a saída provável seja às vezes uma mensagem estruturada "chame essa função com esses argumentos", que a API entrega para você como tool_calls. O modelo nunca executa nada, ele prevê uma solicitação. Seu código executa a função e alimenta o resultado de volta como mais contexto para a próxima previsão, então você é a ponte entre o modelo e o mundo real.

Uso de ferramentas não é uma nova capacidade parafusada no modelo, é a mesma previsão de próximo token de como os LLMs funcionam apontada para um alvo estruturado. As definições de ferramentas entram no contexto, e o modelo foi treinado para que, quando uma ferramenta se encaixa, a continuação de maior probabilidade seja uma chamada formatada em vez de prosa. A API analisa essa continuação e a entrega para você como tool_calls.

A consequência útil: uma chamada de ferramenta é geração restrita, saída que o modelo produz para corresponder a um esquema que você forneceu. Esse é o mecanismo exato de saída estruturada, onde você pede ao modelo para retornar JSON em uma forma que você defina.

A linha entre eles é a intenção. Saída estruturada é "me dê dados nessa forma e pare". Uso de ferramentas é "me dê dados nessa forma para que eu possa executar algo e alimentar o resultado de volta".

Se você apenas precisa de um objeto analisado do modelo, alcance por saída estruturada. Alcance por uso de ferramentas quando o modelo precisa agir e depois reagir ao que seu código retorna.

Como a chamada é prevista, não executada, seu código é a única coisa que realmente toca seus sistemas. O modelo propõe, seu código dispõe, e esse limite é onde você coloca cada verificação antes de qualquer coisa real acontecer.

JunoO que realmente está acontecendo Uma chamada de ferramenta é a mesma previsão de próximo token, apontada para um esquema em vez de prosa: a API superficializa essa previsão como tool_calls. É geração restrita, o mesmo mecanismo de saída estruturada. A diferença é a intenção: saída estruturada lhe dá dados e para, uso de ferramentas lhe dá dados para que você possa executar algo e reagir. De qualquer forma, o modelo apenas propõe, seu código é a única coisa que toca qualquer coisa real.

Uma chamada de ferramenta é texto previsto, não execução, e manter isso em linha reta é o que protege você. O modelo emite a continuação de maior probabilidade dadas as ferramentas no contexto, a API a decodifica em uma chamada estruturada, e seu código decide se honra. Mecanicamente, é saída estruturada com um efeito colateral anexado.

Esse enquadramento estabelece o limite de confiança com precisão. Os argumentos em uma chamada de ferramenta são saída do modelo, o que significa que são entrada não confiável: texto que um sistema probabilístico gerou, com o mesmo status de qualquer coisa que um usuário digitou em um formulário. Trate assim.

O modelo também pode ser direcionado por conteúdo que lê no meio do loop, então se uma ferramenta retorna texto de uma página ou documento, uma injeção de prompt enterrada nesse texto pode fazer o modelo solicitar uma ferramenta diferente com argumentos escolhidos pelo invasor. A defesa é estrutural, não uma instrução de polidez no prompt do sistema: valide argumentos contra um esquema rigoroso, escopo cada ferramenta para a permissão mais restrita de que precisa, e nunca deixe a autoridade de uma ferramenta exceder o que você concederia à parte menos confiável que pode influenciar suas entradas.

Então o modelo mental é um roteador de solicitação, não um colega. O modelo roteia para uma função com argumentos propostos. Seu código autentica, autoriza, valida, e apenas então executa. Tudo a jusante de "apenas então" é onde este capítulo ganha sua utilidade.

JunoO que realmente está acontecendo Uma chamada de ferramenta é texto previsto que a API decodifica para você, saída estruturada com um efeito colateral, nunca o modelo executando nada. Então os argumentos são entrada não confiável, mesmo status de um campo de formulário que um estranho preencheu, e uma injeção de prompt escondida no texto retornado de uma ferramenta pode redirecionar a próxima chamada. Valide, escopo e autorize no limite. O modelo roteia a solicitação, seu código é a única coisa com a mão na alavanca.

Definindo uma ferramenta

Você descreve cada ferramenta para o modelo: seu nome, o que faz e os argumentos que toma. A descrição não é documentação para você, é uma instrução para o modelo sobre quando usar a ferramenta, então escreva como o prompt que é.

python
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Obtenha o clima atual de uma cidade. Use quando o usuário pergunta sobre clima.",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "O nome da cidade, ex: 'São Paulo'"},
                },
                "required": ["city"],
            },
        },
    },
]

O bloco parameters é um esquema, a mesma ideia de saída estruturada: ele define os argumentos que o modelo deve produzir. Quando o modelo decide chamar get_weather, ele envia de volta um argumento city correspondendo a essa forma. Uma descrição vaga ("obtém clima") leva o modelo a usar a ferramenta nos momentos errados. Uma clara ("Use quando o usuário pergunta sobre clima") a guia bem.

JunoDefinindo uma ferramenta Uma definição de ferramenta tem um nome, uma descrição e um esquema parameters para seus argumentos. A descrição é realmente um prompt: ela diz ao modelo quando usar a ferramenta, então escreva com cuidado. O esquema de parâmetros é o mesmo mecanismo de saída estruturada, restringindo os argumentos que o modelo envia.

Uma definição de ferramenta tem três partes que o modelo lê: o nome, a descrição e o esquema parameters. A descrição é a parte sobre a qual as pessoas subestimam. Não é um comentário para seus colegas, é um prompt que o modelo usa para decidir quando chamar a ferramenta, então escreva como um: diga o que a ferramenta faz, quando usá-la e quando não usar.

python
tools = [
    {
        "type": "function",
        "function": {
            "name": "search_orders",
            "description": (
                "Procure os pedidos de um cliente por seu email de conta. "
                "Use apenas quando o usuário pergunta sobre um pedido existente. "
                "Não use para perguntas de produto ou devoluções."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "email": {"type": "string", "description": "Email da conta para buscar"},
                    "status": {
                        "type": "string",
                        "enum": ["pending", "shipped", "delivered", "cancelled"],
                        "description": "Filtro de status opcional",
                    },
                },
                "required": ["email"],
                "additionalProperties": False,
            },
        },
    },
]

Aperte o esquema e você aperta o modelo:

  • required força os argumentos que a ferramenta não pode funcionar sem.
  • Um enum limita um campo a um conjunto fixo, então o modelo não pode inventar um quinto status.
  • additionalProperties: false rejeita campos que você não definiu.

O esquema é sua primeira linha de defesa: quanto mais restrito, menos formas o modelo tem de chamar a ferramenta errado.

Essa forma de ferramenta é estilo OpenAI; as chaves exatas variam por provedor (Anthropic e outros aninham o esquema diferentemente), mas as três partes, nome, descrição, esquema, são as mesmas em todos os lugares.

JunoDefinindo uma ferramenta Nome, descrição e um esquema parameters, isso é uma ferramenta. A descrição é um prompt, não uma docstring: diga ao modelo quando chamar e quando não chamar, ou ele chamará nos momentos errados. Aperte o esquema com required, enum e sem propriedades extras, porque um esquema restrito é menos formas para o modelo chamar a ferramenta errado. As chaves JSON exatas mudam por provedor, mas essas três partes não.

Uma definição de ferramenta é dois prompts usando um chapéu. A descrição orienta quando o modelo chama a ferramenta, o esquema restringe quais argumentos ele pode enviar, e ambos são instruções voltadas para o modelo, não docs internos. O esquema é onde você fica rigoroso, e rigor aqui é o controle de alucinação mais barato que você tem.

python
{
    "name": "issue_refund",
    "description": (
        "Emita um reembolso para uma linha de pedido específica. "
        "Use apenas depois de confirmar o ID do pedido e o valor com o usuário. "
        "Nunca chame para valores acima do total do pedido."
    ),
    "parameters": {
        "type": "object",
        "properties": {
            "order_id": {"type": "string", "pattern": "^ord_[0-9]{8}$"},
            "amount_cents": {"type": "integer", "minimum": 1, "maximum": 100000},
            "reason": {"type": "string", "enum": ["damaged", "wrong_item", "late"]},
        },
        "required": ["order_id", "amount_cents", "reason"],
        "additionalProperties": False,
    },
}

Um campo restrito (um com enum, pattern, minimum/maximum ou required definido) estreita o que o modelo pode emitir, mas leia o limite claramente: muitos provedores validam a estrutura do esquema, ainda assim os valores ainda vêm de previsão, então um esquema rigoroso reduz chamadas malformadas sem provar que a chamada é correta ou segura. order_id correspondendo a um padrão não significa que esse pedido existe ou pertence a esse usuário. Então o esquema é um filtro, não uma verificação de autorização.

Você ainda valida cada argumento contra estado real, o pedido existe, o valor não excede o total, o chamador o possui, em seu código antes do efeito colateral ser executado. Validação de esquema pega a forma errada; apenas seu código pega a ação errada. (As chaves de esquema e quão rigorosamente cada provedor as reforça variam, então confirme o comportamento do seu provedor em vez de assumir que a restrição é reforçada.)

Uma alavanca a mais: contagem de ferramentas. Além de aproximadamente uma dúzia de ferramentas, a precisão de seleção cai e o modelo começa a alcançar a ferramenta errada, e cada definição também fica no contexto queimando tokens a cada chamada. Mantenha o conjunto de ferramentas ativas pequeno e relevante para a tarefa em vez de expor sua superfície de API inteira de uma vez.

JunoDefinindo uma ferramenta A descrição controla quando o modelo chama, o esquema controla o que ele pode enviar, e ambos são prompts. Tranque o esquema com enum, pattern e limites, mas não confunda uma forma válida com uma ação válida: um order_id bem-formado ainda é entrada não confiável, então re-valide contra estado real antes do efeito colateral. E mantenha o conjunto de ferramentas pequeno, porque além de uma dúzia o modelo escolhe errado e cada definição está utilizando seu contexto. O esquema filtra, seu código autoriza.

Manipulando a chamada

Quando o modelo quer uma ferramenta, a resposta contém tool_calls em vez de uma resposta final. Você lê a função solicitada e os argumentos, executa a função real e envia o resultado de volta como uma mensagem tool. Então você chama o modelo novamente para que ele possa terminar.

python
import json

# implementação real da ferramenta
def get_weather(city):
    # em um app real isso chama uma API de clima; aqui fingimos
    return {"city": city, "tempC": 18, "condition": "cloudy"}

messages = [{"role": "user", "content": "Qual é o clima em São Paulo?"}]

# 1. primeira chamada: ofereça as ferramentas
response = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
tool_calls = response.choices[0].message.tool_calls
tool_call = tool_calls[0] if tool_calls else None

if tool_call:
    # 2. execute a função solicitada com os argumentos do modelo
    args = json.loads(tool_call.function.arguments)
    result = get_weather(args["city"])

    # 3. envie a solicitação do modelo e seu resultado de volta
    messages.append(response.choices[0].message)         # a solicitação de ferramenta do assistente
    messages.append({
        "role": "tool",
        "tool_call_id": tool_call.id,
        "content": json.dumps(result),
    })

    # 4. chame novamente para o modelo poder responder usando o resultado
    response = client.chat.completions.create(model=MODEL, messages=messages)

print(response.choices[0].message.content)
# "Está atualmente 18 graus e nublado em São Paulo."

Os argumentos chegam como uma string JSON, então você json.loads para um objeto real, depois verifica antes de usar. Você coloca duas mensagens no histórico: a solicitação de ferramenta do modelo e seu resultado tool, ligados por tool_call_id. A chamada final transforma os dados de clima brutos em uma frase natural.

JunoManipulando a chamada Quando o modelo quer uma ferramenta, sua resposta carrega tool_calls em vez de texto. Você analisa os argumentos de uma string JSON, executa a função real e coloca duas mensagens de volta: a solicitação do modelo e seu resultado, ligadas por tool_call_id. Uma chamada de modelo final transforma seu resultado bruto em uma resposta natural.

Quando o modelo quer uma ferramenta, sua resposta carrega tool_calls em vez de content. Cada chamada lhe dá um nome de função, um id, e argumentos como uma string JSON. Você analisa, executa e anexa uma mensagem tool marcada com o tool_call_id correspondente para que o modelo saiba qual resultado responde qual solicitação.

A parte que vale a pena acertar é a falha. Sua ferramenta vai lançar: um argumento ruim, um 404, um tempo limite. O instinto é deixar a exceção borbulhar, mas isso encerra toda a volta. O melhor movimento é retornar erros como dados: pegue o erro e o entregue de volta como o resultado da ferramenta, para que o modelo possa ler o que deu errado e se recuperar, tentar novamente com um argumento corrigido, ou dizer ao usuário que não pôde fazer a coisa.

python
def run_tool(call):
    try:
        args = json.loads(call.function.arguments)
        result = tool_impls[call.function.name](args)
        content = json.dumps(result)
    except Exception as err:
        # entregue a falha como dados, não uma exceção
        content = json.dumps({"error": str(err)})
    return {"role": "tool", "tool_call_id": call.id, "content": content}

Retornar erros como mensagens de ferramenta é o que torna um loop de ferramenta resiliente em vez de frágil. Um modelo que chama search_orders com um email malformado recebe {"error": "invalid email"} e pode pedir ao usuário para corrigi-lo, em vez de sua solicitação inteira fazer um 500. A forma de resposta (tool_calls, tool_call_id, o papel tool) é estilo OpenAI e varia por provedor, mas o padrão, analisar, executar, retornar o resultado ou o erro, se mantém em todos eles.

JunoManipulando a chamada A resposta carrega tool_calls: analise os argumentos JSON, execute a função, anexe uma mensagem tool com o tool_call_id correspondente. Aqui está o movimento em falhas: não deixe a exceção borbulhar e encerre a volta, pegue-a e entregue {"error": ...} de volta como o resultado da ferramenta para o modelo se recuperar ou tentar novamente. Os nomes de campos exatos variam por provedor, mas analisar, executar, retornar resultado-ou-erro é o mesmo em todos os lugares.

Analisar e despachar a chamada é a parte rotineira. A parte perigosa é tudo entre receber os argumentos e executar o efeito colateral, porque os argumentos são saída do modelo e a função faz algo real.

Três hábitos separam uma demonstração de uma ferramenta que você pode executar em produção:

  • Primeiro, valide antes de executar, contra estado real, não o esquema: o esquema já passou ou você não estaria aqui, então agora confirme que o pedido existe, o usuário o possui, o valor está em alcance.
  • Segundo, construa para idempotência, a propriedade que executar a mesma operação duas vezes tem o mesmo efeito que executá-la uma vez. Modelos tentam novamente, loops disparam novamente, redes duplicam, então uma ferramenta de escrita precisa de uma chave de idempotência (frequentemente derivada do tool_call_id) para que uma repetição não cobre o dobro ou não envie o dobro.
  • Terceiro, retorne erros como dados, estruturado o suficiente para que o modelo possa agir sobre eles, para que uma falha recuperável se torne uma tentativa novamente e uma irrecuperável se torne uma mensagem limpa para o usuário.
python
def run_tool(call):
    name, args = call.function.name, json.loads(call.function.arguments)
    log.info("tool_call", name=name, args=redact(args), call_id=call.id)  # observabilidade

    try:
        validate(name, args)                       # verificações de estado real, lança em entrada ruim
        result = TOOLS[name](args, idem_key=call.id)  # idempotente em nova tentativa
        content = json.dumps(result)
    except ValidationError as err:
        content = json.dumps({"error": "invalid", "detail": str(err)})  # modelo pode tentar novamente
    except Exception as err:
        log.exception("tool_failed", call_id=call.id)
        content = json.dumps({"error": "tool_failed"})  # não vaze internals para o modelo

    return {"role": "tool", "tool_call_id": call.id, "content": content}

Duas realidades de produção a mais:

  • Observabilidade: registre qual ferramenta foi executada, com quais argumentos (desinfete segredos) e o que retornou, porque quando um agente se comporta mal o rastreamento de chamadas de ferramenta é a única forma de reconstruir o que realmente fez.
  • Ferramentas destrutivas: para qualquer coisa irreversível, deletar, pagar, enviar email, não deixe a chamada do modelo ser a autoridade final. Coloque um portão atrás de uma confirmação, uma execução seca que relata o que aconteceria, ou uma etapa de aprovação humana, e escopo a credencial para que o raio de explosão seja limitado mesmo se o modelo estiver errado ou sequestrado.

Uma chamada válida de esquema ainda é não confiável até seu código autorizá-la. (Nomes de campos e o envelope de tentativa novamente são estilo OpenAI; a disciplina se mantém em qualquer provedor.)

JunoManipulando a chamada Analisar é a parte rotineira. Entre os argumentos e o efeito colateral: valide contra estado real não apenas o esquema, torne escritas idempotentes do tool_call_id porque novas tentativas disparam duas vezes, e retorne erros como dados que o modelo pode agir sem vazar seus internals. Registre cada chamada e seus argumentos para poder reconstruir o que um agente realmente fez. E coloque um portão em qualquer coisa destrutiva atrás de confirmação ou uma credencial com escopo, porque um reembolso válido de esquema ainda é um pedido de um estranho até seu código dizer sim.

Na prática

O mesmo fluxo que uma função reutilizável. Repare que a única coisa entre o modelo e seus sistemas é código que você escreveu e controla:

python
import json

tool_impls = {"get_weather": lambda args: get_weather(args["city"])}

def answer_with_tools(user_text):
    messages = [{"role": "user", "content": user_text}]

    response = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
    calls = response.choices[0].message.tool_calls
    if not calls:
        return response.choices[0].message.content  # modelo respondeu diretamente

    call = calls[0]
    args = json.loads(call.function.arguments)
    result = tool_impls[call.function.name](args)

    messages.append(response.choices[0].message)
    messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})

    response = client.chat.completions.create(model=MODEL, messages=messages)
    return response.choices[0].message.content

Isso manipula uma chamada de ferramenta. Um modelo também pode solicitar várias de uma vez, chamadas de ferramentas paralelas, que você manipularia ao fazer um loop sobre cada entrada em tool_calls. E se você deixar o modelo continuar chamando ferramentas em um loop até terminar, decidindo seu próprio próximo passo a cada vez, você fica com um agente, que é exatamente para onde o próximo capítulo vai.

JunoNa prática Envolvido em uma função, uso de ferramentas é uma chamada de modelo para escolher uma ferramenta, seu código a executando, e uma segunda chamada para transformar o resultado em uma resposta. A única coisa entre o modelo e seus sistemas é código que você escreveu, então você permanece no controle. Faça um loop disso até o modelo terminar e você tem um agente.

Em um manipulador de ferramentas real você combina o loop multi-turn, chamadas paralelas e retornos de erro em uma função. O modelo pode pedir várias ferramentas em uma única volta, e o loop corre até parar de pedir.

python
import json

MAX_TURNS = 6

def answer_with_tools(user_text):
    messages = [{"role": "user", "content": user_text}]

    for _ in range(MAX_TURNS):
        response = client.chat.completions.create(
            model=MODEL, messages=messages, tools=tools,
        )
        msg = response.choices[0].message
        messages.append(msg)

        if not msg.tool_calls:
            return msg.content

        for call in msg.tool_calls:        # manipule todas as chamadas paralelas
            messages.append(run_tool(call))  # analise, execute, retorne resultado-ou-erro

    return "Desculpe, não consegui completar isso."  # atingiu o limite de voltas

Três decisões tornam isso pronto para produção em vez de uma demonstração. Faça um loop sobre todas as tool_calls, não [0], ou você silenciosamente descarta chamadas que o modelo pediu. Coloque MAX_TURNS como limite para que um modelo não possa girar para sempre. E roteie falhas através de run_tool como mensagens de erro, para que uma única chamada ruim não quebre toda a troca.

Quando você para de codificar os passos e deixa o modelo decidir seu próprio próximo movimento a cada volta, esse loop exato se torna um agente. A diferença é autonomia, não arquitetura.

JunoNa prática O manipulador de produção é o loop multi-turn com três coisas conectadas: faça um loop sobre cada entrada em tool_calls não apenas a primeira, coloque um limite nas voltas para não poder girar para sempre, e retorne falhas como mensagens de ferramenta para que uma chamada ruim não quebre a execução. Mesmo loop, mais autonomia, e você tem um agente. A arquitetura não muda, o modelo só ganha decidir seu próprio próximo passo.

O manipulador de produção dobra tudo junto: o loop limitado multi-turn, chamadas paralelas executadas simultaneamente, execução validada e registrada, e erros retornados como dados. A questão restante é a que a maioria das equipes pula: quando não dar uma ferramenta ao modelo.

Uma ferramenta é a resposta certa quando a ação realmente depende do julgamento do modelo sobre linguagem natural: qual pedido o usuário quer dizer, se isso conta como um caso de reembolso, o que buscar. É a resposta errada quando o caminho é determinístico, a mesma entrada sempre produzindo a mesma chamada. Se uma solicitação sempre dispara a mesma chamada com os mesmos argumentos deriváveis, coloque isso em código e pule a viagem de ida e volta; você remove uma superfície de alucinação, um salto de latência e um custo de token por zero perda. Entregar uma ferramenta ao modelo compra flexibilidade e paga por ela em não-determinismo, então gaste-a apenas onde a flexibilidade é o ponto.

python
def answer_with_tools(user_text):
    messages = [{"role": "user", "content": user_text}]

    for turn in range(MAX_TURNS):
        response = client.chat.completions.create(
            model=MODEL, messages=messages, tools=tools, tool_choice="auto",
        )
        msg = response.choices[0].message
        messages.append(msg)
        if not msg.tool_calls:
            return msg.content

        results = run_tools_concurrently(msg.tool_calls)  # chamadas independentes em paralelo
        messages.extend(results)

    log.warning("tool_loop_unconverged", turns=MAX_TURNS)
    return fallback_answer()

Duas alavancas para quando você realmente usa ferramentas:

  • Restrinja alucinação de chamada de ferramenta: um modelo pode inventar uma chamada para uma ferramenta que você nunca definiu ou fabricar argumentos, então rejeite nomes de ferramentas desconhecidos imediatamente, e use tool_choice (o parâmetro que força, proíbe ou liberta uso de ferramenta) para exigir uma ferramenta quando você sabe que uma é necessária ou proibir quando você sabe que nenhuma deve disparar, em vez de deixar cada volta ao acaso.
  • Mantenha o rastreamento de observabilidade, ferramenta, argumentos, resultado, contagem de volta, porque um loop autônomo é apenas debugável se você pode reexecutar o que realmente fez.

Este manipulador é a semente literal de um agente: o agente é este loop com um conjunto de ferramentas mais amplo e a latitude para encadear chamadas em direção a um objetivo, que é também por que os modos de falha aqui escalam para lá. (A superfície SDK é estilo OpenAI; tool_choice e o envelope de mensagem diferem por provedor.)

JunoNa prática O manipulador completo é o loop limitado com execução de ferramentas concorrentes, validadas, registradas e erros como dados. A questão pulada é quando não dar uma ferramenta: se a chamada é determinística, coloque em código e solte a viagem de ida e volta, a latência e uma superfície de alucinação. Use tool_choice para forçar ou proibir ferramentas em vez de esperar, rejeite chamadas para ferramentas que nunca definiu, e mantenha o rastreamento, porque isso é um agente com as rodas de treinamento ainda ligadas, e os modos de falha só ficam maiores daqui em diante.