Sitemap

Build deterministic AI Agents

7 min readMar 21, 2026

--

Press enter or click to view image in full size

Gotcha.

You probably clicked this blog with one of two reactions in mind.

The first: “Great. I want to learn how to build deterministic AI agents.”
The second: “There is no such thing as deterministic AI when an LLM is involved.”

If you came with the first thought, good. We are going to talk about how to build systems that behave that way in practice. And if you came with the second thought, also good. You are not wrong.

A calculator is deterministic. A sorting algorithm is deterministic. A rules engine is deterministic.

An LLM-based agent is not.

If you ask the same model the same thing twice, you may get slightly different wording, a different chain of reasoning, or even a different decision. That is not a bug in the usual sense. It is the nature of the system.

So if by deterministic you mean “the model always produces the exact same output for the exact same input,” then no, AI agents are not deterministic.

And I agree with that.

But when engineers build software, they still want the system to be reliable, accurate, and predictable. AI agents should be held to that same standard.

With AI systems, the language can be fuzzy. The outcome cannot be.

So the real goal is not to make the model itself deterministic.

The real goal is to build a system where the model has very limited room to go wrong.

That means:
- business rules live in code
- The model is used for classification and routing
- actions are limited to a small allowed set
- Every critical path is validated and tested

In short:

You don’t make agents deterministic. You constrain them.

A Simple Example: A Refund Support Agent

Let’s use a beginner-friendly example.

Imagine you are building a support agent for an online store. A user sends a message like:

“My package arrived three days late. Can I get a refund?”

At first, it is tempting to build the whole thing inside one prompt:

Read the message, decide whether the customer deserves a refund, calculate the amount, and issue it.

That sounds smart, but it is a bad production design.

Why?

Because you are asking a probabilistic model to do the part that should be policy.

Refund decisions usually depend on rules like:
- was the delivery actually late?
- was the item refundable?
- is the request inside the refund window?
- has a refund already been issued?
- is this a full refund, partial refund, or no refund?

Those are not prompt problems.

Those are business rules.

The Wrong Way

Press enter or click to view image in full size

In the wrong design, the agent does everything:

  1. Read the message
  2. Infer the policy
  3. Decide eligibility
  4. Decide the amount
  5. Call the refund API

This makes the model responsible for both understanding and enforcement.

That is exactly where systems become unreliable.

The Better Way

Press enter or click to view image in full size

In the better design, the LLM handles the fuzzy part and the software handles the critical part.

The agent only extracts structured information from the user’s message:

  • intent: refund_request
  • issue_type: late_delivery
  • order_id: 12345

Then your application code takes over:
1. fetch the order
2. check the policy rules
3. decide whether a refund is allowed
4. return one allowed action

The model does not decide policy.

The model helps the system understand the request.

The system decides what is allowed.

The Core Design Pattern

This pattern works well in far more than support bots.

The same idea shows up in analytics agents, finance workflows, scheduling assistants, HR systems, and internal tooling:

  • let the model interpret
  • let code validate
  • let workflows enforce

That is the difference between a demo and a reliable product.

Step 1: Put Rules in Code, Not Prompts

If your refund policy matters, it should not live only inside a prompt.

A prompt like:

“Only give refunds if the package was late and the request is within 7 days”

is helpful context, but it is not enforcement.

Enforcement means actual code:

def refund_policy_engine(order, issue_type):
if order.already_refunded:
return {"allowed": False, "reason": "already_refunded"}

if order.order_type == "digital":
return {"allowed": False, "reason": "digital_items_not_refundable"}

if issue_type == "late_delivery" and order.delivery_delay_days >= 2:
if order.days_since_delivery <= 7:
return {"allowed": True, "amount": order.total_amount}

return {"allowed": False, "reason": "not_eligible"}

Now the logic is explicit, reviewable, testable, and stable.

The LLM can still help identify the issue type, but it cannot invent a refund policy.

Step 2: Use the Agent for Routing, Not Final Decisions

For most beginner agents, the model should behave more like a classifier than a fully autonomous actor.

A good question for the model is:

What kind of request is this, and what information is missing?

Bad questions are:

  • Should this customer get a refund?
  • How much money should I send back?
  • Should I override the policy?

Those are system decisions.

A safer model output looks like this:

{
"intent": "refund_request",
"issue_type": "late_delivery",
"order_id": "12345",
"confidence": 0.91
}

That output is useful because it routes the workflow without directly triggering a risky action.

