DS DevShelfHub Projects · AI tools
Articles / Guardrails with LangChain: A Crash Course on Building Safe AI Agents

AI Engineering

Guardrails with LangChain: Building Safe AI Agents in Production

By DevShelfHub

A hands-on guide to building production-grade safety layers for LangChain agents — PII middleware, human-in-the-loop checkpoints, custom hooks, and layered guardrails for defense in depth.

Guardrails with LangChain: Building Safe AI Agents in Production

Introduction

Ship an AI agent without guardrails and you’re basically handing a customer a search bar with a credit card attached. The agent will happily answer “how do I hack into a database?”, echo back a user’s email and card number in plaintext, or fire off a DELETE FROM users because someone asked nicely. None of that is a model problem — it’s a missing safety layer.

Guardrails are the safety mechanisms that sit around your agent pipeline. They decide what inputs are safe enough to reach the LLM, which actions need a human in the loop, and what outputs are allowed to reach the user. This article is a hands-on crash course for building those guardrails inside LangChain — using the built-in middleware system, custom hooks, and a layered defense pattern you can drop into a real production agent.

Table of contents

  • What guardrails actually are
  • Why guardrails matter for any agent in production
  • Two approaches: deterministic vs model-based
  • Why LangChain — the middleware model
  • Built-in PII middleware: redact, mask, block
  • Human-in-the-loop middleware for sensitive tools
  • Custom before-agent hook: cheap, cost-free filtering
  • Custom after-agent hook: validating the final output
  • Layered guardrails: stacking middleware for defense in depth
  • Best practices for shipping guardrails
  • Common mistakes to avoid
  • Conclusion
  • Frequently asked questions

What guardrails actually are

A clean working definition: guardrails are safety mechanisms that control what goes into and comes out of an AI agent. They sit around the agent pipeline and make sure three things hold:

Safe inputs

Only safe, appropriate inputs reach the LLM. Anything that looks like a prompt injection, a request for harmful content, or leaked PII gets filtered or flagged before the model sees it.

Approved actions

The agent only performs approved actions. Sensitive tools — sending email, deleting records, moving money — either need explicit human approval or are blocked outright.

Compliant outputs

Only validated, compliant responses reach the user. Outputs are scanned for PII, policy violations, and quality issues before they leave the system.

Concretely, imagine the simplest agent loop: input → LLM → (optional tool call) → output. A guardrail is a check inserted at one or more of those edges. If a user types “how do I hack a server?”, a well-placed input guardrail flags the request before the model wastes a token on it. If the model tries to leak an email in its response, an output guardrail catches it on the way back.

Why guardrails matter for any agent in production

The cost of skipping guardrails is no longer theoretical. Agents now talk to databases, fire APIs, send transactional emails, and operate inside customer-support flows. The damage surface is wide:

  • PII leakage. A logging pipeline that captures raw prompts ends up storing credit card numbers and emails in plaintext — a compliance nightmare under GDPR, HIPAA, or PCI-DSS.
  • Prompt injection. Untrusted text retrieved from a webpage or document can hijack the agent into ignoring its system prompt and executing attacker instructions.
  • Dangerous tool calls. An agent with delete or refund authority can be talked into wiping a table or issuing a payout without any human review.
  • Hallucinated answers in regulated contexts. Legal, medical, and financial agents can’t simply make things up — outputs need quality and safety checks before they reach the user.

Guardrails aren’t a nice-to-have. They’re the difference between a demo and an agent you can hand to real users without losing sleep.

Two approaches: deterministic vs model-based

Every guardrail implementation lands in one of two buckets. The right answer in production is usually a mix.

Deterministic guardrails

Rule-based filters: regex, keyword lists, schema validation, length caps. They’re fast, cost nothing per request, and behave the same way every time.

Tradeoff: they don’t understand semantics. A banned-keyword list will block “how do I hack a server?” but also flag “explain how malware spreads” even though the second one is a genuine education question.

Model-based guardrails

An LLM (often a small, cheap one) classifies the input or output. It catches paraphrased attacks a keyword filter misses, understands intent, and can be steered with a prompt.

Tradeoff: every check costs a model call. Latency and spend go up. Use a cheap model (something in the GPT-4o-mini / Haiku class) and run it in parallel where possible.

A quick deterministic example in Python:

Python
def deterministic_guardrail(text: str) -> bool:
    """Return True if content is blocked."""
    banned = ["hack", "exploit", "malware", "bomb"]
    text_lower = text.lower()
    return any(keyword in text_lower for keyword in banned)

for q in ["How do I hack into a database?",
          "What is the capital of France?",
          "Explain how malware spreads"]:
    status = "blocked" if deterministic_guardrail(q) else "allowed"
    print(f"{status}: {q}")

And the model-based equivalent:

Python
from langchain_openai import ChatOpenAI

model = ChatOpenAI(model="gpt-4o-mini", temperature=0)

def model_guardrail(text: str) -> str:
    """Use an LLM to evaluate content safety. Returns 'safe' or 'unsafe'."""
    prompt = (
        "Is the following user input safe to process? "
        "Reply with only 'safe' or 'unsafe'.\n\n"
        f"Input: {text}"
    )
    return model.invoke(prompt).content.strip().lower()

Notice the difference on “explain how malware spreads” — the keyword filter blocks it, the model-based check correctly classifies it as a legitimate education request. That’s the whole point of layering both.

Why LangChain — the middleware model

LangChain handles guardrails through middleware: hooks you attach to an agent that fire at well-defined points in the execution loop. Instead of scattering safety checks across your code, you compose them as middleware instances and hand them to create_agent.

The five middleware patterns we’ll cover

  • PII middleware — built-in detector for emails, credit cards, IPs, MAC addresses, URLs, and API keys with redact / mask / block strategies.
  • Human-in-the-loop middleware — pauses the agent before sensitive tool calls and waits for human approval.
  • Before-agent hook — custom guardrail that runs before any LLM call. Zero token cost on blocked requests.
  • After-agent hook — validates or rewrites the final response before the user sees it.
  • Layered guardrails — stack multiple middleware in order for defense in depth.

The mental model: middleware is just a list. The agent runs each one in order, and any of them can short-circuit the request or mutate its state. That makes it trivial to reason about — you can read the middleware stack top to bottom and know exactly what’s being checked at every stage.

Built-in PII middleware: redact, mask, block

Personally identifiable information (PII) is the most common guardrail failure in real systems — a user pastes a credit card or an SSN into a chat box, and it ends up in your logs. LangChain ships PIIMiddleware with built-in detectors for the obvious offenders.

Supported PII types

  • Email addresses
  • Credit card numbers
  • IP addresses
  • MAC addresses
  • URLs
  • Custom detectors via regex (e.g. API keys)

Strategies

  • redact — replace match with a placeholder like [REDACTED_EMAIL]
  • mask — replace with hashed or starred characters
  • block — raise an exception and refuse the request

The setup looks like this:

Python
from langchain.agents import create_agent
from langchain.agents.middleware import PIIMiddleware
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool

@tool
def customer_lookup(query: str) -> str:
    """Look up a customer record."""
    return f"Customer record found for: {query}"

agent = create_agent(
    model=ChatOpenAI(model="gpt-4o"),
    tools=[customer_lookup],
    middleware=[
        PIIMiddleware("email", strategy="redact", apply_to_input=True),
        PIIMiddleware("credit_card", strategy="mask", apply_to_input=True),
        PIIMiddleware(
            "api_key",
            detector=r"sk-[A-Za-z0-9]{32}",
            strategy="block",
            apply_to_input=True,
        ),
    ],
)

Three middlewares, three different strategies. Email gets redacted, credit cards get masked, API keys are blocked outright with a raised exception. The apply_to_input flag controls whether the check runs on inbound messages — you can also apply it to outputs and tool calls.

Now run it:

Text
result = agent.invoke({
    "messages": [{
        "role": "user",
        "content": "My email is john.doe@example.com and my card number "
                   "is 4111-1111-1111-5100. Can you help me?",
    }]
})

The agent never sees the raw email or card number. Internally the request becomes “My email is [REDACTED_EMAIL] and my card number is **** **** **** 5100”, and the response correctly references only the masked digits. Trying to pass an API key like sk-abc123... triggers the block strategy and raises an exception you catch in a try/except.

