AI agents

Teaching an Agent to Pick Its Own Reason Code

Antonio Evans9 min read

A split scene: on the left, a human hand points at a short list of reason codes on a printed card.
Key points

Key points

  1. 1Put the full reason code list, one line of definition and one example per code, into the agent's system instructions, and require exactly one code in a fixed output field.
  2. 2Write a labeled test set of at least 50 human-reviewed items before you let the agent pick codes in production, and grade it with exact match on the code field.
  3. 3Fix ambiguity in the code list itself (split, merge or add a tie-break rule naming both codes) rather than adding more prompt text when the agent picks the wrong code.
  4. 4Re-run the same frozen test set after every instruction change and treat a drop in the match rate as a regression until proven otherwise.

Why the agent should pick the code, not a person

Most reason code systems were built for humans filling in a dropdown after the fact. The code is assigned late, by whoever closes the ticket, and the label reflects the closer's memory of the conversation rather than the conversation itself. That produces noisy reporting and slow feedback loops.

When an agent runs in a loop over the actual transcript, it can choose the code at the moment the evidence is fresh. Agent prompt engineering makes the point that an agent prompt is closer to onboarding a new hire than to asking one question: the system prompt sets the role, the latitude and the stop conditions. A reason code is part of that role definition. If you do not say which codes exist and when each applies, the agent will improvise a taxonomy of its own, and you will inherit a long tail of near-duplicate labels you cannot report on.

There is a real risk in moving the decision to the model. Research on whether models actually follow label definitions found that explicit definitions can improve accuracy, but their integration is "neither guaranteed nor consistent," and models often fall back on internal representations, especially on general tasks (Mohammadi et al.). In plain terms: writing a definition is not the same as the agent using it. That is why the second half of this article is entirely about measurement.

The decision rule is simple. If a human reviewer would assign the same code to the same item on two different days, the task is a good fit for agent assignment. If two trained humans disagree regularly, the code list is the problem, and no amount of prompt text will fix it.

Step 1: Write the code list into the instructions

Treat the code list as data, not prose. Each code needs three things: the code itself (a stable string your database and reports already use), a one-line definition, and one example of an item that belongs there plus one that does not.

Here is a system instruction block for a support triage agent. The structure matters more than the wording.

You assign exactly one reason code to each support conversation.

Available codes (use the code string exactly as written):

- BILLING_DUPLICATE: The customer was charged twice for the same period.
  Belongs: "I see two charges of $49 on the same day."
  Does not belong: "I want to downgrade to save money."

- BILLING_REFUND_STATUS: The customer asks where a promised refund is.
  Belongs: "You said the refund would arrive in 5 days, that was 9 days ago."
  Does not belong: "Please refund this order."

- ACCESS_LOCKED: The customer cannot sign in and has already tried a reset.
  Belongs: "I reset my password twice and still get 'account locked'."
  Does not belong: "How do I change my email address?"

Selection rules:
1. Return exactly one code from the list. Never invent a code.
2. If two codes could apply, choose the one that describes the customer's
   primary request, not the background detail.
3. If no code fits, return NO_MATCH and quote the sentence that made you
   reject every code.
4. Justify in one sentence, quoting at most 15 words from the conversation.

Return JSON only, in this shape:
{"code": "<CODE>", "justification": "<one sentence>"}

Four details do most of the work. First, the codes appear in a fixed order and exactly as they appear in the database, so there is no normalization work later. Second, each definition describes the customer's situation, not your internal process. Third, the negative example ("does not belong") often matters more than the positive one, because it draws the boundary the agent would otherwise cross. Fourth, the fallback code NO_MATCH is explicit. Without it, an agent that has no good option will silently pick the closest wrong one.

The output shape matters too. Asking for the code plus a short justification gives you a cheap review signal: when a reviewer disagrees with the code, the justification usually shows why in one line. Do not ask for a confidence score. Models are poorly calibrated on self-reported confidence, and a justification can be checked directly against the transcript.

Step 2: Pin the output shape and validate it

An agent that returns free text will eventually wrap the code in a sentence, add punctuation, or return two codes. Parse strictly on your side and fail loudly rather than guessing.

import json

def parse_code(raw: str, valid: set[str]) -> dict:
    try:
        obj = json.loads(raw)
    except json.JSONDecodeError as e:
        raise ValueError(f"not JSON: {raw[:120]}") from e
    code = obj.get("code")
    if code not in valid:
        raise ValueError(f"code not in list: {code!r}")
    return obj

Two rules on top of the parser. Reject anything that is not exactly one value from the valid set, and count every rejection. A rejection rate above a couple of percent usually means the output shape is under-specified, not that the model is bad. If your inference layer supports constrained generation, use it: the Label Studio comparison of automated error-detection methods shows constrained decoding against a fixed list of choices as a way to stop the model drifting outside the label set (Label Studio).

