baml
BAML 是一種靜態型別、表達式導向的語言——TypeScript 擁有 snake_case 方法、name: type, 欄位、列舉、介面、泛型、閉包、可選鏈、帶 ${...} 插值的反引號字串、所有值上的 .to_string() 方法、以及真正的標準庫。此外,它還包含一個用於 LLM 呼叫的宣告式 DSL(function … { client: prompt: }、test),該 DSL 會脫糖轉換為 BAML 本身,因此模型的結構化輸出只是一個型別化的回傳值。
CLI 就是文件。透過 baml describe 探索:
brew install baml # CLI 二進位檔:`baml`
baml init # 新專案(baml.toml + baml_src/)
baml help <command> # CLI 選項與範例
baml describe baml.json # 查詢任何模組/型別/方法/簽章/關鍵字的參考
baml describe Array --budget 120 # (Array、String、Map、assert、match、patterns、spawn、python、……)
# 結尾顯示「還有 N 行」?重新執行並加上 `--budget <N>`
baml check # 編譯檢查專案
baml run -e 'expr' # 求值表達式;快速回饋 + 語法檢查
baml test --list && baml test # 執行每個 test/testset 區塊
baml fmt baml_src/main.baml # 正規化專案格式
baml describe <name> 會印出標準庫函式的完整原始碼本體——這是驗證行為最快的方式,也是存取像 assert 這類內建函式的唯一途徑(因為它們沒有對應的磁碟檔案)。純函式不需要測試或用戶端——使用 baml run -e 'add(2, 3)' 即可檢查它們。
除非遇到錯誤,否則不需要描述下面已經展示過的 API。你可以根據範例開始使用,如果遇到更多錯誤或需要實際的標準庫細節時再查詢。
大多數情況下,它的行為類似 JavaScript / TypeScript,語法也非常相似——但 BAML 更健全 / 更嚴格。
最佳實踐與資訊
- LLM 函式 = 型別化回傳。 回傳型別 就是 模型必須產生的結構描述(
class、enum、字面量聯集、string[]、T?)。結構化輸出只是一個型別化的值——將其傳遞給一般程式碼即可。 - 提示詞是帶
${...}插值的反引號字串。 寫成prompt:… ${arg} …,並務必注入${ctx.output_format()}以獲得結構化回傳。使用\`` /${` 跳脫;使用更多反引號進行巢狀。 - 用戶端是值,不是設定區塊。
client Fast = openai.ResponsesClient.new(model = "…", api_key = env.OPENAI_API_KEY);——舊的client<llm> Name { provider: …, options: {…} }區塊已移除。任何實作ai.Client的東西都可以使用(openai.ResponsesClient、openai.ChatClient、anthropic.Client、……;建構子參數因供應商而異)。api_key和base_url接受ai.Credential:可以是字面字串、null(使用供應商預設值)、或一個在請求時才解析的晚期繫結env.NAME參考。透過包裝來組合可靠性:ai.clients.Retry.new(inner = c, max_attempts = 3)和ai.clients.RoundRobin.new(members = […])使用.new,但ai.clients.Fallback { members: […] }沒有.new——將其作為類別字面量建構。然後在函式中使用client: Fast,或使用簡寫client: "openai/gpt-4o-mini"。baml describe openai/baml describe ai.Credential/baml describe ai.clients。 - 使用欄位屬性來塑造結構描述。
@description("…")添加一個///提示,模型在${ctx.output_format()}中會看到;@alias("name")重新命名輸出的 JSON 鍵。可串聯:tags: string[] @alias("labels") @description("…")。 - 測試純程式碼,而非模型。 使用
assert.* 對文字資料進行單元測試編排 / 後處理。在test中使用真實用戶端呼叫 LLM 函式會發出真實請求。要離線測試提示詞、請求和解析,請使用F@spec(args)或腳本化用戶端(參見執行 AI 函式)。 - 使用插值建立字串,而非強制轉型。
score=${n}會將任何值字串化(隱式呼叫.to_string());單獨取得字串時請呼叫.to_string()。+需要兩邊都是字串("n=" + 5無法編譯)。 catch用於特定錯誤,catch_all用於所有錯誤。expr catch (e) { baml.errors.ParseError => fallback }處理特定錯誤;expr catch_all (e) { _ => fallback }是窮舉的——用於工作流程頂層 / 進入點。錯誤會隱式傳播;呼叫者無需重新宣告。使用throw baml.errors.InvalidArgument { message: "…" }拋出錯誤(錯誤型別是內建的baml.errors.*類別——InvalidArgument/ParseError/Io/Timeout/ ……;baml describe baml.errors);使用-> T throws ErrType標註可失敗的簽章。對於一般控制流程,建議使用型別化的結果聯集(type R = Ok | Err)而非拋出。- 介面 = 共享行為 + 動態派發。
interface I { function m(self) -> T throws never };每個方法宣告其throws子句。帶有本體的方法是預設實作,實作者可以覆寫它。類別透過implements I { … }選擇加入;型別為I(或I[])的值會在執行時期派發給實作者。介面也可以宣告關聯型別和泛型邊界。baml describe interfaces。 requires,而非繼承。 BAML 沒有繼承:類別不能擴展類別。interface Pet requires Animal, Named { … }使得這些介面成為Pet的前提條件。Pet的預設本體可以呼叫它們的方法,且型別為Pet的值會暴露它們。每個Pet的實作者也必須撰寫自己的implements Animal和implements Named區塊(否則會出現 E0125 錯誤。一個菱形的requires可以編譯,共享的介面只實作一次;迴圈則會報錯。當一個類別上的兩個介面宣告了同名方法時,未限定的呼叫會報錯(E0121);請改用obj.as<I>.m()來呼叫。- 模式匹配。
match (v) { … }用於值 / 型別;分支為pattern => expr——字面量、let x: T(繫結 + 收窄)、類別解構T { f: let y }、或模式A | B、守衛… if cond、_;必須窮舉。還有v is T→ 布林值(同時收窄)和if let x: T = v { … } else { … }。baml describe patterns。 - 收窄——僅限區域變數。
v is T、v != null/v == null以及真值測試只有在v是區域變數(一個let或參數)且沒有被閉包捕獲時才會收窄v:在受保護的if/while分支中、在提前退出之後(if (!(v is T)) { return … })、以及在&&(左側為真)和||(左側為假)的右側——v is T && v.f == 1、s == null || s.length() == 0。不會收窄的情況:被捕獲的區域變數、欄位路徑(h.v is T && h.v.f→E0007)和索引路徑(xs[0] is T)。任務可以在陳述式之間交錯,因此另一個任務或別名可以在測試和讀取之間改變欄位。對於欄位,請先寫let v = h.v;,或使用if let v: T = h.v { … },或match (h.v) { let v: T => … }(每個都只讀取欄位一次)。對區域變數的賦值會用賦予的型別取代該事實。baml describe is。 - 並發 = 綠色執行緒。
spawn { … }回傳一個Future;await收集它。使用baml.future.all/all_settled/race/any組合多個。all_settled為每個輸入回傳Success<T>、Failure<E>或Panicked;輸入取消是一個Panicked結果。使用with子句配置 spawn:spawn with limit, token { … }——列出的每個值都是一個baml.spawn.Modifier,它會從左到右轉換 spawn 建構的計畫。baml.spawn.Limit.new(n)限制並發數(過多的 spawn 會排隊),baml.spawn.CancelToken協作式取消,baml.spawn.Root.new()將任務與其產生者的取消機制分離。baml describe spawn/baml describe baml.future。 - 資源安全——
defer、cleanup、catch (e, ctx)。defer { … }在作用域退出時執行一個區塊,後進先出,且在所有路徑上(回傳 / 拋出 / 貫穿)都會執行——類似 Go。一個名為function cleanup(self) -> void的類別方法是終結器:無論你呼叫它、defer它、還是 GC 回收它,它每個實例最多執行一次。catch (e, ctx)將一個ErrorContext與錯誤一同繫結——一個在處理另一個錯誤時拋出的錯誤會鏈接到它,因此ctx.root_cause()/ctx.cause可以追溯到原始失敗,且ctx.to_string()會呈現整個鏈(類似 Python 的__context__風格)。while let PATTERN = expr { … }會一直迴圈直到模式匹配失敗(例如,耗盡一個回傳T?的.pop())。 - 從 Python / TypeScript 呼叫 BAML。 在
baml.toml中宣告一個[generator.<name>],執行baml generate,然後匯入型別化的baml_sdk。安裝與使用:baml describe python/baml describe typescript/baml describe baml_sdk。 - 安全存取優於索引。 下標在缺少索引 / 鍵時會恐慌;使用
.at(i)/.get(k)(→T?)、使用?.進行穿透、使用??設定預設值(需加括號:(m.get(k) ?? 0) + 1)。 - 標準庫方法為 snake_case,在值上呼叫。 有些回傳新值,有些原地修改,少數兩者兼做(
sort_by_key會排序接收者並回傳它)——若要閱讀文件,請執行baml describe <word/type/identifier/keyword/etc>。 - 類別欄位
name: type,;使用Type { field: val }建構。 方法接受裸self;工廠是自由函式。欄位是可變的(類似 TypeScript):obj.field = v和obj.field += n有效,且self方法可以原地修改——一個有副作用的 method 回傳void。類別是參考型別:find/at(i)/ 下標回傳一個活別名,而非複本,因此修改結果會修改陣列內的該元素(xs.find(p)?.n += 1會更新xs),且傳遞給函式的類別可以被呼叫者修改。結構更新展開是受支援的:User { ...u, tier: Tier.Free }。空類別是合法的(class Marker {})——適合作為聯集變體。列舉是純變體——沒有方法,沒有關聯資料(E.A.foo()無法編譯);將行為放在使用match的自由函式中。enum E { A, B },存取E.A。 - 區塊是表達式——最後一個表達式是值(無
;);使用return x;進行提前退出。一個僅有副作用的函式回傳void——null(單位型別)的另一種寫法——且可以省略其最終表達式或return;。將值留在這種本體的最終表達式位置是錯誤,不會被丟棄:請以;結尾(xs.push(x);——push回傳新的長度)。for (let x in xs)迭代值;while (cond) { … }迴圈。閉包(x) -> { ... }從上下文中推斷參數 / 回傳型別(僅在不明確時標註(x: T) -> R;->是必需的)。.map/.filter直接回傳陣列(無需.collect())。空 map 需要型別:let m: map<string, int> = {};。 - 無三元運算子——
if/else就是表達式。 沒有cond ? a : b;if (cond) { a } else { b }就是一個回傳值的表達式,因此可以直接賦值:let label = if (x > 3) { "big" } else { "small" };。每個分支是一個區塊,其最後一個表達式是其值(無return)。使用else if串聯,並與if let PATTERN = expr { … } else { … }配對用於繫結和收窄。 - 條件使用真值性。
if、while、匹配守衛、&&、||和!接受任何值。假值包括false、null、數字零、空字串、空陣列 / map、空位元組;其他所有值都是真值。&&和||仍回傳bool,而非運算元值;它們會短路。一個真值的可選區域變數會在採用的分支中以及&&的右側被收窄(s && s.length() > 0)。 - 陣列擁有類似 JS 的方法集——
map/filter/filter_map/reduce/find/some/every/flat_map/slice/concat/join/includes/length(),以及原地操作的push/pop/shift/unshift/sort_by/sort_by_key。大多數接受可以throws的閉包。baml describe Array提供更多資訊。 - 區域
let繫結是可重新賦值的(x = x + 1)——沒有mut關鍵字(它是 TypeScript 的 let,而非 Rust);也沒有const。 - 參數:使用
=設定預設值,使用=進行關鍵字呼叫(絕不使用:)。 在簽章中宣告預設值:function f(a: int, b: int = 10);呼叫f(1)或f(1, b = 2)。有預設值的參數必須透過名稱傳遞——f(1, 2)是錯誤(defaulted parameter 'b' must be passed by name)。任何參數(即使是必需的)都可以透過名稱傳遞(f(a = 1, b = 2)),並且你可以跳過中間的預設值來設定後面的參數(f(1, c = 9))。關鍵字語法是name = value;name: value無法解析(:用於型別 / 欄位)。T?並不會讓參數變成可選——與 TypeScript 的b?: T不同,一個b: T?參數仍然是必需的(你必須傳遞null,否則會出現expected N argument(s), got …);加上= null讓它變成可省略。內建函式遵循此規則:baml.http.fetch(url, timeout = baml.time.Duration.from_seconds(10))。 - 與 TypeScript 的差異(無聲陷阱): 算術是型別驅動的,而非 TypeScript 風格。
int / int是截斷整數除法(285 / 100 == 2,而非2.85),%是餘數(285 % 100 == 85);這可以正常編譯,只會給出一個安靜的錯誤數字,因此是最有價值的陷阱。混合浮點數以獲得浮點數除法(285 / 100.0 == 2.85,285.0 / 100 == 2.85);任何混合int/float的運算會提升為float(5 + 2.0 == 7.0)。沒有.to_float()——使用n * 1.0轉換整數(或除以浮點數)。int結果在賦值時不會自動強制轉型為float(let x: float = 285 / 100是編譯錯誤)。+僅限數值:字串串接需要兩邊都是string("n=" + 5無法編譯——請使用${...}插值)。比較(==、<、……、結構化==)和&&/||/!類似 TypeScript。 - 測試: 獨立的
test "name" { ... }(無包裝器);testset僅用於分組。斷言(僅 6 個):assert.equal/approx_equal/is_true/is_type/not_null/contains。assert.equal進行結構化比較(深度比較,跨類別 / 陣列 / map)——純==也是如此,它是布林形式。assert.equal對浮點數是精確的;對於計算出的浮點數,請使用assert.approx_equal(actual, expected, eps)。最後一個斷言:無尾隨;。標準 ID 為頂層測試的root::TestName和 testset 內測試的root::Testset::TestName。使用baml test <id>執行一個測試,使用baml test執行全部。baml test --verbose/baml test --format json。baml describe tests。 - 載入外部資料(JSON / CSV):
let items = baml.open.json("data.json");(→json_value,一個遞迴型別——存取前先鍵入或match)。要載入為型別化的類別,使用let items: T = baml.open.json_as<T>("data.json");(如果型別不匹配則在執行時期恐慌)。CSV:let rows = baml.open.csv("data.csv")(→map<string, string>[],每列一個 map,標題作為鍵)。baml describe baml.open。 - 語言伺服器: 在
baml_src/中編輯時,在編輯器中執行baml lsp。baml describe lsp。baml.json是編譯器的內部格式——直接編輯它(如果你敢)然後執行baml check。 - 組態:
baml.toml在根目錄。專案在這裡定義。baml describe baml.toml。
執行 AI 函式
一個 BAML LLM 函式看起來像這樣:
// baml_src/main.baml
function ClassifyIntent(input: string) -> Intent {
client: "openai/gpt-4o-mini"
prompt: #"
Classify the user's intent into one of the following categories:
- Search
- Bookmark
- Navigate
- Help
- Other
User input: {{input}}
{{ctx.output_format()}}
"#
}
class Intent {
category: string
confidence: float
}
你可以在 BAML 程式碼中建立一個腳本化用戶端進行測試——無需真實 API 金鑰:
test "test intent classification with fake client" {
// 請勿在生產用戶端上使用 F@——它會繞過真實 LLM
// 而是使用 F@spec 來測試提示詞產生和解析
let result = [email protected](input: "find my saved recipes")
assert.equal(result.category, "Search")
assert.equal(result.confidence, 0.95)
}
然後從 Python / TypeScript 生成用戶端 SDK:
baml generate
# app.py
from baml_sdk import baml_client
client = baml_client.BamlClient()
result = client.ClassifyIntent(input="go to settings")
print(result.category, result.confidence)
更多資源
- 使用
baml describe <name>探索任何型別、函式、關鍵字或標準庫模組 - 在專案中執行
baml describe baml.toml查看完整的組態參考 - 使用
baml check驗證你的.baml檔案 - 使用
baml run -e '<expr>'測試表達式 - 在
baml_src/中編輯時使用baml lsp啟用編輯器支援