Как строить с TypeSafe
Проектируйте софт с ИИ, оставляя контроль коду и давая System One узкие структурированные решения.
System One — это модель TypeSafe для создания софта с ИИ, а не агентов. Она не генерирует код и не выбирает своё следующее действие. Она предоставляет ИИ-примитивы, встраиваемые в софт, так что контроль остаётся у кода, а модель берёт на себя суждения здравого смысла о неструктурированных данных.
Три архитектуры софта
TypeSafe создан для построения софта с ИИ, где процессом владеет код, а ИИ берёт на себя узкие структурированные решения.
Традиционный код — это сложное дерево решений, построенное из простых программных примитивов. Поскольку каждый примитив надёжен, разработчики могут компоновать их в абстракции более высокого уровня.
Агент обрабатывает инструкции и выбирает свой следующий шаг. Это хорошо работает, когда процесс контролирует человек, но каждый цикл даёт ещё одну возможность сойти с рельсов.
Код выполняет детерминированную работу и владеет потоком управления. Модель появляется только там, где системе нужен программируемый здравый смысл или интерпретация неструктурированных данных. Каждая задача ИИ остаётся атомарной и ограниченной.


Что делает System One компонуемым
Структурированность
System One типобезопасен по построению. Решения и вероятности соответствуют структурированным программным типам и схеме JSON, которых ожидает ваш код, поэтому ему никогда не приходится извлекать значение из сгенерированного текста.
Параллельность
Вопросы оцениваются независимо и параллельно. Результат одного примитива не становится скрытым контекстом, меняющим результат другого примитива.
Сравнимость
Выводы можно сортировать; они могут управлять умными инструкциями if, порогами и сравнениями.
Скорость
Большинство запросов выполняются примерно за 100 мс. System One достаточно быстр для путей запросов в реальном времени и пользовательских интерфейсов.
Калиброванная уверенность
RLCD сообщает о неопределённости через калиброванные вероятности, а не склоняется к самоуверенности.
Самосогласованность
System One спроектирован так, чтобы возвращать стабильные ответы при повторных оценках. См. cookbook по самосогласованности.
Поскольку каждый вывод ограничен предложенными вариантами, модель возвращает полное распределение вероятностей по этим вариантам, а не изобретает значение вне схемы. Цель TypeSafe — соотношение интеллекта к скорости и стоимости больше 100×; в основе лежит ставка на то, что более дешёвый интеллект создаст гораздо больший спрос.
Проектирование процесса с System One
Используйте код, когда можете
Держите детерминированную работу в коде. Это надёжно и дёшево. Избегайте циклов
whileагента, когда тот же результат можно выразить программным процессом.Пример: держите детерминированные правила в коде
days_overdue = (today - invoice.due_date).days if days_overdue > 30: route_to_collections(invoice)Просмотрите паттерны System One для ограниченных способов компоновать решения модели с кодом.
Декомпозируйте входное состояние
Включайте только контекст, релевантный текущим вопросам. Это помогает модели избегать отвлечений и порчи контекста. Не полагайтесь на знания, хранящиеся в весах модели, когда актуальную информацию можно взять из вашей собственной базы знаний.
Пример: отправляйте только релевантный контекст
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?" } } }Используйте структуру во входном состоянии
Используйте вложенный JSON для полей
stateиquestions. Указывайте вопросы на конкретные значения, когда это устраняет неоднозначность, и заключайте каждый путь внутри вопроса в обратные кавычки.Пример: ссылка на вложенное значение
Используйте путь с точками и индексами в обратных кавычках, чтобы указать вопрос на конкретное вложенное значение, например
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`?" } } }Декомпозируйте вопросы
Задавайте максимально явные, узкие, конкретные и атомарные вопросы. Разбивайте сложные или плохо определённые вопросы на отдельные, каждый из которых оценивает одно свойство.
Пример: декомпозиция обнаружения спама
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`?" } }Пример: проверка трейса вызовов инструментов
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`?" } }Использование структуры в вопросах
Держите вопросы короткими.
instructionsиcriteriaобычно строки, и для короткого однозначного вопроса строки достаточно. Они также могут быть объектами или массивами. Поместите вопрос в одно поле, а данные, направляющие вопрос, — в остальные.Структура помогает в таких случаях:
- Вопросу нужен контекст или примеры. Длинное предложение с предысторией или список примеров ввода лучше поместить в именованные поля рядом с вопросом, где ваш код может их дополнять или заменять, не переписывая вопрос.
- Часть вопроса берётся из вашего кода. Когда значение приходит из базы данных, поместите его в отдельное поле, а не вставляйте в шаблон строки.
- У нескольких вопросов похожие инструкции. Запрос принимает одно состояние и может включать несколько вопросов. Дополнительные данные помогают сделать вопросы различимыми.
Пример: ссылка на запись из вашего кода
Этот Noul сравнивает резюме из состояния с записью из базы данных кандидатов. Запись как есть попадает в поле
potential_duplicate, а вопрос ссылается на неё по имени.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`?" } } }Данные «potential_duplicate», полученные из кода, могут меняться со временем. «question» ссылается на них через обратные кавычки.
Описания внутри
criteriaтоже могут быть объектами. Для Choice описание каждого варианта может быть объектом, который говорит, что покрывает вариант, что относится к другому варианту, и приводит несколько примеров. Используйте одинаковые имена полей во всех вариантах, чтобы модель могла сравнивать их напрямую.Пример: задайте контрастивные критерии Choice
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?" ] } } } }На странице каждого типа вопроса есть разобранный пример:
- Noul сравнивает одно резюме с несколькими записями кандидатов, по одному вопросу на запись, причём вопросы строятся в коде.
- Choice описывает два легко путаемых варианта: что покрывает каждый, для чего он не предназначен, и примеры.
- Score даёт каждому уровню описание и примеры ситуаций.
Cookbook по каскаду извлечения структурированных данных показывает случай с общими формулировками, где один и тот же набор вопросов задаётся о каждом поле извлечённой записи.
Короткий однозначный вопрос или критерий может остаться строкой. Добавляйте структуру, когда она разделяет указания, которые иначе слились бы вместе. Полный набор мест, где принимается структура, см. в разделе Продвинутое: структура.
Задавайте много вопросов
Задавайте много узких независимых вопросов об одном состоянии в одном запросе. Так вы максимизируете эффективность и интеллект на каждый доллар при работе с API: вопросы выполняются параллельно, а код может комбинировать их сигналы, не добавляя последовательных обращений к модели.
См. паттерн спекулятивного fan-out и cookbook по параллельным вопросам.
Комбинируйте выводы вопросов в коде (или подайте их в классическую ML-модель)
Комбинируйте независимые ответы детерминированными правилами или взвешенными суммами. Для обучаемой композиции используйте вероятности как признаки в последующей классической модели машинного обучения.
Пример: комбинируйте сигналы взвешенной оценкой
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) )Композитная оценка показывает, как сохранить отдельные суждения, комбинируя их. Если у вас нет меток для последующей модели, используйте ансамбль дорогих рассуждающих моделей, чтобы их сгенерировать; cookbook по AutoResearch показывает, как обучить классическую модель на выводах System One.
Маршрутизируйте по неопределённости
Пусть код предпринимает разные действия для уверенных и неуверенных ответов. Эскалируйте неопределённые случаи к человеку или более дорогой рассуждающей модели. Проверяйте пороги, строя график зависимости уверенности от точности на ваших данных.
Пример: маршрутизация по уверенности
answer = response.answers["card_help_topic"] if answer.confidence < 0.8: route_to_human_review(ticket) else: route_to_handler(answer.choice, ticket)См. Уверенность и Маршрутизация с gating по уверенности для выбора порогов и сопоставления их с риском каждого действия.
Собираем всё вместе
Этот процесс обработки заявок в поддержку держит детерминированную работу в коде, отправляет только релевантный структурированный контекст, оценивает много атомарных вопросов в одном запросе и компонует ответы с явными gate-ами по уверенности.
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)