baml
BAML ist eine statisch typisierte, ausdrucksorientierte Sprache – TypeScript mit snake_case-Methoden, name: type,-Feldern, Aufzählungen, Interfaces, Generics, Closures, optionalem Chaining, Backtick-Strings mit ${...}-Interpolation, .to_string() auf jedem Wert, einer echten Standardbibliothek. Und einer deklarativen DSL für LLM-Aufrufe (function … { client: prompt: }, test), die sich darin auflöst, sodass die strukturierte Ausgabe eines Modells einfach ein typisierter Rückgabewert ist.
Die CLI ist die Dokumentation. Entdecke via baml describe:
brew install baml # CLI-Binärdatei: `baml`
baml init # neues Projekt (baml.toml + baml_src/)
baml help <command> # CLI-Optionen und Beispiele
baml describe baml.json # Referenz für jedes Modul/jeden Typ/jede Methode/jede Signatur/jedes Schlüsselwort
baml describe Array --budget 120 # (Array, String, Map, assert, match, patterns, spawn, python, ...)
# endet mit „N more lines“? erneut ausführen mit `--budget <N>`
baml check # Projekt auf Kompilierungsfehler prüfen
baml run -e 'expr' # Ausdruck auswerten; schnelles Feedback + Syntaxprüfung
baml test --list && baml test # jeden test/testset-Block ausführen
baml fmt baml_src/main.baml # Formatierung des Projekts kanonisieren
baml describe <name> gibt den vollständigen Quelltext der Stdlib-Funktionen aus – der schnellste Weg, das Verhalten zu überprüfen, und der einzige Pfad für eingebaute Funktionen wie assert (keine Datei auf der Festplatte). Reine Funktionen benötigen keinen Test/Client – überprüfe sie mit baml run -e 'add(2, 3)'.
Beschreibe keine APIs, die bereits unten demonstriert werden, es sei denn, du stößt auf Fehler. Du kannst auf der Grundlage der Beispiele beginnen und sie bei weiteren Fehlern oder für tatsächliche Stdlib-Details verwenden.
Es verhält sich meistens wie JavaScript/TypeScript, mit sehr ähnlicher Syntax – aber BAML ist sounder/strikter.
Best Practices und Informationen
- LLM-Funktion = typisierter Rückgabewert. Der RÜCKGABETYP ist das Schema, das das Modell produzieren muss (
class,enum, Literal-Union,string[],T?). Strukturierte Ausgabe ist einfach ein typisierter Wert – übergib ihn an gewöhnlichen Code. - Prompts sind Backtick-Strings mit
${...}-Interpolation. Schreibeprompt:… ${arg} …und injiziere immer${ctx.output_format()}für eine strukturierte Rückgabe. Escape mit\`` /${`; verschachteln mit zusätzlichen Backticks. - Clients sind Werte, keine Konfigurationsblöcke.
client Fast = openai.ResponsesClient.new(model = "…", api_key = env.OPENAI_API_KEY);– der alte Blockclient<llm> Name { provider: …, options: {…} }ist entfernt. Alles, wasai.Clientimplementiert, funktioniert (openai.ResponsesClient,openai.ChatClient,anthropic.Client, …; Konstruktorparameter unterscheiden sich je nach Anbieter).api_keyundbase_urlakzeptierenai.Credential: einen Literal-String,nullfür den Standard des Anbieters oder eine spät gebundeneenv.NAME-Referenz, die zur Anfragezeit aufgelöst wird. Erhöhe die Zuverlässigkeit durch Wrapper:ai.clients.Retry.new(inner = c, max_attempts = 3)undai.clients.RoundRobin.new(members = […])haben.new, aberai.clients.Fallback { members: […] }hat keines – konstruiere es als Klassenliteral. Verwende dannclient: Fastin der Funktion oder die Kurzformclient: "openai/gpt-4o-mini".baml describe openai/baml describe ai.Credential/baml describe ai.clients. - Forme das Schema mit Feldattributen.
@description("…")fügt einen///-Hinweis hinzu, den das Modell in${ctx.output_format()}sieht;@alias("name")benennt den ausgegebenen JSON-Schlüssel um. Verkettung:tags: string[] @alias("labels") @description("…"). - Teste den reinen Code, nicht das Modell. Unit-Tests für Orchestrierung/Post-Processing auf Literaldaten mit
assert.*. Das Aufrufen einer LLM-Funktion in einemtestmit einem Live-Client führt zu einer echten Anfrage. Um Prompts, Requests und Parsing offline zu testen, verwendeF@spec(args)oder einen skriptgesteuerten Client (siehe KI-Funktionen ausführen). - Baue Strings mit Interpolation, nicht mit Zwang.
score=${n}konvertiert jeden Wert in einen String (implizites.to_string()); rufe.to_string()für den String allein auf.+benötigt beide Seiten bereits als Strings ("n=" + 5wird nicht kompilieren). catchfür einige,catch_allfür alle.expr catch (e) { baml.errors.ParseError => fallback }behandelt einen bestimmten Fehler;expr catch_all (e) { _ => fallback }ist erschöpfend – für einen Workflow-Top / Einstiegspunkt. Fehler propagieren implizit; Aufrufer müssen sie nicht neu deklarieren. Auslösen mitthrow baml.errors.InvalidArgument { message: "…" }(Fehlertypen sind die eingebautenbaml.errors.*-Klassen –InvalidArgument/ParseError/Io/Timeout/…;baml describe baml.errors); annotiere eine fehlerhafte Signatur mit-> T throws ErrType. Bevorzuge eine typisierte Ergebnis-Union (type R = Ok | Err) gegenüber dem Werfen für die gewöhnliche Steuerung.- Interfaces = gemeinsames Verhalten + dynamischer Dispatch.
interface I { function m(self) -> T throws never }; jede Methode deklariert ihrethrows-Klausel. Eine Methode mit einem Rumpf ist eine Standardimplementierung, die ein Implementierer überschreiben kann. Eine Klasse optiert viaimplements I { … }ein; ein Wert, der alsI(oderI[]) typisiert ist, dispatches zur Laufzeit an den Implementierer. Interfaces können auch assoziierte Typen und generische Beschränkungen deklarieren.baml describe interfaces. requires, keine Vererbung. BAML hat keine Vererbung: Eine Klasse kann keine andere Klasse erweitern.interface Pet requires Animal, Named { … }macht diese Interfaces zu Voraussetzungen vonPet. Die Standard-Rümpfe vonPetkönnen ihre Methoden aufrufen, und ein Wert, der alsPettypisiert ist, macht sie verfügbar. JederPet-Implementierer schreibt auch seine eigenenimplements Animal- undimplements Named-Blöcke (sonst E0125). Ein Diamant vonrequireskompiliert, und das gemeinsame Interface wird einmal implementiert; ein Zyklus ist ein Fehler. Wenn zwei Interfaces für eine Klasse denselben Methodennamen deklarieren, ist ein unqualifizierter Aufruf ein Fehler (E0121); rufe stattdessenobj.as<I>.m()auf.- Pattern Matching.
match (v) { … }über Werte/Typen; Zweige sindpattern => expr– Literale,let x: T(binden + eingrenzen), KlassendestrukturierungT { f: let y }, Oder-MusterA | B, Wächter… if cond,_; muss erschöpfend sein. Auchv is T→ bool (schränkt ein) undif let x: T = v { … } else { … }.baml describe patterns. - Eingrenzung – nur lokale Variablen.
v is T,v != null/v == nullund ein Wahrheitstest grenzenvnur ein, wennveine lokale Variable ist (einletoder ein Parameter), die kein Closure erfasst: im bewachtenif-/while-Zweig, nach einem frühen Ausstieg (if (!(v is T)) { return … }) und auf der rechten Seite von&&(linke Seite wahr) und||(linke Seite falsch) –v is T && v.f == 1,s == null || s.length() == 0. NICHT eingegrenzt: eine erfasste lokale Variable, ein Feldpfad (h.v is T && h.v.f→E0007) und ein Indexpfad (xs[0] is T). Tasks können zwischen Anweisungen wechseln, sodass eine andere Task oder ein Alias ein Feld zwischen dem Test und dem Lesen ändern kann. Schreibe für einen Feldzugriff zuerstlet v = h.v;oderif let v: T = h.v { … }odermatch (h.v) { let v: T => … }(jede liest das Feld einmal). Eine Zuweisung an die lokale Variable ersetzt die Tatsache durch den zugewiesenen Typ.baml describe is. - Nebenläufigkeit = grüne Threads.
spawn { … }gibt einFuturezurück;awaitsammelt es ein. Kombiniere viele mitbaml.future.all/all_settled/race/any.all_settledgibt für jede EingabeSuccess<T>,Failure<E>oderPanickedzurück; eine Eingabestornierung ist einPanicked-Ergebnis. Konfiguriere einen Spawn mit einerwith-Klausel:spawn with limit, token { … }– jeder aufgeführte Wert ist einbaml.spawn.Modifier, der den Plan, den der Spawn erstellt, von links nach rechts transformiert.baml.spawn.Limit.new(n)begrenzt die Nebenläufigkeit (überzählige Spawns werden in die Warteschlange gestellt), einbaml.spawn.CancelTokenbricht kooperativ ab,baml.spawn.Root.new()trennt die Aufgabe von der Stornierung ihres Erzeugers.baml describe spawn/baml describe baml.future. - Ressourcensicherheit –
defer,cleanup,catch (e, ctx).defer { … }führt einen Block beim Verlassen des Gültigkeitsbereichs aus, LIFO, auf jedem Pfad (return / throw / fall-through) – wie Go. Eine Klassenmethode namensfunction cleanup(self) -> voidist ein Finalizer: Sie wird höchstens einmal pro Instanz ausgeführt, egal ob du sie aufrufst, perdeferplanst oder der GC sie freigibt.catch (e, ctx)bindet einenErrorContextzusammen mit dem Fehler – ein Fehler, der während der Behandlung eines anderen ausgelöst wird, kettet sich daran an, sodassctx.root_cause()/ctx.causezum ursprünglichen Fehler zurückgehen undctx.to_string()die gesamte Kette darstellt (Python__context__-Stil).while let PATTERN = expr { … }schleift, bis das Muster fehlschlägt (z. B. Abarbeiten einesT?-zurückgebenden.pop()). - Rufe BAML aus Python/TS auf. Deklariere einen
[generator.<name>]inbaml.toml, führebaml generateaus, importiere dann das typisiertebaml_sdk. Installation + Nutzung:baml describe python/baml describe typescript/baml describe baml_sdk. - Sicherer Zugriff über Indizierung. Subscript führt bei einem fehlenden Index/Schlüssel zu einem Fehler; verwende
.at(i)/.get(k)(→T?), greife mit?.durch, verwende Standard mit??(Klammerung:(m.get(k) ?? 0) + 1). - Stdlib-Methoden sind snake_case und werden auf einem Wert aufgerufen. Einige geben ein neues Ergebnis zurück, andere mutieren direkt, wenige tun beides (
sort_by_keysortiert den Empfänger und gibt ihn zurück) – um die Dokumentation zu lesen,baml describe <word/type/identifier/keyword/etc>. - Klassenfelder
name: type,; KonstruktionType { field: val }. Methoden nehmen ein einfachesself; Fabriken sind freie Funktionen. Felder sind veränderlich (wie TS):obj.field = vundobj.field += nfunktionieren, und eineself-Methode kann direkt mutieren – eine Methode mit Seiteneffekt gibtvoidzurück. Klassen sind Referenztypen:find/at(i)/Subscript geben einen Live-Alias zurück, keine Kopie, sodass die Mutation des Ergebnisses dieses Element innerhalb des Arrays mutiert (xs.find(p)?.n += 1aktualisiertxs), und eine an eine Funktion übergebene Klasse kann vom Aufgerufenen mutiert werden. Struct-Update-Spread wird unterstützt:User { ...u, tier: Tier.Free }. Leere Klassen sind legal (class Marker {}) – praktisch als Union-Varianten. Enums sind einfache Varianten – keine Methoden, keine assoziierten Daten (E.A.foo()wird nicht kompilieren); setze Verhalten in freie Funktionen, diematchverwenden.enum E { A, B }, zugreifen mitE.A. - Blöcke sind Ausdrücke – der letzte Ausdruck ist der Wert (kein
;);return x;für frühen Ausstieg. Eine Funktion nur mit Seiteneffekten gibtvoidzurück – eine andere Schreibweise fürnull, den Unit-Typ – und kann ihren letzten Ausdruck weglassen oderreturn;. Ein Wert, der als letzter Ausdruck eines solchen Rumpfes übrig bleibt, ist ein Fehler, wird nicht verworfen: beende ihn mit;(xs.push(x);–pushgibt die neue Länge zurück).for (let x in xs)iteriert WERTE;while (cond) { … }schleift. Closures(x) -> { ... }leiten Parameter/Rückgabe aus dem Kontext ab (annotiere(x: T) -> Rnur bei Mehrdeutigkeit; der->ist erforderlich)..map/.filtergeben Arrays direkt zurück (kein.collect()). Leere Map benötigt einen Typ:let m: map<string, int> = {};. - Kein ternärer Operator –
if/elseist der Ausdruck. Es gibt keincond ? a : b;if (cond) { a } else { b }ist ein Ausdruck, der einen Wert zurückgibt, also weise ihn direkt zu:let label = if (x > 3) { "big" } else { "small" };. Jeder Zweig ist ein Block, dessen letzter Ausdruck sein Wert ist (keinreturn). Verkette mitelse ifund kombiniere mitif let PATTERN = expr { … } else { … }zum Binden und Eingrenzen. - Bedingungen verwenden Wahrheitswerte.
if,while, Match-Wächter,&&,||und!akzeptieren jeden Wert. Falsy-Werte sindfalse,null, numerische Null, leere Strings, leere Arrays/Maps und leere Bytes; alles andere ist truthy.&&und||geben immer nochboolzurück, keinen Operandenwert; sie kursschließen. Eine truthy optionale lokale Variable grenzt im genommenen Zweig und auf der rechten Seite von&&ein (s && s.length() > 0). - Arrays haben einen JS-ähnlichen Methodensatz –
map/filter/filter_map/reduce/find/some/every/flat_map/slice/concat/join/includes/length(), plus direktepush/pop/shift/unshift/sort_by/sort_by_key. Die meisten nehmen Closures, diethrowskönnen.baml describe Arraygibt weitere Informationen. - Lokale
let-Bindungen sind neu zuweisbar (x = x + 1) – keinmut-Schlüsselwort (es ist TSlet, nicht Rust); es gibt auch keinconst. - Argumente: Standardwerte mit
=, Schlüsselwortaufrufe mit=(nie:). Deklariere einen Standardwert in der Signatur:function f(a: int, b: int = 10); rufef(1)oderf(1, b = 2)auf. Ein Parameter mit Standardwert muss per Name übergeben werden –f(1, 2)ist ein Fehler („defaulted parameter 'b' must be passed by name“). Jeder Parameter (auch erforderliche) kann per Name übergeben werden (f(a = 1, b = 2)), und du kannst einen mittleren Standardwert überspringen, um einen späteren zu setzen (f(1, c = 9)). Die Schlüsselwortsyntax istname = value;name: valuewird nicht geparst (:ist für Typen/Felder).T?macht ein Argument NICHT optional – im Gegensatz zu TSb?: Tist einb: T?-Parameter immer noch erforderlich (du musstnullübergeben, sonst „expected N argument(s), got …“); füge= nullhinzu, um es auslassbar zu machen. Eingebaute Funktionen folgen diesem Muster:baml.http.fetch(url, timeout = baml.time.Duration.from_seconds(10)). - Wo es sich von TS unterscheidet (die stillen Fallen): Arithmetik ist typgesteuert, nicht im TS-Stil.
int / intist abschneidende Ganzzahldivision (285 / 100 == 2, NICHT2.85) und%ist der Rest (285 % 100 == 85); dies kompiliert einwandfrei und ergibt einfach eine stillschweigend falsche Zahl, daher ist es die wertvollste Falle. Mische einen Float hinein, um eine Gleitkommadivision zu erhalten (285 / 100.0 == 2.85,285.0 / 100 == 2.85); jede gemischteint/float-Operation fördert zufloat(5 + 2.0 == 7.0). Es gibt kein.to_float()– konvertiere einen int mitn * 1.0(oder dividiere durch einen Float). Einint-Ergebnis wird nicht automatisch infloatumgewandelt (let x: float = 285 / 100ist ein Kompilierungsfehler).+ist nur numerisch: String-Konkatenation erfordert, dass beide Seiten bereitsstringsind ("n=" + 5wird nicht kompilieren – verwende stattdessen${...}-Interpolation). Vergleiche (==,<, …, strukturelles==) und&&/||/!sind wie in TS. - Tests: einzelnes
test "name" { ... }(kein Wrapper);testsetGRUPPIERT nur. Assertions (nur 6):assert.equal/approx_equal/is_true/is_type/not_null/contains.assert.equalvergleicht strukturell (tief, über Klassen/Arrays/Maps) – und das tut auch einfaches==, das die Bool-Form ist.assert.equalist exakt bei Floats; verwendeassert.approx_equal(actual, expected, eps)für berechnete Werte. Letztes Assert: kein abschließendes;. Kanonische IDs sindroot::TestNamefür einen Test der obersten Ebene undroot::Testset::TestNameinnerhalb eines Testsets. Führe einen mitbaml test root::Testset::TestNameaus. - Tabellen und Daten:
baml describe data/baml describe tables. bamlxverwaltet extrahierte Pakete. Stöpsle ein Paket ein:bamlx add <gh/{owner}/{repo}>(oderbamlx add git <url>).bamlx statuslistet installierte Pakete auf;bamlx updateaktualisiert;bamlx remove <name>entfernt;bamlx treezeigt die Abhängigkeitsstruktur. Deklariere ein Paket mit[dependency.<name>]inbaml.toml(Quelle ist eines vonregistry,git,path).baml describe bamlx.
KI-Funktionen ausführen
Führe eine beliebige function aus, die einen { client: prompt: }-Block enthält:
# eval gibt den strukturierten Rückgabetyp aus (JSON-ähnlich)
baml run -e 'my_llm_func(args)'
# my_llm_func(args) -- ruft das LLM auf und gibt die Antwort aus
⏳ Läuft langsam? Kein Problem. Konfiguriere einen Schnellspek-Client für die Offline-Iteration:
client NonLLM = ai.TestClient.new(wire = 🌟 json-string, delay = 0.05) # <-- rufe `ai.TestClient.new` auf
client NonLLM = ai.TestClient.new(file = "specs/some_spec.json", delay = 0.05) # `file` lädt aus einem Pfad
function MyFunc(x: int) -> MyResponse {
client: NonLLM # <-- verwende den Testclient
prompt: ````` # <-- (Backtick-string) wird nicht ausgeführt, aber prompt und ausgabe werden protokolliert
}
**`ai.TestClient`** ist eine Klasse – `baml describe ai.TestClient`. Der Klassentyp hat Methoden `.new(wire = …)` und `.new(file = …)`. Er zeichnet den Prompt/die Anfrage/die Ausgabe auf und gibt `wire` oder `file` als Ausgabe zurück. *Hinweis zur Eingabe von Backtick-Strings in der Konsole*: `baml run` akzeptiert Prompts als String-Parameter (`client: "test"`) – für einen `string`-Prompt, um `baml run`-Eingaben zu umgehen. Prompt-Vorlagen können in Stdlib-Dokumenten übergeben werden.
## LLM-Ausgabe-Parsing
Ein LLM-Aufruf gibt einen Wert des Rückgabetyps zurück (kein roher Text). Die strukturierte Ausgabe wird aus dem Prompt unter Verwendung von `${ctx.output_format()}` geparst.
**Über Parsing-Fehler hinweghelfen:** Füge der Funktion einen `catch`-Block hinzu oder verwende `retry` auf einem Client: `client Fast = ai.clients.Retry.new(inner = BaseClient, max_attempts = 3)`. Verwende für `enum`-Felder einen `@alias`, um Alternativnamen zu unterstützen.
## Kontext und Tools
Für Tool-/Funktionsaufrufe initialisiere den Client mit dem Namen des Tools, das der LLM aufrufen kann. Definiere ein Tool als normale BAML-Funktion:
```baml
function get_weather(city: string) -> string {
# kein `client`/`prompt` – es ist eine reine BAML-Funktion
return if (