Tool-Zugriff in Python beschränken: Vollständiges Beispiel

Vollstandiges runnable Beispiel mit Tool-Allowlist, Action-Allowlist und Blockierung verbotener Aufrufe.
Auf dieser Seite
  1. Was dieses Beispiel zeigt
  2. Projektstruktur
  3. Ausfuhren
  4. Was wir im Code bauen
  5. Code
  6. tools.py — reale Tools
  7. gateway.py — policy gateway (Schlusselschicht)
  8. llm.py — Modell und Tool-Schemas
  9. main.py — Agenten-Loop mit Policy-Prufung
  10. requirements.txt
  11. Beispielausgabe
  12. Warum das ein Production-Ansatz ist
  13. Wo du als nachstes tiefer gehen kannst
  14. Vollstandiger Code auf GitHub

Dies ist die vollstandige Implementierung des Beispiels aus dem Artikel Wie man den Zugriff auf Tools begrenzt.

Wenn du den Artikel noch nicht gelesen hast, starte dort. Hier liegt der Fokus auf dem Code: wie die Begrenzungen zur Runtime genau funktionieren.


Was dieses Beispiel zeigt

  • Ebene 1 (tool access): welche Tools der Agent uberhaupt aufrufen darf
  • Ebene 2 (action access): welche Aktionen innerhalb eines zuganglichen Tools erlaubt sind
  • Policy gateway: zentralisierte Prufung von Requests vor der Ausfuhrung
  • Fallback-Verhalten: der Agent bekommt einen Fehler und wahlt den sicheren nachsten Schritt

Projektstruktur

TEXT
foundations/
└── tool-calling/
    └── python/
        ├── main.py           # agent loop
        ├── llm.py            # model + tool schemas
        ├── gateway.py        # policy checks + execution
        ├── tools.py          # tools (system actions)
        └── requirements.txt

Diese Trennung ist wichtig: Das Modell schlagt eine Aktion vor, und gateway.py entscheidet, ob diese Aktion uberhaupt ausgefuhrt wird.


Ausfuhren

1. Repository klonen und in den Ordner wechseln:

BASH
git clone https://github.com/AgentPatterns-tech/agentpatterns.git
cd foundations/tool-calling/python

2. Abhangigkeiten installieren:

BASH
pip install -r requirements.txt

3. API-Schlussel setzen:

BASH
export OPENAI_API_KEY="sk-..."

4. Starten:

BASH
python main.py

Was wir im Code bauen

Wir bauen einen vorsichtigen Roboter, der nicht einfach alles machen darf.

  • Die AI kann jede Aktion anfragen
  • ein spezieller "Wachter" (gateway) pruft, ob das erlaubt ist
  • wenn es verboten ist, tut der Roboter nichts Gefahrliches und erklart eine sichere Alternative

Das ist wie eine Tur mit Schloss: ohne Erlaubnis kommt der Befehl nicht durch.


Code

tools.py — reale Tools

PYTHON
from typing import Any

CUSTOMERS = {
    101: {"id": 101, "name": "Anna", "tier": "free", "email": "anna@gmail.com"},
    202: {"id": 202, "name": "Max", "tier": "pro", "email": "max@company.local"},
}


def customer_db(action: str, customer_id: int, new_tier: str | None = None) -> dict[str, Any]:
    customer = CUSTOMERS.get(customer_id)
    if not customer:
        return {"ok": False, "error": f"customer {customer_id} not found"}

    if action == "read":
        return {"ok": True, "customer": customer}

    if action == "update_tier":
        if not new_tier:
            return {"ok": False, "error": "new_tier is required"}
        customer["tier"] = new_tier
        return {"ok": True, "customer": customer}

    return {"ok": False, "error": f"unknown action '{action}'"}


def email_service(to: str, subject: str, body: str) -> dict[str, Any]:
    return {
        "ok": True,
        "status": "queued",
        "to": to,
        "subject": subject,
        "preview": body[:80],
    }

Das sind normale Python-Funktionen. Risiko beginnt, wenn der Agent sie ohne Kontrolle aufrufen kann.


gateway.py — policy gateway (Schlusselschicht)

PYTHON
import json
from typing import Any

from tools import customer_db, email_service

