Skip to content
ExponaBLOG
PRACTICAL AI & MACHINE LEARNING · DAY 072 / 100

Choosing an LLM for your product for AI & ML developers

Quality, latency, cost and privacy.

Expona Admin8 min read

Choosing an LLM for your product for AI & ML developers

Building a product that relies on language models means making decisions that ripple through your system's performance, budget, and user experience. The question isn't "which LLM is best" — it's "which LLM is best for our specific constraints and use case." We'll walk through the framework that helps teams move past benchmarks and into production with confidence.

Understanding the Core Trade-offs

When evaluating language models, you're balancing four dimensions that almost never align perfectly: quality, latency, cost, and data privacy. The tension between these factors is real. A state-of-the-art model like GPT-4 delivers exceptional quality but costs significantly more per request and may have higher latency than a smaller alternative. Meanwhile, deploying an open-source model on your infrastructure offers privacy and cost control but requires you to manage the compute yourself.

The right approach is explicit comparison against your specific requirements, not aspirational ones. Ask your team these questions for each model you're considering:

  • Quality: Does it pass your evaluation set? How does it handle edge cases specific to your domain?
  • Latency: Is it fast enough for the UX you're building? A 2-second response time might be acceptable for an async batch job but unacceptable for a chat interface.
  • Cost: What is the cost per request at scale? Don't just look at published pricing — factor in volume discounts, token estimation, and retry logic.
  • Data: Where is data processed and stored? Does the model's privacy policy meet your compliance requirements?

For most teams, this means testing multiple models against your actual workload and measuring all four dimensions simultaneously. The "best" model is the one that meets all your thresholds while minimizing cost.

Evaluate on Your Data

Public benchmarks are useful as a starting point, but they rarely match your use case. A model that scores 92% on MMLU might perform poorly on your specific domain — financial summarization, medical coding, or customer support triage. The disconnect happens because benchmarks test general knowledge, not the specialized reasoning your product requires.

Set up an evaluation pipeline using your own data before committing to a model. Here's a practical approach:

  1. Create a test set of 50–200 examples that represent typical, edge case, and difficult scenarios your product will encounter.
  2. Define clear success criteria. For generation tasks, this might be a rubric scored by a domain expert. For classification, it's accuracy or F1 score.
  3. Run each candidate model against this set and measure quality, latency, and token usage.
  4. Calculate cost per successful request (accounting for retries or regenerations when quality is poor).

Let's say you're building a sales email classifier. You have 150 examples of emails that should be flagged as "follow-up required," "addressed," or "spam." GPT-4 might achieve 97% accuracy but cost $0.015 per request. GPT-3.5 might hit 91% but cost $0.001. If your threshold is 94% accuracy and you process 10,000 emails monthly, GPT-3.5 saves you $120/month but may miss critical follow-ups. The trade-off calculation now becomes concrete rather than theoretical.

Use a simple test harness to capture this data:

python
import json
from datetime import datetime
from anthropic import Anthropic

def evaluate_model(model_id, test_cases, system_prompt):
    client = Anthropic()
    results = []
    
    for test in test_cases:
        start_time = datetime.now()
        message = client.messages.create(
            model=model_id,
            max_tokens=500,
            system=system_prompt,
            messages=[{"role": "user", "content": test["input"]}]
        )
        latency = (datetime.now() - start_time).total_seconds()
        
        result = {
            "input": test["input"],
            "expected": test["expected"],
            "output": message.content[0].text,
            "model": model_id,
            "latency": latency,
            "tokens": message.usage.output_tokens,
            "cost": message.usage.output_tokens * 0.000003  # Example pricing
        }
        results.append(result)
    
    return results

# Run evaluation
test_set = [
    {"input": "Can you summarize this: ...", "expected": "A concise summary"},
    # ... more test cases
]

for model in ["claude-3-5-sonnet-20241022", "claude-3-haiku-20240307"]:
    results = evaluate_model(model, test_set, "You are a helpful assistant.")
    print(f"\n{model}:")
    print(f"  Avg latency: {sum(r['latency'] for r in results) / len(results):.2f}s")
    print(f"  Total tokens: {sum(r['tokens'] for r in results)}")
    print(f"  Estimated cost: ${sum(r['cost'] for r in results):.2f}")

This evaluation becomes your source of truth. It's reproducible, captures real-world characteristics, and justifies your decision to stakeholders.

Route by Task Complexity

Not every request needs your most powerful model. A single LLM in your system is leaving money on the table. Instead, match model capacity to task difficulty.

Simple tasks — entity extraction from structured text, straightforward classification, basic formatting — often work well with smaller, faster models like Claude Haiku or Llama 2 13B. Complex tasks — long-context reasoning, multi-step problem solving, nuanced understanding — warrant larger models like Claude 3.5 Sonnet or GPT-4.

The financial impact compounds at scale. If 60% of your requests are simple classification and you route them through a small model instead of your primary model, you'll cut your LLM costs significantly while improving latency.

Here's a concrete example: A customer support system might route queries as follows:

  • Haiku (small): Sentiment classification, FAQ matching, ticket routing — 40% of volume.
  • Sonnet (medium): Multi-turn conversations, product recommendations — 50% of volume.
  • Claude 3.5 (large): Complex dispute resolution, edge cases escalated from lower tiers — 10% of volume.

