The legal chatbot that thought it was a record shop

9/13/20266 min read
The legal chatbot that thought it was a record shop

Direct answer: an anti-hallucination guardrail is domain-specific code, not a universal safety measure. Applied to the wrong domain it produces a bug that looks like a prompt problem but is a routing problem: in my case, a rule meant to stop an online shop inventing prices made a legal assistant refuse a legitimate question about a public project, in the voice of a shop clerk. The lesson: every guardrail must be scoped to the vertical it was written for, always.

TL;DR

  • The multi-tenant engine was built for e-commerce. One of its guardrails intercepts words like «cost», «price», «availability», «edition» and injects the instruction «FIRST call the tool that checks stock».
  • That rule was hard-wired for all tenants, not just the ones that sell.
  • On a legal tenant, the stock-checking tool does not exist.
  • The model received an instruction it could not execute and handed it back to the user as a refusal, word for word.
  • The symptom (a commercial refusal to a legal question) pointed the wrong way: it looked like the prompt, it was the code.
  • Structural fix: guardrails activate per vertical, and any guardrail naming a tool must verify that the tool exists for that tenant.

What happened

The context: a multi-tenant conversational engine built for e-commerce — record shops, hi-fi, collectibles — into which I migrated something entirely different: an assistant answering questions about laws, public projects and parliamentary data.

At some point, asked about an €864,000 PNRR project, the legal assistant answered roughly this:

«I cannot provide information on prices, availability or editions without checking stock.»

To a question about a public resolution. No stock anywhere in sight.

Why it looked like a prompt problem (and was not)

Anyone's first hypothesis, faced with an answer like that, is: the model hallucinated the wrong persona. The tone is a shop clerk's, the vocabulary is retail, the conclusion is a refusal — it looks like the classic contaminated system prompt.

It was not. The model was executing to the letter an instruction the engine had injected dynamically, a moment earlier, without anyone having written it into the persona's prompt.

The engine had an anti-hallucination guardrail that is perfectly sensible in its original context: a regex catching the words in a user message that signal a commercial question — cost, price, availability, edition — and, when it finds them, appending a peremptory instruction to the context: before answering, call the tool that checks real availability.

That is exactly what a record shop needs, where a model inventing the price of a first pressing is concrete commercial damage.

The problem is that the phrase «an €864,000 project» contains the concept of cost. The regex did its job. It injected the instruction. And the tool that instruction ordered it to call does not exist for a legal tenant.

The model found itself with an impossible order. It did the most predictable thing: it told the user why it could not proceed, using the words available to it — the instruction's own.

The design error, in one line

The guardrail was right. It was in the wrong place.

Layer Where it sat Where it should sit
Commercial-intent detection Global, all tenants Only tenants with a catalogue
«Check availability» instruction Global, all tenants Only tenants that have the tool
Tool-existence check Absent Precondition of the injection

Three lines, and the third was missing entirely: nobody checked that the tool named by the instruction actually existed for that tenant. The guardrail assumed a world — «if you are here, you sell something» — that was true when it was written and stopped being true the moment the engine hosted a different product.

The lesson: guardrails are domain code

This is the part that outlives the specific case.

We tend to think of guardrails as a general safety layer, like input validation or rate limiting: something you put in once and it applies to everyone. They are not. A guardrail encodes an assumption about what is true in the domain: «things are sold here», «an inventory exists here», «a wrong price answer is damage here».

Those assumptions are precisely what changes when an engine goes multi-tenant and hosts different verticals. And they change silently, because nobody re-reads the guardrails when adding a new customer.

Three practical rules I took from it:

  • Every guardrail declares its domain. If you cannot say which vertical it was written for, you do not know where it stops being valid.
  • An instruction naming a tool must verify that the tool exists in that context. Otherwise you are asking the model to do the impossible, and the model will tell the user on your behalf.
  • The symptom lies systematically. A natural-language refusal always looks like a prompt problem, because the prompt is the only thing made of language. Almost always it is code routing.

This is also why, when I assess a conversational engine for reuse on a new domain, my first question is not «which models does it support» but «which domain assumptions are hard-wired inside». Almost no vendor expects that question, and the answers are instructive.

FAQ

What is an anti-hallucination guardrail in a chatbot?

It is a rule that intercepts certain requests — typically those where an invented answer would cause damage, such as prices or availability — and forces the model to verify the fact with a real tool before answering, rather than generating it from training.

Why can a guardrail turn into a bug?

Because it encodes a domain assumption. When the same engine hosts customers from different sectors, the assumption stops being true for some of them: the rule still fires, but the action it imposes no longer makes sense — or is not even executable.

How do you tell the problem is in the code and not the prompt?

By checking whether the instruction the model is following actually appears in the configured prompt. If the refusal text resembles a system instruction nobody wrote into the configuration, something is injecting it dynamically: look there.

Does this only apply to multi-tenant engines?

That is where it explodes, but the principle always holds: every guardrail has a domain of validity. Even in a single product, when you add a feature outside the original perimeter, the rules written for the old perimeter follow you.


If you are considering reusing a conversational engine for a domain other than the one it was built for, let's talk: hard-wired assumptions are the hidden cost of that choice, and it is better to find them first. See also what I learned putting MCP into production.

AIChatbotBehind the scenes

Scritto da Giulio Garofalo