Build your own decision model
Creating Your Own Decision Model
By Nish Tahir — October 10, 2026
"System one" decision models are designed to infer and respond using calibrated probabilities across a set of permissible answers.
The Efficiency Gap: Standard LLMs vs. Decision Models
In a typical language model workflow, if you require a specific format (like JSON), you might employ Structured Output to ensure the result is syntactically valid. However, even with a prefilled prompt, the model must perform a separate forward pass for every single token it generates.
For instance, producing a simple JSON response might require passes.
Conversely, decision models (such as Jev) operate on the premise that there is a fixed set of options. This allows the model to arrive at an answer in a single pass.
How it works:
- The output space is restricted to specific tokens (e.g.,
A,B,C,D,E). - All other tokens in the vocabulary are
allowedmasked. - The token with the highest probability is selected as the final answer.
[!IMPORTANT] While this method guarantees the output will be one of the choices, it
guarantees correctnessdoes not guarantee the answer is actually right. Furthermore, raw token probabilities often reflect the model's confidence in the next token rather than the truth of the answer.
Implementation Guide
You can replicate this behavior by constraining the output tokens of an LLM. In this walkthrough, I used Qwen/Qwen3-1.7B.
🛠️ Development Checklist
- Load Pre-trained Model and Tokenizer
- Define valid option tokens
- Format prompt for multiple-choice
- Apply softmax to constrained logits
The Code
import argparse
import json
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
model_name = "Qwen/Qwen3-1.7B"
options = ["A", "B", "C", "D", "E"]
parser = argparse.ArgumentParser()
parser.add_argument("--input", default="question.json")
args = parser.parse_args()
# Initialize model and tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(
model_name, torch_dtype="auto", device_map="auto"
)
# Map options to their respective token IDs
option_token_ids = [tokenizer.encode(opt, add_special_tokens=False)[0] for opt in options]
def format_prompt(item):
prompt = item["question"] + "\n"
for opt in options:
prompt += f"{opt}. {item[opt]}\n"
prompt += "Answer:"
messages = [{"role": "user", "content": prompt}]
return tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True, enable_thinking=False
)
with open(args.input) as f:
item = json.load(f)
model_inputs = tokenizer(format_prompt(item), return_tensors="pt").to(model.device)
with torch.no_grad():
logits = model(**model_inputs).logits[0, -1]
# Constrained decoding: calculate softmax only for the allowed option tokens
probs = torch.softmax(logits[option_token_ids].float(), dim=-1)
print(f"prediction: {options[probs.argmax().item()]}")
for opt, prob in zip(options, probs.tolist()):
print(f"{opt}: {prob:.4f} {item[opt]}")
Example Execution:
If we provide the following input:
{ "question": "What color is the sky?", "A": "Red", "B": "Blue", "C": "Green", "D": "Purple", "E": "I don't know" }
The output is:
- Prediction: B
- A: 0.0000
- B: 0.9988
- C: 0.0000
- D: 0.0000
- E: 0.0012
Performance Evaluation
To validate the approach, I tested the model against a random holdout sample from the CommonsenseQA dataset.
Baseline Results (1.7B Model):
| Option | Precision | Recall | F1 Score | Support |
|---|---|---|---|---|
| A | 0.5733 | 0.7197 | 0.6382 | 239 |
| B | 0.5506 | 0.7686 | 0.6416 | 255 |
| C | 0.5372 | 0.6598 | 0.5922 | 241 |
| D | 0.7206 | 0.3904 | 0.5065 | 251 |
| E | 0.7519 | 0.4255 | 0.5435 | 235 |
| Total | Acc: 0.5938 | Macro F1: 0.5844 |
After performing a quick finetune on the dataset, the performance improved:
| Option | Precision | Recall | F1 Score | Support |
|---|---|---|---|---|
| A | 0.6475 | 0.6611 | 0.6542 | 239 |
| B | 0.6113 | 0.6784 | 0.6431 | 255 |
| C | 0.6234 | 0.5975 | 0.6102 | 241 |
| D | 0.6700 | 0.5418 | 0.5991 | 251 |
| E | 0.5808 | 0.6426 | 0.6101 | 235 |
| Total | Acc: 0.6241 | Macro F1: 0.6234 |
The Calibration Challenge
A significant issue arises when the model faces ambiguity. Consider this prompt:
Input: "Where would you most likely find a bat?" Options: A: Cave, B: Baseball game, C: Attic, D: Zoo, E: Sporting goods store. Result: Prediction A (Confidence: )
In reality, there is no single correct answer here, yet the model is extremely overconfident. By binning the confidence scores from the previous evaluation, we can see a clear mismatch between confidence and actual accuracy:
| Confidence Bin | Count | Avg Confidence | Actual Accuracy |
|---|---|---|---|
| (0.00, 0.10] | 0 | 0.0000 | 0.0000 |
| (0.10, 0.20] | 0 | 0.0000 | 0.0000 |
| (0.20, 0.30] | 3 | 0.2834 | 0.0000 |
| (0.30, 0.40] | 26 | 0.3761 | 0.2692 |
| (0.40, 0.50] | 41 | 0.4538 | 0.2683 |
| (0.50, 0.60] | 70 | 0.5490 | 0.3286 |
| (0.60, 0.70] | 74 | 0.6476 | 0.3649 |
| (0.70, 0.80] | 77 | 0.7495 | 0.4286 |
| (0.80, 0.90] | 121 | 0.8555 | 0.4711 |
| (0.90, 1.00] | 809 | 0.9855 | 0.7009 |
Observations:
- In the confidence range, the model is only correct of the time.
- In the range, accuracy drops to .
To fix this, we can use temperature scaling. By adjusting the temperature, we can flatten the probability distribution, scaling the output so that the confidence scores more accurately reflect the actual probability of correctness.