DE ▾

OpenAI-kompatible unzensierte LLM-APIuncensoredapis.com

HomeAgents

Agents auf einer unzensierten LLM-API: Function Calling, Frameworks und Guardrails

Ein Agent ist eine Schleife: Das Modell fragt nach einer Funktion, dein Code führt sie aus, und das Ergebnis fließt zurück in die Konversation. Da die unzensierte LLM-API Tool Calling im Standard-Chat-Format beherrscht, kannst du diese Schleife in vierzig Zeilen bauen und denselben Endpunkt dann in LangChain oder LlamaIndex einbinden. Dieser Leitfaden behandelt beides sowie die Checks, die die Tool-Ausführung sicher halten.

Aktualisiert

Wichtige Punkte

  1. Übergib Tools in der Anfrage, achte auf finish_reason gleich tool_calls, führe die Funktionen aus, hänge tool-Nachrichten an und wiederhole, bis das Modell textlich antwortet.
  2. LangChain benötigt ChatOpenAI mit base_url; LlamaIndex benötigt OpenAILike mit api_base und dem Function-Calling-Flag, das gesetzt ist.
  3. Das Modell schlägt Aufrufe vor; dein Code entscheidet, ob sie ausgeführt werden. Whitelist, validiere, setze Zeitlimits und begrenze die Schleife.
  4. Agents vervielfachen die Anzahl der Anfragen, also plane für das Ratenlimit von 300 Anfragen pro Minute und das Kontextfenster mit 100.000 Token.

Anatomie der Tool-Calling-Schleife

Entferne Frameworks, und jeder Agent macht dieselben vier Dinge. Du sendest die Konversation plus eine tools-Liste. Das Modell antwortet entweder mit Text, was bedeutet, dass es fertig ist, oder mit tool_calls, was bedeutet, dass es etwas ausgeführt haben möchte. Du führst jeden Aufruf aus und hängst eine tool-Nachricht pro Aufruf an, unter Zitat der id des Aufrufs. Dann fragst du das Modell mit der längeren Konversation erneut.

Das Signal für die Verzweigung ist finish_reason. Wenn es tool_calls entspricht, hat die Assistentennachricht content auf null gesetzt und ein tool_calls-Array, wobei jeder Eintrag eine id, einen Funktionsnamen und einen arguments-String mit JSON enthält. Die Referenzseite zeigt die genaue Struktur. Zwei Details bringen Leute durcheinander: Die Argumente sind ein String, der analysiert werden muss, und die Assistentennachricht, die die Aufrufe enthält, muss in der Historie vor deinen Tool-Ergebnissen in der richtigen Reihenfolge bleiben.

Eine komplette Schleife von Grund auf

Das folgende Skript definiert eine Funktion, beschreibt sie im JSON-Schema und läuft, bis das Modell normalen Text erzeugt. Es nutzt das offizielle Python SDK, das auf die API zeigt. Jeder Durchlauf hängt die Assistentennachricht an, dann eine Tool-Nachricht pro Aufruf, und ein Schrittzähler stoppt unkontrolliertes Verhalten.

import json, os
from openai import OpenAI

client = OpenAI(base_url="https://api.uncensoredapis.com/v1", api_key=os.environ["API_KEY"])

TICKETS = {"T-101": "open", "T-102": "closed"}

def ticket_status(ticket_id: str) -> str:
    return TICKETS.get(ticket_id, "unknown ticket")

TOOLS = [{
    "type": "function",
    "function": {
        "name": "ticket_status",
        "description": "Look up the status of a support ticket by id.",
        "parameters": {
            "type": "object",
            "properties": {"ticket_id": {"type": "string"}},
            "required": ["ticket_id"],
        },
    },
}]
REGISTRY = {"ticket_status": ticket_status}

messages = [{"role": "user", "content": "Are tickets T-101 and T-102 still open?"}]

for step in range(6):                      # hard cap on loop turns
    r = client.chat.completions.create(
        model="uncensored", messages=messages, tools=TOOLS, max_tokens=400)
    msg = r.choices[0].message
    messages.append(msg.model_dump(exclude_none=True))
    if r.choices[0].finish_reason != "tool_calls":
        print(msg.content)
        break
    for call in msg.tool_calls:
        fn = REGISTRY.get(call.function.name)
        try:
            args = json.loads(call.function.arguments)
            result = fn(**args) if fn else f"error: unknown tool {call.function.name}"
        except Exception as exc:
            result = f"error: {exc}"
        messages.append({"role": "tool", "tool_call_id": call.id, "content": str(result)})