Implementing intelligent routing typically looks like this:

python
from enum import Enum

class TaskComplexity(Enum):
    SIMPLE = "simple"
    MODERATE = "moderate"
    COMPLEX = "complex"

def classify_task_complexity(user_message, context_length):
    # Heuristics: number of turns, message length, keywords
    indicators = {
        "simple": ["classify", "extract", "format", "yes or no"],
        "complex": ["why", "how would you", "given that", "consider"]
    }
    
    message_lower = user_message.lower()
    simple_score = sum(1 for ind in indicators["simple"] if ind in message_lower)
    complex_score = sum(1 for ind in indicators["complex"] if ind in message_lower)
    
    if context_length > 5000 or complex_score > simple_score:
        return TaskComplexity.COMPLEX
    elif context_length > 2000 or simple_score == 0:
        return TaskComplexity.MODERATE
    else:
        return TaskComplexity.SIMPLE

def route_to_model(user_message, conversation_history):
    complexity = classify_task_complexity(user_message, len(str(conversation_history)))
    
    routing = {
        TaskComplexity.SIMPLE: "claude-3-haiku-20240307",
        TaskComplexity.MODERATE: "claude-3-5-sonnet-20241022",
        TaskComplexity.COMPLEX: "claude-3-opus-20240229"
    }
    
    return routing[complexity]

Start with basic heuristics (message length, keyword matching, conversation turn count). As you gather data, train a lightweight classifier to predict optimal model assignment based on your actual success metrics.

Abstract the Provider and Model as Configuration

Your codebase should never hardcode "use GPT-4" or "use Anthropic." Tomorrow, a faster model ships. Next month, a cheaper alternative meets your quality bar. Your switching costs should be measured in configuration changes, not refactoring.

Build an abstraction layer that decouples your application logic from the specific provider and model. This pattern saves weeks of refactoring when you need to pivot:

python
from abc import ABC, abstractmethod
from dataclasses import dataclass

@dataclass
class LLMConfig:
    provider: str  # "anthropic", "openai", "together"
    model_id: str
    max_tokens: int
    temperature: float
    api_key: str

class LLMClient(ABC):
    @abstractmethod
    def generate(self, prompt, system_message=None):
        pass

class AnthropicClient(LLMClient):
    def __init__(self, config: LLMConfig):
        self.config = config
        self.client = Anthropic(api_key=config.api_key)
    
    def generate(self, prompt, system_message=None):
        response = self.client.messages.create(
            model=self.config.model_id,
            max_tokens=self.config.max_tokens,
            temperature=self.config.temperature,
            system=system_message,
            messages=[{"role": "user", "content": prompt}]
        )
        return response.content[0].text

class OpenAIClient(LLMClient):
    def __init__(self, config: LLMConfig):
        self.config = config
        from openai import OpenAI
        self.client = OpenAI(api_key=config.api_key)
    
    def generate(self, prompt, system_message=None):
        response = self.client.chat.completions.create(
            model=self.config.model_id,
            max_tokens=self.config.max_tokens,
            temperature=self.config.temperature,
            messages=[
                {"role": "system", "content": system_message or ""},
                {"role": "user", "content": prompt}
            ]
        )
        return response.choices[0].message.content

def get_llm_client(config: LLMConfig) -> LLMClient:
    clients = {
        "anthropic": AnthropicClient,
        "openai": OpenAIClient
    }
    return clients[config.provider](config)

# Usage in your application
config = LLMConfig(
    provider="anthropic",
    model_id="claude-3-5-sonnet-20241022",
    max_tokens=1024,
    temperature=0.7,
    api_key="sk-ant-..."
)

client = get_llm_client(config)
result = client.generate("Summarize this:", "You are helpful.")

Store configuration in environment variables or a configuration service. When you want to test a new model, update your config and re-run your evaluation suite. No code changes required.

Common Mistakes

  • Trusting benchmarks without validation on your data. A model's MMLU score tells you almost nothing about how it'll perform on your specific domain. Always evaluate locally.
  • Using the same model for all tasks. Routing by complexity can cut costs by 40–60% without quality degradation. It's worth the implementation effort.
  • Ignoring latency during selection. A 50% cheaper model is worthless if it doubles your response time and ruins UX. Measure latency under your expected load.
  • Hardcoding model names and API keys. You'll regret it when you want to switch providers or run A/B tests. Use configuration from day one.
  • Not accounting for token estimation. Published pricing is per token, but your actual tokens vary by model, prompt length, and output length. Calculate based on realistic usage.
  • Overlooking data residency and compliance. Some jurisdictions restrict where data can be processed. Clarify this early; it often eliminates options.

Key Takeaways

  • Compare models on your specific criteria — quality on your data, latency under your load, cost at your scale, and data handling that matches your compliance needs.
  • Build an evaluation harness that tests candidate models against a representative sample of your actual use cases, not public benchmarks.
  • Route requests to different models based on task complexity; using a single large model for all tasks wastes budget and latency.
  • Abstract your LLM provider and model as configuration so switching or testing new models requires only a config change, not code refactoring.
  • Re-evaluate your choice quarterly as new models ship and your workload evolves; the landscape changes fast.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles