baml
BAML é uma linguagem estaticamente tipada e orientada a expressões — TypeScript com métodos snake_case, campos nome: tipo,, enums, interfaces, genéricos, closures, encadeamento opcional, strings com crases e interpolação ${...}, .para_string() em qualquer valor, uma stdlib real. E uma DSL declarativa para chamadas de LLM (function … { client: prompt: }, test) que se dessugara nela, de modo que a saída estruturada de um modelo é apenas um valor de retorno tipado.
A CLI é a documentação. Descubra via baml describe:
brew install baml # Binário CLI: `baml`
baml init # Novo projeto (baml.toml + baml_src/)
baml help <comando> # Opções e exemplos da CLI
baml describe baml.json # Referência para qualquer módulo/tipo/método/assinatura/palavra-chave
baml describe Array --budget 120 # (Array, String, Map, assert, match, patterns, spawn, python, ...)
# Termina com "N mais linhas"? execute novamente com `--budget <N>`
baml check # Verificação de compilação do projeto
baml run -e 'expr' # Avalia uma expressão; feedback rápido + verificação de sintaxe
baml test --list && baml test # Executa todos os blocos de teste/testset
baml fmt baml_src/main.baml # Canonicaliza a formatação do projeto
baml describe <nome> imprime o corpo fonte completo das funções da stdlib — a maneira mais rápida de verificar o comportamento e o único caminho para builtins embutidos como assert (sem arquivo em disco). Funções puras não precisam de teste/cliente — verifique-as com baml run -e 'add(2, 3)'.
Não descreva APIs já demonstradas abaixo, a menos que encontre alguns erros. Você pode começar com base nos exemplos e usá-lo se encontrar mais erros ou se quiser detalhes reais da stdlib.
Na maior parte, comporta-se como JavaScript/TypeScript, com sintaxe muito semelhante — mas BAML é mais sólido/rigoroso.
Melhores práticas e informações
- Função LLM = retorno tipado. O TIPO DE RETORNO é o esquema que o modelo deve produzir (
class,enum, união literal,string[],T?). Saída estruturada é apenas um valor tipado — entregue-o a código comum. - Prompts são strings com crases com interpolação
${...}. Escrevaprompt:… ${arg} …, e sempre injete${ctx.formato_saida()}para um retorno estruturado. Escape com\/\${; aninhe com crases extras. - Clientes são valores, não blocos de configuração.
client Rapido = openai.ResponsesClient.new(model = "…", api_key = env.OPENAI_API_KEY);— o bloco antigoclient<llm> Nome { provider: …, options: {…} }foi removido. Qualquer coisa que implementeai.Clientfunciona (openai.ResponsesClient,openai.ChatClient,anthropic.Client, …; parâmetros do construtor diferem por provedor).api_keyebase_urlaceitamai.Credential: uma string literal,nullpara o padrão do provedor, ou uma referênciaenv.NOMEde ligação tardia resolvida no momento da requisição. Compõe confiabilidade encapsulando:ai.clients.Retry.new(inner = c, max_attempts = 3)eai.clients.RoundRobin.new(members = […])têm.new, masai.clients.Fallback { members: […] }não tem — construa-o como um literal de classe. Em seguida, useclient: Rapidona função, ou a abreviaçãoclient: "openai/gpt-4o-mini".baml describe openai/baml describe ai.Credential/baml describe ai.clients. - Molde o esquema com atributos de campo.
@description("…")adiciona uma dica///que o modelo vê em${ctx.formato_saida()};@alias("nome")renomeia a chave JSON emitida. Encadeie:tags: string[] @alias("rotulos") @description("…"). - Teste o código puro, não o modelo. Teste unitário de orquestração/pós-processamento em dados literais com
assert.*. Chamar uma função LLM em umtestcom um cliente ativo faz uma requisição real. Para testar prompts, requisições e análise offline, useF@spec(args)ou um cliente com script (veja Executando funções de IA). - Construa strings com interpolação, não coerção.
score=${n}transforma qualquer valor em string (.para_string()implícito); chame.para_string()apenas para a string.+precisa que ambos os lados já sejam strings ("n=" + 5não compila). catchpara alguns,catch_allpara todos.expr catch (e) { baml.errors.ParseError => fallback }lida com um erro específico;expr catch_all (e) { _ => fallback }é exaustivo — para um topo de workflow / ponto de entrada. Erros propagam-se implicitamente; os chamadores não precisam redeclarar. Levante comthrow baml.errors.InvalidArgument { message: "…" }(os tipos de erro são as classesbaml.errors.*embutidas —InvalidArgument/ParseError/Io/Timeout/…;baml describe baml.errors); anote uma assinatura falível com-> T throws ErrType. Prefira uma união de resultado tipada (type R = Ok | Err) em vez de lançar para fluxo de controle comum.- Interfaces = comportamento compartilhado + despacho dinâmico.
interface I { function m(self) -> T throws never }; cada método declara sua cláusulathrows. Um método com corpo é uma implementação padrão, que um implementador pode sobrescrever. Uma classe opta viaimplements I { … }; um valor tipado comoI(ouI[]) despacha para o implementador em tempo de execução. Interfaces também podem declarar tipos associados e limites genéricos.baml describe interfaces. requires, não herança. BAML não tem herança: uma classe não pode estender uma classe.interface Pet requires Animal, Named { … }torna essas interfaces pré-requisitos dePet. Os corpos padrão dePetpodem chamar seus métodos, e um valor tipado comoPetos expõe. Cada implementador dePettambém escreve seus próprios blocosimplements Animaleimplements Named(senão E0125). Um diamante derequirescompila, e a interface compartilhada é implementada uma vez; um ciclo é um erro. Quando duas interfaces em uma classe declaram o mesmo nome de método, uma chamada não qualificada é um erro (E0121); chameobj.como<I>.m()em vez disso.- Correspondência de padrões.
match (v) { … }sobre valores/tipos; os braços sãopadrão => expr— literais,let x: T(vincular + restringir), desestruturação de classeT { f: let y }, ou-padrõesA | B, guardas… if cond,_; deve ser exaustivo. Tambémv is T→ bool (restringe) eif let x: T = v { … } else { … }.baml describe patterns. - Restrição — apenas locais.
v is T,v != null/v == nulle um teste de veracidade restringemvapenas quandové um local (umletou um parâmetro) que nenhum closure captura: no ramoif/whileguardado, após uma saída antecipada (if (!(v is T)) { return … }), e no lado direito de&&(lado esquerdo verdadeiro) e||(lado esquerdo falso) —v is T && v.campo == 1,s == null || s.comprimento() == 0. NÃO restringido: um local capturado, um caminho de campo (h.v is T && h.v.campo→E0007) e um caminho de índice (xs[0] is T). Tarefas podem intercalar entre declarações, então outra tarefa ou um alias pode alterar um campo entre o teste e a leitura. Para um campo escrevalet v = h.v;primeiro, ouif let v: T = h.v { … }, oumatch (h.v) { let v: T => … }(cada um lê o campo uma vez). Uma atribuição ao local substitui o fato pelo tipo atribuído.baml describe is. - Concorrência = threads verdes.
spawn { … }retorna umFuture;awaito coleta. Combine muitos combaml.future.all/all_settled/race/any.all_settledretornaSuccess<T>,Failure<E>, ouPanickedpara cada entrada; o cancelamento de entrada é um resultadoPanicked. Configure um spawn com uma cláusulawith:spawn with limite, token { … }— cada valor listado é umbaml.spawn.Modifierque transforma o plano que o spawn constrói, da esquerda para a direita.baml.spawn.Limit.new(n)limita a concorrência (excessos de spawn enfileiram), umbaml.spawn.CancelTokencancela cooperativamente,baml.spawn.Root.new()desanexa a tarefa do cancelamento de seu spawner.baml describe spawn/baml describe baml.future. - Segurança de recursos —
defer,cleanup,catch (e, ctx).defer { … }executa um bloco na saída do escopo, LIFO, em todo caminho (retorno / lançamento / passagem) — como Go. Um método de classe chamadofunction cleanup(self) -> voidé um finalizador: executa no máximo uma vez por instância, seja chamando-o, usandodefer, ou o GC o reivindicando.catch (e, ctx)vincula umErrorContextjunto com o erro — um erro lançado ao lidar com outro encadeia-se nele, entãoctx.causa_raiz()/ctx.causapercorre de volta até a falha original ectx.para_string()renderiza toda a cadeia (estilo Python__context__).while let PADRAO = expr { … }itera até que o padrão falhe (por exemplo, drenando um.pop()que retornaT?). - Chame BAML de Python / TS. Declare um
[generator.<nome>]embaml.toml, executebaml generate, depois importe obaml_sdktipado. Instalação + uso:baml describe python/baml describe typescript/baml describe baml_sdk. - Acesso seguro em vez de indexação. Subscrito entra em pânico em um índice/chave ausente; use
.at(i)/.get(k)(→T?), acesse com?., padrão com??(parentetize:(m.get(k) ?? 0) + 1). - Métodos stdlib são snake_case, chamados em um valor. Alguns retornam novos, alguns mutam no local, alguns fazem ambos (
sort_by_keyclassifica o receptor e o retorna) — para ler a documentação,baml describe <palavra/tipo/identificador/palavra-chave/etc>. - Campos de classe
nome: tipo,; construaTipo { campo: valor }. Métodos recebem umselfsimples; fábricas são funções livres. Campos são mutáveis (como TS):obj.campo = veobj.campo += nfuncionam, e um métodoselfpode mutar no local — um método de efeito colateral retornavoid. Classes são tipos de referência:find/at(i)/subscrito retornam um alias ativo, não uma cópia, então mutar o resultado muta aquele elemento dentro do array (xs.find(p)?.n += 1atualizaxs), e uma classe passada para uma função pode ser mutada pelo chamado. Propagação de atualização de struct é suportada:User { ...u, nivel: Nivel.Gratuito }. Classes vazias são legais (class Marcador {}) — útil como variantes de união. Enums são variantes simples — sem métodos, sem dados associados (E.A.foo()não compila); coloque comportamento em funções livres que usammatch.enum E { A, B }, acesseE.A. - Blocos são expressões — a última expressão é o valor (sem
;);return x;para saída antecipada. Uma função apenas de efeito colateral retornavoid— outra grafia denull, o tipo unidade — e pode omitir sua expressão final oureturn;. Um valor deixado como a expressão final de tal corpo é um erro, não descartado: termine-o com;(xs.push(x);—pushretorna o novo comprimento).for (let x in xs)itera VALORES;while (cond) { … }itera. Closures(x) -> { ... }inferem parâmetro/retorno do contexto (anote(x: T) -> Rapenas quando ambíguo; o->é obrigatório)..map/.filterretornam arrays diretamente (sem.collect()). Mapa vazio precisa de um tipo:let m: map<string, int> = {};. - Sem ternário —
if/elseé a expressão. Não hácond ? a : b;if (cond) { a } else { b }é uma expressão que retorna um valor, então atribua diretamente:let rotulo = if (x > 3) { "grande" } else { "pequeno" };. Cada ramo é um bloco cuja última expressão é seu valor (semreturn). Encadeie comelse if, e emparelhe comif let PADRAO = expr { … } else { … }para vincular e restringir. - Condições usam veracidade.
if,while, guardas de match,&&,||, e!aceitam qualquer valor. Valores falsos sãofalse,null, zero numérico, strings vazias, arrays/mapas vazios e bytes vazios; todo o resto é verdadeiro.&&e||ainda retornambool, não um valor de operando; eles fazem curto-circuito. Um local opcional verdadeiro restringe no ramo tomado e no lado direito de&&(s && s.comprimento() > 0). - Arrays têm um conjunto de métodos semelhante a JS —
map/filter/filter_map/reduce/find/some/every/flat_map/slice/concat/join/includes/comprimento(), maispush/pop/shift/unshift/sort_by/sort_by_keyno local. A maioria recebe closures que podem usarthrows.baml describe Arrayfornece mais informações. - Ligações locais
letsão reassignáveis (x = x + 1) — sem palavra-chave mut (é TS let, não Rust); também não há const. - Args: padrões com
=, chamadas de palavra-chave com=(nunca:). Declare um padrão na assinatura:function f(a: int, b: int = 10); chamef(1)ouf(1, b = 2). Um parâmetro com padrão deve ser passado por nome —f(1, 2)é um erro (parâmetro com padrão 'b' deve ser passado por nome). Qualquer parâmetro (mesmo obrigatório) pode ser passado por nome (f(a = 1, b = 2)), e você pode pular um padrão do meio para definir um posterior (f(1, c = 9)). A sintaxe de palavra-chave énome = valor;nome: valornão será analisado (:é para tipos/campos).T?NÃO torna um argumento opcional — ao contrário de TSb?: T, um parâmetrob: T?ainda é obrigatório (você deve passarnull, senãoesperava N argumento(s), obteve …); adicione= nullpara torná-lo omitível. Builtins seguem isso:baml.http.fetch(url, timeout = baml.time.Duration.from_seconds(10)). - Onde diverge do TS (as armadilhas silenciosas): aritmética é orientada a tipos, não estilo TS.
int / inté divisão inteira truncada (285 / 100 == 2, NÃO2.85) e%é o resto (285 % 100 == 85); isso compila bem e apenas dá um número silenciosamente errado, então é a pegadinha de maior valor. Misture um float para obter divisão float (285 / 100.0 == 2.85,285.0 / 100 == 2.85); qualquer operação mistaint/floatpromove parafloat(5 + 2.0 == 7.0). Não há.para_float()— converta um int comn * 1.0(ou divida por um float). Um resultadointnão coagula automaticamente parafloatna atribuição (let x: float = 285 / 100é um erro de compilação).+é apenas numérico: concatenação de string precisa que ambos os lados já sejamstring("n=" + 5não compila — use interpolação${...}). Comparações (==,<, …,==estrutural) e&&/||/!são semelhantes a TS. - Testes: test
"nome" { ... }isolado (sem wrapper);testsetapenas AGRUPA. Afirmações (apenas 6):assert.equal/approx_equal/is_true/is_type/not_null/contains.assert.equalcompara estruturalmente (profundo, entre classes/arrays/mapas) — e o mesmo faz==simples, que é a forma bool.assert.equalé exato em floats; useassert.approx_equal(real, esperado, eps)para os computados. Última afirmação: sem;no final. IDs canônicos sãoroot::NomeTestepara um teste de nível superior eroot::Testset::NomeTestedentro de um testset. Execute um combaml test root::NomeTeste. - Saída formatada do modelo: para controle preciso sobre o JSON gerado, use
@asType(…)em um campo — funciona como@aliasmas para o tipo:@asType("string")força o campo a ser serializado como string (útil para IDs grandesint@asType("string")).baml describe asType. - Declarações de tipo (type alias).
type Id = int @asType("string");type Lista = array<int>. Útil para abstrações e para controlar a serialização. - Documentação embutida: além de
@descriptionnos campos, você pode usar comentários///para documentar módulos, classes e funções — eles aparecem nas saídas debaml describe. - Importações:
import "caminho/para/arquivo"— sem módulos explícitos; o nome do arquivo (sem extensão) torna-se o namespace. Use imports para organizar funções, tipos e clientes em diferentes arquivos. baml.toml: arquivo de configuração do projeto. Define o nome do projeto, geradores e outras opções. Vejabaml describe baml.tomlpara detalhes completos.- Ambiente: variáveis de ambiente são acessíveis via
env.NOME. Use-as para chaves de API, URLs base, etc.baml describe env. - Erros comuns (E + número): o compilador BAML emite códigos de erro com mensagens descritivas.
baml describe E0121, por exemplo, explica o erro e como corrigi-lo.
Executando funções de IA
Para testar e executar funções de IA sem gerar SDK, use baml run:
baml run -e 'minhaFuncaoIA(args)'
baml run -e 'minhaFuncaoIA(args)' --client Rapido
Para teste offline (sem chamada de modelo real), crie um cliente script:
client Scriptado = openai.ResponsesClient.new(
api_key = "fake",
base_url = "http://localhost:8080/v1" # Servidor mock
);
function minhaFuncaoIA(args: Args) -> Saida {
client Scriptado
prompt: `...`
}
Ou use F@spec(args) para injetar saída diretamente:
test "teste offline" {
let saida = minhaFuncaoIA@spec(args: { ... }) # Injeta especificação diretamente
assert.equal(saida, esperado)
}
Exemplo completo
# baml_src/main.baml
class Entrada {
texto: string,
}
enum Sentimento {
Positivo,
Negativo,
Neutro,
}
function classificar_sentimento(input: Entrada) -> Sentimento {
client "openai/gpt-4o-mini"
prompt: `
Classifique o sentimento do seguinte texto.
${ctx.formato_saida()}
Texto: ${input.texto}
`
}
test "classificar positivo" {
let resultado = classificar_sentimento(Entrada { texto: "Estou muito feliz!" })
assert.equal(resultado, Sentimento.Positivo)
}