惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

J
Java Code Geeks
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Blog — PlanetScale
Blog — PlanetScale
G
Google Developers Blog
Microsoft Security Blog
Microsoft Security Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
腾讯CDC
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Jina AI
Jina AI
雷峰网
雷峰网
T
Tailwind CSS Blog
爱范儿
爱范儿
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
酷 壳 – CoolShell
酷 壳 – CoolShell
大猫的无限游戏
大猫的无限游戏
月光博客
月光博客
博客园 - 司徒正美
I
InfoQ
Engineering at Meta
Engineering at Meta
Vercel News
Vercel News
小众软件
小众软件
U
Unit 42
Google DeepMind News
Google DeepMind News
D
DataBreaches.Net

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Three-Layer Safety for Autonomous Agents: Stopping the In...
Guatu · 2026-05-01 · via DEV Community

Guatu

I watched an autonomous agent spend three hours and 40,000 tokens trying to close a GitHub issue that had an open dependency, only to fail because it kept hallucinating a force_close flag that didn't exist in the API. It didn't just fail; it entered a perfect infinite loop: it would call the tool, get a 400 error, interpret the error as a "temporary network glitch," and try again with the exact same payload.

If you've built agents that actually touch production systems, you know this feeling. Prompting the agent to "be careful" or "follow the schema" is a placebo. When you move from a chat window to an autonomous loop, the gap between the LLM's intent and the system's reality becomes a canyon where agents go to die (and burn through your API credits).

For anyone running agent orchestration in a homelab or production environment, you need a safety architecture that doesn't rely on the model's "good behavior." I've moved to a three-layer safety model: Token-Level Enforcement, Pre-Execution Gates, and Execution Isolation.

What I tried first

My first instinct was to lean heavily on PydanticAI. The idea of using Pydantic for type-safe tool calling seemed like the silver bullet. I spent a week building out complex schemas, thinking that if the code validated the output, the agent would simply "learn" to provide the correct format.

I was wrong. I hit a wall where the agent would produce a JSON object that was almost correct, but it would miss a closing brace or add a trailing comma. Pydantic would throw a ValidationError, the agent would see that error in its history, and then it would attempt to "fix" the JSON by adding even more commentary around the code block. This created a feedback loop of ValidationError $\rightarrow$ Apology $\rightarrow$ Broken JSON.

Then I tried adding a "supervisor" agent to review the actions of the "worker" agent. This just doubled my latency and doubled my token cost without actually solving the root cause. The supervisor often hallucinated the same API capabilities as the worker because they were using the same base model.

The real problem wasn't the logic; it was the lack of deterministic boundaries. I was treating the LLM as a reliable software component when it's actually a probabilistic engine. To make it safe, I had to stop trying to "convince" the model to be safe and start forcing it to be safe at the infrastructure level.

Layer 1: Token-Level Schema Enforcement

The first layer of safety happens before the agent even finishes its sentence. If you're using Ollama v0.5.0 or newer, you can stop relying on the model to "try its best" with JSON.

Most people use the OpenAI-compatible API layer provided by frameworks, but that often just wraps the prompt in "Please return JSON." Ollama now supports a native format parameter that enforces the schema at the token-sampling level. This means the model physically cannot sample a token that violates the JSON schema.

Here is how I implemented this for my homelab health reports using qwen2.5:14b-instruct. I switched from the 32B model to the 14B variant because the 32B was causing 502 timeouts on my Tesla P40s due to VRAM pressure.

import httpx
from pydantic import BaseModel, Field

# Define the strict structure we want
class HomelabHealthReport(BaseModel):
    node_status: dict[str, str]
    critical_alerts: list[str]
    storage_utilization: float = Field(description="Percentage 0-100")

# Extract the JSON schema for Ollama
schema = HomelabHealthReport.model_json_schema()

def get_safe_report():
    # We bypass the high-level wrappers and hit the API directly
    # to ensure the 'format' parameter is actually passed.
    response = httpx.post(
        "http://ollama:11434/api/chat",
        json={
            "model": "qwen2.5:14b-instruct",
            "stream": False,
            "format": schema, # This is the magic: token-level enforcement
            "prompt": "Generate a health report for the homelab based on current metrics."
        },
        timeout=30.0
    )

    if response.status_code != 200:
        print(f"API Error: {response.status_code}")
        return None

    return response.json()["message"]["content"]

# Result is guaranteed to be valid JSON matching HomelabHealthReport

Enter fullscreen mode Exit fullscreen mode

By moving the constraint to the sampler, I eliminated the ValidationError loops entirely. The model no longer "guesses" the JSON; it is constrained by the grammar of the schema.

Layer 2: The Pre-Execution Gate (ActionGate)

Even with perfect JSON, an agent can still decide to do something stupid. Token-level safety ensures the format is right, but it doesn't ensure the intent is safe.

I implemented an ActionGate. This is a deterministic middleware layer that sits between the agent's tool-call and the actual execution. It doesn't use an LLM. It uses hard-coded business logic and state checks.