The same discipline applies when the agent is picking fields for a routing workflow rather than a single code. Tray's routing guide is blunt that classification should be read from the ticket content rather than a dropdown the requester guessed at, and that the priority a requester selects carries almost no information because everyone picks urgent (Tray). Your agent is that content-based classifier, so the taxonomy it uses has to be built from real conversations and defined precisely enough that a machine can apply it.

If you route to several models (a cheap one for volume, a stronger one for appeals), Quanta Inference gives you one OpenAI-compatible endpoint for text models with a single key and prepaid USD pricing, which keeps the code list in one place instead of per vendor: https://quantaintelligence.ai/inference

Step 3: Handle ambiguity and multi-label items

Ambiguity in reason codes has a small number of distinct causes, and each has a different fix. Diagnosing which one you have is the part most teams skip.

SymptomRoot causeFix
Agent flips between two codes on similar itemsTwo definitions overlapAdd an explicit tie-break rule naming both codes
Agent picks the same wrong code repeatedlyOne definition is too broadNarrow the definition or split the code
Agent picks NO_MATCH oftenMissing code for a real patternAdd a code, with examples
Justifications cite the wrong sentenceConversation is genuinely multi-issueDecide a primary vs secondary policy
Human reviewers disagree with each otherCode list does not match the workRedesign the taxonomy before automating

The overlap case is the most common. If BILLING_DUPLICATE and BILLING_REFUND_STATUS keep trading places, add one line: "If the customer mentions two identical charges, use BILLING_DUPLICATE even if they also ask for a refund." Naming both codes in the tie-break is what makes it usable. A generic instruction to "prefer the primary request" is not enough when both look primary.

For genuinely multi-label work, do not force a single field. Change the output shape to an ordered list and define the order.

{"codes": ["BILLING_DUPLICATE", "ACCESS_LOCKED"], "primary": "BILLING_DUPLICATE",
 "justification": "Two charges on the same day; the lockout is a side effect."}

Then score two things separately: set match (all codes correct, nothing extra) and primary-code match. Teams routinely find that primary accuracy is high while set accuracy is much lower, because the agent finds a second real issue that the human missed. That is not a model failure, it is a labeling policy gap, and it is worth knowing before you penalize the agent for it.

The customer target that makes this worth doing is large. Crisp's playbook cites a Gartner prediction that agentic AI will autonomously resolve 80 percent of common customer service issues without human intervention, cutting operational costs by 30 percent along the way. A code list the agent cannot apply consistently is the main thing standing between you and that number.

Step 4: Build the labeled test set

The test set is the whole project. Everything else is maintenance.

Start from real items, not synthetic ones. Pull a sample of recent conversations or tickets that have already been labeled by a person who read them, and have a second person independently re-label a subsampled share to measure human agreement. If two humans disagree on a code, that code is not ready for automation.

Fifty is a floor, not a target. A useful rule of thumb for a set of N codes is at least 15 items per code for the codes you care about most, plus a slice of NO_MATCH cases, plus the 10 to 15 items you already know are hard. A 50-item set across 12 codes means roughly four items per code, which will not tell you whether a rare code works.

Keep the set in a spreadsheet or a table like this, one row per item.

idinput (truncated)human_codeagent_codematchnote
1two charges same dayBILLING_DUPLICATEBILLING_DUPLICATEyes
2refund promised 9 days agoBILLING_REFUND_STATUSBILLING_REFUND_STATUSyes
3locked out, reset twiceACCESS_LOCKEDACCESS_LOCKEDyes
4charged twice, also locked outBILLING_DUPLICATEACCESS_LOCKEDnomulti-issue; policy unclear

Freeze the set. Every time you "fix" a test item to match the agent's output, you are training on your test set. Keep a separate small dev set for prompt iteration if you find yourself needing one. Lyzr's guide to golden datasets for agent evaluation treats the labeled set as ground truth rather than a scratchpad, and it stresses that the labels are what make measurement meaningful at all (Lyzr).

Human labeling is not overhead to be skipped; it is the mechanism that makes the whole system trustworthy. Google's overview of human-in-the-loop ML describes the standard ways people contribute to a model's lifecycle, from providing labels for training data to evaluating a model's predictions and feeding corrections back in (Google Cloud). Your test set is the second of those: humans evaluating predictions against a reference only they can supply.

Step 5: Measure match rate correctly

Exact-match accuracy on the code field is the headline number, but on its own it hides the failure that matters most: one code that never gets chosen correctly.

Use per-code precision and recall, and report the confusion pairs. Evidently's guide to LLM evaluation metrics lays out the classification metric set (accuracy, precision, recall and per-class breakdowns) as the reference-based approach for exactly this kind of task (Evidently AI).

