Training an LLM Email Triage Agent with GRPO Reinforcement Learning
TL;DR: We built an OpenEnv-based RL environment that teaches a 3B LLM to automatically triage a corporate inbox β classifying urgency, routing to the right team, and drafting replies. Using GRPO, the agent improved from an avg reward of 3.47 β 4.10+ over 200 training steps, with correct routing jumping from ~40% to ~85%.
The Problem
Corporate inboxes are chaos. Every day, support teams manually read hundreds of emails β figuring out if something is urgent, who should handle it, and what to reply. A hacked account alert needs immediate escalation. A lottery spam needs to go straight to trash. An overdue invoice needs the billing team. A sales lead needs the sales team.
Getting this wrong is expensive: SLA breaches, missed leads, frustrated customers. Getting it right manually is time-consuming and error-prone.
We built an RL environment to teach an LLM to do this automatically β and then actually trained it.
What We Built
An OpenEnv-based email triage environment where an LLM agent receives a corporate inbox and must triage each email by:
- Classifying it β spam? billing inquiry? security incident? sales lead?
- Prioritizing it β urgent / high / medium / low / spam
- Routing it β to billing, sales, support tier 1/2, HR, trash, archive...
- Drafting a professional reply to the sender
What Makes It Interesting
The environment has real constraints that force the agent to reason carefully:
SLA Warnings β urgent emails must be handled within 2 steps or you get penalized. The agent sees active SLA warnings in its observation and must prioritize accordingly.
Escalation Budget β only 3 emails per session can be flagged for human review. Flag spam or low-priority emails and you waste your budget, getting penalized on legitimate escalations later. The agent must learn to be selective.
Sequential Decisions β 5 emails per episode, processed one at a time. Each decision affects the remaining state (queue capacity, escalation budget).
Multi-dimensional Reward β the environment scores each action across priority accuracy, category correctness, routing quality, reply draft quality (20% of score), and SLA compliance. There's no single shortcut to game.
Environment Design
Observation:
current_email β the email to triage now
inbox β all emails in the episode
escalation_budget β how many flag_review actions remain
sla_warnings β emails approaching deadline
team_queues β routing capacity per destination
Action:
email_id β which email (must match exactly)
priority β urgent | high | medium | low | spam
category β customer_complaint | billing_inquiry |
technical_support | sales_lead | internal_hr |
legal_compliance | spam_phishing | general_inquiry
route_to β support_tier1 | support_tier2 | billing | sales |
legal | hr | management | trash | archive
summary β β€280 char description
flag_review β true/false (uses escalation budget)
reply_draft β professional reply to sender
Reward:
Per email: priority (~0.2) + category (~0.2) + routing (~0.3)
+ reply quality (~0.2) + escalation appropriateness (~0.1)
Penalties: SLA breach, invalid field values, budget overuse
Max score: ~4.5 per episode (5 emails Γ ~0.9 each)
Training with GRPO
We used GRPO (Group Relative Policy Optimization) via HuggingFace TRL
to train a Qwen/Qwen2.5-3B-Instruct model with LoRA adapters.
Why GRPO?
GRPO is well-suited for this task because:
- It doesn't need a learned reward model β the environment IS the verifier
- It compares episode pairs and reinforces better decisions relatively
- It's more compute-efficient than PPO for small models
Why LoRA?
We only update ~2M out of 3B parameters using LoRA (r=4, targeting q_proj and v_proj). The base model already knows language, reasoning, and JSON formatting from pretraining on 18T tokens. LoRA just steers that existing knowledge toward email triage specifically.
Training Configuration
MODEL_NAME = "Qwen/Qwen2.5-3B-Instruct"
TRAINING_STEPS = 200
BATCH_SIZE = 1
NUM_GENERATIONS = 4 # episodes compared per GRPO step
learning_rate = 2e-6
lora_r = 4
lora_alpha = 8
target_modules = ["q_proj", "v_proj"]
lr_scheduler = "cosine"
warmup_steps = 15
The Training Loop
For each step:
1. Run 4 episodes through the email environment
2. Environment scores each decision (priority + category + routing + reply)
3. GRPO compares: which episodes scored higher?
4. Advantage = (reward - mean) / std per group
5. Update LoRA weights: reinforce high-reward decisions,
discourage low-reward ones
6. Repeat 200 times
Key Engineering Decisions
1. Chat Template is Non-Negotiable
Using apply_chat_template() was the single biggest fix in the entire
project. Without it, even a 7B instruct model produces garbage β the model
expects a specific <|im_start|>system\n...<|im_end|> format that was baked
in during instruction fine-tuning. Sending raw text completely bypasses this.
2. Validation as a Safety Net
We built a validate_action() function that catches and corrects invalid
outputs before they reach the environment:
# Catches: invented categories like "sales_leads", "spammer_alert"
# Catches: array-wrapped JSON [{...}] instead of objects {...}
# Catches: null reply_drafts
# Catches: route/category mismatches (e.g. spam β sales)
# Catches: escalation budget abuse (flagging spam for human review)
# Infers: correct values from email body keywords when model fails
This lifted the reward floor from 2.78 to 3.60 β the most impactful single change after the chat template fix.
3. Escalation Budget Management
The model initially flagged spam and low-priority emails for human review,
exhausting the escalation budget early and crashing episode rewards to ~0.45.
We added explicit budget tracking and a rule: only flag urgent or high
priority non-spam emails. This eliminated the reward crashes entirely.
4. model.eval() During Rollout
GRPO trains the model while simultaneously using it for inference. Without
explicitly calling model.eval() during rollouts and model.train() after,
batch normalization and dropout behave incorrectly, causing unstable
generation during training steps.
Results
Reward Curve
The agent improved from 3.47 β 4.10+ average reward over 200 steps β an 18% improvement. More importantly, the floor lifted from 2.78 to 3.60, meaning the agent became consistently reliable, not just occasionally good.
Key Metrics
| Metric | Baseline | After Training |
|---|---|---|
| Avg episode reward | 3.47 | 4.10+ |
| Best episode reward | 3.65 | 4.41 |
| Worst episode reward | 2.78 | 3.60 |
| Final evaluation score | β | 4.189 / 5.0 |
| Correct routing | ~40% | 100% (5/5) |
| Null reply drafts | frequent | eliminated |
| Invalid categories | occasional | eliminated |
| Escalation budget abuse | frequent | eliminated |
Before vs After β Routing Comparison (Final Evaluation)
| Untrained Agent | Trained Agent | Reward | |
|---|---|---|---|
| π΄ Account hacked | general_inquiry β support_tier1 | customer_complaint β support_tier2 β | 0.705 |
| ποΈ Lottery spam | urgent β support_tier1 | spam_phishing β trash β | 0.840 |
| π₯ Team lunch | low β trash | internal_hr β hr β | 0.988 |
| π° Overdue invoice | medium β support_tier1 | billing_inquiry β billing β | 0.798 |
| π Enterprise lead | general_inquiry β archive | sales_lead β sales β | 0.858 |
| Total | 4.189 / ~5.0 |
Key Lessons Learned
1. Model size matters more than you think for structured output. The 0.5B model produced pure garbage (LaTeX, Chinese exam questions, C++ code). The 1.5B was marginal. The 3B consistently produced valid JSON from step 1. For tasks requiring strict output format, don't go below 3B.
2. The reward floor matters as much as the average. A model that scores 4.0 on average but occasionally crashes to 0.4 is less useful than one that scores 3.8 consistently. Most of our engineering effort went into lifting the floor, not the ceiling.
3. RL on small fixed datasets still works. With only 5 fixed emails, we worried the model would memorize rather than generalize. But GRPO's relative reward signal still found meaningful gradients. The model learned general routing principles β spam goes to trash, billing goes to billing β not just specific email answers.
4. Validation beats prompting for reliability.
No matter how carefully you word the system prompt, a 3B model will
occasionally output sales_leads instead of sales_lead, or wrap JSON in
an array. A deterministic validation function that catches and corrects these
errors is far more reliable than trying to prompt your way out of them.
5. Environment constraints create richer learning signals. The escalation budget and SLA warnings force the model to reason about trade-offs, not just classify emails in isolation. This is what makes the environment genuinely interesting from an RL perspective β the agent has to learn a policy, not just a lookup table.
Try It Yourself
Environment: HF Space
Training notebook: Open in Colab β add your link
wandb run: Live metrics β add your link
import requests
BASE_URL = "https://Vansh04092003-multi-agent-email-env2.hf.space"
# Start a fresh episode
obs = requests.post(f"{BASE_URL}/reset").json()["observation"]
print(f"First email: {obs['current_email']['header']['subject']}")
# Submit a triage decision
action = {
"email_id": "e001",
"priority": "urgent",
"category": "customer_complaint",
"route_to": "support_tier2",
"summary": "Account hacked, needs immediate lock.",
"flag_review": True,
"reply_draft": "We are securing your account immediately."
}
result = requests.post(f"{BASE_URL}/step", json=action).json()
print(f"Reward: {result['reward']:.3f}")
What's Next
- More email diversity: The current environment has 5 fixed emails. Adding procedurally generated emails would make the learned policy more robust and generalizable.
- Larger model: Upgrading to Qwen2.5-7B or Qwen3-8B would likely push the reward ceiling from 4.4 toward 4.8+.
- Process-level rewards: Currently rewards are per-email. Adding step-level rewards for good reasoning chains (thinking tokens) could improve sample efficiency.
- Multi-agent extension: Multiple agents collaborating on inbox triage, with handoffs between support tiers β a natural extension of the current environment toward Theme #1.
Built for the OpenEnv Hackathon India 2026 β Theme #3.2 Personalized Tasks.
Environment: Vansh04092003/multi-agent-email-env2
