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ê lhe passa uma lista de funções que ele pode pedir para você executar, e ele decide quando usá-las. O modelo nunca executa nada por si próprio, apenas solicita uma chamada, e seu código faz o trabalho.

Um modelo por si só está selado em uma caixa. Ele não consegue verificar o clima de hoje, procurar um usuário em seu banco de dados ou enviar um e-mail. Ele só prediz texto baseado no que aprendeu, congelado no momento do treinamento, a mesma imagem de como funcionam os LLMs. O uso de ferramentas é como você deixa ele alcançar para fora da caixa, e é a base sobre a qual agentes são construídos.

A palavra para manter em mente é "pedir". O modelo lhe diz qual função ele quer e com quais argumentos. Seu código a executa e devolve o resultado. Você permanece no controle o tempo todo.

Um modelo produz apenas texto, então por si só não consegue buscar dados em tempo real, acessar seu banco de dados ou disparar uma ação. 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ê alimenta o resultado de volta. O modelo orquestra, seu código executa.

Este capítulo é o mecanismo por trás de cada agente que você construirá. Acerte o loop, o schema da ferramenta e o caminho de erro aqui, e agentes se tornam esse mesmo padrão repetido.

O uso de ferramentas é a costura onde um gerador de texto probabilístico encontra seu código determinístico, e a maior parte da dor em produção vive bem nessa costura. O modelo prediz uma chamada estruturada, seu código a executa e o resultado é alimentado de volta como mais contexto. Tudo que torna isso difícil, validar argumentos não confiáveis, idempotência, rodadas extras, chamadas alucinadas, observabilidade, emerge dessa única passagem.

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

O loop

O uso de ferramentas é um vai e volta, 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 ele quer.
  3. Se ele pediu uma ferramenta, seu código executa essa função e envia o resultado de volta para o modelo.
  4. O modelo usa o resultado para escrever sua resposta final.

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

JunoO loop O 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 por si próprio, 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 batidas: enviar 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 iniciantes frequentemente perdem é que isso raramente é uma rodada. O modelo pode solicitar uma ferramenta, ler seu resultado e depois solicitar outra ferramenta baseado no que viu, e assim por diante.

Então a forma real é um loop multi-turno: continue chamando o modelo e executando quaisquer ferramentas que ele pedir até que uma resposta chegue sem chamadas de ferramenta 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 ferramenta.

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: continua até que o modelo retorne uma resposta sem chamadas de ferramenta, então escreva como um loop real, não como um único if-statement. E coloque um limite nas iterações, porque um modelo que continua pedindo ferramentas 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 ferramenta. A parte interessante é que toda iteração é um percurso completo do modelo, e percursos completos são onde sua latência e orçamento de tokens vão.

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

  • Minimize o número de percursos completos dando ao modelo ferramentas que fazem mais por chamada.
  • Mantenha os resultados de ferramentas compactos para que o histórico reenviado não inche.
  • Execute chamadas de ferramenta independentes concorrentemente em vez de serialmente para que a latência de parede 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 turno; 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ê o atingir, falhe alto e registre o rastreamento para você ver o que o modelo estava perseguindo.

JunoO loop O loop é trivial escrever e caro executar: cada turno é uma chamada completa do modelo sobre todo o histórico reenviado, então cinco chamadas de ferramenta significa pagar pelo prompt cinco vezes. Mantenha resultados compactos, execute chamadas independentes concorrentemente e prefira poucas ferramentas mais gordas em vez de muitas conversadoras. E coloque um limite rígido nos turnos, 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 está realmente acontecendo

O uso de ferramentas pode parecer que o modelo ganhou novos poderes, e não ganhou. Sob o capô, o modelo está fazendo a única coisa que sempre faz: predizer texto. Nada mudou sobre a caixa em que ele vive.

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

O modelo não alcançou e executou nada. Ele predisse que uma chamada de ferramenta é o movimento certo e escreveu uma solicitação para uma, em um formato que seu código sabe como 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 prediz sua resposta final a partir desse contexto ampliado. A característica inteira são duas prediçõ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 incluídos, para de parecer misterioso: toda ação que uma IA toma é uma solicitação predita que seu código escolhe executar.