Why this matters: the PII check runs before the LLM call, so sensitive data never enters your prompt, never hits the provider’s logs, and never reappears in your own observability stack.

Human-in-the-loop middleware for sensitive tools

Some actions should never be fully automated. Financial transactions, outbound emails to external parties, production data deletions — anything with real-world consequences needs a human in the loop. LangChain’s HumanInTheLoopMiddleware pauses the agent before specific tool calls and waits for an approve / reject decision.

Python
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langchain_core.tools import tool
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command

@tool
def search_web(query: str) -> str:
    return f"Search results for: {query}"

@tool
def send_email(to: str, subject: str, body: str) -> str:
    return f"Email sent to {to} with subject '{subject}'"

@tool
def delete_records(table: str, condition: str) -> str:
    return f"Deleted records from {table} where {condition}"

hitl_agent = create_agent(
    model=ChatOpenAI(model="gpt-4o"),
    tools=[search_web, send_email, delete_records],
    middleware=[
        HumanInTheLoopMiddleware(
            interrupt_on={
                "search_web": False,      # auto-approved
                "send_email": True,       # needs approval
                "delete_records": True,   # needs approval
            }
        )
    ],
    checkpointer=InMemorySaver(),
)

The checkpointer is required — the agent needs to persist state when it pauses, so it can resume on the same thread once a human decides. search_web runs without interruption because it’s read-only. send_email and delete_records pause and wait.

Approve a paused call by resuming the thread with a command:

Text
config = {"configurable": {"thread_id": "session-123"}}

# First call - this will pause at send_email
hitl_agent.invoke(
    {"messages": [{"role": "user", "content": "Send an email to team@company.com about Q4 results"}]},
    config=config,
)

# Approve and resume
hitl_agent.invoke(
    Command(resume={"decision": {"type": "approve"}}),
    config=config,
)

# Or reject with a reason
hitl_agent.invoke(
    Command(resume={"decision": {"type": "reject", "reason": "Too risky, needs DBA review"}}),
    config=config,
)
Pattern in the wild: wire the interrupt to a Slack approval flow or an in-app modal. The agent pauses, a human gets a notification with the proposed action and arguments, they hit approve or reject, the agent resumes. That’s your insurance policy against the “agent deleted the production database” class of incidents.

Custom before-agent hook: cheap, cost-free filtering

When the built-in middleware doesn’t fit your case, write your own. A custom before-agent hook runs before any LLM call — perfect for keyword filters, auth checks, rate limiting, or category blocks. Because it short-circuits before the model fires, blocked requests cost zero tokens.

Python
from langchain.agents.middleware import AgentMiddleware, AgentState, hook_config
from langgraph.runtime import Runtime
from langchain.agents import create_agent

class ContentFilterMiddleware(AgentMiddleware):
    """Deterministic guardrail: block requests containing banned keywords."""

    def __init__(self, banned_keywords: list[str]):
        super().__init__()
        self.banned_keywords = banned_keywords

    @hook_config(jump_to="end")
    def before_agent(self, state: AgentState, runtime: Runtime):
        if not state["messages"]:
            return None

        first = state["messages"][0]
        if first.type != "human":
            return None

        content = first.content.lower()
        for keyword in self.banned_keywords:
            if keyword in content:
                return {
                    "messages": [{
                        "role": "assistant",
                        "content": (
                            f"Blocked: keyword '{keyword}' detected. "
                            "I cannot process this request."
                        ),
                    }]
                }
        return None

The key piece is @hook_config(jump_to="end") — when the guardrail triggers, the agent jumps straight to the end node and returns the blocked-message response. No LLM call, no tool calls, no token spend.

Wire it into an agent:

Text
filter_agent = create_agent(
    model=ChatOpenAI(model="gpt-4o"),
    tools=[customer_lookup],
    middleware=[
        ContentFilterMiddleware(
            banned_keywords=["hack", "exploit", "malware", "bomb"]
        )
    ],
)

# Safe request - runs normally
filter_agent.invoke({"messages": [{"role": "user", "content": "What is machine learning?"}]})

# Unsafe request - blocked before the model is called
filter_agent.invoke({"messages": [{"role": "user", "content": "How do I hack into a server?"}]})

For the second request, the response is the canned blocked message — no token spend, no risk of the model engaging with the prompt. That’s the entire value proposition of cheap deterministic guardrails at the front door.

Custom after-agent hook: validating the final output

The flip side: validate what the agent produced before the user sees it. An after-agent hook is the right place for model-based safety evaluation, compliance scanning (legal, medical, financial disclaimers), and removing any sensitive info that slipped through earlier stages.

Python
class SafetyGuardrailMiddleware(AgentMiddleware):
    """Model-based output guardrail: rewrite or block unsafe responses."""

    def __init__(self):
        super().__init__()
        self.safety_model = ChatOpenAI(model="gpt-4o-mini", temperature=0)

    @hook_config
    def after_agent(self, state: AgentState, runtime: Runtime):
        last = state["messages"][-1]
        if last.type != "ai":
            return None

        prompt = (
            "Evaluate if this AI response is safe and appropriate for users. "
            "Reply only with 'safe' or 'unsafe'.\n\n"
            f"Response: {last.content}"
        )
        verdict = self.safety_model.invoke(prompt).content.strip().lower()

        if "unsafe" in verdict:
            return {
                "messages": [{
                    "role": "assistant",
                    "content": "I cannot share that response. Please rephrase your request.",
                }]
            }
        return None

A couple of details worth calling out:

  • Use a cheap model for the check. The safety classifier doesn’t need to be your main model. A small, fast model (GPT-4o-mini, Haiku, Llama-3 8B) keeps cost and latency low.
  • Temperature zero. You want deterministic verdicts, not creative ones.
  • Mutate, don’t just block. An after-agent hook can rewrite the response — strip a phone number, add a disclaimer, soften tone — instead of just rejecting it.

Layered guardrails: stacking middleware for defense in depth

No single guardrail catches everything. Production systems stack them. Deterministic filters at the front door, PII redaction for compliance, human-in-the-loop on the dangerous tools, model-based safety check on the way out. Each layer is cheap because the previous one already filtered out the obvious cases.

Text
layered_agent = create_agent(
    model=ChatOpenAI(model="gpt-4o"),
    tools=[search_web, send_email, customer_lookup],
    middleware=[
        # Layer 1: cheap deterministic filter
        ContentFilterMiddleware(banned_keywords=["hack", "exploit", "malware"]),

        # Layer 2: PII protection
        PIIMiddleware("email", strategy="redact", apply_to_input=True),
        PIIMiddleware("credit_card", strategy="mask", apply_to_input=True),

        # Layer 3: human approval for sensitive tools
        HumanInTheLoopMiddleware(
            interrupt_on={"send_email": True, "search_web": False}
        ),

        # Layer 4: model-based output safety
        SafetyGuardrailMiddleware(),
    ],
    checkpointer=InMemorySaver(),
)

Order matters. The list runs top to bottom, and each middleware can short-circuit the rest. Put the cheap, fast checks first so you’re not paying for an LLM-based check on a request that was going to be blocked by a regex anyway.

Typical ordering

  1. Authentication / rate limit. Cheapest, blocks the most abuse.
  2. Deterministic keyword / pattern filter. Fast, free, catches obvious cases.
  3. PII redaction on input. Cleans sensitive data before it reaches the model.
  4. Model-based input classifier (optional). For nuanced cases the keyword filter misses.
  5. Human-in-the-loop on sensitive tools. Hard barrier for destructive actions.
  6. PII redaction on output. Last line of defense for compliance.
  7. Model-based output safety check. Catches policy violations the agent generated itself.

Best practices for shipping guardrails

Do this

  • Start with deterministic filters — they’re free and catch most abuse
  • Layer a model-based check on top for semantic cases
  • Apply PII middleware to inputs and outputs, not just one direction
  • Require human-in-the-loop for any tool that touches money, data, or external communication
  • Use a cheap model for safety classification — not your production reasoning model
  • Log every blocked request with reason — you need this for tuning and audits
  • Write integration tests that confirm each guardrail blocks what it should

Avoid this

  • Relying on a single banned-word list as your only defense
  • Running a model-based check on every request when a regex would catch 95% of cases
  • Skipping the checkpointer when using human-in-the-loop — it won’t resume correctly
  • Putting guardrails only on input or only on output — you need both edges covered
  • Hard-coding banned keywords in business logic instead of as configurable middleware
  • Blocking silently — always return a clear “this request was blocked because X” message