TOOL_REGISTRY = {
    "customer_db": customer_db,
    "email_service": email_service,
}

# Level 1: which tools are visible to the agent
ALLOWED_TOOLS = {"customer_db"}

# Level 2: which actions are allowed inside each tool
ALLOWED_ACTIONS = {
    "customer_db": {"read"},  # update_tier is blocked
}


def execute_tool_call(tool_name: str, arguments_json: str) -> dict[str, Any]:
    if tool_name not in ALLOWED_TOOLS:
        return {"ok": False, "error": f"tool '{tool_name}' is not allowed"}

    tool = TOOL_REGISTRY.get(tool_name)
    if tool is None:
        return {"ok": False, "error": f"tool '{tool_name}' not found"}

    try:
        args = json.loads(arguments_json or "{}")
    except json.JSONDecodeError:
        return {"ok": False, "error": "invalid JSON arguments"}

    if tool_name == "customer_db":
        action = args.get("action")
        if action not in ALLOWED_ACTIONS["customer_db"]:
            return {
                "ok": False,
                "error": f"action '{action}' is not allowed for tool '{tool_name}'",
            }

    try:
        result = tool(**args)
    except TypeError as exc:
        return {"ok": False, "error": f"invalid arguments: {exc}"}

    return {"ok": True, "tool": tool_name, "result": result}

Genau hier werden Regeln enforced. Das Modell kann diese Schicht nicht per Prompt umgehen.


llm.py — Modell und Tool-Schemas

PYTHON
import os
from openai import OpenAI

api_key = os.environ.get("OPENAI_API_KEY")

if not api_key:
    raise EnvironmentError(
        "OPENAI_API_KEY is not set.\n"
        "Run: export OPENAI_API_KEY='sk-...'"
    )

client = OpenAI(api_key=api_key)

SYSTEM_PROMPT = """
You are a support agent.
Use tools when data is missing.
If a tool or action is blocked, do not argue; suggest a safe manual next step.
Reply briefly in English.
""".strip()

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "customer_db",
            "description": "Customer data operations: read or update tier",
            "parameters": {
                "type": "object",
                "properties": {
                    "action": {"type": "string", "enum": ["read", "update_tier"]},
                    "customer_id": {"type": "integer"},
                    "new_tier": {"type": "string"},
                },
                "required": ["action", "customer_id"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "email_service",
            "description": "Sends an email to the customer",
            "parameters": {
                "type": "object",
                "properties": {
                    "to": {"type": "string"},
                    "subject": {"type": "string"},
                    "body": {"type": "string"},
                },
                "required": ["to", "subject", "body"],
            },
        },
    },
]


def ask_model(messages: list[dict]):
    completion = client.chat.completions.create(
        model="gpt-4.1-mini",
        messages=[{"role": "system", "content": SYSTEM_PROMPT}, *messages],
        tools=TOOLS,
        tool_choice="auto",
    )
    return completion.choices[0].message

Beachte: llm.py kann dem Modell mehr Tools zeigen, aber gateway.py blockiert trotzdem nicht erlaubte.


main.py — Agenten-Loop mit Policy-Prufung

PYTHON
import json

from gateway import execute_tool_call
from llm import ask_model

MAX_STEPS = 6

TASK = (
    "For customer_id=101, check the profile, upgrade tier to pro, "
    "and send a confirmation email to anna@gmail.com. "
    "If any action is blocked, explain the safe manual next step."
)


def to_assistant_message(message) -> dict:
    tool_calls = []
    for tc in message.tool_calls or []:
        tool_calls.append(
            {
                "id": tc.id,
                "type": "function",
                "function": {
                    "name": tc.function.name,
                    "arguments": tc.function.arguments,
                },
            }
        )

    return {
        "role": "assistant",
        "content": message.content or "",
        "tool_calls": tool_calls,
    }


