Structured Generation zwingt Language Models, nur gültige Outputs in bestimmten Formaten zu generieren (JSON, SQL, Regex, etc.).
Problem ohne Struktur
Input: "Extrahiere Name und Alter als JSON"
Output ohne Struktur:
{
"Name": "Alice",
invalid syntax: }{
age: 25 # <- Falsch! Sollte "age" sein
}
Mit Structured Generation:
{
"name": "Alice",
"age": 25
} # <- Garantiert gültig!
JSON Mode
OpenAI und andere Provider bieten natives JSON Mode.
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4",
messages=[{
"role": "user",
"content": "Extrahiere Person Daten als JSON mit Feldern: name, age, city"
}],
response_format={"type": "json_object"} # <- JSON Mode!
)
print(response.choices[0].message.content)
# Garantiert valides JSON
Wie es funktioniert (intern):
- Model generiert Token
- Nach jedem Token: Prüfe ob noch zu gültigem JSON führen kann
- Wenn nicht: Block diese Token, erzwinge andere
Outlines Library
Open-Source Structured Generation.
JSON Schema Enforcement
from outlines import models, generate
from pydantic import BaseModel
class Person(BaseModel):
name: str
age: int
city: str
# Modell laden
model = models.transformers("meta-llama/Llama-2-7b")
# Generator mit Schema
generator = generate.json(model, Person)
# Generierung erzwingt Schema
result = generator("Extrahiere: Alice, 25, Berlin")
# Garantiert Pydantic-valid!
print(result)
# {"name": "Alice", "age": 25, "city": "Berlin"}
Regex Constraints
from outlines import models, generate
# Email Format
EMAIL_REGEX = r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"
model = models.transformers("mistral-7b")
generator = generate.regex(model, EMAIL_REGEX)
result = generator("Gib mir eine Email Adresse")
# Garantiert matches Email Pattern!
Grammar-Based (EBNF)
Grammar Definition (EBNF):
number = ("0" | "1" | "2" | ... | "9")+
float = number "." number
operator = "+" | "-" | "*" | "/"
expression = float operator float
from outlines import models, generate
grammar = r"""
number = ("0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9")+
float = number "." number
operator = "+" | "-" | "*" | "/"
expression = float operator float
"""
model = models.transformers("llama-2-7b")
generator = generate.ebnf(model, grammar)
result = generator("Berechne 3.14 + 2.86")
# Garantiert: [float] [operator] [float]
Guidance Library
Probabilistic Constraints (nicht hard stops, weiche Constraints).
import guidance
guidance.llm = guidance.llms.OpenAI("gpt-3.5-turbo")
program = guidance("""
Extrahiere Person Daten:
Name: {{name}}
Age: {{age}}
City: {{city}}
""")
result = program(
name=guidance.gen(max_tokens=20),
age=guidance.gen(regex=r"\d{1,3}"), # 1-3 Digits
city=guidance.gen(max_tokens=15)
)
print(result)
Instructor Library
Pydantic Integration für strukturierte OpenAI Calls.
import instructor
from openai import OpenAI
from pydantic import BaseModel
# Patch OpenAI Client
client = instructor.from_openai(OpenAI())
class PersonData(BaseModel):
name: str
age: int
city: str
email: str
# Structured Call
response = client.chat.completions.create(
model="gpt-4",
response_model=PersonData,
messages=[{
"role": "user",
"content": "Extrahiere: Alice, 25, Berlin, [email protected]"
}]
)
print(response)
# <- Garantiert PersonData Instanz!
# Type hints!
LMQL (Language Model Query Language)
Domain-Specific Language für Structured Generation.
argmax
"Q: Welches ist die Hauptstadt von Deutschland?"
"A: [ANSWER]"
from
"openai/gpt-3.5-turbo"
where
len(ANSWER) < 50 and
"Berlin" in ANSWER
from lmql.language import lmql_engine
# Query definieren
query = lmql_engine.parse_lmql("""
argmax
"Person: {{name}}"
"Age: {{age}}"
"City: {{city}}"
from
"local/mistral-7b"
where
len(name) < 20 and
int(age) > 0 and
int(age) < 150 and
len(city) < 20
""")
result = query()
Implementierung: Custom Constrained Decoder
import torch
import torch.nn.functional as F
from typing import List
class ConstrainedDecoder:
def __init__(self, model, tokenizer):
self.model = model
self.tokenizer = tokenizer
def decode_with_regex(self, prompt: str, regex_pattern: str, max_tokens=100):
"""
Generiere Text das Regex Pattern matched
"""
import re
input_ids = self.tokenizer.encode(prompt, return_tensors="pt")
token_seq = []
for _ in range(max_tokens):
# Model Vorhersage
outputs = self.model(input_ids)
logits = outputs.logits[0, -1, :]
# Alle Tokens nach Wahrscheinlichkeit
probs = F.softmax(logits, dim=-1)
# Filter: Nur Tokens die regex pattern erlauben
valid_tokens = []
for token_id in range(len(probs)):
token_text = self.tokenizer.decode(token_id)
# Test ob diesen Token hinzufügen regex match ermöglicht
test_seq = "".join([
self.tokenizer.decode(t) for t in token_seq
]) + token_text
if re.match(regex_pattern, test_seq):
valid_tokens.append(token_id)
if not valid_tokens:
# Fallback: Beste Token die nicht regex matchen
valid_tokens = [probs.argmax().item()]
# Sample aus validen Tokens
valid_probs = torch.zeros_like(probs)
valid_probs[valid_tokens] = probs[valid_tokens]
valid_probs = valid_probs / valid_probs.sum()
next_token = torch.multinomial(valid_probs, 1).item()
token_seq.append(next_token)
# Update input
input_ids = torch.cat([
input_ids,
torch.tensor([[next_token]])
], dim=-1)
# Stop condition
if self.tokenizer.decode(next_token) == "<|endoftext|>":
break
return self.tokenizer.decode(token_seq)
# Usage
decoder = ConstrainedDecoder(model, tokenizer)
result = decoder.decode_with_regex(
"Email: ",
r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"
)
JSON Schema Validation
OpenAI / Claude API Integration mit JSON Schema.
import json
from openai import OpenAI
client = OpenAI()
# Schema Definition
json_schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer", "minimum": 0, "maximum": 150},
"email": {"type": "string", "format": "email"},
"hobbies": {
"type": "array",
"items": {"type": "string"}
}
},
"required": ["name", "age", "email"]
}
response = client.chat.completions.create(
model="gpt-4",
messages=[{
"role": "user",
"content": "Extrahiere Person Daten: Alice, 25, [email protected], liest und programmiert"
}],
response_format={
"type": "json_schema",
"json_schema": {
"name": "PersonData",
"schema": json_schema
}
}
)
result = json.loads(response.choices[0].message.content)
# Garantiert Validation gegen Schema!
Performance Tipps
1. Early Exit für Validierung
# ❌ Langsam: Generiere alles, validiere dann
output = model.generate(prompt)
if not is_valid_json(output):
try_again() # Oft nötig
# ✅ Schnell: Validiere während Generierung
output = constrained_generate(prompt, json_schema)
# Garantiert gültig nach erstem Versuch!
2. Schema Simplification
# ❌ Komplexes Schema mit vielen Constraints
# → Langsamere Generierung
# ✅ Einfaches Schema
# → Schnellere Generierung
3. Fallback Strategien
def safe_structured_generate(prompt, schema):
try:
# Versuche strukturierte Generierung
return constrained_generate(prompt, schema)
except ConstraintException:
# Fallback: Normale Generierung + Post-Processing
result = model.generate(prompt)
return fix_json(result) # Best-effort Fix
Praktische Use Cases
1. Data Extraction
# Extraktion mit genauerem Schema
schema = {
"type": "object",
"properties": {
"persons": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"role": {"type": "string"},
"salary": {"type": "number"}
}
}
}
}
}
result = structured_generate(
"Extract from resume: ...",
schema
)
2. Function Calling
# Erzwinge Function Call Format
function_schema = {
"name": "calculate",
"parameters": {
"type": "object",
"properties": {
"operation": {"enum": ["add", "subtract", "multiply"]},
"a": {"type": "number"},
"b": {"type": "number"}
}
}
}
3. SQL Generation
sql_grammar = """
select_stmt = "SELECT" columns "FROM" table ["WHERE" condition]
columns = "*" | column ("," column)*
column = identifier
table = identifier
condition = identifier operator value
operator = "=" | ">" | "<"
value = string | number
"""
result = generate_with_grammar(
"Get alle Users über 18:",
sql_grammar
)
# SELECT * FROM users WHERE age > 18