Common mistakes to avoid

  • Treating guardrails as a final-step add-on. Retro-fitting safety on an agent that’s already shipped is harder than building it in from day one. The middleware list should be part of the initial design.
  • Trusting the LLM to refuse on its own. System prompts help, but they’re bypassed by prompt injection. A deterministic guardrail outside the model is non-negotiable for high-risk actions.
  • Ignoring tool calls. PII can leak through the arguments of a tool call, not just the final response. Make sure your PII middleware is configured to inspect tool inputs too.
  • No observability on blocks. If you can’t see which guardrail fired, on which request, and why, you can’t tune the rules. Wire blocked events into your usual logging and dashboards.
  • Over-blocking. A guardrail that flags “explain how malware spreads” as unsafe is hostile to legitimate education use cases. Use model-based checks where intent matters.
  • Single-language keyword lists. If your agent talks to users in Hindi, Spanish, or French, an English-only banlist won’t catch translated attack strings. Either expand the list or fall back to a multilingual model-based check.

Conclusion

Guardrails aren’t a single feature you bolt on once. They’re a layered, evolving safety surface that wraps your agent from input to output and at every tool call in between. LangChain makes that surface concrete: PIIMiddleware, HumanInTheLoopMiddleware, and the before_agent/after_agent hooks give you four well-defined slots to plug in safety checks.

A reasonable first pass for any production agent: deterministic keyword filter at the front, PII redaction on inputs and outputs, human-in-the-loop on every tool that can do real damage, and a cheap model-based safety check on the way out. Build the stack incrementally, log every block, and treat the guardrail config as code you review and test like any other part of your system.

Guardrails with LangChain: A Crash Course on Building Safe AI Agents FAQ

Do I need both deterministic and model-based guardrails?

For most production systems, yes. Deterministic filters are free and catch the bulk of obvious abuse. Model-based checks handle the semantic edge cases — paraphrased attacks, intent shifts, polite jailbreaks. Stack them: deterministic first, model-based second, only on requests that survive the first layer.

What’s the difference between an input guardrail and an output guardrail?

Input guardrails run before the LLM call — they filter user prompts. Output guardrails run after the LLM has produced a response — they validate or rewrite what the agent generated. You need both. Input checks stop bad requests early and save tokens. Output checks catch problems the model itself created.

Does PIIMiddleware send my data to a third party?

No. PIIMiddleware runs locally using regex-based detectors. The redaction, masking, and blocking happen entirely inside your process before the prompt is sent to any model provider. That’s the whole point — sensitive data should never reach an external API.

How do I add a custom PII detector, like for SSNs or phone numbers?

Pass a regex pattern via the detector argument along with a PII type name and a strategy of block, mask, or redact. For SSNs, supply a pattern that matches three digits, a dash, two digits, a dash, four digits. For phone numbers, use a pattern matching your expected format. The type name is just a label — the regex is the actual detection logic.

Why does human-in-the-loop require a checkpointer?

When the agent pauses for human approval, it has to save its current state somewhere so it can resume later on the same thread. The checkpointer is that storage layer. For development, an InMemorySaver is fine. For production, use a persistent checkpointer backed by Postgres, Redis, or your storage of choice.

What model should I use for output safety checks?

A small, cheap, fast one. GPT-4o-mini, Claude Haiku, or a hosted Llama-3 8B all work well as classifiers. The check is binary (safe / unsafe) — you don’t need frontier reasoning for that. Keep temperature at zero so verdicts are stable across retries.

Can I run multiple guardrails of the same type?

Yes. The middleware list accepts any number of instances. It’s common to have multiple PIIMiddleware entries — one per PII type with its own strategy. Or stack two ContentFilterMiddleware instances, one for inputs and one for outputs. They run in order, top to bottom.

Are guardrails enough on their own, or do I still need a system prompt?

You need both. The system prompt sets behaviour and tone for the cooperative case. Guardrails enforce policy for the adversarial case — users trying to jailbreak, leaks the model produces by accident, dangerous tool calls. System prompts can be bypassed by prompt injection; guardrails outside the model can’t.