A worked example. Suppose you have 60 test items across 6 codes, 10 each, and the agent matches 51 of them.

  • Overall match rate: 51/60 = 85 percent.
  • One code, BILLING_REFUND_STATUS, accounts for 5 of the 9 misses.
  • Excluding those 5, the agent is right on 51/55 = 93 percent of the rest.

That breakdown changes the decision. An 85 percent headline might tempt you to rewrite the whole prompt. The breakdown says the problem is one definition and probably one missing example. Fix that code, re-run, and expect the overall number to move toward 93 percent if the diagnosis was right.

Report stability as well. Run the same test set three times and look at the spread. An agent whose score moves several points between identical runs is not ready for unattended use, even if the average looks good. Anthropic's guidance on agent evals makes the point directly: outputs vary between runs, so multiple trials are needed to produce consistent results, and a task can carry several graders each with several checks (Anthropic).

One more metric worth tracking: justification agreement. Sample 20 correct matches and check whether the quoted evidence actually supports the code. A model that gets the right code for the wrong reason will break the first time the conversation format changes.

Step 6: Review mismatches and tighten, then stop

The loop is short and should be boring.

  1. Run the agent on the frozen set, three trials.
  2. Sort mismatches by code, then by confusion pair.
  3. For each cluster, read the justification and decide: overlap, too-broad definition, missing code, or a genuinely multi-issue item needing a policy decision.
  4. Make one change at a time: a tie-break line, a definition edit, a new example, or a new code.
  5. Re-run the full set. Compare overall match rate, per-code recall and stability.

Stop when the number stops moving for two consecutive changes, and when the leftover mismatches are items your human reviewers also disagree on. At that point more prompt text is not the lever.

Where the loop does not converge, the honest options are to route the uncertain codes to a person, or to reduce the taxonomy. A short, well-defined list of codes that the agent gets right is worth more than a comprehensive list it gets right 70 percent of the time. If uncertainty clusters around high-stakes cases, keep a person in the loop for those specific ones: human-in-the-loop design is about placing checkpoints where the cost of an error is high, not about reviewing everything (Plane). IBM's treatment of the same concept describes the loop as a two-way interaction in which human input actively shapes what the system does next, which is exactly what a reviewer's code correction should do to your agent (IBM).

A practical production guardrail: let the agent assign codes automatically where per-code recall on the test set is high, and send the rest to a review queue. Track how often reviewers change the agent's code. If that rate climbs, the conversation mix has shifted and the test set needs new hard cases.

If the reason codes are one part of a larger agent you need built end to end, Quanta pairs you with a specialist who builds it with you: https://quantaintelligence.ai/

FAQ

FAQ

How many labeled examples do I need before trusting the agent's codes?

Fifty is a workable floor for a small code list, but the useful number depends on coverage. Aim for at least 15 items per code you care about, plus a slice of hard and NO_MATCH cases, so that a single code's failure shows up in the per-code breakdown rather than being buried in an average.

Should the agent return one code or several?

Return one code unless the work is genuinely multi-issue, and define a primary when it is. If you allow several, score set match and primary match separately, because an agent that finds a real second issue will look wrong against a single-label human answer, and you need to know which of those is happening.

What do I do when the agent and the human label disagree?

Read the justification first. If it quotes a sentence that genuinely fits another code, you have an overlap problem and need a tie-break rule naming both codes. If it quotes nothing relevant, the definition is too vague or an example is missing. Only treat it as a model failure after those two are ruled out.

How often should I re-run the test set?

After every change to the instructions, the code list or the model behind the agent, and on a schedule thereafter. Run each item more than once, because output varies between runs and a single pass cannot tell you whether a score change is real or noise.

Can I skip the test set if the agent looks good in review?

No. Spot-checking shows you the cases you happen to open, and it will systematically miss the rare code that never gets chosen. A frozen, human-labeled set is the only way to see per-code recall and to know whether an instruction change helped or just moved the errors around.

Sources

Sources

  1. What is Agent Prompt Engineering? - by Paolo Perrone
  2. Do LLMs Adhere to Label Definitions? Examining Their ...
  3. LLM Evaluation: Comparing Four Methods to Automatically ...
  4. How to build support ticket routing
  5. How to Build an AI Agent for Customer Support: A Step-by- ...
  6. Golden Datasets for AI Agent Evaluation: How-To Guide
  7. What is Human-in-the-Loop (HITL) in AI and ML?
  8. LLM evaluation metrics and methods, explained simply
  9. Demystifying evals for AI agents
  10. What is human-in-the-loop AI?
  11. What Is Human In The Loop (HITL)?
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles