An AI agent that actually does the thing.

Client

EU SaaS · anonymised

Industry

AI agents · LangGraph

Engagement

90 days

Crafts

LangGraph · Tool-use · Evals · Salesforce

bunduk · support-agent · production
live

The brief.

Two hundred support tickets a week. Most about billing: duplicate charges, refund requests, address changes, the usual. The support team was drowning, the model was already costing more than it saved, and demo-able AI was getting in the way of useful AI.

The brief: build the smallest agent that reliably closes one named class of tickets, refunds, with an eval set you can show the auditor, observability you can show the CFO, and a hand-off path that respects the user.

§ Architecture

A graph of five typed nodes.

Each node is its own function. Each transition is a typed contract. Failures route to a human, not the user. The whole thing fits on one page.

Inbound ticket
intake · queue
classify
intent · entities
fetch_context
salesforce · billing · KB
check_policy
rules · thresholds
resolve
tool-use · stripe / refund
handoff_human
if conf < 0.78
audit_log
tokens · cost · eval
resolved
reply + receipt
i/o node human hand-off

What we shipped.

A typed LangGraph agent with five nodes, three tools, and a hand-off node that knows when not to act. The model is small where it can be (classify on gpt-4o-mini) and large only where it must be (the resolve step). Every transition is a typed contract. A node returns a structured object, not free-form text, so the next node knows exactly what it's looking at.

The whole thing fits on one page of code. We'll show you.

agents/support_agent.py

LangGraph · 0.2.34
1from langgraph.graph import StateGraph, END
2from .tools import classify, fetch_order, check_policy, issue_refund, handoff
3
4# five nodes, fully typed
5graph = StateGraph(TicketState)
6graph.add_node("classify", classify)
7graph.add_node("fetch_context", fetch_order)
8graph.add_node("check_policy", check_policy)
9graph.add_node("resolve", issue_refund)
10graph.add_conditional_edges(
11  "check_policy", route_by_confidence,
12  {"high": "resolve", "low": "handoff"}
13)
14graph.add_node("handoff", handoff)
15app = graph.compile()  # ready in <120ms
ticket #84321 · refund · audit log
log
01# inbound ticket — Oct 14, 09:42 CET
02subject "Double charge on October invoice?"
03customer ord_84321 · plan_pro_annual
04
05classify intent=billing.refund_request · conf=0.94
06fetch_order ch_3OkW... €218.00 · 2 charges · Δt 4s
07check_policy rule=duplicate_charge · auto-refund: allowed
08resolve stripe.refund(ch_3OkW...) → €218.00 · ETA 3-5d
09handoff drafted reply · routed FYI → anna@support
10audit_log tokens=4205 · cost=€0.018 · latency=11.4s
11
12[done] ticket resolved · human-touched: 0 · sla: green
§ The eval set

A test suite for prompts.

Every change runs the eval set in CI. Regressions don't ship. The pass rate is the only metric we actually trust.

184
eval cases
real tickets · hand-labelled
96.2%
pass rate
last 30 days · ci-gated
1.9s
p95 latency
end-to-end · classify→resolve
€0.018
cost / turn
rolling 7-day average

We can finally point at the agent in a board meeting and show the numbers.

Engineering lead, EU client
§ Prompt versions

Ninety days, seven shipped versions.

  1. v1.0Day 1First shipped prompt. 71% pass.
  2. v1.1Day 6Added Dutch + EN templates for tone.+9.0%
  3. v1.2Day 14Re-rank with reciprocal rank fusion.+5.4%
  4. v1.3Day 22Lowered hand-off threshold to 0.78.+2.1%
  5. v2.0Day 41Switched classify to typed function-calls.+4.8%
  6. v2.1Day 67Cached fetch_order for hot SKUs.p95 → 1.9s
  7. v2.2Day 90Added cost ceiling alerter (Slack #ops).ops
§ Where the budget goes

Most of the cost is in resolve.

Six steps. One of them does most of the thinking. Classify and check_policy stay cheap on purpose. We want the heavy model to only see context it needs.

  • classify18%small model · gpt-4o-mini
  • fetch_context12%vector search + RAG
  • check_policy9%function call · no llm
  • resolve47%drafting · main model
  • handoff6%summary for human
  • audit_log8%tags · cost · trace

The outcome.

Two hundred tickets a week, zero hallucinations in ninety days. Median resolution time on refunds went from eleven minutes (human) to eleven seconds (agent + audit). The support team is back to handling the calls that actually need a human.

Most importantly: the eval set is the contract. Every prompt change is a PR with a CI run. Regressions don't ship. The team has a real, measurable artefact to point at when the question comes up: “does the AI actually work?” The answer is on the dashboard.

200/wk
Tickets fully closed by the agent
0
Hallucinations across 90 days in production
11s
Median resolution time, refund tickets
€0.018
Cost per resolved ticket (rolling 7-day)