DEV Community

Dilip V P
Dilip V P

Posted on

Deploying AI Models Explained: Versions, Canaries and Drift

A team deploys a new version of the AI model behind its support agent, the one that handles refunds. The new version passes every test. On five hundred old conversations it gets the right answer ninety one times in a hundred, and the old version gets eighty seven. So on Tuesday, every conversation goes to the new version.

Everything looks fine. Latency is 410 ms, errors are zero, every health check is green and every trace is complete.

A week later, the team reviews the refunds the agent approved. One in every eight was wrong. The month before, it was one in forty. The agent had been confidently approving refunds it should have refused, and nothing had ever errored.

This article follows that week back to its causes, and ends with the three habits that would have caught it. The video tells the same story in six minutes. This is the written version.

Which model answered this?

The first question was simple. Which model gave these answers? Nobody could say.

Every log line said the model was latest:

{"time":"19:41:02","order_id":"4473","decision":"refund approved","model":"latest"}
Enter fullscreen mode Exit fullscreen mode

But latest is not a version. It just means whichever version is newest today. The team's config asked for latest, so on Tuesday it got the new version, and on the day the vendor moves the name again, it will get another one without anyone deciding it. The vendor named the exact version in every reply, support-v2-0917, and nobody had logged it.

A model is a file of numbers. Nobody wrote those numbers, so you cannot read them to find out what changed. The only way to tell one model from another is its version. That gives two rules:

agent:
  model: support-v2-0917   # pin the exact version, never a moving name like "latest"
Enter fullscreen mode Exit fullscreen mode
{"time":"19:41:02","order_id":"4473","decision":"refund approved",
 "model_requested":"support-v2-0917","model_answered":"support-v2-0917"}
Enter fullscreen mode Exit fullscreen mode

Pin the exact version in your config. And in every log line, write down the version that actually answered, taken from the response, not the one you asked for. Many model APIs accept a moving name and tell you in each response which exact version answered, so the field is there for you to log.

The tests only proved the past

The tests passed, so how could the model be wrong?

Because those five hundred conversations were from last month. The model was graded on the past.

Code is tested against rules, and rules don't change. If a function returned the right total last month, it returns the right total today. A model is tested against real conversations, and those keep changing. A score on last month's conversations tells you how the model would have done last month.

So a passing score doesn't mean you can send the model to everyone. It means the model is ready to meet real customers, a few at a time.

Shadow: run the new model beside the old one

Here's what careful teams do first. Run the new model beside the old one. The customer still gets the old model's answer. The new model's answer goes only into a log.

Answer answer = oldModel.answer(conversation);             // the customer gets this one
shadowPool.submit(() ->                                     // the new model answers too, off the request path
    shadowLog.write(conversation.id(), answer, newModel.answer(conversation)));
return answer;
Enter fullscreen mode Exit fullscreen mode

After a day you have thousands of answers from both, and you read the ones where they disagree. Most pairs will match. The ones that differ, the old model refusing where the new one approves, are exactly the refunds the review found a week too late.

Shadow runs cost a second model call while they run, so most teams run them for a day or two, or on a sample of the traffic.

Canary: five conversations in a hundred

Then the new model answers for real, but only for five conversations in a hundred. That's a canary.

// The same conversation always lands on the same side, so a customer never switches model in the middle of a chat.
boolean useNew = Math.floorMod(conversationId.hashCode(), 100) < canaryPercent;   // 5, then 25, 50, 100
String model = useNew ? config.newModel() : config.oldModel();
Enter fullscreen mode Exit fullscreen mode

You watch those five closely: how often the model approves a refund, and how often someone asks for a human. Compare them with the other ninety five over the same hours, so the time of day and the kind of customer match. If anything looks wrong, only five customers in a hundred saw it.

Widen it, or roll it back

If those five hold up, give the new model a quarter of the conversations, then half, then all of them.

And if it goes wrong, rolling back is one line. You point the config back at the previous version, or set the canary to zero. That only works if you know which version you were on, which is the first habit again.

A passing score earns a model a canary, not every customer.

Drift: it gets worse, and nothing errors

Suppose the team does all of that. The model can still go wrong slowly, and not one error will appear. That's drift.

The customers change. A festival sale brings new buyers who ask things last month's customers never asked. The model keeps answering just as confidently, because it only knows last month's customers. In the story, the share of refunds it approves sits at thirty percent for weeks, then creeps to forty five percent over seven days. Every health check stays green the whole time.

You can't catch drift with an error rate, because nothing errors.

Watch two numbers, then check the truth

There is a second problem: the truth arrives late. You only learn whether a refund was right a week later, at the review.

So you watch in two layers. Every hour, you watch two numbers: how often the model approves, and how often people ask for a human. They are not the truth, but they move first.

SELECT date_trunc('hour', created_at)                                     AS hour,
       model_answered,
       avg(CASE WHEN decision = 'refund approved' THEN 1.0 ELSE 0 END)    AS approve_rate,
       avg(CASE WHEN asked_for_human THEN 1.0 ELSE 0 END)                 AS human_rate
FROM agent_decisions
WHERE created_at > now() - interval '7 days'
GROUP BY 1, 2
ORDER BY 1, 2;
Enter fullscreen mode Exit fullscreen mode

And when the review comes, you check those numbers against the truth. If the approval rate climbed in the same week the wrong refunds climbed, the hourly number was the early warning, and next time you act on it sooner.

The law

A model passes its tests on the past. Pin its version, roll it out slowly, and keep checking it against the truth.

How real teams do it

None of this is new to teams that ship models every week.

  • Uber checks a new model against past production data before release, runs shadow tests in more than 75 percent of its critical online use cases, starts every rollout on a small slice of traffic, and rolls back automatically when failure signals appear: Raising the Bar on ML Model Deployment Safety (Uber Engineering, 2025).
  • DoorDash logs every prediction with the versioned name of the model that made it, and checks the statistics of those predictions every hour: Maintaining Machine Learning Model Accuracy Through Monitoring (DoorDash Engineering, 2021).

Both write about trained models, not only about large language models. The habits are the same.

Next time

We go inside the database and watch what happens to a row the moment you write it.

The support agent, the refunds, the scores and the version names are an invented example. What is the last thing you shipped that passed every test and still went wrong?

Top comments (0)