news 2026/9/25 3:00:28

BAML jsonish 柔性解析器:把 LLM 自由文本可靠地解析成结构化数据

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
BAML jsonish 柔性解析器:把 LLM 自由文本可靠地解析成结构化数据
  • 编程语言
  • AI Agent
  • 编译器
  • CLI
  • 人工智能

【免费下载链接】baml

The programming language for agents

项目地址:https://gitcode.com/gh_mirrors/ba/baml
点击查看免费下载

本文聚焦 BAML 引擎中的jsonish库(位于 engine/baml-lib/jsonish),围绕其原始文档 README.md 展开:它对外暴露的from_str接口保证"只要输入里包含符合 schema 的内容,就能被灵活地解析出来"。读完本文,你可以掌握 jsonish 的两段式解析管线(JSONish 词法/结构解析 + 类型强制转换)、解析候选与补全状态的数据模型、柔性决策如何被标记与打分,以及 BAML 运行时(fetch_as、prompt renderer)是如何消费其结果的。

一、核心接口与设计承诺

jsonish 是 BAML 引擎里的一个 Rust crate(crate 名jsonish,见 Cargo.toml),它解决的核心问题是:大模型或其他上游给出的原始字符串,往往不是干净的 JSON,但业务侧的 schema(类型)是明确的。原始文档 README.md 将其承诺概括为一个函数:

pub fn from_str( of: &OutputFormatContent, target: &FieldType, raw_string: &str, allow_partials: bool, ) -> Result<BamlValueWithFlags>

它提供的保证是"schema 能从输入中被柔性地解析出来"(It provides a guarantee that the schema is able to be flexibly parsed out from the input),典型场景包括:

  • 在带前缀/后缀文本里找出目标对象(如模型回复"The answer is true",目标类型是bool);
  • 按字段名别名(alias)解析字段;
  • 把值强制转换(cast)到正确的目标类型;
  • 在必要时把单个值包装成数组;
  • 遵守约束(constraints)。

需要注意源码与文档的演进差异:当前 src/lib.rs 中的实际签名为

pub fn from_str( of: &OutputFormatContent, target: &TypeIR, raw_string: &str, raw_string_is_done: bool, ) -> Result<BamlValueWithFlags>

即FieldType已由更通用的TypeIR取代,而allow_partials语义被反转为raw_string_is_done(表示输入是否已完整收到)。从调用点 prompt_renderer/mod.rs 可以印证这一对应关系:jsonish::from_str(def, target, raw_string, !allow_partials)。第四个参数之所以存在,是因为解析器必须支持流式场景——LLM 输出尚未结束时(比如只收到"[1, 2"),也要能返回一个"部分完成"的结构,而不是直接报错。

返回值:BamlValueWithFlags

返回类型BamlValueWithFlags(定义在 deserializer/types.rs)是一棵带"解析账本"的值树,每个节点记录:

  • 实际解析出的值(String/Int/Float/Bool/List/Map/Enum/Class/Null/Media);
  • 目标类型TypeIR;
  • DeserializerConditions,即一组柔性转换时打上的Flag(详见第四节)。

它提供了到运行时值体系的多种From转换(转BamlValue、转BamlValueWithMeta<...>),并实现score()方法对整棵树打分——分数越小说明解析过程"越少动用技巧",结果越可信。

二、两段式管线:JSONish 解析 + 类型强制转换

from_str的实现只有两步(见 src/lib.rs):

  1. 第一步:jsonish::parse(raw_string, ParseOptions::default(), raw_string_is_done)——把原始字符串解析成一棵与具体 schema 无关的Value树;
  2. 第二步:target.coerce(&ctx, target, Some(&value))——以目标类型TypeIR为驱动,把Value树强制转换成BamlValueWithFlags。

若目标类型本身就是纯字符串,会直接短路返回原始字符串(lib.rs),因为"schema 是 string 时就不该再解析"。另外,若第二步中出现了Flag::InferedObject(String(...))这类无法接受的标记,会显式报错失败——灵活性有边界。

2.1 解析入口的四级回退策略

