Model I/O
После главы вы сможете спроектировать provider-neutral границу model call и не смешивать output модели с выполненным действием или durable state.
Минимальный контракт
На уровне HTTP model service принимает сериализуемый input и возвращает output. Для runtime полезнее более строгая граница:
interface ModelPort {
complete(request: ModelRequest): Promise<AgentDecision>;
}
type AgentDecision =
| {kind: 'final'; content: string}
| {kind: 'tool'; call: ToolCall};
Vendor adapter переводит этот контракт в Responses API, Messages API или локальный inference endpoint. Runtime не должен знать название SDK.
Что входит в request
Полезно различать:
- instructions: устойчивые правила приложения;
- conversation: реплики пользователя и видимые ответы;
- goal: актуальная задача текущего run;
- task state: структурированная версия мира;
- tool definitions: capability, schema и effect metadata;
- observations: результаты уже выполненных действий;
- retrieved context: выбранные данные с provenance.
Порядок и точная иерархия ролей зависят от API. Не кодируйте policy только порядком строк в одном prompt: authorization должна существовать в обычном коде.
Почему строка END — слабый протокол
assistant: Готово. END
Проблемы:
- пользовательский документ тоже может содержать
END; - parser не знает, завершён ли ответ или только процитирован token;
- невозможно выразить
approval_required,budget_exhausted,cancelled; - tool arguments остаются невалидированным текстом.
Предпочтительнее discriminated union или native tool-call items. Structured output уменьшает двусмысленность, но всё равно требует runtime validation: output модели остаётся недоверенным вводом программы.
Messages прошлого turn
На второй вопрос модели обычно нужен не «весь массив размышлений», а новая проекция рабочего состояния:
Если provider хранит conversation server-side, он может сам присоединить предыдущие items. Если приложение stateless, нужные items передаются снова. В обоих случаях приложение отвечает за то, чтобы актуальные instructions, policy и domain state не исчезли.
Практическое правило: пользовательский ввод, на который ещё отвечают, должен быть доступен; старые tool payloads можно сжимать; скрытый chain-of-thought прошлого turn хранить как application memory не следует.
Reasoning boundary
Reasoning model может выполнять внутреннее вычисление до выдачи output. Это не тот же объект, что application events.
Храните:
- final decision;
- tool call и validated arguments;
- policy result;
- environment outcome;
- краткое объяснение/summary, если оно нужно человеку;
- token/latency/cost metadata.
Не делайте recovery зависимым от скрытого reasoning trace. Современные API могут переносить provider-specific reasoning items между вызовами, но это механизм inference continuity, не ваш audit log.
Streaming
Streaming меняет доставку output, но не обязан менять семантику шага.
Наивный вариант выполняет tool, как только в потоке встретилось похожее имя. Надёжный вариант:
- принимает typed stream events;
- собирает полный tool-call item;
- проверяет завершение item;
- валидирует schema;
- выполняет policy/approval;
- только затем вызывает effect.
Частичный текст можно показывать пользователю, но он не является committed final answer. UI должен различать in_progress, completed, incomplete, cancelled и failed.
Cancellation и timeout
Отмена model request не откатывает уже запущенный внешний tool. Нужны отдельные механизмы:
- abort model/network call;
- cooperative cancellation long-running tool;
- deadline на каждый слой;
- durable interrupt record;
- compensation для уже завершённого effect.
Три стратегии stateful conversation
| Стратегия | Выигрыш | Цена | Плохой выбор, когда… |
|---|---|---|---|
| Передавать весь transcript | Простота | Рост tokens, stale facts | Диалог длинный или sensitive |
| Provider conversation ID | Меньше orchestration | Vendor lifecycle/retention | Нужен portable/ZDR runtime |
| Собственный state + projection | Контроль, portability | Storage и context policy | Одноразовый прототип |
Helios и реальный аналог
Helios получает вопрос о доставке. Model adapter может быть любым, но AgentDecision остаётся одинаковым. Аналогично support-бот neobank должен отделять model output от ledger API: модель предлагает get_transaction, runtime выполняет чтение с tenant identity.
Лаборатория
ScriptedModel записывает два запроса. Проверьте, что второй содержит исходную цель, пользовательский вопрос и tool observation:
npm run test:course:pattern -- "preserving the user goal"
Self-check
- Какие output variants поддерживает ваш adapter?
- Где валидируется structured output?
- Сохраняются ли instructions при переходе между turns?
- Может ли partial stream запустить side effect?
- Что именно отменяет cancellation?
- Можно ли заменить provider, не переписав runtime?
Механика и источники
Механика API, проверено 2026-08-30: OpenAI Responses API принимает instructions/input/tools, поддерживает conversation или previous response linkage, streaming, background status и tool calls. Это vendor mapping, не core contract курса: Responses API reference.