Step 3: Constrain Actions with an Allowed Set

One of the easiest ways to make an agent safer is to give it a tiny action space.

Instead of letting it do anything, only allow actions like:

  • ask_for_order_id
  • check_order_status
  • offer_refund
  • escalate_to_human

That means even if the model behaves unexpectedly, it still cannot escape the rails you defined.

For example:

ALLOWED_ACTIONS = {
"ask_for_order_id",
"check_order_status",
"offer_refund",
"escalate_to_human",
}


def execute_action(action, payload):
if action not in ALLOWED_ACTIONS:
raise ValueError("Action not allowed")

return {"action": action, "payload": payload}

This is a simple idea, but it matters a lot.

If an action is not allowed by the system, the agent simply cannot take it.

Step 4: Validate Before Anything Critical Happens

Even after classification, you should still validate inputs before doing the real thing.

For example:

  • if there is no order_id, ask for it
  • if the order does not exist, stop
  • if the order is already refunded, stop
  • if the action payload is malformed, reject it

So the full flow becomes:

  1. user sends message
  2. model extracts structured fields
  3. code validates required data
  4. policy engine decides eligibility
  5. system returns one safe action

This is much more predictable than asking the model to improvise the entire workflow.

Step 5: Test It Like Software

Press enter or click to view image in full size

This is where many teams stop too early. They test the agent by chatting with it a few times.

That is not enough.

You should test it the same way you test the rest of your product.

A few simple cases:

  • late delivery within 7 days -> refund allowed
  • digital product -> refund denied
  • already refunded order -> refund denied
  • missing order ID -> ask for order ID
  • confusing angry message -> still route safely

Example:

def test_late_delivery_refund_allowed():
order = Order(
order_type="physical",
already_refunded=False,
delivery_delay_days=3,
days_since_delivery=2,
total_amount=120,
)

result = refund_policy_engine(order, "late_delivery")

assert result["allowed"] is True
assert result["amount"] == 120


def test_digital_order_not_refundable():
order = Order(
order_type="digital",
already_refunded=False,
delivery_delay_days=0,
days_since_delivery=1,
total_amount=120,
)

result = refund_policy_engine(order, "late_delivery")

assert result["allowed"] is False
assert result["reason"] == "digital_items_not_refundable"

Notice what is happening here:
- We are not testing whether the model “feels smart.”
- We are testing whether the system behaves correctly.

That is the shift that makes agents reliable.

A Simple End-to-End Version

Here is the whole pattern in one small example:

def handle_refund_request(user_message):
classification = llm_classify(user_message)
# Example output:
# {
# "intent": "refund_request",
# "issue_type": "late_delivery",
# "order_id": "12345"
# }

if classification["intent"] != "refund_request":
return {"action": "escalate_to_human"}

if not classification.get("order_id"):
return {"action": "ask_for_order_id"}

order = get_order(classification["order_id"])
if order is None:
return {"action": "escalate_to_human", "reason": "order_not_found"}

decision = refund_policy_engine(order, classification["issue_type"])

if decision["allowed"]:
return {
"action": "offer_refund",
"amount": decision["amount"],
}

return {
"action": "escalate_to_human",
"reason": decision["reason"],
}

This is still an AI agent.

But it is a constrained one.

And that is why it is more reliable.

A Good Mental Model for Beginners

Think of an LLM as a junior teammate.

It is useful for:

  • understanding messy language
  • extracting structure
  • classifying requests
  • choosing between a few known paths

It is not the part you should trust with final business enforcement.

That part should live in code.

So a good beginner rule is:

Use AI for interpretation. Use software for control.

What “Reliable” Usually Means in Practice

When people say they want a “deterministic agent,” they usually mean one of these:

  • it should not take unsafe actions
  • it should follow policy every time
  • it should handle edge cases predictably
  • it should fail safely

You do not get that by writing a bigger prompt.

You get that by designing a narrower system.

Final Takeaway

The most useful beginner lesson is this:

  • Do not build an agent that decides everything.
  • Build an agent that does one fuzzy job well, inside a deterministic system.

If you remember just one line, let it be this:

Reliable agents are not usually autonomous decision-makers. They are constrained routers inside well-defined software.

That is how you make AI systems predictable enough to trust in real products.

--

--

Sagar Sonwane
Sagar Sonwane

Written by Sagar Sonwane

Sr. Software Engineer @Josh Software Pvt. Ltd.