DEV Community

李超杰
李超杰

Posted on

I Let AI Run a Restaurant for 3 Months. Here's the Architecture That Kept It Safe.

I Let AI Run a Restaurant for 3 Months. Here's the Architecture That Kept It Safe.

Three months ago, I started building an AI restaurant ordering system. Customers talk to it in English, French, or Chinese. The AI takes their order through natural conversation — like a real waiter.

I thought the hard part would be making the AI understand what customers want.

I was wrong. The hard part was stopping the AI from doing things it shouldn't.

The moment I realized I had a problem

Week one. A customer types: "I want the lobster roll."

The AI searches the menu. The lobster roll is sold out. But the AI calls add_to_cart("lobster-roll") anyway.

Why? Because LLMs don't "know" things. They predict the next token. And the most likely next token after a search result is... acting on it.

I fixed the prompt. "Never add a sold-out item." It worked. For a day.

Then the AI started hallucinating item IDs. "smash-burger" became "smash-burger-with-extra-pickles". The tool call failed, the AI got confused, the customer got nothing.

Then the AI added a negative quantity. -1 burger. Because the customer said "I don't want a burger" and the AI reasoned: remove = add with negative quantity.

Three failures. Three different root causes. All from trusting the AI too much.

The rule that changed everything

I wrote one sentence on a sticky note and put it on my screen:

AI understands intent. Code executes. Never let AI directly control state.

Every tool the AI can call now has two layers:

{
  name: "add_to_cart",
  description: "Add an item to the customer's order",
  inputSchema: { /* ... */ },

  // Layer 1: VALIDATE — runs before execution, EVERY time
  validate: async (args, ctx) => {
    const item = ctx.menu.find(i => i.id === args.item_id);
    if (!item) throw new ValidationError("Item not found");
    if (!item.available) throw new ValidationError("Sold out");
    if (!item.price) throw new ValidationError("Price not set");
  },

  // Layer 2: EXECUTE — only runs if validate passes
  execute: async (args, ctx) => {
    ctx.cart.push({ id: args.item_id, quantity: args.quantity });
    return "Added to cart";
  },
}
Enter fullscreen mode Exit fullscreen mode

The AI decides what tool to call and with what parameters. But it cannot bypass validate(). Ever. By design.

What this catches in production

After three months, here's what the validation layer has blocked:

What the AI tried Why it happened Caught by
Add lobster roll (sold out) Ignored the [SOLD OUT] tag in search results validate: !item.available
Add "smash-burger-deluxe" (not real) Hallucinated a variant that doesn't exist validate: !item
Add -1 burger Interpreted "don't want" as negative quantity validate: quantity < 1
Confirm order with no items Got confused mid-conversation validate: cart.length === 0

Not once did bad data reach the cart. The AI made mistakes — as all LLMs do. But it never corrupted state.

The architecture

🧑 Customer message: "I want the lobster roll and a salad"
        ↓
🤖 AI: [searches menu, decides: add_to_cart("lobster-roll"), add_to_cart("quinoa-salad")]
        ↓
🛡️ VALIDATION WALL
    ├── lobster-roll → ❌ REJECTED ("Sold out today")
    └── quinoa-salad → ✅ PASSED
        ↓
⚡ EXECUTION
    └── quinoa-salad added to cart
        ↓
🤖 AI: "I'm sorry, the lobster roll is sold out. I added the quinoa salad.
     Can I suggest something similar — perhaps the shrimp avocado bowl?"
Enter fullscreen mode Exit fullscreen mode

The key insight: the validation error goes BACK to the AI. The AI reads it and recovers. It apologizes. It suggests alternatives. It behaves like a real waiter who checked with the kitchen and came back with bad news.

This is better than silently swallowing the error. The AI learns from the feedback in real time.

Why middleware is the wrong approach

Most AI agent frameworks use middleware: a function that runs before every tool call.

// The middleware pattern — what everyone else does
function middleware(toolName, args) {
  if (toolName === "add_to_cart") {
    // ... some check
  }
}
Enter fullscreen mode Exit fullscreen mode

This doesn't scale. Middleware knows WHAT tool was called. It doesn't know WHY — what business condition this specific tool needs. After 5 tools, your middleware is a 200-line switch statement.

Per-tool validate() scales infinitely. Every tool carries its own safety rules. Add a new tool, add its validation. Remove a tool, its validation leaves with it. No central file to maintain.

What I built: Agent Kit

I extracted this pattern into a small, open-source TypeScript framework:

github.com/ChoukeLee/agent-kit
Enter fullscreen mode Exit fullscreen mode

It's 4 files. No dependencies except the AI provider you choose. Works with Claude, OpenAI, DeepSeek — anything that supports tool calling.

The demo is a restaurant ordering bot. You can run it in 5 minutes:

npm install
export ANTHROPIC_API_KEY="your-key"
npx tsx smoke-test.ts
Enter fullscreen mode Exit fullscreen mode

What I learned

Building AI products isn't about making the AI smarter. It's about acknowledging that the AI will make mistakes — and designing your system so those mistakes can't hurt anything.

The validation wall isn't just a safety net. It's a design philosophy:

  • AI does what it's good at: understanding messy human language
  • Code does what it's good at: enforcing rules with 100% reliability
  • They communicate through a defined interface: tool calls return results. Validation errors return corrections.

That sticky note is still on my screen. If you're building anything with AI tool calling, write it on yours too.


I'm building Agent Kit — a lightweight framework for safe AI tool-calling. Also building Maison Gourmande, an AI restaurant concierge. Based in Abidjan.

Top comments (0)