- Dar ao modelo um banco de dados e três ferramentas que o leem
- Descrever essas ferramentas para que o modelo saiba quando recorrer a cada uma
- Executar o loop que transforma chamadas de ferramenta em resultados de ferramenta
- Vê-lo requisitar várias ferramentas ao mesmo tempo
- Devolver erros ao modelo em vez de lançá-los
- Traçar a linha entre o que o modelo não vai fazer e o que ele não pode fazer
Configuração
Você precisa do Python 3.9 ou superior, do pacoterequests 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.
agent.py com os imports e o bloco de cabeçalho que toda chamada reutiliza:
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.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.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.
4. O que ele realmente faz
Conecte um bloco principal e execute:stderr conforme acontecem, para você acompanhar o funcionamento:
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.usage mostra isso valendo a pena:
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: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:SELECT para clientes espanhóis, não encontrou nenhum porque a coluna armazena ES em vez de Spain, e relatou isso:
run_query é a parte que não depende de uma escolha:
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.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.- Troque as ferramentas de SQL por chamadas HTTP e ele vira um agente de API.
- Adicione Busca e Scraping na Web como ferramenta e ele consegue consultar a web ao vivo no meio de uma resposta.
- Peça um resultado tipado em vez de prosa com Respostas Estruturadas.
- Veja uma versão maior desse padrão no Private Research Agent.
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.