Community生产力与协作github.com

xyzcharlize/hyperframes-genie

最小化的 BAML 技能。BAML 是一种静态类型、面向表达式的语言,拥有一流的 LLM 函数——类似 TypeScript 但使用 snake_case 方法。适用于构建 AI 工作流、智能体和评估。

hyperframes-genie 是什么?

该技能提供了一个最小化的 BAML 开发环境。BAML 是一种静态类型、面向表达式的领域特定语言,结合了 TypeScript 风格的语法和 snake_case 命名约定,专为构建 AI 工作流、智能体和评估而设计。技能的核心是一个命令行工具,通过 `baml describe` 提供完整的文档内省,包括内置标准库和类型系统的即时文档。BAML 的独特之处在于它将 LLM 调用视为类型化函数:结构化的模型输出直接映射为类型化返回值,无需手动解析。技能支持多种 AI 客户端(如 OpenAI 和 Anthropic),提供绿色的线程并发模型、模式匹配、`catch` 异常处理、接口和组合客户端(如 Retry 和 RoundRobin)等高级特性。通过 `baml generate` 可以生成 Python 和 TypeScript 的类型化 SDK,从而实现与外部项目的无缝集成。BAML 还提供了严格但灵活的静态类型系统,包括整数除法陷阱(`int / int` 为截断除法)和纯数值加法(字符串连接需使用插值),以及全面的测试框架和资源安全机制(`defer`、`cleanup`)。

兼容平台✓Claude Code✓Codex CLI~Cursor
npx skills add https://github.com/xyzcharlize/hyperframes-genie/tree/HEAD/.agents/skills/baml-core

Installed? Explore more 生产力与协作 skills: steipete/gemini, steipete/gh-issues, steipete/skill-creator · View all 6 →

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

BAML

BAML 是一种静态类型、面向表达式的 语言——TypeScript 风格,但使用 snake_case 方法、name: type, 字段、枚举、接口、泛型、闭包、可选链、带有 ${...} 插值的反引号字符串、任意值上的 .to_string() 以及真实的标准库。此外,它还包含一个用于 LLM 调用的声明式 DSL(function … { client: prompt: }、test),该 DSL 会被脱糖到基础语言中,因此模型的结构化输出就是一个类型化的返回值。

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 more lines" 结尾,请重新运行 `--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 函数 = 类型化返回值。 返回类型就是模型必须生成的 schema(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: […] } 不使用——将其作为类字面量构造。然后在函数中使用 client: Fast,或者使用简写 client: "openai/gpt-4o-mini"。baml describe openai / baml describe ai.Credential / baml describe ai.clients。
  • 使用字段属性来塑造 schema。 @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 → bool(收窄)和 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 { … } 在作用域退出时运行一个块,LIFO 顺序,在每条路径上执行(return / throw / fall-through)——类似于 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 / TS 调用 BAML。 在 baml.toml 中声明一个 [generator.<name>],运行 baml generate,然后导入类型化的 baml_sdk。安装和使用:baml describe python / baml describe typescript / baml describe baml_sdk。
  • 安全访问优于索引。 下标操作在缺少索引/键时会 panic;使用 .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;工厂函数是自由函数。字段是可变的(类似于 TS):obj.field = v 和 obj.field += n 有效,并且 self 方法可以就地修改——副作用方法返回 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) 遍历 VALUES;while (cond) { … } 循环。闭包 (x) -> { ... } 根据上下文推断参数/返回类型(仅在歧义时注解 (x: T) -> R;-> 是必需的)。.map/.filter 直接返回数组(无需 .collect())。空映射需要类型标注: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、match 守卫、&&、|| 和 ! 接受任何值。假值包括 false、null、数字零、空字符串、空数组/映射以及空字节;其他所有值都是真值。&& 和 || 仍然返回 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 关键字(是 TS 的 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? 并不会使参数变成可选的——与 TS 的 b?: T 不同,b: T? 参数仍然是必需的(你必须传递 null,否则会收到 expected N argument(s), got … 错误);添加 = null 使其可省略。内建函数遵循此规则:baml.http.fetch(url, timeout = baml.time.Duration.from_seconds(10))。
  • 与 TS 的差异之处(静默陷阱): 算术运算是类型驱动的,而非 TS 风格。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 不会编译——使用 ${...} 插值)。比较(==、<、……,结构化的 ==)和 &&/||/! 类似于 TS。
  • 测试: 单独的 test "name" { ... }(不需要包装器);testset 只负责分组。断言(只有 6 种):assert.equal/approx_equal/is_true/is_type/not_null/contains。assert.equal 进行结构化比较(跨类、数组、映射的深度比较)——普通的 == 也是如此,它是布尔形式。assert.equal 在浮点数上是精确的;对于计算得到的浮点数,使用 assert.approx_equal(actual, expected, eps)。最后一个断言:末尾不加 ;。标准 ID 是顶层测试的 root::TestName 和 testset 内部的 root::Testset::TestName。使用 baml test <ID> 运行单个测试。

相关技能