SynapCores

One Support Ticket, Four AI Models: LLM, NLI, AutoML and a Decision Model in One Database

By , Founder & CTO, SynapCores · Published

One support ticket, four AI models

Most AI support pipelines have one model in them: a large language model, prompted to do everything. It reads the ticket, guesses the category, decides if it's urgent, writes the reply and, if anyone thought about it, grades its own reply.

It works in the demo. In production you end up parsing free text for a category name, retrying when the format drifts, paying for tokens on every label, and trusting the model that wrote the reply to tell you whether the reply is correct.

There's a better split. Four kinds of model, each doing the one job it's built for:

  • A decision model judges the ticket: which team, what tone, how urgent.
  • A trained ML model predicts what your history says will happen: will this escalate?
  • An LLM writes the reply.
  • An NLI model checks the reply against your policy before it goes out.

This post runs one ticket through all four, in one database. If you want the "when do I pick which" guide first, read LLM, NLI, ML or a Decision Model: Which One Should You Use?. This post is the worked example.

The ticket

"I was charged twice for order 4471 and I want my money back today." — Dana, customer for 3 months, 4 tickets in the last 90 days, order value $820, waiting 30 hours.

The refund policy has two lines that matter:

  1. If a customer is charged twice for the same order, the duplicate charge is refunded.
  2. Refunds go back to the card that was charged.

Step 1: Triage with a decision model (d1)

The first question is a judgment, not a writing task: where does this go, how upset is the customer, how fast do we need to move?

That's what d1, Liquid AI's decision model, is for. You send it the ticket as the state and ask typed questions. It answers each one with a probability per option, in a single forward pass, without generating any text. SynapCores serves it at /v1/systemone:

curl -s http://localhost:8080/v1/systemone \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "model": "d1:latest",
    "state": "Customer message: I was charged twice for order 4471 and I want my money back today.",
    "questions": {
      "route":   { "type": "choice", "instructions": "Which team should handle this?",
                   "criteria": { "billing": "payments, charges and refunds",
                                 "shipping": "delivery and tracking",
                                 "technical": "bugs and login problems" } },
      "angry":   { "type": "noul",  "instructions": "Is the customer angry?" },
      "urgency": { "type": "score", "instructions": "How urgent is this?",
                   "criteria": ["can wait", "this week", "today", "right now"] }
    }
  }'
"route":   { "choice": "billing", "probabilities": { "billing": 0.90, "shipping": 0.03, "technical": 0.07 } },
"angry":   { "noul": 0.82 },
"urgency": { "score": 2.28 }

Billing, at 0.90. Probably angry. Urgency 2.28 on a 0–3 scale, between "today" and "right now". And output_tokens: 0: nothing was generated, so there's nothing to parse and no format to drift.

The probabilities are the point. Route automatically when confidence is high, and send the borderline tickets to a person. Add a new team tomorrow by adding one line to criteria. There's no model to retrain.

Step 2: Predict escalation with AutoML

d1 judged the message. But some of the strongest signals about this ticket aren't in the message at all. They're in the account: a 3-month-old customer, four tickets in 90 days, a large order, a 30-hour wait.

That's a job for a model trained on your own history. We have 600 past tickets with an escalated column, so we train a classifier in SQL:

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 holdout)

And score the open queue:

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 escalation_risk
4471 Dana 0.986
4472 Sam 0.019

Dana's ticket is near-certain to escalate if it's handled like a normal one. No pre-trained model could know that, because it comes from your customers' history. Training took under a second, and the prediction is a function call inside the query that reads the queue.

Step 3: Draft the reply with an LLM

Now there is a writing job, and that's where the LLM belongs. The draft is generated in SQL, 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;

In a full pipeline you'd put the route, the urgency and the escalation risk into the prompt too, so a high-risk ticket gets a more careful reply and an offer to call. A fixed seed and low temperature keep the draft repeatable.

Step 4: Check the reply with NLI before it goes out

An LLM writes fluent text. It doesn't guarantee that every sentence matches your policy. One reply might promise store credit; another might say the duplicate charge isn't refundable. Both sound plausible, and both are wrong.

An NLI model checks each sentence against the policy lines and returns TRUTH, LIE or UNVERIFIED_PLAUSIBLE, plus the exact policy line involved:

SELECT NLI_VERIFY('nli-default',
         'Your refund will be paid as store credit.',
         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;
Sentence Verdict Confidence
We will refund the duplicate charge. TRUTH 0.959
Your refund will go back to the card you paid with. TRUTH 0.992
Your refund will be paid as store credit. LIE (breaks "Refunds go back to the card that was charged.") 0.902
You are not eligible for a refund of the duplicate charge. LIE (breaks "…the duplicate charge is refunded.") 0.999

A reply with a LIE goes back for a rewrite or to a person. A reply that's all TRUTH goes out. The NLI model is small, runs on a CPU and returns the same scores every time, so you can run it on every sentence of every reply.

What changed by splitting the work

Job One-LLM pipeline Four-model pipeline
Route and rate the ticket Parse a label out of generated text Probabilities per option, zero output tokens (d1)
Predict escalation The LLM guesses from the message A model trained on your history scores the account (AutoML)
Write the reply LLM LLM
Check the reply The same LLM grades itself An independent NLI check against the policy, sentence by sentence

The LLM still does the one thing only an LLM can do: write. Everything around it now has a score you can threshold, log and audit.

Why one database

Each of these models can run as its own service. The difference in one engine is where the data lives. The ticket, the account history, the policy, the trained model, the decision scores and the NLI verdicts are all rows in the same database. Triage, prediction, drafting and checking are a few queries over those rows, not four services and the glue code between them. When you want to know why a ticket was routed to billing, or why a reply was held back, the answer is a SELECT.

Try it