else:
    print("stopped: step limit reached")

Beachte, was die Schleife im Fehlerfall macht: Fehler werden als Strings zu Tool-Ergebnissen. Das ermöglicht es dem Modell, das Problem zu lesen und es erneut zu versuchen, zum Beispiel eine ungültige Ticket-ID zu korrigieren, statt dass dein Programm abstürzt. Die for ... else-Klausel feuert nur, wenn die Grenze erreicht wurde, ohne eine finale Antwort, was dir zeigt, dass das Modell im Kreis läuft.

Halte Prompts fokussiert. Eine kurze Systemnachricht, die angibt, wann ein Tool aufgerufen werden soll und wann direkt geantwortet werden soll, reduziert unnötige Aufrufe, und jeder übersprungene Aufruf spart eine Hin- und Rückreise, Token und einen Slot deines Ratenlimits.

Tool-Ausführung schützen

Modellausgabe ist ein nicht vertrauenswürdiger Input. Ein vom Modell gewählter Tool-Name oder ein Argument kann falsch, seltsam oder durch Text beeinflusst sein, den das Modell von einer Webseite oder einem Benutzer gelesen hat. Behandle jeden Aufruf wie eine Anfrage aus dem öffentlichen Internet.

  • Whitelist nach Name. Suche die Funktion in einem von dir erstellten Registry nach. Nutze den String niemals zum Importieren oder Auswerten von Code.
  • Trenne Lese- von Schreibzugriff. Nur-Lese-Tools laufen frei. Alles, was E-Mails sendet, Geld ausgibt, Daten löscht oder Dateien berührt, benötigt eine explizite menschliche Freigabe.
  • Argumente validieren gegen das Schema, das du angekündigt hast: erforderliche Schlüssel, keine zusätzlichen Schlüssel, Typen und Längengrenzen.
  • Zeit und Ausgabe begrenzen. Wende Timeouts an und kürze Ergebnisse vor dem Zurückgeben, damit eine riesige Tool-Ausgabe dein Kontextfenster mit 100.000 Token nicht auffrisst.
  • Iterationen und Kosten kappen. Begrenze Schritte pro Aufgabe und Token pro Sitzung unter Verwendung des usage-Blocks.
  • Jeden Aufruf protokollieren mit Name, Argumenten und Ergebnis, damit du das Verhalten auditieren kannst.
import json
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutTimeout

ALLOWED = {"ticket_status", "lookup_invoice"}      # read-only tools only
NEEDS_APPROVAL = {"refund_order"}                  # side effects wait for a human
POOL = ThreadPoolExecutor(max_workers=4)

def validate(args: dict, schema: dict) -> None:
    props = schema["properties"]
    for key in schema.get("required", []):
        if key not in args:
            raise ValueError(f"missing argument: {key}")
    for key, val in args.items():
        if key not in props:
            raise ValueError(f"unexpected argument: {key}")
        if props[key]["type"] == "string" and not isinstance(val, str):
            raise ValueError(f"{key} must be a string")
        if isinstance(val, str) and len(val) > 200:
            raise ValueError(f"{key} too long")

def run_tool(name, raw_args, registry, schemas, approve=lambda n, a: False):
    if name in NEEDS_APPROVAL:
        args = json.loads(raw_args)
        if not approve(name, args):
            return "denied: this action needs human approval"
    elif name not in ALLOWED:
        return f"error: tool {name} is not permitted"
    args = json.loads(raw_args)
    validate(args, schemas[name]["parameters"])
    future = POOL.submit(registry[name], **args)
    try:
        return str(future.result(timeout=5))[:2000]    # time and size limits
    except FutTimeout:
        return "error: tool timed out"

Behandle Tool-Ergebnisse auch als nicht vertrauenswürdig. Wenn ein Tool gescrapten Text zurückgibt, der Anweisungen enthält, kann das Modell ihnen folgen. Packe solchen Inhalt in klare Delimiter in die Tool-Nachricht und gib in deinem System-Prompt an, dass Tool-Ausgaben Daten sind, keine Befehle.

Tool-Schemas schreiben, die das Modell nutzen kann

