//Receita

Function calling com decisões tipadas

Um pedido em português entra; sai o nome da função e os argumentos, cada um com a sua confiança. Sem JSON malformado no caminho.

Um analista digita “compara nvda, amd e msft nos últimos três meses” e espera um gráfico. Do outro lado existe uma função com assinatura fixa, que aceita uma lista de tickers de um conjunto conhecido e uma janela de tempo entre quatro valores possíveis. Alguém precisa transformar a frase na chamada.

O jeito clássico é pedir a um LLM que escreva o JSON da chamada. Funciona na maioria das vezes, e a minoria é o problema.

Por que a solução ingênua falha

Quando o modelo escreve a chamada, ele pode escrever qualquer coisa: uma janela de tempo que não existe ("últimos 90 dias" quando a função aceita "3mo"), um ticker com erro de digitação, um campo que sobra, um JSON que não fecha. Você então valida, trata o erro e tenta de novo — e cada tentativa custa tempo e dinheiro.

Pior: a falha é silenciosa quando o valor é plausível. resolution="1day" em vez de "1d" não parece errado numa revisão de código.

A documentação do Jev encosta exatamente nesse ponto ao listar geração de texto entre os modos de falha do jev-1.13 e recomendar a inversão: quando o espaço de resposta é limitado, transforme a tarefa em escolha entre opções.

O desenho das perguntas

A ideia da receita é ler a própria assinatura da função. Os tipos já dizem quais argumentos vêm de um conjunto fechado:

Tipo na assinaturaO que ele éPergunta gerada
Literal["1d", "1w", "1mo", "3mo"]um valor de uma listaum Choice com exatamente esses valores
list[Literal[...]]vários valores da listaum Noul por membro
boolligado ou desligadoum Noul
int, texto livre, datanão é conjunto fechadonenhuma pergunta; vale o padrão da função

O que você escreve à mão não é código: é a descrição. O tipo diz que a janela aceita "3mo"; ele não diz que “neste trimestre” significa isso. A descrição diz.

from typesafe_sdk import Choice, Noul

PERGUNTAS = {
    # qual função chamar
    "__funcao__": Choice(
        instructions="O que o usuário está pedindo ao assistente?",
        criteria={
            "plot_price": "Desenhar o preço de um ativo ao longo do tempo",
            "compare_returns": "Comparar o retorno de vários ativos no mesmo período",
            "rolling_correlation": "Medir se um ativo acompanha outro ao longo do tempo",
            "list_symbols": "Listar os ativos disponíveis",
        },
    ),
    # um argumento de conjunto fechado
    "rolling_correlation.symbol": Choice(
        instructions="Qual ativo está sendo medido, o primeiro nomeado?",
        criteria={"SPY": None, "NVDA": None, "AMD": None, "AAPL": None},
    ),
    "rolling_correlation.benchmark": Choice(
        instructions="Qual é o segundo ativo nomeado, o que serve de régua?",
        criteria={"SPY": None, "NVDA": None, "AMD": None, "AAPL": None},
    ),
    # o argumento é opcional: o pedido menciona a janela?
    "rolling_correlation.window?": Noul(
        instructions="O pedido diz de que período ele fala, como 'no último mês'?",
    ),
}

Dois detalhes que fazem a receita funcionar. Primeiro, a pergunta de menção: quando ela responde não, o argumento sai da chamada e o padrão da função vale. Sem ela, a escolha seria obrigada a nomear alguma janela — e nomearia com confiança alta, o que é pior que omitir.

Segundo, descreva o papel e não o nome do parâmetro. Dois argumentos que sorteiam da mesma lista de tickers só se separam se a descrição disser qual é o medido e qual é a régua. “Qual resolução?” não dá ao pedido nada com que casar.

O que o experimento oficial mediu

O cookbook monta um assistente de análise de mercado com 10 funções e 28 argumentos preenchíveis, sobre uma base de 156.780 barras de um minuto, e manda 54 perguntas por comando em uma requisição. Os números são da TypeSafe, com o jev-1.12:

PedidoChamada resultanteConfiança
“plot rolling correlation between nvda and spy for the past month”rolling_correlation(symbol='NVDA', benchmark='SPY', window='1mo')0,91
“compare nvda amd and msft over the past three months”compare_returns(symbols=['NVDA','AMD','MSFT'], window='3mo')0,94
“show me apple daily with volume”plot_price(symbol='AAPL', resolution='1d', include_volume=True)0,75
“what tickers do you have”list_symbols()1,00
“is amd tracking nvidia lately”rolling_correlation(symbol='AMD', benchmark='NVDA')0,82

O último é o mais instrutivo. “Lately” não diz janela nem resolução, e as duas perguntas de menção responderam isso com 0,96 e 0,99 — então os dois argumentos saíram da chamada e a função rodou com os próprios padrões. Já symbol e benchmark ficaram em 0,87 e 0,78: o modelo colocou cada ticker no papel certo, e a confiança da chamada (0,82) é a do argumento mais fraco.

Essa escolha de cálculo está documentada e vale copiar: a confiança da chamada é o mínimo entre os julgamentos, não o produto. Um argumento errado já estraga o resultado, e o produto cairia só porque a função tem muitos argumentos, mesmo quando nenhum julgamento é frágil.

O que muda em português

A estrutura é idêntica; o trabalho migra todo para as descrições. Três ajustes que fizemos questão de testar em português:

Vocabulário de período. “Último trimestre”, “nos últimos 90 dias”, “desde segunda”, “no acumulado do mês” — cada um precisa aparecer na descrição da opção correspondente, ou a pergunta de menção vai dizer que o pedido não informa.

Papéis na nossa ordem. Em português dizemos “compara a Vale com o Ibovespa” e também “o Ibovespa e a Vale, como se comportaram”. A descrição de papel precisa dizer “o primeiro nomeado” com clareza, porque a ordem da frase carrega o papel.

Pedido educado com verbo escondido. “Será que dá pra ver o gráfico da Petro?” é um pedido de gráfico. Como a documentação declara que o inglês é a língua principal de treino, teste os seus comandos reais antes de confiar nos limiares.

Onde isso quebra

  • Argumento que não é conjunto fechado. Valor, data e texto livre ficam fora. Para eles, a receita companheira é extrair valores — regex acha, o modelo escolhe.
  • Muitas funções. A escolha da função é um Choice, e Choice tem teto de 255 opções. Com centenas de funções, hierarquize: primeiro a família, depois a função.
  • Encadeamento. A receita resolve uma chamada. Sequência de chamadas em que a segunda depende do resultado da primeira exige segunda requisição, e a documentação trata isso como exceção.
  • Leitura literal e negação. “Não quero o gráfico com volume” é o tipo de frase que a documentação aponta como custosa em acurácia; está nos limites do jev-1.13.

Quando a escolha é entre ferramentas de um agente e não argumentos de uma função, a receita companheira é escolher skill de agente. O índice está em receitas.

Perguntas frequentes

Qual a diferença disso e o function calling de um LLM?

O LLM escreve o JSON da chamada, e por isso pode escrever um valor que a função não aceita. Aqui cada argumento de conjunto fechado é uma pergunta cujas opções são exatamente os valores válidos, então o que chega na função é sempre aceito por ela.

E os argumentos que não são conjunto fechado?

Não recebem pergunta. Número, data e texto livre ficam de fora e a função usa o próprio padrão. Se você precisa de um número extraído do pedido, a receita de extrair valores resolve com regex mais escolha.

Como um argumento fica opcional?

Com uma segunda pergunta de sim ou não: o pedido menciona isso? Quando a resposta é não, o argumento sai da chamada e o padrão da função vale. Sem isso, a escolha seria obrigada a nomear algum valor, e nomearia com confiança.

Como a confiança da chamada é calculada?

É o julgamento menos seguro entre os argumentos, não o produto de todos. O cookbook explica a escolha: um argumento errado já estraga o resultado, e o produto cairia só por a função ter muitos argumentos.

Quantas perguntas isso custa?

No experimento, 54 perguntas por comando, todas em uma requisição: a escolha da função mais os argumentos de todas as funções. O código lê apenas os argumentos da função escolhida.

Quer dominar decisões com IA em português? O curso da comunidade está em pré-venda.

Garantir pré-venda por R$ 499,00

Pré-venda: R$ 499,00 · Após o lançamento: R$ 799,00