ДокументацияКонцепции TypeSafeКак строить архитектуру с TypeSafe

Как строить архитектуру с TypeSafe

Design AI-powered software by keeping code in control and giving System One narrow, structured decisions.

Проектируйте ПО на базе ИИ, сохраняя контроль в коде и поручая System One точечные структурированные решения.

export function TypesafeExample({example, display, title}) {
const keyStrUriSafe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$";
function compressToEncodedURIComponent(input) {
if (input == null) return "";
return _compress(input, 6, function (a) {
return keyStrUriSafe.charAt(a);
});
}
function _compress(uncompressed, bitsPerChar, getCharFromInt) {
var i, value, context_dictionary = {}, context_dictionaryToCreate = {}, context_c = "", context_wc = "", context_w = "", context_enlargeIn = 2, context_dictSize = 3, context_numBits = 2, context_data = [], context_data_val = 0, context_data_position = 0, ii;
for (ii = 0; ii < uncompressed.length; ii += 1) {
context_c = uncompressed.charAt(ii);
if (!Object.prototype.hasOwnProperty.call(context_dictionary, context_c)) {
context_dictionary[context_c] = context_dictSize++;
context_dictionaryToCreate[context_c] = true;
}
context_wc = context_w + context_c;
if (Object.prototype.hasOwnProperty.call(context_dictionary, context_wc)) {
context_w = context_wc;
} else {
if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate, context_w)) {
if (context_w.charCodeAt(0) < 256) {
for (i = 0; i < context_numBits; i++) {
context_data_val = context_data_val << 1;
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
} else {
context_data_position++;
}
}
value = context_w.charCodeAt(0);
for (i = 0; i < 8; i++) {
context_data_val = context_data_val << 1 | value & 1;
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
} else {
context_data_position++;
}
value = value >> 1;
}
} else {
value = 1;
for (i = 0; i < context_numBits; i++) {
context_data_val = context_data_val << 1 | value;
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
} else {
context_data_position++;
}
value = 0;
}
value = context_w.charCodeAt(0);
for (i = 0; i < 16; i++) {
context_data_val = context_data_val << 1 | value & 1;
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
} else {
context_data_position++;
}
value = value >> 1;
}
}
context_enlargeIn--;
if (context_enlargeIn == 0) {
context_enlargeIn = Math.pow(2, context_numBits);
context_numBits++;
}
delete context_dictionaryToCreate[context_w];
} else {
value = context_dictionary[context_w];
for (i = 0; i < context_numBits; i++) {
context_data_val = context_data_val << 1 | value & 1;
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
} else {
context_data_position++;
}
value = value >> 1;
}
}
context_enlargeIn--;
if (context_enlargeIn == 0) {
context_enlargeIn = Math.pow(2, context_numBits);
context_numBits++;
}
context_dictionary[context_wc] = context_dictSize++;
context_w = String(context_c);
}
}
if (context_w !== "") {
if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate, context_w)) {
if (context_w.charCodeAt(0) < 256) {
for (i = 0; i < context_numBits; i++) {
context_data_val = context_data_val << 1;
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
} else {
context_data_position++;
}
}
value = context_w.charCodeAt(0);
for (i = 0; i < 8; i++) {
context_data_val = context_data_val << 1 | value & 1;
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
} else {
context_data_position++;
}
value = value >> 1;
}
} else {
value = 1;
for (i = 0; i < context_numBits; i++) {
context_data_val = context_data_val << 1 | value;
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
} else {
context_data_position++;
}
value = 0;
}
value = context_w.charCodeAt(0);
for (i = 0; i < 16; i++) {
context_data_val = context_data_val << 1 | value & 1;
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
} else {
context_data_position++;
}
value = value >> 1;
}
}
context_enlargeIn--;
if (context_enlargeIn == 0) {
context_enlargeIn = Math.pow(2, context_numBits);
context_numBits++;
}
delete context_dictionaryToCreate[context_w];
} else {
value = context_dictionary[context_w];
for (i = 0; i < context_numBits; i++) {
context_data_val = context_data_val << 1 | value & 1;
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
} else {
context_data_position++;
}
value = value >> 1;
}
}
context_enlargeIn--;
if (context_enlargeIn == 0) {
context_enlargeIn = Math.pow(2, context_numBits);
context_numBits++;
}
}
value = 2;
for (i = 0; i < context_numBits; i++) {
context_data_val = context_data_val << 1 | value & 1;
if (context_data_position == bitsPerChar - 1) {
context_data_position = 0;
context_data.push(getCharFromInt(context_data_val));
context_data_val = 0;
} else {
context_data_position++;
}
value = value >> 1;
}
while (true) {
context_data_val = context_data_val << 1;
if (context_data_position == bitsPerChar - 1) {
context_data.push(getCharFromInt(context_data_val));
break;
} else context_data_position++;
}
return context_data.join("");
}
function buildHref(ex) {
const documentText = ex.state === undefined ? "" : typeof ex.state === "string" ? ex.state : JSON.stringify(ex.state, null, 2);
return "https://console.typesafe.ai/decode#share/" + compressToEncodedURIComponent(JSON.stringify({
apiVersion: "v1",
documentText,
promptsText: JSON.stringify(ex.questions, null, 2),
selectedModels: ex.selectedModels
}));
}
const displayedExample = display === "questions" ? example.questions : example.state === undefined ? {
questions: example.questions
} : {
state: example.state,
questions: example.questions
};
const code = JSON.stringify(displayedExample, null, 2);
const href = buildHref(example);
return <div style={{
margin: "1.25rem 0"
}}>
<CodeBlock language="json" filename={title ?? "request"}>
{code}


