AI Navigate

BAMLがLLM搭載システムにエンジニアリングの規律をもたらす方法

Dev.to / 2026/3/21

💬 オピニオンDeveloper Stack & InfrastructureIdeas & Deep AnalysisTools & Practical Usage

要点

  • BAML は、LLM 関数インターフェースを定義するためのドメイン特化言語とツールチェーンを提供し、厳格で回復可能な出力パーシングを実現して、実運用の LLM システムにおける信頼性のギャップを埋めます。
  • スキーマ定義から Python、TypeScript、Go、Ruby ほかの言語に対して型安全なクライアントコードを生成し、多言語統合を可能にします。
  • このシステムは Schema Aligned Parsing を用いて、乱れた/部分的なモデル応答からでも構造化データを回復し、LLM 出力の堅牢性を向上させます。
  • この記事は BAML を標準的な RAG パイプラインの中に位置づけ、テスト、可観測性、動的ランタイムスキーマなどの関連利点を論じ、GitHub にあるリファレンス実装を紹介します。

要約

BAML は、LLM の関数インタフェースを定義するための、厳格で回復可能な出力パースを備えたドメイン特化言語およびツールチェーンです。これにより、プロダクションの LLM システムを構築・維持する際の信頼性のギャップを埋めます。Python、TypeScript、Go、Ruby、そしてその他の言語の複数の言語に跨るスキーマ定義から型安全なクライアントコードを生成し、Schema Aligned Parsing と呼ばれる解析アプローチを使用して、乱れているまたは部分的なモデルの応答からも構造化データを回復します。動作参照実装のためには、以下を参照してください:

GitHub - rajkundalia/error-analyzer-with-baml: ローカル Ollama モデルを使用して BAML で Java のコンパイル時および実行時エラーを分析する。

どのようにして BAML を知ることになったか

