← Back to writing

A rule that throws the reply away

What happens in the seconds between the model finishing and the customer receiving: guards, a retry inside the same turn, and what no guard catches.

chattybotsaiagentsengineering

In the previous post I said the prompt is the last rung for fixing a problem, and that data, tools, and guards come before it. This post is about the third rung: what happens in the seconds between the model finishing its text and the customer receiving something.

The short answer is that the model sends nothing. It writes a draft, and the draft goes through a row of rules that can throw it away.

What a guard is

A guard is an ordinary function. It takes the draft, the outputs of the tools called in that turn, and the chat history, and it returns one of two things: pass, or a reason it does not. There are twenty-eight reasons today. The ones that fire most:

  • Amount with no backing. The draft quotes a figure that appears in no tool output. An invented price, an installment worked out in its head, a conversion by eye.
  • Stock that contradicts the search. The draft claims a phone the search did not return, or denies one it did.
  • Quote without asking. The draft quotes a trade-in without having asked for model, capacity, and battery.
  • Sold out offered as available. A product marked sold out, offered as if it were in stock.
  • Warranty with no data. A warranty claimed for a phone whose record does not carry one.
  • Vocabulary leak. The draft talks to the customer about “the tool”, “the system”, or “the catalog”. Inside words that must not get out.

None of these guards understands language. They compare text against data. That is why they are cheap and why they are reliable: if the amount is not in any tool output, it is not there, and there is no interpretation to argue about.

The retry

When a guard fails, the draft does not reach the customer. But the chat is not handed to a person right away either. First the model gets a second chance.

A rejected draft, inside the same turn
Draft "The 15 Pro is $1,200,000."
Guard: amount with no backing That figure is in no tool output.
Corrective note "You quoted an amount you did not look up. Use only figures from the tools."
Second attempt Same run, same trace. The model writes again.
Passes The reply goes out. The customer never saw the first draft.
Fails again Discarded. The chat goes to a human agent with a note of what happened.
One correction attempt. If the model repeats the mistake, the decision is no longer its own.

The detail that took me longest to get right is where the retry happens. It is not a second turn, and not a second call from outside. It happens inside the same agent run, as one more step before returning. That leaves a single trace, a single memory write, and a single message for the customer. If the retry were a new turn, the customer would see two replies, or memory would store the bad draft as if it had been said.

When the second attempt fails too, the reply is discarded and the chat goes to a person with a note saying which guard failed and why. What the customer gets is a short, honest line: I had trouble putting this answer together, let me pass you to an agent. It is rare: in a typical week it happens on about one turn in a hundred. But those are exactly the turns where the model was about to say something false.

A guard gets it wrong too

This is the part I wish I had read before writing the first one. A guard that is too strict is worse than no guard, because it throws away good replies and turns them into handoffs.

It happened to me with the amounts guard. It started out simple: if the draft has a figure and no tool returned it, reject. It sounded right. In production it rejected things like these:

  • The customer writes “I have 500k, what can I get?” and the bot answers “with 500k you can get…”. The 500k did not come from a tool, it came from the customer.
  • The bot had quoted a phone two messages earlier and now repeats it to compare. The figure was in the chat, not in this turn’s tools.
  • The customer says “nine hundred” and the bot writes “900”. Same number, different shape.

In one month, almost nine out of ten fallback handoffs were the amounts guard rejecting correct replies. The bot was not inventing anything; the rule could not read.

What came out of that is a practice, not a patch. Every new guard is calibrated against about six thousand real production turns before merging, measuring how many good replies it would reject. Every false positive that shows up becomes a test with that exact case. The last three guards I wrote changed design because of that measurement, before touching a single customer.

The guard that took the process down

A guard is production code, and like all production code it can blow up. The amounts guard had an arithmetic detector: if the draft said 1,500,000 and the tools had returned 1,000,000 and 500,000, it tried combinations to see whether the figure was a legitimate sum. It tried every combination. With three amounts that is nothing. With a photo of a receipt full of numbers, described by vision and turned into text, it is exponential. Twice it ran out of memory and took the process down.

Today the detector caps how many figures it looks at, and there is a time budget per turn. A guard that takes longer than the model is not protecting anything.

What no guard catches

Guards compare against data. Where there is no data, there is no guard. A reconstructed example from a real chat:

Customer: I came by this morning and paid. I need to come pick it up.

Bot: If you already paid, I need the sale number or the receipt to help you with the pickup. Do you have it handy?

It sounds reasonable. The problem is that the bot has no tool to look up a sale number: the store has no sales system the bot can query. It is asking for a piece of data it will not be able to use. The right move was to hand off to a person without asking anything.

No guard detects it, because there is no tool output to compare against. The bot quoted no false figure and claimed no missing stock. It promised, implicitly, something it cannot deliver. That kind of mistake is not solved on rung 3: it enters the improvement loop as a new case, with its test, and ends up as a rule in the handoff tool or, as a last resort, a line in the prompt.

Guards are the safety net for what can be verified. For everything else, someone still has to read the conversations.