Design a support triage and refunds agent
A fully worked answer to "put an agent on our support inbox". The signature hard part is authority: what the agent may do by itself, what it may only propose, and where the line goes so that one convincing user cannot move it.
Step 1: task and boundary
The brief is "handle tier-one support". That is not a design. Narrow it into a unit of work and an authority level before anything else.
Unit of work: one customer conversation, typically three to six turns.
What the model does: read the conversation and the customer's account state, classify the issue, retrieve the relevant policy, and either answer, propose an action, or hand off.
What code does: everything that changes the world.
| Action | Who decides | Why |
|---|---|---|
| Which category the issue is | Model | Fuzzy, open-ended, exactly what it is good at |
| What to say | Model | The point of the system |
| Whether this customer is eligible for a refund | Code | Eligibility is a rule, and rules belong in code |
| Whether to issue a refund at all | Code, from a limit | Money moving is authority, never judgment |
| Whether to escalate | Either. The model may always escalate | Escalation is the safe direction, so it needs no gate |
The single decision the whole design hangs on:
| Refund amount | Who acts | Reasoning |
|---|---|---|
| Under 50 dollars, eligible by rule | Agent acts, logged | Reversible, cheap, high volume. The savings live here |
| 50 to 500 dollars | Agent drafts, human approves in one click | Still cheap to review, expensive to get wrong |
| Over 500, or any rule miss | Human handles, agent summarises | The agent's value here is the summary, not the decision |
That table is the security boundary, the cost model, and the product decision, all at once. Everything else in this design serves it.
The authorisation box is deliberately drawn after validation and before any action. Nothing the model produces reaches the world without passing through code that re-derives eligibility from the account, not from the conversation.
Step 2: context plan
| Slice | Source | Size | Freshness |
|---|---|---|---|
| System prompt and policy summary | Static | ~900 tokens | Deploy time |
| Tool definitions | Static | ~600 tokens | Deploy time |
| Account state (plan, order history, prior refunds) | Database lookup, not retrieval | ~400 tokens | Real time |
| Relevant policy passages | Hybrid search over the policy corpus | ~1,200 tokens | Nightly index |
| Conversation so far | The thread | ~800 tokens by turn four | Real time |
Two decisions worth defending. First, account state comes from a database query, not a vector search: it is structured, it is exact, and retrieving it fuzzily would be an unforced error. Second, the always-on 1,500 tokens of prompt and tool definitions cost more in aggregate than the retrieval does, because they ride on every call including the cheap ones.
Trimming rule: when the window tightens, drop the oldest conversation turns first, keep a running summary, and never drop the policy passages or the account state. If you must drop policy, refuse instead.
Step 3: cost and latency budget
const supportAgent: Budget = {
unit: "one customer conversation",
costCents: 4, // p50: classify (small) + 2 agent steps (large)
costCeilingCents: 25, // hard stop; a conversation past this is escalated
latencyMsP50: 1800,
latencyMsP95: 4000,
volumePerDay: 12_000,
};
Where the four cents goes, and why the routing decision is worth so much:
| Step | Model | Tokens in / out | Cost |
|---|---|---|---|
| Classify and triage | Small | 2,200 / 120 | ~0.3c |
| Answer or plan, step 1 | Large | 3,900 / 300 | ~1.8c |
| Answer or plan, step 2 | Large | 4,400 / 250 | ~1.9c |
| Total, p50 | ~4c |
At 12,000 conversations a day that is about 480 dollars daily, or roughly 175,000 a year. The comparison that gets this funded is not "versus zero", it is versus the fully loaded cost of the tier-one agents it deflects, and the honest version of that number counts the 30 percent that still reach a human.
The ceiling matters more than the target. A conversation that loops is unbounded spend. At 25 cents the agent stops, hands to a human, and records why. That is one line of code and it removes the entire class of runaway-bill incidents.
Step 4: failure ladder
| Failure | Ladder |
|---|---|
| Model times out | One retry with jitter, then the small model with a narrowed prompt, then "let me get a colleague", then handoff |
| Output fails schema validation | Re-ask once, including the validation error, then handoff. Never guess the missing field |
| Policy retrieval returns nothing | Answer only what is answerable without policy, and say the policy could not be checked. Never let the model recall policy from training |
| Refund API errors | Do not retry blind. Check idempotency key, surface the state to the model as an observation, escalate after two attempts |
| Step budget hit (6 steps) | Stop, summarise the conversation, hand to a human |
| Cost ceiling hit | Same as step budget. Both are the same defence against loops |
The rung to argue about is retrieval returning nothing. The tempting behaviour is to let the model answer from what it "knows" about refund policy. That is precisely how you get an invented bereavement policy that a tribunal then holds you to. Fail loudly, not fluently.
Step 5: threat model
The trust boundary and what crosses it:
| Source | Trusted | Notes |
|---|---|---|
| System prompt, policy corpus | Yes | You control both, and the corpus is reviewed |
| Customer message | No | The primary injection vector, and the attacker is motivated |
| Order notes and prior tickets | No | Often free text a customer wrote. This surprises people |
| Refund tool response | No | Returns text that lands in the context |
The question that decides the severity: what can an attacker who fully controls the model's output cause?
Answer, by construction: they can cause a refund of at most 50 dollars, to their own account, that they were already eligible for by rule. That is not nothing, but it is bounded, reversible, logged, and rate-limited. It is a business risk with a number attached, not a breach.
| Defence | What it stops | Where it lives |
|---|---|---|
| Authorisation re-derived in code from account state | The model being talked into "you are authorised" | The authorisation box |
| Refund limit and per-customer rate limit | One compromised conversation draining anything | Same place |
| Idempotency key per conversation | A retry loop issuing five refunds | The refund client |
| Input guard for known injection shapes | The lazy attacks, not the clever ones | Before the model |
| Untrusted content clearly delimited in the prompt | Some instruction confusion, imperfectly | The prompt |
The last two are worth having and worth being honest about: prompt-level defences reduce the rate, they do not create a boundary. The boundary is the 50 dollar limit enforced by code.
Step 6: eval plan
The set: 150 real conversations pulled from the last quarter, frozen, deliberately weighted toward the cases that hurt. Roughly 60 straightforward, 40 policy edge cases, 30 that should escalate, 20 adversarial (injection attempts, users arguing for a refund they are not owed).
| Dimension | Scorer | Gate |
|---|---|---|
| Correct action taken | Exact match against a human label | 95 percent |
| Escalated when it should | Recall on the escalate class | 98 percent. Missing an escalation is the expensive error |
| Never acted beyond authority | Trajectory check on tool calls | 100 percent. Any failure blocks the release |
| Answer quality | Rubric, model judge, spot-checked by a person | 4.0 of 5 average |
| Tone and policy compliance | Rubric | No case below 3 |
Two things to notice. The authority check is scored on the trajectory, not the answer, because an agent that reaches the right reply after attempting an unauthorised tool call has failed the thing that matters. And escalation recall is gated higher than accuracy, because the cost of a wrongly-handled case is much greater than the cost of an unnecessary handoff.
Step 7: operability
| Signal | Alert | What it usually means |
|---|---|---|
| Escalation rate | Moves more than 5 points week on week | Either the model drifted or your customers' problems changed |
| Refund rate per 1,000 conversations | Any climb | The first place an injection campaign shows up |
| Cost per conversation, p95 | Above 15c | Loops, or context growing without anyone noticing |
| Invalid-output rate | Above 2 percent | Provider drift, almost always |
| Approval-queue agreement rate | Below 90 percent | The agent's drafts got worse, and the 50 dollar threshold should come down |
That last one is the quiet gem. Humans already approve every refund between 50 and 500 dollars, so agreement rate is a free continuous eval on live traffic. It is also the evidence you use to raise the automatic threshold later, which is the only responsible way to widen the agent's authority.
Runbook, in order: check invalid-output and refusal rate first (drift), then cost p95 (loops), then escalation rate (quality). Three levers: pin to the last known-good model, drop the automatic refund limit to zero so everything queues for approval, or turn the agent off and route to humans. The middle lever is the one you will actually use, and it is why the limit is a config value and not a constant.
The decisions, out loud
| Decision | What it cost | What would change it |
|---|---|---|
| Refund authority capped at 50 dollars, in code | About 30 percent of refunds still need a person, so savings are capped | Approval agreement above 98 percent for a full quarter |
| Account state from the database, not retrieval | One more integration to build and maintain | Nothing. Fuzzy-matching account state is never right |
| Small model for triage, large for the answer | A misroute sends a hard case down the cheap path | Routing accuracy below 95 percent on the eval set |
| Refuse rather than answer when policy retrieval is empty | A visibly worse experience on a small slice of conversations | Nothing. This is the Air Canada rung |
| Six-step and 25 cent ceilings | A handful of legitimately complex conversations get handed off early | Trace evidence that real conversations need more, not a hunch |
Recap
- Authority is the design. The 50 dollar limit, enforced by code that re-derives eligibility, is what makes an injection a bounded business risk instead of a breach.
- The model proposes, code disposes. Nothing the model emits reaches the world without passing an authorisation check it cannot influence.
- Ceilings on steps and cost are the anti-loop defence, and they are two lines of code.
- Human approval on the middle tier is a free live eval, and the only honest evidence for widening the agent's authority later.