How to Classify Text with LLMs in R

llm
ellmer
text classification
Learn to classify text into categories using LLMs in R. Categorize documents, tag content, and label data without training ML models.
Published

April 4, 2026

Introduction

Text classification assigns categories to text. Traditional approaches require labeled training data and ML models. With LLMs, you can classify text with just a prompt - no training needed.

Common use cases:

  • Categorize support tickets (billing, technical, account)
  • Tag documents by topic (finance, legal, HR)
  • Classify emails by intent (inquiry, complaint, feedback)
  • Label survey responses by theme

Why Use LLMs for Classification?

Approach Training Data Setup Time Flexibility
Traditional ML Thousands of examples Days/weeks Fixed categories
LLMs Zero examples Minutes Change categories anytime

LLMs excel when:

  • You don’t have labeled training data
  • Categories may change over time
  • You need quick prototyping

Getting Started

Load the ellmer package for LLM access:

library(ellmer)
library(tidyverse)

This tutorial works with any provider. See ellmer basics for setup.

Basic Classification

Single category classification

Start with a simple prompt that asks the LLM to classify text:

chat <- chat_claude()

text <- "I can't log into my account and I've tried resetting my password three times."

response <- chat$chat(paste(

  "Classify this support ticket into one category:",
  "billing, technical, account, or other.",
  "Reply with just the category name.",
  "\n\nTicket:", text
))

response
# "account"

The prompt has three parts:

  1. Task: “Classify this support ticket”
  2. Categories: “billing, technical, account, or other”
  3. Format: “Reply with just the category name”

Why “just the category name”?

Without format instructions, LLMs tend to explain their reasoning:

# Without format instruction - verbose response
chat$chat("Classify this as billing/technical/account: I can't log in")
# "This appears to be an account-related issue because..."

# With format instruction - clean response
chat$chat("Classify as billing/technical/account. One word only: I can't log in")
# "account"

Clean responses are easier to process programmatically.

Structured Classification

Using extract_data for reliable output

For production use, extract_data() guarantees structured output:

# Define the allowed categories
category_type <- type_enum(
  values = c("billing", "technical", "account", "other"),
  description = "Support ticket category"
)

The type_enum() ensures the response is one of the allowed values.

Extract the category

chat <- chat_claude()

ticket <- "Why was I charged twice this month? Please refund the extra charge."

category <- chat$extract_data(ticket, type = category_type)

category
# "billing"

This approach:

  • Guarantees valid output (no unexpected categories)
  • Returns clean data (no extra text)
  • Works reliably in automated pipelines

Multi-label Classification

When text belongs to multiple categories

Some text fits multiple categories. Use an array type:

categories_type <- type_array(
  items = type_enum(
    values = c("urgent", "billing", "technical", "account", "feedback"),
    description = "Applicable category"
  ),
  description = "All categories that apply to this ticket"
)

Classify with multiple labels

chat <- chat_claude()

ticket <- "URGENT: My payment failed and now I'm locked out of my account!"

labels <- chat$extract_data(ticket, type = categories_type)

labels
# ["urgent", "billing", "account"]

The ticket is urgent, involves billing (payment failed), and account access.

Classification with Confidence

Get confidence scores

Sometimes you want to know how confident the model is:

classification_type <- type_object(
  category = type_enum(
    values = c("billing", "technical", "account", "other"),
    description = "Primary category"
  ),
  confidence = type_number("Confidence score from 0 to 1"),
  reasoning = type_string("Brief explanation for the classification")
)

Extract classification with confidence

chat <- chat_claude()

ticket <- "The app crashes when I try to upload files larger than 10MB"

result <- chat$extract_data(ticket, type = classification_type)

result$category
# "technical"

result$confidence
# 0.95

result$reasoning
# "This describes a software bug related to file uploads"

Low confidence scores indicate ambiguous cases that may need human review.

Batch Classification

Create a reusable classifier

Wrap classification in a function for reuse:

classify_ticket <- function(text, chat = NULL) {
  if (is.null(chat)) {
    chat <- chat_claude()
  }

  category_type <- type_enum(
    values = c("billing", "technical", "account", "other"),
    description = "Ticket category"
  )

  chat$extract_data(text, type = category_type)
}

Classify multiple texts

Use purrr’s map() to process a vector of texts:

library(purrr)

tickets <- c(
 "I was charged twice for my subscription",
 "The export feature isn't working",
 "How do I change my email address?",
 "Your product is amazing, thank you!"
)

# Add delay to respect rate limits
categories <- map_chr(tickets, \(ticket) {
 Sys.sleep(0.5)
 classify_ticket(ticket)
})

categories
# ["billing", "technical", "account", "other"]

The Sys.sleep(0.5) prevents hitting API rate limits.

Create a classified data frame

tibble(
 ticket = tickets,
 category = categories
)
ticket category
I was charged twice… billing
The export feature… technical
How do I change… account
Your product is… other

