Skip to content

Chapter 15: A/B Testing Prompts in Production

Theoretical Foundations

Imagine you are a chef in a bustling restaurant, and you've just invented a new recipe for your signature dish. You suspect this new recipe might be more popular with your customers, but you're not entirely sure. Instead of immediately replacing the old recipe, you decide to run a controlled experiment. For one month, half of your customers receive the old recipe (Version A), and the other half receive the new recipe (Version B). You meticulously track which version leads to more repeat orders, higher satisfaction scores, and fewer complaints. This is the essence of A/B testing: a methodical, data-driven approach to comparing two or more variations of a single element to determine which performs better against a specific goal.

In the world of AI, prompts are the recipes. A prompt is the set of instructions, context, and examples you give a Large Language Model (LLM) to generate a response. A slight change in wording, the addition of a new example, or a different structure can dramatically alter the model's output. A/B Testing Prompts in Production is the practice of systematically deploying different prompt variations to live user traffic and measuring their impact on key metrics. The "why" is rooted in the non-deterministic and highly sensitive nature of LLMs. Unlike traditional software where a function call with the same inputs yields the same output, LLMs can produce varied responses even with identical prompts. This makes intuition-based prompt engineering unreliable. A/B testing replaces guesswork with empirical evidence, allowing developers to optimize for user satisfaction, task completion rates, or even cost efficiency.

The Edge Runtime: A High-Performance Laboratory

To conduct these experiments effectively, especially in a real-time, user-facing application, we need a specialized environment. This is where the Edge Runtime comes into play. As defined earlier, an Edge Runtime is a highly performant, low-latency execution environment built on web standards like V8 Isolates. Think of it as a state-of-the-art, automated laboratory for our culinary experiment. Unlike a traditional server (a full-service kitchen) that might take a long time to "warm up" (cold start) and requires significant resources to run a single test, an Edge Runtime is like a collection of micro-labs. Each lab is lightweight, starts instantly, and is designed for one specific task.

In our context, the Edge Runtime is the ideal host for the A/B testing logic. It sits between the user's request and the LLM. When a request arrives, the Edge Runtime's first job is to act as a weighted router. It decides which prompt version (A or B) to send to the LLM for that specific user. This decision can be as simple as a 50/50 random split or as complex as a multi-armed bandit algorithm that dynamically allocates more traffic to the better-performing variant in real-time. The Edge Runtime's low-latency nature is critical here; it adds negligible overhead to the request-response cycle, ensuring the user doesn't perceive a delay while the experiment is being decided. Its global distribution means that no matter where your users are, the A/B testing logic runs close to them, minimizing network latency.

The Infrastructure: Ollama, Transformers.js, and WebGPU

Now, let's look at the tools that power our experiment. The prompt variations themselves are processed by an LLM. In this book, we're focusing on local LLMs, which offer benefits like data privacy, cost control, and offline capabilities. Ollama is the engine that runs these models on your local machine or server. It's like the specialized cooking equipment in our kitchen—powerful, efficient, and designed to handle complex recipes (prompts) and produce high-quality dishes (responses).

Transformers.js is the JavaScript library that allows us to interact with these models directly from our code. It acts as the translator between our application logic and the LLM engine. When the Edge Runtime decides to use prompt variation B, it uses Transformers.js to send that prompt to the Ollama model and receive the response. This integration is seamless and allows for a unified development experience within the JavaScript ecosystem.

Finally, WebGPU is the performance accelerator. Running LLMs, even locally, is computationally intensive. WebGPU is a modern web API that provides direct access to the graphics processing unit (GPU) of the user's device. Think of it as upgrading from a manual whisk to a high-speed industrial blender. By leveraging the GPU's massively parallel architecture, WebGPU dramatically reduces the time it takes for the LLM to generate a response (inference latency). In an A/B testing scenario, where you might be running thousands of comparisons per minute, this performance boost is not just a luxury—it's a necessity to keep the system responsive and scalable.

The Data Pipeline: From Inference to Insight

Collecting the right data is what separates a scientific experiment from a random guess. The Edge Runtime is not just a router; it's also a data collection point. For every request, it logs which prompt variation was used, the user's input, the model's output, and a unique identifier for the session. This is analogous to a chef meticulously noting down which table received which recipe and collecting feedback cards.

