Hugging Face 支持 Decision Models 并推出统一 decision 接口
Decision Models in Hugging Face
Hugging Face 在 Transformers、Hub 和 llama.cpp 中支持 decision models,让模型对预定义答案直接输出概率分布而非生成文本。
Hugging Face 官方给出 decision models 的统一接口设计、choice/score/noul 三种原语和 Transformers 与 llama.cpp 的完整用法,读者可以据此评估结构化决策场景是否能替代生成式方案。
大语言模型(LLM)极其擅长生成文本。我们用它们回答问题、编写代码、推理问题,以及做更多的事情。但并不是每个问题都需要生成文本。
设想一个客服系统收到如下消息:
“我的订阅被扣了两次款。我今天需要退款。”
系统需要判断哪个团队应该处理该请求、紧急程度如何,以及客户是否在要求退款。
我们可以要求 LLM 返回一个包含这些答案的 JSON 对象。
llama cli -hf ggml-org/gemma-4-e4b-it-GGUF:Q4_0
> We have the following customer support ticket, "I was charged twice for my subscription. I need a refund today.", which team should handle the request, how urgent it is, and whether the customer is requesting a refund. Give the answers in JSON.
[Start thinking]
...
[end thinking]
```json
{
"team_to_handle": "Billing",
"urgency": "High",
"request_refund": true
}
但如果模型能够直接评估可用的选项并返回一个概率分布,而不生成任何文本呢?
from transformers import pipeline
decide = pipeline("decision", model="autotrust/GEV-26B-Decide")
decision = decide(
{
"state": "I was charged twice for my order and want a refund.",
"questions": {
"department": {
"type": "choice",
"instructions": "Which team should handle this?",
"criteria": {
"billing": "Payments and refunds",
"technical": "Bugs and technical issues",
"sales": "Pricing and subscriptions"
}
},
}
}
)
# {
# 'department': {
# 'type': 'choice',
# 'choice': 'billing',
# 'confidence': 0.9974000453948975,
# 'probabilities': {
# 'billing': 0.9982666969299316,
# 'technical': 0.0010246571619063616,
# 'sales': 0.0007087149424478412
# }
# }
# }
这就是决策模型背后的核心思想。
什么是决策模型?
决策模型接收一些输入上下文(状态),用一组预定义的可能答案(基元)评估一个问题,并返回一个带有概率的结构化决策。我们不是让模型逐 token 生成答案,而是让它为我们的应用已经理解的答案赋予概率。
TypeSafe AI 推出了 Jev,并将其描述为一个 系统一模型。这个名字的灵感来自快速、直觉式思考(系统一)与较慢的、深思熟虑的思考(系统二)之间的区别。
社区已统一使用决策模型这一术语,因为它描述的是模型的计算角色,而不是诉诸某种特定的认知理论。在本文的其余部分,我们将把这些模型称为“决策模型”,而不是“系统一模型”。(Twitter 上决定我们该如何称呼这类模型的投票)
对于决策模型来说,其基本接口是:
State + Questions + Allowed Answers → Decisions + Probabilities
为什么不用 LLM 处理所有事情?
我们完全可以提示 LLM 返回一个包含我们所需决策的 JSON 对象。事实上,许多应用目前就是这样使用语言模型进行分类、路由和其他结构化任务的。
然而,这种做法存在一些问题。
- LLM 并不保证遵循所要求的 schema:即使有明确的指令,模型也可能生成无效的 JSON、引入意外的键,或返回超出允许选项范围的答案。
- 约束解码有所帮助,但并不能消除计算开销:像 Outlines 和 XGrammar 这样的库可以通过限制模型在每一步允许生成的 token 来强制执行语法或 JSON schema。这确保了输出符合所支持的结构约束,但也引入了额外的工作。
- 为什么要生成完整的回复:LLM 是为开放式生成而设计的,但当我们的应用只需要在预定义的答案之间做出选择时,自回归解码可能是多余的工作。
决策模型与分类模型有何不同?
在理解决策模型的接口之前,先回顾两种密切相关的方法会很有帮助,即传统分类和零样本分类。
传统分类
假设我们训练一个分类器,将客服请求路由到三个部门:
- 账务
- 技术支持
- 销售
传统的分类模型学习从输入到一个预定义标签集合的映射。在训练过程中,模型学习从样本中预测这些标签。分类头通常为每个类别产生一个 logit,softmax 将这些 logits 转换为概率分布。
这里的局限在于 输出类别被固化在模型的分类头中。
如果我们引入第四个部门,例如法务部,现有的分类器不会自动获得相应的输出类别。我们可能需要修改分类头、收集训练数据,并重新训练或调整模型。
零样本分类
零样本分类解决了这一局限的重要部分。分类器无需针对每个输出类别专门训练,我们可以在推理时提供候选标签。
例如:
from transformers import pipeline
classifier = pipeline(
"zero-shot-classification",
model="facebook/bart-large-mnli",
)
classifier(
"My payment was deducted twice.",
candidate_labels=["billing", "technical support", "sales"],
)
# {'sequence': 'My payment was deducted twice.',
# 'labels': ['billing', 'sales', 'technical support'],
# 'scores': [0.8466812372207642, 0.0962134450674057, 0.05710534751415253]}
与传统的固定标签分类器不同,类别可以在不同请求之间变化。让我们添加另一个部门,例如“法务部”,看看模型的输出。
from transformers import pipeline
classifier = pipeline(
"zero-shot-classification",
model="facebook/bart-large-mnli",
)
classifier(
"My payment was deducted twice.",
candidate_labels=["billing", "technical support", "sales", "legal"],
)
# {'sequence': 'My payment was deducted twice.',
# 'labels': ['billing', 'sales', 'technical support', 'legal'],
# 'scores': [0.8185835480690002, 0.09302053600549698, 0.05521026626229286, 0.03318570926785469]}
一种常见的实现使用为自然语言推理(NLI)训练的模型。每个候选标签被转换为假设,模型衡量输入对该假设的支持程度。
这听起来已经与决策模型非常相似。而且在很多方面,它确实如此。
决策模型的不同之处
决策模型将这一思想扩展到不仅仅是将输入分配到某个类别。决策模型可以接受关于同一状态的多个命名问题,且答案类型各不相同。类型可以是在备选项之间进行选择(choice)、在有序量表上打分(score),或估计某个陈述为真的概率(noul)。
更重要的是,它的接口是围绕可能决策的概率分布设计的。
请看这三种方法:
| 属性 | 分类 | 零样本分类 | 决策模型 |
|---|---|---|---|
| 选项上的概率分布 | 是 | 是 | 是 |
| 推理时定义的标签 | 否 | 是 | 是 |
| 多个带类型的问题 | 否 | 否 | 是 |
| 有序打分和是非决策 | 任务专用 | 可通过适配实现 | 原生原语 |
决策模型的优势并不在于它们发明了运行时定义的标签或概率分类,而在于它们将这些能力组织成一个一致的接口,并可能由专门为决策优化的架构和训练流程来支撑。
决策模型接口
决策模型的请求包含以下两个组成部分:
- state(状态)
- questions(问题)
状态
状态是模型要评估的信息。它可以是一个字符串、一个结构化的 JSON 对象,或者对于支持的全模态模型而言的其他模态。
例如:
{
"customer_message": "I was charged twice. Please refund me today.",
"subscription": "pro",
"previous_tickets": 2
}
问题及其原语
问题描述我们想了解状态的哪些内容。在决策模型接口中,三种基本原语代表不同形式的决策:
- Choice(选择):从多个可能的答案中选择一个。
- Score(打分):在有序量表上对某事物进行评估。
- Noul(是非):以概率方式回答一个是非问题。
让我们逐一介绍它们。
Choice:选项上的概率分布
Choice 是最直观的原语,也最接近传统分类。
假设我们提问:
哪个部门应该处理这个客户请求?
可选的选项为 Billing(计费)、Technical support(技术支持)和 Sales(销售)。
from transformers import pipeline
decide = pipeline("decision", model="autotrust/GEV-26B-Decide")
decision = decide(
{
"state": "I was charged twice for my order and want a refund.",
"questions": {
"department": {
"type": "choice",
"instructions": "Which team should handle this?",
"criteria": {
"billing": "Payments and refunds",
"technical": "Bugs and technical issues",
"sales": "Pricing and subscriptions"
}
},
}
}
)
# {
# 'department': {
# 'type': 'choice',
# 'choice': 'billing',
# 'confidence': 0.9974000453948975,
# 'probabilities': {
# 'billing': 0.9982666969299316,
# 'technical': 0.0010246571619063616,
# 'sales': 0.0007087149424478412
# }
# }
# }
模型为每个选项分配一个 logit。对于 logits($z_1,\ldots,z_K$)(共有 K 个类别),我们可以用 softmax 得到概率:
pi=exp(zi/T)∑j=1Kexp(zj) p_i=\frac{\exp(z_i/T)}{\sum_{j=1}^{K}\exp(z_j)}
这里,(K) 是选项的数量。
考虑下面这个示意性的分布:
| 选项 | 概率 |
|---|---|
| Billing | 0.998 |
| Technical support | 0.001 |
| Sales | 0.001 |
所有概率之和为 1:
∑i=1Kpi=1. \sum_{i=1}^{K}p_i=1.
模型选择概率最大的那个选项:
y^=argmaxipi \hat y=\arg\max_i p_i
在这个例子中,模型选择的是 Billing,概率为 0.998,但其余的概率也很有用。它们告诉我们模型是如何在各个备选项之间分配其信念的。
Score:有序分布的期望值
当存在若干互不相同的备选项时,Choice 很有用。但如果我们想测量某个有序尺度上的量呢?
考虑这样一个问题:
这个客户请求有多紧急?
我们定义三个等级:
- 0:低紧急度
- 1:中紧急度
- 2:高紧急度
from transformers import pipeline
decide = pipeline("decision", model="autotrust/GEV-26B-Decide")
decision = decide(
{
"state": "I was charged twice for my order and want a refund.",
"questions": {
"urgency": {
"type": "score",
"instructions": "How urgent is the request?",
"criteria": ["low", "medium", "high"]
},
}
}
)
# {
# 'urgency': {
# 'type': 'score',
# 'score': 1.7053890228271484,
# 'legend': {
# '0': 'low',
# '1': 'medium',
# '2': 'high'
# },
# 'confidence': 0.5580834252759814,
# 'probabilities': {
# '0': 0.014908230863511562,
# '1': 0.26479458808898926,
# '2': 0.7202972173690796
# }
# }
# }
模型同样会产出一个概率分布:
| 等级 | 概率 |
|---|---|
| 0 — 低 | 0.10 |
| 1 — 中 | 0.26 |
| 2 — 高 | 0.72 |
Score 原语并非只返回可能性最大的等级,而是计算等级索引的期望值。
对于表示等级的随机变量 (X),其期望为:
E[X]=∑i=0K−1ipi \mathbb{E}[X]=\sum_{i=0}^{K-1}ip_i
对于我们的例子:
E[X]=0(0.10)+1(0.26)+2(0.72)=1.7 \mathbb{E}[X] = 0(0.10)+1(0.26)+2(0.72)=1.7
因此报告出的分数是 1.7。
这并不意味着模型选择了一个介于中紧急度和高紧急度之间的虚构等级。它的意思是其概率分布的期望值为 1.7。
Noul:一个伯努利决策
第三个原语是 Noul,它表示一个二元问题。
例如:
客户是否在申请退款?
from transformers import pipeline
decide = pipeline("decision", model="autotrust/GEV-26B-Decide")
decision = decide(
{
"state": "I was charged twice for my order and want a refund.",
"questions": {
"refund": {
"type": "noul",
"instructions": "Is the customer requesting a refund?"
}
}
}
)
# {
# 'refund': {
# 'type': 'noul',
# 'noul': 0.9947046637535095
# }
# }
我们不是定义一个选项列表,而是让模型返回答案为真的概率。我们可以借助伯努利分布来理解这一点(noul 一词源自 Bernoulli)。
设 (Y) 表示一个二元结果:
Y∼Bernoulli(p) Y\sim\operatorname{Bernoulli}(p)
其中
P(Y=1)=p,P(Y=0)=1−p. P(Y=1)=p,\qquad P(Y=0)=1-p.
如果模型返回:
{"type": "noul", "noul": 0.92}
那么它给“是”分配的概率为 0.92,给“否”分配的概率为 0.08。
模型在这里并不是随机采样一个二元答案。它返回的是概率参数本身,由应用程序来决定如何据此行动。
Confidence:模型有多确定?
到目前为止,我们从模型那里得到的是概率分布。但决策模型接口还会为 Choice 和 Score 返回一个名为 confidence 的字段。
完整的分布可能包含多个可能的答案,总结模型信念的集中程度是很有用的。有了 confidence,我们就能做到这一点。
对于一个有 (K) 个选项的 Choice,置信度为:
pmax−1K1−1K \frac{p_{\max}-\frac{1}{K}}{1-\frac{1}{K}}
其中($p_{\max}$)是最大的预测概率。
其直观含义是:
- 均匀分布的置信度为 0。($p_{\max} = \frac{1}{K}$)。这意味着模型估计所有选项的可能性相同。
- 完全集中在某一选项上的分布置信度为 1。($p_{\max} = 1$)
- 相对于均匀分布,获胜概率越大,置信度越高。
在我们之前的三选一示例中,Billing 的概率为 0.80:
C=3(0.99)−12=0.98 C=\frac{3(0.99)-1}{2}=0.98
注意,获胜概率是 0.99,但报告的置信度是 0.98。置信度将最大概率相对于可用选项数量进行了归一化。相比之下,前面的有序 Score 示例的置信度只有 0.558。
Hugging Face 生态中的决策模型
决策模型是一个年轻且快速发展的领域。每个模型在底层做出决策的方式各不相同(联合对整个 schema 打分的头部、在 LLM 骨干之上的独立读出层、边缘尺寸和多模态变体等)。这种多样性是健康的,但也有代价:如果每个模型都暴露自己的接口,那么围绕它的每个工具都需要针对特定模型做适配。
我们在 Hugging Face 的目标是就一个通用的接口达成共识,而不强求统一的实现。决策模型内部可以随心所欲地工作,只要它接受相同的状态和问题,并返回相同类型的决策即可。一旦这一契约确立,生态系统的其余部分就可以一次性地基于它构建:
- 像 llama.cpp 这样的推理引擎可以通过同一 API 服务任何决策模型
- Hub 可以一致地打包和展示这些模型
- 训练工具可以面向共享的原语,而不是各自的架构
本节余下部分将逐一介绍这些组件。
1. 使用 Transformers 进行服务
Transformers 现在支持 DecisionPipeline、AutoModelForDecision 和 DecisionConfig 类。它们遵循 transformers 中 pipeline 和 model 类的整体结构,但专为决策模型量身定制。
from transformers import pipeline
decide = pipeline("decision", model="autotrust/GEV-26B-Decide")
print(decide)
# DecisionPipeline: {'model': 'JevForDecision', 'dtype': 'bfloat16', 'device': 'cuda', 'input_modalities': 'text'}
还可以像这样检查配置和模型:
from transformers import AutoModelForDecision, DecisionConfig
model_id = "autotrust/GEV-26B-Decide"
config = DecisionConfig.from_pretrained(model_id)
model = AutoModelForDecision.from_pretrained(model_id)
Transformers 还支持 /v1/systemone 端点。
我们可以使用服务依赖启动一个本地服务器:
pip install "transformers[serving]"
transformers serve
使用兼容版本的 Transformers,我们随后可以发送决策请求:
curl http://localhost:8000/v1/systemone \
-H "Content-Type: application/json" \
-d '{
"model": "autotrust/GEV-26B-Decide",
"state": "I was charged twice. Please refund me today.",
"questions": {
"department": {
"type": "choice",
"instructions": "Which team should handle this?",
"criteria": {
"billing": "Payments and refunds",
"technical": "Bugs and technical issues",
"sales": "Pricing and subscriptions"
}
},
"urgency": {
"type": "score",
"instructions": "How urgent is the request?",
"criteria": ["low", "medium", "high"]
},
"refund": {
"type": "noul",
"instructions": "Is the customer requesting a refund?"
}
}
}'
响应包含三个具名答案,以及对应的类型化结果和概率。
{
"model":"autotrust/GEV-26B-Decide",
"answers":{
"department":{
"type":"choice",
"choice":"billing",
"confidence":0.9974000453948975,
"probabilities":{
"billing":0.9982666969299316,
"technical":0.0010246571619063616,
"sales":0.0007087149424478412
}
},
"urgency":{
"type":"score",
"score":1.7525686174631119,
"legend":{"0":"low","1":"medium","2":"high"},
"confidence":0.6288529792800546,
"probabilities":{
"0":0.014000383205711842,
"1":0.2194305807352066,
"2":0.7665690183639526
}
},
"refund":{
"type":"noul",
"noul":0.9953224062919617
}
},
"usage":{"input_tokens":148,"output_tokens":0}
}
一个有趣的细节是,上述 API 也适用于兼容的自回归语言模型。Transformers 会构建决策提示词,在批量前向传播中评估答案标签的 logits,并应用 softmax,而无需生成响应文本。这意味着我们无需训练新模型也能实验决策接口。
2. 使用 llama.cpp 进行服务
llama.cpp 也支持决策模型,使用相同的 /v1/systemone API。
例如,我们可以从 Hub 启动一个 GGUF 决策模型:
llama serve -hf ggml-org/Kev-4B-GGUF
默认服务器运行在 9931 端口。
随后我们可以使用 http://localhost:9931/v1/systemone 发送包含相同状态和问题的请求。由于模型已由服务器选定,我们可以省略请求中的 model 字段。
其有用的特性就是接口兼容性。
curl http://localhost:9931/v1/systemone \
-H "Content-Type: application/json" \
-d '{
"model": "ggml-org/Kev-4B-GGUF",
"state": "I was charged twice. Please refund me today.",
"questions": {
"department": {
"type": "choice",
"instructions": "Which team should handle this?",
"criteria": {
"billing": "Payments and refunds",
"technical": "Bugs and technical issues",
"sales": "Pricing and subscriptions"
}
},
"urgency": {
"type": "score",
"instructions": "How urgent is the request?",
"criteria": ["low", "medium", "high"]
},
"refund": {
"type": "noul",
"instructions": "Is the customer requesting a refund?"
}
}
}'
3. 在 Hugging Face Hub 上发现模型
Hugging Face 引入了专门的 decision-model 标签,用于发现兼容的模型。这很有用,因为决策模型不必共享同一个骨干或架构。
例如,该生态既包括基于 ModernBERT 等编码器架构的模型,也包括从更大的生成式语言模型骨干衍生而来的模型。
4. 使用 Hugging Face 工具训练决策模型
理解了推理之后,下一个问题是:我们能否对决策模型进行适配。由于决策模型集成在 transformer 中,我们可以使用 Trainer API 对它们进行微调。
下面是一个微调 JEV-9B 来路由支持请求的简单示例。它刻意做得很小,但展示了真正重要的部分:
- 每个示例是一个决策请求(一个
state加上questions),并配有标准答案。 model.decision_config.prepare_inputs将请求转换为模型输入,方式与推理时相同。- 模型为每个选项返回一个 logit,因此
choice问题通过一个普通的交叉熵损失在选项上进行训练。
import os; os.environ["CUDA_VISIBLE_DEVICES"]="2"
import torch
import torch.nn.functional as F
from transformers import AutoTokenizer, AutoModelForDecision, Trainer, TrainingArguments
checkpoint = "autotrust/JEV-9B"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForDecision.from_pretrained(checkpoint)
examples = [
("I was charged twice. Please refund me.", "refund"),
("My package has not arrived.", "shipping"),
("The application crashes when I open it.", "technical"),
("I want my money back.", "refund"),
("Where is my delivery?", "shipping"),
("I cannot log into my account.", "technical"),
]
options = {
"refund": "Billing issues and refunds",
"shipping": "Orders and deliveries",
"technical": "Software and account issues",
}
def preprocess(text, label):
request = {
"state": text,
"questions": {
"category": {
"type": "choice",
"instructions": "Classify the support request.",
"criteria": options,
}
},
}
(inputs, questions), = model.decision_config.prepare_inputs(
request, tokenizer
)
inputs["marks"] = inputs["marks"].unsqueeze(0)
inputs["num_options"] = torch.tensor([[len(questions[0].options)]])
inputs["question_type_ids"] = torch.tensor([[1]]) # choice
inputs["labels"] = torch.tensor(
[questions[0].options.index(label)]
)
return inputs
dataset = [preprocess(text, label) for text, label in examples]
class DecisionTrainer(Trainer):
def compute_loss(self, model, inputs, return_outputs=False, **kwargs):
inputs = inputs.copy()
labels = inputs.pop("labels")
logits = model(**inputs)
logits = logits.unsqueeze(0) # [3] -> [1, 3]
loss = F.cross_entropy(logits.float(), labels)
return (loss, logits) if return_outputs else loss
trainer = DecisionTrainer(
model=model,
args=TrainingArguments(
output_dir="./decision-toy",
max_steps=30,
per_device_train_batch_size=1,
learning_rate=1e-3,
logging_steps=5,
save_strategy="no",
report_to="none",
remove_unused_columns=False,
),
train_dataset=dataset,
data_collator=lambda batch: batch[0],
)
trainer.train()
对训练决策模型的一流支持也即将登陆 TRL(huggingface/trl#8040),这样我们就不必手写这些管道代码了。在撰写本文时,该 PR 仍处于打开状态。
如果我们想更进一步,把生成式 LLM 变成决策模型,Unsloth 提供了一个方案,使用 LoRA 和决策评分头对 Qwen 等模型进行微调。
结语
决策模型之所以有趣,并不是因为分类是新事物,而是因为它们让决策和概率成为软件的一等输出。
传统分类器早已能输出概率,零样本分类器也早已支持运行时定义的类别。决策模型将这些理念带入一个类型化接口中,具备多种问题原语、非生成式评分,以及为可靠、校准良好的决策进行专门训练的可能性。
来源: Hugging Face Blog · huggingface.co
