Sprachmodelle per API
Kernideen
- Die API ist dasselbe Modell wie im Chat, aber aus dem Code aufrufbar: für viele Texte, wiederholbar und in eine Auswertung eingebunden
- Jede Anfrage ist zustandslos; ein Gespräch entsteht nur, wenn der Verlauf mitgeschickt wird
- Für Weiterverarbeitung strukturierte Ausgabe verlangen, nicht Freitext parsen
- Kosten entstehen je Token, getrennt nach Eingabe und Ausgabe; vor grossen Läufen an einer Stichprobe messen
- Der Schlüssel gehört in eine Umgebungsvariable, nie in den Code oder ins Repository
- Antworten sind nicht reproduzierbar wie eine Rechnung: Ergebnis und Anfrage speichern, Stichproben prüfen
Erklärung
Vorwissen: Bausteine eines Prompts für den Inhalt einer Anfrage, Modelle und ihre Grenzen für das, was dabei schiefgehen kann, und APIs und Web-Scraping für Schnittstellen allgemein. Diese Seite rechnet nichts: Im Build gibt es keinen Modellzugang, alle Codeblöcke sind Listings. Als Beispiel dient die Claude-API von Anthropic; andere Anbieter funktionieren nach demselben Muster mit anderen Namen.
Wann die API statt des Chats
| Aufgabe | Chat | API |
|---|---|---|
| Einzelne Frage, Entwurf, Erklärung | ja | umständlich |
| 2000 Freitextantworten kategorisieren | nicht praktikabel | ja |
| Dieselbe Auswertung jeden Monat | Kopieren und Einfügen, fehleranfällig | ja, im Skript |
| Ergebnis soll direkt in einen Data Frame | Abtippen | ja, mit strukturierter Ausgabe |
Einrichten
pip install anthropic # Pythoninstall.packages("httr2") # R, Anfragen an die REST-SchnittstelleDer Schlüssel wird einmal als Umgebungsvariable gesetzt, etwa in ~/.Renviron für R oder in einer Datei .env, die in .gitignore steht:
ANTHROPIC_API_KEY=sk-ant-...Die einfachste Anfrage
import anthropic
client = anthropic.Anthropic() # liest ANTHROPIC_API_KEY
antwort = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
system="Du bist Statistikerin und antwortest knapp auf Deutsch.",
messages=[{"role": "user",
"content": "Was sagt ein p-Wert von 0.03 aus, und was nicht?"}],
)
for block in antwort.content:
if block.type == "text":
print(block.text)
print(antwort.usage.input_tokens, antwort.usage.output_tokens)library(httr2)
antwort <- request("https://api.anthropic.com/v1/messages") |>
req_headers(
"x-api-key" = Sys.getenv("ANTHROPIC_API_KEY"),
"anthropic-version" = "2023-06-01"
) |>
req_body_json(list(
model = "claude-opus-5",
max_tokens = 16000,
system = "Du bist Statistikerin und antwortest knapp auf Deutsch.",
messages = list(list(role = "user",
content = "Was sagt ein p-Wert von 0.03 aus, und was nicht?"))
)) |>
req_retry(max_tries = 3) |>
req_perform() |>
resp_body_json()
texte <- Filter(function(b) b$type == "text", antwort$content)
cat(vapply(texte, function(b) b$text, ""), sep = "\n")
c(eingabe = antwort$usage$input_tokens, ausgabe = antwort$usage$output_tokens)| Teil | Bedeutung |
|---|---|
model |
welches Modell antwortet; die Bezeichnung steht in der Dokumentation des Anbieters |
max_tokens |
Obergrenze der Antwortlänge; zu knapp gesetzt, bricht die Antwort mitten im Satz ab |
system |
Dauervorgabe für Rolle, Ton und Format, siehe Vom Prompt zum Kontext |
messages |
der Verlauf; für ein Gespräch werden frühere Fragen und Antworten mitgeschickt |
content |
die Antwort als Liste von Blöcken; Text steht in Blöcken vom Typ text |
usage |
verbrauchte Token, die Grundlage der Kosten |
stop_reason |
warum die Antwort endet: end_turn regulär, max_tokens abgeschnitten |
Strukturierte Ausgabe
Wer Antworten weiterverarbeitet, lässt sie in einem festen Schema liefern. In Python beschreibt eine Pydantic-Klasse das Schema, und das SDK prüft die Antwort dagegen:
from typing import Literal
from pydantic import BaseModel
class Einordnung(BaseModel):
kategorie: Literal["Lieferung", "Qualität", "Preis", "Sonstiges"]
stimmung: Literal["positiv", "neutral", "negativ"]
begruendung: str
antwort = client.messages.parse(
model="claude-opus-5",
max_tokens=16000,
messages=[{"role": "user",
"content": "Ordne ein: 'Ware kam drei Tage zu spät, sonst gut.'"}],
output_format=Einordnung,
)
print(antwort.parsed_output)In R wird das Schema als JSON mitgeschickt und die Antwort mit jsonlite gelesen:
schema <- list(
type = "object",
properties = list(
kategorie = list(type = "string", enum = list("Lieferung", "Qualität", "Preis", "Sonstiges")),
stimmung = list(type = "string", enum = list("positiv", "neutral", "negativ")),
begruendung = list(type = "string")
),
required = list("kategorie", "stimmung", "begruendung"),
additionalProperties = FALSE
)
antwort <- request("https://api.anthropic.com/v1/messages") |>
req_headers("x-api-key" = Sys.getenv("ANTHROPIC_API_KEY"),
"anthropic-version" = "2023-06-01") |>
req_body_json(list(
model = "claude-opus-5", max_tokens = 16000,
output_config = list(format = list(type = "json_schema", schema = schema)),
messages = list(list(role = "user",
content = "Ordne ein: 'Ware kam drei Tage zu spät, sonst gut.'"))
)) |>
req_perform() |>
resp_body_json()
jsonlite::fromJSON(antwort$content[[1]]$text)Viele Texte nacheinander
import pandas as pd
rueckmeldungen = pd.read_csv("rueckmeldungen.csv")
ergebnisse = []
for text in rueckmeldungen["text"]:
antwort = client.messages.parse(
model="claude-opus-5", max_tokens=16000,
messages=[{"role": "user", "content": f"Ordne ein: {text!r}"}],
output_format=Einordnung,
)
ergebnisse.append(antwort.parsed_output.model_dump())
eingeordnet = pd.concat([rueckmeldungen, pd.DataFrame(ergebnisse)], axis=1)
eingeordnet.to_csv("rueckmeldungen_eingeordnet.csv", index=False)Für grosse Mengen, bei denen die Antwort nicht sofort gebraucht wird, bieten Anbieter eine Stapelverarbeitung zu reduzierten Preisen an.
Kosten abschätzen
Abgerechnet wird je Million Token, Ausgabe teurer als Eingabe. Ein Token entspricht grob drei bis vier Zeichen deutschen Texts. Die Rechnung vor einem grossen Lauf:
| Grösse | Vorgehen |
|---|---|
| Token je Anfrage | an zehn echten Anfragen usage ablesen, Mittel bilden |
| Anzahl Anfragen | Zeilen im Datensatz |
| Preis | aktuelle Preisliste des Anbieters |
| Kosten | Anfragen × (Eingabetoken × Eingabepreis + Ausgabetoken × Ausgabepreis) / 1 000 000 |
Eine lange, gleichbleibende Systemvorgabe lässt sich zwischenspeichern (Prompt Caching); wiederholte Anfragen mit demselben Anfang werden dann für diesen Teil deutlich günstiger.
Fehler und Wiederholungen
| Fehler | Bedeutung | Vorgehen |
|---|---|---|
401 |
Schlüssel fehlt oder ist ungültig | Umgebungsvariable prüfen |
400 |
Anfrage ungültig, etwa ein nicht unterstützter Parameter | Meldung lesen; Wiederholen hilft nicht |
429 |
Kontingent pro Minute erschöpft | warten, dann wiederholen |
500, 529 |
Fehler oder Überlastung beim Anbieter | mit wachsender Wartezeit wiederholen |
stop_reason: max_tokens |
Antwort abgeschnitten | max_tokens erhöhen |
Das Python-SDK wiederholt 429 und Serverfehler automatisch, standardmässig zweimal. In R übernimmt req_retry() diese Aufgabe.
Reproduzierbarkeit
Eine Modellantwort ist kein Rechenergebnis. Dieselbe Anfrage kann morgen anders beantwortet werden, und bei neueren Modellen lässt sich die Zufälligkeit nicht mehr über einen Parameter wie temperature abschalten; die Tiefe der Antwort wird stattdessen über effort gesteuert. Für eine nachvollziehbare Auswertung heisst das:
- Modellbezeichnung, Systemvorgabe und Anfrage mit dem Ergebnis speichern
- Ergebnisse als Datei ablegen und darauf weiterrechnen, nicht bei jedem Rendern neu abfragen
- Eine Stichprobe der Einordnungen von Hand prüfen und die Übereinstimmung berichten
- Zahlen, die das Modell nennt, im Code nachrechnen, siehe Prompts für die Datenarbeit
Typische Aufgaben
Schlüssel prüfen, ohne ihn anzuzeigen
nzchar(Sys.getenv("ANTHROPIC_API_KEY"))import os
bool(os.environ.get("ANTHROPIC_API_KEY"))Token vor dem Abschicken zählen
client.messages.count_tokens(model="claude-opus-5", messages=[...]).input_tokensAbgeschnittene Antworten finden
if antwort.stop_reason == "max_tokens":
print("Antwort unvollständig")Ergebnisse zwischenspeichern, damit ein Neustart nicht alles neu abfragt
import json, pathlib
ziel = pathlib.Path("cache") / f"{zeilen_id}.json"
if not ziel.exists():
ziel.write_text(json.dumps(ergebnis, ensure_ascii=False), encoding="utf-8")Verständnisfragen
Ein Skript fragt für jede von 3000 Zeilen das Modell und schreibt die Antwort in eine neue Spalte. Nach 1800 Zeilen bricht es mit 429 ab. Was hätte den Verlust verhindert?
max_tokens erhöhen
Warum sollte die Einordnung von Freitexten als strukturierte Ausgabe statt als Freitext angefordert werden?
Eine Auswertung ruft beim Rendern eines Quarto-Berichts jedes Mal das Modell auf. Was ist das Problem?