让大模型"选一个工具并填好参数",传统做法是用 JSON Schema 逼模型输出 JSON,然后自己解析——结果经常遇到模型多说一句废话、JSON 格式不全、字段类型对不上。BAML(BoundaryML)的做法更干脆:你用一门专门的 DSL 定义"工具类"和"函数",它自动生成类型安全的客户端,调用时返回的就是校验过的对象,IDE 有补全、运行时有类型保证。本教程带你跑通。
先搞懂:BAML 解决什么?
用一句话理解:BAML 把"让模型选工具"当成"调用一个返回固定类型的函数"来写——你定义输入、输出类型和一个提示词,它负责把模型输出解析成可信的对象。它的解析器比手写 Pydantic/Zod 更抗模型的"胡说"(比如 JSON 被包在一段文字里也能抠出来)。
| 做法 | 痛点 | BAML 的做法 |
|---|---|---|
| 手写 JSON Schema | 模型输出不稳、解析易崩 | DSL 定义 + 自动解析 |
| 手动 Pydantic | 要自己写 schema、易错 | 类型自动生成 Pydantic/Zod |
| 黑盒调用 | 看不到注入的 prompt | Playground 实时预览 prompt |
Step 1:安装与初始化
pip install baml-py
baml-cli init # 生成 baml_src/ 与示例 .baml 文件
baml_client,还能在 Playground 里实时试 prompt、看注入内容。Step 2:定义模型与函数(.baml)
在 baml_src/ 下新建 resume.baml:先定义返回类型 class,再定义 function 指明输入、输出类型和提示词。函数块里用 client 指定模型:
// baml_src/resume.baml
class Resume {
name string
skills string[] @description("只列编程语言")
}
function ExtractResume(resume_text: string) -> Resume {
client "openai/gpt-5-mini"
prompt #"
从下面简历抽取结构化信息:
{{ resume_text }}
{{ ctx.output_format }}
"#
}
Step 3:生成客户端
baml-cli generate
没装 VSCode 插件就要手动生成。每次改完 .baml 后都要跑一次 baml-cli generate,否则 Python 里调用的还是旧定义。装了插件则保存即自动生成。
Step 4:像调普通函数一样用
from baml_client.sync_client import b
from baml_client.types import Resume
resume = b.ExtractResume("Jason Doe, Python/Rust, UC Berkeley 2020")
assert isinstance(resume, Resume)
print(resume.name, resume.skills) # Jason Doe ['Python', 'Rust']
Resume 自动转成的 Pydantic 模型,IDE 有补全、类型检查器能抓错。模型哪怕输出格式不标准,BAML 解析器也会尽力还原成正确对象。Step 5:把"选工具"也做成类型安全输出
让模型决定"调哪个工具、参数是什么",本质上就是让它返回一个特定结构的对象。定义一个"工具类",让 function 返回它:
// baml_src/tools.baml
class WeatherAPI {
api_name "weather_request"
city string @description("用户所在城市")
}
function ChooseTool(user_message: string) -> WeatherAPI {
client "openai/gpt-5-mini"
prompt #"
从用户消息抽取要调用的工具与参数:
{{ user_message }}
{{ ctx.output_format }}
"#
}
from baml_client import b
tool = b.ChooseTool("旧金山现在天气怎么样?")
print(tool.city) # '旧金山'
模型名写在 .baml 的 client 里,别在 Python 硬编码。换模型只需改 function 块的 client "openai/gpt-5-mini",支持 OpenAI / Anthropic / Ollama 等,以 BAML 文档为准。
Step 6:接入真实工具函数
拿到类型安全的参数后,正常调用你的真实函数即可,没有任何 JSON 解析负担:
def get_weather(city: str):
# 这里接真实天气 API
return {"city": city, "temp": 22, "cond": "晴"}
info = b.ChooseTool("旧金山天气如何?")
data = get_weather(city=info.city)
print(data)
Step 7:测试与可观测(Boundary Studio)
BAML 配套的 Boundary Studio 可以回放生产请求、给每条输入输出打标,比反复改代码试错快得多:
# 在 Playground / Boundary Studio 里:
# 1. 选一个 function
# 2. 填多组输入,点 Run 看解析结果
# 3. 不满意就改 prompt,实时预览注入内容
常见问题速查
| 现象 | 原因 & 解决 |
|---|---|
| ImportError: baml_client | 没跑 baml-cli generate |
| 返回字段不全 | 在 Playground 看注入 prompt,补 @description |
| 模型 401 | .baml 里 client 模型名/密钥不对 |
| 类型对不上 | 改了 class 没重新 generate |