;
}

System One — это модель TypeSafe для создания программного обеспечения на базе ИИ, а не автономных агентов. Она не генерирует код и не выбирает свое следующее действие самостоятельно. Она предоставляет AI-примитивы, которые встраиваются в программный код, поэтому логика остается под полным контролем программы, в то время как модель берет на себя вынесение здравых суждений над неструктурированными данными.

**Кратко:** создавайте стандартный рабочий процесс в программном коде и подключайте System One только там, где действительно необходим ИИ.
  • Сохраняйте управление потоком выполнения (control flow), детерминированные правила и побочные эффекты в коде.
  • Разделяйте обобщенные суждения на узкие типизированные вопросы с явными инструкциями и критериями.
  • Передавайте в каждый вопрос только тот контекст, который ему необходим.
  • Используйте вероятности и уверенность (confidence), чтобы принимать решения в коде, запрашивать ручную проверку или эскалировать кейс.
  • Задавайте независимые вопросы одновременно, а затем объединяйте ответы в коде.

Три архитектуры программного обеспечения

TypeSafe спроектирован для создания программного обеспечения на базе ИИ (AI-powered software), в котором код управляет рабочим процессом, а ИИ решает точечные структурированные задачи.

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

Что делает System One компонуемой

System One является типобезопасной архитектурно (type-safe by construction). Решения и вероятности соответствуют структурированным типам и JSON-схеме, ожидаемым вашим кодом, поэтому ему никогда не приходится извлекать значения из сгенерированного текста. Вопросы оцениваются независимо и параллельно. Результат одного примитива не становится скрытым контекстом, влияющим на результат другого. Результаты работы модели можно сравнивать и сортировать, используя их в логических условиях `if`, пороговых фильтрах и операциях сравнения. Большинство запросов выполняется примерно за 100 мс. System One работает достаточно быстро для использования в синхронной обработке запросов реального времени и пользовательских интерфейсах. [RLCD](/docs/introduction/machine-learning-primer) передает степень неопределенности через калиброванные вероятности вместо склонности к излишней самоуверенности. System One спроектирована для возврата стабильных ответов при повторных оценках. См. [кукбук по самосогласованности](/docs/cookbooks/consistency_noul_cookbook).

Поскольку каждый ответ строго ограничен предоставленными вариантами, модель возвращает полное распределение вероятностей по этим вариантам вместо генерации значений вне заданной схемы. Цель TypeSafe — обеспечить превосходство в соотношении интеллекта к скорости и стоимости более чем в 100 раз; базовая ставка заключается в том, что более доступный интеллект создаст гораздо больший спрос.

Проектирование рабочего процесса с System One

Оставляйте детерминированную работу коду. Это надежно и дешево. Избегайте агентных циклов `while`, когда то же поведение можно выразить стандартным программным алгоритмом.
PLAINTEXT api.wedstack.ru/v1
<Accordion title="Пример: детерминированные правила в коде">
  ```python theme={null}
  days_overdue = (today - invoice.due_date).days

  if days_overdue > 30:
      route_to_collections(invoice)
  ```
</Accordion>