JunoO que está realmente acontecendo O uso de ferramentas é ainda pura prediçã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 esta função com esses argumentos", que a API lhe passa como tool_calls. O modelo nunca executa nada, ele prediz uma solicitação. Seu código executa a função e alimenta o resultado de volta como mais contexto para a próxima predição, então você é a ponte entre o modelo e o mundo real.

O uso de ferramentas não é uma nova capacidade acoplada ao modelo, é a mesma predição de próximo token de como funcionam os LLMs apontada para um alvo estruturado. As definições de ferramentas vão para o contexto, e o modelo foi treinado para que, quando uma ferramenta se encaixe, a continuação de maior probabilidade seja uma chamada formatada em vez de prosa. A API analisa essa continuação e a passa para você como tool_calls.

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

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

Se você só precisa de um objeto analisado saindo do modelo, use saída estruturada. Use o uso de ferramentas quando o modelo precisa agir e depois reagir ao que seu código retorna.

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

JunoO que está realmente acontecendo Uma chamada de ferramenta é a mesma predição de próximo token, apontada para um schema em vez de prosa: a API a expõe como tool_calls. É geração constrangida, a mesma maquinaria que saída estruturada. A diferença é intenção: saída estruturada lhe dá dados e para, o 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 predito, não execução, e manter isso claro é o que lhe protege. O modelo emite a continuação de maior probabilidade dado as ferramentas em contexto, a API a decodifica em uma chamada estruturada, e seu código decide se deve honrá-la. Mecanicamente é saída estruturada com um efeito colateral acoplado.

Esse enquadramento define o limite de confiança com precisão. Os argumentos em uma chamada de ferramenta são saída de 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-os 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 web ou documento, uma injeção de prompt enterrada nesse texto pode convencer o modelo a solicitar uma ferramenta diferente com argumentos escolhidos pelo atacante. A defesa é estrutural, não uma instrução de polidez no prompt do sistema: valide argumentos contra um schema rigoroso, coloque cada ferramenta no escopo da permissão mais estreita 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 seu valor.

JunoO que está realmente acontecendo Uma chamada de ferramenta é texto predito que a API decodifica para você, saída estruturada com um efeito colateral, nunca o modelo executando qualquer coisa. Então os argumentos são entrada não confiável, mesmo status que 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, coloque em escopo e autorize na fronteira. O modelo roteia a solicitação, seu código é a única coisa com sua mão na alavanca.

Definindo uma ferramenta

Você descreve cada ferramenta para o modelo: seu nome, o que ela faz e os argumentos que ela recebe. 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": "Get the current weather for a city. Use when the user asks about weather.",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "The city name, e.g. 'Rio de Janeiro'"},
                },
                "required": ["city"],
            },
        },
    },
]

O bloco parameters é um schema, 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 esta 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") o guia bem.

JunoDefinindo uma ferramenta Uma definição de ferramenta tem um nome, uma descrição e um schema parameters para seus argumentos. A descrição é realmente um prompt: ela diz ao modelo quando usar a ferramenta, então escreva com cuidado. O schema de parâmetros é o mesmo mecanismo que saída estruturada, constrangendo 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 schema parameters. A descrição é a parte que 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.

python
tools = [
    {
        "type": "function",
        "function": {
            "name": "search_orders",
            "description": (
                "Look up a customer's orders by their account email. "
                "Use only when the user asks about an existing order. "
                "Do not use for product questions or refunds."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "email": {"type": "string", "description": "Account email to search"},
                    "status": {
                        "type": "string",
                        "enum": ["pending", "shipped", "delivered", "cancelled"],
                        "description": "Optional status filter",
                    },
                },
                "required": ["email"],
                "additionalProperties": False,
            },
        },
    },
]

Aperte o schema e você aperta o modelo:

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

O schema é sua primeira linha de defesa: quanto mais estreito ele é, menos formas o modelo tem para chamar a ferramenta errado.

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