If an agent tries to close a ticket, the ActionGate checks if there are open dependencies. If it tries to reboot a node, it checks if that node is currently the only one running a critical service.

class SafetyException(Exception):
    pass

def check_action_safety(action_name, params, context):
    """
    Deterministic safety check. 
    No LLMs allowed here.
    """
    # Prevent closing issues that have blocking dependencies
    if action_name == "close_issue":
        issue_id = params.get("issue_id")
        if context.get(f"issue_{issue_id}_has_dependency"):
            raise SafetyException(
                f"Safety Violation: Cannot close issue {issue_id} while dependencies are open."
            )

    # Prevent destructive actions on production nodes during peak hours
    if action_name == "reboot_node":
        node_id = params.get("node_id")
        if context.get("is_production") and context.get("peak_hours"):
            raise SafetyException(
                f"Safety Violation: Reboot of {node_id} forbidden during peak hours."
            )

    return True

# Usage in the agent loop
try:
    if check_action_safety(tool_call.name, tool_call.args, current_context):
        result = execute_tool(tool_call)
except SafetyException as e:
    # We feed the specific error back to the agent so it can pivot
    result = f"Action rejected by Safety Gate: {str(e)}"

Enter fullscreen mode Exit fullscreen mode

This prevents the "infinite loop of failure" I mentioned earlier. Instead of the agent getting a generic 400 error from an API and thinking it's a network glitch, it gets a clear, human-readable explanation: "You cannot do this because X." This forces the agent to change its strategy rather than just retrying the same failed request.

Layer 3: Execution Isolation and Shell Safety

The final layer is where the rubber meets the road. I've spent too many hours debugging "quoting hell."

When you have an agent generating a command that needs to run over SSH, inside a Proxmox container (pct exec), as a specific user (su), and then executing a Python script, you have four layers of shell interpretation. If you use f-strings to build these commands, a single single-quote in the agent's output will break the entire pipeline.

I saw this happen when an agent tried to pass a complex JSON string as an argument to a script. The shell interpreted the quotes, the su command stripped another layer, and by the time it hit Python, the syntax was mangled.

The fix is to stop passing code as shell arguments. Instead, pipe the code directly into the stdin of the remote process.

The wrong way (prone to quoting errors):

# This will break the moment the agent adds a ' or " to the payload
ssh node-a "pct exec 101 -- su - user -c 'python3 -c \"print(\"Hello World\")\"'"

Enter fullscreen mode Exit fullscreen mode

The right way (Shell-safe piping):
I wrote a helper that writes the agent's intended Python logic to a temporary file or pipes it directly. This avoids the shell's interpretation of the string entirely.

# We pipe the actual script content into the remote shell
cat ~/bin/helpers/scout-ideas-helper.py | \
  ssh node-a "pct exec 101 -- su - user -c 'python3 -'"

Enter fullscreen mode Exit fullscreen mode

In this setup, python3 - tells Python to execute the code coming from stdin. The shell only sees the command to start Python, not the code itself. This completely eliminates the quoting nightmare.

To manage the tools themselves, I've moved away from custom boilerplate and started using FastMCP. It allows me to wrap my MSAM (Multi-Agent System Architecture) tools into a standardized server that the agents can discover and use without me having to manually update the tool definitions every time I add a new function. I've detailed the setup for this in my post on Building MCP Servers with FastMCP.

Why this works

This architecture works because it acknowledges that the LLM is the most unreliable part of the system.

  1. Token-level enforcement removes the "formatting" problem. The agent can no longer fail because it forgot a comma.
  2. The ActionGate removes the "logic" problem. The agent can no longer perform an action that is fundamentally unsafe, regardless of how confident it is.
  3. Execution Isolation removes the "infrastructure" problem. The agent's output is treated as data (stdin) rather than as a command (shell argument).

When you combine these, you move from a system that is "mostly working" to one that is "predictably bounded."

Lessons Learned

The biggest surprise was how much the format parameter in Ollama reduced the need for complex prompt engineering. I spent weeks refining a "System Prompt" to ensure JSON compliance, only to find that a single API parameter did the job better than 500 words of instructions.

If I were to do this over again, I would have implemented the ActionGate much sooner. I spent too much time trying to make the agent "smarter" when I should have just made the environment "stricter."

A few caveats:

  • Latency: Each layer adds a small amount of overhead. The ActionGate is negligible (milliseconds), but the token-level enforcement can slightly increase the time to first token because the sampler has to do more work.
  • VRAM: As I noted, model size matters. Qwen 2.5 14B is the sweet spot for my hardware. If you're running on limited VRAM, don't chase the 32B or 70B models just for the sake of "intelligence" if it leads to 502 timeouts and unstable inference.
  • Memory Drift: Ensure your agent's memory is cleaned up. I use a six-layer memory architecture to prevent the agent from getting confused by outdated context, which is often the root cause of why it tries to perform unsafe actions in the first place.

Building autonomous agents isn't about finding the perfect model; it's about building the perfect cage for that model to operate in.