Das Schema ist die einzige Dokumentation, die das Modell sieht, also verdient es dieselbe Sorgfalt wie eine öffentliche API. Benenne Funktionen mit Verben und Substantiven, die zur Aufgabe passen, wie ticket_status oder lookup_invoice, statt generischer Labels wie run oder helper. Schreibe die description als Anweisung: Was die Funktion macht, was sie zurückgibt und wann sie nicht genutzt werden soll.

  • Wenige und flache Parameter halten. Zwei oder drei einfache Felder werden viel häufiger korrekt aufgerufen als ein verschachteltes Objekt mit zehn.
  • Enums verwenden für geschlossene Mengen, zum Beispiel "enum": ["open", "closed"], damit das Modell keinen Wert erfinden kann.
  • Jede Eigenschaft beschreiben mit einer Einheit oder einem Beispiel: "Ticket-ID wie T-101" schlägt einen nackten String.
  • Kompakte Ergebnisse zurückgeben. Strings oder kleine JSON-Objekte funktionieren am besten; lange Dumps verbrennen Kontext für wenig Gewinn.
  • Mehrere schmale Tools bevorzugen gegenüber einem einzigen Omnibus-Tool mit einem Modus-Flag.

Wenn ein Aufruf die Validierung verfehlt, gib die Validierungsnachricht als Tool-Ergebnis zurück, statt einen Fehler auszulösen. Das Modell repariert den Aufruf normalerweise im nächsten Durchgang, da es lesen kann, was falsch war. Wenn es denselben Fehler zweimal macht, schreibe die Beschreibung um, nicht die Schleife.

LangChain-Konfiguration

LangChains ChatOpenAI-Klasse akzeptiert ein base_url, also wird kein benutzerdefiniertes Provider-Paket benötigt. Setze model auf uncensored, übergebe den Schlüssel aus deiner Umgebung und binde Tools mit bind_tools. Installiere langchain-openai und langchain-core.

import os
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool

@tool
def ticket_status(ticket_id: str) -> str:
    """Return the status of a support ticket."""
    return {"T-101": "open", "T-102": "closed"}.get(ticket_id, "unknown ticket")

llm = ChatOpenAI(
    model="uncensored",
    base_url="https://api.uncensoredapis.com/v1",
    api_key=os.environ["API_KEY"],
    temperature=0.3,
    max_tokens=400,
)
llm_with_tools = llm.bind_tools([ticket_status])

ai = llm_with_tools.invoke("What is the status of ticket T-101?")
for call in ai.tool_calls:
    print(call["name"], call["args"], "->", ticket_status.invoke(call["args"]))

Das Ergebnis von invoke ist eine AIMessage, deren tool_calls-Liste analysierte Namen und Argumente enthält, also überspringst du den manuellen JSON-Schritt. Um eine vollständige autonome Schleife zu erhalten, übergebe dasselbe llm-Objekt an LangChain-Agent-Konstruktoren; der Endpunkt muss nur Tool Calling unterstützen, was er tut. Wenn ein Agent seine Tools zu ignorieren scheint, prüfe, ob Docstring und Type Hints vorhanden sind, da sie das Schema bilden, das das Modell sieht.

LlamaIndex-Konfiguration

LlamaIndex liefert OpenAILike für Server, die das Chat-Completion-Format befolgen. Drei Einstellungen sind wichtig. api_base zeigt auf die API, is_chat_model=True wählt den Chat-Endpunkt und is_function_calling_model=True teilt dem Framework mit, dass es Tools senden darf. Setze context_window auf 100000, damit Text-Splitter und Speicherpuffer sich korrekt dimensionieren. Installiere llama-index-llms-openai-like.

import os
from llama_index.llms.openai_like import OpenAILike
from llama_index.core.tools import FunctionTool

def ticket_status(ticket_id: str) -> str:
    """Return the status of a support ticket."""
    return {"T-101": "open", "T-102": "closed"}.get(ticket_id, "unknown ticket")

llm = OpenAILike(
    model="uncensored",
    api_base="https://api.uncensoredapis.com/v1",
    api_key=os.environ["API_KEY"],
    is_chat_model=True,
    is_function_calling_model=True,
    context_window=100000,
    max_tokens=400,
)

tool = FunctionTool.from_defaults(fn=ticket_status)
response = llm.predict_and_call([tool], "Check ticket T-102 for me.")
print(response)

Von hier aus kannst du dasselbe llm an einen Agent-Worker oder eine Query-Engine übergeben. Denke daran, dass Retrieval-Pipelines viele Chunks in einen Prompt stopfen können; begrenze bei einem Kontextfenster mit 100.000 Token, das sich Prompt und Completion teilen, die Anzahl der abgerufenen Chunks und halte max_tokens bescheiden.

