7 Ways to Test an LLM Application When Outputs Are Never the Same Twice A developer-focused guide lays out seven methods for testing LLM applications whose outputs vary between runs, using a lease-extraction example that returns tenant, rent and notice period as JSON. In the golden-dataset approach, a clean lease case scored an 80% pass rate across five runs because one run returned rent as a string, and a property check over 40 outputs caught each string rent without any labels. The author argues no single method suffices and recommends reporting a pass rate per case rather than a single pass or fail. Your test passed this morning. Same input, same prompt, and it failed this afternoon. Written with AI assistance. I wanted to understand how this works beyond the usual hello-world example. So let’s build something practical. Traditional software testing is relatively straightforward. Give an input, expect an output. With LLM applications, the same prompt might produce slightly different answers. That changes how I think about testing. No single method is enough on its own. Here are seven, using one example: extracting tenant, rent and notice period from lease text as JSON. The code is one standard-library Python file with no API key. Seeded fakes shuffle keys and sometimes return rent as the string “1,200”, the kind of drift real models can show. Swap in your provider here: php def call llm messages: list dict - str: """Replace with your provider.""" import anthropic pip install anthropic client = anthropic.Anthropic resp = client.messages.create model="your-model-name", max tokens=1024, messages=messages return resp.content 0 .text How it works: collect inputs with known answers and run each several times. Compare parsed fields, not raw strings. I wouldn’t trust an AI extraction feature just because it works on five carefully selected sample documents. I’d build a test dataset with different document formats, missing fields, scanned PDFs and unusually structured records. The demo has a clean lease, one with no notice period, OCR text with a zero for an O, and an odd field order. php def run golden llm, runs: int = 5 - dict str, float : """Pass rate per case: compare parsed fields, never raw strings.""" results = {} for case in GOLDEN: passed = 0 for in range runs : try: out = json.loads llm extraction prompt case "doc" passed += out == case "expect" except json.JSONDecodeError: pass results case "id" = passed / runs return results The clean lease scores 80%: one run in five returned rent as a string. A single run could have missed it. Report a pass rate per case, not a single pass or fail. Pros: the closest thing to a unit test. Cons: labelling takes effort, and the set goes stale. How it works: some rules hold for every output: valid JSON, the right keys, a positive numeric rent, a tenant name that appears in the source. php def check properties raw: str, doc: str - list str : """Rules every output must obey, even when you don't know the answer.""" try: data = json.loads raw except json.JSONDecodeError: return "not valid JSON" errors = if set data = {"tenant", "rent", "notice days"}: errors.append f"wrong keys: {sorted data }" rent = data.get "rent" if rent is not None and not isinstance rent, int, float or rent <= 0 : errors.append "rent is not a positive number" if data.get "tenant" and data "tenant" not in doc: errors.append "tenant name not in source" return errors Over 40 outputs, it catches each string rent without any labels. A property check needs no expected answer, so you can run it on every response in production. Pros: cheap and deterministic. Cons: a wrong but plausible number passes. How it works: for summaries, embed the output and a reference answer and compare with cosine similarity. Anthropic’s eval guide shows this with a sentence-embedding model. The demo uses word counts so it runs offline. php def embed text: str - Counter: """Stand-in: word counts. Swap for a real sentence-embedding model.""" return Counter re.findall r" a-z0-9 +", text.lower def similarity a: str, b: str - float: va, vb = embed a , embed b dot = sum va w vb w for w in va na = math.sqrt sum v v for v in va.values nb = math.sqrt sum v v for v in vb.values return dot / na nb if na and nb else 0.0 A paraphrase scores 0.88 and an off-topic sentence 0.15. But changing “60 days” to “30 days” scores 0.93, higher than the correct paraphrase. Real embeddings do better than word counts, but one changed number in an identical sentence remains a weak spot. Similarity tells you an answer sounds right, not that it is right. Pros: handles rewording at scale. Cons: blind to small factual changes. How it works: a second model grades the output against the source. Anthropic’s guide recommends clear rubrics, a specific output such as a 1 to 5 score, reasoning before scoring, and generally a different model from the one being graded. Validate the judge’s reply too. RUBRIC = """Grade the summary against the source on a 1-5 scale.5: every name, amount and time period matches the source.3: something important is missing, but nothing is wrong.1: any fact contradicts the source.Reason step by step first, then end with one line: SCORE: <1-5 """ def judge summary: str, source: str, judge llm - int: prompt = f"{RUBRIC}\n