These metrics are the foundation for calculating statistical significance. We're not just looking for which version got a "better" response; we need to prove that the difference is not due to random chance. Key metrics to collect include: * Latency: How long did it take to get a response? This is directly impacted by WebGPU. * User Engagement: Did the user click a button, continue the conversation, or provide a thumbs-up/down? * Task Success Rate: For a specific task (e.g., "summarize this article"), did the model's output meet a predefined quality threshold? * Cost: For models with different pricing tiers, which prompt is more cost-effective?

The data is streamed from the Edge Runtime to a persistent store (like a time-series database) where it can be analyzed. Statistical tests, such as a t-test for continuous metrics (like latency) or a chi-squared test for categorical metrics (like click-through rate), are then applied to determine if the observed difference between Version A and Version B is statistically significant (e.g., with a p-value < 0.05). This rigorous analysis ensures that the "winning" prompt is genuinely better, not just a fluke.

The A/B Testing Workflow: A Visual Breakdown

The entire process can be visualized as a flow of data and decisions. The user's request enters the system, gets routed, processed by the LLM, and the results are fed back for analysis.

A user request flows through the system, where it is routed to the LLM for processing, and the resulting data is returned for analysis to optimize the A/B test.
Hold "Ctrl" to enable pan & zoom

A user request flows through the system, where it is routed to the LLM for processing, and the resulting data is returned for analysis to optimize the A/B test.

Web Development Analogy: Feature Flags vs. Prompt A/B Testing

A common pattern in web development is the use of feature flags to toggle new features on or off for users. This is like the chef deciding to permanently switch to the new recipe for everyone. Prompt A/B testing is a more sophisticated evolution of this concept. Instead of a simple on/off switch, it's a dynamic, multi-variant experiment.

Think of it this way: * Feature Flags are like a light switch: either the old feature (bulb A) is on, or the new feature (bulb B) is on. * Prompt A/B Testing is like a dimmer switch with multiple light sources: you can have 30% of the light from Bulb A and 70% from Bulb B, and you can adjust this ratio in real-time based on which bulb produces better light quality (user satisfaction).

Furthermore, while feature flags typically control entire code paths, prompt A/B testing operates at a higher level of abstraction—the semantic layer. It allows you to experiment with the behavior of your AI without changing the underlying model or application architecture. This is a powerful paradigm shift, enabling continuous, data-driven refinement of AI interactions, much like how modern web teams continuously deploy and refine their user interfaces based on A/B test results. The Edge Runtime serves as the orchestrator for this sophisticated, real-time experiment, ensuring that every user interaction is both a service and a data point for improvement.

Basic Code Example

This example demonstrates a minimal, self-contained Node.js server using Express.js to perform A/B testing on LLM prompts. We will simulate two prompt variations (A and B) for a "summarization" task. The server will route incoming requests using a weighted random strategy, call a local LLM via Ollama, and return the result. This mimics a production SaaS environment where you need to test prompt effectiveness without disrupting the user experience.

Prerequisites

To run this code, you need: 1. Node.js (v18+). 2. Ollama installed and running locally with a model pulled (e.g., ollama pull llama3.2). 3. Dependencies: express, axios. * Install via: npm install express axios

The Code

// Import necessary modules
import express, { Request, Response } from 'express';
import axios from 'axios';

// --- Configuration & Types ---

/**
 * Defines the structure of a prompt variation.
 * @property id - Unique identifier for the variation (e.g., 'A', 'B').
 * @property prompt - The actual text prompt template.
 * @property weight - The probability weight for routing (e.g., 0.5 for 50%).
 */
interface PromptVariation {
  id: string;
  prompt: string;
  weight: number;
}

/**
 * Configuration for the A/B test.
 * We are testing two summarization prompts.
 */
const PROMPT_VARIATIONS: PromptVariation[] = [
  {
    id: 'A',
    prompt: 'Summarize the following text in a single sentence: {text}',
    weight: 0.5,
  },
  {
    id: 'B',
    prompt: 'Extract the main point of this text in 10 words or less: {text}',
    weight: 0.5,
  },
];

// --- Core Logic: Weighted Routing ---

