Wie du mit TypeSafe baust
Entwirf KI-gestützte Software, indem du die Kontrolle im Code behältst und System One eng umrissene, strukturierte Entscheidungen gibst.
System One ist TypeSafes Modell für den Bau KI-gestützter Software, nicht von Agenten. Es generiert keinen Code und wählt nicht seine eigene nächste Aktion. Es stellt KI-Primitive bereit, die sich in Software einbetten, sodass der Code die Kontrolle behält, während das Modell Alltagsurteile über unstrukturierte Daten fällt.
Drei Software-Architekturen
TypeSafe ist darauf ausgelegt, KI-gestützte Software zu bauen, in der der Code den Workflow besitzt und die KI eng umrissene, strukturierte Entscheidungen übernimmt.
Traditioneller Code ist ein komplexer Entscheidungsbaum aus einfachen Software-Primitiven. Weil jedes Primitiv zuverlässig ist, können Entwickler sie zu Abstraktionen höherer Ebene zusammensetzen.
Ein Agent verarbeitet Anweisungen und wählt seinen nächsten Schritt. Das funktioniert gut, wenn eine Person den Prozess überwacht, aber jede Schleife bringt eine weitere Gelegenheit, aus der Bahn zu geraten.
Der Code übernimmt deterministische Arbeit und besitzt den Kontrollfluss. Das Modell erscheint nur dort, wo das System programmierbaren Alltagsverstand braucht oder unstrukturierte Daten interpretieren muss. Jede KI-Aufgabe bleibt atomar und beschränkt.


