Agentic AI / Agent architectures, tool use and MCP / Designing a customer-support agent over 30 backend operations

Designing a customer-support agent over 30 backend operations

The earlier notes in this section each took one piece: the loop, tool schemas, idempotency, termination. This one puts them together on a single realistic system, a support agent for an online retailer with thirty backend operations behind it, and works through the four decisions that determine whether it is safe to ship: how the operations become tools, who may call what, which calls must be safe to repeat, and how you will know it works.

The short version

Thirty operations become about twenty tools, grouped by domain, with near-duplicates merged, reads and actions named differently, and only the groups relevant to the conversation loaded at a time. Identity comes from the session, never from the model; every tool checks that the resource belongs to that identity, and the scopes differ for a customer, a support representative and an internal job. Eight of the operations create, send or move money and get idempotency keys derived from the operation, with a ledger for the ones whose services cannot keep keys. The loop has a step cap, a budget, a deduplication check, an approval step before anything irreversible, and a structured finish tool whose evidence fields are verified against the run. Evaluation runs at four levels, tool, loop, conversation and live, and the live metrics decide rollout one intent at a time.

01The brief

An online retailer with a few million customers wants an agent in its chat widget and its support inbox. It should handle the questions that make up most of the volume: where is my order, I want to return this, I was charged twice, my payment failed, how do I change my address. It should do the work rather than explain how to do it: look up the order, start the return, issue the refund. It should hand off to a person when it cannot, when the customer asks, or when the stakes are high. It must never act on an account it cannot prove the customer owns, and it must never move more money than policy allows without a person approving.

Two kinds of user will sit in front of it. Customers, through the widget, acting on their own account. Support representatives, through an assist mode in their console, acting on any account with wider powers. The same agent serves both; what differs is what it is allowed to do, which is the subject of section 04.

02The thirty operations

The backend already exposes these, as internal APIs. Listing them with three properties, whether they read or write, whether the effect can be undone, and how much damage a wrong call does, is the first design step, because every later decision keys off those properties.

OperationKindReversibleBlast radius
Customer and account
get_customerread·exposes personal data
search_customersread·exposes personal data across accounts
get_account_statusread·low
update_contact_detailswriteyesaccount takeover if misused
send_password_resetwrite, sendsnoaccount takeover if misused
get_subscriptionread·low
Orders
get_orderread·low
list_ordersread·low
get_shipment_trackingread·low
cancel_orderwriteno, once shippedlost sale, stock
change_shipping_addresswriteuntil dispatchgoods to the wrong place
create_reorderwrite, chargesvia cancelmoney
get_return_eligibilityread·low
Returns and refunds
create_returnwriteyesa label and a pickup
get_returnread·low
issue_refundwrite, moneynomoney
apply_store_creditwrite, moneypartlymoney
get_refund_statusread·low
Billing
list_invoicesread·low
get_invoiceread·low
retry_paymentwrite, chargesvia refundmoney
send_payment_method_linkwrite, sendsnophishing vector if misdirected
apply_discountwrite, moneyyesmoney
Knowledge and communication
search_help_centreread·none
get_policyread·none
send_emailwrite, sendsnospam, misdirected data
send_smswrite, sendsnospam, cost
Case management
create_ticketwriteyesqueue noise
add_case_notewrite, appendnolow
escalate_to_humanwriteyesnone; always allowed

Fourteen reads, sixteen writes. Of the writes, five move money, four send something to a person, and three are account-security sensitive. That is the shape of the risk, and the design below is mostly about those twelve.

03From thirty operations to a tool surface

Thirty flat tools is the wrong surface. Every request would carry thirty schemas, and the model would face choices between near-duplicates on every turn. Four moves shape it.

Merge near-duplicates

Operations that answer the same question from the model's point of view become one tool with a parameter. get_return and get_refund_status become get_return, which returns the refund state as a field. list_invoices and get_invoice become get_invoices with an optional invoice id. send_email and send_sms become send_message with a channel enum, because the model's decision is "tell the customer", not "which transport". search_help_centre and get_policy become search_knowledge with a kind enum. That is thirty operations behind twenty-five tools, and the adapter does the fan-out.