Hierarchical Classification

Two-level categories

For complex taxonomies, classify in stages:

# First level: main category
main_category <- type_enum(
  values = c("technical", "billing", "account"),
  description = "Main category"
)

# Second level: subcategories for technical issues
technical_subcategory <- type_enum(
  values = c("bug", "feature_request", "how_to", "performance"),
  description = "Technical subcategory"
)

Two-stage classification

classify_hierarchical <- function(text) {
  chat <- chat_claude()

  # Stage 1: Main category
  main <- chat$extract_data(text, type = main_category)

  # Stage 2: Subcategory (only for technical)
  sub <- if (main == "technical") {
    chat$extract_data(text, type = technical_subcategory)
  } else {
    NA
  }

  list(category = main, subcategory = sub)
}

result <- classify_hierarchical("The dashboard loads very slowly")
# $category: "technical"
# $subcategory: "performance"

Custom Categories

Define your own taxonomy

LLMs work with any categories you define:

# Document types
doc_type <- type_enum(
  values = c("contract", "invoice", "report", "memo", "other"),
  description = "Document type"
)

# Email intents
email_intent <- type_enum(
  values = c("inquiry", "complaint", "request", "thank_you", "spam"),
  description = "Email intent"
)

# News topics
news_topic <- type_enum(
  values = c("politics", "business", "technology", "sports", "entertainment"),
  description = "News article topic"
)

Use with any text

chat <- chat_claude()

article <- "Apple announced record quarterly earnings driven by iPhone sales in Asia."

topic <- chat$extract_data(article, type = news_topic)
# "business"

Improving Accuracy

Add context to the system prompt

Provide domain context for better classification:

chat <- chat_claude(
  system_prompt = "You are a customer support classifier for a SaaS company.

  Category definitions:
  - billing: payments, charges, refunds, subscriptions, pricing
  - technical: bugs, errors, features not working, integrations
  - account: login, password, profile, settings, permissions
  - other: feedback, partnerships, general questions"
)

category_type <- type_enum(
  values = c("billing", "technical", "account", "other"),
  description = "Ticket category"
)

ticket <- "Can you add dark mode to the app?"

chat$extract_data(ticket, type = category_type)
# "other" (feature request, not a bug)

Provide examples (few-shot)

Include examples in the prompt for tricky cases:

chat <- chat_claude(
  system_prompt = "Classify support tickets. Examples:

  'Charge me yearly instead of monthly' -> billing
  'Button doesn't work on mobile' -> technical
  'Reset my 2FA' -> account
  'Love your product!' -> other"
)

Error Handling

Handle API failures gracefully

safe_classify <- function(text) {
  tryCatch({
    chat <- chat_claude()
    category_type <- type_enum(
      values = c("billing", "technical", "account", "other"),
      description = "Category"
    )
    chat$extract_data(text, type = category_type)
  }, error = function(e) {
    warning("Classification failed: ", e$message)
    NA_character_
  })
}

# Use with map
results <- map_chr(tickets, safe_classify)

# Filter out failures
valid_results <- results[!is.na(results)]

Local Classification

Use Ollama for free, private classification

For sensitive data or high volume, use local models:

chat <- chat_ollama(model = "llama3.2")

category_type <- type_enum(
  values = c("billing", "technical", "account", "other"),
  description = "Category"
)

chat$extract_data("I need a refund", type = category_type)
# "billing"

See Local LLMs with Ollama for setup.

Note: Local models may be less accurate than cloud APIs for nuanced classification.

Performance Tips

Batch similar classifications

Process items in batches when possible:

# Instead of classifying one at a time
# Classify multiple items with context

batch_classify <- function(texts) {
  chat <- chat_claude()

  # Build numbered list
  numbered <- paste(seq_along(texts), texts, sep = ". ", collapse = "\n")

  prompt <- paste(
    "Classify each item as billing/technical/account/other.",
    "Return a JSON array of categories in order.",
    "\n\n", numbered
  )

  # Parse JSON response
  jsonlite::fromJSON(chat$chat(prompt))
}

Use cheaper models for simple tasks

# Use Haiku for simple classification
chat <- chat_claude(model = "claude-haiku-4-5")

# Use Opus only for complex multi-label tasks
chat <- chat_claude(model = "claude-opus-4-6")

Summary

Task Code
Simple classification chat$chat("Classify as X/Y/Z: text")
Structured output chat$extract_data(text, type_enum(...))
Multi-label type_array(items = type_enum(...))
With confidence type_object(category = ..., confidence = ...)
Batch processing map_chr(texts, classify_fn)

Key points:

  • Use type_enum() to guarantee valid categories
  • Add system prompts with category definitions for accuracy
  • Use Sys.sleep() in batch processing for rate limits
  • Consider local models for sensitive or high-volume data

Sources