DEV Community

shashank ms
shashank ms

Posted on

LLM vs Rule-Based Systems: A Comparison

We are building a refund triage agent that routes customer requests through hard rules first, then falls back to an LLM when the case is ambiguous. This hybrid approach keeps deterministic cases fast and cheap while letting the model handle edge cases that do not fit a decision table. I will walk through the exact Python module I shipped to production.

What you'll need

Step 1: Scaffold the rule engine

Rule-based systems are deterministic and cheap, but brittle once a case falls outside the decision table. I encode the non-negotiable constraints in plain Python so they run in microseconds without any external dependency.

from dataclasses import dataclass
from typing import Optional, Literal

@dataclass
class RefundRequest:
    days_since_purchase: int
    is_defective: bool
    customer_tone: str  # angry, neutral, polite
    explanation: str

def rule_based_decision(req: RefundRequest) -> Optional[Literal["approve", "deny", "escalate"]]:
    # Beyond 90 days: automatic deny
    if req.days_since_purchase > 90:
        return "deny"
    # Defective within 30 days: automatic approve
    if req.is_defective and req.days_since_purchase <= 30:
        return "approve"
    # Angry customer between 31 and 90 days: escalate to human
    if req.customer_tone == "angry" and req.days_since_purchase > 30:
        return "escalate"
    # No rule matched
    return None

Step 2: Set up the Oxlo.ai client

LLMs handle nuance well, but token-based billing makes long customer threads expensive. Oxlo.ai uses flat per-request pricing, so a verbose explanation does not inflate cost. You can view current plans at https://oxlo.ai/pricing.

from openai import OpenAI

client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")

Step 3: Write the system prompt

The system prompt acts as a programmable policy layer. It repeats the hard constraints so the model does not contradict them, and it asks for structured JSON so parsing is trivial.

SYSTEM_PROMPT = """You are a refund triage assistant. Your job is to decide whether to approve, deny, or escalate a refund request.

Policy:
- Approve if the item is defective and within 30 days.
- Deny if the request is beyond 90 days.
- Escalate if the customer is angry and the request is between 31 and 90 days.
- For all other cases, use your judgment. Consider the explanation, fairness, and company reputation.

Respond with a JSON object containing exactly two keys:
- decision: one of "approve", "deny", "escalate"
- reasoning: a short sentence explaining why
"""

Step 4: Wire rules and LLM together

The orchestrator tries rules first. Only if they return None does it call Llama 3.3 70B on Oxlo.ai. I set response_format to json_object so the output is machine-readable.

import json

def triage_refund(req: RefundRequest) -> dict:
    # Try deterministic rules first
    decision = rule_based_decision(req)
    if decision:
        return {
            "decision": decision,
            "reasoning": "Handled by rule engine.",
            "source": "rule"
        }

    # Build the user message from the request fields
    user_message = f"""Days since purchase: {req.days_since_purchase}
Defective: {req.is_defective}
Customer tone: {req.customer_tone}
Explanation: {req.explanation}"""

    response = client.chat.completions.create(
        model="llama-3.3-70b",
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": user_message},
        ],
        response_format={"type": "json_object"}
    )

    result = json.loads(response.choices[0].message.content)
    result["source"] = "llm"
    return result

Run it

I test three tickets: a clear deny, a clear approve, and a fuzzy case that requires judgment.

if __name__ == "__main__":
    # Case 1: Hard deny
    req1 = RefundRequest(
        days_since_purchase=95,
        is_defective=False,
        customer_tone="neutral",
        explanation="I changed my mind."
    )
    print("Case 1:", triage_refund(req1))

    # Case 2: Hard approve
    req2 = RefundRequest(
        days_since_purchase=10,
        is_defective=True,
        customer_tone="neutral",
        explanation="Battery swells after one charge cycle."
    )
    print("Case 2:", triage_refund(req2))

    # Case 3: Ambiguous, hits the LLM
    req3 = RefundRequest(
        days_since_purchase=45,
        is_defective=False,
        customer_tone="neutral",
        explanation="Color faded after two washes despite following the care label exactly."
    )
    print("Case 3:", triage_refund(req3))

Example output:

Case 1: {'decision': 'deny', 'reasoning': 'Handled by rule engine.', 'source': 'rule'}
Case 2: {'decision': 'approve', 'reasoning': 'Handled by rule engine.', 'source': 'rule'}
Case 3: {'decision': 'escalate', 'reasoning': 'Product did not meet reasonable durability expectations; best handled by human agent.', 'source': 'llm'}

Wrap-up

This pattern separates concerns: rules handle volume, and the LLM handles exceptions. To push it further, log every LLM decision to SQLite and audit them weekly, or swap in DeepSeek R1 671B on Oxlo.ai when you need explicit chain-of-thought reasoning for regulatory compliance.

Top comments (0)