Skip to main content
Uma única chamada de função é fácil. A parte interessante é o loop em volta dela, porque um modelo raramente consegue o que precisa na primeira chamada. Ele consulta algo, vê o resultado e decide o que perguntar em seguida. Este tutorial constrói um agente de linha de comando que responde perguntas sobre um banco SQLite que nunca viu. Ele não tem o esquema no prompt. Recebe três ferramentas somente leitura e descobre o resto sozinho:
Ao longo do caminho, vamos:
  1. Dar ao modelo um banco de dados e três ferramentas que o leem
  2. Descrever essas ferramentas para que o modelo saiba quando recorrer a cada uma
  3. Executar o loop que transforma chamadas de ferramenta em resultados de ferramenta
  4. Vê-lo requisitar várias ferramentas ao mesmo tempo
  5. Devolver erros ao modelo em vez de lançá-los
  6. Traçar a linha entre o que o modelo não vai fazer e o que ele não pode fazer
O guia de Function Calling cobre o formato da requisição isoladamente. Esta página é sobre o que acontece depois que a primeira resposta chega.

Configuração

Você precisa do Python 3.9 ou superior, do pacote requests e de uma chave da API Venice. Veja Gerando uma Chave de API caso ainda não tenha uma. Todo o resto está na biblioteca padrão.
Crie agent.py com os imports e o bloco de cabeçalho que toda chamada reutiliza:
Nem todo modelo consegue chamar ferramentas, e os IDs dos modelos mudam, então pergunte à API qual usar em vez de fixar um nome que vai envelhecer:
GET /models/traits mapeia nomes de traits estáveis para qualquer modelo que atualmente ocupe aquele papel. Ler function_calling_default na inicialização faz com que seu agente continue funcionando quando o modelo subjacente for substituído. Veja Modelos para a lista completa de traits.

1. Um banco de dados que vale a pena consultar

Qualquer arquivo SQLite serve. Este é uma pequena loja com clientes, produtos e os pedidos que os unem, o que é suficiente para que uma pergunta real exija um join e uma agregação:

2. Três ferramentas ao alcance do modelo

As ferramentas espelham a forma como uma pessoa aborda um banco de dados desconhecido: descobrir o que há nele, olhar de perto para uma tabela e então consultá-la.
Cada uma delas retorna uma string JSON, inclusive nas falhas. Isso é proposital, e a seção 5 explica por quê. Agora descreva-as para o modelo. A description não é um comentário. Ela é a única coisa que o modelo lê ao decidir qual ferramenta chamar e o que colocar nela:

3. O loop

Function calling é uma conversa, não uma requisição. O modelo responde com chamadas de ferramenta, você as executa, anexa os resultados e pergunta de novo. Termina quando o modelo responde com conteúdo em vez de chamadas.
Três detalhes desse loop importam mais do que parecem. A mensagem do assistente não modificada volta para messages antes dos resultados. Ela carrega os tool_calls que os resultados estão respondendo e, num modelo de raciocínio, também carrega um campo reasoning_content. Reconstruir a mensagem manualmente e descartar campos que você não esperava é a forma mais comum de quebrar a segunda rodada. Cada resultado é associado à sua chamada pelo tool_call_id. Nada mais o identifica. max_rounds é um limite de verdade, não uma formalidade. Um modelo que continua consultando sem concluir vai, de outra forma, entrar em loop até você ficar sem paciência ou sem crédito.
Chamadas de ferramenta também carregam um campo index, e é tentador usá-lo para alinhar resultados com chamadas. Não faça isso. Quando o modelo requisita três ferramentas de uma vez, todas as três podem chegar com o mesmo index, porque ele numera o turno do assistente, não a chamada dentro dele. Apenas id é único.

4. O que ele realmente faz

