推理服务为什么一接多 Schema 并发就开始抖延迟:从 Grammar 编译缓存到 LRU 污染的工程实战
团队把Structured Output接进推理服务后最初只盯着解码阶段。⚠️ 到线上一旦同一批流量里混入报表提取、工单归档、函数参数生成和审核回执这几类不同Schema首Token延迟就会突然抖起来。模型没变问题却像随机故障。更隐蔽的是很多请求内容相近Schema却不完全相同。 有的多了description有的字段顺序不同有的把可选项展开成了租户私有版本。结果是编译器看见的不是“同一类结构”而是一堆彼此独立的新语法图命中率不低尾延迟仍在抖。图 1多 Schema 并发下真正先抖的往往是首 Token 之前的 Grammar 编译阶段多 Schema 并发为什么会先打爆编译缓存结构化输出一旦开启请求在真正解码前通常要先把JSON Schema编译成约束图。 这一步本来可以复用可线上常见的实现把原始Schema字符串直接当缓存键任何字段排序、注释文本甚至默认值写法差异都会把热命中变成冷编译。全局LRU也很容易被“低频但高基数”的请求污染。 某些租户会按表单动态拼字段高峰时一分钟能打进几十种一次性Schema。如果冷门请求和高频请求共享同一条逐出队列真正该常驻的热点语法图会被不断挤掉混入几条重编译请求就足以拖慢整个批次。✅图 2如果 Schema 指纹不稳定再大的缓存也会被低频变体冲垮一组压测把解码成本和编译成本拆开看这次回放了36路混合流量其中同时出现18类Schema前5类占了71%请求。 基线方案关闭编译缓存方案二使用原始字符串做全局LRU方案三先做结构归一化再拆成热点常驻池和冷门逐出池。结果直接结构化输出抖不抖先看编译有没有被当成首访。⭐方案首 Token p95Grammar 编译 p95缓存命中率吞吐无编译缓存498 ms162 ms0%76 token/s原始 Schema 全局 LRU452 ms119 ms41%81 token/s指纹归一化 两级缓存403 ms54 ms79%89 token/s关键不在于把缓存做大而是先让“结构相同”的请求拿到同一个指纹。️ 去掉description、统一字段顺序、折叠等价枚举写法后命中率才真正可解释再把热点Schema固定在常驻池里冷门变体只去冲击冷池尾延迟才会明显稳住。defgrammar_cache_key(schema:dict)-str:normalizednormalize_schema(schema,drop_docsTrue,sort_keysTrue)returnsha1(json.dumps(normalized,separators(,,:)).encode()).hexdigest()defchoose_pool(fingerprint:str,hotset:set[str])-str:returnpinned_hot_pooliffingerprintinhotsetelsecold_lru_pool图 3先稳定 Schema 指纹再谈缓存容量收益会更直接工程上真正该补的是两级缓存和请求级游标隔离更稳的做法是把编译后的Grammar当成不可变模板把运行中的状态游标留在请求本地。️ 模板负责跨请求复用游标负责当前 token 走到哪一步如果两者混在一起缓存系统就会同时面对命中抖动和状态串写。把模板和请求态游标拆开是多Schema服务上线的基本盘。另一层不能省的是预算治理。⏱️ 热点池要按路由或租户设上限冷池要记录compile_qps、逐出后再命中率和编译等待时间一旦某类动态Schema持续冲高就要回头收敛字段自由度或者把它们改成模板参数化而不是继续纵容LRU当垃圾桶。笔者认为未来结构化输出比拼的不会是谁支持更多格式而是谁能把Schema多样性治理成稳定成本。图 4两级缓存比单个全局 LRU 更适合多租户结构化输出未来 3 到 6 个月 结构化推理会从能用走向可治理一句话总结多Schema并发真正拖慢推理服务的往往不是约束解码本身而是编译缓存把热点和噪声混在了一起。 只要先做Schema归一化再补上热点常驻池、冷池逐出和请求级游标隔离首Token抖动会比盲目扩缓存更容易收住。你们线上更常见的是命中率虚高还是LRU被动态字段打穿