Construire avec TypeSafe
Conçois des logiciels propulsés par l’IA en gardant le code aux commandes et en donnant à System One des décisions étroites et structurées.
System One est le modèle de TypeSafe pour construire des logiciels propulsés par l’IA, pas des agents. Il ne génère pas de code et ne choisit pas sa propre action suivante. Il fournit des primitives d’IA qui s’intègrent au logiciel, de sorte que le code reste aux commandes pendant que le modèle porte des jugements de bon sens sur des données non structurées.
Trois architectures logicielles
TypeSafe est conçu pour construire des logiciels propulsés par l’IA, où le code détient le flux de travail et où l’IA gère des décisions étroites et structurées.
Le code traditionnel est un arbre de décision complexe construit à partir de primitives logicielles simples. Comme chaque primitive est fiable, les développeurs peuvent les composer en abstractions de plus haut niveau.
Un agent traite des instructions et choisit son étape suivante. Cela fonctionne bien quand une personne surveille le processus, mais chaque boucle ouvre une nouvelle occasion de dérailler.
Le code gère le travail déterministe et détient le flux de contrôle. Le modèle n’apparaît que là où le système a besoin d’un bon sens programmable ou doit interpréter des données non structurées. Chaque tâche d’IA reste atomique et contrainte.


Ce qui rend System One composable
Structuré
System One est type-safe par construction. Les décisions et les probabilités se conforment aux types logiciels structurés et au schéma JSON attendus par ton code, qui n’a donc jamais à récupérer une valeur dans de la prose générée.
Parallèle
Les questions sont évaluées indépendamment et en parallèle. Le résultat d’une primitive ne devient pas un contexte caché qui modifierait le résultat d’une autre primitive.
Comparable
Les sorties sont triables et peuvent alimenter des if intelligents, des seuils et des comparaisons.
Rapide
La plupart des requêtes durent environ 100 ms. System One est assez rapide pour les chemins de requête temps réel et les interfaces utilisateur.
Confiance calibrée
RLCD communique l’incertitude via des probabilités calibrées au lieu de pencher vers l’excès de confiance.
Autocohérent
System One est conçu pour renvoyer des réponses stables au fil d’évaluations répétées. Vois le cookbook d’autocohérence.
Comme chaque sortie est contrainte aux options fournies, le modèle renvoie une distribution de probabilité complète sur ces options au lieu d’inventer une valeur hors du schéma. La cible de TypeSafe est un rapport intelligence/vitesse-et-coût supérieur à 100× ; le pari sous-jacent est qu’une intelligence moins chère créera beaucoup plus de demande.
Concevoir un flux de travail System One
Utilise du code quand tu peux
Garde le travail déterministe dans le code. C’est fiable et peu coûteux. Évite les boucles
whiled’agent quand un flux de travail logiciel peut exprimer le même comportement.Exemple : garder les règles déterministes dans le code
days_overdue = (today - invoice.due_date).days if days_overdue > 30: route_to_collections(invoice)Parcours les motifs System One pour des façons délimitées de composer des décisions de modèle avec du code.
Décompose l’état d’entrée
N’inclus que le contexte pertinent pour les questions en cours. Cela aide le modèle à éviter les distractions et la dégradation du contexte. Ne te fie pas aux connaissances stockées dans les poids du modèle quand l’information actuelle peut venir de ta propre base de connaissances.
Exemple : envoyer seulement le contexte pertinent
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?" } } }Utilise la structure dans l’état d’entrée
Utilise du JSON imbriqué pour les champs
stateetquestions. Fais pointer les questions vers des valeurs précises quand cela lève l’ambiguïté, et inclus les caractères de backtick autour de chaque chemin à l’intérieur de la question.Exemple : référencer une valeur imbriquée
Utilise un chemin point-et-index entre backticks pour faire pointer une question vers une valeur imbriquée précise, comme
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`?" } } }Décompose les questions
Pose les questions les plus explicites, étroites, précises et atomiques possible. Décompose les questions complexes ou mal définies en questions distinctes qui évaluent chacune une seule propriété.
Exemple : décomposer la détection de spam
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`?" } }Exemple : vérifier une trace d’appels d’outils
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`?" } }Utilise la structure dans les questions
Garde les questions courtes.
instructionsetcriteriasont généralement des chaînes, et pour une question courte et non ambiguë, une chaîne suffit. Ils peuvent aussi être des objets ou des tableaux. Mets la question dans un champ et les données qui la guident dans les autres.La structure aide dans ces situations :
- La question a besoin de contexte ou d’exemples. Une longue phrase de contexte ou une liste d’entrées d’exemple appartient à des champs nommés à côté de la question, où ton code peut les compléter ou les remplacer sans réécrire la question.
- Une partie de la question vient de ton code. Quand une valeur vient d’une base de données, mets-la dans son propre champ plutôt que de l’insérer dans un gabarit de chaîne.
- Plusieurs questions ont des instructions similaires. Une requête prend un seul état et peut inclure plusieurs questions. Ajouter des données complémentaires peut aider à distinguer les questions.
Exemple : référencer un enregistrement depuis ton code
Ce Noul compare un CV de l’état à un enregistrement d’une base de données de candidats. L’enregistrement est placé tel quel dans
potential_duplicate, et la question y fait référence par son nom.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`?" } } }Les données « potential_duplicate » issues du code peuvent changer au fil du temps. La « question » y fait référence à l’aide de backticks.
Les descriptions à l’intérieur de
criteriapeuvent aussi être des objets. Pour un Choice, la description de chaque option peut être un objet qui indique ce que l’option couvre, ce qui appartient à une autre option, et quelques exemples. Utilise les mêmes noms de champs d’une option à l’autre pour que le modèle puisse les comparer directement.Exemple : définir des criteria Choice contrastifs
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?" ] } } } }La page de chaque type de question contient un exemple détaillé :
- Noul compare un CV à plusieurs enregistrements de candidats, une question par enregistrement, avec les questions construites dans le code.
- Choice décrit deux options facilement confondues avec ce que chacune couvre, ce à quoi elle n’est pas destinée, et des exemples.
- Score donne à chaque niveau une description et des situations d’exemple.
Le cookbook de cascade d’extraction de données structurées montre le cas du libellé partagé : on pose la même batterie de questions sur chaque champ d’un enregistrement extrait.
Une question ou un critère court et non ambigu peut rester une chaîne. Ajoute de la structure quand elle sépare des consignes qui, sinon, se confondraient. Pour l’ensemble des endroits où la structure est acceptée, vois Avancé : structure.
Pose beaucoup de questions
Pose beaucoup de questions étroites et indépendantes sur le même état dans une seule requête. C’est ainsi que tu maximises l’efficacité et l’intelligence par dollar avec l’API : les questions s’exécutent en parallèle, et le code peut combiner leurs signaux sans ajouter d’allers-retours séquentiels vers le modèle.
Vois le motif Fan-out spéculatif et le cookbook Questions en parallèle.
Combine les sorties des questions dans le code (ou alimente un modèle classique de ML)
Combine les réponses indépendantes avec des règles déterministes ou des sommes pondérées. Pour une composition apprise, utilise les probabilités comme caractéristiques dans un modèle classique d’apprentissage automatique en aval.
Exemple : combiner des signaux avec une notation pondérée
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) )La Notation composite montre comment préserver les jugements individuels tout en les combinant. Si tu n’as pas d’étiquettes pour un modèle en aval, utilise un ensemble de modèles de raisonnement coûteux pour les générer ; le cookbook AutoResearch montre comment entraîner un modèle classique sur les sorties de System One.
Route selon l’incertitude
Fais en sorte que le code prenne des actions différentes selon que les réponses sont sûres ou non. Escalade les cas incertains vers une personne ou un modèle de raisonnement plus coûteux. Teste les seuils en traçant la confiance en fonction de l’exactitude sur tes données.
Exemple : router par confiance
answer = response.answers["card_help_topic"] if answer.confidence < 0.8: route_to_human_review(ticket) else: route_to_handler(answer.choice, ticket)Vois Confiance et le routage conditionné par la confiance pour choisir les seuils et les adapter au risque de chaque action.
Tout assembler
Ce flux de travail de tickets de support garde le travail déterministe dans le code, n’envoie que le contexte structuré pertinent, évalue de nombreuses questions atomiques en une seule requête et compose les réponses avec des seuils de confiance explicites.
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)