LLM の出力を扱う何かが存在するのかと考えていたところ、突然 Vaibhav Gupta の講演が飛び込んできた。さらに探求を始めた。もしこの投稿を読む代わりに、どうやって知ったのかを自分で探るには、以下の質問を自分に問いかけてみるとよい:

  • BAML とは何ですか?
  • Pydantic とは何ですか? BAML に関連しますか? もし関連するなら、どのように関連しますか?
  • PydanticAI とは何ですか? BAML とどう比較しますか? PydanticAI を BAML が行うことだけに使うことはできますか? PydanticAI はモデルから正しい出力を得るように再試行しますか?
  • BAML は過度に幻覚的な出力をどう扱いますか?
  • Instructor とは何ですか? [https://github.com/567-labs/instructor]? BAML と比較してください。 - 明確さのフォローアップ: PydanticAI を使っている場合、Instructor を使う意味はありませんか?
  • 標準的な RAG パイプラインには BAML は正確にはどこにはまりますか?
  • BAML はトークン効率にどう役立ちますか?
  • BAML におけるセマンティック・ストリーミングとは何ですか? どんな問題を解決しますか? Generative UI にはどのように役立ちますか(Generative UI が何かを簡潔に説明してください)
  • BAML コードジェネレータとは何ですか?
  • Schema Aligned Parsing とは何ですか? そして何を扱えるのですか?
  • BAML でどのようなテストが実施されている、または実施可能ですか?
  • BAML における union とは何ですか?
  • BAML でのログ記録・トレース・可観測性はどのように機能しますか?
  • BAML は Jinja テンプレーティングをどのように使って、動的コンテキスト、ループ、正確なチャット役割をプロンプトに挿入し、煩雑な文字列連結を回避しますか?
  • BAML における動的型(または実行時スキーマ)とは何ですか?
  • BAML はどのような側面で役立ちますか?
  • Claude Agent SDK のようなものと組み合わせて BAML は有効ですか?

What BAML Is and the Problem It Solves

Every engineer who has tried building an LLM-powered feature knows the first hour of optimism and the next two weeks of fire-fighting. The model returns JSON with an extra key, or wraps it in markdown fences, or truncates mid-response. The prompt worked fine in POC/Demo. Now there are three different parsing bugs during production grade implementation, all subtly different.

BAML (or Basically a made-up language) - Boundary ML - exists to solve this class of problem at the right level of abstraction. It is a language-level contract between the application and the model. You define what you want the model to return, write the prompt logic in a dedicated templating layer, and BAML handles parsing, type-checking, retries, and client generation across Python, TypeScript, Go, Ruby, and other languages - with opt-in retry policies when you need them.

The project positions itself as the Pydantic of LLM engineering - a statement about philosophy rather than API compatibility. Just as Pydantic introduced runtime type validation into Python codebases that previously relied on convention and hope, BAML introduces structural guarantees into LLM pipelines that previously relied on prompt tuning and defensive try/except blocks.

ジェミニ生成済み

How BAML Relates to Pydantic and Tools Like Instructor

Pydantic itself does one thing exceptionally well: it validates Python data structures against declared schemas. Feed it a dictionary, and it tells you whether it conforms to the model definition. It does not know anything about language models, prompts, or API calls - it is a validation library, and a very good one.

Instructor builds on top of Pydantic to handle the LLM layer. It takes a Pydantic model, wraps the OpenAI (or Anthropic, or other) API call, and uses function calling or JSON mode to coax the model into returning something the Pydantic validator can accept. When validation fails, Instructor can retry with the validation error message appended to the conversation, giving the model a chance to self-correct. This is practical, widely used, and works well for straightforward extraction tasks. What Instructor does not do is provide a dedicated authoring layer for prompts, generate client code from schema definitions, or go beyond retry logic when the model output is deeply malformed.

PydanticAI goes further than Instructor. It is an agent framework - it handles tool registration, multi-step agent loops, dependency injection, and result validation as part of a unified system. Validation failures feed back into the agent's run loop through a reflection mechanism, giving the model a chance to self-correct - structurally similar to what Instructor does but integrated at the framework level rather than as a wrapper. Comparing PydanticAI and BAML feature-for-feature would miss the point.

The more accurate comparison is about what layer each tool operates at. PydanticAI and BAML both handle structured output and retry behavior, but they do so with different default assumptions. PydanticAI is a Python framework - everything is Python, configured in Python, tested in Python. BAML is a language-level abstraction with its own syntax, its own code generator, and its own parsing engine that operates below what either Pydantic or the model's native JSON mode provides.

If a team is already using PydanticAI and happy with it, BAML is not a necessary replacement. If the team is hitting parsing failures that retry loops do not reliably fix, or needs multi-language client generation, or wants prompt authoring with first-class tooling support, BAML addresses different parts of the problem.

The BAML DSL and Code Generation

BAML is its own language. Not a Python DSL, not a configuration file format - a purpose-built syntax for describing LLM function signatures, data schemas, and prompt templates in a single, unified file format. A .baml file defines the inputs, the expected output structure, and the prompt template that connects them. The BAML compiler - written in Rust - reads those files and generates native client code in Python, TypeScript, Go, Ruby, and other languages. The Rust foundation is also what makes the SAP parsing engine fast enough to run inline on streaming responses without meaningful latency overhead - error correction applies in under 10ms, orders of magnitude cheaper than a retry API call. This is why BAML can credibly claim to be a language-level abstraction rather than a Python-centric library with thin wrappers for other runtimes.

これは見た目だけと見なされがちな理由でありながら実際には構造的な理由です:スキーマとプロンプトが同じファイルに存在すると、離れてしまうことはありません。典型的な設定では、Pydanticモデルは1つのファイルに、プロンプト文字列は別のファイルに、解析ロジックは別の場所にあります。プロンプトを変更しても、スキーマは変わらないことがある。スキーマが変わっても、プロンプトはしばしば変わりません。これは利便性の問題というより、プロンプト、パーサ、アプリケーションコード間のスキーマドリフトといった、レビューで捉えにくく、プロダクションで表面化するまで見えない欠陥の全体を排除することに関係しています。BAMLは設計上、これらを同居・同バージョン化します。

スキーマ整合パーシング - BAMLのコア信頼性メカニズム

ほとんどの構造化出力アプローチは、JSONモード(モデルに有効なJSONを出力させるよう依頼する)または関数/ツール呼び出し(出力形式をAPIレベルで制約する構造化プロンプティング)のいずれかに依存します。いずれのアプローチも同じ障害モードを共有します:モデルの出力が準拠しない場合、パースが失敗します。

BAMLがない場合、その失敗は次のように見えます:モデルがわずかに不正なJSONを返し、パーサが例外を投げ、アプリケーションが再試行し、モデルが同じ出力を再び生成する可能性があり、リクエストはエラーを返すか黙ってフォールバックします。BAMLでは、同じ不正な出力がSAPを通過し、モデルが明示的に意図した構造データを抽出し、アプリケーションへ型付きオブジェクトを返します。再試行は必要ありません。

スキーマ整合パーシング - SAP - は異なるアプローチを採用します。解釈を開始する前にモデル出力が有効なJSONであることを要求するのではなく、BAMLのパーサはモデルが実際に返すすべてのデータから構造化データを抽出し、何を探すべきかの手掛かりとして宣言されたスキーマを用います。

実務でSAPが実際に処理する内容を考えてみましょう。JSONをマークダウンのコードフェンスで包むモデルは、厳密なJSONパーサを壊します。SAPはフェンスを削除します。末尾のカンマや引用符なしの文字列値を出力するモデルは、技術的には無効なJSONとなり、JSON.parseに失敗します。SAPはそれらを修正します。構造化コンテンツの前に推論過程のテキストを出力する推論モデルは、ほとんどのパーサを混乱させます。SAPは構造化コンテンツがどこから始まるのかを特定し、そこから解析します。別の大文字表記や周囲の句読点を伴う列挙値が、スキーマに宣言された列挙値と正規化されます。

SAPがしないことは、欠落データを幻視することです。モデルが必須フィールドを完全に省略し、出力に回復可能な信号がない場合、BAMLはパースの失敗を報告します。仕組みは再取得のための回復に関するもので、創作ではありません。実務的な結果として、偽陰性のパース失敗の大幅な削減——厳密なJSONパーシングが拒否する形でモデルが実際には正しい概念的回答を出したケース— が得られます。

これがBAMLの信頼性主張の技術的核であり、毎回モデルが有効なJSONを出力する能力だけに全て依存するアプローチとの実質的な区別です。

Jinjaテンプレートを用いたプロンプト作成

BAMLは、Jinjaスタイルの構文をプロンプト作成に使用します - Jinjaテンプレーティング言語を実装するRustネイティブのテンプレートエンジンであるMinijinja によって動作します - これにより、ほとんどの代替手段が文字列連結または場当たり的なフォーマット関数となっている領域に、成熟してよく理解されたテンプレーティングモデルをもたらします。

実用的な利点は、聞こえるよりもクリーンです。動的コンテキスト注入 - 文書のリスト、ユーザーの履歴、取得したチャンクのセットなど - は、アプリケーションコードの文字列構築としてではなく、テンプレート内のループとして表現されます。チャットの役割分担(システムプロンプト、ユーザーターン、アシスタントターン)は、プロンプトの外部データ構造で組み立てられるのではなく、テンプレート内で直接ロールマクロを用いてインラインで処理されます - _.role("system")_.role("user") - です。特定のフラグが設定されている場合のみ拡張指示セットを含めるといった条件付きプロンプトロジックは、迷路のような条件付き文字列追加の代わりにテンプレートとして読めます。

代替案は、f文字列や連結によってプロンプトを作成することです - 実際に機能するのはそれが機能している間だけです。動的セクションを含む数百トークンのプロンプトになると、デバッグする唯一の方法は最終的に組み立てられた文字列をログに取り、どのように作られたかを手動で再構築することです - これは生成元のアプリケーションコードを理解する必要があり、プロンプト自体を理解する必要はありません。BAMLでは、プロンプトテンプレートが真実の出発点であり、直接検査・バージョン管理・テストが可能です。Jinjaレイヤーは、プロンプト構造とそれに流入するデータを分離することも簡単にします。これにより、アプリケーションロジックに触れることなくプロンプト内容を反復する際に役立ちます。

ユニオンと動的型

BAMLの型システムはユニオン型をサポートします - フィールドまたは戻り値が、いくつかの異なるスキーマのいずれかになる可能性があることを宣言できる機能です。クエリに応じて SearchResult または ErrorResponse のいずれかを返す可能性があるモデルは、出力の実行時検査ではなく、スキーマ定義にその区別を表現できます。

ダイナミック型は、関連するが別の問題を解決します。ユニオンは、可能なスキーマがコンパイル時に既知である場合に機能します。スキーマ自体が、実行時のみ存在するデータ(データベースから取得されるカテゴリ、ユーザー設定で定義されたフィールド、またはテナント固有の構造)に依存する場合、BAMLは型定義に @@dynamic アノテーションを提供し、生成されたクライアントには TypeBuilder API を用意します。実行時には、アプリケーションコードが TypeBuilder を使って呼び出し前にフィールドや列挙体のバリアントを追加し、パーサーは拡張されたスキーマを用いてレスポンスを解釈します。

その両方を説明する具体的な例:可能な文書タイプ(請求書、契約、医療記録)が固定されており、既知である抽出パイプライン - それはユニオンであり、.baml ファイル内で一度宣言されます。もしそれらの文書タイプとそれらのフィールドが、代わりにリクエスト時にデータベーススキーマからロードされる場合、それが @@dynamic および TypeBuilder の出番です。区別は重要です:ユニオンはスキーマ設計の選択肢、動的型は実行時の拡張機構です。

トークン効率

BAMLのスキーマ認識プロンプティングは、手作業で行われる同等のプロンプトエンジニアリングより、短いシステム指示を生成する傾向があります。出力構造がスキーマで宣言され、ランタイムがパースの柔軟性を扱うため、出力フォーマット、JSONの妥当性、フィールド命名規則についての広範な指示はプロンプトには不要です。これらの懸念はツール層で処理されます。トークンコストが意味を持つ高ボリュームのアプリケーションでは、システムプロンプトのオーバーヘッドの削減が蓄積します。

意味論的ストリーミングと生成的UI

LLMの応答はトークンごとに到着します。チャットインターフェースでは、生のテキストをストリーミングするのは簡単です。構造化出力パイプラインでは、ストリーミングには問題が生じます:出力は完了するまで解析可能ではなく、したがってアプリケーションはすべてをバッファして最後に解析し、UIを更新します。これにより、ユーザーの視点から遅延が生じます — モデルは機能していますが、画面には何も表示されません。

BAML のセマンティック・ストリーミングは、トークンが到着するたびに出力を逐次解析することでこれを解決します。パーサーが期待されるスキーマを知っているため、ストリームが進むにつれてどのフィールドが埋められているかを識別できます。スキーマフィールド上のストリーミング属性は、原子性に対する開発者の明示的な制御を提供します - フィールドは完全に完了したときにのみ表面化するように構成したり、UI にとって意味のある形で部分値としてトークンごとにストリーミングするように設定したりできます。

これは、モデルが応答を生成する際に部分的な構造化データを意味のあるインターフェースコンポーネントへレンダリングする、生成的UI(ジェネレーティブUI)と呼ばれるパターンを可能にします。文書から抽出された行アイテムのリストを表示するインターフェースは、すべての行アイテムが同時に読み込まれるのを待つ必要はありません。各アイテムは解析されるにつれて順次表示されます。モデルで抽出された分析フィールドを表示するダッシュボードは、各カードを空の状態から完全な状態へ一気に切り替えるのではなく、段階的に埋めていくことができます。

この仕組みは特定のUIフレームワークだけに特有のものではありません - それは生成されたクライアントが公開するストリーミングパーサーの性質です。ストリームを受け取るアプリケーションは、直接レンダリング可能な型付きの部分オブジェクトを受け取ります。

Testing in BAML

BAML includes a testing layer that allows declaring test cases directly in .baml files alongside the function definitions they test. A test case specifies the input and optionally assertions about specific field values or structural properties of the result, using @@assert expressions evaluated against the actual model output.

BAML には、テスト対象の関数 defin...

  • https://docs.boundaryml.com/guide/introduction/what-is-baml#demo-video
  • https://thedataquarry.com/blog/baml-and-future-agentic-workflows/
  • https://thedataquarry.com/blog/baml-is-building-blocks-for-ai-engineers/
  • https://youtu.be/leDdmneq2UA?si=1cjuko9ZMnbuWOmC
  • https://towardsai.net/p/machine-learning/the-prompting-language-every-ai-engineer-should-know-a-baml-deep-dive - 参考になる深掘り
  • https://gradientflow.com/seven-features-that-make-baml-ideal-for-ai-developers/
  • https://youtu.be/XDZ5i7hWgaI?si=0_8ZbalUbvyMpmYe
  • https://www.youtube.com/watch?v=XwT7MhT_BEY
  • 探索中に見つけたサンプルプロジェクト:

    BAMLを試してみる: