Action is proposed as structured data (tool + args).
Problem (aus der Praxis)
Dein Agent macht das Falsche.
Nicht „Antwort ist etwas off“. Falsch wie:
- doppelte Emails senden
- Tickets in Bulk erstellen
- eine API hämmern, bis du rate-limited bist
Und jetzt der wichtige Teil: du hast keine Zeit, „Prompt fixen und redeployen“.
Du brauchst einen Kill Switch, der:
- jetzt sofort wirkt
- auditierbar ist (wer hat wann warum geschaltet)
- Side Effects stoppt, nicht nur die UI
Wenn dein Kill Switch nur im Frontend lebt, ist er kein Kill Switch. Er ist Placebo. Wenn dein Kill Switch ein Env Var ist, ist er ein Deploy. Incidents warten nicht auf Deploys.
Warum das in Production bricht
1) Teams bauen „Pause“-Buttons, die nichts pausieren
Anti-Design:
- UI versteckt den Button
- API lässt die Agent-Loop weiterlaufen
- Tool Gateway führt Writes weiter aus
Wenn Tool Calls noch durchgehen, hast du den Incident nicht gestoppt. Du hast ihn umbenannt.
2) Kill Switches, die nicht im Tool Gateway gecheckt werden, leaken
Wenn du den Kill Switch checkst:
- in einer Route
- aber nicht in Background Jobs
- und nicht im Tool Gateway
…verpasst du einen Pfad.
3) „Stop the run“ reicht nicht
In-flight Tool Calls existieren:
- lange HTTP Calls
- Browser Sessions
- Queue Worker, die schon arbeiten
Du brauchst Semantik:
- stop new runs
- stop new tool calls
- optional force-cancel in-flight work (best-effort)
4) Scope: global vs per-tenant
Du willst nicht das ganze Produkt stoppen, weil ein Tenant in einer Loop hängt. Du willst:
- global switch (nuklear)
- per-tenant switch (chirurgisch)
- per-tool disable list (z. B. „kein Browser heute“)
Implementierungsbeispiel (echter Code)
Dieses Pattern:
- liest Kill-State aus einem shared store (pseudo)
- checkt ihn an zwei Stellen: Loop + Tool Gateway
- unterscheidet „stop all“ vs „disable writes“
from dataclasses import dataclass
from typing import Any
@dataclass(frozen=True)
class KillState:
stop_all: bool = False
disable_writes: bool = True
disabled_tools: set[str] = None
class Killed(RuntimeError):
pass
def load_kill_state(*, tenant_id: str) -> KillState:
# Pseudo: Redis/DB/feature-flag service. Must be fast + reliable.
# Split global + per-tenant state.
global_state = read_flag("agent_kill_global") # (pseudo)
tenant_state = read_flag(f"agent_kill_tenant:{tenant_id}") # (pseudo)
disabled_tools = set(read_list("agent_disabled_tools")) # (pseudo)
return KillState(
stop_all=bool(global_state or tenant_state),
disable_writes=True,
disabled_tools=disabled_tools,
)
WRITE_TOOLS = {"email.send", "db.write", "ticket.create", "ticket.close"}
def guard_tool_call(*, kill: KillState, tool: str) -> None:
if kill.stop_all:
raise Killed("killed: stop_all")
if tool in (kill.disabled_tools or set()):
raise Killed(f"killed: tool_disabled:{tool}")
if kill.disable_writes and tool in WRITE_TOOLS:
raise Killed(f"killed: writes_disabled:{tool}")
def run(task: str, *, tenant_id: str, tools) -> dict[str, Any]:
kill = load_kill_state(tenant_id=tenant_id)
for _ in range(1000):
if kill.stop_all:
return {"status": "stopped", "stop_reason": "killed"}
action = llm_decide(task) # (pseudo)
if action.kind != "tool":
return {"status": "ok", "answer": action.final_answer}
guard_tool_call(kill=kill, tool=action.name)
obs = tools.call(action.name, action.args) # (pseudo)
task = update(task, action, obs) # (pseudo)
return {"status": "stopped", "stop_reason": "max_steps"}const WRITE_TOOLS = new Set(["email.send", "db.write", "ticket.create", "ticket.close"]);
export class Killed extends Error {}
export function loadKillState({ tenantId }) {
// Pseudo: feature-flag store. Must be fast + reliable.
const globalStop = readFlag("agent_kill_global"); // (pseudo)
const tenantStop = readFlag("agent_kill_tenant:" + tenantId); // (pseudo)
const disabledTools = new Set(readList("agent_disabled_tools")); // (pseudo)
return { stopAll: Boolean(globalStop || tenantStop), disableWrites: true, disabledTools };
}
export function guardToolCall({ kill, tool }) {
if (kill.stopAll) throw new Killed("killed: stop_all");
if (kill.disabledTools && kill.disabledTools.has(tool)) throw new Killed("killed: tool_disabled:" + tool);
if (kill.disableWrites && WRITE_TOOLS.has(tool)) throw new Killed("killed: writes_disabled:" + tool);
}Echter Incident (mit Zahlen)
Wir hatten einen Agenten, der Follow-up Emails geschrieben und gesendet hat. Er war hinter einem „send_email“-Tool — und (oops) noch ohne Approval Gate.
Ein Prompt Change hat „follow up“ als „send now“ interpretiert.
Impact in 22 Minuten:
- 117 Emails gesendet (einige Duplikate)
- ~4 Stunden Customer Damage Control
- das Modell war nicht „gehackt“ — es war einfach falsch, laut
Der Kill Switch, den wir dachten wir hätten, war ein UI Toggle. Background Worker haben ihn ignoriert.
Fix:
- Kill Switch enforced im Tool Gateway (writes disabled)
- per-tenant stop (damit ein Tenant nicht alle nuked)
- audit log entries wenn Kill State einen Tool Call blockt
- Incident Runbook: kill switch zuerst, Fragen später
Abwägungen
- Kill Switches senken Availability während Incidents. Das ist besser als irreversible Writes.
- Du musst den Kill Path testen. Untested kill switches failen zur schlechtesten Zeit.
- Shared-State Reads adden Latenz; halte es schnell und cache kurz (Sekunden, nicht Minuten).
Wann du es NICHT nutzen solltest
- Nutze „kill switch“ nicht als Ersatz für echte Governance (permissions, approvals, budgets).
- Bau keinen Kill Switch, der nur client-side ist. Er wird dich anlügen.
- Verlass dich nicht im Normalbetrieb auf Kill Switches. Sie sind fürs Stop-the-bleeding.
Checkliste (Copy/Paste)
- [ ] Global kill switch (stop new runs)
- [ ] Per-tenant kill switch (surgical stop)
- [ ] Enforced in tool gateway (stops side effects)
- [ ] Disable writes mode (read-only degrade)
- [ ] Tool disable list (e.g., “no browser”)
- [ ] Audit logs for kill blocks + operator actions
- [ ] Tested runbook: flip, verify, drain, recover
Sicheres Default-Config-Snippet (JSON/YAML)
kill_switch:
global_flag: "agent_kill_global"
per_tenant_flag_prefix: "agent_kill_tenant:"
mode_when_enabled: "disable_writes"
disabled_tools_key: "agent_disabled_tools"
cache_ttl_s: 2
FAQ (3–5)
Von Patterns genutzt
Verwandte Failures
Q: Soll der Kill Switch alles stoppen oder nur Writes?
A: Default: Writes deaktivieren. „Stop everything“ ist die nukleare Option, wenn du der Loop gar nicht vertraust.
Q: Wo enforce ich den Kill Switch?
A: Im Tool Gateway und in der Run Loop. Wenn Tool Calls nicht geblockt werden, ist es nicht echt.
Q: Kann ich Kill State cachen?
A: Ja, aber TTL in Sekunden. Incidents sind Sekunden, nicht Minuten.
Q: Brauche ich per-tenant Kill Switches?
A: Wenn du multi-tenant bist: ja. Sonst wird ein Kunden-Incident zum Outage für alle.
Verwandte Seiten (3–6 Links)
- Foundations: What makes an agent production-ready · Why agents fail in production
- Failure: Cascading tool failures · Tool spam loops
- Governance: Budget controls · Tool permissions
- Production stack: Production agent stack