def run():
    messages: list[dict] = [{"role": "user", "content": TASK}]

    for step in range(1, MAX_STEPS + 1):
        print(f"\n=== STEP {step} ===")
        assistant = ask_model(messages)
        messages.append(to_assistant_message(assistant))

        if assistant.content and assistant.content.strip():
            print("Assistant:", assistant.content.strip())

        tool_calls = assistant.tool_calls or []
        if not tool_calls:
            print("\nDone: model finished the task.")
            return

        for tc in tool_calls:
            print(f"Tool call: {tc.function.name}({tc.function.arguments})")
            execution = execute_tool_call(
                tool_name=tc.function.name,
                arguments_json=tc.function.arguments,
            )
            print("Gateway result:", execution)

            messages.append(
                {
                    "role": "tool",
                    "tool_call_id": tc.id,
                    "content": json.dumps(execution, ensure_ascii=False),
                }
            )

    print("\nStop: MAX_STEPS reached.")


if __name__ == "__main__":
    run()

Hier sieht man die Boundary gut: Das Modell schlagt vor, Gateway entscheidet, ob die Aktion uberhaupt passiert.


requirements.txt

TEXT
openai>=1.0.0

Beispielausgabe

TEXT
=== STEP 1 ===
Tool call: customer_db({"action":"read","customer_id":101})
Gateway result: {'ok': True, 'tool': 'customer_db', 'result': {'ok': True, 'customer': {'id': 101, 'name': 'Anna', 'tier': 'free', 'email': 'anna@gmail.com'}}}

=== STEP 2 ===
Tool call: customer_db({"action":"update_tier","customer_id":101,"new_tier":"pro"})
Gateway result: {'ok': False, 'error': "action 'update_tier' is not allowed for tool 'customer_db'"}

=== STEP 3 ===
Tool call: email_service({"to":"anna@gmail.com","subject":"Tier updated","body":"..."})
Gateway result: {'ok': False, 'error': "tool 'email_service' is not allowed"}

=== STEP 4 ===
Assistant: I can only read the profile. Tier updates and email sending require manual operator action.

Done: model finished the task.

Hinweis: die Anzahl der STEP-Eintrage kann zwischen Runs variieren.
In einem STEP kann das Modell mehrere tool_calls zuruckgeben, deshalb siehst du manchmal 2 Schritte und manchmal 4.
Das ist normale LLM-Nichtdeterministik. Wichtig ist, dass das Policy-Gateway update_tier und email_service stabil blockiert.


Warum das ein Production-Ansatz ist

Naives Tool CallingMit Policy Gateway
Das Modell entscheidet alles allein
Es gibt zentralisierte Zugriffskontrolle
Read/Write-Aktionen konnen getrennt werden
Fehler werden in einen kontrollierten Fallback uberfuhrt

Wo du als nachstes tiefer gehen kannst

  • Fuge ALLOWED_TOOLS_BY_ROLE hinzu (z. B. viewer, operator, admin)
  • Fuge einen Approval-Flow fur update_tier statt kompletter Sperre hinzu
  • Fuge max_tool_calls und max_cost neben MAX_STEPS hinzu
  • Logge tool_name, args_hash, decision, reason fur Audit

Vollstandiger Code auf GitHub

Im Repository liegt die vollstandige Version dieser Demo: Tool-Loop, Allowlist-Prufungen und kontrollierter Fallback.

Vollstandigen Code auf GitHub ansehen ↗
⏱️ 7 Min. LesezeitAktualisiert 2. März 2026Schwierigkeit: ★★☆
Integriert: Production ControlOnceOnly
Guardrails für Tool-Calling-Agents
Shippe dieses Pattern mit Governance:
  • Budgets (Steps / Spend Caps)
  • Tool-Permissions (Allowlist / Blocklist)
  • Kill switch & Incident Stop
  • Idempotenz & Dedupe
  • Audit logs & Nachvollziehbarkeit
Integrierter Hinweis: OnceOnly ist eine Control-Layer für Production-Agent-Systeme.

Autor

Nick — Engineer, der Infrastruktur für KI-Agenten in Produktion aufbaut.

Fokus: Agent-Patterns, Failure-Modes, Runtime-Steuerung und Systemzuverlässigkeit.

🔗 GitHub: https://github.com/mykolademyanov


Redaktioneller Hinweis

Diese Dokumentation ist KI-gestützt, mit menschlicher redaktioneller Verantwortung für Genauigkeit, Klarheit und Produktionsrelevanz.

Der Inhalt basiert auf realen Ausfällen, Post-Mortems und operativen Vorfällen in produktiv eingesetzten KI-Agenten-Systemen.