<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>karandashov.com – Ai</title><link>https://karandashov.com/tags/ai/</link><description>Recent content in Ai on karandashov.com</description><generator>Hugo -- gohugo.io</generator><language>ru</language><managingEditor>vlad.karandashov.tech@gmail.com (Влад Карандашов)</managingEditor><webMaster>vlad.karandashov.tech@gmail.com (Влад Карандашов)</webMaster><lastBuildDate>Fri, 19 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://karandashov.com/tags/ai/index.xml" rel="self" type="application/rss+xml"/><item><title>GPT из Java без шаманства</title><link>https://karandashov.com/posts/2026-06-19-java-gpt-timeweb/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><author>vlad.karandashov.tech@gmail.com (Влад Карандашов)</author><guid>https://karandashov.com/posts/2026-06-19-java-gpt-timeweb/</guid><description>
&lt;p&gt;&lt;figure&gt;
&lt;img src="https://karandashov.com/posts/2026-06-19-java-gpt-timeweb/cover.png" title="GPT из Java без шаманства" alt="GPT из Java без шаманства" loading="lazy" /&gt;
&lt;figcaption&gt;GPT из Java без шаманства&lt;/figcaption&gt;
&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Подключить GPT-модель к Java-приложению технически несложно. Сложнее не превратить это в метод на двести строк, где prompt
склеивается из строк, ответ парсится на удачу, а в базу потом улетает текст, который модель придумала с очень уверенным
видом. Здесь будет практический разбор именно backend-интеграции: Java, Spring Boot, &lt;code&gt;openai-java&lt;/code&gt;, немного Spring AI, Responses
API, web search и JSON на выходе.&lt;/p&gt;
&lt;h2&gt;Почему не только Spring AI&lt;span class="hx:absolute hx:-mt-20" id="почему-не-только-spring-ai"&gt;&lt;/span&gt;
&lt;a href="#%d0%bf%d0%be%d1%87%d0%b5%d0%bc%d1%83-%d0%bd%d0%b5-%d1%82%d0%be%d0%bb%d1%8c%d0%ba%d0%be-spring-ai" class="subheading-anchor" aria-label="Ссылка на этот раздел"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Когда приложение уже на Spring Boot, рука сама тянется к Spring AI. Это нормальный первый выбор. У Spring AI есть
&lt;code&gt;ChatClient&lt;/code&gt;, properties, prompt templates, advisors, converters и привычная Spring-интеграция.
Через properties можно задать &lt;code&gt;base-url&lt;/code&gt;, ключ, модель, retry-настройки, response format, headers и &lt;code&gt;extra-body&lt;/code&gt; для
OpenAI-compatible серверов. Это описано
&lt;a
href="https://docs.spring.io/spring-ai/reference/api/chat/openai-chat.html"target="_blank" rel="noopener"&gt;в документации&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Но у абстракций есть цена. Как только нужен не просто chat, а конкретные возможности Responses API, hosted &lt;code&gt;web_search&lt;/code&gt;
и точный контроль над request body, официальный SDK оказывается проще. Он ближе к реальному API. Не надо гадать, как
общая модель Spring AI переложит provider-specific фичу в HTTP-запрос.&lt;/p&gt;
&lt;p&gt;Есть еще одна неприятная особенность: в OpenAI Chat-настройках Spring AI &lt;code&gt;tools&lt;/code&gt; описаны как function tools. Это не то же самое,
что hosted tools Responses API, например &lt;code&gt;web_search&lt;/code&gt;.
В README &lt;code&gt;openai-java&lt;/code&gt; Responses API назван основным API для работы с моделями, а Chat Completions — предыдущим
стандартом, который
&lt;a
href="https://raw.githubusercontent.com/openai/openai-java/main/README.md"target="_blank" rel="noopener"&gt;по-прежнему поддерживается&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Не хочу разводить холивар на тему «Spring AI или официальный SDK», для себя я сформулировал простое правило.
Если требуется построить систему похожую на полноценный чат (ai-бот с поддержкой диалога, RAG, агентность) — берем Spring AI с кучей готовых решений.
Если же требуется более тонкая настройка или задача состоит в точечном вызове без поддержи диалога — лучше взять официальный SDK.&lt;/p&gt;
&lt;h2&gt;Клиент как обычный Spring bean&lt;span class="hx:absolute hx:-mt-20" id="клиент-как-обычный-spring-bean"&gt;&lt;/span&gt;
&lt;a href="#%d0%ba%d0%bb%d0%b8%d0%b5%d0%bd%d1%82-%d0%ba%d0%b0%d0%ba-%d0%be%d0%b1%d1%8b%d1%87%d0%bd%d1%8b%d0%b9-spring-bean" class="subheading-anchor" aria-label="Ссылка на этот раздел"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Обычно достаточно такого набора зависимостей (проверьте актуальные версии в maven central):&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;build.gradle.kts&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-kotlin" data-lang="kotlin"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dependencies {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; implementation(&lt;span style="color:#e6db74"&gt;&amp;#34;com.openai:openai-java:4.39.1&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; implementation(&lt;span style="color:#e6db74"&gt;&amp;#34;org.springframework.ai:spring-ai-model:2.0.0&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Скопировать код"
aria-label="Скопировать код"
data-copied-label="Скопировано!"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;code&gt;openai-java&lt;/code&gt; нужен для транспорта и моделей API.
&lt;code&gt;spring-ai-model&lt;/code&gt; дает несколько полезных классов вокруг structured output.&lt;/p&gt;
&lt;p&gt;OpenAI SDK умеет настраивать &lt;code&gt;baseUrl&lt;/code&gt;, &lt;code&gt;apiKey&lt;/code&gt;, timeout, retries и другие параметры. Поэтому начинаем с конфигов:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;AiProperties.java&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@ConfigurationProperties&lt;/span&gt;(prefix &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;ai&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;record&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;AiProperties&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; String baseUrl,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; String apiKey,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; String model,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Duration timeout
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Скопировать код"
aria-label="Скопировать код"
data-copied-label="Скопировано!"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;И теперь можем построить bean &lt;code&gt;OpenAIClient&lt;/code&gt; используя данные из &lt;code&gt;application.yml&lt;/code&gt;:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;AiConfiguration.java&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Configuration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@EnableConfigurationProperties&lt;/span&gt;(AiProperties.&lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;AiConfiguration&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Bean&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; OpenAIClient &lt;span style="color:#a6e22e"&gt;openAiClient&lt;/span&gt;(AiProperties properties) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; OpenAIOkHttpClient.&lt;span style="color:#a6e22e"&gt;builder&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;baseUrl&lt;/span&gt;(properties.&lt;span style="color:#a6e22e"&gt;baseUrl&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;apiKey&lt;/span&gt;(properties.&lt;span style="color:#a6e22e"&gt;apiKey&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;timeout&lt;/span&gt;(properties.&lt;span style="color:#a6e22e"&gt;timeout&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;maxRetries&lt;/span&gt;(1)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;build&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Скопировать код"
aria-label="Скопировать код"
data-copied-label="Скопировано!"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Отдельная мелочь, про которую легко забыть: client не стоит создавать на каждый запрос. В README SDK есть прямое
предупреждение на этот счет: внутри
&lt;a
href="https://raw.githubusercontent.com/openai/openai-java/main/README.md"target="_blank" rel="noopener"&gt;есть connection pool и thread pools&lt;/a&gt;,
ими лучше пользоваться повторно.&lt;/p&gt;
&lt;p&gt;Модель не надо хардкодить. Сегодня вы смотрите на качество, завтра на цену, послезавтра выясняется, что web search
доступен только у соседнего варианта. Выносите в конфиг как в примере выше.&lt;/p&gt;
&lt;p&gt;А вот retry я предпочитаю не доверять llm-клиенту: может приводить к лишнему потреблению токенов, даже когда это не сильно нужно.
Выставляю &lt;code&gt;maxRetries(1)&lt;/code&gt; и организую нужную retry-политику кодом.&lt;/p&gt;
&lt;h2&gt;Timeweb AI Gateway&lt;span class="hx:absolute hx:-mt-20" id="timeweb-ai-gateway"&gt;&lt;/span&gt;
&lt;a href="#timeweb-ai-gateway" class="subheading-anchor" aria-label="Ссылка на этот раздел"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;В рамках РФ прямой доступ к языковым моделям затруднен из-за санкций, это ни для кого не секрет.
Все это прекрасно обходилось через vpn, пока одно доблестное ведомство не решило ввести уже свои санкции против собственных граждан.
Отечественные &amp;ldquo;аналоги&amp;rdquo; мало того что стоят в три раза дороже, так еще и сильно уступают в качестве.
Поэтому в качестве условно законного варианта хочу предложить Timeweb AI Gateway.
Это не реклама, просто это объективно достаточно удобный сервис для интеграции &lt;strong&gt;своих приложений&lt;/strong&gt; с иностранными языковыми моделями
без лишних препонов.&lt;/p&gt;
&lt;p&gt;&lt;a
href="https://timeweb.cloud/docs/ai-agents/api-usage/ai-gateway"target="_blank" rel="noopener"&gt;Timeweb AI Gateway&lt;/a&gt; — это прямой API к языковым моделям.
Не агент, не RAG-платформа, не MCP-хаб. По сути прокси к различным моделям.
Самое прекрасное — позапросная тарификация. Вам может казаться, что подписка выгоднее, но в случае приложений это совсем не так.
Сегодня вашим приложением никто не пользуется, а вы продолжаете платить подписку. Завтра случилась черная пятница и лимиты исчерпались за час.
Позапросная тарификация сильно выгодней для сервисов.&lt;/p&gt;
&lt;p&gt;Минимальный сценарий такой:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;В панели Timeweb Cloud открыть «AI-агенты» и вкладку «AI Gateway»
&lt;ul&gt;
&lt;li&gt;у timeweb есть свои надстройки над моделями, которые они называют &amp;ldquo;агенты&amp;rdquo;, но вам нужен именно «AI Gateway»&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Создать API-ключ и выбрать модель
&lt;ul&gt;
&lt;li&gt;ориентируйтесь на необходимое качество, доступные &lt;code&gt;tools&lt;/code&gt; и цену&lt;/li&gt;
&lt;li&gt;на момент написания статьи самая дешевая — &lt;code&gt;openai/gpt-5-nano&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;В приложении указать &lt;code&gt;baseUrl=https://api.timeweb.ai/v1&lt;/code&gt;, ключ и имя модели&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Серьезно: обращайте внимание на поддерживаемые &lt;code&gt;tools&lt;/code&gt;.
OpenAI-compatible означает совместимую форму API, а не гарантию, что любая модель поддержит &lt;code&gt;responses&lt;/code&gt;, native structured output и web search одновременно.
Это надо проверять интеграционным тестом или хотя бы отдельным ручным вызовом. Заглядывайте в документацию конкретной модели.&lt;/p&gt;
&lt;h2&gt;Request: не мешаем инструкцию и данные&lt;span class="hx:absolute hx:-mt-20" id="request-не-мешаем-инструкцию-и-данные"&gt;&lt;/span&gt;
&lt;a href="#request-%d0%bd%d0%b5-%d0%bc%d0%b5%d1%88%d0%b0%d0%b5%d0%bc-%d0%b8%d0%bd%d1%81%d1%82%d1%80%d1%83%d0%ba%d1%86%d0%b8%d1%8e-%d0%b8-%d0%b4%d0%b0%d0%bd%d0%bd%d1%8b%d0%b5" class="subheading-anchor" aria-label="Ссылка на этот раздел"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Пример плохого prompt:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;prompt.txt&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Вот данные пользователя: ...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Сделай с ними ... и верни JSON.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Скопировать код"
aria-label="Скопировать код"
data-copied-label="Скопировано!"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Такое часто даже работает. До первого странного пользовательского текста, смены модели или попытки покрыть это тестами.
Проблема в том, что здесь смешаны задача, данные, формат ответа и бизнес-ограничения.&lt;/p&gt;
&lt;p&gt;Я стараюсь раскладывать request на несколько частей:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;instructions&lt;/code&gt; — стабильные правила поведения модели;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;input&lt;/code&gt; — данные конкретного запроса;&lt;/li&gt;
&lt;li&gt;schema или native structured output — контракт ответа;&lt;/li&gt;
&lt;li&gt;allowlist — значения, которые модель может выбрать, но не может расширять сама;&lt;/li&gt;
&lt;li&gt;tools — внешние инструменты, которые модели разрешено вызвать.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Возьмем в качестве примера простую задачу: пользователь вводит название лекарства и сканирует штрихкод.
Мы хотим сделать сервис, который будет возвращать правильное полное название лекарства, его описание и список подходящих категорий.
Если есть возможность — выполняем web-поиск по штрихкоду.&lt;/p&gt;
&lt;p&gt;Пример входного dto:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;EnrichmentRequest.java&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;record&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;EnrichmentRequest&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// примерное название от пользователя&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; String approximateTitle,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// штрихкод&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; String barcode,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// какие категории есть в нашей бд&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Set&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;String&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; allowedLabels
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Скопировать код"
aria-label="Скопировать код"
data-copied-label="Скопировано!"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Пример ответа:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;EnrichmentResponse.java&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;record&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;EnrichmentResponse&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; String title,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; String summary,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Set&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;String&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; labels
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Скопировать код"
aria-label="Скопировать код"
data-copied-label="Скопировано!"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;code&gt;input&lt;/code&gt; лучше собирать как конструктор, в зависимости от того, что нужно в конкретной ситуации.&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;PromptBuilder.java&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; String &lt;span style="color:#a6e22e"&gt;buildInput&lt;/span&gt;(EnrichmentRequest request) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; Ты помогаешь найти в интернете лекарство по штрихкоду. Найди информацию о лекарстве,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; отвечай на русском языке и верни только название, описание и подходящие теги.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; Краткое описание должно понятно объяснять, что делает лекарство без сложных терминов.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; Выбирай теги исключительно из переданного списка, не придумывай новые.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; Штрихкод:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; %s
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; Предполагаемое название:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; %s
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; Допустимые теги:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; %s
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; Правила:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; - верни только теги из списка допустимых;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; - если данных недостаточно, не выдумывай факты;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; - summary должен быть коротким и проверяемым;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; - ответ должен соответствовать JSON-схеме.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;formatted&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; request.&lt;span style="color:#a6e22e"&gt;barcode&lt;/span&gt;(),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; request.&lt;span style="color:#a6e22e"&gt;approximateTitle&lt;/span&gt;(),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; String.&lt;span style="color:#a6e22e"&gt;join&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;, &amp;#34;&lt;/span&gt;, request.&lt;span style="color:#a6e22e"&gt;allowedLabels&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; )
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; );
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Скопировать код"
aria-label="Скопировать код"
data-copied-label="Скопировано!"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Обязательно выносите промпты в конфиг! Они постоянно меняются, не хочется каждый раз пересобирать приложение.&lt;/p&gt;
&lt;h2&gt;Structured output: сначала native&lt;span class="hx:absolute hx:-mt-20" id="structured-output-сначала-native"&gt;&lt;/span&gt;
&lt;a href="#structured-output-%d1%81%d0%bd%d0%b0%d1%87%d0%b0%d0%bb%d0%b0-native" class="subheading-anchor" aria-label="Ссылка на этот раздел"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Если endpoint и модель поддерживают native structured output, начинать надо с него. Схема передается в API отдельным
параметром, а не просьбой внутри prompt&amp;rsquo;а. Это сильнее, чем «верни валидный JSON, пожалуйста».&lt;/p&gt;
&lt;p&gt;В &lt;code&gt;openai-java&lt;/code&gt; для Responses API это делается через &lt;code&gt;ResponseCreateParams.Builder.text(Class&amp;lt;T&amp;gt;)&lt;/code&gt;. SDK строит JSON
Schema по Java-классу, локально проверяет ее на совместимость с требованиями OpenAI и возвращает &lt;code&gt;StructuredResponse&amp;lt;T&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;Responses API request&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;StructuredResponseCreateParams&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;EnrichmentResponse&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; params &lt;span style="color:#f92672"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ResponseCreateParams.&lt;span style="color:#a6e22e"&gt;builder&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;model&lt;/span&gt;(properties.&lt;span style="color:#a6e22e"&gt;model&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;instructions&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; buildInput(request)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; )
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;input&lt;/span&gt;(buildInput(request))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;addTool&lt;/span&gt;(WebSearchTool.&lt;span style="color:#a6e22e"&gt;builder&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;type&lt;/span&gt;(WebSearchTool.&lt;span style="color:#a6e22e"&gt;Type&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;WEB_SEARCH&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;build&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;toolChoice&lt;/span&gt;(ToolChoiceOptions.&lt;span style="color:#a6e22e"&gt;REQUIRED&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;text&lt;/span&gt;(EnrichmentResponse.&lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;build&lt;/span&gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Скопировать код"
aria-label="Скопировать код"
data-copied-label="Скопировано!"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Spring AI тоже считает native structured output более надежным вариантом для моделей, которые умеют работать с JSON Schema
напрямую (&lt;a
href="https://docs.spring.io/spring-ai/reference/api/structured-output-converter.html"target="_blank" rel="noopener"&gt;Spring AI Structured Output&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Тогда зачем вообще вспоминать schema-in-prompt? Из-за совместимости. В OpenAI-compatible мире один gateway принимает
&lt;code&gt;text.format&lt;/code&gt;, другой отклоняет параметр, третья модель поддерживает только Chat Completions, а web search может быть
доступен отдельно от structured output. В таких случаях &lt;code&gt;BeanOutputConverter&amp;lt;T&amp;gt;&lt;/code&gt; из Spring AI остается нормальным запасным
вариантом:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;Fallback structured output&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; BeanOutputConverter&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;EnrichmentResponse&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; outputConverter &lt;span style="color:#f92672"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; BeanOutputConverter&lt;span style="color:#f92672"&gt;&amp;lt;&amp;gt;&lt;/span&gt;(EnrichmentResponse.&lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;String input &lt;span style="color:#f92672"&gt;=&lt;/span&gt; buildInput(request) &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;\n\n&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; outputConverter.&lt;span style="color:#a6e22e"&gt;getFormat&lt;/span&gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Скопировать код"
aria-label="Скопировать код"
data-copied-label="Скопировано!"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Но это именно запасной путь. Если native structured output работает у вашей модели через ваш gateway, лучше использовать
его.&lt;/p&gt;
&lt;h2&gt;Responses API и web search&lt;span class="hx:absolute hx:-mt-20" id="responses-api-и-web-search"&gt;&lt;/span&gt;
&lt;a href="#responses-api-%d0%b8-web-search" class="subheading-anchor" aria-label="Ссылка на этот раздел"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Теперь полный вызов. Web search подключается как hosted tool. Если поиск обязателен, можно выставить
&lt;code&gt;toolChoice(REQUIRED)&lt;/code&gt;. Если это дорогой или редкий сценарий, лучше оставить автоматический выбор.&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;EnrichmentService.java&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; EnrichmentResponse &lt;span style="color:#a6e22e"&gt;enrich&lt;/span&gt;(EnrichmentRequest request) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; StructuredResponseCreateParams&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;EnrichmentResponse&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; params &lt;span style="color:#f92672"&gt;=&lt;/span&gt; ...;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; StructuredResponse&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;EnrichmentResponse&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; response &lt;span style="color:#f92672"&gt;=&lt;/span&gt; client.&lt;span style="color:#a6e22e"&gt;responses&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;create&lt;/span&gt;(params);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; EnrichmentResponse result &lt;span style="color:#f92672"&gt;=&lt;/span&gt; extractStructuredOutput(response);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; validateAndNormalize(result, request.&lt;span style="color:#a6e22e"&gt;allowedLabels&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Скопировать код"
aria-label="Скопировать код"
data-copied-label="Скопировано!"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;В документации OpenAI web search описан именно как &lt;a
href="https://developers.openai.com/api/docs/guides/tools-web-search"target="_blank" rel="noopener"&gt;tool для Responses API&lt;/a&gt;.
Там же есть раздел про sources: если модель и API возвращают источники, их можно смотреть отдельно.&lt;/p&gt;
&lt;p&gt;Даже при structured output ответ Responses API остается деревом output items. Там могут быть сообщения, tool calls,
reasoning items и другие элементы. Поэтому извлечение typed result лучше спрятать в helper.&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div class="hextra-code-filename not-prose" dir="auto"&gt;EnrichmentService.java&lt;/div&gt;&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; EnrichmentResponse &lt;span style="color:#a6e22e"&gt;extractStructuredOutput&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; StructuredResponse&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;EnrichmentResponse&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; response
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; response.&lt;span style="color:#a6e22e"&gt;output&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;stream&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;filter&lt;/span&gt;(StructuredResponseOutputItem::isMessage)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;map&lt;/span&gt;(StructuredResponseOutputItem::asMessage)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;flatMap&lt;/span&gt;(message &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; message.&lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;stream&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;filter&lt;/span&gt;(StructuredResponseOutputMessage.&lt;span style="color:#a6e22e"&gt;Content&lt;/span&gt;::isOutputText)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;map&lt;/span&gt;(StructuredResponseOutputMessage.&lt;span style="color:#a6e22e"&gt;Content&lt;/span&gt;::asOutputText)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;findFirst&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;orElseThrow&lt;/span&gt;(() &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; IllegalStateException(&lt;span style="color:#e6db74"&gt;&amp;#34;AI response has no structured output&amp;#34;&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-8"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Скопировать код"
aria-label="Скопировать код"
data-copied-label="Скопировано!"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;code&gt;StructuredResponse&amp;lt;T&amp;gt;&lt;/code&gt; при этом не выбрасывает исходный ответ. Raw response доступен, а значит можно смотреть status,
usage, tool calls, sources и служебную информацию. В production рядом с этим helper&amp;rsquo;ом придется обработать refusal,
пустой output, incomplete status, timeout и ошибки gateway. Иначе «модель не ответила» быстро превращается в
&lt;code&gt;NullPointerException&lt;/code&gt; где-нибудь в контроллере.&lt;/p&gt;
&lt;h3&gt;Ответ модели не становится правдой&lt;span class="hx:absolute hx:-mt-20" id="ответ-модели-не-становится-правдой"&gt;&lt;/span&gt;
&lt;a href="#%d0%be%d1%82%d0%b2%d0%b5%d1%82-%d0%bc%d0%be%d0%b4%d0%b5%d0%bb%d0%b8-%d0%bd%d0%b5-%d1%81%d1%82%d0%b0%d0%bd%d0%be%d0%b2%d0%b8%d1%82%d1%81%d1%8f-%d0%bf%d1%80%d0%b0%d0%b2%d0%b4%d0%be%d0%b9" class="subheading-anchor" aria-label="Ссылка на этот раздел"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Даже валидный JSON может быть неправильным. Модель могла выбрать несуществующую метку, растянуть summary на полстраницы,
вернуть HTML, markdown или уверенно записать факт, которого нет.&lt;/p&gt;
&lt;p&gt;Минимальная защита выглядит так:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Получили structured output.&lt;/li&gt;
&lt;li&gt;Прогнали валидацию.&lt;/li&gt;
&lt;li&gt;Нормализовали строки.&lt;/li&gt;
&lt;li&gt;Отфильтровали значения по доступным (например из бд).&lt;/li&gt;
&lt;li&gt;Проверили бизнес-инварианты.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Я бы относился к LLM-ответу как к ответу внешнего API, причем API с богатым воображением. Схема помогает. Валидация
помогает сильнее. Allowlist решает больше, чем еще одна фраза в prompt&amp;rsquo;е.&lt;/p&gt;
&lt;h2&gt;Задержки, кэш и другие вещи, о которых вспоминают поздно&lt;span class="hx:absolute hx:-mt-20" id="задержки-кэш-и-другие-вещи-о-которых-вспоминают-поздно"&gt;&lt;/span&gt;
&lt;a href="#%d0%b7%d0%b0%d0%b4%d0%b5%d1%80%d0%b6%d0%ba%d0%b8-%d0%ba%d1%8d%d1%88-%d0%b8-%d0%b4%d1%80%d1%83%d0%b3%d0%b8%d0%b5-%d0%b2%d0%b5%d1%89%d0%b8-%d0%be-%d0%ba%d0%be%d1%82%d0%be%d1%80%d1%8b%d1%85-%d0%b2%d1%81%d0%bf%d0%be%d0%bc%d0%b8%d0%bd%d0%b0%d1%8e%d1%82-%d0%bf%d0%be%d0%b7%d0%b4%d0%bd%d0%be" class="subheading-anchor" aria-label="Ссылка на этот раздел"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;h4&gt;Задержки&lt;span class="hx:absolute hx:-mt-20" id="задержки"&gt;&lt;/span&gt;
&lt;a href="#%d0%b7%d0%b0%d0%b4%d0%b5%d1%80%d0%b6%d0%ba%d0%b8" class="subheading-anchor" aria-label="Ссылка на этот раздел"&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;Вызов модели почти никогда не ведет себя как быстрый запрос к соседнему сервису. Даже обычный ответ может занимать
&lt;strong&gt;десятки секунд&lt;/strong&gt;. Web search, reasoning и большой context добавляют еще. Если это закладывать в архитектуру после релиза, будет
больно.&lt;/p&gt;
&lt;p&gt;Сначала стоит решить, может ли пользователь ждать синхронно. Для коротких сценариев request-response еще приемлемо.
Хотя даже в этом случае нужно подумать, чтобы случайно не сделать вызов ai внутри database-транзакции.&lt;/p&gt;
&lt;p&gt;Лучше всего конечно, делать асинхронно:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;приняли запрос, сохранили, вернули id;&lt;/li&gt;
&lt;li&gt;обработали по мере возможностей, можно использовать kafka для очереди;&lt;/li&gt;
&lt;li&gt;результат отдали через polling или уведомление (SSE, WebSocket и тп).&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Кэш&lt;span class="hx:absolute hx:-mt-20" id="кэш"&gt;&lt;/span&gt;
&lt;a href="#%d0%ba%d1%8d%d1%88" class="subheading-anchor" aria-label="Ссылка на этот раздел"&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;Кэш нужен не только ради скорости. В случае AI — он буквально экономит деньги.
Декомпозируйте задачу и кжшируйте все, что можно кэшировать, чтобы не делать лишних ai-вызовов.
Значение кэша лучше хранить уже после валидации и нормализации.&lt;/p&gt;
&lt;p&gt;TTL зависит от задачи: результат с web search по актуальным данным стареет быстрее, чем простая нормализация текста.
В некоторых случаях TTL может быть не нужен вовсе. Из хорошего — вам не нужен Redis: задержки llm настолько велики,
что запрос к основной БД работает на порядки быстрее. Если хранение долговременное — используйте основную БД.&lt;/p&gt;
&lt;h4&gt;Retry&lt;span class="hx:absolute hx:-mt-20" id="retry"&gt;&lt;/span&gt;
&lt;a href="#retry" class="subheading-anchor" aria-label="Ссылка на этот раздел"&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;С retries тоже легко перестараться. Повторить timeout или 5xx один раз с backoff&amp;rsquo;ом — нормально. Три раза подряд
переспрашивать модель, которая стабильно возвращает невалидный JSON, — просто способ заплатить втрое больше. Нужны лимиты:
max input size, max output tokens, timeout, rate limit, ограничение tool calls и circuit breaker на внешний gateway.&lt;/p&gt;
&lt;h4&gt;Дедубликация&lt;span class="hx:absolute hx:-mt-20" id="дедубликация"&gt;&lt;/span&gt;
&lt;a href="#%d0%b4%d0%b5%d0%b4%d1%83%d0%b1%d0%bb%d0%b8%d0%ba%d0%b0%d1%86%d0%b8%d1%8f" class="subheading-anchor" aria-label="Ссылка на этот раздел"&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;Более того, на входе обязательно нужно организовать дедубликацию!
Нельзя переплачивать за то, что пользователь нажал два раза кнопку.&lt;/p&gt;
&lt;h4&gt;Observability&lt;span class="hx:absolute hx:-mt-20" id="observability"&gt;&lt;/span&gt;
&lt;a href="#observability" class="subheading-anchor" aria-label="Ссылка на этот раздел"&gt;&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;И отдельно observability. Помимо стандартных latency, timeout, доля fallback&amp;rsquo;ов и тд — нужно отслеживаться количество потраченных токенов.
Разделяйте input и output токены, их цена отличается. Выделяйте лимиты на пользователя и алерты по системе в целом.&lt;/p&gt;
&lt;h2&gt;Что тестировать&lt;span class="hx:absolute hx:-mt-20" id="что-тестировать"&gt;&lt;/span&gt;
&lt;a href="#%d1%87%d1%82%d0%be-%d1%82%d0%b5%d1%81%d1%82%d0%b8%d1%80%d0%be%d0%b2%d0%b0%d1%82%d1%8c" class="subheading-anchor" aria-label="Ссылка на этот раздел"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Prompt руками не рефакторится безопасно. Нужны тесты, которые фиксируют форму запроса и поведение на плохих ответах.&lt;/p&gt;
&lt;p&gt;Проверяем:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;какой JSON уходит в ai: &lt;code&gt;model&lt;/code&gt;, &lt;code&gt;instructions&lt;/code&gt;, &lt;code&gt;input&lt;/code&gt;, &lt;code&gt;tools&lt;/code&gt;, &lt;code&gt;tool_choice&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;request содержит native structured output config;&lt;/li&gt;
&lt;li&gt;web search включен только там, где он нужен;&lt;/li&gt;
&lt;li&gt;невалидный response не проходит в бизнес-логику;&lt;/li&gt;
&lt;li&gt;выдуманные поля и неизвестные labels отбрасываются;&lt;/li&gt;
&lt;li&gt;API-ключ не попадает в логи.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Для HTTP-уровня хорошо подходит WireMock. Он позволяет зафиксировать request body и вернуть response, похожий на реальный
Responses API. Это особенно полезно для web search: гонять настоящую модель ради проверки сериализации дорого и медленно.&lt;/p&gt;
&lt;h2&gt;Где провести границы&lt;span class="hx:absolute hx:-mt-20" id="где-провести-границы"&gt;&lt;/span&gt;
&lt;a href="#%d0%b3%d0%b4%d0%b5-%d0%bf%d1%80%d0%be%d0%b2%d0%b5%d1%81%d1%82%d0%b8-%d0%b3%d1%80%d0%b0%d0%bd%d0%b8%d1%86%d1%8b" class="subheading-anchor" aria-label="Ссылка на этот раздел"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Spring AI можно брать там, где он снимает рутину, но не там, где прячет важную provider-specific деталь.
Для сложных интеграций лучше использовать собственный SDK.&lt;/p&gt;
&lt;p&gt;Timeweb AI Gateway — входная точка, а не магическая замена OpenAI: endpoint совместим по форме, но поддержку конкретной модели,
tools и structured output все равно надо проверять.&lt;/p&gt;
&lt;p&gt;А приложение делает самую неблагодарную работу: не верит модели на слово. Проверяет схему. Проверяет значения. Режет
лишнее. Кэширует валидный результат. И заранее учитывает, что нейросеть может отвечать долго, дорого и иногда неправильно.&lt;/p&gt;
&lt;p&gt;Это чуть больше кода, чем «спросить GPT и распарсить строку». Зато такой код можно поддерживать, тестировать и переносить
между провайдерами без переписывания половины сервиса.&lt;/p&gt;</description></item></channel></rss>