/**
 * Selects a prompt variation based on weighted random probability.
 * 
 * Algorithm:
 * 1. Generate a random number between 0 and 1.
 * 2. Iterate through variations, accumulating weights.
 * 3. Return the variation where the accumulated weight exceeds the random number.
 * 
 * @param variations - Array of prompt variations with weights.
 * @returns The selected variation object.
 */
function selectPromptVariation(variations: PromptVariation[]): PromptVariation {
  const totalWeight = variations.reduce((sum, v) => sum + v.weight, 0);
  const random = Math.random() * totalWeight;

  let accumulated = 0;
  for (const variation of variations) {
    accumulated += variation.weight;
    if (random < accumulated) {
      return variation;
    }
  }

  // Fallback (should not happen if weights sum to 1)
  return variations[0];
}

// --- LLM Integration (Ollama) ---

/**
 * Calls the local Ollama instance to generate text.
 * 
 * @param prompt - The formatted prompt string.
 * @returns The generated text response.
 */
async function callLocalLLM(prompt: string): Promise<string> {
  const OLLAMA_URL = 'http://localhost:11434/api/generate';

  try {
    const response = await axios.post(OLLAMA_URL, {
      model: 'llama3.2', // Ensure this model is pulled in Ollama
      prompt: prompt,
      stream: false, // We want the full response at once for this simple example
    });

    // Ollama returns a response object containing the generated text
    return response.data.response;
  } catch (error) {
    console.error('Error calling Ollama:', error);
    throw new Error('LLM inference failed');
  }
}

// --- Express Server Setup ---

const app = express();
app.use(express.json());

/**
 * Route: POST /summarize
 * 
 * Body: { "text": "The text to summarize..." }
 * 
 * Logic:
 * 1. Receive text input.
 * 2. Select prompt variation (A or B).
 * 3. Format prompt with input text.
 * 4. Call LLM.
 * 5. Return result along with the variation ID for tracking.
 */
