10 KiB
| translation | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
Strukturierte Ausgabe
Ein Tool, das einen einfachen str zurückgibt, liefert das Ergebnis doppelt: als Text in content und als {"result": "..."} in structured_content.
Auf dieser Seite geht es um diesen zweiten Kanal: woher er kommt, welche Formen er annehmen kann und wie das SDK dafür sorgt, dass er hält, was er verspricht.
Die Kurzfassung: Die Annotation des Rückgabetyps ist das Ausgabeschema. Du hast sie schon geschrieben.
Das Ausgabeschema
--8<-- "docs_src/structured_output/tutorial001.py"
Die entscheidende Zeile ist die Signatur: -> int.
Ihretwegen trägt das Tool, das das SDK bei tools/list sendet, ein output_schema neben dem Eingabeschema, das es aus deinen Parametern baut (darum kümmert sich Tools):
{
"properties": {
"result": {"title": "Result", "type": "integer"}
},
"required": ["result"],
"title": "get_temperatureOutput",
"type": "object"
}
Ein nackter int ist kein JSON-Objekt, also verpackt das SDK ihn in {"result": ...}. Ruf das Tool auf, und beide Kanäle sind gefüllt:
result.content # [TextContent(text="17")]
result.structured_content # {"result": 17}
Jeder skalare Wert bekommt dieselbe Hülle: str, int, float, bool, bytes, None.
Zwei Kanäle
Warum denselben Wert zweimal senden?
contentist für das Modell. Ein Sprachmodell liest Text; das ist der einzige Teil des Ergebnisses, den es sieht.structured_contentist für die Anwendung, in der das Modell läuft: Code, der17will und keinen Satz, in dem „17“ vorkommt.output_schemaist der Vertrag zwischen beiden, veröffentlicht, bevor das Tool überhaupt aufgerufen wird.
Du gibst einen einzigen Python-Wert zurück. Das SDK füllt alle drei.
Ein Modell zurückgeben
Deklariere die Form als Pydantic-BaseModel und gib eine Instanz zurück:
--8<-- "docs_src/structured_output/tutorial002.py"
WeatherData ist jetzt das Schema. Keine Hülle, kein result-Schlüssel:
{
"properties": {
"temperature": {"description": "Degrees Celsius.", "title": "Temperature", "type": "number"},
"humidity": {"description": "Relative humidity, 0 to 1.", "title": "Humidity", "type": "number"},
"conditions": {"title": "Conditions", "type": "string"}
},
"required": ["temperature", "humidity", "conditions"],
"title": "WeatherData",
"type": "object"
}
structured_content ist das Objekt, Feld für Feld:
result.structured_content # {"temperature": 16.2, "humidity": 0.83, "conditions": "Overcast"}
Und das Modell geht nicht leer aus. Das SDK serialisiert dasselbe Objekt für content zu JSON-Text:
{
"temperature": 16.2,
"humidity": 0.83,
"conditions": "Overcast"
}
Beachte, dass das Field(description=...) an temperature und humidity im Schema gelandet ist. Dasselbe Field, das deine Eingaben beschrieben hat, beschreibt auch deine Ausgaben.
!!! info
Wenn du FastAPIs response_model kennst, kennst du das hier schon: ein Pydantic-Modell als deklarierte
Response, für dich serialisiert und dokumentiert. Der einzige Unterschied: Hier ist die Annotation des
Rückgabetyps die ganze Deklaration.
Ein TypedDict
Nicht jede Form verdient eine Klasse. Ein TypedDict erzeugt dasselbe Schema:
--8<-- "docs_src/structured_output/tutorial003.py"
Ein TypedDict ist zur Laufzeit ein einfaches dict, also baust du genau das und gibst es zurück. Das Schema, die Validierung und structured_content sind identisch mit der BaseModel-Variante (abgesehen von den Beschreibungen, für die ein TypedDict keinen Platz hat).
Eine Dataclass
Dataclasses funktionieren auch, genauso wie jede gewöhnliche Klasse, deren Attribute Type Hints tragen. Das SDK baut unter der Haube aus den Annotationen ein Pydantic-Modell.
--8<-- "docs_src/structured_output/tutorial004.py"
Drei Schreibweisen, ein Schema. Nimm die, die deine Codebasis ohnehin schon verwendet.
Listen
Eine list[...] ist ebenfalls kein JSON-Objekt, also bekommt sie die {"result": ...}-Hülle, mit deinem Elementtyp als $defs-Referenz darin:
--8<-- "docs_src/structured_output/tutorial005.py"
{
"$defs": {
"WeatherData": {
"properties": {
"temperature": {"title": "Temperature", "type": "number"},
"humidity": {"title": "Humidity", "type": "number"},
"conditions": {"title": "Conditions", "type": "string"}
},
"required": ["temperature", "humidity", "conditions"],
"title": "WeatherData",
"type": "object"
}
},
"properties": {
"result": {"items": {"$ref": "#/$defs/WeatherData"}, "title": "Result", "type": "array"}
},
"required": ["result"],
"title": "get_forecastOutput",
"type": "object"
}
Fordere eine Zwei-Tage-Vorhersage an, und structured_content ist {"result": [{...}, {...}]}. content wird zu zwei TextContent-Blöcken, einer pro Element: Eine Liste wird für das Modell aufgefächert, statt als ein einziger String ausgegeben zu werden.
tuple[...], Unions und Optional[...] werden genauso verpackt.
Dictionaries
dict[str, ...] ist der eine generische Typ, der bereits ein JSON-Objekt ist, und wird deshalb nicht verpackt:
--8<-- "docs_src/structured_output/tutorial006.py"
{
"additionalProperties": {"type": "number"},
"title": "get_temperaturesDictOutput",
"type": "object"
}
result.structured_content # {"London": 16.2, "Reykjavik": 4.4}
Die Schlüssel müssen str sein. Ein dict[int, float] kann kein JSON-Objekt sein und fällt deshalb auf die {"result": ...}-Hülle zurück.
Validierung
output_schema ist keine Dokumentation. Was auch immer deine Funktion zurückgibt, wird dagegen validiert, bevor es den Server verlässt.
Solange du den Wert von Hand baust, merkst du davon nichts: Pydantic hat schon sichergestellt, dass dein WeatherData ein WeatherData ist. Du merkst es an dem Tag, an dem die Daten von irgendwo kommen, das du nicht kontrollierst:
--8<-- "docs_src/structured_output/tutorial007.py"
Die Annotation verspricht WeatherData. Die Upstream-Response liefert humidity nicht mehr mit.
!!! check
Ruf get_weather auf, und es reicht dem Client nicht stillschweigend ein halb leeres Objekt weiter. Der Aufruf schlägt fehl,
und die ersten Zeilen des Fehlers nennen das Feld:
```text
Error executing tool get_weather: 1 validation error for WeatherData
humidity
Field required [type=missing, input_value={'temperature': 16.2, 'conditions': 'Overcast'}, input_type=dict]
```
Dieser Text kommt als Tool-Ergebnis mit `is_error=True` zurück. So weiß das Modell, dass der Aufruf fehlgeschlagen ist,
statt selbstbewusst Wetterdaten abzulesen, die gar nicht da sind.
Ein einfaches dict aus einem -> WeatherData-Tool zurückzugeben ist übrigens in Ordnung. Genau das hat json.loads erzeugt. Validiert wird der Wert, nicht der Python-Typ.
Abschalten
Manchmal ist die Annotation des Rückgabetyps für den Type Checker da, nicht für das Protokoll. Übergib structured_output=False, und das Tool liefert nur Text:
--8<-- "docs_src/structured_output/tutorial008.py"
Kein output_schema, keine Hülle, keine Validierung. structured_content ist None, und content ist der String, den du zurückgegeben hast.
Das Gegenteil, structured_output=True, macht aus der automatischen Erkennung eine Anforderung: Ein Tool, dessen Rückgabetyp kein Schema erzeugen kann, löst beim Import eine Exception aus, statt auf Text zurückzufallen.
Eine Klasse ohne Type Hints
Es gibt einen Weg, unstrukturiert zu enden, ohne es gewollt zu haben: eine Klasse zurückzugeben, die keine Annotationen im Klassenrumpf hat.
--8<-- "docs_src/structured_output/tutorial009.py"
Station setzt name und online in __init__, aber die Klasse deklariert nichts. Das SDK liest die Klassenannotationen, findet keine und gibt auf.
!!! warning
Es gibt stillschweigend auf. output_schema ist None, structured_content ist None, und der Text,
den das Modell liest, ist das repr des Objekts:
```text
"<server.Station object at 0x7f539d75b230>"
```
Kein Fehler, keine Warnung, ein nutzloses Tool. Verschiebe die Annotationen in den Klassenrumpf oder übergib
`structured_output=True`. Das macht daraus einen harten Fehler, sobald das Modul importiert wird:
`Function get_station: return type <class 'server.Station'> is not serializable for structured output`.
!!! tip
Brauchst du die volle Kontrolle (das CallToolResult selbst bauen oder _meta anhängen, das die
Anwendung sieht, das Modell aber nicht)? Das ist Der Low-Level-Server.
Zusammenfassung
- Die Annotation des Rückgabetyps ist das Ausgabeschema. Sie wird in
tools/listalsoutput_schemaveröffentlicht. - Skalare, Listen, Tupel und Unions werden in
{"result": ...}verpackt. Modelle,TypedDicts, Dataclasses, annotierte Klassen unddict[str, ...]sind schon Objekte und bleiben, wie sie sind. - Jedes Ergebnis trägt
content(Text, für das Modell) undstructured_content(Daten, für die Anwendung). - Was du zurückgibst, wird gegen das Schema validiert. Eine Abweichung ist ein Tool-Fehler, kein kaputtes Ergebnis.
structured_output=Falsenimmt ein Tool davon aus. Eine Klasse ohne Type Hints nimmt sich stillschweigend aus; achte darauf.
Damit hast du alles in der Hand, was ein Tool zurückmelden kann. Als Nächstes das zweite Primitiv: Ressourcen.