大型语言模型(LLMs)非常擅长生成文本。我们用它们来回答问题、编写代码、推理问题等等。但并非所有问题都需要生成文本。
考虑一个客户支持系统收到以下消息:
“我的订阅被重复收费了。我今天需要退款。”
系统需要确定哪个团队应处理该请求、请求的紧急程度以及客户是否在请求退款。
我们可以要求 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
# }
# }
# }
这就是决策模型背后的核心思想。
什么是决策模型?
决策模型接收一些输入上下文(状态),评估一个带有预定义可能答案(原语)的问题,并返回一个结构化的决策以及概率。我们不是要求模型逐词生成答案,而是要求它为我们的应用已经理解的答案分配概率。

TypeSafe AI 推出了 Jev,将其描述为系统一模型。这个名字的灵感来源于快速、直觉的思维(系统1)与较慢、审慎的思维(系统2)之间的区别。
社区已经统一使用术语决策模型,因为它描述了模型的计算角色,而不是援引特定的认知理论。在本文的剩余部分,我们将把这些模型称为“决策模型”而不是“系统一模型”。(Twitter 上的一项投票决定了我们应该如何称呼此类模型)
对于决策模型,基本接口是:
State + Questions + Allowed Answers → Decisions + Probabilities
为什么不使用 LLM 做所有事情?
我们可以简单地提示 LLM 返回一个包含我们决策的 JSON 对象。事实上,许多应用已经这样使用语言模型进行分类、路由和其他结构化任务。
然而,这种方法存在一些问题。
- LLM 不保证遵循请求的模式:即使有明确的指令,模型也可能产生无效的 JSON、引入意外的键或返回允许选项之外的答案。
- 约束解码有帮助,但不能消除计算开销:像 Outlines 和 XGrammar 这样的库可以通过限制模型在每一步允许生成的 token 来强制语法或 JSON 模式。这确保输出遵循支持的结构约束,但引入了额外的工作。
- 为什么要生成整个响应:LLM 是为开放式生成而设计的,但当我们的应用只需要在预定义答案之间进行选择时,自回归解码可能是不必要的工作。
决策模型与分类模型有何不同?
在理解决策模型的接口之前,回顾两种密切相关的方法——传统分类和零样本分类——是有用的。
传统分类
假设我们训练一个分类器,将客户支持请求路由到三个部门:
- 账单
- 技术支持
- 销售
传统分类模型学习从输入到预定义标签集的映射。在训练期间,模型学习从示例中预测这些标签。分类头通常为每个类别产生一个 logit,softmax 将这些 logit 转换为概率分布。
这里的限制是输出类别被固化在模型的分类头中。
如果我们引入第四个部门,例如法务,现有的分类器不会自动获得相应的输出类别。我们可能需要修改分类头、收集训练数据,并重新训练或调整模型。
零样本分类
零样本分类解决了这一限制的重要部分。我们不需要分类器专门针对每个输出类别进行训练,而是在推理时提供候选标签。
例如:
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)训练的模型。每个候选标签被转换为一个假设,模型衡量输入蕴含该假设的强度。
这听起来已经与决策模型非常相似。在许多方面,确实如此。
决策模型的不同之处
决策模型将这一思想扩展到不仅仅是将输入分配给一个类别。决策模型可以接受关于同一状态的多个命名问题,并具有不同的答案类型。类型可以是选择备选方案(选择)、对有序量表评分(评分)或估计陈述为真的概率(noul)。
更重要的是,其接口围绕可能决策上的概率分布设计。
考虑这三种方法:
| 属性 | 分类 | 零样本分类 | 决策模型 |
|---|---|---|---|
| 选择上的概率 | 是 | 是 | 是 |
| 推理时定义标签 | 否 | 是 | 是 |
| 多个类型化问题 | 否 | 否 | 是 |
| 有序评分和是/否决策 | 任务特定 | 可通过适配实现 | 原生原语 |
决策模型的优势不在于它们发明了运行时定义的标签或概率分类。而在于它们将这些能力组织成一个一致的接口,可能由专门为决策优化的架构和训练程序支持。
决策模型接口
决策模型请求包含以下两个组成部分:
- 状态
- 问题
状态
状态是模型应当评估的信息。它可以是字符串、结构化 JSON 对象,或受支持的全模态模型的其他模态。
例如:
{
"customer_message": "I was charged twice. Please refund me today.",
"subscription": "pro",
"previous_tickets": 2
}
问题及其原语
问题描述我们想了解状态的哪些方面。在决策模型接口中,三个基本原语代表不同形式的决策:
- 选择(Choice):从若干可能答案中选择一个。
- 评分(Score):在有序尺度上评估某事物。
- Noul:以概率方式回答是/否问题。
我们逐一来看。
选择:选项上的概率分布
选择是最直观的原语,也最接近传统分类。
假设我们问:
这个客户请求应由哪个部门处理?
可用选项为 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 得到概率:
这里,(K)是选项数量。
考虑这个示例分布:
| 选择 | 概率 |
|---|---|
| Billing | 0.998 |
| Technical support | 0.001 |
| Sales | 0.001 |
概率之和为一:
模型选择概率最大的选项:
在本例中,选择为 Billing,概率为 0.998,但其余概率也有用。它们告诉我们模型如何在备选项之间分配其信念。
评分:有序分布的期望
当存在若干不同备选项时,选择原语适用。但如果我们想在有序尺度上度量某事物呢?
考虑这个问题:
这个客户请求有多紧急?
我们定义三个等级:
- 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 |
评分原语不返回最可能的等级,而是计算等级索引的期望值。
对于表示等级的随机变量(X),期望为:
对于我们的示例:
因此报告的评分为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)表示二元结果:
其中
如果模型返回:
{"type": "noul", "noul": 0.92}
它将概率0.92分配给 Yes,将0.08分配给 No。
模型在这里并不是随机采样一个二元答案。它返回概率参数本身,由应用决定如何据此行动。
置信度:模型有多确定?
到目前为止,我们已经从模型获得了概率分布。但决策模型接口还会为 Choice 和 Score 返回一个名为confidence的字段。
完整分布可能包含多个可能的答案,总结模型信念的集中程度是很有用的。借助confidence,我们正好可以实现这一点。
对于具有 (K) 个选项的 Choice,置信度为:
其中 ($p_{\max}$) 是最大的预测概率。
这里的直觉是:
- 均匀分布的置信度为 0。($p_{\max} = \frac{1}{K}$)。这意味着模型估计所有选项的可能性相同。
- 完全集中于一个选项的分布置信度为 1。($p_{\max} = 1$)
- 获胜概率相对于均匀分布越大,置信度就越高。
在我们之前的三个选项示例中,Billing 的概率为 0.80:
请注意,获胜概率为 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 和模型类的整体结构,但专为决策模型量身定制。
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 工具训练决策模型
一旦我们理解了推理,下一个问题是我们能否适配决策模型。由于决策模型已集成到 transformers 中,我们可以使用 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 等模型进行微调。
最终思考
决策模型之所以有趣,不是因为分类是新的,而是因为它们使决策和概率成为软件的一流输出。
传统分类器已经产生概率,零样本分类器已经允许运行时定义类别。决策模型将这些想法带入一个类型化接口,具有多种问题原语、非生成式评分,以及为可靠、校准的决策进行专门训练的可能性。