第一步的核心是 parser/entry.rs 中的parse_func,它按顺序尝试多级策略,前一级失败才进入下一级:

  1. 严格 JSON 解析:先serde_json::from_str。成功即返回,并依据值类型设定补全状态——比如裸数字1会被标记为Incomplete(因为模型可能还会继续输出12),而带引号的字符串、对象、数组一旦解析成功必然是完整的;
  2. Markdown 解析(allow_markdown_json):markdown_parser.rs 提取 ```json 代码块;若文本里出现多个 JSON 对象,会构造一个候选集(AnyOf),候选包括"每个对象单独""所有对象作为列表""按字符串解析"等多种读法;
  3. 全文扫描 JSON 对象(all_finding_all_json_objects):multi_json_parser.rs 在前后缀任意文本中"grep 出"所有 JSON 片段,结果打上Fixes::GreppedForJSON标记;
  4. 修复式解析(allow_fixes):fixing_parser.rs 处理残缺 JSON(缺失括号、未闭合引号等),对应Fixes枚举里的修复记录;
  5. 兜底为字符串(allow_as_string):以上全失败时,把整段文本当作字符串候选返回,交由第二步的强制转换层去做"从文本里抠出 bool/数字"之类的工作。

每级之间的切换由 parser/mod.rs 的ParseOptions控制,默认全开:

pub struct ParseOptions { all_finding_all_json_objects: bool, // 默认 true allow_markdown_json: bool, // 默认 true allow_fixes: bool, // 默认 true allow_as_string: bool, // 默认 true depth: usize, // 递归深度保护 }

next_from_mode会根据当前所处解析模式逐级收窄选项,避免重复尝试同一层策略;同时parse_func对递归深度设了上限(超过 100 层即报 "Depth limit reached"),防止病态输入引发深递归。

2.2 Value 树:候选集与补全状态

解析产物是 jsonish/value.rs 中的Value枚举。除了常规 JSON 值(String/Number/Boolean/Null/Object/Array),还有三个对"柔性"至关重要的变体:

  • Markdown(String, Box<Value>, CompletionState):从 Markdown 代码块里挖出的 JSON,附带原文标记;
  • FixedJson(Box<Value>, Vec<Fixes>):经过修复/扫描才得到的 JSON,Fixes枚举记录了GreppedForJSON(全文扫描所得)、InferredArray(推断出的数组包装)等修复动作;
  • AnyOf(Vec<Value>, String):多个解析候选。同一段文本可能有多种合理解读(对象、列表、字符串),jsonish 不急着裁决,而是把候选集原样传给第二步,让"目标类型"来选最优。

每个带状态的值都携带CompletionState(Complete/Incomplete),支撑流式解析:completion_state()会自底向上聚合(任一候选未完成则整体未完成),complete_deeply()可在输出结束后把整棵树标记为完成。顶层裸数字会被刻意标为 Incomplete(见 entry.rs),因为数字可能被后续 token 延长。

三、强制转换层:五个柔性场景的落地

第二步的实现在 deserializer/coercer/ 目录下,按类型分文件组织:

  • coerce_alias.rs:字段名别名匹配(对应文档中 "Parsing in field names with aliases");
  • coerce_class.rs / coerce_enum.rs:类与枚举的字段/取值匹配;
  • coerce_primitive.rs:基础类型转换("Casting to the right type");
  • coerce_array.rs:数组解析与"单值包装为数组"("Wrapping around arrays when necessary");
  • coerce_map.rs / coerce_union.rs:map 与联合类型(含打分选择最优分支);
  • match_string.rs:从自由文本中匹配目标取值。

约束("Obeying constraints")则由 deserialize_flags.rs 中的Flag::ConstraintResults(Vec<(String, JinjaExpression, bool)>)承载——每个约束的求值结果(名称、Jinja 表达式、是否通过)被记录在解析账本里,最终通过constraint_results()汇总给运行时。

测试用例直观展示了这些柔性行为(摘自 tests/test_basics.rs):

// 千分位数字直接解析 test_deserializer!(test_number_2, EMPTY_FILE, "12,111", TypeIR::int(), 12111); // 大小写不敏感的布尔 test_deserializer!(test_bool_2, EMPTY_FILE, "True", TypeIR::bool(), true); // 前缀文本里抠出 bool,并自动包成列表 test_deserializer!( test_bool_wrapped, EMPTY_FILE, "The answer is true", TypeIR::bool().as_list(), [true] ); // 带加粗 Markdown 的结论 test_deserializer!( test_bool_wrapped_mismatched_case_preceded_by_text, EMPTY_FILE, "The tax return you provided has section for dependents.\n\nAnswer: **True**", TypeIR::bool(), true ); // 歧义输入则明确失败,而不是猜 test_failing_deserializer!( test_ambiguous_bool, EMPTY_FILE, "The answer is true or false", TypeIR::bool() );

这个测试目录还覆盖 别名、类、约束、联合类型、部分值/流式 等主题;Cargo.toml 中用 criterion 配置了 字面量、类、列表、联合类型、部分值 等基准,说明该库在性能与行为边界上都有系统性验证。

四、Flags 与 Score:让每一次"开恩"都留痕

柔性解析最危险的副作用是"静默地歪曲数据"。jsonish 的对策是决策留痕:每动用一次技巧,就向DeserializerConditions打一个Flag。Flag 枚举 相当详尽,能回答"这个值是怎么来的",例如:

  • ObjectFromMarkdown/ObjectFromFixedJson(Vec<Fixes>):对象来自 Markdown 代码块或修复后的 JSON;
  • ImpliedKey(String)/ExtraKey(String, Value):字段名被推断出来,或出现了 schema 之外的多余键;
  • SubstringMatch/StrippedNonAlphaNumeric:值是从文本子串匹配、或剥掉非字母数字字符后得到的;
  • SingleToArray:单值被包装成了数组;
  • StringToBool/StringToFloat/FloatToInt等类型强转记录;
  • FirstMatch/UnionMatch:联合类型在多个候选中选了第一个/某个分支,并保留全部候选的成败结果;
  • Incomplete/Pending:流式场景下的完成状态标记。

基于这层账本,库提供两类可观测能力:

  1. 打分:score.rs 中的WithScore让每个Flag有代价,BamlValueWithFlags::score()递归求和(见 types.rs)。打分机制使联合类型等场景能从多个候选分支中择优,也让上层能判断"这个结果干净还是勉强";
  2. 可解释错误:explanation_json()(types.rs、lib.rs)把失败原因按<root>.field.parsed:0这样的作用域路径组织成 UI 可渲染的 JSON(ParsingErrorToUiJson),供编辑器/Studio 等前端直接展示。

五、运行时消费:从解析结果到 BAML 值

jsonish 的产出最终服务于 BAML 运行时。在 async_vm_runtime.rs 中,表达式函数的fetch_as拿到 HTTP 响应体后直接调用jsonish::from_str(&output_format, &parse_as_type, &body, true),把任意响应体按目标类型解析成BamlValueWithFlags,再包成ResponseBamlValue;prompt_renderer 同理,用!allow_partials传入完成标志。

ResponseBamlValue(lib.rs)是面向流式的最终封装:元数据ResponseValueMeta携带(flags, checks, completion, TypeIR)。其Serialize实现区分SerializeMode::Final与Partial两种模式——流式输出未完成时,序列化会附带state(流状态)字段,且类字段按"该字段是否已 required 完成"单独决定用 Final 还是 Partial 序列化(lib.rs),从而让 SDK 用户能在 token 到达的瞬间看到"部分但有效"的结构化响应。

六、工程要点小结

  • 两段式架构:先得到 schema 无关的Value候选树,再由目标类型coerce收敛,是 jsonish 能同时服务 LLM 输出、HTTP 响应、prompt 渲染结果等多种来源的原因;
  • 候选集而非裁决:AnyOf把"多种合理解读"延迟到类型已知时才选择,配合Fixes/Flag保证选择过程可追溯、可打分;
  • 流式一等公民:CompletionState与raw_string_is_done参数贯穿解析、转换、序列化三层,残缺输入返回部分值而不是抛错;
  • 跨平台:Cargo.toml 为 wasm32 目标单独引入了uuid/getrandom的 js feature,说明该库也能编译进 WebAssembly(供浏览器端语言客户端使用)。

如需继续深入,建议按 src/lib.rs → parser/entry.rs → coercer/ → tests/ 的顺序阅读源码,测试目录基本就是每个柔性能力的活文档。

  • 编程语言
  • AI Agent
  • 编译器
  • CLI
  • 人工智能

【免费下载链接】baml

The programming language for agents

项目地址:https://gitcode.com/gh_mirrors/ba/baml
点击查看免费下载

相关推荐

上一篇:消息队列中的UUID终极指南:在Kafka和RabbitMQ中实现全局唯一标识符
下一篇:OpenCode:开源AI编程助手如何让你的开发效率提升3倍?

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 2:54:43

MySQL报错only_full_group_by:原因、排查与SQL改写实战

最近群里一位老同事贴了张报错截图&#xff0c;红彤彤一行英文&#xff1a;this is incompatible with sql_modeonly_full_group_by。这大概是 MySQL 5.7 之后后端同学最常撞见的“老朋友”了。很多人第一反应是“SQL 哪里写错了”&#xff0c;但把 SQL 翻来覆去看&#xff0c;…

作者头像 李华
网站建设 2026/9/25 2:54:35

USB转I2C适配器实现400KHz总线扫描与Excel导出实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华