app.post('/summarize', async (req: Request, res: Response) => {
  const { text } = req.body;

  if (!text) {
    return res.status(400).json({ error: 'Text field is required' });
  }

  try {
    // 1. A/B Test Routing
    const variation = selectPromptVariation(PROMPT_VARIATIONS);

    // 2. Prompt Formatting
    const finalPrompt = variation.prompt.replace('{text}', text);

    // 3. Inference
    const result = await callLocalLLM(finalPrompt);

    // 4. Response with Metadata
    res.json({
      summary: result,
      variationId: variation.id, // Crucial for logging and analysis
      promptUsed: finalPrompt,  // Optional: useful for debugging
    });
  } catch (error) {
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Start Server
const PORT = 3000;
app.listen(PORT, () => {
  console.log(`A/B Testing Server running on http://localhost:${PORT}`);
});

Line-by-Line Explanation

1. Imports and Type Definitions

import express, { Request, Response } from 'express';
import axios from 'axios';
* Why: We use express for the web server framework and axios to make HTTP requests to the Ollama API. * Types: We define a PromptVariation interface. In TypeScript, this ensures that our variation objects strictly adhere to having an id, prompt, and weight. This prevents runtime errors caused by typos or missing properties.

2. Configuration (PROMPT_VARIATIONS)

const PROMPT_VARIATIONS: PromptVariation[] = [ ... ];
* The Setup: This array acts as our "experiment configuration." * Prompt A: Uses a standard instruction ("Summarize..."). * Prompt B: Uses a more constrained instruction ("10 words or less"). * Weights: Both are set to 0.5. In a real scenario, you might start with 50/50, but if Prompt B proves superior early, you might shift the weight to 0.9 (90% traffic) to maximize the benefit.

3. Weighted Routing Logic (selectPromptVariation)

function selectPromptVariation(variations: PromptVariation[]): PromptVariation {
  const totalWeight = variations.reduce((sum, v) => sum + v.weight, 0);
  const random = Math.random() * totalWeight;
  // ... loop logic
}
* Under the Hood: This implements a Weighted Random Selection algorithm. 1. We calculate totalWeight (e.g., 0.5 + 0.5 = 1.0). 2. We generate a random float between 0 and 1. 3. We iterate through the list. If random is 0.3, and Variation A takes up the first 0.5 of the range, Variation A is selected. * Edge Case: If weights don't sum to 1.0 (e.g., 0.5 and 0.5 = 1.0), the math still holds because Math.random() * totalWeight scales the range. If the weights were 10 and 20, Math.random() * 30 ensures the probability matches the ratio.

4. LLM Integration (callLocalLLM)

async function callLocalLLM(prompt: string): Promise<string> {
  const OLLAMA_URL = 'http://localhost:11434/api/generate';
  // ... axios.post logic
}
* The Request: We target the standard Ollama API endpoint. We set stream: false to simplify the example; this waits for the full completion before sending the response back to the client. * Error Handling: We wrap the call in a try/catch. If Ollama is down or the model fails, we catch the error and throw a generic one to prevent leaking internal stack traces to the client.

5. The API Endpoint (POST /summarize)

app.post('/summarize', async (req: Request, res: Response) => { ... });
* Step 1: Routing: We immediately call selectPromptVariation. This happens before any expensive LLM inference, ensuring the routing logic is lightweight. * Step 2: Formatting: We inject the user's text into the prompt template using .replace(). * Step 3: Inference: We await the LLM call. * Step 4: Metadata Injection: The response includes variationId. This is critical. Without logging this ID to a database (e.g., Postgres or ClickHouse), you cannot calculate statistical significance later. You need to know which prompt generated which result to compare user feedback or performance metrics.

Visualizing the Data Flow

The following diagram illustrates the request lifecycle in this A/B testing server.

The diagram traces a user's request through a routing layer that assigns a prompt variant, generates an AI response, and logs the specific pairing of prompt and result for later A/B testing analysis.
Hold "Ctrl" to enable pan & zoom

The diagram traces a user's request through a routing layer that assigns a prompt variant, generates an AI response, and logs the specific pairing of prompt and result for later A/B testing analysis.

Common Pitfalls

When implementing A/B testing for LLM prompts in a production environment, watch out for these specific JavaScript/TypeScript issues:

1. Async/Await Loops and Concurrency * The Issue: If you use Promise.all or concurrent processing for high-throughput traffic, ensure your logging mechanism (Step 6 in the diagram) is non-blocking. Writing to a database synchronously inside the request loop will cause the server to hang. * The Fix: Use a fire-and-forget pattern for logging or a dedicated message queue (like Redis or Kafka) to decouple inference from analytics.

2. Hallucinated JSON / Schema Drift * The Issue: LLMs are non-deterministic. If you ask an LLM to return JSON directly, it might return a string, a code block, or malformed JSON. * The Fix: Never rely on the LLM to format the API response. Always wrap the LLM's raw text output in a standard TypeScript interface on the server side. Do not pass the LLM's output directly to res.json() without validation (e.g., using Zod).

3. Vercel/AWS Lambda Timeouts * The Issue: If you host this on serverless platforms, the default timeout is often 10 seconds. Local LLM inference can take longer, especially on CPU-only hardware. * The Fix: Increase the timeout limit (e.g., maxDuration in Vercel) or implement streaming. Streaming allows the client to receive tokens as they are generated, keeping the connection alive and preventing timeout errors, even if the total generation time is long.

4. Memory Leaks in Node.js Streams * The Issue: If you modify this example to use stream: true (to reduce latency), you must handle the stream consumption correctly. Failing to consume the stream or closing it improperly can lead to memory leaks in the Node.js event loop. * The Fix: Always ensure streams are piped to a destination or explicitly destroyed in the finally block of a try/catch statement.

5. Statistical Insignificance * The Issue: Switching traffic weights too quickly based on early results. If Variation A gets a good result in the first 10 requests, you might be tempted to route 100% of traffic there. * The Fix: Implement a minimum sample size (e.g., 100 requests per variation) and a confidence interval check (e.g., 95%) before declaring a winner. Do not rely on simple averages in the code; use a statistical library (like z-score calculations) in your analytics pipeline.

The chapter continues with advanced code, exercises and solutions with analysis, you can find them on the ebook on Leanpub.com or Amazon


Loading knowledge check...



Code License: All code examples are released under the MIT License. Github repo.

Content Copyright: Copyright © 2026 Edgar Milvus | Privacy & Cookie Policy. All rights reserved.

All textual explanations, original diagrams, and illustrations are the intellectual property of the author. To support the maintenance of this site via AdSense, please read this content exclusively online. Copying, redistribution, or reproduction is strictly prohibited.