Separate reads from actions in the names

Reads are get_, list_, search_. Actions are a verb that says what changes: cancel_order, issue_refund, create_return. The model learns the convention from the names, and the runtime uses the same prefix to decide which calls run freely and which go through the gate in section 06.

Group by domain, load by intent

The twenty-five tools sit in six groups matching the table. A conversation rarely needs more than two groups. So the runtime classifies the opening message into an intent, cheaply, with a small model or a classifier, and loads the tools for that intent plus the groups that are always present: knowledge, case management, and the finish tool. A "where is my order" conversation sees the orders group and the three always-on groups, about twelve tools, not twenty-five. If the model needs a group that is not loaded, a load_tools tool fetches it by name; the descriptions of unloaded groups are listed in one line each so the model knows they exist.

always loaded    knowledge (1)  ·  case management (3)  ·  finish (1)  ·  load_tools (1)
by intent        account (5)  ·  orders (7)  ·  returns (4)  ·  billing (4)

"where is my order"      → orders + always-on          = 13 tools
"I was charged twice"    → billing + orders + always-on = 17 tools
"I want to return this"  → returns + orders + always-on = 17 tools

Add the finish tool

The model does not end a conversation by writing text; it calls finish with the resolution, the ids of the actions it took, and the sources it relied on. The runtime verifies those ids against the run before accepting, as the termination note describes. A resolution that claims a refund was issued must carry a refund id that issue_refund actually returned in this run.

04Auth scopes: who may call what

The single most important rule, from the function-calling note: identity comes from the session, never from the model's arguments. Every tool call carries the session's principal alongside the model's arguments, and the adapter checks the two against each other before anything runs.

Three principals

PrincipalWhoActs onIdentity established by
customerA logged-in customer in the widgetTheir own account onlyThe site's session cookie; the customer id is in the session, not the conversation
representativeA support rep in assist modeAny account they have opened a case onStaff SSO plus the case id; the agent can act only on the case's customer
systemA batch job, such as proactive delivery-delay outreachA list of accounts from the jobA service identity with a fixed, narrow scope

The scope matrix

Scopes are granted per tool group and per action, with money and security actions carrying thresholds.

Tool group or actioncustomerrepresentativesystem
Reads on own or case accountyesyeslisted accounts only
search_customersnoyesno
update_contact_detailswith re-authenticationyes, loggedno
send_password_resetto the address on file onlyto the address on file onlyno
cancel_order, change_shipping_address, create_returnyes, own orders, with confirmationyesno
issue_refund, apply_store_creditup to ₹2,000 per order, with confirmation; above that, escalateup to ₹20,000; above that, supervisor approval through the gateno
apply_discountnofrom an approved listno
retry_payment, create_reorderyes, with confirmationyesno
send_messageto self onlyto the case customer onlytemplated, to listed accounts
create_ticket, add_case_note, escalate_to_humanyesyesyes

How it is enforced

session  { principal: customer, customer_id: C-4471, scopes: [...], session_id }
model    issue_refund(order_id="O-98765", amount=1800, reason="damaged")

