进阶 📋 7 个步骤 第 431 / 473 篇

用 BAML 把 LLM 工具调用变成类型安全的函数

用 BAML(BoundaryML)把“让模型选哪个工具”做成可靠的结构化输出:定义工具类 + function、baml-cli generate 生成客户端、像调普通函数一样用,避免 JSON 解析翻车。

2026.09.14· 18 分钟阅读· 约 1146 字· 🧱 BAML / 🔒 类型安全

让大模型"选一个工具并填好参数",传统做法是用 JSON Schema 逼模型输出 JSON,然后自己解析——结果经常遇到模型多说一句废话、JSON 格式不全、字段类型对不上。BAML(BoundaryML)的做法更干脆:你用一门专门的 DSL 定义"工具类"和"函数",它自动生成类型安全的客户端,调用时返回的就是校验过的对象,IDE 有补全、运行时有类型保证。本教程带你跑通。

🧱 本教程适合:被"LLM 输出 JSON 解析翻车"折磨过、想把工具调用做成可靠结构化输出的 Python/TS 开发者。需要 Python 3.10+。

先搞懂:BAML 解决什么?

用一句话理解:BAML 把"让模型选工具"当成"调用一个返回固定类型的函数"来写——你定义输入、输出类型和一个提示词,它负责把模型输出解析成可信的对象。它的解析器比手写 Pydantic/Zod 更抗模型的"胡说"(比如 JSON 被包在一段文字里也能抠出来)。

做法痛点BAML 的做法
手写 JSON Schema模型输出不稳、解析易崩DSL 定义 + 自动解析
手动 Pydantic要自己写 schema、易错类型自动生成 Pydantic/Zod
黑盒调用看不到注入的 promptPlayground 实时预览 prompt

Step 1:安装与初始化

1 装 baml-py 并生成脚手架
pip install baml-py
baml-cli init   # 生成 baml_src/ 与示例 .baml 文件
🔌 强烈建议装 VSCode 的 BAML 插件:保存 .baml 文件时自动生成 baml_client,还能在 Playground 里实时试 prompt、看注入内容。

Step 2:定义模型与函数(.baml)

2 用 DSL 写"抽取简历"

在 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:生成客户端

3 baml-cli generate
baml-cli generate

没装 VSCode 插件就要手动生成。每次改完 .baml 后都要跑一次 baml-cli generate,否则 Python 里调用的还是旧定义。装了插件则保存即自动生成。

Step 4:像调普通函数一样用

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:把"选工具"也做成类型安全输出

5 工具类 + function 返回它

让模型决定"调哪个工具、参数是什么",本质上就是让它返回一个特定结构的对象。定义一个"工具类",让 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:接入真实工具函数

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)

7 一次点击测多组输入输出

BAML 配套的 Boundary Studio 可以回放生产请求、给每条输入输出打标,比反复改代码试错快得多:

# 在 Playground / Boundary Studio 里:
# 1. 选一个 function
# 2. 填多组输入,点 Run 看解析结果
# 3. 不满意就改 prompt,实时预览注入内容
🧪 迭代建议:先用 Playground 把 prompt 调稳,再回到代码调用。BAML 的实时 prompt 预览能让你看到"模型实际吃进去了什么",这是它比手写 JSON Schema 高效的关键。

常见问题速查

现象原因 & 解决
ImportError: baml_client没跑 baml-cli generate
返回字段不全在 Playground 看注入 prompt,补 @description
模型 401.baml 里 client 模型名/密钥不对
类型对不上改了 class 没重新 generate
← 返回教程中心