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 assinatura | O que ele é | Pergunta gerada |
|---|---|---|
Literal["1d", "1w", "1mo", "3mo"] | um valor de uma lista | um Choice com exatamente esses valores |
list[Literal[...]] | vários valores da lista | um Noul por membro |
bool | ligado ou desligado | um Noul |
int, texto livre, data | não é conjunto fechado | nenhuma 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:
| Pedido | Chamada resultante | Confianç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.