← writing
Writing

Making an AI agent reliable

I built a small multi-agent app that recommends flights. Two agents: one searches, one advises over the results. Simple on paper. But three separate times, the same class of bug stopped it from working, and every time the fix turned out to be the same idea.

The agent had to hand exact values to a real API. A date. A booking token. A URL. The model mangled all three.

The model is bad at copying things

The first one was a date. My tool wanted YYYY-MM-DD. The model passed 2027-3-15. The API rejected it with a 400. The model had taken a correctly formatted date from the task description and quietly dropped the zero padding on its way into the tool call.

The fix isn’t a better prompt. You can ask the model to “use strict YYYY-MM-DD” all day and it will still slip. The fix is to stop trusting it with the format:

def _normalize_date(value: str) -> str:
    y, m, d = value.strip().split("-")
    return f"{int(y):04d}-{int(m):02d}-{int(d):02d}"

Run every date through that before it reaches the API. The model decides which date to search. The code guarantees the format.

Constraints belong in the tool, not the prompt

The next problem was filtering. I wanted “no flights with more than one stop.” Putting that in the prompt works most of the time, which is another way of saying it fails some of the time, and a recommendation engine that occasionally recommends a flight you told it to exclude is worse than useless.

So the constraint moved into the tool. The search tool itself drops anything over the limit before the model ever sees it:

def _within_limits(flight, max_stops):
    return max_stops is None or flight["stops"] <= max_stops

Now the model physically cannot return a flight that breaks the rule, because the rule is enforced before retrieval, not after reasoning. The prompt is for judgment. The code is for rules.

Keep opaque values out of the model entirely

The last one was the booking link. The flight API returns a booking token that’s a 200-plus character opaque blob. The model needs to reference a specific flight, but if you ask it to carry that token around and pass it back, it will corrupt it, the same way it corrupted the date, just less visibly.

So the model never touches the token. Each flight gets a short id (1, 2, 3). The model works with the id. A lookup table maps the id back to the real token in code, and the booking link gets generated outside the model’s output:

flights_by_id = {str(i): f for i, f in enumerate(flights, start=1)}
# the model says "book flight 2"; the code resolves the token.

The model is good at picking. It’s bad at transcribing. Don’t make it transcribe.

The principle

All three bugs are the same bug. Anything that has to be exact, a date, a token, a URL, an id, should be produced and validated by code, not held in the model’s hands. The model is a reasoning engine, not a clipboard.

This is the boring, unglamorous part of building with LLMs, and it’s most of the work. A demo gets the model to respond. A product makes the system around the model not break.