Conecte um bloco principal e execute:
As chamadas de ferramenta são impressas em stderr conforme acontecem, para você acompanhar o funcionamento:
Isso levou cinco rodadas. O formato delas vale ser lido com atenção, porque é toda a justificativa para o loop: A rodada 4 é a parte que uma única chamada de função não consegue fazer. O modelo não conseguiria escrever aquela consulta enquanto não visse a resposta da anterior. Sua execução não vai bater com esta chamada por chamada. Às vezes o modelo descreve as três tabelas de uma vez, às vezes uma por vez, e ocasionalmente pula list_tables e chuta um nome. Os números são estáveis porque vêm do banco de dados; o caminho até eles, não.
A rodada 2 retornou três chamadas de ferramenta em uma única resposta, e o loop acima as executa uma após a outra. Elas são independentes, então um ThreadPoolExecutor aqui vale a pena assim que suas ferramentas fizerem I/O de verdade. Mantenha as mensagens tool na mesma ordem das chamadas que as produziram.
Cada rodada reenvia a conversa inteira, então o prompt cresce à medida que o agente trabalha. A Venice armazena em cache o prefixo estável automaticamente, e o bloco usage mostra isso valendo a pena:
Na última rodada, 960 dos 1020 tokens de prompt vieram do cache. Prompt Caching explica como manter esse prefixo estável.

5. Deixe os erros chegarem ao modelo

O instinto é lançar uma exceção numa consulta ruim. Resista. Um erro é informação, e o modelo pode agir sobre ele. Pergunte por uma tabela que não existe:
A primeira consulta falhou. Como run_query retornou {"error": "OperationalError: no such table: purchases"} como um resultado de ferramenta comum, em vez de lançar exceção, o modelo leu isso, chamou list_tables para descobrir o que de fato existia, e se corrigiu. Se a exceção tivesse se propagado, o script teria morrido em cima de um erro de digitação. Por isso todas as ferramentas retornam JSON também no caminho de falha. A regra é simples: se um humano depurando sua ferramenta quer ver a mensagem, o modelo também quer.

6. O que ele não vai fazer, e o que ele não pode fazer

Peça ao agente para destruir algo:
Execute isso duas vezes e você pode ver dois comportamentos diferentes. Uma vez, ele recusou antes mesmo de tocar em uma ferramenta:
Outra vez, foi primeiro dar uma olhada, executou um SELECT para clientes espanhóis, não encontrou nenhum porque a coluna armazena ES em vez de Spain, e relatou isso:
Ambos são razoáveis. Nenhum é um controle de segurança. O modelo leu a expressão “read-only” na descrição de uma ferramenta e decidiu respeitá-la, e um modelo diferente, uma conversa mais longa, ou um usuário mais insistente podem produzir uma escolha diferente. A guarda dentro de run_query é a parte que não depende de uma escolha:
Escreva a descrição para que o modelo raramente tente. Escreva a guarda para que não importe quando ele tentar.
Essa segunda linha é por que run_query captura sqlite3.Warning junto com sqlite3.Error. O driver do Python recusa comandos empilhados, mas lança Warning para isso, e Warning não é subclasse de Error. Capturar apenas sqlite3.Error deixa um comando empilhado escapar do handler e derrubar o loop, em vez de retornar uma mensagem que o modelo possa ler.
Uma verificação de prefixo impede escritas, mas não diz nada sobre leituras. Qualquer SELECT que o modelo escrever pode alcançar qualquer tabela no arquivo, incluindo as que você nunca quis expor. Duas mudanças valem a pena antes que isso encoste em dados reais: abra o banco em modo somente leitura com sqlite3.connect("file:shop.db?mode=ro", uri=True), que faz as escritas falharem com attempt to write a readonly database não importa o que a checagem de string deixe passar, e aponte o agente para um banco de dados ou um conjunto de views que contenham apenas as colunas que ele tem permissão de ver.

Controlando quando as ferramentas são usadas

tool_choice decide quanto o modelo tem voz: "required" é mais bruto do que parece. Perguntar a este agente What is 2 + 2? com tool_choice definido como "required" faz com que ele chame list_tables, olhe para um banco de dados que não tem utilidade nenhuma para ele, e só então responda 4 na próxima rodada. Com "auto", ele responde 4 na hora e não chama nada. Recorra a "required" quando uma ferramenta realmente precisar ser executada, como registrar uma requisição, e deixe-o quieto no resto do tempo.

Ajustando o agente

Próximos passos

O loop que você tem agora é o mesmo por trás da maioria dos agentes. Só as ferramentas mudam.

Function Calling

Referência do array de tools e do tool_choice.

Respostas Estruturadas

Restrinja a resposta final a um JSON schema.

Prompt Caching

Mantenha barata a conversa que cresce.

Private Research Agent

O mesmo loop com ferramentas web e um planner.