Problem (aus der Praxis)
Irgendwann landest du bei demselben Prod-Problem: Output-Shape ist wichtiger als Prosa.
Wenn du Tools callst und Side Effects triggerst, brauchst du:
- Schema Validation
- Invariants
- fail-closed Verhalten
Typed-first Frameworks sind deshalb attraktiv. Flexible Frameworks sind nĂŒtzlich â bis Teams die Boundaries vergessen.
Schnelle Entscheidung (wer sollte was wÀhlen)
- Typed-first (PydanticAI-style): wenn Tools/Actions dominieren und Validation Default sein soll.
- LangChain agents: wenn du viele Integrationen willst und Boundaries bewusst selbst enforceâst.
- Ohne Validation: beide Optionen enden in Silent Failures.
Warum man in Prod die falsche Wahl trifft
- Framework wird als Governance-Ersatz gesehen (ist es nicht).
- Structured Outputs werden als ânice-to-haveâ behandelt (in Prod sind sie Safety).
- âViele Integrationenâ wird ĂŒberbewertet (mehr Blast Radius ohne Gateway).
Vergleichstabelle
| Kriterium | Typed-first | Flexible agents | Prod-Relevanz | |---|---|---|---| | Default Validation | höher | hĂ€ngt von dir ab | Fail closed | | Integration Surface | kleiner | gröĂer | Blast radius | | Debuggability | besser mit Typen | besser mit Instrumentierung | Traces | | Drift Handling | besser testbar | riskanter | Golden tasks |
Wo das in Production bricht
Typed-first:
- Schema Maintenance
- Over-constraints (mehr rejects)
- Typing als âSecurityâ missverstanden
Flexible:
- best-effort parsing
- silent coercion
- Tool Outputs als Instruktionen
- Output shapes driften ohne Tests
Implementierungsbeispiel (echter Code)
Framework egal: strict boundary zwischen Model Output und Actions.
from dataclasses import dataclass
from typing import Any
@dataclass(frozen=True)
class Decision:
kind: str # "final" | "tool"
tool: str | None
args: dict[str, Any] | None
answer: str | None
class InvalidDecision(RuntimeError):
pass
def validate_decision(obj: Any) -> Decision:
if not isinstance(obj, dict):
raise InvalidDecision("expected object")
kind = obj.get("kind")
if kind not in {"final", "tool"}:
raise InvalidDecision("invalid kind")
if kind == "final":
ans = obj.get("answer")
if not isinstance(ans, str) or not ans.strip():
raise InvalidDecision("missing answer")
return Decision(kind="final", tool=None, args=None, answer=ans)
tool = obj.get("tool")
args = obj.get("args")
if not isinstance(tool, str):
raise InvalidDecision("missing tool")
if not isinstance(args, dict):
raise InvalidDecision("missing args")
return Decision(kind="tool", tool=tool, args=args, answer=None)export class InvalidDecision extends Error {}
export function validateDecision(obj) {
if (!obj || typeof obj !== "object") throw new InvalidDecision("expected object");
const kind = obj.kind;
if (kind !== "final" && kind !== "tool") throw new InvalidDecision("invalid kind");
if (kind === "final") {
if (typeof obj.answer !== "string" || !obj.answer.trim()) throw new InvalidDecision("missing answer");
return { kind: "final", answer: obj.answer };
}
if (typeof obj.tool !== "string") throw new InvalidDecision("missing tool");
if (!obj.args || typeof obj.args !== "object") throw new InvalidDecision("missing args");
return { kind: "tool", tool: obj.tool, args: obj.args };
}Echter Incident (mit Zahlen)
Flexible Agent mit best-effort Tool Call Parsing.
Bei Partial Outage kam HTML im Tool Output, Modell kopierte Teile in args.
Parser coercte es, Write passierte.
Impact:
- 17 Runs schrieben Garbage in eine Queue
- downstream Workers crashten ~25 Minuten
- On-call ~2 Stunden Root Cause, weil Logs nur âfinal answerâ hatten
Fix: strict parsing + schema validation + fail-closed vor Writes + metric invalid_decision_rate.
Migrationspfad (A â B)
- Flexible â typed-first: Boundary validieren, kleines Decision Schema definieren, Writes zuerst typisieren.
- Typed-first â flexible: Typen fĂŒr Actions behalten, free-form Text nur ohne Side Effects.
Entscheidungshilfe
- Writes/Money â typed/validated boundaries zuerst.
- Experimente â Flex ok, aber Budgets + Logs.
- Multi-Tenant â strict validation non-negotiable.
AbwÀgungen
- Validation senkt completion rate (meistens gut).
- Typing kostet Maintenance.
- FlexibilitÀt shippt schneller, aber mit mehr Prod-Surprises.
Wann du es NICHT nutzen solltest
- Typing ist keine Security.
- Kein best-effort parsing fĂŒr Write Actions.
- Validation Failures sind ein Metric, kein Makel.
Checkliste (Copy/Paste)
- [ ] Model decisions schema-validate
- [ ] Tool outputs schema+invariants validate
- [ ] Fail-closed fĂŒr Writes
- [ ] Budgets + stop reasons
- [ ] Tool call logs + audit
- [ ] Canary changes; drift messen
Sicheres Default-Config-Snippet (JSON/YAML)
validation:
model_decision:
fail_closed: true
schema: "Decision(kind, tool?, args?, answer?)"
tool_output:
fail_closed: true
max_chars: 200000
budgets:
max_steps: 25
max_tool_calls: 12
monitoring:
track: ["invalid_decision_rate", "tool_output_invalid_rate", "stop_reason"]
FAQ (3â5)
Von Patterns genutzt
Verwandte Failures
Verwandte Seiten (3â6 Links)
- Foundations: Tool calling basics · LLM limits
- Failure: Response corruption · Silent drift
- Governance (EN): Tool permissions · Budget controls
- Production stack: Production stack