The situation: a customer writes in: "I was charged twice for order 4471 and I want my money back today." Four different things need to happen. Someone has to decide where the ticket goes and how urgent it is. Someone has to guess whether it will escalate. Someone has to write the reply. And someone has to make sure the reply doesn't promise something the refund policy doesn't allow.
The default today is to prompt an LLM for all four. It will produce something for each. But three of the four jobs aren't writing jobs, and a smaller, purpose-built model does them better, faster and more consistently.
We build SynapCores. The guidance on this page applies to any stack. The SQL ran on SynapCores 2.4.7-ce, and the decision-model call uses the
/v1/systemoneendpoint in 2.5.0-ce.
The one question that picks the model
Ask what shape the output has, and what the model has to learn from.
| You need… | Learns from | Use | Example output |
|---|---|---|---|
| Free text: a reply, a summary, an explanation | Its pre-training plus your prompt | LLM | "Sorry about the double charge on order 4471…" |
| A verdict on whether text A supports text B | Pre-trained on sentence pairs | NLI | entailment 0.96 / contradiction 0.99 |
| A number or category from structured columns | Your labelled history | Trained ML model | escalation_risk 0.99 |
| A probability over answers you describe in words, about a piece of text | Pre-trained; your question defines the answers | Decision model (d1) | billing 0.90, shipping 0.03, technical 0.07 |
If you remember one thing: LLMs write, NLI checks, ML learns your history, decision models judge.
What do you need back?
│
┌──────────────┬──────────┴─────────┬──────────────────────┐
free text support/contradict number or label probability over
│ between two texts from columns answers you name
│ │ │ about one text
▼ ▼ ▼ ▼
LLM NLI Have labelled outcomes? Decision model
yes → train ML (no labels needed)
no → decision model,
collect labels,
train ML later
1. LLM: when the output is language
An LLM generates text one token at a time. That makes it the right tool whenever the answer is open-ended: a customer reply, a summary of a long thread, an explanation of a query result, a draft that a person will edit.
Pick an LLM when nobody can list the possible answers in advance.
Don't pick it when the answer is a label, a score or a yes/no. You pay for every generated token, you have to parse the text back into a value, and you get one sampled answer instead of a probability for each option.
In SynapCores the LLM is a SQL function, so the draft is written next to the ticket:
SELECT GENERATE(
'Write a two-sentence, friendly reply to this customer message. '
|| 'Message: I was charged twice for order 4471 and I want my money back today.',
json_object('max_tokens', 120, 'temperature', 0.2, 'seed', 42)
) AS reply;
I'm sorry to hear that you were charged twice for your recent order, Order #4471.
Our team is looking into it right away and will make the necessary adjustments
to ensure you are not overcharged. Thank you for bringing this to our attention
so we can resolve it quickly!
A low temperature and a fixed seed make the draft repeatable. The reply sounds fine, but nothing in it has been checked against the policy yet. That's the next model's job.
2. NLI: when you need to know if one text backs up another
An NLI (natural language inference) model reads a premise and a claim and returns three scores that add up to 1: entailment (the premise supports the claim), contradiction (it says the opposite) and neutral (it doesn't settle it). It's small, it runs on a CPU, and it returns the same scores every time.
Pick NLI when you have a source of truth and a piece of text that must agree with it: an AI answer against your policy, a new memory against old ones, a call transcript against a script.
Don't pick it when the question isn't about two texts. NLI doesn't route tickets or rate urgency, and it only knows what's in the premise you give it.
Here the refund policy is the source of truth, and each sentence an agent (human or AI) might send is checked against it with NLI_VERIFY:
SELECT NLI_VERIFY('nli-default',
'You are not eligible for a refund of the duplicate charge.',
JSON_ARRAY(
'If a customer is charged twice for the same order, the duplicate charge is refunded.',
'Refunds go back to the card that was charged.')
) AS check_result;
The same check, run on four candidate sentences:
| Sentence in the reply | Verdict | Confidence | Policy line it hit |
|---|---|---|---|
| We will refund the duplicate charge. | TRUTH |
0.959 | …the duplicate charge is refunded. |
| Your refund will go back to the card you paid with. | TRUTH |
0.992 | Refunds go back to the card that was charged. |
| Your refund will be paid as store credit. | LIE |
0.902 | Refunds go back to the card that was charged. |
| You are not eligible for a refund of the duplicate charge. | LIE |
0.999 | …the duplicate charge is refunded. |
Every LIE names the exact policy line it breaks, so the reply can be fixed or sent to a person before the customer sees it. For the full method (similarity floor, numbers and dates, sentence-by-sentence checks), see How to check if an AI answer is actually backed by your documents.
3. Trained ML: when your history knows the answer
A trained model (logistic regression, gradient boosting, a random forest) learns from rows where you already know the outcome. It turns structured columns into a number or a category: churn risk, fraud score, delivery time, escalation risk.
Pick ML when you have labelled outcomes and the signal is in your columns. Nothing beats it on cost and speed per row, and it learns patterns that are specific to your business, which no pre-trained model can know.
Don't pick it when you have no labels yet, or the input is mostly free text. You also need to retrain and monitor it as your data changes.
Say we have 600 past tickets with an escalated column. Training is one statement:
CREATE EXPERIMENT escalation_model AS
SELECT tenure_months, prior_tickets_90d, order_value, hours_waiting,
escalated AS target
FROM ticket_history
WITH (
task_type = 'binary_classification', target_column = 'target',
algorithms = ['logistic_regression', 'xgboost'], max_trials = 6,
validation_strategy = 'holdout', validation_split = 0.2,
optimization_metric = 'roc_auc', random_seed = 42
);
-- best_score 0.873 (ROC AUC on the 20% holdout), trained in under a second
Scoring the open tickets is a function call in a normal query:
SELECT id, customer, tenure_months, prior_tickets_90d, order_value, hours_waiting,
AUTOML.PREDICT('escalation_model',
tenure_months, prior_tickets_90d, order_value, hours_waiting) AS escalation_risk
FROM open_tickets
ORDER BY escalation_risk DESC;
| id | customer | tenure_months | prior_tickets_90d | order_value | hours_waiting | escalation_risk |
|---|---|---|---|---|---|---|
| 4471 | Dana | 3 | 4 | 820.0 | 30 | 0.986 |
| 4472 | Sam | 48 | 0 | 25.0 | 2 | 0.019 |
Dana is a new customer with four recent tickets, a large order and a 30-hour wait. The model has seen that pattern escalate before. Notice that it never read Dana's message: the history in the columns was enough.