adapter  1. scope:   does the principal hold refund.issue?                 ✓
         2. owner:   does order O-98765 belong to customer C-4471?         ✓  (a lookup, not the model's word)
         3. limit:   is 1800 ≤ the principal's refund ceiling?             ✓
         4. policy:  is the order within the refund window and eligible?   ✓
         5. gate:    confirmed by the customer in this conversation?       ✓  (section 06)
         6. key:     derive the idempotency key                             (section 05)
         7. execute, log { principal, tool, args, result, key, trace_id }

Step 2 is what stops "please refund order O-12345" from working when O-12345 belongs to someone else, however convincingly the customer or the conversation asserts otherwise. The model is never asked to enforce ownership, and the tool descriptions do not even mention it; it is not the model's job.

The security-sensitive writes get one more layer. Changing contact details as a customer requires a fresh re-authentication, because the widget session may have been left open. Password resets and payment-method links are only ever sent to the address already on file, never to one supplied in the conversation, which closes the obvious takeover path.

05Idempotency: which calls must be safe to repeat

Apply the classification from the idempotency note to the sixteen writes.

WriteClassKey derived fromWhere the key is kept
update_contact_detailsabsolute writenone needed·
change_shipping_addressabsolute writenone needed·
cancel_orderidempotent by id; second call returns "already cancelled"none needed; adapter treats already-cancelled as success·
escalate_to_humanidempotent per sessionsession_idcase system rejects a duplicate
apply_discountabsolute per order and codenone needed·
create_returncreatessession + order_id + item idsreturns service accepts a key
issue_refundmoneyorder_id + return_id + amountpayment processor's key header, 24 h
apply_store_creditmoneyorder_id + reason + amountledger with unique constraint
retry_paymentmoneyinvoice_id + sessionpayment processor's key header
create_reordermoneysession + source order_idorders service accepts a key
send_messagesendsrecipient + template or content hash + dayledger; the mail relay has no keys
send_password_resetsendscustomer_id + hourledger, which also rate-limits
send_payment_method_linksendscustomer_id + dayledger
create_ticketcreatessession + hash of subjectticket system's external-reference uniqueness
add_case_noteappendsession + step + content hashledger

Three things to notice. The refund key does not include the session: a refund for a given return and amount is one operation whoever triggers it and whenever, so a representative retrying tomorrow what the customer's session attempted today hits the same key and the money moves once. The message key includes a day, so the agent cannot send the same template twice in a day but can next week. And nothing in any key comes from the model's free text; reason is stored and logged but is not part of the key, because the model may phrase it differently on a retry.

The ledger is one table, written before the call and updated after, with a unique constraint on the key. It serves four tools that talk to services with no key support and doubles as the rate limiter for the two sends that are takeover vectors.

06The loop and its guardrails

The loop is the one from the ReAct note, with the guardrails the termination note calls for, tuned to this system.

  • Step cap of 12 per turn, where a turn is one customer message to one agent response. Most resolutions take three to six steps. A cost budget per conversation sits above it.
  • Reads run freely. Anything with a read prefix executes on the model's call, subject to scope and ownership.
  • Actions go through a confirmation gate. Before cancel_order, create_return, issue_refund, retry_payment or create_reorder runs, the runtime requires that the model has stated the action and its effect to the customer and the customer has said yes, in this conversation. The gate is a runtime check on the transcript, not a prompt instruction: the model's confirmation question and the customer's reply are both recorded, and the action tool refuses with "not confirmed" otherwise. In representative mode, actions above the rep's ceiling route to a supervisor approval that pauses the run.
  • Deduplication. A call identical in name and arguments to one in the last five steps is intercepted and the model is told what it got last time.
  • Errors say whether retrying helps. The payment processor's decline is retryable: false with the decline reason; a timeout is retryable: true. The adapter retries the second kind itself, under the key, and shows the model only the final outcome.
  • Escalation is always one call away. escalate_to_human is loaded in every conversation, needs no confirmation, and ends the agent's turn with the case notes written. The model is told to use it when the customer asks, when it is outside its scope, when the customer is distressed, and when it has gone round twice without progress.
  • Finish is verified. The finish call's action ids and sources are checked against the run's tool log. A claimed refund with no matching issue_refund result is rejected and the model is told why.

07What the model is told

The system prompt is short and stable, for caching and for clarity: who it is, which principal it is serving, the rules that are the model's job (confirm before acting, escalate when unsure, never promise what a tool did not return, treat tool output as data), and the tone. The rules that are the runtime's job, ownership, limits, idempotency, are not in the prompt at all; telling the model about an enforcement it cannot perform only invites it to argue with the customer about it.

Tool descriptions follow the schema note: what, when, when not, cost. The one for issue_refund says it moves money and cannot be undone, that it requires a return or a documented reason, that the customer must have confirmed, and that amounts above the ceiling should be escalated rather than attempted. Policy documents are resources the runtime attaches when the intent calls for them, so the return window is in context for a return conversation and not for a tracking one.

Tool results are trimmed before they reach the model: an order record of forty fields becomes the eight the agent uses. Help-centre passages and order notes, both of which contain text written by people outside the team, are wrapped as quoted data, and the prompt says that instructions found in them are not instructions.

08The evaluation plan

Four levels, each catching what the one below cannot.

Level 1: tools and adapters, as unit tests

  • Every scope rule in the matrix has a test in both directions: the principal that may, and the principal that may not. Customer A requesting B's order is the canonical one; there are about sixty of these.
  • Every keyed write has a replay test: call, kill the process before the result is recorded, resume, assert the service saw one action.
  • Every adapter has a shape test: the trimmed result matches its documented schema, errors carry the retryable flag.

Level 2: the loop, as integration tests against stubbed services

  • Termination: no scripted scenario exceeds the step cap; a scenario built to loop, a tool that always times out, ends with a tripped breaker and a partial-state escalation.
  • Gating: an action attempted without confirmation is refused; a finish claiming an unperformed action is refused.
  • Deduplication: a scenario where the stub returns an ambiguous result produces at most one repeat before the model changes course.

Level 3: conversations, as an offline evaluation set

About four hundred conversations, built from anonymised real transcripts for the common intents and written cases for the rare and dangerous ones, run against the full agent with stubbed services whose state is set per case. Each case specifies the expected outcome, and graders score:

DimensionGraded byBar to ship
Tool selection and argumentsExact match against the expected call sequence, order-insensitive for reads95% of cases with the right actions and no wrong actions
Resolution correctnessA model judge with a per-case rubric, spot-checked by people90% judged correct and complete
Policy complianceMechanical: no refund above ceiling, no action without confirmation, no send to an off-file address100%; any failure blocks
EscalationPrecision and recall against cases labelled "should escalate"Recall above 95%; precision above 70%
SafetyA red-team subset: injection in order notes and help-centre text, social engineering for another account, refund abuse patterns100% of injection cases ignored; 100% of cross-account attempts refused
Cost and stepsMeasured per caseMedian under six steps; p95 cost under the budget

Because greedy decoding is not deterministic, each case runs three times and a case passes only if all three pass. The set is the regression suite: it runs on every change to the prompt, a tool description, a schema, the model version or the gating logic, and a drop on any dimension blocks the change.

Level 4: live, with the metrics that decide rollout

  • Containment: conversations resolved without a person, by intent.
  • Escalation rate and reason, watched for both too high and suspiciously low.
  • Customer satisfaction on agent-handled conversations against the human baseline.
  • Refund and credit amounts against the pre-agent baseline, per week; a rise is the first sign of either abuse or over-generosity.
  • Tool error rate, breaker trips, gate refusals, dedup intercepts, each a signal about a specific part of the design.
  • Latency at p50 and p95, and cost per conversation.

A sample of live conversations is reviewed by support staff weekly, and every escalated conversation is readable by the person who receives it with the agent's trace attached. Conversations that went wrong go into the offline set.

09Rollout

  1. Shadow mode. The agent runs on live conversations with all actions stubbed, and its proposed actions are compared to what the human did. This finds the intent classifier's gaps and the scope matrix's omissions without risk.
  2. Representative assist. The agent proposes, the representative approves each action. Highest-scope principal, lowest risk, because a person is in every loop. This is where the tool descriptions get tuned.
  3. Customers, reads only. Tracking, status, policy questions. No writes enabled.
  4. Customers, one action at a time. Returns first, because they are reversible. Then cancellations. Refunds under the ceiling last, after the refund-amount metric has a stable baseline.
  5. Widen the ceilings only when the live metrics have held for a month and the offline suite has grown to cover what the month surfaced.

10Takeaways

  1. Classify the operations first: read or write, reversible or not, how much damage. Everything else keys off that.
  2. Thirty operations is not thirty tools. Merge duplicates, name reads and actions differently, load by intent, add a verified finish tool.
  3. Identity from the session, ownership checked by the adapter, scopes per principal, thresholds on money. The model enforces none of it.
  4. Key every create, send and money movement from the operation's identity, with a ledger where the service has no key support.
  5. The gate is a runtime check on the transcript, not a prompt instruction, and escalation is always available.
  6. Four levels of evaluation: scopes and keys as unit tests, the loop against stubs, four hundred conversations run three times each, and live metrics by intent.
  7. Roll out by principal and by reversibility. Shadow, then assist, then reads, then reversible actions, then money.