GrammarManager、约束预处理缓存与超时#

这章解决什么问题#

前面的结构化生成章节已经讲了 grammar backend 自身的状态推进、rollback 和 accept_token(...) 语义,但还有一层非常重要的运行时控制没有单独展开:这些 grammar 对象到底是谁创建的、何时创建、是否缓存、编译超时后怎样退化成错误对象。

这一章的目标,就是把 GrammarManager 这层“约束预处理控制面”拉回主线里。

为什么这一层不能只在 backend 章节里一笔带过#

因为 backend 章节讲的是“对象创建之后怎样活着”;这一章讲的是“对象能否被及时、正确地创建出来”。在真实系统里,很多结构化生成问题根本还没走到 accept_token(...),就已经在下面几步出问题了:

  • backend 被关闭
  • key 选择错误
  • 预处理超时
  • cache 命中或未命中带来的行为差异
  • 编译失败后退化成 InvalidGrammarObject

这是一层完全不同的问题。

一张图:grammar 约束进入 generation path 之前,还要先过一层管理器#

这张图解决的理解障碍是:读者容易把 grammar backend 想成“请求一来就直接 new 一个对象”,忽略中间还有缓存、dispatch 和超时控制。

flowchart LR
    Req["sampling_params.json_schema / regex / ebnf / structural_tag"] --> GM["GrammarManager"]
    GM --> Key["grammar_key selection"]
    Key --> Cache["backend cache / future / cache hit"]
    Cache --> Obj["BaseGrammarObject / InvalidGrammarObject"]
    Obj --> Decode["generation path"]

图比纯文字多解释的一点是:约束不是直接进入 decode loop,而是先被 GrammarManager 归档、缓存和调度。

GrammarManager 先做的不是编译,而是分类#

grammar_manager.py 可以看到,它会先判断当前请求是否真的带了结构化约束:

  • json_schema
  • regex
  • ebnf
  • structural_tag

然后再决定 key 类型,例如:

  • ("json", req.sampling_params.json_schema)
  • ("regex", req.sampling_params.regex)
  • ("ebnf", req.sampling_params.ebnf)
  • ("structural_tag", req.sampling_params.structural_tag)

这说明结构化生成不是“带了一个 schema 字段就完事”,而是要先把约束形态显式归类,后续缓存与编译都依赖这个 key。

backend 被关闭时会发生什么#

源码里有一条非常清楚的分支:如果 server 以 --grammar-backend none 启动,而请求又带了 json_schema / regex / ebnf / structural_tagGrammarManager 不会让它默默退化,而是直接生成错误信息。

这很值得写进书里,因为它说明:

  • 结构化生成不是软依赖。
  • 运行时不会假装支持一个已经在启动参数里关闭的能力。

这也是系统性技术书应当强调的边界条件,而不是只讲快乐路径。

cache 命中为什么会改变你对问题的理解#

base_grammar_backend.pyGrammarManager 共同说明,这一层存在 grammar cache,cache item 可能是:

  • 已编译好的 grammar object
  • 一个 future
  • InvalidGrammarObject

这意味着 cache 命中不是只代表“更快一点”,它还会影响你看到的问题是:

  • 新鲜编译问题
  • 历史缓存复用问题
  • 已缓存的失败对象再次被复用

这类区别在现场排障时非常重要。

InvalidGrammarObject 为什么是正式状态,不是临时异常#

很多系统会把编译失败直接抛异常;这里则显式有 InvalidGrammarObject。它的价值是:约束失败可以被当作对象留在运行时语义里,而不是只在日志中短暂出现一下。

这会带来两个好处:

  • 后续路径可以一致地判断“这是一个失效 grammar”。
  • 错误可以和具体 grammar key、dispatch type 绑定起来。

这也是好技术书喜欢强调的那种“失败也是第一等状态”的设计。

“Grammar preprocessing timed out” 说明了什么#

grammar_manager.py 里显式会把超时转换成:

  • req.grammar_key
  • InvalidGrammarObject("Grammar preprocessing timed out")
  • 对应错误信息

这说明系统承认 grammar 预处理本身可能是昂贵或不稳定的阶段,因此对它设置了独立超时语义。也就是说,结构化生成并不是“只有 decode 慢”,约束编译阶段本身也可能成为延迟或失败源。

dispatch type 为什么值得读者记住#

GrammarStats 里带 dispatch_typeis_cache_hitnum_timeout 等字段,这说明系统对 grammar 预处理并不是黑箱态度,而是在试图记录:

  • 这是哪类约束
  • 是否命中缓存
  • 有没有超时

这使得结构化生成问题可以被更细粒度地归因,而不是统统归成“schema 不工作”。

这一层最容易出现的误判#

1. 把 grammar backend 问题和 grammar manager 问题混为一谈#

前者更偏对象语义,后者更偏对象创建与缓存控制。

2. 把 cache hit 当成纯性能优化#

它也可能改变失败模式和排障入口。

3. 看到 decode 阶段的 grammar 错误,就以为编译阶段没问题#

实际上编译阶段可能早就退化成 InvalidGrammarObject,只是你没顺着 manager 层去看。

如果结构化约束“看起来没生效”,先怎么查#

建议按这个顺序:

  1. 先确认 request 到底传的是 json_schemaregexebnf 还是 structural_tag
  2. 看启动参数是否启用了 grammar backend。
  3. GrammarManager 为它生成的 grammar_keydispatch_type 是什么。
  4. 看是否命中了缓存,以及命中的到底是成功对象还是 InvalidGrammarObject
  5. 最后再深入具体 backend 的 accept_token、rollback 和 mask 语义。

小结#

这一章真正想补齐的,是结构化生成里最容易被忽略的“编译前置层”:

  • grammar backend 之前,还有 GrammarManager 这层预处理控制面。
  • key 选择、cache 命中、超时和失败对象都发生在这里。
  • 只有把这层看清,结构化生成章节才真正覆盖“约束如何进入系统”而不是只覆盖“约束对象如何运行”。