Output Filtering

Check Before You Show

Treat model output like user input: it is untrusted. Before it reaches the user, a browser, a database or another tool, check it.

  • Escape HTML before rendering, so a model-written <script> cannot run (XSS).
  • Allow only links to your own domains.
  • Block promises the business cannot keep ("guaranteed delivery tomorrow", "100% refund").
  • Validate actions (JSON) against a schema and business rules before executing.

Why Output Needs Its Own Layer

Input checks try to stop bad requests; output filtering (an output guardrail) checks what the model produced before anything uses it. You need both, because a model can produce harmful output from an innocent input: an injected review, a hallucinated promise, or simply a bad generation. The output is the last point where you are still in control; after it, the text is in a browser, a database or a payment system.

It is the same idea as a newspaper's sub-editor. Reporters can be trusted, yet every article is checked for libel and wrong facts before printing, because the cost of a mistake after printing is much higher. Output checks come in a few kinds:

KindChecksTypical tool
Formatvalid JSON, matches the schema, required fieldsPydantic, JSON Schema
Safety of the channelno executable HTML/JS, no unsafe links, no SQLescaping, allow-lists
Business rulesno promises, prices or discounts outside policyregex lists, rule code
Contenttoxicity, off-topic or disallowed topicsa small classifier or moderation model
Groundingclaims supported by the sourcescitation checks (see Hallucination Reduction)

Example

Example

import html, re
ALLOWED_DOMAINS = {"shopease.in", "help.shopease.in"}
BANNED = [r"\bguarantee(d)?\b", r"\b100% refund\b", r"\blegal advice\b"]

def filter_output(text):
    issues = []
    for url in re.findall(r"https?://([\w.-]+)", text):
        if url not in ALLOWED_DOMAINS:
            issues.append(f"link to {url}")
            text = text.replace(url, "[link removed]")
    for pat in BANNED:
        m = re.search(pat, text, re.IGNORECASE)
        if m:
            issues.append(f"banned phrase '{m.group()}'")
    return html.escape(text), issues

for out in ["Track it at https://shopease.in/orders/SE1042 .",
            "Delivery is guaranteed tomorrow! Pay at http://free-coupons.example",
            "<script>alert('hi')</script> Your refund is processing."]:
    safe, issues = filter_output(out)
    print("issues:", issues or "none")
    print("shown :", safe)

Output

issues: none
shown : Track it at https://shopease.in/orders/SE1042 .
issues: ['link to free-coupons.example', "banned phrase 'guaranteed'"]
shown : Delivery is guaranteed tomorrow! Pay at http://[link removed]
issues: none
shown : &lt;script&gt;alert(&#x27;hi&#x27;)&lt;/script&gt; Your refund is processing.

The first reply links to ShopEase and passes. The second has two problems: a link to an unknown domain (removed) and the banned word "guaranteed". This filter only reports the phrase; the caller then decides to regenerate or fall back to a safe reply, because deleting words can change the meaning. The third reply's <script> tag is escaped, so the browser shows it as text instead of running it.

Validating an Action

from pydantic import BaseModel, Field

class RefundAction(BaseModel):
    order_id: str = Field(pattern=r"^SE\d{4,8}$")
    amount_inr: int = Field(gt=0, le=2000)          # above this needs a human

def execute(action_json: str, customer_orders: set[str]):
    action = RefundAction.model_validate_json(action_json)   # raises if invalid
    if action.order_id not in customer_orders:
        raise PermissionError("order does not belong to this customer")
    return payments.refund(action.order_id, action.amount_inr)

Two separate checks happen here. The schema check (Pydantic) makes sure the action is well-formed: an order ID in the right pattern and an amount between 1 and 2,000. The authorisation check makes sure this customer is allowed to act on this order. A schema-valid action can still be an attack, so never skip the second check.

Note: Trade-offs: strict filters create false positives ("guarantee" also appears in "warranty guarantee card"), which frustrate users; loose ones let problems through. Tune lists on real outputs and log every block. Streaming makes filtering harder because text reaches the user before the full answer exists; check chunks as they arrive, or buffer sentences for risky routes.

  • Model output is untrusted input to whatever comes next.
  • Check format, channel safety, business rules, content and grounding.
  • On a failed check: regenerate or fall back; do not silently edit meaning.
  • Validate actions for both schema and authorisation before executing.