Was System One kombinierbar macht
Strukturiert
System One ist per Konstruktion typsicher. Entscheidungen und Wahrscheinlichkeiten entsprechen den strukturierten Softwaretypen und dem JSON-Schema, die dein Code erwartet, sodass er nie einen Wert aus generierter Prosa zurückgewinnen muss.
Parallel
Fragen werden unabhängig und parallel ausgewertet. Das Ergebnis eines Primitivs wird kein versteckter Kontext, der das Ergebnis eines anderen Primitivs verändert.
Vergleichbar
Ausgaben sind sortierbar und können kluge if-Anweisungen, Schwellenwerte und Vergleiche antreiben.
Schnell
Die meisten Anfragen sind in etwa 100 ms abgeschlossen. System One ist schnell genug für Echtzeit-Anfragepfade und Benutzeroberflächen.
Kalibrierte Konfidenz
RLCD kommuniziert Unsicherheit durch kalibrierte Wahrscheinlichkeiten, statt zu Selbstüberschätzung zu neigen.
Selbstkonsistent
System One ist darauf ausgelegt, bei wiederholten Auswertungen stabile Antworten zurückzugeben. Siehe das Selbstkonsistenz-Cookbook.
Weil jede Ausgabe auf die angegebenen Optionen beschränkt ist, gibt das Modell eine vollständige Wahrscheinlichkeitsverteilung über diese Optionen zurück, statt einen Wert außerhalb des Schemas zu erfinden. TypeSafes Ziel ist ein Verhältnis von Intelligenz zu Geschwindigkeit und Kosten von mehr als 100×; die zugrunde liegende Wette ist, dass günstigere Intelligenz viel mehr Nachfrage erzeugen wird.
Einen System One-Workflow entwerfen
Nutze Code, wenn du kannst
Behalte deterministische Arbeit im Code. Sie ist zuverlässig und günstig. Vermeide
while-Schleifen von Agenten, wenn ein Software-Workflow dasselbe Verhalten ausdrücken kann.Beispiel: deterministische Regeln im Code behalten
days_overdue = (today - invoice.due_date).days if days_overdue > 30: route_to_collections(invoice)Durchsuche die System One-Muster nach begrenzten Wegen, Modellentscheidungen mit Code zu kombinieren.
Zerlege den Eingabezustand
Nimm nur den Kontext auf, der für die aktuellen Fragen relevant ist. Das hilft dem Modell, Ablenkungen und Kontextverfall zu vermeiden. Verlass dich nicht auf Wissen, das in Modellgewichten gespeichert ist, wenn aktuelle Informationen aus deiner eigenen Wissensbasis kommen können.
Beispiel: nur relevanten Kontext senden
request { "state": { "ticket_message": "My flight was cancelled. Can I get a refund?", "refund_policy": "Cancelled flights are eligible for a full refund." }, "questions": { "policy_supports_refund": { "type": "noul", "instructions": "Does the refund policy support the refund requested in the ticket?" } } }Nutze Struktur im Eingabezustand
Verwende verschachteltes JSON für die Felder
stateundquestions. Richte Fragen auf konkrete Werte, wenn das Mehrdeutigkeit beseitigt, und setze die Backtick-Zeichen um jeden Pfad innerhalb der Frage.Beispiel: einen verschachtelten Wert referenzieren
Verwende einen Pfad mit Punkten und Indizes in Backticks, um eine Frage auf einen konkreten verschachtelten Wert zu richten, etwa
support.tickets[0].message.request { "state": { "support": { "tickets": [ { "message": "I was charged twice for order A-104." }, { "message": "How do I reset my password?" } ] }, "commerce": { "orders": [ { "id": "A-104", "charges": [ { "amount_usd": 49, "status": "captured" }, { "amount_usd": 49, "status": "captured" } ] } ] }, "account": { "security": { "password_reset": "Email a reset link to the address on file." } } }, "questions": { "duplicate_charge": { "type": "noul", "instructions": "Do `support.tickets[0].message` and `commerce.orders[0].charges` indicate a duplicate charge?" }, "password_reset_supported": { "type": "noul", "instructions": "Can `account.security.password_reset` resolve the request in `support.tickets[1].message`?" } } }Zerlege die Fragen
Stelle die explizitesten, eng umrissensten, konkretesten und atomarsten Fragen, die du kannst. Zerlege komplexe oder schlecht definierte Fragen in separate Fragen, die jeweils eine Eigenschaft auswerten.
Beispiel: Spam-Erkennung zerlegen
One broad question (bad) { "is_spam": { "type": "noul", "instructions": "Is `message` spam?" } }Decomposed questions (good) { "requests_credentials": { "type": "noul", "instructions": "Does `message.body` ask the recipient to provide a password or other login credential?" }, "offers_unexpected_reward": { "type": "noul", "instructions": "Does `message.body` claim the recipient received an unexpected prize, payment, or reward?" }, "creates_time_pressure": { "type": "noul", "instructions": "Does `message.subject` or `message.body` pressure the recipient to act quickly?" }, "sender_identity_mismatch": { "type": "noul", "instructions": "Does the organization named in `message.sender.display_name` conflict with the domain in `message.sender.email`?" }, "link_domain_mismatch": { "type": "noul", "instructions": "Does the domain in `message.links[0].url` conflict with the organization named in `message.sender.display_name`?" }, "disguises_link_destination": { "type": "noul", "instructions": "Does `message.links[0].text` conceal or misrepresent the destination in `message.links[0].url`?" } }Beispiel: einen Tool-Aufruf-Trace verifizieren
One broad question (bad) { "tool_calls_are_correct": { "type": "noul", "instructions": "Is `trace.tool_calls` correct for `request` and `available_tools`?" } }Decomposed questions (good) { "geocode_tool_is_relevant": { "type": "noul", "instructions": "Is `trace.tool_calls[0].name` an appropriate tool for resolving `request.location`?" }, "geocode_location_matches": { "type": "noul", "instructions": "Does `trace.tool_calls[0].arguments.city` match `request.location`?" }, "geocode_arguments_match_schema": { "type": "noul", "instructions": "Does `trace.tool_calls[0].arguments` conform to `available_tools.geocode_city.parameters`?" }, "geocode_result_matches_call": { "type": "noul", "instructions": "Does `trace.tool_results[0].tool_call_id` match `trace.tool_calls[0].id`?" }, "weather_tool_is_relevant": { "type": "noul", "instructions": "Is `trace.tool_calls[1].name` an appropriate tool for answering `request.text`?" }, "weather_arguments_match_schema": { "type": "noul", "instructions": "Does `trace.tool_calls[1].arguments` conform to `available_tools.get_weather.parameters`?" }, "weather_uses_geocoded_coordinates": { "type": "noul", "instructions": "Do the coordinates in `trace.tool_calls[1].arguments` match those in `trace.tool_results[0].output`?" }, "weather_date_matches": { "type": "noul", "instructions": "Does `trace.tool_calls[1].arguments.date` match `request.date`?" }, "weather_unit_matches": { "type": "noul", "instructions": "Does `trace.tool_calls[1].arguments.unit` match `request.unit`?" } }Nutze Struktur in den Fragen
Halte Fragen kurz.
instructionsundcriteriasind meist Zeichenketten, und für eine kurze, eindeutige Frage reicht eine Zeichenkette. Sie können auch Objekte oder Arrays sein. Lege die Frage in ein Feld und die Daten, die die Frage leiten, in die anderen.Struktur hilft in diesen Situationen:
- Die Frage braucht Kontext oder Beispiele. Ein langer Satz Hintergrundinformation oder eine Liste von Beispieleingaben gehört in benannte Felder neben der Frage, wo dein Code sie ergänzen oder austauschen kann, ohne die Frage neu zu schreiben.
- Ein Teil der Frage kommt aus deinem Code. Wenn ein Wert aus einer Datenbank kommt, lege ihn in ein eigenes Feld, statt ihn in eine Zeichenkettenvorlage einzusetzen.
- Mehrere Fragen haben ähnliche Anweisungen. Eine Anfrage nimmt einen Zustand und kann mehrere Fragen enthalten. Zusätzliche Daten können helfen, die Fragen voneinander abzugrenzen.
Beispiel: einen Datensatz aus deinem Code referenzieren
Dieser Noul vergleicht einen Lebenslauf im Zustand mit einem Datensatz aus einer Kandidatendatenbank. Der Datensatz geht so, wie er ist, in
potential_duplicate, und die Frage verweist über den Namen darauf.questions { "same_as_record_18": { "type": "noul", "instructions": { "potential_duplicate": { "name": "John Smith", "location": "Oakland, California", "last_employer": "Google" }, "question": "Is the resume for the same person as `potential_duplicate`?" } } }Die Daten aus dem Code unter “potential_duplicate” können sich mit der Zeit ändern. Die “question” verweist über Backticks darauf.
Die Beschreibungen innerhalb von
criteriakönnen ebenfalls Objekte sein. Bei einer Choice kann die Beschreibung jeder Option ein Objekt sein, das sagt, was die Option abdeckt, was zu einer anderen Option gehört und ein paar Beispiele. Verwende dieselben Feldnamen über die Optionen hinweg, damit das Modell sie direkt vergleichen kann.Beispiel: kontrastive Choice-criteria definieren
questions { "card_help_topic": { "type": "choice", "instructions": { "question": "Which disposable virtual card topic is the user asking about?", "focus": "Classify the information the user wants." }, "criteria": { "get_disposable_virtual_card": { "what": "Purpose, eligibility, or setup", "not_for": "Quantity, transaction, or merchant restrictions", "examples": [ "How can I get a disposable virtual card?", "What are disposable cards for?" ] }, "disposable_card_limits": { "what": "Quantity, transaction, or merchant restrictions", "not_for": "Purpose, eligibility, or setup", "examples": [ "How many disposable cards can I make per day?", "Where can I use a disposable card?" ] } } } }Die Seite jedes Fragetyps enthält ein ausgearbeitetes Beispiel:
- Noul vergleicht einen Lebenslauf mit mehreren Kandidatendatensätzen, eine Frage pro Datensatz, wobei die Fragen im Code gebaut werden.
- Choice beschreibt zwei leicht verwechselbare Optionen mit dem, was jede abdeckt, wofür sie nicht gedacht ist und Beispielen.
- Score gibt jeder Stufe eine Beschreibung und Beispielsituationen.
Das Cookbook zur Kaskade der strukturierten Datenextraktion zeigt den Fall mit gemeinsamer Formulierung, bei dem dieselbe Batterie von Fragen zu jedem Feld eines extrahierten Datensatzes gestellt wird.
Eine kurze, eindeutige Frage oder ein Kriterium kann eine Zeichenkette bleiben. Füge Struktur hinzu, wenn sie eine Anleitung trennt, die sonst ineinander verschwimmen würde. Die vollständige Menge der Stellen, an denen Struktur akzeptiert wird, findest du unter Fortgeschritten: Struktur.
Stelle viele Fragen
Stelle viele eng umrissene, unabhängige Fragen zum selben Zustand in einer Anfrage. So maximierst du Wirksamkeit und Intelligenz pro Dollar mit der API: Fragen laufen parallel, und der Code kann ihre Signale kombinieren, ohne serielle Modell-Roundtrips hinzuzufügen.
Siehe das Muster Spekulativer Fan-Out und das Cookbook zu parallelen Fragen.
Kombiniere Frageausgaben im Code (oder speise sie in ein klassisches ML-Modell ein)
Kombiniere unabhängige Antworten mit deterministischen Regeln oder gewichteten Summen. Für eine gelernte Komposition verwende die Wahrscheinlichkeiten als Merkmale in einem nachgelagerten klassischen Machine-Learning-Modell.
Beispiel: Signale mit einem gewichteten Score kombinieren
answers = response.answers # Combine independent signals into one application-specific score. quality = ( 0.4 * answers["answers_request"].noul + 0.4 * answers["citations_are_supported"].noul + 0.2 * (1 - answers["contradicts_context"].noul) )Zusammengesetzte Bewertung (Composite Scoring) zeigt, wie du einzelne Urteile bewahrst, während du sie kombinierst. Wenn du keine Labels für ein nachgelagertes Modell hast, verwende ein Ensemble teurer Reasoning-Modelle, um sie zu erzeugen; das AutoResearch-Cookbook zeigt, wie du ein klassisches Modell auf System One-Ausgaben trainierst.
Route anhand der Unsicherheit
Lass den Code für sichere und unsichere Antworten unterschiedliche Aktionen ausführen. Eskaliere unsichere Fälle an eine Person oder ein teureres Reasoning-Modell. Teste Schwellenwerte, indem du Konfidenz gegen Genauigkeit auf deinen Daten aufträgst.
Beispiel: nach Konfidenz routen
answer = response.answers["card_help_topic"] if answer.confidence < 0.8: route_to_human_review(ticket) else: route_to_handler(answer.choice, ticket)Siehe Konfidenz und Konfidenz-gesteuertes Routing für die Wahl von Schwellenwerten und ihre Abstimmung auf das Risiko jeder Aktion.
Alles zusammenfügen
Dieser Support-Ticket-Workflow behält deterministische Arbeit im Code, sendet nur relevanten strukturierten Kontext, wertet viele atomare Fragen in einer Anfrage aus und kombiniert die Antworten mit expliziten Konfidenz-Gates.
triage_ticket.py
from typesafe_sdk import Choice, Noul, NoulCriteria, Score, TypeSafeClient
def triage_ticket(ticket, customer):
# Handle deterministic states without calling a model.
if ticket["status"] == "closed":
return "no_action"
open_orders = [
order for order in customer["orders"] if order["status"] != "delivered"
]
# Include only the structured context needed by the questions below.
state = {
"ticket": {
"message": ticket["message"],
"sender": ticket["sender"],
"links": ticket["links"],
},
"customer": {
"plan": customer["plan"],
"open_orders": open_orders,
},
"policy": {
"sensitive_credentials": ["password", "security code", "API key"],
},
}
# Ask structured, atomic questions together so they run in parallel.
questions = {
"topic": Choice(
instructions={
"question": "Which team should handle `ticket.message`?",
"focus": "Classify the customer's primary request.",
},
criteria={
"billing": {
"what": "Charges, invoices, refunds, or subscriptions",
"not_for": "Order tracking or account access",
"examples": ["I was charged twice", "Where is my refund?"],
},
"orders": {
"what": "Order status, delivery, cancellation, or returns",
"not_for": "Charges or account access",
"examples": ["Where is my order?", "Cancel my shipment"],
},
"account": {
"what": "Login, profile, permissions, or security",
"not_for": "Charges or order tracking",
"examples": ["Reset my password", "I cannot sign in"],
},
},
),
"requests_credentials": Noul(
instructions={
"question": "Does the message request a sensitive credential?",
"compare": [
"`ticket.message`",
"`policy.sensitive_credentials`",
],
"focus": "Look for a request to disclose the credential itself.",
},
criteria=NoulCriteria(
true={
"what": "Asks the recipient to disclose a listed credential",
"examples": [
"Reply with your password",
"Send us your API key",
],
},
false={
"what": "Does not ask the recipient to disclose a credential",
"not_for": "A legitimate instruction to reset a credential",
"examples": ["Use this link to reset your password"],
},
),
),
"sender_identity_mismatch": Noul(
instructions={
"question": "Does the claimed sender identity conflict with its domain?",
"compare": [
"`ticket.sender.display_name`",
"`ticket.sender.email`",
],
"focus": "Compare the named organization with the email domain.",
},
criteria=NoulCriteria(
true={
"what": "Claims an organization unrelated to the email domain",
"examples": ["Acme Payroll sent from claim-bonus.example"],
},
false={
"what": "The identity and domain agree or make no conflicting claim",
"examples": ["Acme Payroll sent from acme.example"],
},
),
),
"unexpected_reward": Noul(
instructions={
"question": "Does the message announce an unexpected reward?",
"inspect": "`ticket.message`",
"focus": "Look for an unsolicited prize, payment, or reward claim.",
},
criteria=NoulCriteria(
true={
"what": "Announces an unrequested prize, payment, or reward",
"examples": ["You were selected for a $1,000 bonus"],
},
false={
"what": "Contains no reward claim or discusses an expected payment",
"not_for": "A customer asking about a known refund or payroll deposit",
"examples": ["When will my approved refund arrive?"],
},
),
),
"refund_requested": Noul(
instructions={
"question": "Does the customer explicitly request a refund or credit?",
"inspect": "`ticket.message`",
"focus": "Require a requested remedy, not a billing complaint alone.",
},
criteria=NoulCriteria(
true={
"what": "Directly asks for money back or an account credit",
"examples": ["Please refund the duplicate charge"],
},
false={
"what": "Does not ask for a refund or credit",
"not_for": "A complaint or billing question without a requested remedy",
"examples": ["Why was I charged twice?"],
},
),
),
"mentions_open_order": Noul(
instructions={
"question": "Does the message refer to a supplied open order?",
"compare": [
"`ticket.message`",
"`customer.open_orders`",
],
"focus": "Match an order id or other identifying details.",
},
criteria=NoulCriteria(
true={
"what": "Refers to an open order by id or identifying details",
"examples": ["Where is order A-104?"],
},
false={
"what": "Does not identify any supplied open order",
"not_for": "A generic order question with no matching details",
"examples": ["How long does shipping usually take?"],
},
),
),
"frustration": Score(
instructions={
"question": "How frustrated does the customer appear?",
"inspect": "`ticket.message`",
"focus": "Judge expressed frustration, not issue severity.",
},
criteria=[
{
"what": "Calm and matter-of-fact",
"signals": ["Neutral wording", "No complaint about the experience"],
},
{
"what": "Frustrated but civil",
"signals": ["Expresses annoyance", "Remains constructive"],
},
{
"what": "Very angry or threatening to leave",
"signals": ["Hostile language", "Threatens cancellation or churn"],
},
],
),
}
with TypeSafeClient() as client:
response = client.system_one(
state=state,
questions=questions,
)
# Compose independent spam signals with weights controlled by code.
answers = response.answers
spam_risk = (
0.45 * answers["requests_credentials"].noul
+ 0.30 * answers["sender_identity_mismatch"].noul
+ 0.25 * answers["unexpected_reward"].noul
)
# Escalate uncertain judgments instead of guessing.
spam_is_uncertain = 0.4 < spam_risk < 0.6
if spam_is_uncertain or answers["topic"].confidence < 0.75:
return route_to_human_review(ticket)
if spam_risk >= 0.6:
return quarantine_as_spam(ticket)
# Let code decide which speculative answers matter on this path.
if answers["topic"].choice == "billing":
return route_to_billing(
ticket,
refund_requested=answers["refund_requested"].noul >= 0.7,
)
if answers["topic"].choice == "orders":
return route_to_orders(
ticket,
mentions_open_order=answers["mentions_open_order"].noul >= 0.7,
)
priority = (
"high"
if answers["frustration"].confidence >= 0.7
and answers["frustration"].score >= 1.5
else "normal"
)
return route_to_account_support(ticket, priority=priority)