这道题其实问的是,怎么把一个通用生成模型硬掰成分类模型,让它只输出我们想要的那几个标签。我做过一些落地,聊一下我的思路。
先说最简单的,提示工程加输出解析。就是在Prompt里把可选标签全列出来,告诉模型只能选一个,然后后面用正则或者规则把输出兜住。这个方案的好处是零改造成本,你甚至不用微调,拿个Prompt就能跑。但说实话,它非常不可靠。模型可能自己造个标签出来,或者格式乱七八糟,你解析的时候就得各种兜底。所以它只适合快速验证或者Demo,真正上线我不太敢只用它。
真正生产环境我比较推荐[[Function Calling]]。先核对具体模型和API的strict能力。你定义一个工具schema,比如{"name": "classify", "parameters": {"type": "object", "properties": {"label": {"type": "string", "enum": ["positive", "negative"]}}}},这只是schema形状示意;要在支持的端点启用strict并满足required/additionalProperties等规则,不能凭这段普通工具定义承诺严格输出。拒绝、截断和工具未调用需单独处理。而且它还能跟后续的业务逻辑无缝衔接,比如直接解析出标签去触发动作。不过有个前提,就是你的模型得支持这个能力,有些老模型或者小模型可能不行,那就得换方案。
再一个方案是约束解码。这个理解起来很简单,就是在模型生成每个token的时候,用有限状态机或CFG去限制它只能走合法的路径。标签字符串与token边界不一定对齐,约束器按实际tokenizer与前缀屏蔽无法继续组成合法输出的token。正常完成时可限制格式与标签集合,但仍须检测拒绝、截断和业务误判。可以用引擎现成的结构化输出接口,例如所用版本vLLM的structured_outputs,无需一律自行改源码;文法编译和每步掩码的开销要实测。所以它适合那种零容忍错的场景,比如金融风控,你绝对不能输出一个不存在的标签。
最后一个方案是分类头微调。把LLM的最后一层换成线性层,直接映射到标签空间,本质上变成了传统分类模型。类别索引天然在有限集合内,但准确率和延迟仍要测。分类接口不负责通用生成,是否保留基座生成能力取决于冻结与微调方案,并非加头就必然失去通用性;它仍需要标注数据。所以它适合那种标签固定、数据量够、对延迟要求极高的场景,比如电商客服的退款原因分类;响应目标与可达延迟要在实际模型、硬件和负载下验证。
实际落地我很少只用一种方案,往往是组合使用。比如主链路用Function Calling,因为它兼顾了可控性和灵活性,strict工具模式本身可能已使用约束解码,关键路径还要做失败分支和业务校验,避免重复堆叠同一机制。或者对于一些高频但简单的分类,我会用微调好的小模型,只有复杂情况才调大模型。
这里有个坑,就是大规模枚举可能触及具体API的schema限制、编译成本或分类难度。strict正常完成时不应越出被支持的枚举;请求被拒、截断和语义错分要分别统计。所以我会做分层,先粗分类再细分类,或者用[[检索增强]]的方式,让模型只从候选子集里选。
另外,约束解码通常在采样前调整logits,不会仅因限制候选token就使同一已生成前缀的KV Cache失效。它新增的是文法编译、解析状态和token掩码计算开销;若采用回滚或跳跃生成,再按引擎实现核对缓存更新,不能笼统说一定破坏KV Cache。
所以整体上,我更倾向于把Function Calling作为默认方案,然后根据业务场景的延迟和准确率要求,决定要不要加约束解码或者微调。没有银弹,就是权衡。