There Is No Application · Chapter 11

Testing and Debugging Non-Deterministic Systems

You've built your agent. It works on your machine. Now, how do you prove it's ready for production? How do you test a system when its core component—the LLM—doesn't produce the same output every time?

This is one of the biggest mental shifts for developers moving into the AI space. You cannot write a simple assert(myAgent.run("topic"), "expected summary") test. The summary will be slightly different every time.

The key insight is this: You don't test the LLM; you test the robust system you build around it.

Testing an AI system requires a multi-layered strategy. We test the deterministic parts with traditional tests, and we evaluate the non-deterministic parts with a new set of tools.

Strategy 1: Unit Test Your Deterministic Logic

A surprising amount of your agent's code has nothing to do with AI. It's regular, testable, deterministic business logic. This code should be rigorously unit tested just like in any other project.

What kind of code falls into this category?

  • Functions that parse or transform data before it's fed into a prompt.
  • Utility functions that format data after it comes back from the LLM (and has been validated).
  • Complex business rules that don't involve the LLM directly.

We use a tool like vitest, which is already in our project template. For a function that formats a list of concepts into a markdown string, a unit test is simple and essential.

```typescript

// in src/utils.ts

export function formatConcepts(concepts: string[]): string {

if (concepts.length === 0) return "No concepts found.";

return "Key Concepts:\n" + concepts.map(c => - ${c}).join('\n');

}

// in test/utils.test.ts

import { expect, test } from 'vitest';

import { formatConcepts } from '../src/utils';

test('formats a list of concepts correctly', () => {

const concepts = ['Cloudflare', 'AI Gateway', 'Workers'];

const expected = 'Key Concepts:\n- Cloudflare\n- AI Gateway\n- Workers';

expect(formatConcepts(concepts)).toBe(expected);

});

```

This is your first line of defense. Ensure all the predictable parts of your system are working perfectly.

Strategy 2: Integration Test Your Contracts (with Mocks)

The next layer is testing the agent's public interface. Does it correctly handle valid and invalid HTTP requests? This is an integration test.

However, we don't want to make real, expensive, and slow AI calls every time we run our test suite. So, we mock the AI. We replace the LLMChain with a fake version that returns a predictable, canned response. This allows us to test our API contract, validation logic, and data flow in complete isolation from the non-deterministic LLM.

Using vitest, we can test our Research Agent's endpoint like this:

```typescript

// in test/index.test.ts

import { expect, test, vi } from 'vitest';

import worker from '../src/index'; // Our Hono app

// Mock the LangChain module

vi.mock('langchain/chains', () => ({

LLMChain: vi.fn().mockImplementation(() => ({

// Mock the .invoke() method

invoke: vi.fn().mockResolvedValue({

summary: "This is a mocked summary.",

concepts: ["mocked", "test"],

}),

})),

}));

test('POST /research with valid topic should succeed', async () => {

const request = new Request('http://localhost/research', {

method: 'POST',

headers: { 'Content-Type': 'application/json' },

body: JSON.stringify({ topic: "A valid topic" }),

});

const resp = await worker.fetch(request, { AI: {} }, {});

expect(resp.status).toBe(200);

const json = await resp.json();

expect(json.summary).toBe("This is a mocked summary.");

});

test('POST /research with invalid topic should fail', async () => {

const request = new Request('http://localhost/research', {

method: 'POST',

headers: { 'Content-Type': 'application/json' },

body: JSON.stringify({ topic: "bad" }), // Topic is too short

});

const resp = await worker.fetch(request, { AI: {} }, {});

expect(resp.status).toBe(400); // Expect a validation error

});

```

This test proves that our Hono routing, Zod validation, and JSON response logic are all working correctly, without ever touching a real LLM.

Strategy 3: Evaluate Your Prompts

We've tested our code, but how do we test the prompt itself? This is where we shift from "testing" to "evaluation."

The goal of an evaluation is to measure the quality of your prompt and AI configuration. You do this by creating an evaluation dataset: a list of inputs and their corresponding "ideal" outputs that you write by hand.

For our Research Agent, a dataset entry might look like this:

```json

{

"input": { "topic": "The benefits of serverless computing" },

"ideal_output": {

"summary": "Serverless computing allows developers to build applications without managing servers, offering benefits like automatic scaling, reduced operational cost, and paying only for what you use.",

"concepts": ["serverless", "scalability", "pay-per-use", "operational cost"]

}

}

```

You might create 10, 50, or even hundreds of these examples. Then, you write an evaluation script that does the following:

  1. Loops through each item in your dataset.
  2. Sends the input to your actual agent (the real chain, not a mock).
  3. Compares the agent's actual_output to your ideal_output.

But how do you compare them? An exact match is too brittle. The most powerful technique is LLM-as-a-Judge. You use a second, powerful LLM (like GPT-4 or Claude 3 Opus) to act as a neutral third-party judge.

Your evaluation prompt would look like this:

> You are an impartial evaluator. Your task is to assess the quality of an AI agent's response.

>

> User's Request: "The benefits of serverless computing"

>

> Ideal Expected Response:

> ```json

> { "summary": "...", "concepts": ["..."] }

> ```

>

> Agent's Actual Response:

> ```json

> { "summary": "...", "concepts": ["..."] }

> ```

>

> Please score the agent's response on a scale of 1-10 for its accuracy and relevance. Provide only a single JSON object with your score and a brief justification.

> {"score": 9, "justification": "The summary was accurate but missed the key concept of 'pay-per-use'."}

By running this evaluation script every time you change your prompt, you can get an aggregate score for your agent's performance. Did your change improve the average score from 8.5 to 9.2? Great, it's an improvement! Did it drop to 7.5? It's a regression; revert the change and try again.

Debugging: The Power of Logging

You can't use a step-debugger on an LLM. When things go wrong, your single most powerful tool is logging. Your agent's core logic should log three things for every AI interaction:

  1. The final, complete prompt sent to the model, with all variables filled in.
  2. The raw string response that came back from the model.
  3. The result of the Zod validation on that response (success or failure).

When a user reports a bug, you can look at these logs and see the entire interaction. 99% of the time, the bug will be obvious: the prompt was malformed, the model returned garbage, or the validation failed. This allows you to debug the failure without needing to reproduce it.

A New Mindset for Quality

Testing in the age of AI requires a multi-layered approach and a new mindset. We use:

  • Unit Tests for our deterministic code.
  • Integration Tests for our API contracts, with mocked AI.
  • Evaluations for our non-deterministic prompt and model performance.

It's a shift from a world of simple pass/fail to a world of continuous quality measurement. It's more complex, but it's the key to building AI systems you can trust.

Want this thinking applied to your build?