Ознакомьтесь с [паттернами System One](/docs/patterns) для изучения надежных способов комбинирования решений модели с кодом.
Включайте только тот контекст, который относится к текущим вопросам. Это помогает модели избегать отвлекающих факторов и деградации контекста (context rot). Не полагайтесь на знания, заложенные в весах модели, если актуальную информацию можно передать из собственной базы знаний.
PLAINTEXT api.wedstack.ru/v1
<Accordion title="Пример: передача только релевантного контекста">
  <TypesafeExample
    title="request"
    display="request"
    example={{
  state: {
    ticket_message: 'My flight was cancelled. Can I get a refund?',
    refund_policy: 'Cancelled flights are eligible for a full refund.',
  },
  selectedModels: ['jev-latest'],
  questions: {
    policy_supports_refund: {
      type: 'noul',
      instructions:
        'Does the refund policy support the refund requested in the ticket?',
    },
  },
}}
  />
</Accordion>
Используйте вложенный JSON для полей `state` и `questions`. Указывайте вопросам на конкретные значения, когда это устраняет неоднозначность, выделяя пути обратными кавычками (backticks) внутри текста вопроса.
PLAINTEXT api.wedstack.ru/v1
<Accordion title="Пример: ссылка на вложенное значение">
  Используйте путь через точку и индекс в обратных кавычках, чтобы адресовать вопрос к конкретному вложенному значению, например `support.tickets[0].message`.

  <TypesafeExample
    title="request"
    display="request"
    example={{
  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.',
      },
    },
  },
  selectedModels: ['jev-latest'],
  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`?',
    },
  },
}}
  />
</Accordion>
Формулируйте максимально явные, узкие, конкретные и атомарные вопросы. Разбивайте сложные или неоднозначные вопросы на отдельные вопросы, каждый из которых оценивает одно конкретное свойство.
PLAINTEXT api.wedstack.ru/v1
<Info>
  Это, пожалуй, важнейшая концепция данного руководства. Широкие вопросы скрывают множество суждений за одним ответом. Атомарные вопросы делают эти суждения прозрачными, позволяя анализировать, настраивать и комбинировать их в коде.
</Info>

<Accordion title="Пример: декомпозиция обнаружения спама">
  <TypesafeExample
    title="Один широкий вопрос (плохо)"
    display="questions"
    example={{
  state: {
    message: {
      sender: {
        display_name: 'Acme Payroll',
        email: 'rewards@claim-bonus.example',
      },
      subject: 'Urgent: claim your employee bonus',
      body:
        'You have been selected for a $1,000 bonus. Confirm your payroll password today to receive it.',
      links: [
        {
          text: 'Claim bonus',
          url: 'http://claim-bonus.example/acme',
        },
      ],
    },
  },
  selectedModels: ['jev-latest'],
  questions: {
    is_spam: {
      type: 'noul',
      instructions: 'Is `message` spam?',
    },
  },
}}
  />

  <TypesafeExample
    title="Декомпозированные вопросы (хорошо)"
    display="questions"
    example={{
  state: {
    message: {
      sender: {
        display_name: 'Acme Payroll',
        email: 'rewards@claim-bonus.example',
      },
      subject: 'Urgent: claim your employee bonus',
      body:
        'You have been selected for a $1,000 bonus. Confirm your payroll password today to receive it.',
      links: [
        {
          text: 'Claim bonus',
          url: 'http://claim-bonus.example/acme',
        },
      ],
    },
  },
  selectedModels: ['jev-latest'],
  questions: {
    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`?',
    },
  },
}}
  />
</Accordion>

<Accordion title="Пример: верификация трассировки вызовов инструментов">
  <TypesafeExample
    title="Один широкий вопрос (плохо)"
    display="questions"
    example={{
  state: {
    request: {
      text: "What's the weather in Seattle tomorrow in Fahrenheit?",
      location: 'Seattle, WA',
      date: '2026-09-03',
      unit: 'fahrenheit',
    },
    available_tools: {
      geocode_city: {
        description: 'Resolve a city to latitude and longitude.',
        parameters: { city: 'string' },
      },
      get_weather: {
        description: 'Get the forecast for coordinates and a date.',
        parameters: {
          latitude: 'number',
          longitude: 'number',
          date: 'YYYY-MM-DD',
          unit: ['fahrenheit', 'celsius'],
        },
      },
    },
    trace: {
      tool_calls: [
        {
          id: 'call_1',
          name: 'geocode_city',
          arguments: { city: 'Seattle, WA' },
        },
        {
          id: 'call_2',
          name: 'get_weather',
          arguments: {
            latitude: 47.6062,
            longitude: -122.3321,
            date: '2026-09-03',
            unit: 'celsius',
          },
        },
      ],
      tool_results: [
        {
          tool_call_id: 'call_1',
          output: { latitude: 47.6062, longitude: -122.3321 },
        },
      ],
    },
  },
  selectedModels: ['jev-latest'],
  questions: {
    tool_calls_are_correct: {
      type: 'noul',
      instructions:
        'Is `trace.tool_calls` correct for `request` and `available_tools`?',
    },
  },
}}
  />

  <TypesafeExample
    title="Декомпозированные вопросы (хорошо)"
    display="questions"
    example={{
  state: {
    request: {
      text: "What's the weather in Seattle tomorrow in Fahrenheit?",
      location: 'Seattle, WA',
      date: '2026-09-03',
      unit: 'fahrenheit',
    },
    available_tools: {
      geocode_city: {
        description: 'Resolve a city to latitude and longitude.',
        parameters: { city: 'string' },
      },
      get_weather: {
        description: 'Get the forecast for coordinates and a date.',
        parameters: {
          latitude: 'number',
          longitude: 'number',
          date: 'YYYY-MM-DD',
          unit: ['fahrenheit', 'celsius'],
        },
      },
    },
    trace: {
      tool_calls: [
        {
          id: 'call_1',
          name: 'geocode_city',
          arguments: { city: 'Seattle, WA' },
        },
        {
          id: 'call_2',
          name: 'get_weather',
          arguments: {
            latitude: 47.6062,
            longitude: -122.3321,
            date: '2026-09-03',
            unit: 'celsius',
          },
        },
      ],
      tool_results: [
        {
          tool_call_id: 'call_1',
          output: { latitude: 47.6062, longitude: -122.3321 },
        },
      ],
    },
  },
  selectedModels: ['jev-latest'],
  questions: {
    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`?',
    },
  },
}}
  />
</Accordion>
Делайте вопросы лаконичными. Поля `instructions` и `criteria` обычно являются строками, и для короткого однозначного вопроса строки вполне достаточно. Однако они также могут быть объектами или массивами. Помещайте вопрос в одно поле, а вспомогательные данные — в другие.
PLAINTEXT api.wedstack.ru/v1
Структурирование полезно в следующих ситуациях:

* Вопросу требуется контекст или примеры. Длинное описание предыстории или список примеров входных данных лучше вынести в именованные поля рядом с вопросом, где ваш код сможет дополнять или изменять их без переписывания самого вопроса.
* Часть вопроса поступает из вашего кода. Когда значение берется из базы данных, вынесите его в отдельное поле вместо конкатенации в строковый шаблон.
* Несколько вопросов содержат похожие инструкции. Запрос принимает одно состояние и может содержать множество вопросов. Добавление структурированных данных помогает четко разграничить вопросы.

<Accordion title="Пример: ссылка на запись из вашего кода">
  Этот вопрос типа Noul сравнивает резюме из состояния с записью из базы данных кандидатов. Запись передается в поле `potential_duplicate` как есть, а вопрос ссылается на нее по имени.

  <TypesafeExample
    title="questions"
    display="questions"
    example={{
  state: {
    resume: {
      name: 'John Smith',
      location: 'Oakland, CA',
      summary: 'Backend engineer with eight years of Python and Go experience.',
      experience: [
        { employer: 'Google', title: 'Senior Backend Engineer', years: '2021-2025' },
        { employer: 'Microsoft', title: 'Software Engineer', years: '2017-2021' },
      ],
    },
  },
  selectedModels: ['jev-latest'],
  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`?',
      },
    },
  },
}}
  />
</Accordion>

Данные `potential_duplicate`, полученные из кода, могут меняться со временем. Вопрос `question` ссылается на них с помощью обратных кавычек.

Описания внутри `criteria` также могут быть объектами. Для Choice описание каждого варианта может быть объектом, в котором указано, что охватывает этот вариант, что относится к другим вариантам, и приведено несколько примеров. Используйте одинаковые имена полей для всех вариантов, чтобы модель могла напрямую сопоставлять их.

<Accordion title="Пример: контрастные критерии для Choice">
  <TypesafeExample
    title="questions"
    display="questions"
    example={{
  state: 'How many disposable virtual cards can I make per day?',
  selectedModels: ['jev-latest'],
  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?',
          ],
        },
      },
    },
  },
}}
  />
</Accordion>

На странице каждого типа примитива приведен подробный пример:

* [Noul](/docs/primitives/noul#structured-instructions) сравнивает одно резюме с несколькими записями кандидатов, по одному вопросу на запись, при этом вопросы формируются в коде.
* [Choice](/docs/primitives/choice#structured-instructions-and-criteria) описывает два легко путаемых варианта с указанием того, что охватывает каждый из них, для чего он не предназначен, и примерами.
* [Score](/docs/primitives/score#structured-level-descriptions) задает для каждого уровня описание и примеры ситуаций.

В [кукбуке по каскадному извлечению структурированных данных](/docs/cookbooks/sde_cascade) показан сценарий использования общих формулировок с серией одинаковых вопросов к каждому полю извлеченной записи.

Короткий однозначный вопрос или критерий может оставаться простой строкой. Добавляйте структуру тогда, когда это помогает разделить указания, которые иначе могли бы смешаться. Полный список мест, где допускается использование структуры, см. в разделе [Продвинутый уровень: структура](/docs/primitives/advanced).
Задавайте множество узких независимых вопросов к одному и тому же состоянию в рамках одного запроса. Именно так вы максимизируете эффективность и отдачу от каждого затраченного доллара при работе с API: вопросы выполняются параллельно, а код может объединять их сигналы без добавления последовательных задержек на вызовы модели.
PLAINTEXT api.wedstack.ru/v1
См. паттерн [Спекулятивное ветвление (Speculative Fan-Out)](/docs/patterns/fan-out) и [Кукбук по параллельным вопросам](/docs/cookbooks/parallel_questions).
Объединяйте независимые ответы с помощью детерминированных правил или взвешенных сумм. Для обучаемой композиции используйте вероятности в качестве признаков (features) во внешней классической модели машинного обучения.
PLAINTEXT api.wedstack.ru/v1
<Accordion title="Пример: объединение сигналов во взвешенную оценку">
  ```python theme={null}
  answers = response.answers

  # Объединение независимых сигналов в единую оценку для приложения.
  quality = (
      0.4 * answers["answers_request"].noul
      + 0.4 * answers["citations_are_supported"].noul
      + 0.2 * (1 - answers["contradicts_context"].noul)
  )
  ```
</Accordion>

В разделе [Композитная оценка (Composite Scoring)](/docs/patterns/composite-scoring) показано, как сохранить детальность отдельных оценок при их объединении. Если у вас нет разметки для обучения downstream-модели, используйте ансамбль более тяжелых reasoning-моделей для ее генерации; в [кукбуке AutoResearch](/docs/cookbooks/autoresearch_feature_discovery) продемонстрировано обучение классической модели на результатах System One.
Настройте код на выполнение различных действий в зависимости от уверенности или неуверенности ответа. Эскалируйте спорные случаи человеку или более дорогой reasoning-модели. Подбирайте пороги уверенности, сопоставляя ее с точностью на ваших реальных данных.
PLAINTEXT api.wedstack.ru/v1
<Accordion title="Пример: маршрутизация по уверенности">
  ```python theme={null}
  answer = response.answers["card_help_topic"]

  if answer.confidence < 0.8:
      route_to_human_review(ticket)
  else:
      route_to_handler(answer.choice, ticket)
  ```
</Accordion>

О выборе пороговых значений и сопоставлении их с рисками каждого действия см. разделы [Уверенность (Confidence)](/docs/confidence) и [Маршрутизация с контролем уверенности](/docs/patterns/confidence-routing).
Декомпозиция не требует дополнительных сетевых запросов. Вопросы к одному состоянию выполняются параллельно.

Собираем всё воедино

Этот рабочий процесс обработки обращений в службу поддержки сохраняет детерминированную логику в коде, отправляет только релевантный структурированный контекст, оценивает множество атомарных вопросов в одном запросе и объединяет ответы с явными проверками уверенности.

PYTHON TITLE="TRIAGE_TICKET.PY" THEME={NULL} api.wedstack.ru/v1
from typesafe_sdk import Choice, Noul, NoulCriteria, Score, TypeSafeClient


def triage_ticket(ticket, customer):
    # Обработка детерминированных состояний без вызова модели.
    if ticket["status"] == "closed":
        return "no_action"

    open_orders = [
        order for order in customer["orders"] if order["status"] != "delivered"
    ]

    # Включаем только структурированный контекст, необходимый для вопросов ниже.
    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"],
        },
    }

    # Задаем структурированные атомарные вопросы вместе для параллельного выполнения.
    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,
        )

    # Объединяем независимые сигналы спама с весами, заданными в коде.
    answers = response.answers
    spam_risk = (
        0.45 * answers["requests_credentials"].noul
        + 0.30 * answers["sender_identity_mismatch"].noul
        + 0.25 * answers["unexpected_reward"].noul
    )

    # Эскалируем неуверенные суждения вместо угадывания.
    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)

    # Позволяем коду решить, какие спекулятивные ответы важны на этой ветке.
    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)