Agents im Budget betreiben

Ein Agent wandelt eine einzelne Benutzer-Nachricht in mehrere Anfragen um, plane also die Kapazität. Eine Aufgabe mit fünf Tool-Runden kostet sechs Modellaufrufe. Zehn gleichzeitige Aufgaben erzeugen daher in kurzer Zeit sechzig Anfragen, und das Limit von 300 Anfragen pro Minute ist schneller erreicht als in einer Chat-Anwendung. Platziere den Token-Bucket-Throttle aus dem Ratgeber zu Fehlern und Ratenlimits vor deinem Client.

Der Kontext wächst mit jeder Runde, da jedes Tool-Ergebnis angehängt wird. Fasse alte Tool-Ausgaben zusammen oder entferne sie, sobald sie verwendet wurden, und behalte usage.prompt_tokens im Auge; wenn es sich 50.000 nähert, komprimiere den Verlauf. Gehe bei den Kosten von einer beispielhaften Aufgabe mit 6 Aufrufen aus, die durchschnittlich 3.000 Prompt-Token und 200 Completion-Token verbraucht: Das sind 18.000 Input-Token (ca. 0,0045 $ bei 0,25 $ pro Million) und 1.200 Output-Token (ca. 0,0012 $ bei 1,00 $ pro Million). Diese Token-Anzahlen sind Annahmen für die Summen, keine Messwerte. Wenn du neu am Endpunkt bist, starte mit den Dokumenten und dem kostenlosen Testguthaben.

Agent testen und debuggen

Agents scheitern auf Arten, die Unit-Tests für eine einzelne Funktion verpassen, also teste die Konversation. Speichere die vollständige Nachrichtenliste für jeden Durchlauf, einschließlich Tool-Argumenten und -Ergebnissen, und spiele Fehler aus dem Protokoll ab. Drei Gewohnheiten lohnen sich schnell.

  1. Simuliere das Modell. Mock den Client, sodass er skriptgesteuerte tool_calls-Nachrichten zurückgibt. So testest du deine Registry, Validierung und Schleifenbeendigung ohne Netzwerk und ohne Kosten.
  2. Pin-Sampling. Verwende ein niedriges temperature, z. B. 0,2, für tool-gesteuerte Aufgaben. Weniger Zufälligkeit führt zu besser wiederholbaren Argument-JSONs, und du kannst es später für schreibintensive Schritte erhöhen.
  3. Behaupte über Trajektorien. Prüfe für eine Handvoll fester Prompts, welche Tools aufgerufen wurden und in welcher Reihenfolge, nicht nur den abschließenden Text.

Beobachte auch finish_reason. Ein Wert von length mitten in einem Tool-Aufruf bedeutet, dass die Argumente abgeschnitten wurden; erhöhe max_tokens (bis zu 32.000), und das JSON wird vervollständigt. Streaming funktioniert auch mit Tools, aber sammle Argumentfragmente pro Aufrufindex an und parst sie erst, nachdem das Stream endet. Zur Auffrischung dieser Chunks sieh dir den Streaming-Abschnitt der Referenz an.

Fragen und Antworten

Unterstützt die API parallele Werkzeugaufrufe in einer Antwort?

Die Nachricht enthält ein tool_calls-Array, also behandle jeden Eintrag darin und sende eine Werkzeugnachricht pro Aufruf-ID.

Warum läuft meine Agentenschleife endlos?

Normalerweise sind die Werkzeugfehler nicht aussagekräftig oder der System-Prompt sagt nie, wann er aufhören soll. Füge eine Schritt-Obergrenze hinzu und gib klare Fehlerstrings zurück.

Brauche ich LangChain oder LlamaIndex?

Nein. Eine einfache Schleife mit dem OpenAI SDK reicht aus, und Frameworks fügen nur Orchestrierung auf Basis desselben Endpunkts hinzu.

Wie verhindere ich, dass das Modell zerstörerische Werkzeuge ausführt?

Registriere sie niemals ohne einen Freigabeschritt. Nur die Funktionen in deinem Registry können ausgeführt werden, halte also Schreibaktionen hinter einer menschlichen Bestätigung zurück.

Dein Schlüssel ist nur ein Formular entfernt

Erstelle ein Konto, kopiere den Schlüssel und ändere die Base URL. Das ist die gesamte Einrichtung.