JunoDefinindo uma ferramenta Nome, descrição e um schema parameters, é isso uma ferramenta. A descrição é um prompt, não uma docstring: diga ao modelo quando chamá-lo e quando não, ou ele a chamará nos momentos errados. Aperte o schema com required, enum e sem propriedades extras, porque um schema estreito é 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 são dois prompts usando um chapéu. A descrição orienta quando o modelo chama a ferramenta, o schema constrange quais argumentos ele pode enviar, e ambos são instruções voltadas para o modelo, não docs internos. O schema é onde você consegue ser rigoroso, e rigor aqui é o controle de alucinação mais barato que você tem.

python
{
    "name": "issue_refund",
    "description": (
        "Issue a refund for a specific order line. "
        "Use only after confirming the order ID and amount with the user. "
        "Never call for amounts above the order total."
    ),
    "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 constrangido (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 schema, ainda assim os valores ainda vêm de predição, então um schema 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 este usuário. Então o schema é um filtro, não uma verificação de autorização.

Você ainda valida cada argumento contra o estado real, o pedido existe, o valor não excede o total, o chamador é o proprietário, em seu código antes do efeito colateral executar. Validação de schema detecta a forma errada; apenas seu código detecta a ação errada. (Chaves de schema e como cada provedor as valida estritamente variam, então confirme o comportamento do seu provedor em vez de assumir que a restrição é aplicada.)

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

JunoDefinindo uma ferramenta A descrição controla quando o modelo chama, o schema controla o que ele pode enviar, e ambos são prompts. Tranque o schema 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 é não confiável, então revalide contra o estado real antes do efeito colateral. E mantenha o conjunto de ferramentas pequeno, porque depois de uma dúzia o modelo escolhe errado e toda definição está taxando seu contexto. O schema filtra, seu código autoriza.

Tratando 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 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 uma app real isso chamaria uma API de clima; aqui fingimos
    return {"city": city, "tempC": 18, "condition": "cloudy"}

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

# 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)         # solicitação de ferramenta do assistente
    messages.append({
        "role": "tool",
        "tool_call_id": tool_call.id,
        "content": json.dumps(result),
    })

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

print(response.choices[0].message.content)
# "It's currently 18 degrees and cloudy in Rio de Janeiro."

Os argumentos chegam como uma string JSON, então você json.loads eles em 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, vinculados por tool_call_id. A chamada final transforma os dados brutos de clima em uma sentença natural.

JunoTratando 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 de volta duas mensagens: a solicitação do modelo e seu resultado, vinculadas 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 é falha. Sua ferramenta vai lançar: um argumento ruim, um 404, um timeout. O instinto é deixar a exceção se propagar, mas isso termina o turno inteiro. O movimento melhor é retornar erros como dados: pegue o erro e devolva 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 conseguiu 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:
        # devolva 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 ferramentas é o que torna um loop de ferramentas resiliente em vez de frágil. Um modelo que chama search_orders com um e-mail malformado recebe {"error": "invalid email"} e pode pedir ao usuário para corrigi-lo, em vez de sua solicitação inteira sofrer um erro 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 resultado-ou-erro, funciona em todos eles.

JunoTratando 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 se propagar e terminar o turno, pegue-a e devolva {"error": ...} como o resultado da ferramenta para que o modelo possa 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 chata. A parte perigosa é tudo entre receber os argumentos e executar o efeito colateral, porque os argumentos são saída de modelo e a função faz algo real.

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

  • Primeiro, valide antes de executar, contra o estado real, não o schema: o schema já passou ou você não estaria aqui, então agora confirme que o pedido existe, o usuário é o proprietário dele, o valor está dentro do intervalo.
  • Segundo, construa para idempotência, a propriedade de que executar a mesma operação duas vezes tem o mesmo efeito que executar uma vez. Modelos tentam novamente, loops refazem, 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 duplique-cobre ou duplique-envie.
  • Terceiro, retorne erros como dados, estruturados o suficiente para que o modelo possa agir com base neles, para que uma falha recuperável se torne uma nova tentativa 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, levanta 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 internos para o modelo

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

Mais duas realidades de produção:

  • Observabilidade: registre qual ferramenta executou, com quais argumentos (oculte segredos) e o que retornou, porque quando um agente se comporta mal o rastreamento de chamadas de ferramentas é a única forma de reconstruir o que ele realmente fez.
  • Ferramentas destrutivas: para qualquer coisa irreversível, excluir, pagar, enviar e-mail, não deixe a chamada do modelo ser a autoridade final. Coloque uma confirmação, uma corrida de teste que relata o que aconteceria, ou uma etapa de aprovação humana, e coloque em escopo a credencial para que o raio de explosão seja limitado mesmo se o modelo estiver errado ou comprometido.

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

JunoTratando a chamada Analisar é a parte chata. Entre os argumentos e o efeito colateral: valide contra o estado real não apenas o schema, faça escritas idempotentes fora do tool_call_id porque tentativas novamente vão disparar duas vezes, e retorne erros como dados que o modelo possa agir sem vazar seus internos. Registre cada chamada e seus args para reconstruir o que um agente realmente fez. E coloque qualquer coisa destrutiva atrás de confirmação ou uma credencial com escopo, porque uma devolução válida em schema ainda é uma solicitação de um estranho até seu código dizer sim.

Na prática

O mesmo fluxo que uma função reutilizável. Perceba 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 trata uma chamada de ferramenta. Um modelo também pode solicitar várias ao mesmo tempo, chamadas paralelas de ferramenta, que você trataria fazendo um loop sobre cada entrada em tool_calls. E se você deixar o modelo continuar chamando ferramentas em um loop até que seja feito, decidindo seu próprio próximo passo a cada vez, você tem um agente, que é exatamente para onde o próximo capítulo vai.

JunoNa prática Envolvido em uma função, o uso de ferramentas é uma chamada de modelo para escolher uma ferramenta, seu código executando-a, 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 até que o modelo esteja feito e você tem um agente.

Em um verdadeiro tratador de ferramentas você combina o loop multi-turno, chamadas paralelas e retornos de erro em uma função. O modelo pode pedir várias ferramentas em um único turno, e o loop executa até que ele pare 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:        # trate todas as chamadas paralelas
            messages.append(run_tool(call))  # analise, execute, retorne resultado-ou-erro

    return "Sorry, I could not complete that."  # atingiu o limite de turnos

Três decisões tornam isso pronto para produção em vez de uma demo. Faça um loop sobre todos em tool_calls, não [0], ou você silenciosamente descarta chamadas que o modelo pediu. Coloque um limite em MAX_TURNS para que o 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 derrube toda a troca.

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

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

O tratador de produção junta tudo acima: o loop multi-turno limitado, chamadas paralelas executadas concorrentemente, execução validada e registrada, e erros retornados como dados. A questão restante é aquela que a maioria das equipes pula: quando não dar uma ferramenta ao modelo de forma alguma.

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 devolução, o que procurar. É 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 o percurso completo; você remove uma superfície de alucinação, um salto de latência e um custo de token por zero perda. Dar ao modelo uma ferramenta compra flexibilidade e paga por isso em não-determinismo, então gaste 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()

Dois controles para quando você realmente usar ferramentas:

  • Controle 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 ferramenta desconhecidos imediatamente, e use tool_choice (o parâmetro que força, proíbe ou liberta o uso de ferramentas) para exigir uma ferramenta quando você sabe que uma é necessária ou proibi-las quando você sabe que nenhuma deve disparar, em vez de deixar cada turno ao acaso.
  • Mantenha o rastreamento de observabilidade, ferramenta, argumentos, resultado, contagem de turnos, porque um loop autônomo é apenas depurável se você conseguir repetir o que ele realmente fez.

Este tratador é a semente literal de um agente: o agente é este loop com um conjunto de ferramentas mais amplo e a liberdade de encadear chamadas para um objetivo, o 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 tratador completo é o loop limitado com chamadas de ferramenta concorrentes, validadas e 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 elimine o percurso completo, a latência e uma superfície de alucinação. Use tool_choice para forçar ou proibir ferramentas em vez de apenas esperar, rejeite chamadas para ferramentas que você nunca definiu, e mantenha o rastreamento, porque este é um agente com os amortecedores de treinamento ainda ligados, e os modos de falha só ficam maiores daqui.