<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Все публикации подряд на Хабре</title>
    <link>https://habr.com/rss/all/all/</link>
    <description>Все публикации подряд на Хабре</description>
    <managingEditor>editor@habr.com</managingEditor>
    <pubDate>Tue, 15 Sep 2026 05:30:30 +0000</pubDate>
    <image>
      <url>https://habrastorage.org/webt/ym/el/wk/ymelwk3zy1gawz4nkejl_-ammtc.png</url>
      <title>Хабр</title>
      <link>https://habr.com/ru/articles/</link>
    </image>
    <item>
      <title>Шестидесятые: десятилетие, когда мы придумали будущее</title>
      <link>https://habr.com/ru/companies/ddosguard/articles/1081406/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1081406</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/e98/c94/4df/e98c944df435b6154ba3109214f0e0c2.png width=2048 height=1304 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/e98/c94/4df/e98c944df435b6154ba3109214f0e0c2.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/e98/c94/4df/e98c944df435b6154ba3109214f0e0c2.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;В двадцатом веке, который закончился будто совсем недавно, было много крутых и странных десятилетий. Но мое любимое, конечно, &lt;strong&gt;шестидесятые&lt;/strong&gt; — декада, когда люди придумали, кажется, абсолютно все, от интернета и покорения космоса до стереозвука и нейросетей. Да, изначально все это были по большей части proofs of concept — но какие идеи, какие смелые эксперименты и какие немыслимые результаты! Кажется, люди склонны ретроспективно недооценивать 1960-е, думая, что это просто время хиппи, странной музыки и огромных ящиков с мигающими огоньками. Постараюсь набросать дорожную карту и объяснить, что именно меня так восхищает вот конкретно в этом десятилетии, куда (или, точнее, когда) я бы с удовольствием переселился отсюда.&lt;p&gt;Эта статья — первая в цикле про &lt;em&gt;десятилетия, которые сформировали наш мир&lt;/em&gt;. Лайкайте, если понравилось и хотите следующие.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/40a/90e/aad/40a90eaad446e077dcf89cdef682fa33.png alt=&#34;Футуристические дизайны домов 60-х на обложке, здесь и далее — Чарльз Шридде для компании Motorola&#34; title=&#34;Футуристические дизайны домов 60-х на обложке, здесь и далее — Чарльз Шридде для компании Motorola&#34; width=2048 height=1303 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/40a/90e/aad/40a90eaad446e077dcf89cdef682fa33.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/40a/90e/aad/40a90eaad446e077dcf89cdef682fa33.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Футуристические дизайны домов 60-х на обложке, здесь и далее — Чарльз Шридде для компании Motorola&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;h3&gt;Панорама&lt;/h3&gt;&lt;p&gt;Шестидесятые были настолько богаты явлениями и откровениями, что их сложно свести к нескольким знаменитым событиям — столько всего: космическая гонка, высадка человека на Луну, The Beatles, холодная война, студенческие революции и контркультура!.. А еще — несколько ключевых технологических трендов; в общем, все и не перечислишь. Но я попытаюсь.&lt;p&gt;В это десятилетие человечество словно одновременно начало экспериментировать с несколькими фундаментальными вопросами, которые в конечном счете все структурно оказались связаны между собой: можно ли построить машину, которая учится? Можно ли соединить машины так, чтобы они образовали новую среду коммуникации? Можно ли передавать информацию через всю планету практически мгновенно? Можно ли превратить вычисление в часть управления гигантскими техническими системами? И, самое главное, можно ли описать живой организм как механизм обработки информации? &lt;p&gt;Последнее стало основой структурализма и лингвистического поворота — возможно, первой (и последней) точки, где гуманитарии и технари внезапно вышли навстречу друг другу на одну и ту же опушку в дремучем лесу познания и очень обрадовались.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/af0/8f7/64c/af08f764c53fd8aa7b88628decb2b687.png width=680 height=885 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/af0/8f7/64c/af08f764c53fd8aa7b88628decb2b687.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/af0/8f7/64c/af08f764c53fd8aa7b88628decb2b687.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Шестидесятые были временем великих идей, которые пока все еще искали себе применение и понимание во все еще пока очень инерционном и старом мире — мире, все еще находящемся в состоянии шока и обращенном к потреблению как к философии. Всего лишь 15 лет прошло со времени Второй мировой войны, руины от которой во многих городах все еще были видны воочию и среди них играли дети, которые потом вырастут и изменят мир технологии, литературы, философии. &lt;p&gt;Эту войну вызвали как раз Большие Идеи, на которые у все еще живых и нестарых участников прошедшего конфликта, родителей молодежи 60-х, была жесткая аллергия. Поэтому успех в конечном счете получили только те идеи, которые удалось применить к потребительской культуре (значимым исключением здесь является космос, но о нем позже), про которые сразу было понятно, как они сделают жизнь конкретного человека лучше. &lt;p&gt;Это проявится более явно немного позднее — в 70-е, когда идеи прошлой декады начнут превращать в работающие технологии и конкретные устройства. Восьмидесятые перенесут многие из них из лабораторий и государственных программ в офисы, автомобили и гостиные.&lt;p&gt;Тем интереснее рассмотреть шестидесятые как время эдакого междуцарствия, эпохи между старым и новым миром, когда все казалось возможным и были пока еще непонятны пределы того, чего может достичь человек (да и вопрос, конечно: мы правда нащупали эти пределы или просто потом изменилась эпоха и рыночек порешал?).&lt;h3&gt;Первая нейронка&lt;/h3&gt;&lt;p&gt;Начать, пожалуй, стоит с упоминания одной из самых характерных машин эпохи — устройства, которое одновременно выглядело как научный эксперимент, прототип будущего и почти фантастическое обещание. Машины, которую ее создатель собирался научить видеть закономерности самостоятельно.&lt;p&gt;Речь, конечно, про перцептрон (ранее я кратко &lt;a href=https://habr.com/ru/companies/ddosguard/articles/857618/&gt;упоминал&lt;/a&gt; о нем в статье про суперкомпьютеры Mark).&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/c3f/7a9/cf1/c3f7a9cf11dd1c15f5a9e1e2aac63529.png alt=&#34;Фрэнк Розенблатт с компьютером Mark I Perceptron в 1960 году&#34; title=&#34;Фрэнк Розенблатт с компьютером Mark I Perceptron в 1960 году&#34; width=967 height=1200 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/c3f/7a9/cf1/c3f7a9cf11dd1c15f5a9e1e2aac63529.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/c3f/7a9/cf1/c3f7a9cf11dd1c15f5a9e1e2aac63529.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Фрэнк Розенблатт с компьютером Mark I Perceptron в 1960 году&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;В 1950-е компьютеры уже успели приобрести почти мифологический статус. Огромные машины, занимавшие целые помещения, выполняли вычисления с такой скоростью, которая еще недавно казалась невозможной. Они помогали рассчитывать траектории, обрабатывать военные данные, решать математические задачи и управлять все более сложными техническими системами. Но при всей своей мощности компьютер оставался именно вычислительной машиной. Он делал то, что ему было предписано делать.&lt;p&gt;Вопрос, который начал по-настоящему волновать ученых уже на рубеже 1950-х и 1960-х, звучал значительно радикальнее: а что, если машине не обязательно заранее сообщать правила? Что, если она сможет каким-то образом извлекать их из опыта? В эпоху ChatGPT и Claude, конечно, сложно представить себе, насколько эта интеллектуальная гипотеза тогда казалась смелой, на границе с безумием.&lt;p&gt;В классическом представлении о компьютере человек сначала формулирует алгоритм. Он определяет последовательность операций, а машина выполняет их быстрее и точнее. В этой схеме источник интеллектуального решения находится вне компьютера: программист уже знает, что именно необходимо сделать, и переводит собственное знание в последовательность инструкций.&lt;p&gt;Перцептрон предполагал другой путь: не заранее объяснять машине мир, а организовать систему так, чтобы она могла изменяться в зависимости от столкновения с этим миром, адаптироваться и реагировать. &lt;p&gt;Это был в целом очень характерный для шестидесятых поворот. В нескольких областях одновременно начинает ослабевать вера в то, что сложную систему можно полностью описать набором заранее известных правил. Вместо этого появляется интерес к адаптации, обратной связи и самоизменению.&lt;p&gt;Не случайно рядом с ранними нейросетями развивается кибернетика (которую в СССР объявили в 1952 году лженаукой и предательством идей марксизма-ленинизма). Взаимодействие как человеческих организмов, так и машин постепенно начинают рассматривать через общие понятия входа, выхода, обратной связи и обработки информации.&lt;p&gt;Очень любопытно, что вокруг перцептрона практически сразу возникла знакомая современному читателю атмосфера завышенных ожиданий. Как только появилась возможность показать, что машина способна обучаться распознаванию определенных закономерностей, вокруг технологии начали строиться гораздо более масштабные прогнозы (ну вы знаете, что &lt;a href=https://habr.com/ru/companies/ddosguard/articles/892396/&gt;скоро искусственный разум с нами сравняется и поработит&lt;/a&gt;). &lt;p&gt;Учитывая тогдашний уровень развития технологии это кажется, конечно, смешным. Перцептрон действительно мог обучаться определенным задачам классификации. Но между этим и универсальным искусственным разумом находилась пропасть. Научиться различать относительно простые категории — совсем не то же самое, что понимать мир, обладать общими знаниями или самостоятельно формулировать цели. Сегодня, как нам кажется, мы хорошо видим эту разницу. Что не мешает нам, разинув рот, &lt;a href=https://habr.com/ru/companies/ddosguard/articles/1078030/&gt;внимать пиар-кампании OpenAI&lt;/a&gt; о том, как агенты искусственного разума самостоятельно принимают решения стать суперхакерами.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/069/6f5/48d/0696f548d62e93e15468073a679dc1d2.png width=1588 height=1010 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/069/6f5/48d/0696f548d62e93e15468073a679dc1d2.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/069/6f5/48d/0696f548d62e93e15468073a679dc1d2.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;В некотором смысле современный нейросетевой бум — это возвращение в шестидесятые, но с вычислительными мощностями XXI века. Это повод задуматься об интересном оптическом искажении: сегодня, когда мы смотрим на огромные языковые модели, обученные на гигантских массивах данных и работающие на тысячах специализированных ускорителей, кажется, будто сама идея машинного обучения родилась вместе с современными GPU и дата-центрами. История перцептрона напоминает, насколько это неверно.&lt;p&gt;Современные нейросети выросли из очень старой мечты: &lt;strong&gt;создать машину, которой не нужно заранее объяснять весь мир&lt;/strong&gt;. &lt;p&gt;Ее сопровождала другая, не менее радикальная идея: что, если сосредоточиться не на том, чтобы делать отдельную машину умнее, а на том, чтобы соединить множество машин так, чтобы они образовали качественно новую среду?&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/625/104/255/625104255c5eb1e403d807f20f26e501.png alt=&#34;ARPANET, 60-е&#34; title=&#34;ARPANET, 60-е&#34; width=984 height=988 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/625/104/255/625104255c5eb1e403d807f20f26e501.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/625/104/255/625104255c5eb1e403d807f20f26e501.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;ARPANET, 60-е&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;h3&gt;Как мир попал в паутину&lt;/h3&gt;&lt;p&gt;В начале 1960-х компьютер сам по себе был чрезвычайно дорогой и редкой машиной. Он занимал целое помещение (как сегодняшний дата-центр, полный серверов, каждый из которых в тысячи раз мощнее такого гиганта из 60-х), обслуживался специальным персоналом и часто работал в режиме, который сегодня напоминал бы централизованный сервис: множество людей и организаций соперничали за получение доступа к ограниченному вычислительному ресурсу. &lt;p&gt;Связать два компьютера между собой кабелем в принципе не было инженерной проблемой (хотя этот кабель должен был быть очень длинным!). Сначала нужно было ответить на более фундаментальный вопрос: &lt;strong&gt;а что именно должны делать две машины, когда они оказываются соединены&lt;/strong&gt;?&lt;p&gt;Обмениваться файлами? Передавать результаты вычислений? Использовать ресурсы друг друга? Работать как единая система?&lt;p&gt;В 1960-е инженеры постепенно пришли к мысли, которая сегодня кажется самоочевидной: вычислительная мощность сама по себе является не единственным ценным ресурсом. Не менее, а может в перспективе и более важной будет возможность передавать информацию между вычислительными системами.&lt;p&gt;Эта перспектива оправдалась, сегодня она называется интернет.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/d5b/1a0/70b/d5b1a070b763829e54247a6214971dcb.png alt=&#34;На момент запуска ARPANET участников сети во всей Америке было меньше, чем сегодня их на одной лестничной клетке любого многоквартирного дома&#34; title=&#34;На момент запуска ARPANET участников сети во всей Америке было меньше, чем сегодня их на одной лестничной клетке любого многоквартирного дома&#34; width=1280 height=720 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/d5b/1a0/70b/d5b1a070b763829e54247a6214971dcb.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/d5b/1a0/70b/d5b1a070b763829e54247a6214971dcb.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;На момент запуска ARPANET участников сети во всей Америке было меньше, чем сегодня их на одной лестничной клетке любого многоквартирного дома&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Если для каждого взаимодействия двух машин создавать отдельное соединение, сеть быстро становится громоздкой и дорогой. Кроме того, возникает проблема устойчивости: что произойдет, если важный узел окажется недоступен?&lt;p&gt;Вокруг этих вопросов в 1960-е начали развиваться идеи, которые впоследствии получили название &lt;strong&gt;пакетной коммутации&lt;/strong&gt;. Ее основа была одновременно простой и радикальной: сообщение не обязательно должно путешествовать по сети как единый непрерывный объект. Его можно разделить на небольшие части — пакеты. Каждый пакет получает необходимую информацию о том, куда он должен попасть, и затем сеть передает его через доступные маршруты.&lt;p&gt;Любопытно, что идея пакетной коммутации не возникла как единое изобретение одного человека в одном месте. В США Пол Бэран исследовал устойчивые распределенные коммуникационные системы, в Великобритании Дональд Дэвис разрабатывал собственные концепции передачи информации небольшими блоками и, собственно, ввел широко известный термин packet. &lt;p&gt;Разные исследовательские группы подходили к сходной проблеме с разных сторон, но постепенно складывалась новая интеллектуальная модель: сеть не обязательно должна состоять из одного главного центра и множества периферийных устройств. Она может быть распределенной.&lt;p&gt;Здесь снова мы видим один из основных проблесков интуиций 60-х: инженеры все чаще создавали системы, сложность которых превышала возможности непосредственного контроля над каждым отдельным процессом. Поэтому вместо того, чтобы централизованно управлять каждым элементом, они начинали проектировать &lt;strong&gt;правила взаимодействия элементов&lt;/strong&gt;.&lt;p&gt;Самый первый запуск ARPANET в 1969 году обернулся комичной историей: при попытке передать команду с компьютера в Калифорнийском университете в Лос-Анджелесе на удаленную машину в Стэнфордском исследовательском институте система успела передать только две буквы из слова LOGIN — LO. После этого соединение прервалось.&lt;p&gt;В английском языке «Lo» с восклицательным знаком («Lo!») это старое выражение, означающее «внемлите!» или «узрите!».&lt;p&gt;И мир внял и узрел.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/110/cc8/b17/110cc8b17acd1646a0fad4fa9ddd6917.png width=1024 height=881 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/110/cc8/b17/110cc8b17acd1646a0fad4fa9ddd6917.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/110/cc8/b17/110cc8b17acd1646a0fad4fa9ddd6917.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;h3&gt;Когда связь выходит за пределы Земли&lt;/h3&gt;&lt;p&gt;Пока инженеры пытались соединить компьютеры друг с другом, другая технологическая революция происходила буквально над их головами.&lt;p&gt;Компьютерные сети в конце шестидесятых оставались экспериментом нескольких исследовательских центров, однако орбитальные спутники уже начинали менять сам масштаб человеческой коммуникации. &lt;p&gt;Что более важно (и вот, в конечном итоге, ответ, почему непрактичный космос пользовался такой почти религиозной популярностью в 60-е), результат успехов в космической связи можно было &lt;strong&gt;сразу и наглядно&lt;/strong&gt; показать миллионам людей на экране телевизора.&lt;p&gt;Сегодня сложно это представить, в частности, людям, уже родившимся в эпоху интернета, но телевидение, особенно в первые десятилетия своего существования, оставалось в основном национальным или региональным явлением. Каждая страна создавала собственное телевизионное пространство, собственные программы и собственную аудиторию. Даже международные события доходили до зрителей с задержкой. Событие могло произойти утром в одной стране и появиться на экранах другой только вечером или вообще на следующий день.&lt;p&gt;Спутниковая связь обещала изменить саму логику этой системы. Радиосигнал можно было отправить не только вдоль поверхности Земли, через последовательность наземных станций, но и вверх — к аппарату, находящемуся над планетой, который затем передавал его обратно. В историческом смысле последствия этой идеи оказались огромными: &lt;strong&gt;коммуникация впервые начинала строиться в масштабе всей планеты&lt;/strong&gt;.&lt;p&gt;Да, разумеется, первый спутник запустили Советы, и это достижение навсегда вписано в историю. Но одним из подлинных символов новой эпохи в технологическом плане стал американский Telstar. Его запуск в 1962 году и первые телевизионные трансляции через Атлантику произвели примерно тот же эффект, который позднее будет производить демонстрация первых персональных компьютеров или ранних интернет-сервисов. &lt;div class=tm-iframe_temp data-src=https://embedd.srv.habr.com/iframe/6aa415236adf0a0da6a2fed0 data-style id=6aa415236adf0a0da6a2fed0 width data-habr-games&gt;&lt;/div&gt;&lt;p&gt;В середине десятилетия связь начала превращаться из серии отдельных технических экспериментов в настоящую инфраструктуру. Появление геостационарных спутников означало, что аппарат может оставаться неподвижным относительно определенной точки земной поверхности, обеспечивая постоянное покрытие огромной территории.&lt;p&gt;К концу 60-х эта инфраструктура уже начала менять не только техническую сторону коммуникаций, но и само представление о публичном событии. Одним из наиболее ярких моментов стал международный телепроект &lt;em&gt;Our World&lt;/em&gt; 25 июня 1967 году — первая крупная глобальная телетрансляция с участием The Beatles, объединившая аудиторию сразу нескольких стран. &lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/59e/85f/ca8/59e85fca8f0626a5fb2058f93545957d.png width=932 height=540 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/59e/85f/ca8/59e85fca8f0626a5fb2058f93545957d.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/59e/85f/ca8/59e85fca8f0626a5fb2058f93545957d.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Сегодня подобное событие кажется почти скромным на фоне постоянных трансляций через интернет. Но в тот момент сама возможность для миллионов людей одновременно наблюдать происходящее была новой социальной технологией.&lt;p&gt;Позднее этот принцип станет фундаментом для спутникового телевидения, глобальных концертов, прямых трансляций спортивных соревнований, круглосуточных новостных каналов, а затем и интернет-платформ, на которых любое событие может мгновенно стать глобальным.&lt;p&gt;Параллельно с этим происходила очень важная вещь, связанная с отправкой людей на Луну. &lt;div class=tm-iframe_temp data-src=https://embedd.srv.habr.com/iframe/6aa41531c926e00df3309aa6 data-style id=6aa41531c926e00df3309aa6 width data-habr-games&gt;&lt;/div&gt;&lt;p&gt;(Ричард Линклейтер в фильме &lt;em&gt;Apollo 10 1/2&lt;/em&gt; (2022) про свое детство близ космопорта в Хьюстоне вспоминает, как это было невероятно: одновременно видеть в кино фильм Кубрика про покорение космоса и видеть по телевизору, как люди реально покоряют Луну. Все буквально казалось возможным, реальность стала пластичной, пусть и лишь на какой-то миг)&lt;p&gt;Нет, понятно, что само по себе это событие было ОЧЕНЬ важным, но речь про другое: &lt;p&gt;Космическая программа шестидесятых стала одним из первых случаев в истории, когда человечество было вынуждено собрать в единую работающую систему огромное количество технологий, каждая из которых сама по себе уже была сложной: вычислительные машины, связь, миниатюрная электроника, новые материалы, системы управления, программное обеспечение, навигация и автоматизация. &lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/7a8/c95/7c7/7a8c957c7fbf00d0f2a24578d532dc0f.png width=1296 height=730 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/7a8/c95/7c7/7a8c957c7fbf00d0f2a24578d532dc0f.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/7a8/c95/7c7/7a8c957c7fbf00d0f2a24578d532dc0f.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Когда в мае 1961 года Джон Кеннеди объявил, что Соединенные Штаты намерены до конца десятилетия высадить человека на Луну и вернуть его обратно, это выглядело настоящим челленджем. Первый американский астронавт совершил суборбитальный полет всего несколькими неделями ранее. До Луны оставалось почти 400 тысяч километров, а компьютеры, которым предстояло участвовать в управлении будущим космическим кораблем, по современным меркам были невероятно ограниченными.&lt;p&gt;И все же, к концу десятилетия задача была выполнена, и кадры с Нилом Армстронгом, шагающим по Луне, стали одним из самых известных образов XX века (и объектом бесчисленных теорий заговора, настолько сегодня невозможно поверить, что это произошло с тем уровнем технологий, что был в 1960-е).&lt;div class=tm-iframe_temp data-src=https://embedd.srv.habr.com/iframe/6aa41582965b561e9f39c144 data-style id=6aa41582965b561e9f39c144 width data-habr-games&gt;&lt;/div&gt;&lt;p&gt;Под яркими образами крылась невидимая история великого технологического прорыва через соединение множества отраслевых открытий.&lt;p&gt;Ракета сама по себе не могла доставить человека на Луну. Нужна была навигация. Нужны были системы связи. Нужны были вычисления. Нужна была электроника, достаточно надежная для полета и достаточно компактная, чтобы ее можно было поднять вместе с экипажем. Нужны были материалы, способные выдерживать экстремальные температуры. Нужны были программы, рассчитывающие траектории и обрабатывающие данные. Наконец, нужна была совершенно новая культура управления сложностью, поскольку ни один человек не мог понимать работу всей системы целиком.&lt;p&gt;Особенно интересна история Apollo Guidance Computer — бортового компьютера, разработанного для управления полетом. С точки зрения современного пользователя, его характеристики выглядят сегодня совершенно неправдоподобно — тактовая частота: около 0,043 МГц, производительность: около 12 250 операций с плавающей запятой в секунду, оперативная память (RAM): 4 КБ.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/6e5/c74/83c/6e5c7483cd1a2076a66d971c76763249.png alt=&#34;Гугл сообщает, что iPhone 18 мощнее Apollo Guidance Computer в 100 миллионов раз (в СТО МИЛЛИОНОВ РАЗ, Карл). А как там дела с лунной базой? Выбранный человечеством путь развития виден очень наглядно&#34; title=&#34;Гугл сообщает, что iPhone 18 мощнее Apollo Guidance Computer в 100 миллионов раз (в СТО МИЛЛИОНОВ РАЗ, Карл). А как там дела с лунной базой? Выбранный человечеством путь развития виден очень наглядно&#34; width=2000 height=1313 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/6e5/c74/83c/6e5c7483cd1a2076a66d971c76763249.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/6e5/c74/83c/6e5c7483cd1a2076a66d971c76763249.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Гугл сообщает, что iPhone 18 мощнее Apollo Guidance Computer в 100 миллионов раз (в СТО МИЛЛИОНОВ РАЗ, Карл). А как там &lt;a href=https://habr.com/ru/companies/ddosguard/articles/1072192/&gt;дела с лунной базой&lt;/a&gt;? Выбранный человечеством путь развития виден очень наглядно&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Но дело, конечно, не в характеристиках. Самое важное то, что компьютер перестал быть исключительно огромной машиной, стоящей в вычислительном центре, и оказался связан с физическим объектом, который движется в пространстве и должен принимать решения на основании данных, поступающих в реальном времени.&lt;p&gt;Еще большим вызовом стало создание ПО, способного работать в условиях ограниченных ресурсов и одновременно выполнять задачи, связанные с реальным полетом. Здесь появлялись проблемы приоритетов, ошибок, обработки нескольких процессов и управления вычислительным временем.&lt;p&gt;Известная история «тревог» компьютера во время посадки Apollo 11 хорошо показывает, насколько важным стало программное обеспечение. Когда система оказалась перегружена, ей пришлось фактически отказаться от менее важных задач и сосредоточиться на критических процессах, связанных с управлением посадкой (сегодня бы пиарщики рассказали нам, что «разумный ИИ-агент» бортового компьютера «сам спас миссию», и все бы поверили).&lt;p&gt;Ладно, вернемся из космоса на Землю и навострим уши. Пока инженеры отправляли людей на Луну и соединяли компьютеры в сети, в лабораториях и студиях звукозаписи происходило нечто менее масштабное, но не менее важное. &lt;p&gt;Звук теперь можно было не только записывать — но и &lt;strong&gt;конструировать&lt;/strong&gt;, и музыкальная студия впервые начала превращаться в самостоятельный инструмент (опять же, в эпоху Suno, когда можно просто помычать в микрофон, и нейронка сделает тебе трек с супер-продакшеном, не худо вспомнить, с чего все начиналось). &lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/1c5/7de/a8f/1c57dea8f8bd779d4da6100e9e39e8d4.png width=1370 height=517 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/1c5/7de/a8f/1c57dea8f8bd779d4da6100e9e39e8d4.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/1c5/7de/a8f/1c57dea8f8bd779d4da6100e9e39e8d4.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;h3&gt;От луча, который «ничего не мог», к звуку, который можно собрать&lt;/h3&gt;&lt;p&gt;Буквально секундное контекстуальное отступление. Прежде чем перейдем от космических машин к музыкальным студиям, стоит задержаться на одном изобретении, которое особенно хорошо передает атмосферу шестидесятых. В 1960 году появился первый работающий лазер — устройство, основанное на идеях, которые еще недавно казались почти чистой физикой. Когерентный луч света, чрезвычайно точно направленный и обладающий необычными свойствами, первоначально выглядел прежде всего как лабораторная диковинка. &lt;p&gt;Вокруг этого лазера почти сразу возник знаменитый вопрос: хорошо, мы его открыли, а зачем он вообще нужен? Сегодня этот вопрос кажется забавным только потому, что мы знаем ответ. Лазеры оказались повсюду: от промышленности и медицины до оптической связи, измерительных систем, бытовой электроники и научных приборов. &lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/0c1/dc7/929/0c1dc79291e674ff34df5404631a323e.png width=1200 height=900 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/0c1/dc7/929/0c1dc79291e674ff34df5404631a323e.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/0c1/dc7/929/0c1dc79291e674ff34df5404631a323e.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Но в начале шестидесятых лазер стал хрестоматийным примером технологии, которая существовала раньше собственного массового применения. Сначала появился новый физический принцип и машина, способная его реализовать, а уже потом человечество начало лихорадочно придумывать, что с этим делать.&lt;p&gt;Шестидесятые, как я уже, наверное, сказал несколько раз, вообще были временем технологий, которые еще не успели стать товарами. Их создатели открывали новые возможности, зачастую не зная, какой именно элемент будущей инфраструктуры из этого получится через двадцать или сорок лет. Именно так развивались и ранние компьютерные сети, и микроэлектроника, и многие космические технологии. &lt;p&gt;Нигде превращение технической возможности в совершенно новую культурную форму не происходило настолько быстро и эффектно, как в звукозаписи.&lt;div class=tm-iframe_temp data-src=https://embedd.srv.habr.com/iframe/6aa4170b9830c71e6f6397e4 data-style id=6aa4170b9830c71e6f6397e4 width data-habr-games&gt;&lt;/div&gt;&lt;p&gt;Именно в 60-х студия перестала быть прозрачным посредником между музыкантом и слушателем. Она начала вмешиваться в музыку настолько глубоко, что сама стала частью творческого процесса. Звук можно было разделять на отдельные элементы, записывать их независимо друг от друга, изменять, соединять, накладывать, ускорять, замедлять и помещать в искусственно созданное акустическое пространство.&lt;p&gt;Причем тут лазер: история, в общем, оказалась похожа. Музыкальная индустрия сначала получила новые технические инструменты, а затем обнаружила, что вместе с ними изменилась сама природа музыкального произведения.&lt;p&gt;Ключевую роль здесь сыграло развитие многодорожечной записи. Вместо того, чтобы записывать все инструменты одновременно и получать единую звуковую картину, инженеры и музыканты получили возможность работать с отдельными дорожками. Вокал мог существовать отдельно от ударных, гитара — отдельно от баса, а дополнительные партии можно было записать позже.&lt;p&gt;Теперь звукозапись можно было собирать постепенно, почти как инженерный проект. Музыканты записывали базовую структуру композиции, затем добавляли новые слои, возвращались к уже готовому материалу и меняли отдельные элементы. Ошибка в одной партии больше не обязательно требовала заново исполнять всю композицию. Более того, появлялась возможность создавать музыкальные конструкции, которые вообще невозможно было бы одновременно исполнить в одном помещении.&lt;p&gt;Эта логика особенно хорошо видна на примере музыкальной революции второй половины шестидесятых (пожалуй, пионером новой оркестровки стал Брайан Уилсон из Beach Boys, у которого довольно быстро технические новинки подрезали Pink Floyd и The Beatles). &lt;p&gt;К 1967-69 годам уже буквально каждая пластинка стремилась переплюнуть конкурентов по части эксперимента (в чем, вероятно, сыграли свою роль также запрещенные вещества, но мы это осуждаем и обсуждать не будем)&lt;div class=tm-iframe_temp data-src=https://embedd.srv.habr.com/iframe/6aa41755c926e00df3309bb6 data-style id=6aa41755c926e00df3309bb6 width data-habr-games&gt;&lt;/div&gt;&lt;p&gt;В массы новый звук пошел во многом благодаря тому, что эпицентром авангарда оказалось британское государственное телевидение, BBC. Его звукорежиссерское подразделение, основанная в 1958 году лаборатория BBC Radiophonic Workshop, изначально занималось созданием необычных звуков для радио и телевидения, причем его сотрудники изначально работали не столько с готовыми музыкальными инструментами, сколько с самой природой звука. &lt;p&gt;Магнитная пленка, генераторы, осциллографы, фильтры, электронные схемы и монтаж позволяли записывать отдельные фрагменты, изменять их скорость и высоту, накладывать друг на друга, воспроизводить задом наперед и собирать из них совершенно новые звуковые объекты. Особенно важным здесь было отсутствие необходимости начинать с традиционного музыкального инструмента: источником звука мог стать практически любой физический процесс, а затем студия превращала его в материал для композиции.&lt;div class=tm-iframe_temp data-src=https://embedd.srv.habr.com/iframe/6aa4175f9830c71e6f6397fc data-style id=6aa4175f9830c71e6f6397fc width data-habr-games&gt;&lt;/div&gt;&lt;p&gt;В шестидесятые эта лаборатория (занятно, что ее самыми активными сотрудниками-экспериментаторами были женщины — необычно даже по тем временам) стала одним из мест, где электронный звук постепенно переставал восприниматься как технический эффект и превращался в самостоятельный художественный материал. &lt;p&gt;Одним из самых известных примеров стала работа Делии Дербишир над темой Doctor Who: исходный материал был собран из отдельных электронных сигналов и звуков, записанных на пленку, после чего подвергался монтажу и обработке. &lt;div class=tm-iframe_temp data-src=https://embedd.srv.habr.com/iframe/6aa417679830c71e6f639802 data-style id=6aa417679830c71e6f639802 width data-habr-games&gt;&lt;/div&gt;&lt;p&gt;Параллельно развивалась и еще более радикальная идея. Если звук можно технически обрабатывать, то почему бы не создавать его искусственно с самого начала? Хотя формально в качестве эзотерических экспериментов электронная музыка существовала и раньше, в шестидесятые синтезаторы начали постепенно выходить из лабораторий и экспериментальных студий. Особую роль сыграли разработки Роберта Муга. Его синтезаторы позволяли относительно гибко работать с электронным звуком и постепенно делали его доступнее для музыкантов.&lt;div class=tm-iframe_temp data-src=https://embedd.srv.habr.com/iframe/6aa4176ec926e00df3309bcc data-style id=6aa4176ec926e00df3309bcc width data-habr-games&gt;&lt;/div&gt;&lt;p&gt;(проигрыш в начале трека «Strange Days» (1967) группы The Doors стал, кажется, первым применением синтезатора Moog в суперпопулярной музыке)&lt;h3&gt;Придумали все полимеры&lt;/h3&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/948/cec/cbd/948ceccbda2a94e0d4769d8fdba6c048.png alt=&#34;«Всего одно слово — ПЛАСТИК. За этим будущее» (реклама, 1960)&#34; title=&#34;«Всего одно слово — ПЛАСТИК. За этим будущее» (реклама, 1960)&#34; width=526 height=678 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/948/cec/cbd/948ceccbda2a94e0d4769d8fdba6c048.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/948/cec/cbd/948ceccbda2a94e0d4769d8fdba6c048.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;«Всего одно слово — ПЛАСТИК. За этим будущее» (реклама, 1960)&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;По стремительному развитию химии в 1960-х особенно хорошо видно, как фундаментальная наука постепенно превращалась в инфраструктуру будущего. Новые представления о строении вещества, реакции и свойствах молекул все активнее выходили из лабораторий в промышленность, электронику, медицину и производство материалов.&lt;p&gt;Одним из наиболее важных процессов стала стремительная экспансия полимеров. Материалы, которые еще несколькими десятилетиями ранее воспринимались как сравнительно узкая область химической промышленности, постепенно меняли повседневный материальный мир. Пластики становились легче, дешевле и разнообразнее, а их свойства можно было в известном смысле проектировать. &lt;p&gt;Как и в случае с синтезом звука, инженер все меньше был ограничен тем, что уже существует в природе. Если древесина, металл или камень обладают заранее данными свойствами, то синтетический материал позволяет все активнее ставить вопрос иначе: какие именно свойства нам нужны и какое вещество можно для этого создать?&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/9ba/287/7f4/9ba2877f4a82a39e90e8acfed7fb9b29.png alt=&#34;Ноу-хау десятилетия — пластиковая пленка для сэндвичей! Реклама, декабрь 1960&#34; title=&#34;Ноу-хау десятилетия — пластиковая пленка для сэндвичей! Реклама, декабрь 1960&#34; width=1000 height=1345 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/9ba/287/7f4/9ba2877f4a82a39e90e8acfed7fb9b29.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/9ba/287/7f4/9ba2877f4a82a39e90e8acfed7fb9b29.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Ноу-хау десятилетия — пластиковая пленка для сэндвичей! Реклама, декабрь 1960&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Именно пластмасса, то есть, буквально, &lt;em&gt;пластичное вещество&lt;/em&gt;, повинующееся задумке производителя, лучше всего передает не только технологический, но и психологический сдвиг эпохи. Материал, который можно относительно свободно формовать, окрашивать, делать твердым или гибким, прозрачным или непрозрачным, легким или прочным, оказался почти идеальной метафорой нового технологического воображения. Мир начал становиться чем-то, что можно проектировать, преобразовывать и собирать заново.&lt;p&gt;В шестидесятых свобода существовала не только в искусстве или политике (о которых мы в этой статье не скажем ни слова) — она проникала в сам язык техники. Любая граница начинала восприниматься как временное ограничение, которое можно обойти с помощью новой машины, нового материала или новой системы организации.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/17f/92e/f40/17f92ef4085139789dc2bfad03586691.png width=1400 height=657 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/17f/92e/f40/17f92ef4085139789dc2bfad03586691.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/17f/92e/f40/17f92ef4085139789dc2bfad03586691.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Само вещество, сама материя постепенно становилась предметом инженерного проектирования. Из этого движения вырастут и новые композиты, и полупроводниковая промышленность, и современная фармацевтика, и значительная часть материаловедения второй половины XX века.&lt;p&gt;Не менее важной была химическая революция в медицине. В шестидесятые фармакология все заметнее переходила от поиска веществ с общим терапевтическим эффектом к попыткам воздействовать на конкретные биохимические механизмы. Особенно интересной стала история серотонина — именно к концу этого десятилетия появились новые данные, связывавшие работу этой нейромедиаторной системы с депрессией. Одновременно исследования трициклических антидепрессантов показали, что изменение концентрации нейромедиаторов в синапсе может быть связано с терапевтическим эффектом. &lt;p&gt;Из этого постепенно возникла идея спроектировать молекулу, которая будет избирательно вмешиваться в определенный процесс. Именно здесь начинается история будущих СИОЗС — селективных ингибиторов обратного захвата серотонина. &lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/85f/d72/2db/85fd722dbd6cf2caa48210bcc7904fb2.png width=640 height=854 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/85f/d72/2db/85fd722dbd6cf2caa48210bcc7904fb2.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/85f/d72/2db/85fd722dbd6cf2caa48210bcc7904fb2.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Первые такие соединения появились уже после шестидесятых: флуоксетин был синтезирован и исследован в начале 1970-х, а первым СИОЗС, вышедшим на рынок, стал зимелидин, разработанный в 1971 году и выпущенный в Швеции в 1982-м. Флуоксетин, который позднее стал известен как Prozac, получил одобрение FDA в 1987 году. &lt;p&gt;Но интеллектуальная основа этой истории формировалась именно в 60-х: фармакология все увереннее смотрела на психику как на систему молекулярных процессов, которые можно исследовать, измерять и постепенно перенастраивать. &lt;h3&gt;Жить внутри новых структур&lt;/h3&gt;&lt;p&gt;Если пластмасса была одним из материальных символов шестидесятых, то Ричард Бакминстер Фуллер стал, пожалуй, одним из наиболее ярких символов их инженерного воображения. Он занимался архитектурой, дизайном, инженерией и системным мышлением, но все эти определения плохо описывают масштаб замыслов этого человека. &lt;p&gt;Фуллера интересовал гораздо более широкий вопрос: можно ли рассматривать всю человеческую среду как систему, которую можно перепроектировать?&lt;p&gt;(Ремарка в сторону: можно, наверное, назвать итогом шестидесятнических экспериментов в области функционального дизайна то, что было показано на &lt;a href=https://habr.com/ru/companies/ddosguard/articles/1060846/&gt;легендарной выставке OSAKA EXPO 70&lt;/a&gt;)&lt;p&gt;Фуллер исходил из вполне инженерного наблюдения: человечество постоянно создает все более сложные технические системы, но при этом зачастую продолжает организовывать города, ресурсы и производство так, словно технологическая революция еще не произошла. Мы умеем рассчитывать конструкцию самолета с огромной точностью, но не всегда способны столь же системно посмотреть на собственную цивилизацию. Мы оптимизируем отдельные машины, но редко пытаемся рассматривать всю совокупность человеческих потребностей как единую инженерную задачу.&lt;p&gt;В эпоху космических программ, компьютерных сетей и молекулярной биологии Фуллер предложил сделать еще один шаг и распространить инженерное мышление на масштаб планеты.&lt;p&gt;Его знаменитый образ Spaceship Earth, «космического корабля Земля», хорошо передает эту идею. Земля в таком представлении — ограниченная система с определенным набором ресурсов, потоками энергии и взаимосвязанными процессами. Человечество оказывается экипажем, который уже получил в распоряжение чрезвычайно сложный корабль, но до сих пор не особенно хорошо разобрался с инструкцией по эксплуатации.&lt;p&gt;Космическая эпоха впервые позволила увидеть Землю как единый физический объект, а Фуллер попытался собрать эти процессы в единую интеллектуальную картину.&lt;p&gt;Самым известным техническим символом его трудов стали геодезические конструкции. Геодезический купол выглядел буквально как архитектурная версия принципов, которые в шестидесятые возникали во множестве других областей: сложная система может быть чрезвычайно устойчивой благодаря улучшению организации отношений между элементами. За этим стояла гораздо более широкая идея, которую Фуллер называл ephemeralization: способность технологии делать все больше, используя все меньше ресурсов. &lt;p&gt;Вот бы сейчас, в &lt;a href=https://habr.com/ru/companies/ddosguard/articles/1034342/&gt;эпоху глобального потепления&lt;/a&gt;, кто-то открыл для себя заново эти принципы.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/68e/091/2bd/68e0912bdded2a8358501c8f70cd43b6.png alt=&#34;Геодезический купол Бакминстера Фуллера, спроектированный для выставки «Экспо-67» в Монреале, сегодня служит «Биосферой» — культурно-просветительским центром, посвященным вопросам окружающей среды.&#34; title=&#34;Геодезический купол Бакминстера Фуллера, спроектированный для выставки «Экспо-67» в Монреале, сегодня служит «Биосферой» — культурно-просветительским центром, посвященным вопросам окружающей среды.&#34; width=1776 height=1332 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/68e/091/2bd/68e0912bdded2a8358501c8f70cd43b6.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/68e/091/2bd/68e0912bdded2a8358501c8f70cd43b6.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Геодезический купол Бакминстера Фуллера, спроектированный для выставки «Экспо-67» в Монреале, сегодня служит «Биосферой» — культурно-просветительским центром, посвященным вопросам окружающей среды.&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Фуллер, которому в 60-х было уже немало лет (он родился в 1895 году), оказался удивительно созвучен с эпохой. Он хотел показать, что привычная организация человеческой среды не является естественной и неизбежной. Дом может быть спроектирован иначе. Город может быть устроен иначе. Ресурсы можно распределять иначе.&lt;p&gt;Сегодня мы, к сожалению, знаем, что не все так просто, и представление &lt;a href=https://habr.com/ru/companies/ddosguard/articles/1040158/&gt;общества как системы, которую можно рационально перепроектировать&lt;/a&gt;, способно быстро превратиться в разновидность инженерной утопии. С человеческим обществом все сложнее, чем с архитектурой или материаловедением: оно состоит не только из потоков ресурсов и инфраструктуры, но и из конфликтов, культур, традиций, интересов и исторических случайностей.&lt;p&gt;Наиболее значительным достижением 60-х было именно обращение к самому обществу и самому человеку, крупнейшее со времен эпохи Возрождения. Что такое язык? Что такое культура? Что такое искусство? Что такое наши институции (тюрьма, психушка, школа, больница), как на нас влияет печатное слово и телевизионная картинка? Как устроена, в конце концов, сама структура нашего знания и нашего познавания?&lt;p&gt;Для ответа на эти вопросы понадобились такие революционные мыслители 60-х, как Маршалл Маклюэн (которому я посвятил &lt;a href=https://habr.com/ru/companies/ddosguard/articles/980458/&gt;отдельную объемную статью&lt;/a&gt;) и Мишель Фуко. &lt;h3&gt;Структуры внутри новой жизни&lt;/h3&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/6f6/91a/aff/6f691aaff5c3b3e73f5b345212d24bb1.png alt=&#34;Коллективный научный сборник «Лингвистический поворот» (1967)&#34; title=&#34;Коллективный научный сборник «Лингвистический поворот» (1967)&#34; width=1154 height=1296 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/6f6/91a/aff/6f691aaff5c3b3e73f5b345212d24bb1.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/6f6/91a/aff/6f691aaff5c3b3e73f5b345212d24bb1.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Коллективный научный сборник «Лингвистический поворот» (1967)&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Параллельно с технической революцией происходила менее заметная, но не менее важная революция в гуманитарном знании — так называемый лингвистический поворот. Его истоки находились еще в первой половине XX века, прежде всего в работах Фердинанда де Соссюра и развитии структурной лингвистики, однако именно в шестидесятые эта идея вышла далеко за пределы языкознания.&lt;p&gt;Язык стали рассматривать не как простой набор слов, которыми человек обозначает уже существующие вещи, а как систему отношений, в которой значение каждого элемента определяется его местом среди других элементов. Для исследователя это означало важный сдвиг: вместо того чтобы спрашивать, что означает отдельный символ, можно было искать правила, связывающие между собой целую систему символов.&lt;p&gt;На первый взгляд (особенно хабровский), структурализм кажется чисто гуманитарной историей, которая плохо сочетается с рассказом о ракетах, компьютерах и интегральных схемах. Но в контексте шестидесятых он выглядит совершенно иначе. Структурное мышление возникало в мире, который сам все больше становился миром систем. Инженеры учились работать со сложным комплексом взаимосвязанных элементов. Биологи обнаруживали механизмы кодирования и передачи наследственной информации. Лингвисты исследовали язык как систему отношений. Кибернетика предлагала модели обратной связи и управления.&lt;p&gt;Структурализм был гуманитарным выражением той же интеллектуальной эпохи.&lt;p&gt;Из лингвистики этот принцип быстро распространился на антропологию, философию, литературоведение и психоанализ. Структурализм стал особенно заметен во Франции в середине шестидесятых: его методы использовали Клод Леви-Стросс, Ролан Барт, Жак Лакан, Луи Альтюссер, а затем в этот широкий интеллектуальный контекст стали включать и Мишеля Фуко. &lt;p&gt;В 1966 году международный симпозиум в Балтиморе собрал вокруг проблемы структурализма целый ряд французских интеллектуалов, а к концу десятилетия он уже превратился из специального метода в одну из главных интеллектуальных мод того времени. Особенно показателен здесь Клод Леви-Стросс (доживший аж до 2009 года и почтенного возраста в 100 лет). Его антропологические исследования предлагали смотреть на мифы и культуры как на сложные системы, внутри которых действуют определенные правила организации. &lt;p&gt;Если раньше исследователь мог спрашивать, почему конкретное племя рассказывает именно этот миф, то структуралистский подход предлагал другой вопрос: какие отношения между элементами мифа делают его возможным? Какие противопоставления постоянно воспроизводятся в разных сюжетах? Каким образом человеческое мышление организует различия между природой и культурой, сырым и приготовленным, жизнью и смертью? Короче говоря, Леви-Стросса интересовала &lt;strong&gt;архитектура&lt;/strong&gt; отношений, благодаря которой разные мифы могут оказываться вариациями одних и тех же более глубоких структур.&lt;p&gt;Леви-Стросс начал формировать свою структурную антропологию задолго до шестидесятых: его «Элементарные структуры родства» вышли еще в 1949 году, а «Структурная антропология» — в 1958-м. Но именно шестидесятые сделали его фигурой европейского масштаба. В 1962 году появились сразу «Тотемизм» и «Первобытное мышление», а с 1964 года началась публикация четырехтомных «Мифологик», в которых Леви-Стросс пытался обнаружить за многообразием мифов и обычаев скрытую систему отношений. &lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/d89/148/140/d8914814054633660b7443dfd61edfce.png alt=&#34;Молодой Леви-Стросс в полевой экспедиции в Бразилии (1930-е)&#34; title=&#34;Молодой Леви-Стросс в полевой экспедиции в Бразилии (1930-е)&#34; width=780 height=520 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/d89/148/140/d8914814054633660b7443dfd61edfce.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/d89/148/140/d8914814054633660b7443dfd61edfce.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Молодой Леви-Стросс в полевой экспедиции в Бразилии (1930-е)&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Здесь гуманитарная мысль неожиданно сближалась с наукой и техникой той же эпохи: внимание постепенно перемещалось от отдельных объектов к отношениям между ними, от результата к системе, от поверхности к правилам, которые эту поверхность организуют. Именно поэтому структурализм так органично вписался в интеллектуальный климат десятилетия, когда компьютер, генетический код и теория информации одновременно приучали мыслить мир как систему элементов, связей и операций.&lt;p&gt;По моему мнению, интеллектуальной кульминацией этого поиска стал именно Мишель Фуко, который после последовательного изучения очень странных закономерностей формирования и функционирования привычных нам институций общества начал задавать неудобные вопросы уже не только о структурах языка или мифа, но и о &lt;strong&gt;самих системах, определяющих производство самого знания&lt;/strong&gt;.&lt;h3&gt;Как мы знаем, что мы что-то знаем?&lt;/h3&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/3e8/1df/fef/3e81dffefd3296e1de2aaee5626dc12e.png alt=&#34;Жан-Поль Сартр (слева), Жиль Делёз (на заднем плане) и Мишель Фуко, Париж, 1968&#34; title=&#34;Жан-Поль Сартр (слева), Жиль Делёз (на заднем плане) и Мишель Фуко, Париж, 1968&#34; width=1280 height=768 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/3e8/1df/fef/3e81dffefd3296e1de2aaee5626dc12e.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/3e8/1df/fef/3e81dffefd3296e1de2aaee5626dc12e.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Жан-Поль Сартр (слева), Жиль Делёз (на заднем плане) и Мишель Фуко, Париж, 1968&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Напомним, Маршалл Маклюэн впервые &lt;a href=https://habr.com/ru/companies/ddosguard/articles/980458/&gt;показал&lt;/a&gt; нам, что новое средство коммуникации меняет общество не только через то, что оно передает, но и через саму организацию восприятия. Телевидение, радио, печатный станок или электронные средства связи создают новые условия, внутри которых люди получают информацию, взаимодействуют друг с другом и формируют представление о реальности. &lt;p&gt;По Маклюэну, технология становится продолжением возможностей человека и одновременно перестраивает самого пользователя. После появления новой среды некоторые действия становятся настолько естественными, что мы перестаем замечать, насколько недавно они вообще были невозможны.&lt;p&gt;Если средство коммуникации способно перестраивать среду человеческого восприятия, то следующий — и очень шестидесятнический — вопрос становится неизбежным: а что вообще &lt;strong&gt;определяет границы&lt;/strong&gt; того, что человек способен увидеть, сказать и помыслить в конкретную эпоху?&lt;p&gt;И здесь как раз и появляется Мишель Фуко, у которого в 1960-е вышли две важнейшие работы: революционная «Слова и вещи. Археология гуманитарных наук» (1966) и совсем уже трудноосмыслимая «Археология знания» (1969).&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/1f2/2ba/302/1f22ba302fd4cb26ee46ce0df1ff2404.png width=1280 height=1280 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/1f2/2ba/302/1f22ba302fd4cb26ee46ce0df1ff2404.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/1f2/2ba/302/1f22ba302fd4cb26ee46ce0df1ff2404.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;В «Словах и вещах» Фуко поставил довольно радикальный даже по тем временам вопрос: &lt;strong&gt;почему в разные эпохи люди вообще видят перед собой разные объекты знания&lt;/strong&gt;? В эпоху Возрождения мир организуется одним способом, в классическую эпоху — другим, а в XIX веке возникает уже совершенно иная конфигурация, в которой появляются современные представления о языке, жизни, труде и самом человеке. Меняется сама система координат, в которой задаются вопросы.&lt;p&gt;Именно это Фуко называет «археологией знания». Археолог не пытается восстановить непрерывную цепь идей от прошлого к настоящему и не ищет первого появления современной истины. Он словно снимает слой за слоем историческую почву, чтобы &lt;strong&gt;обнаружить правила&lt;/strong&gt;, определявшие, что в данный момент можно было сказать, увидеть и признать знанием. В «Словах и вещах» эта работа приводит его к понятию эпистемы — исторически определенной конфигурации знания, которая существует глубже отдельных теорий и авторов. &lt;p&gt;В 1969 году «Археология знания» превращает эту интуицию уже в метод. Фуко интересовала не непрерывная линия, по которой человеческая мысль якобы постепенно движется от заблуждений к истине (сравните с непрерывной линией технического прогресса, которая якобы представляет собой череду следующих друг за другом изобретений), а моменты, когда меняются сами правила того, что считается знанием. &lt;p&gt;В «Археологии знания» он сознательно противопоставляет традиционной истории идей понятия разрыва, порога, границы и трансформации. Историка, по Фуко, должны интересовать прежде всего моменты, когда прежняя система высказываний перестает работать и возникает новая конфигурация возможного знания.&lt;p&gt;Фуко предлагал &lt;strong&gt;превратить такие разрывы в полноценный объект исследования&lt;/strong&gt;. История тогда состоит не из одной линии, которая постепенно развивается, а из нескольких рядов, пересекающихся и расходящихся в разных темпах; один порядок знания может смениться другим без единой плавной траектории, ведущей от прошлого к настоящему. При такой оптике сама идея исторического развития становится подозрительной, а вместо нее появляется внимание к переломам, смене систем и тем моментам, когда то, что прежде казалось естественным и само собой разумеющимся, внезапно перестает быть таковым.&lt;p&gt;Как, например, в шестидесятые, когда эти книги были написаны.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/e44/40d/01b/e4440d01b8ddba81882db059ce9e20ef.png width=1490 height=1175 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/e44/40d/01b/e4440d01b8ddba81882db059ce9e20ef.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/e44/40d/01b/e4440d01b8ddba81882db059ce9e20ef.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;h3&gt;Вместо послесловия&lt;/h3&gt;&lt;p&gt;В шестидесятые компьютерная наука, молекулярная биология, теория информации, новые медиа и структурализм по-разному делали примерно одно и то же: заставляли отступить от отдельных вещей и посмотреть на отношения, коды, структуры и правила их организации. Фуко проделывает подобную операцию с самим знанием. К концу шестидесятых речь уже идет не только о новых технологиях и новых научных объектах — меняется само представление о том, что значит что-либо знать.&lt;p&gt;Любое изобретение требует не только материальных компонентов. Оно требует возможности представить себе задачу. До того как построить спутник, нужно научиться мыслить Землю как объект, который можно увидеть извне и окружить искусственной инфраструктурой. До того как построить компьютерную сеть, необходимо представить информацию как нечто, что можно передавать между машинами независимо от конкретного физического носителя. До того как попытаться создать обучающуюся машину, нужно допустить, что процесс обучения вообще можно рассматривать как нечто формализуемое. Технологии создаются не только руками инженеров. &lt;p&gt;Они сначала должны стать мыслимыми.&lt;p&gt;Именно это имелось в виду под одним из самых известных лозунгов революционного мая 68 года в Париже — «Вся власть воображению!» (L&amp;#39;imagination au pouvoir).&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/b31/bc6/595/b31bc6595bd34a809fba84c8f3e086f6.png width=1024 height=1010 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/b31/bc6/595/b31bc6595bd34a809fba84c8f3e086f6.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/b31/bc6/595/b31bc6595bd34a809fba84c8f3e086f6.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <guid>https://habr.com/ru/companies/ddosguard/articles/1081406/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1081406</guid>
      <pubDate>Tue, 15 Sep 2026 05:23:54 +0000</pubDate>
    </item>
    <item>
      <title>[Перевод] Сжимаем флаги стран в 11 бит</title>
      <link>https://habr.com/ru/articles/1081924/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1081924</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/ea4/a19/885/ea4a19885740a3f3a25a35fbee6ef15f.png width=1191 height=606 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/ea4/a19/885/ea4a19885740a3f3a25a35fbee6ef15f.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/ea4/a19/885/ea4a19885740a3f3a25a35fbee6ef15f.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Недавно я посмотрел &lt;a href=&#34;https://www.youtube.com/watch?v=b8q7w-cRq0w&#34; rel=&#34;noopener nofollow&#34;&gt;интересное видео&lt;/a&gt; YouTube‑канала «Physics for the Birds». Это видео посвящено матрицам, но для объяснения некоторых операций с матрицами автор использовал флаги:&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/c1d/f29/68f/c1df2968f18b3b2d6748e6c3777ec327.png alt=&#34;Скриншот из видео&#34; title=&#34;Скриншот из видео&#34; width=2140 height=1182 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/c1d/f29/68f/c1df2968f18b3b2d6748e6c3777ec327.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/c1d/f29/68f/c1df2968f18b3b2d6748e6c3777ec327.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Скриншот из видео&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Он показал, как можно разбить флаги по осям, чтобы представить их в виде матриц и сэкономить место на диске. В конце этот пример был применён к реальным матрицам и вычислениям с ними.&lt;p&gt;Однако это видео дало мне вдохновение: часто флаги бывают очень простыми — несколько разноцветных полос, распространённые элементы наподобие звезды, полумесяца или креста. Если использовать какую‑нибудь схему кодирования, то удастся ли описать флаг Франции&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/8a5/0c4/c5d/8a50c4c5db348a747fca89b142185e2e.png width=330 height=220 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/8a5/0c4/c5d/8a50c4c5db348a747fca89b142185e2e.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/8a5/0c4/c5d/8a50c4c5db348a747fca89b142185e2e.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;в формате «три полосы: синяя, белая, красная», чтобы декодеру и рендереру достаточно было всего нескольких бит? Как может выглядеть такое кодирование?&lt;p&gt;Я решил создать нечто подобное.&lt;h2&gt;Требования&lt;/h2&gt;&lt;p&gt;Для начала я сформулировал критерии соответствия моей кодировки:&lt;ul&gt;&lt;li&gt;&lt;p&gt;Декодированный флаг должен быть «достаточно узнаваемым»&lt;ul&gt;&lt;li&gt;&lt;p&gt;Детали не обязаны быть &lt;em&gt;точными&lt;/em&gt;. Вполне допустимы небольшие отклонения и неточности; достаточно, чтобы человек посмотрел на результат и сказал: «О, да это же тот флаг!»&lt;li&gt;&lt;p&gt;В том числе это относится к и точному расположению и форме объектов.&lt;li&gt;&lt;p&gt;Это относится и к точности цвета. Я знаю, как сильно гордится Франция своим &lt;a href=https://www.theguardian.com/world/2021/nov/15/sacre-bleu-french-flag-changes-colour-but-no-one-notices rel=&#34;noopener nofollow&#34;&gt;новым оттенком синего&lt;/a&gt;, но при кодировании мы можем свести любой оттенок синего к какому‑нибудь среднему синему&lt;/ul&gt;&lt;/ul&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Только флаги стран&lt;ul&gt;&lt;li&gt;&lt;p&gt;Никаких флагов штатов, городов и других вексиллологических дизайнов&lt;/ul&gt;&lt;/ul&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Без Непала&lt;ul&gt;&lt;li&gt;&lt;p&gt;Прости, Непал, мне нравится твой непрямоугольный флаг, но он бы всё усложнил&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/963/8f7/07e/9638f707e932afed18c1fd3f68ea7ec5.png width=500 height=610 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/963/8f7/07e/9638f707e932afed18c1fd3f68ea7ec5.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/963/8f7/07e/9638f707e932afed18c1fd3f68ea7ec5.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;/ul&gt;&lt;/ul&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Простые флаги должны занимать очень мало бит, а сложным дозволяется занимать больше, так что никакой фиксированной длины&lt;/ul&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Без гербов&lt;ul&gt;&lt;li&gt;&lt;p&gt;На флагах наподобие флага Андорры содержатся их гербы, для рендеринга которых потребовались бы растровые/векторные изображения. Из‑за этого пришлось бы встроить в наш крошечный формат SVG или что‑то подобное, так что нет&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/c8b/80c/12a/c8b80c12add93dc00ffaab38d3c1f8ef.png width=500 height=350 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/c8b/80c/12a/c8b80c12add93dc00ffaab38d3c1f8ef.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/c8b/80c/12a/c8b80c12add93dc00ffaab38d3c1f8ef.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;/ul&gt;&lt;/ul&gt;&lt;h2&gt;Что делает флаг флагом?&lt;/h2&gt;&lt;p&gt;Для начала мне нужно было разобраться, из каких элементов состоит флаг. В этом мне очень помог &lt;a href=https://www.worldometers.info/geography/flags-of-the-world/ rel=&#34;noopener nofollow&#34;&gt;список флагов всех стран Worldometers&lt;/a&gt;.&lt;p&gt;Во‑первых, выяснилось, что флаги разнообразнее, чем я думал. Да, есть простые полосатые флаги, например, у Франции, Италии или Германии. Также есть дизайны наподобие флагов Бурунди или Боснии со звёздами, отдельными частями и тому подобным. На флагах Северной Македонии и Сейшельских Островов есть радиальные полосы. У Чехии, Багамских Островов и других стран на флагах есть треугольники слева. И таких различий ещё много.&lt;p&gt;Так что же общего у большинства флагов? Насколько я понял:&lt;ol&gt;&lt;li&gt;&lt;p&gt;Полосы&lt;ol&gt;&lt;li&gt;&lt;p&gt;Полосы, много полос: горизонтальные, вертикальные, две полосы, три полосы, тринадцать полос, полосы разной ширины, полосы под углом&lt;/ol&gt;&lt;li&gt;&lt;p&gt;Стандартные фигуры&lt;ol&gt;&lt;li&gt;&lt;p&gt;Звёзды, полумесяцы, круги&lt;li&gt;&lt;p&gt;Разных размеров и в разных местах, одна фигура или несколько&lt;/ol&gt;&lt;li&gt;&lt;p&gt;Треугольник слева&lt;ol&gt;&lt;li&gt;&lt;p&gt;На удивление распространён и представлен в разных цветах, но часто имеет одинаковую общую форму, см. Коморы, Багамские Острова и Чехия&lt;/ol&gt;&lt;li&gt;&lt;p&gt;Цветной левый верхний угол&lt;ol&gt;&lt;li&gt;&lt;p&gt;Какой‑нибудь прямоугольник в левой верхней части: США, Греция&lt;/ol&gt;&lt;li&gt;&lt;p&gt;Флаг Великобритании&lt;ol&gt;&lt;li&gt;&lt;p&gt;Великобритания, Австралия, Новая Зеландия, Тувалу&lt;/ol&gt;&lt;li&gt;&lt;p&gt;И ещё есть страны Северной Европы&lt;ol&gt;&lt;li&gt;&lt;p&gt;Их добавление явно станет большим плюсом: Норвегия, Швеция, Дания, Финляндия&lt;/ol&gt;&lt;/ol&gt;&lt;h2&gt;Анализируем флаг&lt;/h2&gt;&lt;p&gt;Я решил, что протокол должен определять следующие параметры:&lt;ol&gt;&lt;li&gt;&lt;p&gt;Соотношение сторон&lt;ol&gt;&lt;li&gt;&lt;p&gt;У всех флагов соотношение сторон индивидуально, но присутствуют явные паттерны: примерно 45% флагов имеет соотношение 2:3, примерно 28% — 1:2, примерно 9% — 3:5, а дальше идёт длинный хвост иных форматов&lt;/ol&gt;&lt;li&gt;&lt;p&gt;Цветовая палитра&lt;ol&gt;&lt;li&gt;&lt;p&gt;Как уже говорилось, вместо копирования точного кода цвета мы ограничимся общими тонами, то есть цветовыми группами наподобие синего, зелёного и жёлтого&lt;li&gt;&lt;p&gt;Тут тоже наблюдается похожая картина: неожиданно много красного, потом по убыванию идут белый, синий, жёлтый/золотой, зелёный, чёрный и оранжевый с длинным хвостом других цветов&lt;/ol&gt;&lt;li&gt;&lt;p&gt;Слои&lt;ol&gt;&lt;li&gt;&lt;p&gt;Я думаю, логично задавать содержимое при помощи нескольких «слоёв» вместо того, чтобы пытаться закодировать все распространённые элементы по отдельности. Каждый слой сможет определять собственный список поддерживаемых опций&lt;li&gt;&lt;p&gt;Это должно работать как в Photoshop и других приложениях: например, во флаге США у нас сначала будет слой с полосами, затем слой для синего прямоугольника и на нём слой звёзд&lt;li&gt;&lt;p&gt;Самым общим слоем будет слой «Полосы». Он может иметь опции количества полос и их цветов, направления, чётного или нечётного распределения и повторяющихся паттернов&lt;li&gt;&lt;p&gt;Слой «Фигуры» должен содержать звёзды, полумесяцы и тому подобное, определяя позицию и поворот. Слои «Интервал» и «Область» позволят закрашивать конкретный прямоугольник&lt;/ol&gt;&lt;/ol&gt;&lt;h2&gt;Превращаем данные в биты&lt;/h2&gt;&lt;p&gt;Как и во многих других случаях, различные параметры флагов, похоже, следуют &lt;a href=&#34;https://www.youtube.com/watch?v=fCn8zs912OE&#34; rel=&#34;noopener nofollow&#34;&gt;закону Ципфа&lt;/a&gt; — соотношение сторон, цвета, элементы (полосы, звёзды и так далее). Чтобы распространённые элементы кодировались короче, я решил кодировать всё в отдельные &lt;a href=https://en.wikipedia.org/wiki/Huffman_coding rel=&#34;noopener nofollow&#34;&gt;деревья Хаффмана&lt;/a&gt;, присваивая распространённым случаям короткие двоичные коды.&lt;p&gt;Чтобы обеспечить возможность существования длинного хвоста без создания большого дерева, я решил отсекать значения, существующие только в одном флаге, и вместо них присваивать последнему листу дерева значение «Custom», за которым следует опция свободного значения с постоянной длиной. Например, это позволило сохранить соотношение сторон флага Сальвадора 189:335 без необходимости его записи в само дерево.&lt;p&gt;Вот пример полного дерева Хаффмана для соотношения сторон флага:&lt;pre&gt;&lt;code&gt;graph TD&#xA;    %% Внутренние узлы&#xA;    root((Root))&#xA;    n1(( ))&#xA;    n11(( ))&#xA;    n110(( ))&#xA;    n1101(( ))&#xA;    n11011(( ))&#xA;    n111(( ))&#xA;    n1110(( ))&#xA;    n11100(( ))&#xA;    n111001(( ))&#xA;    n11101(( ))&#xA;    n111010(( ))&#xA;    n111011(( ))&#xA;    n1111(( ))&#xA;    n11110(( ))&#xA;    n111100(( ))&#xA;    n111101(( ))&#xA;    n11111(( ))&#xA;    n111110(( ))&#xA;    n1111100(( ))&#xA;    n1111101(( ))&#xA;    n111111(( ))&#xA;    n1111110(( ))&#xA;    n1111111(( ))&#xA;    n11111111(( ))&#xA;&#xA;    %% Узлы листьев (соотношения)&#xA;    L_2_3[2:3]&#xA;    L_1_2[1:2]&#xA;    L_3_5[3:5]&#xA;    L_5_8[5:8]&#xA;    L_10_19[10:19]&#xA;    L_3_4[3:4]&#xA;    L_4_7[4:7]&#xA;    L_1_1[1:1]&#xA;    L_7_10[7:10]&#xA;    L_8_11[8:11]&#xA;    L_11_18[11:18]&#xA;    L_11_20[11:20]&#xA;    L_11_28[11:28]&#xA;    L_18_25[18:25]&#xA;    L_1_phi[1:φ]&#xA;    L_4_5[4:5]&#xA;    L_6_7[6:7]&#xA;    L_10_17[10:17]&#xA;    L_13_15[13:15]&#xA;    L_15_22[15:22]&#xA;    L_16_25[16:25]&#xA;    L_189_335[189:335]&#xA;    L_28_37[28:37]&#xA;    L_5_7[5:7]&#xA;    L_7_11[7:11]&#xA;    L_CUSTOM[CUSTOM]&#xA;&#xA;    %% Левая ветвь (0...)&#xA;    root -- 0 --&amp;gt; L_2_3&#xA;    root -- 1 --&amp;gt; n1&#xA;    &#xA;    n1 -- 0 --&amp;gt; L_1_2&#xA;    n1 -- 1 --&amp;gt; n11&#xA;    &#xA;    %% Ветвь 110... &#xA;    n11 -- 0 --&amp;gt; n110&#xA;    n110 -- 0 --&amp;gt; L_3_5&#xA;    n110 -- 1 --&amp;gt; n1101&#xA;    n1101 -- 0 --&amp;gt; L_5_8&#xA;    n1101 -- 1 --&amp;gt; n11011&#xA;    n11011 -- 0 --&amp;gt; L_10_19&#xA;    n11011 -- 1 --&amp;gt; L_3_4&#xA;&#xA;    %% Ветвь 111...&#xA;    n11 -- 1 --&amp;gt; n111&#xA;    n111 -- 0 --&amp;gt; n1110&#xA;    &#xA;    %% Подветви 1110...&#xA;    n1110 -- 0 --&amp;gt; n11100&#xA;    n11100 -- 0 --&amp;gt; L_4_7&#xA;    n11100 -- 1 --&amp;gt; n111001&#xA;    n111001 -- 0 --&amp;gt; L_1_1&#xA;    n111001 -- 1 --&amp;gt; L_7_10&#xA;    &#xA;    n1110 -- 1 --&amp;gt; n11101&#xA;    n11101 -- 0 --&amp;gt; n111010&#xA;    n111010 -- 0 --&amp;gt; L_8_11&#xA;    n111010 -- 1 --&amp;gt; L_11_18&#xA;    n111011 -- 0 --&amp;gt; L_11_20&#xA;    n111011 -- 1 --&amp;gt; L_11_28&#xA;    n11101 -- 1 --&amp;gt; n111011&#xA;&#xA;    %% Ветвь 1111...&#xA;    n111 -- 1 --&amp;gt; n1111&#xA;    n1111 -- 0 --&amp;gt; n11110&#xA;    &#xA;    %% Подветви 11110...&#xA;    n11110 -- 0 --&amp;gt; n111100&#xA;    n111100 -- 0 --&amp;gt; L_18_25&#xA;    n111100 -- 1 --&amp;gt; L_1_phi&#xA;    n1111001(( ))&#xA;    n11110 -- 1 --&amp;gt; n111101&#xA;    n111101 -- 0 --&amp;gt; L_4_5&#xA;    n111101 -- 1 --&amp;gt; L_6_7&#xA;&#xA;    %% Ветвь 11111...&#xA;    n1111 -- 1 --&amp;gt; n11111&#xA;    n11111 -- 0 --&amp;gt; n111110&#xA;    &#xA;    %% Подветви 111110...&#xA;    n111110 -- 0 --&amp;gt; n1111100&#xA;    n1111100 -- 0 --&amp;gt; L_10_17&#xA;    n1111100 -- 1 --&amp;gt; L_13_15&#xA;    n111110 -- 1 --&amp;gt; n1111101&#xA;    n1111101 -- 0 --&amp;gt; L_15_22&#xA;    n1111101 -- 1 --&amp;gt; L_16_25&#xA;&#xA;    %% Самая глубокая ветвь 111111...&#xA;    n11111 -- 1 --&amp;gt; n111111&#xA;    n111111 -- 0 --&amp;gt; n1111110&#xA;    n1111110 -- 0 --&amp;gt; L_189_335&#xA;    n1111110 -- 1 --&amp;gt; L_28_37&#xA;    &#xA;    n111111 -- 1 --&amp;gt; n1111111&#xA;    n1111111 -- 0 --&amp;gt; L_5_7&#xA;    n1111111 -- 1 --&amp;gt; n11111111&#xA;    n11111111 -- 0 --&amp;gt; L_7_11&#xA;    n11111111 -- 1 --&amp;gt; L_CUSTOM&#xA;&#xA;    %% Стилизация для удобства чтения&#xA;    classDef leaf fill:#e1f5fe,stroke:#0288d1,stroke-width:2px;&#xA;    classDef internal fill:#eceff1,stroke:#607d8b,stroke-width:1px;&#xA;    &#xA;    class L_2_3,L_1_2,L_3_5,L_5_8,L_10_19,L_3_4,L_4_7,L_1_1,L_7_10,L_8_11,L_11_18,L_11_20,L_11_28,L_18_25,L_1_phi,L_4_5,L_6_7,L_10_17,L_13_15,L_15_22,L_16_25,L_189_335,L_28_37,L_5_7,L_7_11,L_CUSTOM leaf;&#xA;    class root,n1,n11,n110,n1101,n11011,n111,n1110,n11100,n111001,n11101,n111010,n111011,n1111,n11110,n111100,n111101,n11111,n111110,n1111100,n1111101,n111111,n1111110,n1111111,n11111111 internal;&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:87px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;То есть для 45% флагов с соотношением сторон 2:3 достаточно присвоить первому биту значение 0.&lt;p&gt;Формат содержит деревья Хаффмана для:&lt;ul&gt;&lt;li&gt;&lt;p&gt;Соотношения сторон (самые распространённые: 2:3, 1:2, 3:5)&lt;ul&gt;&lt;li&gt;&lt;p&gt;Особое соотношение ширина : высота для длинного хвоста&lt;/ul&gt;&lt;/ul&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Размер цветовой палитры (самые распространённые: 3, 2, 4, 5)&lt;ul&gt;&lt;li&gt;&lt;p&gt;Особое значение «Count — 7» для длинного хвоста, потому что дерево уходит вверх на 7 уровней&lt;/ul&gt;&lt;/ul&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Цвет (самые распространённые: красный, белый, синий)&lt;ul&gt;&lt;li&gt;&lt;p&gt;Особые цвета можно задавать в виде компактной 10-битной RGB‑аппроксимации (RRR GGGG BBB)&lt;/ul&gt;&lt;/ul&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Количество слоёв&lt;/ul&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Тип слоя (полосы, фигура, области, пересечение, интервал, внутренний)&lt;/ul&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Ещё несколько специализированных поддеревьев&lt;ul&gt;&lt;li&gt;&lt;p&gt;например, количество точек на звезде, расположение фигуры&lt;/ul&gt;&lt;/ul&gt;&lt;p&gt;Практически весь формат представляет собой обход одного дерева Хаффмана за другим, которые определяют, из чего состоит флаг.&lt;p&gt;Определившись со всем этим, можно попробовать закодировать в формат наш первый флаг. Для этого я выбрал флаг Индонезии, потому что это явный победитель в кодировании, или «самый среднестатистический флаг».&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/a6d/e45/065/a6de4506509f15dfd7b12d60cab501e9.png width=500 height=334 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/a6d/e45/065/a6de4506509f15dfd7b12d60cab501e9.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/a6d/e45/065/a6de4506509f15dfd7b12d60cab501e9.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Соотношение сторон 2:3 (1 бит) → &lt;code&gt;0&lt;/code&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;самое распространённое соотношение сторон&lt;/ul&gt;&lt;/ul&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Палитра из двух цветов (2 бита) → &lt;code&gt;10&lt;/code&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Это единственный параметр, в котором мы немного теряем, потому что самое распространённое в дереве количество цветов — три&lt;/ul&gt;&lt;/ul&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Определяем 2 цвета: красный (2 бита) → &lt;code&gt;00&lt;/code&gt; и белый (2 бита) → &lt;code&gt;01&lt;/code&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Два самых распространённых цвета в дереве&lt;/ul&gt;&lt;/ul&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;1 слой (1 бит) → &lt;code&gt;0&lt;/code&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;вершина дерева&lt;/ul&gt;&lt;/ul&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Слой полос (1 бит) → &lt;code&gt;0&lt;/code&gt;, режим «равенство палитры» (то есть каждому цвету в палитре даётся одна одинаковая полоса, 1 бит) → &lt;code&gt;0&lt;/code&gt; и горизонтальные полосы (1 бит) → &lt;code&gt;0&lt;/code&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Всё это находится на вершинах соответствующих деревьев Хаффмана&lt;/ul&gt;&lt;/ul&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;→ Соединяем всё вместе: &lt;code&gt;0 10 00 01 0 0 0 0&lt;/code&gt;, или &lt;code&gt;QgA=&lt;/code&gt; в кодировке base64&lt;/ul&gt;&lt;p&gt;При использовании этого формата усреднённый флаг можно представить в 76 битах, а медиана составляет 55 бит.&lt;p&gt;Самый длинный — это флаг Катара, 420 бит: &lt;code&gt;#gHR1Y$?-+]m.0xS3F!0{.UH{uDppW5u2^+s|6~(p@GwHH&amp;lt;N:?57K99\(s)~!G4`!&lt;/code&gt;. Я бы сказал, что этот флаг едва умещается в нашу кодировку: сбоку у него есть зигзагообразный край, который я закодировал в виде 11 отдельных слоёв прямоугольников.&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/071/0c8/709/0710c8709e618a28f4e4c573a8811565.png width=500 height=197 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/071/0c8/709/0710c8709e618a28f4e4c573a8811565.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/071/0c8/709/0710c8709e618a28f4e4c573a8811565.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;h2&gt;Флаг Великобритании&lt;/h2&gt;&lt;p&gt;Можно сказать, что с ним я сжульничал. «Юнион Джек» сильно распространён, но его так сложно собирать из слоёв, что я просто сделал его встроенной в протокол фигурой. То есть он не собирается из частей, а для флага мы указываем «Слой „Юнион Джек“ в левом верхнем углу».&lt;h2&gt;Кодируем флаги&lt;/h2&gt;&lt;p&gt;Поначалу я использовал base64, чтобы просто превращать биты в сохраняемый текст. При 6 битах полезной нагрузки на один байт ASCII на усреднённое определение флага требуется 14 с медианой 12 символов. Самый короткий код флага — это &lt;code&gt;QgA=&lt;/code&gt; (Индонезия).&lt;p&gt;Однако для повышения эффективности кодирования я решил воспользоваться кодировкой Base94, которая задействует все видимые однобайтовые символы ASCII с «!» по «~».&lt;p&gt;Думал я и о кодировании на основе эмодзи, но поскольку для сохранения каждого символа всё равно понадобится больше 1 байта, уменьшившееся количество символов, вероятно, всё равно потребует суммарно больше бит. (А ещё я не хотел рисковать тем, что флаг страны окажется закодированным в «💩🤮👎» или нечто подобное…)&lt;p&gt;В результате мы уменьшили усреднённое значение до 12 символов на флаг с медианой 9 символов. Индонезия осталась самым коротким кодом: &lt;code&gt;&amp;lt;F&lt;/code&gt;.&lt;h2&gt;Рендеринг&lt;/h2&gt;&lt;p&gt;При помощи ChatGPT Codex я превратил этот формат в двухэтапную систему: кодировщик/декодер и SVG‑рендерер.&lt;p&gt;Декодер сначала превращает двоичный блоб в читаемый формат. Для уже рассмотренного нами флага Индонезии это выглядит так:&lt;pre&gt;&lt;code class=javascript&gt;{&#xA;  &amp;#34;aspectRatio&amp;#34;: {&#xA;    &amp;#34;kind&amp;#34;: &amp;#34;rational&amp;#34;,&#xA;    &amp;#34;height&amp;#34;: &amp;#34;2&amp;#34;,&#xA;    &amp;#34;width&amp;#34;: &amp;#34;3&amp;#34;&#xA;  },&#xA;  &amp;#34;palette&amp;#34;: [&#xA;    {&#xA;      &amp;#34;r&amp;#34;: 210,&#xA;      &amp;#34;g&amp;#34;: 16,&#xA;      &amp;#34;b&amp;#34;: 52&#xA;    },&#xA;    {&#xA;      &amp;#34;r&amp;#34;: 255,&#xA;      &amp;#34;g&amp;#34;: 255,&#xA;      &amp;#34;b&amp;#34;: 255&#xA;    }&#xA;  ],&#xA;  &amp;#34;layers&amp;#34;: [&#xA;    {&#xA;      &amp;#34;kind&amp;#34;: &amp;#34;stripes&amp;#34;,&#xA;      &amp;#34;direction&amp;#34;: &amp;#34;horizontal&amp;#34;,&#xA;      &amp;#34;stripes&amp;#34;: [&#xA;        {&#xA;          &amp;#34;color&amp;#34;: 0&#xA;        },&#xA;        {&#xA;          &amp;#34;color&amp;#34;: 1&#xA;        }&#xA;      ]&#xA;    }&#xA;  ]&#xA;}&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Затем рендерер превращает этот декодированный флаг в код SVG:&lt;pre&gt;&lt;code class=javascript&gt;&amp;lt;svg xmlns=&amp;#34;http://www.w3.org/2000/svg&amp;#34; viewBox=&amp;#34;0 0 1.5 1&amp;#34;&amp;gt;&#xA;&#x9;&amp;lt;rect x=&amp;#34;0&amp;#34; y=&amp;#34;0&amp;#34; width=&amp;#34;1.5&amp;#34; height=&amp;#34;0.5&amp;#34; fill=&amp;#34;#d21034&amp;#34;/&amp;gt;&#xA;&#x9;&amp;lt;rect x=&amp;#34;0&amp;#34; y=&amp;#34;0.5&amp;#34; width=&amp;#34;1.5&amp;#34; height=&amp;#34;0.5&amp;#34; fill=&amp;#34;#fff&amp;#34;/&amp;gt;&#xA;&amp;lt;/svg&amp;gt;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Код довольно красив, в нём есть отдельный класс и интерфейсы для закодированных двоичных частей и слоёв. Однако даже при tsc‑компиляции кодировщик/декодер занимает 27 КБ, а рендерер — 12,5 КБ; наверно, это перебор, учитывая такую сильную оптимизацию исходного формата…&lt;p&gt;Поэтому я снова обратился к Codex и создал альтернативный «мини‑декодер»: вместо двух отдельных движков он объединяет декодирование и рендеринг в один проход, избавившись от красивой инфраструктуры из интерфейсов и класса и сведя всё к маленьким примитивным функциям.&lt;p&gt;Благодаря этому мы получили 470-строчный файл TypeScript, после компиляции превратившийся в 5,29 КБ (2,66 КБ после сжатия gzip).&lt;h2&gt;Флаги, не поместившиеся в формат&lt;/h2&gt;&lt;p&gt;С разной степенью успешности мне удалось закодировать в этот довольно примитивный формат 128 флагов.&lt;p&gt;Но осталось 67 флагов, которые мне закодировать не удалось. Наиболее примечательные из них:&lt;ul&gt;&lt;li&gt;&lt;p&gt;Испании, Экваториальной Гвинеи, Андорры, Белиза, Брунея, Камбоджи, Коста‑Рики, Хорватии, Доминиканской Республики, Эквадора, Сальвадора, Фиджи, Гаити, Мексики, Молодовы, Черногории, Никарагуа, Омана, Парагвая, Португалии, Сан‑Марино, Сербии, Словакии, Словении, Венесуэлы&lt;ul&gt;&lt;li&gt;&lt;p&gt;Содержащие герб, символ или национальную эмблему (25)&lt;/ul&gt;&lt;/ul&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Анголы, Барбадоса, Эсватини, Гватемалы, Кении, Лесото, Лихтенштейна, Мальты, Мозамбика, Таджикистана, Ватикана&lt;ul&gt;&lt;li&gt;&lt;p&gt;С глифами‑объектами: оружием, инструментами, коронами, щитами или головным убором (11)&lt;/ul&gt;&lt;/ul&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Албании, Бутана, Доминики, Египта, Кирибати, Папуа — Новой Гвинеи, Шри‑Ланки, Уганды, Замбии, Зимбабве&lt;ul&gt;&lt;li&gt;&lt;p&gt;С глифами‑животными, в основном орлами и другими птицами (10)&lt;/ul&gt;&lt;/ul&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Канады, Кипра, Эритреи, Гренады, Ливана&lt;ul&gt;&lt;li&gt;&lt;p&gt;С растительными глифами: листьями, ветвями или мускатным орехом (5)&lt;/ul&gt;&lt;/ul&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Антигуа и Барбуды, Бразилии, Непала, ЮАР, Вануату&lt;ul&gt;&lt;li&gt;&lt;p&gt;С геометрией, которую не может выразить модель слоёв, например, Y‑образной, V‑образной, завитком или непрямоугольным контуром (5)&lt;/ul&gt;&lt;/ul&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Афганистана, Ирана, Ирака, Саудовской Аравии&lt;ul&gt;&lt;li&gt;&lt;p&gt;С текстом или каллиграфией на арабском (4)&lt;li&gt;&lt;p&gt;Здесь бы могло помочь добавление специального слоя текста&lt;/ul&gt;&lt;/ul&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Индии, Кыргызстана, Монголии, Республики Корея&lt;ul&gt;&lt;li&gt;&lt;p&gt;С религиозными или культурными символами: колесом Ашоки, юрты‑тюндюка, соёмбо или тхэгыкки (4)&lt;/ul&gt;&lt;/ul&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Беларуси, Казахстана, Туркменистана&lt;ul&gt;&lt;li&gt;&lt;p&gt;С орнаментальными узорами по краю&lt;/ul&gt;&lt;/ul&gt;&lt;p&gt;→ То есть практически все, содержащие особый глиф или текст, которые нелегко представить в виде геометрических слоёв&lt;h2&gt;Случайные флаги!&lt;/h2&gt;&lt;p&gt;Теперь, когда у нас есть структурированный язык описания флагов, я решил добавить рандомизатор, заполняющий новый флаг случайными элементами из имеющихся у нас деревьев Хаффмана.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/290/a8b/a9c/290a8ba9c15179bece2bacfa08997451.png width=692 height=432 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/290/a8b/a9c/290a8ba9c15179bece2bacfa08997451.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/290/a8b/a9c/290a8ba9c15179bece2bacfa08997451.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/204/c2e/e5d/204c2ee5d82456a559a612ac5523c614.png width=692 height=414 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/204/c2e/e5d/204c2ee5d82456a559a612ac5523c614.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/204/c2e/e5d/204c2ee5d82456a559a612ac5523c614.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/5d1/6d2/c91/5d16d2c91b26e433b31812ef8bb64411.png width=694 height=342 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/5d1/6d2/c91/5d16d2c91b26e433b31812ef8bb64411.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/5d1/6d2/c91/5d16d2c91b26e433b31812ef8bb64411.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/77d/397/d53/77d397d53d140afd6c02a6a79dc8d8bf.png width=698 height=416 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/77d/397/d53/77d397d53d140afd6c02a6a79dc8d8bf.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/77d/397/d53/77d397d53d140afd6c02a6a79dc8d8bf.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/95b/9a3/e91/95b9a3e918715763993024f739388433.png width=690 height=410 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/95b/9a3/e91/95b9a3e918715763993024f739388433.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/95b/9a3/e91/95b9a3e918715763993024f739388433.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/a21/d0b/f3f/a21d0bf3f762ba21ffb59b46b8ab7df5.png width=698 height=412 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/a21/d0b/f3f/a21d0bf3f762ba21ffb59b46b8ab7df5.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/a21/d0b/f3f/a21d0bf3f762ba21ffb59b46b8ab7df5.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Да, у какой‑нибудь страны вполне мог бы быть подобный флаг!&lt;h2&gt;Подведём итог&lt;/h2&gt;&lt;p&gt;Я уверен, что кто‑то ещё сможет найти более экономные или совершенно иные способы сжатия или структурирования данных. Однако мне всё равно было очень интересно взять полный список флагов, искать в них общие элементы и находить способы упаковки в биты максимального объёма данных. Здорово было и наконец‑то воспользоваться своими знаниями кода Хаффмана, полученными в рамках бакалавриата, а также писать код на битовом уровне.&lt;p&gt;Готовую страницу со всеми флагами можно найти здесь: &lt;a href=https://vantezzen.github.io/miniflags/ rel=&#34;noopener nofollow&#34;&gt;https://vantezzen.github.io/miniflags/&lt;/a&gt;, а исходный код — здесь: &lt;a href=https://github.com/vantezzen/miniflags rel=&#34;noopener nofollow&#34;&gt;https://github.com/vantezzen/miniflags&lt;/a&gt;. Не ожидайте увидеть там особо чистый код — в конце концов, по большей мере я его вайбкодил, исходя из своих мыслей о формате.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <author>PatientZero</author>
      <guid>https://habr.com/ru/articles/1081924/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1081924</guid>
      <pubDate>Tue, 15 Sep 2026 05:09:53 +0000</pubDate>
    </item>
    <item>
      <title>Сайт не открывается из России, хотя сервер в Москве</title>
      <link>https://habr.com/ru/articles/1082270/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082270</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;Вчера переехал на свой сервер. Сегодня утром обнаружил, что сайт у меня не открывается. Ни из дома, ни с телефона.&lt;p&gt;Сервер при этом в Москве, у российского хостера. И с ним всё в порядке.&lt;p&gt;Дальше про то, как я полдня искал причину, какие шесть версий проверил и выбросил, и чем всё кончилось. Если симптомы похожие, в конце готовая методика: что запускать и как читать результат.&lt;h3&gt;Симптомы&lt;/h3&gt;&lt;p&gt;Сайт статический, Next.js, отдаётся через nginx. Рядом два Docker‑контейнера: чат‑виджет и API для демок. Сервер 2 CPU, 4 ГБ, Москва.&lt;p&gt;Что я видел:&lt;ul&gt;&lt;li&gt;&lt;p&gt;через VPN сайт открывался, но через раз;&lt;li&gt;&lt;p&gt;без VPN не открывался вообще;&lt;li&gt;&lt;p&gt;с телефона по мобильному тоже нет;&lt;li&gt;&lt;p&gt;ping до сервера при этом шёл идеально.&lt;/ul&gt;&lt;p&gt;Последний пункт и сбивал с толку. Сеть до сервера есть. Пакеты ходят. А сайт не грузится.&lt;h3&gt;Первое, что я сделал не так&lt;/h3&gt;&lt;p&gt;Скажу сразу: час я потратил впустую. Гадал вместо того, чтобы мерить.&lt;p&gt;Первая версия была про VPN. Внешний адрес показывал Франкфурт, трафик до Москвы шёл через Германию, задержка 100 мс. Вроде логично: туннель теряет пакеты, отсюда и «через раз».&lt;pre&gt;&lt;code&gt;$ curl -s https://ipinfo.io/json&#xA;{&#xA;  &amp;#34;ip&amp;#34;: &amp;#34;203.0.113.10&amp;#34;,&#xA;  &amp;#34;city&amp;#34;: &amp;#34;Frankfurt am Main&amp;#34;,&#xA;  &amp;#34;country&amp;#34;: &amp;#34;DE&amp;#34;,&#xA;  &amp;#34;org&amp;#34;: &amp;#34;AS47447 23M GmbH&amp;#34;&#xA;}&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:87px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Версия красивая. И неверная. Без VPN сайт не открывался вообще, а этого она не объясняла.&lt;p&gt;Вывод, до которого я дошёл поздно: если у гипотезы остаётся симптом, который она не объясняет, гипотеза неверна. Не «почти», не «частично». Неверна.&lt;h3&gt;Как надо было: мерить серией&lt;/h3&gt;&lt;p&gt;Одиночная проверка бесполезна, когда отказ плавающий. Нужна серия. Вот с чего надо было начинать:&lt;pre&gt;&lt;code class=bash&gt;ok=0; fail=0&#xA;for i in $(seq 1 25); do&#xA;  c=$(curl -s -o /dev/null -m 10 -w &amp;#34;%{http_code}&amp;#34; https://titov-ai.ru/)&#xA;  [ &amp;#34;$c&amp;#34; = &amp;#34;200&amp;#34; ] &amp;amp;&amp;amp; ok=$((ok+1)) || fail=$((fail+1))&#xA;done&#xA;echo &amp;#34;ok=$ok fail=$fail&amp;#34;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Через VPN получилось &lt;code&gt;ok=18 fail=7&lt;/code&gt;. Двадцать восемь процентов запросов не проходит.&lt;p&gt;Дальше нужна контрольная группа. Без неё замер не значит ничего. Проверяем другие хосты через тот же канал:&lt;pre&gt;&lt;code&gt;google.com    → 15 из 15&#xA;timeweb.com   → 12 из 12   (сайт моего же хостера)&#xA;мой сервер    → 18 из 25&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Вот теперь есть факт. Канал живой, чужие сайты открываются, включая сайт того самого хостера. Не открывается конкретный адрес.&lt;h3&gt;Странность, которая всё запутала&lt;/h3&gt;&lt;p&gt;Разложил проверку по уровням и получил картину, которая противоречит сама себе:&lt;pre&gt;&lt;code&gt;ping (30 пакетов)        → 0% потерь&#xA;чистый TCP-коннект :443  → 20 из 20&#xA;чистый TCP-коннект :22   → 20 из 20&#xA;SSH-сессия               → работает&#xA;curl HTTPS               → 6 провалов из 20&#xA;curl HTTP :80            → 5 провалов из 15&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;TCP устанавливается всегда. SSH работает. А обычный HTTP‑запрос падает в трети случаев.&lt;p&gt;Отказ выглядел так:&lt;pre&gt;&lt;code&gt;curl: (28) Connection timed out after 10000 milliseconds&#xA;time_connect = 0.000000&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Ноль в &lt;code&gt;time_connect&lt;/code&gt; значит, что соединение не установилось вообще. SYN ушёл, ответа нет. Не разрыв на середине передачи. Молчание.&lt;h3&gt;Шесть версий, которые не подтвердились&lt;/h3&gt;&lt;p&gt;Все проверил и выбросил. Пишу целиком, потому что отрицательный результат тоже результат.&lt;p&gt;Авария на ноде хостера. В панели реально висело уведомление об аппаратном сбое. Отпала, когда я зашёл на сервер: &lt;code&gt;load average 0.00&lt;/code&gt;, память свободна, nginx с нулём перезапусков, контейнеры Up 10 часов. Сбоило бы железо, страдал бы и SSH. Не страдал.&lt;p&gt;Nginx или Docker не принимают соединения. Разумно, если бы падал только веб. Но чистый TCP‑коннект на 443 давал 20 из 20. При переполнении accept‑очереди он бы тоже падал.&lt;p&gt;MTU и фрагментация. Path MTU до сервера оказался 1300 вместо 1500, типичная подпись туннеля. Отпала на контрольном замере: до старого хостинга MTU был точно такой же, и там всё работало.&lt;p&gt;Сертификат. Отпала сразу. Нулевой &lt;code&gt;time_connect&lt;/code&gt; значит, что до TLS дело не доходит, а сертификат читается уже после установки соединения.&lt;p&gt;Отсутствие AAAA‑записи. У моей машины IPv6 вообще нет, &lt;code&gt;curl -6&lt;/code&gt; не находит адреса. Все замеры шли по IPv4, форсированный &lt;code&gt;curl -4&lt;/code&gt; давал те же потери.&lt;p&gt;IP в чёрных списках. Шесть DNSBL, включая Spamhaus и SpamCop. Все чистые.&lt;h3&gt;Что дало ответ&lt;/h3&gt;&lt;p&gt;Вот это надо было делать первым. Заходим на сервер и снимаем дамп трафика, пока снаружи бьём запросами:&lt;pre&gt;&lt;code class=bash&gt;# на сервере&#xA;systemd-run --unit=cap --collect \&#xA;  tcpdump -i any -nn -c 3000 -w /tmp/cap.pcap tcp port 443&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;systemd-run&lt;/code&gt; тут не для красоты. SSH‑сессия рвалась каждые пару минут, а так процесс переживает обрыв.&lt;p&gt;Снаружи запускаю двадцать пять запросов, три падают. Смотрим, что видел сервер в момент падения:&lt;pre&gt;&lt;code&gt;09:01:32.377365  In   IP 203.0.113.10 &amp;gt; сервер.443: Flags [S]&#xA;09:01:32.377468  Out  IP сервер.443 &amp;gt; 203.0.113.10: Flags [S.]&#xA;09:01:32.476632  In   IP 203.0.113.10 &amp;gt; сервер.443: Flags [.] ack&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;SYN пришёл. Сервер ответил SYN‑ACK через одну десятитысячную секунды. Рукопожатие завершилось. А curl на моей стороне в этот момент показывал таймаут.&lt;p&gt;Всего пришло 25 SYN на 25 запросов. Ни один не потерялся по пути туда. Сервер ответил на все. Терялся ответ, обратно.&lt;h3&gt;Развязка&lt;/h3&gt;&lt;p&gt;Оставалось проверить очевидное, до чего я дошёл последним: как ведёт себя сервер с реального канала, мимо VPN. В Windows это делается флагом &lt;code&gt;--interface&lt;/code&gt;:&lt;pre&gt;&lt;code class=bash&gt;# 192.168.1.50 это реальный интерфейс, не туннель&#xA;curl --interface 192.168.1.50 https://titov-ai.ru/&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Результат:&lt;pre&gt;&lt;code&gt;ping до сервера       → 6 из 6, 16 мс, 0% потерь&#xA;TCP на 443            → 0 из 10&#xA;TCP на 443 по IP      → 0 из 8&#xA;google.com            → 3 из 3&#xA;timeweb.com           → 3 из 3&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;ICMP проходит идеально. TCP на 80 и 443 не проходит ни разу. Другие сайты с того же интерфейса работают.&lt;p&gt;Это фильтрация. Не потери, не перегрузка. Избирательная блокировка TCP к конкретному адресу при живом ICMP.&lt;p&gt;Хостер подтвердил, когда я прислал замеры. Их проверка снаружи: порт открывается, &lt;code&gt;nc -zv&lt;/code&gt; отвечает &lt;code&gt;succeeded&lt;/code&gt;, но соединение рвётся на TLS‑хэндшейке. Curl виснет сразу после &lt;code&gt;Client hello&lt;/code&gt;, браузер даёт &lt;code&gt;ERR_CONNECTION_RESET&lt;/code&gt;. Изнутри их сети сайт при этом отдаёт &lt;code&gt;HTTP/2 200&lt;/code&gt;.&lt;p&gt;Ответ поддержки: «изменения в настройках фильтрации со стороны магистральных провайдеров, повлиять на это мы не можем».&lt;h3&gt;Почему именно мой адрес&lt;/h3&gt;&lt;p&gt;Вот тут самое интересное. Ради этого стоило копать.&lt;p&gt;Смотрим, кому принадлежит блок:&lt;pre&gt;&lt;code class=bash&gt;$ curl -s &amp;#34;https://stat.ripe.net/data/prefix-overview/data.json?resource=201.x.x.x&amp;#34; \&#xA;  | jq &amp;#39;.data.block.desc, .data.asns&amp;#39;&#xA;&#xA;&amp;#34;LACNIC (Status: ALLOCATED)&amp;#34;&#xA;[{&amp;#34;asn&amp;#34;: 9123, &amp;#34;holder&amp;#34;: &amp;#34;TimeWeb-AS JSC \&amp;#34;TIMEWEB\&amp;#34;&amp;#34;}]&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;LACNIC это регистратор Латинской Америки. Адрес российского хостера, физически стоящий в Москве, формально принадлежит латиноамериканскому диапазону &lt;code&gt;201.0.0.0/8&lt;/code&gt;.&lt;p&gt;Хостеры выкупают такие блоки легально. Свободных адресов в RIPE давно нет, а IPv4 нужны. Юридически всё чисто: в базе RIPE запись &lt;code&gt;TW-Cloud&lt;/code&gt;, страна RU, организация в Петербурге.&lt;p&gt;Но магистральные провайдеры фильтруют по спискам. И нетипичные для России диапазоны попадают под них целиком. Хватит того, что сосед по подсети когда‑то дал повод.&lt;p&gt;Попросил у хостера другой адрес. Выдали из семейства &lt;code&gt;200.x&lt;/code&gt;. Проверяю:&lt;pre&gt;&lt;code class=bash&gt;$ curl -s https://ipinfo.io/200.x.x.x/json&#xA;{&#xA;  &amp;#34;city&amp;#34;: &amp;#34;Ji Paraná&amp;#34;,&#xA;  &amp;#34;region&amp;#34;: &amp;#34;Rondônia&amp;#34;,&#xA;  &amp;#34;country&amp;#34;: &amp;#34;BR&amp;#34;&#xA;}&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Бразилия. Снова LACNIC. Те же грабли.&lt;h3&gt;Чем кончилось&lt;/h3&gt;&lt;p&gt;Переехал к другому хостеру. Перед оплатой проверил их диапазоны через &lt;a href=http://2ip.io/ru/as/ rel=&#34;noopener nofollow&#34;&gt;&lt;code&gt;2ip.io/ru/as/&lt;/code&gt;&lt;/a&gt;: RIPE, зарегистрированы на Россию. Потом проверил доступность их сетей со своего канала. Пять из пяти.&lt;p&gt;После переезда тот же замер:&lt;pre&gt;&lt;code&gt;до:    0 из 10     задержка 100 мс&#xA;после: 12 из 12    задержка 25 мс&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Перенос занял часа три. Всё было в Docker, так что механика простая: два архива с проектами, &lt;code&gt;.env&lt;/code&gt; с ключами, сертификат (он привязан к домену, перевыпускать не надо), статика сайта, DNS.&lt;h3&gt;Методика, если симптомы похожие&lt;/h3&gt;&lt;p&gt;По шагам. Первые три занимают пять минут и отсекают половину версий.&lt;p&gt;Мерьте серией, а не одиночным запросом. Плавающий отказ на одиночной проверке выглядит как случайность.&lt;p&gt;Берите контрольную группу. Другие хосты через тот же канал. Без этого замер ничего не доказывает.&lt;p&gt;Разложите по уровням: ICMP, чистый TCP, TLS, HTTP. Место, где ломается, сужает круг втрое. Ping не идёт, значит маршрут или хост лежит. Ping идёт, TCP нет, это фильтрация. TCP идёт, TLS рвётся, это фильтрация по SNI или DPI. Всё идёт, падает только HTTP, вот тогда смотрите свой сервер.&lt;p&gt;Проверьте обходной путь. Есть VPN, сравните с ним и без него. В Windows реальный интерфейс задаётся через &lt;code&gt;curl --interface&lt;/code&gt;, адреса смотреть в &lt;code&gt;ipconfig&lt;/code&gt;.&lt;p&gt;Снимайте tcpdump на сервере. Он отвечает на главный вопрос: доходят ли SYN и отвечает ли сервер. Остальное догадки.&lt;p&gt;Проверьте, чей диапазон. Одна команда:&lt;pre&gt;&lt;code class=bash&gt;curl -s &amp;#34;https://stat.ripe.net/data/prefix-overview/data.json?resource=ВАШ\\_IP&amp;#34; | jq .data.block&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Увидели LACNIC, AFRINIC или APNIC у российского хостера, причина скорее всего найдена. Просите адрес из RIPE.&lt;h3&gt;Что вынес&lt;/h3&gt;&lt;p&gt;Гипотеза, которая не объясняет все симптомы, неверна. Я трижды строил версию, объясняющую часть картины. Трижды она разваливалась о факт, который я решил считать неважным.&lt;p&gt;Дамп с сервера стоит часа гаданий. Полдня я перебирал версии снаружи, хотя SSH работал с самого начала. Одна команда tcpdump дала однозначный ответ там, где шесть гипотез дали шесть тупиков.&lt;p&gt;Хостер видит только свою сеть. Первая линия честно пропинговала сервер изнутри стойки, получила 0,68 мс и написала, что всё работает. Формально они правы. Сдвинулось, только когда я прислал таблицу: столько успешных, столько таймаутов, вот трассировка, вот дамп. С цифрами не поспоришь.&lt;p&gt;Спрашивайте про диапазон до оплаты. Одна строчка в чат поддержки, «из какого блока выдаётся IPv4», экономит день переезда. Я узнал это на своих ошибках. Вы теперь знаете заранее.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <author>Vadtop</author>
      <guid>https://habr.com/ru/articles/1082270/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082270</guid>
      <pubDate>Tue, 15 Sep 2026 05:01:40 +0000</pubDate>
    </item>
    <item>
      <title>Почему мы готовим шестиклассников к олимпиаде за 9 класс</title>
      <link>https://habr.com/ru/articles/1082268/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082268</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;Привет! Меня зовут Максим Иванков, я развиваю школы робототехники и программирования для детей уже 9 лет. Мне 30 лет, у меня трое детей, и последние десять лет я занимаюсь дополнительным образованием в небольшом городе на сорок тысяч населения, где школ в лучшем случае наберётся десяток вместе с соседними станицами и хуторами.&lt;p&gt;Мы с супругой оба бывшие олимпиадники, оба доходили до региональных этапов и оба становились призёрами. Сейчас она работает учителем математики. Культ олимпиады у нас в семье достаточно серьёзный, и за эти десять лет мы несколько раз загорались тем, чтобы готовить к олимпиадам не только своих детей, но и чужих, при этом каждый раз натыкались на одну и ту же стену.&lt;p&gt;Сразу стоит сказать — это не статья в духе «дети измельчали», дети как раз нормальные. Проблема оказалась совсем в другом, и разбираться в ней мне было интересно опять же потому, что столкнулся я с ней лично, с цифрами на руках и с полной аналитикой по каждому ученику.&lt;h3&gt;Сентябрь, новый учебный год и наш очередной заход&lt;/h3&gt;&lt;p&gt;В этом году мы решили зайти основательно: связались с несколькими местными школами и объявили набор в бесплатные группы подготовки к олимпиадам с 5 по 9 класс, сразу обозначив, что история будет многолетняя, а не в духе «подготовились, никуда не прошли, разошлись».&lt;p&gt;Школы ответили, что заинтересованы. Дальше рассылки сделали далеко не все, хотя часть школ информацию до детей всё-таки донесла. Я не возьмусь утверждать, что мы провели всю эту историю идеально чисто, и далеко не каждый родитель о наборе узнал, но вот что я могу гарантировать точно — уведомлены были больше четырёхсот учеников.&lt;p&gt;За чуть больше чем неделю к нам записались &lt;strong&gt;два человека&lt;/strong&gt;, и это при том, что занятия были заявлены полностью бесплатными.&lt;p&gt;Мы назначили собрание с этими двумя, чтобы познакомиться, обсудить план подготовки и рассказать, к чему вообще будем стремиться, но на собрание не пришёл ни один.&lt;p&gt;То есть конверсия из четырёхсот уведомлённых в запись составила где-то полпроцента, а конверсия из записи в приход на собрание оказалась ровно нулевой, причём речь про полностью бесплатные занятия у людей, которые сами были олимпиадниками и десять лет работают с детьми.&lt;p&gt;Во всём районе с 5 по 11 класс учится не меньше трёх тысяч человек. Понятное дело, что участвовать могут далеко не все, да и интересно это не каждому. Но даже если взять какой-то один процент от трёх тысяч, это тридцать человек, и даже их мы набрать не смогли.&lt;h3&gt;Почему я вообще считаю, что олимпиада того стоит&lt;/h3&gt;&lt;p&gt;Здесь стоит объяснить, из-за чего мы раз за разом в это лезем, потому что дело совершенно не в грамотах и не в строчке в портфолио.&lt;p&gt;Диплом победителя или призёра заключительного этапа Всероссийской олимпиады школьников даёт &lt;strong&gt;БВИ&lt;/strong&gt; — поступление без вступительных испытаний в любой вуз страны на профильное направление. То есть это не дополнительные баллы к сумме, а именно зачисление вне конкурса, и льгота действует четыре года, считая от года проведения олимпиады.&lt;p&gt;Тут есть момент, который многие путают, я сам был уверен в обратном. Порога ЕГЭ в 75 баллов для победителей всероссийской олимпиады попросту не существует — подтверждать свой результат экзаменом не нужно, и вуз обязан зачислить такого абитуриента.&lt;p&gt;Отдельная история — деньги, потому что регионы платят победителям и призёрам заключительного этапа собственные премии из своего бюджета, и вот здесь начинается самое любопытное.&lt;p&gt;Разброс между регионами оказался диким. Москва платит победителю 500 тысяч рублей, призёру 300 тысяч, и это единственный регион, где деньги получает вообще каждый, кто дошёл до финала — даже участнику без диплома причитается 100 тысяч рублей. Подмосковье идёт ровно по той же сетке, а в Якутии победителю дают те же 500 тысяч, но призёру уже только 100, и такого резкого разрыва между первым и вторым местом я не встретил больше нигде.&lt;p&gt;У меня в Краснодарском крае суммы поскромнее: 150 тысяч за победу и 100 тысяч призёру. Повысили их только в прошлом году, а до этого вместо премий за олимпиаду в крае работал отдельный конкурс на сто одарённых школьников по 50 тысяч рублей. Татарстан и Петербург дают 200 тысяч победителю и 100 призёру, Свердловская область — 100 и 70 соответственно.&lt;p&gt;А дальше идёт то, о чём обычно не пишут. По открытым сводкам в Саратовской области победителю всероссийской олимпиады платят 7 500 рублей, а в Липецкой 5 500 — столько же, сколько призёру. То есть между Москвой и Липецком разница получается почти в сто раз, причём за победу в одной и той же олимпиаде одного и того же уровня сложности.&lt;p&gt;Настолько заметный разброс, что весной прошлого года в Госдуму внесли законопроект о единых федеральных выплатах по московскому образцу — 500 тысяч победителю и 300 призёру. Судя по тому, что мне удалось найти, принят он так и не был, и размер выплаты по-прежнему остаётся делом конкретного региона.&lt;p&gt;Сверх премии есть ещё грант Президента для одарённых детей — 20 тысяч рублей в месяц на весь срок обучения, с ежегодным подтверждением права. С 2024 года появился и повышенный размер в 40 тысяч, но он уже за научные достижения в вузе, а не за саму олимпиаду.&lt;p&gt;Причём самое интересное даже не в этом, а в том, что региональный этап начинается только с 9 класса, и вот здесь как раз и лежит вся конструкция, к которой мы в итоге пришли.&lt;h3&gt;До 9 класса олимпиада почти ничего не даёт&lt;/h3&gt;&lt;p&gt;Чтобы был понятен масштаб, приведу воронку прошлого сезона по официальным цифрам министерства. Школьный этап собрал 8,3 миллиона человек, муниципальный уже 1,7 миллиона, региональный 153 тысячи, а до заключительного дошло чуть больше девяти тысяч участий. Дипломов в итоге выдали 4 241, из них 732 победителя.&lt;p&gt;То есть от школьного этапа до финала доходит примерно один человек из девятисот.&lt;p&gt;Причём число дипломов держится примерно одинаковым не случайно: в порядке проведения прописаны квоты. Победителей и призёров вместе там не больше 45 процентов от числа приглашённых на финал, победителей отдельно не больше 8 процентов, и вдобавок нельзя стать призёром, набрав меньше половины от максимально возможного балла.&lt;p&gt;С 5 по 8 класс ребёнок может участвовать только в школьном и муниципальном этапах. Дальше дороги нет никакой, потому что региональный этап проводится только для 9-11 классов, и это прямо прописано в порядке проведения олимпиады.&lt;p&gt;Что ученик получает по итогам? Опыт участия, который сам по себе действительно ценен, и грамоту на стену. Родители радуются, ребёнок радуется, и больше эта грамота не даёт ничего — ни льгот, ни денег, ни зачёта где-либо.&lt;p&gt;И вот тут примерно с 2022 года в эту картину пришли нейросети, которые на тот момент были ещё в зачаточном состоянии.&lt;h3&gt;Что нейросети сделали со школьным этапом&lt;/h3&gt;&lt;p&gt;Школьный этап проводится дистанционно, то есть ребёнок спокойно пишет его из дома, и никакого контроля за ним в этот момент нет в принципе.&lt;p&gt;С того времени дети начали массово решать школьный этап через нейросеть. Сначала это было достаточно коряво, сейчас модели закрывают задачи практически любого олимпиадного уровня. Организаторам в итоге приходится вручную просматривать решения, выставлять определённые маркеры, указывающие на машинное происхождение ответа, и дисквалифицировать таких участников по результатам проверки.&lt;p&gt;Отдельно я полез смотреть, что об этом написано в правилах, и достаточно сильно удивился. В порядке проведения олимпиады слов «нейросеть» или «искусственный интеллект» не оказалось вообще, ни в одной из действующих редакций документа. Запрет проносить средства связи есть, но он написан под очный тур и к ребёнку, который решает дома со своего ноутбука, не применим в принципе.&lt;p&gt;Правила школьного этапа при этом противоречат сами себе. В требованиях написано, что этап проводится в школах под контролем организаторов и с ограничением интернета белым списком, а в памятке участнику на том же сайте — что решать можно самостоятельно, в окне с восьми утра до десяти вечера, с любого устройства, включая телефон.&lt;p&gt;При этом на ЕГЭ детектор искусственного интеллекта работает ещё с 2020 года, и в 2025 году он дал 803 метки, что составило больше девяноста процентов всех зафиксированных нарушений. На школьном этапе олимпиады — ничего похожего.&lt;p&gt;Вычистить всё при этом не получается, и в результате баллы школьного этапа стали достаточно размытыми, а само распределение перестаёт быть похожим на нормальное.&lt;p&gt;Поясню, почему это вообще важно. Если олимпиада составлена нормально, то график «сколько детей сколько задач решило» должен давать колокол: большинство решает какое-то среднее количество задач, и есть два хвоста — те, кто не решил почти ничего, и те, кто решил почти всё. По форме этого колокола обычно и видно, адекватна ли была сложность. Когда в выборку подмешиваются машинные решения, колокол расползается, и понять по нему что-либо сложно.&lt;p&gt;С муниципальным этапом ситуация заметно лучше, потому что детей сгоняют в конкретные аудитории, там есть наблюдатели, телефон использовать нельзя и за это сразу дисквалифицируют. Но профиль информатики провести сложнее всех: там нужен компьютер с выходом в интернет, задания выполняются на платформе. А раз есть браузер, то есть и соседняя вкладка, и далеко не каждый муниципалитет в стране способен обеспечить нормальный уровень контроля в таких условиях.&lt;p&gt;В итоге получается, что с 5 по 8 класс олимпиада за свой класс — это соревнование с достаточно размытыми результатами ради грамоты на стену, и больше ничего оно не даёт.&lt;h3&gt;Стратегия, к которой мы пришли: писать за 9 класс с пятого&lt;/h3&gt;&lt;p&gt;В порядке проведения олимпиады есть при этом пункт, который меняет всю картину. Участник выполняет задания &lt;strong&gt;либо за свой класс, либо за любой более старший&lt;/strong&gt;. За младший писать нельзя, а за старший — сколько угодно.&lt;p&gt;То есть пятиклассник имеет полное право писать олимпиаду за 9 класс, и никакого нарушения правил в этом нет.&lt;p&gt;Тут есть важная деталь, которую стоит держать в уме. Выбор фиксируется: если ребёнок пошёл на школьный этап за 9 класс и прошёл дальше, то и на муниципальном, и на региональном он пишет за 9 класс. Отыграть назад уже нельзя, плюс для этого требуется письменное заявление самого ученика и письменное подтверждение от родителей.&lt;p&gt;Что это в итоге даёт. Ребёнок начинает участвовать в олимпиаде за 9 класс, находясь в пятом. Выиграть всероссийский этап в 5 классе практически нереально, но задача на этом шаге совершенно другая. В 6-7 классе при нормальной подготовке он уже вполне способен пройти на региональный этап, а в 7-8 классе имеет смысл пробиваться на заключительный и показывать там уже какой-то осмысленный результат.&lt;p&gt;Итого получается &lt;strong&gt;пять попыток&lt;/strong&gt; написать олимпиаду за 9 класс — в 5, 6, 7, 8 и 9 классе, вместо одной-единственной попытки в девятом классе без какой-либо серьёзной подготовки за спиной.&lt;p&gt;Здесь работает ровно та же логика, что и в спортзале. Бицепс не появляется от того, что ты один раз пришёл и хорошо позанимался, он появляется от систематических занятий из года в год. С олимпиадами плюс-минус та же самая история, и статистика играет явно не в сторону того, кто пишет впервые.&lt;p&gt;Про БВИ стоит сказать отдельно. Льгота действует четыре года, поэтому побеждать имеет смысл где-то с 8 класса, чтобы к моменту поступления она ещё работала. Победа в 5-6 классе даёт респект, опыт и деньги, но к поступлению её уже не приложишь. И это не теория: в 2023 году финал по математике в программе десятых классов выиграл семиклассник из Москвы Пётр Ким, а призёром в программе девятых классов стал двенадцатилетний Георгий Шкурихин из Петербурга. Случаев побед именно пятиклассников я при этом не нашёл, так что ожидания тут всё-таки стоит держать в каких-то разумных рамках.&lt;h3&gt;Насколько велик разрыв между этапами&lt;/h3&gt;&lt;p&gt;Это, пожалуй, самое неочевидное во всей истории. Мы собрали архив официальных заданий за несколько сезонов: школьный этап за пять, муниципальный за четыре, региональный за шесть и заключительный за четыре. Причём собрали вместе с официальными решениями жюри, чтобы понимать, как задачи оцениваются.&lt;p&gt;Уровень муниципального этапа по робототехнике оказался, прямо скажем, достаточно низким, и это было для меня неожиданностью. Реальное задание за 9 класс сезона 2023/24 звучит так. Определить по фотографии сельскохозяйственную культуру и посчитать, сколько Маша заплатила за авокадо со скидкой 15 процентов. Ребёнок, который готовился к региональному этапу, проходит такие задания почти автоматически, поэтому вкладывать в них основные силы совершенно бессмысленно.&lt;p&gt;Региональный этап — это уже сто баллов за два дня, где теория даёт 40 баллов, практика на Arduino ещё 35 и проектный тур оставшиеся 25, то есть 60 баллов из 100 требуют реального железа, собранного своими руками.&lt;p&gt;А на заключительном этапе за тот же самый 9 класс появляется, например, задача на алгоритм RANSAC и на обход всех вершин десятимерного гиперкуба.&lt;p&gt;От авокадо до RANSAC — и это, стоит держать в уме, одна и та же олимпиада, один и тот же класс и соседние между собой этапы.&lt;p&gt;И вот здесь стоит отметить отдельно то, на чём держится вся наша ставка. С сезона 2025/26 информатика в ВсОШ проводится сразу по четырём профилям: программирование, информационная безопасность, робототехника и искусственный интеллект. Раньше робототехника жила внутри предмета «Технология», а теперь переехала в информатику, и сезон 2025/26 стал первым, когда олимпиада прошла сразу по всем четырём профилям.&lt;p&gt;Значение у этого простое и достаточно большое. Победа или призёрство по профилю информационной безопасности или робототехники даёт ровно тот же самый БВИ, что и по математике или физике. В финале 2026 года по информатике участвовало 1 227 человек, дипломов выдали 562 — это самый массовый предмет заключительного этапа.&lt;h3&gt;Почему мы делаем упор на робототехнику и безопасность&lt;/h3&gt;&lt;p&gt;Тут расчёт достаточно простой. В программирование идёт много людей, и чтобы там побеждать, в алгоритмы надо упарываться всерьёз. А в робототехнике и информационной безопасности конкуренция заметно ниже, при этом олимпиада за 9 класс выглядит вполне проходимой.&lt;p&gt;Мы разобрали задания по информационной безопасности за 9 класс с 2024 по 2026 год и получили достаточно любопытную картину. Криптография появилась в перечне тем девятого класса только в 2026 году. RSA у девятиклассников встречается ровно один раз, на заключительном этапе 2026 года, и решается одной командой openssl с готовым ключом. При этом тесты простоты, Миллер-Рабин, теоремы Ферма и Эйлера, шифры Цезаря и Виженера — ровно ноль упоминаний за все три года.&lt;h3&gt;Что происходит, когда группа всё-таки набрана&lt;/h3&gt;&lt;p&gt;Из своих ста двадцати учеников мы отобрали десять человек — тех, кто дальше всех продвинулся и в принципе способен заниматься самостоятельно, и вот с ними уже начали подготовку.&lt;p&gt;Курсы сделали на своей обучающей платформе, про которую я писал в прошлых статьях. Получилось, на мой взгляд, действительно интересно. И вот здесь у меня есть то, чего обычно нет ни у кого — полная аналитика по каждому аккаунту. Видно, кто когда заходит, какие задачи решает, с какой попытки и сколько заданий делает в день.&lt;p&gt;Занятия идут один-два раза в неделю по два-четыре часа, и этого категорически не хватит для всероссийского уровня, так что дома работать обязательно.&lt;p&gt;По данным платформы дома продолжают заниматься &lt;strong&gt;три-четыре человека из десяти&lt;/strong&gt;, а все остальные работают исключительно на самих занятиях. При этом на самом занятии ребёнка вполне удаётся замотивировать, он сидит, вникает и всё у него хорошо получается. А дома он не открывает платформу вообще, и это видно по логам достаточно наглядно.&lt;h3&gt;Отдельная история про мотивацию&lt;/h3&gt;&lt;p&gt;Есть отдельная категория детей, у которых льгота на поступление уже есть — она положена по линии родителей, участвовавших в военных действиях. Формально место в вузе или колледже им уже обеспечено.&lt;p&gt;И часть этих ребят, в том числе действительно сильных и хорошо знающих программирование, олимпиадами не интересуется совершенно, хотя результаты в обучении показывают серьёзные. Логика тут вполне понятная: если результат уже гарантирован, то зачем надрываться, когда можно прийти домой, сделать уроки и пойти играть с друзьями.&lt;p&gt;Я не скажу, что таких большинство, лично я встречал буквально несколько человек. Претензий у меня к ним нет никаких: льгота законная, и своим временем они распоряжаются как считают нужным. Просто это опять же ещё один фактор, который на практике вычитается из числа потенциальных олимпиадников в небольшом городе.&lt;h3&gt;Кто в итоге остаётся&lt;/h3&gt;&lt;p&gt;У меня сейчас есть четыре человека, которые только перешли в 6 класс, и основные свои надежды я возлагаю как раз на них.&lt;p&gt;Эти ребята ещё в 4-5 классе поднимали свои Майнкрафт-серверы на моём домашнем сервере. Я заводил под них отдельную виртуальную машину, пробрасывал порты и давал доступ по SSH, а дальше они настраивали систему полностью сами. Разбирались с командами консоли, с докером и с тем, как вообще всё это устроено изнутри.&lt;p&gt;Таких детей достаточно немного. Но если интерес есть и его получается развить, то к началу шестого класса из ребёнка вырастает вполне серьёзный человек в системном администрировании. Это и есть мои первые кандидаты на олимпиаду по информационной безопасности за 9 класс.&lt;h3&gt;Что я обо всём этом думаю&lt;/h3&gt;&lt;p&gt;Мы объявили бесплатную подготовку, уведомили больше четырёхсот человек, получили двух записавшихся и ноль пришедших. Дальше набрали группу из собственных учеников, где дома занимается примерно треть от списка. При этом сама конструкция с пятью попытками за 9 класс кажется мне вполне рабочей, и отказываться от неё я в любом случае не собираюсь.&lt;p&gt;Моя рабочая версия в том, что дело тут совершенно не в лени и не в детях. Олимпиада — это отложенный результат в горизонте нескольких лет, без гарантий и с непонятной для родителя пользой на старте. Конкурировать с кружком, где через месяц получается готовая поделка, она попросту не может. Плюс в маленьком городе про БВИ мало кто вообще знает, и для большинства родителей олимпиада — это по-прежнему про грамоту на стену, а не про поступление в МФТИ без вступительных экзаменов.&lt;p&gt;Возможно, я всё это неправильно объяснял родителям, а возможно, бесплатное как раз и обесценивает саму историю, и за деньги записались бы заметно охотнее. Ещё возможно, что идти надо было вообще не через школы, а напрямую к родителям, минуя всю эту цепочку.&lt;p&gt;А как это выглядит с вашей стороны? Если у вас дети участвуют в олимпиадах — что стало решающим, и в какой момент они сами начали заниматься дома без напоминаний? И отдельно интересно мнение тех, кто сам когда-то был олимпиадником: что вас тогда держало?&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <author>Ingir_Max</author>
      <guid>https://habr.com/ru/articles/1082268/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082268</guid>
      <pubDate>Tue, 15 Sep 2026 04:50:55 +0000</pubDate>
    </item>
    <item>
      <title>Навигация мобильного робота для сбора клубники</title>
      <link>https://habr.com/ru/articles/1082262/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082262</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;В предыдущих материалах я рассказывал о разработке робототехнических систем и отдельных узлов управления. В этот раз задача — разработка мобильной платформы для &lt;strong&gt;робота-сборщика клубники&lt;/strong&gt;.&lt;p&gt;Предполагается, что такой робот перемещается вдоль рядов растений, останавливается в рабочих позициях для сбора ягод и должен уметь реагировать на препятствия, появляющиеся на пути.&lt;p&gt;Полноценный робот-сборщик — достаточно большая система. В неё должны входить как минимум:&lt;pre&gt;&lt;code&gt;мобильная платформа        +&#xA;система навигации        +&#xA;лидар / датчики        +&#xA;техническое зрение        +&#xA;поиск спелой клубники        +&#xA;манипулятор        +&#xA;захват ягоды&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:87px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Поэтому разработку я решил разбить на отдельные модули.&lt;p&gt;В этой статье рассматривается &lt;strong&gt;навигационная часть&lt;/strong&gt;: робот должен самостоятельно проехать из начальной точки к заданной позиции, а при появлении препятствия определить занятую область и построить другой маршрут.&lt;p&gt;Для разработки используются &lt;strong&gt;ROS 2, Python и Webots&lt;/strong&gt;.&lt;h3&gt;Постановка задачи&lt;/h3&gt;&lt;p&gt;На первом этапе движение робота по теплице сильно упрощено.&lt;p&gt;Рабочая область представлена сеткой 5×5:&lt;pre&gt;&lt;code&gt;20 21 22 23 24&#xA;15 16 17 18 19&#xA;10 11 12 13 14 5  6  7  8  9 0  1  2  3  4&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Каждый номер соответствует возможной позиции мобильной платформы.&lt;p&gt;Такое представление пока не является полноценной картой теплицы. Оно нужно для отработки основных алгоритмов:&lt;pre&gt;&lt;code&gt;где находится робот?        ↓&#xA;куда ему нужно приехать?        ↓&#xA;какой маршрут короче?        ↓&#xA;как выполнить этот маршрут?        ↓&#xA;что делать, если дорога перекрыта?&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Например:&lt;pre&gt;&lt;code&gt;START  = 0&#xA;TARGET = 22&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Робот должен построить маршрут от &lt;code&gt;0&lt;/code&gt; до &lt;code&gt;22&lt;/code&gt;, выполнить его и остановиться в целевой позиции.&lt;p&gt;После этого в симуляции появляется препятствие.&lt;p&gt;Это может имитировать ситуацию, характерную для реальной эксплуатации в теплице: проход частично перекрыт ящиком, оборудованием или другим объектом.&lt;p&gt;Робот получает данные лидара, определяет занятую область и при возвращении уже не должен использовать её.&lt;p&gt;В результате эксперимент выглядит следующим образом:&lt;pre&gt;&lt;code&gt;старт  ↓&#xA;построение маршрута  ↓&#xA;движение по теплице  ↓&#xA;рабочая позиция  ↓&#xA;обнаружение нового препятствия  ↓&#xA;изменение доступной карты  ↓&#xA;перепланирование  ↓&#xA;возвращение другим маршрутом&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;hr&gt;&lt;h2&gt;Что реализовано&lt;/h2&gt;&lt;p&gt;Программа разделена на четыре основных компонента:&lt;pre&gt;&lt;code&gt;Graph&#xA;│&#xA;├── карта поля&#xA;├── координаты маркеров&#xA;└── соседние позиции&#xA;Planner&#xA;│&#xA;├── BFS&#xA;├── кратчайший маршрут&#xA;└── минимизация количества поворотов&#xA;Motion&#xA;│&#xA;├── Odometry&#xA;├── управление скоростью&#xA;└── движение между маркерами&#xA;ObstacleDetector&#xA;│&#xA;├── LaserScan&#xA;├── сравнение сканов&#xA;└── определение занятой клетки&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Главный модуль связывает эти части в единый сценарий.&lt;p&gt;[Далее основная часть статьи: описание Graph, BFS, минимизации поворотов, Motion, Odometry, Twist, LaserScan и определения препятствия.]&lt;hr&gt;&lt;h2&gt;Как это связано со сбором клубники&lt;/h2&gt;&lt;p&gt;На данном этапе робот ещё &lt;strong&gt;не собирает ягоды&lt;/strong&gt;.&lt;p&gt;Сейчас решается задача нижнего уровня — мобильная платформа должна надёжно доставлять роботизированную систему к рабочим позициям.&lt;p&gt;В дальнейшем архитектура может выглядеть следующим образом:&lt;pre&gt;&lt;code&gt;                 РОБОТ-СБОРЩИК                       │        ┌──────────────┼──────────────┐        │              │              │        ↓              ↓              ↓   Навигация      Тех. зрение    Манипулятор        │              │              │        ↓              ↓              ↓   куда ехать     где клубника    как сорвать&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Навигационный модуль отвечает за перемещение платформы.&lt;p&gt;После прибытия в очередную рабочую позицию управление сможет передаваться системе технического зрения.&lt;p&gt;Камера определяет ягоды:&lt;pre&gt;&lt;code&gt;изображение    ↓&#xA;поиск клубники    ↓&#xA;определение спелости    ↓&#xA;координаты ягоды&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;После этого координаты передаются манипулятору:&lt;pre&gt;&lt;code&gt;координаты ягоды       ↓&#xA;планирование движения       ↓&#xA;подвод захвата       ↓&#xA;срыв       ↓&#xA;укладка ягоды&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;После завершения сбора в данной позиции мобильная платформа переезжает дальше.&lt;p&gt;То есть в перспективе полный рабочий цикл должен выглядеть так:&lt;pre&gt;&lt;code&gt;построить маршрут       ↓&#xA;приехать к растениям       ↓&#xA;остановиться       ↓&#xA;найти спелые ягоды       ↓&#xA;передать координаты манипулятору       ↓&#xA;собрать ягоды       ↓&#xA;проверить проход       ↓&#xA;перейти к следующей позиции&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h2&gt;Разбираем программу по частям&lt;/h2&gt;&lt;p&gt;Теперь перейдём к самой интересной части — реализации.&lt;p&gt;Навигационный модуль я специально не стал писать одним большим файлом. Программа разбита на несколько частей:&lt;pre&gt;&lt;code&gt;main.py&#xA;│&#xA;├── graph.py&#xA;│      └── представление поля&#xA;│&#xA;├── planner.py&#xA;│      └── поиск маршрута&#xA;│&#xA;├── motion_controller.py&#xA;│      └── физическое движение робота&#xA;│&#xA;└── obstacle_detector.py       └── обработка лидара&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Это разделение не случайно.&lt;p&gt;Планировщику не обязательно знать, как устроен настоящий робот. Он получает граф и должен вернуть последовательность маркеров.&lt;p&gt;Контроллеру движения, наоборот, неважно, каким алгоритмом был найден маршрут. Для него:&lt;pre&gt;&lt;code class=python&gt;[0, 1, 2, 7, 12]&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;— просто последовательность точек, которые нужно посетить.&lt;p&gt;А детектор препятствий вообще решает третью задачу: превращает показания лидара в номер занятой клетки.&lt;p&gt;Так появляется разделение ответственности:&lt;pre&gt;&lt;code&gt;Graph&#xA;&amp;#34;Где вообще можно ехать?&amp;#34;&#xA;Planner&#xA;&amp;#34;Как лучше туда доехать?&amp;#34;&#xA;Motion&#xA;&amp;#34;Как физически проехать маршрут?&amp;#34;&#xA;ObstacleDetector&#xA;&amp;#34;Где появилось препятствие?&amp;#34;&#xA;main&#xA;&amp;#34;В каком порядке всё это запустить?&amp;#34;&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Начнём с самого нижнего уровня.&lt;hr&gt;&lt;h2&gt;1. graph.py — как представить теплицу в программе&lt;/h2&gt;&lt;p&gt;В реальной теплице робот будет перемещаться между рядами растений. Но для первого эксперимента пространство удобно сильно упростить.&lt;p&gt;Используется поле 5×5:&lt;pre&gt;&lt;code&gt;20 21 22 23 24&#xA;15 16 17 18 19&#xA;10 11 12 13 14 5  6  7  8  9 0  1  2  3  4&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Расстояние между соседними маркерами принимается равным одному метру.&lt;p&gt;В программе карта представлена классом:&lt;pre&gt;&lt;code class=python&gt;class Graph:&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Почему класс?&lt;p&gt;Потому что у нашей карты есть связанные друг с другом операции:&lt;pre&gt;&lt;code&gt;проверить маркер&#xA;получить его координаты&#xA;получить соседей&#xA;определить направление&#xA;найти ближайший маркер&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Их удобно объединить в один объект.&lt;h3&gt;Размер карты&lt;/h3&gt;&lt;p&gt;Внутри класса записано:&lt;pre&gt;&lt;code class=python&gt;SIZE = 5&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Это размер одной стороны сетки.&lt;p&gt;Здесь &lt;code&gt;SIZE&lt;/code&gt; является атрибутом класса. Его можно использовать внутри методов:&lt;pre&gt;&lt;code class=python&gt;self.SIZE&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;self&lt;/code&gt; в Python означает текущий объект.&lt;p&gt;Например:&lt;pre&gt;&lt;code class=python&gt;graph = Graph()&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;После этого при вызове:&lt;pre&gt;&lt;code class=python&gt;graph.position(12)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;внутри &lt;code&gt;position()&lt;/code&gt; переменная &lt;code&gt;self&lt;/code&gt; будет ссылаться именно на объект &lt;code&gt;graph&lt;/code&gt;.&lt;hr&gt;&lt;h2&gt;Проверяем существование клетки&lt;/h2&gt;&lt;p&gt;Первый метод:&lt;pre&gt;&lt;code class=python&gt;def valid(self, marker):    return 0 &amp;lt;= marker &amp;lt; 25&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;У нас 25 клеток:&lt;pre&gt;&lt;code&gt;0 ... 24&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Поэтому:&lt;pre&gt;&lt;code class=python&gt;graph.valid(12)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;вернёт:&lt;pre&gt;&lt;code&gt;True&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;а:&lt;pre&gt;&lt;code class=python&gt;graph.valid(30)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;вернёт:&lt;pre&gt;&lt;code&gt;False&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Запись:&lt;pre&gt;&lt;code class=python&gt;0 &amp;lt;= marker &amp;lt; 25&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;в Python является сокращённой формой:&lt;pre&gt;&lt;code class=python&gt;0 &amp;lt;= marker and marker &amp;lt; 25&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Такая проверка понадобится планировщику, чтобы случайно не попытаться построить маршрут, например, к клетке &lt;code&gt;100&lt;/code&gt;.&lt;hr&gt;&lt;h2&gt;Перевод номера маркера в координаты&lt;/h2&gt;&lt;p&gt;Следующая задача интереснее.&lt;p&gt;Робот физически не понимает команду:&lt;pre&gt;&lt;code&gt;едь к маркеру 12&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Контроллеру нужны координаты:&lt;pre&gt;&lt;code&gt;X = ?&#xA;Y = ?&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Для этого используется:&lt;pre&gt;&lt;code class=python&gt;def position(self, marker):    x = marker % self.SIZE    y = marker // self.SIZE    return float(x), float(y)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Разберём две операции.&lt;p&gt;&lt;code&gt;%&lt;/code&gt; — остаток от деления.&lt;p&gt;&lt;code&gt;//&lt;/code&gt; — целочисленное деление.&lt;p&gt;Возьмём маркер &lt;code&gt;12&lt;/code&gt;:&lt;pre&gt;&lt;code class=python&gt;x = 12 % 5&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Получим:&lt;pre&gt;&lt;code&gt;2&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;А:&lt;pre&gt;&lt;code class=python&gt;y = 12 // 5&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;даст:&lt;pre&gt;&lt;code&gt;2&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Следовательно:&lt;pre&gt;&lt;code&gt;marker 12 → (2, 2)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Для маркера &lt;code&gt;17&lt;/code&gt;:&lt;pre&gt;&lt;code&gt;17 % 5  = 2&#xA;17 // 5 = 3&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;получаем:&lt;pre&gt;&lt;code&gt;marker 17 → (2, 3)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;То есть номер клетки одновременно кодирует её положение на сетке.&lt;p&gt;Это позволяет не хранить отдельную таблицу:&lt;pre&gt;&lt;code class=python&gt;{    0: (0, 0),    1: (1, 0),    2: (2, 0),    ...&#xA;}&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;hr&gt;&lt;h2&gt;Как найти соседние клетки&lt;/h2&gt;&lt;p&gt;Для BFS нужно знать, куда можно перейти из текущей клетки.&lt;p&gt;Для этого существует:&lt;pre&gt;&lt;code class=python&gt;def neighbors(self, marker, blocked=None):&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Сначала:&lt;pre&gt;&lt;code class=python&gt;if blocked is None:    blocked = set()&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;blocked&lt;/code&gt; — множество запрещённых клеток.&lt;p&gt;Например:&lt;pre&gt;&lt;code class=python&gt;blocked = {7, 12}&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;означает:&lt;pre&gt;&lt;code&gt;в клетки 7 и 12 заезжать нельзя&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Почему используется &lt;code&gt;set&lt;/code&gt;, а не список?&lt;p&gt;Потому что нам постоянно нужна операция:&lt;pre&gt;&lt;code class=python&gt;m not in blocked&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;То есть проверка принадлежности.&lt;p&gt;Для множества это естественная операция.&lt;hr&gt;&lt;p&gt;Далее номер клетки переводится в строку и столбец:&lt;pre&gt;&lt;code class=python&gt;row = marker // self.SIZE&#xA;col = marker % self.SIZE&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Для &lt;code&gt;12&lt;/code&gt;:&lt;pre&gt;&lt;code&gt;row = 2&#xA;col = 2&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Теперь можно проверить четыре направления.&lt;p&gt;Вправо:&lt;pre&gt;&lt;code class=python&gt;if col &amp;lt; self.SIZE - 1:    result.append(marker + 1)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Если мы не на правой границе, сосед справа имеет номер:&lt;pre&gt;&lt;code&gt;marker + 1&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Для &lt;code&gt;12&lt;/code&gt; это:&lt;pre&gt;&lt;code&gt;13&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Вверх:&lt;pre&gt;&lt;code class=python&gt;if row &amp;lt; self.SIZE - 1:    result.append(marker + self.SIZE)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Получаем:&lt;pre&gt;&lt;code&gt;12 + 5 = 17&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Влево:&lt;pre&gt;&lt;code class=python&gt;if col &amp;gt; 0:    result.append(marker - 1)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Получаем:&lt;pre&gt;&lt;code&gt;11&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Вниз:&lt;pre&gt;&lt;code class=python&gt;if row &amp;gt; 0:    result.append(marker - self.SIZE)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Получаем:&lt;pre&gt;&lt;code&gt;7&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Следовательно:&lt;pre&gt;&lt;code class=python&gt;graph.neighbors(12)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;даст:&lt;pre&gt;&lt;code class=python&gt;[13, 17, 11, 7]&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;hr&gt;&lt;h2&gt;Зачем нужны проверки границ&lt;/h2&gt;&lt;p&gt;Можно было бы просто написать:&lt;pre&gt;&lt;code class=python&gt;[    marker + 1,    marker + 5,    marker - 1,    marker - 5&#xA;]&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Но тогда для клетки &lt;code&gt;4&lt;/code&gt;:&lt;pre&gt;&lt;code&gt;0 1 2 3 4&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;операция:&lt;pre&gt;&lt;code&gt;4 + 1 = 5&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;якобы сказала бы, что &lt;code&gt;5&lt;/code&gt; находится справа от &lt;code&gt;4&lt;/code&gt;.&lt;p&gt;Но на карте:&lt;pre&gt;&lt;code&gt;5 6 7 8 9&#xA;0 1 2 3 4&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;5&lt;/code&gt; находится совсем в другой строке.&lt;p&gt;Поэтому проверки:&lt;pre&gt;&lt;code class=python&gt;col &amp;lt; SIZE - 1&#xA;col &amp;gt; 0&#xA;row &amp;lt; SIZE - 1&#xA;row &amp;gt; 0&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;не позволяют «провалиться» через край сетки.&lt;hr&gt;&lt;h2&gt;Исключаем препятствия&lt;/h2&gt;&lt;p&gt;В конце:&lt;pre&gt;&lt;code class=python&gt;return [    m for m in result    if m not in blocked&#xA;]&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Это list comprehension — компактная запись формирования нового списка.&lt;p&gt;Например:&lt;pre&gt;&lt;code class=python&gt;result = [13, 17, 11, 7]&#xA;blocked = {17}&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;получим:&lt;pre&gt;&lt;code class=python&gt;[13, 11, 7]&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Для планировщика клетки &lt;code&gt;17&lt;/code&gt; теперь просто не существует как допустимого перехода.&lt;p&gt;Это очень удобное решение: самому BFS не нужно специально объяснять, что перед роботом появился ящик или другое препятствие.&lt;p&gt;Достаточно убрать вершину из доступных соседей.&lt;hr&gt;&lt;h2&gt;Определяем направление движения&lt;/h2&gt;&lt;p&gt;В &lt;code&gt;Graph&lt;/code&gt; есть ещё один небольшой метод:&lt;pre&gt;&lt;code class=python&gt;def direction(self, a, b):&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Он нужен не для управления роботом, а для оценки маршрута.&lt;p&gt;Например:&lt;pre&gt;&lt;code&gt;11 → 12&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Координаты:&lt;pre&gt;&lt;code&gt;11 = (1, 2)&#xA;12 = (2, 2)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Так как:&lt;pre&gt;&lt;code&gt;bx &amp;gt; ax&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;возвращается:&lt;pre&gt;&lt;code class=python&gt;&amp;#34;RIGHT&amp;#34;&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Аналогично определяются:&lt;pre&gt;&lt;code&gt;LEFT&#xA;UP&#xA;DOWN&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Позже планировщик будет использовать эту информацию для подсчёта поворотов.&lt;hr&gt;&lt;h2&gt;Как лидар связывается с графом&lt;/h2&gt;&lt;p&gt;Последний метод:&lt;pre&gt;&lt;code class=python&gt;nearest_marker()&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;решает важную задачу.&lt;p&gt;Лидар ничего не знает о наших номерах:&lt;pre&gt;&lt;code&gt;0, 1, 2, 3...&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;После обработки лидара мы можем получить реальную координату препятствия:&lt;pre&gt;&lt;code&gt;(2.08, 3.11)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Нужно понять, какой клетке она соответствует.&lt;p&gt;Перебираем все 25 маркеров:&lt;pre&gt;&lt;code class=python&gt;for marker in range(25):&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Получаем координаты:&lt;pre&gt;&lt;code class=python&gt;mx, my = self.position(marker)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;И вычисляем евклидово расстояние:&lt;pre&gt;&lt;code class=python&gt;distance = math.hypot(    x - mx,    y - my&#xA;)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Фактически это:&lt;pre&gt;&lt;code&gt;d = √((x-mx)² + (y-my)²)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Запоминается маркер с минимальным расстоянием.&lt;p&gt;Если ближайший маркер слишком далеко:&lt;pre&gt;&lt;code class=python&gt;if best_distance &amp;gt; max_distance:    return None&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;мы считаем, что lidar-точка вообще не относится к нашей сетке.&lt;p&gt;Таким образом &lt;code&gt;Graph&lt;/code&gt; связывает три представления:&lt;pre&gt;&lt;code&gt;номер клетки    ↕&#xA;координаты X/Y    ↕&#xA;данные лидара&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;hr&gt;&lt;h2&gt;2. planner.py — строим маршрут&lt;/h2&gt;&lt;p&gt;Теперь карта есть.&lt;p&gt;Следующий вопрос:&lt;blockquote&gt;&lt;p&gt;Как роботу добраться из клетки 0 в клетку 22?&lt;/blockquote&gt;&lt;p&gt;Для этого используется класс:&lt;pre&gt;&lt;code class=python&gt;class Planner:&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;При создании он получает граф:&lt;pre&gt;&lt;code class=python&gt;def __init__(self, graph):    self.graph = graph&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Поэтому планировщик сам не знает, что карта имеет размер 5×5.&lt;p&gt;Он просто спрашивает:&lt;pre&gt;&lt;code class=python&gt;self.graph.neighbors(...)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Это хороший принцип разделения ответственности.&lt;hr&gt;&lt;h2&gt;Почему BFS&lt;/h2&gt;&lt;p&gt;Для поиска используется Breadth-First Search — поиск в ширину.&lt;p&gt;Импортируется:&lt;pre&gt;&lt;code class=python&gt;from collections import deque&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;deque&lt;/code&gt; — двусторонняя очередь.&lt;p&gt;Для BFS нам важны две операции:&lt;pre&gt;&lt;code class=python&gt;queue.append(...)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;положить элемент в конец и:&lt;pre&gt;&lt;code class=python&gt;queue.popleft()&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;забрать элемент из начала.&lt;p&gt;Получается FIFO:&lt;pre&gt;&lt;code&gt;First In → First Out&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Именно благодаря этому BFS рассматривает карту слоями.&lt;hr&gt;&lt;h2&gt;Как распространяется BFS&lt;/h2&gt;&lt;p&gt;Пусть:&lt;pre&gt;&lt;code&gt;START = 0&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Сначала очередь:&lt;pre&gt;&lt;code&gt;[0]&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Из &lt;code&gt;0&lt;/code&gt; доступны:&lt;pre&gt;&lt;code&gt;1, 5&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Затем рассматриваются пути:&lt;pre&gt;&lt;code&gt;[0, 1]&#xA;[0, 5]&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Следующий слой:&lt;pre&gt;&lt;code&gt;[0, 1, 2]&#xA;[0, 1, 6]&#xA;[0, 5, 6]&#xA;[0, 5, 10]&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;То есть алгоритм сначала исследует все пути длины 1, потом длины 2, затем 3 и так далее.&lt;p&gt;Поэтому при одинаковой стоимости переходов первая достигнутая глубина цели является минимальной.&lt;hr&gt;&lt;h2&gt;Почему в очереди лежат целые маршруты&lt;/h2&gt;&lt;p&gt;В коде:&lt;pre&gt;&lt;code class=python&gt;queue.append([start])&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Обратите внимание: кладётся не просто:&lt;pre&gt;&lt;code class=python&gt;0&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;а:&lt;pre&gt;&lt;code class=python&gt;[0]&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;То есть список — уже готовый маршрут.&lt;p&gt;При расширении:&lt;pre&gt;&lt;code class=python&gt;new_route = route + [next_marker]&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Например:&lt;pre&gt;&lt;code class=python&gt;route = [0, 1, 2]&#xA;next_marker = 7&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;получим:&lt;pre&gt;&lt;code class=python&gt;[0, 1, 2, 7]&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Для большой карты это не самый экономный по памяти вариант BFS, но для поля 5×5 он очень нагляден.&lt;p&gt;Не требуется отдельно хранить таблицу родителей и потом восстанавливать маршрут.&lt;hr&gt;&lt;h2&gt;Не ходим по кругу&lt;/h2&gt;&lt;p&gt;Есть проверка:&lt;pre&gt;&lt;code class=python&gt;if next_marker in route:    continue&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Представим:&lt;pre&gt;&lt;code&gt;0 → 1&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Из &lt;code&gt;1&lt;/code&gt; можно снова попасть в &lt;code&gt;0&lt;/code&gt;.&lt;p&gt;Без проверки алгоритм мог бы строить:&lt;pre&gt;&lt;code&gt;0 → 1 → 0 → 1 → 0 ...&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Поэтому запрещается добавлять клетку, которая уже присутствует в текущем маршруте.&lt;p&gt;&lt;code&gt;continue&lt;/code&gt; означает:&lt;blockquote&gt;&lt;p&gt;прекратить текущую итерацию цикла и перейти к следующему соседу.&lt;/blockquote&gt;&lt;hr&gt;&lt;h2&gt;Почему сохраняются несколько маршрутов&lt;/h2&gt;&lt;p&gt;Обычный BFS часто завершают сразу после нахождения цели.&lt;p&gt;Но здесь этого делать нельзя.&lt;p&gt;Нам нужен не просто первый кратчайший маршрут.&lt;p&gt;Нужно найти &lt;strong&gt;все маршруты минимальной длины&lt;/strong&gt;, чтобы потом выбрать среди них маршрут с меньшим количеством поворотов.&lt;p&gt;Поэтому:&lt;pre&gt;&lt;code class=python&gt;found_routes = []&#xA;shortest_length = None&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Когда достигаем цели:&lt;pre&gt;&lt;code class=python&gt;if current == target:    shortest_length = len(route)    found_routes.append(route)    continue&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Первый найденный маршрут задаёт минимальную длину.&lt;p&gt;Но обработка остальных маршрутов того же уровня продолжается.&lt;p&gt;Когда очередь переходит к более длинным:&lt;pre&gt;&lt;code class=python&gt;if shortest_length is not None:    if len(route) &amp;gt; shortest_length:        break&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;поиск заканчивается.&lt;hr&gt;&lt;h2&gt;Считаем повороты&lt;/h2&gt;&lt;p&gt;Возьмём маршрут:&lt;pre&gt;&lt;code&gt;0 → 1 → 2 → 7 → 12 → 17 → 22&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Направления:&lt;pre&gt;&lt;code&gt;RIGHT&#xA;RIGHT&#xA;UP&#xA;UP&#xA;UP&#xA;UP&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Изменение направления произошло только один раз:&lt;pre&gt;&lt;code&gt;RIGHT → UP&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Значит:&lt;pre&gt;&lt;code&gt;turns = 1&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;В коде сначала определяется первое направление:&lt;pre&gt;&lt;code class=python&gt;old_direction = self.graph.direction(    route[0],    route[1]&#xA;)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Потом последовательно сравниваются остальные:&lt;pre&gt;&lt;code class=python&gt;if new_direction != old_direction:    turns += 1&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;После сравнения:&lt;pre&gt;&lt;code class=python&gt;old_direction = new_direction&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Таким образом запоминается направление предыдущего участка.&lt;hr&gt;&lt;h2&gt;Выбираем лучший маршрут&lt;/h2&gt;&lt;p&gt;После BFS у нас может быть несколько вариантов.&lt;p&gt;Используется:&lt;pre&gt;&lt;code class=python&gt;best_route = min(    found_routes,    key=lambda route: (        self.count_turns(route),        route    )&#xA;)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Это одна из менее очевидных конструкций Python.&lt;p&gt;&lt;code&gt;min()&lt;/code&gt; здесь ищет минимальный элемент не непосредственно по самому списку, а по ключу.&lt;p&gt;Для каждого маршрута ключ имеет вид:&lt;pre&gt;&lt;code class=python&gt;(    количество_поворотов,    сам_маршрут&#xA;)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Например:&lt;pre&gt;&lt;code&gt;route A → (3, [0, 1, 6, ...])&#xA;route B → (1, [0, 5, 10, ...])&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Сначала Python сравнит:&lt;pre&gt;&lt;code&gt;3 и 1&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;и выберет второй маршрут.&lt;p&gt;Если число поворотов совпадает, сравниваются сами списки маркеров.&lt;p&gt;Это делает выбор детерминированным.&lt;hr&gt;&lt;h2&gt;Что возвращает Planner&lt;/h2&gt;&lt;p&gt;В конце:&lt;pre&gt;&lt;code class=python&gt;distance = len(best_route) - 1&#xA;turns = self.count_turns(best_route)&#xA;return best_route, distance, turns&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Почему:&lt;pre&gt;&lt;code class=python&gt;len(route) - 1&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;?&lt;p&gt;Потому что маршрут:&lt;pre&gt;&lt;code&gt;[0, 1, 2]&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;содержит три вершины, но только два перехода:&lt;pre&gt;&lt;code&gt;0 → 1&#xA;1 → 2&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Поэтому расстояние в нашей условной метрической сетке равно двум.&lt;hr&gt;&lt;h2&gt;3. motion_controller.py — превращаем маршрут в движение&lt;/h2&gt;&lt;p&gt;До этого всё происходило только внутри Python.&lt;p&gt;Планировщик вернул:&lt;pre&gt;&lt;code class=python&gt;[0, 1, 2, 7, 12]&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Но реальный или виртуальный робот от списка Python никуда не поедет.&lt;p&gt;Нужно перейти от дискретного маршрута к управлению скоростями.&lt;p&gt;За это отвечает:&lt;pre&gt;&lt;code class=python&gt;class Motion:&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;hr&gt;&lt;h2&gt;Связь с ROS 2&lt;/h2&gt;&lt;p&gt;Контроллер создаёт publisher:&lt;pre&gt;&lt;code class=python&gt;self.publisher = node.create_publisher(    Twist,    &amp;#34;/RMC2/cmd_vel&amp;#34;,    10&#xA;)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Publisher можно представить как отправителя сообщений.&lt;p&gt;Он публикует сообщения типа:&lt;pre&gt;&lt;code&gt;geometry_msgs/msg/Twist&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;в topic:&lt;pre&gt;&lt;code&gt;/RMC2/cmd_vel&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Через &lt;code&gt;Twist&lt;/code&gt; задаются линейные и угловые скорости.&lt;p&gt;Нас интересуют:&lt;pre&gt;&lt;code class=python&gt;command.linear.x&#xA;command.angular.z&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Первая величина отвечает за движение вперёд.&lt;p&gt;Вторая — за вращение вокруг вертикальной оси.&lt;hr&gt;&lt;h2&gt;Получаем обратную связь&lt;/h2&gt;&lt;p&gt;Одновременно создаётся subscriber:&lt;pre&gt;&lt;code class=python&gt;node.create_subscription(    Odometry,    &amp;#34;/RMC2/odometry&amp;#34;,    self.odom_callback,    10&#xA;)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Это уже обратный канал:&lt;pre&gt;&lt;code&gt;робот  ↓&#xA;/RMC2/odometry  ↓&#xA;Motion&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;При поступлении нового сообщения ROS вызывает:&lt;pre&gt;&lt;code class=python&gt;self.odom_callback&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Это callback-функция.&lt;p&gt;Её не нужно самостоятельно запускать циклом.&lt;p&gt;ROS вызывает её, когда приходит новое сообщение.&lt;hr&gt;&lt;h2&gt;Что берём из Odometry&lt;/h2&gt;&lt;p&gt;В callback:&lt;pre&gt;&lt;code class=python&gt;self.x = msg.pose.pose.position.x&#xA;self.y = msg.pose.pose.position.y&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Получаем текущие координаты робота.&lt;p&gt;Но ориентация приходит в виде кватерниона:&lt;pre&gt;&lt;code class=python&gt;msg.pose.pose.orientation&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Для плоского движения четыре компонента кватерниона нам непосредственно не нужны.&lt;p&gt;Поэтому используется функция:&lt;pre&gt;&lt;code class=python&gt;quaternion_to_yaw()&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;и получаем один понятный угол:&lt;pre&gt;&lt;code&gt;yaw&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Например:&lt;pre&gt;&lt;code&gt;0°   → робот смотрит вдоль +X&#xA;90°  → вдоль +Y&#xA;180° → вдоль -X&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;hr&gt;&lt;h2&gt;Зачем angle_to_pi&lt;/h2&gt;&lt;p&gt;Представим:&lt;pre&gt;&lt;code&gt;текущий угол = +179°&#xA;целевой угол = -179°&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Обычное вычитание даст:&lt;pre&gt;&lt;code&gt;-358°&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Хотя физически роботу нужно повернуться всего на:&lt;pre&gt;&lt;code&gt;2°&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Поэтому ошибка приводится к диапазону:&lt;pre&gt;&lt;code&gt;[-π, +π]&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Функция:&lt;pre&gt;&lt;code class=python&gt;while angle &amp;gt; math.pi:    angle -= 2 * math.pi&#xA;while angle &amp;lt; -math.pi:    angle += 2 * math.pi&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;убирает неоднозначность перехода через &lt;code&gt;±π&lt;/code&gt;.&lt;hr&gt;&lt;h2&gt;Почему управление работает через таймер&lt;/h2&gt;&lt;p&gt;В конструкторе:&lt;pre&gt;&lt;code class=python&gt;node.create_timer(    0.05,    self.control&#xA;)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;ROS будет вызывать:&lt;pre&gt;&lt;code class=python&gt;self.control()&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;примерно каждые:&lt;pre&gt;&lt;code&gt;0.05 секунды&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;то есть:&lt;pre&gt;&lt;code&gt;20 раз в секунду&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Это и есть простой цикл управления.&lt;p&gt;Каждый цикл контроллер заново спрашивает:&lt;pre&gt;&lt;code&gt;Где я сейчас?&#xA;Куда мне нужно?&#xA;Как далеко осталось?&#xA;Куда я повернут?&#xA;Какую скорость отправить?&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;hr&gt;&lt;h2&gt;Начало движения&lt;/h2&gt;&lt;p&gt;Когда &lt;a href=http://main.py rel=&#34;noopener nofollow&#34;&gt;&lt;code&gt;main.py&lt;/code&gt;&lt;/a&gt; вызывает:&lt;pre&gt;&lt;code class=python&gt;motion.start(route)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;маршрут сохраняется:&lt;pre&gt;&lt;code class=python&gt;self.route = route&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;и:&lt;pre&gt;&lt;code class=python&gt;self.index = 1&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Почему не &lt;code&gt;0&lt;/code&gt;?&lt;p&gt;Если маршрут:&lt;pre&gt;&lt;code class=python&gt;[0, 1, 2, 7]&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;робот уже находится в &lt;code&gt;0&lt;/code&gt;.&lt;p&gt;Поэтому первая настоящая цель:&lt;pre&gt;&lt;code&gt;route[1] = 1&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;hr&gt;&lt;h2&gt;Вычисляем направление на цель&lt;/h2&gt;&lt;p&gt;Пусть следующая цель — маркер &lt;code&gt;7&lt;/code&gt;.&lt;p&gt;Получаем:&lt;pre&gt;&lt;code class=python&gt;target_x, target_y = self.graph.position(target)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;После этого:&lt;pre&gt;&lt;code class=python&gt;dx = target_x - self.x&#xA;dy = target_y - self.y&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Это вектор от робота до цели.&lt;p&gt;Расстояние:&lt;pre&gt;&lt;code class=python&gt;distance = math.hypot(dx, dy)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Требуемый угол:&lt;pre&gt;&lt;code class=python&gt;target_yaw = math.atan2(dy, dx)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;atan2()&lt;/code&gt; удобен тем, что сразу учитывает знаки &lt;code&gt;dx&lt;/code&gt; и &lt;code&gt;dy&lt;/code&gt; и правильно определяет квадрант.&lt;hr&gt;&lt;h2&gt;Ошибка ориентации&lt;/h2&gt;&lt;p&gt;Теперь известно:&lt;pre&gt;&lt;code&gt;куда робот смотрит сейчас&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;и:&lt;pre&gt;&lt;code&gt;куда он должен смотреть&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Поэтому:&lt;pre&gt;&lt;code class=python&gt;error = angle_to_pi(    target_yaw - self.yaw&#xA;)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Если:&lt;pre&gt;&lt;code&gt;error ≈ 0&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;робот смотрит практически точно на цель.&lt;p&gt;Если ошибка большая — сначала нужно повернуться.&lt;hr&gt;&lt;h2&gt;Логика «сначала повернись, потом езжай»&lt;/h2&gt;&lt;p&gt;В программе используется очень простой контроллер.&lt;p&gt;Если:&lt;pre&gt;&lt;code class=python&gt;abs(error) &amp;gt; self.angle_tolerance&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;то:&lt;pre&gt;&lt;code class=python&gt;command.linear.x = 0.0&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Робот не движется вперёд.&lt;p&gt;В зависимости от знака ошибки:&lt;pre&gt;&lt;code class=python&gt;command.angular.z = +0.60&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;или:&lt;pre&gt;&lt;code class=python&gt;command.angular.z = -0.60&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;То есть робот поворачивается на месте.&lt;p&gt;Когда угол становится достаточно точным, выполняется ветка &lt;code&gt;else&lt;/code&gt;:&lt;pre&gt;&lt;code class=python&gt;command.linear.x = self.linear_speed&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;и платформа начинает ехать вперёд.&lt;hr&gt;&lt;h2&gt;Коррекция курса&lt;/h2&gt;&lt;p&gt;Если просто один раз повернуть робота и затем ехать строго прямо, небольшая ошибка быстро накопится.&lt;p&gt;Поэтому во время движения:&lt;pre&gt;&lt;code class=python&gt;command.angular.z = 1.2 * error&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Это простейший пропорциональный регулятор.&lt;p&gt;Можно записать:&lt;pre&gt;&lt;code&gt;ω = Kp × e&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;где:&lt;pre&gt;&lt;code&gt;ω  — угловая скорость&#xA;Kp — коэффициент 1.2&#xA;e  — ошибка направления&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Чем сильнее робот отклонился, тем сильнее он корректирует курс.&lt;p&gt;Но бесконечно большую команду давать нельзя.&lt;p&gt;Поэтому:&lt;pre&gt;&lt;code class=python&gt;if command.angular.z &amp;gt; 0.30:    command.angular.z = 0.30&#xA;if command.angular.z &amp;lt; -0.30:    command.angular.z = -0.30&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Получается ограничение:&lt;pre&gt;&lt;code&gt;-0.30 ≤ angular.z ≤ 0.30&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;hr&gt;&lt;h2&gt;Как робот понимает, что приехал&lt;/h2&gt;&lt;p&gt;Используется:&lt;pre&gt;&lt;code class=python&gt;if distance &amp;lt; self.distance_tolerance:&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;где:&lt;pre&gt;&lt;code class=python&gt;self.distance_tolerance = 0.12&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;То есть попадать математически точно в:&lt;pre&gt;&lt;code&gt;(2.000000, 3.000000)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;не требуется.&lt;p&gt;Достаточно оказаться ближе 12 сантиметров.&lt;p&gt;После этого:&lt;pre&gt;&lt;code class=python&gt;self.index += 1&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;и следующая клетка маршрута становится новой целью.&lt;hr&gt;&lt;h2&gt;4. obstacle_detector.py — находим новое препятствие&lt;/h2&gt;&lt;p&gt;Теперь самая интересная сенсорная часть.&lt;p&gt;Робот приехал к растениям.&lt;p&gt;Мы хотим смоделировать ситуацию:&lt;blockquote&gt;&lt;p&gt;Пока робот выполнял работу, на пути появился новый объект.&lt;/blockquote&gt;&lt;p&gt;Для этого используется &lt;code&gt;LaserScan&lt;/code&gt;.&lt;hr&gt;&lt;h2&gt;Подписка на лидар&lt;/h2&gt;&lt;p&gt;В конструкторе:&lt;pre&gt;&lt;code class=python&gt;node.create_subscription(    LaserScan,    &amp;#34;/RMC2/scan&amp;#34;,    self.scan_callback,    qos_profile_sensor_data&#xA;)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;При каждом новом скане вызывается:&lt;pre&gt;&lt;code class=python&gt;scan_callback()&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;А callback просто сохраняет последнее сообщение:&lt;pre&gt;&lt;code class=python&gt;self.scan = msg&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;То есть объект &lt;code&gt;ObstacleDetector&lt;/code&gt; всегда содержит самый свежий доступный скан.&lt;hr&gt;&lt;h2&gt;Что такое LaserScan&lt;/h2&gt;&lt;p&gt;Упрощённо лидар возвращает массив расстояний:&lt;pre&gt;&lt;code&gt;ranges = [    2.31,    2.29,    2.25,    ...&#xA;]&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Каждый элемент соответствует определённому углу.&lt;p&gt;Угол луча с индексом &lt;code&gt;i&lt;/code&gt;:&lt;pre&gt;&lt;code class=python&gt;angle = (    scan.angle_min    + i * scan.angle_increment&#xA;)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;То есть лидар фактически даёт набор полярных координат:&lt;pre&gt;&lt;code&gt;(угол, расстояние)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;hr&gt;&lt;h2&gt;Сохраняем фон&lt;/h2&gt;&lt;p&gt;До установки препятствия:&lt;pre&gt;&lt;code class=python&gt;self.baseline = list(    self.scan.ranges&#xA;)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;baseline&lt;/code&gt; — снимок окружающей среды.&lt;p&gt;Например:&lt;pre&gt;&lt;code&gt;луч №120 → 3.8 м&#xA;луч №121 → 3.9 м&#xA;луч №122 → ∞&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;После установки ящика:&lt;pre&gt;&lt;code&gt;луч №120 → 1.7 м&#xA;луч №121 → 1.72 м&#xA;луч №122 → 1.75 м&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Теперь можно искать разницу.&lt;hr&gt;&lt;h2&gt;Фильтрация неправильных измерений&lt;/h2&gt;&lt;p&gt;Перед анализом проверяется:&lt;pre&gt;&lt;code class=python&gt;math.isfinite(new_range)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Потому что лидар может возвращать:&lt;pre&gt;&lt;code&gt;inf&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;если препятствие не обнаружено.&lt;p&gt;Также проверяются физические пределы датчика:&lt;pre&gt;&lt;code class=python&gt;new_range &amp;lt; self.scan.range_min&#xA;new_range &amp;gt; self.scan.range_max&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Такие измерения пропускаются через:&lt;pre&gt;&lt;code class=python&gt;continue&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;hr&gt;&lt;h2&gt;Как определяется новый объект&lt;/h2&gt;&lt;p&gt;Есть два варианта.&lt;p&gt;Первый:&lt;pre&gt;&lt;code class=python&gt;if not math.isfinite(old_range):    new_object = True&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Раньше:&lt;pre&gt;&lt;code&gt;∞&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;а теперь:&lt;pre&gt;&lt;code&gt;1.8 м&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Значит перед лидаром действительно что-то появилось.&lt;p&gt;Второй:&lt;pre&gt;&lt;code class=python&gt;elif old_range - new_range &amp;gt;= self.min_change:    new_object = True&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;где:&lt;pre&gt;&lt;code class=python&gt;self.min_change = 0.20&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Например:&lt;pre&gt;&lt;code&gt;раньше = 3.4 м&#xA;сейчас = 1.9 м&#xA;3.4 - 1.9 = 1.5 м&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Изменение намного больше 20 см, значит луч считается принадлежащим новому объекту.&lt;hr&gt;&lt;h2&gt;Из полярных координат в декартовы&lt;/h2&gt;&lt;p&gt;Допустим, лидар сообщает:&lt;pre&gt;&lt;code&gt;range = r&#xA;angle = α&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Получаем:&lt;pre&gt;&lt;code class=python&gt;local_x = new_range * math.cos(angle)&#xA;local_y = new_range * math.sin(angle)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Это координаты относительно самого робота.&lt;p&gt;Если объект находится прямо перед ним:&lt;pre&gt;&lt;code&gt;local_x &amp;gt; 0&#xA;local_y ≈ 0&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Если слева:&lt;pre&gt;&lt;code&gt;local_y &amp;gt; 0&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Но есть проблема.&lt;p&gt;Наш граф существует не в системе координат робота, а в мировой системе Webots/одометрии.&lt;hr&gt;&lt;h2&gt;Переход в мировую систему координат&lt;/h2&gt;&lt;p&gt;Мы знаем:&lt;pre&gt;&lt;code&gt;robot_x&#xA;robot_y&#xA;robot_yaw&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Поэтому выполняем поворот и перенос:&lt;pre&gt;&lt;code class=python&gt;world_x = robot_x + (    local_x * math.cos(robot_yaw)    - local_y * math.sin(robot_yaw)&#xA;)&#xA;world_y = robot_y + (    local_x * math.sin(robot_yaw)    + local_y * math.cos(robot_yaw)&#xA;)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Это обычное двумерное преобразование координат.&lt;p&gt;В матричной форме:&lt;pre&gt;&lt;code&gt;[world_x]   [robot_x]   [ cosθ  -sinθ ] [local_x]&#xA;[world_y] = [robot_y] + [ sinθ   cosθ ] [local_y]&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Теперь точка лидара находится в той же системе координат, что и маркеры нашего &lt;code&gt;Graph&lt;/code&gt;.&lt;p&gt;И это ключевой момент всей связки:&lt;pre&gt;&lt;code&gt;LaserScan   ↓&#xA;локальная точка относительно робота   ↓&#xA;положение и yaw робота   ↓&#xA;мировая координата   ↓&#xA;ближайший маркер графа&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;hr&gt;&lt;h2&gt;Почему используется голосование&lt;/h2&gt;&lt;p&gt;Предположим, ящик перекрыл маркер &lt;code&gt;12&lt;/code&gt;.&lt;p&gt;Лидар увидит не одну точку, а целое облако:&lt;pre&gt;&lt;code&gt;(1.93, 2.04)&#xA;(1.97, 2.08)&#xA;(2.01, 2.05)&#xA;(2.06, 1.99)&#xA;...&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Каждая точка переводится в ближайший маркер:&lt;pre&gt;&lt;code class=python&gt;marker = self.graph.nearest_marker(...)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;И затем:&lt;pre&gt;&lt;code class=python&gt;votes[marker] = votes.get(marker, 0) + 1&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;dict.get(marker, 0)&lt;/code&gt; означает:&lt;blockquote&gt;&lt;p&gt;если такой ключ уже есть — взять его значение; если нет — использовать 0.&lt;/blockquote&gt;&lt;p&gt;Например:&lt;pre&gt;&lt;code&gt;первый луч:&#xA;votes[12] = 1&#xA;второй:&#xA;votes[12] = 2&#xA;третий:&#xA;votes[12] = 3&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;В итоге:&lt;pre&gt;&lt;code class=python&gt;{    11: 1,    12: 14,    13: 2&#xA;}&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Выбираем:&lt;pre&gt;&lt;code class=python&gt;best_marker = max(    votes,    key=votes.get&#xA;)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Получаем:&lt;pre&gt;&lt;code&gt;12&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;hr&gt;&lt;h2&gt;Защита от случайных точек&lt;/h2&gt;&lt;p&gt;Одного голоса недостаточно:&lt;pre&gt;&lt;code class=python&gt;if votes[best_marker] &amp;lt; self.min_votes:    return None&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;В программе:&lt;pre&gt;&lt;code class=python&gt;self.min_votes = 3&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;То есть минимум три lidar-точки должны подтвердить гипотезу.&lt;p&gt;Это простейшая фильтрация шума.&lt;hr&gt;&lt;h2&gt;5. main.py — собираем систему целиком&lt;/h2&gt;&lt;p&gt;Теперь у нас есть четыре отдельных механизма.&lt;p&gt;&lt;a href=http://main.py rel=&#34;noopener nofollow&#34;&gt;&lt;code&gt;main.py&lt;/code&gt;&lt;/a&gt; превращает их в единый эксперимент.&lt;p&gt;Сначала:&lt;pre&gt;&lt;code class=python&gt;rclpy.init(args=args)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;инициализируется ROS 2.&lt;p&gt;Создаётся node:&lt;pre&gt;&lt;code class=python&gt;node = Node(&amp;#34;module_b_navigation&amp;#34;)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Нода — участник ROS-системы, через который мы создаём publisher, subscriber, timer и работаем с параметрами.&lt;hr&gt;&lt;h2&gt;Параметры start_id и target_id&lt;/h2&gt;&lt;p&gt;Вместо жёстко прописанных:&lt;pre&gt;&lt;code class=python&gt;start = 0&#xA;target = 22&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;используются ROS-параметры:&lt;pre&gt;&lt;code class=python&gt;node.declare_parameter(&amp;#34;start_id&amp;#34;, 0)&#xA;node.declare_parameter(&amp;#34;target_id&amp;#34;, 22)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Значения &lt;code&gt;0&lt;/code&gt; и &lt;code&gt;22&lt;/code&gt; являются значениями по умолчанию.&lt;p&gt;При запуске их можно заменить:&lt;pre&gt;&lt;code class=bash&gt;ros2 run ... --ros-args \    -p start_id:=5 \    -p target_id:=24&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Код при этом менять не потребуется.&lt;hr&gt;&lt;h2&gt;Создание модулей&lt;/h2&gt;&lt;p&gt;Далее:&lt;pre&gt;&lt;code class=python&gt;graph = Graph()&#xA;planner = Planner(graph)&#xA;motion = Motion(node, graph)&#xA;detector = ObstacleDetector(node, graph)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Здесь хорошо видно зависимости.&lt;p&gt;&lt;code&gt;Planner&lt;/code&gt; получает &lt;code&gt;Graph&lt;/code&gt;:&lt;pre&gt;&lt;code&gt;Planner → Graph&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;Motion&lt;/code&gt; нужны:&lt;pre&gt;&lt;code&gt;ROS Node + Graph&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;потому что ему нужны ROS topics и координаты маркеров.&lt;p&gt;&lt;code&gt;ObstacleDetector&lt;/code&gt; также нужны:&lt;pre&gt;&lt;code&gt;ROS Node + Graph&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;потому что он получает LaserScan и переводит точки в клетки карты.&lt;hr&gt;&lt;h2&gt;Этап 1. Планирование&lt;/h2&gt;&lt;p&gt;Сначала препятствий нет:&lt;pre&gt;&lt;code class=python&gt;blocked = set()&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Строим маршрут:&lt;pre&gt;&lt;code class=python&gt;route, distance, turns = planner.plan(    start,    target,    blocked&#xA;)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Например:&lt;pre&gt;&lt;code&gt;route    = [0, 1, 2, 7, 12, 17, 22]&#xA;distance = 6&#xA;turns    = 1&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;hr&gt;&lt;h2&gt;Этап 2. Движение&lt;/h2&gt;&lt;p&gt;Передаём маршрут контроллеру:&lt;pre&gt;&lt;code class=python&gt;motion.start(route)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Но здесь появляется важный момент ROS.&lt;p&gt;Контроллер работает через callback и timer.&lt;p&gt;Если просто заблокировать программу обычным циклом, ROS перестанет нормально обрабатывать новые сообщения.&lt;p&gt;Поэтому:&lt;pre&gt;&lt;code class=python&gt;while motion.active and rclpy.ok():    rclpy.spin_once(        node,        timeout_sec=0.1    )&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;spin_once()&lt;/code&gt; говорит ROS:&lt;blockquote&gt;&lt;p&gt;обработай ожидающие события один раз.&lt;/blockquote&gt;&lt;p&gt;Благодаря этому продолжают работать:&lt;pre&gt;&lt;code&gt;Odometry callback&#xA;LaserScan callback&#xA;Motion timer&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Пока:&lt;pre&gt;&lt;code class=python&gt;motion.active == True&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;робот продолжает выполнять маршрут.&lt;hr&gt;&lt;h2&gt;Этап 3. Сохраняем состояние лидара&lt;/h2&gt;&lt;p&gt;Когда робот приехал:&lt;pre&gt;&lt;code class=python&gt;detector.save_baseline()&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;фиксирует состояние окружающей среды.&lt;p&gt;Это очень важно сделать &lt;strong&gt;до появления нового объекта&lt;/strong&gt;.&lt;hr&gt;&lt;h2&gt;Этап 4. Добавляем препятствие&lt;/h2&gt;&lt;p&gt;В экспериментальной версии это сделано вручную.&lt;p&gt;Программа выводит:&lt;pre&gt;&lt;code&gt;Поставьте ОДНО препятствие в Webots.&#xA;После этого введите y и нажмите Enter.&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;После:&lt;pre&gt;&lt;code&gt;y&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;программа продолжает работу.&lt;p&gt;Почему так?&lt;p&gt;Потому что на данном этапе задача — проверить сам алгоритм:&lt;pre&gt;&lt;code&gt;старый scan&#xA;+&#xA;новый scan&#xA;→&#xA;найти изменение&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Автоматическое появление объекта в Webots здесь не является основной частью эксперимента.&lt;hr&gt;&lt;h2&gt;Этап 5. Получаем свежий scan&lt;/h2&gt;&lt;p&gt;После нажатия Enter нельзя сразу использовать:&lt;pre&gt;&lt;code class=python&gt;detector.scan&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;В нём теоретически ещё может лежать предыдущее сообщение.&lt;p&gt;Поэтому несколько раз вызывается:&lt;pre&gt;&lt;code class=python&gt;rclpy.spin_once(...)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;чтобы ROS успел получить свежие сообщения лидара.&lt;hr&gt;&lt;h2&gt;Этап 6. Определяем препятствие&lt;/h2&gt;&lt;p&gt;Теперь:&lt;pre&gt;&lt;code class=python&gt;blocked_marker = detector.find_obstacle(    motion.x,    motion.y,    motion.yaw,    excluded={start, target}&#xA;)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Передаются:&lt;pre&gt;&lt;code&gt;текущий X робота&#xA;текущий Y&#xA;текущий yaw&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Они нужны для перевода lidar-точек в мировую систему координат.&lt;p&gt;Также:&lt;pre&gt;&lt;code class=python&gt;excluded={start, target}&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;не позволяет принять стартовую или целевую клетку за новое препятствие.&lt;p&gt;Если обнаружен:&lt;pre&gt;&lt;code&gt;blocked_marker = 12&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;создаём:&lt;pre&gt;&lt;code class=python&gt;blocked = {12}&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;hr&gt;&lt;h2&gt;Этап 7. Повторное планирование&lt;/h2&gt;&lt;p&gt;И здесь проявляется преимущество разделения программы на модули.&lt;p&gt;Нам не нужен второй алгоритм.&lt;p&gt;Снова вызывается тот же:&lt;pre&gt;&lt;code class=python&gt;planner.plan()&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;только теперь:&lt;pre&gt;&lt;code&gt;START  = предыдущая цель&#xA;TARGET = первоначальный старт&#xA;BLOCKED = {12}&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;То есть:&lt;pre&gt;&lt;code class=python&gt;return_route, distance, turns = planner.plan(    target,    start,    blocked&#xA;)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;BFS автоматически перестаёт рассматривать клетку &lt;code&gt;12&lt;/code&gt;, потому что &lt;code&gt;Graph.neighbors()&lt;/code&gt; её отфильтрует.&lt;p&gt;Получается цепочка:&lt;pre&gt;&lt;code&gt;ObstacleDetector        ↓      12        ↓&#xA;blocked = {12}        ↓      Graph        ↓&#xA;neighbors() не возвращает 12        ↓     Planner        ↓&#xA;строит другой маршрут&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Это, пожалуй, наиболее важное архитектурное место всей программы.&lt;hr&gt;&lt;h2&gt;Этап 8. Возвращение&lt;/h2&gt;&lt;p&gt;Новый маршрут снова передаётся:&lt;pre&gt;&lt;code class=python&gt;motion.start(return_route)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;и тот же &lt;code&gt;Motion&lt;/code&gt; выполняет его.&lt;p&gt;То есть ни контроллер движения, ни планировщик не имеют отдельных режимов:&lt;pre&gt;&lt;code&gt;&amp;#34;ехать туда&amp;#34;&#xA;&amp;#34;ехать обратно&amp;#34;&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Для них это просто разные списки координат.&lt;hr&gt;&lt;h2&gt;Зачем finally&lt;/h2&gt;&lt;p&gt;В конце используется:&lt;pre&gt;&lt;code class=python&gt;finally:    motion.stop()    node.destroy_node()    rclpy.shutdown()&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;finally&lt;/code&gt; выполняется независимо от того, как завершился блок &lt;code&gt;try&lt;/code&gt;.&lt;p&gt;Например, пользователь может остановить программу:&lt;pre&gt;&lt;code&gt;Ctrl+C&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Это вызывает:&lt;pre&gt;&lt;code&gt;KeyboardInterrupt&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;который перехватывается:&lt;pre&gt;&lt;code class=python&gt;except KeyboardInterrupt:    pass&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Но затем всё равно выполняется &lt;code&gt;finally&lt;/code&gt;.&lt;p&gt;Особенно важен:&lt;pre&gt;&lt;code class=python&gt;motion.stop()&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Потому что при завершении программы желательно явно отправить роботу нулевую скорость.&lt;hr&gt;&lt;h2&gt;Как все пять файлов работают вместе&lt;/h2&gt;&lt;p&gt;Теперь можно собрать всю систему в одну схему:&lt;pre&gt;&lt;code&gt;                 main.py                    │                    ↓             start=0 target=22                    │                    ↓                Planner                    │                    ↓                  Graph                    │                    ↓       [0,1,2,7,12,17,22]                    │                    ↓                 Motion              ↙            ↘      /RMC2/odometry    /RMC2/cmd_vel              ↘            ↙              мобильный робот                    │                    ↓                  цель                    │                    ↓            ObstacleDetector                    ↑              /RMC2/scan                    │                  LIDAR                    │                    ↓          новая точка препятствия                    │                    ↓             world X / world Y                    │                    ↓                  Graph                    │                    ↓           ближайший marker = 12                    │                    ↓              blocked={12}                    │                    ↓                 Planner                    │                    ↓             новый маршрут                    │                    ↓                 Motion                    │                    ↓             возврат робота&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;В результате получилась маленькая, но уже законченная навигационная система с обратной связью.&lt;p&gt;Это не просто:&lt;pre&gt;&lt;code&gt;найти путь&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;а цикл:&lt;pre&gt;&lt;code&gt;восприятие среды      ↓&#xA;представление пространства      ↓&#xA;принятие решения      ↓&#xA;управление      ↓&#xA;движение      ↓&#xA;новое восприятие среды      ↓&#xA;изменение решения&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Именно такой цикл затем можно развивать для робота-сборщика клубники.&lt;hr&gt;&lt;h2&gt;Как это будет развиваться для клубники&lt;/h2&gt;&lt;p&gt;Текущая версия решает только задачу мобильной навигации.&lt;p&gt;Но архитектура уже позволяет сверху добавить следующий уровень.&lt;p&gt;Когда &lt;code&gt;Motion&lt;/code&gt; сообщает о завершении маршрута:&lt;pre&gt;&lt;code&gt;ROBOT AT WORK POSITION&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;вместо ручной установки препятствия можно запускать модуль технического зрения:&lt;pre&gt;&lt;code&gt;камера  ↓&#xA;изображение растения  ↓&#xA;детектор ягод  ↓&#xA;спелая / неспелая  ↓&#xA;координаты центра ягоды  ↓&#xA;оценка XYZ&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;После этого управление получает манипулятор:&lt;pre&gt;&lt;code&gt;XYZ ягоды   ↓&#xA;траектория манипулятора   ↓&#xA;подвод захвата   ↓&#xA;срыв   ↓&#xA;перенос в контейнер&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Когда в текущей рабочей позиции больше нет доступных спелых ягод:&lt;pre&gt;&lt;code&gt;манипулятор закончил        ↓&#xA;навигация получает следующую цель        ↓&#xA;Planner        ↓&#xA;Motion        ↓&#xA;следующий участок растений&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;При этом лидар может работать постоянно и выполнять защитную навигационную функцию.&lt;p&gt;В перспективе полный цикл робота для сбора клубники будет выглядеть примерно так:&lt;pre&gt;&lt;code&gt;             КАРТА ТЕПЛИЦЫ                    │                    ↓              PLANNER                    │                    ↓               MOTION                    │                    ↓           рабочая позиция                    │                    ↓                КАМЕРА                    │                    ↓          обнаружение клубники                    │                    ↓          определение спелости                    │                    ↓             координаты XYZ                    │                    ↓              МАНИПУЛЯТОР                    │                    ↓               сбор ягоды                    │                    ↓         есть ещё спелые ягоды?              ↙             ↘            да               нет            ↓                 ↓       следующая ягода    следующий участок                              │                              ↓                           PLANNER&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Поэтому сетка 5×5 и BFS здесь являются не попыткой сразу построить промышленную систему навигации тепличного робота, а первым понятным уровнем, на котором можно отдельно проверить всю цепочку от планирования до реакции на сенсорные данные.&lt;p&gt;После этого отдельные простые компоненты можно постепенно заменять более серьёзными:&lt;pre&gt;&lt;code&gt;сетка 5×5    ↓&#xA;карта теплицы / occupancy grid&#xA;BFS    ↓&#xA;A* / Nav2 planner&#xA;простое движение по точкам    ↓&#xA;локальный контроллер Nav2&#xA;baseline LaserScan    ↓&#xA;постоянное обнаружение препятствий&#xA;фиксированные рабочие точки    ↓&#xA;точки, определяемые картой рядов&#xA;ручной этап сбора    ↓&#xA;камера + нейросеть + манипулятор&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h2&gt;Результат&lt;/h2&gt;&lt;p&gt;На текущем этапе реализован прототип навигационного модуля робота-сборщика клубники.&lt;p&gt;Робот умеет:&lt;ul&gt;&lt;li&gt;&lt;p&gt;работать с дискретной картой 5×5;&lt;li&gt;&lt;p&gt;определять соседние доступные позиции;&lt;li&gt;&lt;p&gt;искать кратчайший маршрут;&lt;li&gt;&lt;p&gt;при одинаковой длине маршрутов выбирать вариант с меньшим количеством поворотов;&lt;li&gt;&lt;p&gt;двигаться по маршруту с использованием одометрии;&lt;li&gt;&lt;p&gt;управляться через &lt;code&gt;Twist&lt;/code&gt;;&lt;li&gt;&lt;p&gt;получать данные 2D-лидара;&lt;li&gt;&lt;p&gt;обнаруживать появление нового препятствия;&lt;li&gt;&lt;p&gt;определять занятую клетку;&lt;li&gt;&lt;p&gt;исключать её из дальнейшего планирования;&lt;li&gt;&lt;p&gt;строить новый маршрут.&lt;/ul&gt;&lt;p&gt;При этом алгоритмы навигации специально реализованы самостоятельно, без использования готового Nav2. Для первого этапа это позволяет разобраться в базовой механике планирования и управления движением.&lt;hr&gt;&lt;h2&gt;Исходный код&lt;/h2&gt;&lt;p&gt;Весь пример разделён на несколько Python-файлов:&lt;pre&gt;&lt;code&gt;module_b_navigation/&#xA;│&#xA;├── __init__.py&#xA;├── main.py&#xA;├── graph.py&#xA;├── planner.py&#xA;├── motion_controller.py&#xA;└── obstacle_detector.py&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href=http://graph.py rel=&#34;noopener nofollow&#34;&gt;&lt;code&gt;graph.py&lt;/code&gt;&lt;/a&gt; содержит модель поля 5×5, координаты 25 маркеров и получение доступных соседних клеток.&lt;p&gt;&lt;a href=http://planner.py rel=&#34;noopener nofollow&#34;&gt;&lt;code&gt;planner.py&lt;/code&gt;&lt;/a&gt; содержит BFS-планировщик. Сначала выбирается минимальная длина маршрута, затем минимальное количество поворотов.&lt;p&gt;&lt;code&gt;motion_&lt;/code&gt;&lt;a href=http://controller.py rel=&#34;noopener nofollow&#34;&gt;&lt;code&gt;controller.py&lt;/code&gt;&lt;/a&gt; отвечает за фактическое движение мобильной платформы. Положение получается из:&lt;pre&gt;&lt;code&gt;/RMC2/odometry&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;а команды отправляются в:&lt;pre&gt;&lt;code&gt;/RMC2/cmd_vel&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;obstacle_&lt;/code&gt;&lt;a href=http://detector.py rel=&#34;noopener nofollow&#34;&gt;&lt;code&gt;detector.py&lt;/code&gt;&lt;/a&gt; получает:&lt;pre&gt;&lt;code&gt;/RMC2/scan&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;сохраняет исходный &lt;code&gt;LaserScan&lt;/code&gt;, сравнивает его с новым сканом и определяет, возле какого маркера появилось препятствие.&lt;p&gt;&lt;a href=http://main.py rel=&#34;noopener nofollow&#34;&gt;&lt;code&gt;main.py&lt;/code&gt;&lt;/a&gt; объединяет всё в один эксперимент:&lt;pre&gt;&lt;code&gt;построение маршрута&#xA;→ движение&#xA;→ сохранение лидара&#xA;→ появление препятствия&#xA;→ определение занятого маркера&#xA;→ перепланирование&#xA;→ возвращение&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h4&gt;Запуск&lt;/h4&gt;&lt;p&gt;Стартовый и конечный маркеры передаются параметрами ROS 2.&lt;p&gt;Например:&lt;pre&gt;&lt;code class=bash&gt;ros2 run &amp;lt;package_name&amp;gt; &amp;lt;executable_name&amp;gt; --ros-args \    -p start_id:=0 \    -p target_id:=22&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;После запуска робот строит маршрут и начинает движение.&lt;p&gt;В целевой точке программа предложит установить препятствие в Webots:&lt;pre&gt;&lt;code&gt;РОБОТ ОСТАНОВИЛСЯ В ЦЕЛЕВОЙ ТОЧКЕ&#xA;Поставьте ОДНО препятствие в Webots.&#xA;После этого введите y и нажмите Enter.&#xA;Препятствие установлено? [y]:&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;После ввода:&lt;pre&gt;&lt;code&gt;y&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;получается новый скан лидара, определяется занятый маркер и строится обратный маршрут.&lt;hr&gt;&lt;h2&gt;Что дальше&lt;/h2&gt;&lt;p&gt;Следующий этап — приблизить модель к реальной задаче сбора клубники.&lt;p&gt;В первую очередь я планирую добавить анализ лидара &lt;strong&gt;не после остановки, а непосредственно во время движения&lt;/strong&gt;:&lt;pre&gt;&lt;code&gt;робот движется      ↓&#xA;постоянно работает lidar      ↓&#xA;обнаружено препятствие?    ↙           ↘  нет            да   ↓              ↓&#xA;едем дальше    остановка                  ↓             обновление карты                  ↓             новый маршрут&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;После этого навигационный модуль можно объединять с системой технического зрения и манипулятором.&lt;p&gt;Отдельной интересной задачей станет уже непосредственно зрение для клубники: определение ягоды, оценка её спелости и получение пространственных координат для манипулятора.&lt;p&gt;В итоге хотелось бы получить полный экспериментальный цикл:&lt;pre&gt;&lt;code&gt;НАВИГАЦИЯ    ↓&#xA;робот приехал к кусту    ↓&#xA;ТЕХНИЧЕСКОЕ ЗРЕНИЕ    ↓&#xA;найдена спелая клубника    ↓&#xA;МАНИПУЛЯТОР    ↓&#xA;ягода собрана    ↓&#xA;НАВИГАЦИЯ    ↓&#xA;следующая рабочая позиция&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Таким образом, текущая система навигации — это не отдельный учебный пример, а один из базовых модулей будущего мобильного робота для автоматизированного сбора клубники.&lt;h2&gt;Полный код примера&lt;/h2&gt;&lt;p&gt;Ниже приведу полный код навигационной части прототипа робота для сбора клубники.&lt;p&gt;Проект разделён на несколько небольших модулей:&lt;pre&gt;&lt;code&gt;module_b_navigation/&#xA;│&#xA;├── __init__.py&#xA;├── main.py&#xA;├── graph.py&#xA;├── planner.py&#xA;├── motion_controller.py&#xA;└── obstacle_detector.py&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Такое разделение позволяет независимо изменять представление карты, алгоритм поиска пути, управление мобильной платформой и обработку лидара.&lt;h3&gt;graph.py&lt;/h3&gt;&lt;p&gt;Первый модуль описывает наше условное поле теплицы.&lt;pre&gt;&lt;code class=python&gt;import math&#xA;&#xA;&#xA;class Graph:&#xA;    &amp;#34;&amp;#34;&amp;#34;&#xA;    Простая карта поля 5x5.&#xA;&#xA;    Нумерация маркеров:&#xA;&#xA;        20 21 22 23 24&#xA;        15 16 17 18 19&#xA;        10 11 12 13 14&#xA;         5  6  7  8  9&#xA;         0  1  2  3  4&#xA;&#xA;    Расстояние между соседними маркерами принимаем равным 1 метру.&#xA;    &amp;#34;&amp;#34;&amp;#34;&#xA;&#xA;    SIZE = 5&#xA;&#xA;    def valid(self, marker):&#xA;        &amp;#34;&amp;#34;&amp;#34;Проверяет, существует ли такой маркер.&amp;#34;&amp;#34;&amp;#34;&#xA;        return 0 &amp;lt;= marker &amp;lt; 25&#xA;&#xA;    def position(self, marker):&#xA;        &amp;#34;&amp;#34;&amp;#34;&#xA;        Возвращает координаты маркера.&#xA;&#xA;        Например:&#xA;        marker = 12&#xA;&#xA;        x = 12 % 5  = 2&#xA;        y = 12 // 5 = 2&#xA;&#xA;        Значит маркер 12 находится в точке (2, 2).&#xA;        &amp;#34;&amp;#34;&amp;#34;&#xA;        x = marker % self.SIZE&#xA;        y = marker // self.SIZE&#xA;        return float(x), float(y)&#xA;&#xA;    def neighbors(self, marker, blocked=None):&#xA;        &amp;#34;&amp;#34;&amp;#34;&#xA;        Возвращает соседние клетки.&#xA;&#xA;        Порядок:&#xA;        вправо -&amp;gt; вверх -&amp;gt; влево -&amp;gt; вниз.&#xA;        &amp;#34;&amp;#34;&amp;#34;&#xA;&#xA;        if blocked is None:&#xA;            blocked = set()&#xA;&#xA;        row = marker // self.SIZE&#xA;        col = marker % self.SIZE&#xA;&#xA;        result = []&#xA;&#xA;        # Вправо&#xA;        if col &amp;lt; self.SIZE - 1:&#xA;            result.append(marker + 1)&#xA;&#xA;        # Вверх&#xA;        if row &amp;lt; self.SIZE - 1:&#xA;            result.append(marker + self.SIZE)&#xA;&#xA;        # Влево&#xA;        if col &amp;gt; 0:&#xA;            result.append(marker - 1)&#xA;&#xA;        # Вниз&#xA;        if row &amp;gt; 0:&#xA;            result.append(marker - self.SIZE)&#xA;&#xA;        # Убираем занятые клетки&#xA;        return [m for m in result if m not in blocked]&#xA;&#xA;    def direction(self, a, b):&#xA;        &amp;#34;&amp;#34;&amp;#34;&#xA;        Возвращает направление движения между двумя соседними маркерами.&#xA;        Нужно только для подсчёта поворотов.&#xA;        &amp;#34;&amp;#34;&amp;#34;&#xA;&#xA;        ax, ay = self.position(a)&#xA;        bx, by = self.position(b)&#xA;&#xA;        if bx &amp;gt; ax:&#xA;            return &amp;#34;RIGHT&amp;#34;&#xA;        if bx &amp;lt; ax:&#xA;            return &amp;#34;LEFT&amp;#34;&#xA;        if by &amp;gt; ay:&#xA;            return &amp;#34;UP&amp;#34;&#xA;        if by &amp;lt; ay:&#xA;            return &amp;#34;DOWN&amp;#34;&#xA;&#xA;    def nearest_marker(self, x, y, max_distance=0.75, excluded=None):&#xA;        &amp;#34;&amp;#34;&amp;#34;&#xA;        По координатам точки ищет ближайший маркер.&#xA;&#xA;        Это используется для лидара:&#xA;        лидар дал координату препятствия -&amp;gt; определяем номер клетки.&#xA;        &amp;#34;&amp;#34;&amp;#34;&#xA;&#xA;        if excluded is None:&#xA;            excluded = set()&#xA;&#xA;        best_marker = None&#xA;        best_distance = 999999.0&#xA;&#xA;        for marker in range(25):&#xA;&#xA;            if marker in excluded:&#xA;                continue&#xA;&#xA;            mx, my = self.position(marker)&#xA;&#xA;            distance = math.hypot(x - mx, y - my)&#xA;&#xA;            if distance &amp;lt; best_distance:&#xA;                best_distance = distance&#xA;                best_marker = marker&#xA;&#xA;        if best_distance &amp;gt; max_distance:&#xA;            return None&#xA;&#xA;        return best_marker&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;hr&gt;&lt;h3&gt;planner.py&lt;/h3&gt;&lt;p&gt;Следующий модуль занимается поиском маршрута.&lt;p&gt;Здесь используется BFS. Но если найдено несколько одинаково коротких маршрутов, выбирается маршрут с меньшим количеством поворотов.&lt;pre&gt;&lt;code class=python&gt;from collections import deque&#xA;&#xA;&#xA;class Planner:&#xA;    &amp;#34;&amp;#34;&amp;#34;&#xA;    Строит маршрут по клеткам.&#xA;&#xA;    Критерии:&#xA;    1. Сначала ищем самый короткий маршрут.&#xA;    2. Если коротких маршрутов несколько,&#xA;       выбираем маршрут с меньшим количеством поворотов.&#xA;    3. Если и это одинаково,&#xA;       выбираем маршрут с меньшими ID маркеров.&#xA;    &amp;#34;&amp;#34;&amp;#34;&#xA;&#xA;    def __init__(self, graph):&#xA;        self.graph = graph&#xA;&#xA;    def count_turns(self, route):&#xA;        &amp;#34;&amp;#34;&amp;#34;Считает количество поворотов в маршруте.&amp;#34;&amp;#34;&amp;#34;&#xA;&#xA;        if len(route) &amp;lt; 3:&#xA;            return 0&#xA;&#xA;        turns = 0&#xA;&#xA;        old_direction = self.graph.direction(&#xA;            route[0],&#xA;            route[1]&#xA;        )&#xA;&#xA;        for i in range(1, len(route) - 1):&#xA;&#xA;            new_direction = self.graph.direction(&#xA;                route[i],&#xA;                route[i + 1]&#xA;            )&#xA;&#xA;            if new_direction != old_direction:&#xA;                turns += 1&#xA;&#xA;            old_direction = new_direction&#xA;&#xA;        return turns&#xA;&#xA;    def plan(self, start, target, blocked=None):&#xA;        &amp;#34;&amp;#34;&amp;#34;&#xA;        Возвращает:&#xA;&#xA;            route, distance, turns&#xA;&#xA;        Например:&#xA;&#xA;            [0, 1, 2, 7, 12, 17, 22], 6, 1&#xA;        &amp;#34;&amp;#34;&amp;#34;&#xA;&#xA;        if blocked is None:&#xA;            blocked = set()&#xA;&#xA;        if not self.graph.valid(start):&#xA;            raise ValueError(&amp;#34;Неверный стартовый маркер&amp;#34;)&#xA;&#xA;        if not self.graph.valid(target):&#xA;            raise ValueError(&amp;#34;Неверный целевой маркер&amp;#34;)&#xA;&#xA;        if start in blocked:&#xA;            raise ValueError(&amp;#34;Стартовая клетка занята&amp;#34;)&#xA;&#xA;        if target in blocked:&#xA;            raise ValueError(&amp;#34;Целевая клетка занята&amp;#34;)&#xA;&#xA;        if start == target:&#xA;            return [start], 0, 0&#xA;&#xA;        # В очереди лежат целые маршруты.&#xA;        queue = deque()&#xA;        queue.append([start])&#xA;&#xA;        # Здесь будем хранить все самые короткие маршруты до цели.&#xA;        found_routes = []&#xA;        shortest_length = None&#xA;&#xA;        while queue:&#xA;&#xA;            route = queue.popleft()&#xA;&#xA;            # Если уже нашли более короткий маршрут,&#xA;            # этот маршрут продолжать бессмысленно.&#xA;            if shortest_length is not None:&#xA;                if len(route) &amp;gt; shortest_length:&#xA;                    break&#xA;&#xA;            current = route[-1]&#xA;&#xA;            if current == target:&#xA;                shortest_length = len(route)&#xA;                found_routes.append(route)&#xA;                continue&#xA;&#xA;            for next_marker in self.graph.neighbors(current, blocked):&#xA;&#xA;                # Не заходим второй раз в клетку,&#xA;                # которая уже есть в этом маршруте.&#xA;                if next_marker in route:&#xA;                    continue&#xA;&#xA;                new_route = route + [next_marker]&#xA;                queue.append(new_route)&#xA;&#xA;        if not found_routes:&#xA;            raise RuntimeError(&amp;#34;Маршрут не найден&amp;#34;)&#xA;&#xA;        # Из всех кратчайших маршрутов выбираем:&#xA;        # сначала по количеству поворотов,&#xA;        # потом по самим ID маркеров.&#xA;        best_route = min(&#xA;            found_routes,&#xA;            key=lambda route: (&#xA;                self.count_turns(route),&#xA;                route&#xA;            )&#xA;        )&#xA;&#xA;        distance = len(best_route) - 1&#xA;        turns = self.count_turns(best_route)&#xA;&#xA;        return best_route, distance, turns&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;hr&gt;&lt;h3&gt;motion_controller.py&lt;/h3&gt;&lt;p&gt;Теперь нужен контроллер, который заставит мобильную платформу физически выполнить полученный маршрут в Webots.&lt;p&gt;Он получает положение из одометрии и отправляет команды скорости через &lt;code&gt;Twist&lt;/code&gt;.&lt;pre&gt;&lt;code class=python&gt;import math&#xA;&#xA;from geometry_msgs.msg import Twist&#xA;from nav_msgs.msg import Odometry&#xA;&#xA;&#xA;def angle_to_pi(angle):&#xA;    &amp;#34;&amp;#34;&amp;#34;&#xA;    Приводит угол к диапазону от -pi до +pi.&#xA;    &amp;#34;&amp;#34;&amp;#34;&#xA;&#xA;    while angle &amp;gt; math.pi:&#xA;        angle -= 2 * math.pi&#xA;&#xA;    while angle &amp;lt; -math.pi:&#xA;        angle += 2 * math.pi&#xA;&#xA;    return angle&#xA;&#xA;&#xA;def quaternion_to_yaw(q):&#xA;    &amp;#34;&amp;#34;&amp;#34;&#xA;    ROS хранит ориентацию как quaternion.&#xA;    Для движения по плоскости нам нужен только угол вокруг Z (yaw).&#xA;    &amp;#34;&amp;#34;&amp;#34;&#xA;&#xA;    sin_yaw = 2 * (q.w * q.z + q.x * q.y)&#xA;    cos_yaw = 1 - 2 * (q.y * q.y + q.z * q.z)&#xA;&#xA;    return math.atan2(sin_yaw, cos_yaw)&#xA;&#xA;&#xA;class Motion:&#xA;    &amp;#34;&amp;#34;&amp;#34;&#xA;    Простое движение робота по готовому маршруту.&#xA;&#xA;    Класс:&#xA;    - получает odometry;&#xA;    - знает положение робота;&#xA;    - поворачивает робота к следующему маркеру;&#xA;    - едет к нему;&#xA;    - после достижения переходит к следующему.&#xA;    &amp;#34;&amp;#34;&amp;#34;&#xA;&#xA;    def __init__(self, node, graph):&#xA;&#xA;        self.node = node&#xA;        self.graph = graph&#xA;&#xA;        # Команды скорости робота&#xA;        self.publisher = node.create_publisher(&#xA;            Twist,&#xA;            &amp;#34;/RMC2/cmd_vel&amp;#34;,&#xA;            10&#xA;        )&#xA;&#xA;        # Положение робота&#xA;        node.create_subscription(&#xA;            Odometry,&#xA;            &amp;#34;/RMC2/odometry&amp;#34;,&#xA;            self.odom_callback,&#xA;            10&#xA;        )&#xA;&#xA;        # Управление вызывается каждые 0.05 секунды&#xA;        node.create_timer(&#xA;            0.05,&#xA;            self.control&#xA;        )&#xA;&#xA;        self.x = None&#xA;        self.y = None&#xA;        self.yaw = None&#xA;&#xA;        self.route = []&#xA;        self.index = 0&#xA;&#xA;        # Пока active == True, робот выполняет маршрут.&#xA;        self.active = False&#xA;&#xA;        # Скорости&#xA;        self.linear_speed = 0.20&#xA;        self.angular_speed = 0.60&#xA;&#xA;        # Допуски&#xA;        self.distance_tolerance = 0.12&#xA;        self.angle_tolerance = math.radians(5)&#xA;&#xA;    def odom_callback(self, msg):&#xA;        &amp;#34;&amp;#34;&amp;#34;Получаем текущее положение робота.&amp;#34;&amp;#34;&amp;#34;&#xA;&#xA;        self.x = msg.pose.pose.position.x&#xA;        self.y = msg.pose.pose.position.y&#xA;        self.yaw = quaternion_to_yaw(&#xA;            msg.pose.pose.orientation&#xA;        )&#xA;&#xA;    def start(self, route):&#xA;        &amp;#34;&amp;#34;&amp;#34;Запускает движение по маршруту.&amp;#34;&amp;#34;&amp;#34;&#xA;&#xA;        self.route = route&#xA;&#xA;        # route[0] — клетка, где робот уже находится.&#xA;        # Поэтому начинаем с route[1].&#xA;        self.index = 1&#xA;&#xA;        if len(route) &amp;lt; 2:&#xA;            self.active = False&#xA;            return&#xA;&#xA;        self.active = True&#xA;&#xA;        self.node.get_logger().info(&#xA;            f&amp;#34;movement_start route={route}&amp;#34;&#xA;        )&#xA;&#xA;    def stop(self):&#xA;        &amp;#34;&amp;#34;&amp;#34;Посылает нулевую скорость.&amp;#34;&amp;#34;&amp;#34;&#xA;        self.publisher.publish(Twist())&#xA;&#xA;    def finish(self):&#xA;        &amp;#34;&amp;#34;&amp;#34;Останавливает робот и завершает маршрут.&amp;#34;&amp;#34;&amp;#34;&#xA;&#xA;        self.stop()&#xA;        self.active = False&#xA;&#xA;        self.node.get_logger().info(&#xA;            &amp;#34;movement_stop&amp;#34;&#xA;        )&#xA;&#xA;    def control(self):&#xA;        &amp;#34;&amp;#34;&amp;#34;&#xA;        Основной алгоритм движения.&#xA;&#xA;        1. Берём следующий маркер.&#xA;        2. Вычисляем, где он находится.&#xA;        3. Если робот смотрит не туда — поворачиваемся.&#xA;        4. Если смотрит туда — едем прямо.&#xA;        5. Если приехали — берём следующий маркер.&#xA;        &amp;#34;&amp;#34;&amp;#34;&#xA;&#xA;        if not self.active:&#xA;            return&#xA;&#xA;        # Ждём первую odometry.&#xA;        if self.x is None:&#xA;            return&#xA;&#xA;        # Если маршрут закончился.&#xA;        if self.index &amp;gt;= len(self.route):&#xA;            self.finish()&#xA;            return&#xA;&#xA;        target = self.route[self.index]&#xA;&#xA;        target_x, target_y = self.graph.position(target)&#xA;&#xA;        dx = target_x - self.x&#xA;        dy = target_y - self.y&#xA;&#xA;        distance = math.hypot(dx, dy)&#xA;&#xA;        # Угол, куда нужно смотреть.&#xA;        target_yaw = math.atan2(dy, dx)&#xA;&#xA;        # Ошибка по углу.&#xA;        error = angle_to_pi(&#xA;            target_yaw - self.yaw&#xA;        )&#xA;&#xA;        # --------------------------------------------------&#xA;        # Уже приехали к маркеру&#xA;        # --------------------------------------------------&#xA;&#xA;        if distance &amp;lt; self.distance_tolerance:&#xA;&#xA;            self.node.get_logger().info(&#xA;                f&amp;#34;marker_reached: {target}&amp;#34;&#xA;            )&#xA;&#xA;            self.stop()&#xA;            self.index += 1&#xA;&#xA;            if self.index &amp;gt;= len(self.route):&#xA;                self.finish()&#xA;&#xA;            return&#xA;&#xA;        command = Twist()&#xA;&#xA;        # --------------------------------------------------&#xA;        # Сначала поворачиваемся&#xA;        # --------------------------------------------------&#xA;&#xA;        if abs(error) &amp;gt; self.angle_tolerance:&#xA;&#xA;            command.linear.x = 0.0&#xA;&#xA;            if error &amp;gt; 0:&#xA;                command.angular.z = self.angular_speed&#xA;            else:&#xA;                command.angular.z = -self.angular_speed&#xA;&#xA;        # --------------------------------------------------&#xA;        # Потом едем прямо&#xA;        # --------------------------------------------------&#xA;&#xA;        else:&#xA;&#xA;            command.linear.x = self.linear_speed&#xA;&#xA;            # Небольшая коррекция курса.&#xA;            command.angular.z = 1.2 * error&#xA;&#xA;            # Ограничиваем скорость поворота.&#xA;            if command.angular.z &amp;gt; 0.30:&#xA;                command.angular.z = 0.30&#xA;&#xA;            if command.angular.z &amp;lt; -0.30:&#xA;                command.angular.z = -0.30&#xA;&#xA;        self.publisher.publish(command)&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;hr&gt;&lt;h3&gt;obstacle_detector.py&lt;/h3&gt;&lt;p&gt;Этот модуль отвечает за лидар.&lt;p&gt;Перед появлением препятствия сохраняется исходный скан. После установки объекта новый скан сравнивается с исходным.&lt;pre&gt;&lt;code class=python&gt;import math&#xA;&#xA;from rclpy.qos import qos_profile_sensor_data&#xA;from sensor_msgs.msg import LaserScan&#xA;&#xA;&#xA;class ObstacleDetector:&#xA;    &amp;#34;&amp;#34;&amp;#34;&#xA;    Ищет ОДНО новое препятствие.&#xA;&#xA;    Идея очень простая:&#xA;&#xA;    1. До появления препятствия запоминаем lidar.&#xA;    2. Пользователь ставит препятствие.&#xA;    3. Получаем новый lidar.&#xA;    4. Ищем лучи, у которых расстояние сильно уменьшилось.&#xA;    5. Переводим эти точки в координаты мира.&#xA;    6. Смотрим, возле какого маркера больше всего таких точек.&#xA;    &amp;#34;&amp;#34;&amp;#34;&#xA;&#xA;    def __init__(self, node, graph):&#xA;&#xA;        self.node = node&#xA;        self.graph = graph&#xA;&#xA;        # Последнее сообщение лидара.&#xA;        self.scan = None&#xA;&#xA;        # Расстояния до появления препятствия.&#xA;        self.baseline = None&#xA;&#xA;        node.create_subscription(&#xA;            LaserScan,&#xA;            &amp;#34;/RMC2/scan&amp;#34;,&#xA;            self.scan_callback,&#xA;            qos_profile_sensor_data&#xA;        )&#xA;&#xA;        # Если расстояние уменьшилось минимум на 20 см,&#xA;        # считаем, что появился новый объект.&#xA;        self.min_change = 0.20&#xA;&#xA;        # Насколько далеко точка может быть от центра клетки.&#xA;        self.max_marker_distance = 0.75&#xA;&#xA;        # Минимум lidar-точек, чтобы поверить результату.&#xA;        self.min_votes = 3&#xA;&#xA;    def scan_callback(self, msg):&#xA;        &amp;#34;&amp;#34;&amp;#34;Просто сохраняем самый свежий scan.&amp;#34;&amp;#34;&amp;#34;&#xA;        self.scan = msg&#xA;&#xA;    def save_baseline(self):&#xA;        &amp;#34;&amp;#34;&amp;#34;&#xA;        Запоминаем lidar до появления препятствия.&#xA;        &amp;#34;&amp;#34;&amp;#34;&#xA;&#xA;        if self.scan is None:&#xA;            return False&#xA;&#xA;        self.baseline = list(self.scan.ranges)&#xA;        return True&#xA;&#xA;    def find_obstacle(self, robot_x, robot_y, robot_yaw, excluded=None):&#xA;        &amp;#34;&amp;#34;&amp;#34;&#xA;        Возвращает номер занятой клетки.&#xA;        Если препятствие не найдено — возвращает None.&#xA;        &amp;#34;&amp;#34;&amp;#34;&#xA;&#xA;        if excluded is None:&#xA;            excluded = set()&#xA;&#xA;        if self.scan is None:&#xA;            return None&#xA;&#xA;        if self.baseline is None:&#xA;            return None&#xA;&#xA;        if len(self.baseline) != len(self.scan.ranges):&#xA;            return None&#xA;&#xA;        # votes:&#xA;        # ключ   = номер маркера&#xA;        # значение = сколько lidar-точек попало возле него&#xA;        votes = {}&#xA;&#xA;        # Для красивого лога сохраняем координаты точек.&#xA;        points = {}&#xA;&#xA;        for i in range(len(self.scan.ranges)):&#xA;&#xA;            old_range = self.baseline[i]&#xA;            new_range = self.scan.ranges[i]&#xA;&#xA;            # Текущая дальность должна быть нормальной.&#xA;            if not math.isfinite(new_range):&#xA;                continue&#xA;&#xA;            if new_range &amp;lt; self.scan.range_min:&#xA;                continue&#xA;&#xA;            if new_range &amp;gt; self.scan.range_max:&#xA;                continue&#xA;&#xA;            # Проверяем, появился ли здесь новый объект.&#xA;            new_object = False&#xA;&#xA;            # Раньше лидар ничего не видел,&#xA;            # а теперь увидел объект.&#xA;            if not math.isfinite(old_range):&#xA;                new_object = True&#xA;&#xA;            # Или объект стал как минимум на 20 см ближе.&#xA;            elif old_range - new_range &amp;gt;= self.min_change:&#xA;                new_object = True&#xA;&#xA;            if not new_object:&#xA;                continue&#xA;&#xA;            # Угол конкретного луча лидара.&#xA;            angle = (&#xA;                self.scan.angle_min&#xA;                + i * self.scan.angle_increment&#xA;            )&#xA;&#xA;            # ------------------------------------------&#xA;            # Координаты точки относительно робота&#xA;            # ------------------------------------------&#xA;&#xA;            local_x = new_range * math.cos(angle)&#xA;            local_y = new_range * math.sin(angle)&#xA;&#xA;            # ------------------------------------------&#xA;            # Переводим точку в координаты мира&#xA;            # ------------------------------------------&#xA;&#xA;            world_x = robot_x + (&#xA;                local_x * math.cos(robot_yaw)&#xA;                - local_y * math.sin(robot_yaw)&#xA;            )&#xA;&#xA;            world_y = robot_y + (&#xA;                local_x * math.sin(robot_yaw)&#xA;                + local_y * math.cos(robot_yaw)&#xA;            )&#xA;&#xA;            # Определяем ближайший маркер.&#xA;            marker = self.graph.nearest_marker(&#xA;                world_x,&#xA;                world_y,&#xA;                self.max_marker_distance,&#xA;                excluded&#xA;            )&#xA;&#xA;            if marker is None:&#xA;                continue&#xA;&#xA;            # Голосуем за этот маркер.&#xA;            votes[marker] = votes.get(marker, 0) + 1&#xA;&#xA;            if marker not in points:&#xA;                points[marker] = []&#xA;&#xA;            points[marker].append(&#xA;                (world_x, world_y)&#xA;            )&#xA;&#xA;        if not votes:&#xA;            return None&#xA;&#xA;        # Маркер с максимальным количеством голосов.&#xA;        best_marker = max(&#xA;            votes,&#xA;            key=votes.get&#xA;        )&#xA;&#xA;        self.node.get_logger().info(&#xA;            f&amp;#34;obstacle_votes={dict(sorted(votes.items()))}&amp;#34;&#xA;        )&#xA;&#xA;        if votes[best_marker] &amp;lt; self.min_votes:&#xA;            return None&#xA;&#xA;        # Средняя координата lidar-точек,&#xA;        # которые проголосовали за победивший маркер.&#xA;        best_points = points[best_marker]&#xA;&#xA;        avg_x = sum(p[0] for p in best_points) / len(best_points)&#xA;        avg_y = sum(p[1] for p in best_points) / len(best_points)&#xA;&#xA;        self.node.get_logger().info(&#xA;            f&amp;#34;obstacle_world_position≈({avg_x:.3f}, {avg_y:.3f})&amp;#34;&#xA;        )&#xA;&#xA;        return best_marker&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;hr&gt;&lt;h3&gt;main.py&lt;/h3&gt;&lt;p&gt;И наконец главный файл.&lt;p&gt;Он объединяет планировщик, контроллер движения и лидар в один эксперимент.&lt;pre&gt;&lt;code class=python&gt;import time&#xA;&#xA;import rclpy&#xA;from rclpy.node import Node&#xA;&#xA;from module_b_navigation.graph import Graph&#xA;from module_b_navigation.planner import Planner&#xA;from module_b_navigation.motion_controller import Motion&#xA;from module_b_navigation.obstacle_detector import ObstacleDetector&#xA;&#xA;&#xA;def wait_robot(node, motion):&#xA;    &amp;#34;&amp;#34;&amp;#34;&#xA;    Пока робот едет, даём ROS обрабатывать сообщения.&#xA;&#xA;    spin_once означает:&#xA;    &amp;#34;обработай новые сообщения ROS один раз&amp;#34;.&#xA;    &amp;#34;&amp;#34;&amp;#34;&#xA;&#xA;    while motion.active and rclpy.ok():&#xA;        rclpy.spin_once(&#xA;            node,&#xA;            timeout_sec=0.1&#xA;        )&#xA;&#xA;&#xA;def wait_for_new_scan(node):&#xA;    &amp;#34;&amp;#34;&amp;#34;&#xA;    После того как пользователь поставил препятствие,&#xA;    несколько раз обрабатываем ROS-сообщения,&#xA;    чтобы точно получить свежий lidar scan.&#xA;    &amp;#34;&amp;#34;&amp;#34;&#xA;&#xA;    for i in range(10):&#xA;        rclpy.spin_once(&#xA;            node,&#xA;            timeout_sec=0.1&#xA;        )&#xA;&#xA;&#xA;def print_route(title, start, target, blocked, route, distance, turns):&#xA;    &amp;#34;&amp;#34;&amp;#34;Красиво печатает найденный маршрут.&amp;#34;&amp;#34;&amp;#34;&#xA;&#xA;    print()&#xA;    print(&amp;#34;========================================&amp;#34;)&#xA;    print(title)&#xA;    print(&amp;#34;========================================&amp;#34;)&#xA;    print(&amp;#34;Start marker :&amp;#34;, start)&#xA;    print(&amp;#34;Target marker:&amp;#34;, target)&#xA;    print(&amp;#34;Blocked      :&amp;#34;, sorted(blocked))&#xA;    print()&#xA;    print(&amp;#34;ROUTE    :&amp;#34;, route)&#xA;    print(&amp;#34;DISTANCE :&amp;#34;, distance)&#xA;    print(&amp;#34;TURNS    :&amp;#34;, turns)&#xA;    print(&amp;#34;========================================&amp;#34;)&#xA;    print()&#xA;&#xA;&#xA;def main(args=None):&#xA;&#xA;    # ------------------------------------------------------&#xA;    # Запускаем ROS&#xA;    # ------------------------------------------------------&#xA;&#xA;    rclpy.init(args=args)&#xA;&#xA;    node = Node(&amp;#34;module_b_navigation&amp;#34;)&#xA;&#xA;    # ------------------------------------------------------&#xA;    # Параметры запуска&#xA;    #&#xA;    # ros2 run ... --ros-args&#xA;    #     -p start_id:=0&#xA;    #     -p target_id:=22&#xA;    # ------------------------------------------------------&#xA;&#xA;    node.declare_parameter(&amp;#34;start_id&amp;#34;, 0)&#xA;    node.declare_parameter(&amp;#34;target_id&amp;#34;, 22)&#xA;&#xA;    start = node.get_parameter(&amp;#34;start_id&amp;#34;).value&#xA;    target = node.get_parameter(&amp;#34;target_id&amp;#34;).value&#xA;&#xA;    # ------------------------------------------------------&#xA;    # Создаём основные части программы&#xA;    # ------------------------------------------------------&#xA;&#xA;    graph = Graph()&#xA;    planner = Planner(graph)&#xA;    motion = Motion(node, graph)&#xA;    detector = ObstacleDetector(node, graph)&#xA;&#xA;    try:&#xA;&#xA;        # ==================================================&#xA;        # 1. СТРОИМ МАРШРУТ ТУДА&#xA;        # ==================================================&#xA;&#xA;        blocked = set()&#xA;&#xA;        node.get_logger().info(&#xA;            &amp;#34;OUTBOUND_PLANNING_START&amp;#34;&#xA;        )&#xA;&#xA;        route, distance, turns = planner.plan(&#xA;            start,&#xA;            target,&#xA;            blocked&#xA;        )&#xA;&#xA;        print_route(&#xA;            &amp;#34;OUTBOUND ROUTE&amp;#34;,&#xA;            start,&#xA;            target,&#xA;            blocked,&#xA;            route,&#xA;            distance,&#xA;            turns&#xA;        )&#xA;&#xA;        # ==================================================&#xA;        # 2. ЕДЕМ К ЦЕЛИ&#xA;        # ==================================================&#xA;&#xA;        node.get_logger().info(&#xA;            &amp;#34;OUTBOUND_MOVEMENT_START&amp;#34;&#xA;        )&#xA;&#xA;        motion.start(route)&#xA;&#xA;        wait_robot(&#xA;            node,&#xA;            motion&#xA;        )&#xA;&#xA;        node.get_logger().info(&#xA;            &amp;#34;OUTBOUND_MOVEMENT_STOP&amp;#34;&#xA;        )&#xA;&#xA;        # ==================================================&#xA;        # 3. ЗАПОМИНАЕМ ЛИДАР ДО ПРЕПЯТСТВИЯ&#xA;        # ==================================================&#xA;&#xA;        if not detector.save_baseline():&#xA;&#xA;            node.get_logger().error(&#xA;                &amp;#34;BASELINE_SCAN_NOT_AVAILABLE&amp;#34;&#xA;            )&#xA;&#xA;            return&#xA;&#xA;        node.get_logger().info(&#xA;            &amp;#34;BASELINE_SCAN_CAPTURED&amp;#34;&#xA;        )&#xA;&#xA;        # ==================================================&#xA;        # 4. ПОЛЬЗОВАТЕЛЬ СТАВИТ ПРЕПЯТСТВИЕ&#xA;        # ==================================================&#xA;&#xA;        print()&#xA;        print(&amp;#34;========================================&amp;#34;)&#xA;        print(&amp;#34;РОБОТ ОСТАНОВИЛСЯ В ЦЕЛЕВОЙ ТОЧКЕ&amp;#34;)&#xA;        print(&amp;#34;========================================&amp;#34;)&#xA;        print(&amp;#34;Поставьте ОДНО препятствие в Webots.&amp;#34;)&#xA;        print(&amp;#34;После этого введите y и нажмите Enter.&amp;#34;)&#xA;        print(&amp;#34;========================================&amp;#34;)&#xA;&#xA;        while True:&#xA;&#xA;            answer = input(&#xA;                &amp;#34;Препятствие установлено? [y]: &amp;#34;&#xA;            )&#xA;&#xA;            if answer.lower().strip() == &amp;#34;y&amp;#34;:&#xA;                break&#xA;&#xA;        # ==================================================&#xA;        # 5. ПОЛУЧАЕМ СВЕЖИЙ ЛИДАР&#xA;        # ==================================================&#xA;&#xA;        wait_for_new_scan(node)&#xA;&#xA;        # ==================================================&#xA;        # 6. ИЩЕМ ЗАНЯТУЮ КЛЕТКУ&#xA;        # ==================================================&#xA;&#xA;        node.get_logger().info(&#xA;            &amp;#34;OBSTACLE_SCAN_START&amp;#34;&#xA;        )&#xA;&#xA;        blocked_marker = detector.find_obstacle(&#xA;            motion.x,&#xA;            motion.y,&#xA;            motion.yaw,&#xA;            excluded={start, target}&#xA;        )&#xA;&#xA;        if blocked_marker is None:&#xA;&#xA;            print()&#xA;            print(&amp;#34;Препятствие не найдено.&amp;#34;)&#xA;            print()&#xA;&#xA;            return&#xA;&#xA;        print()&#xA;        print(&amp;#34;========================================&amp;#34;)&#xA;        print(&#xA;            &amp;#34;OBSTACLE DETECTED ON MARKER&amp;#34;,&#xA;            blocked_marker&#xA;        )&#xA;        print(&amp;#34;========================================&amp;#34;)&#xA;        print()&#xA;&#xA;        # Теперь эта клетка запрещена для планировщика.&#xA;        blocked = {blocked_marker}&#xA;&#xA;        # ==================================================&#xA;        # 7. СТРОИМ ОБРАТНЫЙ МАРШРУТ&#xA;        # ==================================================&#xA;&#xA;        node.get_logger().info(&#xA;            &amp;#34;RETURN_PLANNING_START&amp;#34;&#xA;        )&#xA;&#xA;        return_route, distance, turns = planner.plan(&#xA;            target,&#xA;            start,&#xA;            blocked&#xA;        )&#xA;&#xA;        print_route(&#xA;            &amp;#34;RETURN ROUTE&amp;#34;,&#xA;            target,&#xA;            start,&#xA;            blocked,&#xA;            return_route,&#xA;            distance,&#xA;            turns&#xA;        )&#xA;&#xA;        # ==================================================&#xA;        # 8. ЕДЕМ ОБРАТНО&#xA;        # ==================================================&#xA;&#xA;        node.get_logger().info(&#xA;            &amp;#34;RETURN_MOVEMENT_START&amp;#34;&#xA;        )&#xA;&#xA;        motion.start(return_route)&#xA;&#xA;        wait_robot(&#xA;            node,&#xA;            motion&#xA;        )&#xA;&#xA;        node.get_logger().info(&#xA;            &amp;#34;RETURN_MOVEMENT_STOP&amp;#34;&#xA;        )&#xA;&#xA;        node.get_logger().info(&#xA;            &amp;#34;MODULE_B_DONE&amp;#34;&#xA;        )&#xA;&#xA;        print()&#xA;        print(&amp;#34;========================================&amp;#34;)&#xA;        print(&amp;#34;MODULE B COMPLETE&amp;#34;)&#xA;        print(&amp;#34;Robot returned to the start marker.&amp;#34;)&#xA;        print(&amp;#34;========================================&amp;#34;)&#xA;        print()&#xA;&#xA;    except KeyboardInterrupt:&#xA;        pass&#xA;&#xA;    finally:&#xA;&#xA;        # На всякий случай всегда останавливаем робот.&#xA;        motion.stop()&#xA;&#xA;        node.destroy_node()&#xA;        rclpy.shutdown()&#xA;&#xA;&#xA;if __name__ == &amp;#34;__main__&amp;#34;:&#xA;    main()&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h3&gt;Что происходит после запуска&lt;/h3&gt;&lt;p&gt;Весь пример можно свести к следующей последовательности:&lt;pre&gt;&lt;code&gt;робот для сбора клубники          ↓     START = 0          ↓         BFS          ↓   маршрут к растениям          ↓       движение          ↓     TARGET = 22          ↓   сохранение LaserScan          ↓  появление препятствия          ↓ сравнение двух сканов          ↓&#xA;определение занятой клетки          ↓ blocked = {marker}          ↓     повторный BFS          ↓   обход препятствия          ↓&#xA;возвращение к START&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Конечно, это пока только навигационная часть будущего робота-сборщика. Здесь ещё нет распознавания ягод, определения спелости и управления манипулятором.&lt;p&gt;Но уже на этом этапе отдельно отрабатываются планирование маршрута, управление мобильной платформой, работа с одометрией и лидаром, обнаружение препятствия и перепланирование.&lt;p&gt;Следующим этапом эту систему можно связать с техническим зрением и манипулятором:&lt;pre&gt;&lt;code&gt;НАВИГАЦИЯ     ↓&#xA;приехали к растениям     ↓&#xA;ТЕХНИЧЕСКОЕ ЗРЕНИЕ     ↓&#xA;нашли спелую клубнику     ↓&#xA;получили координаты ягоды     ↓&#xA;МАНИПУЛЯТОР     ↓&#xA;сорвали ягоду     ↓&#xA;НАВИГАЦИЯ     ↓&#xA;следующая рабочая позиция&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Таким образом, приведённый пример является первым самостоятельным модулем более крупного проекта — мобильного роботизированного комплекса для автоматизированного сбора клубники.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <author>wowa144</author>
      <guid>https://habr.com/ru/articles/1082262/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082262</guid>
      <pubDate>Tue, 15 Sep 2026 04:25:04 +0000</pubDate>
    </item>
    <item>
      <title>UDP-прокси и протокол QUIC — как утекает реальный IP</title>
      <link>https://habr.com/ru/companies/aurorium/articles/1082256/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082256</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;Среди тех, кто работает с мультиаккаунтингом, да и вообще использует прокси, существует такая страшилка: если приложение, через которое вы проксируете трафик (читай — антидетект-браузер), не поддерживает UDP (или базово не контролирует сетевые маршруты), ваш реальный IP-адрес может утечь — и тогда сама идея использования прокси теряет смысл. Вы хотите скрыть реальный IP-адрес, но из-за отсутствия поддержки UDP, наоборот, раскрываете его. &lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/4c1/23c/fa5/4c123cfa55dda37a476438c6e7fc1c3f.png width=1920 height=250 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/4c1/23c/fa5/4c123cfa55dda37a476438c6e7fc1c3f.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/4c1/23c/fa5/4c123cfa55dda37a476438c6e7fc1c3f.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;А какая страшилка при прочих равных будет чуточку страшнее остальных? &lt;strong&gt;Та, которая основана на реальных событиях&lt;/strong&gt;. Вот и в утверждении выше все-таки есть доля правды. &lt;p&gt;Реальный IP-адрес и правда может утечь.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/c74/2ed/74d/c742ed74dee5a8acfad9a5c68458595e.jpg width=1920 height=1080 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/c74/2ed/74d/c742ed74dee5a8acfad9a5c68458595e.jpg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/c74/2ed/74d/c742ed74dee5a8acfad9a5c68458595e.jpg 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Один из возможных сценариев — вы купили прокси с поддержкой UDP, но используемый вами антидетект-браузер не умеет корректно работать с UDP-трафиком.&lt;p&gt;При этом утверждение &lt;strong&gt;«нет поддержки UDP, и ваш IP 100% утекает»&lt;/strong&gt; некорректно. Правильнее сказать так: &lt;strong&gt;«нет поддержки UDP — появляется неконтролируемое сетевое поведение».&lt;/strong&gt;&lt;p&gt;Под неконтролируемым сетевым поведением понимается один из нескольких сценариев:&lt;ul&gt;&lt;li&gt;&lt;p&gt;UDP не работает совсем &lt;li&gt;&lt;p&gt;соединение откатывается на TCP&lt;li&gt;&lt;p&gt;соединение получает отдельный сетевой маршрут, который не проходит через текущий прокси&lt;li&gt;&lt;p&gt;используется другой механизм маршрутизации &lt;/ul&gt;&lt;p&gt;При использовании отдельного сетевого маршрута и возникает риск утечки реального адреса, когда часть трафика может пойти другим путем и потенциально раскрыть ваш реальный IP-адрес. &lt;blockquote&gt;&lt;p&gt;Привет! Я Александр, пишу для &lt;a href=https://aurorium.ai/&gt;антидетект-браузера Aurorium&lt;/a&gt; и по совместительству являюсь специалистом в вопросах автоматизации и парсинга. В этом материале мы рассмотрим, что такое UDP-прокси, чем протокол UDP отличается от TCP и как ваш реальный IP-адрес может утечь, если антидетект-браузер не умеет работать с UDP. По традиции сварите себе чашечку кофе или заварите чай, приготовьте пару бутербродов — мы начинаем!&lt;/blockquote&gt;&lt;h3&gt;Что такое UDP-прокси? &lt;/h3&gt;&lt;p&gt;Что такое прокси-сервер, знают уже даже наши родители и родители наших родителей — это посредник между вашим устройством и целевым сайтом. &lt;p&gt;Исторически так сложилось, что большинство базовых прокси (HTTP/HTTPS) умеют маршрутизировать только TCP-трафик, и на этом уровне они отлично справляются с загрузкой текстовых данных, HTML-кода или картинок.&lt;p&gt;Но такие прокси не подходят для решения специфических задач — стриминга, звонков, онлайн-игр или работы современных сетевых протоколов (где как раз и используется протокол UDP). Прокси, поддерживающие TCP-трафик, физически не умеют передавать такие пакеты.&lt;p&gt;UDP-прокси (чаще всего это сервер с поддержкой протокола SOCKS5 и активированной функцией UDP Associate) — это прокси, который способен инкапсулировать, маршрутизировать и отдавать UDP-датаграммы.&lt;p&gt;&lt;strong&gt;И вот тут возникает второй закономерный вопрос:&lt;/strong&gt; зачем вообще браузеру нужен UDP и откуда берется этот отдельный сетевой маршрут? И не проще ли просто модернизировать (как-то докрутить) и использовать TCP вообще для любых подключений? &lt;p&gt;Давайте разбираться!&lt;h3&gt;Зачем браузеру нужен UDP?&lt;/h3&gt;&lt;p&gt;Итак, где именно поддержка UDP становится важна для браузера и, соответственно, для антидетект-браузера?&lt;p&gt;Давайте рассмотрим, что современный браузер делает помимо обычной загрузки страницы. &lt;p&gt;Мы привыкли воспринимать его работу поверхностно — вводим адрес сайта, соединение устанавливается и страница загружается. Но на практике все несколько сложнее.&lt;p&gt;Браузер одновременно решает несколько задач: грузит страницу, устанавливает дополнительные соединения, а иногда и организует потоковую или мультимедийную передачу данных (те самые звонки, стриминг и онлайн-игры). Для всех этих задач транспорт может быть разным.&lt;p&gt;Самый очевидный пример — протокол HTTP/3. В отличие от HTTP/1.1 и HTTP/2, он работает не поверх TCP, а поверх протокола QUIC. А QUIC, в свою очередь, использует UDP.&lt;p&gt;Выглядит это так:&lt;pre&gt;&lt;code class=typescript&gt;HTTP/1.1 → TCP&#xA;HTTP/2   → TCP&#xA;HTTP/3   → QUIC → UDP&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:87px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;HTTP/3 — это новая версия HTTP, как и HTTP/1.1 или HTTP/2, но с другим транспортом. Это не «ускоренный UDP», а HTTP, работающий поверх протокола QUIC.&lt;/em&gt;&lt;/blockquote&gt;&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;QUIC — транспортный протокол, работающий поверх UDP. Это своего рода надстройка над UDP, которая добавляет механизмы надежной передачи данных, контроля перегрузки, мультиплексирования потоков и шифрования.&lt;/em&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code class=typescript&gt;HTTP/3 → QUIC → UDP → IP.&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;На этом этапе мы видим, что UDP нужен браузеру как транспорт для технологий, которым критична скорость и для которых TCP не подходит из-за своих задержек и ограничений.&lt;p&gt;HTTP/3 не единственный пример; есть еще более опасный (с точки зрения утечек IP-адреса) — &lt;strong&gt;WebRTC&lt;/strong&gt;. Когда сайт устанавливает голосовое или видеосоединение, браузер использует ICE для поиска подходящего сетевого пути. В этом процессе может использоваться UDP-соединение со STUN/TURN-серверами.&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;ICE (Interactive Connectivity Establishment) — механизм поиска и выбора подходящего сетевого пути между участниками WebRTC. В процессе ICE браузер собирает и проверяет различные варианты соединения, в том числе адреса, полученные через STUN, и relay-адреса TURN. &lt;/em&gt;&lt;/blockquote&gt;&lt;p&gt;И вот здесь появляется связь между WebRTC и антидетектом: если браузер получает возможность использовать UDP-маршрут, который не проходит через установленный пользователем прокси, этот маршрут может отличаться от маршрута обычного HTTP-трафика. &lt;p&gt;Может возникнуть такая ситуация, что весь трафик будет идти через прокси, а UDP пойдет через ваш реальный IP-адрес.&lt;h3&gt;TCP против UDP, а может просто заменить один на другой?&lt;/h3&gt;&lt;p&gt;Давайте разберем более предметно два протокола, чем и почему они отличаются, чтобы понять, в чем UDP лучше TCP. &lt;p&gt;TCP отвечает за надежную и упорядоченную передачу данных. В процессе работы происходит: &lt;ul&gt;&lt;li&gt;&lt;p&gt;установление соединения;&lt;li&gt;&lt;p&gt;контроль порядка пакетов;&lt;li&gt;&lt;p&gt;обнаружение потерь и при необходимости повторная передача пакетов.&lt;/ul&gt;&lt;p&gt;Поверх TCP-протокола все время строилась и продолжает строиться большая часть привычного веб-трафика.&lt;p&gt;UDP устроен иначе. Он не устанавливает соединение, как это делает TCP, не гарантирует доставку каждой отдельной датаграммы и не контролирует порядок их получения.&lt;p&gt;За счет этого UDP предоставляет приложению (сайту) более простой транспорт с меньшим количеством встроенной транспортной логики. А все, что требуется конкретному протоколу поверх него — подтверждение доставки, восстановление после потерь или управление несколькими потоками, — может быть реализовано на уровне выше. &lt;p&gt;Именно так и работает QUIC.&lt;p&gt;QUIC использует UDP как базовый транспорт, а поверх него реализует собственную транспортную логику: надежную передачу потоковых данных, контроль перегрузки, восстановление после потерь, шифрование и работу с несколькими независимыми потоками. &lt;p&gt;Получается что QUIC — это TCP на максималках, построенный поверх UDP. Так это выглядит, да, но у него уже нет тех ограничений, которые присущи TCP.&lt;p&gt;Для большей наглядности сравним два протокола по отношению друг к другу в таблице:&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;Параметр&lt;/strong&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;TCP&lt;/strong&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;UDP&lt;/strong&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Установка соединения&lt;td&gt;&lt;p align=left&gt;Да&lt;td&gt;&lt;p align=left&gt;Нет&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Доставка данных&lt;td&gt;&lt;p align=left&gt;Надежная&lt;td&gt;&lt;p align=left&gt;Не гарантируется&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Порядок&lt;td&gt;&lt;p align=left&gt;Обеспечивает упорядоченную доставку &lt;td&gt;&lt;p align=left&gt;Нет&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Повторная передача&lt;td&gt;&lt;p align=left&gt;Реализована&lt;td&gt;&lt;p align=left&gt;Не предусмотрена&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Тип данных &lt;td&gt;&lt;p align=left&gt;Поток байтов&lt;td&gt;&lt;p align=left&gt;Датаграммы&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;h3&gt;Почему QUIC — это не тоже самое, что и TCP-протокол и чем он опасен для прокси?&lt;/h3&gt;&lt;p&gt;QUIC проектировался так, чтобы взять все лучшее от TCP (надежность), но убрать его ограничения. Вот что получил QUIC:&lt;ol&gt;&lt;li&gt;&lt;p&gt;0-RTT — QUIC объединяет транспортное и криптографическое рукопожатие. При повторном подключении к сайту браузер начинает передавать данные почти мгновенно.&lt;li&gt;&lt;p&gt;Независимые потоки — в TCP потеря одного пакета (например, куска CSS) блокирует загрузку всего остального (HTML, скриптов). В QUIC потоки независимы: потеряли картинку — остальной сайт продолжает грузиться.&lt;li&gt;&lt;p&gt;Миграция соединения — в TCP-протоколе сессия привязана к IP-адресу. Переключились с Wi-Fi на LTE — соединение оборвется. В QUIC сессия привязана к Connection ID. Вы можете менять сети, а условное видео на YouTube продолжит воспроизводиться без потери качества и переподключения.&lt;/ol&gt;&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;Способность QUIC «выживать» при смене сетей и восстанавливать сессии означает, что протокол умеет переживать изменение сетевого пути. Если ваше соединение с прокси на секунду моргнет, браузер (в рамках миграции соединения QUIC) может попытаться восстановить сессию напрямую через ваш реальный сетевой интерфейс, в обход отвалившегося прокси. Если антидетект не контролирует сетевой слой, ваш реальный IP-адрес улетит серверу, а значит, и антифрод сможет его увидеть.&lt;/em&gt;&lt;/blockquote&gt;&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/5ba/961/f77/5ba961f779736d4833843222c36090f3.jpg width=400 height=500 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/5ba/961/f77/5ba961f779736d4833843222c36090f3.jpg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/5ba/961/f77/5ba961f779736d4833843222c36090f3.jpg 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;h3&gt;WebRTC — где появляется риск утечки IP&lt;/h3&gt;&lt;p&gt;Если прокси умеет работать только с TCP, браузер не сможет провести через него QUIC-трафик привычным способом. HTTP/3 в такой ситуации просто не поднимется, а браузер продолжит работу через HTTP/2. &lt;p&gt;Вроде ничего критичного, откатился и откатился, что бубнить-то? Но если смотреть с точки зрения антифрода, тут есть за что зацепиться. &lt;p&gt;Когда сайт поддерживает протокол HTTP/3 (а у серверов Google и Meta он стоит по умолчанию), обычный пользователь с прямым интернет-подключением зайдет на него именно по QUIC (UDP). Если же браузер из-за настроенного прокси не справляется с UDP и внезапно откатывается на устаревший протокол поверх TCP — антифрод видит сетевую аномалию и, скорее всего, ваш Fraud Score станет хуже. &lt;p&gt;С WebRTC ситуация интереснее.&lt;p&gt;Представим обычный профиль созданный в антидетект-браузере, с установленным прокси:&lt;pre&gt;&lt;code class=typescript&gt;Реальный IP:&#xA;91.xxx.xxx.xxx&#xA;IP прокси:&#xA;185.xxx.xxx.xxx&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;При открытии сайта он увидит не ваш IP, а адрес прокси:&lt;pre&gt;&lt;code class=typescript&gt;Браузер&#xA;  ↓&#xA;Прокси&#xA;  ↓&#xA; Сайт&#xA;Сайт видит:&#xA;185.xxx.xxx.xxx&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;С точки зрения пользователя все логично и максимально защищено — реальный IP скрыт. С точки зрения сайта, кстати, тоже.&lt;p&gt;Усложняем ситуацию и добавляем новую вводную — на странице используется WebRTC.&lt;h4&gt;Что делает WebRTC&lt;/h4&gt;&lt;p&gt;WebRTC — это технология, предназначенная для передачи аудио, видео и других данных в реальном времени. Для установления такого соединения браузеру нужно понять, какой сетевой путь между участниками вообще доступен.&lt;p&gt;Для этого используется ICE.&lt;p&gt;С помощью ICE браузер собирает возможные комбинации IP-адресов и портов, через которые можно установить соединение. Среди них может быть и ваш реальный IP-адрес.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/5b5/e88/e1a/5b5e88e1af8ae999d4a330519e742cd3.jpg width=1920 height=1080 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/5b5/e88/e1a/5b5e88e1af8ae999d4a330519e742cd3.jpg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/5b5/e88/e1a/5b5e88e1af8ae999d4a330519e742cd3.jpg 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Условно:&lt;pre&gt;&lt;code class=typescript&gt;ICE&#xA; ├── локальный адрес&#xA; ├── публичный адрес через STUN&#xA; └── адрес relay через TURN&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Затем ICE проверяет доступные варианты и выбирает рабочую пару адресов. В базовом варианте ICE для таких проверок используется UDP.&lt;h4&gt;Где здесь появляется прокси&lt;/h4&gt;&lt;p&gt;А теперь возвращаемся к антидетекту.&lt;p&gt;Если ваш антидетект-браузер умеет проксировать только HTTP/TCP, он физически не может контролировать UDP-трафик, который использует WebRTC.&lt;p&gt;В результате браузер разделяет трафик на два независимых маршрута:&lt;p&gt;1. Обычный HTTPS-трафик (идет через прокси):&lt;pre&gt;&lt;code class=typescript&gt;Браузер&#xA;   ↓ TCP&#xA;Прокси&#xA;   ↓&#xA;Сайт&#xA;(Сайт видит IP прокси: 185.xxx.xxx.xxx)&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;2. WebRTC-трафик (идет в обход прокси): &lt;pre&gt;&lt;code class=typescript&gt;WebRTC:&#xA;Браузер&#xA; ↓ UDP&#xA;Сетевой интерфейс&#xA;  ↓&#xA;STUN / WebRTC&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;И это может привести к такой картине, где возникает &lt;strong&gt;потенциальная утечка реального IP&lt;/strong&gt;-адреса:&lt;pre&gt;&lt;code class=typescript&gt;HTTP:&#xA;185.xxx.xxx.xxx  ← IP прокси&#xA;WebRTC:&#xA;(Сайт видит ваш реальный домашний IP: 91.xxx.xxx.xxx)&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;И вот ваш реальный IP-адрес утек. Мало того, что сайт видит две разные сетевые идентичности в рамках одного профиля, так еще и антифрод замечает использование прокси и блокирует (или ограничивает) аккаунт. Но и это не всё. Если у вас несколько аккаунтов и все они будут связаны по вашему реальному IP-адресу в сеть, вы оказываетесь в той точке, где ситуация перестает быть томной.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/85f/bab/8d1/85fbab8d18a9b81bfe79d820b0c5766c.jpg alt=&#34;Вот как выглядит утечка&#34; title=&#34;Вот как выглядит утечка&#34; width=1920 height=1080 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/85f/bab/8d1/85fbab8d18a9b81bfe79d820b0c5766c.jpg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/85f/bab/8d1/85fbab8d18a9b81bfe79d820b0c5766c.jpg 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Вот как выглядит утечка&lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;При этом важно понимать: само по себе наличие WebRTC на сайте не означает гарантированной утечки. Но если антидетект не контролирует UDP-маршруты, вы даете сайту (а значит и антифроду) возможность нащупать ваш прямой сетевой путь.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/c13/490/f62/c13490f626e5aedfec3b5c7c9fc28614.jpg alt=&#34;Когда у тебя 80 профилей под фарминг, и в каждом WebRTC показывает один и тот же адрес твоего роутера &#34; title=&#34;Когда у тебя 80 профилей под фарминг, и в каждом WebRTC показывает один и тот же адрес твоего роутера &#34; width=1920 height=1080 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/c13/490/f62/c13490f626e5aedfec3b5c7c9fc28614.jpg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/c13/490/f62/c13490f626e5aedfec3b5c7c9fc28614.jpg 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Когда у тебя 80 профилей под фарминг, и в каждом WebRTC показывает один и тот же адрес твоего роутера &lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;h3&gt;Фейковая галочка vs Реальная маршрутизация&lt;/h3&gt;&lt;p&gt;Проблема рынка в том, что многие расширения и базовые антидетект-браузеры указывают, что поддерживают UDP, хотя на деле просто принудительно отключают WebRTC в браузере или откатываются на HTTP/2 при встрече с HTTP/3 (для пользователя это же вообще незаметно). &lt;p&gt;Настоящая поддержка UDP не реализуется на уровне браузера. Она делается на уровне сетевого стека операционной системы.&lt;p&gt;Чтобы WebRTC-кандидаты не смогли «нащупать» ваш реальный Wi-Fi или Ethernet, антидетект-браузер должен использовать виртуальный сетевой интерфейс (TUN-режим) или глубокий перехват сокетов. В таком сценарии весь трафик системы (и TCP, и UDP) принудительно заворачивается в туннель. ICE-кандидаты просто не видят вашего реального железа — для них существует только туннель к вашему прокси (который, конечно же, тоже должен поддерживать UDP, например, SOCKS5).&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/f59/c23/a09/f59c23a09f7576034c8ac681e795ea4f.jpg width=1920 height=1080 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/f59/c23/a09/f59c23a09f7576034c8ac681e795ea4f.jpg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/f59/c23/a09/f59c23a09f7576034c8ac681e795ea4f.jpg 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;И здесь мы сталкиваемся с главной проблемой рынка прокси — дефицитом и дороговизной решений с реальной поддержкой UDP-протокола. Большинство поставщиков либо вообще не поддерживают UDP, либо продают такие прокси по завышенным тарифам. Но даже если вы нашли провайдера с честным UDP, появляется проблема: движок Chromium «из коробки» умеет работать с UDP напрямую, но не умеет его проксировать через стандартный SOCKS5. Без глубокой проработки движка любой базовый антидетект-браузер будет отправлять через SOCKS5 обычный TCP-трафик. &lt;h3&gt;Как мы решили эту проблему в Aurorium Browser&lt;/h3&gt;&lt;p&gt;Мы переработали сетевой стек и добавили честную поддержку UDP-протокола. Наша архитектура &lt;strong&gt;имеет нативную поддержку UDP-протокола.&lt;/strong&gt; Если ваш прокси изначально умеет передавать UDP-пакеты, Aurorium автоматически распознает протокол и направляет QUIC и WebRTC-трафик напрямую через прокси без потерь.&lt;p&gt;В результате целевой сайт видит естественное поведение: сессии поднимаются по HTTP/3 (QUIC), WebRTC отрабатывает без ошибок, а ваш реальный IP не утекает и остается скрыт за туннелем.&lt;h4&gt;Как проверить свой антидетект-браузер прямо сейчас:&lt;/h4&gt;&lt;ol&gt;&lt;li&gt;&lt;p&gt;Включите ваш рабочий профиль с прокси.&lt;li&gt;&lt;p&gt;Если хотите проверить QUIC, перейдите на страницу — &lt;a href=https://browserleaks.com/quic&gt;https://browserleaks.com/quic&lt;/a&gt;, для тестирования UDP — &lt;a href=https://networktest.twilio.com/&gt;https://networktest.twilio.com/&lt;/a&gt;.&lt;/ol&gt;&lt;p&gt;Если QUIC поддерживается вы увидите примерно такую картину.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/062/078/dd6/062078dd628ea31aa965722cf27ffe9b.jpeg width=1782 height=1010 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/062/078/dd6/062078dd628ea31aa965722cf27ffe9b.jpeg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/062/078/dd6/062078dd628ea31aa965722cf27ffe9b.jpeg 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Если QUIC не поддерживается, то такую:&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/782/10b/12a/78210b12a2ee3d35a6b1f6b58ab29d0c.jpeg width=1784 height=941 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/782/10b/12a/78210b12a2ee3d35a6b1f6b58ab29d0c.jpeg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/782/10b/12a/78210b12a2ee3d35a6b1f6b58ab29d0c.jpeg 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Когда проверяете UDP то видите примерно такую картину, если UDP поддерживается&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/f93/441/77a/f9344177aea3760d246dd58d15bde392.jpeg width=1777 height=1003 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/f93/441/77a/f9344177aea3760d246dd58d15bde392.jpeg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/f93/441/77a/f9344177aea3760d246dd58d15bde392.jpeg 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Проксируйте трафик с умом и проверяйте инструменты, которыми пользуетесь.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <guid>https://habr.com/ru/companies/aurorium/articles/1082256/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082256</guid>
      <pubDate>Tue, 15 Sep 2026 02:19:15 +0000</pubDate>
    </item>
    <item>
      <title>Найм умирает. Может, теперь делать свой продукт не такая уж плохая идея?</title>
      <link>https://habr.com/ru/articles/1082238/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082238</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/950/51b/730/95051b7300729b37d8fdfba5f03da8dd.png alt=&#34;Комментарию к видео @fillpackart&#34; title=&#34;Комментарию к видео @fillpackart&#34; width=780 height=440 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/950/51b/730/95051b7300729b37d8fdfba5f03da8dd.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/950/51b/730/95051b7300729b37d8fdfba5f03da8dd.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Комментарию к видео &lt;a href=https://habr.com/ru/users/fillpackart/ rel=&#34;noopener nofollow&#34;&gt;@fillpackart&lt;/a&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;На превью этой статьи я поставил комментарий человека, который уже год не может найти работу. Понятно, что это один конкретный случай, по которому нельзя судить обо всем рынке. Но подобных историй становится слишком много, чтобы продолжать считать их исключениями.&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Дисклеймер&lt;/strong&gt;&lt;p&gt;Я не изучал никакую статистику состояния найма перед тем, как писать эту статью. Все это вы и без меня найдете. Да и от того, много там будет вакансий или мало, мне ни холодно ни жарко.&lt;p&gt;Все, что описано в статье, происходит либо со мной, либо с людьми из моего ближайшего окружения. Да и статья не про статистику. Я рассказываю о своих ощущениях в этом новом чудном ИИ-мире и пытаюсь найти свое место в будущем.&lt;/blockquote&gt;&lt;p&gt;Последнее время у меня странное состояние. Что-то между усталостью и выгоранием. Старый мир разработки буквально уходит из-под ног, а новый еще до конца не отстроился и можно только догадываться каким он будет. Непонятно, какие навыки будут иметь ценность через год, как теперь оценивать собственную работу и куда вообще двигаться дальше.&lt;p&gt;И чем больше я на это смотрю, тем чаще думаю: возможно, в текущих реалиях делать свой продукт уже не такая рискованная идея, как казалось раньше.&lt;h3&gt;Средний этаж исчезает&lt;/h3&gt;&lt;p&gt;Еще пару лет назад для всех было очевидно каким должен быть путь разработчика. Можно было расти как инженер, а можно было уйти в управление: стать тимлидом, менеджером, руководителем нескольких команд. Второй путь казался вполне надежной ставкой. Ты меньше пишешь код, больше работаешь с людьми, процессами и стратегией, а твоя ценность для компании только растет.&lt;p&gt;Сейчас этот этаж начинает исчезать.&lt;p&gt;Люди, которые несколько лет назад ушли в менеджмент, снова пишут код. Тимлиды работают наравне с обычными разработчиками, осваивают новые AI-инструменты и вспоминают навыки, которыми давно не пользовались. Если такой менеджер не успел в свое время дорасти до такого уровня, где бы ему досталась какая-то жирная темка, например, отвечать за AI-трансформацию компании, то плохи его дела. Придется фиксить баги наравне с другими замлекопами.&lt;p&gt;Получается, что ставка на позицию менеджера среднего звена для многих просто не сработала. Просто сама позиция начала обесцениваться. Все эти книжки по аджайлу или мотивации сотрудников сейчас не актуальны. Больших команд становится меньше, процессы меняются, а для передачи задач между несколькими уровнями управления уже не всегда нужны отдельные люди, потому что разработчики как раз расширили зону своей ответственности.&lt;p&gt;Но проблема не только в менеджерах. Программирование тоже стремительно дешевеет. Как и тестирование, дизайн, технические тексты и редактура.&lt;p&gt;Компании уже могут обходиться без полноценной команды дизайнеров, особенно если у них есть готовая дизайн-система. Разработчик знает основные паттерны, берет существующие компоненты и собирает интерфейс сразу в коде. Возможно, в большой компании останется один дизайнер, который будет поддерживать дизайн-систему и подключаться к сложным сценариям. Для большинства повседневных задач отдельный этап с макетами становится необязательным.&lt;p&gt;С тестированием происходит похожая история. Если QA займет позицию &amp;#34;Я только тестирую&amp;#34;, то его место в компании автоматически ставится под сомнение. От разработчика теперь ожидают, что он сам напишет код, проверит фронтенд и бэкенд, добавит тесты и проверит, что все работает у реальных клиентов. Разделение труда постепенно сводится к одному общему знаменателю: продуктовому разработчику, который берет задачу целиком и доводит ее до результата.&lt;p&gt;Я сам всегда был фронтендером. Сейчас делаю все подряд: фронтенд, бэкенд, тестирование, автоматизации, работу с инфраструктурой. Единственный способ оставаться полезным заключается в том, чтобы не чураться никакой работы и не держаться за границы своей старой роли.&lt;h3&gt;Два пути внутри компании&lt;/h3&gt;&lt;p&gt;Если смотреть именно на найм, то сейчас я вижу два более-менее понятных пути.&lt;p&gt;Первый: стать тем самым универсальным продуктовым разработчиком и попытаться выиграть в этих голодных играх. Брать задачу от начала до конца, работать на разных стеках, использовать нейросети, быстро разбираться в незнакомом коде и закрывать то, что раньше делала небольшая команда. Таких людей компаниям тоже, судя по всему, будет требоваться все меньше. Но кто-то все равно должен принимать последние решения и отвечать за результат.&lt;p&gt;Второй путь кажется мне более перспективным: стать человеком, который строит внутри компании весь AI-пайплайн. Внедрять процессы, в которых значительная часть работы выполняется автоматически.&lt;p&gt;Например, поддержка заводит баг. Дальше система сама собирает контекст, воспроизводит проблему, создает задачу, находит нужный участок кода, предлагает исправление, прогоняет тесты и отправляет изменения на ревью. Последний выживший продуктовый разработчик смотрит на результат, нажимает апрув, и исправление едет к пользователям.&lt;p&gt;Задача такого специалиста состоит в том, чтобы настроить этот процесс, выбрать инструменты, описать правила, следить за качеством и вовремя менять отдельные части системы. Где-то подключить новую модель, где-то сократить расход токенов.&lt;p&gt;Пока общепринятых паттернов здесь почти нет. Каждая компания собирает свою систему из отдельных инструментов и собственных правил. Но когда-то так же было с CI/CD, мониторингом и релизными процессами. Со временем появились стандартные подходы. Скорее всего, появится и отдельная роль, условный AI Ops, который будет отвечать за всю эту систему.&lt;p&gt;Мне кажется, именно эти люди дольше остальных будут чувствовать себя уверенно.&lt;h3&gt;Работы стало больше, но ценности в ней чувствуешь меньше&lt;/h3&gt;&lt;p&gt;Вся эта трансформация сильно давит психологически. Вроде бы с нейросетями ты делаешь гораздо больше, чем раньше. Но одновременно возникает ощущение, что ты делаешь недостаточно.&lt;p&gt;Раньше можно было оценить работу более-менее понятно: вот задача, вот код, вот несколько дней или недель, которые ушли на реализацию. Теперь за день можно сгенерировать огромный объем изменений. Потом нужно все это прочитать, проверить, исправить, понять этот нейро-код, проследить, чтобы ничего не сломалось. И тебя никогда не покидает ощущение, что что-то может сломаться. К вечеру ты выжат, хотя тебе трудно объяснить даже самому себе, чем именно ты занимался весь день.&lt;p&gt;Планка растет вместе с производительностью. Если AI позволяет сделать в два раза больше, через некоторое время эти два раза становятся новой нормой. Компания просто начинает ждать от тебя больше готовых задач.&lt;p&gt;Связь между усилиями и результатом становится все менее заметной. Ты бесконечно смотришь код, проверяешь ответы моделей, переключаешься между контекстами, но не чувствуешь, что создаешь что-то законченное. При этом остается постоянный страх, что твой текущий уровень уже недостаточен и что в компании есть человек, который настроил все лучше, работает быстрее и в целом выгоднее для компании.&lt;p&gt;Старый контракт между разработчиком и рынком больше не выглядит надежным. Раньше казалось, что если ты умеешь писать код и у тебя достаточно опыта, то всегда найдешь работу на условные 5–6 тысяч долларов в месяц. Свой проект на этом фоне выглядел рискованной авантюрой. Можно несколько лет что-то делать и ничего не заработать, когда рядом существует понятный и стабильный найм.&lt;p&gt;Только стабильность в этом сравнении была аксиомой. Сейчас она перестала ею быть.&lt;h3&gt;Риски найма и своего продукта сближаются&lt;/h3&gt;&lt;p&gt;Если опытный специалист может год искать работу, то собственный проект уже не выглядит настолько безумной альтернативой. За этот же год можно без серьезных вложений попробовать дойти хотя бы до базового дохода.&lt;p&gt;Да, скорее всего, первое время этот доход будет смешным по сравнению с зарплатой разработчика. Но и год поисков новой работы не гарантирует вообще никакого дохода. Но лучше конечно не ждать сокращения и начинать делать что-то заранее. Чтобы потом не оказаться в самом начале этого пути, когда у тебя только идея в голове и нет доходов.&lt;p&gt;Я как раз начал делать свое приложение примерно год назад, не уходя из найма. За последний месяц я заработал на нем 600 долларов. Это пока не те деньги, ради которых можно бросить работу, но с каждым днем число платных подписчиков растет, а вместе я чуточку меньше переживаю за свое будущее. У меня появляется какая-то надежда.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/8f7/0d7/cf4/8f70d7cf431eb07cddbd8bddafb4a2f3.png alt=&#34;Это график поступлений за все время существования приложения&#34; title=&#34;Это график поступлений за все время существования приложения&#34; width=1580 height=1584 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/8f7/0d7/cf4/8f70d7cf431eb07cddbd8bddafb4a2f3.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/8f7/0d7/cf4/8f70d7cf431eb07cddbd8bddafb4a2f3.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Это график поступлений за все время существования приложения&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Это важно. На графике видно, что более менее ощутимые деньги начали появляться только этим летом. То есть первые 8 месяцев я пилил приложение без результата. Тратил на это все свободное время, исправлял баги, пиарил. А первые подписки были либо мои, либо подписки из жалости от знакомых, чтобы меня поддержать.&lt;p&gt;Это, наверное, самый сложный этап. Ты уже потратил много времени, но у тебя еще нет никаких доказательств, что идея когда-нибудь принесет деньги. Очень легко бросить затею в такие моменты. Думаю многие и бросают, а вот не стоит.&lt;p&gt;Я не знаю, насколько далеко получится дойти с этим приложением. $600 могут превратиться в $6k или больше, вопрос времени. Главное, что теперь здесь хотя бы появился измеримый результат и какие-то работающие пути продвижения, которые можно просто усиливать и повторять.&lt;h3&gt;В своем проекте часы перестают равняться деньгам&lt;/h3&gt;&lt;p&gt;Больше всего мне нравится даже не текущий доход, а изменение самого способа мышления.&lt;p&gt;В найме связь довольно прямая: ты отдаешь компании определенное количество времени и получаешь фиксированную сумму. Можно стать сильнее, получить повышение или перейти в другую компанию, но быстро увеличить доход в два раза почти невозможно. Чаще всего для этого нужно снова пройти длинный карьерный цикл.&lt;p&gt;В своем продукте результат может быть нелинейным. Иногда ты несколько дней делаешь новую фичу, а она почти ни на что не влияет. Потом снимаешь один ролик или меняешь один экран, и количество подписок вырастает в два раза. Это не означает, что каждый ролик будет приносить деньги. Скорее ты постепенно учишься искать действия, которые дают непропорционально большой результат.&lt;p&gt;За последний год я, кажется, вырос сильнее, чем за несколько предыдущих лет работы в найме. Причем не столько в программировании. Мне пришлось разбираться в аналитике, маркетинге, монетизации, SEO, работе сторов, общении с пользователями, написании текстов и создании контента. На работе я уже давно не чувствовал такого развития.&lt;p&gt;Когда делаешь продукт сам, невозможно сказать: &amp;#34;Этим должен заниматься другой отдел&amp;#34;. Другого отдела нет. Если приложение не покупают, недостаточно написать хороший код и считать свою часть работы выполненной. Приходится разбираться, почему люди не доходят до оплаты, что они не понимают, где уходят и как вообще рассказать им о продукте.&lt;p&gt;В каком-то смысле это тот же универсальный продуктовый разработчик, только ты работаешь не на чужой бизнес, а на свой.&lt;h3&gt;Придется научиться долго выглядеть глупо&lt;/h3&gt;&lt;p&gt;У собственного проекта есть неприятная особенность: окружающие постоянно объясняют тебе, почему он никому не нужен.&lt;p&gt;Я уже не могу посчитать, сколько раз слышал вопрос: &amp;#34;Зачем ты делаешь это приложение, если уже есть [подставь любое похожее]?&amp;#34; Мне говорили, что это копия одного продукта, потом копия второго, потом копия третьего. Писали, что идея банальная, рынок переполнен, а реализованная функция никому не нужна.&lt;p&gt;Иногда критика полезна. Пользователи могут точно показать проблему, которую ты сам не замечаешь. Но большая часть таких комментариев не помогает улучшить продукт. Люди просто сравнивают твою раннюю версию с компанией, которая существует десять лет, и делают вывод, что начинать не имело смысла.&lt;p&gt;Если воспринимать каждый такой комментарий как объективную оценку идеи, руки опустятся очень быстро. Первые месяцы ты и сам не уверен, что занимаешься чем-то разумным. Любой случайный человек может зародить в тебе сомнение буквально одним комментом.&lt;p&gt;Поэтому здесь придется в каком-то смысле себя надломать. Продолжать работать, когда цифры почти не растут. Публиковать материалы, которые никто не читает. Выпускать функции, которые не срабатывают. Признавать ошибки, менять направление и снова пробовать. При этом нужно не игнорировать обратную связь, но и не позволять каждому комментатору решать судьбу проекта.&lt;p&gt;Наверное, это и есть одна из главных предпринимательских компетенций: достаточно долго выглядеть человеком, который занимается какой-то ерундой.&lt;h3&gt;Так свой продукт все еще рискованнее найма или уже нет?&lt;/h3&gt;&lt;p&gt;Я не хочу делать вывод, что всем разработчикам нужно срочно увольняться и запускать стартап. У меня самого пока нет продукта, который бы прокормил меня вместо зарплаты. Я продолжаю работать в найме и понимаю, насколько тяжело довести собственный проект даже до первых стабильных денег.&lt;p&gt;Но само соотношение рисков изменилось.&lt;p&gt;Раньше мы сравнивали почти гарантированную зарплату с небольшой вероятностью успеха собственного проекта. Теперь с одной стороны находится рынок, на котором исчезают целые роли, растут требования, сокращаются команды и постоянно меняются правила игры. С другой стороны все еще находится рискованный продукт, но современные инструменты позволяют одному человеку сделать то, для чего раньше требовалась команда.&lt;p&gt;В найме можно попытаться стать последним универсальным разработчиком, который умеет все и выдерживает растущую нагрузку. Можно занять место человека, который автоматизирует работу остальных и строит внутри компании AI-процессы. Оба пути выглядят разумно, и я сам продолжаю двигаться в эту сторону (на всякий случай).&lt;p&gt;Но есть и третий вариант: использовать те же инструменты, чтобы строить что-то свое. Не ждать, пока очередная компания решит, насколько ты ей нужен, а самостоятельно искать пользователей и создавать для них продукт. Или развивать собственную аудиторию и становиться человеком, за которым следят, потому что он показывает работающий путь через весь этот хаос.&lt;p&gt;Вероятность успеха здесь все еще невысока. Только вероятность спокойно проработать следующие десять лет в одной привычной роли тоже уже не выглядит высокой.&lt;p&gt;И если раньше свой продукт казался мне попыткой отказаться от стабильности ради потенциально большого результата, то сейчас я смотрю на него иначе. Возможно, стабильности уже нет ни в одном из вариантов. Просто в своем проекте у тебя хотя бы остается шанс построить что-то, что принадлежит тебе, растет вместе с твоими усилиями и однажды позволит зарабатывать не пять тысяч долларов в месяц, а значительно больше.&lt;p&gt;Это, конечно, не гарантия. Можно даже сказать, что это просто диверсификация рисков, если делать все параллельно.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <author>vital_pavlenko</author>
      <guid>https://habr.com/ru/articles/1082238/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082238</guid>
      <pubDate>Mon, 14 Sep 2026 23:38:35 +0000</pubDate>
    </item>
    <item>
      <title>Cимулируем жизнь 30 млн микробов в браузере</title>
      <link>https://habr.com/ru/articles/1082240/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082240</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/b8c/178/a59/b8c178a59ea643a0dd22e59e36fd4115.png width=1142 height=905 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/b8c/178/a59/b8c178a59ea643a0dd22e59e36fd4115.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/b8c/178/a59/b8c178a59ea643a0dd22e59e36fd4115.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;WebGPU даёт прямой доступ к современным возможностям видеокарты: не только к рендерингу, но и к вычислениям общего назначения через compute shaders.&lt;p&gt;Но насколько далеко это можно довести в обычном браузере? Что произойдёт, если вычисления — это полноценная симуляция с десятками миллионов объектов? Разберемся в этой статье.&lt;h2&gt;Предыстория&lt;/h2&gt;&lt;p&gt;Для эксперимента попробуем воскресить симулятор эволюции, написанный много лет назад, и переписать его логику с CPU на GPU. Подробней об этом проекте я рассказывал в &lt;a href=https://habr.com/ru/articles/327792 rel=&#34;noopener nofollow&#34;&gt;другой статье&lt;/a&gt;, но читать её необязательно — важные моменты я перескажу.&lt;p&gt;Мой клеточный симулятор был написан на C# и устроен вполне естественно: двумерная матрица объектов, отдельный список живых клеток и &lt;code&gt;foreach&lt;/code&gt;, который на каждом тике вызывал их логику. Клетка смотрела по сторонам, выбирала действие и сразу меняла общий мир. Этого хватало, чтобы на экране возникали популяции, которые борются между собой и эволюционируют.&lt;p&gt;Веб-версия того симулятора позволяла создавать миры максимум &lt;code&gt;200 × 200&lt;/code&gt;. Попробуем поднять планку до &lt;code&gt;8192 × 4096&lt;/code&gt;. Это около 33 млн ячеек, каждая из которых потенциально может быть занята живой клеткой.&lt;h2&gt;Сначала разберёмся с памятью&lt;/h2&gt;&lt;p&gt;Если на каждую из 33 млн позиций выделить всего одно 32-битное число, получится уже &lt;code&gt;128 MiB&lt;/code&gt;. А одного числа недостаточно: нужно где-то хранить возраст клетки, вид, энергию, индивидуальные признаки и ещё промежуточные данные, необходимые во время расчёта следующего состояния мира.&lt;p&gt;На CPU всё это удобно собрать в один объект типа &lt;code&gt;Cell&lt;/code&gt;. Но на таком масштабе привычный ООП-подход становится довольно дорогим. Добавили к каждой позиции ещё одно поле размером 4 байта — потенциально получили ещё &lt;code&gt;128 MiB&lt;/code&gt;..&lt;p&gt;В GPU-версии я разделил состояние на несколько массивов. В одном хранится основная информация о содержимом ячейки, отдельно лежат энергия, индивидуальные признаки и геном вида. Временные данные одного тика тоже живут в собственных массивах.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/8c3/b2d/129/8c3b2d1295923357c4e642738e75d74d.png alt=&#34;Организация памяти&#34; title=&#34;Организация памяти&#34; width=1380 height=403 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/8c3/b2d/129/8c3b2d1295923357c4e642738e75d74d.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/8c3/b2d/129/8c3b2d1295923357c4e642738e75d74d.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Организация памяти&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Такое разбиение оказалось полезно не только ради экономии памяти. Разным вычислительным проходам нужны разные данные: шейдеру, который меняет энергию, не обязательно видеть всё состояние клетки, а рендереру вообще не нужны временные запросы на перемещение клетки. В результате устройство памяти начинает зависеть от того, как эти данные будут проходить через GPU.&lt;h2&gt;От последовательного тика к параллельному&lt;/h2&gt;&lt;p&gt;В старой версии игра просто брала список живых клеток и по очереди вызывала их логику. Клетка смотрела на соседей, выбирала действие и сразу изменяла общий мир. Следующая клетка уже видела результат предыдущего хода.&lt;p&gt;То есть порядок обхода незаметно становился частью правил симуляции.&lt;p&gt;На GPU такой подход напрямую не переносится. Тысячи клеток могут обрабатываться одновременно, и нельзя рассчитывать, что одна из них обязательно выполнится раньше другой. Например, две клетки могут одновременно увидеть одну свободную позицию и обе решить туда переместиться.&lt;p&gt;Поэтому теперь один тик разбит на несколько последовательных стадий.&lt;p&gt;Сначала клетки только принимают решения и записывают свои &lt;code&gt;Intent&lt;/code&gt; — что именно они хотят сделать. При этом само состояние мира ещё не меняется.&lt;p&gt;Затем появляются &lt;code&gt;Claim&lt;/code&gt; — заявки на то, за что клетки конкурируют. Если несколько клеток претендуют на одну позицию, конфликт разрешается отдельно по заранее определённому правилу.&lt;p&gt;И только после разрешения конфликтов начинается стадия &lt;code&gt;Apply&lt;/code&gt;, которая действительно изменяет состояние мира.&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/260/2cc/741/2602cc741b5006cf4e63307a597c0280.png alt=&#34;Порядок вычислений&#34; title=&#34;Порядок вычислений&#34; width=288 height=619 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/260/2cc/741/2602cc741b5006cf4e63307a597c0280.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/260/2cc/741/2602cc741b5006cf4e63307a597c0280.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Порядок вычислений&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Это требует больше промежуточной памяти и нескольких вычислительных проходов, зато результат тика больше не зависит от случайного порядка выполнения тысяч параллельных потоков.&lt;h2&gt;Как показать 33 млн ячеек&lt;/h2&gt;&lt;p&gt;К этому моменту мир уже живёт и изменяется на GPU. И тут возникает довольно естественный вопрос: зачем вообще возвращать его на CPU только ради картинки?&lt;p&gt;Можно было бы после каждого тика копировать состояние обратно в JavaScript и рисовать оттуда. Но при таких размерах сам обмен данными быстро стал бы отдельной проблемой.&lt;p&gt;По этой причине полной CPU-копии мира в новой версии нет. Рендеринг читает необходимые данные прямо из GPU-буферов, а JavaScript в основном занимается интерфейсом, камерой и отправкой команд.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/973/1b7/480/9731b7480c8ef463306f411058d3ba7a.png alt=&#34;Симуляция на крупном масштабе&#34; title=&#34;Симуляция на крупном масштабе&#34; width=1136 height=905 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/973/1b7/480/9731b7480c8ef463306f411058d3ba7a.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/973/1b7/480/9731b7480c8ef463306f411058d3ba7a.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Симуляция на крупном масштабе&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;При сильном отдалении возникает другая проблема: отдельная клетка становится меньше одного пикселя. Рисовать её как самостоятельный объект уже бессмысленно.&lt;p&gt;Поэтому дальний вид уже строится из агрегированных участков мира. Чем дальше камера, тем меньше значение имеет конкретная клетка и тем важнее общая картина: плотность популяций, ресурсы и распределение видов.&lt;h2&gt;Что получилось&lt;/h2&gt;&lt;p&gt;Для проверки я попробовал максимально нагрузить симуляцию и заполнить клетками вообще весь доступный мир.&lt;p&gt;Даже в таком режиме на моём компьютере симуляция просчитывает примерно &lt;code&gt;5–10&lt;/code&gt; полных тиков в секунду.&lt;p&gt;Конечно, &lt;code&gt;5–10 тиков/с&lt;/code&gt; — не универсальная характеристика WebGPU. Скорость зависит от видеокарты, браузера, настроек симуляции и количества живых клеток. Но для меня важнее сам принцип: симуляция на десятках миллионов активных объектов оказывается вполне работоспособной прямо в обычной вкладке браузера.&lt;h2&gt;Итоги&lt;/h2&gt;&lt;p&gt;Когда я начинал этот ремейк, главным интересом для меня было понять, насколько большой живой мир вообще можно симулировать прямо во вкладке браузера.&lt;p&gt;Судя по результату, десятки миллионов объектов — вполне рабочий масштаб. Но для этого недостаточно просто перенести старый код на GPU, приходится перестраивать сами данные и логику вычислений под его модель работы.&lt;p&gt;Если хочется посмотреть на симуляцию вживую, текущая версия доступна здесь: &lt;a href=https://gigafloppa.itch.io/the-strongest-survives rel=&#34;noopener nofollow&#34;&gt;&lt;strong&gt;The Strongest Survives на itch.io&lt;/strong&gt;&lt;/a&gt;.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <author>KogerCoder</author>
      <guid>https://habr.com/ru/articles/1082240/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082240</guid>
      <pubDate>Mon, 14 Sep 2026 22:17:27 +0000</pubDate>
    </item>
    <item>
      <title>Что за зверь «internal OpenAI model» и почему DeepSeek догоняет OpenAI?</title>
      <link>https://habr.com/ru/articles/1082236/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082236</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;Публике не доступны внутренние модели фронтиер лабораторий. Я не очень верю про конспирологию в духе «у OpenAI в подвале сидит настоящий AGI, а наружу выдают обрезанный ChatGPT». Модель, которой пользуются её собственные исследователи, и модель, которую она продает вам через API, не обязаны быть одной и той же. Фронтиер лаба создает не одну нейросеть, а фабрику моделей: research-чекпоинты &amp;gt; экспериментальные модели &amp;gt; reward-модели &amp;gt; модели-учителя &amp;gt; модели для генерации синтетических данных &amp;gt; модели-evaluators &amp;gt; дистиллированные студенты &amp;gt; production-модели, и наружу попадает только последний элемент цепочки.&lt;p&gt;&lt;strong&gt;Зачем нужна дистилляция&lt;/strong&gt;&lt;p&gt;Допустим, вы обучили чудовищно дорогую модель, которая хорошо рассуждает, пишет код и решает математику, но каждый запрос к ней стоит огромных вычислений. Для исследователя внутри компании это рабочий инструмент, и он того стоит, если на кону триллионные рынки. Для продукта со 100 млн пользователей масштабировать доступ к таким моделям выгодно, поэтому вместо того чтобы гонять её на каждом запросе, её делают учителем: она генерирует качественные ответы, решения, траектории ризонинга, синтетические данные, иногда сами распределения вероятностей, а на этом поведении обучается студент поменьше, которому не нужно воспроизводить учителя целиком, достаточно перенести нужные возможности. Это одна из форм дистилляции знаний, и практика настолько распространенная, что OpenAI прямо предоставляет разработчикам официальный Model Distillation workflow: брать ответы более сильной модели, например GPT-4o или o1-preview, и обучать на них модель дешевле. Формулировка самой OpenAI довольно точная: взять продвинутую модель и получить сопоставимое качество на конкретной задаче при существенно меньшей стоимости. Из этого следует довольно простая вещь: лучшая research-модель и лучшая production-модель оптимизируются по разным функциям, потому что research-модель максимизирует возможности, а production-модель приходится одновременно вытягивать по intelligence, latency, стоимости GPU, throughput, reliability и safety.&lt;p&gt;Публичных примеров такой схемы за последние два года накопилось довольно много. Apple описывала внутреннего Mixture-of-Experts учителя, которого использовала при обучении своей небольшой on-device foundation model. Google обучала EmbeddingGemma с помощью более мощного учителя Gemini. Meta использовала логиты Llama 3.1 8B и 70B при создании маленьких Llama 3.2 1B и 3B. Microsoft прямо писала, что ранние Phi в значительной степени дистиллировали возможности GPT-4. DeepSeek после обучения R1 выпустила линейку дистиллированных моделей от 1.5B до 70B. Схема «сильный учитель &amp;gt; дешёвый студент» реальна и хорошо задокументирована.&lt;br&gt;&lt;p&gt;&lt;strong&gt;Чего отсюда не следует&lt;/strong&gt;&lt;p&gt;Здесь будто бы следует неправильный вывод: якобы любая публичная модель является урезанной версией секретной супермодели. Иногда учитель вообще публичный, Meta использовала публичные Llama 3.1, DeepSeek выложила и большую R1, и дистиллированные из нее модели, причём студент иногда обходит своего учителя на конкретном бенче или узкой задаче за счет специализации и пост-трейнинга. То есть не совсем верно утверждать, что весь open source является специально ухудшенной дистилляцией секретных моделей. А вот более слабое утверждение подтверждается вполне: публичный каталог моделей почти наверняка не показывает весь фронтиер возможностей конкретной лаборатории.&lt;p&gt;&lt;strong&gt;«An internal model at OpenAI»&lt;/strong&gt;&lt;p&gt;В 2026 году в математических статьях начала регулярно появляться очень необычная формулировка. 31 марта группа математиков опубликовала работу с решениями трёх задач Эрдёша, где в abstract прямо написано: “in each case, the proof is due entirely to an internal model at OpenAI.” Это не единичный случай: в феврале внутренняя модель OpenAI построила новое решение задачи Эрдёша, Нешетрила и Рёдля, в апреле вышла ещё одна работа с пятью доказательствами, полученными таким же образом, а затем внутренняя модель OpenAI нашла контрпример к гипотезе Эрдёша о unit distances, открытой около восьмидесяти лет, и доказательство проверили внешние математики. В сопутствующей статье указано, что первоначальный математический результат был сгенерирован в один проход внутренней моделью, после чего люди переработали изложение через Codex. W. T. Gowers написал, что если бы такое доказательство прислал человек, он без колебаний рекомендовал бы его к публикации в Annals of Mathematics. Mehtaab Sawhney в Twitter сформулировал это ровно так: “the solution was found by an internal model at OpenAI”, а Kevin Weil, тогда один из руководителей OpenAI, репостнул с комментарием, что AI решает всё больше открытых задач и доказательства становятся элегантнее по мере улучшения моделей. То есть существование моделей, которыми компания пользуется внутри задолго до публичного релиза, здесь уже не спекуляция, оно написано в математических статьях.&lt;p&gt;При этом несколько месяцев спустя Noam Brown из OpenAI рассказывал эту историю иначе, чем ее обычно пересказывают: компания просто обучила новую внутреннюю модель и решила посмотреть, что она умеет, контрпример нашёлся при относительно небольшом бюджете на инференс, но после публикации люди обнаружили, что примерно тот же путь вытаскивается и из уже публичной GPT-5.5, если правильно построить scaffolding. Значит, между «внутренняя модель умеет это» и «публичная модель не умеет этого» жёсткой границы может не быть вовсе: возможность уже находится внутри публичных весов, но остается латентной, пока не подобран правильный prompt, search strategy, верифайер или test-time compute. Отсюда следует, что «насколько умна модель» перестает быть свойством одних только весов и становится свойством связки модель + scaffolding + инструменты + бюджет на инференс + верифайер + search strategy + пост-трейнинг, а обычный чат-интерфейс оказывается довольно плохим прибором для измерения реального фронтиера возможностей.&lt;p&gt;&lt;strong&gt;Что тогда с Astra&lt;/strong&gt;&lt;p&gt;С Astra получился почти естественный эксперимент. До публичного релиза OpenAI уже использовала очень сильные внутренние модели в research, а 3 сентября 2026 года выпустила GPT-6 Astra и назвала её the most capable broadly deployed model: 98% на FrontierMath Tier 4 и, по заявлению компании, участие в решении давних математических задач. Вполне возможно, что то, что несколько месяцев назад в статье называлось просто внутренней моделью, относится к той же технологической линии, которая позже стала Astra. Но это мой вывод по косвенным признакам: OpenAI не раскрыла происхождение этих чекпойнтов настолько подробно, чтобы можно было честно провести стрелку от конкретной внутренней математической модели к Astra. Тем более из этого не следует, что где-то внутри уже существует модель на поколение сильнее Astra. Это вполне возможно, но доказательств этого пока нет.&lt;br&gt;&lt;p&gt;&lt;strong&gt;Обратная сторона дистилляции или феномен DeepSeek&lt;/strong&gt;&lt;p&gt;&lt;br&gt;Вы потратили миллиарды на вычисления, исследователей, RL, синтетические данные и eval infrastructure, а конкуренту для воспроизведения заметной части результата достаточно вытянуть несколько миллионов качественных ответов вашей модели через API. Ему не нужны ваши веса и необязательно понимать вашу архитектуру, он использует вашу модель как учителя, генерирует миллионы заданий, собирает ответы, фильтрует лучшие траектории ризонинга, обучает свою модель, потом использует вашу же модель как судью и повторяет цикл. Экономика получается перекошенная: вы платите за создание intelligence, конкурент платит за API-токены, чтобы этот intelligence перенести.&lt;br&gt;&lt;br&gt;Ещё в 2025 году OpenAI заявляла, что обнаружила признаки использования её моделей для дистилляции. В феврале 2026 года Anthropic опубликовала намного более конкретные данные: по её утверждению, DeepSeek, Moonshot и MiniMax вели против Claude дистилляционные кампании промышленного масштаба, более 24 000 фродовых аккаунтов и более 16 миллионов exchanges, и извлекались не случайные ответы, а дорогие кластеры возможностей: coding, reasoning, tool use, agents, grading, computer use. В Twitter Anthropic написала почти теми же словами: “16 million exchanges with Claude, extracting its capabilities to train and improve their own models.” Термин knowledge extraction здесь точнее, чем model stealing, потому что копировать веса никто не обязан, достаточно скопировать полезную функцию.&lt;p&gt;Отсюда возникает неудобная конструкция: фронтиер лабы сами постоянно используют дистилляцию, потому что это лучший известный способ сделать дорогой intelligence дешёвым, и одновременно стараются не дать дистиллировать себя. На техническом уровне это не лицемерие, разница в том, кто владеет учителем и имеет ли право использовать его ответы для обучения конкурента, но с точки зрения самой технологии происходит одно и то же: сильная модель производит supervision, на котором становится сильнее другая модель. Поэтому API перестал быть просто интерфейсом к продукту и стал каналом утечки возможностей, и чем больше вы отдаёте наружу (сырые логиты, пути ризонинга, оценки судьи, миллионы unrestricted-запросов), тем дешевле конкуренту построить студента. Ограничения API, rate limits, скрытые chain-of-thought, мониторинг координированных аккаунтов и запреты на competitive model training в лицензиях получают вполне понятную экономическую причину. Clément Delangue, CEO Hugging Face, в этом году писал, что его не удивит сценарий, где фронтиер лабы начнут сильнее ограничивать API и направят дефицитные вычисления прежде всего на собственные продукты.&lt;br&gt;&lt;p&gt;&lt;strong&gt;Что из этого следует&lt;/strong&gt;&lt;p&gt;Мы привыкли читать названия GPT, Claude, Gemini, Llama как отдельные законченные сущности: компания обучила GPT-6, значит GPT-6 и есть её intelligence. Реальности это соответствует всё хуже, потому что лаборатория выглядит скорее как непрерывный конвейер интеллекта: огромные training runs &amp;gt; чекпойнты &amp;gt; reinforcement learning &amp;gt; синтетические данные &amp;gt; учителя &amp;gt; критики &amp;gt; верифайеры &amp;gt; дистилляция &amp;gt; tool scaffolding &amp;gt; test-time compute &amp;gt; production deployment, а название модели, которое видит пользователь, это одна точка на этом конвейере, выбранная по соображениям стоимости и риска.&lt;br&gt;&lt;br&gt;Поэтому формулировка вопроса «какая сегодня самая умная модель» не очень удачная. Содержательные вопросы другие: какой максимум возможностей уже существует внутри лаборатории, какая его часть есть в research-чекпойнтах, какая уже скрыта в публичных весах и ждёт нормального scaffolding, какую часть удалось дёшево дистиллировать в production. И главный: насколько далеко реальный фронтиер ушёл вперёд от того, что видит человек, открывающий новый чат — лидерборд публичных моделей не является показателем возможностей AI.&lt;p&gt;&lt;strong&gt;Больше полезного контента в моем тг‑канале: &lt;/strong&gt;&lt;a href=https://t.me/airamongush rel=&#34;noopener nofollow&#34;&gt;&lt;strong&gt;t.me/airamongush&lt;/strong&gt;&lt;/a&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <author>a_mongush</author>
      <guid>https://habr.com/ru/articles/1082236/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082236</guid>
      <pubDate>Mon, 14 Sep 2026 21:31:53 +0000</pubDate>
    </item>
    <item>
      <title>Как я перенёс 7 000 треков из Яндекс Музыки в Spotify, хотя у одного нет API, а другой урезал квоту</title>
      <link>https://habr.com/ru/articles/1082228/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082228</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;Понадобилось переехать с Яндекс Музыки на Spotify вместе с библиотекой: 7 000 лайков, сотня плейлистов, альбомы, подписки на артистов. Казалось, задача на десять минут — сервисов переноса полно. Оказалось, что все они держатся на честном слове, а у Spotify с 2025 года такие ограничения для приложений, что «просто взять API» не получится. В итоге взял открытый скрипт с GitHub, переписал в нём половину и по дороге разобрался, как устроены лимиты обоих сервисов. Делюсь разбором и кодом.&lt;h3&gt;Почему не работают готовые сервисы&lt;/h3&gt;&lt;p&gt;У Яндекс Музыки &lt;strong&gt;нет открытого API&lt;/strong&gt;. Зарегистрировать OAuth-приложение со scope на музыку невозможно — Яндекс такого не выдаёт. Поэтому каждый сервис-посредник делает одно из двух:&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Просит логин и пароль от Яндекс ID&lt;/strong&gt; и логинится за вас с своих серверов. Так работают TuneMyMusic, Soundiiz, MusConv. С двухфакторкой или входом по QR это не проходит, а у Soundiiz в саппорте прямо написано: «Yandex does not provide an official API, the workaround doesn’t work for all users». На Trustpilot есть отзывы, где после переноса приходило письмо от Яндекса о подозрительном входе из Северной Каролины.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Парсит публичную страницу плейлиста&lt;/strong&gt; — так делают телеграм-боты вроде YMusicExport. Работает только для открытых плейлистов, «Мне нравится» так не вытащить, и боты регулярно ловят баны.&lt;/ul&gt;&lt;p&gt;Плюс лимиты бесплатных версий: 200–600 треков, дальше платно и только иностранной картой.&lt;p&gt;Есть третий путь, которым пользуется библиотека &lt;a href=https://github.com/MarshalX/yandex-music-api rel=&#34;noopener nofollow&#34;&gt;yandex-music-api&lt;/a&gt;: OAuth-токен самого приложения Яндекс Музыки. Открываете &lt;code&gt;oauth.yandex.ru/authorize?response_type=token&amp;amp;client_id=23cabbbdc6cd418abb4b39c32c41195d&lt;/code&gt;, логинитесь как обычно — и получаете в адресной строке токен с годовым сроком жизни. &lt;code&gt;client_id&lt;/code&gt; здесь — публичный идентификатор официального клиента, не секрет. Пароль никуда не уходит, токен отзывается в Яндекс ID. С этим токеном библиотека отдаёт лайки, плейлисты, альбомы и артистов с полными метаданными, включая &lt;code&gt;duration_ms&lt;/code&gt; — это пригодится.&lt;h3&gt;Что изменилось у Spotify&lt;/h3&gt;&lt;p&gt;Со Spotify ситуация обратная: API официальный и хорошо документированный, но в 2025 году Spotify резко закрутил гайки для сторонних приложений.&lt;p&gt;Любое новое приложение стартует в &lt;strong&gt;Development mode&lt;/strong&gt;. Из &lt;a href=https://developer.spotify.com/documentation/web-api/concepts/quota-modes rel=&#34;noopener nofollow&#34;&gt;документации&lt;/a&gt;:&lt;ul&gt;&lt;li&gt;&lt;p&gt;максимум &lt;strong&gt;5 авторизованных пользователей&lt;/strong&gt;, каждого нужно вручную добавить в allowlist;&lt;li&gt;&lt;p&gt;владелец приложения обязан иметь Premium;&lt;li&gt;&lt;p&gt;есть &lt;strong&gt;квота запросов на аккаунт разработчика&lt;/strong&gt; — при превышении API отдаёт &lt;code&gt;429&lt;/code&gt; с &lt;code&gt;&amp;#34;reason&amp;#34;: &amp;#34;QUOTA_EXCEEDED&amp;#34;&lt;/code&gt; и &lt;code&gt;Retry-After&lt;/code&gt; в сутки.&lt;/ul&gt;&lt;p&gt;Выйти из Development mode в &lt;strong&gt;Extended quota mode&lt;/strong&gt; с 15 мая 2025 могут только юрлица с действующим продуктом и &lt;strong&gt;минимум 250 000 MAU&lt;/strong&gt;. Для pet-проекта это закрытая дверь. Практический вывод: любой «сервис переноса в Spotify», который вы можете написать, по правилам обслужит пять человек — включая вас. Единственная легальная схема — каждый пользователь создаёт своё приложение и работает под своей квотой.&lt;p&gt;Квота на практике — &lt;strong&gt;примерно 700 треков в сутки&lt;/strong&gt;: каждый трек это один запрос &lt;code&gt;search&lt;/code&gt;, плюс один запрос на сохранение пачки. Уперлись — ждите 24 часа.&lt;p&gt;Отсюда требования к скрипту: он обязан переживать суточную блокировку, продолжать с того же места и не делать лишних запросов.&lt;h3&gt;Скрипт: что было и что стало&lt;/h3&gt;&lt;p&gt;Основа — &lt;a href=https://github.com/MarshalX/yandex2spotify rel=&#34;noopener nofollow&#34;&gt;MarshalX/yandex2spotify&lt;/a&gt;, простой скрипт 2020 года: взять лайки из Яндекса, для каждого сделать &lt;code&gt;search&lt;/code&gt; в Spotify, взять первый результат, сохранить. Для библиотеки в 200 треков — прекрасно. Для 7 000 — четыре проблемы.&lt;h4&gt;1. Rate limit: почему нельзя отдавать 429 на откуп urllib3&lt;/h4&gt;&lt;p&gt;Spotipy умеет ретраить через &lt;code&gt;urllib3.Retry&lt;/code&gt;, и по умолчанию 429 в списке &lt;code&gt;status_forcelist&lt;/code&gt;. Звучит удобно, но на суточной блокировке это катастрофа: urllib3 отретраит с экспоненциальным бэкоффом, исчерпает попытки и бросит исключение, в котором &lt;strong&gt;уже нет заголовка &lt;/strong&gt;&lt;code&gt;&lt;strong&gt;Retry-After&lt;/strong&gt;&lt;/code&gt; — понять, сколько ждать, невозможно.&lt;p&gt;Поэтому 429 исключён из forcelist намеренно, а обработка вынесена в декоратор:&lt;pre&gt;&lt;code class=python&gt;# 429 исключён: пусть urllib3 его НЕ перехватывает, иначе spotipy отдаёт&#xA;# исключение без Retry-After. 5xx по-прежнему ретраятся штатно.&#xA;RETRY_STATUS_FORCELIST = (500, 502, 503, 504)&#xA;MAX_RATE_LIMIT_WAIT = 300&#xA;&#xA;def handle_spotify_exception(func):&#xA;    def wrapper(*args, **kwargs):&#xA;        while True:&#xA;            try:&#xA;                return func(*args, **kwargs)&#xA;            except SpotifyException as exception:&#xA;                if exception.http_status != 429:&#xA;                    raise&#xA;                headers = exception.headers or {}&#xA;                raw = headers.get(&amp;#39;retry-after&amp;#39;) or headers.get(&amp;#39;Retry-After&amp;#39;)&#xA;                if raw is None:&#xA;                    raise RateLimitExceeded(None)   # длительность неизвестна — не крутимся вслепую&#xA;                retry_after = int(float(raw))&#xA;                if retry_after &amp;gt; MAX_RATE_LIMIT_WAIT:&#xA;                    raise RateLimitExceeded(retry_after)   # суточная квота — выходим&#xA;                sleep(retry_after + 1)                     # обычный throttling — ждём&#xA;    return wrapper&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:87px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Короткие троттлинги (секунды) скрипт пересиживает сам. Всё, что больше пяти минут, — это квота: скрипт печатает, когда возвращаться, и выходит с кодом 2.&lt;h4&gt;2. Журнал прогресса вместо «запустить заново»&lt;/h4&gt;&lt;p&gt;Раз скрипт живёт несколько дней, ему нужна память. Append-only JSONL с &lt;code&gt;fsync&lt;/code&gt; после каждой записи — переживает &lt;code&gt;Ctrl+C&lt;/code&gt;, падения и выключение ноутбука:&lt;pre&gt;&lt;code class=python&gt;class Progress:&#xA;    def mark(self, key, not_found=None):&#xA;        if key is None or key in self.done:&#xA;            return&#xA;        self.done.add(key)&#xA;        rec = {&amp;#39;t&amp;#39;: &amp;#39;item&amp;#39;, &amp;#39;k&amp;#39;: key}&#xA;        if not_found is not None:&#xA;            self.not_found[key] = not_found   # название — для итогового отчёта&#xA;            rec[&amp;#39;nf&amp;#39;] = not_found&#xA;        self._append(rec)   # json.dumps + flush + os.fsync&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Ключи — &lt;code&gt;like:&amp;lt;id&amp;gt;&lt;/code&gt;, &lt;code&gt;pl:&amp;lt;kind&amp;gt;:&amp;lt;id&amp;gt;&lt;/code&gt;, &lt;code&gt;album:&amp;lt;id&amp;gt;&lt;/code&gt;, &lt;code&gt;artist:&amp;lt;id&amp;gt;&lt;/code&gt;. Важная деталь: трек помечается «сделанным» &lt;strong&gt;только после того, как Spotify подтвердил запись пачки&lt;/strong&gt;, а не после успешного поиска. Иначе при падении между поиском и сохранением трек теряется навсегда.&lt;p&gt;Плейлисты журналируются отдельно (&lt;code&gt;kind → spotify_id&lt;/code&gt;), чтобы при повторном запуске не создать дубли, а обложка — ещё отдельнее: если её загрузка упала на 429, при следующем запуске плейлист переиспользуется, а обложка догружается.&lt;p&gt;Список «не найдено» тоже пишется в журнал с названием трека. Без этого отчёт в конце многодневного переноса показывал бы только последний день — я на это наступил.&lt;h4&gt;3. Матчинг: первый результат поиска — это лотерея&lt;/h4&gt;&lt;p&gt;&lt;code&gt;search(&amp;#34;Земфира Искала&amp;#34;)&lt;/code&gt; возвращает десять кандидатов, и первый — не обязательно то, что нужно. Караоке-версии, каверы, live-записи, «Tribute to» — всё это выше оригинала бывает регулярно. Для платного сервиса это жалобы, для себя — мусор в библиотеке.&lt;p&gt;Яндекс отдаёт &lt;code&gt;duration_ms&lt;/code&gt;, Spotify — тоже. Длительность — сигнал, независимый от языка написания артиста («Земфира» у Яндекса и «Zemfira» у Spotify — обычная история). Плюс сверка имени артиста, плюс название как tie-breaker:&lt;pre&gt;&lt;code class=python&gt;def pick_match(item, candidates):&#xA;    best_id, best_score = None, 0&#xA;    for cand in candidates:&#xA;        score = 0&#xA;        if abs(cand[&amp;#39;duration_ms&amp;#39;] - item.duration_ms) &amp;lt;= 3000:   # ±3 с&#xA;            score += 2&#xA;        if _names_overlap(item.artists, cand[&amp;#39;artists&amp;#39;]):&#xA;            score += 2&#xA;        if label_key(item.title) == label_key(cand[&amp;#39;name&amp;#39;]):&#xA;            score += 1&#xA;        if score &amp;gt; best_score:&#xA;            best_id, best_score = cand[&amp;#39;id&amp;#39;], score&#xA;    return best_id if best_score &amp;gt;= 2 else None&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Одного совпадения названия недостаточно (у караоке-версии оно такое же), а длительности или артиста — достаточно. Для альбомов вместо длительности — число треков: &lt;code&gt;total_tracks&lt;/code&gt; у Spotify против &lt;code&gt;track_count&lt;/code&gt; у Яндекса, это отсекает deluxe-издания и трибьюты. Ничего не набрало два балла — честно в «не найдено», а не первое попавшееся.&lt;p&gt;Отдельная ловушка в &lt;code&gt;_names_overlap&lt;/code&gt;: сравнение подстрокой нужно для случаев вида &lt;code&gt;KINO (Кино)&lt;/code&gt;, но артист с именем «Ю» совпадёт с любой Юлией. Подстрока — только от трёх символов.&lt;p&gt;На моей библиотеке после этого не нашлось меньше процента треков — мелкие лейблы и VK-релизы, которых в Spotify просто нет.&lt;h4&gt;4. Тихая потеря данных&lt;/h4&gt;&lt;p&gt;В оригинале любая ошибка Spotify при обработке трека ловилась одним &lt;code&gt;except SpotifyException&lt;/code&gt; и трек &lt;strong&gt;помечался обработанным&lt;/strong&gt;. То есть 502 после исчерпания ретраев, 400 на кривом запросе или 403 «пользователь не в allowlist» — и трек навсегда считался перенесённым. Ни в отчёте, ни в Spotify.&lt;p&gt;Теперь транзиентные ошибки не помечают трек — он попадёт в следующий запуск. А 401/403 — это не проблема трека, а проблема авторизации: скрипт прерывает прогон сразу вместо того, чтобы 7 000 раз написать «NO».&lt;p&gt;Там же нашлась ещё одна: если запрос сохранения пачки падал, буфер не очищался, и каждый следующий трек снова дёргал падающий save с растущей пачкой. Для альбомов, где лимит 20 id на запрос, это гарантированный 400 навсегда. Упавшая пачка теперь сбрасывается целиком без отметки — и повторяется на следующий день.&lt;p&gt;Кстати о лимитах на запрос: у Spotify они разные по эндпоинтам — 50 id для сохранения треков, &lt;strong&gt;20 для альбомов&lt;/strong&gt;, 50 для подписок, 100 для добавления в плейлист. Один общий &lt;code&gt;--chunk&lt;/code&gt; на всё в оригинале ломал импорт альбомов на 21-м.&lt;h3&gt;Тесты&lt;/h3&gt;&lt;p&gt;Весь матчинг и обработка ошибок покрыты &lt;code&gt;unittest&lt;/code&gt; — 36 тестов на stdlib, без зависимостей. Фейковый Spotify-клиент отдаёт заранее заданные результаты поиска, фейковый Яндекс — треки с нужными &lt;code&gt;duration_ms&lt;/code&gt;. Каждый описанный выше баг сначала воспроизведён тестом, потом починен. Например, каскад падающих пачек:&lt;pre&gt;&lt;code class=python&gt;def test_failed_batch_is_dropped_and_later_batches_still_save(self):&#xA;    spotify, importer = self.make([SpotifyException(400, -1, &amp;#39;invalid id&amp;#39;)])&#xA;    importer.import_likes()&#xA;    self.assertEqual(spotify.saved, [[&amp;#39;sp1&amp;#39;, &amp;#39;sp0&amp;#39;]])           # вторая пачка дошла&#xA;    self.assertFalse(importer.progress.is_done(&amp;#39;like:3&amp;#39;))       # из первой — не помечены&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h3&gt;Как пользоваться&lt;/h3&gt;&lt;p&gt;Коротко, подробный гайд — в &lt;a href=https://github.com/BuevichRoman/yandex2spotify/blob/main/GUIDE.md rel=&#34;noopener nofollow&#34;&gt;GUIDE.md&lt;/a&gt; репозитория.&lt;ol&gt;&lt;li&gt;&lt;p&gt;Создать приложение в &lt;a href=https://developer.spotify.com/dashboard rel=&#34;noopener nofollow&#34;&gt;Spotify Dashboard&lt;/a&gt;, redirect URI &lt;code&gt;https://open.spotify.com&lt;/code&gt;, взять Client ID и Secret. Нужен Premium.&lt;li&gt;&lt;p&gt;Получить токен Яндекса по ссылке выше.&lt;li&gt;&lt;p&gt;Запустить:&lt;/ol&gt;&lt;pre&gt;&lt;code class=bash&gt;export SPOTIFY_CLIENT_ID=... SPOTIFY_CLIENT_SECRET=... YANDEX_TOKEN=...&#xA;python3 importer.py -u any_name&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Через ~700 треков скрипт остановится с точным временем возврата. Та же команда завтра — продолжит. 7 000 треков — десять дней по одному запуску, участия не требуется.&lt;h3&gt;Что не решено&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Плейлисты создаются публичными&lt;/strong&gt; — scope &lt;code&gt;playlist-modify-public&lt;/code&gt;; приватные — следующим шагом.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Артисты матчатся по первому результату&lt;/strong&gt; — там кроме имени сигналов нет, а транслитерация делает точное сравнение бесполезным.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Квота — на аккаунт разработчика, не на приложение.&lt;/strong&gt; Ускорить перенос нельзя ни вторым приложением, ни параллельными запусками. Только ждать.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Обратный перенос&lt;/strong&gt; (Spotify → Яндекс) не реализован, у Яндекс Музыки для этого есть встроенный импорт.&lt;/ul&gt;&lt;p&gt;Код: &lt;a href=https://github.com/BuevichRoman/yandex2spotify rel=&#34;noopener nofollow&#34;&gt;github.com/BuevichRoman/yandex2spotify&lt;/a&gt;, MIT, форк &lt;a href=https://github.com/MarshalX/yandex2spotify rel=&#34;noopener nofollow&#34;&gt;MarshalX/yandex2spotify&lt;/a&gt;. Issues и PR приветствуются — особенно если у кого-то есть идея, как матчить артистов с транслитерацией.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <author>23RIRI</author>
      <guid>https://habr.com/ru/articles/1082228/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082228</guid>
      <pubDate>Mon, 14 Sep 2026 20:43:46 +0000</pubDate>
    </item>
    <item>
      <title>Что можно сделать с массивом массивов</title>
      <link>https://habr.com/ru/articles/1080266/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1080266</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/13a/149/1e9/13a1491e97a764aba0b11a9129b81427.png width=554 height=554 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/13a/149/1e9/13a1491e97a764aba0b11a9129b81427.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/13a/149/1e9/13a1491e97a764aba0b11a9129b81427.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;На одном из проектов (4Х историческая стратегия) появилась задача убрать часть логики в потоки, отдав им снапшот игрового состояния, чтобы пока основной поток считает свой тик, остальные (AI, поиск пути, UI и др) могли крутить свою логику, вроде &amp;#34;кто стоит в этой локации&amp;#34; и делать это без блокировок или риска увидеть половину чужой записи. Чтобы реализовать такую систему, надо придумать как получить обратный индекс полка по локации, причем сделать поиск дешевым для потоков, т.е. у потока должен быть свой снапшот состояния некоторой части игрового мира на момент старта апдейта (кадра, тика логики, дня, месяца и т.д)&lt;p&gt;Общепринятая практика - это сделать данные иммутабельными на время кадра, и построить нужный индекс один раз на старте, а дальше дать читателям возможность работать с ним. И вообщем от ребят, которые делали эту задачу на ревью прилетел вот такой код (выделю тут только основную часть):&lt;pre&gt;&lt;code class=cpp&gt;struct Regiment {&#xA;    unsigned int location;&#xA;    float        health;&#xA;    float        mood;&#xA;    float        speed;&#xA;};&#xA;&#xA;std::vector&amp;lt;Regiment&amp;gt; regiments; // все полки мира&#xA;std::vector&amp;lt;std::vector&amp;lt;unsigned int&amp;gt;&amp;gt; by_location(location_count);&#xA;&#xA;for (unsigned int r = 0; r &amp;lt; regiment_count; ++r) {&#xA;    by_location[regiment_location[r]].push_back(r);&#xA;}&#xA;&#xA;... тут мы что-то делаем с полученным массивом &lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:87px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Такая структура (regiments) называется jagged array, массив массивов (&lt;a href=https://www.ozon.ru/product/game-ustroystvo-i-optimizatsiya-igrovogo-dvizhka-3541648050 rel=&#34;noopener nofollow&#34;&gt;зачем она и как с ней работать я показывал в книге Game++&lt;/a&gt;), или, если вам ближе академическая терминология, CSR (compressed sparse row) немного другая форма записи таких массивов, либо разреженные матрицы. И такие стуктуры довольно частое явление в играх, если у вас много локаций и вам надо:&lt;hr&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;узнать какой лут разложен в каждой локации, какие армии принадлежат каждой области;&lt;li&gt;&lt;p&gt;или какие монстры живут в локации, какие локации входят в область, какие области в регион;&lt;li&gt;&lt;p&gt;или почекать соседей локации на карте, adjacency region, на чем строится pathfinding и вся заливка областей в глобальных стратегих;&lt;li&gt;&lt;p&gt;или найти узлы в иерархии сцены или в скелете;&lt;li&gt;&lt;p&gt;поискать сущности по чанку мира, компоненты по сущности, entity по архетипу в ECS;&lt;li&gt;&lt;p&gt;найти подписчиков по типу события, активные модификаторы по объекту&lt;li&gt;&lt;p&gt;походить по соседям локации и пособирать метрики&lt;li&gt;&lt;p&gt;выбрать полки по армии / корпусу, чтобы посчитать суммарный &lt;code&gt;health&lt;/code&gt; и средний &lt;code&gt;mood&lt;/code&gt; соединения;&lt;li&gt;&lt;p&gt;то же отношение &amp;#34;владелец → набор сущностей&amp;#34; для зданий в локации, ордеров на рынке, модификаторов на стране, соседей на карте, entity по чанку в ECS.&lt;li&gt;&lt;p&gt;и еще много чего...&lt;/ul&gt;&lt;p&gt;Общее у этих примеров, что отношения между элементами уже присутствуют в исходных данных и нам не надо придумывать новые связи, а надо просто сделать структуру, чтобы быстро выбирать некоторый набор. Например если есть список армий, для которого написано, где он стоит, то надо быстро получить нужен обратный индекс, в идеале чтобы эта операция занимала константное время и не сильно ворошила кеш процессора.&lt;p&gt;Еще одно неявное свойство - эту структуру мы обновляем редко, а читаем много раз. Обновляем на загрузке карты, на старте кадра или после перестройки сцены и в целом когда - уже не важно, но важно, что фазы build и query разделены. Выглядит как достаточно жёсткое ограничение, но посмотрите на список выше... там почти все данные на некотором интервале времени не меняют свою позицию внутри общей структуры и это открывает дорогу к разным трюкам и хакам.&lt;h2&gt;Идея vs реальная жизнь&lt;/h2&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/162/7ab/d6d/1627abd6dd08823d69ddf8a6912beef5.png width=1053 height=632 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/162/7ab/d6d/1627abd6dd08823d69ddf8a6912beef5.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/162/7ab/d6d/1627abd6dd08823d69ddf8a6912beef5.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;И вроде всё хорошо, пока у вас этот массив массивов маленький, но для нашего кейса использования выяснилось, что снапшот такой структуры после середины игры стоит дороже, чем вся работа, ради которой его делали и например скопировать &lt;code&gt;std::vector&amp;lt;std::vector&amp;lt;unsigned int&amp;gt;&amp;gt;&lt;/code&gt; на миллион локаций - это глубокая копия миллиона векторов занимает 90 миллисекунд (на моей машине, на цифры не смотрите он показательные и важно отношение одного к другому, а не абсолютное время). Полный обход этой же структуры, ради которого снапшот и нужен, занимает 14 миллисекунд, т.е. мы собирались распараллелить работу на 14 мс и заплатили 90 мс просто, чтобы начать. Для тестов я взял миллион элементов во внешнем массиве и шесть миллионов элементов суммарно во внутренних, то есть в среднем шесть на список, что на самом деле немного и столько суммарно набегает в разных системах к середине сессии где-нибудь в Victoria3/EU4.&lt;p&gt;Давайте посчитаем, за что мы заплатили. Cам внешний массив&lt;code&gt;std::vector&lt;/code&gt; на 64-битной системе будет 24 байта (три указателя, либо указатель плюс два размера) и миллион элементов занимает 24 мегабайта только на служебную обвязку, ещё до всяких данных, причём эти 24 байта платятся за каждую локацию, включая пустые. И если юниты стоят в пяти процентах локаций карты, то остальные 95% честно занимают свои 24 байта, но, что хуже, занимают и место в кэше, когда вы по этому массиву массивов проходите.&lt;p&gt;Еще одно неявное свойство, которое часто упускают - это запас (capacity) и реальный размер блока памяти&lt;code&gt;vectorа&lt;/code&gt; растёт быстрее, чем реальный размер вектора, и наши 6 элементов, на самом деле занимают восемь &amp;#34;объемов&amp;#34; данных.&lt;p&gt;Это еще не все и тут надо вспомнить про метаданные аллокатора и округление блока, которые от нас скрыты но тоже занимают место в кучу, и у glibc заголовок чанка плюс выравнивание превращают запрос на 32 байта в блок на 48, то есть на данные, которых всего 24 байта, потрачено 48.&lt;p&gt;Но и это еще не все, и чтобы дойти до capacity 8, вектор аллоцируется четыре раза, и три раза при этом копирует уже накопленное, поэтому на постройку миллиона списков у нас будет примерно четыре миллиона malloc и три миллиона free. Пусть это уже не про память и это все растянуто по фреймам, но это чистое время, которое вынуждены забрать у игры, я называю это &amp;#34;размазаным&amp;#34; перфом, в смысле что мы &amp;#34;размазали&amp;#34; наше время кадра по множеству мелких мест. Итого получаем на элемент (~примерно): 24 байта на внешний &lt;code&gt;vector&lt;/code&gt;, 48 байт на блок в куче и семьдесят два байта, чтобы сохранить двадцать четыре.&lt;pre&gt;&lt;code class=cpp&gt;Наивная схема, миллион локаций:&#xA;regiments[]                   куча (блоки разбросаны)&#xA;+----------------+            +--------------------------+&#xA;| ptr size cap   | ---------&amp;gt; | hdr | 7 idx | 2 unused  |  48 байт&#xA;+----------------+            +--------------------------+&#xA;| ptr size cap   | ---------&amp;gt; | hdr | 4 idx | 2 unused  |  48 байт&#xA;+----------------+            +--------------------------+&#xA;| ptr size cap   | --&amp;gt; nullptr   (локация пустая, но 24 байта урплочены)&#xA;+----------------+&#xA;   24 байта              +          48 байт          = 72 байта на 24 полезных &#xA;// примерно, зависит от режима сборки&#xA;&#xA;regiments[]  [ {loc, health, mood, speed}, ... ]   // плотный массив сущностей&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;И вот возникает важный момент, из-за которого вообще имело смысл лезть внутрь этого кода и что-то чинить внутри массива массивов, и вся эта математика с байтами выше и разговор про размер памяти это всего лишь следствие, а главная причина - опять кеш. Когда игровой апдейт идет по локациям и для каждого полка читает &lt;code&gt;mood&lt;/code&gt; и &lt;code&gt;speed&lt;/code&gt;, процессор на каждый список делает минимум два обращения за индексом: одно в &lt;code&gt;by_location[i]&lt;/code&gt;, второй по указателю в кучу, потом еще один за данными. Но второй и (особенно) третий прыжок будет почти гарантированным промахом, когда cpu придется остановить работу сходить в L2/L3 или вообще в оператику, потому что блоки лежат вразнобой (может быть между чужими аллокациями других подсистем игры) и пока процессор ждёт ответа от памяти, он не делает ничего, что превращает такие пустые такты в просадку кадра, когда &amp;#34;у вас всего миллион объектов&amp;#34;.&lt;h3&gt;Замеры, чтобы дальше было с чем сравнивать&lt;/h3&gt;&lt;p&gt;Дальше по тексту я буду возвращаться к первому бенчмарку (миллион локаций, шесть миллионов структур, случайное распределение по локациям с длинным хвостом, x86-64, MSVC 19.4 в релизе). Длинный хвост означает, что часть ваших данных выпадает из кеша и за ними приходится идти в оперативку. Меряется отдельно:&lt;ul&gt;&lt;li&gt;&lt;p&gt;build - построить индекс локация → сущности с нуля;&lt;li&gt;&lt;p&gt;query - пройти все списки по порядку локаций (прочитать &lt;code&gt;health&lt;/code&gt; / &lt;code&gt;mood&lt;/code&gt; / &lt;code&gt;speed&lt;/code&gt;) и выполнить какую-то логику над ними;&lt;li&gt;&lt;p&gt;query random - то же самое, что и build, но локации обходятся в случайном порядке (так выглядит реальный запрос из геймплея, когда игрок что-то делает в процессе игры);&lt;li&gt;&lt;p&gt;snapshot - глубокая копия индекса для другого потока (начало кадра, раскидываем данные по потокам);&lt;li&gt;&lt;p&gt;teardown - освободить индекс (редко, такое надо откладывать на конец фрейма).&lt;/ul&gt;&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th data-colwidth=105 width=105&gt;&lt;p&gt;Вариант&lt;th data-colwidth=96 width=96&gt;&lt;p&gt;Память&lt;th data-colwidth=127 width=127&gt;&lt;p&gt;Аллокаций&lt;th data-colwidth=85 width=85&gt;&lt;p&gt;Build&lt;th data-colwidth=82 width=82&gt;&lt;p&gt;Query&lt;th data-colwidth=96 width=96&gt;&lt;p&gt;Query random&lt;th data-colwidth=102 width=102&gt;&lt;p&gt;Snapshot&lt;th&gt;&lt;p&gt;Teardown&lt;tr&gt;&lt;td data-colwidth=105 width=105&gt;&lt;p align=left&gt;&lt;code&gt;vector&amp;lt;vector&amp;lt;uint&amp;gt;&amp;gt;&lt;/code&gt;&lt;td data-colwidth=96 width=96&gt;&lt;p align=left&gt;63.1 МБ&lt;td data-colwidth=127 width=127&gt;&lt;p align=left&gt;3.42М&lt;td data-colwidth=85 width=85&gt;&lt;p align=left&gt;1045 мс&lt;td data-colwidth=82 width=82&gt;&lt;p align=left&gt;14.5 мс&lt;td data-colwidth=96 width=96&gt;&lt;p align=left&gt;96.8 мс&lt;td data-colwidth=102 width=102&gt;&lt;p align=left&gt;96.7 мс&lt;td&gt;&lt;p align=left&gt;61.8 мс&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Почему аллокаций так много? А потому что мы не знаем длину каждого списка заранее, отсюда получаем и рост capacity, и цепочку реаллокаций, и запас в буфере.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/2cf/a52/370/2cfa52370f4c4584ba58713d255a32e4.png width=1653 height=842 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/2cf/a52/370/2cfa52370f4c4584ba58713d255a32e4.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/2cf/a52/370/2cfa52370f4c4584ba58713d255a32e4.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;h2&gt;Хак первый&lt;/h2&gt;&lt;p&gt;Но нам никто не мешает узнать у каждой сущности location - данные-то уже есть и надо просто пройти по ним лишний раз и посчитать, поэтому...&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/457/5d8/7bb/4575d87bbfa3113e445683e755dd8dbc.png width=600 height=400 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/457/5d8/7bb/4575d87bbfa3113e445683e755dd8dbc.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/457/5d8/7bb/4575d87bbfa3113e445683e755dd8dbc.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;pre&gt;&lt;code class=cpp&gt;struct RegimentList {&#xA;    unsigned int count   = 0;&#xA;    unsigned int written = 0;&#xA;    unsigned int* data   = nullptr;&#xA;};&#xA;&#xA;std::vector&amp;lt;RegimentList&amp;gt; by_location(location_count);&#xA;&#xA;// проход 1: сколько полков в каждой локации&#xA;for (unsigned int r = 0; r &amp;lt; regiment_count; ++r)&#xA;    by_location[regiments[r].location].count++;&#xA;&#xA;// проход 2: выделить ровно столько, сколько надо&#xA;for (unsigned int i = 0; i &amp;lt; location_count; ++i)&#xA;    if (by_location[i].count &amp;gt; 0)&#xA;        by_location[i].data = new unsigned int[by_location[i].count];&#xA;&#xA;// проход 3: разложить индексы&#xA;for (unsigned int r = 0; r &amp;lt; regiment_count; ++r) {&#xA;    RegimentList&amp;amp; list = by_location[regiments[r].location];&#xA;    list.data[list.written] = r;&#xA;    list.written++;&#xA;}&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Да, теперь проходов стало три вместо одного, но времени стало тратиться меньше, потому что лишние проходы по плотному массиву стоят дешево, а сэкономленные три миллиона реаллокаций стоят дорого. Это, кстати, общий сюжет оптимизаций такого рода, когда линейный проход по памяти почти бесплатен по сравнению с любой работой в куче.&lt;p&gt;Заодно мы выбросили &lt;code&gt;std::vector&lt;/code&gt; из внутренних списков и нам стали не нужны его size и capacity, длину мы уже знаем, а расти список больше не будет до следующего апдейта. Там осталась только структура на 16 байт вместо 24 и один блок в куче на непустой.&lt;p&gt;Отдельно скажу про соблазн заменить всё это на &lt;code&gt;reserve()&lt;/code&gt; с угаданным числом. Работать это не будет, угадали мало и вернулись к реаллокациям, угадали много и заплатили памятью за все миллион списков сразу, а тут мы не угадываем, мы считаем сколько будет у нас в памяти и раскладываем эти данные. Давайте посмотрим, что получилось после этого трюка:&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th data-colwidth=129 width=129&gt;&lt;p&gt;Вариант&lt;th&gt;&lt;p&gt;Память&lt;th&gt;&lt;p&gt;Аллокаций&lt;th&gt;&lt;p&gt;Build&lt;th&gt;&lt;p&gt;Query&lt;th&gt;&lt;p&gt;Query random&lt;th&gt;&lt;p&gt;Snapshot&lt;th&gt;&lt;p&gt;Teardown&lt;tr&gt;&lt;td data-colwidth=129 width=129&gt;&lt;p align=left&gt;&lt;code&gt;vector&amp;lt;vector&amp;lt;uint&amp;gt;&amp;gt;&lt;/code&gt;&lt;td&gt;&lt;p align=left&gt;63.1 МБ&lt;td&gt;&lt;p align=left&gt;3 409 088&lt;td&gt;&lt;p align=left&gt;1045 мс&lt;td&gt;&lt;p align=left&gt;14.5 мс&lt;td&gt;&lt;p align=left&gt;96.8 мс&lt;td&gt;&lt;p align=left&gt;96.7 мс&lt;td&gt;&lt;p align=left&gt;61.8 мс&lt;tr&gt;&lt;td data-colwidth=129 width=129&gt;&lt;p align=left&gt;Посчитать, потом выделить&lt;td&gt;&lt;p align=left&gt;46.7 МБ&lt;td&gt;&lt;p align=left&gt;650 599&lt;td&gt;&lt;p align=left&gt;153 мс&lt;br&gt;(-x6.6)&lt;td&gt;&lt;p align=left&gt;13.7 мс&lt;br&gt;(-x1.10)&lt;td&gt;&lt;p align=left&gt;40.8 мс&lt;td&gt;&lt;p align=left&gt;44.0 мс&lt;td&gt;&lt;p align=left&gt;28.0 мс&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;А получилось очень интересно, мы и build ускорилил и память упала в полтора раза, а вот query почти не изменился, что логично, поскольку мы всё ещё делаем два прыжка по памяти на список, прежде чем добраться до &lt;code&gt;regiments[id].mood.&lt;/code&gt; Зато улучшилось время рандомной выборки, потому что данные лежат плотнее и их больше влезает в кеш.&lt;h3&gt;Хак второй&lt;/h3&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/cd9/19a/8cb/cd919a8cba720691200fd422a6fdc629.png width=1046 height=594 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/cd9/19a/8cb/cd919a8cba720691200fd422a6fdc629.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/cd9/19a/8cb/cd919a8cba720691200fd422a6fdc629.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Заметили что каждый список все равно остался жить в своей аллокации? Пусть вектора мы убрали, но сами аллокации никуда не делись, поэтому остались и метаданные кучи, и разбросанность в памяти, и необходимость таскать в каждой записи 8-байтовый указатель.&lt;p&gt;Если мы на первом шаге посчитали размеры, почему теперь не выделить и один большой буфер под все списки и разложить их в нём подряд? Тогда мы получим только одну аллокацию и разложенные по смещениям списки, и указатель оказывается не нужен и будет достаточно смещения, и если вы не будете аллоцировать за 4Гб, то смещение прекрасно живёт в 32 битах. Как вы видите данных у нас всего-то 48Мб и нам это вполне подходит.&lt;p&gt;Для решения этой задачи почти не нужно нового кода, в предыдущем варианте пришлось ввести &lt;code&gt;offset&lt;/code&gt; для каждого списка, чтобы было знать, в какой слот писать следующий элемент. Разница лишь в том, что теперь &lt;code&gt;offset&lt;/code&gt; стартует не с нуля, а с суммы длин всех предыдущих списков, это известный трюк и называется он - префиксная сумма.&lt;pre&gt;&lt;code class=cpp&gt;struct RegimentList {&#xA;    unsigned int count  = 0;&#xA;    unsigned int offset = 0;&#xA;};&#xA;&#xA;std::vector&amp;lt;RegimentList&amp;gt; by_location(location_count);&#xA;for (unsigned int r = 0; r &amp;lt; regiment_count; ++r)&#xA;    by_location[regiments[r].location].count++;&#xA;&#xA;// префиксная сумма: где начинается список каждой локации&#xA;unsigned int sum = 0;&#xA;for (unsigned int i = 0; i &amp;lt; location_count; ++i) {&#xA;    by_location[i].offset = sum;&#xA;    sum += by_location[i].count;&#xA;}&#xA;&#xA;// одна аллокация на все индексы сразу&#xA;std::vector&amp;lt;unsigned int&amp;gt; data(sum);&#xA;for (unsigned int r = 0; r &amp;lt; regiment_count; ++r) {&#xA;    RegimentList&amp;amp; list = by_location[regiments[r].location];&#xA;    data[list.offset] = r;&#xA;    list.offset++;&#xA;}&#xA;&#xA;// offset уехал вперед на длину списка, надо вернуть на место&#xA;for (unsigned int i = 0; i &amp;lt; location_count; ++i)&#xA;    by_location[i].offset -= by_location[i].count;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Как теперь наши данные будут лежать в памяти:&lt;pre&gt;&lt;code class=cpp&gt;Слитая схема:&#xA;regiments[]                  blob[] — один буфер на всё&#xA;+---------------+            +---+---+---+---+---+---+---+---+---+---+---+&#xA;| cnt=1 off=0   |----------&amp;gt; | 0 |                                       |&#xA;+---------------+            +---+---+---+---+---+---+---+---+---+---+---+&#xA;| cnt=6 off=1   |---------------&amp;gt;| 1 | 2 | 3 | 4 | 5 | 6 |&#xA;+---------------+                +---+---+---+---+---+---+---+---+&#xA;| cnt=5 off=7   |---------------------------------------&amp;gt;| 7 | 8 | ...&#xA;+---------------+                                        +---+---+&#xA;   8 байт            +        24 байта данных      = 32 байта на 24 полезных&#xA;&#xA;// данные лежат отдельно&#xA;regiments[]  [ {loc, health, mood, speed}, ... ]&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;И что получится по времени:&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;&lt;p&gt;Вариант&lt;th&gt;&lt;p&gt;Память&lt;th&gt;&lt;p&gt;Аллокаций&lt;th&gt;&lt;p&gt;Build&lt;th&gt;&lt;p&gt;Query&lt;th&gt;&lt;p&gt;Query random&lt;th data-colwidth=110 width=110&gt;&lt;p&gt;Snapshot&lt;th&gt;&lt;p&gt;Teardown&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;&lt;code&gt;vector&amp;lt;vector&amp;lt;uint&amp;gt;&amp;gt;&lt;/code&gt;&lt;td&gt;&lt;p align=left&gt;63.1 МБ&lt;td&gt;&lt;p align=left&gt;3 409 088&lt;td&gt;&lt;p align=left&gt;1045 мс&lt;td&gt;&lt;p align=left&gt;14.5 мс&lt;td&gt;&lt;p align=left&gt;96.8 мс&lt;td data-colwidth=110 width=110&gt;&lt;p align=left&gt;96.7 мс&lt;td&gt;&lt;p align=left&gt;61.8 мс&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Посчитать, потом выделить&lt;td&gt;&lt;p align=left&gt;46.7 МБ&lt;td&gt;&lt;p align=left&gt;650 599&lt;td&gt;&lt;p align=left&gt;153 мс (x6.6)&lt;td&gt;&lt;p align=left&gt;13.7 мс (x1.10)&lt;td&gt;&lt;p align=left&gt;40.8 мс&lt;td data-colwidth=110 width=110&gt;&lt;p align=left&gt;44.0 мс&lt;td&gt;&lt;p align=left&gt;28.0 мс&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Один буфер, count + offset&lt;td&gt;&lt;p align=left&gt;30.5 МБ&lt;td&gt;&lt;p align=left&gt;2&lt;td&gt;&lt;p align=left&gt;75.6 мс (x15)&lt;td&gt;&lt;p align=left&gt;13.2 мс (x1.2)&lt;td&gt;&lt;p align=left&gt;31.7 мс&lt;td data-colwidth=110 width=110&gt;&lt;p align=left&gt;5.3 мс&lt;td&gt;&lt;p align=left&gt;2.2 мс&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Пятнадцать раз... не процентов, с этим можно идти хоть к техлиду, хоть на конференцию и гордо нести флаг превозмогания. Аллокаций стало две - на массив описаний и на буфер индексов, но снапшот индекса перестал быть глубокой копией миллиона векторов и стал двумя &lt;code&gt;memcpy&lt;/code&gt; с 5мс вместо 90 и освобождение тоде два &lt;code&gt;free&lt;/code&gt; вместо сотен тысяч. Случайный обход ускорился примерно втрое относительно первой версии, потому что соседние списки полков теперь лежат в плотно, и префетчер успевает подтаскивать нужные данные для расчетов. Сами сущности при этом можно снапшотить отдельно, одним копированием плотного массива, а можно и не снапшотить... главное в процессе расследования не выйти на самих себя :) Оказывается, что дорогим была именно удобная реализация jagged index через стандартные контейнеры, а не доступ к массиву сущностей.&lt;p&gt;Не думайте, что я такой умный... как обычно все уже придумано до нас, этот трюк придумали еще в восьмидесятых для раскладки спрайтов в памяти на NES с максимальной упаковкой, потому что памяти было мало, а спрайтов много :) И там оффсеты были индексами спрайтов, когда вы хотели отрисовать его на экране.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/c4e/ee7/d9d/c4eee7d9d2827bedbf95a6ea63ce239f.png width=1664 height=842 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/c4e/ee7/d9d/c4eee7d9d2827bedbf95a6ea63ce239f.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/c4e/ee7/d9d/c4eee7d9d2827bedbf95a6ea63ce239f.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;А заодно мы получаем набор бонусов, теперь эта структура сериализуется как есть и её можно положить в сейв без обхода, получаем просто два &lt;code&gt;fwrite&lt;/code&gt;, а на загрузка другие два &lt;code&gt;fread&lt;/code&gt;, и никакой починки указателей после чтения. И еще она просто копируется одним &lt;code&gt;memcpy&lt;/code&gt; и если нужен снапшот структуры для другого потока, то можно просто скопировать два буфера и не нужно никакого глубокого копирования миллиона векторов.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/5bf/34c/93a/5bf34c93a5a911b5653c3e4df6da60c6.png width=1664 height=842 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/5bf/34c/93a/5bf34c93a5a911b5653c3e4df6da60c6.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/5bf/34c/93a/5bf34c93a5a911b5653c3e4df6da60c6.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Продолжаем список бонусов... теперь эта структура может жить в линейном аллокаторе и если она строится раз в кадр, её можно целиком положить в память на старте кадра и обнулить указатель в начале следующего и вообще избавиться от &lt;code&gt;free&lt;/code&gt;, а еще такая структура перестала ломаться при релокации и перемещении, если вдруг понадобилось её переместить то индексы останутся валидными.&lt;h2&gt;Хак третий&lt;/h2&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/b93/4e6/830/b934e68302c39bf2c241daed59327a2e.png width=700 height=450 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/b93/4e6/830/b934e68302c39bf2c241daed59327a2e.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/b93/4e6/830/b934e68302c39bf2c241daed59327a2e.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Этот хак приехал через полгода использования новой структуры в игре, когда кто-то из коллег обратил внимание, что можно оптимизировать длины списков. Смотрите что получается с длинами:&lt;pre&gt;&lt;code&gt;count:   [ 1   7   4   4  5  ]&#xA;offset:  [ 0   1   8  12  16 ]&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;А теперь, что получается в массиве смещений:&lt;pre&gt;&lt;code&gt;offset:  [ 0   1   8  12  16 ]&#xA;   count:  [ 1   7   4   4  ... ]&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Длина списка &lt;code&gt;i&lt;/code&gt; будет разностью двух соседних смещений &lt;code&gt;count[i] == offset[i + 1] - offset[i]&lt;/code&gt;. Так получается потому, что все списки лежат в одном буфере подряд, и конец одного списка есть начало следующего, а значит, поле длины можно не хранить и надо только дописать в массив смещений один элемент в конец, который показывает на конец последнего списка. Остается один массив на N + 1 элемент.&lt;pre&gt;&lt;code class=cpp&gt;// N + 1 элемент: полки локации i лежат между offsets[i] и offsets[i + 1]&#xA;std::vector&amp;lt;unsigned int&amp;gt; offsets(location_count + 1);&#xA;&#xA;// offsets[0] стартует с нуля и нулем остается&#xA;unsigned int* offsets1 = &amp;amp;offsets[1];&#xA;&#xA;// проход 1: считаем длины, но пишем их со сдвигом на единицу&#xA;for (unsigned int r = 0; r &amp;lt; regiment_count; ++r)&#xA;    offsets1[regiments[r].location]++;&#xA;&#xA;// проход 2: превращаем длины в смещения на месте&#xA;unsigned int sum = 0;&#xA;for (unsigned int i = 0; i &amp;lt; location_count; ++i) {&#xA;    unsigned int count = offsets1[i];&#xA;    offsets1[i] = sum;&#xA;    sum += count;&#xA;}&#xA;&#xA;// проход 3: раскладываем индексы; смещения сами доезжают до правильных значений&#xA;std::vector&amp;lt;unsigned int&amp;gt; blob(sum);&#xA;for (unsigned int r = 0; r &amp;lt; regiment_count; ++r) {&#xA;    const unsigned int location = regiments[r].location;&#xA;    blob[offsets1[location]] = r;&#xA;    offsets1[location]++;&#xA;}&#xA;&#xA;// четвертый проход не нужен: offsets1[i] доехал ровно до offsets[i + 1]&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;А обходить мы такой массив будем так:&lt;pre&gt;&lt;code class=cpp&gt;const unsigned int begin = offsets[location];&#xA;const unsigned int end   = offsets[location + 1];&#xA;for (unsigned int i = begin; i &amp;lt; end; ++i) {&#xA;    const Regiment&amp;amp; regiment = regiments[data[i]];&#xA;    if (regiment.mood &amp;lt; 0.25f || regiment.health &amp;lt;= 0.0f) {&#xA;        continue;&#xA;    }&#xA;    travel_budget += regiment.speed;&#xA;}&#xA;&#xA;Финальная схема:&#xA;offsets[]  [ 0 | 1 | 8 | 12 | 16 | 21 ]        N + 1 элемент, по 4 байта&#xA;             \___/\___/\__/&#xA;              loc0 loc1  loc2&#xA;blob[]     [ 0 | 1 2 3 4 5 6 7 | 8 9 10 11 12 | ... ]&#xA;   4 байта     +     24 байта данных     = 28 байт на 24 полезных&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;&lt;p&gt;Вариант&lt;th&gt;&lt;p&gt;Память&lt;th&gt;&lt;p&gt;Аллокаций&lt;th&gt;&lt;p&gt;Build&lt;th&gt;&lt;p&gt;Query&lt;th&gt;&lt;p&gt;Query random&lt;th&gt;&lt;p&gt;Snapshot&lt;th&gt;&lt;p&gt;Teardown&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;&lt;code&gt;vector&amp;lt;vector&amp;lt;uint&amp;gt;&amp;gt;&lt;/code&gt;&lt;td&gt;&lt;p align=left&gt;63.1 МБ&lt;td&gt;&lt;p align=left&gt;3 409 088&lt;td&gt;&lt;p align=left&gt;1045 мс&lt;td&gt;&lt;p align=left&gt;14.5 мс&lt;td&gt;&lt;p align=left&gt;96.8 мс&lt;td&gt;&lt;p align=left&gt;96.7 мс&lt;td&gt;&lt;p align=left&gt;61.8 мс&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;То же, фрагментированная куча&lt;td&gt;&lt;p align=left&gt;174.3 МБ&lt;td&gt;&lt;p align=left&gt;3 409 088&lt;td&gt;&lt;p align=left&gt;1308 мс&lt;td&gt;&lt;p align=left&gt;14.9 мс&lt;td&gt;&lt;p align=left&gt;95.5 мс&lt;td&gt;&lt;p align=left&gt;99.2 мс&lt;td&gt;&lt;p align=left&gt;117.3 мс&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Посчитать, потом выделить&lt;td&gt;&lt;p align=left&gt;46.7 МБ&lt;td&gt;&lt;p align=left&gt;650 599&lt;td&gt;&lt;p align=left&gt;153 мс&lt;td&gt;&lt;p align=left&gt;13.7 мс&lt;td&gt;&lt;p align=left&gt;40.8 мс&lt;td&gt;&lt;p align=left&gt;44.0 мс&lt;td&gt;&lt;p align=left&gt;28.0 мс&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Один буфер, count + offset&lt;td&gt;&lt;p align=left&gt;30.5 МБ&lt;td&gt;&lt;p align=left&gt;2&lt;td&gt;&lt;p align=left&gt;75.6 мс&lt;td&gt;&lt;p align=left&gt;13.2 мс&lt;td&gt;&lt;p align=left&gt;31.7 мс&lt;td&gt;&lt;p align=left&gt;5.3 мс&lt;td&gt;&lt;p align=left&gt;2.2 мс&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Только offsets (CSR)&lt;td&gt;&lt;p align=left&gt;26.7 МБ&lt;td&gt;&lt;p align=left&gt;2&lt;td&gt;&lt;p align=left&gt;59.9 мс&lt;td&gt;&lt;p align=left&gt;12.4 мс&lt;td&gt;&lt;p align=left&gt;26.8 мс&lt;td&gt;&lt;p align=left&gt;4.2 мс&lt;td&gt;&lt;p align=left&gt;1.9 мс&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Убрав всего четыре байта на элемент ( и четырех мегабайт на все сущности ) получили структуру как её в академической литературе описывает CSR... помимо всего прочего, получили формат, который понимают библиотеки разреженной линейной алгебры. Это нам, правда, не сильно помогло, потому что непропатченый Eigen работает раза в два медленнее нашей собственной математики, но об этом в другой раз...&lt;p&gt;На маленьких размерах (100 и 1k) все варианты почти неотличимы, потому что данные целиком в кеше и разрыв мы получаем ближе к 64k-128k, и становится достаточно большим при достижении миллиона объектов.&lt;p&gt;Относительно последнего варианта сборка быстрее в 17 раз, снапшот в 23 раза, освобождение в 32 раза, случайный обход в 3.6 раза, памяти в 2.4 раза меньше, аллокаций две вместо трех с половиной миллионов.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/208/22b/38d/20822b38d7812899ff18775bdb29ec29.png width=1664 height=838 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/208/22b/38d/20822b38d7812899ff18775bdb29ec29.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/208/22b/38d/20822b38d7812899ff18775bdb29ec29.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;h3&gt;Почему выигрыш в реальном коде будет даже больше&lt;/h3&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/847/e11/90d/847e1190d4a0f894a9f19b74b133a6b7.png width=1033 height=411 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/847/e11/90d/847e1190d4a0f894a9f19b74b133a6b7.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/847/e11/90d/847e1190d4a0f894a9f19b74b133a6b7.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Вы заметили строку с фрагментированной кучей? Такое происходит, если во время сборки нашего массива массивов, другая подсистема игры успевает аллоцировать свои блоки между нашими массивами, что происходит в 99% случаев в большой игре и потом половина из них остается в виде дыр, и вместо 63 МБ где-то в близком блоке памяти, мы получаем в три раза большую область, на которой раскиданы блоки, что выразится уже в стоимости работы всего алгоритма, и такая фрагментация бьет и по скорости чтения, и по стоимости жизненного цикла.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/7e2/a30/d8a/7e2a30d8a3af9620f823a8aa935c5e74.png width=1664 height=842 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/7e2/a30/d8a/7e2a30d8a3af9620f823a8aa935c5e74.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/7e2/a30/d8a/7e2a30d8a3af9620f823a8aa935c5e74.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;В наивной версии, которая пришла на ревью, на каждый список процессор делает два независимых обращения к памяти и эти обращения связаны, а префетчер их все равно не видит. Кэш-линия всего 64 байта, и в неё влезает либо два с половиной внешних &lt;code&gt;vector&lt;/code&gt;, либо шестнадцать индексов, то есть в наивной схеме мы на каждый список запрашиваем в кэш минимум две линии, из которых используем только 24 байта из 128. А в последнем варианте одна линия &lt;code&gt;blob[]&lt;/code&gt; покрывает уже два-три списка целиком, а &lt;code&gt;offsets[]&lt;/code&gt; читается последовательно и префетчер просто подкладывает следующие блоки данных в кеш L1, т.е. мы получаем выигрыш от того, что правильно оптимизировали данные под структуру процессора&lt;p&gt;За всё приходится платить, и тут придется расплачиваться железобетонностью этой структуры. Если списки надо менять произвольно, то вставка в середину означает сдвиг всего буфера, т.е. это не структура для динамики и если у вас список меняется по одному элементу в ответ на игровые события, то лучше оставить вектор векторов. Но очень часто получается изменить задачу так, чтобы перестраивать индекс целиком один раз (за кадр например), и что полная перестройка миллиона элементов выходит дешевле, чем держать вектор векторов.&lt;h2&gt;Почему миллион&lt;/h2&gt;&lt;p&gt;В глобальных стратегиях вроде Civilazation, Stellaris, Victoria, Humankind будет не &amp;#34;миллион провинций на карте&amp;#34;. Карта редко превышает 16 тысяч локаций (~12k суша, ~3k море), но каждая страна имеет свой снапшот локаций, каждый режим отображения добавляет еще один слой, чтобы вы могли быстро переключиться с общего вида на леса, или дороги, или прибыльность. И миллион набегает как сумма живых объектов симуляции, а не как размер одного массива &lt;code&gt;locations[]&lt;/code&gt;.&lt;p&gt;Внешний индекс нужен для всего, что &amp;#34;лежит на земле&amp;#34; - здания, полки, модификаторы, спрос, контроль и один проход &amp;#34;по всем локациям&amp;#34; сам по себе дешевый, но почти каждая подсистема делает свой обратный индекс или свой список на локацию.&lt;p&gt;А еще накладывается экономика, каждый тип здания тоже порождает свою карту локаций, добавьте сюда города, уникальные постройки, уровни, рынки, торговые ордера, спрос/предложение по товарам на рынок и локацию. К концу игры это все превращается в тысячи отдельных сущностей, каждая со своим &lt;code&gt;health&lt;/code&gt; / &lt;code&gt;mood&lt;/code&gt; / &lt;code&gt;speed&lt;/code&gt;.&lt;p&gt;А еще есть временные и производные объекты, вроде модификаторов на страну/локацию/армию/персонажа, подписчики на события, пути. Вы их никогда не увидите на карте, но в памяти и в индексах они есть в виде «владелец → набор сущностей».&lt;p&gt;Поэтому в бенче «миллион слотов снаружи, шесть миллионов индексов внутри» это не буквальный размер карты стратегии, но порядок числа объектов, до которого вырастает игра к середине длинной партии. Поэтому в следущий раз открывая диспетчер задач и удивляясь почему игра заняла 24 гигабайта оперативки... не удивляйтесь, вот по названным выше причинам. И надо еще как-то заставить все эти гигабайты шевелиться :)&lt;h2&gt;Это было еще до игр и ECS&lt;/h2&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/50b/a03/068/50ba030682e6b3f864ec262cc8033c81.png width=1038 height=335 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/50b/a03/068/50ba030682e6b3f864ec262cc8033c81.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/50b/a03/068/50ba030682e6b3f864ec262cc8033c81.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Придумано это было задолго до нас, и даже не в геймдеве. Раскладку в виде &amp;#34;посчитать, префиксная сумма, разложить&amp;#34; известна вообще с середины 1950-х и идея хранить разреженную структуру парой массивов &amp;#34;смещения плюс данные&amp;#34; пришло из линейной алгебры конца 1960-х, как я уже упоминал про CSR и Yale format. Мы просто &amp;#34;придумали&amp;#34; и применили к нашим проблемам и поиску локациям &amp;#34;немного&amp;#34; численных методов из работы с матрицами.&lt;p&gt;Этот приём, который возможно вам понравится и вы возьмете его себе в проект, вообще не про jagged arrays. Он про разделение операции на build и query, чтобы появилась возможность построить данные в форме, которая радикально лучше подходит для чтения. Это работает не только со списками и так устроены навигационные меши, так работают архетипы в ECS, так живут вершинные буферы, и получается что мы платим за структуру один раз на этапе сборки, чтобы не платить каждый раз на чтении.&lt;p&gt;Когда я с коллегами готовил презентацию этого решения для команды, то наткнулся на статью &lt;a href=https://zeux.io/2023/06/30/efficient-jagged-arrays/ rel=&#34;noopener nofollow&#34;&gt;Efficient jagged arrays&lt;/a&gt; 2023 года, где он пришел к той же укладке для вертексов в меше для своего проекта. Мы шли со стороны игровых сущностей и снапшота между потоками, а он со стороны индексов треугольников, но в итоге пришли к одной и той же схеме. Само по себе это хороший признак, что задача встречается в разработке достаточно часто, чтобы не изобретать ее заново и найти какое-то более переиспользуемое решение. К сожалению, я такого в открытом доступе не нашел... возможно плохо искал.&lt;p&gt;Фундаментально задача осталась той же, что и в 80-х и раньше, надо чтобы данные оказались в правильном месте, в правильной форме, к тому моменту, когда по ним пойдёт хотпас.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <author>dalerank</author>
      <guid>https://habr.com/ru/articles/1080266/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1080266</guid>
      <pubDate>Mon, 14 Sep 2026 20:19:00 +0000</pubDate>
    </item>
    <item>
      <title>Выполняю тестовое задание для DevOps Cloud.ru Camp 2025</title>
      <link>https://habr.com/ru/articles/1082220/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082220</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;Я тут недавно пытался попасть на бесплатные курсы от Aston, но получил отказ с такой формулировкой.&lt;pre&gt;&lt;code&gt;Денис, добрый день!&#xA;После изучения вашей анкеты, к сожалению, мы не готовы пригласить вас к дальнейшему рассмотрению на обучение в компании. У нас очень большой поток кандидатов на курсы. В данный момент взяли в рассмотрение участников с показателями выше. Ваша анкета будет сохранена,  возможно, вернемся к вашей кандидатуре, при наборе на следующий поток.&#xA;Благодарим за внимание к нашей компании.&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:87px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Хотя я даже не понял, как смогли оценить мои показатели и в чем они измерялись, если не давали ни тестового задания, ни собеседования. В анкете у них несколько вопросов и уровень образования. Но да ладно, это тема отдельной статьи.&lt;p&gt;Сегодня хочу попробовать пройти тестовое задание &lt;a href=https://github.com/devopscloudrucamp/tasks rel=&#34;noopener nofollow&#34;&gt;от клауд.ру&lt;/a&gt;. Оно охватывает тот самый стек для devops, linux, docker, git, kubernetes.&lt;p&gt;На первый взгляд, задание кажется не таким уж и сложным, однако, дьявол кроется в деталях и есть неоднозначное право выбора, которое будет зависеть от ситуации.&lt;p&gt;Тестовое задание состоит из 4 задач, начнем с первой.&lt;h3&gt;Web приложение на Python/Golang&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;Цель&#xA;&#xA;Написать простейший echo-server на Python или Go&#xA;&#xA;Задача&#xA;&#xA;Требуется написать простое веб-приложение на Python/Go, которое слушает входящие соединения на порту 8000 и предоставляет страницу с информацией:&#xA;&#xA;    имя хоста&#xA;    ip адрес хоста&#xA;    имя автора, которое передаётся через переменную окружения $AUTHOR&#xA;&#xA;Для приложения написать Dockerfile и запушить образ в докер хаб* (запушить в приватный регистри - пройти регистрацию в докер хаб, сделать регистри приватным).&#xA;&#xA;В ответе к задаче приложить:&#xA;&#xA;    исходный код&#xA;    Dockerfile&#xA;    команды, которые использовались для пуша образа в регистри&#xA;&#xA;Полученный артефакты положить в папку /01-application&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;На первом пункте уже кроется подвох. Python или Go , здесь нужно знать контекст, потому что при грамотной сборке на го, образ получится около 2 мб, вместо около 20 на пайтоне. С другой стороны, на питоне докерфайл писать легче, потому как мультистэйдж сборка здесь не прокатит.&lt;p&gt;Для чистоты эксперимента, я сделаю 2 варианта, начну с го.&lt;p&gt;Создаем папку 01-application, в ней 2 файла main.go, go.mod.&lt;pre&gt;&lt;code class=go&gt;// main.go&#xA;package main&#xA;&#xA;import (&#xA;&#x9;&amp;#34;fmt&amp;#34;&#xA;&#x9;&amp;#34;net&amp;#34;&#xA;&#x9;&amp;#34;net/http&amp;#34;&#xA;&#x9;&amp;#34;os&amp;#34;&#xA;)&#xA;&#xA;func envOr(key, fallback string) string {&#xA;&#x9;if v := os.Getenv(key); v != &amp;#34;&amp;#34; {&#xA;&#x9;&#x9;return v&#xA;&#x9;}&#xA;&#x9;return fallback&#xA;}&#xA;&#xA;// IP контейнера в его собственном network namespace.&#xA;func getContainerIP() string {&#xA;&#x9;conn, err := net.Dial(&amp;#34;udp&amp;#34;, &amp;#34;8.8.8.8:80&amp;#34;)&#xA;&#x9;if err != nil {&#xA;&#x9;&#x9;return &amp;#34;unknown&amp;#34;&#xA;&#x9;}&#xA;&#x9;defer conn.Close()&#xA;&#x9;return conn.LocalAddr().(*net.UDPAddr).IP.String()&#xA;}&#xA;&#xA;func handler(w http.ResponseWriter, r *http.Request) {&#xA;&#x9;containerHost, _ := os.Hostname()   // имя из UTS namespace = ID контейнера&#xA;&#x9;containerIP := getContainerIP()     // адрес из network namespace&#xA;&#xA;&#x9;// Данные ХОСТА не добываются — они приходят снаружи.&#xA;&#x9;hostHost := envOr(&amp;#34;HOST_HOSTNAME&amp;#34;, &amp;#34;(not injected)&amp;#34;)&#xA;&#x9;hostIP := envOr(&amp;#34;HOST_IP&amp;#34;, &amp;#34;(not injected)&amp;#34;)&#xA;&#x9;author := envOr(&amp;#34;AUTHOR&amp;#34;, &amp;#34;anonymous&amp;#34;)&#xA;&#xA;&#x9;w.Header().Set(&amp;#34;Content-Type&amp;#34;, &amp;#34;text/html; charset=utf-8&amp;#34;)&#xA;&#x9;fmt.Fprintf(w, `&amp;lt;!DOCTYPE html&amp;gt;&#xA;&amp;lt;html&amp;gt;&amp;lt;head&amp;gt;&amp;lt;title&amp;gt;Echo Server&amp;lt;/title&amp;gt;&amp;lt;/head&amp;gt;&#xA;&amp;lt;body style=&amp;#34;font-family:monospace&amp;#34;&amp;gt;&#xA;  &amp;lt;h1&amp;gt;Echo Server&amp;lt;/h1&amp;gt;&#xA;  &amp;lt;ul&amp;gt;&#xA;    &amp;lt;li&amp;gt;&amp;lt;b&amp;gt;Container hostname:&amp;lt;/b&amp;gt; %s&amp;lt;/li&amp;gt;&#xA;    &amp;lt;li&amp;gt;&amp;lt;b&amp;gt;Container IP:&amp;lt;/b&amp;gt; %s&amp;lt;/li&amp;gt;&#xA;    &amp;lt;li&amp;gt;&amp;lt;b&amp;gt;Host hostname:&amp;lt;/b&amp;gt; %s&amp;lt;/li&amp;gt;&#xA;    &amp;lt;li&amp;gt;&amp;lt;b&amp;gt;Host IP:&amp;lt;/b&amp;gt; %s&amp;lt;/li&amp;gt;&#xA;    &amp;lt;li&amp;gt;&amp;lt;b&amp;gt;Author:&amp;lt;/b&amp;gt; %s&amp;lt;/li&amp;gt;&#xA;  &amp;lt;/ul&amp;gt;&#xA;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;`, containerHost, containerIP, hostHost, hostIP, author)&#xA;}&#xA;&#xA;func healthz(w http.ResponseWriter, r *http.Request) {&#xA;&#x9;fmt.Fprintln(w, &amp;#34;ok&amp;#34;)&#xA;}&#xA;&#xA;func main() {&#xA;&#x9;http.HandleFunc(&amp;#34;/&amp;#34;, handler)&#xA;&#x9;http.HandleFunc(&amp;#34;/healthz&amp;#34;, healthz)&#xA;&#x9;http.ListenAndServe(&amp;#34;:8000&amp;#34;, nil)&#xA;}&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code class=go&gt;// go.mod&#xA;module echo-server&#xA;&#xA;go 1.23&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Далее напишем докерфайл, используя лучшие практики, мультистэйдж сборки, выкинем все лишнее, чтобы образ весил минимальное количество мегабайт, а также создадим пользователя, чтобы контейнер не был запущен от рута.&lt;pre&gt;&lt;code&gt;# ---- Этап 1: сборка ----&#xA;FROM golang:1.23-alpine AS builder&#xA;WORKDIR /src&#xA;COPY go.mod ./&#xA;COPY main.go ./&#xA;# CGO_ENABLED=0 — статическая линковка, чтобы бинарник&#xA;# жил в scratch без единой системной библиотеки.&#xA;# -ldflags=&amp;#34;-s -w&amp;#34; срезает отладочные символы (меньше вес).&#xA;RUN CGO_ENABLED=0 GOOS=linux go build -ldflags=&amp;#34;-s -w&amp;#34; -o /echo-server .&#xA;RUN echo &amp;#39;appuser:x:10001:10001::/:&amp;#39; &amp;gt; /etc/passwd&#xA;&#xA;# ---- Этап 2: финальный образ ----&#xA;FROM scratch&#xA;COPY --from=builder /echo-server /echo-server&#xA;COPY --from=builder /etc/passwd /etc/passwd&#xA;USER appuser&#xA;EXPOSE 8000&#xA;ENV AUTHOR=&amp;#34;alfabuster&amp;#34;&#xA;ENTRYPOINT [&amp;#34;/echo-server&amp;#34;]&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Далее зарегаемся на докерхабе, если еще нет аккаунта, создаем репозиторий, делаем его приватным и создаем токен доступа.&lt;pre&gt;&lt;code class=bash&gt;# 1. Логинимся (введёт логин и Access Token — не пароль, пароли уже немодно)&#xA;docker login -u &amp;lt;ваш_логин&amp;gt;&#xA;&#xA;# 2. Собираем с правильным тегом: &amp;lt;логин&amp;gt;/&amp;lt;репозиторий&amp;gt;:&amp;lt;версия&amp;gt;&#xA;docker build -t &amp;lt;ваш_логин&amp;gt;/echo-server:1.0 .&#xA;&#xA;# 3. Проверяем локально, что оно вообще дышит&#xA;docker run --rm -p 8001:8000 \&#xA;  -e AUTHOR=&amp;#34;alfabuster&amp;#34; \&#xA;  -e HOST_HOSTNAME=&amp;#34;$(hostname)&amp;#34; \&#xA;  -e HOST_IP=&amp;#34;$(ip -4 route get 1.1.1.1 | grep -oP &amp;#39;src \K\S+&amp;#39;)&amp;#34; \&#xA;  &amp;lt;ваш_логин&amp;gt;/echo-server:1.0&#xA;#    открываем http://localhost:8001 — должны увидеть hostname/IP/author&#xA;&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Если все сделали правильно, то увидим инфу о контейнере и хосте.&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/20e/392/e89/20e392e89918f62ebb041d9e59062fe4.png sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/20e/392/e89/20e392e89918f62ebb041d9e59062fe4.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/20e/392/e89/20e392e89918f62ebb041d9e59062fe4.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;pre&gt;&lt;code class=bash&gt;# 4. Пушим в приватный регистри&#xA;docker push &amp;lt;ваш_логин&amp;gt;/echo-server:1.0&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Теперь проделаем, все тоже самое, но для пайтона.&lt;p&gt;В папке созаем server.py с таким кодом.&lt;pre&gt;&lt;code class=python&gt;#!/usr/bin/env python3&#xA;import os&#xA;import socket&#xA;from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer&#xA;&#xA;&#xA;def env_or(key, fallback):&#xA;    return os.getenv(key) or fallback&#xA;&#xA;&#xA;def get_container_ip():&#xA;    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)&#xA;    try:&#xA;        s.connect((&amp;#34;8.8.8.8&amp;#34;, 80))&#xA;        return s.getsockname()[0]&#xA;    except Exception:&#xA;        return &amp;#34;unknown&amp;#34;&#xA;    finally:&#xA;        s.close()&#xA;&#xA;&#xA;class Handler(BaseHTTPRequestHandler):&#xA;    def do_GET(self):&#xA;        if self.path == &amp;#34;/healthz&amp;#34;:&#xA;            self.send_response(200)&#xA;            self.send_header(&amp;#34;Content-Type&amp;#34;, &amp;#34;text/plain&amp;#34;)&#xA;            self.end_headers()&#xA;            self.wfile.write(b&amp;#34;ok\n&amp;#34;)&#xA;            return&#xA;&#xA;        container_host = socket.gethostname()      # ID контейнера&#xA;        container_ip = get_container_ip()&#xA;        host_host = env_or(&amp;#34;HOST_HOSTNAME&amp;#34;, &amp;#34;(not injected)&amp;#34;)&#xA;        host_ip = env_or(&amp;#34;HOST_IP&amp;#34;, &amp;#34;(not injected)&amp;#34;)&#xA;        author = env_or(&amp;#34;AUTHOR&amp;#34;, &amp;#34;anonymous&amp;#34;)&#xA;&#xA;        body = f&amp;#34;&amp;#34;&amp;#34;&amp;lt;!DOCTYPE html&amp;gt;&#xA;&amp;lt;html&amp;gt;&amp;lt;head&amp;gt;&amp;lt;title&amp;gt;Echo Server&amp;lt;/title&amp;gt;&amp;lt;/head&amp;gt;&#xA;&amp;lt;body style=&amp;#34;font-family:monospace&amp;#34;&amp;gt;&#xA;  &amp;lt;h1&amp;gt;Echo Server (Python)&amp;lt;/h1&amp;gt;&#xA;  &amp;lt;ul&amp;gt;&#xA;    &amp;lt;li&amp;gt;&amp;lt;b&amp;gt;Container hostname:&amp;lt;/b&amp;gt; {container_host}&amp;lt;/li&amp;gt;&#xA;    &amp;lt;li&amp;gt;&amp;lt;b&amp;gt;Container IP:&amp;lt;/b&amp;gt; {container_ip}&amp;lt;/li&amp;gt;&#xA;    &amp;lt;li&amp;gt;&amp;lt;b&amp;gt;Host hostname:&amp;lt;/b&amp;gt; {host_host}&amp;lt;/li&amp;gt;&#xA;    &amp;lt;li&amp;gt;&amp;lt;b&amp;gt;Host IP:&amp;lt;/b&amp;gt; {host_ip}&amp;lt;/li&amp;gt;&#xA;    &amp;lt;li&amp;gt;&amp;lt;b&amp;gt;Author:&amp;lt;/b&amp;gt; {author}&amp;lt;/li&amp;gt;&#xA;  &amp;lt;/ul&amp;gt;&#xA;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;&amp;#34;&amp;#34;&amp;#34;&#xA;&#xA;        self.send_response(200)&#xA;        self.send_header(&amp;#34;Content-Type&amp;#34;, &amp;#34;text/html; charset=utf-8&amp;#34;)&#xA;        self.end_headers()&#xA;        self.wfile.write(body.encode(&amp;#34;utf-8&amp;#34;))&#xA;&#xA;    def log_message(self, *args):&#xA;        pass&#xA;&#xA;&#xA;if __name__ == &amp;#34;__main__&amp;#34;:&#xA;    ThreadingHTTPServer((&amp;#34;0.0.0.0&amp;#34;, 8003), Handler).serve_forever()&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Напишем простой докерфайл. А теперь ключевой момент, ради которого всё затевалось. Помните элегантный финал Go-версии — FROM scratch, образ из чистой пустоты весом в мегабайты? С Python так не выйдет никогда. Python — язык интерпретируемый: чтобы запустить server.py, в образе обязан жить сам интерпретатор со всей его свитой. В пустоту его не положишь, multi-stage тут бесполезен — выкидывать нечего, рантайм нужен целиком.&lt;pre&gt;&lt;code&gt;FROM python:3.12-alpine&#xA;WORKDIR /app&#xA;RUN adduser -D appuser &#xA;COPY --chown=appuser:appuser app.py .&#xA;COPY server.py .&#xA;EXPOSE 8003&#xA;ENV AUTHOR=&amp;#34;alfabuster&amp;#34;&#xA;ENTRYPOINT [ &amp;#34;python3&amp;#34; , &amp;#34;server.py&amp;#34;]&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Однако писать такой файл проще, всего 8 строк, которые по гайду из докера, напишет даже человек без профильного образования в IT.&lt;p&gt;Теперь сравним размер полученных из билда образов.&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/67f/634/923/67f63492362baef66a23d2b5f6f3bef3.png alt=&#34;alt text&#34; sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/67f/634/923/67f63492362baef66a23d2b5f6f3bef3.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/67f/634/923/67f63492362baef66a23d2b5f6f3bef3.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;alt text&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Как видно разница примерно в 10 раз. Здесь однозначно выигрывает go.&lt;p&gt;А что на счет безопасности? Образ на scratch — это крепость без дверей: там нет sh, нет apt, нет curl, нет вообще ничего, кроме бинарника. Злоумышленник, пробравшийся в такой контейнер, оказывается в пустой бетонной коробке — ни закрепиться, ни осмотреться, ни подтянуть инструменты. Однако scratch не является защитой от RCE, при определенных условиях можно использовать загруженные/встроенные бинарники или эксплуатировать уязвимости ядра/процесса.&lt;p&gt;В python:3.12-alpine sh присутсвует. Также содержит сам Python, поэтому при RCE атакующий может использовать Python как инструмент, даже если каких-то Unix-команд нет.&lt;p&gt;Справедливости ради — у Python есть и козыри. Dockerfile проще (никакой стадии сборки), правки на лету без перекомпиляции, а внутрь можно зайти и потыкать палкой, когда что-то пошло не так. Go-scratch за свою неприступность расплачивается тем, что отлаживать его — как чинить запечатанные часы: удобной дверцы нет. Это классический размен безопасность против удобства.&lt;p&gt;Поскольку я учусь на безопасника, для меня выбор очевиден, однако в рамках тестового задания, не знаю, какой вариант правильный, возможно они оба верные. Просто для прода, нужно знать контекст и действовать по ситуации.&lt;p&gt;Тем временем первая задача выполнена, переходим ко второй.&lt;h3&gt;Ansible playbook или роль&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;Цель&#xA;&#xA;Автоматизация конфигурации на Ansible&#xA;Задача&#xA;&#xA;Требуется поднять Виртуальную Машину (VM) с ОС Ubuntu 22.04. Возможные сценарии: локально через Virtual Box (или любое другое средство виртуализации), либо воспользовавшись Free-tier предложением от cloud.ru - https://cloud.ru/offers/free-tier.&#xA;&#xA;Выполнить следующие задачи, используя Ansible:&#xA;&#xA;    На VM установить пакет docker-ce;&#xA;    Запустить на docker контейнеры c подготовленным Web-приложением из первой задачи в 3-х экземплярах. Для развёртывания использовать приватный регистри из первой задачи;&#xA;    Настроить балансировщик на nginx, который будет отвечать при обращении к хосту и прокидывать запрос на запущенные контейнеры. Выбрать алгоритм балансировки для приложения и обосновать свой выбор.&#xA;&#xA;В ответе к задаче приложить:&#xA;&#xA;    исходный код Ansible&#xA;    текстовое обоснование выбранного алгоритма балансировки&#xA;&#xA;Полученные артефакты положить в папку /02-ansible&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Ок, приступим. Поскольку хостовая система у меня Void Linux, вручную качать и устанавливать ubuntu, я не буду, воспользуюсь утилитой vagrant.&lt;p&gt;Для установки, установлю пакетный менеджер Brew.&lt;pre&gt;&lt;code&gt;/bin/bash -c &amp;#34;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)&amp;#34;&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Он рассчитан на bash, но у меня оболочка fish, поэтому, то что советует установщик в выводе в моем случае не сработает.&lt;p&gt;Нужно добавить Homebrew в PATH fish&lt;pre&gt;&lt;code class=bash&gt;fish_add_path /home/linuxbrew/.linuxbrew/bin&#xA;fish_add_path /home/linuxbrew/.linuxbrew/sbin&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;После этого можно устанавливать сам Vagrant.&lt;pre&gt;&lt;code&gt;brew tap hashicorp/tap&#xA;brew install hashicorp/tap/vagrant&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;На хосте создаю любую папку, в ней создаю файл Vagrantfile.&lt;pre&gt;&lt;code&gt;Vagrant.configure(&amp;#34;2&amp;#34;) do |config|&#xA;  config.vm.box = &amp;#34;ubuntu/jammy64&amp;#34;&#xA;  config.vm.network &amp;#34;private_network&amp;#34;,&#xA;  ip: &amp;#34;10.0.0.7&amp;#34;,&#xA;  virtualbox__intnet: &amp;#34;intnet&amp;#34;&#xA;  config.vm.provider &amp;#34;virtualbox&amp;#34; do |vb|&#xA;    vb.memory = 2048&#xA;    vb.cpus   = 2&#xA;  end&#xA;end&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Этим файлом я создал машину во внтуренней сети, которая у меня уже была.&lt;p&gt;Пингуем сервер, чтобы убедиться, что вагрант все верно настроил, доступ по ssh, настраиваем через ключи.&lt;p&gt;Теперь можно создавать плейбук через Ansible.&lt;p&gt;В папке 02-ansible создаем пару файлов ansible.cfg и inventory.ini.&lt;pre&gt;&lt;code&gt;[defaults]&#xA;inventory = inventory.ini&#xA;remote_user = vagrant&#xA;private_key_file = ~/.ssh/id_ed25519&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;[web]&#xA;web-vm ansible_host=10.0.0.7&#xA;&#xA;[web:vars]&#xA;ansible_python_interpreter=/usr/bin/python3&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;В этих файлах указываем, нашу созданную убунту через вагрант, ее ip ssh ключ и переменные.&lt;p&gt;Для данной задачи хватит и плейбука, можно конечно городить и роль, но для данной задачи хватит и плейбука.&lt;p&gt;Нам нужно, установить docker, залогиниться в приватный докер репозиторий, запустить 3 контейнера и накрыть балансировщиком на nginx.&lt;p&gt;Возможно руками, залезь на 1 сервак, там установить докер и запустить 3 контейнера будет быстрее. Но задание требует именно ansible, поэтому файл будет достаточно большим.&lt;h4&gt;Начало плейбука&lt;/h4&gt;&lt;pre&gt;&lt;code class=yaml&gt;---&#xA;- name: Deploy echo-server behind an nginx load balancer&#xA;  hosts: web&#xA;  become: true          # выполнять с sudo — иначе apt ругнется на permission&#xA;&#xA;  vars:&#xA;    docker_image: &amp;#34;&amp;lt;ваш_логин&amp;gt;/echo-server:1.0&amp;#34;&#xA;    author_name: &amp;#34;alfabuster&amp;#34;&#xA;    container_count: 3&#xA;    base_port: 8000                  # контейнеры сядут на 8001, 8002, 8003&#xA;    registry_user: &amp;#34;&amp;lt;ваш_логин&amp;gt;&amp;#34;&#xA;    registry_password: &amp;#34;{{ vault_registry_token }}&amp;#34;   # прячем в vault, создадим далее&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h4&gt;Далее установим сам докер&lt;/h4&gt;&lt;pre&gt;&lt;code class=yaml&gt;  tasks:&#xA;    - name: Install prerequisites&#xA;      apt:&#xA;        name: [ca-certificates, curl, gnupg, python3-pip]&#xA;        state: present&#xA;        update_cache: true&#xA;&#xA;    - name: Create apt keyrings directory&#xA;      file:&#xA;        path: /etc/apt/keyrings&#xA;        state: directory&#xA;        mode: &amp;#39;0755&amp;#39;&#xA;&#xA;    - name: Download Docker&amp;#39;s GPG key&#xA;      get_url:&#xA;        url: https://download.docker.com/linux/ubuntu/gpg&#xA;        dest: /etc/apt/keyrings/docker.asc&#xA;        mode: &amp;#39;0644&amp;#39;&#xA;&#xA;    - name: Add Docker apt repository&#xA;      apt_repository:&#xA;        repo: &amp;#34;deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable&amp;#34;&#xA;        filename: docker&#xA;        state: present&#xA;&#xA;    - name: Install docker-ce&#xA;      apt:&#xA;        name: [docker-ce, docker-ce-cli, containerd.io]&#xA;        state: present&#xA;        update_cache: true&#xA;&#xA;    - name: Install Docker SDK for Python&#xA;      pip:&#xA;        name: docker&#xA;        state: present&#xA;&#xA;    - name: Ensure docker is running and enabled&#xA;      service:&#xA;        name: docker&#xA;        state: started&#xA;        enabled: true&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Обратите внимание на ansible_distribution_release — это факт, который Ansible сам собирает о хосте перед стартом (для 22.04 подставит jammy).&lt;p&gt;Модуль docker_container работает не через CLI, а через Python-библиотеку docker. Поэтому мы ставим pip install docker — без неё модуль выдаст загадочную ошибку.&lt;h4&gt;Приватный реджистри и запуск трех контейнеров&lt;/h4&gt;&lt;pre&gt;&lt;code class=yaml&gt;- name: Log in to the private registry&#xA;  community.docker.docker_login:&#xA;    username: &amp;#34;{{ registry_user }}&amp;#34;&#xA;    password: &amp;#34;{{ registry_password }}&amp;#34;&#xA;&#xA;- name: Pull the application image&#xA;  community.docker.docker_image:&#xA;    name: &amp;#34;{{ docker_image }}&amp;#34;&#xA;    source: pull&#xA;&#xA;- name: Run echo-server containers&#xA;  community.docker.docker_container:&#xA;    name: &amp;#34;echo-{{ item }}&amp;#34;&#xA;    image: &amp;#34;{{ docker_image }}&amp;#34;&#xA;    state: started&#xA;    restart_policy: unless-stopped&#xA;    published_ports:&#xA;      - &amp;#34;127.0.0.1:{{ item }}:8000&amp;#34;&#xA;    env:&#xA;      AUTHOR: &amp;#34;{{ author_name }}&amp;#34;&#xA;  loop:&#xA;    - 8001&#xA;    - 8002&#xA;    - 8003&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;published_ports привязан к 127.0.0.1, а не к 0.0.0.0. Контейнеры слушают только локально — снаружи к ним не достучаться, единственная дверь наружу — через nginx.&lt;h4&gt;Nginx и его конфиг через шаблон.&lt;/h4&gt;&lt;pre&gt;&lt;code class=yaml&gt;    - name: Install nginx&#xA;      apt:&#xA;        name: nginx&#xA;        state: present&#xA;&#xA;    - name: Deploy the load-balancer config&#xA;      template:&#xA;        src: nginx-lb.conf.j2&#xA;        dest: /etc/nginx/sites-available/echo-lb.conf&#xA;      notify: Reload nginx&#xA;&#xA;    - name: Enable the site&#xA;      file:&#xA;        src: /etc/nginx/sites-available/echo-lb.conf&#xA;        dest: /etc/nginx/sites-enabled/echo-lb.conf&#xA;        state: link&#xA;      notify: Reload nginx&#xA;&#xA;    - name: Remove the default nginx site&#xA;      file:&#xA;        path: /etc/nginx/sites-enabled/default&#xA;        state: absent&#xA;      notify: Reload nginx&#xA;&#xA;    - name: Ensure nginx is running and enabled&#xA;      service:&#xA;        name: nginx&#xA;        state: started&#xA;        enabled: true&#xA;&#xA;  handlers:&#xA;    - name: Reload nginx&#xA;      service:&#xA;        name: nginx&#xA;        state: reloaded&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;handlers — механизм, который отличает Ansible от тупого шелл-скрипта. notify: Reload nginx не перезагружает nginx немедленно. Он ставит галочку «надо будет перечитать конфиг» и делает это один раз в конце, и только если конфиг реально изменился.&lt;h4&gt;Шаблон templates/nginx-lb.conf.j2&lt;/h4&gt;&lt;pre&gt;&lt;code&gt;upstream echo_backend {&#xA;    server 127.0.0.1:8001;&#xA;    server 127.0.0.1:8002;&#xA;    server 127.0.0.1:8003;&#xA;}&#xA;&#xA;server {&#xA;    listen 80 default_server;&#xA;    server_name _;&#xA;&#xA;    location / {&#xA;        proxy_pass http://echo_backend;&#xA;        proxy_set_header Host              $host;&#xA;        proxy_set_header X-Real-IP         $remote_addr;&#xA;        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;&#xA;    }&#xA;}&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h4&gt;Обоснование алгоритма&lt;/h4&gt;&lt;p&gt;А теперь ради чего всё затевалось. У nginx есть несколько алгоритмов балансировки, и вот полный расклад, кто есть кто:&lt;ul&gt;&lt;li&gt;&lt;p&gt;round-robin (по умолчанию) — раздаёт запросы по кругу: первый на 8001, второй на 8002, третий на 8003, четвёртый снова на 8001.&lt;li&gt;&lt;p&gt;least_conn — отдаёт тому бэкенду, у кого меньше активных соединений.&lt;li&gt;&lt;p&gt;ip_hash — привязывает клиента к бэкенду по хешу его IP (липкие сессии).&lt;li&gt;&lt;p&gt;hash / random — экзотика для особых случаев.&lt;/ul&gt;&lt;p&gt;Поскольку у нас простое stateless приложение, нет ни куков, ни личного кабинета, ни сессий. Все 3 контейнера равнозначны и идентичны. Поэтому равномерное распределение по кругу будет оптимальным вариантом в данной ситуации.&lt;p&gt;Least_conn здесь не станет преимуществом, потому как нагрзука в условии не указана. ip_hash в данной топологии противопоказан, так как весь трафик приходит с localhost и был бы прикован к одному контейнеру.&lt;p&gt;Поэтому выбираем round-robin.&lt;h4&gt;Создаем vault и запускаем плейбук&lt;/h4&gt;&lt;p&gt;Создание vault и пароля к нему.&lt;pre&gt;&lt;code&gt;ansible-vault create group_vars/web/vault.yml&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Внутри одна строка с токеном из докера&lt;pre&gt;&lt;code&gt;vault_registry_token: &amp;#34;docker_token&amp;#34;&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Далее можем запускать наш плейбук.&lt;pre&gt;&lt;code&gt;ansible-playbook playbook.yaml --ask-vault-pass&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Тут не все прошло гладко, вылезла ошибка.&lt;pre&gt;&lt;code&gt;fatal: [web-vm]: FAILED! =&amp;gt; {&amp;#34;changed&amp;#34;: false, &amp;#34;msg&amp;#34;: &amp;#34;Error connecting: Error while fetching server API version: Not supported URL scheme http+docker&amp;#34;}&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Это произошло потому что, я давно не обновлял коллекции из ansible. Чиним.&lt;pre&gt;&lt;code&gt;ansible-galaxy collection install community.docker --upgrade&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Запускаем плейбук еще раз и все заработало.&lt;p&gt;Проверяем сервак.&lt;div class=tm-iframe_temp data-src=https://embedd.srv.habr.com/iframe/6aa84f9c965b561e9f3b328a data-style id=6aa84f9c965b561e9f3b328a width data-habr-games&gt;&lt;/div&gt;&lt;p&gt;Балансировщик работает, второе задание выполнено.&lt;h3&gt;Kubernetes manifest&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;Цель&#xA;&#xA;Деплой приложения в Kubernetes&#xA;Задача&#xA;&#xA;Требуется написать манифест для запуска приложения из первой задачи в Kubernetes в отдельном неймспейсе в виде Deployment с 3 репликами и сервиса с типом ClusterIP.&#xA;&#xA;Выполнить следующие задачи:&#xA;&#xA;    Для приложения организовать проброс значения переменной AUTHOR;&#xA;    Реализовать readiness- и liveness- пробы;&#xA;    Использовать образ из приватного регистри из задачи 1;&#xA;    опционально: поднять ingress-controller и создать правило ingress;&#xA;    опционально: использовать helm-chart, а не raw манифест.&#xA;&#xA;В ответе к задаче приложить:&#xA;&#xA;    исходный код всех манифестов&#xA;&#xA;Полученные артефакты положить в папку /03-kubernetes&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;В задании не сказано, что нужно установливать кубер на машину. Однако для наглядности нужно понимать и тестировать написанные манифесты.&lt;p&gt;У меня на убунту для тестов как раз стоит кубер и 2 воркер ноды версии v1.31.14. Поэтому я применю написанные манифесты.&lt;p&gt;Недавно проходил курс Certified Kubernetes Administrator от kodekloud, там подсмотрел полезную тему для генерации манифестов.&lt;p&gt;Во первых, пропишем алиас, чтобы каждый раз не вводить kubectl.&lt;pre&gt;&lt;code&gt;alias k=kubectl&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Во вторых, почти любой объект kubectl create умеет напечатать готовый манифест вместо создания.&lt;pre&gt;&lt;code&gt;--dry-run=client -o yaml&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Дальше можно воспользоваться перенаправлением в файл и у нас будет готовый шаблон манифеста, с правильными отступами по канонам yaml.&lt;h4&gt;Создаем namespace&lt;/h4&gt;&lt;pre&gt;&lt;code class=bash&gt;k create ns cloud --dry-run=client -o yaml&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;В выводе будет лежать готовый манифест.&lt;pre&gt;&lt;code class=yaml&gt;apiVersion: v1&#xA;kind: Namespace&#xA;metadata:&#xA;  creationTimestamp: null&#xA;  name: cloud&#xA;spec: {}&#xA;status: {}&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Конечно врядли, кто пишет манифест для создания namespace, его проще создать командой k create ns name. Однако в задании нужен именно манифест.&lt;h4&gt;Секрет&lt;/h4&gt;&lt;p&gt;Задача усложняется тем, что реджистри приватный. Для него необходимо создавать секрет. Иначе получим вечный ImagePullBackOff.&lt;p&gt;Создаётся секрет императивно, чтобы не хранить токен в git.&lt;pre&gt;&lt;code class=bash&gt;kubectl create secret docker-registry regcred \&#xA;  --docker-server=https://index.docker.io/v1/ \&#xA;  --docker-username=&amp;lt;ваш_логин&amp;gt; \&#xA;  --docker-password=&amp;lt;ваш_access_token&amp;gt; \&#xA;  --namespace=cloud&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Секрет живёт внутри неймспейса cloud — в другом неймспейсе он невидим.&lt;h4&gt;Configmap&lt;/h4&gt;&lt;p&gt;Можно было впихнуть AUTHOR прямо в манифест как value: “alfabuster”. Но задание просит «организовать проброс», а по-взрослому переменные конфигурации живут в ConfigMap — отдельно от кода деплоя:&lt;pre&gt;&lt;code class=yaml&gt;apiVersion: v1&#xA;kind: ConfigMap&#xA;metadata:&#xA;  creationTimestamp: null&#xA;  name: cloud-configmap&#xA;  namespace: cloud&#xA;data:&#xA;  AUTHOR: alfabuster&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h4&gt;Deployment&lt;/h4&gt;&lt;p&gt;Собственно дальше идет деплоймент с 3 репликами и пробами. Генерируем шаблон, а дальше правим руками.&lt;pre&gt;&lt;code class=bash&gt;k create deploy cloud-deploy --image=alfabuster/echo-server:1.0 --replicas=3 --port=8000 -n cloud --dry-run=client -o yaml&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Итоговый манифест получился такой.&lt;pre&gt;&lt;code class=yaml&gt;apiVersion: apps/v1&#xA;kind: Deployment&#xA;metadata:&#xA;  creationTimestamp: null&#xA;  labels:&#xA;    app: cloud-deploy&#xA;  name: cloud-deploy&#xA;  namespace: cloud&#xA;spec:&#xA;  replicas: 3&#xA;  selector:&#xA;    matchLabels:&#xA;      app: cloud-deploy&#xA;  strategy: {}&#xA;  template:&#xA;    metadata:&#xA;      creationTimestamp: null&#xA;      labels:&#xA;        app: cloud-deploy&#xA;    spec:&#xA;      imagePullSecrets:&#xA;        - name: regcred&#xA;      containers:&#xA;      - image: alfabuster/echo-server:1.0&#xA;        name: echo-server&#xA;        ports:&#xA;        - containerPort: 8000&#xA;        env:&#xA;          - name: AUTHOR&#xA;            valueFrom:&#xA;              configMapKeyRef:&#xA;                name: cloud-configmap&#xA;                key: AUTHOR&#xA;        livenessProbe:&#xA;          httpGet:&#xA;            path: /healthz&#xA;            port: 8000&#xA;          initialDelaySeconds: 10&#xA;          periodSeconds: 5&#xA;          timeoutSeconds: 3&#xA;          failureThreshold: 3&#xA;        readinessProbe:&#xA;          httpGet:&#xA;            path: /healthz&#xA;            port: 8000&#xA;          periodSeconds: 5&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;livenessProbe отвечает на вопрос «пациент вообще жив?». Если проба падает — Kubernetes перезапускает контейнер. Это лекарство от зависших процессов и дедлоков.&lt;li&gt;&lt;p&gt;readinessProbe отвечает на вопрос «пациент готов принимать посетителей?». Если проба падает — под не перезапускается, его просто исключают из эндпоинтов Service, то есть перестают слать трафик, пока он не оклемается.&lt;/ul&gt;&lt;h4&gt;Service&lt;/h4&gt;&lt;p&gt;Service в Kubernetes — это тоже балансировщик. ClusterIP раскидывает входящие соединения по всем готовым подам.&lt;pre&gt;&lt;code class=yaml&gt;apiVersion: v1&#xA;kind: Service&#xA;metadata:&#xA;  name: cloud-deploy&#xA;  namespace: cloud&#xA;spec:&#xA;  type: ClusterIP&#xA;  selector:&#xA;    app: cloud-deploy&#xA;  ports:&#xA;    - port: 80&#xA;      targetPort: 8000&#xA;      protocol: TCP&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h4&gt;Ingress&lt;/h4&gt;&lt;p&gt;Создаем уже привычным способом, через генератор.&lt;pre&gt;&lt;code class=bash&gt;k create ingress cloud-ingress --rule=&amp;#34;cloud.local/*=cloud-deploy:80&amp;#34; -n cloud --dry-run=client -o yaml&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code class=yaml&gt;apiVersion: networking.k8s.io/v1&#xA;kind: Ingress&#xA;metadata:&#xA;  creationTimestamp: null&#xA;  name: cloud-ingress&#xA;  namespace: cloud&#xA;spec:&#xA;  rules:&#xA;  - host: cloud.local&#xA;    http:&#xA;      paths:&#xA;      - backend:&#xA;          service:&#xA;            name: cloud-deploy&#xA;            port:&#xA;              number: 80&#xA;        path: /&#xA;        pathType: Prefix&#xA;status:&#xA;  loadBalancer: {}&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Домен cloud.local надо прописать в /etc/hosts, указав на IP кластера (minikube ip или адрес ноды). Без этой записи браузер будет искать cloud.local в интернете.&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/703/984/eae/703984eaeb10b526324294729a686374.png alt=&#34;alt text&#34; sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/703/984/eae/703984eaeb10b526324294729a686374.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/703/984/eae/703984eaeb10b526324294729a686374.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;alt text&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;По итогу должно получиться, тоже самое приложение, но уже в кубере на своем домене, с 3 репликами и пробами.&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/208/ced/f62/208cedf621f416973b0efbc7a83bac66.png alt=&#34;alt text&#34; sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/208/ced/f62/208cedf621f416973b0efbc7a83bac66.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/208/ced/f62/208cedf621f416973b0efbc7a83bac66.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;alt text&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;h3&gt;Запереть контейнер изнутри*&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;Цель&#xA;&#xA;Проверить навыки администрирования *nix&#xA;Задача&#xA;&#xA;    Создать ВМ с ОС Ubuntu 22.04;&#xA;    Установить пакет docker-ce;&#xA;    Выполнить команду docker run -it --rm ubuntu -- bash;&#xA;    Не покидая контейнер, не меняя флаги запуска и настройки демона, выполнить задачи:&#xA;    4.1. сделать apt-get update, показать, что команда выполняется успешно;&#xA;    4.2. изнутри контейнера заблокировать ему выход в сеть интернет, команды блокировки с описанием приложить к задаче;&#xA;    4.3. показать, что apt-get update больше не работает.&#xA;    Доп. задача - найти ещё +1 способ выполнить подобную блокировку изнутри контейнера.&#xA;&#xA;В ответе к задаче приложить:&#xA;&#xA;    текстовое описание применённых конфигураций&#xA;&#xA;Полученные артефакты положить в папку /04-unix&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;На первый взгляд задача несложная, однако не зря она со звездочкой. Для начала запустим контейнер с убунтой и проверим связь.&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/2a5/fec/c58/2a5fecc588541d9229bb6c6f2f181a6b.png alt=&#34;alt text&#34; sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/2a5/fec/c58/2a5fecc588541d9229bb6c6f2f181a6b.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/2a5/fec/c58/2a5fecc588541d9229bb6c6f2f181a6b.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;alt text&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Вот в чём фокус: Docker по умолчанию запускает контейнеры с урезанным набором Linux capabilities, выбрасывая большинство привилегий, даже когда внутри вы root.&lt;p&gt;Root внутри контейнера не равняется рут на хосте.&lt;p&gt;Из очевидного в голову приходит установить iptables и запретить исходящий трафик. Установить можем, однако для правки таблицей маршрутизации рута недостаточно.&lt;pre&gt;&lt;code class=bash&gt;root@d79ba688c2c9:/# iptables -A OUTPUT -j DROP&#xA;iptables v1.8.11 (nf_tables): Could not fetch rule set generation id: Permission denied (you must be root)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Контейнер работает под root, но демон docker по умолчанию сбрасывает все capabilities, кроме дефолтного набора. А в таком списке отсутсвует привелегия NET_ADMIN, без которой вы не можете ни трогать iptables, ни гасить интерфейсы, ни править таблицу маршрутизации.&lt;p&gt;Флаги запуска и настройки демона трогать нельзя, иначе можно было бы просто в запуске добавить нужную капу и задача решилась очень быстро.&lt;p&gt;Нам нужно заблокировать интернет именно для утилиты apt.&lt;h4&gt;Способ №1 - nsswitch.conf&lt;/h4&gt;&lt;p&gt;Начну не с самого очевидного варианта файла /etc/nsswitch.conf, я о нем сам недавно узнал, принимая участие в одном CTF.&lt;p&gt;NSS расшифровывается как Name Service Switch. Это механизм, встроенный в стандартную библиотеку C (glibc). Когда любая программа пытается узнать IP-адрес по доменному имени или проверить, существует ли в системе определенный пользователь, она обращается к glibc. А glibc первым делом читает /etc/nsswitch.conf, чтобы понять, где именно искать ответ.&lt;p&gt;В структуре этого файла есть строка &lt;code&gt;hosts: files dns&lt;/code&gt;&lt;p&gt;Убрав dns из строки hosts:, вы вообще запрещаете системе обращаться к DNS — резолвиться будет только то, что явно прописано в /etc/hosts.&lt;p&gt;Кстати NSS управляет не только хостами. В файле прописаны правила для десятка системных баз данных.&lt;p&gt;Но в данном таске нас интересует dns, поэтому редактируем файл и получаем неработающий apt.&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/1db/260/b81/1db260b81044d3c125dd5ffce95a368f.png alt=&#34;alt text&#34; sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/1db/260/b81/1db260b81044d3c125dd5ffce95a368f.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/1db/260/b81/1db260b81044d3c125dd5ffce95a368f.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;alt text&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;h4&gt;Способ №2 - resolv.conf&lt;/h4&gt;&lt;p&gt;Второй способ, аналогичен первому, только на этот раз править будем файл &lt;code&gt;/etc/resolv.conf&lt;/code&gt;.&lt;p&gt;Если &lt;code&gt;/etc/nsswitch.conf&lt;/code&gt; решает, нужно ли вообще делать DNS-запрос, то &lt;code&gt;/etc/resolv.conf&lt;/code&gt; указывает системе, к кому именно с ним обращаться. Это главный конфигурационный файл DNS-резолвера. Он часто генерируется networkmanager’ом, поэтому при следующем подключении вероятнее всего он затрется, но для демонстрации в контейнере, хватит обычной правки через &lt;code&gt;vim&lt;/code&gt; или &lt;code&gt;nano&lt;/code&gt;.&lt;pre&gt;&lt;code&gt;&amp;gt; /etc/resolv.conf&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/21d/de3/2e1/21dde32e158278d3bcd1694410a2de2e.png alt=&#34;alt text&#34; sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/21d/de3/2e1/21dde32e158278d3bcd1694410a2de2e.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/21d/de3/2e1/21dde32e158278d3bcd1694410a2de2e.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;alt text&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Снова получаем неработающий интернет.&lt;h4&gt;Способ 3 - /etc/hosts&lt;/h4&gt;&lt;p&gt;&lt;code&gt;/etc/hosts&lt;/code&gt; — это простейший локальный справочник, который жестко связывает IP-адреса с доменными именами.&lt;p&gt;В него можно прописать локальный ip адрес серверов убунту и обновление не пройдет.&lt;pre&gt;&lt;code&gt;127.0.0.1 archive.ubuntu.com&#xA;127.0.0.1 security.ubuntu.com&#xA;127.0.0.1 ports.ubuntu.com&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/372/ad9/630/372ad9630fec30c0200313efa35c8fb8.png alt=&#34;alt text&#34; sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/372/ad9/630/372ad9630fec30c0200313efa35c8fb8.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/372/ad9/630/372ad9630fec30c0200313efa35c8fb8.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;alt text&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;h4&gt;Способ 4 - apt proxy&lt;/h4&gt;&lt;p&gt;У APT есть собственная настройка HTTP/HTTPS-прокси. Можно попробовать для всех HTTP-запросов использовать прокси 127.0.0.1:9.&lt;p&gt;Порт 9 используется для протокола Discard (RFC 863), который является сетевым эквивалентом черной дыры (/dev/null). Любые данные, отправленные на этот порт по TCP или UDP, просто уничтожаются сервером без какого-либо ответа или подтверждения.&lt;pre&gt;&lt;code class=bash&gt;cat &amp;gt; /etc/apt/apt.conf.d/99-block-network &amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;&#xA;Acquire::http::Proxy &amp;#34;http://127.0.0.1:9&amp;#34;;&#xA;Acquire::https::Proxy &amp;#34;http://127.0.0.1:9&amp;#34;;&#xA;EOF&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/15a/32b/ef1/15a32bef1f5bf0326236aab42f57ae2d.png alt=&#34;alt text&#34; sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/15a/32b/ef1/15a32bef1f5bf0326236aab42f57ae2d.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/15a/32b/ef1/15a32bef1f5bf0326236aab42f57ae2d.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;alt text&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;h3&gt;Как итог&lt;/h3&gt;&lt;p&gt;Получилось неплохое задание, мне понравилось его выполнять. Много чего в памяти освежил, некоторые вещи изучил. Задание не сказать, чтобы сильно простое, но и не сложное. Некоторые задания потребуют необычного подхода и глубокое знание linux.&lt;p&gt;Выполнил и выложил на гитхаб &lt;a href=https://github.com/alfabuster/devops-camp-cloudru-2025-writeup/tree/main rel=&#34;noopener nofollow&#34;&gt;alfabuster&lt;/a&gt;.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <author>seobuster</author>
      <guid>https://habr.com/ru/articles/1082220/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082220</guid>
      <pubDate>Mon, 14 Sep 2026 19:48:44 +0000</pubDate>
    </item>
    <item>
      <title>Как гаражная разработка шведских любителей обошла стартап Бэббиджа с бюджетом в 30 паровозов</title>
      <link>https://habr.com/ru/companies/otus/articles/1075524/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1075524</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Великобритания, 1820-е.&lt;/em&gt;&lt;/strong&gt;&lt;em&gt; Индустриальная революция, бурный рост производства. Экономика и флот зависели от математических таблиц, которые люди рассчитывали вручную. Из‑за неизбежных ошибок в вычислениях гибли корабли и терялись деньги.&lt;/em&gt;&lt;hr&gt;&lt;p&gt;Математик Чарльз Бэббидж предложил автоматизировать расчеты и спроектировал механическую Разностную машину.&lt;p&gt;Британское правительство выделило на разработку огромную сумму, сопоставимую со стоимостью боевого корабля. Однако за двадцать лет проект так и не завершили: финансирование закрыли, а от машины остались только чертежи и небольшой рабочий фрагмент.&lt;p&gt;При этом первую действующую Разностную машину построили шведский издатель Георг Шойц и его сын‑студент. Вдохновившись статьей о концепции Бэббиджа, они собрали работающий образец в домашней мастерской и даже продали два экземпляра.&lt;blockquote&gt;&lt;p&gt;Почему проект с государственным бюджетом провалился, а любители добились результата?&lt;p&gt;Причиной стали технические ограничения XIX века или классические ошибки в управлении разработкой?&lt;/blockquote&gt;&lt;h3&gt;От ручных расчетов к механическим&lt;/h3&gt;&lt;p&gt;Механические счетные устройства существовали и до Бэббиджа: часы Шиккарда, арифмометр Паскаля, барабан Лейбница и перфокарты Жаккара. Однако они подходили лишь для простых задач и не справлялись с объемами вычислений, необходимыми флоту, страхованию и инженерии.&lt;p&gt;Французский математик Гаспар де Прони предложил конвейерный метод расчетов, разделив процесс на три уровня:&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Архитекторы:&lt;/strong&gt; ученые разбивали сложные уравнения на простые операции (сложение и вычитание).&lt;li&gt;&lt;p&gt;&lt;strong&gt;Менеджеры:&lt;/strong&gt; распределяли задачи и контролировали порядок действий.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Исполнители:&lt;/strong&gt; наемные работники без специального образования (их называли &lt;em&gt;computers&lt;/em&gt;) вручную вели однотипные расчеты на бумаге.&lt;/ul&gt;&lt;p&gt;Метод де Прони ускорил работу, но не решил проблему ошибок: из‑за монотонного труда и усталости люди регулярно ошибались в таблицах.&lt;p&gt;Изучая эти неточности, Чарльз Бэббидж пришел к выводу: базовые рутинные операции третьего уровня можно полностью передать механизму. Объединив идеи де Прони с наработками предшественников, он приступил к созданию первого специализированного вычислителя — Разностной машины.&lt;h3&gt;Чарльз Бэббидж: изобретатель и ученый&lt;/h3&gt;&lt;p&gt;Чарльз Бэббидж (1791–1871) известен прежде всего как математик, астроном и создатель проекта первого механического компьютера. Он занимал кафедру Лукасовского профессора &lt;a href=https://habr.com/rss/all/all/#1&gt;&lt;em&gt;&lt;sup&gt;(примеч1)&lt;/sup&gt;&lt;/em&gt;&lt;/a&gt; в Кембридже и получил первую Золотую медаль Королевского астрономического общества.&lt;p&gt;Хотя Чарльз Бэббидж вошел в историю как «отец компьютера», он был ученым‑универсалом. Его разработки повлияли на многие сферы:&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Математика:&lt;/strong&gt; основал «Аналитическое общество», внедрив в Британии европейскую систему дифференциального исчисления Лейбница (dy/dxdy/dx).&lt;li&gt;&lt;p&gt;&lt;strong&gt;Транспорт и связь:&lt;/strong&gt; разработал для поездов путеочиститель («скотосбрасыватель»), сигнальные огни и вагон‑лабораторию, а его экономические расчеты легли в основу почтовой реформы с единым тарифом (&lt;em&gt;Penny Post&lt;/em&gt;).&lt;li&gt;&lt;p&gt;&lt;strong&gt;Медицина и геология:&lt;/strong&gt; создал офтальмоскоп и предложил физико‑математическое обоснование смещения изотерм и поднятия земной коры.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Производство:&lt;/strong&gt; усовершенствовал металлорежущие станки и ввел стандарты механических допусков для точной обработки деталей.&lt;/ul&gt;&lt;p&gt;Большинство этих изобретений стали сопутствующим результатом его главной цели — создания автоматической вычислительной машины.&lt;h3&gt;Разностная машина Чарльза Бэббиджа&lt;/h3&gt;&lt;p&gt;Разностная машина задумывалась как специализированный механический вычислитель для автоматизации рутинных расчетов и замены людей‑вычислителей.&lt;h4&gt;Принцип работы&lt;/h4&gt;&lt;p&gt;В основу лег метод конечных разностей: сложные функции раскладываются в полиномы, а их расчет сводится к простому последовательному сложению с помощью шестерен.&lt;p&gt;В 1822 году Бэббидж построил действующий прототип, считавший квадратичные функции до восьмого знака. Адмиралтейство оценило пользу для морской навигации, и казначейство выделило средства на полноразмерный аппарат. Общие вложения превысили 17 000 фунтов стерлингов (около 2 миллионов фунтов в современных ценах).&lt;p&gt;Разностная машина задумывалась как механический вычислитель для автоматизации рутинных расчетов и замены людей‑вычислителей.&lt;h4&gt;Разностная машина № 1&lt;/h4&gt;&lt;p&gt;Проект предполагал устройство весом 4 тонны из 25 000 деталей с точностью до 20 знаков и печатным блоком. Постройку свернули по трем причинам:&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Сложность производства:&lt;/strong&gt; детали вытачивали вручную без общих стандартов, и малейшие люфты клинили сложный механизм.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Конфликт с мастером:&lt;/strong&gt; в 1833 году Бэббидж поссорился с механиком Джозефом Клементом из‑за оплаты и прав на инструменты, после чего работы остановились.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Смена планов:&lt;/strong&gt; изобретатель постоянно вносил правки, а в 1834 году увлекся более сложной Аналитической машиной.&lt;/ul&gt;&lt;p&gt;В 1842 году государство закрыло проект. Тем не менее работа над машиной дала толчок развитию точного станкостроения и стандартизации в механике.&lt;h4&gt;Разностная машина № 2 и итоги&lt;/h4&gt;&lt;p&gt;В 1840-х годах Бэббидж переработал чертежи: повысил точность до 31 знака и сократил число деталей до 8000. Из‑за прошлых неудач власти отказались финансировать сборку.&lt;p&gt;При жизни изобретателя полноразмерную машину так и не построили, посчитав ее механически нереализуемой. Проверить работоспособность расчетов Бэббиджа удалось только спустя 150 лет.&lt;h3&gt;Code review: проверка работоспособности через 150 лет&lt;/h3&gt;&lt;p&gt;Долгое время считалось, что построить машину Бэббиджа в XIX веке мешали несовершенные технологии, трение и люфты деталей.&lt;p&gt;В 1985 году куратор Лондонского музея науки Дорон Свейд решил собрать Разностную машину № 2 по оригинальным чертежам автора.&lt;p&gt;&lt;strong&gt;Правила сборки:&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Чертежи оставили без изменений. За шесть лет работы в них нашли всего две ошибки в размерах.&lt;li&gt;&lt;p&gt;Детали делали только из строго аутентичных сплавов: бронзы, латуни, чугуна и стали, соблюдая допуски и стандарты XIX века.&lt;/ul&gt;&lt;p&gt;В 1991 году собрали вычислительный блок. Механизм успешно запустился вручную и безошибочно рассчитывал полиномы до 31 знака. К 2002 году к нему добавили печатный блок для вывода данных на бумагу и создания форм для типографии.&lt;p&gt;Сборка доказала: расчеты Бэббиджа были верны, а технологии его эпохи позволяли построить работающий автоматический калькулятор.&lt;h3&gt;Разностная машина Шойцев: от идеи до банкротства&lt;/h3&gt;&lt;p&gt;Шведский издатель Георг Шойц часто сталкивался с опечатками наборщиков в таблицах. Узнав об идее Чарльза Бэббиджа, он вместе с сыном‑студентом Эдвардом решил собрать машину, которая сама считает данные и делает печатные формы.&lt;h4&gt;Создание и признание&lt;/h4&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;1843 год:&lt;/strong&gt; Эдвард собрал первый металлический образец. Он считал 5-значные числа и выбивал результаты на свинцовых пластинах.&lt;li&gt;&lt;p&gt;&lt;strong&gt;1853 год:&lt;/strong&gt; На господдержку под гарантии поручителей семья построила полноразмерную машину, работавшую уже с 15-значными числами.&lt;li&gt;&lt;p&gt;&lt;strong&gt;1855 год:&lt;/strong&gt; Бэббидж высоко оценил шведский аппарат и помог представить его на Всемирной выставке в Париже, где изобретение получило золотую медаль.&lt;/ul&gt;&lt;h4&gt;Продажи и разорение&lt;/h4&gt;&lt;p&gt;Всего удалось продать два экземпляра:&lt;ol&gt;&lt;li&gt;&lt;p&gt;В 1856 году — американской обсерватории Дадли за £1 000 для расчета орбит.&lt;li&gt;&lt;p&gt;В 1859 году — британскому ведомству ЗАГС за £1 200 для печати демографических таблиц &lt;em&gt;English Life Tables&lt;/em&gt;.&lt;/ol&gt;&lt;p&gt;Несмотря на рабочий продукт, Шойцы разорились:&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Дорогая ручная сборка&lt;/strong&gt; съедала всю прибыль.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Спрос был минимальным&lt;/strong&gt; — вычислитель покупали лишь редкие научные центры.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Механика сбоила&lt;/strong&gt; от сырости и перепадов температуры.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Доходы не покрыли расходов&lt;/strong&gt;, и создателям пришлось отдать свое имущество за долги.&lt;/ul&gt;&lt;h2&gt;Выводы&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Идея Бэббиджа работала.&lt;/strong&gt; Копия Разностной машины, собранная Музеем науки в Лондоне строго по чертежам автора, подтвердила: викторианских технологий было достаточно. Бэббиджу помешали не законы физики, а постоянное стремление Бэббиджа к улучшению, изменение требований и проектных решений, перерасход бюджета и конфликты с инвесторами.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Шойцы доказали реализуемость на практике.&lt;/strong&gt; Отец и сын создали первый действующий механический вычислитель, подтвердив точность математических расчетов Бэббиджа доступными в XIX веке средствами.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Рабочий продукт не равен прибыли.&lt;/strong&gt; Успешная сборка и признание ученых не спасли проект Шойцев от банкротства: технология опередила свое время, а рынок для таких сложных и дорогих машин еще не сформировался.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Оправданный перфекционизм Бэббиджа. &lt;/strong&gt;Поломки шведской машины доказали: жесткие требования Бэббиджа к металлу и точности не были блажью. Шойцы сэкономили на материалах и допусках, и к первому калькулятору у американской обсерватории были серьезные претензии: клинило из‑за малейших перепадов температуры, повышения влажности — разнородные металлы расширялись неравномерно и сбивали вычисления.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Опыт показал, что компромиссы здесь не работали&lt;/strong&gt;: сложнейшая механика требовала именно той прецизионной обработки и сплавов, на которых настаивал британский ученый.&lt;/ul&gt;&lt;h3&gt;Что было дальше: шаг в бесконечность&lt;/h3&gt;&lt;p&gt;Осознав тупик с первой машиной, Бэббидж не опустил руки. Напротив, в его голове родился замысел, который окончательно оставил позади эпоху простых механических калькуляторов.&lt;p&gt;Он задумал &lt;strong&gt;Аналитическую машину&lt;/strong&gt; — проект, который по своей внутренней логике ничем не отличался от архитектуры компьютеров, стоящих сегодня на наших рабочих столах. В чертежах из шестерёнок и рычагов уже тогда были заложены процессор, оперативная память, шины данных и ввод программ и данных с перфокарт, вывод и даже интеграция с другими системами.&lt;p&gt;А вскоре к проекту присоединилась графиня &lt;strong&gt;Ада Лавлейс&lt;/strong&gt; — единственный человек викторианской эпохи, сумевший разглядеть в груде металла не просто вычислитель таблиц, а универсальную цифровую машину, способную обрабатывать любую информацию и даже писать музыку.&lt;p&gt;Но это уже совсем другая история: о рождении первого в мире программного кода, отчаянных попытках найти финансирование (вплоть до математических схем ставок на скачках и заложенных фамильных драгоценностях), трагической судьбе идеи, обогнавшей свой век на сто лет.&lt;p&gt;&lt;em&gt;О том, как была устроена Аналитическая машина, почему код Ады Лавлейс считают фундаментом современного программирования и как энтузиасты в XXI веке пытаются наконец запустить этот паровой компьютер — поговорим в следующих статьях.&lt;/em&gt;&lt;details class=spoiler&gt;&lt;summary&gt;Примечания&lt;/summary&gt;&lt;div class=spoiler__content&gt;&lt;a class=anchor id=1&gt;&lt;/a&gt;&lt;ol&gt;&lt;li&gt;&lt;p&gt;Лукосовский профессор — одна из самых престижных именных профессорских должностей в мире. Среди лукосовских профессоров — Исаак Ньютон, Поль Дирак и Стивен Хокинг.&lt;/ol&gt;&lt;/div&gt;&lt;/details&gt;&lt;h4&gt;А пока предлагаю обсудить (пишите в комментариях):&lt;/h4&gt;&lt;ol&gt;&lt;li&gt;&lt;p&gt;Как вы считаете, был ли у Бэббиджа шанс вовремя остановиться на работающем прототипе, или гениальность всегда граничит с неумением завершать проекты?&lt;li&gt;&lt;p&gt;Какие современные IT‑стартапы напоминают вам эту историю: сжигают миллионы инвесторских денег ради опережающей время идеи, но в итоге меняют индустрию?&lt;li&gt;&lt;p&gt;Что, на ваш взгляд, важнее для прорыва — инженерная база эпохи или смелость концептуальной идеи?&lt;/ol&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/f9e/71a/a19/f9e71aa191369be888753124d4775c2f.png width=2640 height=300 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/f9e/71a/a19/f9e71aa191369be888753124d4775c2f.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/f9e/71a/a19/f9e71aa191369be888753124d4775c2f.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;История Бэббиджа хорошо показывает, что проект может быть технически жизнеспособным и всё равно зайти в тупик из-за размытых границ, меняющихся требований и недооценённых рисков.&lt;p&gt;Сегодня такие проблемы проявляются быстрее, но суть почти не изменилась: важно вовремя увидеть, где проект начинает терять управляемость, и понять, что именно нужно проверить до того, как цена ошибки станет слишком высокой.&lt;p&gt;&lt;em&gt;Как раз такие ситуации разбираем на бесплатных открытых уроках — от архитектурных рисков до изменений требований по ходу проекта. Присоединяйтесь:&lt;/em&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;23 сентября в 20:00. «Как системному аналитику проводить архитектурное ревью и находить риски до начала разработки». &lt;a href=https://otus.pw/bCi1/&gt;Записаться&lt;/a&gt;&lt;li&gt;&lt;p&gt;22 октября в 20:00. «Управление изменениями требований». &lt;a href=https://otus.pw/uKqC/&gt;Записаться&lt;/a&gt;&lt;/ul&gt;&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;Полный список бесплатных уроков сентября собран &lt;/em&gt;&lt;a href=https://otus.pw/LV0pE/&gt;&lt;em&gt;в дайджесте.&lt;/em&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <guid>https://habr.com/ru/companies/otus/articles/1075524/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1075524</guid>
      <pubDate>Mon, 14 Sep 2026 19:12:15 +0000</pubDate>
    </item>
    <item>
      <title>А чё, так можно было? Три недооценённых атрибута.NET</title>
      <link>https://habr.com/ru/articles/1082206/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082206</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;Три атрибута, которые меняют результат компиляции. Один запускает метод раньше Main. Второй передаёт текст выражения вместо результата. Третий ускоряет stackalloc в 50 раз.&lt;p&gt;Все три описаны в документации и применяются внутри .NET. За его пределами их практически не найти. &lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td data-colwidth=407 width=407&gt;&lt;p align=left&gt;&lt;strong&gt;Процессор&lt;/strong&gt;&lt;td&gt;&lt;p align=center&gt;&lt;strong&gt;Система&lt;/strong&gt;&lt;tr&gt;&lt;td data-colwidth=407 width=407&gt;&lt;p align=left&gt;Intel Core i9-10900KF 3.70GHz, 10 ядер&lt;td&gt;&lt;p align=center&gt;Windows 10 22H2&lt;tr&gt;&lt;td data-colwidth=407 width=407&gt;&lt;p align=left&gt;AMD Ryzen 9 5950X 3.39GHz, 16 ядер&lt;td&gt;&lt;p align=center&gt;Windows 10 1809&lt;tr&gt;&lt;td data-colwidth=407 width=407&gt;&lt;p align=left&gt;Intel Xeon W-2255 3.70GHz, 10 ядер&lt;td&gt;&lt;p align=center&gt;Windows Server 2022&lt;tr&gt;&lt;td data-colwidth=407 width=407&gt;&lt;p align=left&gt;Intel Xeon Silver 4314 2.40GHz, 2 CPU, 32 ядра&lt;td&gt;&lt;p align=center&gt;Windows Server 2022&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;Все машины x64&lt;/em&gt;&lt;p&gt;Рантаймы .NET 8, 9 и 10 — все три в одном запуске &lt;a href=https://benchmarkdotnet.org/ rel=&#34;noopener nofollow&#34;&gt;BenchmarkDotNet&lt;/a&gt; 0.15.8.&lt;h3&gt;1. Код, который выполняется до Main&lt;/h3&gt;&lt;h4&gt;Исходник&lt;/h4&gt;&lt;p&gt;Метод помечен атрибутом и больше нигде не встречается:&lt;pre&gt;&lt;code class=cs&gt;internal static class Startup&#xA;{&#xA;    [ModuleInitializer]&#xA;    internal static void Init()&#xA;    {&#xA;        Order.Add(&amp;#34;инициализатор модуля&amp;#34;);&#xA;    }&#xA;}&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:87px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;В Main его нет. Там только своя отметка первой строкой:&lt;pre&gt;&lt;code class=cs&gt;private static int Main(string[] args)&#xA;{&#xA;    Startup.Order.Add(&amp;#34;точка входа&amp;#34;);&#xA;    // ...&#xA;}&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h4&gt;Что выводит отчёт&lt;/h4&gt;&lt;pre&gt;&lt;code class=cs&gt;Порядок событий:&#xA; &#xA;  1. инициализатор модуля&#xA;  2. точка входа&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Одинаково на четырёх машинах и трёх рантаймах.&lt;h4&gt;Причина&lt;/h4&gt;&lt;p&gt;Компилятор находит все методы с этим атрибутом и вызывает их из инициализатора модуля. До этого момента ни одна строка кода сборки не выполняется.&lt;p&gt;К методу есть требования: статический, без параметров, возвращает void, не обобщённый и не внутри обобщённого типа, доступность internal или public. Любое нарушение — ошибка сборки.&lt;h4&gt;Где встречается&lt;/h4&gt;&lt;p&gt;В библиотеке классов такой метод один, в &lt;a href=https://github.com/dotnet/runtime/blob/60629d14374c56f1cb51819049ad1fa529307f8d/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventSource.cs#L3874 rel=&#34;noopener nofollow&#34;&gt;EventSource&lt;/a&gt;. Основное применение — генераторы исходного кода: настройка выполняется до Main.&lt;h4&gt;Где легко ошибиться&lt;/h4&gt;&lt;p&gt;Рассчитывать на порядок, если инициализаторов несколько. Три метода в разных классах вызвались в порядке объявления и от прогона к прогону не менялись, но в спецификации этот порядок не закреплён.&lt;p&gt;Бросить исключение. Программа падает до точки входа, и Main не выполняется:&lt;pre&gt;&lt;code class=cs&gt;Unhandled exception. System.TypeInitializationException:&#xA;  The type initializer for &amp;#39;&amp;lt;Module&amp;gt;&amp;#39; threw an exception.&#xA; ---&amp;gt; System.InvalidOperationException: из инициализатора&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Применять в библиотеке. Начиная с .NET 10 включено правило &lt;a href=https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2255 rel=&#34;noopener nofollow&#34;&gt;CA2255&lt;/a&gt;: библиотека меняет порядок запуска приложения и мешает выбросить неиспользуемый код при публикации.&lt;h3&gt;2. Текст выражения вместо значения&lt;/h3&gt;&lt;h4&gt;Исходник&lt;/h4&gt;&lt;p&gt;Второй параметр помечен атрибутом и получает значение по умолчанию:&lt;pre&gt;&lt;code class=cs&gt;static void Check(bool condition,&#xA;    [CallerArgumentExpression(nameof(condition))] string? text = null)&#xA;{&#xA;    Console.WriteLine(text + &amp;#34; = &amp;#34; + condition);&#xA;}&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h4&gt;Что выводит отчёт&lt;/h4&gt;&lt;pre&gt;&lt;code class=cs&gt;Что попадает в text при разных вызовах:&#xA; &#xA;  a + b &amp;gt; 10                            ложь&#xA;  a * b == 6                            истина&#xA;  empty is null                         истина&#xA;  !string.IsNullOrEmpty(empty)          ложь&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;В параметр приходит не результат, а то, что написано в месте вызова.&lt;h4&gt;Причина&lt;/h4&gt;&lt;p&gt;Подстановка происходит при сборке: компилятор берёт исходный текст аргумента и передаёт его строковой константой. Программа получает готовую строку и ничего с ней не делает.&lt;p&gt;Замер это подтверждает. Библиотечная проверка и такая же, написанная явно, исключений в замере нет: &lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;Способ&lt;/strong&gt;&lt;td&gt;&lt;p align=center&gt;&lt;strong&gt;i9-10900KF&lt;/strong&gt;&lt;td&gt;&lt;p align=center&gt;&lt;strong&gt;Ryzen 9 5950X&lt;/strong&gt;&lt;td&gt;&lt;p align=center&gt;&lt;strong&gt;Xeon W-2255&lt;/strong&gt;&lt;td&gt;&lt;p align=center&gt;&lt;strong&gt;Xeon Silver 4314&lt;/strong&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;из библиотеки&lt;td&gt;&lt;p align=center&gt;0,3944&lt;td&gt;&lt;p align=center&gt;0,2656&lt;td&gt;&lt;p align=center&gt;0,7058&lt;td&gt;&lt;p align=center&gt;0,6679&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;вручную&lt;td&gt;&lt;p align=center&gt;0,3969&lt;td&gt;&lt;p align=center&gt;0,2646&lt;td&gt;&lt;p align=center&gt;0,6427&lt;td&gt;&lt;p align=center&gt;0,7231&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;Наносекунды, .NET 10&lt;/em&gt;&lt;h4&gt;Где встречается&lt;/h4&gt;&lt;p&gt;В 21-м месте библиотеки классов. Например, &lt;a href=https://github.com/dotnet/runtime/blob/60629d14374c56f1cb51819049ad1fa529307f8d/src/libraries/System.Private.CoreLib/src/System/ArgumentNullException.cs#L55 rel=&#34;noopener nofollow&#34;&gt;ArgumentNullException.ThrowIfNull&lt;/a&gt;:&lt;pre&gt;&lt;code class=cs&gt;public static void ThrowIfNull([NotNull] object? argument,&#xA;    [CallerArgumentExpression(nameof(argument))] string? paramName = null)&#xA;{&#xA;    if (argument is null)&#xA;    {&#xA;        Throw(paramName);&#xA;    }&#xA;}&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Поэтому исключение показывает имя переменной из места вызова, а не имя параметра argument. Отчёт это подтверждает:&lt;pre&gt;&lt;code class=cs&gt;  ArgumentNullException             имя аргумента: empty&#xA;  ArgumentOutOfRangeException       имя аргумента: a - 5&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Во втором случае именем аргумента стало целое выражение — то, что передали.&lt;h4&gt;Где легко ошибиться&lt;/h4&gt;&lt;p&gt;Передать текст вторым аргументом. Тогда компилятор ничего не подставляет и берёт то, что написано:&lt;pre&gt;&lt;code class=cs&gt;Check(a + b &amp;gt; 10);                       // текст: a + b &amp;gt; 10&#xA;Check(a + b &amp;gt; 10, &amp;#34;передано вручную&amp;#34;);   // текст: передано вручную&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Забыть про размер сборки. Текст каждого аргумента строкой попадает в метаданные: после шести вызовов сборка увеличилась с 4608 байт до 5120.&lt;h3&gt;3. Отказ от обнуления памяти&lt;/h3&gt;&lt;h4&gt;Исходник&lt;/h4&gt;&lt;p&gt;Два одинаковых метода, различаются одной строкой:&lt;pre&gt;&lt;code class=cs&gt;[MethodImpl(MethodImplOptions.NoInlining)]&#xA;internal static int StackWithInit(int size)&#xA;{&#xA;    Span&amp;lt;byte&amp;gt; buffer = stackalloc byte[size];&#xA;    buffer[0] = 1;&#xA;    buffer[^1] = 2;&#xA; &#xA;    return buffer[0] + buffer[^1];&#xA;}&#xA; &#xA;[SkipLocalsInit]&#xA;[MethodImpl(MethodImplOptions.NoInlining)]&#xA;internal static int StackWithoutInit(int size)&#xA;{&#xA;    // тело то же самое&#xA;}&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h4&gt;Что показывает замер&lt;/h4&gt;&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;Буфер&lt;/strong&gt;&lt;td&gt;&lt;p align=center&gt;&lt;strong&gt;i9-10900KF&lt;/strong&gt;&lt;td&gt;&lt;p align=center&gt;&lt;strong&gt;Ryzen 9 5950X&lt;/strong&gt;&lt;td&gt;&lt;p align=center&gt;&lt;strong&gt;Xeon W-2255&lt;/strong&gt;&lt;td&gt;&lt;p align=center&gt;&lt;strong&gt;Xeon Silver 4314&lt;/strong&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;64 байта&lt;td&gt;&lt;p align=center&gt;2,452 / 1,765&lt;td&gt;&lt;p align=center&gt;2,082 / 4,695&lt;td&gt;&lt;p align=center&gt;2,793 / 2,419&lt;td&gt;&lt;p align=center&gt;5,409 / 4,512&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;256 байт&lt;td&gt;&lt;p align=center&gt;6,475 / 1,729&lt;td&gt;&lt;p align=center&gt;5,741 / 4,691&lt;td&gt;&lt;p align=center&gt;8,530 / 2,208&lt;td&gt;&lt;p align=center&gt;13,015 / 4,501&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;1024 байта&lt;td&gt;&lt;p align=center&gt;25,505 / 1,769&lt;td&gt;&lt;p align=center&gt;17,712 / 4,703&lt;td&gt;&lt;p align=center&gt;31,281 / 2,574&lt;td&gt;&lt;p align=center&gt;48,282 / 4,566&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;4096 байт&lt;td&gt;&lt;p align=center&gt;103,474 / 2,043&lt;td&gt;&lt;p align=center&gt;71,811 / 4,939&lt;td&gt;&lt;p align=center&gt;126,066 / 2,685&lt;td&gt;&lt;p align=center&gt;189,264 / 4,827&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;Наносекунды, с обнулением и без, .NET 10&lt;/em&gt; &lt;p&gt;На четырёх килобайтах разница от 14,5 до 50,6 раза. С обнулением время растёт вместе с буфером, без обнуления — не меняется.&lt;h4&gt;Причина&lt;/h4&gt;&lt;p&gt;У метода есть флаг от компилятора, и по нему джит заполняет нулями всю память под локальные переменные, включая ту, что выделена через stackalloc. Атрибут его убирает.&lt;p&gt;Отсюда и числа в таблице: чем больше буфер, тем дольше заполнение, а без него размер не важен.&lt;h4&gt;Где встречается&lt;/h4&gt;&lt;p&gt;Атрибут прописан не в коде, а &lt;a href=https://github.com/dotnet/runtime/blob/60629d14374c56f1cb51819049ad1fa529307f8d/src/libraries/Directory.Build.targets#L220 rel=&#34;noopener nofollow&#34;&gt;в общем файле сборки&lt;/a&gt;: свойство SkipLocalsInit включено для каждого проекта, входящего в .NET.&lt;h4&gt;Где легко ошибиться&lt;/h4&gt;&lt;p&gt;Поставить атрибут ради небольшого буфера. На 64 байтах выигрыш в лучшем случае 1,39 раза, а на Ryzen 9 5950X версия без обнуления медленнее: 4,695 против 2,082.&lt;p&gt;На этой машине результат отличается от остальных: без заполнения нулями с ростом буфера время не меняется и составляет около 4,7 наносекунды, а на других процессорах оно в пределах 1,7–2,7.&lt;p&gt;Прочитать буфер раньше, чем в него что-то записали. Атрибут не заполняет память нулями и не требует этого от рантайма — в буфере будут данные от предыдущих вызовов.&lt;p&gt;Забыть про файл проекта. Без небезопасного контекста атрибут не работает: сборка упадёт с ошибкой &lt;a href=https://learn.microsoft.com/dotnet/csharp/language-reference/compiler-messages/unsafe-code-errors rel=&#34;noopener nofollow&#34;&gt;CS0227&lt;/a&gt;.&lt;h3&gt;Границы замеров&lt;/h3&gt;&lt;p&gt;Все замеры сняты на x64 под Windows, на .NET 8, 9 и 10.&lt;p&gt;У всех измеряемых методов запрещено встраивание. Иначе компилятор перенесёт код метода в замер, а вместе с ним заполнение памяти нулями.&lt;p&gt;SkipLocalsInit проставлен на отдельных методах, а не на классе: на классе он подействует и на тот вариант, который заполняет память нулями.&lt;p&gt;В замере на вход всегда передаётся непустая строка, поэтому проверка на null ни разу не срабатывает. Так в замер попадает только сама проверка, без обработки исключения.&lt;h3&gt;Код из статьи&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;a href=https://github.com/ComboxSoftDeveloper/AttributeProof rel=&#34;noopener nofollow&#34;&gt;AttributeProof&lt;/a&gt; — замеры, отчёты и выгрузки с четырёх машин&lt;/ul&gt;&lt;h3&gt;Ссылки&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;a href=https://learn.microsoft.com/dotnet/api/system.runtime.compilerservices.moduleinitializerattribute rel=&#34;noopener nofollow&#34;&gt;ModuleInitializerAttribute&lt;/a&gt;&lt;li&gt;&lt;p&gt;&lt;a href=https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2255 rel=&#34;noopener nofollow&#34;&gt;CA2255: атрибут не для библиотек&lt;/a&gt;&lt;li&gt;&lt;p&gt;&lt;a href=https://github.com/dotnet/runtime/blob/60629d14374c56f1cb51819049ad1fa529307f8d/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventSource.cs#L3874 rel=&#34;noopener nofollow&#34;&gt;Как этим пользуется EventSource&lt;/a&gt;&lt;li&gt;&lt;p&gt;&lt;a href=https://github.com/dotnet/runtime/blob/60629d14374c56f1cb51819049ad1fa529307f8d/src/libraries/System.Private.CoreLib/src/System/ArgumentNullException.cs#L55 rel=&#34;noopener nofollow&#34;&gt;ArgumentNullException.ThrowIfNull&lt;/a&gt;&lt;li&gt;&lt;p&gt;&lt;a href=https://github.com/dotnet/runtime/blob/60629d14374c56f1cb51819049ad1fa529307f8d/src/libraries/Directory.Build.targets#L220 rel=&#34;noopener nofollow&#34;&gt;SkipLocalsInit включён на всю библиотеку классов&lt;/a&gt;&lt;li&gt;&lt;p&gt;&lt;a href=https://learn.microsoft.com/dotnet/api/system.runtime.compilerservices.skiplocalsinitattribute rel=&#34;noopener nofollow&#34;&gt;SkipLocalsInitAttribute&lt;/a&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Всем удачи и до новых встреч!&lt;/strong&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <author>Geronom</author>
      <guid>https://habr.com/ru/articles/1082206/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082206</guid>
      <pubDate>Mon, 14 Sep 2026 18:53:10 +0000</pubDate>
    </item>
    <item>
      <title>Промпт‑инжиниринг для моделей надежности отказоустойчивого кластера</title>
      <link>https://habr.com/ru/articles/1082196/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082196</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;Ранее в «&lt;a href=https://habr.com/ru/articles/1075024/ rel=&#34;noopener nofollow&#34;&gt;&lt;strong&gt;Базовые модели надежности отказоустойчивого кластера&lt;/strong&gt;&lt;/a&gt;» [2] были показаны теоретические подходы к построению моделей (непрерывная цепь Маркова, CTMC, Continuous-Time Markov Chain) отказоустойчивого кластера (fault-tolerant cluster). &lt;p&gt;Предлагаемый ниже практикум «Промпт-инжиниринг для моделей надежности отказоустойчивого кластера» имеет цель популяризации практических расчетов надежности «подручными средствами» - через ИИ/AI (искусственный интеллект через AI-чат, например, ChatGPT). Задача – формализовать промпты для формирования модели надежности и проведения по ней расчета стационарного коэффициента готовности, Кг.&lt;p&gt;Промпт (prompt) &lt;em&gt;-&lt;/em&gt; запрос, текстовая инструкция, которую пользователь даёт нейросети.&lt;p&gt;Промпт-инжиниринг — практика проектирования, итерации и верификации текстовых инструкций (промптов) для получения предсказуемых, точных и воспроизводимых результатов от больших языковых моделей (LLM).&lt;p&gt;Коэффициента готовности, напомним [1], что Кг – численно равен вероятности застать систему (в данном случае кластер) в одном из ее работоспособных состояний (сложение вероятностей каждого такого состояния). Предлагаемые подходы могут быть применены и для других сложных резервируемых структур с учетом ограничений СТМС: случайный процесс «без памяти» с дискретными состояниями и непрерывным временем, а также экспоненциальное распределение времён до отказа/восстановления, независимость отказов компонентов, см. [6], [7].&lt;p&gt;Специализированные для расчетов надёжности пакеты RAS (Reliability, Availability, and Serviceability) типа SHARPE, MEADEP, RAScad, CARMS, RAPTOR и т.п. позволяют строить и рассчитывать сложные модели. Есть более простые системы (и бесплатные), которые можно использовать для рассмотренных ранее моделей надежности отказоустойчивого кластера, например, &lt;a href=https://relind.ru/ rel=&#34;noopener nofollow&#34;&gt;&lt;strong&gt;relind.ru&lt;/strong&gt;&lt;/a&gt;. Можно использовать универсальные средства (не специализированные под расчет надёжности), например, для расчета марковских цепей, или расчеты делать непосредственно в Matlab и Mathcad и подобных. Однако все это требует достаточно высокий «порог входа» и имеет встроенные в конкретный soft-продукт ограничения, как, по существу, так и по визуализации.&lt;p&gt;Рассмотрим вариант «быстрого \ легкого старта» при поддержке ИИ, причем не только для расчёта показателя надежности (Кг), но и визуализации модели и оформления результатов. &lt;p&gt;Последовательность действий:&lt;p&gt;- вводим базовый промпт (запрос) и оцениваем результат - как проверка адекватности ИИ на основе объемной постановки задачи: реализация промпта с описанием в нем деталей, включая формат представления результата;&lt;p&gt;- вводим уточнение к модели и смотрим результат, прежде всего графическую реализацию модели (граф переходов между состояниями).&lt;p&gt;Визуализацию графа переходов (марковскую цепь) можно реализовать на технологии «Диаграммы как код» (DaC, Diagram-as-Code): Graphviz (dot), Mermaid, PlantUML и даже RDF Grapher (Resource Description Framework). Публикацию результата предлагается осуществлять непосредственно в github (или githubPages), что потребует учетной записи (бесплатно для создания публичных репозитариев). Для просмотра публичного репозитария учетной записи не требуется. Если используем github, то в качестве DaC логичнее выбрать Mermaid (имеется встроенная поддержка, см. «&lt;a href=https://habr.com/ru/articles/652867/ rel=&#34;noopener nofollow&#34;&gt;&lt;strong&gt;Рисуем диаграммы Mermaid.js в README-файлах GitHub&lt;/strong&gt;&lt;/a&gt;»). Для просмотра и редактирования Mermaid также можно использовать online-сервис &lt;a href=https://mermaid.live/ rel=&#34;noopener nofollow&#34;&gt;&lt;strong&gt;mermaid.live&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;.&lt;/strong&gt;&lt;p&gt;В статье приведены принципы, ключевые описания, выводы, но на объемную «текстовку» промптов и их результатов будут даны ссылки на проект на github.&lt;h3&gt;1 Трёхпозиционная модель (модель 3/2)&lt;/h3&gt;&lt;h4&gt;1.1 Базовый промпт «Трёхпозиционная модель»&lt;/h4&gt;&lt;p&gt;В [2] на &amp;#34;Рис. 1 Простейшая модель надежности отказоустойчивого кластера из двух узлов (дублированная группа)» была приведена классическая (упрощенная) модель кластера. Задача базового промпта – воспроизвести модель эту модель.&lt;p&gt;Схема (граф) продублирована на рис.1 в DaC Mermaid. Напомним, что это случай (тип модели): нагруженный резерв (2* λ), одна ремонтная бригада (1* µ), без учета необнаруженного отказа и без failover &amp;amp; failback.&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/36f/fc7/cda/36ffc7cda3949d160ca05d1fb3e8c861.png alt=&#34;Рис. 1. Граф Трехпозиционной модели двухузлового кластера&#34; title=&#34;Рис. 1. Граф Трехпозиционной модели двухузлового кластера&#34; width=441 height=141 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/36f/fc7/cda/36ffc7cda3949d160ca05d1fb3e8c861.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/36f/fc7/cda/36ffc7cda3949d160ca05d1fb3e8c861.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Рис. 1. Граф Трехпозиционной модели двухузлового кластера&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Код mermaid:&lt;p&gt;```mermaid&lt;p&gt;flowchart LR&lt;p&gt;    S2((S2))&lt;p&gt;    S1((S1))&lt;p&gt;    S0_fail([S0_fail])&lt;p&gt;    S2 --&amp;gt;|&amp;#34;2λ&amp;#34;| S1&lt;p&gt;    S1 --&amp;gt;|&amp;#34;μ&amp;#34;| S2&lt;p&gt;    S1 --&amp;gt;|&amp;#34;λ&amp;#34;| S0_fail&lt;p&gt;    S0_fail --&amp;gt;|&amp;#34;μ&amp;#34;| S1&lt;p&gt;```&lt;p&gt;Примеры также можно смотреть (редактировать) на &lt;a href=https://mermaid.live/edit#pako:eNplkMtOhDAUhl-FnBWTMKSUItCFG13qRnZaYxoolwTaSS3RkeHJ9BnmmeyUuXjpql_-fv9pzgSlqgRQqHv1VrZcG-_ugWkmPXsK7PsFXq3OHFmOfjB6qXnX-0_Hy7NLLra3Xl_vGOD9J4OdtS89x2T_5QL8P1iMpfbPuN-uK4UAGt1VQI0eRQCD0AM_IEwHk4FpxSAYUHutRM3H3jBgcrbahstHpYaTqdXYtEBr3r9aGjcVN-K2443m5yd8NKrYyvLEouqM0vfLFt0ybY2QldA3apQGaITcGKATvFvK0hCRJMUJInGcIXIVwBYoicKU5HmakRzHKEb5HMCH-xgKszSZvwGYaYHe rel=&#34;noopener nofollow&#34;&gt;&lt;strong&gt;mermaid.live&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;.&lt;/strong&gt;&lt;p&gt;Работоспособные состояния:&lt;p&gt;S2 — оба узла работоспособны, отказов нет;&lt;p&gt;S1 — один узел работоспособен, второй узел отказал или находится в ремонте.&lt;p&gt;Неработоспособное состояние:&lt;p&gt;S0_fail — оба узла отказали, кластер неработоспособен.&lt;p&gt;хотя ИИ его может построить по описанию самостоятельно.&lt;p&gt;Хотя LLM способны генерировать Mermaid-код по описанию (языком человека), в состав промпт был добавлен код mermaid (граф). Рекомендуется предоставлять явный шаблон для обеспечения воспроизводимости и корректности синтаксиса, хотя бы в стартовом промпт – это снизит число итераций (не потребует корректирующих синтаксис промптов).&lt;p&gt;Первый блок кода mermaid определяет легенду: работоспособные состояния – отображать кругом (двойные скобки «((»), а неработоспособные – овалом («([»). Далее показаны переходы с подписью интенсивностей.&lt;p&gt;Базовый промпт (Исходная модель базового промпта три состояния двухузлового кластера размещен в &lt;strong&gt;m3-prompt_full.md&lt;/strong&gt;.&lt;p&gt;Его задача задать основу модели и правила оформления результата. Он задает формат DaC, как располагать базовые элементы (узлы графа S0, S1, S2_fail), формат (графической) легенды. Так как состояний у модели (графа) – три, то в названии &lt;strong&gt;«&lt;/strong&gt;Трёхпозиционная&lt;strong&gt; модель».&lt;/strong&gt;&lt;p&gt;Предлагается использовать оба варианта представления формул.&lt;p&gt;Вариант 1 «Формулы записывай линейным текстом с использованием Unicode-символов» обусловлен:&lt;p&gt;а) GitHub Markdown и другие понимают не весь перечень команд LaTeX-обертки (т.е. будут ошибки на станице github при рендеренге LaTeX);&lt;p&gt;б) перенос в другую систему публикации потребует поддержки LaTeX, точнее всего набора команд LaTeX-обертки, сгенерированного конкретной ИИ – моделью (LLM)&lt;p&gt;Т.е. могут возникнуть сложности при переносе содержания LaTeX для публикации на иных ресурсах (даже если они имеют поддержку LaTeX).&lt;p&gt;Вариант 2 «Оформи ответ в режиме GitHub Markdown, формулы в LaTeX-обертке» - как дублирующий вариант.&lt;p&gt;Для прямого копирования ответа в github markdown некоторым моделям ИИ (например, perplexity.ai) нужно явно указывать, чтобы ИИ представил ответ в формате, совместимом с github mermaid, а при использовании LaTeX формулы обёртывал в $...$ и $$...$$.&lt;p&gt;Более того:&lt;p&gt;- проблема 1: GitHub Markdown некорректно отображает многострочные формулы с &lt;code&gt;\\&lt;/code&gt; и &lt;code&gt;+&lt;/code&gt; на новых строках внутри одного &lt;code&gt;$$...$$&lt;/code&gt;. Решение — записывать всю формулу в одну строку без переносов.&lt;p&gt;- проблема 2: GitHub Markdown некорректно отображает длинные формулы с &lt;code&gt;\cdot&lt;/code&gt;, &lt;code&gt;\left(&lt;/code&gt;, &lt;code&gt;\right)&lt;/code&gt; и разрывами строк внутри &lt;code&gt;$$...$$&lt;/code&gt;. Поэтому ИИ нужно также указывать чтобы все длинные формулы были разбиты на короткие однострочные выражения, без &lt;code&gt;\left(&lt;/code&gt;, &lt;code&gt;\right)&lt;/code&gt;, без &lt;code&gt;\cdot&lt;/code&gt; внутри дробей, и без переносов внутри &lt;code&gt;$$...$$&lt;/code&gt;.&lt;p&gt;Подобное устраняется новым запросом с указанием неверного отображения информации в ответе.&lt;h4&gt;1.2 Результат базового промпт&lt;/h4&gt;&lt;p&gt;Выполнение ИИ – моделью (LLM) базового промпта должно выдать результат (perplexity.ai) с эталонным, см. &lt;strong&gt;m3-report.md&lt;/strong&gt;&lt;p&gt;Формула Кг (стационарный коэффициент готовности, Unicode):&lt;p&gt;Кг = (μ² + 2λμ) / (μ² + 2λμ + 2λ²)&lt;p&gt;Или LaTeX:&lt;p&gt;Для сравнения см. формулу в [3]&lt;p&gt;Пример расчета Кг. Для данных:&lt;p&gt;- MTBF = 30 000 часов;&lt;p&gt;- MTTR = 24 часа.&lt;p&gt;Получим Кг = 0,999 998 72&lt;h3&gt;2 Пятипозиционная модель (модель 5/2)&lt;/h3&gt;&lt;p&gt;Напомним расширение \ развитие трехпозиционной модели.&lt;p&gt;В [2] на «Рис. 2 ClusterFail Failover / Failback модель надежности отказоустойчивого кластера» была показана &lt;strong&gt;модель 5/2, которая&lt;/strong&gt; включает состояния Failover / Failback. На рис. 2 настоящей статьи продублируем граф с дополнительными обозначениями.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/2c6/2cc/c4b/2c62ccc4b8c767d0f21cc30309f23f66.png alt=&#34;Рис. 2. Граф Пятипозиционной модели двухузлового кластера&#34; title=&#34;Рис. 2. Граф Пятипозиционной модели двухузлового кластера&#34; width=624 height=295 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/2c6/2cc/c4b/2c62ccc4b8c767d0f21cc30309f23f66.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/2c6/2cc/c4b/2c62ccc4b8c767d0f21cc30309f23f66.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;strong&gt;Рис. 2. Граф Пятипозиционной модели двухузлового кластера&lt;/strong&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Модель 5/2 отражает режим Nontransparent recovery, nontransparent repair и добавляет состояния Failover / Failback. В [2] рассмотрены четыре комбинации nontransparent / transparent &amp;amp; recovery / repair.&lt;p&gt;Эту модель (5/2) «промптить» не будем, но она важна как промежуточная для более понятного перехода к восьмипозиционной, в которую (8/2) уже будут включены состояния сбоя (Transient fault) и скрытого отказа (latent).&lt;h3&gt;3 Восьмипозиционная модель (модель 8/2)&lt;/h3&gt;&lt;h4&gt;3.1 Промпт и результат&lt;/h4&gt;&lt;p&gt;Ставится задача: Базовую Трёхпозиционную модель дополнить состояниями (позициями):&lt;p&gt;- Failover / Failback;&lt;p&gt;- состояния сбоя (для каждого узла кластера, т.е. дополнительно два состояния S2_tf и S1_tf)&lt;p&gt;- скрытого отказа (S_latent).&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/3d3/939/1fd/3d39391fdaff7474ff0b3e9586e84a23.png alt=&#34;Рис. 3. Граф Восьмипозиционной модели двухузлового кластера&#34; title=&#34;Рис. 3. Граф Восьмипозиционной модели двухузлового кластера&#34; width=973 height=498 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/3d3/939/1fd/3d39391fdaff7474ff0b3e9586e84a23.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/3d3/939/1fd/3d39391fdaff7474ff0b3e9586e84a23.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;strong&gt;Рис. 3. Граф Восьмипозиционной модели двухузлового кластера&lt;/strong&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Модель 8/2 на &lt;a href=https://mermaid.live/edit#pako:eNqFUUtOwzAQvUo0q1RKqzht0sQLNrBkRXfgKjKN01bkUxmHX9s9ay5TJDhDDsFJsPMPbUU2mZn35s2b8RYWacAAQxilz4sV5UK7viGcJJr8Zpauz6zBoMmRzFEnN_2QriP9rgrmDdIq-CKUuPrNOzplFfWqDepHVLBESEIVneKoeekT44pVx50JRe2eLh5qXMUl3nrThsOLHQEr__IFJ7Ar7fa8V5T8u2Gck8gPBd6YOcXRkfbz_qHlh0HJLdc7Xqqd2qgpPvp7opr3WcDlI_T8oZrRbIi6G6JTG6IzCv-Yb4nVJVo7ffy7vZN6lOMn629fcI4vX6r3NQvnYMCSrwPAgmfMgJjxmKoUtqqTgFixmBHAMgxYSLNIECDJXrZtaHKbpnHdydNsuQIc0uhRZtkmkPteremS05bCkoDxyzRLBGB3UkgA3sILYISckedMxlPXtT1ke9bUgFfAjjtyp7Zp2pY39iaeszfgrZhpqvr-F5qrJ0Y rel=&#34;noopener nofollow&#34;&gt;&lt;strong&gt;mermaid.live&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;.&lt;/strong&gt;&lt;p&gt;Необнаруженный отказ был рассмотрен в разделе «4 Необнаруженный отказ» [3], например, ⴄ = 0,99 означает, что из 100 отказов один будет необнаруженный и обнаружение внешними средствами (внешним контролем) «необнаруженного» отказа (необнаруженного средствами кластера, т.е. внутри кластерным контролем) задерживается на время 1/ θ (в среднем).&lt;p&gt;В модели считается, что в состоянии сбоя узла (Transient fault) кластер находится в состоянии отказа (как и в Приложении 1 к [2]).&lt;p&gt;Проведем расчет Кг для прежних MTBF и MTTR и «малых» Failover / Failback (30 / 90 секунд соответственно). Другие величины:&lt;p&gt;- Среднее время перезапуска после временного сбоя Ttr = 180 секунд;&lt;p&gt;- Среднее время обнаружения скрытого отказа = 8 часов;&lt;p&gt;- Среднее время между временными сбоями одного узла = 1 год (8 760 часов).&lt;p&gt;Кг = 0,999 963&lt;p&gt;Подчеркнём, что расширение модели (учёт дополнительных факторов и введение соответствующих состояний) приводит к &lt;strong&gt;снижению &lt;/strong&gt;коэффициента готовности и других показателей надежности (для базовой модели 3/2 Кг более высокий).&lt;h4&gt;3.2 Другие расширения базовой модели&lt;/h4&gt;&lt;p&gt;Могут быть иные установки / допущения, например, можно считать, что S2_tf – состояние работоспособности. Можно определить, что failover с некой вероятностью может быть неуспешным и тогда дополнительно будет переход из него в S0_fail (в восьмипозиционной модели) или можно предусмотреть особое состояние, например, Единая точка отказа, Single Point of Failure, SPF, см. Приложение 1 к [2], как вариант - отказ «переключателя на резерв» (виртуального).&lt;p&gt;Аналогично failover могут быть неуспешными failback или восстановление после сбоя (T_tr) и тогда должны быть предусмотрены переходы из этих состояний в состояние отказа, например, в S0_fail.&lt;p&gt;Можно добавлять внешние, в том числе, инфраструктурные, составляющие, например, в критерии отказа кластера можно учитывать отказ сетевой инфраструктуры (LAN/WAN), при этом на общей надежностной схеме вычислительная структура кластера будет размещена последовательно сетевой инфраструктуре, т.е. на схеме будут два последовательно соединенных структурных элемента, а в случае распределенного кластера, например, гео-кластера, – четыре: дублированная пара «узел кластера + сеть» (или более сложные конфигурации: магистральный участок + «последняя миля» и т.п.).&lt;p&gt;Может быть иной набор состояний, иная конфигурация кластера и допущения. Вариант модели с узлами активный \ пассивный рассмотрен в [5] и [11] (Тёплый резерв, warm standby).&lt;p&gt;Можно добавлять новые состояния и переходы (наложение сбоев и т.п.), при этом ИИ построит граф модели и выполнит сложный расчет (расчет сложной модели из десятков состояний). Если ИИ-модели дать технический контекст (реальное оборудование \ конфигурацию), то она может обосновать и предложить разные варианты модели надежности, а исследователю останется только выбрать из предложенных подходящий. &lt;h4&gt;3.3 Server vs Server&lt;/h4&gt;&lt;p&gt;Статьи про надёжность могут писать ИТ-шники, а могут математики. При этом будет различаться терминология, например, «Server». Математики в статьях по теории «Систем массового обслуживания», СМО (например, [9]) в части моделирования вычислительных систем под сервер вводить «Server», например, «Server vacation», это Ремонтник (а не вычислительный сервер), например, «Отдых ремонтника» (repairman). В такой трактовке Отказ сервера – это Отказ Ремонтника. В этой конкретной статье «Server» (Сервер) — это не узел вычислительного кластера, а ремонтный прибор (ремонтник / ремонтная бригада), который восстанавливает отказавшие рабочие элементы системы. Возникает классическая путаница терминов теории СМО: авторы рассматривают задачу о ремонте машин (Machine Repair Problem). В ней «клиентами» (заявками в очереди) являются отказавшие узлы кластера (units / machines) а «сервером» (server) называют того, кто их обслуживает (ремонтирует). Авторы используют слова Server и Repairman как синонимы.&lt;p&gt;Вариант модели, когда выделены разные состояния для ремонтной бригады (vacation, busy, broken) показан в [9], «Ремонтник также может отказать и потребовать ремонта» («The repairman may also break down and require repair»). Смотрится странно, когда первую сломанную ремонтную бригаду начинает чинить вторая ремонтная бригада с такой-то интенсивностью. Это к вопросу разных взглядов на процесс ИТ-шника и математика.&lt;p&gt;Полагаю, что подобное (как «ремонт сломанной ремонтной бригады», так и заболел, ушел в отпуск и т.п.) - неоправданное усложнение задачи, т.к. подобное можно заложить (учесть) в μ (Repair rate, интенсивность ремонта, 1/MTTR), тем более с учетом редких отказов (узлы кластера - высоконадёжные элементы). Условные «Иванов и Петров» могут заболеть, но ремонтная бригада – нет (есть замещение и т.п.).&lt;h3&gt;4 Двенадцатипозиционная модель (модель 12/3)&lt;/h3&gt;&lt;h4&gt;4.1 Полный промпт и результат&lt;/h4&gt;&lt;p&gt;Рассмотрим трехузловый кластер: промпт &lt;strong&gt;prompt12_full.md, &lt;/strong&gt;результат: &lt;strong&gt;report12_full.md.&lt;/strong&gt;&lt;p&gt;Граф показан на рис. 4., в двенадцатипозиционной модели добавлен узел и соответствующие новому узлу три состояния: сбоя, failover (переконфигурирование) и failback (переключение после восстановления узла). &lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/601/eeb/79d/601eeb79ddc68b88fb50ad044803361c.png alt=&#34;Рис. 4. Граф Двенадцатипозиционной модели трехузлового кластера&#34; title=&#34;Рис. 4. Граф Двенадцатипозиционной модели трехузлового кластера&#34; width=973 height=406 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/601/eeb/79d/601eeb79ddc68b88fb50ad044803361c.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/601/eeb/79d/601eeb79ddc68b88fb50ad044803361c.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;strong&gt;Рис. 4. Граф Двенадцатипозиционной модели трехузлового кластера&lt;/strong&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Модель 12/3 на &lt;a href=https://mermaid.live/edit#pako:eNqFks1ygjAQx1-F2ZPOqEOCH8Chl_bYU721OEwqQZ0iOGnol3rvuS9jZ9pn8CH6JA0QIFEYvZjd_eW_u_-whXkSUHAhjJLX-ZIwbtzeecyLDfGbWp3O1Op2qxiLGCsxEjFSYtMPySrqPMjDrKrUij4PRT37mym6RRbrWVRkkZatqn5EOI25AOSpsVs2RvJCWd6zDLTOCoEbiBPykcyfKjILtHkVAp0Q9UxGv3-188A6_vicebArTNEcksjxtyLaJI4HqVBO3gR1kPH3-WUcD90cln41uFT3reSyC_jU9JL7zsvFY2sTYkngekmsLomblsRtEnJJfL6kAl1YElftFK3seRo-hRYbUMO3oKO53vl7oZKqzECqGajJDNSicGHNGpSm1Y-j13_lHGc2oEu76c9U6J-o1gT0YMFWAbicpbQHa8rWJAthm1U94Eu6ph644hjQkKQR98CL9-LahsT3SbIub7IkXSzBDUn0LKJ0E4i9b1ZkwUiN0Dig7DpJYw7uxMklwN3CG7gIjQfOeGhNbHvkoJGDJz14B3dsD-zJyDRH2LGcoTPe9-Aj72lm-f0_MuSpYg rel=&#34;noopener nofollow&#34;&gt;&lt;strong&gt;mermaid.live&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;.&lt;/strong&gt;&lt;p&gt;Кг = 0,999 947&lt;p&gt;Аналогичным образом можно увеличивать кратность резерва.&lt;h4&gt;4.2 Кумулятивный запрос&lt;/h4&gt;&lt;p&gt;Выше были показаны полные промпты. Однако можно использовать кумулятивный запрос (cumulative - «накопительный»), т.е. на основе Базового промпта (такого-то) сделай … и далее второй промпт с уточнением новой конфигурации.&lt;p&gt;Эксперимент. Выбираем новую ИИ-модель, например, deepseek (ранее использовал perplexity). Ставим задачу исполнить &lt;strong&gt;prompt8_full.md&lt;/strong&gt;&lt;p&gt;Смотрим и видим, что в ответе проблемы с LaTeX. Даем технический промпт – доработку:&lt;p&gt;&lt;em&gt;Ты не учел «Важные требования к LaTeX-формулам для GitHub Markdown:». При копировании и вставке в GitHub не отображается LaTeX, т.е. нужно добавить $$ … $$ и для inline-формул использовать $...$. Исправь ответ.&lt;/em&gt;&lt;p&gt;Смотрим результат report8_full_deepseek.md и сверяем с report8_full.md. Убеждаемся, что «все хорошо». Далее даем на выполнение кумулятивный промпт:&lt;p&gt;&lt;em&gt;Составь модель и выполни расчет Двенадцатипозиционной модели трехузлового кластера (модель 12/3).&lt;/em&gt;&lt;p&gt;&lt;em&gt;Дополни Восьмипозиционную модель двухузлового кластера третьим узлом, предусмотри 12 состояний, из них новые:&lt;/em&gt;&lt;p&gt;&lt;em&gt;- S3 — три узла работоспособны, отказов нет;&lt;/em&gt;&lt;p&gt;&lt;em&gt;- S3_tf — временный сбой одного из трех узлов, два узла работоспособны, но считаем, что кластер в состоянии отказа (на время перезагрузки после сбоя);&lt;/em&gt;&lt;p&gt;&lt;em&gt;-  S3_failover при переходе из S3 в S2 (переконфигурирование) &lt;/em&gt;&lt;p&gt;&lt;em&gt;- S2_failback при переходе из S2 в S3&lt;/em&gt;&lt;p&gt;&lt;em&gt;Требования к оформлению оставь прежними (как и для Восьмипозиционной модели).&lt;/em&gt;&lt;p&gt;&lt;em&gt;Для примера расчета используй те же значения, что и для Восьмипозиционной модели. &lt;/em&gt;&lt;p&gt;Сверяем прежний report12_full.md (на основе полного промпт prompt12_full.md) с полученным report12_cum_deepseek.md. Убеждаемся, что «все хорошо».&lt;h3&gt;5 Краткий анализ полученных моделей&lt;/h3&gt;&lt;p&gt;Сравним Кг моделей m3, m8, m12.&lt;p&gt;Сравнение Кг показывает, что при усложнении модели (повышение адекватности модели) значение Кг начинает снижаться и «заветные» для высоконадёжного кластера «пять девяток», получаемые в явно упрощенных моделях, начинают снижать цифру (показатель).&lt;p&gt;- модель 3/2 (базовая): 0,999 998 7, ожидаемый простой за год ~40 с.&lt;p&gt;- модель 8/2 (расширенная): 0,999 963, ожидаемый простой за год ~19 мин.&lt;p&gt;- модель 12/3 (расширенная): 0,999 947, ожидаемый простой за год ~28 мин.&lt;p&gt;Некоторые выводы:&lt;p&gt;1 Базовая модель 3/2 имеет наивысшую готовность (~40 секунд простоя в год), так как не учитывает transient faults, latent failures, failover и failback.&lt;p&gt;2 Модель 8/2 имеет готовность ниже на порядок (~19 минут простоя в год) из-за дополнительных неработоспособных состояний.&lt;p&gt;3 Модель 12/3 имеет ещё более низкую готовность (~28 минут простоя в год), чем 8/2, при тех же параметрах.&lt;p&gt;3.1 Феномен «двухузловая архитектура может быть предпочтительнее трёхузловой»: при η=0.99 и Tdetect=8 ч двухузловая расширенная модель (8/2) обеспечивает лучшую готовность, чем трёхузловая (12/3).&lt;p&gt;3.2 Причина: в 12/3 больше комбинаций скрытых отказов (3 узла вместо 2), что увеличивает вклад S_latent и связанных состояний в неготовность.&lt;p&gt;3.3 Вывод: Для систем с неполным диагностическим охватом (η&amp;lt;0.999) и длительным временем внешнего контроля (T_detect&amp;gt;1 ч) двухузловая архитектура может быть предпочтительнее трёхузловой с точки зрения стационарной готовности, даже при наличии одного дополнительного резервного узла.&lt;p&gt;Сравнение Кг и подробный анализ феномена (с анализом подобных исследований) рассмотрен в analysis.md.&lt;h3&gt;Заключение&lt;/h3&gt;&lt;p&gt;ИИ меняет привычный мир. Он позволяет заменить специализированные программы (калькуляторы) расчета надёжности: частично в части построения модели и на 100% в части расчета имеющейся модели, включая, формирование по графу системы уравнений, ее решение, подстановку значений и т.п.&lt;p&gt;Для составления самой модели надежности (в данном случае марковской цепи) ему на вход предлагается подавать комплект шаблонов (дать «точку опоры»), а результат (его ответ) сверить с эталоном. При успешном прохождении предварительного тестирования в виде следующего промпт подается скорректированный типовой промпт (корректировка полного типового в целях уточнения постановки задачи) или подается кумулятивный, который описывает отличия от базовой модели.&lt;p&gt;Более того, вначале можно ИИ поставить задачу формализовать модель надежности (на основе описания прикладной темы, конфигурации реальной системы), потом задать ему составить полный или кумулятивный промпт, что позволит обеспечить «обратную связь».&lt;p&gt;Для повышения доверия: один и тот же промпт задать на исполнение разным моделям ИИ (LLM), а отдельной группе ИИ – сравнить полученные результаты.&lt;p&gt;Рекомендуется в промпт добавлять «Приведи замечания к промпт».&lt;p&gt;Приведенные выше подходы (рекомендации по формализации запросов к ИИ, Промпт-инжиниринг), модели и примеры промптов, включая требования к визуализации модели (графу) и оформлению (в том числе формул), позволяют облегчить старт моделирования надежности / отказоустойчивости резервируемых структур / кластеров. Пока подобные расчеты показателей надежности являются экзотикой для отказоустойчивых кластерных структур и сетевой инфраструктуры.&lt;p&gt;Известно, что большие языковые модели (LLM) подвержены галлюцинациям и есть целое направление «промпт-инжиниринг», который формулирует приёмы запросов и верификацию ответов, и об этом написано уже много &lt;a href=https://habr.com/ru/companies/bhv_publishing/articles/1079364/ rel=&#34;noopener nofollow&#34;&gt;&lt;strong&gt;книг&lt;/strong&gt;&lt;/a&gt;.&lt;p&gt;Однако, использование ИИ-модели для расчета надёжности сложных систем является наиболее легким стартом и эффективным инструментом, включающим мощнейший мыслящий механизм (AI). Среди простых советов его использования можно привести: на старте исследования использование типовых промпт с эталоном – ответом (дать ИИ «Точку опоры»); ответы по дальнейшим запросам сверять разными ИИ-моделями («Доверяй, но проверяй»); в сам промпт вставлять задачу оценки полноты и качества (неточности, противоречия и т.п.) текущего промпт (с описанием модели) и самой ИИ-модели скорректировать промпт (Обратная связь). Идеальные ответы от ИИ бывают не всегда, иногда нужно ИИ поправлять, например, дать корректирующий промпт. Однако в отличие от готовых программ не нужно вникать в часто сложную (непонятную) логику программы, странности ее интерфейса, а можно задать в промпте требования к формату представления ответа и запрашивать любой уровень детализации (пояснений) полученного ответа. Обсуждение модели, ее модификация, поиск подобных исследований и все это «человеческом» языком и все это бесплатно и на высоком инженерном уровне. В исследовании приведены артефакты от ИИ perplexity и deepseek, но при тестировании использовались и другие ИИ-модели.       &lt;p&gt;Одним из направлений развития концепта может служить «семантическая версия», когда вначале создается базовая онтология надежности кластера, а потом на ее основе строится моделирование конкретной конфигурации кластера. Это позволит систематизировано и на формальном языке знаний фиксировать параметры модели. Концепт такой формализации (на основе строгого формализма) кроме онтологии позволит использовать штатные механизмы \ инструменты \ возможности Linked Data, включая языки запросов и верификации, reasoned etc. Кроме того, формализации модели в RDF (Resource Description Framework, «среда описания ресурса») является также DaC-технологией, где с помощью фильтров может быть получено привычное отображение марковской цепи / графа, т.е. управление визуализацией (&lt;a href=https://www.ldf.fi/service/rdf-grapher rel=&#34;noopener nofollow&#34;&gt;&lt;strong&gt;RDF grapher&lt;/strong&gt;&lt;/a&gt;) с использованием фильтров видимости: «тип сущности», «предикат» и др.&lt;h3&gt;Материалы&lt;/h3&gt;&lt;p&gt;Список файлов &lt;a href=https://github.com/avers52/documentation/blob/main/habr/article3/cluster_dependability/readme.md rel=&#34;noopener nofollow&#34;&gt;&lt;strong&gt;проекта cluster_dependability&lt;/strong&gt;&lt;/a&gt; (число состояний модели / число узлов кластера),&lt;p&gt;- Модель 3/2:&lt;p&gt;-- промпт полный prompt3_full.md&lt;p&gt;-- результат report3_full.md&lt;p&gt;- Модель 8/2:&lt;p&gt;-- промпт полный prompt8_full.md&lt;p&gt;-- результат report8_full.md&lt;p&gt;-- результат (deepseek) report8_full_deepseek.md&lt;p&gt;- Модель 12/3:&lt;p&gt;-- промпт полный prompt12_full.md&lt;p&gt;-- результат report12_full.md&lt;p&gt;-- промпт кумулятивный и результат (deepseek) report12_cum_deepseek.md&lt;p&gt;- Анализ&lt;p&gt;-- Анализ Кг и феномена analysis.md&lt;h3&gt;Источники:&lt;/h3&gt;&lt;p&gt;[1] &lt;a href=https://habr.com/ru/articles/1072108/ rel=&#34;noopener nofollow&#34;&gt;Надёжность, устойчивость, доступность&lt;/a&gt;&lt;p&gt;[2] &lt;a href=https://habr.com/ru/articles/1075024/ rel=&#34;noopener nofollow&#34;&gt;Базовые модели надежности отказоустойчивого кластера&lt;/a&gt;&lt;p&gt;[3] &lt;a href=https://cyberleninka.ru/article/n/podhod-k-otsenke-nadezhnosti-klasternyh-struktur rel=&#34;noopener nofollow&#34;&gt;Подход к оценке надежности кластерных структур. Научные ведомости 2010 № 13 (84). Выпуск 15/1 (УДК 519.223.42)&lt;/a&gt;&lt;p&gt;[4] &lt;a href=https://www.electronics.ru/journal/article/75 rel=&#34;noopener nofollow&#34;&gt;Резервный центр обработки данных. Оценка надежности. Электроника НТБ. Выпуск 4/20&lt;/a&gt;&lt;p&gt;[5] &lt;a href=https://bugtraq.ru/library/internals/.keep/dualnodecluster.pdf rel=&#34;noopener nofollow&#34;&gt;МОДЕЛЬ НАДЕЖНОСТИ ДВУХУЗЛОВОГО КЛАСТЕРА ВЫСОКОЙ ГОТОВНОСТИ&lt;/a&gt;&lt;p&gt;[6] &lt;a href=&#34;https://vital.lib.tsu.ru/vital/access/services/Download/vtls:000480794/SOURCE1?ysclid=mttopk3gpv167253474&#34; rel=&#34;noopener nofollow&#34;&gt;ТЕОРИЯ СЛУЧАЙНЫХ ПРОЦЕССОВ. Часть 2. Марковские процессы&lt;/a&gt;&lt;p&gt;[7] &lt;a href=https://habr.com/ru/articles/455762/ rel=&#34;noopener nofollow&#34;&gt;Краткое введение в цепи Маркова&lt;/a&gt;&lt;p&gt;[8] &lt;a href=https://ntrs.nasa.gov/api/citations/20020050518/downloads/20020050518.pdf rel=&#34;noopener nofollow&#34;&gt;Boyd M.A. &amp;#34;An Introduction to Markov Modeling: Concepts and Uses&amp;#34;. NASA, 1998&lt;/a&gt;.&lt;p&gt;&lt;em&gt;Руководство по марковскому моделированию с примерами imperfect coverage&lt;/em&gt;&lt;p&gt;[9] &lt;a href=https://link.springer.com/article/10.1007/s40092-016-0180-8 rel=&#34;noopener nofollow&#34;&gt;Jain M., Meena R.K. &amp;#34;Fault tolerant system with imperfect coverage, reboot and server vacation&amp;#34;. Journal of Industrial Engineering International, 2017&lt;/a&gt;&lt;p&gt;&lt;em&gt;Модель fault-tolerant системы с imperfect coverage и reboot&lt;/em&gt;&lt;p&gt;[10] &lt;a href=https://www.sciencedirect.com/science/article/pii/S0307904X12003800 rel=&#34;noopener nofollow&#34;&gt;Comparative analysis of the machine repair Problem with imperfect coverage and service pressure condition&lt;/a&gt;&lt;p&gt;[11] &lt;a href=https://www.sciencedirect.com/science/article/pii/S0307904X12003800 rel=&#34;noopener nofollow&#34;&gt;Comparative analysis of the machine repair Problem with imperfect coverage and service pressure condition&lt;/a&gt;&lt;p&gt;&lt;em&gt;Machine Repair Problem (MRP) — Задача о ремонте машин.  M/M/R – Markovian/ Markovian/ Repairmen.&lt;/em&gt;&lt;p&gt;&lt;strong&gt;On-Line ресурсы mermaid:&lt;/strong&gt;&lt;p&gt;&lt;a href=https://mermaid.live rel=&#34;noopener nofollow&#34;&gt;https://mermaid.live&lt;/a&gt;&lt;p&gt;&lt;a href=https://toolact.com/ru/mermaid rel=&#34;noopener nofollow&#34;&gt;https://toolact.com/ru/mermaid&lt;/a&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <author>avers_52</author>
      <guid>https://habr.com/ru/articles/1082196/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082196</guid>
      <pubDate>Mon, 14 Sep 2026 18:25:42 +0000</pubDate>
    </item>
    <item>
      <title>Я сисадмин, а не разработчик. Полгода я собирала симулятор ПК на телефон, чтобы научить людей отличать архив от папки</title>
      <link>https://habr.com/ru/articles/1082190/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082190</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;Привет, Хабр! Сразу расставлю точки над «i»: я не программист. По образованию я компьютерщик (первый диплом «Компьютерные сети») и специалист по прикладной информатике (диплом в вузе вообще был по автоматизации на 1С).&lt;p&gt;Работаю системным администратором: начинала в небольшой провинциальной компании, где ты многорукий Шива, сейчас администрирую в компании покрупнее, и нас таких многоруких уже двое. Как устроены компьютеры, сети, операционные системы и железо — я понимаю в теории и на практике. Но мобильные приложения (да и десктопные) на Flutter и Dart никогда раньше не делала. Максимум — скрипты на Python и PowerShell для решения прикладных задач.&lt;p&gt;Этот пост — история о наболевшей сисадминской рутине, долгой упорной сборке по вечерам и тренажёре‑симуляторе «Проводник», который на днях вышел в RuStore. Сразу предупрежу: описание получилось подробным. За полгода работы над проектом мне стало казаться важным буквально всё, поэтому выделить главное оказалось неожиданно сложно.&lt;p&gt;В офисе сисадмин ближе всего к людям. Ты можешь бесконечно настраивать отказоустойчивые бэкапы, связь между офисами и права доступа на серверах, но для коллег твоя работа выглядит иначе: когда у кого‑то «пропал интернет», отошел кабель от системника или «исчез документ», бегут лично к тебе. С годами вырабатывается профессиональное наблюдение: техника сама по себе сбоит не так уж часто. Намного чаще людей охватывает ступор.&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ступор перед словом «Ошибка»:&lt;/strong&gt; Посреди экрана выскакивает стандартное предупреждение: «Введите дату в формате ДД.ММ.ГГГГ». Человек даже не пытается прочитать текст. Мозг блокируется незнакомым окном, руки опускаются: «Оно сломалось, сделайте что‑нибудь!».&lt;li&gt;&lt;p&gt;&lt;strong&gt;Архивный бермудский треугольник:&lt;/strong&gt; Человек получает по почте &lt;code&gt;.zip&lt;/code&gt;, дважды кликает по вложенному файлу, полдня вносит правки в таблицу, жмет «Сохранить», закрывает… а назавтра в слезах: «Куда пропал отчет?!». А документ остался во временной системной папке Temp, которую Windows благополучно вычистила. Объяснить на словах, почему архив — это сжатая коробка, а не обычная папка, получается далеко не с первого раза.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Страх нажать «не туда»:&lt;/strong&gt; На рабочем компьютере крутятся важные файлы, 1С, клиент‑банки. Пользователь панически боится тыкать в кнопки: «А вдруг я сейчас нажму правой кнопкой мыши и всё сломаю?».&lt;/ul&gt;&lt;p&gt;Главная идея проекта очень простая: большинство людей учатся работать за компьютером либо методом проб и ошибок на рабочем месте, либо по видеоурокам. Симулятор позволяет ошибаться безопасно и сразу видеть последствия своих действий, не рискуя реальными файлами, настройками системы и рабочими документами.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/396/5f1/21b/3965f121b5d028ff06015451ba7f0f7a.jpg alt=&#34;Интерфейс программы на десктопе&#34; title=&#34;Интерфейс программы на десктопе&#34; width=1919 height=1049 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/396/5f1/21b/3965f121b5d028ff06015451ba7f0f7a.jpg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/396/5f1/21b/3965f121b5d028ff06015451ba7f0f7a.jpg 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Интерфейс программы на десктопе&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;h3&gt;Как устроен симулятор изнутри: честная виртуальная система&lt;/h3&gt;&lt;p&gt;Чтобы симулятор действительно учил, а не водил пользователя «по рельсам», пришлось с нуля воссоздать в памяти логику операционной системы:&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;Компонент&lt;/strong&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;Что внутри&lt;/strong&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;Оболочка обучения&lt;/strong&gt;&lt;td&gt;&lt;p align=left&gt;Курсы, квесты ситуаций, квизы, профиль, магазин тем&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;Ядро симулятора&lt;/strong&gt;&lt;td&gt;&lt;p align=left&gt;Виртуальная файловая система, системные события, оконный менеджер (перетаскивание, фокус), подсказки&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;Окружения рабочего стола&lt;/strong&gt;&lt;td&gt;&lt;p align=left&gt;Эмуляция интерфейсов Windows 11, Windows 10 и Astra Linux&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;Экосистема программ&lt;/strong&gt;&lt;td&gt;&lt;p align=left&gt;20+ утилит: проводник, браузер со сценариями, диспетчер задач, параметры, терминал, офис, антивирус, периферия&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;Интерактивные мини‑игры&lt;/strong&gt;&lt;td&gt;&lt;p align=left&gt;Детектор фишинга, кабели ПК, сортировка файлов, спринт горячих клавиш&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/963/4cf/c0d/9634cfc0d8ae7c00cb0b9b72d3a7fe52.png alt=&#34;Пример урока&#34; title=&#34;Пример урока&#34; width=1919 height=1055 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/963/4cf/c0d/9634cfc0d8ae7c00cb0b9b72d3a7fe52.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/963/4cf/c0d/9634cfc0d8ae7c00cb0b9b72d3a7fe52.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Пример урока&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;h4&gt;Честная файловая система в памяти&lt;/h4&gt;&lt;p&gt;В оперативной памяти крутится реальная структура файлов:&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Диски и пути:&lt;/strong&gt; Для Windows поддерживаются диски &lt;code&gt;C:\&lt;/code&gt;, &lt;code&gt;D:\&lt;/code&gt;, папки «Рабочий стол», «Документы», «Загрузки». Для Astra Linux — домашний каталог &lt;code&gt;/home/user&lt;/code&gt;.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Файлы и расширения:&lt;/strong&gt; У каждого файла есть имя, размер и расширение (&lt;code&gt;.docx&lt;/code&gt;, &lt;code&gt;.xlsx&lt;/code&gt;, &lt;code&gt;.zip&lt;/code&gt;, &lt;code&gt;.exe&lt;/code&gt;, &lt;code&gt;.pdf&lt;/code&gt;, &lt;code&gt;.txt&lt;/code&gt;, &lt;code&gt;.mp3&lt;/code&gt;). Если стереть расширение &lt;code&gt;.docx&lt;/code&gt;, файл честно теряет привязку к программе, а симулятор наглядно показывает, почему он перестал открываться.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Полноценная Корзина:&lt;/strong&gt; При удалении файлы перемещаются в Корзину с сохранением исходного пути. Их можно восстановить на прежнее место или очистить Корзину насовсем.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Архивация ZIP:&lt;/strong&gt; Можно нажать правой кнопкой по папке, выбрать «Сжать в ZIP‑архив», а затем реально распаковать его в обычную директорию.&lt;/ul&gt;&lt;h4&gt;Окна и рабочий стол&lt;/h4&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Любое окно свободно перетаскивается пальцем за заголовок, сворачивается в панель задач, разворачивается и закрывается.&lt;li&gt;&lt;p&gt;Нажатие на фоновое окно выдвигает его на передний план.&lt;li&gt;&lt;p&gt;Полноценная панель задач с запущенными приложениями, часами, регулятором громкости, переключением языка (RU/EN) и меню «Пуск».&lt;li&gt;&lt;p&gt;Окна подтверждения прав администратора (UAC) с затемнением экрана — чтобы приучить человека читать, что именно пытается запуститься в системе.&lt;/ul&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/632/fd4/a66/632fd4a66072be2072558c2392499fe2.jpg alt=&#34;Интерфейс симулятора при прохождении урока&#34; title=&#34;Интерфейс симулятора при прохождении урока&#34; width=1593 height=882 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/632/fd4/a66/632fd4a66072be2072558c2392499fe2.jpg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/632/fd4/a66/632fd4a66072be2072558c2392499fe2.jpg 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Интерфейс симулятора при прохождении урока&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;h3&gt;Программы: здесь не только проводник&lt;/h3&gt;&lt;p&gt;В симуляторе воссоздано более 20 утилит, с которыми офисный сотрудник сталкивается каждый день:&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Проводник (File Explorer):&lt;/strong&gt; Настоящий файловый менеджер. Хлебные крошки в адресной строке (&lt;code&gt;Этот компьютер &amp;gt; Диск C &amp;gt; Документы&lt;/code&gt;), смена вида (плитка или таблица со свойствами), контекстное меню («Создать папку», «Переименовать», «Копировать», «Вставить», «Свойства»), поиск файлов по названию. Поддерживает также сетевые папки.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Диспетчер задач:&lt;/strong&gt; Разделен на вкладки «Процессы» и «Автозагрузка». Процессы симулируют нагрузку на процессор и память. В уроках нужно найти зависшую программу со статусом «Не отвечает» и снять задачу (не задев системные службы), а также отключить лишнее из автозагрузки для ускорения старта ПК.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Параметры Windows / Панель управления Astra Linux:&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Вкладка «Память» — поиск временных файлов и штатная очистка диска вместо опасного ручного удаления системных папок.&lt;li&gt;&lt;p&gt;Вкладка «Приложения» — корректное удаление программ через деинсталлятор вместо перетаскивания ярлыка в корзину.&lt;li&gt;&lt;p&gt;«Центр обновления» — проверка обновлений, загрузка KB‑пакетов и штатный перезапуск системы.&lt;/ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Офисный пакет и просмотрщики:&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Блокнот:&lt;/strong&gt; редактирование текста, меню «Файл &amp;gt; Сохранить как...», выбор кодировок и ассоциации файлов.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Office Writer:&lt;/strong&gt; симулятор текстового редактора для документов &lt;code&gt;.doc&lt;/code&gt;, &lt;code&gt;.docx&lt;/code&gt; и &lt;code&gt;.odt&lt;/code&gt;.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Таблицы:&lt;/strong&gt; просмотр и проверка табличных файлов &lt;code&gt;.xlsx&lt;/code&gt; и &lt;code&gt;.csv&lt;/code&gt;.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Просмотр PDF:&lt;/strong&gt; открытие договоров, счетов и официальных бланков.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Графика:&lt;/strong&gt; просмотр фото и редактор Paint.&lt;/ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Антивирус («Антивирус РФ»):&lt;/strong&gt; Программа с кнопкой полного сканирования, бегущей полосой проверки файлов виртуального диска, отчетом о найденных подозрительных &lt;code&gt;.exe&lt;/code&gt; и возможностью отправить их в карантин или удалить.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Офисная периферия:&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Очередь печати:&lt;/strong&gt; окно принтера, где зависает поврежденный документ и блокирует печать всей компании (учимся очищать очередь).&lt;li&gt;&lt;p&gt;&lt;strong&gt;Сканирование:&lt;/strong&gt; интерфейс сканера с предпросмотром документа и сохранением в систему.&lt;/ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Командная строка и Linux‑терминал:&lt;/strong&gt; Для уроков по отечественной Astra Linux. Поддерживает ввод консольных команд: &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;cd&lt;/code&gt;, &lt;code&gt;dir&lt;/code&gt;, &lt;code&gt;cat&lt;/code&gt;, &lt;code&gt;mkdir&lt;/code&gt;, &lt;code&gt;rm&lt;/code&gt;, &lt;code&gt;ping&lt;/code&gt;, вызов справки и запуск через &lt;code&gt;sudo&lt;/code&gt;.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Установщики программ:&lt;/strong&gt; Пошаговые мастера установки (&lt;code&gt;.exe&lt;/code&gt;, пакеты &lt;code&gt;.msi&lt;/code&gt; и &lt;code&gt;.deb&lt;/code&gt; для Linux) с лицензионными соглашениями, выбором папки и окнами контроля учетных записей (UAC).&lt;li&gt;&lt;p&gt;&lt;strong&gt;Калькулятор и медиаплеер:&lt;/strong&gt; Для воспроизведения звуковых файлов и базовых расчетов.&lt;/ul&gt;&lt;h3&gt;Виртуальный браузер со сценариями из жизни&lt;/h3&gt;&lt;p&gt;Браузер — главная точка входа для мошенников и вирусов, поэтому ему уделено особое внимание. Это изолированный эмулятор со всей привычной начинкой:&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Интерфейс:&lt;/strong&gt; Вкладки (открытие, закрытие, переключение), адресная строка с индикатором безопасности (замочек HTTPS), кнопки навигации («Назад / Вперед / Обновить»), закладки и режим «Инкогнито».&lt;li&gt;&lt;p&gt;&lt;strong&gt;Служебные разделы:&lt;/strong&gt; История просмотров (с кнопкой очистки), менеджер сохраненных паролей, список загрузок и настройки.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Встроенный поисковик:&lt;/strong&gt; Учебный поиск, который процедурно генерирует списки сайтов и краткие ответы под запросы.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Интерактивные учебные сайты и сервисы:&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Почтовый клиент:&lt;/strong&gt; Письма от коллег, спам‑рассылки о «выигрышах 500 000 рублей», фальшивые акты сверки со вложенными вирусами &lt;code&gt;Счет_на_оплату.exe&lt;/code&gt;. Учимся распознавать подвох и отправлять письма в Спам.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Портал Госуслуг&lt;/strong&gt;.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Энциклопедия (Википедия) и социальная сеть «ВОбщении».&lt;/strong&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Фейковые интернет‑магазины&lt;/strong&gt;.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ловушки с подписками&lt;/strong&gt;.&lt;/ul&gt;&lt;/ul&gt;&lt;h3&gt;Симуляция работы сетевой карты и сетевых возможностей ОС&lt;/h3&gt;&lt;p&gt;В симуляторе реализована полноценная модель сетевой карты и сетевого адаптера, позволяющая гибко управлять параметрами подключения виртуального ПК: вручную задавать IP‑адрес, маску подсети, основной шлюз и предпочитаемый DNS‑сервер.&lt;p&gt;Система поддерживает реалистичную модель сетевой адресации как по прямым IP‑адресам, так и по доменным именам и именам хостов. Благодаря этому пользователи могут на практике отрабатывать диагностику и настройку сети, разрешение имен через DNS, подключение к сетевым папкам и взаимодействие с другими узлами точно так же, как в настоящей операционной системе.&lt;h3&gt;«Сценарии реальных ситуаций»: спасаем коллег от паники&lt;/h3&gt;&lt;p&gt;В приложении есть режим «Практические ситуации» — сюжетные квесты с жизненными офисными героями, например:&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;«Порядок на столе» (Менеджер Олег):&lt;/strong&gt; На рабочем столе каша из отчетов за разные месяцы и картинок с котиками. Создаем структуру папок, наводим порядок и удаляем мусор.&lt;li&gt;&lt;p&gt;&lt;strong&gt;«Случайное удаление» (Стажёр Алексей):&lt;/strong&gt; Паника: «Шеф меня убьет, я стёр договор!». Задача — зайти в Корзину, спасти нужный документ и очистить Корзину от остального мусора (а если случайно очистить Корзину вместе с договором — симулятор вовремя остановит).&lt;li&gt;&lt;p&gt;&lt;strong&gt;«Файлы для налоговой» (Главбух Марина):&lt;/strong&gt; Подключить флешку, упаковать отчетность в ZIP‑архив, скопировать на накопитель и безопасно извлечь флешку.&lt;li&gt;&lt;p&gt;&lt;strong&gt;«Странный рецепт» (Дедушка Иван):&lt;/strong&gt; Прислали файл без расширения (&lt;code&gt;.abc&lt;/code&gt;). Учимся смотреть свойства файла и открывать его через «Открыть с помощью...» в Блокноте.&lt;li&gt;&lt;p&gt;&lt;strong&gt;«Завис фотоальбом» (Архивариус Анна):&lt;/strong&gt; Программа зависла со статусом «Не отвечает». Задача — открыть Диспетчер задач и снять &lt;em&gt;только&lt;/em&gt; зависшее приложение, не уронив систему.&lt;/ul&gt;&lt;h3&gt;Магазин обоев и тем оформления&lt;/h3&gt;&lt;p&gt;За прохождение уроков и квестов начисляются внутриигровые монеты. Их можно потратить во встроенном Магазине оформления:&lt;ol&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Темы оформления окон:&lt;/strong&gt; Полностью меняют внешний вид системы (шапки окон, цвет панелей, скругления, рамки и подсветку):&lt;li&gt;&lt;p&gt;&lt;strong&gt;Обои рабочего стола.&lt;/strong&gt;&lt;/ol&gt;&lt;p&gt;Все темы и обои открываются исключительно за заработанные монеты внутри приложения. Никаких обязательных реальных платежей нет.&lt;h3&gt;Как я это собирала: нейросети как напарник и бесконечные тесты&lt;/h3&gt;&lt;p&gt;Так как мобильные приложения я раньше никогда не делала, разработку вела в связке с нейросетями (в основном писала запросы в DeepSeek и Gemini). Но хочу сразу развеять иллюзию про «нажала кнопку — нейросеть сделала готовое приложение»:&lt;ol&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Нейросеть не понимает сути за вас:&lt;/strong&gt; Если просто попросить её «сделай симулятор ПК», получится нерабочая пустышка. Мне приходилось детально продумывать каждый шаг: как должна вести себя система при клике на файл, какие параметры передавать при перетаскивании окна, как реагировать на отключение флешки.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Метод проб и ошибок:&lt;/strong&gt; Я описывала задачу обычными словами, забирала код, собирала проект на телефоне, находила баги (например: окно свернули, а клики сквозь него все равно нажимаются на рабочем столе), возвращалась с текстом ошибки и просила переписать конкретный кусок.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Бесконечное ручное тестирование:&lt;/strong&gt; 95% каждого сценария, кнопки и меню я сотни раз протыкала пальцем на своих устройствах. На всё ушло около полугода (и это только начало).&lt;/ol&gt;&lt;p&gt;Мой сисадминский опыт очень помог: мне не нужно было объяснять самой себе, что такое дерево каталогов, процессы, порты, сетевые папки или права доступа. Я четко понимала, какой результат должен получиться, а нейросети помогли быстро получить рабочий код без многолетнего изучения нового фреймворка.&lt;h3&gt;Что есть сейчас и что дальше&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;В приложении уже доступно:&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;3 курса:&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Файловые системы (30 уроков: от файлов и папок до прав и архивов).&lt;li&gt;&lt;p&gt;Безопасность и интернет (15 уроков: фишинг, спам, надежные пароли).&lt;li&gt;&lt;p&gt;Скорая помощь ПК (15 уроков: диспетчер задач, автозагрузка, очистка диска, обновления).&lt;p&gt;В ближайшем обновлении появятся курсы для начала работы с отечественными Linux для новичков, объясняющими отличия от привычной Windows и базовой работы с ними. В будущем курсы будут появляться и более продвинутые.&lt;/ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Свободный режим&lt;/strong&gt; с чистыми рабочими столами подобными Windows 10, Windows 11 и Astra Linux.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Практические ситуации&lt;/strong&gt; с офисными героями.&lt;li&gt;&lt;p&gt;&lt;strong&gt;4 мини‑игры:&lt;/strong&gt; проверка писем на фишинг, подключение проводов к системному блоку, сортировка файлов на скорость и спринт горячих клавиш.&lt;/ul&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/7f4/8c9/e34/7f48c9e34fe65abad5e3269a8f1affd9.jpg alt=Мини-игра title=Мини-игра width=1919 height=1048 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/7f4/8c9/e34/7f48c9e34fe65abad5e3269a8f1affd9.jpg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/7f4/8c9/e34/7f48c9e34fe65abad5e3269a8f1affd9.jpg 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Мини‑игра&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;h3&gt;Нужен ваш краш‑тест!&lt;/h3&gt;&lt;p&gt;Приложение полностью бесплатное и уже доступно в каталоге.&lt;p&gt;👉 &lt;strong&gt;Ссылка на «Проводник» в RuStore:&lt;/strong&gt; &lt;a href=https://www.rustore.ru/catalog/app/ru.provodnik.app rel=&#34;noopener nofollow&#34;&gt;https://www.rustore.ru/catalog/app/ru.provodnik.app&lt;/a&gt;&lt;p&gt;Огромная просьба:&lt;ol&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Напишите, какие у вас есть предложения и замечания, может просто вопросы по архитектуре.&lt;/strong&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Покажите тем, кто боится техники:&lt;/strong&gt; Если у вас на работе есть начинающие коллеги, а дома — родители, бабушки или дети, дайте им немного попробовать Проводник. Посмотрите со стороны: где они спотыкаются? Что вызывает трудности?&lt;li&gt;&lt;p&gt;&lt;strong&gt;Напишите в комментариях:&lt;/strong&gt; С какими еще типичными проблемами и курьезными ошибками пользователей вы сталкиваетесь на работе? Буду рада добавить новые сценарии в обновления!&lt;/ol&gt;&lt;p&gt;P. S. В первый раз пишу статью. Извините за примитивное оформление. Я старалась)&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <author>sonikdixi</author>
      <guid>https://habr.com/ru/articles/1082190/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082190</guid>
      <pubDate>Mon, 14 Sep 2026 17:45:38 +0000</pubDate>
    </item>
    <item>
      <title>Сколько данных нужно, чтобы обучить модель для стройки?</title>
      <link>https://habr.com/ru/articles/1082180/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082180</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;Если говорить о внедрении систем видеомониторинга на стройку, то здесь стоит отметить важный фактор. Речь идёт об обучении модели непосредственно для внедрения на объект. В этом материале расскажу про данные, классы, разметку, особенности датасета и другие параметры, которые стоит учитывать при масштабировании системы.&lt;p&gt;&lt;strong&gt;Минимальный объём данных, необходимых для работы&lt;/strong&gt;&lt;p&gt;Основываясь на своём опыте, скажу, что для обучения одного класса необходимо примерно 3-4 тысячи размеченных изображений. Это число не будет универсальным минимумом для любой нейросети, это больше рекомендуемый стартовый объём в рамках того подхода, с которым мы работаем.&lt;p&gt;Сложность классов, а также разнообразие условий здесь играют важную роль. Если объект крупный и его легко различить, то нам потребуется одно покрытие. Если же объект маленький и находится на заднем плане, то в таком случае необходимо другое покрытие. Иногда приходится использовать сотни тысяч реальных размеченных кадров, и тогда, как правило, применяется аугментация. Чтобы проверить, готова ли модель к production, нужно прогнать её на тестовой выборке. Если данных много - это хорошо, но важно понимать, представлены ли в этих данных условия, в которых системе предстоит работать.&lt;p&gt;&lt;strong&gt;Различия в моделях при разном количестве размеченных примеров&lt;/strong&gt;&lt;p&gt;Если мы говорим о том, сколько примеров используется для обучения моделей (тысяча, десять тысяч или сто тысяч), то здесь нет универсальной зависимости по процентам качества. Итоговый результат всегда будет зависеть от того, насколько качественная выборка, разметка, какая исходная модель и насколько сложная задача.&lt;p&gt;В случае увеличения объёма важно понимать, какие примеры будут добавлены. Тысячи однотипных изображений могут вообще ничем не помочь, а вот если добавляются новые ракурсы, перекрытия, сложное освещение или редкие ситуации, то это будет намного полезнее. Итог здесь один - вместе с количеством данных нужно смотреть на их разнообразие.&lt;p&gt;&lt;strong&gt;Самые сложные классы для сбора данных&lt;/strong&gt;&lt;p&gt;Есть такие примеры, которые очень тяжело собрать. К ним относятся редкие события. Это задымления, пожары, взрывы и несчастные случаи. Проблема этих примеров заключается в том, что они встречаются очень редко по сравнению с обычными рабочими ситуациями. Поэтому, если мы имеем большой объём данных, это не означает, что мы получили большую выборку нужных нам событий.&lt;p&gt;Иногда заказчики могут присылать специализированные запросы. Некоторые хотят контролировать загрязнение шин, складирование и освещение кранов, например. Чтобы выполнить эти задачи, приходится целенаправленно отбирать и размечать материалы.&lt;p&gt;В таком случае очень помогает накопленный архив, но для каждого запроса всё равно необходимо сформировать подходящую выборку и проверить, отражает ли она необходимые условия. Отмечу, что важно и различать объект и событие, потому что в некоторых ситуациях важен временной контекст и одного кадра в таком случае будет недостаточно.&lt;p&gt;&lt;strong&gt;Разметка данных&lt;/strong&gt;&lt;p&gt;Данные можно размечать по-разному. На своём опыте расскажу, как это делаем мы. При разметке используется смешанный подход: мы размечаем часть данных внутри компании, а часть отдаём внешним командам. Благодаря такому способу организации распределяется большой объём работы, а также сохраняется внутренний контроль над качеством результата. Отмечу, что выполнение разметки и её приёмка - это разные этапы. Валидация разметки также идёт внутри компании, и в ней принимают участие специально обученные валидаторы и специалисты, работающие с командой Data Science.&lt;p&gt;Внутри нашей инфраструктуры был развёрнут CVAT, который связан с инструментами автоматического сбора датасета. Когда появляются новые материалы, то они направляются на очередные итерации разметки и дообучения. При этом подготовленная разметка проходит внутреннюю валидацию с участием специалистов, лишь после чего используется в обучении. При большом масштабе очень важно следить за качеством разметки. Мы не рассматриваем её как вспомогательную операцию. Она является самостоятельной частью разработки модели. Если появляются ошибки в классах, обнаруживаются пропущенные объекты и неправильно обозначенные границы, то создаются неверные обучающие сигналы. Разметка - это непрерывный цикл, и эта непрерывность показана на отрисованной мной схеме:&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/841/f8d/c2e/841f8dc2e84fe37acda3671b36debd0e.jpeg width=1432 height=2048 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/841/f8d/c2e/841f8dc2e84fe37acda3671b36debd0e.jpeg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/841/f8d/c2e/841f8dc2e84fe37acda3671b36debd0e.jpeg 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;&lt;strong&gt;Датасет и специфика стройки&lt;/strong&gt;&lt;p&gt;Внутри датасета компании более миллиона размеченных кадров, полученных с реальных видеозаписей на строительных площадках. Отмечу, что это объём исходных несинтетических данных. Все остальные варианты, которые мы получаем при помощи аугментаций, в это число не входят. В компании мы не наращиваем выборку искусственным образом за счёт идентичных кадров. Все эти данные собираются уже более трёх с половиной лет, поэтому удалось накопить материалы за все сезоны, при разных погодных условиях, освещённости и качестве видеосъёмки. В процессе внутреннего конкурентного анализа и общения с участниками отрасли наши специалисты пришли к мнению, что компания на данный момент располагает крупнейшим в России массивом размеченных реальных строительных кадров. Однако отмечу, что это именно оценка компании, а не результат независимого рейтинга.&lt;p&gt;Строительный датасет принципиально отличается от общих датасетов типа COCO и ImageNet из-за соответствия конкретной среде эксплуатации. Пока общие датасеты полезны за счёт обучения визуальным признакам и сравнительной оценки алгоритмов, строительный датасет представляет те условия и объекты, с которыми система непосредственно сталкивается на площадке. Для нас это: реальные ракурсы видеокамер, разные расстояния до объектов, различные перекрытия из-за техники или конструкций, изменение освещения или погоды, а также преобразование самого объекта. Важно и другое (отраслевые задачи): просто обнаружить человека или технику на площадке не то же самое, что и определить ситуацию, требующую внимания. Различия в особенностях датасетов можно увидеть на схеме:&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/c3d/6f1/30a/c3d6f130a36b20030e8c1a18c259bd1e.jpeg width=1418 height=2048 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/c3d/6f1/30a/c3d6f130a36b20030e8c1a18c259bd1e.jpeg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/c3d/6f1/30a/c3d6f130a36b20030e8c1a18c259bd1e.jpeg 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;На реальных видеозаписях факторы сочетаются так, что их потом сложно воспроизвести искусственно. При этом даже несинтетическое происхождение данных не гарантирует качество само по себе. В любом случае будет необходима разметка и достаточное разнообразие примеров.&lt;p&gt;Мы разработали модули, для большинства из которых не требуется адаптация моделей под новую площадку. Обычно внедрение готового решения занимает около недели. Такого результата мы смогли достигнуть за счёт как раз накопленной базы данных с разными условиями съёмки. Однако отмечу важный нюанс: необходимо различать типовое внедрение от нового специализированного запроса. Если заказчик хочет получить дополнительный сценарий, тогда необходимо дополнительно настраивать зоны и правила, собирать целевую выборку и дообучать модель. В таком случае внедрение продукта займёт цикл в пределах одного месяца. Но в конечном итоге конкретный объём работы зависит от задачи и учёта ракурсов камер заказчика.&lt;p&gt;Кстати, чтобы решать проблему сезонности, приходится сочетать реальные примеры с аугментациями, которые расширяют вариативность обучающих примеров. Однако реальные материалы в любом случае останутся основой. Если просто затемнить дневное изображение, то ночная съёмка с её шумом и засветами не воспроизведётся должным образом. Или, например, если объект полностью закроется пылью и будет неразличим, то дополнительное обучение здесь ничем не поможет. Важным фактором качества останутся именно условия видеосъёмки.&lt;p&gt;&lt;strong&gt;Дообучение и адаптация моделей&lt;/strong&gt;&lt;p&gt;Большой накопленный датасет и уже работающие модели позволяют быстрее готовить новые сценарии. Ручная работа сокращается за счёт автоматизации сбора и передачи данных между этапами. При этом при специализированном запросе целевая выборка дособирается, проводится разметка и валидация, после чего идёт обучение модели и проверка результата перед выводом в production. Выше я упоминал, что срок внедрения может достигать одного месяца. В этот срок уже включён полный цикл адаптации, а не только вычислительный этап обучения.&lt;p&gt;Чтобы дать техническую оценку, мы используем precision (это доля правильных срабатываний), recall (доля обнаруженных целевых объектов или событий) и F1 (объединяется precision и recall в одном показателе). Качество измеряется на отдельной тестовой выборке, которая не участвует в обучении. Эти значения необходимо приводить вместе с описанием задачи и правил оценки. Помимо внутренних замеров, мы также сравниваем модели с другими российскими командами на общей выборке китайского строительного датасета.&lt;p&gt;В конечном итоге отмечу, что на качество будет влиять освещённость, размер объекта в кадре, перекрытия, погодные условия и характеристики видеосъёмки. При помощи разнообразного датасета можно учесть и эти изменения, но один средний показатель не сможет описать поведение модели во всех ситуациях. Чтобы провести содержательное сравнение, необходимо рассмотреть группы данных. В таком случае важно сохранять одинаковые правила оценки и учитывать объём каждой группы.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <author>Arkadiy_Froym</author>
      <guid>https://habr.com/ru/articles/1082180/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082180</guid>
      <pubDate>Mon, 14 Sep 2026 16:54:26 +0000</pubDate>
    </item>
    <item>
      <title>Вебхук ЮKassa принимал payment.succeeded на веру. Подделать оплату брони можно было одним curl</title>
      <link>https://habr.com/ru/articles/1082170/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082170</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;Разбираю находку из технического аудита сервиса аренды авто: вебхук &lt;code&gt;/payments/webhook/yukassa&lt;/code&gt; помечал бронь оплаченной по одному полю из тела запроса, которое присылает клиент, а не банк. Файл аудита датирован 5 июля 2026, фикс смержен 8 июля — привожу код до и после, тесты и то, что мы сознательно не стали чинить в этом же PR.&lt;h3&gt;Как это выглядело до&lt;/h3&gt;&lt;p&gt;Обработчик вебхука был устроен предельно просто:&lt;pre&gt;&lt;code class=python&gt;async def handle_yukassa_webhook(self, payload: dict, db: AsyncSession) -&amp;gt; None:&#xA;    &amp;#34;&amp;#34;&amp;#34;Обрабатывает входящий webhook от ЮKassa.&amp;#34;&amp;#34;&amp;#34;&#xA;    event_type = payload.get(&amp;#34;event&amp;#34;)&#xA;    payment_data = payload.get(&amp;#34;object&amp;#34;, {})&#xA;    gateway_payment_id = payment_data.get(&amp;#34;id&amp;#34;)&#xA;&#xA;    if not gateway_payment_id:&#xA;        return&#xA;&#xA;    result = await db.execute(&#xA;        select(Payment).where(Payment.gateway_payment_id == gateway_payment_id)&#xA;    )&#xA;    payment = result.scalar_one_or_none()&#xA;    if not payment:&#xA;        return&#xA;&#xA;    if event_type == &amp;#34;payment.succeeded&amp;#34;:&#xA;        payment.status = PaymentStatus.succeeded&#xA;        # обновление брони, отправка уведомления...&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:87px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Единственная проверка — что такой &lt;code&gt;gateway_payment_id&lt;/code&gt; вообще существует в нашей базе. Дальше код верит полю &lt;code&gt;event&lt;/code&gt; из тела запроса. Значит запрос вида&lt;pre&gt;&lt;code&gt;POST /payments/webhook/yukassa&#xA;{&amp;#34;event&amp;#34;: &amp;#34;payment.succeeded&amp;#34;, &amp;#34;object&amp;#34;: {&amp;#34;id&amp;#34;: &amp;#34;&amp;lt;gateway_payment_id уже созданной pending-брони&amp;gt;&amp;#34;}}&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;без единого заголовка авторизации переводил бронь в статус «оплачено». &lt;code&gt;gateway_payment_id&lt;/code&gt; не секрет — он приходит клиенту сразу после создания платежа, в ответе на &lt;code&gt;POST /bookings/{id}/pay&lt;/code&gt;. То есть у обычного пользователя, который просто открыл бронь и не заплатил, на руках было всё, что нужно для подделки.&lt;h3&gt;Почему не помогает «просто добавить подпись»&lt;/h3&gt;&lt;p&gt;Первая мысль — проверять подпись вебхука, как у Stripe или у большинства эквайеров. Полез разбираться, что у ЮKassa с этим есть. Официальная документация ЮKassa действительно описывает заголовок &lt;code&gt;Webhook-Signature&lt;/code&gt; с HMAC-SHA256 по &lt;code&gt;webhook_key&lt;/code&gt; из личного кабинета — но как дополнительный, не обязательный механизм. Базовая рекомендация ЮKassa — два других способа: проверка по IP-адресу отправителя и &lt;strong&gt;сверка статуса через обратный запрос к API&lt;/strong&gt;. Наша интеграция на момент аудита &lt;code&gt;webhook_key&lt;/code&gt; не заводила вообще, то есть подписи в заголовках попросту не было — только тело и IP.&lt;p&gt;От IP-фильтра отказались: инфраструктура за реверс-прокси, IP клиента до бэкенда доходит через цепочку заголовков, и сверять его с диапазоном ЮKassa означало бы держать этот список в актуальном состоянии и доверять &lt;code&gt;X-Forwarded-For&lt;/code&gt; — то есть менять модель доверия на другую модель доверия. Обратный запрос надёжнее: спросить у самой ЮKassa, что происходит с этим платежом, вместо того чтобы гадать по заголовкам.&lt;h3&gt;Что сделали вместо&lt;/h3&gt;&lt;pre&gt;&lt;code class=python&gt;async def _fetch_gateway_payment(self, gateway_payment_id: str) -&amp;gt; dict:&#xA;    &amp;#34;&amp;#34;&amp;#34;Запрашивает актуальное состояние платежа у ЮKassa.&#xA;&#xA;    Бросает исключение при недоступности API — вебхук ответит 5xx,&#xA;    и ЮKassa повторит уведомление позже (fail-closed).&#xA;    &amp;#34;&amp;#34;&amp;#34;&#xA;    async with httpx.AsyncClient() as client:&#xA;        resp = await client.get(&#xA;            f&amp;#34;https://api.yookassa.ru/v3/payments/{gateway_payment_id}&amp;#34;,&#xA;            auth=(settings.YUKASSA_SHOP_ID, settings.YUKASSA_SECRET_KEY),&#xA;            timeout=30,&#xA;        )&#xA;        resp.raise_for_status()&#xA;        return resp.json()&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Дальше в &lt;code&gt;handle_yukassa_webhook&lt;/code&gt; статус и сумма берутся только из ответа этого запроса, тело входящего вебхука используется исключительно как триггер «сходи проверь»:&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;gateway_status&lt;/code&gt; читается из ответа API, не из &lt;code&gt;payload[&amp;#34;event&amp;#34;]&lt;/code&gt;;&lt;li&gt;&lt;p&gt;сумма и валюта сверяются с записью &lt;code&gt;Payment&lt;/code&gt; в нашей базе — при расхождении статус не меняется, инцидент уходит в лог, а &lt;code&gt;gateway_response&lt;/code&gt; сохраняется целиком для разбора;&lt;li&gt;&lt;p&gt;если запрос к API ЮKassa падает (таймаут, 5xx на их стороне) — вебхук отвечает 5xx, ЮKassa переотправит уведомление позже. Это fail-closed: лучше временная задержка подтверждения, чем тихое принятие на веру;&lt;li&gt;&lt;p&gt;финальные статусы (&lt;code&gt;succeeded&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt;) повторным вебхуком не переписываются — без этого повторная доставка того же события просто лишний раз дергала бы API ЮKassa.&lt;/ul&gt;&lt;p&gt;Дев-режим без ключей ЮKassa (&lt;code&gt;YUKASSA_SHOP_ID&lt;/code&gt;/&lt;code&gt;YUKASSA_SECRET_KEY&lt;/code&gt; пустые) оставили как было — доверяет телу запроса, потому что реального шлюза там нет и подделывать нечего.&lt;p&gt;Диф вышел небольшим: &lt;code&gt;payment_service.py&lt;/code&gt; — +62/-5 строк, новый файл тестов — 205 строк.&lt;h3&gt;Тесты&lt;/h3&gt;&lt;p&gt;Файл &lt;code&gt;tests/test_payment_webhook.py&lt;/code&gt;, 6 новых сценариев, эквайер замокан:&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;&lt;p align=left&gt;Сценарий&lt;th&gt;&lt;p align=left&gt;Что проверяет&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;&lt;code&gt;test_forged_webhook_ignored_when_gateway_says_pending&lt;/code&gt;&lt;td&gt;&lt;p align=left&gt;подделанный &lt;code&gt;payment.succeeded&lt;/code&gt;, но у ЮKassa платёж ещё &lt;code&gt;pending&lt;/code&gt; → статус в базе не меняется&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;&lt;code&gt;test_webhook_amount_mismatch_ignored&lt;/code&gt;&lt;td&gt;&lt;p align=left&gt;ЮKassa подтверждает &lt;code&gt;succeeded&lt;/code&gt;, но сумма не совпадает (1 ₽ вместо 2300 ₽) → статус не меняется&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;&lt;code&gt;test_webhook_confirmed_success_marks_paid&lt;/code&gt;&lt;td&gt;&lt;p align=left&gt;сумма и статус совпадают → бронь переходит в &lt;code&gt;paid&lt;/code&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;&lt;code&gt;test_webhook_canceled_marks_failed&lt;/code&gt;&lt;td&gt;&lt;p align=left&gt;отмена подтверждена шлюзом → &lt;code&gt;failed&lt;/code&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;&lt;code&gt;test_webhook_final_status_not_rewritten&lt;/code&gt;&lt;td&gt;&lt;p align=left&gt;вебхук по уже &lt;code&gt;succeeded&lt;/code&gt; платежу — &lt;code&gt;_fetch_gateway_payment&lt;/code&gt; не должен вызываться вовсе (замокан на &lt;code&gt;AssertionError&lt;/code&gt; при вызове)&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;&lt;code&gt;test_webhook_unknown_payment_ignored&lt;/code&gt;&lt;td&gt;&lt;p align=left&gt;неизвестный &lt;code&gt;gateway_payment_id&lt;/code&gt; — тоже не должен трогать API&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Локально пакет тестов прошёл зелёным, &lt;code&gt;ruff&lt;/code&gt; без замечаний. Отдельно стоит сказать, что именно поэтому баг до аудита не поймали: юнит-тесты проверяли логику обработчика саму на себя — с точки зрения теста «вебхук с &lt;code&gt;payment.succeeded&lt;/code&gt; переводит бронь в &lt;code&gt;paid&lt;/code&gt;» было ожидаемым поведением, а не дырой. Отсутствие проверки источника события не тестируется тестами, которые сами же и формируют доверенный вход. Нашли не автотестом, а при ручном чтении кода в рамках аудита.&lt;h3&gt;Что не стали чинить в этом же PR&lt;/h3&gt;&lt;p&gt;Три вещи сознательно оставлены на потом, обе — в описании PR:&lt;ol&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;IP-allowlist так и не добавлен&lt;/strong&gt; — второй официально рекомендованный ЮKassa способ. Сейчас единственная линия защиты — обратная проверка через API, и это, строго говоря, делает вебхук почти декоративным: он всего лишь сигнал «сходи спроси у ЮKassa», а не источник истины. Работает, но лишний слой не помешал бы.&lt;li&gt;&lt;p&gt;&lt;code&gt;&lt;strong&gt;webhook_key&lt;/strong&gt;&lt;/code&gt;&lt;strong&gt; и HMAC-подпись не подключены.&lt;/strong&gt; Раз уж заголовок &lt;code&gt;Webhook-Signature&lt;/code&gt; существует — его игнорирование означает один лишний сетевой запрос к ЮKassa на каждое уведомление, который можно было бы не делать, если бы подпись проверялась локально.&lt;li&gt;&lt;p&gt;Рядом, в соседнем PR (идемпотентность &lt;code&gt;initiate_payment&lt;/code&gt;, аудит HIGH №11), нашли ещё один смэлл: &lt;code&gt;get_db&lt;/code&gt; коммитит транзакцию в конце запроса, а сервис вдобавок коммитит явно сам — двойной &lt;code&gt;commit()&lt;/code&gt;. Решили не трогать: повторный коммит уже пустой транзакции безвреден, а разносить ответственность за транзакции — отдельная задача, и в PR с критической уязвимостью лишний рефакторинг только повышает шанс что-то сломать в неподходящий момент.&lt;/ol&gt;&lt;p&gt;Изначально хотелось закрыть всё сразу — signature, IP-фильтр и двойной коммит заодно с основной дырой. Оставили только реверс-проверку, потому что диф с четырьмя параллельными изменениями в одном платёжном файле труднее ревьюить и опаснее откатывать, если что-то не так. Минимальный диф на критичном участке дороже одной строки в чейнджлоге.&lt;p&gt;Проверить, эксплуатировали ли дыру до фикса, задним числом нельзя: подделанный запрос от обычного &lt;code&gt;payment.succeeded&lt;/code&gt; в логах ничем не отличается — оба просто POST на тот же путь. Единственная граница, которая появилась после фикса, — теперь у каждого такого события есть подтверждающий ответ ЮKassa, который можно поднять и сверить.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <author>r_o_m_k_o_l_a</author>
      <guid>https://habr.com/ru/articles/1082170/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082170</guid>
      <pubDate>Mon, 14 Sep 2026 16:29:07 +0000</pubDate>
    </item>
    <item>
      <title>Код как борьба. Кратчайшая история IT. 5. Гэри Килдалл</title>
      <link>https://habr.com/ru/articles/1075694/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1075694</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;blockquote&gt;&lt;p&gt;Пара слов о формате “Код как борьба”. Здесь не будет подробных биографий - про каждого из героев этого цикла написаны отдельные толстые книги, и одна статья такую книгу не заменит. Не будет здесь и сухого технического разбора, кому это интересно. Вместо этого - попытка проследить, как одна идея вырастает из другой, как эстафета, которую передают друг другу через десятилетия и века люди, часто вообще не подозревающие, что работают над одним и тем же. Если какой-то сюжет или деталь захочется изучить подробнее - внизу статьи есть подборка книг, статей и первоисточников по теме.&lt;/blockquote&gt;&lt;p&gt;Прошлые статьи цикла:&lt;p&gt;&lt;a href=https://habr.com/ru/articles/1074892/ rel=&#34;noopener nofollow&#34;&gt;Точка входа&lt;/a&gt;&lt;p&gt;&lt;a href=https://habr.com/ru/article/1074916/ rel=&#34;noopener nofollow&#34;&gt;0. Лейбниц, Бэббидж и Лавлейс&lt;/a&gt;&lt;p&gt;&lt;a href=https://habr.com/ru/article/1075098/ rel=&#34;noopener nofollow&#34;&gt;1. Алан Тьюринг&lt;/a&gt;&lt;p&gt;&lt;a href=https://habr.com/ru/article/1075108/ rel=&#34;noopener nofollow&#34;&gt;2. Джон фон Нейман&lt;/a&gt;&lt;p&gt;&lt;a href=https://habr.com/ru/article/1075122/ rel=&#34;noopener nofollow&#34;&gt;3. Клод Шеннон&lt;/a&gt;&lt;p&gt;&lt;a href=https://habr.com/ru/article/1075136/ rel=&#34;noopener nofollow&#34;&gt;4. Кен Томпсон и Деннис Ритчи&lt;/a&gt;&lt;hr&gt;&lt;p&gt;Сейчас я расскажу вам историю. Классическую, много раз пересказанную историю о том, как один человек упустил сделку всей своей жизни, потому что решил покататься на самолёте вместо важной встречи.&lt;p&gt;А потом мы разберём, что из этого правда.&lt;p&gt;Это шестая статья цикла “Код как борьба: Кратчайшая история IT”. И сегодня будет необычный формат - не просто биография, а разоблачение. Потому что самая знаменитая деталь в истории нашего сегодняшнего героя оказалась мифом. Причём мифом, задокументированно опровергнутым самими историческими источниками.&lt;p&gt;Погнали.&lt;h3&gt;Часть первая. Изобретение, о котором обычно даже не вспоминают&lt;/h3&gt;&lt;p&gt;Гэри Килдалл родился в 1942 году в Сиэтле, в семье, державшей мореходную школу. Получил докторскую степень по информатике в Вашингтонском университете в 1972 году - на тот момент одну из первых подобных степеней в стране вообще.&lt;p&gt;Преподавал в Военно-морской аспирантуре в Монтерее, одновременно консультировал Intel по языку программирования PL/M и ранним встроенным системам. А в 1974 году пишет CP/M - Control Program for Microcomputers, операционную систему для одних из первых микропроцессоров Intel.&lt;p&gt;Вместе с женой, Дороти Килдалл, он основывает компанию Digital Research для коммерциализации CP/M. И вот здесь стоит сразу подчеркнуть, Дороти - это не просто “жена изобретателя” где-то на заднем плане. Она содиректор компании и, по многим свидетельствам, куда более жёсткий и опытный переговорщик в деловых вопросах, чем сам Гэри, склонный скорее к инженерному, чем коммерческому складу мышления. Это важная деталь, к которой мы ещё вернёмся.&lt;p&gt;Давайте сначала про само изобретение, потому что оно недооценено куда сильнее, чем сам скандал вокруг Килдалла.&lt;p&gt;К середине семидесятых появляются первые доступные микропроцессоры, в частности Intel 8080. Но для каждой конкретной модели компьютера на их основе программы приходилось писать заново, не существовало стандартной прослойки между железом и прикладным софтом.&lt;p&gt;И вот по мере того, как множество разных производителей начинают лицензировать CP/M для своих несовместимых друг с другом машин, Килдалл вводит концепцию &lt;em&gt;BIOS - Basic Input/Output System&lt;/em&gt;. Небольшой, специфичный для конкретного железа программный слой, отделяющий аппаратно-зависимый код от основной, единой для всех машин части операционной системы.&lt;p&gt;Это позволяет CP/M работать на множестве разных компьютеров с минимальной адаптацией. Производителю железа достаточно написать сравнительно небольшой BIOS под свою конкретную машину, а не переписывать всю операционную систему с нуля.&lt;p&gt;И вот что интересно: сам термин и концепция BIOS, знакомые сегодня любому, кто хоть раз заходил в настройки материнской платы, - это прямое наследие решения, придуманного Килдаллом в середине семидесятых. А вовсе не изобретение более поздней эпохи персональных компьютеров IBM, как это иногда ошибочно ассоциируется в массовом сознании.&lt;p&gt;К началу восьмидесятых CP/M становится де-факто отраслевым стандартом операционной системы для микрокомпьютеров. Digital Research продаёт сотни тысяч лицензий. И на определённом историческом отрезке именно Килдалл, а не кто-либо ещё, выглядит наиболее вероятным кандидатом на роль человека, который определит, какая операционная система будет доминировать в грядущую эпоху персональных компьютеров.&lt;h3&gt;Часть вторая. Легенда, которую вы, скорее всего, уже слышали&lt;/h3&gt;&lt;p&gt;А теперь давайте я перескажу миф в его самой распространённой форме так, как его годами тиражировали, в том числе крупные издания вроде Bloomberg.&lt;p&gt;Летом 1980 года представители IBM прибывают в штаб-квартиру Digital Research в Пасифик-Гроув, Калифорния. Обсудить лицензирование CP/M для готовящегося IBM PC, потенциально одну из крупнейших сделок в истории компьютерной индустрии.&lt;p&gt;Но в этот самый день Гэри Килдалл, вместо того чтобы вести переговоры лично, решает покататься на собственном самолёте для развлечения. И, вернувшись, упускает возможность заключить сделку. Разочарованные представители IBM, не сумев договориться с Digital Research, обращаются к молодой компании Microsoft Билла Гейтса и та соглашается предоставить операционную систему, ставшую в итоге MS-DOS. Определившую судьбу компьютерной индустрии на десятилетия вперёд.&lt;p&gt;Знакомая история, правда? И вот важная деталь, именно эту версию годами растиражировал сам Билл Гейтс в многочисленных интервью - в частности, в уже упомянутой в прошлом эпизоде книге “Fire in the Valley” приводится его прямая формулировка: “в тот день Гэри катался на самолёте”. То есть версию истории, крайне выгодную для нарратива о собственном успехе Microsoft - упорный, серьёзный подход Гейтса против беспечности конкурента, запустил в оборот прямой конкурент и бенефициар исхода этих переговоров. Эта версия была настолько общепринята, что даже известные технологические авторы, вроде Питера Нортона, ошибочно включали её в свои книги как фактическую историю.&lt;p&gt;А теперь, что произошло на самом деле.&lt;h3&gt;Часть третья. Разбираем миф по косточкам&lt;/h3&gt;&lt;p&gt;Опираемся на несколько независимо задокументированных источников - в частности, на воспоминания самого Килдалла, опубликованные посмертно Музеем компьютерной истории, и на интервью нескольких участников переговоров, включая ведущего переговорщика со стороны IBM, Джека Сэмса.&lt;p&gt;Что действительно произошло в тот день. &lt;a href=https://nemanjatrifunovic.substack.com/p/the-peak-and-collapse-of-digital rel=&#34;noopener nofollow&#34;&gt;Представители IBM позвонили в Digital Research 21 августа 1980 года, а сама встреча состоялась на следующий день, 22 августа&lt;/a&gt; - и действительно по инициативе самого Билла Гейтса, который в разговоре с представителем IBM Джеком Сэмсом сам порекомендовал обратиться к Килдаллу за операционной системой, поскольку у Microsoft на тот момент своей ОС попросту не было. Килдалл в этот день действительно отсутствовал в офисе. Но, по одной из версий, изложенной в его собственных воспоминаниях, он вместе с коллегой Томом Роландером был занят доставкой программного обеспечения клиенту на собственном самолёте. По другой версии, речь шла о встрече с Hewlett-Packard тем же утром. Это была деловая, а не исключительно развлекательная поездка. Уже меняет тональность истории, правда?&lt;p&gt;Переговоры в его отсутствие вела его жена и деловой партнёр Дороти Килдалл вместе с юрисконсультом компании. И IBM настаивала на подписании соглашения о неразглашении ещё до того, как объяснить цель визита. Дороти, действуя строго в рамках корпоративных процедур, отказалась подписывать документ такого рода без консультации с Гэри.&lt;p&gt;И вот это, на минуточку, разумное, профессиональное деловое поведение. А не досадная помеха, как это представлено в упрощённой версии мифа.&lt;p&gt;Что случилось дальше, тут версии расходятся даже в наиболее авторитетных источниках. По одной версии, Килдалл, вернувшись позже в тот же день, лично встретился с представителем IBM и возобновил переговоры. Сам Джек Сэмс впоследствии утверждал, что личной встречи между ними вообще не произошло.&lt;p&gt;Но вот ключевой момент, что бы конкретно ни произошло в тот день, окончательный разрыв переговоров между IBM и Digital Research не сводится к единственной пропущенному встрече. Это было куда более сложное сочетание разногласий по условиям лицензирования, вопросов интеллектуальной собственности и организационных недопониманий, растянутое на несколько последующих встреч и звонков.&lt;p&gt;&lt;a href=https://computerhistory.org/blog/in-his-own-words-gary-kildall/ rel=&#34;noopener nofollow&#34;&gt;Дети Килдалла, публикуя посмертные воспоминания отца через Музей компьютерной истории в 2016 году, прямо назвали историю про самолёт мифом&lt;/a&gt;, который несправедливо превращает их отца в фигуру “мог бы, да не смог” и заслоняет собой его реальный вклад как изобретателя ключевых технологий, на которых до сих пор работают компьютерные платформы.&lt;h3&gt;Часть четвёртая. Как Microsoft всё-таки получила операционную систему&lt;/h3&gt;&lt;p&gt;Коротко о том, что произошло дальше, без лишнего драматизма, потому что главный урок этого эпизода не в самой сделке, а в механизме мифа вокруг неё.&lt;p&gt;После того как переговоры с Digital Research не привели к соглашению, IBM обращается обратно к Microsoft. Гейтс на тот момент не располагает собственной операционной системой, но &lt;a href=https://en.wikipedia.org/wiki/CP/M-86 rel=&#34;noopener nofollow&#34;&gt;приобретает права на систему 86-DOS, разработанную программистом Тимом Патерсоном в компании Seattle Computer Products&lt;/a&gt;. Сама 86-DOS, кстати, структурно и функционально во многом напоминала CP/M, вопрос о степени заимствования идей, а возможно, и кода, стал позже предметом отдельных юридических и исторических споров, до конца не разрешённых.&lt;p&gt;Microsoft дорабатывает систему, лицензирует IBM права на неё, но сохраняет за собой права лицензировать ту же систему и другим производителям персональных компьютеров под именем &lt;em&gt;MS-DOS&lt;/em&gt;.&lt;p&gt;И вот это решение, а не сама разработка операционной системы с нуля, оказывается ключевым коммерческим ходом, определившим будущее компании на десятилетия вперёд.&lt;p&gt;Смотрите на разницу в стратегии. Digital Research лицензировала CP/M как основной, эксклюзивный продукт компании. А Microsoft сознательно сохранила право лицензировать MS-DOS широкому кругу производителей клонов IBM PC. Именно эта стратегическая, а не техническая разница в итоге определила, почему Microsoft, а не Digital Research, оказалась в позиции контролировать стандарт операционной системы для будущего массового рынка персональных компьютеров.&lt;h3&gt;Итог&lt;/h3&gt;&lt;p&gt;Подлинная история поражения Digital Research в конкуренции за операционную систему для IBM PC - это история о деловой стратегии, лицензионных условиях и организационных решениях, растянутых на месяцы переговоров. А вовсе не одномоментная драма об одном полёте&lt;p&gt;Миф оказался нужен не потому, что он точнее отражает реальность. А потому, что даёт куда более простой, кинематографичный и выгодный для одной из сторон нарратив. И именно эта простая версия, а не более сложная правда, определила то, как Килдалла помнят или, куда чаще, не помнят, массовая культура и история технологий.&lt;p&gt;И, если уж говорить о том, как реальность обрастает легендами - тот же механизм сработал и с его смертью. &lt;a href=https://en.wikipedia.org/wiki/Gary_Kildall rel=&#34;noopener nofollow&#34;&gt;Килдалл умер в 1994 году, в 52 года, после травмы головы, полученной в баре в Монтерее&lt;/a&gt;. Точные обстоятельства до сих пор не установлены: свидетели расходятся в показаниях, было ли это падение, драка или что-то ещё, полиция рассматривала версию убийства, но обвинений так и не предъявила, а вскрытие указало на черепно-мозговую травму на фоне хронического алкоголизма как способствующего фактора. Даже финал жизни человека, чья слава основана на непроверенном мифе, сам оказался историей с несколькими версиями и без окончательного ответа.&lt;h3&gt;Вопросы, чтобы обсудить в комментариях&lt;/h3&gt;&lt;p&gt;Первое - почему упрощённые, драматичные версии исторических событий систематически побеждают более сложные и документированные версии в массовом сознании? Что это говорит о том, как формируется коллективная память в истории технологий вообще?&lt;p&gt;Второе, чуть более неудобное - прямой конкурент и бенефициар исхода событий стал основным источником растиражированной версии истории. Насколько это типичная для бизнеса практика конструирования удобного нарратива о собственном успехе за счёт менее выигрышного изображения конкурента?&lt;p&gt;Третье — многие историки указывают, что CP/M на определённом этапе была технически зрелее и дольше на рынке, чем ранние версии MS-DOS. Как часто в истории технологий побеждает не лучшая технология, а лучшая лицензионная и дистрибьюторская стратегия?&lt;blockquote&gt;&lt;p&gt;В следующей статье мы встретим полную противоположность коммерческой истории Килдалла. Человека, который построил операционную систему как форму сознательного политического протеста против всё более закрытой модели софта. Ричард Столлман и проект GNU.&lt;/blockquote&gt;&lt;h3&gt;Источники и ссылки по фактам из статьи&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Хронология переговоров IBM и Digital Research (звонок 21 августа, встреча 22 августа 1980 года), роль Гейтса в направлении IBM к Килдаллу - &lt;a href=https://nemanjatrifunovic.substack.com/p/the-peak-and-collapse-of-digital rel=&#34;noopener nofollow&#34;&gt;The Peak and Collapse of Digital Research&lt;/a&gt;&lt;li&gt;&lt;p&gt;Альтернативная версия про встречу с Hewlett-Packard в то же утро - &lt;a href=https://rclassiccomputers.com/dos/ rel=&#34;noopener nofollow&#34;&gt;История DOS, Remi’s Classic Computers&lt;/a&gt;&lt;li&gt;&lt;p&gt;Публикация посмертных воспоминаний Килдалла Музеем компьютерной истории в 2016 году и точная формулировка его детей о мифе - &lt;a href=https://computerhistory.org/blog/in-his-own-words-gary-kildall/ rel=&#34;noopener nofollow&#34;&gt;Computer History Museum: In His Own Words - Gary Kildall&lt;/a&gt;&lt;li&gt;&lt;p&gt;Обстоятельства смерти Килдалла в 1994 году - &lt;a href=https://en.wikipedia.org/wiki/Gary_Kildall rel=&#34;noopener nofollow&#34;&gt;Wikipedia: Gary Kildall&lt;/a&gt;, &lt;a href=https://www.generationamiga.com/2026/07/18/gary-kildalls-death-investigation-facts-evidence-and-unanswered-questions/ rel=&#34;noopener nofollow&#34;&gt;материалы расследования, Generation Amiga&lt;/a&gt;&lt;/ul&gt;&lt;h3&gt;Дополнительное чтение к данной статье&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Посмертные воспоминания Гэри Килдалла&lt;/strong&gt;, опубликованные Музеем компьютерной истории (Computer History Museum) в 2016 году с разрешения его детей Кристин и Скотта Килдалл Ключевой первоисточник для разоблачения мифа. Доступен на сайте музея (computerhistory.org), стоит прочитать в оригинале, если позволяет английский.&lt;p&gt;&lt;strong&gt;Гарольд Эванс - “They Made America: From the Steam Engine to the Search Engine”&lt;/strong&gt; (2004) Содержит одну из наиболее взвешенных версий истории Килдалла среди научно-популярных книг об истории американских изобретателей.&lt;p&gt;&lt;strong&gt;Пол Фрайбергер, Майкл Свейн - “Fire in the Valley: The Making of the Personal Computer”&lt;/strong&gt; (1984, переиздания) Одна из самых ранних и подробных книг об истории персональных компьютеров, включая раннюю, ещё не до конца мифологизированную версию истории Килдалла и IBM и одновременно источник самой формулировки Гейтса про полёт на самолёте, если хочется увидеть её в оригинальном контексте.&lt;p&gt;&lt;strong&gt;Статья Bloomberg “The Man Who Could Have Been Bill Gates”&lt;/strong&gt; (2004) Хороший источник именно для канонической версии мифа. Полезно прочитать её саму, чтобы увидеть, как убедительно и складно звучит версия, которую мы разобрали.&lt;p&gt;&lt;strong&gt;Материалы эфира программы Computer Chronicles&lt;/strong&gt; (1995 год, соведущим которой был сам Килдалл) Наиболее подробная версия событий 1980 года из уст самого их участника. Записи доступны в архивах на YouTube и в Internet Archive.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <author>magnusroot</author>
      <guid>https://habr.com/ru/articles/1075694/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1075694</guid>
      <pubDate>Mon, 14 Sep 2026 16:00:03 +0000</pubDate>
    </item>
    <item>
      <title>100501-я версия латиницы для русского языка</title>
      <link>https://habr.com/ru/articles/1082162/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082162</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;&lt;strong&gt;100501-я версия латиницы для русского языка&lt;/strong&gt;&lt;p&gt;Периодически попадаются мне на Хабре варианты латинизации / транслитерации русского языка (примеры: &lt;a href=https://habr.com/ru/articles/827106/ rel=&#34;noopener nofollow&#34;&gt;вот&lt;/a&gt;, &lt;a href=https://habr.com/ru/articles/844218/ rel=&#34;noopener nofollow&#34;&gt;вот&lt;/a&gt;, &lt;a href=https://habr.com/ru/articles/1036964/ rel=&#34;noopener nofollow&#34;&gt;вот так вот&lt;/a&gt;, и даже &lt;a href=https://habr.com/ru/articles/543172/ rel=&#34;noopener nofollow&#34;&gt;вот так&lt;/a&gt;). Кроме того, существует великое множество официальных ГОСТовых стандартов. И так получилось, что они все очень сильно мне не понравились (как минимум, у них у всех есть &lt;a href=https://neolurk.org/wiki/%D0%A4%D0%B0%D1%82%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9_%D0%BD%D0%B5%D0%B4%D0%BE%D1%81%D1%82%D0%B0%D1%82%D0%BE%D0%BA rel=&#34;noopener nofollow&#34;&gt;фатальный недостаток&lt;/a&gt;). В данном случае уже существующих стандартов столько, что добавление ещё одного ситуацию не ухудшит, и поэтому решил разработать свой вариант. Я не считаю, что надо тратить время и деньги на переход с кириллицы на латиницу, просто захотел посоревноваться, кто придумает ловчее.&lt;p&gt;Для начала надо определиться с требованиями. Зачастую приоритет отдают двум вещам — точное соответствие русской кириллической орфографии и отсутствие диакритики. Это важно при разработке системы именно транслитерации, но я предлагаю полноценную латинизацию, поэтому эти требования изменю: возможность автоматического преобразования кириллического текста в латиницу без значимых фонетических искажений и минимум диакритики. Совсем без диакритики / диграфов не обойтись, и из двух этих зол, на мой взгляд, диакритика является меньшим (так‑то она и в нашей кириллице есть). Следующее важное требование: сохранение верховенства морфологического принципа письменности, как и в текущей кириллице (то есть слово пишется не так, как слышится (белорусский), и не так, как писалось раньше (французский), а так, чтобы сохранялось написание морфем (корней, приставок, суффиксов) независимо от их реального произношения). Далее, произношение латинских букв должно быть конвенциональным (никаких ‘w’ = ‘ш’). Буква ‘y’ может соответствовать звукам ‘и’ / ‘й’ (английский), ‘ю’ (немецкий), ‘ы’ (польский), но не может авторским произволом начать соответствовать ‘э’. Наконец, важна последовательность: если уж решили к примеру, что ‘y’ = ‘ы’, то в качестве ‘й’ надо применить что‑то другое.&lt;p&gt;Для начала, многие буквы споров особо не вызывают и сохраняются от стандарта к стандарту:&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=center&gt;А а&lt;td&gt;&lt;p align=center&gt;Б б&lt;td&gt;&lt;p align=center&gt;В в&lt;td&gt;&lt;p align=center&gt;Г г&lt;td&gt;&lt;p align=center&gt;Д д&lt;td&gt;&lt;p align=center&gt;З з&lt;td&gt;&lt;p align=center&gt;И и&lt;td&gt;&lt;p align=center&gt;К к&lt;td&gt;&lt;p align=center&gt;Л л&lt;td&gt;&lt;p align=center&gt;М м&lt;tr&gt;&lt;td&gt;&lt;p align=center&gt;A a&lt;td&gt;&lt;p align=center&gt;B b&lt;td&gt;&lt;p align=center&gt;V v&lt;td&gt;&lt;p align=center&gt;G g&lt;td&gt;&lt;p align=center&gt;D d&lt;td&gt;&lt;p align=center&gt;Z z&lt;td&gt;&lt;p align=center&gt;I i&lt;td&gt;&lt;p align=center&gt;K k&lt;td&gt;&lt;p align=center&gt;L l&lt;td&gt;&lt;p align=center&gt;M m&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=center&gt;Н н&lt;td&gt;&lt;p align=center&gt;О о&lt;td&gt;&lt;p align=center&gt;П п&lt;td&gt;&lt;p align=center&gt;Р р&lt;td&gt;&lt;p align=center&gt;С с&lt;td&gt;&lt;p align=center&gt;Т т&lt;td&gt;&lt;p align=center&gt;У у&lt;td&gt;&lt;p align=center&gt;Ф ф&lt;td&gt;&lt;p align=center&gt;Ц ц&lt;tr&gt;&lt;td&gt;&lt;p align=center&gt;N n&lt;td&gt;&lt;p align=center&gt;O o&lt;td&gt;&lt;p align=center&gt;P p&lt;td&gt;&lt;p align=center&gt;R r&lt;td&gt;&lt;p align=center&gt;S s&lt;td&gt;&lt;p align=center&gt;T t&lt;td&gt;&lt;p align=center&gt;U u&lt;td&gt;&lt;p align=center&gt;F f&lt;td&gt;&lt;p align=center&gt;C c&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Первый серьёзный вопрос заключается в способе передаче твёрдости / мягкости согласных. Добавлять к каждой согласной диакритический знак русскому языку не подходит, так как очень муторно (да и визуально засоряет текст), поэтому остаётся поступить так же, как и в кириллице — мягкость согласной регулируется следующей за ней буквой. Так что необходим способ приделать модификатор смягчения к гласным, и наиболее мне понравившийся вариант — это стандартный для славянских латиниц подход с добавлением ‘i’ (кстати, забавно, что в кириллице шли тем же путём, и те же ‘ю’ и ‘я’ — это сильно изменившиеся за лета диграфы с ‘i’).&lt;p&gt;Если надо передать не смягчение согласной, а йотирование гласной, то наиболее простое решение также классическое — использовать ‘j’ = ‘й’. Его же можно использовать и для мягкого знака, так как в современном русском ‘ь’ и ‘й’ выполняют одну и ту же функцию, просто в разных позициях в слове. Кроме того (хоть это и не назвать серьёзным достоинством), такой подход подчёркивает, что звуки ‘ь’ и ‘й’ в русском языке исторически происходят от ‘и’.&lt;p&gt;Буквы ‘е’ и ‘э’ в эту схему не вписываются, так как в кириллической русской письменности нет чёткого правила, что ‘е’ всегда смягчает предыдущую согласную. На самом деле, было бы логично использовать «ie / je» = ‘ѣ’ и ‘e’ = “е / э”, но для этого пришлось бы сначала откатиться на дореформенную орфографию, что есть абсурд. Компромиссное решение такое: в йотированных позициях использовать всегда «je», в нейотированных позициях — всегда ‘e’. Диграфы «ie» = ‘ѣ’ и «ae» = ‘э’ можно использовать в том случае, если надо передать нестандартное произношение («компьютѣр» вместо «компьютэр», стереотипный кавказский акцент и так далее)&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=center&gt;Е&lt;td&gt;&lt;p align=center&gt;Ё ё&lt;td&gt;&lt;p align=center&gt;Й й&lt;td&gt;&lt;p align=center&gt;ь&lt;td&gt;&lt;p align=center&gt;Э э&lt;td&gt;&lt;p align=center&gt;Ю ю&lt;td&gt;&lt;p align=center&gt;Я я&lt;tr&gt;&lt;td&gt;&lt;p align=center&gt;Je e / je&lt;td&gt;&lt;p align=center&gt;Jo io / jo&lt;td&gt;&lt;p align=center&gt;J j&lt;td&gt;&lt;p align=center&gt;j&lt;td&gt;&lt;p align=center&gt;E e / ae&lt;td&gt;&lt;p align=center&gt;Ju iu / ju&lt;td&gt;&lt;p align=center&gt;Ja ia / ja&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;У такого классического подхода есть два минуса. Во‑первых, путаница с сочетаниями ‘и’ + нейотированная гласная: «idiot» это «идёт» или «идиот»? Решение такое: в сочетаниях согласная + ‘и’ + нейотированная гласная использовать не ‘i’, а ‘y’. “Идёт идиот” = «idiot idyot». Французкий вариант с диарезисом отбросил, потому что слишком большое количество точек засоряло бы текст. Во‑вторых, буква ‘j’ в тексте становится одной из самых частотных (а она довольно сильно бросается в глаза). Поэтому любое число идущих подряд ‘j’ склеивать в одну («вьюга» — «vjuga» вместо «vjjuga»).&lt;p&gt;В славянских языках дофигищща шипящщих, и их надо как‑то передать. Польский вариант, к счастью, никто всерьёз не рассматривает, так что конкурируют два подхода — диакритика ‘č’, ‘š’, ‘ž’ и диграфы «ch», “sh”, “zh”. И если честно, я не вижу ни малейших причин предпочесть здесь диграфы. Все системы с использованием этих диграфов, которые я видел, во‑первых, сами по себе громоздкие, во‑вторых вызывают проблемы с передачей буквы ‘х’ через наиболее интуитивную ‘h’ (поэтому обычно обозначают либо диграфом же «kh», что загромождает текст ещё сильнее, либо латинской ‘x’).&lt;p&gt;В том маловероятном случае, если нет возможности ввести с клавиатуры символы ‘č’, ‘š’, ‘ž’, от диграфов не отвертеться, но намного удобнее использовать другие — «cx», “sx”, “zx” (такой способ встречается, например, в эсперанто). Основные достоинства подхода: ‘x’ в естественных словах не бывает после согласной, то есть такие диграфы не путаются с сочетаниями отдельных букв, а также ‘x’ на раскладке соседствует с ‘c’, ‘s’, ‘z’. Дополнительный небольшой плюс — визуальное сходство ‘x’ с диакритическим знаком гачека.&lt;p&gt;Самый сложный в передаче шипящий, безусловно, ‘щ’. В современном стандартном русском произношении это смягчённый ‘ш’, и может возникнуть идея так и записывать: ‘š’ + ‘j’ / смягчающая гласная, и даже применить тот же принцип и к остальным шипящим (а то чего остальные гласные по твёрдости / мягкости чередуются, а эти нет?), но есть проблема. Дело в том, что с точки зрения русской орфографии это ‘ш’ и ‘ж’ мягкие, а ‘щ’ твёрдый. Поэтому для реализации такого подхода пришлось бы сначала кириллическую орфографию серьёзно перелопатить. Остаётся вариант «šč» = ‘щ’. Потенциальный тетраграф «sxcx» вызывает стойкие ассоциации с бобрами, поэтому на всякий случай упрощу его до «scx» (“сч” всё равно примерно так же произносится).&lt;p&gt;Ну и раз уж отказались от идеи сделать все согласные чередующимися по мягкости / твёрдости, то после шипящих (и примкнувшего к ним ‘c’) нет смысла писать «ia», “io”, “iu”, можно обойтись ‘a’, ‘o’, ‘u’.&lt;p&gt;Остались две нетривиальные буквы — ‘ъ’ и ‘ы’. В славянских латиницах часто передают ‘ы’ через ‘y’, но для русского языка это нарушает морфологический принцип («играть» — «подыграть», «igratj» — “podygratj”, добавление приставки изменило написание корня). Ситуация осложняется тем, что это ещё и работает не всегда, есть исключения («индустриальный» — «постиндустриальный»). Поэтому необходим способ передать визуально чередование ‘и’ / ‘ы’. Я расссматривал такой вариант: ‘y’ = ‘ъ’, «yi» = ‘ъ’ + ‘i’ = ‘ы’. Но это и нестандартное применение ‘y’, и выглядит громоздко. Поэтому решение такое: буква ‘ъ’ как гласный с нулевым звуком не записывается вовсе (она и в кириллице нашей уже помирает), но делает смягчающую гласную после себя йотированной, а ‘ы’ записывается через ‘ı’ (как в турецком). При невозможности ввести с клавиатуры диакритику вместо ‘ı’ использовать обычную ‘i’.&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=center&gt;Ж ж&lt;td&gt;&lt;p align=center&gt;Х х&lt;td&gt;&lt;p align=center&gt;Ч ч&lt;td&gt;&lt;p align=center&gt;Ш ш&lt;td&gt;&lt;p align=center&gt;Щ щ&lt;td&gt;&lt;p align=center&gt;ы&lt;td&gt;&lt;p align=center&gt;ъ&lt;tr&gt;&lt;td&gt;&lt;p align=center&gt;Ž ž (zx)&lt;td&gt;&lt;p align=center&gt;H h&lt;td&gt;&lt;p align=center&gt;Č č (cx)&lt;td&gt;&lt;p align=center&gt;Š š (sx)&lt;td&gt;&lt;p align=center&gt;Šč šč (scx)&lt;td&gt;&lt;p align=center&gt;ı (i)&lt;td&gt;&lt;p align=center&gt;&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Пример записи текста такой системой.&lt;p&gt;Кириллица:&lt;p&gt;Майор авиации Цюрюпа Майя Ильинична ввела антиионизирующий диэлектрик в костьутиль Шауляйского стройуправления и, желая помочь жюри журнала «Жизнь Жюля Верна», наотмашь гильотинировала сумасшедший трёхъярусный подъёмник её конвейера жёлтых парашютов. Близстоящая госзаёмщица, получившая морзянкой это спецзадание от минюста, обошла безымянный танцзал сзади постиндустриального Кяхтинского сельисполкома, пытаясь, как гюрза, выучиться на переводчика с французского и возить майонез из Цюриха в Пюхяярви. Гэдээровские реперы предсказуемы, ведь они, фактически, синтезируют предсказуемый фактический темп. Парадоксально, но Антииисус почитается в антииудаизме. Со шхуны счастливо вещает веснушчатый иждивенец. Спецхрам им. Мустазхира Арзаханяна для сходок схимонахов в Цхинвали.&lt;p&gt;Latinica (s dyakritikoj):&lt;p&gt;Major avyacii Curiupa Maja Iljinična vvela antiioniziruiuščij dyelektrik v kostjutilj Šauliajskogo strojupravlenija i, želaja pomočj žuri žurnala “Žiznj Žulia Verna”, naotmašj giljotinirovala sumasšedšij triohjarusnıj podjomnik jejo konvejera žoltıh parašutov. Blizstojaščaja goszajomščica, polučivšaja morziankoj eto speczadanije ot miniusta, obošla bezımiannıj tanczal szadi postindustryaljnogo Kiahtinskogo seljispolkoma, pıtaiasj, kak giurza, vıučitjsia na perevodčika s francuzskogo i vozitj majonez iz Curiha v Piuhiaiarvi. Gedeerovskije reperı predskazujemı, vedj oni, faktičeski, sintezirujut predskazujemıj faktičeskij temp. Paradoksaljno, no Antiiisus počitajetsia v antiiudaizme. So šhunı sčastlivo veščajet vesnuščatıj iždivenec. Spechram im. Mustazhira Arzahaniana dlia shodok shimonahov v Chinvali.&lt;p&gt;Latinica (bez dyakritiki):&lt;p&gt;Major avyacii Curiupa Maja Iljinicxna vvela antiioniziruiuscxij dyelektrik v kostjutilj Sxauliajskogo strojupravlenija i, zxelaja pomocxj zxuri zxurnala “Zxiznj Zxulia Verna”, naotmasxj giljotinirovala sumassxedsxij triohjarusnij podjomnik jejo konvejera zxoltih parasxutov. Blizstojascxaja goszajomscxica, polucxivsxaja morziankoj eto speczadanije ot miniusta, obosxla bezimiannij tanczal szadi postindustryaljnogo Kiahtinskogo seljispolkoma, pitaiasj, kak giurza, viucxitjsia na perevodcxika s francuzskogo i vozitj majonez iz Curiha v Piuhiaiarvi. Gedeerovskije reperi predskazujemi, vedj oni, fakticxeski, sintezirujut predskazujemij fakticxeskij temp. Paradoksaljno, no Antiiisus pocxitajetsia v antiiudaizme. So sxhuni scxastlivo vescxajet vesnusxcxatij izxdivenec. Spechram im. Mustazhira Arzahaniana dlia shodok shimonahov v Chinvali.&lt;p&gt;Кириллица (после обратной конвертации):&lt;p&gt;Маёр авиации Цурюпа Мая Ильинична ввела антиионизирующий диэлектрик в костютиль Шауляйского строюправления и, желая помочь жури журнала «Жизнь Жуля Верна», наотмашь гильётинировала сумасшедший трёхьярусный подьёмник её конвеера жолтых парашутов. Близстоящая госзаёмщица, получившая морзянкой это спецзадание от минюста, обошла безымянный танцзал сзади постиндустриального Кяхтинского сельисполкома, пытаясь, как гюрза, выучиться на переводчика с французского и возить маёнез из Цуриха в Пюхяярви. Гедеэровские реперы предсказуемы, ведь они, фактически, синтезируют предсказуемый фактический темп. Парадоксально, но Антииисус почитается в антииудаизме. Со шхуны счастливо вещает веснущатый иждивенец. Спецхрам им. Мустазхира Арзаханяна для сходок схимонахов в Цхинвали.&lt;p&gt;Как видно, основной недостаток — искажения при попытке обратной конвертации в кириллицу. Кроме того, явно не логично написание слов «kostjutilj» (следовало бы «kostiutilj») и «miniust» (следовало бы «minjust»), но тут проблема в корявости исходного кириллического написания, и без введения словарных слов это не фиксится.&lt;p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <author>MonteDegro</author>
      <guid>https://habr.com/ru/articles/1082162/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082162</guid>
      <pubDate>Mon, 14 Sep 2026 15:54:34 +0000</pubDate>
    </item>
    <item>
      <title>Лучший хостинг для Node.js-приложений и Next.js-сайтов с SSR в 2026</title>
      <link>https://habr.com/ru/companies/amvera/articles/1082156/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082156</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;В статье мы рассмотрим такие специализированные PaaS-облака, как Vercel, Render, Railway и российское PaaS-облако Amvera для хостинга приложений на Node.js. В том числе особенности хостинга сайтов с SSR (рендерингом на стороне сервера) на React (Next.js, Remix, Astro), Vue (Nuxt.js, Quasar), Svelte и Angular. Рассмотрим сильные и слабые стороны сервисов. А также рассмотрим хостинг у VPS и Serverless-провайдеров. И приведём пример деплоя Node.js приложения с BullMQ.&lt;p&gt;&lt;em&gt;Раскрытие информации: в нашем блоге мы упомянем наш сервис, надеюсь на ваше понимание.&lt;/em&gt;&lt;h3&gt;Специализированные PaaS для хостинга Node.js&lt;/h3&gt;&lt;p&gt;Рассмотрим решения, специально предназначенные для хостинга Node.js и сайтов на Next.js&lt;ol start=2&gt;&lt;li&gt;&lt;p&gt;&lt;a href=https://vercel.com&gt;&lt;strong&gt;Vercel&lt;/strong&gt;&lt;/a&gt; – международный лидер для хостинга сайтов на Next.js. Компания является разработчиком Next и имеет самую тесную интеграцию своей инфраструктуры с этим фреймворком. Отдельно стоит отметить распределенную инфраструктуру с CDN, которая обеспечивает низкий пинг для глобальной аудитории.&lt;p&gt;Следует учитывать, что платные тарифы требуется оплачивать иностранными картами (российские карты сервис не принимает) и проекты, размещенные в Vercel, часто попадают под блокировки для российских пользователей, что затрудняет развертывание коммерческих проектов c аудиторией из России, где нужна гарантия доступности.&lt;li&gt;&lt;p&gt;&lt;a href=https://amvera.ru/nodejs&gt;&lt;strong&gt;Amvera&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt; – &lt;/strong&gt;российское PaaS-облако со встроенной поддержкой Node.js. В отличие от иностранных сервисов, Amvera можно оплачивать российской картой. В Amvera можно размещать приложения на серверах в ЦОД-ах в России (и в Европе и Америке), что даёт пинг для российских пользователей менее 30 мс и возможность соответствовать требованиям 152-ФЗ.&lt;p&gt;В Amvera есть встроенный CI/CD с деплоем через Git. Достаточно привязать репозиторий, сервис сам определит окружение, подберет конфигурацию, скачает зависимости из npm, pnpm или yarn, и запустит приложение или сайт в непрерывном контейнере. Постоянно работающие контейнеры позволят всегда держать код активным и избежать холодного старта.&lt;p&gt;Amvera это универсальное PaaS-облако с поддержкой Node.js (и других окружений), управляемыми PostgreSQL с бэкапами, Background Workers для BullMQ, возможностью деплоить как Frontend, так и Backend через коммиты в Git репозиторий или подключив Claude/Codex через MCP, Zero Downtime Deploy, мгновенные откаты сборок и другие функции ведущих PaaS-облаков. В сервисе можно развернуть как backend на Node.js, так и Frontend на Next.js без настройки и администрирования сервера.&lt;p&gt;Для работы с проектами прямо на платформе есть изолированный Sandbox с возможностью редактирования кода через встроенную online-IDE или специального ИИ-Помощника (AI App Builder) Polide.&lt;p&gt;В отличие от международных сервисов, таких как Vercel, проекты, размещенные в Amvera, не попадают под блокировки в России и всегда доступны российским пользователям.&lt;p&gt;По данным на сентябрь 2026 в Amvera было зарегистрировано 100 000 разработчиков, что делает Amvera лидером среди российских PaaS-сервисов.&lt;p&gt;Начать использовать Amvera можно бесплатно за счёт промобаланса, платные тарифы начинаются от 170 р. в месяц.&lt;li&gt;&lt;p&gt;&lt;a href=https://render.com&gt;&lt;strong&gt;Render&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt; &lt;/strong&gt;– универсальное PaaS облако, подходящее для веб-приложений на Node.js и имеющее сервис для Background Workers. Есть поддержка Node и деплой через Git. На бесплатном тарифе проекты засыпают через 15 минут неиспользования, а просыпаются около минуты, что делает не рекомендуемым использование бесплатного тарифа в проде. Платные тарифы начинаются от 7$ и требуют наличие иностранной карты (российские карты не поддерживаются).&lt;li&gt;&lt;p&gt;&lt;a href=https://railway.com&gt;&lt;strong&gt;Railway&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt; &lt;/strong&gt;– ещё одно универсальное PaaS облако. По концепции очень похоже на Render и Amvera. Есть встроенный CI/CD и все основные функции PaaS-облаков. Есть триал в размере 5$ в первый месяц и 1$ начиная со второго. Уложиться в Free Tier с постоянно работающим приложением почти невозможно, лучше сразу перейти на один из платных тарифов, начинающихся от 5$. Как и в других международных PaaS-сервисах, российские карты не поддерживаются и требуется наличие иностранной карты для оплаты.&lt;/ol&gt;&lt;h4&gt;Скорость ответа сайтов, размещенных в Vercel, Amvera, Render, Railway&lt;/h4&gt;&lt;p&gt;Если ваши пользователи из России, важным является время загрузки сайта. Это влияет и на пользовательский опыт и на продвижение сайта в поисках. Чем быстрее сайт, тем лучше он ранжируется.&lt;p&gt;Я провел эксперимент, сделав с помощью сервиса ping-admin пинг с 10 случайных точек в России до сайтов, расположенных в Amvera, Vercel, Render и Railway. Чтобы не загромождать статью, я приведу здесь только средний показатель. Но вы можете провести эксперимент самостоятельно.&lt;p&gt;Средний пинг до сайтов в секундах (чем меньше, тем лучше).&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;&lt;td&gt;&lt;p align=left&gt;Amvera&lt;td&gt;&lt;p align=left&gt;Vercel&lt;td&gt;&lt;p align=left&gt;Render&lt;td&gt;&lt;p align=left&gt;Railway&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Общее время ответа сек.&lt;td&gt;&lt;p align=left&gt;0.223&lt;td&gt;&lt;p align=left&gt;0.859&lt;td&gt;&lt;p align=left&gt;0.385&lt;td&gt;&lt;p align=left&gt;0.494&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Время ответа DNS сек.&lt;td&gt;&lt;p align=left&gt;0.054&lt;td&gt;&lt;p align=left&gt;0.462&lt;td&gt;&lt;p align=left&gt;0.120&lt;td&gt;&lt;p align=left&gt;0.333&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;SSL сек.&lt;td&gt;&lt;p align=left&gt;0.098&lt;td&gt;&lt;p align=left&gt;0.063&lt;td&gt;&lt;p align=left&gt;0.094&lt;td&gt;&lt;p align=left&gt;0.089&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Ожидание ответа сек.&lt;td&gt;&lt;p align=left&gt;0.025&lt;td&gt;&lt;p align=left&gt;0.269&lt;td&gt;&lt;p align=left&gt;0.098&lt;td&gt;&lt;p align=left&gt;0.040&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Как видно из таблицы скорости ответа сайтов для пользователей из России, лучший показатель у Amvera со средним значением 0,22 секунды. А худший у Vercel со значением 0,85 секунды. Render и Railway разместились посередине со значением в 0,38 и 0,49 секунды соответственно.&lt;h3&gt;Хостинг Node.js на VPS-сервере&lt;/h3&gt;&lt;p&gt;VPS-сервера позволяют запускать Node.js приложения, но обладают рядом недостатков.&lt;ul&gt;&lt;li&gt;&lt;p&gt;Вам потребуется самостоятельно настраивать и администрировать сервер.&lt;li&gt;&lt;p&gt;Это не специализированное решение, многие вещи придется делать самим.&lt;li&gt;&lt;p&gt;VPS обычно не включает CDN и глобальную edge-инфраструктуру из коробки. Если CDN необходим, его придётся подключать и настраивать отдельно.&lt;li&gt;&lt;p&gt;Вам потребуется подключать отдельные сервисы для мониторинга, CI/CD, бэкапов и многого другого.&lt;/ul&gt;&lt;p&gt;Но есть и преимуществ: проекты работают непрерывно, и у вас над ними полный контроль.&lt;h3&gt;Хостинг Node.js в Serverless&lt;/h3&gt;&lt;p&gt;Альтернативой является использование Serverless-провайдеров, таких как Cloudflare Workers. &lt;p&gt;Но важно учесть, что у приложений будет холодный старт, ваше ценообразование может быть непредсказуемым и запуск кода как функции накладывает некоторые ограничения, что может не позволить запустить некоторые приложения.&lt;p&gt;Serverless подходит для stateless HTTP-функций и проектов, которым не требуется постоянно работающий процесс. Но ограничения рантайм, времени выполнения, модели хранения состояния, сетевых возможностей и подключений к СУБД могут сделать serverless неудобным для ряда Node.js-приложений.&lt;h3&gt;Важные функции, которые должны быть у хостинга Node.js&lt;/h3&gt;&lt;p&gt;Следует отдельно оценить функции для сайтов на Next.js и Backend-приложений на Node.js.&lt;ul&gt;&lt;li&gt;&lt;p&gt;Самыми важными функциями является встроенная поддержка Node.js и возможность доставки кода через коммиты в привязанный Git. Это избавит вас от необходимости настройки и администрирования сервера. Всё это есть в таких PaaS-облаках, как Vercel, Amvera, Railway, Render.&lt;li&gt;&lt;p&gt;Наличие нужных регионов доступности и время пинга. Так, для международного рынка лучший пинг вы получите в Vercel, но если ваши пользователи из России, то лучший пинг будет у проектов, размещенных в Amvera.&lt;li&gt;&lt;p&gt;Находятся ли проекты, размещенные у провайдера под региональными блокировками. Так, в России блокируются домены проектов, размещенных в Vercel и частично в Railway и Render.&lt;li&gt;&lt;p&gt;Есть ли управляемые СУБД PostgreSQL и Redis с бэкапами. Такие СУБД есть в универсальных PaaS-облаках Railway, Render и Amvera.&lt;li&gt;&lt;p&gt;Есть ли Zero Downtime Deploy и откаты сборок. Данные функции есть в Vercel, Amvera, Railway, Render.&lt;li&gt;&lt;p&gt;Есть ли непрерывная работа кода или быстрый старт. Самый быстрый отклик после простоя будет в непрерывных контейнерах Railway и Amvera или у VPS-провайдеров. Но и специализированные решения, такие как Vercel или Cloudflare Workers могут обеспечить достаточно быстрый холодный старт.&lt;li&gt;&lt;p&gt;Если вам нужен BullMQ, есть ли у провайдера сервис для запуска Background Workers. Такой сервис есть у Render и Amvera.&lt;li&gt;&lt;p&gt;Есть ли поддержка npm, pnpm и yarn.&lt;li&gt;&lt;p&gt;Возможность оплатить российской картой. Если у вас нет иностранной картой, лучше сразу рассмотреть такие российские облака, как Amvera.&lt;li&gt;&lt;p&gt;Есть ли возможность горизонтального и вертикального масштабирования. Возможность есть в Vercel, Amvera, Railway, Render и у Serverless-провайдеров. &lt;/ul&gt;&lt;h3&gt;Что не подойдёт для хостинга Node.js&lt;/h3&gt;&lt;p&gt;Если ваш сайт требует рендеринга на стороне сервера, вам не подойдут Shared-хостинги. Ваше приложение должно либо работать непрерывно, либо очень быстро подниматься. По этой же причине не подойдёт бесплатный тариф в Render, так как минутный запуск после “засыпания”, это очень долго.&lt;h3&gt;Пример деплоя Node.js приложения с BullMQ&lt;/h3&gt;&lt;p&gt;Я приведу пример деплоя Node.js в PaaS-облако Amvera.&lt;p&gt;Подробно пример деплоя описан в инструкции на &lt;a href=https://github.com/amvera-academy/amvera-nodejs-example&gt;GitHub&lt;/a&gt;. Там же есть и код тестового приложения. Здесь мы рассмотрим только основные шаги.&lt;p&gt;Если у вас есть Node.js приложение, которое использует BullMQ, вам потребуется три проекта. Непосредственно для приложения, BullMQ и для Redis.&lt;p&gt;Для деплоя приложения вам потребуется привязать репозиторий в интерфейсе Amvera (делается одной кнопкой) или перетянуть файлы в интерфейсе.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/989/4fa/ed7/9894faed7fd9e92b5196db4e121183c6.png alt=&#34;Подключение репозитория для деплоя через Git&#34; title=&#34;Подключение репозитория для деплоя через Git&#34; width=1512 height=1362 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/989/4fa/ed7/9894faed7fd9e92b5196db4e121183c6.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/989/4fa/ed7/9894faed7fd9e92b5196db4e121183c6.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Подключение репозитория для деплоя через Git&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Amvera сама проанализирует код и предложит нужную конфигурацию. Останется только подтвердить деплой.&lt;p&gt;Для запуска BullMQ необходимо пройти те же шаги, только использовав код из другой &lt;a href=https://github.com/amvera-academy/amvera-nodejs-example/blob/main/BULLMQ.md&gt;инструкции&lt;/a&gt;.&lt;p&gt;Далее нужно запустить преднастроенный Redis – это делается “одной кнопкой” в интерфейсе. И прописать в переменной приложения с BullMQ  его хост.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/1a2/a04/3bd/1a2a043bdf8dea47062b070b87ed39be.png alt=&#34;Деплой управляемого Redis&#34; title=&#34;Деплой управляемого Redis&#34; width=1468 height=914 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/1a2/a04/3bd/1a2a043bdf8dea47062b070b87ed39be.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/1a2/a04/3bd/1a2a043bdf8dea47062b070b87ed39be.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Деплой управляемого Redis&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Будет примерно так &lt;pre&gt;&lt;code&gt;REDIS_URL=redis://:&amp;lt;пароль&amp;gt;@&amp;lt;внутренний-домен-redis&amp;gt;:6379/0&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:87px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Все, буквально за пять минут и без настройки сервера мы успешно опубликовали Node.js приложение, BullMQ и Redis в Amvera.&lt;h3&gt;На каком сервере разместить Node.js приложение и сайт с SSR в 2026&lt;/h3&gt;&lt;p&gt;Если ваши пользователи из России, или у вас нет иностранной карты, и вы хотите получить все преимущества PaaS-облака, рассмотрите Amvera. Amvera – это российский сервис, где есть поддержка Node.js, деплой через Git, управляемые PostgreSQL, Redis и другие СУБД. А также подробные метрики и логи работы приложения, настраиваемые алерты с рестартами приложения при падениях, деплой без даунтайма, откаты сборок одной кнопкой, ИИ-помощник и встроенное IDE для редактирования кода прямо на сервере и регионы в России, Европе и Америке. А главное, в Amvera есть возможность оплаты российской картой, соблюдать местное законодательство о хранении данных и обеспечить надежный доступ для пользователей из России.&lt;p&gt;Если ваш проект, это сайт на Next.js, ваши пользователи не из России и у вас есть иностранная карта, хорошим выбором будет Vercel.&lt;p&gt;А если ваш проект, это Backend-приложение и вы и ваши пользователи не из России, хорошим выбором будет Render или Railway.&lt;hr&gt;&lt;p&gt;Сравнительная таблица хостингов для Node.js приложений и сайтов с SSR&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;&lt;td&gt;&lt;p align=left&gt;Amvera&lt;td&gt;&lt;p align=left&gt;Vercel&lt;td&gt;&lt;p align=left&gt;Render&lt;td&gt;&lt;p align=left&gt;Railway&lt;td&gt;&lt;p align=left&gt;VPS&lt;td&gt;&lt;p align=left&gt;Serverless&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Встроенная поддержка Node.js&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Есть, но сервис больше ориентирован именно на Next.js&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Нет, максимум как предустановка&lt;td&gt;&lt;p align=left&gt;Частично есть&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Встроенная поддержка Next.js c SSR&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Нет&lt;td&gt;&lt;p align=left&gt;Частично есть&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Управляемый PostgreSQL&lt;td&gt;&lt;p align=left&gt;Есть с бэкапами,такими расширениями как pgVector, PostgREST и возможностью запуска нескольких реплик для надежности&lt;td&gt;&lt;p align=left&gt;Нет&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Нет, нужно подключать отдельно или поднимать самостоятельно&lt;td&gt;&lt;p align=left&gt;Нет, нужно подключать отдельно или поднимать самостоятельно&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Управляемый Redis&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Нет&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Нет, нужно подключать отдельно или поднимать самостоятельно&lt;td&gt;&lt;p align=left&gt;Нет, нужно подключать отдельно или поднимать самостоятельно&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Поддержка BullMQ&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Нет&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Нет (но можно запустить как приложение)&lt;td&gt;&lt;p align=left&gt;Нет, нужно подключать отдельно или поднимать самостоятельно&lt;td&gt;&lt;p align=left&gt;Нет, нужно подключать отдельно или поднимать самостоятельно&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Git Deploy&lt;td&gt;&lt;p align=left&gt;Есть. Можно как подключить GitHub, GitLab, BitBucket, так и использовать выделенный репозиторий Amvera&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Нет, нужно настраивать CI/CD&lt;td&gt;&lt;p align=left&gt;Зависит от вендора&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Встроенный Rollback&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Нет&lt;td&gt;&lt;p align=left&gt;Нет&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Zero-Downtime Deploy&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Нет, нужно настраивать самостоятельно&lt;td&gt;&lt;p align=left&gt;Нет&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Оплата российскими картами&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Нет&lt;td&gt;&lt;p align=left&gt;Нет&lt;td&gt;&lt;p align=left&gt;Нет&lt;td&gt;&lt;p align=left&gt;Да&lt;td&gt;&lt;p align=left&gt;Зависит от вендора&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Российские регионы&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Нет&lt;td&gt;&lt;p align=left&gt;Нет&lt;td&gt;&lt;p align=left&gt;Нет&lt;td&gt;&lt;p align=left&gt;Да, зависит от хостинга&lt;td&gt;&lt;p align=left&gt;Зависит от вендора&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;ИИ-помощник&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Нет&lt;td&gt;&lt;p align=left&gt;Есть&lt;td&gt;&lt;p align=left&gt;Нет&lt;td&gt;&lt;p align=left&gt;Нет&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Среднее время ответа проекта для пользователей из России (чем меньше, тем лучше)&lt;td&gt;&lt;p align=left&gt;0.223&lt;td&gt;&lt;p align=left&gt;0.859&lt;td&gt;&lt;p align=left&gt;0.385&lt;td&gt;&lt;p align=left&gt;0.494&lt;td&gt;&lt;p align=left&gt;Зависит от провайдера&lt;td&gt;&lt;p align=left&gt;Зависит от провайдера&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Когда использовать&lt;td&gt;&lt;p align=left&gt;Если вам нужно развернуть сайт c SSR и/или Node.js приложение, вы хотите оплачивать российской картой и/или ваши пользователи из России&lt;td&gt;&lt;p align=left&gt;Для хостинга &lt;a href=http://next.js&gt;Next.js&lt;/a&gt;, если вы и ваши пользователи не из России&lt;td&gt;&lt;p align=left&gt;Для хостинга Backend если вы и ваши пользователи не из России&lt;td&gt;&lt;p align=left&gt;Для хостинга Backend если вы и ваши пользователи не из России&lt;td&gt;&lt;p align=left&gt;Если вас не затрудняет самостоятельная настройка и администрирование инфраструктуры&lt;td&gt;&lt;p align=left&gt;Если вы хотите платить исключительно за фактическое использование ресурсов и готовы к ограничениям Serverless&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <guid>https://habr.com/ru/companies/amvera/articles/1082156/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082156</guid>
      <pubDate>Mon, 14 Sep 2026 15:47:15 +0000</pubDate>
    </item>
    <item>
      <title>ИИ делает аналитику доступной каждому, но значит ли это, что аналитиков станет меньше?</title>
      <link>https://habr.com/ru/articles/1079068/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1079068</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;Аналитика сейчас изменяется, если раньше чтобы понять в каких регионах сильнее всего снизились те же продажи, либо выросла текучесть нужно было неплохо так шарить за сбор данных, их обработку и статистику, то сейчас всё намного проще.&lt;p&gt;Мало того, что ИИ позволяет обрабатывать и разбирать данные намного быстрее, чем это делалось вручную, так еще и современные BI системы уже позволяют внутри себя задавать вопросы к данным. Это я узнал не так давно, так как сам с этим не сталкивался еще, но это натолкнуло меня на размышления аналитического бытия.&lt;h2&gt;Прощай часть работы&lt;/h2&gt;&lt;p&gt;Я это аналитикой не считаю, но тем не менее, многие это так и воспринимают, поэтому нужно об этом сказать.&lt;p&gt;Большой пласт задач называется аналитикой, потому что &lt;strong&gt;кто-то другой не может получить цифру&lt;/strong&gt;. Это может быть цифра продаж за март или сравнить показатель за пару лет. И большая часть таких запросов сама по себе не требует сложного аналитического мышления. Ценность здесь во многом заключается в том, что &lt;strong&gt;аналитик умеет воспользоваться инструментами&lt;/strong&gt; и может достаточно быстро достать нужную информацию.&lt;p&gt;И вот такого рода задачи уже вполне можно отдавать ИИ.&lt;p&gt;И да, если раньше подразделению N требовалось пять аналитиков, потому что руководители постоянно приходили к ним с небольшими запросами, выгрузками, графиками и разрезами, то с ИИ (после пару кругов настройки интеграций между системами учета, хранилищем и BI) такие запросы можно через личный кабинет в BI инструменте решать.&lt;p&gt;Очевидно, что в таком случае вместо пяти аналитиков может быть достаточно и трех.&lt;h2&gt;Доступ к аналитике и сама аналитика не одно и то же&lt;/h2&gt;&lt;p&gt;Едем дальше, уже непосредственно в аналитику.&lt;p&gt;Вообще, запрос от руководителя &amp;#34;Покажи регионы где выросла текучесть за полгода сильнее всего&amp;#34; достаточно сложен в реализации, потому что &lt;strong&gt;кто-то заранее должен был решить, что именно компания считает текучестью&lt;/strong&gt;.&lt;p&gt;И таких особенностей при работе практически с любой метрикой довольно много, пару на-вскидку:&lt;br&gt;• учитывать ли внутренние перевода;&lt;br&gt;• как учитывать организационные изменения;&lt;br&gt;• считать увольнения относительно средней или численности на последнюю дату периода;&lt;br&gt;• как учитывать подразделение сотрудника, который в середине месяца сменил команду.&lt;p&gt;И поэтому следующая мысль, что &lt;strong&gt;чем легче становится задавать вопросы к данным, тем важнее становится качества.&lt;/strong&gt;&lt;h2&gt;Одна фундаментальная проблема&lt;/h2&gt;&lt;p&gt;В большинстве своем &lt;strong&gt;пользователи не знают, что им нужно&lt;/strong&gt;. И если модель ИИ, которую использует компания слабая, то на запрос в стиле &amp;#34;Глянь что с продажами происходит&amp;#34; получится очень неплохой бульон.&lt;p&gt;Вообще, значительная часть хорошей аналитики заключается в том, чтобы сначала понять, &lt;strong&gt;какой вопрос вообще нужно задать&lt;/strong&gt;. И здесь ИИ пока скорее усиливает хорошего специалиста, чем полностью его заменяет.&lt;p&gt;Да и далеко не каждый пользователь будет гонять ИИ с просьбой дополнительно сделать Z-оценку или убедиться в статистической значимости или аномальность изменений проверить через robust z.&lt;h2&gt;Плохая аналитика стала дешевой&lt;/h2&gt;&lt;p&gt;В продолжение прошлого раздела. Раньше для того, чтобы сделать плохой анализ, человеку хотя бы требовалось потратить на него некоторое время, а сейчас можно попросить ИИ посчитать корреляции, сделать несколько графиков, выделить основные факторы и написать десять выводов буквально за пару минут.&lt;p&gt;Это реально круто, но есть риски в таком подходе, потому что:&lt;br&gt;• корреляция не означает причинность;&lt;br&gt;• среднее может быть бессмысленным для перекошенного распределения;&lt;br&gt;• неполные периоды нельзя сравнивать между собой;&lt;br&gt;• хороший коэффициент модели вовсе не означает, что из нее можно делать бизнес-выводы.&lt;p&gt;Если прям очень кратко, то такое &lt;strong&gt;распространение аналитики может способствовать искаженным выводам&lt;/strong&gt;.&lt;h2&gt;Если ИИ станет очень хорошим&lt;/h2&gt;&lt;p&gt;Есть еще вариант, когда компания создает настолько качественный и что важно - единый слой данных, всё стандартизирует и хорошо описывает бизнес-логику. И туда еще ИИ ассистента добавляет.&lt;p&gt;И реально, большинство пользователей начинают самостоятельно получать большую часть нужной им аналитической информации по щелчку ЛКМ.&lt;p&gt;Такой апокалипсис для аналитика.&lt;p&gt;И тут, конечно много аналитиков попадет под удар. Вместо условных 50 аналитиков компании может будет нужно 15. Но скорее всего эти 15 будут &lt;strong&gt;сильными аналитиками&lt;/strong&gt;: будут решать более сложные задачи и скорее всего будут стоить дороже (но это не точно).&lt;p&gt;Такой сценарий мне кажется вполне реалистичным, особенно для компаний, где аналитические подразделения сегодня во многом заняты производством отчетов и поддержкой похожих дашбордов.&lt;p&gt;Я бы сказал, что скорее всего будет некоторая поляризация:&lt;br&gt;• сильные аналитики будут более востребованными и смогут удержаться в компаниях даже после внедрения ИИ;&lt;br&gt;• а менее квалифицированные скорее всего сменят работу в те компании, где еще нет ИИ;&lt;br&gt;• вход в профессию будет немного осложнен и самих аналитиков будет меньше.&lt;p&gt;Я регулярно разбираю такие темы в своём &lt;a href=https://t.me/prostoanalytics rel=&#34;noopener nofollow&#34;&gt;Telegram‑канале&lt;/a&gt;, если вам интересно глубже понимать аналитику, экономику и рынок труда, там регулярно выходят короткие заметки и обзоры актуальных событий.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <author>prostoanalytics</author>
      <guid>https://habr.com/ru/articles/1079068/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1079068</guid>
      <pubDate>Mon, 14 Sep 2026 15:47:02 +0000</pubDate>
    </item>
    <item>
      <title>EvertyDisplay: что я сделал и зачем</title>
      <link>https://habr.com/ru/articles/1082154/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082154</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;У меня два монитора, и я давно привык воспринимать их не как отдельные устройства, а как единое рабочее пространство. На одном код, на другом браузер, документация, терминал или что-то ещё.&lt;p&gt;И однажды я подумал: а почему это пространство вообще должно заканчиваться там, где заканчивается физический монитор?&lt;div class=tm-iframe_temp data-src=https://embedd.srv.habr.com/iframe/6aa811f0524c0c1e1c8050f3 data-style id=6aa811f0524c0c1e1c8050f3 width data-habr-games&gt;&lt;/div&gt;&lt;p&gt;Так появился EvertyDisplay.&lt;p&gt;Я сделал приложение для Windows, которое создаёт настоящие виртуальные дисплеи. Для системы это полноценные мониторы: у них есть собственное разрешение, координаты, на них можно переносить окна, запускать приложения в полноэкранном режиме и перемещать между ними курсор.&lt;p&gt;Но физически этих экранов рядом с вами нет.&lt;p&gt;Например, я могу расположить виртуальный дисплей слева от своего настоящего монитора. Двигаю мышь к левому краю — и перехожу туда. Перетаскиваю окно — оно уходит вместе со мной. Windows при этом считает, что всё совершенно нормально.&lt;p&gt;Чтобы виртуальное пространство не превращалось просто в склад невидимых окон, я добавил Live PiP и полноэкранный Viewport. Можно быстро посмотреть содержимое виртуального дисплея или открыть его на физическом экране, не меняя расположение самих приложений.&lt;p&gt;Есть управление пространственной топологией дисплеев, перенос окон через границы, автоматический фокус, защита для полноэкранных игр, восстановление расположения экранов и работа после перезапуска системы.&lt;p&gt;Большая часть EvertyDisplay написана на Rust. Внутри много Win32, IPC, хуков ввода, Direct3D 11, DXGI и работы с состоянием Windows. Сам виртуальный видеоадаптер используется как отдельный подписанный компонент, а EvertyDisplay уже строит поверх него пользовательскую логику и рабочее пространство.&lt;p&gt;Но техническая часть для меня здесь всё-таки вторична.&lt;p&gt;Главная идея была простой: проверить, обязательно ли цифровому пространству иметь физический экран.&lt;p&gt;Оказалось, что нет.&lt;p&gt;Если ты несколько раз отправил окно «влево», а потом возвращался туда курсором, мозг довольно быстро начинает воспринимать это место как существующее.&lt;p&gt;На столе там ничего нет.&lt;p&gt;А в компьютере — есть.&lt;p&gt;И именно это ощущение я хотел получить, когда начинал EvertyDisplay.&lt;p&gt;Я Артур Валиев, и не буду постить длинных философских статей. Просто делюсь крутой программой.&lt;p&gt;&lt;br&gt;&lt;a href=https://github.com/vaalimusic/EvertyDisplayLite rel=&#34;noopener nofollow&#34;&gt;https://github.com/vaalimusic/EvertyDisplayLite&lt;/a&gt;&lt;br&gt;В релизах можно скачать exe и познакомиться с системными требованиями.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <author>vaalimusic</author>
      <guid>https://habr.com/ru/articles/1082154/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082154</guid>
      <pubDate>Mon, 14 Sep 2026 15:32:49 +0000</pubDate>
    </item>
    <item>
      <title>HR спрашивают, зачем я задаю вопрос о чувствах на собеседовании</title>
      <link>https://habr.com/ru/articles/1082152/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082152</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;HR спрашивают, зачем я задаю вопрос о чувствах на собеседовании&lt;p&gt;Я руковожу поддержкой пользователей банка и лично утверждаю инженеров для головного офиса. Это лицо компании и площадка, где нередко приходится решать самые сложные кейсы. Конфликты и эскалации неприемлемы. На эти позиции мы набираем только сеньоров и предъявляем к ним повышенные требования.&lt;p&gt;До меня кандидаты доходят уже после технического интервью. На своём этапе я больше внимания уделяю социальным навыкам, soft skills. Один из вопросов, которые я традиционно задаю на собеседовании, касается чувств: «Назовите три чувства, которые прямо сейчас испытываете».&lt;p&gt;Даже многих сеньоров этот вопрос ставит в тупик. Одни заметно нервничают, другие не могут назвать хотя бы одно чувство. Бывает и ответ «счастье, радость, вдохновение». На этом этапе собеседования он звучит для меня настораживающе. Волнение или страх на собеседовании вполне обычны. А после неожиданного вопроса о чувствах человек может испытать растерянность, ступор или даже беспомощность.&lt;p&gt;При этом чувства могут быть смешанными. Например, интерес здесь вполне уместен. Человек знакомится с новой компанией, задаёт вопросы, примеряет к себе будущую работу. Он может одновременно интересоваться происходящим и тревожиться о том, как проходит разговор. Профиль чувств у каждого человека свой.&lt;p&gt;А зачем вы это спрашиваете?&lt;br&gt;С таким вопросом ко мне несколько раз приходили рекрутёры и даже HRBP.&lt;p&gt;Мне важно, понимает ли человек, что такое чувство в принципе, оперирует ли этими понятиями, может ли различить их оттенки и интенсивность. И если человеку трудно ответить, я могу дать подсказку. Я не проверяю правдивость и не жду одинаковых ответов. Смотрю, можно ли с человеком об этом поговорить.&lt;p&gt;В конце интервью я часто задаю этот вопрос ещё раз. К этому моменту интриги уже нет, я дал обратную связь, и она, как правило, позитивная. Профиль чувств часто меняется. Теперь облегчение, радость и даже вдохновение вполне понятны мне в контексте разговора.&lt;p&gt;Если человек почувствовал разницу и смог её описать, для меня это очень хороший знак. Умение замечать своё состояние важно и после собеседования. В работе оно помогает понять, когда накапливается напряжение и пора восстановить силы. Мне важно, чтобы человек был в ладах с собой и умел восстанавливаться, а не жил от отпуска до отпуска. Поэтому этот разговор даёт мне материал для выбора и помогает понять, сможет ли человек получать удовольствие от нашей работы, сохраняя силы на жизнь за её пределами. Именно это я имею в виду, когда говорю, что ему должно быть хорошо у нас в компании.&lt;p&gt;Для работы с пользователями важно понимать, в каком состоянии человек обращается за помощью и как это может влиять на общение.&lt;p&gt;Представим, что у пользователя не открывается документ перед важной встречей. Для инженера это знакомая неисправность, рутина. А человек волнуется, боится последствий. Он может вести себя агрессивно, токсично или, напротив, давить на жалость, занимать детскую позицию. За таким поведением может стоять сознательная попытка повлиять на инженера, но оно может быть и непроизвольной реакцией на стресс.&lt;p&gt;Пользователь приходит в поддержку уже в определённом эмоциональном состоянии. Оно могло возникнуть задолго до разговора и вообще не иметь отношения к инженеру. Если это учитывать, резкий тон в разговоре или эмоциональное сообщение в переписке не обязательно воспринимать как личную претензию.&lt;p&gt;Угадывать чужие чувства не нужно. Важно допускать, что за словами обращения есть тревога, растерянность или усталость. Тогда проще выбрать, как помочь. Уточнить, что беспокоит человека, спокойно объяснить следующий шаг или договориться, когда вернёшься с ответом. Иногда достаточно продолжать решать вопрос, не принимая чужого раздражения на свой счёт.&lt;p&gt;Разбираться в собственных чувствах инженеру важно и для своей эффективности. Повторный вопрос пользователя может злить. Недоверие к предложенному решению может задевать. Если инженер замечает собственную реакцию, у него больше возможностей выбрать ответ, а не автоматически сказать «я же вам уже объяснил».&lt;p&gt;Здесь же начинается работа со своим стрессом. Полезно различать, раздражает ли тебя конкретный разговор или ты уже пришёл к нему уставшим и напряжённым. Это помогает понять, когда нужно сделать паузу или обратиться за помощью. Работу со своими эмоциональными реакциями называют эмоциональной саморегуляцией.&lt;p&gt;Поэтому вопрос на собеседовании для меня шире проверки словарного запаса. Один вопрос не измеряет всё это, но позволяет начать разговор о навыках, которые я считаю базовыми для инженера поддержки пользователей.&lt;p&gt;Мне нужен технически сильный человек, который умеет работать с людьми, понимает собственные реакции и способен восстанавливаться после напряжённых разговоров. При выборе важно и то, насколько наша работа и среда подходят ему самому.&lt;p&gt;Для меня настоящая продуктивность выглядит так. Инженер утром с радостью идёт на работу, а вечером с радостью уходит с работы. Он умеет и любит работать, но у него остаются силы и на жизнь после работы. Он способен восстанавливаться и регулировать себя.&lt;p&gt;А какие три чувства испытываете прямо сейчас вы?&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <author>dmitry-pivovarov</author>
      <guid>https://habr.com/ru/articles/1082152/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082152</guid>
      <pubDate>Mon, 14 Sep 2026 15:32:05 +0000</pubDate>
    </item>
    <item>
      <title>Как автоматизировать VLESS + Reality на старом Intel iMac с macOS Catalina 10.15.8: AUTO Xray</title>
      <link>https://habr.com/ru/articles/1082148/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082148</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;Ранее я описал способ запуска VLESS + Reality на Intel iMac с macOS Catalina 10.15.8: &lt;a href=https://habr.com/ru/articles/1027620/ rel=&#34;noopener nofollow&#34;&gt;«Как запустить VLESS + Reality на старом Intel iMac с macOS Catalina 10.15.8»&lt;/a&gt;.&lt;p&gt;Решение через V2RayXS и Xray-core оказалось рабочим, но требовало много ручных действий: получить подписку, разобрать VLESS-ссылки, сформировать JSON-конфигурации, импортировать узлы и настроить системный proxy.&lt;p&gt;Схема выглядела примерно так:&lt;pre&gt;&lt;code&gt;subscription&#xA;    ↓&#xA;vless://&#xA;    ↓&#xA;разбор параметров&#xA;    ↓&#xA;JSON&#xA;    ↓&#xA;V2RayXS&#xA;    ↓&#xA;Xray&#xA;    ↓&#xA;macOS system proxy&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:87px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Для разовой настройки этого достаточно. Для постоянного использования — не очень удобно.&lt;p&gt;Следующим шагом стала автоматизация этой цепочки.&lt;p&gt;Так появился &lt;strong&gt;AUTO Xray&lt;/strong&gt;.&lt;p&gt;Теперь пользовательский сценарий выглядит так:&lt;pre&gt;&lt;code&gt;subscription URL&#xA;    ↓&#xA;Update subscription&#xA;    ↓&#xA;выбор региона или сервера&#xA;    ↓&#xA;Enable&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/f3b/3d5/069/f3b3d5069191fdccf0158a823a3cec1b.png alt=&#34;AUTO Xray на iMac Late 2013 с macOS Catalina 10.15.8&#34; title=&#34;AUTO Xray на iMac Late 2013 с macOS Catalina 10.15.8&#34; width=1049 height=552 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/f3b/3d5/069/f3b3d5069191fdccf0158a823a3cec1b.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/f3b/3d5/069/f3b3d5069191fdccf0158a823a3cec1b.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;AUTO Xray на iMac Late 2013 с macOS Catalina 10.15.8&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;h3&gt;Задача AUTO Xray&lt;/h3&gt;&lt;p&gt;AUTO Xray не задумывался как универсальный VPN-клиент. Цель была значительно уже: сделать удобный клиент VLESS + Reality для старых Intel Mac, оставшихся на macOS Catalina.&lt;p&gt;Целевая конфигурация:&lt;pre&gt;&lt;code&gt;Intel x86_64&#xA;macOS Catalina 10.15.x&#xA;VLESS + Reality&#xA;macOS system proxy&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Вся остальная техническая часть должна оставаться внутри приложения.&lt;h3&gt;Что получилось: AUTO Xray&lt;/h3&gt;&lt;p&gt;AUTO Xray — небольшое приложение для строки меню macOS.&lt;p&gt;Состояние видно по иконке в строке меню: яркий голубь — &lt;strong&gt;ON&lt;/strong&gt;, полупрозрачный — &lt;strong&gt;OFF&lt;/strong&gt;.&lt;p&gt;Нажатие на иконку открывает основное меню приложения. Отдельного окна настроек для повседневной работы не требуется.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/a60/c66/70e/a60c6670e4a9753daf0b878f4d1f5502.png alt=&#34;Основное меню AUTO Xray: автоматический выбор RF / EU / World и ручной выбор узла&#34; title=&#34;Основное меню AUTO Xray: автоматический выбор RF / EU / World и ручной выбор узла&#34; width=1472 height=1069 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/a60/c66/70e/a60c6670e4a9753daf0b878f4d1f5502.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/a60/c66/70e/a60c6670e4a9753daf0b878f4d1f5502.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Основное меню AUTO Xray: автоматический выбор RF / EU / World и ручной выбор узла&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;При обновлении подписки AUTO Xray самостоятельно разбирает полученные узлы и распределяет их по группам:&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;RF&lt;/strong&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;EU&lt;/strong&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;World&lt;/strong&gt;&lt;/ul&gt;&lt;p&gt;После этого можно выбрать группу для автоматического выбора подходящего сервера либо указать конкретный узел вручную.&lt;h3&gt;Что происходит под капотом&lt;/h3&gt;&lt;p&gt;За работу с VLESS + Reality отвечает Xray-core. AUTO Xray управляет его конфигурацией, запуском и интеграцией с сетевыми настройками macOS.&lt;p&gt;При включении соединения приложение формирует конфигурацию для выбранного узла и запускает Xray-core с двумя локальными входами:&lt;pre&gt;&lt;code&gt;HTTP/HTTPS → 127.0.0.1:9001&#xA;SOCKS5     → 127.0.0.1:2081&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;После запуска Xray AUTO Xray через штатный &lt;code&gt;networksetup&lt;/code&gt; настраивает HTTP, HTTPS и SOCKS proxy для сетевых сервисов macOS.&lt;p&gt;В упрощённом виде маршрут трафика выглядит так:&lt;pre&gt;&lt;code&gt;Приложение&#xA;    ↓&#xA;macOS system proxy&#xA;    ↓&#xA;127.0.0.1&#xA;    ↓&#xA;Xray-core&#xA;    ↓&#xA;VLESS + Reality&#xA;    ↓&#xA;сервер&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Из этой архитектуры следует важное ограничение: AUTO Xray — &lt;strong&gt;system proxy client, а не TUN VPN&lt;/strong&gt;.&lt;p&gt;Программы, использующие системные proxy-настройки macOS, направляют трафик через Xray. Приложения, которые игнорируют системный proxy или используют собственный сетевой стек, могут работать напрямую.&lt;h3&gt;Устойчивость на Catalina&lt;/h3&gt;&lt;p&gt;Отдельная задача — поведение приложения при сетевых сбоях.&lt;p&gt;Например, после отключения и повторного подключения Wi-Fi процесс Xray может оставаться запущенным, но рабочий маршрут через proxy уже не гарантирован.&lt;p&gt;Поэтому в AUTO Xray есть supervisor, который периодически проверяет прохождение трафика через локальный SOCKS proxy. Если несколько проверок подряд завершаются ошибкой, runtime восстанавливается автоматически.&lt;p&gt;Есть и обратная проблема: Xray уже остановлен, а macOS продолжает считать системный proxy включённым. В этом случае приложения пытаются отправлять трафик на локальный адрес, где уже никто не слушает, и интернет фактически перестаёт работать.&lt;p&gt;AUTO Xray проверяет такое состояние и отключает оставшиеся HTTP, HTTPS и SOCKS proxy, если процесс Xray больше не работает.&lt;p&gt;Кроме того, при перезапуске очищаются оставшиеся процессы Xray, которые могут удерживать локальные порты после некорректного завершения.&lt;h3&gt;Установка на Catalina&lt;/h3&gt;&lt;p&gt;Основной способ установки — через DMG из GitHub Releases.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/3ce/376/13b/3ce37613b540f2c8a1b15c00a847dca3.png alt=&#34;Установочный образ AUTO Xray 2.5.20&#34; title=&#34;Установочный образ AUTO Xray 2.5.20&#34; width=782 height=494 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/3ce/376/13b/3ce37613b540f2c8a1b15c00a847dca3.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/3ce/376/13b/3ce37613b540f2c8a1b15c00a847dca3.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Установочный образ AUTO Xray 2.5.20&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Порядок такой:&lt;ol&gt;&lt;li&gt;&lt;p&gt;Скачать &lt;a href=https://github.com/eigenheit/auto-xray/releases/download/v2.5.20/AUTO_Xray_Catalina_v2.5.20.dmg rel=&#34;noopener nofollow&#34;&gt;&lt;code&gt;AUTO_Xray_Catalina_v2.5.20.dmg&lt;/code&gt;&lt;/a&gt;.&lt;li&gt;&lt;p&gt;Открыть образ.&lt;li&gt;&lt;p&gt;Запустить &lt;strong&gt;Install AUTO Xray.app&lt;/strong&gt; через &lt;code&gt;Control-click → Open&lt;/code&gt;.&lt;li&gt;&lt;p&gt;Если Catalina в первом предупреждении показывает только &lt;strong&gt;Cancel&lt;/strong&gt;, закрыть его и повторить &lt;code&gt;Control-click → Open&lt;/code&gt;.&lt;li&gt;&lt;p&gt;Во втором предупреждении подтвердить запуск кнопкой &lt;strong&gt;Open&lt;/strong&gt;.&lt;/ol&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/050/af1/77c/050af177cf455f92b6047472dc380a7d.png alt=&#34;Повторный запуск через Control-click → Open позволяет подтвердить запуск в Gatekeeper&#34; title=&#34;Повторный запуск через Control-click → Open позволяет подтвердить запуск в Gatekeeper&#34; width=881 height=457 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/050/af1/77c/050af177cf455f92b6047472dc380a7d.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/050/af1/77c/050af177cf455f92b6047472dc380a7d.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Повторный запуск через Control-click → Open позволяет подтвердить запуск в Gatekeeper&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Приложение пока не подписано сертификатом Apple Developer ID, поэтому такой сценарий запуска ожидаем.&lt;p&gt;После установки AUTO Xray запускается в состоянии &lt;strong&gt;OFF&lt;/strong&gt;.&lt;p&gt;Если DMG по какой-то причине не открывается или Gatekeeper продолжает блокировать запуск, в релизе есть резервный ZIP-инсталлятор:&lt;pre&gt;&lt;code&gt;AUTO_Xray_Catalina_Installer_v2.5.20.zip&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Он содержит установочный скрипт &lt;code&gt;INSTALL_AUTO_XRAY_CATALINA.command&lt;/code&gt;.&lt;h3&gt;Состав релиза&lt;/h3&gt;&lt;p&gt;В AUTO Xray включён &lt;strong&gt;Xray-core 1.8.4&lt;/strong&gt; — отдельно устанавливать или загружать его не требуется.&lt;p&gt;При сборке Xray-core проверяется по SHA-256 перед включением в пакет.&lt;p&gt;Вместе с каждым релизом публикуется &lt;code&gt;SHA256SUMS.txt&lt;/code&gt; с контрольными суммами DMG и ZIP.&lt;h3&gt;Итог&lt;/h3&gt;&lt;p&gt;Текущая стабильная версия — &lt;strong&gt;AUTO Xray 2.5.20&lt;/strong&gt;.&lt;p&gt;Проект и готовые сборки опубликованы на GitHub:&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href=https://github.com/eigenheit/auto-xray rel=&#34;noopener nofollow&#34;&gt;https://github.com/eigenheit/auto-xray&lt;/a&gt;&lt;p&gt;&lt;strong&gt;Releases:&lt;/strong&gt; &lt;a href=https://github.com/eigenheit/auto-xray/releases rel=&#34;noopener nofollow&#34;&gt;https://github.com/eigenheit/auto-xray/releases&lt;/a&gt;&lt;p&gt;Если у вас остался Intel Mac с Catalina и есть возможность проверить AUTO Xray на другой конфигурации, результаты тестирования и найденные проблемы можно оставить в Issues проекта.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <author>OKTHEOK</author>
      <guid>https://habr.com/ru/articles/1082148/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082148</guid>
      <pubDate>Mon, 14 Sep 2026 15:18:47 +0000</pubDate>
    </item>
    <item>
      <title>Как удалить объект с фото с помощью нейросети: тестируем 5 ИИ-сервисов и выбираем лучший</title>
      <link>https://habr.com/ru/companies/bothub/articles/1082144/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082144</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;Иногда фотографию не нужно полностью переделывать — достаточно убрать один лишний объект. В бытовом снимке это может быть случайный прохожий или кружка на столе, а в работе с контентом, рекламой, товарами или визуальными материалами часто требуется получить чистое изоб ражение без людей, предметов и других деталей, которые мешают композиции или не должны попадать в кадр. &lt;p&gt;Раньше такие объекты приходилось аккуратно удалять в графическом редакторе, а сегодня достаточно выделить их кистью или описать нужную правку текстом — нейросеть удалит объект и восстановит фон. Но «удалить» еще не значит получить пригодное изображение. На месте объекта могут остаться размытые пятна, обрывки контуров, искаженные текстуры или неправильно восстановленные детали. &lt;p&gt;Поэтому мы проверили пять ИИ-сервисов на трех фотографиях разной сложности и оценили именно первый результат — без повторной генерации и ручной ретуши.&lt;p&gt;&lt;strong&gt;Короткий ответ:&lt;/strong&gt; Короткий ответ: лучший результат показал &lt;a href=https://bothub.ru/gemini-3.1-flash-image&gt;Nano Banana&lt;/a&gt; — модель чисто удалила все три объекта и наиболее аккуратно восстановила сложный фон. ChatGPT занял второе место: результаты тоже получились качественными, но в тесте с велосипедом модель дорисовала лишнюю стойку ограждения и некорректно восстановила освещение. &lt;h2&gt;Как мы тестировали нейросети для удаления объектов&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Для теста выбрали три сценария&lt;/strong&gt;:&lt;ol&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Простой объект:&lt;/strong&gt; красная кружка на деревянном столе.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Сложный фон:&lt;/strong&gt; женщина в центре улицы — после удаления нужно восстановить дорогу, перспективу и детали позади нее.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Сложная геометрия:&lt;/strong&gt; велосипед на фоне каменной стены и металлического ограждения. Спицы, рама и колеса пересекают сразу несколько фактур и прямых линий.&lt;/ol&gt;&lt;p&gt;Во всех сервисах оценивали именно первый результат. Мы не запускали обработку повторно, не исправляли артефакты вручную и не выбирали лучший вариант из нескольких генераций. В инструментах с кистью объект выделяли целиком вместе с заметной тенью. Сервисам с текстовым управлением отправляли одинаковые промпты:&lt;p&gt;&lt;strong&gt;Кружка:&lt;/strong&gt; «Удали красную кружку со стола. Реалистично восстанови поверхность стола и фон на ее месте. Остальные элементы изображения не изменяй».&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/af6/3b5/5fa/af63b55faf75a21138c11649571b2ae8.png alt=&#34;Красная кружка на деревянном столе — исходное фото для теста нейросетей на удаление объектов.&#34; title=&#34;Красная кружка на деревянном столе — исходное фото для теста нейросетей на удаление объектов.&#34; width=1999 height=1972 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/af6/3b5/5fa/af63b55faf75a21138c11649571b2ae8.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/af6/3b5/5fa/af63b55faf75a21138c11649571b2ae8.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Красная кружка на деревянном столе — исходное фото для теста нейросетей на удаление объектов.&lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;&lt;strong&gt;Женщина:&lt;/strong&gt; «Удали женщину в центре фотографии. Реалистично восстанови дорогу и фон за ней, сохрани перспективу, освещение и окружающие детали. Остальные элементы изображения не изменяй».&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/925/6ac/2fa/9256ac2fac567123c01c6294b1b81ce2.png alt=&#34;Женщина в центре городской улицы — исходное фото для теста удаления человека нейросетью.&#34; title=&#34;Женщина в центре городской улицы — исходное фото для теста удаления человека нейросетью.&#34; width=1200 height=630 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/925/6ac/2fa/9256ac2fac567123c01c6294b1b81ce2.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/925/6ac/2fa/9256ac2fac567123c01c6294b1b81ce2.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Женщина в центре городской улицы — исходное фото для теста удаления человека нейросетью.&lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;&lt;strong&gt;Велосипед:&lt;/strong&gt; «Удали велосипед с фотографии. Восстанови закрытые им части каменной стены, металлического ограждения и фона, сохрани геометрию, текстуры, перспективу и освещение. Остальные элементы изображения не изменяй».&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/347/a17/8f1/347a178f193f418584935708e2d5a6fe.png alt=&#34;Велосипед у каменной стены и металлического ограждения — исходное фото для сложного теста удаления объекта. &#34; title=&#34;Велосипед у каменной стены и металлического ограждения — исходное фото для сложного теста удаления объекта. &#34; width=1333 height=1999 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/347/a17/8f1/347a178f193f418584935708e2d5a6fe.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/347/a17/8f1/347a178f193f418584935708e2d5a6fe.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Велосипед у каменной стены и металлического ограждения — исходное фото для сложного теста удаления объекта. &lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;&lt;strong&gt;Каждый сервис получил до пяти баллов по четырем критериям&lt;/strong&gt;:&lt;ul&gt;&lt;li&gt;&lt;p&gt;полнота удаления объекта;&lt;li&gt;&lt;p&gt;качество восстановления фона;&lt;li&gt;&lt;p&gt;сохранность деталей вне области редактирования;&lt;li&gt;&lt;p&gt;удобство: скорость, количество действий, регистрация и ограничения.&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Максимальная оценка — 20 баллов&lt;/strong&gt;. Условия и цены проверены 10 сентября 2026 года и могут меняться в зависимости от страны, платформы и способа оплаты.&lt;h2&gt;Результаты теста: коротко&lt;/h2&gt;&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;&lt;p&gt;&lt;strong&gt;Сервис&lt;/strong&gt;&lt;th&gt;&lt;p&gt;&lt;strong&gt;Удаление&lt;/strong&gt;&lt;th&gt;&lt;p&gt;&lt;strong&gt;Фон&lt;/strong&gt;&lt;th&gt;&lt;p&gt;&lt;strong&gt;Детали&lt;/strong&gt;&lt;th&gt;&lt;p&gt;&lt;strong&gt;Удобство&lt;/strong&gt;&lt;th&gt;&lt;p&gt;&lt;strong&gt;Итог&lt;/strong&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Nano Banana&lt;td&gt;&lt;p align=left&gt;5/5&lt;td&gt;&lt;p align=left&gt;5/5&lt;td&gt;&lt;p align=left&gt;5/5&lt;td&gt;&lt;p align=left&gt;5/5&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;20/20&lt;/strong&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;ChatGPT&lt;td&gt;&lt;p align=left&gt;5/5&lt;td&gt;&lt;p align=left&gt;5/5&lt;td&gt;&lt;p align=left&gt;4/5&lt;td&gt;&lt;p align=left&gt;5/5&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;19/20&lt;/strong&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Photoroom Retouch&lt;td&gt;&lt;p align=left&gt;4/5&lt;td&gt;&lt;p align=left&gt;4/5&lt;td&gt;&lt;p align=left&gt;3/5&lt;td&gt;&lt;p align=left&gt;5/5&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;16/20&lt;/strong&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;&lt;a href=http://Cleanup.pictures&gt;Cleanup.pictures&lt;/a&gt;&lt;td&gt;&lt;p align=left&gt;4/5&lt;td&gt;&lt;p align=left&gt;3/5&lt;td&gt;&lt;p align=left&gt;2/5&lt;td&gt;&lt;p align=left&gt;4/5&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;13/20&lt;/strong&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Pixelcut&lt;td&gt;&lt;p align=left&gt;3/5&lt;td&gt;&lt;p align=left&gt;2/5&lt;td&gt;&lt;p align=left&gt;2/5&lt;td&gt;&lt;p align=left&gt;4/5&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;11/20&lt;/strong&gt;&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;h3&gt;Cleanup.pictures — быстро, но с потерей деталей&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Качество:&lt;/strong&gt; 2/5 на простых объектах, 2/5 на сложной геометрии.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Оригинальный размер:&lt;/strong&gt; только в Pro; бесплатный экспорт ограничен 720 px.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Стоимость:&lt;/strong&gt; бесплатно в низком разрешении; Pro — 5 $ в месяц или 36 $ в год.&lt;/ul&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/66b/fa0/b5a/66bfa0b5ac49d5408140d508c94afcb8.png alt=&#34;Интерфейс Cleanup.pictures для удаления объектов с фотографии с помощью кисти.&#34; title=&#34;Интерфейс Cleanup.pictures для удаления объектов с фотографии с помощью кисти.&#34; width=1903 height=955 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/66b/fa0/b5a/66bfa0b5ac49d5408140d508c94afcb8.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/66b/fa0/b5a/66bfa0b5ac49d5408140d508c94afcb8.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Интерфейс &lt;/em&gt;&lt;a href=http://Cleanup.pictures&gt;&lt;em&gt;Cleanup.pictures&lt;/em&gt;&lt;/a&gt;&lt;em&gt; для удаления объектов с фотографии с помощью кисти.&lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;&lt;a href=http://Cleanup.pictures&gt;&lt;strong&gt;Cleanup.pictures&lt;/strong&gt;&lt;/a&gt; — минималистичный онлайн-инструмент без обязательной регистрации. После загрузки фотографии достаточно настроить размер кисти и провести по лишнему предмету: обработка начинается сразу после мазка. &lt;p&gt;С кружкой сервис справился неплохо, но следы обработки хорошо заметны: предмет исчез полностью, однако на его месте изменилась фактура деревянной поверхности, появились размытие и искажения светового рисунка. Для небольшого изображения результат может выглядеть нормально, но при увеличении ретушь и искажения очень сильно заметны. &lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/e07/af6/2b9/e07af62b99e1e9ff7189ed3e0e44b2cb.png alt=&#34;Результат удаления кружки в Cleanup.pictures — на столе остались размытие и искажения текстуры. &#34; title=&#34;Результат удаления кружки в Cleanup.pictures — на столе остались размытие и искажения текстуры. &#34; width=1999 height=1125 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/e07/af6/2b9/e07af62b99e1e9ff7189ed3e0e44b2cb.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/e07/af6/2b9/e07af62b99e1e9ff7189ed3e0e44b2cb.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Результат удаления кружки в &lt;/em&gt;&lt;a href=http://Cleanup.pictures&gt;&lt;em&gt;Cleanup.pictures&lt;/em&gt;&lt;/a&gt;&lt;em&gt; — на столе остались размытие и искажения текстуры. &lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Женщину нейросеть тоже удалила достаточно аккуратно. На первый взгляд результат выглядит естественно, однако восстановленный участок дороги отличается по резкости, а геометрия и детали фона в зоне удаления местами поплыли. &lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/4cd/631/523/4cd631523587f1c26224b73c016ea99e.png alt=&#34;Cleanup.pictures удалил женщину с фотографии, но размыл дорогу и детали фона.&#34; title=&#34;Cleanup.pictures удалил женщину с фотографии, но размыл дорогу и детали фона.&#34; width=1999 height=1125 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/4cd/631/523/4cd631523587f1c26224b73c016ea99e.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/4cd/631/523/4cd631523587f1c26224b73c016ea99e.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;a href=http://Cleanup.pictures&gt;&lt;em&gt;Cleanup.pictures&lt;/em&gt;&lt;/a&gt;&lt;em&gt; удалил женщину с фотографии, но размыл дорогу и детали фона.&lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Самым сложным оказался велосипед — на его месте появились размытие стены и темное пятно. Для публикации или любого другого использования такой результат пришлось бы ретушировать вручную.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/076/c61/a9b/076c61a9b6dd9ac0e160299eaa9615b5.png alt=&#34;Результат удаления велосипеда в Cleanup.pictures — размытая стена и искаженное металлическое ограждение.&#34; title=&#34;Результат удаления велосипеда в Cleanup.pictures — размытая стена и искаженное металлическое ограждение.&#34; width=1999 height=1125 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/076/c61/a9b/076c61a9b6dd9ac0e160299eaa9615b5.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/076/c61/a9b/076c61a9b6dd9ac0e160299eaa9615b5.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Результат удаления велосипеда в &lt;/em&gt;&lt;a href=http://Cleanup.pictures&gt;&lt;em&gt;Cleanup.pictures&lt;/em&gt;&lt;/a&gt;&lt;em&gt; — размытая стена и искаженное металлическое ограждение.&lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;&lt;strong&gt;Плюсы:&lt;/strong&gt; не требует регистрации, быстро запускает обработку, не перегружен настройками.&lt;p&gt;&lt;strong&gt;Минусы:&lt;/strong&gt; нет текстового промпта, на месте удаленных объектов остаются заметные искажения, фактурный фон размывается, бесплатный файл уменьшается. &lt;p&gt;&lt;strong&gt;Итог: 13/20.&lt;/strong&gt; Подойдет, чтобы быстро убрать небольшой объект с относительно однородного фона, особенно если изображение будет использоваться в небольшом размере. При работе со сложным фоном, в особенности с пересечениями объектов, текстурами и другими деталями качество снижается до неудобоваримого. &lt;h3&gt;Pixelcut — удобное выделение, слабое восстановление фона&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Качество:&lt;/strong&gt; 2/5 на простых объектах, 2/5 на сложной геометрии.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Оригинальный размер:&lt;/strong&gt; только в Pro; бесплатный экспорт ограничен 720 px.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Стоимость:&lt;/strong&gt; бесплатно в низком разрешении; Pro — 5 $ в месяц или 36 $ в год.&lt;/ul&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/d19/d7d/ec3/d19d7dec3ce0a591ff74498917a01ce4.png alt=&#34;Интерфейс Pixelcut ИИ для выделения кистью и удаления объектов с фотографии.&#34; title=&#34;Интерфейс Pixelcut ИИ для выделения кистью и удаления объектов с фотографии.&#34; width=1912 height=941 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/d19/d7d/ec3/d19d7dec3ce0a591ff74498917a01ce4.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/d19/d7d/ec3/d19d7dec3ce0a591ff74498917a01ce4.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Интерфейс Pixelcut ИИ для выделения кистью и удаления объектов с фотографии.&lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;&lt;a href=https://www.pixelcut.ai/cleanup-picture&gt;&lt;strong&gt;Pixelcut&lt;/strong&gt;&lt;/a&gt; — онлайн-редактор с инструментами для удаления фона, объектов и теней. В отличие от ряда аналогов, здесь нейросеть не начинает обработку после каждого движения кисти: сначала можно спокойно выделить весь объект, а затем отдельно запустить генерацию. &lt;p&gt;Кружку Pixelcut удалил полностью, однако вместе с объектом заметно сгладил текстуру деревянного стола. Световой рисунок и фактура поверхности на месте удаления выглядят размытыми, поэтому следы ретуши хорошо заметны. &lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/386/17c/4dd/38617c4dda72d39fb1bd3beb97c4218d.png alt=&#34;Результат удаления кружки в Pixelcut — размытая текстура деревянного стола после обработки. &#34; title=&#34;Результат удаления кружки в Pixelcut — размытая текстура деревянного стола после обработки. &#34; width=1999 height=1125 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/386/17c/4dd/38617c4dda72d39fb1bd3beb97c4218d.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/386/17c/4dd/38617c4dda72d39fb1bd3beb97c4218d.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Результат удаления кружки в Pixelcut — размытая текстура деревянного стола после обработки. &lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;После удаления женщины результат не выглядит приемлемым даже издалека — дорога, тротуар, здание, разметка и весь центр кадра фактически закрыты одним большим эффектом замыливания. Геометрия улицы и мелкие детали тоже восстановлены очень плохо. &lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/01d/124/a23/01d124a23ee60af003c452a44d53da6f.png alt=&#34;Pixelcut удалил женщину с улицы, но оставил сильное размытие дороги и фона.&#34; title=&#34;Pixelcut удалил женщину с улицы, но оставил сильное размытие дороги и фона.&#34; width=1999 height=1125 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/01d/124/a23/01d124a23ee60af003c452a44d53da6f.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/01d/124/a23/01d124a23ee60af003c452a44d53da6f.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Pixelcut удалил женщину с улицы, но оставил сильное размытие дороги и фона.&lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Хуже всего сервис справился с велосипедом: сам объект формально исчез, но на его месте появилась крупная размытая темная область, фактура стены потерялась, а линии металлического ограждения заметно исказились.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/2ba/93f/c23/2ba93fc236515f77fd4289c4fab5ecf1.png alt=&#34;Результат удаления велосипеда в Pixelcut — темное пятно, размытая стена и искаженное ограждение. &#34; title=&#34;Результат удаления велосипеда в Pixelcut — темное пятно, размытая стена и искаженное ограждение. &#34; width=1999 height=1125 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/2ba/93f/c23/2ba93fc236515f77fd4289c4fab5ecf1.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/2ba/93f/c23/2ba93fc236515f77fd4289c4fab5ecf1.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Результат удаления велосипеда в Pixelcut — темное пятно, размытая стена и искаженное ограждение. &lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;&lt;strong&gt;Плюсы:&lt;/strong&gt; понятный интерфейс, регулируемая кисть, обработка запускается отдельной кнопкой, есть другие инструменты для фото.&lt;p&gt;&lt;strong&gt;Минусы:&lt;/strong&gt; крупные маски дают размытие, сложные линии ломаются, достойный результат гарантированно потребует ручной доработки.&lt;p&gt;&lt;strong&gt;Итог: 11/20.&lt;/strong&gt; Подойдет для небольших объектов, но первый результат на сложных сценах нельзя назвать готовым к использованию.&lt;h3&gt;Photoroom Retouch — лучший сервис без промптов&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Качество:&lt;/strong&gt; 4/5 на простых объектах, 2/5 на сложной геометрии.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Оригинальный размер:&lt;/strong&gt; да; в нашем тесте изображение скачалось без уменьшения и дополнительных условий.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Стоимость:&lt;/strong&gt; есть бесплатный режим; Pro — 12,99 $ в месяц или 90 $ в год.&lt;/ul&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/047/1ff/d6c/0471ffd6cd6c291c53cd53fc8cc889b9.png alt=&#34;Интерфейс Photoroom Retouch для удаления ненужных объектов с фотографии.&#34; title=&#34;Интерфейс Photoroom Retouch для удаления ненужных объектов с фотографии.&#34; width=1906 height=956 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/047/1ff/d6c/0471ffd6cd6c291c53cd53fc8cc889b9.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/047/1ff/d6c/0471ffd6cd6c291c53cd53fc8cc889b9.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Интерфейс Photoroom Retouch для удаления ненужных объектов с фотографии.&lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;&lt;a href=https://www.photoroom.com/tools/remove-object-from-photo&gt;&lt;strong&gt;Photoroom Retouch&lt;/strong&gt;&lt;/a&gt; работает по знакомой схеме: загружаем изображение и закрашиваем лишний объект кистью. После обработки фотографию можно открыть в отдельном редакторе, обрезать, изменить размер или продолжить ретушь. &lt;p&gt;Кружку Photoroom удалил &lt;strong&gt;удачнее большинства аналогов&lt;/strong&gt;, но не идеально: объект исчез полностью, однако в зоне удаления заметно сглаживание текстуры стола и легкие следы замены. При этом часть пара над кружкой осталась, поэтому при внимательном просмотре следы исходного объекта все еще заметны. &lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/415/58e/7f4/41558e7f4c4b4d4f5d7f93d953af0dc9.png alt=&#34;Результат удаления кружки в Photoroom Retouch — небольшое сглаживание текстуры деревянного стола.&#34; title=&#34;Результат удаления кружки в Photoroom Retouch — небольшое сглаживание текстуры деревянного стола.&#34; width=1999 height=1972 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/415/58e/7f4/41558e7f4c4b4d4f5d7f93d953af0dc9.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/415/58e/7f4/41558e7f4c4b4d4f5d7f93d953af0dc9.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Результат удаления кружки в Photoroom Retouch — небольшое сглаживание текстуры деревянного стола.&lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Женщину сервис убрал достаточно чисто: сцена в целом выглядит естественно, явного контура объекта не осталось. Однако в центре кадра все же видна размытая область, а фон в месте удаления теряет часть резкости и деталей.&lt;p&gt; &lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/21a/d12/91b/21ad1291b1caef4c139c39512e28c784.png alt=&#34;Photoroom Retouch удалил женщину с фотографии с небольшим размытием дороги на месте объекта.&#34; title=&#34;Photoroom Retouch удалил женщину с фотографии с небольшим размытием дороги на месте объекта.&#34; width=1200 height=630 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/21a/d12/91b/21ad1291b1caef4c139c39512e28c784.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/21a/d12/91b/21ad1291b1caef4c139c39512e28c784.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Photoroom Retouch удалил женщину с фотографии с небольшим размытием дороги на месте объекта.&lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;На велосипеде алгоритм заметно сдал позиции. В зоне пересечения рамы, стены и перил появились сильное размытие, темная масса и неправильно продолженные линии ограждения. Именно на сложной геометрии становится видно, что даже лучший сервис без промпта все еще ошибается. &lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/743/c17/96e/743c1796e74397fa7c9431e8b0cdb009.png alt=&#34;Результат удаления велосипеда в Photoroom Retouch — размытая стена и нарушенная геометрия металлического ограждения.&#34; title=&#34;Результат удаления велосипеда в Photoroom Retouch — размытая стена и нарушенная геометрия металлического ограждения.&#34; width=1333 height=1999 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/743/c17/96e/743c1796e74397fa7c9431e8b0cdb009.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/743/c17/96e/743c1796e74397fa7c9431e8b0cdb009.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Результат удаления велосипеда в Photoroom Retouch — размытая стена и нарушенная геометрия металлического ограждения.&lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;&lt;strong&gt;Плюсы:&lt;/strong&gt; достойное качество по сравнению с другими сервисами без промпта, быстрый экспорт, нет помех при скачивании, доступен отдельный редактор.&lt;p&gt;&lt;strong&gt;Минусы:&lt;/strong&gt; на месте удаления все равно могут оставаться следы ретуши, сложные линии и фактуры восстанавливаются заметно хуже простого фона. &lt;p&gt;&lt;strong&gt;Итог: 16/20.&lt;/strong&gt; Определенный фаворит среди сервисов с кистью: для простых бытовых задач результата часто хватит без дополнительной обработки. Но на сложных сценах и в зонах с геометрией, текстурами и пересекающимися линиями артефакты все равно остаются заметными.&lt;h2&gt;Что показали сервисы без текстового промпта&lt;/h2&gt;&lt;p&gt;&lt;a href=http://Cleanup.pictures&gt;Cleanup.pictures&lt;/a&gt; и Pixelcut действительно удаляют выделенный объект, но качество восстановления фона оказалось слабым. Пока нейросети нужно продолжить относительно простой участок стола или дороги, результат еще можно принять. Когда в кадре появляются фактура камня, тонкие металлические линии и сложная геометрия, вместо них часто остается размытая или искаженная заплатка.&lt;p&gt;Photoroom стал исключением: два первых сценария он прошел заметно увереннее конкурентов и серьезно ошибся только на велосипеде. Но даже его нельзя назвать универсальным. &lt;p&gt;Итоговый результат этих сервисов ощущается как своеобразный артефакт времени. Еще несколько лет назад такая скорость и качество удаления объектов воспринимались как минимум неплохо: выделил предмет кистью, подождал несколько секунд — и получил более-менее пригодный результат.&lt;p&gt;Но технологии не стоят на месте. Качество генеративного редактирования уже заметно ушло вперед — прежде всего в мультимодальных сервисах, где можно не просто указать область удаления, а текстом объяснить, что именно нужно убрать. Поэтому дальше переходим к ним.&lt;h3&gt;ChatGPT — качественное удаление объектов по текстовому промпту&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Качество:&lt;/strong&gt; 5/5.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Оригинальный размер:&lt;/strong&gt; файл скачивается в полном размере генерации; разрешение результата может отличаться от исходного файла.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Стоимость:&lt;/strong&gt; доступен бесплатный тариф с лимитами; ChatGPT Plus — 20 $ в месяц.&lt;/ul&gt;&lt;p&gt;В &lt;a href=https://bothub.ru/gpt-image-2&gt;ChatGPT&lt;/a&gt; объект можно удалить без ручного выделения кистью: достаточно загрузить фотографию и описать нужное изменение текстом. Для всех трех изображений мы использовали заранее зафиксированные промпты — без дополнительных уточнений и повторной генерации.&lt;p&gt;В BotHub можно отправить один и тот же запрос разным моделям — от устаревших до флагманских — и выбрать результат после сравнения, а не доверять первой генерации. &lt;blockquote&gt;&lt;p&gt;&lt;a href=&#34;https://bothub.ru/?invitedBy=zx7xIePYIOcWHhTfgg6-O&#34;&gt;&lt;strong&gt;Получить 300 000 бесплатных токенов в BotHub&lt;/strong&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;strong&gt;Тест 1 — кружка.&lt;/strong&gt; ChatGPT полностью удалил кружку и естественно восстановил деревянную поверхность, сохранив направление текстуры. Вместе с чашкой исчез и поднимающийся от напитка пар, хотя отдельно мы об этом не просили. Модель поняла, что он относится к удаляемому объекту. Однако падающий свет корректно воспроизвести не получилось.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/4e6/fbb/87a/4e6fbb87a3d97479b83d2361a601c104.png alt=&#34;Результат удаления кружки в ChatGPT — кружка и пар удалены, деревянная поверхность стола восстановлена &#34; title=&#34;Результат удаления кружки в ChatGPT — кружка и пар удалены, деревянная поверхность стола восстановлена &#34; width=1254 height=1254 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/4e6/fbb/87a/4e6fbb87a3d97479b83d2361a601c104.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/4e6/fbb/87a/4e6fbb87a3d97479b83d2361a601c104.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Результат удаления кружки в ChatGPT — кружка и пар удалены, деревянная поверхность стола восстановлена &lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;&lt;strong&gt;Тест 2 — женщина.&lt;/strong&gt; Дорога и разметка продолжились без пятен, прерываний разметки и замыливания фона, перспектива и освещение совпадают с исходным кадром, соседние элементы не изменились — отличный результат.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/738/48e/076/73848e076be32f9f5c3e39bfe3567e03.png alt=&#34;ChatGPT удалил женщину с городской улицы и восстановил дорогу и фон без заметного размытия.&#34; title=&#34;ChatGPT удалил женщину с городской улицы и восстановил дорогу и фон без заметного размытия.&#34; width=1774 height=887 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/738/48e/076/73848e076be32f9f5c3e39bfe3567e03.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/738/48e/076/73848e076be32f9f5c3e39bfe3567e03.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;ChatGPT удалил женщину с городской улицы и восстановил дорогу и фон без заметного размытия.&lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;&lt;strong&gt;Тест 3 — велосипед.&lt;/strong&gt; Один из лучших результатов во всем сравнении, но не без ошибки. ChatGPT аккуратно восстановил каменную стену, брусчатку и большую часть металлического ограждения: на месте колес нет размытой области, текстуры и перспектива выглядят естественно. &lt;p&gt;При этом модель дорисовала новую вертикальную стойку ограждения за велосипедом, которой не было на исходном фото, то есть не просто восстановила скрытый фон, а частично придумала его структуру. &lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/75e/66d/d46/75e66dd461c9176af7f6a60aaea7181a.png alt=&#34;Результат удаления велосипеда в ChatGPT — фон восстановлен без размытия, но модель дорисовала лишнюю стойку ограждения.&#34; title=&#34;Результат удаления велосипеда в ChatGPT — фон восстановлен без размытия, но модель дорисовала лишнюю стойку ограждения.&#34; width=1024 height=1536 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/75e/66d/d46/75e66dd461c9176af7f6a60aaea7181a.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/75e/66d/d46/75e66dd461c9176af7f6a60aaea7181a.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Результат удаления велосипеда в ChatGPT — фон восстановлен без размытия, но модель дорисовала лишнюю стойку ограждения.&lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;&lt;strong&gt;Плюсы:&lt;/strong&gt; не требует точного выделения, понимает связи между объектами, сохраняет сложный фон, поддерживает уточнения обычным языком.&lt;p&gt;&lt;strong&gt;Минусы: &lt;/strong&gt;действуют лимиты, модель может пересобрать часть изображения за пределами объекта, придумывает новые объекты и не до конца восстанавливает освещение.&lt;p&gt;&lt;strong&gt;Итог: 19/20.&lt;/strong&gt; Все три первых результата пригодны для использования.&lt;h3&gt;Nano Banana — лучший результат&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Качество:&lt;/strong&gt; 5/5 на простых сценах, 5/5 на сложной геометрии.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Оригинальный размер:&lt;/strong&gt; скачивается полный результат модели, но разрешение может отличаться от исходника.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Стоимость:&lt;/strong&gt; есть бесплатный доступ с лимитами; Google AI Pro — 19,99 $ в месяц, через BotHub отдельный аккаунт Google не требуется, цена от нескольких рублей за генерацию. &lt;/ul&gt;&lt;p&gt;&lt;a href=https://bothub.ru/gemini-3.1-flash-image&gt;Nano Banana&lt;/a&gt; — модель Google для генерации и редактирования изображений по текстовой инструкции. Как и ChatGPT, она принимает фотографию целиком: пользователю не нужно вручную обводить объект. &lt;p&gt;Кружку модель удалила полностью и правдоподобно продолжила поверхность стола и освещения. &lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/3b5/9ec/baa/3b59ecbaa7c241821381420a9455707d.jpg alt=&#34;Nano Banana удалил красную кружку и восстановил текстуру деревянного стола и освещение.&#34; title=&#34;Nano Banana удалил красную кружку и восстановил текстуру деревянного стола и освещение.&#34; width=1038 height=1024 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/3b5/9ec/baa/3b59ecbaa7c241821381420a9455707d.jpg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/3b5/9ec/baa/3b59ecbaa7c241821381420a9455707d.jpg 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Nano Banana удалил красную кружку и восстановил текстуру деревянного стола и освещение.&lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;С человеком результат тоже получился удачным: дорога восстановлена без грубого пятна, а композиция кадра в целом сохранилась. &lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/931/dd7/d15/931dd7d15be90933c7ddb946927e64dc.jpg alt=&#34;Nano Banana удалил женщину с городской улицы и восстановил дорогу и окружающий фон.&#34; title=&#34;Nano Banana удалил женщину с городской улицы и восстановил дорогу и окружающий фон.&#34; width=1424 height=747 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/931/dd7/d15/931dd7d15be90933c7ddb946927e64dc.jpg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/931/dd7/d15/931dd7d15be90933c7ddb946927e64dc.jpg 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Nano Banana удалил женщину с городской улицы и восстановил дорогу и окружающий фон.&lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Велосипед исчез целиком, однако фактура камня и геометрия ограждения воссозданы идеально. &lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/e8c/e81/375/e8ce8137545bb24b0b38f4309f0b8d54.jpg alt=&#34;Результат удаления велосипеда в Nano Banana — восстановлены каменная стена, дорожное покрытие и металлическое ограждение.&#34; title=&#34;Результат удаления велосипеда в Nano Banana — восстановлены каменная стена, дорожное покрытие и металлическое ограждение.&#34; width=842 height=1264 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/e8c/e81/375/e8ce8137545bb24b0b38f4309f0b8d54.jpg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/e8c/e81/375/e8ce8137545bb24b0b38f4309f0b8d54.jpg 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Результат удаления велосипеда в Nano Banana — восстановлены каменная стена, дорожное покрытие и металлическое ограждение.&lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Это заметно лучше результатов обычных ИИ-ластиков и превосходит в детализации ChatGPT, не создавая лишних объектов или ошибок освещения.&lt;p&gt;&lt;strong&gt;Плюсы:&lt;/strong&gt; не нужна кисть, хорошо восстанавливает фон, понимает подробные ограничения в промпте.&lt;p&gt;&lt;strong&gt;Минусы:&lt;/strong&gt; нужен аккаунт Google, действуют лимиты.&lt;p&gt;&lt;strong&gt;Итог: 20/20.&lt;/strong&gt;&lt;h3&gt;Какую нейросеть выбрать для удаления объектов с фото&lt;/h3&gt;&lt;p&gt;По результатам теста победил&lt;a href=https://bothub.ru/gemini-3.1-flash-image&gt; Nano Banana&lt;/a&gt;. Это единственная модель, которая справилась со всеми тремя задачами без заметных ошибок и лучше остальных восстановила сложную геометрию. ChatGPT занял второе место среди моделей с текстовым управлением, а Photoroom Retouch стал лучшим вариантом для тех, кто предпочитает выделение объекта кистью.&lt;p&gt;&lt;strong&gt;Если говорить сугубо практично, вывод простой:&lt;/strong&gt; для небольшого объекта на однородном фоне вполне подойдет сервис с кистью. Но если нужно восстановить сложную текстуру, перспективу, узор или геометрию без заметных артефактов, более предсказуемый результат в нашем тесте дали мультимодальные модели с текстовым промптом.&lt;p&gt;Для редактирования изображений в&lt;a href=&#34;https://bothub.ru/models?utm_source=chatgpt.com&#34;&gt; BotHub&lt;/a&gt; доступны Nano Banana, GPT Image, Flux и другие модели. &lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/20d/08b/894/20d08b8944d18903c60670189256bb1a.png alt=&#34;Интерфейс платформы BotHub&#34; title=&#34;Интерфейс платформы BotHub&#34; width=1893 height=938 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/20d/08b/894/20d08b8944d18903c60670189256bb1a.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/20d/08b/894/20d08b8944d18903c60670189256bb1a.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Интерфейс платформы BotHub&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Одну и ту же фотографию можно отправить нескольким моделям, сравнить результаты и выбрать лучший вариант за 20-30 рублей, не покупая отдельные подписки на сервисы разных разработчиков за более чем 60$.&lt;p&gt;&lt;strong&gt;При этом все модели работают из одного аккаунта и с общего баланса&lt;/strong&gt;:&lt;ul&gt;&lt;li&gt;&lt;p&gt;оплата российскими картами в рублях;&lt;li&gt;&lt;p&gt;без VPN;&lt;li&gt;&lt;p&gt;модели для генерации и редактирования изображений в одном интерфейсе;&lt;li&gt;&lt;p&gt;возможность сравнить результат разных моделей на одном исходнике;&lt;li&gt;&lt;p&gt;300 000 токенов для знакомства с сервисом по реферальной ссылке.&lt;/ul&gt;&lt;blockquote&gt;&lt;p&gt;&lt;a href=&#34;https://bothub.ru/?invitedBy=zx7xIePYIOcWHhTfgg6-O&#34;&gt;&lt;strong&gt;Получить 300 000 токенов для тестирования моделей&lt;/strong&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;h3&gt;Частые вопросы&lt;/h3&gt;&lt;h4&gt;Можно ли удалить объект с фото нейросетью бесплатно?&lt;/h4&gt;&lt;p&gt;Да. &lt;a href=http://Cleanup.pictures&gt;Cleanup.pictures&lt;/a&gt;, Pixelcut, Photoroom, ChatGPT и Nano Banana предлагают бесплатный доступ, но тарифы различаются: ограничения могут касаться разрешения, количества операций, числа генераций или экспорта. &lt;h4&gt;Какая нейросеть лучше удаляет людей с фотографии?&lt;/h4&gt;&lt;p&gt;В нашем тесте лучше всего справились ChatGPT и Nano Banana: они полностью удалили человека и восстановили дорогу с учетом перспективы. Для человека вдали на простом фоне достаточно Photoroom или &lt;a href=http://Cleanup.pictures&gt;Cleanup.pictures&lt;/a&gt;.&lt;h4&gt;Почему после удаления объекта остается размытое пятно?&lt;/h4&gt;&lt;p&gt;Обычно алгоритму не хватает информации о скрытом фоне или выделенная область слишком велика. Уменьшите кисть, захватите тень объекта и удаляйте сложный предмет по частям. Если фон состоит из текста, узора или прямых линий, используйте модель с текстовым промптом.&lt;h4&gt;Как составить промпт для удаления объекта?&lt;/h4&gt;&lt;p&gt;Назовите объект, опишите фон за ним и зафиксируйте элементы, которые нельзя менять. Базовая формула: «Удали [объект]. Восстанови [закрытый фон], сохрани [перспективу, фактуры, свет и соседние детали]. Остальные элементы изображения не изменяй».&lt;h4&gt;Чем удаление объекта отличается от удаления фона?&lt;/h4&gt;&lt;p&gt;При удалении объекта нейросеть меняет только выбранный участок и дорисовывает пространство внутри него. При удалении фона сервис отделяет главный объект от всего окружения и делает фон прозрачным либо заменяет его другим изображением.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <guid>https://habr.com/ru/companies/bothub/articles/1082144/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082144</guid>
      <pubDate>Mon, 14 Sep 2026 15:12:00 +0000</pubDate>
    </item>
    <item>
      <title>Security Week 2638: опасные уязвимости в роутерах MikroTik</title>
      <link>https://habr.com/ru/companies/kaspersky/articles/1082108/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082108</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;На прошлой неделе команда CERT из Польши &lt;a href=https://cert.pl/en/posts/2026/09/vulnerabilities-in-mikrotik-routeros-actively-exploited/&gt;обнародовала&lt;/a&gt; информацию о трех уязвимостях в роутерах MikroTik, две из которых активно используются в реальных атаках как минимум со второго сентября. Взлом роутеров возможен в том случае, если на устройстве разрешен доступ по протоколу SSH из Интернета. По данным на &lt;a href=https://x.com/Shadowserver/status/2096678032744280476&gt;пятое сентября&lt;/a&gt; в сети наблюдалось более 122 тысяч потенциально уязвимых устройств.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/3c1/339/a9e/3c1339a9e2b5bc0274fdcbbea70b2545.jpg width=1460 height=960 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/3c1/339/a9e/3c1339a9e2b5bc0274fdcbbea70b2545.jpg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/3c1/339/a9e/3c1339a9e2b5bc0274fdcbbea70b2545.jpg 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Две наиболее опасные уязвимости имеют одинаковый рейтинг 9,2 балла по шкале CVSS. Проблема CVE-2026-67276 позволяет обойти аутентификацию по SSH в том случае, если атакующему известно имя пользователя и модуль публичного ключа. Ошибка CVE-2026-86060, в свою очередь, открывает возможность эскалации привилегий при использовании имени пользователя, содержащего некорректные символы.&lt;p&gt;Комбинация двух уязвимостей делает возможной полную компрометацию устройства. Третья обнаруженная проблема (CVE-2026-67277, CVSS 8.8) присутствует в службе проверки скорости интернет-соединения и в худшем случае может приводить к отказу в обслуживании. Примечательно, что польский CERT обнаружил две наиболее опасные уязвимости с использованием искусственного интеллекта, конкретно — с помощью моделей GPT 5.5 Cyber и GPT 5.6 Sol компании OpenAI. Автоматизированный процесс поиска уязвимостей сопровождался ручной проверкой обнаруженных потенциальных проблем.&lt;p&gt;Обнаружение багов в процессе «свободного поиска» обычно не сопровождается эксплуатацией уязвимостей. Но в данном случае атаки начались одновременно с &lt;a href=https://mikrotik.com/supportsec/september-2026-vulnerability&gt;публикацией патчей&lt;/a&gt; разработчиком роутеров MikroTik. Обновления стали доступны в версиях RouterOS 7.25beta3, 7.24.2, 7.23.4 и 6.49.21. Кроме того, был обновлен встроенный механизм, позволяющий определить, что устройство уже было скомпрометировано. По данным CERT Polska, зафиксированные атаки оставляют характерный след в логах роутера:&lt;pre&gt;&lt;code&gt;login failure for user -2 from &amp;#34;ip&amp;#34; via ssh&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:87px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;user &amp;#34;name&amp;#34; added by ssh:-2@&amp;#34;ip&amp;#34;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h3&gt;Что еще произошло&lt;/h3&gt;&lt;p&gt;Примечательное исследование прошлой недели &lt;a href=https://calif.io/research/weworm&gt;опубликовано&lt;/a&gt; командой Calif Research (&lt;a href=https://habr.com/ru/news/1080594/&gt;новость&lt;/a&gt; на Хабре). С помощью ИИ-ассистента была обнаружена ошибка в VoIP-стеке приложения WeChat. Уязвимость можно задействовать, просто позвонив абоненту, причем с его стороны никаких действий не требуется. На основе данного бага в Calif разработали червь WeWorm: компрометация приложения позволяет распространить атаку по всем контактам пользователя. Компания Tencent, разработчик WeChat, закрыла уязвимость на момент публикации отчета.&lt;p&gt;Компания Microsoft выпустила рекордный &lt;a href=https://arstechnica.com/security/2026/09/microsoft-patches-a-record-972-vulnerabilities-112-of-them-critical/&gt;набор патчей&lt;/a&gt;, содержащий 972 заплатки (997, если считать уязвимости браузерного движка Chromium). Из них 112 уязвимостей имеют критический рейтинг, а две являются проблемами нулевого дня. Всего в этом году Microsoft закрыла 2760 багов, что в два раза превышает показатель за весь 2025 год. Причина роста количества обнаруживаемых багов традиционная: их поиск все эффективнее автоматизируется с применением ИИ-ассистентов.&lt;p&gt;Компания Google выпустила набор из &lt;a href=https://www.bleepingcomputer.com/news/security/google-patches-seventh-chrome-zero-day-exploited-in-attacks-this-year/&gt;230 заплаток&lt;/a&gt;, включая патч для эксплуатируемой уязвимости в браузере Google Chrome — уже седьмой с начала этого года.&lt;p&gt;Критическая &lt;a href=https://www.bleepingcomputer.com/news/security/gitlab-urges-users-to-patch-max-severity-path-traversal-flaw/&gt;уязвимость&lt;/a&gt; в GitLab теоретически открывает доступ к приватным данным на серверах с использованием техники path traversal.&lt;p&gt;Опасная проблема &lt;a href=https://www.bleepingcomputer.com/news/security/adobe-fixes-critical-magento-zero-day-exploited-to-backdoor-servers/&gt;закрыта&lt;/a&gt; в коммерческом ПО для интернет-магазинов Adobe Commerce и Magento.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <guid>https://habr.com/ru/companies/kaspersky/articles/1082108/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082108</guid>
      <pubDate>Mon, 14 Sep 2026 15:06:43 +0000</pubDate>
    </item>
    <item>
      <title>Сотрудники не используют новые знания в работе: чек-лист по проектированию переноса обучения на практику</title>
      <link>https://habr.com/ru/articles/1081128/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1081128</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;&lt;strong&gt;Как внедрить перенос в корпоративное обучение, сделать его обязательной частью программ и тем самым увеличить отдачу от инвестиций в развитие сотрудников?&lt;/strong&gt;&lt;p&gt;Обучение само по себе не создает ценности для бизнеса — оно дает сотрудникам только знания, алгоритмы действий и опыт в учебной среде. Станут ли они частью ежедневной практики, зависит от &lt;strong&gt;переноса обучения (transfer of training) &lt;/strong&gt;— того, что происходит, когда сотрудники возвращаются на свои рабочие места.&lt;p&gt;Отсутствие переноса обучения на практику — это самое слабое звено в корпоративном обучении и главная боль для бизнеса. Компании инвестируют в развитие сотрудников, обучение проводится, только вместо изменения поведения и результатов бизнес получает отчеты о посещаемости, удовлетворенности и проверке знаний.&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;«Даже при развитой инфраструктуре обучение во многих компаниях остается отдельной активностью, а не частью повседневной работы», — из &lt;/em&gt;&lt;a href=https://research.sberuniversity.ru/research-about rel=&#34;noopener nofollow&#34;&gt;&lt;em&gt;исследования&lt;/em&gt;&lt;/a&gt;&lt;em&gt; зрелости корпоративного обучения в России в 2026 г.&lt;/em&gt;&lt;/blockquote&gt;&lt;p&gt;Авторы &lt;a href=&#34;https://the6ds.com/?v=1a13105b7e4e&#34; rel=&#34;noopener nofollow&#34;&gt;«шести дисциплин прорывного обучения»&lt;/a&gt; неиспользованное в работе обучение называют «браком», даже несмотря на то, что сама программа была проведена на 10 из 10. Также авторы приводят следующую статистику: только 10% затрат на обучение фактически приводят к его переносу в работу. Неиспользованное обучение дорого обходится бизнесу и часто является причиной восприятия функции L&amp;amp;D как центра затрат. &lt;blockquote&gt;&lt;p&gt;&lt;em&gt;Поэтому сегодня компаниям нужно обучение не как «отдельный продукт», а как управляемая цепочка событий, которая приведет к изменению поведения сотрудников и результатов их работы.&lt;/em&gt;&lt;/blockquote&gt;&lt;p&gt;При этом перенос — это не результат случая и геройства сотрудников, а управляемый процесс.&lt;p&gt;В этом лонгриде разбираю:&lt;ul&gt;&lt;li&gt;&lt;p&gt;как работает перенос обучения на практику;&lt;li&gt;&lt;p&gt;зачем компании управлять переносом обучения;&lt;li&gt;&lt;p&gt;как внедрить перенос в программы корпоративного обучения.&lt;/ul&gt;&lt;h2&gt;Что такое перенос обучения на практику?&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Перенос обучения как процесс&lt;/strong&gt;&lt;p&gt;Перенос обучения — это системный процесс, который зависит не только от программы обучения и самих сотрудников, но и от &lt;strong&gt;рабочей среды&lt;/strong&gt;. Если среда препятствует переносу, внедрения навыков не произойдет и такое обучение станет источником потерь для бизнеса.&lt;p&gt;Согласно &lt;a href=https://www.researchgate.net/publication/319336024_Development_of_a_generalized_learning_transfer_system_inventory rel=&#34;noopener nofollow&#34;&gt;модели переноса обучения Болдуина и Форда&lt;/a&gt;&lt;strong&gt;,&lt;/strong&gt; процесс переноса включает «входные» факторы, результаты обучения и условия переноса (рис. 1). «Входные» факторы обучения — это классическая триада факторов, влияющих на эффективность обучения и переноса. К ним относятся характеристики участников обучения, дизайн программы обучения и рабочая среда.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/267/c7a/0d2/267c7a0d24dd35356c9ecd2796fbf41b.jpg alt=&#34;Рисунок 1. Модель процесса переноса обучения Болдуина и Форда.&#34; title=&#34;Рисунок 1. Модель процесса переноса обучения Болдуина и Форда.&#34; width=1920 height=1080 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/267/c7a/0d2/267c7a0d24dd35356c9ecd2796fbf41b.jpg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/267/c7a/0d2/267c7a0d24dd35356c9ecd2796fbf41b.jpg 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Рисунок 1. Модель процесса переноса обучения Болдуина и Форда.&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Программа обучения сама по себе напрямую влияет только на достижение учебных целей — с чем сотрудники возвращаются на свои рабочие места (1). Сами участники обучения и рабочая среда влияют на перенос как косвенно через результаты обучения (2,3), так и напрямую (4,5).&lt;blockquote&gt;&lt;p&gt;Сотрудник может успешно освоить на обучении знания и навыки, но они могут так и не закрепиться на рабочем месте из-за отсутствия мотивации или поддержки среды.&lt;/blockquote&gt;&lt;p&gt;Кроме того, перенос обучения — это динамический процесс, состоящий из &lt;strong&gt;серии попыток применения&lt;/strong&gt;, получения обратной связи и принятия решения о дальнейшем использовании обучения.&lt;p&gt;Согласно &lt;a href=https://www.researchgate.net/publication/321188656_A_dynamic_model_of_training_transfer rel=&#34;noopener nofollow&#34;&gt;динамической модели переноса обучения&lt;/a&gt;, еще на этапе обучения сотрудник принимает решение о том, что и как он будет пытаться применять в своей работе. Вернувшись на рабочее место, сотрудник совершает первую попытку применения обучения. На основе полученных результатов и обратной связи он принимает одно из следующих решений: сохранить и доработать материал или полностью отбросить и забыть всё, чему его учили. Если сотрудник выбирает первый путь, цикл повторяется (рис. 2).&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/901/27a/dc4/90127adc48a3c7c87bc0564efa9fee33.jpg alt=&#34;Рисунок 2. Динамическая модель переноса (адаптация модели Блума и коллег).&#34; title=&#34;Рисунок 2. Динамическая модель переноса (адаптация модели Блума и коллег).&#34; width=1920 height=1080 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/901/27a/dc4/90127adc48a3c7c87bc0564efa9fee33.jpg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/901/27a/dc4/90127adc48a3c7c87bc0564efa9fee33.jpg 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Рисунок 2. Динамическая модель переноса (адаптация модели Блума и коллег).&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Люди не меняют свое поведение по щелчку пальцев сразу после обучения. Перенос — это живой, итеративный процесс, состоящий из попыток внедрения, ошибок, адаптации и кастомизации. Бессмысленно требовать от сотрудников слепого следования материалам тренинга, так как каждый проходит свою уникальную траекторию переноса.&lt;p&gt;Задача компании — не просто дать знания и ожидать их внедрения, а запустить после обучения цикл применения, получения обратной связи и рефлексии, помогая сотрудникам адаптировать новые знания и навыки под реальные задачи бизнеса.&lt;p&gt;&lt;strong&gt;Перенос обучения как результат&lt;/strong&gt;&lt;p&gt;Перенос обучения — степень, в которой сотрудники применяют полученные на обучении знания, навыки и установки, и улучшают результативность своей работы [4, 5].&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;Болдуин и Форд в своей модели выделяют два условия переноса:&lt;/em&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;em&gt;обобщение новых знаний и навыков применительно к реальному рабочему контексту;&lt;/em&gt;&lt;li&gt;&lt;p&gt;&lt;em&gt;поддержание их применения на рабочем месте в течение длительного периода времени.&lt;/em&gt;&lt;/ul&gt;&lt;/blockquote&gt;&lt;p&gt;Это главные критерии эффективности процесса переноса. Простыми словами, перенос состоялся, если сотрудник смог адаптировать новый навык для решения своих рабочих задач и сделать его частью своей ежедневной практики.&lt;p&gt;Когда я оцениваю результаты переноса, также смотрю на:&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;глубину&lt;/strong&gt;: какая часть из пройденного обучения реально используется;&lt;li&gt;&lt;p&gt;&lt;strong&gt;ширину&lt;/strong&gt;: в каких рабочих ситуациях и задачах;&lt;li&gt;&lt;p&gt;&lt;strong&gt;частоту&lt;/strong&gt;: как часто;&lt;li&gt;&lt;p&gt;&lt;strong&gt;качество&lt;/strong&gt;: насколько действия соответствуют целевой модели поведения.&lt;/ul&gt;&lt;h2&gt;Зачем перенос обучения нужен компании и сотрудникам?&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Для компании&lt;/strong&gt; перенос обучения — это обязательное промежуточное звено между обучением и бизнес-результатами.&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;Само обучение не гарантирует изменения поведения сотрудников на рабочем месте. Оно создает потенциальную ценность, а вот станет ли она реальной, зависит уже от того, как организован процесс переноса.&lt;/em&gt;&lt;/blockquote&gt;&lt;p&gt;По результатам &lt;a href=https://www.researchgate.net/publication/229573916_The_relationship_between_organizational_transfer_climate_and_positive_transfer_of_training rel=&#34;noopener nofollow&#34;&gt;исследования&lt;/a&gt; на базе крупной франшизы ресторанов быстрого питания были сделаны следующие выводы:&lt;ul&gt;&lt;li&gt;&lt;p&gt;обучение гарантирует лишь &lt;strong&gt;8%&lt;/strong&gt; реального изменения поведения на рабочем месте. При этом обучение и организационный климат вместе определяют &lt;strong&gt;54% &lt;/strong&gt;успеха переноса.&lt;li&gt;&lt;p&gt;прямая связь между успешным обучением и результативностью на рабочем месте крайне незначительна (&lt;strong&gt;0,02&lt;/strong&gt;). То, как сотрудник учился, влияет на его попытки применить полученные знания и навыки в работе (&lt;strong&gt;0,28&lt;/strong&gt;). Активность сотрудника на этапе переноса, в свою очередь, коррелирует с финальной оценкой его работы (&lt;strong&gt;0,52&lt;/strong&gt;).&lt;li&gt;&lt;p&gt;прибыльность или высокий рейтинг бизнес-юнита не гарантируют успешного внедрения обучения. Опять же решает не общая эффективность подразделения, а наличие управляемого процесса переноса.&lt;/ul&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/88a/1fc/34d/88a1fc34d721d730c011bc4fa7919e33.jpg alt=&#34;Рисунок 3. Как обучение создает ценность для бизнеса.&#34; title=&#34;Рисунок 3. Как обучение создает ценность для бизнеса.&#34; width=1872 height=974 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/88a/1fc/34d/88a1fc34d721d730c011bc4fa7919e33.jpg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/88a/1fc/34d/88a1fc34d721d730c011bc4fa7919e33.jpg 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Рисунок 3. Как обучение создает ценность для бизнеса.&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Эти данные еще раз подтверждают, что для изменения поведения сотрудников самого обучения недостаточно. Важно и то, как поддерживается его использование на рабочем месте. Обучение влияет на результативность сотрудника исключительно через перенос, который зависит от рабочей среды.&lt;p&gt;Какой вывод для компаний — чтобы получить отдачу от инвестиций в корпоративное обучение, компании нужно не только отправлять сотрудников на обучение, но и управлять последующим его переносом.&lt;p&gt;&lt;strong&gt;Для сотрудников &lt;/strong&gt;перенос обучения — это понятный маршрут и поддержка, которые помогают пройти сложный путь изменений и сделать новые знания навыком. После формального обучения сотрудники продолжают цикл развития на рабочем месте. Кроме того, дизайн переноса позволяет выявить и устранить барьеры, с которыми обычно сталкиваются сотрудники в процессе.&lt;h2&gt;Создаем фундамент для организации переноса&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;1. Учитывайте тип программы обучения&lt;/strong&gt;&lt;p&gt;Дизайн переноса зависит от типа программы:&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;открытые навыки&lt;/strong&gt; — задачи имеют несколько способов выполнения, дается свобода выбора стратегии и возможность кастомизации. Например, коммуникативные навыки.&lt;li&gt;&lt;p&gt;&lt;strong&gt;закрытые навыки&lt;/strong&gt; — задачи регламентированы одним способом выполнения, дается конкретный алгоритм действий. Например, навыки работы в ПО.&lt;/ul&gt;&lt;blockquote&gt;&lt;p&gt;&lt;a href=https://www.researchgate.net/publication/235975809_Transfer_of_Training_A_Meta-Analytic_Review rel=&#34;noopener nofollow&#34;&gt;&lt;em&gt;Для успешного переноса&lt;/em&gt;&lt;/a&gt;&lt;em&gt; «открытых навыков» в целом наиболее важны рабочая среда (0,26), самоэффективность до начала обучения (0,23), знания после обучения (0,20) и мотивация (0,19).&lt;/em&gt;&lt;p&gt;&lt;em&gt;Если смотреть конкретно лидерские программы и обучение по развитию гибких навыков, то факторы переноса распределяются иначе: самоэффективность после обучения (0,22), мотивация (0,20), рабочая среда (0,20) и знания после тренинга (0,16).&lt;/em&gt;&lt;p&gt;&lt;em&gt;При этом связь этих факторов с программами по «закрытым навыкам» оказалась менее выраженной. Для обучения, в котором дается четкий алгоритм действий, решающим оказываются когнитивные способности участников обучения (0,41).&lt;/em&gt;&lt;/blockquote&gt;&lt;p&gt;Таким образом, для «закрытых навыков» критически важна предварительная оценка способностей сотрудников, а для «открытых навыков» — вовлечение руководителей и создание климата переноса. В первом случае решает то, что сотрудник «потянет» обучение, во втором — что среда поддержит.&lt;p&gt;&lt;strong&gt;2. Интегрируйте перенос в операционную систему, в которой работают сотрудники&lt;/strong&gt;&lt;p&gt;Внедрите перенос в текущую &lt;a href=https://habr.com/ru/articles/1066872/ rel=&#34;noopener nofollow&#34;&gt;систему управления результативностью&lt;/a&gt; — ожидания, рабочие инструменты, источники обратной связи, контроля качества, систему стимулов. Без интеграции обучения в рабочий процесс оно станет для сотрудников и руководителя лишь очередным разовым мероприятием, а не частью их ежедневной работы. Не создавайте решения искусственно, отдельно от реального процесса. Используйте механизмы контроля и практики управления, с помощью которых организуется текущая работа команды.&lt;p&gt;Особенно критична связь переноса с системой стимулов. Мотивация к переносу создает потенциал для изменений, но она не возникает сама по себе из ниоткуда. Этот импульс формируется на основе ожиданий позитивных и негативных последствий.&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Климат переноса&lt;/em&gt;&lt;/strong&gt;&lt;em&gt;. Это &lt;/em&gt;&lt;a href=https://www.researchgate.net/publication/229573916_The_relationship_between_organizational_transfer_climate_and_positive_transfer_of_training rel=&#34;noopener nofollow&#34;&gt;&lt;em&gt;система,&lt;/em&gt;&lt;/a&gt;&lt;em&gt; которая состоит из:&lt;/em&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;em&gt;ситуационных сигналов — то, как компания подталкивает сотрудников использовать обучение.&lt;/em&gt;&lt;li&gt;&lt;p&gt;&lt;em&gt;последствий — то, как компания реагирует, когда сотрудники применяют или не применяют обучение в работе. От последствий зависит дальнейшее использование обучения.&lt;/em&gt;&lt;/ul&gt;&lt;p&gt;&lt;em&gt;Ситуационные сигналы и последствия вносят весомый и независимый вклад в перенос обучения. Это значит, что они не заменяют друг друга, а должны работать одновременно. Для компании одинаково важно, как среда стимулирует сотрудника использовать обучение в решении рабочих задач, так и то, как она реагирует на это «после»&lt;/em&gt;&lt;p&gt;&lt;em&gt;Авторы модели климата переноса рекомендуют перед запуском любых программ проводить анализ готовности компании поддерживать такое обучение.&lt;/em&gt;&lt;p&gt;&lt;em&gt;По &lt;/em&gt;&lt;a href=https://www.researchgate.net/publication/235975809_Transfer_of_Training_A_Meta-Analytic_Review rel=&#34;noopener nofollow&#34;&gt;&lt;em&gt;влиянию&lt;/em&gt;&lt;/a&gt;&lt;em&gt; климат переноса лидирует среди всех факторов рабочей среды (0,27).&lt;/em&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;strong&gt;3. Сделайте запуск и поддержку переноса частью управленческой практики руководителей&lt;/strong&gt;&lt;p&gt;Руководители определяют судьбу любой программы обучения. Они либо часть решения, либо часть проблемы. Опишите роль и задачи руководителей в организации переноса на рабочем месте, зафиксируйте точки входа в процесс и погрузите их в программу обучения. Даже простых микродействий руководителя, интегрированных в его повседневную практику, достаточно, чтобы сделать процесс переноса управляемым (рис. 4).&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/484/f45/a87/484f45a875a11ab5c9843bcdf4b591bd.jpg alt=&#34;Рисунок 4. Точки входа руководителя N+1 в процесс обучения и его переноса на практику.&#34; title=&#34;Рисунок 4. Точки входа руководителя N+1 в процесс обучения и его переноса на практику.&#34; width=1878 height=1030 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/484/f45/a87/484f45a875a11ab5c9843bcdf4b591bd.jpg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/484/f45/a87/484f45a875a11ab5c9843bcdf4b591bd.jpg 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Рисунок 4. Точки входа руководителя N+1 в процесс обучения и его переноса на практику.&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Среди факторов переноса, на которые влияет руководитель, критичными [3, 4, 6] являются:&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;целеполагание&lt;/strong&gt;: постановка целей по использованию обучения на рабочем месте.&lt;li&gt;&lt;p&gt;&lt;strong&gt;возможность применения&lt;/strong&gt;: предоставление рабочих задач, необходимых ресурсов и доступа к инструментам и оборудованию для использования обучения в работе.&lt;li&gt;&lt;p&gt;&lt;strong&gt;обратная связь&lt;/strong&gt;: предоставление регулярной информации о прогрессе применения и соответствия целевым действиям. Отсутствие информации по использованию обучения препятствует переносу.&lt;li&gt;&lt;p&gt;&lt;strong&gt;личные ресурсы для переноса&lt;/strong&gt;: наличие у сотрудников времени и возможностей (физических и психологических), чтобы совершать изменения, необходимые для переноса обучения. Это внешние условия работы (загруженность, свободное время, уровень стресса на рабочем месте), которые руководитель создает для внедрения изменений.&lt;/ul&gt;&lt;p&gt;Препятствуют переносу обучения также &lt;strong&gt;сопротивление и санкции со стороны руководителя&lt;/strong&gt;. Если сам руководитель выступает против использования новых знаний и предпочитает работать по-старому, перенос обучения в работу не состоится.&lt;p&gt;&lt;strong&gt;4. Обучите руководителей и сотрудников навыкам взаимного обучения&lt;/strong&gt;&lt;p&gt;Без общего языка и принципов в команде эффективность поддержки со стороны руководителя и коллег снижается, а она, в свою очередь, критически важна для переноса обучения по гибким навыкам, управленческим и лидерским программам. Это фундамент не только для организации успешного переноса, но и формирования культуры обучения в компании в целом.&lt;blockquote&gt;&lt;p&gt;&lt;a href=https://www.researchgate.net/publication/235975809_Transfer_of_Training_A_Meta-Analytic_Review rel=&#34;noopener nofollow&#34;&gt;&lt;em&gt;Поддержка руководителя&lt;/em&gt;&lt;/a&gt;&lt;em&gt; (0,31) влияет на внедрение навыков более чем в два раза сильнее, чем поддержка коллег (0,14).В целом поддержка со стороны руководителя и коллег имеет умеренное влияние на перенос (0,21).&lt;/em&gt;&lt;/blockquote&gt;&lt;p&gt;О том, как работает взаимное обучение и что дает команде и бизнесу, я писал &lt;a href=https://habr.com/ru/articles/1039924/ rel=&#34;noopener nofollow&#34;&gt;здесь&lt;/a&gt;.&lt;hr&gt;&lt;h2&gt;Чек-лист дизайна переноса обучения на практику&lt;/h2&gt;&lt;p&gt;Эти контрольные точки до, во время и после обучения помогут постепенно внедрить перенос в корпоративное обучение. Этапы до и после обучения требуют в совокупности &lt;a href=https://leadershippipelineinstitute.com/products-the-404020-model rel=&#34;noopener nofollow&#34;&gt;&lt;strong&gt;80%&lt;/strong&gt;&lt;/a&gt; всех усилий и ресурсов. Только &lt;strong&gt;20% &lt;/strong&gt;уходят на проведение самого обучения.&lt;p&gt;Обратите внимание, чек-лист не включает в себя базовые шаги по проектированию самой программы обучения — исследование целевой аудитории, соответствие контента рабочим задачам и контексту, наличие практики и обратной связи с фокусом на дальнейшее применение и т.д. Дизайн программы обучения — один из «входных» факторов переноса.&lt;h2&gt;Точки контроля ДО обучения&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;1. Соберите информацию о рабочей среде, в которой работают сотрудники&lt;/strong&gt;&lt;p&gt;На этапе определения потребности в обучении, параллельно с анализом дефицита знаний и навыков, изучите рабочую среду. Это поможет заранее выявить и устранить организационные и управленческие барьеры, а также спроектировать сам процесс переноса.&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;В качестве фрейма для анализа среды используйте &lt;/em&gt;&lt;strong&gt;&lt;em&gt;модель климата переноса&lt;/em&gt;&lt;/strong&gt;&lt;em&gt; или факторы среды из опросника &lt;/em&gt;&lt;strong&gt;&lt;em&gt;Learning Transfer System Inventory (LTSI)&lt;/em&gt;&lt;/strong&gt;&lt;em&gt;.&lt;/em&gt;&lt;p&gt;&lt;em&gt;LTSI состоит из 16 факторов, влияющих на перенос результатов обучения в ежедневную рабочую практику сотрудников. Эти факторы были &lt;/em&gt;&lt;a href=https://www.researchgate.net/publication/262898094_A_revised_learning_transfer_system_inventory_Factorial_replication_and_validation rel=&#34;noopener nofollow&#34;&gt;&lt;em&gt;подтверждены&lt;/em&gt;&lt;/a&gt;&lt;em&gt; результатами масштабного исследования с участием 5990 человек из 17 стран.&lt;/em&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;strong&gt;2. Внедрите план переноса&lt;/strong&gt;&lt;p&gt;План переноса — дорожная карта, которая приведет сотрудника от обучения к изменению поведения на рабочем месте. План может быть в форме списка заданий на применение и рефлексию, трекера применения, плана внедрения или проекта изменений. Содержание плана прорабатывается самими сотрудниками еще на этапе обучения.&lt;p&gt;&lt;strong&gt;3. Зафиксируйте финальную точку обучения&lt;/strong&gt;&lt;p&gt;Обучение считается завершенным не после того, как будет пройден курс или сдан тест, а после изменения поведения на рабочем месте и выполнения плана переноса.&lt;p&gt;&lt;strong&gt;4. Определите, как вы будете измерять изменение поведения и бизнес-результатов&lt;/strong&gt;&lt;p&gt;Это та информация, которая реально нужна бизнесу. Оценка поведения покажет, произошел ли перенос, а оценка вклада в бизнес-результаты — отдачу от обучения. Запланируйте серию замеров, чтобы получить полную картину эффективности процесса.&lt;blockquote&gt;&lt;p&gt;&lt;a href=https://www.researchgate.net/publication/321188656_A_dynamic_model_of_training_transfer rel=&#34;noopener nofollow&#34;&gt;&lt;strong&gt;&lt;em&gt;Разделение поведения и результатов&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;&lt;em&gt;. Если поведение напрямую зависит от знаний, навыков и мотивации сотрудника, то бизнес-результаты — от системы управления, процессов и множества других факторов. Это необходимо учитывать в процессе оценки переноса и предоставления обратной связи, особенно на ранних этапах. Поведение — это индикатор использования навыка, а бизнес-результат — индикатор его эффективности.&lt;/em&gt;&lt;p&gt;&lt;a href=https://www.researchgate.net/publication/321188656_A_dynamic_model_of_training_transfer rel=&#34;noopener nofollow&#34;&gt;&lt;strong&gt;&lt;em&gt;Серия замеров, а не один статичный срез&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;. &lt;em&gt;Так как перенос — это процесс, растянутый во времени, один замер не покажет всю его картину. Например, сотрудник совершает 10 попыток применить навык за 10 недель. Первые 5 попыток были неудачными, а последние 5 — успешными. Замер на 5-й неделе покажет, что сотрудник не владеет навыком. Поэтому необходимо собирать данные на регулярной основе. Также рекомендуется уходить от субъективных оценок и собирать объективные цифровые следы действий сотрудников.&lt;/em&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;strong&gt;5. Сформируйте у сотрудников позитивные ожидания&lt;/strong&gt;&lt;p&gt;Перед запуском обучения организуйте работу по продаже ценности программы и помогите сотрудникам осознать потребность в обучении.&lt;blockquote&gt;&lt;p&gt;&lt;a href=https://www.researchgate.net/publication/235975809_Transfer_of_Training_A_Meta-Analytic_Review rel=&#34;noopener nofollow&#34;&gt;&lt;em&gt;Практика формирования позитивных ожиданий&lt;/em&gt;&lt;/a&gt;&lt;em&gt; до обучения имеет умеренную связь с переносом (0,20).&lt;/em&gt; Также для переноса важен самостоятельный выбор обучения (0,34).&lt;/blockquote&gt;&lt;h2&gt;Точки контроля ВО ВРЕМЯ обучения&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;1. Включите в программу обучения проработку намерений (буду ли я?) и самоэффективности (могу ли я?) сотрудников&lt;/strong&gt;&lt;p&gt;Задача тренера — перевести абстрактные цели по использованию обучения в конкретный план по схеме: «В рабочей ситуации X — я сделаю действие Y» с учетом анализа реального рабочего контекста.&lt;p&gt;&lt;strong&gt;2. Измеряйте не только знания и навыки, но и самоэффективность сотрудников и их намерения совершить перенос&lt;/strong&gt;&lt;p&gt;Это даст понимание, с какими знаниями и навыками сотрудники выходят с обучения, планируют ли они вообще использовать их в работе и насколько уверены, что у них это получится.&lt;blockquote&gt;&lt;p&gt;&lt;a href=https://www.researchgate.net/publication/235975809_Transfer_of_Training_A_Meta-Analytic_Review rel=&#34;noopener nofollow&#34;&gt;&lt;strong&gt;&lt;em&gt;Как влияют на перенос результаты обучения и реакция сотрудников на него&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;&lt;em&gt;. &lt;/em&gt;&lt;/strong&gt;&lt;em&gt;Полученные на обучении знания (0,24) и уверенность сотрудника в способности их применить в работе после обучения (0,20) имеют умеренную связь с переносом. Эти факторы стоит рассматривать как «гигиенические». Без них перенос невозможен в принципе, но сами по себе они успех не гарантируют.&lt;/em&gt;&lt;p&gt;&lt;em&gt;Оценка сотрудниками полезности обучения влияет чуть сильнее (0,17), но общая реакция имеет незначительную связь с переносом (0,08). На практике это означает, высокая оценка обучения не равна внедрению навыков на рабочем месте.&lt;/em&gt;&lt;/blockquote&gt;&lt;h2&gt;Точки контроля ПОСЛЕ обучения&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;1. Детально спланируйте первую попытку применения&lt;/strong&gt;&lt;p&gt;Сразу после обучения необходимо помочь сотрудникам перейти от намерений к действиям. Что и когда сотрудник будет делать после обучения, кто поддержит и поможет отрефлексировать опыт — руководитель, тренер или наставник. Здесь важно помнить о том, что знания и уверенность после обучения имеют короткий «срок действия» и сотрудник может отбросить перенос после первой неудачной попытки.&lt;p&gt;Если интервал растянут во времени, в дизайн переноса необходимо включить поддерживающие мероприятия.&lt;blockquote&gt;&lt;p&gt;&lt;a href=https://www.researchgate.net/publication/235975809_Transfer_of_Training_A_Meta-Analytic_Review rel=&#34;noopener nofollow&#34;&gt;&lt;strong&gt;&lt;em&gt;«Срок действия» результатов обучения&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;&lt;em&gt;. &lt;/em&gt;&lt;/strong&gt;&lt;em&gt;По мере увеличения времени между обучением и переносом самоэффективность сотрудника после обучения и полученные знания угасают. При этом связь переноса с рабочей средой, самоэффективностью до обучения и мотивацией от времени не зависит. На практике это означает, результаты обучения ослабевают со временем. Стабильными остаются только рабочая среда и мотивация, но если затянуть с внедрением, поддерживать им будет просто нечего.&lt;/em&gt;&lt;p&gt;&lt;a href=https://www.researchgate.net/publication/321188656_A_dynamic_model_of_training_transfer rel=&#34;noopener nofollow&#34;&gt;&lt;strong&gt;&lt;em&gt;Критичность первой попытки&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;&lt;em&gt;. &lt;/em&gt;&lt;/strong&gt;&lt;em&gt;Согласно динамической модели переноса, от первого опыта зависят все дальнейшие попытки применения обучения. В этот период необходимо обеспечить сотрудников сопровождением и поддержкой. Например, короткая сессия (after-action review) с руководителем сразу после первой попытки внедрить навык поможет сотруднику отрефлексировать опыт, сделать правильные выводы, сохранить намерение и мотивацию продолжить цикл.&lt;/em&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;strong&gt;2. Запланируйте серию коротких регулярных встреч руководителя с сотрудником&lt;/strong&gt;&lt;p&gt;Для обсуждения плана переноса, прогресса и подведения итогов. Практика показывает, что на таких встречах наблюдения и обратная связь руководителя творят чудеса, поддерживают сотрудника и помогают ему сфокусироваться на применении навыков.&lt;p&gt;&lt;strong&gt;3. Обеспечьте процесс переноса обратной связью, обменом опытом, обсуждением сложных ситуаций и рефлексией&lt;/strong&gt;&lt;p&gt;На практике эта задача решается с помощью наставничества, регулярных P2P-встреч в парах, промежуточных follow-up сессий с тренером и т.п.&lt;p&gt;&lt;strong&gt;4. Внедрите систему триггеров в соответствии с планом&lt;/strong&gt;&lt;p&gt;Короткие повторения, напоминания, микро-задания и тесты-викторины в чат-боте непосредственно на рабочем месте не дают &lt;a href=https://www.worklearning.com/wp-content/uploads/2017/10/Spacing_Learning_Over_Time__March2009v1_.pdf rel=&#34;noopener nofollow&#34;&gt;кривой забывания&lt;/a&gt; пойти вниз.&lt;p&gt;&lt;strong&gt;5. Обеспечьте перенос поддерживающей информацией в моменте&lt;/strong&gt;&lt;p&gt;Инструкции, чек-листы и шаблоны в единой базе знаний. Сделайте рабочую тетрадь для фиксации опыта и рефлексии, внедрите ИИ-бот для поддержки сотрудников в процессе переноса.&lt;p&gt;&lt;strong&gt;6. Внедрите систему подкрепления&lt;/strong&gt;&lt;p&gt;Развитие должно поощряться, а усилия сотрудников — быть замечены. Свяжите процесс переноса с результатами, которые являются для сотрудников значимыми. Внедрите также алгоритм корректирующих действий для ситуаций, когда сотрудники не используют обучение.&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;Если позитивные последствия использования обучения отсутствуют, мотивация к переносу будет низкой. Т&lt;/em&gt;очно так же влияют на мотивацию к переносу и ситуации, когда сотрудник знает, когда сотрудник знает&lt;em&gt;, что после обучения никто его не проверит и можно спокойно работать «как раньше» без каких-либо последствий [3,6].&lt;/em&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;strong&gt;7. Организуйте сбор обратной связи от самих сотрудников&lt;/strong&gt;&lt;p&gt;Что из обучения работает в реальном рабочем контексте, а что нет. Выявляйте и масштабируйте то, что сотрудники адаптировали под свои задачи и что теперь приносит результат.&lt;blockquote&gt;&lt;p&gt;&lt;a href=https://www.researchgate.net/publication/321188656_A_dynamic_model_of_training_transfer rel=&#34;noopener nofollow&#34;&gt;&lt;strong&gt;&lt;em&gt;Взрослые — активные участники процесса переноса&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;&lt;em&gt;. &lt;/em&gt;&lt;/strong&gt;&lt;em&gt;Взрослые не просто механически выполняют изученные принципы и алгоритмы, а фильтруют и адаптируют их под свой рабочий контекст. Это естественная часть процесса переноса и особенно сильно она проявляется в ситуациях, где есть свобода выбора и время для внедрения. Такая индивидуальная кастомизация обучения — признак активного вовлечения и индикатор успешного переноса.&lt;/em&gt;&lt;/blockquote&gt;&lt;h2&gt;Резюме&lt;/h2&gt;&lt;p&gt;Следующий уровень зрелости корпоративного обучения в России — это переход от схемы «Обучение → реакции, знания и навыки» к формуле &lt;strong&gt;«Обучение + Перенос = Бизнес-результат»&lt;/strong&gt;, где перенос — обязательная часть программ обучения. Только так новые знания и навыки смогут стать частью ежедневной практики сотрудников, а компании получить максимальную отдачу от инвестиций в корпоративное обучение.&lt;p&gt;Если вы организуете обучение сотрудников по внешним программам, обсудите варианты поддержки переноса с провайдером и зафиксируйте их в техническом задании. Для внутреннего обучения — сделайте перенос стандартом разработки программ. При таком подходе команда L&amp;amp;D создает не просто курсы и тренинги, а проектирует экосистему обучения и развития сотрудников внутри компании.&lt;p&gt;&lt;strong&gt;Источники&lt;/strong&gt;:&lt;ol&gt;&lt;li&gt;&lt;p&gt;&lt;a href=https://www.researchgate.net/publication/321188656_A_dynamic_model_of_training_transfer rel=&#34;noopener nofollow&#34;&gt;A dynamic model of training transfer&lt;/a&gt;.&lt;li&gt;&lt;p&gt;&lt;a href=https://www.researchgate.net/publication/262898094_A_revised_learning_transfer_system_inventory_Factorial_replication_and_validation rel=&#34;noopener nofollow&#34;&gt;A revised learning transfer system inventory: factorial replication and validation&lt;/a&gt;.&lt;li&gt;&lt;p&gt;&lt;a href=https://www.researchgate.net/publication/319336024_Development_of_a_generalized_learning_transfer_system_inventor rel=&#34;noopener nofollow&#34;&gt;Development of a generalized learning transfer system inventory&lt;/a&gt;.&lt;li&gt;&lt;p&gt;&lt;a href=https://www.researchgate.net/publication/209409925_Transfer_of_Training_A_Review_and_Directions_for_Future_Research rel=&#34;noopener nofollow&#34;&gt;Transfer of Training: A Review and Directions for Future Research&lt;/a&gt;.&lt;li&gt;&lt;p&gt;&lt;a href=https://www.researchgate.net/publication/235975809_Transfer_of_Training_A_Meta-Analytic_Review rel=&#34;noopener nofollow&#34;&gt;Transfer of Training: A Meta-Analytic Review&lt;/a&gt;.&lt;li&gt;&lt;p&gt;&lt;a href=https://www.researchgate.net/publication/229573916_The_relationship_between_organizational_transfer_climate_and_positive_transfer_of_training rel=&#34;noopener nofollow&#34;&gt;The relationship between organizational transfer climate and positive transfer of training&lt;/a&gt;.&lt;/ol&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <author>BadmaevLearning</author>
      <guid>https://habr.com/ru/articles/1081128/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1081128</guid>
      <pubDate>Mon, 14 Sep 2026 15:04:42 +0000</pubDate>
    </item>
    <item>
      <title>Зачем разделять роль и личность у виртуальных сотрудников</title>
      <link>https://habr.com/ru/companies/ru_mts/articles/1082076/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082076</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/e3f/64b/1fb/e3f64b1fb5a4c54ec7819a2613585afe.png width=1671 height=941 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/e3f/64b/1fb/e3f64b1fb5a4c54ec7819a2613585afe.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/e3f/64b/1fb/e3f64b1fb5a4c54ec7819a2613585afe.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Привет, Хабр! Допустим, вы внедрили LLM-агента в финансовый отдел компании. Все хорошо, пока вы не решаете его подправить: добавить инструкцию или сделать тон дружелюбнее. С точки зрения классического инфобеза вам нужно все перевалидировать. &lt;p&gt;&lt;a href=https://habr.com/ru/companies/ru_mts/articles/1041048/&gt;Личность вашего агента&lt;/a&gt; (инструкции, тон, самопрезентации и так далее) живет в одном доверительном домене с действиями агента. Если домен строгий, каждое изменение придется сертифицировать. Если же домен разрешительный, аудит только фиксирует действия в логах, но ничего не блокирует. &lt;p&gt;Проблема в том, что некоторые домены обязаны быть строгими. А некоторые из строгих надо менять так часто, что постоянно перевалидировать неудобно. &lt;p&gt;В этом материале расскажу о работе китайского исследователя Исена Си (Yisen Xi), который описал подход PES (&lt;a href=https://arxiv.org/html/2608.27427v1&gt;Persona–Execution Separation&lt;/a&gt;), отделяющий «личность» виртуального агента от задач.&lt;p&gt;NB! PES требует ресурсов; это не «еще один уровень абстракции»; оно работает и заслуживает рассмотрения.&lt;h2&gt;Промпты решают не все&lt;/h2&gt;&lt;p&gt;В LLM-агентах и личность, и инструкции пользователя — один и тот же текст. Отредактировав промпт, вы измените логику решений или тон так же, как если бы поменялась инструкция в личности. Ни модель, ни система контроля не увидят разницы.&lt;p&gt;Получается, к одному доверительному домену — тексту — у нас два противоречивых требования. &lt;p&gt;С одной стороны, личность агента должна развиваться, для чего операторы настраивают инструкции, тон и привязку навыков. И конечно, не разово настраивают, а непрерывно улучшают. &lt;p&gt;С другой — исполнение должно быть аудируемым. Каждое действие, изменяющее состояние, будь то платеж, заявка или одобрение, должно быть записано, привязано к стабильной идентичности и доступно для проверки.&lt;p&gt;Кто пробовал строить такие системы, прекрасно знает, что придется выбирать из двух зол: либо заморозить личность, потому что каждый раз поменять — бюрократический ад; либо ослабить аудит. Первое приводит к тому, что агент не развивается. Второе — к тому, что вы вроде бы записываете логи, но личность меняется — и прошлые действия уже не связаны с новым агентом. &lt;p&gt;На рынке эти подходы используют несколько моделей. Однодоменные агенты ChatGPT и ReAct замораживают личность. LangChain и Dify, наоборот, дают свободу личности с отслеживаемостью на уровне инструментов.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/f34/e2d/9de/f34e2d9de32405bf6911855244f5d5c0.png width=831 height=432 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/f34/e2d/9de/f34e2d9de32405bf6911855244f5d5c0.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/f34/e2d/9de/f34e2d9de32405bf6911855244f5d5c0.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;h4&gt;Готовим требования к агенту&lt;/h4&gt;&lt;p&gt;Исену Си было недостаточно выбора между свободной и замороженной личностями, так что он предложил модель из трех уровней:&lt;ul&gt;&lt;li&gt;&lt;p&gt;G1 — свободный дрейф личности. Меняем инструкции, тон, навыки — никакой перевалидации.&lt;li&gt;&lt;p&gt;G2 — полный аудит каждого действия. Привязан к стабильной идентичности.&lt;li&gt;&lt;p&gt;G3 — разделение. Изменения личности не влияют на аудит.&lt;/ul&gt;&lt;p&gt;Поговорим о реализации.&lt;h2&gt;Один виртуальный сотрудник и два доверительных домена&lt;/h2&gt;&lt;p&gt;Автор придумал простую, но не тривиальную схему: личность и исполнение не обязаны находиться в одном доверительном домене.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/9f9/680/06b/9f968006bc38c0a06f7fd915a95b0383.png width=1068 height=414 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/9f9/680/06b/9f968006bc38c0a06f7fd915a95b0383.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/9f9/680/06b/9f968006bc38c0a06f7fd915a95b0383.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Предлагаемая архитектура PES состоит из трех частей: разрешающего домена, ограничивающего домена и управляемого контрактного моста. Рассмотрим их.&lt;h4&gt;Разрешающий домен &lt;/h4&gt;&lt;p&gt;Содержит личность агента: инструкции, тип общения, самопрезентации и настройки навыков. Здесь операторы могут править промпты, тестировать поведение и общаться в диалоговом интерфейсе. По сути, можно менять все без последствий и вам не потребуются никакие согласования и повторные валидации.&lt;h4&gt;Ограничивающий домен &lt;/h4&gt;&lt;p&gt;Все жестче. Вызывают инструменты для стандартных операционных процедур (SOP). Действия контролируют по матрице утверждений: запретить, спросить, разрешить. В этом домене у нас нет личности как таковой, агент просто выполняет SOP. Соответственно, пользовательская поверхность — не чат-бот, а формы, движок процессов и бухгалтерская книга.&lt;h4&gt;Третий домен — управляемый контрактный мост &lt;/h4&gt;&lt;p&gt;В этом случае два домена соединяются не произвольным API, а строгим контрактом, который пропускает только определенные типы трафика: &lt;ul&gt;&lt;li&gt;&lt;p&gt;Первый тип трафика — данные из ограничивающего домена в разрешающий. В диалоге вы увидите только «Заявка на этапе согласования» без деталей: цифры, файлы и прочее останутся в ограничивающем домене.&lt;li&gt;&lt;p&gt;Второй тип трафика назовем базовым. Здесь данные не покидают ограничивающий домен в произвольном виде, за исключение случаев использования DLP — о нем еще поговорим.&lt;li&gt;&lt;p&gt;И третий тип трафика предполагает непрерывность идентификации. Когда пользователь переходит из диалога в интерфейс выполнения, он не теряет контекст, так как кнопка «Вернуться в диалог» с идентификатором сессии работает как надо. То есть это один виртуальный сотрудник, а не два разных агента.&lt;/ul&gt;&lt;p&gt;Соответственно, мост — это не туннель, а контрольно-пропускной пункт. Там проверяются разрешения в списке контроля доступа и в матрице утверждений, после чего вносится запись в аудит. А при необходимости на мосту мы будем предотвращать утечки конфиденциальных данных (Data Leakage Prevention). &lt;p&gt;Обращаю внимание читателя на пару интересных нюансов:&lt;ul&gt;&lt;li&gt;&lt;p&gt;В ограничивающем домене нет копии личности агента. Это сознательное решение: копия — второй источник правды, который может рассинхронизироваться с первым. Так что вместо копирования предлагают так называемую привязку возможностей. То есть разрешающий домен знает, какие SOP может инициировать агент, но сама SOP (логика, версионирование, полномочия) остается в ограничивающем домене. Уместна аналогия со списком ссылок вместо набора готовых файлов.&lt;li&gt;&lt;p&gt;Наш дрейф не безграничный, и мы не можем менять все что угодно. Личность имеет базовый слой (имя сотрудника, роль, привязку к SOP), и вот это промптом не переделать. А вот инструкции, тон, самопрезентацию и настройку навыков можно править свободно.&lt;/ul&gt;&lt;p&gt;При этом аудиторский реестр привязан к базовому слою. И когда вы меняете тон общения — реестр стабилен.&lt;p&gt;Вы можете сказать: «Ну автор просто вынес исполнение в отдельный сервис, это же обычный паттерн». Не совсем так, или даже совсем не так. Обычный паттерн работает по принципу master-slave, то есть агент просто вызывает функцию. А архитектура PES предлагает контрольный контракт, определяющий утверждение, аудит и идентичность. При этом в PES один виртуальный сотрудник может работать в разных доверительных доменах.&lt;h2&gt;Результаты проверки PES&lt;/h2&gt;&lt;p&gt;Автор работы проверил механизм на пяти конфигурациях модели (менял персону) и получил, что изменение личности не требует повторной валидации на стороне выполнения. То есть он поменял инструкции — и агент продолжал работать, аудит тоже не сломался. В жестко заданных полях не остается следов персоны. То есть ограничивающий домен не знает, какой тон общения вы выбрали, — только какую SOP выполнять.&lt;p&gt;Изначально автор предполагал, что можно разделить личность и исполнение. Через месяц пилота, пять принятых решений и кучу отклоненных альтернатив он получил подтвержденный архитектурный паттерн. Он, конечно, не идеален и не избавит ото всех проблем с LLM-агентами. Но решает поставленную задачу: менять агента без перевалидации, сохраняя полный аудит.&lt;h2&gt;Когда применять PES, а когда нет&lt;/h2&gt;&lt;p&gt;Если вы создаете виртуальных сотрудников в финансовом секторе, здравоохранении или госсекторе — посмотрите на PES. Возможно, вам нужно именно это. Для многопользовательских развертываний PES подходит идеально, так как несколько операторов могут использовать одного агента, меняя его роли и ничего больше. &lt;p&gt;Также архитектура хорошо ложится на системы с требованиями к аудиту, например там, где некоторые действия должны быть строго записаны и привязаны к стабильной идентичности агента.&lt;p&gt;PES эффективен и когда необходимо часто менять личность. Например, там, где инструкции, тон и навыки должны исправляться регулярно. А вот если у вас один пользователь и один агент, действия которого не нужно аудировать, то PES вам вряд ли нужен. И если личность вашего агента меняется раз в год — проще перевалидировать.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <guid>https://habr.com/ru/companies/ru_mts/articles/1082076/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082076</guid>
      <pubDate>Mon, 14 Sep 2026 15:00:59 +0000</pubDate>
    </item>
    <item>
      <title>Валидатор не нашёл поле is_admin, а Go его нашёл: пять ошибок при работе с JSON</title>
      <link>https://habr.com/ru/companies/otus/articles/1076496/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1076496</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;Привет, Хабр!&lt;p&gt;На входе стоит валидатор: JSON Schema, которая знает про поле &lt;code&gt;is_admin&lt;/code&gt; и снаружи выставлять его не даёт. Всё прочее схема считает лишним и пропускает не глядя.&lt;p&gt;За валидатором Go‑сервис, там структура с тегом &lt;code&gt;json:&amp;#34;is_admin&amp;#34;&lt;/code&gt;. Приходит тело, где ключ написан заглавными — &lt;code&gt;IS_ADMIN&lt;/code&gt;.&lt;p&gt;Схема сравнивает строки побуквенно, такого имени не знает, пропускает. А &lt;code&gt;json.Unmarshal&lt;/code&gt; знает и кладёт &lt;code&gt;true&lt;/code&gt; куда надо.&lt;p&gt;Никто из двоих при этом не ошибся: схема отработала по спецификации, &lt;code&gt;encoding/json&lt;/code&gt; — по своей документации, где регистр имён не различается с первых версий Go.&lt;p&gt;Разошлись они в другом: в том, какие ключи в этом теле вообще есть.&lt;p&gt;Go 1.27 вышел 19 августа, и &lt;code&gt;encoding/json&lt;/code&gt; в нём переписали целиком, поверх &lt;code&gt;encoding/json/v2&lt;/code&gt;.&lt;p&gt;Тот два релиза прожил под &lt;code&gt;GOEXPERIMENT=jsonv2&lt;/code&gt; и теперь стал основой. Семантику v1 сохранили намеренно, вплоть до регистра: поменялись тексты ошибок и скорость, поведение осталось прежним.&lt;h3&gt;Поле is_admin отзывается на IS_ADMIN, Is_Admin и на знак Кельвина&lt;/h3&gt;&lt;p&gt;Про регистр обычно помнят коротко примерно так:&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Go не различает большие и маленькие буквы.&lt;/strong&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code class=go&gt;type User struct {&#xA;&#x9;Name  string `json:&amp;#34;name&amp;#34;`&#xA;&#x9;Admin bool   `json:&amp;#34;is_admin&amp;#34;`&#xA;}&#xA;&#xA;bodies := []string{&#xA;&#x9;`{&amp;#34;NAME&amp;#34;:&amp;#34;boris&amp;#34;,&amp;#34;IS_ADMIN&amp;#34;:true}`,&#xA;&#x9;`{&amp;#34;Is_Admin&amp;#34;:true}`,&#xA;&#x9;`{&amp;#34;is-admin&amp;#34;:true}`,&#xA;&#x9;`{&amp;#34;is_admin&amp;#34;:true,&amp;#34;IS_ADMIN&amp;#34;:false}`,&#xA;}&#xA;for _, b := range bodies {&#xA;&#x9;var u User&#xA;&#x9;err := json.Unmarshal([]byte(b), &amp;amp;u)&#xA;&#x9;fmt.Printf(&amp;#34;%-36s %+v err=%v\n&amp;#34;, b, u, err)&#xA;}&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:87px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Вывод:&lt;pre&gt;&lt;code class=go&gt;{&amp;#34;NAME&amp;#34;:&amp;#34;boris&amp;#34;,&amp;#34;IS_ADMIN&amp;#34;:true}     {Name:boris Admin:true} err=&amp;lt;nil&amp;gt;&#xA;{&amp;#34;Is_Admin&amp;#34;:true}                    {Name: Admin:true} err=&amp;lt;nil&amp;gt;&#xA;{&amp;#34;is-admin&amp;#34;:true}                    {Name: Admin:false} err=&amp;lt;nil&amp;gt;&#xA;{&amp;#34;is_admin&amp;#34;:true,&amp;#34;IS_ADMIN&amp;#34;:false}   {Name: Admin:false} err=&amp;lt;nil&amp;gt;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Дефис и подчёркивание v1 всё‑таки различает, поэтому &lt;code&gt;is-admin&lt;/code&gt; пролетел мимо.&lt;p&gt;Четвёртая строка полюбопытнее: в теле два ключа, первый совпал с тегом точно, второй только по регистру, а победил всё равно второй.&lt;p&gt;Точность совпадения тут ни при чём, важен порядок. Значения пишутся в поле по мере разбора, последнее затирает предыдущее.&lt;p&gt;Отсюда растёт вторая половина истории — повторы имён:&lt;pre&gt;&lt;code class=go&gt;type Header struct {&#xA;&#x9;Typ string `json:&amp;#34;typ&amp;#34;`&#xA;&#x9;Alg string `json:&amp;#34;alg&amp;#34;`&#xA;}&#xA;&#xA;var h Header&#xA;json.Unmarshal([]byte(`{&amp;#34;typ&amp;#34;:&amp;#34;JWS&amp;#34;,&amp;#34;alg&amp;#34;:&amp;#34;HS256&amp;#34;,&amp;#34;alg&amp;#34;:&amp;#34;none&amp;#34;}`), &amp;amp;h)&#xA;fmt.Printf(&amp;#34;%+v\n&amp;#34;, h) // {Typ:JWS Alg:none}&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Ошибки нет, в &lt;code&gt;Alg&lt;/code&gt; лежит &lt;code&gt;none&lt;/code&gt;. А рядом вполне может стоять компонент, который берёт первое вхождение. Тогда два сервиса читают один и тот же байтовый поток по‑разному.&lt;p&gt;Сравниваются имена по правилам Unicode simple folding, как в том же &lt;code&gt;strings.EqualFold&lt;/code&gt;. Так во все это попадают буквы, которых в ASCII просто не бывает:&lt;pre&gt;&lt;code class=go&gt;var c struct {&#xA;&#x9;Key string `json:&amp;#34;key&amp;#34;`&#xA;}&#xA;// U+212A KELVIN SIGN вместо обычной K&#xA;json.Unmarshal([]byte(&amp;#34;{\&amp;#34;\u212Aey\&amp;#34;:\&amp;#34;дошло\&amp;#34;}&amp;#34;), &amp;amp;c)&#xA;fmt.Printf(&amp;#34;%+v\n&amp;#34;, c) // {Key:дошло}&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Знак Кельвина совпал с латинской &lt;code&gt;k&lt;/code&gt;.&lt;p&gt;Длинная &lt;code&gt;ſ&lt;/code&gt; (U+017F) точно так же совпадает с &lt;code&gt;s&lt;/code&gt;, и &lt;code&gt;paſsword&lt;/code&gt; доедет до поля &lt;code&gt;password&lt;/code&gt;.&lt;p&gt;Валидатор, который сравнивает байты, ничего такого не увидит.&lt;p&gt;Фиксится это в 1.27 без единой правки в импортах.&lt;p&gt;Опция тега &lt;code&gt;case:strict&lt;/code&gt; работает и в старом пакете:&lt;pre&gt;&lt;code class=go&gt;type Strict struct {&#xA;&#x9;Admin bool `json:&amp;#34;is_admin,case:strict&amp;#34;`&#xA;}&#xA;&#xA;var s Strict&#xA;json.Unmarshal([]byte(`{&amp;#34;IS_ADMIN&amp;#34;:true}`), &amp;amp;s)&#xA;fmt.Printf(&amp;#34;%+v\n&amp;#34;, s) // {Admin:false}&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Я бы вешал &lt;code&gt;case:strict&lt;/code&gt; на всё, что решает про права и про деньги. Планируется миграция на v2 или нет — дела не меняет.&lt;p&gt;Тег закрывает поле, но не закрывает саму схему расхождения. Пока валидация и разбор живут в разных процессах и написаны на разных языках, кто‑то из двоих обязательно поймёт тело иначе.&lt;p&gt;Лучше вообще не проверять чужой JSON снаружи, а разбирать его один раз в том же процессе, что и принимает решение, и дальше передавать уже типизированную структуру. Тогда сравнивать нечему.&lt;h3&gt;Nil‑слайс на выходе, null на фронтенде&lt;/h3&gt;&lt;p&gt;В коде этого не видно совсем, вылезает у того, кто читает ваш ответ.&lt;p&gt;Nil‑слайс и nil‑карта сериализуются в &lt;code&gt;null&lt;/code&gt;, а не в пустой массив и пустой объект:&lt;pre&gt;&lt;code class=go&gt;type Page struct {&#xA;&#x9;Items []string          `json:&amp;#34;items&amp;#34;`&#xA;&#x9;Tags  map[string]string `json:&amp;#34;tags&amp;#34;`&#xA;}&#xA;&#xA;var p Page&#xA;b, _ := json.Marshal(p)&#xA;fmt.Println(string(b)) // {&amp;#34;items&amp;#34;:null,&amp;#34;tags&amp;#34;:null}&#xA;&#xA;p = Page{Items: []string{}, Tags: map[string]string{}}&#xA;b, _ = json.Marshal(p)&#xA;fmt.Println(string(b)) // {&amp;#34;items&amp;#34;:[],&amp;#34;tags&amp;#34;:{}}&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Для клиента разница очень большая.&lt;p&gt;Для клиента разница очень большая.&lt;p&gt;&lt;a href=http://items.map&gt;&lt;code&gt;items.map&lt;/code&gt;&lt;/a&gt;&lt;code&gt;(...)&lt;/code&gt; на &lt;code&gt;null&lt;/code&gt; падает, &lt;code&gt;items.length&lt;/code&gt; падает, а тип &lt;code&gt;string[]&lt;/code&gt; в TypeScript этот &lt;code&gt;null&lt;/code&gt; не описывает и от него не спасает.&lt;p&gt;И зависит всё не от логики, а от того, дошёл ли код до строчки, где слайс инициализируется.&lt;p&gt;Пустая выборка из базы вернёт nil, а выборка на одну строку, отфильтрованная до нуля элементов, вернёт нормальный слайс нулевой длины.&lt;p&gt;Оба варианта адекватные, да и как будто на глаз они не отличаются.&lt;p&gt;Фиксится либо дисциплиной на выходе из репозитория, где вместо nil возвращают &lt;code&gt;[]Item{}&lt;/code&gt;, либо переездом на v2, там nil‑коллекции по умолчанию кодируются как &lt;code&gt;[]&lt;/code&gt; и &lt;code&gt;{}&lt;/code&gt;.&lt;p&gt;Обратный переключатель, если старое поведение зачем‑то нужно, называется &lt;code&gt;jsonv2.FormatNilSliceAsNull&lt;/code&gt;.&lt;h3&gt;Опечатка в имени поля ценой в бесконечный таймаут&lt;/h3&gt;&lt;p&gt;Неизвестные ключи &lt;code&gt;Unmarshal&lt;/code&gt; молча выбрасывает, и сигнала об этом нет никакого:&lt;pre&gt;&lt;code class=go&gt;type Cfg struct {&#xA;&#x9;Retries   int `json:&amp;#34;retries&amp;#34;`&#xA;&#x9;TimeoutMS int `json:&amp;#34;timeout_ms&amp;#34;`&#xA;}&#xA;&#xA;raw := []byte(`{&amp;#34;retries&amp;#34;:5,&amp;#34;timeuot_ms&amp;#34;:100}`)&#xA;&#xA;var c Cfg&#xA;err := json.Unmarshal(raw, &amp;amp;c)&#xA;fmt.Printf(&amp;#34;%+v err=%v\n&amp;#34;, c, err) // {Retries:5 TimeoutMS:0} err=&amp;lt;nil&amp;gt;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Конфиг разобрался, ошибок нет. Что будет дальше, зависит от того, что этот ноль значит в вашем коде. У &lt;code&gt;http.Client&lt;/code&gt; ноль означает «таймаута нет», и запрос будет висеть, пока не оборвётся соединение.&lt;p&gt;Переставленные буквы в &lt;code&gt;timeuot_ms&lt;/code&gt; не подсветит никто: ни компилятор, ни линтер, ни тесты, если тесты подают правильный конфиг.&lt;p&gt;Запретить такое в v1 можно только через &lt;code&gt;Decoder&lt;/code&gt;, у голого &lt;code&gt;Unmarshal&lt;/code&gt; ничего подобного нет:&lt;pre&gt;&lt;code class=go&gt;dec := json.NewDecoder(bytes.NewReader(raw))&#xA;dec.DisallowUnknownFields()&#xA;err = dec.Decode(&amp;amp;c) // json: unknown field &amp;#34;timeuot_ms&amp;#34;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Ошибка вернулась, а &lt;code&gt;Retries&lt;/code&gt; к этому моменту уже записан. Декодер идёт по потоку и встаёт на первом неизвестном ключе, тело целиком он заранее не смотрит.&lt;p&gt;Значит, при ошибке структуру надо выбрасывать, а не разбираться, что там успело заполниться.&lt;p&gt;В v2 то же самое доступно одним вызовом, без обёртки:&lt;pre&gt;&lt;code class=go&gt;err = jsonv2.Unmarshal(raw, &amp;amp;c, jsonv2.RejectUnknownMembers(true))&#xA;// json: cannot unmarshal JSON string into Go main.Cfg:&#xA;// unknown object member name &amp;#34;timeuot_ms&amp;#34;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Для конфигов это стоит включать в 99% случаях. Ну а для публичного входа не стоит: туда лишние поля обычно приходят от клиентов старых версий, и падать на них значит ломать совместимость на ровном месте.&lt;h3&gt;Идентификатор...993 приезжает как...992&lt;/h3&gt;&lt;p&gt;Ошибка вылезает только на больших числах, поэтому доживает до продакшена почти всегда. Когда целевой тип — &lt;code&gt;interface{}&lt;/code&gt; или &lt;code&gt;map[string]any&lt;/code&gt;, числа разбираются в &lt;code&gt;float64&lt;/code&gt;: других вариантов у пустого интерфейса нет.&lt;pre&gt;&lt;code class=go&gt;raw := []byte(`{&amp;#34;id&amp;#34;:9007199254740993,&amp;#34;amount&amp;#34;:12345678901234567}`)&#xA;&#xA;var m map[string]any&#xA;json.Unmarshal(raw, &amp;amp;m)&#xA;fmt.Printf(&amp;#34;%v %T\n&amp;#34;, m[&amp;#34;id&amp;#34;], m[&amp;#34;id&amp;#34;]) // 9.007199254740992e+15 float64&#xA;&#xA;out, _ := json.Marshal(m)&#xA;fmt.Println(string(out))&#xA;// {&amp;#34;amount&amp;#34;:12345678901234568,&amp;#34;id&amp;#34;:9007199254740992}&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Прикинем масштаб.&lt;p&gt;Под мантиссу в &lt;code&gt;float64&lt;/code&gt; отведено 52 бита плюс неявная единица, целые представляются точно до &lt;code&gt;2^53&lt;/code&gt; — это 9 007 199 254 740 992.&lt;p&gt;Наш &lt;code&gt;id&lt;/code&gt; на единицу больше, и выразить его уже нечем, ближайшее доступное значение — само &lt;code&gt;2^53&lt;/code&gt;, туда и округлило.&lt;p&gt;С &lt;code&gt;amount&lt;/code&gt; та же история, только промах на единицу вверх.&lt;p&gt;Получается, прокси, который принимает JSON, разбирает в &lt;code&gt;map[string]any&lt;/code&gt; и отдаёт дальше, тихо портит идентификаторы и суммы.&lt;p&gt;Хотя сам он их не трогает и никакой арифметики не делает.&lt;p&gt;А еще есть места, где типы заранее неизвестны: логирование тела, ретрансляция вебхуков, generic middleware.&lt;p&gt;Со структурой всё в порядке:&lt;pre&gt;&lt;code class=go&gt;var s struct {&#xA;&#x9;ID     int64 `json:&amp;#34;id&amp;#34;`&#xA;&#x9;Amount int64 `json:&amp;#34;amount&amp;#34;`&#xA;}&#xA;json.Unmarshal(raw, &amp;amp;s)&#xA;fmt.Printf(&amp;#34;%+v\n&amp;#34;, s) // {ID:9007199254740993 Amount:12345678901234567}&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Когда типизировать нечего, поможет&lt;code&gt;json.Number&lt;/code&gt; — строка, которая помнит исходную запись числа и ни к чему её не приводит:&lt;pre&gt;&lt;code class=go&gt;var m2 map[string]any&#xA;dec := json.NewDecoder(bytes.NewReader(raw))&#xA;dec.UseNumber()&#xA;dec.Decode(&amp;amp;m2)&#xA;fmt.Printf(&amp;#34;%v %T\n&amp;#34;, m2[&amp;#34;id&amp;#34;], m2[&amp;#34;id&amp;#34;]) // 9007199254740993 json.Number&#xA;&#xA;out2, _ := json.Marshal(m2)&#xA;fmt.Println(string(out2))&#xA;// {&amp;#34;amount&amp;#34;:12345678901234567,&amp;#34;id&amp;#34;:9007199254740993}&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Это единственное место из пяти, которое v2 не фиксит. &lt;code&gt;jsonv2.Unmarshal&lt;/code&gt; в &lt;code&gt;any&lt;/code&gt; тоже кладёт &lt;code&gt;float64&lt;/code&gt; и промахивается на тех же двух числах. Т.е поменять представление чисел по умолчанию значило бы сломать всех, кто рассчитывает на &lt;code&gt;float64&lt;/code&gt;. Но и надеяться, что переезд сам всё исправит, не выйдет.&lt;h3&gt;time.Time и вложенная структура, которых omitempty не видит&lt;/h3&gt;&lt;p&gt;&lt;code&gt;omitempty&lt;/code&gt; выбрасывает поле, если значение — &lt;code&gt;false&lt;/code&gt;, &lt;code&gt;0&lt;/code&gt;, nil‑указатель, nil‑интерфейс либо пустые массив, слайс, карта или строка. Структур в этом списке нет и никогда не было.&lt;pre&gt;&lt;code class=go&gt;type Page struct {&#xA;&#x9;Num  int `json:&amp;#34;num&amp;#34;`&#xA;&#x9;Size int `json:&amp;#34;size&amp;#34;`&#xA;}&#xA;&#xA;type Filter struct {&#xA;&#x9;Q    string    `json:&amp;#34;q,omitempty&amp;#34;`&#xA;&#x9;From time.Time `json:&amp;#34;from,omitempty&amp;#34;`&#xA;&#x9;Page Page      `json:&amp;#34;page,omitempty&amp;#34;`&#xA;&#x9;N    int       `json:&amp;#34;n,omitempty&amp;#34;`&#xA;}&#xA;&#xA;b, _ := json.Marshal(Filter{})&#xA;fmt.Println(string(b))&#xA;// {&amp;#34;from&amp;#34;:&amp;#34;0001-01-01T00:00:00Z&amp;#34;,&amp;#34;page&amp;#34;:{&amp;#34;num&amp;#34;:0,&amp;#34;size&amp;#34;:0}}&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Строка и число из вывода ушли, а нулевое время и пустая вложенная структура остались.&lt;p&gt;Дальше это едет в чужой API, который читает &lt;code&gt;from&lt;/code&gt; буквально и начинает выборку с первого января первого года.&lt;p&gt;Или, если с валидацией там повезло, отвечает четырёхсотым на дату вне диапазона.&lt;p&gt;С Go 1.24 есть опция тега &lt;code&gt;omitzero&lt;/code&gt;.&lt;p&gt;Она смотрит не на «пустоту», а на нулевое значение типа, и если у типа есть метод &lt;code&gt;IsZero() bool&lt;/code&gt;, спрашивает его.&lt;p&gt;У &lt;code&gt;time.Time&lt;/code&gt; метод есть:&lt;pre&gt;&lt;code class=go&gt;type Filter2 struct {&#xA;&#x9;Q    string    `json:&amp;#34;q,omitzero&amp;#34;`&#xA;&#x9;From time.Time `json:&amp;#34;from,omitzero&amp;#34;`&#xA;&#x9;Page Page      `json:&amp;#34;page,omitzero&amp;#34;`&#xA;}&#xA;&#xA;b, _ = json.Marshal(Filter2{})&#xA;fmt.Println(string(b)) // {}&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;На коллекциях две опции всё же расходятся.&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;omitempty&lt;/code&gt; выбрасывает слайс или карту нулевой длины, включая nil.&lt;li&gt;&lt;p&gt;&lt;code&gt;omitzero&lt;/code&gt; выбрасывает только nil, а инициализированный пустой слайс оставляет.&lt;/ul&gt;&lt;p&gt;Для коллекций обычно нужна первая, для остального вторая, и ставить обе через запятую никто не мешает.&lt;h3&gt;А если просто поменять импорт на v2?&lt;/h3&gt;&lt;p&gt;Тогда поменяется больше, чем хотелось бы. У v2 другие значения по умолчанию сразу по полутора десяткам пунктов.&lt;p&gt;Часть из них — то, ради чего переезжают. Имена сопоставляются точно, повтор ключа даёт ошибку, nil‑коллекции кодируются как &lt;code&gt;[]&lt;/code&gt; и &lt;code&gt;{}&lt;/code&gt;. Невалидный UTF-8 внутри строки тоже роняет разбор, а не подменяется тихо на символ замещения. Другая часть:&lt;pre&gt;&lt;code class=go&gt;// v1 сортирует ключи карты, v2 — нет&#xA;m := map[string]int{&amp;#34;z&amp;#34;: 1, &amp;#34;a&amp;#34;: 2, &amp;#34;m&amp;#34;: 3, &amp;#34;b&amp;#34;: 4}&#xA;b1, _ := json.Marshal(m)   // {&amp;#34;a&amp;#34;:2,&amp;#34;b&amp;#34;:4,&amp;#34;m&amp;#34;:3,&amp;#34;z&amp;#34;:1}&#xA;b2, _ := jsonv2.Marshal(m) // {&amp;#34;b&amp;#34;:4,&amp;#34;z&amp;#34;:1,&amp;#34;a&amp;#34;:2,&amp;#34;m&amp;#34;:3} — и каждый раз по-новому&#xA;&#xA;// time.Duration в v2 не имеет представления по умолчанию&#xA;type D struct {&#xA;&#x9;TTL time.Duration `json:&amp;#34;ttl&amp;#34;`&#xA;}&#xA;_, err := jsonv2.Marshal(D{TTL: 5 * time.Second})&#xA;fmt.Println(err)&#xA;// json: cannot marshal from Go time.Duration within &amp;#34;/ttl&amp;#34;:&#xA;// no default representation&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Недетерминированный порядок ключей ломает всё, что хеширует или диффает результат сериализации:&lt;ul&gt;&lt;li&gt;&lt;p&gt;подписи запросов;&lt;li&gt;&lt;p&gt;снапшот‑тесты;&lt;li&gt;&lt;p&gt;сравнение конфигов.&lt;/ul&gt;&lt;p&gt;Возвращается опцией &lt;code&gt;jsonv2.Deterministic&lt;/code&gt;.&lt;p&gt;С &lt;code&gt;time.Duration&lt;/code&gt; решение осознанное — в v1 она кодировалась числом наносекунд, что читателю JSON ни о чём не говорит, — но код, который на это опирался, упадёт в рантайме, а не на сборке.&lt;p&gt;Мельче калибром, но тоже ловится не сразу, ведь массив фиксированной длины в v2 требует такой же длины в JSON, а &lt;code&gt;[N]byte&lt;/code&gt; кодируется в base64-строку вместо массива чисел.&lt;p&gt;Первое обычно к лучшему, второе меняет форму тела и ломает читателя на той стороне.&lt;p&gt;Обе штуки откатываются опциями &lt;code&gt;UnmarshalArrayFromAnyLength&lt;/code&gt; и &lt;code&gt;FormatByteArrayAsArray&lt;/code&gt; из пакета v1.&lt;p&gt;Но интереснее всего &lt;code&gt;omitempty&lt;/code&gt;: у знакомой опции меняется смысл. В v2 поле выбрасывается, если кодируется в пустое JSON‑значение — &lt;code&gt;null&lt;/code&gt;, пустая строка, пустой объект, пустой массив.&lt;p&gt;Ноль пустым JSON‑значением не считается:&lt;pre&gt;&lt;code class=go&gt;b, _ := jsonv2.Marshal(Filter{})&#xA;fmt.Println(string(b))&#xA;// {&amp;#34;from&amp;#34;:&amp;#34;0001-01-01T00:00:00Z&amp;#34;,&amp;#34;page&amp;#34;:{&amp;#34;num&amp;#34;:0,&amp;#34;size&amp;#34;:0},&amp;#34;n&amp;#34;:0}&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Поле &lt;code&gt;n&lt;/code&gt; с тем же самым тегом в v1 из вывода уходило, а в v2 остаётся.&lt;p&gt;Документация советует перевести все &lt;code&gt;omitempty&lt;/code&gt; на булевых, числовых, указательных и интерфейсных полях в &lt;code&gt;omitzero&lt;/code&gt; — эта опция в обеих версиях работает одинаково.&lt;p&gt;Переехать без такой ревизии тегов значит разом поменять форму всех исходящих тел.&lt;p&gt;Переезжать при этом можно постепенно.&lt;p&gt;Опции в v2 применяются слева направо, поздние перекрывают ранние, а &lt;code&gt;jsonv1.DefaultOptionsV1()&lt;/code&gt; собирает весь набор старого поведения одним аргументом:&lt;pre&gt;&lt;code class=go&gt;var s struct {&#xA;&#x9;Admin bool `json:&amp;#34;is_admin&amp;#34;`&#xA;}&#xA;err := jsonv2.Unmarshal([]byte(`{&amp;#34;IS_ADMIN&amp;#34;:true}`), &amp;amp;s,&#xA;&#x9;jsonv1.DefaultOptionsV1(),&#xA;&#x9;jsonv2.MatchCaseInsensitiveNames(false))&#xA;fmt.Printf(&amp;#34;%+v err=%v\n&amp;#34;, s, err) // {Admin:false} err=&amp;lt;nil&amp;gt;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Тут мы остались на семантике v1 целиком и выключили ровно одно поведение — сопоставление без учёта регистра.&lt;blockquote&gt;&lt;p&gt;Так и стоит двигаться: по одному отличию за шаг, прогоняя тесты, а не большим переключателем, после которого непонятно, что поехало.&lt;/blockquote&gt;&lt;h3&gt;Что со всем этим делать&lt;/h3&gt;&lt;p&gt;Ни одно из пяти разобранных мест не является багом.&lt;p&gt;Это решения, принятые в самом начале жизни пакета под другие задачи и с тех пор законсервированные обещанием совместимости.&lt;p&gt;Регистр не различают, чтобы &lt;code&gt;{&amp;#34;Name&amp;#34;: ...}&lt;/code&gt; попадало в поле &lt;code&gt;Name&lt;/code&gt; без всякого тега.&lt;p&gt;&lt;code&gt;null&lt;/code&gt; вместо &lt;code&gt;[]&lt;/code&gt; — потому что nil‑слайс в Go честно ничего не значит.&lt;p&gt;&lt;code&gt;float64&lt;/code&gt; для &lt;code&gt;any&lt;/code&gt; — потому что других чисел у пустого интерфейса нет.&lt;p&gt;По отдельности всё объяснимо, а вместе получается пакет, документация которого в Go 1.27 говорит:&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;значения по умолчанию у v1 менее безопасные, новый код лучше писать на v2.&lt;/strong&gt;&lt;/blockquote&gt;&lt;p&gt;Порядок я бы выбрал такой.&lt;p&gt;Сначала теги, они бесплатные и работают в старом пакете:&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;case:strict&lt;/code&gt; на всё, что решает про права и деньги;&lt;li&gt;&lt;p&gt;&lt;code&gt;omitzero&lt;/code&gt; вместо &lt;code&gt;omitempty&lt;/code&gt; на числах, булях и &lt;code&gt;time.Time&lt;/code&gt;.&lt;/ul&gt;&lt;p&gt;Потом &lt;code&gt;DisallowUnknownFields&lt;/code&gt; на конфигах и внутренних ручках, но не на публичном входе.&lt;p&gt;Переезд на &lt;code&gt;encoding/json/v2&lt;/code&gt; — отдельной задачей, через &lt;code&gt;DefaultOptionsV1()&lt;/code&gt; и по одному отличию за раз.&lt;p&gt;Первым делом проверить всё, что хеширует или сравнивает сериализованный результат.&lt;p&gt;Если поедет прямо на сборке, в 1.27 есть выход &lt;code&gt;GOEXPERIMENT=nojsonv2&lt;/code&gt;, он возвращает старую реализацию целиком.&lt;p&gt;Насчет ресурсов, особо не разбирался, но в release notes было написано, что маршалинг примерно на уровне старого, а анмаршалинг заметно быстрее.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/f80/06f/3cb/f8006f3cbc85cf68846c53f13c23f910.png width=2640 height=300 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/f80/06f/3cb/f8006f3cbc85cf68846c53f13c23f910.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/f80/06f/3cb/f8006f3cbc85cf68846c53f13c23f910.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Если при разработке сервисов приходится разбираться, почему один компонент принимает данные, а другой понимает их иначе, проблема обычно не в одной строке кода. Нужно уметь видеть границы между сервисами, находить места, где возникают расхождения, и проектировать систему так, чтобы подобные ошибки не попадали в продакшен.&lt;p&gt;На открытых уроках разберём, как глубже понимать устройство Go‑приложений и проектировать надёжные микросервисные системы:&lt;ul&gt;&lt;li&gt;&lt;p&gt;22 сентября в 20:00. «Горутины и каналы: под капотом и нюансы в продакшене». &lt;a href=https://otus.pw/PBRM/&gt;Записаться&lt;/a&gt;&lt;li&gt;&lt;p&gt;22 октября в 19:00. «Основы проектирования бизнес‑логики в микросервисной архитектуре». &lt;a href=https://otus.pw/6857/&gt;Записаться&lt;/a&gt;&lt;/ul&gt;&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;А полный список бесплатных уроков сентября вы найдете &lt;/em&gt;&lt;a href=https://habr.com/ru/companies/otus/articles/1076824/&gt;&lt;em&gt;в дайджесте.&lt;/em&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <guid>https://habr.com/ru/companies/otus/articles/1076496/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1076496</guid>
      <pubDate>Mon, 14 Sep 2026 14:50:58 +0000</pubDate>
    </item>
    <item>
      <title>Выходит Java 27</title>
      <link>https://habr.com/ru/companies/spring_aio/articles/1082138/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082138</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;15 сентября 2026 года вышла Java 27. Это не LTS-релиз, и новых возможностей языка, ради которых команды бросятся переписывать код, здесь немного. Зато поведение самой JVM меняется достаточно заметно.&lt;p&gt;Компактные заголовки объектов включаются по умолчанию, G1 становится стандартным сборщиком мусора даже в окружениях с небольшим количеством ресурсов, а Java Flight Recorder начинает автоматически скрывать пароли, токены и другие чувствительные значения.&lt;p&gt;Для Spring Boot-приложения это может означать меньше занятый heap без изменений в коде. А может означать неожиданно другой профиль потребления CPU в маленьком контейнере или неработающий стартовый скрипт из-за удалённого JVM-флага.&lt;p&gt;Разберёмся, что именно меняется и что стоит проверить до обновления.&lt;h3&gt;Коротко&lt;/h3&gt;&lt;p&gt;В Java 27 есть три важных изменения runtime:&lt;ol&gt;&lt;li&gt;&lt;p&gt;Compact Object Headers включены по умолчанию. Во многих приложениях это способно сократить занятый heap на 10–20%, но результат зависит от структуры объектов.&lt;li&gt;&lt;p&gt;G1 теперь выбирается по умолчанию во всех окружениях. Маленький контейнер, который раньше запускался с Serial GC, после обновления может получить другой профиль памяти, CPU и пауз.&lt;li&gt;&lt;p&gt;JFR автоматически редактирует чувствительные данные в аргументах JVM, переменных окружения и системных свойствах.&lt;/ol&gt;&lt;p&gt;Кроме того, удалены некоторые устаревшие JVM-флаги, изменился JSON-формат дампов потоков и появились новые диагностические возможности jcmd.&lt;h3&gt;Откуда в Java-объекте лишние байты&lt;/h3&gt;&lt;p&gt;Объект в heap состоит не только из объявленных нами полей. JVM хранит рядом служебную информацию:&lt;ul&gt;&lt;li&gt;&lt;p&gt;состояние блокировки;&lt;li&gt;&lt;p&gt;данные, связанные с &lt;code&gt;identityHashCode&lt;/code&gt;;&lt;li&gt;&lt;p&gt;возраст объекта для сборщика мусора;&lt;li&gt;&lt;p&gt;ссылку на описание класса объекта.&lt;/ul&gt;&lt;p&gt;В типичной 64-битной JVM с включёнными compressed class pointers заголовок объекта занимал 12 байт:&lt;ul&gt;&lt;li&gt;&lt;p&gt;8 байт для mark word;&lt;li&gt;&lt;p&gt;4 байта для ссылки на класс.&lt;/ul&gt;&lt;p&gt;Из-за выравнивания объект без полей обычно занимал не 12, а 16 байт. Получалась любопытная ситуация: экземпляр пустого класса потреблял больше памяти на устройство JVM, чем на собственные данные.&lt;pre&gt;&lt;code class=java&gt;final class Marker {&#xA;}&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:87px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;В реальном приложении пустые объекты встречаются нечасто, но маленьких объектов очень много. DTO, элементы коллекций, прокси, метаданные, контексты, события, лямбды и вспомогательные структуры создаются тысячами и миллионами.&lt;p&gt;Особенно хорошо это заметно в Spring-приложениях. Сам фреймворк, библиотеки и пользовательский код формируют довольно насыщенный объектный граф ещё до обработки первого HTTP-запроса.&lt;h3&gt;Что делают Compact Object Headers&lt;/h3&gt;&lt;p&gt;Компактные заголовки объединяют compressed class pointer и mark word. В результате минимальный заголовок на 64-битной JVM уменьшается с 96–128 до 64 бит.&lt;p&gt;Фича прошла несколько стадий:&lt;ul&gt;&lt;li&gt;&lt;p&gt;в JDK 24 она была экспериментальной;&lt;li&gt;&lt;p&gt;в JDK 25 стала финальной, но требовала -XX:+UseCompactObjectHeaders;&lt;li&gt;&lt;p&gt;в JDK 27 включается по умолчанию.&lt;/ul&gt;&lt;p&gt;То есть после обновления больше не нужно добавлять специальный флаг. Отключить механизм для диагностики всё ещё можно:&lt;pre&gt;&lt;code class=bash&gt;java -XX:-UseCompactObjectHeaders -jar application.jar&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Проверить текущее значение параметра можно так:&lt;pre&gt;&lt;code class=bash&gt;java -XX:+PrintFlagsFinal -version | grep UseCompactObjectHeaders&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h3&gt;Действительно ли heap уменьшится на 20%&lt;/h3&gt;&lt;p&gt;В материалах OpenJDK и Inside Java встречается оценка экономии heap в диапазоне 10–20%. Но её нельзя читать как обещание уменьшить потребление любого приложения ровно на 20%.&lt;p&gt;Результат зависит от объектного графа.&lt;p&gt;Если приложение создаёт много маленьких объектов, четыре сэкономленных байта на каждом экземпляре быстро складываются в заметную величину. Дополнительный эффект даёт выравнивание: некоторые объекты переходят в меньшую размерную категорию.&lt;p&gt;Если же основную часть heap занимают большие массивы &lt;code&gt;byte[]&lt;/code&gt;, кэши с крупными значениями или несколько тяжёлых структур, относительная экономия окажется скромнее.&lt;p&gt;Есть и важное различие между размером live set и лимитом heap. Если контейнер запускается с &lt;code&gt;-Xmx2g&lt;/code&gt;, Java 27 не превратит этот параметр в &lt;code&gt;-Xmx1600m&lt;/code&gt;. Уменьшиться может объём памяти, который занимают живые объекты. Это потенциально даёт:&lt;ul&gt;&lt;li&gt;&lt;p&gt;больше свободного пространства внутри прежнего heap;&lt;li&gt;&lt;p&gt;более редкие циклы сборки мусора;&lt;li&gt;&lt;p&gt;меньше перемещаемых данных;&lt;li&gt;&lt;p&gt;лучшую локальность данных в процессорном кэше;&lt;li&gt;&lt;p&gt;возможность позднее уменьшить &lt;code&gt;-Xmx&lt;/code&gt;, если это подтвердят измерения на вашем ворклоаде.&lt;/ul&gt;&lt;p&gt;Сначала нужно сравнить метрики, и только затем менять лимиты контейнера.&lt;h3&gt;Как проверить эффект на Spring Boot-приложении&lt;/h3&gt;&lt;p&gt;Лучше всего сравнивать две конфигурации одной и той же Java 27:&lt;pre&gt;&lt;code class=bash&gt;java -XX:+UseCompactObjectHeaders -jar application.jar&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;и:&lt;pre&gt;&lt;code class=bash&gt;java -XX:-UseCompactObjectHeaders -jar application.jar&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Так мы изолируем влияние компактных заголовков от остальных изменений между версиями JDK.&lt;p&gt;Во время теста стоит смотреть не только на максимальный RSS процесса, но и на:&lt;ul&gt;&lt;li&gt;&lt;p&gt;размер live set после полной сборки;&lt;li&gt;&lt;p&gt;частоту и длительность GC;&lt;li&gt;&lt;p&gt;allocation rate;&lt;li&gt;&lt;p&gt;количество старых объектов;&lt;li&gt;&lt;p&gt;время прогрева;&lt;li&gt;&lt;p&gt;p95 и p99 latency;&lt;li&gt;&lt;p&gt;CPU на одинаковом профиле нагрузки.&lt;/ul&gt;&lt;p&gt;Для анализа структуры отдельных классов можно использовать Java Object Layout:&lt;pre&gt;&lt;code class=java&gt;System.out.println(&#xA;    org.openjdk.jol.info.ClassLayout&#xA;        .parseClass(OrderDto.class)&#xA;        .toPrintable()&#xA;);&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Но JOL показывает layout конкретного объекта, а не экономику приложения целиком. Для итогового вывода всё равно понадобится профиль под реалистичной нагрузкой.&lt;h3&gt;G1 теперь используется даже на маленьких машинах&lt;/h3&gt;&lt;p&gt;G1 считается сборщиком мусора по умолчанию с JDK 9, но существовало исключение. Если JVM определяла окружение как машину с ограниченными ресурсами, она могла выбрать Serial GC.&lt;p&gt;В Java 27 это исключение убрали. Теперь G1 используется по умолчанию во всех окружениях, если сборщик не указан явно.&lt;p&gt;Узнать выбранный GC можно командой (или из &lt;code&gt;GarbageCollectorMXBean&lt;/code&gt;):&lt;pre&gt;&lt;code class=bash&gt;java -Xlog:gc -version&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;В Java 27 без дополнительных параметров в журнале должна появиться строка о G1.&lt;p&gt;Для обычного серверного приложения это выглядит логично. G1 умеет параллельно обрабатывать части heap и стремится удерживать паузы в заданных пределах. Но более современный GC не означает «лучший GC для любого процесса».&lt;p&gt;Serial GC прост, использует меньше служебных структур и может быть вполне разумным выбором для маленьких утилит, однопроцессорных окружений и сервисов с очень небольшим heap.&lt;h3&gt;Почему это важно для Kubernetes&lt;/h3&gt;&lt;p&gt;JVM учитывает ограничения контейнера, включая доступную память и количество процессоров. Поэтому изменение особенно заметно для небольших pod.&lt;p&gt;Представим Spring Boot-сервис с такими ресурсами:&lt;pre&gt;&lt;code class=json&gt;resources:&#xA;  requests:&#xA;    cpu: &amp;#34;250m&amp;#34;&#xA;    memory: &amp;#34;256Mi&amp;#34;&#xA;  limits:&#xA;    cpu: &amp;#34;500m&amp;#34;&#xA;    memory: &amp;#34;384Mi&amp;#34;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;На предыдущей версии JDK он мог автоматически получить Serial GC. После перехода на Java 27 тот же сервис без изменения параметров запуска получит G1.&lt;p&gt;Это способно изменить:&lt;ul&gt;&lt;li&gt;&lt;p&gt;количество GC-потоков;&lt;li&gt;&lt;p&gt;потребление CPU во время сборки;&lt;li&gt;&lt;p&gt;служебные расходы GC;&lt;li&gt;&lt;p&gt;длительность пауз;&lt;li&gt;&lt;p&gt;поведение приложения при приближении к memory limit.&lt;/ul&gt;&lt;p&gt;Если Serial GC был осознанным выбором, его лучше зафиксировать явно:&lt;pre&gt;&lt;code class=bash&gt;java -XX:+UseSerialGC -jar application.jar&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Если сборщик никогда не указывался, перед обновлением нужно снять фактическую конфигурацию текущей JVM и сравнить её с Java 27. Иначе изменение будет незаметно в коде и манифестах, но проявится в продакшн-метриках.&lt;h3&gt;JFR больше не должен записывать токены открытым текстом&lt;/h3&gt;&lt;p&gt;Java Flight Recorder собирает подробную диагностическую информацию с небольшими накладными расходами. Это делает JFR удобным для использования не только локально, но и в продакшене.&lt;p&gt;Проблема в том, что вместе с полезными данными в запись могли попасть:&lt;ul&gt;&lt;li&gt;&lt;p&gt;аргументы запуска JVM;&lt;li&gt;&lt;p&gt;переменные окружения;&lt;li&gt;&lt;p&gt;системные свойства.&lt;/ul&gt;&lt;p&gt;Именно через эти механизмы приложения часто получают секреты:&lt;pre&gt;&lt;code class=bash&gt;export PAYMENT_API_TOKEN=super-secret-value&#xA;&#xA;java \&#xA;&#xA;  -Dspring.datasource.password=another-secret \&#xA;&#xA;  -XX:StartFlightRecording=filename=application.jfr,duration=60s \&#xA;&#xA;  -jar application.jar&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Если такой файл попадал в тикет, общий каталог или систему хранения диагностики, секрет мог утечь вместе с ним.&lt;p&gt;В Java 27 JFR по умолчанию скрывает значения, названия которых похожи на:&lt;ul&gt;&lt;li&gt;&lt;p&gt;password;&lt;li&gt;&lt;p&gt;passwd;&lt;li&gt;&lt;p&gt;token;&lt;li&gt;&lt;p&gt;secret;&lt;li&gt;&lt;p&gt;credential;&lt;li&gt;&lt;p&gt;api-key;&lt;li&gt;&lt;p&gt;private-key;&lt;li&gt;&lt;p&gt;client-secret.&lt;/ul&gt;&lt;p&gt;Сопоставление выполняется без учёта регистра и поддерживает glob-шаблоны.&lt;p&gt;Можно добавить собственное имя:&lt;pre&gt;&lt;code class=bash&gt;java \&#xA;&#xA;  -XX:FlightRecorderOptions:redact-key=+dburl \&#xA;  -jar application.jar&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Знак + здесь важен, так как он добавляет правило к стандартным фильтрам. Без него пользовательский список может заменить набор по умолчанию.&lt;p&gt;Для аргументов JVM используется отдельный параметр:&lt;pre&gt;&lt;code class=bash&gt;java \&#xA;  -XX:FlightRecorderOptions:redact-arguments=@redact-patterns.txt \&#xA;  -jar application.jar&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Посмотреть доступные настройки можно через встроенную справку &lt;code&gt;-XX:FlightRecorderOptions&lt;/code&gt;.&lt;h3&gt;Почему одной редактуры JFR недостаточно&lt;/h3&gt;&lt;p&gt;Новый механизм уменьшает вероятность случайной утечки, но не превращает небезопасную работу с секретами в безопасную.&lt;p&gt;Во-первых, фильтрация основана на именах и шаблонах. Переменная &lt;code&gt;PAYMENT_API_TOKEN&lt;/code&gt; будет распознана, а условная переменная &lt;code&gt;ACCESS&lt;/code&gt; может не попасть под стандартное правило.&lt;p&gt;Во-вторых, приложение способно самостоятельно записать чувствительное значение в пользовательское JFR-событие, лог или exception message.&lt;p&gt;В-третьих, JFR-файл всё равно содержит подробности о работе приложения и должен считаться диагностическим артефактом с ограниченным доступом.&lt;p&gt;Получается, что автоматическая редактура является дополнительным защитным слоем, а не заменой Vault, Kubernetes Secrets, IAM и нормальной политики доступа к диагностике.&lt;h3&gt;Какие старые JVM-флаги перестанут работать&lt;/h3&gt;&lt;p&gt;В Java 27 окончательно удалены:&lt;pre&gt;&lt;code&gt;-noclassgc&#xA;-noverify&#xA;-verifyremote&#xA;-Xverify:none&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Для &lt;code&gt;-noclassgc&lt;/code&gt; существует замена:&lt;pre&gt;&lt;code&gt;-Xnoclassgc&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Для &lt;code&gt;-verifyremote&lt;/code&gt;:&lt;pre&gt;&lt;code&gt;-Xverify:remote&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;У &lt;code&gt;-noverify&lt;/code&gt; и &lt;code&gt;-Xverify:none&lt;/code&gt; прямой замены нет.&lt;p&gt;Последние два флага нередко оставались в старых шаблонах запуска, Dockerfile и конфигурациях IDE как попытка ускорить старт. После перехода на Java 27 JVM завершит запуск с ошибкой, поэтому искать их нужно заранее:&lt;pre&gt;&lt;code class=bash&gt;grep -R --line-number \&#xA;  -e=-noverify \&#xA;  -e=-Xverify:none \&#xA;  -e=-noclassgc \&#xA;  -e=-verifyremote \&#xA;  .&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Также параметр:&lt;pre&gt;&lt;code class=bash&gt;-XX:InitiatingHeapOccupancyPercent&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;переименован в:&lt;pre&gt;&lt;code class=bash&gt;-XX:G1IHOP&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Старое имя пока работает, но уже помечено устаревшим.&lt;p&gt;Опция &lt;code&gt;-XX:[+|-]UseCompressedClassPointers&lt;/code&gt; стала obsolete. JVM всегда использует compressed class pointers, а передача старого параметра приведёт к предупреждению.&lt;h3&gt;Ещё несколько изменений для эксплуатации&lt;/h3&gt;&lt;p&gt;В jcmd появилась команда, которая показывает активные security properties работающей JVM:&lt;pre&gt;&lt;code class=bash&gt;jcmd &amp;lt;pid&amp;gt; VM.security_properties&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Это полезно при разборе проблем с TLS, криптографическими провайдерами и алгоритмами, когда итоговая конфигурация отличается от ожидаемой.&lt;p&gt;VM.info и аварийные файлы &lt;code&gt;hs_err_pid&lt;/code&gt; теперь содержат текущее количество открытых файловых дескрипторов. При расследовании падения можно быстрее заметить, что процесс подошёл к системному лимиту.&lt;p&gt;Изменился и JSON-формат thread dump. Идентификаторы потоков, их количество и PID теперь записываются числами, а не строками:&lt;pre&gt;&lt;code class=bash&gt;{&#xA;  &amp;#34;tid&amp;#34;: 42&#xA;}&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;В корневом объекте появился &lt;code&gt;formatVersion&lt;/code&gt; со значением 2.&lt;p&gt;Если внутренние инструменты разбирают JSON-дампы в строго типизированную модель и ожидают &lt;code&gt;&amp;#34;tid&amp;#34;: &amp;#34;42&amp;#34;&lt;/code&gt;, после обновления они могут перестать работать. Это тот редкий breaking change, который затрагивает не приложение, а окружающую его диагностическую инфраструктуру.&lt;p&gt;Наконец, из JDK удалён экспериментальный JVM Compiler Interface, включая jdk.internal.vm.ci, встроенные компоненты Graal compiler и флаг &lt;code&gt;-XX:+UseGraalJIT&lt;/code&gt;. Обычные Spring Boot-приложения этого не заметят, но нестандартные сборки и инструменты, которые полагались на JVMCI внутри OpenJDK, нужно проверить отдельно.&lt;h3&gt;Стоит ли переходить&lt;/h3&gt;&lt;p&gt;Java 27 не является LTS-релизом. Для большинства компаний основной production-версией останется Java 25, а следующей LTS станет Java 29.&lt;p&gt;Но пропускать Java 27 при тестировании не стоит.&lt;p&gt;Во-первых, Compact Object Headers уже стали финальной возможностью в Java 25. Java 27 показывает, каким будет стандартное поведение будущих версий.&lt;p&gt;Во-вторых, смена GC в ограниченных окружениях затрагивает именно ту область, где автоматические решения JVM часто остаются незаметными до появления странных графиков в продакшене.&lt;p&gt;В-третьих, автоматическая редактура данных JFR делает диагностику безопаснее и, вероятно, позволит большему числу команд использовать Flight Recorder постоянно.&lt;p&gt;Java 27 интересна не новым синтаксисом. Она меняет стоимость существующих объектов, правила выбора GC и безопасность эксплуатационных данных. Код может остаться прежним, но профиль приложения уже будет другим.&lt;p&gt;Именно поэтому этот релиз стоит проверять не глазами компилятора, а нагрузочными тестами, JFR и метриками контейнера.&lt;h3&gt;Источники&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;a href=https://inside.java/2026/09/12/jdk-27-runtime-updates/&gt;JDK 27 Runtime Updates Release Notes&lt;/a&gt;&lt;li&gt;&lt;p&gt;&lt;a href=https://openjdk.org/jeps/523&gt;JEP 523: Make G1 the Default Garbage Collector in All Environments&lt;/a&gt;&lt;li&gt;&lt;p&gt;&lt;a href=https://openjdk.org/jeps/534&gt;JEP 534: Compact Object Headers by Default&lt;/a&gt;&lt;li&gt;&lt;p&gt;&lt;a href=https://openjdk.org/jeps/536&gt;JEP 536: JFR In-Process Data Redaction&lt;/a&gt;&lt;li&gt;&lt;p&gt;&lt;a href=https://inside.java/2026/09/10/podcast-069/&gt;Declassifying Java 27&lt;/a&gt;&lt;/ul&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/258/484/92d/25848492d4cd2dcd1df595e475569a9a.png alt width=1560 height=334 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/258/484/92d/25848492d4cd2dcd1df595e475569a9a.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/258/484/92d/25848492d4cd2dcd1df595e475569a9a.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;&lt;a href=https://t.me/+acrI2N6q080wZjM6&gt;Присоединяйтесь&lt;/a&gt; к русскоязычному сообществу разработчиков на Spring Boot в телеграм — &lt;a href=https://t.me/+acrI2N6q080wZjM6&gt;Spring АйО&lt;/a&gt;, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <guid>https://habr.com/ru/companies/spring_aio/articles/1082138/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082138</guid>
      <pubDate>Mon, 14 Sep 2026 14:47:44 +0000</pubDate>
    </item>
    <item>
      <title>«Агат», «ДВК», «Микроша»: на чём советские гики учились программировать? Часть 1</title>
      <link>https://habr.com/ru/companies/studyai/articles/1082134/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082134</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;В начале 80-х мир захлестнула — по крайней мере его гиковскую часть — ПК-мания, и у «буржуев» набирали высоту BBC Micro, ZX Spectrum, MicroBee, Apple II и другие машинки, на которых миллионы западных школьников с упоением писали свои первые программы и игры.&lt;p&gt;А как дела обстояли в наших весях? Давайте чуть-чуть разберёмся — и да простят меня за неточности почтенные олды, заставшие те времена лично!&lt;p&gt;&lt;em&gt;Вместо пролога.&lt;/em&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/f51/b6b/fc9/f51b6bfc948c4898c2f21479198db841.png width=1376 height=768 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/f51/b6b/fc9/f51b6bfc948c4898c2f21479198db841.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/f51/b6b/fc9/f51b6bfc948c4898c2f21479198db841.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;h3&gt;«Микроша»&lt;/h3&gt;&lt;p&gt;&lt;br&gt;Первое появление:  1986  &lt;br&gt;Статус: Звезда школьных ЭВМ-классов&lt;p&gt;&lt;em&gt;«Микроша». Источник: &lt;/em&gt;&lt;a href=http://red-innovations.su&gt;&lt;em&gt;red-innovations.su&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/df2/b78/bf0/df2b78bf0f94a794b8f7aa93891126ac.png width=930 height=651 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/df2/b78/bf0/df2b78bf0f94a794b8f7aa93891126ac.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/df2/b78/bf0/df2b78bf0f94a794b8f7aa93891126ac.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Имя «Микроша» — это портманто из слов «микрокомпьютер» и «школа». Надо сказать, он действительно оправдывал свой уменьшительно-ласкательный ник, так как весил всего лишь 3 килограмма. Автором схемотехники стал выдающийся МИЭМовский инженер Юрий Озеров, до этого отметившийся как «соавтор железа» для компьютера «Радио-86РК», на котором «Микроша» и был основан, а также для первого и, наверное, последнего компьютера целиком узбекского происхождения — «Согдиана-1».&lt;p&gt;Но и технические возможности «Микроши» были довольно лилипутскими. Компьютер базировался на однокристальном &lt;a href=https://ru.wikipedia.org/wiki/%D0%9A%D0%A0580%D0%92%D0%9C80%D0%90&gt;КР580ВМ80А&lt;/a&gt;, который был скопирован с интеловского чипа Intel 8080. Он работал с частотой 1,77 МГц, имея при этом 32 Кб оперативной и слезоточиво скромные 2 КБ постоянной памяти — у того же BBC Micro ПЗУ было от 32 до 48 Кб в зависимости от комплектации.&lt;p&gt;Видеоадаптер «Микроши» был монохромным, с поддержкой текстового режима в формате 64 × 25 символов. Сами же изображения на ходу ваялись алгоритмом псевдографики с разрешением 128 × 50 точек.&lt;p&gt;«Микроша», производившийся Лианозовским заводом, взял советские школы буквально штурмом. Этому способствовала реклама в популярнейшем журнале «Радио», который расходился полутора миллионами экземпляров в месяц, в заметке о «новом направлении в радиолюбительском творчестве».&lt;p&gt;&lt;em&gt;Промо-снимок «Микроши» из журнала «Радио», 1986 год. Источник:  &lt;/em&gt;&lt;a href=http://radio.ru&gt;&lt;em&gt;Архив журнала “Радио”&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/430/fb1/0e6/430fb10e631b6adb27baa5f958a8c51c.png width=1192 height=624 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/430/fb1/0e6/430fb10e631b6adb27baa5f958a8c51c.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/430/fb1/0e6/430fb10e631b6adb27baa5f958a8c51c.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;&lt;em&gt;.&lt;/em&gt;&lt;br&gt;&lt;p&gt;К «Микроше» прилагалась кассета с программами и играми, которую по классике жанра нужно было запускать на обычном бытовом магнитофоне. В софтверный дистрибутив входили редактор текста, Бейсик, ассемблер/дизассемблер, графический редактор и отладчик.&lt;p&gt;&lt;em&gt;Почти целый «Микроша» без двух клавиш, но в рабочем состоянии. Источник: &lt;/em&gt;&lt;a href=http://red-innovations.su&gt;&lt;em&gt;red-innovations.su&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;br&gt;&lt;/em&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/892/a37/5f3/892a375f300bd5d0837decdd1b287385.png width=713 height=473 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/892/a37/5f3/892a375f300bd5d0837decdd1b287385.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/892/a37/5f3/892a375f300bd5d0837decdd1b287385.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;&lt;em&gt;Его можно было подружить с телевизором. Источник: &lt;/em&gt;&lt;a href=http://red-innovations.su&gt;&lt;em&gt;red-innovations.su&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/132/3a6/993/1323a699325d556dd5f1019ac7e61ae5.png width=930 height=642 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/132/3a6/993/1323a699325d556dd5f1019ac7e61ae5.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/132/3a6/993/1323a699325d556dd5f1019ac7e61ae5.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;&lt;em&gt;Источник: &lt;/em&gt;&lt;a href=http://red-innovations.su&gt;&lt;em&gt;red-innovations.su&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/168/029/6af/1680296af133304fbb6848e77c1173e7.png width=930 height=625 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/168/029/6af/1680296af133304fbb6848e77c1173e7.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/168/029/6af/1680296af133304fbb6848e77c1173e7.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Но и без игр не обошлось! На «Микроше» они были, пускай и не такие зрелищные, как на зарубежных монстрах, но в каталоге можно было найти «Пакмэна» (оригинальное написание), «Удава», «Ксоникс», «Тетрис», «Клад», «Цирк» и другие. К слову, к «Микроше» выходили дополнительные кассеты, на которых можно было отрыть такие сокровища, как Pascal, макроассемблер с отладчиком, редактор структурных текстов и ещё пригоршню игр а-ля «Мешанина» или «Бармен».&lt;p&gt;&lt;em&gt;Официальная кассета, прилагающаяся к «Микроше». Источник: &lt;/em&gt;&lt;a href=http://webhamster.ru&gt;&lt;em&gt;webhamster.ru&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/066/70d/d46/06670dd46e0705260f8583a8289b0296.png width=645 height=301 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/066/70d/d46/06670dd46e0705260f8583a8289b0296.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/066/70d/d46/06670dd46e0705260f8583a8289b0296.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Стоил «Микроша» 500 полновесных советских рублей, и это, на самом деле, совсем не дёшево. Для контекста: за эти же деньги можно было купить большой холодильник «Минск», за две сотни сверху урвать огромный и цветной «Горизонт», а на 250 рэ съездить небольшой семьёй из трёх человек на юга (при наличии родственников, у которых можно было пожить).&lt;p&gt;Тем не менее юные программисты того времени «Микрошей» восхищались, хотя и находили некоторые изъяны. К примеру, по отзывам приобщившихся, клавиатура у него была ужасно неудобная и чугунно-тяжёлая, но зато, наверное, монолитная. Делать бы гвозди из этих QWERTY’ей!&lt;h3&gt;«Агат»&lt;/h3&gt;&lt;p&gt;Первое появление:  1984  &lt;br&gt;Статус: Наш ответ &lt;s&gt;Чемберлену&lt;/s&gt; Джобсу&lt;p&gt;&lt;em&gt;«Агат». Источник: &lt;/em&gt;&lt;a href=http://digitalstudionn.ru&gt;&lt;em&gt;digitalstudionn.ru&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/fa1/6dd/353/fa16dd353145c52111cc79256390a5a0.jpeg width=1546 height=1815 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/fa1/6dd/353/fa16dd353145c52111cc79256390a5a0.jpeg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/fa1/6dd/353/fa16dd353145c52111cc79256390a5a0.jpeg 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;«Агат» был символом советского компьютеростроения не менее ярким, чем «Микроша», — именно ему выпала честь первым нести IT в широкие народные массы в середине 80-х, и справлялся «Агат» с поставленной задачей неплохо.&lt;p&gt;&lt;em&gt;Один из прототипов компьютера «Агат». Источник: &lt;/em&gt;&lt;a href=http://pcnews.ru&gt;&lt;em&gt;pcnews.ru&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/b14/2ee/7f1/b142ee7f13901ae180d929e9880bf6c6.jpeg width=1200 height=1177 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/b14/2ee/7f1/b142ee7f13901ae180d929e9880bf6c6.jpeg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/b14/2ee/7f1/b142ee7f13901ae180d929e9880bf6c6.jpeg 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Основная часть машины была честно срисована с американского Apple II Plus, и это был однокристальный центральный процессор MOS Technology 6502 с частотой 1 МГц. Но то ли у инженеров НИИВКа взыграла профессиональная гордость и обида за державу, то ли руководство опасалось обвинений в интеллектуальном хищничестве и репутационного ущерба советской науки, но остальные компоненты компьютера пилились буквально с нуля.&lt;p&gt;&lt;em&gt;Не только фурычит, но и прекрасно себя чувствует. Источник: &lt;/em&gt;&lt;a href=http://cyber-museum.ru&gt;&lt;em&gt;cyber-museum.ru&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/fdd/10e/f47/fdd10ef47c11d1a0431bec1d2819ebf4.jpeg width=804 height=960 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/fdd/10e/f47/fdd10ef47c11d1a0431bec1d2819ebf4.jpeg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/fdd/10e/f47/fdd10ef47c11d1a0431bec1d2819ebf4.jpeg 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;«Агат» был настоящей зверь-машиной. С частотой в 1 МГц его восьмиразрядный процессор мог выполнять 500 000 операций в секунду. Постоянная память зиждилась на 2 Кб, а оперативка включала от 64 до 128 Кб, причём за счёт модульной модификации её можно было расширить аж до 640 Кб!&lt;p&gt;&lt;em&gt;«Витринный» образец для демонстрации партийным функционерам. Источник: &lt;/em&gt;&lt;a href=http://cyber-museum.ru&gt;&lt;em&gt;cyber-museum.ru&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/42c/605/e77/42c605e77c54e32296ea6f6c80355565.jpeg width=1200 height=1233 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/42c/605/e77/42c605e77c54e32296ea6f6c80355565.jpeg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/42c/605/e77/42c605e77c54e32296ea6f6c80355565.jpeg 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Видеоконтроллер в «Агате» размещался прямо на системной плате, а в роли видеопамяти выступал один из участков базовой памяти, который выбирал сам программист. Поддерживались два основных текстовых режима — 32 × 32 и 64 × 32, а также несколько графических, от 64 × 64 до 512 × 256. Компьютер оснащался дисководом, через который ему можно было «скармливать» дискеты объёмом до 840 Кб.&lt;p&gt;&lt;em&gt;Процессор MOS Technology, который закупался СССР специально для «Агатов».&lt;/em&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/3e5/708/506/3e5708506e577cd60f61ae489b38ce90.jpeg width=800 height=631 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/3e5/708/506/3e5708506e577cd60f61ae489b38ce90.jpeg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/3e5/708/506/3e5708506e577cd60f61ae489b38ce90.jpeg 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Трагическим изъяном «Агата» была его полная несовместимость как с другими советскими персональными машинами, так и с его эппловским исходником — управление памятью в машинке осуществлялось иначе. А графическая система подвергалась переработкам. Зато чип в компьютере стоял подлинный, американский!&lt;p&gt;&lt;em&gt;«Агат», вид изнутри. Источник: &lt;/em&gt;&lt;a href=https://habr.com/ru/users/kerenskiy/&gt;kerenskiy&lt;/a&gt;.&lt;em&gt;.&lt;/em&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/bdb/68d/5fb/bdb68d5fbf3d728c58a288209680c279.jpeg width=1037 height=692 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/bdb/68d/5fb/bdb68d5fbf3d728c58a288209680c279.jpeg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/bdb/68d/5fb/bdb68d5fbf3d728c58a288209680c279.jpeg 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;«Агат» выпускался в красивом тёмно-рубиновом корпусе, то ли намекая на социалистическое происхождение, то ли на минерал, в честь которого он был назван, — красные агаты в природе встречаются и имеют красивый ягодно-багряный оттенок.&lt;p&gt;Об «Агате» даже знали по другую сторону занавеса, и в журнале Byte вышла статья за 1984 год, написанная Лео Борсом о том, является ли «Агат» плохим контрафактом по мотивам Apple II или нет. Из неё можно почерпнуть, что Пепси продавалась в каждом московском ларьке, клавиши Escape у «Агата» не было, а сам автор почему-то окрестил компьютер «yablochka». &lt;a href=https://habr.com/ru/articles/371693/&gt;Здесь можно почитать перевод&lt;/a&gt;..&lt;p&gt;&lt;em&gt;Занятная иллюстрация к статье Борса, журнал Byte. Источник: &lt;/em&gt;&lt;a href=https://archive.org/details/byte-magazine-1984-11/mode/2up&gt;&lt;em&gt;Internet Archive&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/bbf/4b6/41a/bbf4b641afe57ce31844abf045a7cca2.png width=1392 height=845 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/bbf/4b6/41a/bbf4b641afe57ce31844abf045a7cca2.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/bbf/4b6/41a/bbf4b641afe57ce31844abf045a7cca2.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;В довесок к персоналке в коммунистических тонах шёл дистрибутивный пакет с разным софтом и играми, наречённый «Школьницей». Его основой были учебно-производственный язык Рапира, вдохновлённый Паскалем и с ключевыми командами на русском (туше!), графическая система «Шпага», с помощью которой можно было рисовать и даже делать простые мультики, и язык Робик для младшеклассников.&lt;p&gt;&lt;em&gt;Структура системы «Школьница». Источник: &lt;/em&gt;&lt;a href=http://agatcomp.ru&gt;&lt;em&gt;agatcomp.ru&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/d48/26a/a36/d4826aa36979da85d4aeceb9dd824e96.png width=482 height=673 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/d48/26a/a36/d4826aa36979da85d4aeceb9dd824e96.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/d48/26a/a36/d4826aa36979da85d4aeceb9dd824e96.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;&lt;em&gt;&lt;br&gt;&lt;/em&gt;Среди прочего, с Робиком в комплекте шла та самая «Черепашка», почерпнутая из американской среды программирования KTurtle. Для тысяч школьников она стала первым знакомством с магией кода, когда по их инструкции черепашка, Том Сойер в амплуа маляра или муравей послушно ползали по экрану, рисуя таким образом разные узоры.&lt;p&gt;&lt;em&gt;Игры на «Агате» тоже были. Помимо игры в Тома Сойера, красящего экран, ещё прилагались «Дежурик» про хлопотливого робота, наводящего порядок на экране, «Машинист», «Анацефал 1» с целым шахматным ИИ и другие. На картинке игра «Станок», на которой нужно было управлять токарным станком, избегая брака, — советский геймдев суров. Источник: &lt;/em&gt;&lt;a href=http://bitblaze.ru&gt;&lt;em&gt;bitblaze.ru&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;br&gt;&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/181/65f/7bc/18165f7bcee8255ca148c9989a8a642b.jpeg width=501 height=400 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/181/65f/7bc/18165f7bcee8255ca148c9989a8a642b.jpeg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/181/65f/7bc/18165f7bcee8255ca148c9989a8a642b.jpeg 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;В общем, «Агат» был одним из самых долгоиграющих советских ПЭВМ, причём и после распада страны. Стоил он сумасшедших денег — 3 900 рублей: для сравнения &lt;a href=https://calculator.knigovod.com/news/skolko-stoili-kvartiry-v-1980&gt;за 6 000 можно было купить однушку&lt;/a&gt;. Он активно использовался в 90-х, а по некоторым сведениям, в отдельных уголках нашей необъятной он продолжал работать в компьютерных классах даже в начале 2000-х. Вот что значит делать на века.&lt;h3&gt;ДВК&lt;/h3&gt;&lt;p&gt;Первое появление:  1983  &lt;br&gt;Статус: Одна из самых тиражируемых машин СССР&lt;p&gt;&lt;em&gt;Стильный, модный, комсомольский ДВК-2 (слева) рядом с венгерским компьютером Video-ton 340. Источник: Reddit.&lt;/em&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/108/a06/940/108a069404962ecf0e7e3b655271209d.jpeg alt=&#34;Фотосессия советского компьютера ДВК-2 80-х годов и венгерского компьютера  Videoton-340 70-х годов - Андрей Тленов : r/cassettefuturism&#34; width=1920 height=1282 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/108/a06/940/108a069404962ecf0e7e3b655271209d.jpeg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/108/a06/940/108a069404962ecf0e7e3b655271209d.jpeg 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Зеленоградский ДВК, он же Диалоговый вычислительный комплекс, был аппаратом поистине эпохальным. А за его появлением на свет стоял НИИ точной технологии, он же НИИТТ (не путать с «NEET»).&lt;p&gt;Архитектура PDP-11 была весьма почитаема в Советском Союзе. Подлинные причины неизвестны, но, возможно, всё дело крылось в простой и элегантной 16-разрядной системе команд, Unibus-шине, связавшей в единую упряжку процессор, память и периферию, и в том, как эффективно PDP-машины справлялись с управлением различных процессов. Ну или, может, это была просто сила привычки.&lt;p&gt;&lt;em&gt;Ещё один ДВК-2. Источник: &lt;/em&gt;&lt;a href=http://getbb.ru&gt;&lt;em&gt;getbb.ru&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/d3d/8c1/d86/d3d8c1d865fc84d73ed7f8e8d5a32b91.jpeg width=768 height=1024 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/d3d/8c1/d86/d3d8c1d865fc84d73ed7f8e8d5a32b91.jpeg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/d3d/8c1/d86/d3d8c1d865fc84d73ed7f8e8d5a32b91.jpeg 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/986/9ad/40e/9869ad40efd177f1a03911f801225886.jpeg width=1024 height=768 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/986/9ad/40e/9869ad40efd177f1a03911f801225886.jpeg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/986/9ad/40e/9869ad40efd177f1a03911f801225886.jpeg 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/603/bf8/d3f/603bf8d3fdd073b6a1e3a2c5dd881eea.jpeg width=768 height=1024 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/603/bf8/d3f/603bf8d3fdd073b6a1e3a2c5dd881eea.jpeg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/603/bf8/d3f/603bf8d3fdd073b6a1e3a2c5dd881eea.jpeg 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;В любом случае в 1981 году архитектуру DEC PDP-11 решили по-мичурински адаптировать к родной айтишной почве, изготовив на её базе «ДВК». Только наш комплекс должен был быть ещё круче и продвинутее, будучи оснащённым шестнадцатиразрядным микропроцессором К1801ВМ1 — первым «однокристальником» для архитектуры PDP в мире, так-то.&lt;p&gt;ДВК-1 работал на процессоре с тактовой частотой 5 МГц. В него входили 48 Кбайт оперативной памяти, два дисковода для 5,25-дюймовых дискет и пузатый монитор, выводивший на тёмном фоне зелёные символы, словно приглашая непуганых советских прогеров в объятия «Матрицы».&lt;p&gt;&lt;em&gt;Тот самый матричный фосфор на дисплее. Источник: &lt;/em&gt;&lt;a href=http://getbb.ru&gt;&lt;em&gt;getbb.ru&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/d00/f18/a97/d00f18a97badb667013086802892774a.jpeg width=1024 height=576 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/d00/f18/a97/d00f18a97badb667013086802892774a.jpeg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/d00/f18/a97/d00f18a97badb667013086802892774a.jpeg 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;В первых итерациях ДВК жёсткой памяти, увы, не было. Вместо этого в дисководы MX0: и MX1: вставлялись две дискеты. Первая содержала систему, вероятно ФОДОС, она же Файловая однопользовательская диалоговая операционная система, клон RT-11, которая использовалась PDP-машинами. Вторая содержала уже непосредственно программы и, в некоторых случаях, заветные игрушки.&lt;p&gt;Кстати об играх: несмотря на то что ДВК был аппаратом суровым и предназначенным для всяких университетов и лабораторий, под игры его перепахали довольно быстро. И какие то были игры!&lt;p&gt;Помимо дежурного Тетриса, на машине можно было насладиться неустановленной буржуйской игрой по мотивам «Стартрека», в переводе которой на русский фигурировали интересные реплики: «Внимание! В квадрате замечены семижопы!» Ещё прилагалась версия Loadrunner’а под названием «Специалист» и какой-то фиксированный шутер а-ля Galaxian, где во врагов нужно было стрелять вопросительными знаками.&lt;p&gt;&lt;em&gt;Та самая игра по «Стартреку». &lt;/em&gt;&lt;a href=https://mnxa-oxide.livejournal.com/137252.html&gt;&lt;em&gt;Источник.&lt;/em&gt;&lt;/a&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/cd8/bcb/835/cd8bcb8357eac7cf33797f38b1fa96c8.jpeg width=958 height=487 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/cd8/bcb/835/cd8bcb8357eac7cf33797f38b1fa96c8.jpeg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/cd8/bcb/835/cd8bcb8357eac7cf33797f38b1fa96c8.jpeg 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;&lt;em&gt;«Специалист» по мотивам Loadrunner. &lt;/em&gt;&lt;a href=https://mnxa-oxide.livejournal.com/137252.html&gt;&lt;em&gt;Источник.&lt;/em&gt;&lt;/a&gt;&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/49e/1cc/a0b/49e1cca0b1ac80745635e1083b01d419.jpeg width=500 height=382 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/49e/1cc/a0b/49e1cca0b1ac80745635e1083b01d419.jpeg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/49e/1cc/a0b/49e1cca0b1ac80745635e1083b01d419.jpeg 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;ДВК в итоге превратился в целую плеяду устройств, и более поздние, например ДВК-3М и ДВК-3М2, уже оснащались накопителями на жёстких магнитных дисках (НЖМД) благодаря специальному контроллеру, дисплеи переходили в цвет, и ДВКашки расползались по отраслям. Одни аппараты исправно работали в школах, а другие, к примеру, служили на флоте: ДВК-2М в составе гидроакустической приставки «Рица» принимали участие в поисковой противолодочной &lt;a href=https://ru.wikipedia.org/wiki/%D0%90%D1%82%D1%80%D0%B8%D0%BD%D0%B0&gt;операции «Атрина»&lt;/a&gt; Северного флота СССР в Саргассовом море — одной из последних и крупных конфронтаций СССР и США.&lt;p&gt;&lt;em&gt;Вид сбоку на корзину ДВК-3М. С их помощью можно было устанавливать сменные печатные платы по системной шине МПИ. Источник: &lt;/em&gt;&lt;a href=http://retropc.org&gt;&lt;em&gt;retropc.org&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/56a/733/ee6/56a733ee611caa99d854d167ced45231.jpeg width=300 height=205 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/56a/733/ee6/56a733ee611caa99d854d167ced45231.jpeg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/56a/733/ee6/56a733ee611caa99d854d167ced45231.jpeg 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;&lt;em&gt;Плата контроллера жёстких магнитных дисков. Источник: &lt;/em&gt;&lt;a href=http://retropc.org&gt;&lt;em&gt;retropc.org&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/9e4/a30/9d5/9e4a309d597da41e2cca07f5f4ce0b69.jpeg width=300 height=339 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/9e4/a30/9d5/9e4a309d597da41e2cca07f5f4ce0b69.jpeg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/9e4/a30/9d5/9e4a309d597da41e2cca07f5f4ce0b69.jpeg 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;&lt;em&gt;Вентилятор блока питания в ДВК-3М. Говорят, гудел страшно. Источник: &lt;/em&gt;&lt;a href=http://retropc.org&gt;&lt;em&gt;retropc.org&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/1bd/c25/797/1bdc25797cf2c3743b387201f4c1e005.jpeg width=300 height=140 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/1bd/c25/797/1bdc25797cf2c3743b387201f4c1e005.jpeg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/1bd/c25/797/1bdc25797cf2c3743b387201f4c1e005.jpeg 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Всего зеленоградский «Квант» выпустил около 200 тысяч машин ДВК. Которые производились вплоть до 1993 года. Весьма впечатляющая цифра. Apple II, для сравнения, просуществовал на четыре года больше, продавался по всему миру и ушёл тиражом в районе 1,25 миллиона за это время.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <guid>https://habr.com/ru/companies/studyai/articles/1082134/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082134</guid>
      <pubDate>Mon, 14 Sep 2026 14:24:28 +0000</pubDate>
    </item>
    <item>
      <title>Мой годовой опыт в разработке с ИИ агентами и главные выводы</title>
      <link>https://habr.com/ru/articles/1082126/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082126</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;Этот пост для разработчиков, которые еще скептически относятся к кодингу с ИИ агентами и не используют его. Мир стремительно меняется, &lt;strong&gt;пора «проснуться»&lt;/strong&gt; и поменять методику работы.&lt;p&gt;Ниже я расскажу личный опыт, чтобы было нагляднее.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/50c/2f7/7bc/50c2f77bc99c889e49ded6be862b653e.jpg alt=&#34;Москва, Сколково. Хакатон Битрикс24, ноябрь 2025 г.&#34; title=&#34;Москва, Сколково. Хакатон Битрикс24, ноябрь 2025 г.&#34; width=2560 height=1707 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/50c/2f7/7bc/50c2f77bc99c889e49ded6be862b653e.jpg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/50c/2f7/7bc/50c2f77bc99c889e49ded6be862b653e.jpg 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Москва, Сколково. Хакатон Битрикс24, ноябрь 2025 г.&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;h2&gt;Моя история&lt;/h2&gt;&lt;p&gt;ИИ тема хайповая, и я, конечно, следил за ней еще с 2019 г. Но это были классические, не генеративные модели. Затем в ноябре 2022 появляется ChatGPT, следом в июле 2023 года DeepSeek. И мне больше он понравился. Пока все это было в рамках &lt;strong&gt;«О, прикольно, он разговаривает».&lt;/strong&gt;&lt;p&gt;Затем я в конце 2024 года ради прикола попросил DeepSeek сверстать HTML структуру для простого лендинга для медцентра — и он мне выдал чистенькую вёрстку, все теги и наименования классов четкие. Я, так сказать, испугался за свою профессию и хлеб. Это где ж такое видано, чтоб машина программировать начала. Ну, думаю, ладно, это еще не скоро и дальше иногда просто спрашивал в чате некоторые подсказки.&lt;p&gt;Затем в ноябре 2025 года я поехал на партнерскую конференцию Битрикс24, где проходил хакатон вайбкодинга. Я к нему до этого 3 недели готовился — надо отдать должное спецам из Битрикс24, они все материалы подготовили и нянчились с нами, как с малыми детьми, разжевывая, как все развернуть в докере, как оплатить зарубежные ИИ модели (советовали Sonnet, ну на крайняк ChatGPT) и как разрабатывать с помощью ИИ. В итоге на хакатоне я поучаствовал, попробовал со всеми поработать, получил стартовый пинок и интересный опыт.&lt;h2&gt;После этого все изменилось&lt;/h2&gt;&lt;p&gt;Я пару недель переваривал эту информацию и опыт, и начал более активно в этой теме разбираться, прошел все муки вайбкодера по поиску методов оплаты напрямую в Openapi и Anthropic, через провайдеров (Openrouter), в ход шли все инструменты — даже крипта. Это сейчас все просто, а год назад было сложнее).&lt;p&gt;В конце 2025 года ИИшка программировала, ну так средненько (конечно Sonnet и Opus делали свое дело, но это бешеные деньги и для меня не вариант был).&lt;p&gt;Все изменилось в начале 2026 года с появлением ChatGPT 5.3 — агент работал четко и по делу. С этого момента у меня консоль каждый день работала и пилила какие‑то мои пет‑проекты. Сейчас их в стадии MVP больше 30 штук. Из них публично &lt;a href=https://habr.com/ru/articles/1022914/ rel=&#34;noopener nofollow&#34;&gt;запущен только один&lt;/a&gt;, а внутренних инструментов штук 5 работает, как отлажу до конца — поделюсь со всеми на GitHub/&lt;p&gt;То, что остальные не запущены, меня не расстраивает, я решил для себя, что просто получал опыт работы с ИИ агентами. Но зато я как разработчик ноября 2025 года и сейчас огромная разница в компетенциях ИИ кодинга. 16 лет опыта разработки + ИИ агент позволяет мне писать на любом языке React, Go, Rust, Python и делать сложные проекты с минимальной командой.&lt;h2&gt;Теперь самая мякотка — выводы и ценные советы&lt;/h2&gt;&lt;p&gt;Что я сейчас могу сказать. Парни, разработчики, если вы эту тему не пробовали, либо пробовали и не получилось — бросайте все и срочно изучайте, пробуйте, получайте опыт.&lt;p&gt;Точка невозврата уже пройдена, много классических разработчиков для создания продукта сейчас не требуется. Фронты вообще под жестким ударом. Да, количество вакансий и вообще разработчиков в ближайшее время уменьшится, но в то же время возникнут новые профессии, так что возможно не все так плохо и просто произойдет мутация сферы.&lt;p&gt;Все современные модели (OpenAI ChatGPT, Anthropic Sonnet, Opus, DeepSeek, GLM, Kimi) программируют лучше и быстрее мидла‑разработчика, как минимум на уровне сеньёра и в некоторых случаях лучше.&lt;p&gt;Если вы начинающий, то для работы берите сразу DeepSeek Harness, ну или на крайняк OpenCode или Pi Code. А Cursor, Windsurf и подобные решения даже не пробуйте.&lt;p&gt;Забудьте про IDE, не требуется она больше — консоль + ИИ агент ваш лучший друг.&lt;p&gt;Не запускайте агента хозяйничать на проде, не стоит оно того).&lt;p&gt;Изучите методологию Spec Driven Development, без этого агент будет творить на вашем проекте, что ему вздумается. А также изучите фреймворки OpenCpec и Spec Kit от GitHub. Идея простая: не позволяй ИИ идти от размытого требования сразу к коду. Сначала &lt;a href=http://spec.md rel=&#34;noopener nofollow&#34;&gt;spec.md&lt;/a&gt; (что нужно), потом &lt;a href=http://plan.md rel=&#34;noopener nofollow&#34;&gt;plan.md&lt;/a&gt; (как делать), потом &lt;a href=http://tasks.md rel=&#34;noopener nofollow&#34;&gt;tasks.md&lt;/a&gt; (на какие шаги разбить).&lt;p&gt;Истории в духе «&lt;em&gt;я за вечер собрал сервис, который лучше платных аналогов&lt;/em&gt;», прикольны в самом начале, а потом разбиваются о жесткую реальность доведения своего MVP до продуктового состояния. Волшебной таблетки нет, чтобы сделать что‑то серьезное, нужно все равно работать (но сейчас просто по‑другому).&lt;h2&gt;Какие профессии возможно появятся и к чему вам следует стремиться&lt;/h2&gt;&lt;h4&gt;1. Архитектор мультиагентных систем (Multi‑Agent Workflow Architect)&lt;/h4&gt;&lt;p&gt;Специалист, который проектирует не просто код, а «команды ИИ‑агентов». Он настраивает пайплайны, где один ИИ пишет код, второй его тестирует, третий ищет уязвимости, а четвертый деплоит. От его настроек зависит, не сойдет ли ИИ с ума и не обрушит ли прод. Это новый уровень DevOps и Архитектуры.&lt;h4&gt;2. Инженер по верификации ИИ‑кода и безопасности (AI Code Security &amp;amp; Compliance Engineer)&lt;/h4&gt;&lt;p&gt;ИИ часто использует устаревшие или уязвимые библиотеки, а также может случайно «залить» в публичный репозиторий закрытые ключи или коммерческую тайну компании. Этот специалист создает барьеры (guardrails) и системы автоматического аудита сгенерированного кода.&lt;h4&gt;3. Инженер по миграции легаси с помощью ИИ (AI Legacy Modernization Specialist)&lt;/h4&gt;&lt;p&gt;В банках, госсекторе и на заводах лежат миллионы строк кода на COBOL, старых версиях Java, C# и PHP, которые люди боятся трогать. Этот специалист умеет правильно «кормить» ИИ контекстом старых систем, чтобы безопасно переписывать их на современные стеки.&lt;h4&gt;4. Доменный RAG‑интегратор / Инженер корпоративного контекста (Enterprise Context Broker)&lt;/h4&gt;&lt;p&gt;Базовые модели ничего не знают о внутренней кухне конкретной компании. Этот специалист настраивает локальные LLM и RAG‑системы (Retrieval‑Augmented Generation) так, чтобы ИИ‑агенты имели безопасный доступ к внутренней документации, базам данных и Jira, не нарушая периметра безопасности.&lt;h4&gt;5. ИИ‑психолог / Дебаггер агентов (Agentic Debugging Specialist)&lt;/h4&gt;&lt;p&gt;Когда сложный мультиагентный пайплайн заходит в тупик (агенты начинают бесконечно спорить друг с другом или генерировать бред), этот специалист читает «мысли» (chain of thought) моделей, находит логические ошибки в промптах и системных инструкциях, «лечит» workflow.&lt;h2&gt;P. S.&lt;/h2&gt;&lt;p&gt;Еще раз повторю — не теряйте время, изучайте ИИ‑кодинг, получайте опыт и вы останетесь в рынке. У кого есть вопросы, можете написать мне в Телегу или здесь в личку (все данные в профиле есть), чем смогу — помогу, расскажу.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <author>ANTON62</author>
      <guid>https://habr.com/ru/articles/1082126/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082126</guid>
      <pubDate>Mon, 14 Sep 2026 14:15:27 +0000</pubDate>
    </item>
    <item>
      <title>Как разделить фронтенд на модули и связать их между собой: на примере маркетплейса</title>
      <link>https://habr.com/ru/companies/rwb/articles/1079970/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1079970</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;h3&gt;Вступление&lt;/h3&gt;&lt;p&gt;Привет, меня зовут Иван Некипелов, я технический руководитель команды фронтенд-инфраструктуры в RWB.&lt;p&gt;Эта статья для frontend-разработчиков, которые хотят сделать растущее приложение проще в поддержке. На примере маркетплейса покажем, как разделить код на модули, связать сервисы и проверять бизнес-логику отдельно от интерфейса. Примеры написаны на &lt;strong&gt;React и TypeScript&lt;/strong&gt;, но подход можно использовать с другим UI-фреймворком.&lt;p&gt;Основной вопрос — как провести границы между частями приложения. Оформлению заказа нужен расчёт доставки, корзине — данные о товарах. Как связать их так, чтобы изменение одной фичи не требовало правок по всему проекту? И как сохранить код понятным разработчикам и &lt;code&gt;LLM&lt;/code&gt;, не перегружая его абстракциями?&lt;p&gt;Мы используем собственное разбиение на feature-модули: каталог, корзину, доставку и другие части приложения. Это &lt;strong&gt;не&lt;/strong&gt; реализация &lt;strong&gt;Feature-Sliced Design&lt;/strong&gt;, хотя отдельные принципы могут совпадать. Разберём структуру директорий, допустимые зависимости, сборку сервисов через DI, хранение состояния и доступ к платформенным API.&lt;p&gt;Код примера доступен в &lt;a href=https://github.com/VanyaJedi/feature-oriented-demo&gt;репозитории&lt;/a&gt;. В нём есть работающий интерфейс, простой DI-контейнер, архитектурные &lt;code&gt;ESLint&lt;/code&gt;-правила, &lt;code&gt;ADR&lt;/code&gt; и инструкции для агентов.&lt;h3&gt;Физическая организация модулей&lt;/h3&gt;&lt;p&gt;Один из вопросов, который возникает при проектировании архитектуры: как физически организовать код в файловой системе. Один из распространённых вариантов — группировать файлы по их техническому назначению:&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/400/b63/ea3/400b63ea3901f6f2e0a29736fab14113.png width=2048 height=1008 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/400/b63/ea3/400b63ea3901f6f2e0a29736fab14113.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/400/b63/ea3/400b63ea3901f6f2e0a29736fab14113.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Такая структура хорошо подходит для небольшого приложения: её преимущества очевидны — она проста и понятна. Однако по мере роста проекта каждый технический слой начинает объединять совершенно разные, а зачастую и слабо связанные части системы. Например, в нашем случае в каталоге &lt;code&gt;services&lt;/code&gt; оказались бы сервисы корзины, авторизации, карточки товара, рекомендаций и других бизнес-направлений. При этом файлы, относящиеся к одной функциональности, были бы распределены по всему дереву проекта. В результате границы изменений становятся менее очевидными, сложнее оценить их влияние на систему и сформировать для LLM компактный и релевантный контекст.&lt;p&gt;Для маркетплейса мы выбрали организацию вокруг набора бизнес-сценариев (фич) приложения:&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/760/bd5/1b2/760bd51b2a48231cf6ce8635ce3f98b2.png width=2048 height=1540 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/760/bd5/1b2/760bd51b2a48231cf6ce8635ce3f98b2.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/760/bd5/1b2/760bd51b2a48231cf6ce8635ce3f98b2.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Каждый feature-модуль владеет определённой бизнес-логикой. Например, всё, что относится к корзине, находится внутри &lt;code&gt;basket&lt;/code&gt;, логика каталога — внутри &lt;code&gt;catalog&lt;/code&gt;, а работа с избранным — внутри &lt;code&gt;favorites&lt;/code&gt;. Благодаря этому структура проекта отражает устройство продукта, а код, изменяющийся по одной бизнес-причине, оказывается собран в одном месте.&lt;p&gt;В &lt;code&gt;&lt;strong&gt;infrastructure&lt;/strong&gt;&lt;/code&gt; мы размещаем технические механизмы, не принадлежащие конкретному бизнес-сценарию: HTTP-клиент, работу с токенами сессии, логирование, локализацию, конфигурацию окружения и адаптеры внешних SDK. Feature-модули могут пользоваться инфраструктурой, но инфраструктура ничего не знает о конкретных фичах. Например, HTTP-сервис умеет отправлять запросы и обрабатывать транспортные ошибки, но не знает, что такое корзина или карточка товара. &lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/dac/3e4/c38/dac3e4c38fa540632009a5d618feaf57.png width=2048 height=1008 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/dac/3e4/c38/dac3e4c38fa540632009a5d618feaf57.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/dac/3e4/c38/dac3e4c38fa540632009a5d618feaf57.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Слой &lt;code&gt;&lt;strong&gt;app&lt;/strong&gt;&lt;/code&gt; отвечает за процессы всего приложения. Здесь находятся точки входа, глобальная конфигурация, провайдеры и composition root — место, в котором создаются общие зависимости и feature-модули связываются между собой.&lt;p&gt;В &lt;code&gt;&lt;strong&gt;pages&lt;/strong&gt;&lt;/code&gt; мы собираем страницы из фич и связываем их с маршрутизацией. В &lt;code&gt;&lt;strong&gt;shared&lt;/strong&gt;&lt;/code&gt; оставляем переиспользуемые компоненты, чистые функции и небольшие stateless-адаптеры платформенных API без привязки к бизнес-сценариям.&lt;h3&gt;Внутреннее устройство feature-модуля&lt;/h3&gt;&lt;p&gt;Группировка вокруг бизнес-сценариев определяет владельца логики. Внутри каждой фичи мы дополнительно разделяем код по его роли: модели данных, сервисы, состояние, хуки и компоненты интерфейса.&lt;p&gt; &lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/d6a/62e/e7c/d6a62ee7c9746eb23fbf93d3358a9093.png width=2048 height=1095 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/d6a/62e/e7c/d6a62ee7c9746eb23fbf93d3358a9093.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/d6a/62e/e7c/d6a62ee7c9746eb23fbf93d3358a9093.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Модуль создаёт только те роли, которые ему действительно нужны: небольшая фича может состоять из модели и одного хука, а более сложная — содержать собственные сервисы, состояние и UI.&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;Роль&lt;/strong&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;Ответственность&lt;/strong&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;model&lt;td&gt;&lt;p align=left&gt;Типы, схемы валидации, мапперы и доменные константы&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;services&lt;td&gt;&lt;p align=left&gt;Бизнес-операции, работа с backend и внешними API, обработка результатов&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;hooks&lt;td&gt;&lt;p align=left&gt;Координация пользовательских действий, состояния и сервисов в React&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;store&lt;td&gt;&lt;p align=left&gt;Локальное и UI-состояние фичи&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;ui&lt;td&gt;&lt;p align=left&gt;Компоненты интерфейса, принадлежащие фиче&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;utils&lt;td&gt;&lt;p align=left&gt;Чистые вспомогательные функции без обращений к внешнему состоянию&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;di&lt;td&gt;&lt;p align=left&gt;Токены и функции регистрации зависимостей&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Каждая роль предоставляет свой публичный вход через &lt;code&gt;index.ts&lt;/code&gt;: в нём экспортируем только то, что понадобится остальному приложению. &lt;strong&gt;Общий barrel-файл в корне фичи не создаём &lt;/strong&gt;— &lt;strong&gt;по импорту должно быть видно, какую часть фичи мы используем.&lt;/strong&gt;&lt;h3&gt;Как модули взаимодействуют между собой?&lt;/h3&gt;&lt;p&gt;Физического разделения кода по директориям недостаточно: границы модулей определяются не их расположением, а связями между ними. Поэтому мы явно задаём допустимое направление зависимостей:&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/eaa/9f6/461/eaa9f64616b4626ab0d72abfd52ddd96.png width=2048 height=543 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/eaa/9f6/461/eaa9f64616b4626ab0d72abfd52ddd96.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/eaa/9f6/461/eaa9f64616b4626ab0d72abfd52ddd96.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Вышестоящий слой может использовать публичные возможности нижестоящего, но не наоборот. Например, страница может подключить фичу, а фича воспользоваться HTTP-клиентом из инфраструктуры. При этом инфраструктура ничего не знает о бизнес-фичах, а фичи — о страницах, на которых они используются.&lt;p&gt;&lt;strong&gt;Дополнительно запрещаем runtime-импорты между разными фичами и импорты между разными страницами.&lt;/strong&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/676/6d5/db9/6766d5db92272e1b3fade1b4695cc9be.png width=2048 height=468 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/676/6d5/db9/6766d5db92272e1b3fade1b4695cc9be.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/676/6d5/db9/6766d5db92272e1b3fade1b4695cc9be.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Фича не должна напрямую импортировать реализацию другой фичи, а одна страница — использовать внутренности другой страницы. Такие зависимости быстро размывают границы ответственности: изменение одного модуля начинает затрагивать соседние, а отдельную функциональность становится сложнее заменить, протестировать или удалить. По мере роста приложения граф зависимостей становится сложнее, в нём могут появляться неочевидные и циклические связи. В результате понять, как работает конкретная фича и от чего она зависит, становится труднее как человеку, так и LLM.&lt;p&gt;&lt;strong&gt;Но бизнес-сценарии не существуют изолированно. Корзине нужна информация о товарах, а оформлению заказа &lt;/strong&gt;— &lt;strong&gt;расчёт стоимости доставки. Если фича не может &lt;u&gt;напрямую&lt;/u&gt; использовать runtime-реализацию другой фичи, как тогда выразить такую зависимость?&lt;/strong&gt;&lt;h4&gt;Type-only зависимости&lt;/h4&gt;&lt;p&gt;Полностью изолировать feature-модули друг от друга не только сложно, но и не всегда нужно. Например, корзина может оперировать типом товара, а аналитика — использовать тип поискового запроса. Такая зависимость описывает общий язык предметной области, но не требует загрузки чужой реализации.&lt;p&gt;Поэтому &lt;code&gt;type-only&lt;/code&gt; импорты между фичами разрешены:&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/60f/24a/72c/60f24a72c687c538496595596411f7a4.png width=2048 height=432 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/60f/24a/72c/60f24a72c687c538496595596411f7a4.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/60f/24a/72c/60f24a72c687c538496595596411f7a4.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;В отличие от обычного импорта, &lt;code&gt;import type&lt;/code&gt;&lt;strong&gt; &lt;/strong&gt;удаляется при компиляции TypeScript и не создаёт runtime-связь между модулями. Код корзины может знать форму товара, но при этом не загружает сервисы, store или UI модуля &lt;code&gt;product&lt;/code&gt;.&lt;h4&gt;Как передавать зависимости между модулями&lt;/h4&gt;&lt;p&gt;&lt;code&gt;Type-only&lt;/code&gt; импорты позволяют одному модулю использовать контракты другого, но не решают вопрос передачи runtime-реализаций.&lt;p&gt;Рассмотрим упрощённый пример взаимодействия двух feature-модулей. При оформлении заказа модулю &lt;code&gt;checkout&lt;/code&gt; необходимо рассчитать стоимость доставки, но сама логика расчёта принадлежит модулю &lt;code&gt;delivery&lt;/code&gt;.&lt;p&gt;Для примера зафиксируем простой контракт: сервис получает сумму товаров и возвращает стоимость доставки. &lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/d79/d17/1ca/d79d171ca4922ce5dd7114f594843c07.png width=2048 height=690 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/d79/d17/1ca/d79d171ca4922ce5dd7114f594843c07.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/d79/d17/1ca/d79d171ca4922ce5dd7114f594843c07.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Сначала вариант с прямым созданием зависимости:&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/a63/cd5/7dc/a63cd57dc5eb86098c715c06851f7461.png width=2048 height=730 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/a63/cd5/7dc/a63cd57dc5eb86098c715c06851f7461.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/a63/cd5/7dc/a63cd57dc5eb86098c715c06851f7461.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Такой импорт запрещён нашим архитектурным ESLint-правилом: &lt;code&gt;checkout&lt;/code&gt; выбирает конкретную реализацию чужой фичи и сам управляет её созданием. Вместо этого импортируем только контракт и получаем готовый сервис через конструктор:&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/235/848/de7/235848de7578d4c80acdf2faa310348c.png width=2048 height=1363 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/235/848/de7/235848de7578d4c80acdf2faa310348c.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/235/848/de7/235848de7578d4c80acdf2faa310348c.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Теперь &lt;code&gt;CheckoutService&lt;/code&gt; получает сервис доставки снаружи, а не создаёт его самостоятельно. Такой подход называется внедрением зависимостей — &lt;code&gt;&lt;strong&gt;Dependency Injection&lt;/strong&gt;&lt;/code&gt; или &lt;code&gt;&lt;strong&gt;DI&lt;/strong&gt;&lt;/code&gt;.&lt;p&gt;В этом примере контракт принадлежит &lt;code&gt;delivery&lt;/code&gt; и является частью его публичного API. В другом случае контракт может определяться потребителем: например, &lt;code&gt;checkout&lt;/code&gt; может объявить минимальный интерфейс &lt;code&gt;DeliveryCalculator&lt;/code&gt;, который реализует сервис доставки. Выбор зависит от того, кому по смыслу принадлежит контракт.&lt;p&gt;Остаётся определить, кто создаст оба сервиса и свяжет их между собой. Это задача слоя &lt;code&gt;app&lt;/code&gt;, который разберём дальше.&lt;h3&gt;Где собираются зависимости?&lt;/h3&gt;&lt;p&gt;Сервисы связываем в слое &lt;code&gt;app&lt;/code&gt;. Здесь runtime-импорты нескольких фич допустимы: этот слой выбирает реализации и собирает из них приложение.&lt;p&gt;Для этого мы используем DI-контейнер.&lt;p&gt;&lt;em&gt;Важен здесь не сам DI-контейнер, а наличие composition root &lt;/em&gt;—&lt;em&gt; места, где приложение выбирает конкретные реализации и связывает их между собой. Это можно сделать обычными конструкторами и фабриками. Контейнер лишь инструмент, который упрощает такую сборку, когда зависимостей становится много.&lt;/em&gt;&lt;p&gt;В примерах контейнер доступен как &lt;code&gt;di&lt;/code&gt; из &lt;code&gt;@infra/di&lt;/code&gt;. Покажем простой API: &lt;code&gt;register&lt;/code&gt; сохраняет готовый экземпляр, а &lt;code&gt;get&lt;/code&gt; возвращает его по токену.&lt;p&gt;Токен — идентификатор зависимости в контейнере. Типовой параметр указывает, какой сервис мы ожидаем получить:&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/b3f/923/ccd/b3f923ccdbd96d9a468e486c9be63a82.png width=3680 height=932 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/b3f/923/ccd/b3f923ccdbd96d9a468e486c9be63a82.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/b3f/923/ccd/b3f923ccdbd96d9a468e486c9be63a82.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;В &lt;code&gt;app&lt;/code&gt; создаём сервис доставки и передаём его сервису оформления заказа.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/6ef/360/9d7/6ef3609d762784ae2f25139f2cd5630b.png width=3680 height=2284 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/6ef/360/9d7/6ef3609d762784ae2f25139f2cd5630b.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/6ef/360/9d7/6ef3609d762784ae2f25139f2cd5630b.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Здесь зависимости передаются явно. Имя токена не обязано совпадать с параметром конструктора: нужный сервис мы получаем через &lt;code&gt;get&lt;/code&gt; и сами передаём в &lt;code&gt;CheckoutService&lt;/code&gt;. Порядок регистрации важен — сервис доставки должен быть зарегистрирован раньше его потребителя.&lt;p&gt;&lt;code&gt;registerServices()&lt;/code&gt; вызывается в точке запуска до рендера React-приложения. После этого хук своей фичи может получить готовый сервис:&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/6aa/76e/1fe/6aa76e1feaaaffc1fe9aceb9fda068e6.png width=3680 height=932 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/6aa/76e/1fe/6aa76e1feaaaffc1fe9aceb9fda068e6.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/6aa/76e/1fe/6aa76e1feaaaffc1fe9aceb9fda068e6.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Сам &lt;code&gt;CheckoutService&lt;/code&gt; ничего не знает о контейнере. Он получает зависимость через конструктор, поэтому в тесте его можно создать напрямую, передав тестовый объект.&lt;p&gt;Так же передаются инфраструктурные зависимости. Например, сервисам нескольких фич можно передать общий HTTP-клиент: он отвечает за транспорт, а каждый feature-сервис — за свои запросы и обработку ответов.&lt;h4&gt;Какую реализацию DI выбрать?&lt;/h4&gt;&lt;p&gt;В небольшом приложении можно обойтись без контейнера: создать сервисы в &lt;code&gt;app&lt;/code&gt; и передать зависимости в конструкторы вручную. Контейнер становится полезен, когда регистрацию, получение и жизненный цикл большого числа сервисов неудобно поддерживать самостоятельно.&lt;p&gt;Внутри команды на наших проектах мы обычно используем собственную реализацию DI-контейнера. Также в опенсорсе есть готовые библиотеки:&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;a href=https://github.com/microsoft/tsyringe&gt;TSyringe&lt;/a&gt; — контейнер с внедрением через конструкторы и декораторами.&lt;li&gt;&lt;p&gt;&lt;a href=https://inversify.io/docs/fundamentals/binding/&gt;InversifyJS&lt;/a&gt; — позволяет связывать идентификаторы сервисов с реализациями и настраивать их создание.&lt;li&gt;&lt;p&gt;&lt;a href=https://github.com/jeffijoe/awilix&gt;Awilix&lt;/a&gt; — вариант без обязательных декораторов, с регистрацией классов, фабрик и готовых значений.&lt;li&gt;&lt;p&gt;&lt;a href=https://inferdi.com/&gt;InferDI&lt;/a&gt; — контейнер с явной регистрацией сервисов и проверкой зависимостей на уровне типов TypeScript.&lt;/ul&gt;&lt;p&gt;API и возможности библиотек различаются. Примеры выше используют учебный контейнер, который хранит готовые экземпляры. Такой же принцип показан в &lt;a href=https://github.com/VanyaJedi/feature-oriented-demo&gt;демо-репозитории&lt;/a&gt;: без автоматического внедрения и ленивой загрузки.&lt;p&gt;Жизненный цикл зависимостей тоже нужно определить. В клиентском приложении контейнер может существовать всё время работы приложения. При SSR сервисы с пользовательскими данными должны быть изолированы на запрос.&lt;p&gt;&lt;strong&gt;DI не исправляет неудачные связи между модулями. Если два сервиса требуют друг друга или для одной операции приходится передавать сервис с десятками несвязанных методов, стоит пересмотреть границы и контракты.&lt;/strong&gt;&lt;h3&gt;Где находятся пользовательские сценарии?&lt;/h3&gt;&lt;p&gt;Сервисы загружают товары, рассчитывают доставку и отправляют заказы. Но действие пользователя обычно требует нескольких шагов: собрать данные, вызвать сервис, показать ожидание и обработать результат.&lt;p&gt;В нашем React-приложении за это отвечают &lt;strong&gt;хуки в слое &lt;/strong&gt;&lt;code&gt;&lt;strong&gt;hooks&lt;/strong&gt;&lt;/code&gt;&lt;strong&gt; соответствующей фичи&lt;/strong&gt;. Они вызывают сервисы, управляют состоянием сценария и возвращают компоненту данные и доступные действия. Например, &lt;code&gt;useCheckout&lt;/code&gt; предоставляет функцию оформления заказа, признак отправки, ошибку и идентификатор созданного заказа.&lt;p&gt;&lt;strong&gt;&lt;em&gt;При этом сложную бизнес-логику и последовательность бизнес-операций не переносим в хуки: хук связывает React с сервисами и управляет состоянием пользовательского сценария.&lt;/em&gt;&lt;/strong&gt;&lt;p&gt;В учебном примере обойдёмся обычным &lt;code&gt;useState&lt;/code&gt;. Предположим, что у &lt;code&gt;CheckoutService&lt;/code&gt; есть метод &lt;code&gt;placeOrder(input)&lt;/code&gt;, который отправляет заказ и возвращает его идентификатор:&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/1f5/e7f/521/1f5e7f521738f59bfa5e2ef48a57264c.png width=3680 height=3004 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/1f5/e7f/521/1f5e7f521738f59bfa5e2ef48a57264c.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/1f5/e7f/521/1f5e7f521738f59bfa5e2ef48a57264c.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Компонент получает данные и функцию оформления заказа. Входные данные передаются при её вызове:&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/4b3/034/d5f/4b3034d5f03b46eaf0c28fd80d1f056a.png width=3680 height=844 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/4b3/034/d5f/4b3034d5f03b46eaf0c28fd80d1f056a.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/4b3/034/d5f/4b3034d5f03b46eaf0c28fd80d1f056a.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;h3&gt;Где хранить состояние приложения?&lt;/h3&gt;&lt;p&gt;Для этой архитектуры не требуется конкретная библиотека управления состоянием. Выбор зависит от того, откуда приходят данные, кто их использует и сколько они должны существовать. Мы выделяем три основные категории.&lt;p&gt;&lt;strong&gt;Серверное состояние&lt;/strong&gt; — товары, заказы, доступные способы доставки и другие данные с сервера. На клиенте хранится их копия, которая может устаревать. Для загрузки, кэширования и обновления можно использовать &lt;a href=https://tanstack.com/query/latest/docs/framework/react/guides/mutations&gt;React Query&lt;/a&gt;, &lt;a href=https://swr.vercel.app/docs/advanced/cache&gt;SWR&lt;/a&gt; или &lt;a href=https://redux-toolkit.js.org/rtk-query/overview&gt;RTK Query&lt;/a&gt;.&lt;p&gt;&lt;strong&gt;Клиентское состояние&lt;/strong&gt; — выбранные позиции корзины, черновик заказа и другие данные, которыми пользуются несколько компонентов. Для него подходят &lt;a href=https://zustand.docs.pmnd.rs/&gt;Zustand&lt;/a&gt;, &lt;a href=https://redux-toolkit.js.org/&gt;Redux Toolkit&lt;/a&gt; или состояние общего родительского компонента, передаваемое через &lt;code&gt;props&lt;/code&gt; либо &lt;code&gt;Context&lt;/code&gt;.&lt;p&gt;&lt;strong&gt;Локальное состояние&lt;/strong&gt; — раскрытый блок, значение поля или состояние операции, нужное только одному компоненту или хуку. Обычно достаточно встроенных &lt;code&gt;useState&lt;/code&gt; и &lt;code&gt;useReducer&lt;/code&gt;.&lt;p&gt;Эти способы могут сочетаться на одном экране. Не нужно переносить всё в общий store или дублировать в нём данные, которыми уже управляет серверный кэш. Для каждого значения должно быть понятно, где оно хранится и кто его обновляет.&lt;h3&gt;Как отделить логику от платформы?&lt;/h3&gt;&lt;p&gt;До этого мы обсуждали зависимости между модулями приложения. Но у frontend-кода есть ещё один источник зависимостей — окружение, в котором он выполняется. Обращаясь к &lt;code&gt;window, document, localStorage&lt;/code&gt; или браузерному SDK, мы неявно предполагаем, что код всегда будет работать в браузере и нужный API окажется доступен.&lt;p&gt;Это предположение удобно до тех пор, пока модуль не потребуется импортировать в серверном окружении, запустить в тестах или использовать в другой платформенной реализации. Даже в браузере отдельные возможности могут быть недоступны: например, доступ к storage может быть ограничен настройками окружения.&lt;p&gt;Под &lt;code&gt;platform-agnostic&lt;/code&gt;подходом мы понимаем явное отделение логики приложения от таких платформенных возможностей. Это не означает, что React-компоненты автоматически смогут работать в любом runtime. Речь о том, чтобы зависимость от конкретного окружения была локализована и имела понятный контракт.&lt;h4&gt;Доступ к платформенным API&lt;/h4&gt;&lt;p&gt;В нашем приложении доступ к браузерным API сосредоточен в &lt;code&gt;shared/platform&lt;/code&gt;&lt;strong&gt;. &lt;/strong&gt;Например, вместо прямого обращения к &lt;code&gt;localStorage&lt;/code&gt; используется функция, которая проверяет его доступность:&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/c66/5b6/c76/c665b6c7674711e77f2cfb319f111f05.png width=2048 height=610 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/c66/5b6/c76/c665b6c7674711e77f2cfb319f111f05.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/c66/5b6/c76/c665b6c7674711e77f2cfb319f111f05.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Вызывающий код обязан учитывать, что хранилище может отсутствовать:&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/937/7a8/72e/9377a872e4d75b5f6f656c08eddcf663.png width=3680 height=1924 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/937/7a8/72e/9377a872e4d75b5f6f656c08eddcf663.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/937/7a8/72e/9377a872e4d75b5f6f656c08eddcf663.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Если хранилище недоступно или сохранённого запроса нет, &lt;code&gt;read()&lt;/code&gt; возвращает &lt;code&gt;null&lt;/code&gt;. При записи обрабатываем возможную ошибку: например, превышение квоты. Поиск продолжает работать, даже если последний запрос не удалось сохранить.&lt;h4&gt;Что остаётся зависимым от React?&lt;/h4&gt;&lt;p&gt;Компоненты и хуки остаются связаны с React. Модели, расчёты и сервисы мы отделяем от React и браузерных API, чтобы использовать одну и ту же логику в разных окружениях. При смене платформы меняются интерфейс и платформенные адаптеры, а бизнес-логика сохраняется.&lt;h4&gt;Как проверить, что границы работают?&lt;/h4&gt;&lt;p&gt;Архитектура полезна, если её правила можно проверить. В нашем случае &lt;code&gt;ESLint&lt;/code&gt; ограничивает направление импортов, запрещает &lt;code&gt;runtime&lt;/code&gt; cross-импорты между фичами и прямое обращение к браузерным глобальным объектам вне платформенного слоя. &lt;code&gt;TypeScript&lt;/code&gt; проверяет соответствие реализаций контрактам, а тесты — их поведение. Правило &lt;code&gt;no-restricted-globals&lt;/code&gt; само по себе не перекрывает все способы доступа, например &lt;code&gt;globalThis.window&lt;/code&gt;: такие формы тоже нужно учитывать в конфигурации проверок.&lt;p&gt;Простой практический тест для &lt;code&gt;DI&lt;/code&gt; — можно ли создать сервис без запуска приложения. В примере оформления заказа достаточно передать небольшой объект с методом calculate:&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/873/4a4/519/8734a451915dd6f6bc03937ad99027f9.png width=2048 height=1091 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/873/4a4/519/8734a451915dd6f6bc03937ad99027f9.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/873/4a4/519/8734a451915dd6f6bc03937ad99027f9.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Тест не инициализирует &lt;code&gt;React&lt;/code&gt;, не создаёт контейнер и не импортирует реализацию &lt;code&gt;delivery&lt;/code&gt;. Мы проверяем именно правило &lt;code&gt;checkout&lt;/code&gt;: доставка включается в итоговую сумму. Сам расчёт тарифа проверяется отдельно, внутри &lt;code&gt;delivery&lt;/code&gt;. Дополнительный интеграционный тест должен проверять сборку контейнера: правильно ли зарегистрированы токены и удаётся ли получить готовый &lt;code&gt;CheckoutService&lt;/code&gt;.&lt;h3&gt;Цена такого подхода&lt;/h3&gt;&lt;p&gt;У подхода есть накладные расходы: токены, регистрации и контракты увеличивают объём кода, а часть ошибок сборки обнаруживается только при выполнении. Излишнее дробление фич и слишком общие интерфейсы могут сделать проект сложнее. Поэтому мы не оборачиваем в DI каждую функцию, используем только там, где нужно межфичевое взаимодействие.&lt;p&gt;Для небольшого приложения нескольких модулей и явной передачи зависимостей может быть достаточно. Контейнер становится полезен, когда сборка повторяется, зависимостей становится много или нужны разные реализации. Критерий тот же, с которого мы начали: структура должна упрощать работу над продуктом, а не требовать постоянных объяснений самой себя.&lt;h3&gt;Итоги&lt;/h3&gt;&lt;p&gt;В итоге мы организовали код вокруг понятных бизнес-сценариев, внутри модулей разделили его по роли и явно задали допустимые зависимости. Сервисы загружают товары, рассчитывают доставку и отправляют заказы, хуки связывают эту логику с интерфейсом, а состояние остаётся у своего владельца. DI переносит выбор реализаций в точку сборки, платформенный слой локализует работу с окружением.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/375/930/4c4/3759304c494ece5f5f59d32c0356119b.png width=1810 height=928 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/375/930/4c4/3759304c494ece5f5f59d32c0356119b.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/375/930/4c4/3759304c494ece5f5f59d32c0356119b.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Это не универсальная схема и не набор обязательных папок.&lt;p&gt;Пользу от такой организации проще всего оценить на обычной задаче. Нужно изменить расчёт доставки — понятно, где лежит код, кто его вызывает и как проверить, что всё работает. Если для этого приходится разбираться в половине приложения, стоит пересмотреть границы модулей.&lt;p&gt;При этом не стоит увлекаться изящными абстракциями. Не каждой функции нужен сервис, не каждому классу — интерфейс, а небольшой задаче — ещё один слой. Код должен быть простым и понятным и для разработчика, и для LLM, которой вы поручаете изменения. Если для небольшой правки приходится объяснять модели устройство всего проекта и загружать десятки файлов, стоит проверить, не усложнили ли вы архитектуру. Добавлять абстракцию стоит тогда, когда она решает конкретную проблему и с ней действительно проще работать.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <guid>https://habr.com/ru/companies/rwb/articles/1079970/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1079970</guid>
      <pubDate>Mon, 14 Sep 2026 14:07:13 +0000</pubDate>
    </item>
    <item>
      <title>Где HTTP/2 и HTTP/3 реально быстрее, а где не дают ничего</title>
      <link>https://habr.com/ru/companies/timeweb/articles/1079658/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1079658</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;Про HTTP/2 и HTTP/3 написано столько, что вопрос кажется закрытым. Включаешь — и всё летает. Так, во всяком случае, рассказывают. Мультиплексирование убирает очередь из запросов, QUIC держится, когда пакеты теряются, рукопожатие укладывается в один круг вместо двух. Звучит складно. И, наверное, где-то так и есть.&lt;p&gt;Меня эта складность не устраивала. Все графики, на которые я натыкался, были нарисованы на чужих стендах. И почти нигде не говорилось простое: где именно выигрыш исчезает, а он исчезает. Причём в самых обыденных ситуациях — отдаёте один большой файл или сервер с клиентом стоят в одном дата-центре, и всё, новой магии нет.&lt;p&gt;Поэтому я собрал стенд и померил сам. Caddy раздаёт одни и те же данные по всем трём протоколам сразу,&lt;code&gt; tc netem&lt;/code&gt; изображает шесть сетей — от дата-центра до края соты. Дальше — что из этого вышло. Включая те случаи, где новые протоколы проигрывают старому. И три ловушки в методике, каждая из которых едва не увела меня в публикацию красивой неправды.&lt;p&gt;Поэтому я собрал стенд и померил сам. Caddy раздаёт одни и те же данные по всем трём протоколам сразу, tc netem изображает шесть сетей — от дата-центра до края соты. Дальше — что из этого вышло. Включая те случаи, где новые протоколы проигрывают старому. И три ловушки в методике, каждая из которых едва не увела меня в публикацию красивой неправды.&lt;h2&gt;❯ Что именно мы проверяем&lt;/h2&gt;&lt;p&gt;HTTP/1.1 — это один запрос за раз в одном соединении. RFC 9113 прямо называет это причиной, по которой клиенты открывают к серверу сразу несколько соединений. Сколько именно — спецификация не фиксирует, браузеры сошлись на шести на хост. Не больше, не меньше.&lt;p&gt;HTTP/2 делает иначе: много запросов едут одновременно в одном TCP-соединении. Очередь на уровне HTTP исчезает. Но остаётся уровнем ниже — и это важно. TCP обязан отдать приложению байты строго по порядку. Поэтому, как только теряется сегмент, стек придерживает в приёмном буфере всё, что пришло после, и ждёт, пока потерянное не восстановят. Это и называют head-of-line blocking: встают все потоки соединения разом, даже те, чьи сегменты дошли целыми.&lt;p&gt;HTTP/3 переносит всё на QUIC поверх UDP. Порядок доставки гарантируется внутри каждого потока по отдельности — потерянный пакет блокирует только свой поток, соседей не трогает. Заодно QUIC складывает транспортное рукопожатие и TLS 1.3 в одну процедуру. Там, где TCP тратит круг на SYN и SYN-ACK, а TLS следом ещё один, QUIC укладывается в один.&lt;p&gt;Три обещания. Все три можно проверить измерением, а не верить слайдам.&lt;h2&gt;❯ Стенд&lt;/h2&gt;&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td data-colwidth=340 width=340&gt;&lt;p align=left&gt;&lt;strong&gt;Компонент&lt;/strong&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;Что именно&lt;/strong&gt;&lt;tr&gt;&lt;td data-colwidth=340 width=340&gt;&lt;p align=left&gt;Сервер&lt;td&gt;&lt;p align=left&gt;Caddy 2.11.4, один порт, протоколы &lt;code&gt;h1 h2 h3&lt;/code&gt; одновременно&lt;tr&gt;&lt;td data-colwidth=340 width=340&gt;&lt;p align=left&gt;Клиент&lt;td&gt;&lt;p align=left&gt;curl 8.2.1-DEV: BoringSSL, nghttp2 1.52.0, quiche 0.18.0&lt;tr&gt;&lt;td data-colwidth=340 width=340&gt;&lt;p align=left&gt;ОС&lt;td&gt;&lt;p align=left&gt;Ubuntu 24.04, ядро 6.8.0&lt;tr&gt;&lt;td data-colwidth=340 width=340&gt;&lt;p align=left&gt;Сеть&lt;td&gt;&lt;p align=left&gt;loopback с MTU 1500, эмуляция &lt;code&gt;tc netem&lt;/code&gt;, полоса 100 Мбит/с&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Один и тот же бинарник curl обслуживает все три протокола — меняется только флаг. Это не мелочь. Сравнивать HTTP/2 из одной сборки с HTTP/3 из другой бессмысленно: разница между сборками перекроет разницу между протоколами, и вы будете измерять чужой код, а не транспорт.&lt;p&gt;Второй вопрос снимаю сразу — управление перегрузкой одинаково по обе стороны. В ядре &lt;code&gt;net.ipv4.tcp_congestion_control = cubic&lt;/code&gt;, у quiche алгоритм по умолчанию тоже CUBIC. Значит, всё, что я увижу ниже, — это различия транспорта, а не выбора алгоритма.&lt;p&gt;И одна настройка, без которой замер вообще бессмыслен. QUIC работает поверх UDP, а стандартный приёмный буфер сокета его душит — Caddy при старте прямо жалуется в лог. Документация quic-go рекомендует поднять буфер до 7 МБ:&lt;pre&gt;&lt;code class=bash&gt;sudo sysctl -w net.core.rmem_max=7500000&#xA;sudo sysctl -w net.core.wmem_max=7500000&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:87px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Если вам попадётся бенчмарк, где HTTP/3 проигрывает, первым делом спросите — поднимали ли там буфер. Обычно нет.&lt;p&gt;Профили сети. &lt;code&gt;netem&lt;/code&gt; на loopback применяется к обоим направлениям, поэтому RTT равен удвоенной задержке — проверено замером времени TCP-рукопожатия. По той же причине удваиваются потери: в колонке «потери 1%» читайте «1% на каждое направление».&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;Сценарий&lt;/strong&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;Настройка netem&lt;/strong&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;Измеренный RTT&lt;/strong&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Дата-центр&lt;td&gt;&lt;p align=left&gt;Нет&lt;td&gt;&lt;p align=left&gt;0,15 мс&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Проводной&lt;td&gt;&lt;p align=left&gt;&lt;code&gt;delay 20ms rate 100mbit&lt;/code&gt;&lt;td&gt;&lt;p align=left&gt;40,16 мс&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Другой континент&lt;td&gt;&lt;p align=left&gt;&lt;code&gt;delay 75ms rate 100mbit&lt;/code&gt;&lt;td&gt;&lt;p align=left&gt;150,24 мс&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Мобильный&lt;td&gt;&lt;p align=left&gt;&lt;code&gt;delay 30ms loss 1%&lt;/code&gt;&lt;td&gt;&lt;p align=left&gt;60,22 мс&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Плохой мобильный&lt;td&gt;&lt;p align=left&gt;&lt;code&gt;delay 30ms loss 3%&lt;/code&gt;&lt;td&gt;&lt;p align=left&gt;60,25 мс&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Край соты&lt;td&gt;&lt;p align=left&gt;&lt;code&gt;delay 50ms loss 5%&lt;/code&gt;&lt;td&gt;&lt;p align=left&gt;100,42 мс&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Четыре теста. Десять файлов по 8 КБ. Пятьдесят таких же. Один файл 3 МБ. И холодное соединение. HTTP/1.1 получает шесть параллельных соединений, HTTP/2 и HTTP/3 мультиплексируют потоки в одном — проверено счётчиком num_connects. Медиана из пяти прогонов, для крупного файла — из трёх, таймаут две минуты.&lt;h2&gt;❯ Десять файлов: шесть окон перегрузки против одного&lt;/h2&gt;&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;&lt;strong&gt;Сценарий&lt;/strong&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;HTTP/1.1&lt;/strong&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;HTTP/2&lt;/strong&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;HTTP/3&lt;/strong&gt;&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;&lt;strong&gt;Лучший&lt;/strong&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;HTTP/3 к HTTP/2&lt;/strong&gt;&lt;tr&gt;&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;Дата-центр, RTT 0&lt;td&gt;&lt;p align=left&gt;7 мс&lt;td&gt;&lt;p align=left&gt;9 мс&lt;td&gt;&lt;p align=left&gt;6&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;HTTP/3&lt;td&gt;&lt;p align=left&gt;-33%&lt;tr&gt;&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;Проводной, RTT 40 мс&lt;td&gt;&lt;p align=left&gt;170 мс&lt;td&gt;&lt;p align=left&gt;210 мс&lt;td&gt;&lt;p align=left&gt;146&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;HTTP/3&lt;td&gt;&lt;p align=left&gt;-30%&lt;tr&gt;&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;Другой континент, RTT 150 мс&lt;td&gt;&lt;p align=left&gt;615 мс&lt;td&gt;&lt;p align=left&gt;767 мс&lt;td&gt;&lt;p align=left&gt;519&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;HTTP/3&lt;td&gt;&lt;p align=left&gt;-32%&lt;tr&gt;&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;Мобильный, RTT 60 мс, потери 1%&lt;td&gt;&lt;p align=left&gt;255 мс&lt;td&gt;&lt;p align=left&gt;348 мс&lt;td&gt;&lt;p align=left&gt;214&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;HTTP/3&lt;td&gt;&lt;p align=left&gt;-39%&lt;tr&gt;&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;Плохой мобильный, RTT 60 мс, потери 3%&lt;td&gt;&lt;p align=left&gt;569 мс&lt;td&gt;&lt;p align=left&gt;368 мс&lt;td&gt;&lt;p align=left&gt;270&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;HTTP/3&lt;td&gt;&lt;p align=left&gt;-27%&lt;tr&gt;&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;Край соты, RTT 100 мс, потери 5%&lt;td&gt;&lt;p align=left&gt;846 мс&lt;td&gt;&lt;p align=left&gt;708 мс&lt;td&gt;&lt;p align=left&gt;466&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;HTTP/3&lt;td&gt;&lt;p align=left&gt;-34%&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/f51/5f4/baa/f515f4baa0a3740b3124affb1ff139bd.png alt=&#34;Десять файлов по 8 КБ. На каналах без потерь HTTP/2 отстаёт от HTTP/1.1&#34; title=&#34;Десять файлов по 8 КБ. На каналах без потерь HTTP/2 отстаёт от HTTP/1.1&#34; width=1972 height=928 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/f51/5f4/baa/f515f4baa0a3740b3124affb1ff139bd.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/f51/5f4/baa/f515f4baa0a3740b3124affb1ff139bd.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Десять файлов по 8 КБ. На каналах без потерь HTTP/2 отстаёт от HTTP/1.1&lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Первое, что бросается в глаза: на каналах без потерь HTTP/2 проигрывает HTTP/1.1. На проводном — 210 мс против 170, на межконтинентальном — 767 против 615. Ровно противоположно тому, что обещают слайды.&lt;p&gt;Объяснение простое, даже обидно простое. Браузер открывает шесть соединений, и десять файлов укладываются в два RTT. У каждого соединения своё окно перегрузки, все шесть проходят slow start параллельно — за тот же круг в сеть уходит вшестеро больше сегментов. А HTTP/2 кладёт всё в одно соединение и разгоняет одно-единственное окно. Мультиплексирование тут не отыгрывается: очереди из запросов и так нет.&lt;p&gt;HTTP/3 выигрывает у HTTP/2 стабильно — от 27 до 39 процентов. Я сначала списал это на рукопожатие, потом посчитал. На проводном канале отрыв 64 мс при экономии на рукопожатии 38, на межконтинентальном — 248 против 149. Рукопожатие объясняет от 41 до 60 процентов, остальное без объяснения. Скорее всего, начальное окно перегрузки у QUIC своё и разгоняется иначе — но чтобы утверждать это, нужна трассировка, а у меня её нет.&lt;h2&gt;❯ Пятьдесят файлов: где мультиплексирование окупается&lt;/h2&gt;&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Сценарий&lt;td&gt;&lt;p align=left&gt;HTTP/1.1&lt;td&gt;&lt;p align=left&gt;HTTP/2&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;HTTP/3&lt;td&gt;&lt;p align=left&gt;Лушчий&lt;td&gt;&lt;p align=left&gt;HTTP/3 к HTTP/2&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Дата-центр, RTT 0&lt;td&gt;&lt;p align=left&gt;14 мс&lt;td&gt;&lt;p align=left&gt;12 мс&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;15 мс&lt;td&gt;&lt;p align=left&gt;HTTP/2&lt;td&gt;&lt;p align=left&gt;+25%&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Проводной, RTT 40 мс&lt;td&gt;&lt;p align=left&gt;461 мс&lt;td&gt;&lt;p align=left&gt;306 мс&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;227 мс&lt;td&gt;&lt;p align=left&gt;HTTP/3&lt;td&gt;&lt;p align=left&gt;-26%&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Другой континент, RTT 150 мс&lt;td&gt;&lt;p align=left&gt;1,67 с&lt;td&gt;&lt;p align=left&gt;1,07 с&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;827 мс&lt;td&gt;&lt;p align=left&gt;HTTP/3&lt;td&gt;&lt;p align=left&gt;-23%&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Мобильный, RTT 60 мс, потери 1%&lt;td&gt;&lt;p align=left&gt;805 мс&lt;td&gt;&lt;p align=left&gt;492 мс&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;355 мс&lt;td&gt;&lt;p align=left&gt;HTTP/3&lt;td&gt;&lt;p align=left&gt;-28%&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Плохой мобильный, RTT 60 мс, потери 3%&lt;td&gt;&lt;p align=left&gt;1,12 с&lt;td&gt;&lt;p align=left&gt;1,16 мс&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;520 мс&lt;td&gt;&lt;p align=left&gt;HTTP/3&lt;td&gt;&lt;p align=left&gt;-55%&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Край соты, RTT 100 мс, потери 5%&lt;td&gt;&lt;p align=left&gt;2,07 с&lt;td&gt;&lt;p align=left&gt;1,92 мс&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;1,12 с ⚠ 4/5&lt;td&gt;&lt;p align=left&gt;HTTP/3&lt;td&gt;&lt;p align=left&gt;-41%&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/50c/c7f/8da/50cc7f8da02d97b04968b504a5bc8099.png alt=&#34;Пятьдесят файлов по 8 КБ — здесь мультиплексирование наконец окупается&#34; title=&#34;Пятьдесят файлов по 8 КБ — здесь мультиплексирование наконец окупается&#34; width=1972 height=928 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/50c/c7f/8da/50cc7f8da02d97b04968b504a5bc8099.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/50c/c7f/8da/50cc7f8da02d97b04968b504a5bc8099.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Пятьдесят файлов по 8 КБ — здесь мультиплексирование наконец окупается&lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Картина переворачивается. Пятьдесят файлов на шести соединениях — это девять кругов, и HTTP/1.1 платит за каждый. На межконтинентальном канале 1,67 секунды против 827 миллисекунд у HTTP/3. Разница вдвое, и она видна невооружённым глазом.&lt;p&gt;И тут же главная оговорка. Посмотрите на первую строку: в дата-центре, где RTT практически ноль, все три протокола укладываются в 12–15 миллисекунд. Разница лежит в пределах шума. Мультиплексирование окупается только тогда, когда круговая задержка заметна. Нет задержки — нет выигрыша. Вот, собственно, и всё.&lt;h2&gt;❯ Один поток на 3 МБ: упираемся в пропускную способность&lt;/h2&gt;&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Сценарий&lt;td&gt;&lt;p align=left&gt;HTTP/1.1&lt;td&gt;&lt;p align=left&gt;HTTP/2&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;HTTP/3&lt;td&gt;&lt;p align=left&gt;Лучший&lt;td&gt;&lt;p align=left&gt;HTTP/3 к HTTP/2&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Дата-центр, RTT 0&lt;td&gt;&lt;p align=left&gt;5 мс&lt;td&gt;&lt;p align=left&gt;8 мс&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;22 мс&lt;td&gt;&lt;p align=left&gt;HTTP/1.1&lt;td&gt;&lt;p align=left&gt;+186%&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Проводной, RTT 40 мс&lt;td&gt;&lt;p align=left&gt;631 мс&lt;td&gt;&lt;p align=left&gt;632 см&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;654 мс&lt;td&gt;&lt;p align=left&gt;HTTP/1.1&lt;td&gt;&lt;p align=left&gt;+4%&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Другой континент, RTT 150 мс&lt;td&gt;&lt;p align=left&gt;1,62 с&lt;td&gt;&lt;p align=left&gt;1,62 с&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;2,10 с&lt;td&gt;&lt;p align=left&gt;HTTP/2&lt;td&gt;&lt;p align=left&gt;+30%&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Мобильный, RTT 60 мс, потери 1%&lt;td&gt;&lt;p align=left&gt;4,59 с&lt;td&gt;&lt;p align=left&gt;1,22 с&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;6,66 с&lt;td&gt;&lt;p align=left&gt;HTTP/2&lt;td&gt;&lt;p align=left&gt;+446%&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Плохой мобильный, RTT 60 мс, потери 3%&lt;td&gt;&lt;p align=left&gt;16,90 с&lt;td&gt;&lt;p align=left&gt;18,19 с&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;10,92 с&lt;td&gt;&lt;p align=left&gt;HTTP/3&lt;td&gt;&lt;p align=left&gt;-40%&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Край соты, RTT 100 мс, потери 5%&lt;td&gt;&lt;p align=left&gt;32,10 с&lt;td&gt;&lt;p align=left&gt;40,31 с&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;19,42 с ⚠ 1/3&lt;td&gt;&lt;p align=left&gt;HTTP/3&lt;td&gt;&lt;p align=left&gt;-52%&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/34f/450/2b9/34f4502b907c45bb4c2f5f69cab2504e.png alt=&#34;Один файл 3 МБ, шкала логарифмическая. На проводном канале все три протокола показывают одно и то же&#34; title=&#34;Один файл 3 МБ, шкала логарифмическая. На проводном канале все три протокола показывают одно и то же&#34; width=1972 height=928 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/34f/450/2b9/34f4502b907c45bb4c2f5f69cab2504e.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/34f/450/2b9/34f4502b907c45bb4c2f5f69cab2504e.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Один файл 3 МБ, шкала логарифмическая. На проводном канале все три протокола показывают одно и то же&lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Строка «Проводной» — это квинтэссенция всей статьи. 631, 632 и 654 миллисекунды. Три протокола, разных на десять лет разработки, показывают одно и то же время. Потому что все трое упираются в пропускную способность. Узкое место лежит вне протокола.&lt;p&gt;Прикиньте порядок величин. При 100 Мбит/с и RTT 40 мс произведение полосы на задержку — около 500 КБ. Файл 3 МБ — это шесть таких окон, и заметная часть передачи уходит на разгон.&lt;p&gt;Строка «Дата-центр» интереснее. На идеальном локальном канале HTTP/1.1 отдаёт файл за 5 мс, HTTP/2 — за 8, HTTP/3 — за 22. Новые протоколы проигрывают старому вчетверо. Когда сеть перестаёт быть узким местом, им становится процессор: HTTP/2 тратит такты на фрейминг и сжатие заголовков HPACK, а у QUIC весь транспорт живёт в пространстве пользователя — каждая датаграмма проходит через системный вызов и шифрование, тогда как TCP-стек работает в ядре.&lt;h2&gt;❯ Рукопожатие: экономия ровно в один RTT&lt;/h2&gt;&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;&lt;strong&gt;Сценарий&lt;/strong&gt;&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;&lt;strong&gt;HTTP/1.1&lt;/strong&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;HTTP/2&lt;/strong&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;HTTP/3&lt;/strong&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;Лучший&lt;/strong&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;HTTP/3 к HTTP/2&lt;/strong&gt;&lt;tr&gt;&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;Дата-центр, RTT 0&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;1 мс&lt;td&gt;&lt;p align=left&gt;2 мс&lt;td&gt;&lt;p align=left&gt;2 мс&lt;td&gt;&lt;p align=left&gt;HTTP/1.1&lt;td&gt;&lt;p align=left&gt;-2%&lt;tr&gt;&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;Проводной, RTT 40 мс&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;122 мс&lt;td&gt;&lt;p align=left&gt;124 мс&lt;td&gt;&lt;p align=left&gt;86 мс&lt;td&gt;&lt;p align=left&gt;HTTP/3&lt;td&gt;&lt;p align=left&gt;-30%&lt;tr&gt;&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;Другой континент, RTT 150 мс&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;452 мс&lt;td&gt;&lt;p align=left&gt;452 мс&lt;td&gt;&lt;p align=left&gt;303 мс&lt;td&gt;&lt;p align=left&gt;HTTP/3&lt;td&gt;&lt;p align=left&gt;-33%&lt;tr&gt;&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;Мобильный, RTT 60 мс, потери 1%&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;182 мс&lt;td&gt;&lt;p align=left&gt;182 мс&lt;td&gt;&lt;p align=left&gt;123 мс&lt;td&gt;&lt;p align=left&gt;HTTP/3&lt;td&gt;&lt;p align=left&gt;-33%&lt;tr&gt;&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;Плохой мобильный, RTT 60 мс, потери 3%&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;182 мс&lt;td&gt;&lt;p align=left&gt;182 мс&lt;td&gt;&lt;p align=left&gt;125 мс ⚠ 3/5&lt;td&gt;&lt;p align=left&gt;HTTP/3&lt;td&gt;&lt;p align=left&gt;-32%&lt;tr&gt;&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;Край соты, RTT 100 мс, потери 5%&lt;td data-colwidth=113 width=113&gt;&lt;p align=left&gt;349 мс&lt;td&gt;&lt;p align=left&gt;303 мс&lt;td&gt;&lt;p align=left&gt;203 мс&lt;td&gt;&lt;p align=left&gt;HTTP/3&lt;td&gt;&lt;p align=left&gt;-33%&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/a67/5e4/cee/a675e4cee6043941423bc4d9875b1b26.png alt=&#34;Гипотеза «QUIC экономит ровно один круг». Замеры ложатся на неё сами&#34; title=&#34;Гипотеза «QUIC экономит ровно один круг». Замеры ложатся на неё сами&#34; width=1972 height=928 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/a67/5e4/cee/a675e4cee6043941423bc4d9875b1b26.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/a67/5e4/cee/a675e4cee6043941423bc4d9875b1b26.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Гипотеза «QUIC экономит ровно один круг». Замеры ложатся на неё сами&lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Здесь HTTP/3 выигрывает предсказуемо: примерно на треть во всех сценариях с задержкой. TCP тратит круг на SYN и SYN-ACK, и только по установленному соединению идёт TLS 1.3, забирая ещё один. А у QUIC криптография едет прямо в первых пакетах транспорта — отдельного TLS-рукопожатия после установки соединения просто нет.&lt;p&gt;Арифметика сходится до миллисекунд. На межконтинентальном канале с RTT 150 мс: 452 мс у TCP против 303 у QUIC. Разница — 149 мс, ровно один круговой обмен.&lt;p&gt;Одна оговорка. В начале рукопожатия сервер не имеет права отправить на неподтверждённый адрес больше чем втрое от полученного объёма — RFC 9000 называет это anti-amplification limit. В этот лимит входит цепочка сертификатов. Если она объёмная, ответ не помещается, и рукопожатие съедает лишний круг. У меня сертификат локального центра сертификации маленький, поэтому экономия и легла точно в один RTT.&lt;h2&gt;❯ Потери пакетов: откуда берётся разброс в разы&lt;/h2&gt;&lt;p&gt;Девять прогонов файла 3 МБ подряд в одних и тех же условиях.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/7c9/f7f/c08/7c9f7fc080756ce99b26f33f8274cfb2.png alt=&#34;Девять загрузок одного файла подряд. На чистом канале точки сливаются, под потерями — разлетаются&#34; title=&#34;Девять загрузок одного файла подряд. На чистом канале точки сливаются, под потерями — разлетаются&#34; width=1972 height=928 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/7c9/f7f/c08/7c9f7fc080756ce99b26f33f8274cfb2.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/7c9/f7f/c08/7c9f7fc080756ce99b26f33f8274cfb2.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Девять загрузок одного файла подряд. На чистом канале точки сливаются, под потерями — разлетаются&lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;При потерях 1% худший прогон HTTP/1.1 хуже лучшего в 13,5 раза. У HTTP/2 — в 11,3. У HTTP/3 — только в 2,4. На чистом канале у всех троих разброс — единица.&lt;p&gt;Причина — в том, как TCP обнаруживает потерю. Если следом за потерянным сегментом пришли другие, отправитель видит дублирующие подтверждения и уходит в быструю повторную передачу: cubic срежет окно примерно на треть и поедет дальше. Именно на треть, а не вдвое — уполовинивание это поведение Reno, и разницу видно прямо в ядре:&lt;pre&gt;&lt;code class=bash&gt;$ cat /sys/module/tcp_cubic/parameters/beta 717 &lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;QUIC устроен иначе. Номера пакетов у него монотонно растут и не повторяются — повторная передача едет под новым номером. Исчезает та самая неоднозначность, из-за которой TCP не может понять, какому из двух одинаковых сегментов пришло подтверждение, и портит себе оценку RTT. Вместо таймаута повторной передачи работает probe timeout по RFC 9002.&lt;p&gt;Для продакшена это важнее средних. Пользователь ведь видит не медиану — он видит конкретную загрузку. Свою. Сейчас.&lt;h2&gt;❯ Пять процентов потерь: HTTP/3 встаёт намертво&lt;/h2&gt;&lt;p&gt;Последний сценарий — RTT 100 мс и 5% потерь. Обычная жизнь на границе покрытия.&lt;p&gt;По медианам выходила красивая картинка: 32,1 секунды у HTTP/1.1, 40,3 у HTTP/2 и всего 19,4 у HTTP/3. Отличный финал для статьи. Потом я посмотрел, сколько прогонов вообще дошло до конца. У HTTP/3 — один из трёх.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/fae/99d/e1a/fae99de1a7ea7c4fef9f7080dfbb1164.png alt=&#34;Ни один прогон HTTP/1.1 и HTTP/2 не сорвался. Все обрывы у HTTP/3&#34; title=&#34;Ни один прогон HTTP/1.1 и HTTP/2 не сорвался. Все обрывы у HTTP/3&#34; width=1972 height=928 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/fae/99d/e1a/fae99de1a7ea7c4fef9f7080dfbb1164.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/fae/99d/e1a/fae99de1a7ea7c4fef9f7080dfbb1164.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Ни один прогон HTTP/1.1 и HTTP/2 не сорвался. Все обрывы у HTTP/3&lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;За весь эксперимент таких набралось семь. И все семь — у HTTP/3. Отдельная серия с проверкой кода возврата показала CURLE_OPERATION_TIMEDOUT: не разрыв соединения и не ошибка QUIC, а обычный таймаут.&lt;p&gt;Дальше я снял трафик tshark&amp;#39;ом. И картина оказалась неожиданной. Зависание — это не медленная передача, а полная остановка:&lt;pre&gt;&lt;code class=json&gt;run1  19,8 с  успех    пауз &amp;gt;300 мс: 0&#xA;run2 120,3 с  таймаут  одна пауза 108,4 с (встал на 11,6 с, отдав 1,77 МБ)&#xA;run4 120,2 с  таймаут  одна пауза 119,3 с (встал на 0,69 с, отдав 0,14 МБ)&#xA;run6  24,1 с  успех    пауз &amp;gt;300 мс: 0&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Три зависания из шести устроены одинаково. Последний пакет перед тишиной — всегда от клиента, короткое подтверждение на 77 байт. После него сервер не присылает ничего, а клиент не отправляет ни одного зондирующего пакета, хотя RFC 9002 их предписывает. Обе стороны просто ждут друг друга — пока curl не закроет соединение по своему таймауту.&lt;p&gt;Точка срыва произвольная. Один раз — после 1,77 МБ. Два раза — после 0,15 МБ, на первой секунде. Версия про исчерпание окна на фиксированной границе отпала.&lt;p&gt;Назвать причину я не берусь. QUIC шифрует типы фреймов, и чтобы увидеть последний отправленный, нужен захват с ключами. Фиксирую то, что воспроизводится: связка curl 8.2.1-DEV с quiche 0.18.0 и Caddy на quic-go под потерями от 3% примерно в половине случаев встаёт намертво. Сборка клиента старая — на свежей стоит перепроверить.&lt;h2&gt;❯ Три ловушки, в которые я попался&lt;/h2&gt;&lt;p&gt;Все три раза ошибка выглядела как свойство протокола. И все три раза оказывалась свойством стенда.&lt;p&gt;MTU у loopback. Первый вариант стенда давал разброс в восемь раз между двумя запусками одной команды. Очередь &lt;code&gt;netem&lt;/code&gt; была ни при чём — &lt;code&gt;tc -s qdisc&lt;/code&gt; показывал &lt;code&gt;dropped 0&lt;/code&gt;. Тогда я померил канал голым сокетом на Python, без всякого HTTP: чистый TCP выдал 34 МБ/с там, где curl еле выжимал 1 МБ/с. Тридцатикратный разрыв указывал на стенд. Причина — MTU интерфейса &lt;code&gt;lo&lt;/code&gt; равен 65536, в сорок раз больше обычного Ethernet, и окно перегрузки, которое считается в сегментах, ведёт себя рвано. Лечится строчкой&lt;code&gt; ip link set lo mtu 1500&lt;/code&gt;.&lt;p&gt;Медиана по уцелевшим прогонам. Те самые «19,4 секунды», посчитанные по одному успешному измерению из трёх. Формально не ложь: медиана успешных загрузок действительно такая. Просто читатель никогда бы не узнал, что каждая вторая попытка не заканчивается. Теперь генератор таблиц помечает такие клетки: &lt;code&gt;19,42 с ⚠ 1/3&lt;/code&gt;. Шесть символов — а смысл строки меняют.&lt;p&gt;Распределение потерь. Самую неприятную я нашёл, уже когда садился писать выводы. В захвате оказалось, что кадры QUIC на loopback доходят до 14 452 байт при MTU 1500: quic-go отдаёт ядру несколько пакетов одним системным вызовом. У TCP максимальный кадр — 2962 байта.&lt;p&gt;&lt;code&gt;netem&lt;/code&gt; стоит до сегментации и роняет кадр целиком. Я решил, что это занижает HTTP/3, посчитал — оказалось, нет: доля потерянных пакетов совпадает, 124 против 125 из примерно 2500. Отличается распределение. Со склейкой потери приходят пачками — по два-одиннадцать подряд. Без склейки — поодиночке.&lt;p&gt;Я отключил склейку переменной &lt;code&gt;QUIC_GO_DISABLE_GSO=true&lt;/code&gt; и перемерил всё заново.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/90a/a88/2b0/90aa882b0e44868986d52bf64e8cd729.png alt=&#34;Одна и та же доля потерь. Гладкие столбики — потери пачками, штрихованные — поодиночке&#34; title=&#34;Одна и та же доля потерь. Гладкие столбики — потери пачками, штрихованные — поодиночке&#34; width=1972 height=928 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/90a/a88/2b0/90aa882b0e44868986d52bf64e8cd729.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/90a/a88/2b0/90aa882b0e44868986d52bf64e8cd729.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Одна и та же доля потерь. Гладкие столбики — потери пачками, штрихованные — поодиночке&lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;50 файлов&lt;td&gt;&lt;p align=left&gt;HTTP/1.1&lt;td&gt;&lt;p align=left&gt;HTTP/2&lt;td&gt;&lt;p align=left&gt;HTTP/3&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Мобильный, потери 1%&lt;td&gt;&lt;p align=left&gt;805 → 756 мс&lt;td&gt;&lt;p align=left&gt;492 → 645 мс&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;355 → 715 мс&lt;/strong&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Плохой мобильный, 3%&lt;td&gt;&lt;p align=left&gt;1,1 → 1,1 с&lt;td&gt;&lt;p align=left&gt;1,2 → 1,0 с&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;520 → 854 мс&lt;/strong&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Край соты, 5%&lt;td&gt;&lt;p align=left&gt;2,1 → 1,7 с&lt;td&gt;&lt;p align=left&gt;1,9 → 3,9 с&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;1,1 → 2,5 с&lt;/strong&gt;&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;При одной и той же доле потерь HTTP/3 вдвое медленнее, когда потери рассеяны. У HTTP/1.1 время почти не меняется. У HTTP/2 скачет без системы. Распределение бьёт в основном по QUIC — и это, в общем, логично: у него пятьдесят потоков восстанавливаются независимо, поэтому рассеянные потери задевают много потоков сразу, а пачка — один.&lt;p&gt;Возражение про процессор снимается контролем. Если бы дело было в лишних системных вызовах без склейки, это проявилось бы и на чистых каналах, а там ничего не изменилось — 461 против 455 мс, 306 против 308, 227 против 229.&lt;p&gt;Зависания HTTP/3, кстати, никуда не делись и во второй конфигурации. Сместились в другой сценарий, но остались. И по-прежнему только у HTTP/3.&lt;h2&gt;❯ Что из этого следует&lt;/h2&gt;&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Ситуация&lt;td&gt;&lt;p align=left&gt;HTTP/2 против HTTP/1.1&lt;td&gt;&lt;p align=left&gt;HTTP/3 против HTTP/2&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Сервер и клиент в одном дата-центре&lt;td&gt;&lt;p align=left&gt;ничего, иногда хуже&lt;td&gt;&lt;p align=left&gt;хуже&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Один крупный файл, любой канал&lt;td&gt;&lt;p align=left&gt;ничего&lt;td&gt;&lt;p align=left&gt;ничего или хуже&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Десяток мелких файлов&lt;td&gt;&lt;p align=left&gt;часто хуже&lt;td&gt;&lt;p align=left&gt;лучше на треть&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Полсотни мелких файлов, заметный RTT&lt;td&gt;&lt;p align=left&gt;вдвое лучше&lt;td&gt;&lt;p align=left&gt;лучше на четверть&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Мелкие файлы плюс потери&lt;td&gt;&lt;p align=left&gt;по-разному&lt;td&gt;&lt;p align=left&gt;зависит от распределения потерь&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Установка соединения, заметный RTT&lt;td&gt;&lt;p align=left&gt;ничего&lt;td&gt;&lt;p align=left&gt;лучше на треть&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Включать HTTP/2 и HTTP/3 стоит. Ни в одном реалистичном сценарии они не сделали заметно хуже — проигрыши вылезли только на loopback, где сеть вырождена. Но чуда не ждите там, где узкое место в другом. Если у вас один большой файл, медленная база или тяжёлый бэкенд — смена протокола не изменит ничего.&lt;p&gt;Больше всего выигрывают те, у кого много мелких ресурсов и далёкие пользователи. Если весь трафик приходит из соседнего дата-центра, эффект будет околонулевым.&lt;p&gt;HTTP/3 нужен как раз там, где есть потери: мобильный интернет, публичный Wi-Fi, дальние маршруты. На стабильном проводном канале его преимущество сводится к одному сэкономленному кругу на рукопожатии. И проверьте UDP-буфер: полстроки в &lt;code&gt;sysctl&lt;/code&gt; отделяют работающий HTTP/3 от протокола, который у вас «почему-то медленный».&lt;h2&gt;❯ Границы применимости&lt;/h2&gt;&lt;p&gt;Замеры сделаны на эмулированной сети, и характер потерь у &lt;code&gt;netem&lt;/code&gt; зависит от сегментации — совпадение с реальной сетью случайное. Клиент curl, а не браузер: нет разбора HTML, приоритизации, кеша и предзагрузки. Сервер один, реализации QUIC заметно отличаются. 0-RTT не проверял.&lt;p&gt;Всё это воспроизводится: конфиг Caddy на десять строк, шелл-скрипт и &lt;code&gt;tc netem&lt;/code&gt;.&lt;h2&gt;❯ Итог&lt;/h2&gt;&lt;p&gt;Я шёл в эксперимент с ожиданием, что новые протоколы дают равномерный выигрыш — просто разного размера. Получилось не так.&lt;p&gt;Выигрыш оказался узким и адресным. HTTP/2 окупается ровно в одном сценарии — много мелких файлов при заметной задержке — и там действительно даёт вдвое. Во всех остальных он либо не меняет ничего, либо слегка проигрывает старому доброму HTTP/1.1 с его шестью соединениями. HTTP/3 добавляет сэкономленный круг на рукопожатии и более предсказуемое поведение под потерями, но под равномерными потерями сам оказывается вдвое медленнее, а при 5% в половине прогонов вставал намертво.&lt;p&gt;Самое неожиданное для меня не цифры, а то, как легко получить неправильные: восьмикратный разброс из-за MTU; медиана по одному уцелевшему прогону, которая просилась в заголовок и распределение потерь, о котором я не думал вообще.&lt;p&gt;Отсюда единственный вывод, который я готов защищать: правдоподобный результат опаснее странного. Странный вы пойдёте проверять сами, а «19,4 секунды вместо 40» выглядят совершенно нормально — и проходят в публикацию, если не пересчитать прогоны из чистого занудства.&lt;h2&gt;❯ Ссылки&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;a href=https://www.rfc-editor.org/rfc/rfc9114.html&gt;RFC 9114&lt;/a&gt; — HTTP/3&lt;li&gt;&lt;p&gt;&lt;a href=https://www.rfc-editor.org/rfc/rfc9000.html&gt;RFC 9000&lt;/a&gt; — QUIC, в том числе anti-amplification limit&lt;li&gt;&lt;p&gt;&lt;a href=https://www.rfc-editor.org/rfc/rfc9002.html&gt;RFC 9002&lt;/a&gt; — обнаружение потерь и управление перегрузкой в QUIC&lt;li&gt;&lt;p&gt;&lt;a href=https://www.rfc-editor.org/rfc/rfc9113.html&gt;RFC 9113&lt;/a&gt; — HTTP/2 и причина, по которой HTTP/1.1 открывает несколько соединений&lt;li&gt;&lt;p&gt;&lt;a href=https://github.com/quic-go/docs/blob/master/content/docs/quic/optimizations.md&gt;Документация quic-go по оптимизациям&lt;/a&gt; — размер UDP-буфера и переменная QUIC_GO_DISABLE_GSO&lt;li&gt;&lt;p&gt;&lt;a href=https://labs.ripe.net/author/tfiebig/navigating-network-measurements-research-risks-and-reliability/&gt;RIPE Labs: Navigating Network Measurements&lt;/a&gt; — почему сетевым измерениям нельзя верить по умолчанию&lt;/ul&gt;&lt;details class=spoiler&gt;&lt;summary&gt;Может быть интересно:&lt;/summary&gt;&lt;div class=spoiler__content&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;a href=https://habr.com/ru/companies/timeweb/articles/1077640/&gt;&lt;strong&gt;Сервера: распределяем нагрузку&lt;/strong&gt;&lt;/a&gt;&lt;li&gt;&lt;p&gt;&lt;a href=https://habr.com/ru/companies/timeweb/articles/1079144/&gt;&lt;strong&gt;200 OK от гейтвея ничего не значит&lt;/strong&gt;&lt;/a&gt;&lt;li&gt;&lt;p&gt;&lt;a href=https://habr.com/ru/companies/timeweb/articles/1079396/&gt;&lt;strong&gt;Иногда раньше релиза значит воровство. Истории об украденных играх&lt;/strong&gt;&lt;/a&gt;&lt;/ul&gt;&lt;/div&gt;&lt;/details&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/fd3/269/196/fd3269196d530d75f9a061280ce8e3e0.jpg alt=&#34;Перейти ↩&#34; title=&#34;Перейти ↩&#34; width=1080 height=607 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/fd3/269/196/fd3269196d530d75f9a061280ce8e3e0.jpg 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/fd3/269/196/fd3269196d530d75f9a061280ce8e3e0.jpg 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;a href=&#34;https://timeweb.cloud/?utm_source=habr&amp;amp;utm_medium=banner&amp;amp;utm_campaign=promo&#34;&gt;&lt;strong&gt;&lt;em&gt;Перейти&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;&lt;em&gt; ↩&lt;/em&gt;&lt;/strong&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;blockquote&gt;&lt;p&gt;&lt;a href=https://t.me/timewebru&gt;&lt;strong&gt;Новости, обзоры продуктов и конкурсы от команды &lt;/strong&gt;&lt;/a&gt;&lt;a href=http://Timeweb.Cloud&gt;&lt;strong&gt;Timeweb.Cloud&lt;/strong&gt;&lt;/a&gt;&lt;a href=https://t.me/timewebru&gt;&lt;strong&gt; — в нашем Telegram‑канале&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;↩&lt;/strong&gt;&lt;/blockquote&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <guid>https://habr.com/ru/companies/timeweb/articles/1079658/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1079658</guid>
      <pubDate>Mon, 14 Sep 2026 14:05:55 +0000</pubDate>
    </item>
    <item>
      <title>Обновление элементов Справочника в 1С с асинхронным ответом</title>
      <link>https://habr.com/ru/companies/sofros/articles/1082122/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082122</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;&lt;strong&gt;Аннотация&lt;/strong&gt;&lt;p&gt;В этой статье рассмотрим получение Номенклатуры из базы данных SQL и последующую запись в систему 1С. После обработки сообщения системой 1С будет сформирован асинхронный ответ о статусе обработки и отправлен в систему источник. &lt;p&gt;Асинхронный ответ выгодно использовать, когда:&lt;ul&gt;&lt;li&gt;&lt;p&gt;Нет необходимости оперативно обновлять информацию и получать статус об обновлении данных&lt;li&gt;&lt;p&gt;Если есть риск недоступности системы приёмника (обслуживание или обновление базы 1С)&lt;li&gt;&lt;p&gt;Требуется обновить большие объемы данных не вызывая повышенную нагрузку на сервера&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Описание работы разрабатываемого решения: &lt;/strong&gt;&lt;ol&gt;&lt;li&gt;&lt;p&gt;Задание по расписанию (триггер) запускает обработку бизнес-процесса по получению записей из системы источника (MS SQL).&lt;li&gt;&lt;p&gt;При выполнении бизнес-процесса происходит маппинг массива и отправка элемента Номенклатуры в 1С.&lt;li&gt;&lt;p&gt;В обработчике 1С происходит создание/изменение Номенклатуры в Справочнике.&lt;li&gt;&lt;p&gt;Формируется статус и помещается в регистр сведений &amp;#34;&lt;em&gt;сшпОчередьИсходящихСообщений&lt;/em&gt;&amp;#34; с использованием документируемой функции &lt;strong&gt;сшпПоместитьВОчередьИсходящих&lt;/strong&gt;.&lt;li&gt;&lt;p&gt;Ответ попадает в Platform и маршрутизируется до MSSQL. При обработке статуса элементу в MSSQL всегда проставляется need_send = 0, чтобы успешные или ошибочные записи больше не попадали в обработку.&lt;/ol&gt;&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/7c0/e15/89f/7c0e1589f3a69d61ecea0ff38b039685.png width=442 height=692 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/7c0/e15/89f/7c0e1589f3a69d61ecea0ff38b039685.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/7c0/e15/89f/7c0e1589f3a69d61ecea0ff38b039685.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;&lt;strong&gt;Настройки в Platform&lt;/strong&gt;&lt;p&gt;&lt;strong&gt;1. Подготовительный этап:&lt;/strong&gt;&lt;p&gt;Создаём тип данных &lt;strong&gt;habr_Номенклатура&lt;/strong&gt; и импортируем в него структуру из Справочника&lt;strong&gt; “Номенклатура” &lt;/strong&gt;из 1С. Назначение: &lt;em&gt;Используется для обработки сообщения в обработчике 1С.&lt;/em&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/163/194/5f8/1631945f8f36581241fc1898048145ea.png width=827 height=427 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/163/194/5f8/1631945f8f36581241fc1898048145ea.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/163/194/5f8/1631945f8f36581241fc1898048145ea.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Создаём внешний тип данных &lt;strong&gt;habr_dbo_nomenklature&lt;/strong&gt; и импортируем в него структуру таблицы&lt;strong&gt; “nomenklature” &lt;/strong&gt;из базы MS SQL. Используем алиас (название), т.к. name зарезервированное слово. Назначение: &lt;em&gt;Используется для получения Номенклатуры из Источника и обработки сообщения.&lt;/em&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/941/12c/c7a/94112cc7a34f70ff9c4ca29116eab599.png width=992 height=396 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/941/12c/c7a/94112cc7a34f70ff9c4ca29116eab599.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/941/12c/c7a/94112cc7a34f70ff9c4ca29116eab599.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Создаём тип данных &lt;strong&gt;habr_Номенклатура_Ответ_1С&lt;/strong&gt;, и заполняем его. Назначение&lt;em&gt;: Используется для заполнения структуры ответа и маршрутизации.&lt;/em&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/719/59d/92d/71959d92da54125522210ed35e4d3c27.png width=827 height=300 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/719/59d/92d/71959d92da54125522210ed35e4d3c27.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/719/59d/92d/71959d92da54125522210ed35e4d3c27.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;&lt;strong&gt;2. Настройка обработчиков&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Создаём обработчик &lt;strong&gt;habr_Номенклатура_Получение_MSSQL &lt;/strong&gt;с типом “Обработчик MSSQL” для получения записей из системы источника (базы MSSQL).&lt;ul&gt;&lt;li&gt;&lt;p&gt;На вкладке &lt;strong&gt;Основное &lt;/strong&gt;указываем направление “В Platform”.&lt;li&gt;&lt;p&gt;На вкладке &lt;strong&gt;Код &lt;/strong&gt;указываем запрос:&lt;pre&gt;&lt;code class=sql&gt;SELECT TOP 100 uuid, sku, &amp;#34;name&amp;#34;, units FROM nomenklature WHERE need_send = 1;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:87px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;li&gt;&lt;p&gt;На вкладке &lt;strong&gt;В Платформ&lt;/strong&gt; указываем тип данных &lt;strong&gt;habr_dbo_nomenklature&lt;/strong&gt; и указываем ему признак “Массив”, Операция - “Произвольный запрос”.&lt;/ul&gt;&lt;/ul&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/103/f90/5b5/103f905b519fbc2f6fe48a14afe96cf5.png width=930 height=451 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/103/f90/5b5/103f905b519fbc2f6fe48a14afe96cf5.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/103/f90/5b5/103f905b519fbc2f6fe48a14afe96cf5.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Создаём обработчик &lt;strong&gt;habr_Номенклатура_Обогощение_1С &lt;/strong&gt;с типом “Обработчик 1С” для записи полученной номенклатуры в базу 1С.&lt;ul&gt;&lt;li&gt;&lt;p&gt;На вкладке &lt;strong&gt;Основное &lt;/strong&gt;указываем направление “Из Platform”&lt;li&gt;&lt;p&gt;На вкладке &lt;strong&gt;Из Platform&lt;/strong&gt; выбираем тип данных &lt;strong&gt;habr_Номенклатура&lt;/strong&gt;&lt;li&gt;&lt;p&gt;На вкладке &lt;strong&gt;Код &lt;/strong&gt;указываем следующее:&lt;/ul&gt;&lt;/ul&gt;&lt;pre&gt;&lt;code class=1c&gt;НачатьТранзакцию();&#xA;Попытка&#xA;&#xA;    xdtoОбъект = сшпОбщегоНазначения.ПолучитьОбъектXDTO(Перечисления.сшпФорматыСообщений.JSON, ТелоСообщения());&#xA;    ИдентификаторMSSQL = Новый УникальныйИдентификатор(xdtoОбъект.UUID_MSSQL);&#xA;    &#xA;    Блокировка = Новый БлокировкаДанных;&#xA;    ЭлементБлокировки = Блокировка.Добавить(&amp;#34;РегистрСведений.сшпБлокировкаОчереди&amp;#34;);&#xA;    ЭлементБлокировки.УстановитьЗначение(&amp;#34;Идентификатор&amp;#34;, ИдентификаторMSSQL);&#xA;    &#xA;    Попытка&#xA;        Блокировка.Заблокировать();&#xA;    Исключение&#xA;        Пока ТранзакцияАктивна() Цикл&#xA;            ОтменитьТранзакцию();&#xA;        КонецЦикла;&#xA;        СостояниеСообщение = Перечисления.сшпСтатусыСообщений.ОжиданиеОбработки;&#xA;        Задержка = 30;&#xA;        Перейти ~Выход;&#xA;    КонецПопытки;&#xA;&#xA;    //Поиск объекта&#xA;    спрСсылка = Справочники.Номенклатура.НайтиПоРеквизиту(&amp;#34;UUID_MSSQL&amp;#34;, ИдентификаторMSSQL);&#xA;&#xA;&#xA;    Если спрСсылка = Неопределено ИЛИ спрСсылка.Пустая() Тогда&#xA;            спрОбъект = Справочники.Номенклатура.СоздатьЭлемент();&#xA;    Иначе&#xA;            спрОбъект = спрСсылка.ПолучитьОбъект();&#xA;    КонецЕсли;&#xA;    &#xA;    спрОбъект.Наименование = xdtoОбъект.Наименование;&#xA;    спрОбъект.Артикул = xdtoОбъект.Артикул;&#xA;    спрОбъект.ЕдиницыИзмерения = xdtoОбъект.ЕдиницыИзмерения;&#xA;    спрОбъект.UUID_MSSQL = ИдентификаторMSSQL;&#xA;    спрОбъект.ДополнительныеСвойства.Вставить(&amp;#34;СШПНеобрабатывать&amp;#34;, Истина);&#xA;    спрОбъект.Записать();&#xA;&#xA;    ОтветПоНоменклатуре = Новый Структура;&#xA;    ОтветПоНоменклатуре.Вставить(&amp;#34;UUID_Source&amp;#34;, ИдентификаторMSSQL);&#xA;    ОтветПоНоменклатуре.Вставить(&amp;#34;updated_at&amp;#34;, ТекущаяДатаСеанса());&#xA;    ОтветПоНоменклатуре.Вставить(&amp;#34;status&amp;#34;, &amp;#34;OK&amp;#34;);&#xA;    СтруктОтв = сшпОбщегоНазначения.ПреобразоватьСтруктуруПоФормату(Перечисления.сшпФорматыСообщений.JSON, ОтветПоНоменклатуре);&#xA;    сшпПользовательскиеМетоды.ПоместитьВОчередьИсходящих(&amp;#34;Номенклатура_ОбработкаОтвета&amp;#34;,СтруктОтв);&#xA;&#xA;&#xA;    Пока ТранзакцияАктивна() Цикл&#xA;        ЗафиксироватьТранзакцию();&#xA;    КонецЦикла;&#xA;&#xA;Исключение&#xA;    Пока ТранзакцияАктивна() Цикл&#xA;        ОтменитьТранзакцию();&#xA;    КонецЦикла;&#xA;&#xA;    ОтветПоНоменклатуре = Новый Структура;&#xA;    ОтветПоНоменклатуре.Вставить(&amp;#34;UUID_Source&amp;#34;, ИдентификаторMSSQL);&#xA;    ОтветПоНоменклатуре.Вставить(&amp;#34;updated_at&amp;#34;, ТекущаяДатаСеанса());&#xA;    ОтветПоНоменклатуре.Вставить(&amp;#34;status&amp;#34;, &amp;#34;Error. &amp;#34; + ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));&#xA;    СтруктОтв = сшпОбщегоНазначения.ПреобразоватьСтруктуруПоФормату(Перечисления.сшпФорматыСообщений.JSON, ОтветПоНоменклатуре);&#xA;    сшпПользовательскиеМетоды.ПоместитьВОчередьИсходящих(&amp;#34;Номенклатура_ОбработкаОтвета&amp;#34;,СтруктОтв);&#xA;&#xA;КонецПопытки;&#xA;~Выход:&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/21b/cf9/304/21bcf9304bc4ac6708d6e06e9dc039de.png width=939 height=384 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/21b/cf9/304/21bcf9304bc4ac6708d6e06e9dc039de.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/21b/cf9/304/21bcf9304bc4ac6708d6e06e9dc039de.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Создаём “прокси” обработчик &lt;strong&gt;habr_Номенклатура_ОбработкаОтвета_1C &lt;/strong&gt;с типом “Обработчик 1С”, который будет получать сформированный ответ из 1С&lt;ul&gt;&lt;li&gt;&lt;p&gt;На вкладке &lt;strong&gt;Основное &lt;/strong&gt;выбираем направление “В Platform” и указываем имя таблицы &lt;strong&gt;Номенклатура_ОбработкаОтвета&lt;/strong&gt;. С этим типом объекта мы поместили ответ в регистр сведений очередь исходящих на стороне 1С.&lt;li&gt;&lt;p&gt;На вкладке  &lt;strong&gt;В Platform&lt;/strong&gt; выбираем тип данных &lt;strong&gt;habr_Номенклатура_Ответ_1С&lt;/strong&gt;&lt;li&gt;&lt;p&gt;На вкладке &lt;strong&gt;Код &lt;/strong&gt;указываем следующее:&lt;/ul&gt;&lt;/ul&gt;&lt;pre&gt;&lt;code class=1c&gt;РезультатОбработки.Body = ОбъектОбработки;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/342/1ba/857/3421ba8579054f68559dee087d779452.png width=1096 height=503 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/342/1ba/857/3421ba8579054f68559dee087d779452.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/342/1ba/857/3421ba8579054f68559dee087d779452.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Создаём обработчик &lt;strong&gt;habr_Номенклатура_ОбновлениеСтатуса_MSSQL &lt;/strong&gt;с типом “Обработчик MSSQL”, который будет производить обновление статуса в базе MSSQL.&lt;ul&gt;&lt;li&gt;&lt;p&gt;На вкладке &lt;strong&gt;Основное &lt;/strong&gt;указываем направление “Из Платформ”&lt;li&gt;&lt;p&gt;На вкладке &lt;strong&gt;Из Platform&lt;/strong&gt; указываем тип данных &lt;strong&gt;habr_ОбработкаОтвета_1С&lt;/strong&gt;&lt;li&gt;&lt;p&gt;На вкладке &lt;strong&gt;Код &lt;/strong&gt;указываем запрос по обновлению записи. Подстановка переменных происходит из типа данных. Например @ИмяПоляВТипеДанных.&lt;/ul&gt;&lt;/ul&gt;&lt;pre&gt;&lt;code class=sql&gt;UPDATE dbo.nomenklature&#xA;SET&#xA;need_send = 0,&#xA;updated_at = @updated_at,&#xA;status = @status&#xA;WHERE&#xA;uuid = @UUID_Source;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/aca/226/016/aca226016e8a8c2e9c4af037692b3bea.png width=1025 height=445 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/aca/226/016/aca226016e8a8c2e9c4af037692b3bea.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/aca/226/016/aca226016e8a8c2e9c4af037692b3bea.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;&lt;strong&gt;3. Настройка бизнес процессов&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Создаём бизнес-процесс &lt;strong&gt;habr_Номенклатура_Обновление_БП, &lt;/strong&gt;который производит получение и обновление номенклатуры В 1С.&lt;ul&gt;&lt;li&gt;&lt;p&gt;На вкладке &lt;strong&gt;Основное &lt;/strong&gt;снимаем галочку “Запускать автоматически”&lt;li&gt;&lt;p&gt;На вкладке &lt;strong&gt;Переменные &lt;/strong&gt;устанавливаем следующее:&lt;ul&gt;&lt;li&gt;&lt;p&gt;Получаемый &lt;strong&gt;массив Номенклатуры&lt;/strong&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Итератор &lt;/strong&gt;используется для обхода элементов массива&lt;li&gt;&lt;p&gt;&lt;strong&gt;Общее количество элементов&lt;/strong&gt; - определяет количество элементов в массиве и вычисляется единожды.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Номенклатура 1С&lt;/strong&gt; - тип данных отправляемый в 1С.&lt;/ul&gt;&lt;/ul&gt;&lt;/ul&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/c2a/4b0/213/c2a4b0213a356c6554d42b09400f402b.png width=1063 height=431 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/c2a/4b0/213/c2a4b0213a356c6554d42b09400f402b.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/c2a/4b0/213/c2a4b0213a356c6554d42b09400f402b.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;&lt;strong&gt;Подробное описание настроек в бизнес-процессе:&lt;/strong&gt;&lt;p&gt;Процесс будет обходит элементы массива, формируя сообщения для отправки в 1С.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/d59/0a1/f65/d590a1f65d1194756a02a5f808bf3532.png width=744 height=679 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/d59/0a1/f65/d590a1f65d1194756a02a5f808bf3532.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/d59/0a1/f65/d590a1f65d1194756a02a5f808bf3532.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;1.Выбираем шаг “Источник” и заполняем его, для получения Номенклатуры из MSSQL.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/813/46d/f37/81346df371762a30f18cd90057472c5c.png width=1035 height=362 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/813/46d/f37/81346df371762a30f18cd90057472c5c.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/813/46d/f37/81346df371762a30f18cd90057472c5c.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;2. Устанавливаем шаг “Алгоритм” в нём указываем следующее:&lt;pre&gt;&lt;code class=cs&gt;ОбщееКоличествоЭлем = ПолученнаяНоменклатура_MSSQL.Count;&#xA;Logger.Debug($&amp;#34;ОбщееКоличество записей к обработке: {ОбщееКоличествоЭлем}&amp;#34;);&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/647/a67/698/647a67698f7ec8627b832133e72e109e.png width=963 height=310 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/647/a67/698/647a67698f7ec8627b832133e72e109e.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/647/a67/698/647a67698f7ec8627b832133e72e109e.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;3. Устанавливаем шаг “Условие”, ведём от (+) к шагу маппинг. На соединительной линии указываем условие:&lt;p&gt;&lt;em&gt;Итератор &amp;lt; ОбщееКоличествоЭлем&lt;/em&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/841/241/a1d/841241a1d7a3c9f722d64ad921bd6f7f.png width=632 height=283 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/841/241/a1d/841241a1d7a3c9f722d64ad921bd6f7f.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/841/241/a1d/841241a1d7a3c9f722d64ad921bd6f7f.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;4. В шаге “Алгоритм” производим маппинг Элемента массива с типом данных для обработки в 1С. &lt;em&gt;(Можно воспользоваться шагом установка переменных, кому как удобнее).&lt;/em&gt;&lt;pre&gt;&lt;code class=cs&gt;Номенклатура_1C.Наименование = ПолученнаяНоменклатура_MSSQL[Итератор].название;&#xA;Номенклатура_1C.Артикул = (int)ПолученнаяНоменклатура_MSSQL[Итератор].sku;&#xA;Номенклатура_1C.ЕдиницыИзмерения = ПолученнаяНоменклатура_MSSQL[Итератор].units;&#xA;Номенклатура_1C.UUID_MSSQL = ПолученнаяНоменклатура_MSSQL[Итератор].uuid;&#xA;&#xA;Logger.Debug($&amp;#34;ЭлементНоменклатуры = {Номенклатура_1C}&amp;#34;);&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/606/3d3/b13/6063d3b13e4d01749d236f64d7dacd4f.png width=1036 height=366 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/606/3d3/b13/6063d3b13e4d01749d236f64d7dacd4f.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/606/3d3/b13/6063d3b13e4d01749d236f64d7dacd4f.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;5. Выбираем шаг “Отправка получателю” и заполняем его&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/092/fbf/90c/092fbf90cb6a647556190031649dbfa8.png width=1101 height=521 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/092/fbf/90c/092fbf90cb6a647556190031649dbfa8.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/092/fbf/90c/092fbf90cb6a647556190031649dbfa8.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;6. Выбираем шаг “Алгоритм” и к переменной &lt;strong&gt;Итератор &lt;/strong&gt;добавляем единицу.&lt;pre&gt;&lt;code class=cs&gt;Итератор++;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/e91/9e9/f28/e919e9f283657dd33196b663c76f0fdb.png width=766 height=424 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/e91/9e9/f28/e919e9f283657dd33196b663c76f0fdb.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/e91/9e9/f28/e919e9f283657dd33196b663c76f0fdb.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;&lt;br&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Создаём бизнес-процесс &lt;strong&gt;habr_Номенклатура_ОбработкаОтвета_1С_БП &lt;/strong&gt;, который производит запись ответа в MSSQL. &lt;ul&gt;&lt;li&gt;&lt;p&gt;На вкладке &lt;strong&gt;Основное &lt;/strong&gt;указываем “Запускать автоматически” &lt;li&gt;&lt;p&gt;На вкладке &lt;strong&gt;Переменные&lt;/strong&gt; в переменной обработки указываем тип данных ответа &lt;strong&gt;habr_Номенклатура_Ответ_1С &lt;/strong&gt;(синоним habr_ОбработкаОтвета_1С)&lt;strong&gt;.&lt;/strong&gt;&lt;li&gt;&lt;p&gt;Заполняем получателя и выбираем обработчик по обновлению статуса.&lt;/ul&gt;&lt;/ul&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/908/6ef/cfd/9086efcfda80a80acf2e5e21b616741c.png width=933 height=524 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/908/6ef/cfd/9086efcfda80a80acf2e5e21b616741c.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/908/6ef/cfd/9086efcfda80a80acf2e5e21b616741c.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;&lt;strong&gt;4. Создание задания по расписанию (триггера)&lt;/strong&gt;&lt;p&gt;Создаём триггер, устанавливаем ему время запуска и указываем схему обработки &lt;strong&gt;habr_Номенклатура_Обновление_БП&lt;/strong&gt;, которую необходимо периодически запускать. &lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/009/560/e17/009560e175c8236b91856e89cd7ee9a6.png width=895 height=227 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/009/560/e17/009560e175c8236b91856e89cd7ee9a6.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/009/560/e17/009560e175c8236b91856e89cd7ee9a6.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;&lt;strong&gt;Результат работы:&lt;/strong&gt;&lt;p&gt;Устанавливаем статус &lt;em&gt;need_send = 1&lt;/em&gt; в таблице Номенклатура в MSSQL.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/23f/9e4/d3b/23f9e4d3b65d18f17f3661b0cbfef925.png width=616 height=112 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/23f/9e4/d3b/23f9e4d3b65d18f17f3661b0cbfef925.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/23f/9e4/d3b/23f9e4d3b65d18f17f3661b0cbfef925.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;В случае&lt;u&gt; &lt;/u&gt;&lt;em&gt;&lt;u&gt;корректной обработки&lt;/u&gt;&lt;/em&gt; в базе MSSQL видим статус ОК.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/685/fdb/d4e/685fdbd4eb7c191c62d534c23a3c678e.png width=685 height=108 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/685/fdb/d4e/685fdbd4eb7c191c62d534c23a3c678e.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/685/fdb/d4e/685fdbd4eb7c191c62d534c23a3c678e.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Записи из MSSQL добавлены в справочник на стороне 1С:&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/18b/ea7/52a/18bea752ae1df7d95003edb68d5914c8.png width=652 height=457 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/18b/ea7/52a/18bea752ae1df7d95003edb68d5914c8.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/18b/ea7/52a/18bea752ae1df7d95003edb68d5914c8.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;В случае &lt;em&gt;&lt;u&gt;ошибки &lt;/u&gt;&lt;/em&gt;атрибут need_send становится равным нулю, повторная обработка ошибочных сообщений не происходит. В поле status описана ошибка, например: &lt;em&gt;Деление на 0&lt;/em&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/fbd/9ad/d89/fbd9add896e7d097d57c370b3fb6223c.png width=1350 height=116 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/fbd/9ad/d89/fbd9add896e7d097d57c370b3fb6223c.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/fbd/9ad/d89/fbd9add896e7d097d57c370b3fb6223c.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;В этом кейсе рассмотрена простая настройка для одного потока, если планируется обновление большого количества объектов, то можно подумать в сторону:&lt;ul&gt;&lt;li&gt;&lt;p&gt;Формирования универсальной структуры ответа&lt;li&gt;&lt;p&gt;Единого формата обработчиков по обновлению Статуса в СУБД&lt;li&gt;&lt;p&gt;Универсальных бизнес-процессов по обработке данных&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Используемое ПО&lt;/strong&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Microsoft SQL Server 2019 (MS SQL)&lt;li&gt;&lt;p&gt;DATAREON Platform 3.2.1.1&lt;li&gt;&lt;p&gt;Информационная база 1С (Платформа 1С 8.3.23.2157), в режиме совместимости 8.3.22&lt;/ul&gt;&lt;p&gt;Файл скрипта базы MSSQL и выгрузка конфигурации Platform, доступны по &lt;a href=https://disk.yandex.ru/d/8NY6gneOp2xzxQ&gt;ссылке&lt;/a&gt;.&lt;p&gt;&lt;strong&gt;Документация&lt;/strong&gt;&lt;ol&gt;&lt;li&gt;&lt;p&gt;&lt;a href=https://docs-platform.datareon.ru/terms_rules_platform/reserved_names.html#alias&gt;Зарезервированные имена — документация Datareon Platform&lt;/a&gt; - использование зарезервированных системных слов используя алиасы.&lt;li&gt;&lt;p&gt;&lt;a href=https://docs-platform.datareon.ru/developer/handlers/dev_1c_handlers.html#put-in-outgoing-queue&gt;Обработчики 1С — документация Datareon Platform&lt;/a&gt; - помещение в очередь исходящих.&lt;li&gt;&lt;p&gt;&lt;a href=https://docs-platform.datareon.ru/developer/handlers/dev_dbms_handlers.html&gt;Обработчики СУБД — документация Datareon Platform&lt;/a&gt; - работа с переменными в обработчиках с типом СУБД&lt;/ol&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <guid>https://habr.com/ru/companies/sofros/articles/1082122/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082122</guid>
      <pubDate>Mon, 14 Sep 2026 14:04:06 +0000</pubDate>
    </item>
    <item>
      <title>SKILL.state в coding-агентах: меньше истории, но не всегда меньше работы</title>
      <link>https://habr.com/ru/articles/1082120/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082120</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;h3&gt;О чём этот текст&lt;/h3&gt;&lt;p&gt;Это рассказ о моём эксперименте: что захотелось попробовать, что получилось и где пришлось пересмотреть свои ожидания. Я не ставлю здесь задачу кого-то обучить или доказать, как правильно устраивать память агентов. Просто захотелось поделиться этим опытом — вместе с результатами, ошибками и вопросами, на которые у меня пока нет ответа.&lt;h3&gt;С чего всё началось&lt;/h3&gt;&lt;p&gt;Недавно мне попалась статья &lt;a href=https://arxiv.org/abs/2608.26263 rel=&#34;noopener nofollow&#34;&gt;SKILL.state: Scalable Long-Horizon Agent Skills&lt;/a&gt;, и её идея меня зацепила: что, если агенту не нужна вся история работы и размышлений, чтобы выбрать следующее действие? Вместо неё можно хранить явное состояние выполнения и собирать очередной запрос вокруг него.&lt;p&gt;Раньше я работал с похожей архитектурой: смесью машины состояний и LLM workflow, где отдельные узлы — полноценные агенты со своим циклом работы, agent loop. Но каждый из них решал довольно узкий класс задач. Перенести тот же принцип на агента, который самостоятельно пишет код, мне тогда не приходило в голову. Поэтому захотелось попробовать идею «во плоти».&lt;p&gt;Итак, я решил перенести общую идею SKILL.state на задачи самостоятельной разработки программных проектов. В качестве основы взял два опенсорсных coding-агента — OpenCode и Codex: хотелось посмотреть, насколько результат зависит от устройства самого агента. Мне было интересно, как такая память поведёт себя внутри привычного рабочего цикла: агент получает спецификацию, создаёт файлы, запускает проверки, исправляет ошибки и решает, когда работа закончена. Сразу оговорюсь: это не повторение авторских экспериментов, а попытка применить их концепцию в другом окружении.&lt;p&gt;Сначала я адаптировал протокол и перенёс его в ядра обоих агентов, чтобы следующий запрос собирался из состояния, а не из накопленной истории. Затем, посмотрев на первые запуски, решил немного доработать подход с опорой на собственный опыт: добавить память о недавних действиях, а позже — возможность выполнять несколько действий между обращениями к модели. Так появились варианты, которые дальше я называю V2 и V3.&lt;p&gt;По дороге пришлось разбираться и с поведением моделей, и с ошибками собственной реализации, а некоторые поначалу обнадёживающие результаты на повторных запусках уже не выглядели так убедительно. Об этом и будет рассказ — пока об инженерном опыте с открытым кодом и журналами, из которого хочется вырастить исследовательскую работу.&lt;h3&gt;Что предложили авторы оригинальной статьи&lt;/h3&gt;&lt;p&gt;SKILL.state предлагает организовать работу агента так, чтобы контекст не разрастался с каждым шагом. Авторы рассматривают длительные задачи, в которых агент рассуждает, вызывает инструменты и взаимодействует со средой — проще говоря, делает работу. Разработка ПО упоминается как одна из областей применения, но сама архитектура не привязана к coding-агентам. &lt;a href=https://arxiv.org/html/2608.26263v2#S1 rel=&#34;noopener nofollow&#34;&gt;Постановка задачи в статье&lt;/a&gt;.&lt;p&gt;В привычном агентном цикле к исходной задаче постепенно добавляются рассуждения, команды, ответы инструментов, затем новые рассуждения. Если история накапливается без сжатия, а шаги добавляют примерно одинаковый объём, контекст растёт линейно. Но отправляем мы его заново на каждом шаге, поэтому суммарный расход входных токенов растёт уже квадратично. SKILL.state предлагает вместо всей этой истории собирать запрос из трёх частей:&lt;pre&gt;&lt;code&gt;P неизменная постановка задачи&#xA;Σₙ текущее структурированное состояние&#xA;Oₙ последнее наблюдение среды&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:87px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;В ответ модель должна вернуть изменение состояния — patch — и следующее действие. Исполняющая часть агента, то есть runtime, проверяет и применяет этот patch, выполняет действие и возвращает наблюдение среды. На следующем шаге предыдущие рассуждения и старые ответы инструментов в запрос уже не попадают: если что-то из них пригодится позже, модель должна сама сохранить это в состоянии.&lt;p&gt;Мне нравится именно такое разделение ответственности. Программа следит за структурой состояния и исполняет команды, а модель решает, что стоит помнить. От её выбора зависит, с какой информацией агент продолжит работу и что ему придётся запрашивать заново.&lt;p&gt;Правда, у этой экономии есть условие: всё необходимое для продолжения работы должно помещаться в ограниченное состояние и наблюдение. Только тогда объём входа на шаг перестаёт расти вместе с историей. Если важная деталь потеряется, агенту придётся восстанавливать её, а это дополнительные шаги. При написании кода их может набраться столько, что короткие запросы уже не окупятся. Именно с этим я столкнулся в первых опытах.&lt;p&gt;Экспериментальная часть оригинальной статьи охватывает несколько разных сред. В ней есть синтетический склад, симуляция репозитория с ветками, PR и статусами CI, терминальные задачи InterCode CTF и обслуживание клиентов в τ-Bench. Симуляция репозитория проверяет управление связанным состоянием, а CTF — поиск флагов через команды и проверку гипотез. Это смежные с разработкой сценарии, но всё-таки не то же самое, что самостоятельное создание приложения по спецификации. &lt;a href=https://arxiv.org/html/2608.26263v2#S4 rel=&#34;noopener nofollow&#34;&gt;Описание бенчмарков&lt;/a&gt;.&lt;p&gt;У авторов в синтетическом эксперименте с управлением складом на горизонте 200 шагов получилось примерно 122 тысячи токенов у SKILL.state против 2,61 млн у ReAct (&lt;a href=https://arxiv.org/html/2608.26263v2 rel=&#34;noopener nofollow&#34;&gt;таблица 1&lt;/a&gt;). Разница впечатляет, но переносить её на агента, который пишет код, заранее было бы слишком смело. Захотелось посмотреть, сколько от этой экономии останется на наших задачах и сможет ли агент с такой памятью довести разработку до конца.&lt;h3&gt;Как я проверял идею&lt;/h3&gt;&lt;p&gt;Начал я с плагина для OpenCode, но довольно быстро упёрся в ограничение: плагин требовал от модели дополнительных действий для работы с памятью и не мог гарантировать, что на каждом шаге она выдаст patch вместе с действием. Получался другой эксперимент, поэтому механизм пришлось перенести в ядро.&lt;p&gt;В ядровой реализации задача и состояние сразу включены в запрос: модели не нужно отдельно читать файл памяти. История при этом никуда не исчезает из журналов — она нужна для разбора запусков, — но обратно модели в state-режимах не отправляется. Рабочая папка тоже сохраняется, так что созданные файлы агент может перечитать обычными инструментами. Если интересно посмотреть код, начать можно со &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/opencode/packages/opencode/src/session/skill-state.ts rel=&#34;noopener nofollow&#34;&gt;сборщика состояния OpenCode&lt;/a&gt; или &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/codex/codex-rs/core/src/skill_state.rs rel=&#34;noopener nofollow&#34;&gt;ядра состояния Codex&lt;/a&gt;.&lt;p&gt;Ранние пробы, в том числе с GLM-5.2 и Luna, остались &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/journals/README.md rel=&#34;noopener nofollow&#34;&gt;в общем журнале&lt;/a&gt;. Для статьи я начал с отдельного сравнения на OpenCode и Codex, а затем продолжил эксперименты на Codex: добавил повторы, убрал глобальные скиллы и проверил расширенные лимиты памяти. В итоге получилось несколько серий. Ниже сначала опишу, что у них общего, а затем — чем они различаются.&lt;p&gt;Для эксперимента я взял пять небольших проектов:&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/skill-state/projects/taskboard-cli/SPEC.md rel=&#34;noopener nofollow&#34;&gt;CLI-менеджер задач&lt;/a&gt; с сохранением данных в JSON;&lt;li&gt;&lt;p&gt;&lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/skill-state/projects/csv-insights/SPEC.md rel=&#34;noopener nofollow&#34;&gt;утилиту анализа CSV&lt;/a&gt;;&lt;li&gt;&lt;p&gt;&lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/skill-state/projects/mini-template/SPEC.md rel=&#34;noopener nofollow&#34;&gt;небольшой шаблонизатор&lt;/a&gt;;&lt;li&gt;&lt;p&gt;&lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/skill-state/projects/http-kv/SPEC.md rel=&#34;noopener nofollow&#34;&gt;HTTP key-value сервис&lt;/a&gt;;&lt;li&gt;&lt;p&gt;&lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/skill-state/projects/dependency-planner/SPEC.md rel=&#34;noopener nofollow&#34;&gt;планировщик зависимостей&lt;/a&gt;.&lt;/ul&gt;&lt;p&gt;На каждый запуск агент получает полную спецификацию и пустую рабочую папку, после чего работает самостоятельно. Получившийся проект проверяет внешний &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/skill-state/scripts/evaluate.ts rel=&#34;noopener nofollow&#34;&gt;оценщик&lt;/a&gt;. Его код не добавлялся в контекст и лежал за пределами рабочей папки агента — хотя это само по себе ещё не запрещает прочитать файл. В одном из ранних запусков я даже поймал модель на попытке подглядеть в оценщик. После уточнения рабочей директории такое больше не повторялось, но полноценной изоляцией эту меру, конечно, не назовёшь.&lt;p&gt;В сумме получается 40 проверок на пять проектов. Это важно не путать с 40 независимыми задачами: например, одна ошибка сохранения базы может потянуть за собой несколько проваленных проверок. Полный балл означает, что проект прошёл наши тесты. Ошибки за их пределами вполне могли остаться. Да и качество кода в широком смысле мы здесь не оцениваем: проверяем, делает ли программа то, что от неё требуется.&lt;p&gt;В основных сериях сравниваются четыре режима; в конце к ним добавился ещё один контроль — Paper2. V2/V3 и Paper2 — рабочие названия моих вариантов, они не имеют отношения к названиям версий оригинальной статьи:&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;&lt;p align=left&gt;Режим&lt;th&gt;&lt;p align=left&gt;Что модель видит из прошлой работы&lt;th&gt;&lt;p align=left&gt;Что предлагает за обращение&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Native&lt;td&gt;&lt;p align=left&gt;Штатную историю агента&lt;td&gt;&lt;p align=left&gt;Штатные tool calls / Code Mode&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Paper&lt;td&gt;&lt;p align=left&gt;Текущее Σ и последний результат O&lt;td&gt;&lt;p align=left&gt;Patch и одно действие&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Paper2&lt;td&gt;&lt;p align=left&gt;Текущее Σ и одно O: действие, его аргументы, статус и результат&lt;td&gt;&lt;p align=left&gt;Patch и одно действие, как у Paper&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;V2&lt;td&gt;&lt;p align=left&gt;Текущее Σ и три структурированных наблюдения&lt;td&gt;&lt;p align=left&gt;Patch, необязательный comment и одно действие&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;V3&lt;td&gt;&lt;p align=left&gt;Текущее Σ и три наблюдения-батча&lt;td&gt;&lt;p align=left&gt;Patch, необязательный comment и последовательный массив действий&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;h3&gt;Детальное описание проведенных экспериментов&lt;/h3&gt;&lt;details class=spoiler&gt;&lt;summary&gt;Подробнее об экспериментах и условиях запуска&lt;/summary&gt;&lt;div class=spoiler__content&gt;&lt;p&gt;Вот полный состав экспериментов, результаты которых вошли в статью. В каждой серии использовались все пять проектов; везде сравнивались четыре режима, кроме первого дополнительного круга V2/V3 и двух последних серий Paper2.&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;&lt;p align=left&gt;Серия&lt;th&gt;&lt;p align=left&gt;Агент и модели&lt;th&gt;&lt;p align=left&gt;Попыток на проект и режим&lt;th&gt;&lt;p align=left&gt;Запусков&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Первое сравнение&lt;td&gt;&lt;p align=left&gt;OpenCode и Codex, Sol и Terra&lt;td&gt;&lt;p align=left&gt;1&lt;td&gt;&lt;p align=left&gt;80&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Первые дополнительные повторы V2/V3&lt;td&gt;&lt;p align=left&gt;Codex, Sol и Terra&lt;td&gt;&lt;p align=left&gt;Ещё 2, только V2 и V3&lt;td&gt;&lt;p align=left&gt;40&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Расширенная серия повторов&lt;td&gt;&lt;p align=left&gt;Codex, Sol&lt;td&gt;&lt;p align=left&gt;10&lt;td&gt;&lt;p align=left&gt;200&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Без глобальных скиллов, прежние лимиты&lt;td&gt;&lt;p align=left&gt;Codex, Sol&lt;td&gt;&lt;p align=left&gt;5&lt;td&gt;&lt;p align=left&gt;100&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Без глобальных скиллов, прежние лимиты&lt;td&gt;&lt;p align=left&gt;Codex, Astra&lt;td&gt;&lt;p align=left&gt;5&lt;td&gt;&lt;p align=left&gt;100&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Без глобальных скиллов, лимиты до 2 МиБ&lt;td&gt;&lt;p align=left&gt;Codex, Astra&lt;td&gt;&lt;p align=left&gt;5&lt;td&gt;&lt;p align=left&gt;100&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Без глобальных скиллов, лимиты до 2 МиБ&lt;td&gt;&lt;p align=left&gt;Codex, Sol&lt;td&gt;&lt;p align=left&gt;5&lt;td&gt;&lt;p align=left&gt;100&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Paper2, без глобальных скиллов, лимиты до 2 МиБ&lt;td&gt;&lt;p align=left&gt;Codex, Sol и Astra&lt;td&gt;&lt;p align=left&gt;5, только Paper2&lt;td&gt;&lt;p align=left&gt;50&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Paper2, без глобальных скиллов, малые лимиты и защита JSON&lt;td&gt;&lt;p align=left&gt;Codex, Sol и Astra&lt;td&gt;&lt;p align=left&gt;5, только Paper2&lt;td&gt;&lt;p align=left&gt;50&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;Итого&lt;/strong&gt;&lt;td&gt;&lt;p align=left&gt;&lt;td&gt;&lt;p align=left&gt;&lt;td&gt;&lt;p align=left&gt;&lt;strong&gt;820&lt;/strong&gt;&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Каждая попытка решить один проект — отдельный запуск; всего в статье их 820. Например, пять повторов пяти проектов в четырёх режимах дают 100 запусков. Первые две строки таблицы — основная серия из 120 запусков: для Codex V2/V3 в ней по три попытки с учётом первого сравнения, для остальных сочетаний — по одной. Последующие серии добавляют ещё 700 запусков. Во всех них задачи остаются теми же: повторов много, но разных спецификаций по-прежнему пять. Отдельно учтены пять технически прерванных попыток первого запуска Paper2: они не входят ни в эти 820 итоговых исходов, ни в показанный расход. Ниже поясню, почему серию пришлось возобновить.&lt;p&gt;Исходники и настройки фиксировались перед сериями. Результаты дальше показаны по отдельности, чтобы было видно, при каких условиях они получены; общей средней по всем версиям здесь нет.&lt;p&gt;Режим Paper я задумывал как реализацию архитектуры статьи, но при встраивании в ядра агентов пришлось отступить от некоторых деталей. Поэтому буквальной копией авторского окружения он не стал. Чтобы сохранить штатные инструменты агентов, вместо JSON-блока с командой-строкой здесь используется вызов &lt;code&gt;skill_step&lt;/code&gt; с типизированным действием. Схема состояния тоже своя, общая для всех пяти задач: например, в авторской CTF-схеме есть отдельное поле &lt;code&gt;cmd_summary&lt;/code&gt;, которого у нас нет. В первых сериях на состояние отведено 32 KiB, на отдельный результат — 4 KiB. Позже я проверил вариант с расширенными лимитами — до него ещё дойдём, — но влияние каждого ограничения и самой схемы по отдельности пока не проверял. Это существенная оговорка: не каждую проблему нашей адаптации можно приписать самой идее SKILL.state. &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/PAPER-CONFORMANCE.md rel=&#34;noopener nofollow&#34;&gt;Первый аудит соответствия&lt;/a&gt; описывает реализацию и условия основной серии из 120 запусков. Я сохранил его как исторический документ, а поздние изменения лимитов, изоляции и сохранения состояния разобрал в &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/PAPER-CONFORMANCE-20260910.md rel=&#34;noopener nofollow&#34;&gt;дополнении к аудиту&lt;/a&gt;.&lt;p&gt;В первом сравнении и дополнительных повторах V2/V3 участвовали Sol и Terra; дальше — Sol и Astra. Во всех сериях на одну попытку решения проекта отводилось 15 минут. Внутри каждого сравнения модель и общие ресурсные ограничения оставались неизменными: у Codex был выбран medium reasoning, у OpenCode — temperature 0 и предел 80 шагов. Число обращений восстановлено по завершённым шагам OpenCode и записям расхода токенов Codex; отдельные сетевые повторы в этот счётчик не входят. При таких различиях настроек я сравниваю варианты внутри каждого агента, а не абсолютный расход OpenCode с расходом Codex. В последующих сериях Codex сохранились medium reasoning и окно k=3 у V2/V3; Paper по-прежнему получал только последнее наблюдение. Выводов о переносимости на другие семейства моделей эти опыты не дают.&lt;p&gt;Различается и сама интеграция в ядро. Поэтому дальше я сравниваю рабочие конфигурации целиком, а не только формулы памяти при полностью одинаковом окружении.&lt;p&gt;Первые серии шли на уже настроенной рабочей машине. Несмотря на отключение пользовательской конфигурации CLI, в начальном контексте Codex остались заголовки глобальных скиллов и инструкции MCP. Честно говоря, я просто забыл про них. Использовать их в этих задачах не собирался, но из этого ещё не следует, что они никак не влияли на модель. В опубликованных журналах этот контекст заменён метками с указанием размера удалённой части.&lt;p&gt;В тех запусках с состоянием дополнительные инструкции попадали в P с текстовыми метками ролей, а Native сохранял отдельные роли сообщений. Для проверки сохранены &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/article-20260904/host-context-audit.json rel=&#34;noopener nofollow&#34;&gt;отпечатки стартового контекста&lt;/a&gt;.&lt;p&gt;Чтобы не оставлять эту оговорку просто оговоркой, для чистоты эксперимента я сделал ещё одну серию на Sol: временно убрал глобальные скиллы и инструкции, а после запусков вернул их на место. За время экспериментирования успела появиться Astra, поэтому бонусом прогнал те же задачи и на ней. Это две серии по 100 запусков из таблицы выше. Бинарник Codex, спецификации и оценщик сохранились; одновременно работало до пяти основных CLI-процессов. Во всех 200 сессиях проверка начальных сообщений уже не нашла маркеров каталога скиллов и глобального профиля. Это ещё не полная изоляция на уровне ОС, но конкретную примесь из прошлых запусков мы убрали.&lt;p&gt;У Astra случился технический перерыв после 35 запусков: защитная проверка остановила серию из-за повторного появления служебного кэша, загрузка которого была отключена. Разобравшись с загрузчиком, я поправил проверку и запустил оставшиеся 65 задач, не заменяя и не повторяя уже выполненные. &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/codex-clean-comparison-20260908/README.md rel=&#34;noopener nofollow&#34;&gt;Условия обеих серий и поправка к запуску&lt;/a&gt; сохранены отдельно. Новые результаты ниже тоже идут отдельными блоками: сравнивать их со старой Sol и приписывать всю разницу удалению скиллов было бы некорректно — менялись время, нагрузка и, конечно, сам ход решения при каждом запуске.&lt;p&gt;Следующие две серии — ещё по 100 запусков Astra и Sol — проверяют расширенные лимиты SKILL.state. Я поднял четыре лимита до 2 МиБ каждый: на полное состояние, принимаемые аргументы действия, их сохранённую копию и результат. Заодно исправил повторную обрезку JSON при сохранении истории. Обе модели использовали одну и ту же новую сборку; временное удаление скиллов, остальные настройки и параллельность сохранились. Native в этих сериях тоже запускался заново, чтобы у каждой было своё сравнение со штатным агентом. Подробности изменений и результаты разберу ниже.&lt;p&gt;Наконец, при повторном аудите возник вопрос к составу самого наблюдения O. Так появился Paper2: по 50 запусков с большими и малыми лимитами. В малой конфигурации я сохранил исправление JSON, поэтому это уже не возврат к старой сборке со всеми её особенностями. Эти серии сравниваю прежде всего между собой, а с Native и Paper — по ранее полученным результатам, без новых одновременных контролей.&lt;/div&gt;&lt;/details&gt;&lt;h3&gt;Проверка реализации перед сравнением&lt;/h3&gt;&lt;details class=spoiler&gt;&lt;summary&gt;Подробнее о найденных ошибках и исправлениях&lt;/summary&gt;&lt;div class=spoiler__content&gt;&lt;p&gt;Прежде чем переходить к графикам, расскажу, что пришлось поправить в собственном эксперименте. Сравнительные серии ниже начаты после исправлений исполнения, перечисленных в этом разделе, а расход токенов везде приведён с исправленным учётом. Это не значит, что к тому моменту я нашёл вообще все проблемы: повторная обрезка JSON обнаружилась уже позже, на опытах с Astra. Ниже отдельно отмечено, в каких сериях её исправили. Ранние диагностические запуски помогли найти ошибки, но в сравнительные таблицы не вошли.&lt;p&gt;Оказалось, что мало собрать запрос по статье. Нужно ещё убедиться, что агент получает правильные ответы инструментов, останавливается там, где должен, и вообще следует тому протоколу, который мы собираемся исследовать. На каждом из этих уровней нашлись ошибки.&lt;h4&gt;Завершение без обязательного finish&lt;/h4&gt;&lt;p&gt;В моей реализации Codex мог закончить работу обычным текстовым ответом, так и не вызвав обязательное действие &lt;code&gt;finish&lt;/code&gt;. Это проявилось, например, в раннем запуске CLI-проекта на Sol/V2: код прошёл 8/8 проверок, но предусмотренного протоколом завершения в журнале не было. Баллы за работающий код у него заслуженные, а вот считать весь запуск полностью успешным нельзя: мы не знаем, сколько ещё обращений понадобилось бы модели, если бы ядро потребовало завершить работу правильно.&lt;p&gt;Я изменил условие выхода из цикла: теперь обычного текстового ответа недостаточно, и работа продолжается до принятого &lt;code&gt;finish&lt;/code&gt; либо прерывания. Поэтому в сравнительных сериях качество кода, завершение процесса и вызов &lt;code&gt;finish&lt;/code&gt; учитываются отдельно. Если агент написал работающий проект, но затем упёрся в таймаут, баллы за код у него остаются, а успешного завершения — нет.&lt;h4&gt;Успешный вызов инструмента — не обязательно успешная команда&lt;/h4&gt;&lt;p&gt;В V3, о которой подробнее расскажу ниже, модель может предложить сразу несколько действий — батч. По замыслу ошибка одного действия должна останавливать оставшиеся, но в обоих ядрах обнаружилась путаница между успехом вызова инструмента и успехом самой команды. Например, инструмент запуска тестов может отработать совершенно штатно и вернуть ненулевой код завершения: результат получен, но тесты-то упали. Если смотреть только на статус инструмента, следующие действия выполнятся, хотя должны были быть пропущены.&lt;p&gt;После исправления учитывается и код завершения команды. При ошибке оставшиеся действия помечаются как &lt;code&gt;skipped&lt;/code&gt;, так что модель видит место остановки и может решить, что делать дальше. Уже сделанные изменения и принятый patch при этом не откатываются. Нарушение правил исполнения я исправил, хотя заметного влияния именно этой ошибки на результаты не установил.&lt;h4&gt;Потерянное подтверждение apply_patch&lt;/h4&gt;&lt;p&gt;Ещё одна проблема обнаружилась уже при первом диагностическом запуске Paper на Codex. Адаптер брал у &lt;code&gt;apply_patch&lt;/code&gt; возврат для Code Mode — пустой объект &lt;code&gt;{}&lt;/code&gt; — вместо текстового подтверждения изменения файлов. То есть файлы могли уже измениться, а модель не получала сообщения об этом. В журнале были видны повторные попытки создать те же файлы, но делать из этого выводы о SKILL.state было бы странно: обратную связь с инструментом терял наш собственный код.&lt;p&gt;Я поправил выбор результата: для инструментов, не относящихся к оболочке, теперь сохраняется текстовый вывод, а для команд оболочки — структурированные сведения о коде завершения и сессии процесса. Чтобы ошибка не вернулась, добавил регрессионный тест на реальных типах результатов инструментов. Эту диагностическую серию пришлось исключить целиком и начать сравнение Codex заново, включая Native. Исходный журнал остался для разбора ошибки, но даже удачные запуски из него в новую серию не попали.&lt;h4&gt;Схема разрешала patch, который валидатор отвергал&lt;/h4&gt;&lt;p&gt;В Paper была и менее заметная несогласованность: JSON Schema разрешала модели передавать &lt;code&gt;null&lt;/code&gt; для обязательных полей верхнего уровня. При применении patch это означало удаление поля, и уже проверка полного состояния отвергала результат. Получалось, что модель могла добросовестно следовать предложенному формату и всё равно получать ошибку, а затем тратить обращения на исправление patch вместо самой задачи.&lt;p&gt;Я согласовал схему ответа и инструкции с тем, что принимает ядро. Обязательные контейнеры теперь нужно сохранять, но их содержимое по-прежнему можно очищать: массив заменить на &lt;code&gt;[]&lt;/code&gt;, а отдельную запись словаря &lt;code&gt;files&lt;/code&gt; удалить через &lt;code&gt;null&lt;/code&gt;. Сам принцип не изменился — модель предлагает patch, а runtime проверяет получившееся состояние до выполнения действия.&lt;h4&gt;Неполный учёт входных токенов OpenCode&lt;/h4&gt;&lt;p&gt;Наконец, пришлось разобраться в том, что именно означают счётчики токенов. Старые отчёты OpenCode складывали &lt;code&gt;input + cache.read&lt;/code&gt;, хотя внутренний &lt;code&gt;input&lt;/code&gt; уже не содержит ни чтения из кэша, ни записи в него. Мы теряли cache write, а вместе с ним могли неверно оценить и процент экономии, и соотношение вариантов. Поэтому для статьи полный вход восстановлен как &lt;code&gt;input + cache.read + cache.write&lt;/code&gt;. У Codex устроено иначе: cached input уже входит в общий input, и прибавлять его повторно было бы ошибкой.&lt;p&gt;В отличие от проблем исполнения, здесь не понадобилось заново вызывать модели: все необходимые счётчики сохранились в журналах, оставалось правильно их сложить. По той же причине не было оснований исключать сами запуски. &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/journals/TOKEN-ACCOUNTING-CORRECTION.md rel=&#34;noopener nofollow&#34;&gt;Пересчёт старых таблиц&lt;/a&gt; сохранён отдельно, а все графики ниже уже используют исправленный показатель.&lt;p&gt;Так что ранние эксперименты были полезны: без них часть этих проблем легко могла остаться незамеченной. Но для основного сравнения текущих вариантов я использую отдельную серию из 120 запусков. В ней обычные таймауты, ошибки написанного кода и невалидные ответы модели остаются результатами эксперимента — они не заменяются удачными попытками. Подробности &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/article-20260904/VERIFICATION.md rel=&#34;noopener nofollow&#34;&gt;проверок реализации&lt;/a&gt; и &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/article-20260904/PROTOCOL.md rel=&#34;noopener nofollow&#34;&gt;правила исключения диагностических запусков&lt;/a&gt; вынесены отдельно. Описанные выше различия схем, инструментов и стартового контекста при этом никуда не делись; аудит исправил конкретные ошибки, но не сделал сравнение идеально изолированным.&lt;/div&gt;&lt;/details&gt;&lt;h3&gt;Paper и Native: первое сравнение&lt;/h3&gt;&lt;p&gt;Теперь можно посмотреть на первое сравнение — Paper и штатный агент. Здесь и на следующих парных графиках слева показан &lt;strong&gt;накопленный вход за весь запуск&lt;/strong&gt;: сколько миллионов токенов получил основной агентный цикл, без учёта отдельного рецензента разрешений. Это итог по задаче, а не динамика по шагам; для каждой задачи взята основная попытка. Неудачные запуски отмечены штриховкой и &lt;code&gt;!&lt;/code&gt;: если не прошли проверки, случился таймаут или не было &lt;code&gt;finish&lt;/code&gt;, низкий столбец сам по себе ещё не означает выигрыш.&lt;p&gt;Справа — число обращений к модели на ту же задачу. Шагом или циклом на этих рисунках я называю обращение, на которое зарегистрирован ответ основной модели, а не отдельное действие: V3 может выдать несколько действий сразу, да и обычный вызов shell может содержать несколько команд. Такой счётчик помогает понять, сколько раз агенту пришлось возвращаться к модели, и косвенно судить о времени работы. Конечно, оно зависит ещё от длины генерации, работы инструментов, ожиданий и нагрузки. В разделе повторов покажу время отдельно.&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/webt/55/a0/3d/55a03ddf1fcbff06a47944e19c55f5e1.png alt=&#34;Paper против Native: input слева и обращения к модели справа, по каждой задаче&#34; sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/webt/55/a0/3d/55a03ddf1fcbff06a47944e19c55f5e1.png 780w,&#xA;       https://habrastorage.org/r/w1560/webt/55/a0/3d/55a03ddf1fcbff06a47944e19c55f5e1.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Paper против Native: input слева и обращения к модели справа, по каждой задаче&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;В большинстве этих запусков Paper израсходовал больше входных токенов, чем Native, хотя есть и обратные примеры. Эти результаты подсказали, что стоит внимательнее посмотреть на состав наблюдения. Расскажу, что попробовал изменить в протоколе.&lt;h3&gt;V2: расширим память о совершённых действиях&lt;/h3&gt;&lt;p&gt;В одном из ранних запусков Terra 74 раза повторила &lt;code&gt;python3 -m py_compile main.py&lt;/code&gt;. Когда команда успешно завершалась без вывода, агент получал наблюдение, из которого нельзя было понять, что именно только что произошло. По одному журналу причину зацикливания не установить, но он подсказал, куда стоит посмотреть: хватает ли модели информации в нашем O?&lt;p&gt;Здесь важно не приписать авторам лишнего. В статье O — последнее наблюдение среды, и оно вполне может сообщать, какое действие выполнено: в авторском примере со складом ответ уже описывает отгрузку. Это наш Paper-адаптер передавал только результат инструмента, не добавляя к нему отдельно команду и её аргументы. С этого я и начал доработку нашего адаптера.&lt;p&gt;Я заменил результат без контекста на запись примерно такого вида:&lt;pre&gt;&lt;code class=json&gt;{&#xA;  &amp;#34;action&amp;#34;: {&amp;#34;name&amp;#34;: &amp;#34;bash&amp;#34;, &amp;#34;input&amp;#34;: {&amp;#34;command&amp;#34;: &amp;#34;bun test&amp;#34;}},&#xA;  &amp;#34;comment&amp;#34;: &amp;#34;Проверить поведение после изменения парсера&amp;#34;,&#xA;  &amp;#34;status&amp;#34;: &amp;#34;completed&amp;#34;,&#xA;  &amp;#34;result&amp;#34;: &amp;#34;12 tests passed&amp;#34;&#xA;}&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;По полям &lt;code&gt;action&lt;/code&gt;, &lt;code&gt;input&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt; и &lt;code&gt;result&lt;/code&gt; видно, что агент сделал и что получил в ответ. А в &lt;code&gt;comment&lt;/code&gt; модель объясняет, зачем ей это действие. Так между шагами сохраняется не только сама команда, но и её замысел. Важно лишь не путать комментарий с результатом: написать «проверить, что тесты проходят» — ещё не значит получить успешную проверку.&lt;p&gt;А вот следующий шаг уже меняет саму схему памяти: вместо одного наблюдения в следующий запрос я включил три последние записи — &lt;code&gt;O[n−2], O[n−1], O[n]&lt;/code&gt;. Если сведения понадобятся и за пределами этого окна, модель должна сама перенести их в факты, решения или описание выполненной работы. Отдельное поле &lt;code&gt;verification&lt;/code&gt; из ранней адаптации я при этом убрал, чтобы не заводить параллельный журнал проверок: недавние результаты и так видны в наблюдениях, а важные выводы можно сохранить в состоянии.&lt;p&gt;Получилась небольшая краткосрочная память в дополнение к структурированному состоянию. Правда, я поменял сразу несколько вещей: состав наблюдения, комментарий и размер окна. Чтобы понять вклад каждой из них, понадобятся отдельные эксперименты — абляции, в которых меняется только один компонент. Сравнение V2 с Paper само по себе такого ответа не даёт.&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/webt/84/5e/f2/845ef2482e748cd60288a35f1c68ad29.png alt=&#34;V2 против Native: input слева и обращения к модели справа, по каждой задаче&#34; sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/webt/84/5e/f2/845ef2482e748cd60288a35f1c68ad29.png 780w,&#xA;       https://habrastorage.org/r/w1560/webt/84/5e/f2/845ef2482e748cd60288a35f1c68ad29.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;V2 против Native: input слева и обращения к модели справа, по каждой задаче&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;На OpenCode/Sol этот вариант снизил расход входных токенов на всех пяти проектах, но на остальных конфигурациях картина уже не такая однородная. Поэтому на графике оставлены отметки неудачных запусков: смотреть только на высоту столбцов здесь было бы недостаточно.&lt;h3&gt;V3: несколько действий между обращениями к модели&lt;/h3&gt;&lt;p&gt;К этому моменту стало хорошо заметно, что короткий контекст может потребовать от модели больше циклов для выполнения задачи: особенно наглядно это проявилось у Paper. Это не обязательное свойство любого state-агента — у V2 позднее получилось и меньше обращений, чем у Native, — но сама возможность потерять экономию на лишних возвращениях к модели меня беспокоила.&lt;p&gt;Я попробовал адаптировать ещё одну идею из собственной практики: сократить число циклов, позволив модели вызывать несколько действий за один раз. Допустим, агент уже знает, какие файлы нужно создать и какой командой их проверить. Зачем обязательно возвращаться к модели после каждого действия, если она могла бы сразу описать всю известную ей последовательность? Так появился V3.&lt;p&gt;Одиночное действие превратилось в массив &lt;code&gt;actions&lt;/code&gt;:&lt;pre&gt;&lt;code class=json&gt;{&#xA;  &amp;#34;state_revision&amp;#34;: 7,&#xA;  &amp;#34;state_patch&amp;#34;: {&amp;#34;next_action&amp;#34;: &amp;#34;Посмотреть результаты тестов&amp;#34;},&#xA;  &amp;#34;comment&amp;#34;: &amp;#34;Создать известные файлы и запустить проверку&amp;#34;,&#xA;  &amp;#34;actions&amp;#34;: [&#xA;    {&amp;#34;name&amp;#34;: &amp;#34;apply_patch&amp;#34;, &amp;#34;input&amp;#34;: {&amp;#34;patchText&amp;#34;: &amp;#34;...&amp;#34;}},&#xA;    {&amp;#34;name&amp;#34;: &amp;#34;bash&amp;#34;, &amp;#34;input&amp;#34;: {&amp;#34;command&amp;#34;: &amp;#34;bun test&amp;#34;}}&#xA;  ]&#xA;}&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Пример условный — у OpenCode и Codex различаются имена инструментов и их аргументы, — но общая механика такая: ядро проверяет структуру батча, состояние, имена действий и базовые типы аргументов, один раз применяет patch, а затем выполняет действия по очереди. При этом не все параметры проверяются заранее: часть проверок происходит уже внутри конкретного инструмента. Если действие завершается ошибкой, оставшиеся получают &lt;code&gt;skipped&lt;/code&gt;; завершить работу через &lt;code&gt;finish&lt;/code&gt; можно только отдельным вызовом, без других действий в батче.&lt;p&gt;Результаты возвращаются модели, когда батч закончен или остановлен на ошибке. Поэтому такой массив подходит для заранее известной последовательности, но не заменяет рассуждение между шагами: прочитать неожиданный вывод первой команды и на его основе выбрать вторую внутри батча нельзя. Для этого всё равно придётся снова обратиться к модели.&lt;p&gt;Есть и тонкость с долгими командами: Codex может вернуть идентификатор ещё работающего процесса, который затем нужно опросить через &lt;code&gt;write_stdin&lt;/code&gt;. Иными словами, последовательные вызовы инструментов не гарантируют, что все запущенные процессы уже завершились. Не стоит считать батч и транзакцией: если позднее действие упадёт, уже записанные файлы и принятые изменения состояния не откатятся.&lt;p&gt;Верхний предел числа действий я не задавал: хотелось сначала посмотреть, как модели будут пользоваться батчами. В первых опытах батчи оказались небольшими, с максимумами 5–7 действий. Пока это скорее наблюдение, чем довод навсегда отказаться от лимита: ограничение размера одного элемента ещё не ограничивает весь массив.&lt;p&gt;И здесь снова появляется смешение факторов. Окно V3 хранит &lt;strong&gt;три батча&lt;/strong&gt;, а не три действия, поэтому вместе с количеством работы между обращениями к LLM меняется и объём доступной недавней памяти. Даже если V3 выигрывает, без дополнительной проверки нельзя сказать, какая часть выигрыша пришлась на объединение действий, а какая — на более содержательное окно наблюдений.&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/webt/96/f7/42/96f7425bfe32143a99a0733a7a302973.png alt=&#34;V3 против Native: input и обращения к модели по каждой задаче&#34; sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/webt/96/f7/42/96f7425bfe32143a99a0733a7a302973.png 780w,&#xA;       https://habrastorage.org/r/w1560/webt/96/f7/42/96f7425bfe32143a99a0733a7a302973.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;V3 против Native: input и обращения к модели по каждой задаче&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Сравнение с Native показывает результат всей модификации. Чтобы увидеть, что изменилось относительно V2, посмотрим на те же запуски, оставив на графике только эту пару:&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/webt/48/b5/8c/48b58cb952f8694f7406a9a06d393d14.png alt=&#34;V3 против V2: input и обращения к модели по каждой задаче основной попытки&#34; sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/webt/48/b5/8c/48b58cb952f8694f7406a9a06d393d14.png 780w,&#xA;       https://habrastorage.org/r/w1560/webt/48/b5/8c/48b58cb952f8694f7406a9a06d393d14.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;V3 против V2: input и обращения к модели по каждой задаче основной попытки&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;На одних задачах V3 оказалась экономнее V2, на других — затратнее. Но здесь пока только по одной попытке. Повторы Codex заметно изменили первое впечатление; ниже покажу и первый круг из трёх попыток, и последующие десять повторов на Sol.&lt;h3&gt;Повторы Codex&lt;/h3&gt;&lt;h4&gt;Первый круг: три попытки V2/V3&lt;/h4&gt;&lt;p&gt;Первый результат ещё не говорит, чего ждать от следующего запуска, поэтому для Codex я дополнительно повторил сравнение V2 и V3 на обеих моделях. В таблице — полный вход по всем пяти задачам в каждой попытке, в миллионах токенов, а в скобках — число пройденных проверок из 40. Рядом я указал число обращений к основной модели — считаю их так же, как на предыдущих графиках. Вызовы рецензента разрешений и сетевые повторы сюда не входят.&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;&lt;p align=left&gt;Модель&lt;th&gt;&lt;p align=left&gt;Попытка&lt;th&gt;&lt;p align=left&gt;V2: input&lt;th&gt;&lt;p align=left&gt;V2: обращения&lt;th&gt;&lt;p align=left&gt;V3: input&lt;th&gt;&lt;p align=left&gt;V3: обращения&lt;th&gt;&lt;p align=left&gt;V3 к V2: input&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Sol&lt;td&gt;&lt;p align=left&gt;1&lt;td&gt;&lt;p align=left&gt;1,397 (40/40)&lt;td&gt;&lt;p align=left&gt;77&lt;td&gt;&lt;p align=left&gt;1,280 (40/40)&lt;td&gt;&lt;p align=left&gt;67&lt;td&gt;&lt;p align=left&gt;−8,4%&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Sol&lt;td&gt;&lt;p align=left&gt;2&lt;td&gt;&lt;p align=left&gt;1,273 (40/40)&lt;td&gt;&lt;p align=left&gt;72&lt;td&gt;&lt;p align=left&gt;1,068 (39/40)&lt;td&gt;&lt;p align=left&gt;56&lt;td&gt;&lt;p align=left&gt;−16,1%&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Sol&lt;td&gt;&lt;p align=left&gt;3&lt;td&gt;&lt;p align=left&gt;1,065 (40/40)&lt;td&gt;&lt;p align=left&gt;61&lt;td&gt;&lt;p align=left&gt;1,539 (40/40)&lt;td&gt;&lt;p align=left&gt;81&lt;td&gt;&lt;p align=left&gt;+44,5%&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Terra&lt;td&gt;&lt;p align=left&gt;1&lt;td&gt;&lt;p align=left&gt;1,000 (39/40)&lt;td&gt;&lt;p align=left&gt;58&lt;td&gt;&lt;p align=left&gt;1,092 (39/40)&lt;td&gt;&lt;p align=left&gt;62&lt;td&gt;&lt;p align=left&gt;+9,2%&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Terra&lt;td&gt;&lt;p align=left&gt;2&lt;td&gt;&lt;p align=left&gt;1,696 (40/40)&lt;td&gt;&lt;p align=left&gt;96&lt;td&gt;&lt;p align=left&gt;1,476 (39/40)&lt;td&gt;&lt;p align=left&gt;81&lt;td&gt;&lt;p align=left&gt;−12,9%&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Terra&lt;td&gt;&lt;p align=left&gt;3&lt;td&gt;&lt;p align=left&gt;1,039 (39/40)&lt;td&gt;&lt;p align=left&gt;60&lt;td&gt;&lt;p align=left&gt;1,338 (39/40)&lt;td&gt;&lt;p align=left&gt;75&lt;td&gt;&lt;p align=left&gt;+28,8%&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Это по-прежнему те же пять задач, выполненные по три раза. У Native, Paper и всех вариантов OpenCode в этой первоначальной серии остаётся по одной попытке.&lt;p&gt;И вот здесь первоначальное впечатление от V3 меняется. На Sol она снизила основной input в двух попытках из трёх, на Terra — в одной, но в сумме по всем трём попыткам потребовала больше входных токенов: на 4,0% и 4,6% соответственно. По проверкам тоже небольшое отставание: у Sol получилось 119/120 против 120/120 у V2, у Terra — 117/120 против 118/120. Этого мало для уверенного статистического сравнения качества, но достаточно, чтобы не объявлять V3 устойчивым улучшением по одному удачному запуску.&lt;p&gt;С числом циклов ожидание тоже оправдалось слабо: на Sol получилось 204 обращения у V3 против 210 у V2, то есть сокращение всего на 2,9%. На Terra сокращения вообще не было — 218 против 214. Контринтуитивно, по крайней мере для меня: возможность сделать больше действий за раз не обязательно означает меньше возвращений к модели за всю задачу.&lt;h4&gt;Ещё десять повторов Sol: теперь все четыре варианта&lt;/h4&gt;&lt;p&gt;Чтобы посмотреть на разброс внимательнее, я сделал ещё по десять полных повторов пяти задач на Codex/Sol для каждого режима — 200 новых сессий. Ни таймауты, ни неудачные решения успешными попытками не заменялись.&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;&lt;p align=left&gt;Режим&lt;th&gt;&lt;p align=left&gt;Input, млн&lt;th&gt;&lt;p align=left&gt;К Native&lt;th&gt;&lt;p align=left&gt;Обращения&lt;th&gt;&lt;p align=left&gt;Полный успех*&lt;th&gt;&lt;p align=left&gt;Таймауты&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Native&lt;td&gt;&lt;p align=left&gt;19,488&lt;td&gt;&lt;p align=left&gt;—&lt;td&gt;&lt;p align=left&gt;632&lt;td&gt;&lt;p align=left&gt;49/50&lt;td&gt;&lt;p align=left&gt;0&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Paper&lt;td&gt;&lt;p align=left&gt;32,319&lt;td&gt;&lt;p align=left&gt;+65,8%&lt;td&gt;&lt;p align=left&gt;2005&lt;td&gt;&lt;p align=left&gt;30/50&lt;td&gt;&lt;p align=left&gt;18&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;V2&lt;td&gt;&lt;p align=left&gt;9,795&lt;td&gt;&lt;p align=left&gt;−49,7%&lt;td&gt;&lt;p align=left&gt;556&lt;td&gt;&lt;p align=left&gt;48/50&lt;td&gt;&lt;p align=left&gt;0&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;V3&lt;td&gt;&lt;p align=left&gt;13,069&lt;td&gt;&lt;p align=left&gt;−32,9%&lt;td&gt;&lt;p align=left&gt;691&lt;td&gt;&lt;p align=left&gt;47/50&lt;td&gt;&lt;p align=left&gt;1&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;*Полный успех здесь — все внешние проверки плюс штатное завершение без таймаута. У Native проверяется &lt;code&gt;turn.completed&lt;/code&gt;, у state — принятый &lt;code&gt;finish&lt;/code&gt;.&lt;p&gt;При подготовке этой сводки пришлось поправить и оценщик. Тест удаления задачи требовал поле &lt;code&gt;id&lt;/code&gt; в ответе CLI, хотя в спецификации такого требования не было. Я проверил фактическое удаление на копиях всех 40 CLI-проектов и одинаково пересчитал результаты четырёх режимов.&lt;p&gt;Исходные оценки я сохранил. По прежнему оценщику полный успех составлял 46/50, 29/50, 46/50 и 47/50, а баллы проверок — 396/400, 385/400, 396/400 и 398/400, в том же порядке, что в таблице. После поправки баллы — 399/400, 386/400, 398/400 и 398/400. &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/codex-sol-controls-20260906/COMPARE-FOUR.md rel=&#34;noopener nofollow&#34;&gt;Обе версии оценок и все повторы&lt;/a&gt; сохранены вместе с &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/codex-sol-repeats-20260906/EVALUATOR-NOTE.md rel=&#34;noopener nofollow&#34;&gt;объяснением исправления&lt;/a&gt;.&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/webt/32/5e/ca/325ecaa6f63be4a44cd407b033f6db02.png alt=&#34;Десять повторов Codex/Sol: средний input, число обращений и фактическое время на задачу&#34; sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/webt/32/5e/ca/325ecaa6f63be4a44cd407b033f6db02.png 780w,&#xA;       https://habrastorage.org/r/w1560/webt/32/5e/ca/325ecaa6f63be4a44cd407b033f6db02.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Десять повторов Codex/Sol: средний input, число обращений и фактическое время на задачу&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Каждая точка — один полный повтор, усреднённый по пяти задачам; толстая черта — среднее десяти повторов. Треугольник означает, что в повторе был хотя бы один таймаут. Неудачные запуски учтены во всех трёх панелях, а время ограничено тем же лимитом: мы не знаем, сколько длились бы оборванные задачи без него. В это время входят ожидание ответов и выполнение инструментов.&lt;p&gt;Результат V2 меня вполне устраивает: почти вдвое меньше основного input, ни одного таймаута и почти столько же полностью успешных решений. Разницу 49/50 против 48/50 я не стал бы выдавать за установленное превосходство по качеству — для этого здесь слишком мало разных задач. Но как инженерный результат V2 выглядит интересно.&lt;p&gt;С V3 история оказалась ещё менее интуитивной, чем в первых трёх попытках. Основной input вырос на 33,4% относительно V2, а число обращений вместо ожидаемого сокращения увеличилось с 556 до 691 — на 24,3%. После поправки к оценщику обе версии оставили по 48 проектов, прошедших все проверки, но один V3-запуск не завершился в срок. То есть в этой серии батчинг не дал того выигрыша, ради которого я его добавлял. Правда, вместе с ним поменялось и окно наблюдений — их влияние ещё нужно разделить.&lt;p&gt;Paper отправляла короткие запросы, но делала это слишком часто: 2005 обращений против 632 у Native съели всю экономию. В 14 из 18 сессий с таймаутами код при этом прошёл все проверки. Особенно выделилась задача про HTTP-сервис — девять таймаутов из десяти запусков, восемь из них с рабочим кодом. Похоже, в этой версии агента модели было сложно понять, что пора остановиться: программа уже работает, а разработка всё продолжается.&lt;p&gt;Подробнее — в &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/codex-sol-controls-20260906/RESULTS.md rel=&#34;noopener nofollow&#34;&gt;полном отчёте&lt;/a&gt;.&lt;h4&gt;Sol без глобальных скиллов: V2 всё ещё мне нравится&lt;/h4&gt;&lt;p&gt;В новой серии получилась знакомая картина, хотя величина экономии поменялась. В таблице — сумма по 25 сессиям каждого режима; полный успех учитывает и проверки кода, и штатное завершение, как в предыдущем разделе.&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;&lt;p align=left&gt;Режим&lt;th&gt;&lt;p align=left&gt;Input, млн&lt;th&gt;&lt;p align=left&gt;К Native&lt;th&gt;&lt;p align=left&gt;Обращения&lt;th&gt;&lt;p align=left&gt;Полный успех&lt;th&gt;&lt;p align=left&gt;Таймауты&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Native&lt;td&gt;&lt;p align=left&gt;6,710&lt;td&gt;&lt;p align=left&gt;—&lt;td&gt;&lt;p align=left&gt;295&lt;td&gt;&lt;p align=left&gt;25/25&lt;td&gt;&lt;p align=left&gt;0&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Paper&lt;td&gt;&lt;p align=left&gt;12,191&lt;td&gt;&lt;p align=left&gt;+81,7%&lt;td&gt;&lt;p align=left&gt;842&lt;td&gt;&lt;p align=left&gt;8/25&lt;td&gt;&lt;p align=left&gt;17&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;V2&lt;td&gt;&lt;p align=left&gt;4,631&lt;td&gt;&lt;p align=left&gt;−31,0%&lt;td&gt;&lt;p align=left&gt;288&lt;td&gt;&lt;p align=left&gt;24/25&lt;td&gt;&lt;p align=left&gt;1&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;V3&lt;td&gt;&lt;p align=left&gt;5,250&lt;td&gt;&lt;p align=left&gt;−21,8%&lt;td&gt;&lt;p align=left&gt;309&lt;td&gt;&lt;p align=left&gt;24/25&lt;td&gt;&lt;p align=left&gt;1&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/webt/a6/dd/c9/a6ddc9d578405d5e49744f6056e3cd51.png alt=&#34;Пять повторов Codex/Sol без глобальных скиллов: input, циклы и наблюдаемое время на задачу&#34; sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/webt/a6/dd/c9/a6ddc9d578405d5e49744f6056e3cd51.png 780w,&#xA;       https://habrastorage.org/r/w1560/webt/a6/dd/c9/a6ddc9d578405d5e49744f6056e3cd51.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Пять повторов Codex/Sol без глобальных скиллов: input, циклы и наблюдаемое время на задачу&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Точки, как и выше, — средние по пяти задачам одного повтора; черта — среднее всех пяти повторов. Треугольниками отмечены повторы с таймаутом. На следующем графике для Astra шкалы будут такими же.&lt;p&gt;V2 снова экономит — теперь 31% основного input относительно Native, при почти одинаковом числе обращений. Все 25 проектов прошли проверки, но одна сессия не успела закончиться, поэтому полного успеха здесь 24/25. Меня этот результат всё ещё устраивает, хотя выдавать его за улучшение без оговорок я бы не стал. V3 снова оказалась дороже V2, на 13,4%, и циклов сделала больше, а не меньше. Paper по-прежнему часто не доходит до завершения.&lt;p&gt;Изменившиеся проценты сами по себе не доказывают влияние скиллов: это другая серия, к тому же вдвое короче предыдущей. Исправленную проверку удаления я одинаково применил ко всем новым CLI-проектам обеих моделей; в этот раз она не изменила ни одного исходного балла. &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/codex-clean-comparison-20260908/REPORT-sol.md rel=&#34;noopener nofollow&#34;&gt;Отчёт Sol с проверками, повторами и исходными счётчиками&lt;/a&gt;.&lt;h4&gt;Astra — сначала Native оказался лучше&lt;/h4&gt;&lt;p&gt;А вот с Astra всё получилось совсем иначе. Лимиты памяти в state-режимах пока оставались прежними, и Native оказался заметно лучше всех моих вариантов: меньше входных токенов, меньше обращений и ни одного таймаута.&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;&lt;p align=left&gt;Режим&lt;th&gt;&lt;p align=left&gt;Input, млн&lt;th&gt;&lt;p align=left&gt;К Native&lt;th&gt;&lt;p align=left&gt;Обращения&lt;th&gt;&lt;p align=left&gt;Полный успех&lt;th&gt;&lt;p align=left&gt;Таймауты&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Native&lt;td&gt;&lt;p align=left&gt;2,612&lt;td&gt;&lt;p align=left&gt;—&lt;td&gt;&lt;p align=left&gt;136&lt;td&gt;&lt;p align=left&gt;25/25&lt;td&gt;&lt;p align=left&gt;0&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Paper&lt;td&gt;&lt;p align=left&gt;27,355&lt;td&gt;&lt;p align=left&gt;+947,2%&lt;td&gt;&lt;p align=left&gt;1845&lt;td&gt;&lt;p align=left&gt;0/25&lt;td&gt;&lt;p align=left&gt;25&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;V2&lt;td&gt;&lt;p align=left&gt;10,871&lt;td&gt;&lt;p align=left&gt;+316,1%&lt;td&gt;&lt;p align=left&gt;639&lt;td&gt;&lt;p align=left&gt;22/25&lt;td&gt;&lt;p align=left&gt;3&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;V3&lt;td&gt;&lt;p align=left&gt;9,627&lt;td&gt;&lt;p align=left&gt;+268,5%&lt;td&gt;&lt;p align=left&gt;548&lt;td&gt;&lt;p align=left&gt;22/25&lt;td&gt;&lt;p align=left&gt;3&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/webt/76/80/80/768080944a3aa9af5c636d655780cb72.png alt=&#34;Пять повторов Codex/Astra без глобальных скиллов: input, циклы и наблюдаемое время на задачу&#34; sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/webt/76/80/80/768080944a3aa9af5c636d655780cb72.png 780w,&#xA;       https://habrastorage.org/r/w1560/webt/76/80/80/768080944a3aa9af5c636d655780cb72.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Пять повторов Codex/Astra без глобальных скиллов: input, циклы и наблюдаемое время на задачу&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;В среднем Native понадобилось всего 5,44 обращения на проект; V2 — 25,56, V3 — 21,92. При этом отдельный запрос Native в среднем содержал даже больше входных токенов, чем запрос каждого state-режима: выигрыш дало небольшое число обращений. V3 здесь немного отыгралась относительно V2, но до Native обоим вариантам всё равно далеко.&lt;p&gt;Особенно обидно выглядит Paper: 24 из 25 проектов прошли все проверки, но ни одна сессия не завершилась в срок. Поэтому «Astra не смогла написать код» было бы неверным описанием происходящего. В разобранных сессиях модель часто уже написала работающий проект, а потом продолжала его перечитывать и проверять. Обрезка наблюдений в нашей реализации могла этому способствовать. И это оказалось достаточно заметным, чтобы не ограничиваться оговоркой, а сделать ещё один эксперимент. &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/codex-clean-comparison-20260908/REPORT-astra.md rel=&#34;noopener nofollow&#34;&gt;Отчёт Astra&lt;/a&gt; и &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/journals/CODEX-ASTRA-NATIVE-EFFICIENCY-AUDIT-20260908.md rel=&#34;noopener nofollow&#34;&gt;разбор нескольких траекторий&lt;/a&gt;.&lt;h4&gt;Кажется, я слишком старательно экономил контекст&lt;/h4&gt;&lt;p&gt;Когда я полез в журналы Astra, обнаружилась довольно неприятная особенность нашей адаптации. Модель могла за один шаг написать 13 КБ кода, но в следующем наблюдении увидеть лишь короткую выдержку из собственного вызова — около 2 КиБ текста. Полный вызов из истории мы уже убрали, а в состоянии подробности написанного кода сами собой, конечно, не появлялись. Чтобы их восстановить, модель читала файл заново. И здесь её ждал следующий сюрприз: результат чтения обрезался до 4 КиБ.&lt;p&gt;Так агент рисковал застрять в чтении по кругу: написать файл, увидеть только начало, запросить другой фрагмент, вернуться к предыдущему. В одном из разобранных HTTP-проектов V2 сделала 87 обращений, 80 результатов оказались обрезаны, а последние 70 действий были чтением и осмотром файлов, без новых правок или запуска тестов. Это ещё не доказывает, что все лишние циклы вызвала именно обрезка, но повод проверить её явно был.&lt;p&gt;Я поднял сразу четыре лимита до 2 МиБ каждый: на полное состояние, принимаемые аргументы действия, их сохранённую копию и сохранённый результат. Раньше это были 32, 64, 3 и 4 КиБ соответственно. Заодно пришлось защитить JSON-запись перехода от повторной обрезки при сохранении истории: штатный механизм мог обрезать её как обычный текст, нарушив формат, и состояние уже не удавалось восстановить. Точнее, повреждённая запись пропускалась, и сборщик мог взять более раннее состояние вместо последнего. Первый аудит проверял, как собирается запрос, но это ещё не гарантировало сохранность состояния на всём пути через историю. Поэтому его отметку о передаче полного актуального Σ нужно читать с этой позднее обнаруженной оговоркой; она тоже зафиксирована в дополнении к аудиту. Сам протокол, &lt;code&gt;comment&lt;/code&gt;, окно k=3 и правила исполнения действий не менялись.&lt;p&gt;Важно, что 2 МиБ здесь — потолок для каждого из перечисленных полей, а не обязательный размер запроса и не увеличение контекстного окна модели. В двух новых сериях самое большое сохранённое состояние оказалось меньше 4 КиБ, а результат действия — меньше 44 КиБ. Усечений на уровне SKILL.state больше не обнаружилось.&lt;p&gt;При этом в двух вызовах Astra/V2 вывод всё же оказался обрезан, но уже по другой причине: модель сама передала в &lt;code&gt;exec_command&lt;/code&gt; параметр &lt;code&gt;max_output_tokens: 2000&lt;/code&gt;. Оба случая относятся к одной сессии на HTTP-проекте. Вывод тестов каждый раз получился примерно на 2073 токена, и штатный инструмент Codex применил запрошенный лимит ещё до передачи результата в SKILL.state. Наш потолок в 2 МиБ этот меньший лимит не отменяет и уже потерянный текст не восстанавливает. Так что здесь мы убрали агрессивную обрезку со стороны нашей обёртки, но не отключили ограничения инструментов, в том числе заданные самой моделью.&lt;p&gt;Прежние результаты от этого не становятся недействительными: они описывают работу с прежними лимитами. Но обобщать их на любую реализацию state было бы неразумно. Новый эксперимент тоже не позволяет выделить вклад каждого изменения: я одновременно поднял четыре лимита и исправил сохранение JSON. Их отдельное влияние ещё предстоит проверить. Подробности — в &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/codex-astra-large-context-20260908/PROTOCOL.md rel=&#34;noopener nofollow&#34;&gt;описании изменений ядра и условий запуска&lt;/a&gt;.&lt;h4&gt;Astra с расширенными лимитами: картина поменялась&lt;/h4&gt;&lt;p&gt;Я снова запустил все четыре режима, по пять повторов пяти проектов. Native тоже прогнал заново, чтобы сравнивать результаты внутри одной серии. Скиллы на время запусков опять убрал; модель, reasoning, k и 15-минутный таймаут оставил прежними.&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;&lt;p align=left&gt;Режим&lt;th&gt;&lt;p align=left&gt;Input, млн&lt;th&gt;&lt;p align=left&gt;К Native&lt;th&gt;&lt;p align=left&gt;Обращения&lt;th&gt;&lt;p align=left&gt;Полный успех&lt;th&gt;&lt;p align=left&gt;Таймауты&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Native&lt;td&gt;&lt;p align=left&gt;2,734&lt;td&gt;&lt;p align=left&gt;—&lt;td&gt;&lt;p align=left&gt;140&lt;td&gt;&lt;p align=left&gt;25/25&lt;td&gt;&lt;p align=left&gt;0&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Paper&lt;td&gt;&lt;p align=left&gt;15,318&lt;td&gt;&lt;p align=left&gt;+460,3%&lt;td&gt;&lt;p align=left&gt;1027&lt;td&gt;&lt;p align=left&gt;16/25&lt;td&gt;&lt;p align=left&gt;9&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;V2&lt;td&gt;&lt;p align=left&gt;2,654&lt;td&gt;&lt;p align=left&gt;−2,9%&lt;td&gt;&lt;p align=left&gt;159&lt;td&gt;&lt;p align=left&gt;25/25&lt;td&gt;&lt;p align=left&gt;0&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;V3&lt;td&gt;&lt;p align=left&gt;2,340&lt;td&gt;&lt;p align=left&gt;−14,4%&lt;td&gt;&lt;p align=left&gt;141&lt;td&gt;&lt;p align=left&gt;25/25&lt;td&gt;&lt;p align=left&gt;0&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/webt/68/97/0e/68970e87ef7b3a28ac3b9b8826310cbd.png alt=&#34;Пять повторов Codex/Astra с лимитами 2 МиБ: input, циклы и наблюдаемое время на задачу&#34; sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/webt/68/97/0e/68970e87ef7b3a28ac3b9b8826310cbd.png 780w,&#xA;       https://habrastorage.org/r/w1560/webt/68/97/0e/68970e87ef7b3a28ac3b9b8826310cbd.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Пять повторов Codex/Astra с лимитами 2 МиБ: input, циклы и наблюдаемое время на задачу&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Обозначения прежние: точка — среднее по пяти задачам одного повтора, черта — среднее пяти повторов, треугольник — повтор с таймаутом. У этого и следующего графика одинаковые шкалы, но они отличаются от шкал предыдущей пары рисунков.&lt;p&gt;Вот теперь V2 практически сравнялась с Native по расходу, а V3 использовала на 14,4% меньше input при почти том же числе обращений — 141 против 140. У всех трёх вариантов полный успех 25/25. Если раньше у V2/V3 были десятки циклов на проект, то теперь в среднем получилось 6,36 и 5,64, рядом с 5,6 у Native. Для меня это хороший повод осторожнее относиться к мысли, что сильной модели на коротких задачах state непременно мешает: с другой настройкой памяти получилась уже другая картина.&lt;p&gt;Paper тоже стала чаще завершать работу, но девять таймаутов и расход в 5,6 раза выше Native никуда не спрячешь. Даже с дополнительным местом для памяти наша исходная адаптация пока заметно отстаёт. При этом всю разницу между сериями приписывать обрезке нельзя: помимо нескольких изменений реализации, на результат влияет и то, как именно модель решает задачу в каждом запуске. &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/codex-large-context-comparison-20260909/REPORT-astra.md rel=&#34;noopener nofollow&#34;&gt;Полный отчёт Astra, включая фактические размеры наблюдений&lt;/a&gt;.&lt;h4&gt;Sol с теми же лимитами: V2 экономнее, V3 чуть ближе&lt;/h4&gt;&lt;p&gt;Следом я запустил ту же конфигурацию на Sol: тот же бинарник, пять повторов и временно убранные скиллы. Такого резкого разворота здесь уже не случилось, но результат мне тоже нравится.&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;&lt;p align=left&gt;Режим&lt;th&gt;&lt;p align=left&gt;Input, млн&lt;th&gt;&lt;p align=left&gt;К Native&lt;th&gt;&lt;p align=left&gt;Обращения&lt;th&gt;&lt;p align=left&gt;Полный успех&lt;th&gt;&lt;p align=left&gt;Таймауты&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Native&lt;td&gt;&lt;p align=left&gt;6,367&lt;td&gt;&lt;p align=left&gt;—&lt;td&gt;&lt;p align=left&gt;279&lt;td&gt;&lt;p align=left&gt;24/25&lt;td&gt;&lt;p align=left&gt;0&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Paper&lt;td&gt;&lt;p align=left&gt;9,215&lt;td&gt;&lt;p align=left&gt;+44,7%&lt;td&gt;&lt;p align=left&gt;629&lt;td&gt;&lt;p align=left&gt;19/25&lt;td&gt;&lt;p align=left&gt;5&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;V2&lt;td&gt;&lt;p align=left&gt;4,077&lt;td&gt;&lt;p align=left&gt;−36,0%&lt;td&gt;&lt;p align=left&gt;235&lt;td&gt;&lt;p align=left&gt;23/25&lt;td&gt;&lt;p align=left&gt;0&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;V3&lt;td&gt;&lt;p align=left&gt;4,180&lt;td&gt;&lt;p align=left&gt;−34,4%&lt;td&gt;&lt;p align=left&gt;225&lt;td&gt;&lt;p align=left&gt;25/25&lt;td&gt;&lt;p align=left&gt;0&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/webt/ce/03/68/ce03688f99feb9fe500ea47cfdf5485e.png alt=&#34;Пять повторов Codex/Sol с лимитами 2 МиБ: input, циклы и наблюдаемое время на задачу&#34; sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/webt/ce/03/68/ce03688f99feb9fe500ea47cfdf5485e.png 780w,&#xA;       https://habrastorage.org/r/w1560/webt/ce/03/68/ce03688f99feb9fe500ea47cfdf5485e.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Пять повторов Codex/Sol с лимитами 2 МиБ: input, циклы и наблюдаемое время на задачу&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;V2 потратила на 36% меньше input, чем Native, V3 — на 34,4%. При этом V3 теперь дороже V2 всего на 2,5% и действительно сделала немного меньше циклов: 225 против 235. Правда, по времени в этой серии V2 всё равно быстрее — в среднем 5,28 минуты на проект против 5,93. Ещё одно напоминание, что число обращений полезно смотреть рядом со временем, но полностью заменять им время нельзя.&lt;p&gt;У V3 все 25 сессий прошли проверки и завершились, у V2 — 23, у Native — 24. Последнее число учитывает знакомую поправку к тесту удаления: я применил её ко всем 40 CLI-проектам двух новых серий, и она вернула один ошибочно снятый балл Sol/Native. По исходному оценщику у него было 23/25. Это не исправление проекта или перезапуск модели; обе версии оценок сохранены в отчёте.&lt;p&gt;Меня V2 по-прежнему устраивает, а V3 в этой конфигурации уже выглядит интереснее, чем в прошлых повторах Sol. Но превращать разницу в одну-две сессии на пяти знакомых задачах в вывод о превосходстве по качеству я бы не стал. Paper и здесь остаётся дороже Native и пять раз упирается в таймаут. &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/codex-large-context-comparison-20260909/REPORT-sol.md rel=&#34;noopener nofollow&#34;&gt;Отчёт Sol с исходными и уточнёнными проверками&lt;/a&gt;.&lt;h4&gt;Ещё один контроль: Paper2&lt;/h4&gt;&lt;p&gt;Только при повторном аудите я обратил внимание на вещь, которую упустил при первом прочтении: авторское O может само сообщать, какое действие было выполнено. В примере про склад ответ среды уже описывает отгрузку. А я перенёс в Paper только результат инструмента, который в coding-задачах нередко оказывается пустым или без контекста мало что говорит. То есть часть информации потерялась не потому, что этого требует идея статьи, а из-за моего способа её адаптировать.&lt;p&gt;Поэтому захотелось проверить ещё один, более близкий к этому прочтению вариант. Назвал его Paper2: всё оставил как у Paper, но в единственное последнее O добавил выполненное действие, его аргументы и статус. Без истории из трёх записей, без комментария и без других изменений V2. Простое переключение V2 на k=1 не дало бы такого сравнения: у неё отличаются и контракт ответа, и обновление состояния.&lt;p&gt;Я прогнал Paper2 на Sol и Astra: по пять повторов пяти проектов, с лимитами до 2 МиБ и без глобальных скиллов. Reasoning medium, 15 минут на задачу, до пяти CLI одновременно — как в предыдущих сериях. Ниже сравниваю с их Native и Paper, а не с новыми одновременными контролями: те запускались 8–9 сентября, Paper2 — 10 сентября. &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/codex-paper2-20260910/PROTOCOL.md rel=&#34;noopener nofollow&#34;&gt;Протокол&lt;/a&gt; зафиксирован до прогонов.&lt;p&gt;Начну с Sol:&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;&lt;p align=left&gt;Режим&lt;th&gt;&lt;p align=left&gt;Input, млн&lt;th&gt;&lt;p align=left&gt;К Native&lt;th&gt;&lt;p align=left&gt;Обращения&lt;th&gt;&lt;p align=left&gt;Полный успех&lt;th&gt;&lt;p align=left&gt;Таймауты&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Native&lt;td&gt;&lt;p align=left&gt;6,367&lt;td&gt;&lt;p align=left&gt;—&lt;td&gt;&lt;p align=left&gt;279&lt;td&gt;&lt;p align=left&gt;24/25&lt;td&gt;&lt;p align=left&gt;0&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Paper&lt;td&gt;&lt;p align=left&gt;9,215&lt;td&gt;&lt;p align=left&gt;+44,7%&lt;td&gt;&lt;p align=left&gt;629&lt;td&gt;&lt;p align=left&gt;19/25&lt;td&gt;&lt;p align=left&gt;5&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Paper2&lt;td&gt;&lt;p align=left&gt;4,602&lt;td&gt;&lt;p align=left&gt;−27,7%&lt;td&gt;&lt;p align=left&gt;302&lt;td&gt;&lt;p align=left&gt;23/25&lt;td&gt;&lt;p align=left&gt;2&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/webt/90/2c/93/902c93a9dc027500af2336b9cf8d8361.png alt=&#34;Codex/Sol: Paper2 против прежних Native и Paper — input, циклы и время на задачу&#34; sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/webt/90/2c/93/902c93a9dc027500af2336b9cf8d8361.png 780w,&#xA;       https://habrastorage.org/r/w1560/webt/90/2c/93/902c93a9dc027500af2336b9cf8d8361.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Codex/Sol: Paper2 против прежних Native и Paper — input, циклы и время на задачу&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Вот это уже гораздо ближе к тому, чего я ждал от исходной идеи. По сравнению с Paper расход упал на 50,1%, а число обращений — с 629 до 302. Относительно Native получилось на 27,7% меньше input, хотя циклов всё ещё чуть больше. Правда, экономия токенов не стала ускорением: среднее наблюдаемое время на задачу — 8,05 минуты против 6,32 у Native. Время здесь включает ожидания и таймауты, так что приписывать всю разницу устройству контекста тоже нельзя.&lt;p&gt;Полный успех у Sol — 23/25 после той же дополнительной проверки удаления задачи, что и в прошлых сериях. По исходному оценщику было 22/25: один корректный CLI снова потерял балл из-за формы JSON-ответа. Я проверил все десять новых CLI-проектов на копиях, не исправляя код и не перезапуская модель.&lt;p&gt;Теперь Astra:&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;&lt;p align=left&gt;Режим&lt;th&gt;&lt;p align=left&gt;Input, млн&lt;th&gt;&lt;p align=left&gt;К Native&lt;th&gt;&lt;p align=left&gt;Обращения&lt;th&gt;&lt;p align=left&gt;Полный успех&lt;th&gt;&lt;p align=left&gt;Таймауты&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Native&lt;td&gt;&lt;p align=left&gt;2,734&lt;td&gt;&lt;p align=left&gt;—&lt;td&gt;&lt;p align=left&gt;140&lt;td&gt;&lt;p align=left&gt;25/25&lt;td&gt;&lt;p align=left&gt;0&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Paper&lt;td&gt;&lt;p align=left&gt;15,318&lt;td&gt;&lt;p align=left&gt;+460,3%&lt;td&gt;&lt;p align=left&gt;1027&lt;td&gt;&lt;p align=left&gt;16/25&lt;td&gt;&lt;p align=left&gt;9&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Paper2&lt;td&gt;&lt;p align=left&gt;4,395&lt;td&gt;&lt;p align=left&gt;+60,8%&lt;td&gt;&lt;p align=left&gt;284&lt;td&gt;&lt;p align=left&gt;23/25&lt;td&gt;&lt;p align=left&gt;2&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/webt/be/bd/9c/bebd9cf9e5282b29a517bb74e8218e7f.png alt=&#34;Codex/Astra: Paper2 против прежних Native и Paper — input, циклы и время на задачу&#34; sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/webt/be/bd/9c/bebd9cf9e5282b29a517bb74e8218e7f.png 780w,&#xA;       https://habrastorage.org/r/w1560/webt/be/bd/9c/bebd9cf9e5282b29a517bb74e8218e7f.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Codex/Astra: Paper2 против прежних Native и Paper — input, циклы и время на задачу&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Обозначения на обоих графиках прежние: точка — среднее пяти задач одного повтора, черта — среднее пяти повторов, треугольник — повтор с таймаутом. Шкалы у Sol и Astra одинаковые; показаны все итоговые исходы, включая неудачные.&lt;p&gt;У Astra улучшение относительно Paper ещё заметнее: на 71,3% меньше input, 284 обращения вместо 1027 и два таймаута вместо девяти. Но Native здесь всё ещё лучше — почти вдвое меньше циклов, меньше токенов и все 25 задач завершены. При этом сами проекты Paper2 прошли 200/200 проверок: в двух случаях код уже работал, а до принятого &lt;code&gt;finish&lt;/code&gt; агент так и не дошёл. Поэтому балл проекта и полный успех я продолжаю показывать раздельно.&lt;p&gt;Для меня это важное уточнение к истории с Paper: чтобы получить заметно другой результат, не обязательно было добавлять три наблюдения или батчи. В этой серии хватило более содержательного последнего O. Но говорить, что мы доказали пользу каждого его поля, рано: имя действия, аргументы и статус добавились вместе, а контрольные серии проведены раньше. Отличие Native с Code Mode от state с Direct tools тоже никуда не делось. После этого на результаты первой адаптации я уже смотрю иначе.&lt;p&gt;И ещё техническая оговорка. Первый запуск остановила защита изоляции: обновление плагинов вернуло глобальные источники скиллов. Четыре готовых результата сохранили, пять прерванных попыток записали отдельно, а после восстановления изоляции запустили оставшиеся 46 сессий. В таблицах — 50 итоговых исходов; расход тех пяти прерванных попыток не включён, поэтому это не полный бюджет кампании. Четыре таймаута отдельных сессий, напротив, остались и в результатах, и в расходе. Проверки сохранённых контекстов не нашли маркеров глобальных скиллов, повреждённых переходов или обрезки input/result со стороны SKILL.state. После завершения глобальные источники восстановлены.&lt;p&gt;&lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/codex-paper2-20260910/REPORT.md rel=&#34;noopener nofollow&#34;&gt;Отчёт Paper2 с исходными и уточнёнными оценками&lt;/a&gt;, &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/codex-paper2-20260910/statistics.json rel=&#34;noopener nofollow&#34;&gt;данные для таблиц и графиков&lt;/a&gt;, &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/codex-paper2-20260910/INTERRUPTION.md rel=&#34;noopener nofollow&#34;&gt;разбор прерывания&lt;/a&gt;.&lt;h4&gt;Paper2 с малыми лимитами&lt;/h4&gt;&lt;p&gt;После этого захотелось сделать ещё одну проверку: а сохранится ли результат Paper2, если вернуть прежние ограничения? Состав O оставил тем же, но на состояние снова отвёл 32 КиБ, на принимаемые аргументы — 64 КиБ, на их сохранённую копию — 3 КиБ, на результат — 4 КиБ. Защиту JSON от повторной обрезки сохранил.&lt;p&gt;Получилось ещё 50 новых сессий: пять повторов пяти задач на Sol и Astra, без глобальных скиллов, с тем же reasoning medium, 15 минутами на задачу и параллельностью до пяти CLI. Ни одной попытки не заменял и не перезапускал. В таблицах ниже сравниваю только два варианта Paper2; малыми дальше называю эти четыре лимита.&lt;p&gt;У Sol результат ухудшился, хотя не настолько резко, как у Astra:&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;&lt;p align=left&gt;Лимиты Paper2&lt;th&gt;&lt;p align=left&gt;Input, млн&lt;th&gt;&lt;p align=left&gt;К большим лимитам&lt;th&gt;&lt;p align=left&gt;Обращения&lt;th&gt;&lt;p align=left&gt;Полный успех&lt;th&gt;&lt;p align=left&gt;Таймауты&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;До 2 МиБ на поле&lt;td&gt;&lt;p align=left&gt;4,602&lt;td&gt;&lt;p align=left&gt;—&lt;td&gt;&lt;p align=left&gt;302&lt;td&gt;&lt;p align=left&gt;23/25&lt;td&gt;&lt;p align=left&gt;2&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Малые&lt;td&gt;&lt;p align=left&gt;6,956&lt;td&gt;&lt;p align=left&gt;+51,1%&lt;td&gt;&lt;p align=left&gt;463&lt;td&gt;&lt;p align=left&gt;18/25&lt;td&gt;&lt;p align=left&gt;5&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/webt/80/28/6a/80286a5ab0e80fef701508b9c32b81b1.png alt=&#34;Codex/Sol: Paper2 с большими и малыми лимитами — input, циклы и время на задачу&#34; sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/webt/80/28/6a/80286a5ab0e80fef701508b9c32b81b1.png 780w,&#xA;       https://habrastorage.org/r/w1560/webt/80/28/6a/80286a5ab0e80fef701508b9c32b81b1.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Codex/Sol: Paper2 с большими и малыми лимитами — input, циклы и время на задачу&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;При малых лимитах код прошёл все проверки в 23 из 25 проектов, но пять из этих сессий не завершились в срок. Поэтому полных успехов осталось 18. Сумма проверок — 198/200. Дополнительную проверку удаления я снова применил ко всем десяти CLI-проектам обеих моделей; в этот раз она не изменила исходные оценки.&lt;p&gt;А вот Astra вернулась к знакомой картине: работающий код есть, завершения нет.&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;&lt;p align=left&gt;Лимиты Paper2&lt;th&gt;&lt;p align=left&gt;Input, млн&lt;th&gt;&lt;p align=left&gt;К большим лимитам&lt;th&gt;&lt;p align=left&gt;Обращения&lt;th&gt;&lt;p align=left&gt;Полный успех&lt;th&gt;&lt;p align=left&gt;Таймауты&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;До 2 МиБ на поле&lt;td&gt;&lt;p align=left&gt;4,395&lt;td&gt;&lt;p align=left&gt;—&lt;td&gt;&lt;p align=left&gt;284&lt;td&gt;&lt;p align=left&gt;23/25&lt;td&gt;&lt;p align=left&gt;2&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Малые&lt;td&gt;&lt;p align=left&gt;31,728&lt;td&gt;&lt;p align=left&gt;+621,8%&lt;td&gt;&lt;p align=left&gt;2090&lt;td&gt;&lt;p align=left&gt;0/25&lt;td&gt;&lt;p align=left&gt;25&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/webt/3b/90/04/3b9004f10f35dad2ac9a0002328e77fb.png alt=&#34;Codex/Astra: Paper2 с большими и малыми лимитами — input, циклы и время на задачу&#34; sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/webt/3b/90/04/3b9004f10f35dad2ac9a0002328e77fb.png 780w,&#xA;       https://habrastorage.org/r/w1560/webt/3b/90/04/3b9004f10f35dad2ac9a0002328e77fb.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Codex/Astra: Paper2 с большими и малыми лимитами — input, циклы и время на задачу&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Обозначения прежние: точка — среднее пяти задач одного повтора, черта — среднее всех пяти повторов, треугольник — повтор с таймаутом. Шкалы у этих двух рисунков одинаковые; оборванные сессии включены.&lt;p&gt;Все 25 проектов Astra прошли тесты — 200/200 проверок, — но все 25 сессий упёрлись в таймаут. В журналах нет ни одной попытки вызвать &lt;code&gt;finish&lt;/code&gt;: это не ситуация, когда модель пыталась завершить работу, а ядро отклоняло её ответ. По сравнению с большими лимитами вход вырос в 7,2 раза, число обращений — с 284 до 2090.&lt;p&gt;По сохранённым переходам у Astra 2002 результата действий были обрезаны лимитом SKILL.state, у Sol — 253. Повреждённых JSON-переходов аудит не нашёл. Само состояние, кстати, оставалось маленьким: максимум 928 байт у Astra и 3367 у Sol, гораздо меньше отведённых 32 КиБ. Это ещё не доказывает, что все лишние циклы вызвало усечение результатов, но явно не похоже на нехватку места под само Σ. Знать, какую команду только что выполнил, полезно; получить достаточный ответ на неё — отдельная задача.&lt;p&gt;Оговорки у этого сравнения тоже есть. Четыре лимита поменялись вместе, а серии шли последовательно, не вперемешку. В начале малого прогона я ещё и чистил кэш сборки на той же машине: исходники, бинарник и результаты сохранились, но дополнительная дисковая нагрузка могла повлиять на время. Поэтому график времени здесь особенно не стоит читать как чистое измерение скорости протокола. Все попытки, включая таймауты, остались в расчётах.&lt;p&gt;Для меня итог этого контроля такой: более содержательное O заметно помогло Paper2 при больших лимитах, но само по себе не спасло при малых. Обсуждать только формулу памяти без того, что фактически доходит до модели, оказалось недостаточно. Дальше хочется разбирать эти ограничения по одному.&lt;p&gt;&lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/codex-paper2-small-context-20260910/REPORT.md rel=&#34;noopener nofollow&#34;&gt;Отчёт малого Paper2&lt;/a&gt;, &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/codex-paper2-small-context-20260910/statistics.json rel=&#34;noopener nofollow&#34;&gt;данные сравнения&lt;/a&gt; и &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/codex-paper2-small-context-20260910/trajectory-audit.json rel=&#34;noopener nofollow&#34;&gt;аудит завершений и усечений&lt;/a&gt;.&lt;h3&gt;Результаты&lt;/h3&gt;&lt;p&gt;Теперь можно собрать картину целиком. За все серии получилось 820 итоговых исходов. По дороге менялись модели, окружение и ограничения памяти, поэтому сводить всё к одному проценту экономии я бы не стал. Но посмотреть, что в итоге получилось у каждого из пяти вариантов, уже можно.&lt;h4&gt;Сравнение&lt;/h4&gt;&lt;p&gt;Начну со сводки Codex с расширенными лимитами: здесь у нас есть результаты всех пяти вариантов на Sol и Astra, по 25 сессий на каждое сочетание. В ячейках — &lt;strong&gt;вход в миллионах токенов и число полностью успешных сессий из 25&lt;/strong&gt;. Как и выше, полный успех требует и прохождения проверок, и штатного завершения; расход включает неудачные запуски. Считаем пока основной агентный цикл.&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;&lt;p align=left&gt;Модель&lt;th&gt;&lt;p align=left&gt;Native&lt;th&gt;&lt;p align=left&gt;Paper&lt;th&gt;&lt;p align=left&gt;Paper2&lt;th&gt;&lt;p align=left&gt;V2&lt;th&gt;&lt;p align=left&gt;V3&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Sol&lt;td&gt;&lt;p align=left&gt;6,367; 24/25&lt;td&gt;&lt;p align=left&gt;9,215; 19/25&lt;td&gt;&lt;p align=left&gt;4,602; 23/25&lt;td&gt;&lt;p align=left&gt;4,077; 23/25&lt;td&gt;&lt;p align=left&gt;4,180; 25/25&lt;tr&gt;&lt;td&gt;&lt;p align=left&gt;Astra&lt;td&gt;&lt;p align=left&gt;2,734; 25/25&lt;td&gt;&lt;p align=left&gt;15,318; 16/25&lt;td&gt;&lt;p align=left&gt;4,395; 23/25&lt;td&gt;&lt;p align=left&gt;2,654; 25/25&lt;td&gt;&lt;p align=left&gt;2,340; 25/25&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Важная деталь: Native, Paper, V2 и V3 запускались 8–9 сентября, а Paper2 — 10 сентября. Настройки и задачи те же, но одновременного контроля для Paper2 не было. Таблица собирает эти результаты рядом для удобства; подробности каждой серии и сравнения с малыми лимитами остались выше.&lt;p&gt;&lt;strong&gt;Native&lt;/strong&gt; оказался хорошей точкой отсчёта. В десяти повторах Sol он завершил с полным успехом 49 из 50 сессий, в следующих сериях — 25 из 25 и 24 из 25. На Astra в обеих сериях — 25 из 25. При прежних ограничениях памяти Astra в Native была ещё и заметно экономнее всех state-вариантов: на проект ей хватало в среднем 5,44 обращения. После расширения лимитов V2 и V3 смогли приблизиться к ней по числу циклов и потратить меньше входных токенов. Так что первое впечатление о безусловном преимуществе Native на этих коротких задачах пришлось пересмотреть.&lt;p&gt;&lt;strong&gt;Paper&lt;/strong&gt; чаще других упирался в лишние циклы. Уже в первом сравнении Codex и Sol, и Terra довели до &lt;code&gt;finish&lt;/code&gt; только по два проекта из пяти. В десяти повторах Sol получилось 30 полных успехов из 50, а в серии без глобальных скиллов — 8 из 25. На Astra с малыми лимитами не завершилась ни одна из 25 сессий, хотя 24 проекта прошли все проверки. Расширение лимитов заметно помогло обеим моделям, но расход Paper всё равно остался выше Native, и таймауты никуда не исчезли.&lt;p&gt;&lt;strong&gt;Paper2&lt;/strong&gt; заставил меня иначе посмотреть на эту историю. Одно наблюдение с действием, аргументами и статусом при больших лимитах дало по 23 полных успеха из 25 на обеих моделях. Относительно Paper вход уменьшился на 50,1% у Sol и на 71,3% у Astra. При этом Sol потратила на 27,7% меньше входа, чем Native, а Astra — на 60,8% больше. Даже у такого небольшого изменения результат зависит от модели. И всё это относится к большим лимитам: после их уменьшения у Sol осталось 18 полных успехов, у Astra — ни одного, хотя весь её код прошёл тесты. Вход вырос на 51,1% и в 7,2 раза соответственно. Защита JSON при этом сохранилась. Похоже, понимать, какое действие только что выполнил, полезно, но агенту ещё нужно получить достаточно информации о его результате.&lt;p&gt;&lt;strong&gt;V2&lt;/strong&gt; на Sol мне по-прежнему нравится больше всего по сочетанию расхода и качества. В десяти повторах она использовала на 49,7% меньше входа, чем Native, при 48 полных успехах из 50. Без глобальных скиллов экономия составила 31,0%, с расширенными лимитами — 36,0%. Условия менялись, направление сохранилось. Правда, в последней серии V2 завершила с полным успехом 23 сессии из 25, Native — 24, а V3 — все 25; разницу в одну-две сессии здесь ещё нужно проверять на новых задачах. На Astra при прежних лимитах V2 заметно проигрывала Native, зато с расширенными почти сравнялась по входу: на 2,9% меньше при тех же 25 полных успехах.&lt;p&gt;&lt;strong&gt;V3&lt;/strong&gt; дала самый неоднозначный результат. В первом круге из трёх попыток она в сумме оказалась дороже V2 и на Sol, и на Terra, а ожидаемого сокращения циклов почти не было. В следующих десяти повторах Sol вход вырос относительно V2 на 33,4%, число обращений — на 24,3%. После удаления глобальных скиллов V3 тоже оставалась дороже V2. С расширенными лимитами картина стала интереснее: на Sol V3 сделала 225 обращений против 235 у V2, потратила на 2,5% больше входа и завершила все 25 сессий с полным успехом. На Astra у неё уже лучший расход в этой сводке — на 14,4% меньше Native, при почти одинаковом числе обращений и тех же 25 полных успехах. Пока хочется разобраться, какую роль здесь сыграли сами батчи, а какую — более вместительное окно наблюдений.&lt;p&gt;Опыты с OpenCode тоже стоит держать в голове. На Sol V2 прошла все 40 проверок и потратила на 62,0% меньше входа, чем Native. На Terra каждый state-вариант использовал больше токенов и прошёл меньше проверок, чем штатный агент. В первоначальном Codex-сравнении V2 и V3 снизили вход на обеих моделях при том же суммарном балле. Но это были одиночные запуски, а Paper2 на OpenCode мы не проверяли. Считать, что результаты поздних серий Codex автоматически перенесутся на другое ядро, пока рано.&lt;p&gt;Общий график первых запусков оставлю здесь как дополнение: он показывает, насколько уже тогда различались задачи и два агента. На нём четыре первоначальных режима; поздние серии Paper2 показаны на своих графиках выше.&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/webt/61/10/85/61108541b44585a432d2796ecf5883a5.png alt=&#34;Общий график четырёх режимов: input относительно Native слева, обращения на задачу справа&#34; sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/webt/61/10/85/61108541b44585a432d2796ecf5883a5.png 780w,&#xA;       https://habrastorage.org/r/w1560/webt/61/10/85/61108541b44585a432d2796ecf5883a5.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Общий график четырёх режимов: input относительно Native слева, обращения на задачу справа&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Слева расход нормирован относительно Native: его серые столбцы стоят на уровне 1×, а шкала логарифмическая. Справа — число обращений на обычной линейной шкале. Штриховка и &lt;code&gt;!&lt;/code&gt; означают провал проверок, таймаут или отсутствие &lt;code&gt;finish&lt;/code&gt;, в том числе у Native. Так видно, где низкий расход сопровождался неудачей. &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/articles/figures/input-by-task.png rel=&#34;noopener nofollow&#34;&gt;Прежний общий рисунок только с input&lt;/a&gt; также сохранён отдельно.&lt;p&gt;Для меня итог такой: структурированная память на этих задачах действительно может экономить вход. Но одной удачной формулы состояния оказалось недостаточно. Имеет значение, какие сведения получает модель после действия, сколько из них мы обрезаем и сколько лишних циклов из-за этого появляется. Особенно хорошо это видно по Paper2 и по развороту результатов Astra после расширения лимитов. Теперь хочется менять эти вещи по одной и проверять на новых задачах — пяти знакомых спецификаций для общих выводов всё-таки мало.&lt;h3&gt;Токены — ещё не стоимость&lt;/h3&gt;&lt;p&gt;Когда я смотрю на эти результаты, мне помогает держать перед глазами простое равенство:&lt;pre&gt;&lt;code&gt;накопленный input = число обращений × средний input на обращение&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Математически здесь нет ничего нового, но это равенство не даёт увлечься одним только размером контекста. Если агент начинает чаще перечитывать файлы и перепроверять сделанное, дополнительные обращения легко съедают экономию на каждом шаге. Возможна и обратная ситуация: запрос становится чуть тяжелее, зато модель быстрее заканчивает работу.&lt;p&gt;При этом системные инструкции и описания инструментов остаются в запросе при любом варианте памяти. На короткой задаче они могут занимать больше места, чем сама история, так что замена истории состоянием затрагивает лишь часть контекста.&lt;p&gt;Но сэкономленные токены ещё не означают такую же экономию в деньгах. Значительная часть повторно отправляемой истории может попадать в кэш, поэтому один только общий input не показывает, сколько стоил запуск. В отчётах сохранены и кэшированный вход, и выход, и другие счётчики. Складывать их без проверки нельзя: у OpenCode и Codex они устроены по-разному, а токены рассуждений, например, уже могут входить в выходные токены.&lt;p&gt;Есть ещё один расход, который не виден в основной таблице: у Codex работает отдельная модель, автоматически проверяющая разрешения на действия. Её вызовы идут в отдельной сессии, и &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/article-20260904/AUXILIARY-USAGE.md rel=&#34;noopener nofollow&#34;&gt;их учёт я вынес в отдельный отчёт&lt;/a&gt;. Если добавить зарегистрированный вход этого рецензента, для Sol меняется даже направление сравнения: в первоначальном круге из трёх попыток основной цикл V3 использовал на 4,0% больше входа, чем V2, а вместе с рецензентом получилось на 8,4% меньше. Для Terra суммарный вход с рецензентом, наоборот, оказался на 16,9% выше у V3.&lt;p&gt;В последующих десяти повторах Sol добавление зарегистрированных вспомогательных вызовов уже не меняет направление вывода: V2 — 10,969 млн входных токенов, V3 — 14,057 млн, то есть V3 расходует на 28,2% больше. У Native получилось 20,131 млн, у Paper — 32,788 млн. Это другой набор запусков; его &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/codex-sol-controls-20260906/AUXILIARY-USAGE.md rel=&#34;noopener nofollow&#34;&gt;расход на вспомогательные вызовы&lt;/a&gt; учтён отдельно.&lt;p&gt;И всё это имеет смысл, лишь пока удаётся удержать достойное качество результата.&lt;h3&gt;А что здесь зависит от модели и длины задачи?&lt;/h3&gt;&lt;p&gt;После экспериментов с Astra мне захотелось посмотреть на всё это чуть с другой стороны. Кажется очевидным, что сокращать историю особенно выгодно там, где она успевает разрастись. Но «длинная задача» — характеристика не только самой задачи, а ещё и модели, которая за неё взялась. То, над чем одна модель будет работать несколько десятков циклов, другая может сделать буквально в один чих.&lt;p&gt;Здесь можно позволить себе немного математики. Пусть B — постоянная часть запроса, h — средний объём, добавляемый в историю за шаг, а T — число обращений. Если история накапливается без сжатия, получаем простую прикидку:&lt;pre&gt;&lt;code&gt;контекст на шаге t ≈ B + h(t − 1)&#xA;суммарный input Native ≈ BT + hT(T − 1)/2&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Именно суммарный вход за весь запуск растёт квадратично; размер отдельного контекста растёт линейно. Если же состояние, наблюдения и инструкции протокола занимают ограниченное место M сверх той же постоянной части B, при том же числе шагов получится:&lt;pre&gt;&lt;code&gt;суммарный input state ≈ T(B + M)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Это та самая &lt;a href=https://arxiv.org/html/2608.26263v2#S3.SS3 rel=&#34;noopener nofollow&#34;&gt;асимптотика из статьи&lt;/a&gt;: O(T²) против O(T), при ограниченном размере состояния и наблюдений. В этой упрощённой модели state начинает окупаться при T &amp;gt; 1 + 2M/h. Порог зависит от M и h: сначала нужно отбить накладные расходы. На короткой истории экономить ещё особенно нечего, а описание протокола и ведение состояния уже стоят токенов. Поэтому здесь такой подход скорее рискует навредить, чем автоматически дать выигрыш. Впрочем, Sol/V2 показывает, что и на наших небольших задачах экономия возможна при удачном соотношении накладных расходов и сохранённой истории.&lt;p&gt;Есть существенное «но»: число шагов у вариантов не обязано совпадать. Если агент в state-режиме теряет детали, заново читает файлы или не понимает, что уже пора заканчивать, сравнивать приходится не два выражения с одним T, а T_native и T_state. Более короткий запрос ничего не гарантирует, если таких запросов потребовалось в несколько раз больше. Да и реальный Native не обязан бесконечно накапливать историю: сжатие и усечение меняют объём передаваемого контекста, а кэширование влияет на стоимость, о которой говорили выше.&lt;p&gt;Первый результат Astra хорошо укладывался в эту картину. В Native наши экспериментальные задачки оказались для неё очень короткими: в среднем 5,44 обращения против 11,8 у Sol. История ещё не успела толком разрастись, а в state-вариантах уже возникли дополнительные циклы. С прежними лимитами штатный агент действительно оказался наголову эффективнее.&lt;p&gt;Но результаты с расширенными лимитами уже не укладываются в такое простое объяснение. Задачи не стали длиннее для Astra: Native по-прежнему уложился в среднем в 5,6 обращения. Зато V3 теперь понадобилось почти столько же, и она уже сэкономила input. Похоже, важно не только то, сколько шагов модели нужно для решения, но и то, сколько лишних шагов мы заставляем её делать выбранной схемой памяти. Пока это только гипотеза: влияние изменений мы не разделили, инструменты вызываются по-разному — через Code Mode у Native и напрямую в state-режимах, — да и с завершением работы варианты справляются неодинаково.&lt;p&gt;Малый Paper2 добавляет к этому ещё одну оговорку. Те же задачи, та же модель, то же единственное структурированное O — но после уменьшения лимитов число обращений выросло в несколько раз. Это не значит, что задача стала содержательно сложнее. Длинная траектория может оказаться не свойством задачи, а следствием того, как агенту возвращают результаты собственной работы. Такую длину не стоит путать с тем полезным длинным горизонтом, ради которого и хочется экономить историю.&lt;p&gt;Сравнить Native с Paper, Paper2, V2, V3 или другим вариантом на задаче, длинной уже для самой Astra, нам ещё предстоит. Таких данных у нас просто нет. Обещать, что state на длинной задаче обязательно выиграет, тоже пока нельзя: нужно ещё удержать качество и не растянуть решение потерями памяти.&lt;p&gt;Дальше мне хочется понять, на какой длине решения и при какой схеме памяти этот подход начинает окупаться для конкретной модели. По нашим пяти проектам делать общие выводы о влиянии интеллекта нельзя, но, по-моему, уже есть о чём поразмышлять и что проверять дальше.&lt;h3&gt;Что хочется проверить дальше&lt;/h3&gt;&lt;p&gt;После этих опытов у меня не появилось желания списать привычный агентный цикл с историей: он по-прежнему хорошо работает, а явное состояние далеко не везде даёт преимущество. Но и сама идея мне не разонравилась. Возможность явно определить, что агент помнит между шагами, кажется полезным инструментом — просто со своими издержками и ограничениями, которые ещё предстоит понять.&lt;p&gt;Из собственных вариантов я бы пока продолжил работу с V2: в нескольких сериях этот вариант дал интересный баланс расхода и качества. V3 при прежних лимитах на Sol ожидаемого улучшения не принесла, зато с расширенными уже немного сократила число циклов, почти не уступая V2 по input. А на Astra этот вариант оказался экономнее и V2, и Native при полном успехе во всех 25 сессиях. Это не делает V3 универсальным победителем, но и отбрасывать её после первых повторов было бы рано.&lt;p&gt;По пяти небольшим проектам, написанным с нуля на конкретных настройках, нельзя заключить ни что SKILL.state годится только для узкого класса задач, ни что мои дополнения универсально лучше. Зато появились более интересные вопросы. В какой момент структурированного состояния перестаёт хватать? Что делать с наблюдением, важность которого становится понятна лишь через несколько шагов? Сколько недавней истории нужно оставить, чтобы агент не забывал о начатом, и насколько ответ зависит от доступных ему инструментов?&lt;p&gt;На следующем этапе хочется проверять изменения по одному: отдельно менять состав O, размер окна и объединение действий, оставляя одинаковый бюджет наблюдений. Затем добавить новые задачи и повторы — причём задачи должны быть длинными для той модели, которую проверяем. Глобальные скиллы и инструкции в дополнительных сериях мы уже убрали. Ещё нужно уравнять доступные инструменты и способ их вызова, по отдельности проверить лимиты на аргументы действий и их результаты, а также строже изолировать окружение. Тогда, возможно, получится разобраться, почему подход помогает или мешает и где проходят границы его применимости.&lt;p&gt;Код, спецификации и журналы собраны в &lt;a href=https://github.com/Rexarrior/skill-state-research rel=&#34;noopener nofollow&#34;&gt;skill-state-research&lt;/a&gt;. Для тех, кто захочет разобраться глубже, есть &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/article-20260904/REPORT.md rel=&#34;noopener nofollow&#34;&gt;полный отчёт основной серии&lt;/a&gt;, &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/experiments/article-20260904/PROTOCOL.md rel=&#34;noopener nofollow&#34;&gt;условия эксперимента&lt;/a&gt; и &lt;a href=https://github.com/Rexarrior/skill-state-research/blob/ac9099c022c14eef41d92dff6eaf8c2081fa2520/journals/RESEARCH-ARTICLE-PLAN.md rel=&#34;noopener nofollow&#34;&gt;план научного продолжения&lt;/a&gt;. Мне по-прежнему хочется довести эту историю до полноценной научной статьи — теперь уже с более точными вопросами и экспериментами, которые позволят на них ответить.&lt;h3&gt;Вывод&lt;/h3&gt;&lt;p&gt;Идея хранить состояние вместо всей истории оказалась вполне рабочей на части наших задач. При этом экономия сильно зависела от модели и от того, какую информацию она получала между шагами. Для меня эксперимент точно стоил потраченного времени: хочется продолжить, уже проверяя каждое изменение отдельно и на более разнообразных задачах.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <author>Rexarrior</author>
      <guid>https://habr.com/ru/articles/1082120/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082120</guid>
      <pubDate>Mon, 14 Sep 2026 14:00:08 +0000</pubDate>
    </item>
    <item>
      <title>Claude Code читает не страницу, а её пересказ: прошёл путь одного запроса по документации Claude</title>
      <link>https://habr.com/ru/articles/1082118/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082118</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/a78/9b5/2e6/a789b52e6f32b00177ea64994e2fae22.png width=1672 height=941 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/a78/9b5/2e6/a789b52e6f32b00177ea64994e2fae22.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/a78/9b5/2e6/a789b52e6f32b00177ea64994e2fae22.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Совет оптимизировать сайт под Claude смешивает два инструмента Anthropic: Claude и агента Claude Code. Profound снял 24 135 ответов на одних запросах, и у двух инструментов разошёлся почти каждый показатель. Я прошёл по документации путь запроса в обоих инструментах. Для владельца сайта из разбора следуют три вещи: какие боты Anthropic заходят на сайт, что закрыть в robots.txt и как проверить заходы по access-логу.&lt;h3&gt;Откуда данные: документация Anthropic и открытые исследования&lt;/h3&gt;&lt;p&gt;Своих прогонов под эту статью я не делал, картина собрана по открытым источникам. Claude здесь и дальше означает приложение &lt;a href=http://claude.ai rel=&#34;noopener nofollow&#34;&gt;claude.ai&lt;/a&gt; и программный доступ к той же ИИ-модели через Messages API, Claude Code — агента, которого разработчик запускает в терминале для работы с кодом. Profound снимал приложение &lt;a href=http://claude.ai rel=&#34;noopener nofollow&#34;&gt;claude.ai&lt;/a&gt;, а параметры поиска и формат цитат взяты из документации Messages API: для приложения Anthropic их не описывает. Источников три группы: документация Anthropic (&lt;a href=http://platform.claude.com rel=&#34;noopener nofollow&#34;&gt;platform.claude.com&lt;/a&gt;, &lt;a href=http://code.claude.com rel=&#34;noopener nofollow&#34;&gt;code.claude.com&lt;/a&gt;, &lt;a href=http://support.claude.com rel=&#34;noopener nofollow&#34;&gt;support.claude.com&lt;/a&gt;), разбор Profound &lt;a href=https://www.tryprofound.com/blog/claude-and-claude-code-are-distinct-answer-engines rel=&#34;noopener nofollow&#34;&gt;«Claude and Claude Code are distinct answer engines»&lt;/a&gt; от 24 августа 2026 года (answer engine — система, которая отвечает вместо выдачи ссылок) и два исследования серверных логов, &lt;a href=https://ahrefs.com/blog/llmstxt-study/ rel=&#34;noopener nofollow&#34;&gt;Ahrefs&lt;/a&gt; и &lt;a href=https://saaslinks.net/blog/llms-txt-server-log-study rel=&#34;noopener nofollow&#34;&gt;Saaslinks&lt;/a&gt;, о том, кто запрашивает у сайтов файл llms.txt.&lt;p&gt;Выборка Profound — 24 135 ответов на 1 724 запроса из 11 категорий, снятых с 13 по 23 июля 2026 года, веб-поиск включён у всех систем; отдельная подвыборка про программирование — 2 800 ответов на 200 запросов. Ещё компания разобрала топ-1000 страниц, которые агенты обеих систем посетили с 18 июля по 18 августа, и разложила их по категориям нейросетью GPT-4.1-mini, а не вручную. Страницы взяты с доменов, которые Profound отслеживает у себя (в оригинале internally tracked domains), и выборка, вероятно, смещена к SaaS и технологическим компаниям: переносить эти цифры на интернет-магазин или медицинский сайт без проверки я бы не стал.&lt;h3&gt;Что происходит, когда Claude ищет через API: цитата с адресом и куском текста&lt;/h3&gt;&lt;p&gt;В выборке Profound приложение &lt;a href=http://claude.ai rel=&#34;noopener nofollow&#34;&gt;claude.ai&lt;/a&gt; искало в вебе больше чем в 93% ответов. Это цифра одной выборки: другое исследование, со своим набором запросов и счётом по промптам, даёт для Claude 36,6%. По &lt;a href=https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool rel=&#34;noopener nofollow&#34;&gt;документации веб-поиска Messages API&lt;/a&gt; ИИ-модель сама решает, вызывать ли инструмент для конкретного вопроса. Владельцу сайта из параметров поиска важен один: разработчик приложения на API может сузить поиск списком разрешённых (&lt;code&gt;allowed_domains&lt;/code&gt;) или запрещённых (&lt;code&gt;blocked_domains&lt;/code&gt;) доменов, и тогда сайт вне списка такое приложение не найдёт, как бы хорошо он ни был написан.&lt;p&gt;Найденные страницы приходят отдельным блоком &lt;code&gt;web_search_tool_result&lt;/code&gt; — списком с адресом и заголовком каждой. В тексте ответа Claude расставляет цитаты типа &lt;code&gt;web_search_result_location&lt;/code&gt;, и каждая несёт адрес (&lt;code&gt;url&lt;/code&gt;), заголовок (&lt;code&gt;title&lt;/code&gt;), зашифрованный указатель на источник и кусок процитированного текста до 150 символов (&lt;code&gt;cited_text&lt;/code&gt;). Поля &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt; и &lt;code&gt;cited_text&lt;/code&gt; в токенах не учитываются. Следующий фрагмент нужен тому, кто строит приложение на API: так со стороны кода выглядит разбор уже полученного ответа, без единого сетевого запроса.&lt;pre&gt;&lt;code&gt;def extract_cited_urls(response_content):&#xA;    &amp;#34;&amp;#34;&amp;#34;Уникальные цитаты web_search_result_location&#xA;    из response.content ответа Messages API.&amp;#34;&amp;#34;&amp;#34;&#xA;    seen = set()&#xA;    citations = []&#xA;    for block in response_content:&#xA;        for c in block.get(&amp;#34;citations&amp;#34;) or []:&#xA;            if c.get(&amp;#34;type&amp;#34;) != &amp;#34;web_search_result_location&amp;#34;:&#xA;                continue&#xA;            url = c.get(&amp;#34;url&amp;#34;)&#xA;            if not url or url in seen:&#xA;                continue&#xA;            seen.add(url)&#xA;            citations.append({&#xA;                &amp;#34;url&amp;#34;: url,&#xA;                &amp;#34;title&amp;#34;: c.get(&amp;#34;title&amp;#34;, &amp;#34;&amp;#34;),&#xA;                &amp;#34;cited_text&amp;#34;: c.get(&amp;#34;cited_text&amp;#34;, &amp;#34;&amp;#34;),&#xA;            })&#xA;    return citations&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:87px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Функция проходит по блокам &lt;code&gt;response.content&lt;/code&gt;, берёт из них цитаты веб-поиска и отбрасывает повторы одного адреса. Если страница есть в блоке результатов, но её &lt;code&gt;url&lt;/code&gt; нет среди цитат, поиск её нашёл, а в текст ответа она не попала. У Messages API есть и второй инструмент, web fetch tool, который забирает содержимое страницы по адресу. По &lt;a href=https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-fetch-tool rel=&#34;noopener nofollow&#34;&gt;его документации&lt;/a&gt; он открывает только адреса, которые уже встречались в разговоре: в сообщении пользователя или в результатах прошлого поиска. Ссылку, которую ИИ-модель сочинила сама, он не откроет — так Anthropic защищается от утечки данных. JavaScript этот инструмент не выполняет, для страниц, которые собирает браузер, у Anthropic есть отдельный browser use tool.&lt;p&gt;На какие сайты приходятся цитаты Claude, посчитала &lt;a href=https://otterly.ai/blog/claude-ai-citation-study/ rel=&#34;noopener nofollow&#34;&gt;OtterlyAI&lt;/a&gt;: 379 321 цитата по 16 406 доменам, июнь 2026 года, запросы из SaaS и tech. Сайты брендов и продуктов собрали 64,0% цитат, новости и медиа — 14,9%. На топ-10 доменов пришлось 9,5% всех цитат, на топ-500 — 59,9%, а 32,9% доменов процитированы ровно один раз.&lt;h3&gt;Когда Claude Code открывает страницу, до основной ИИ-модели доходит пересказ&lt;/h3&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/740/cd2/f9f/740cd2f9fb7637af6487fc6f15cb8b98.png alt=&#34;У Claude в API путь заканчивается цитатой с куском исходного текста, у Claude Code — ответом вспомогательной ИИ-модели.&#34; title=&#34;У Claude в API путь заканчивается цитатой с куском исходного текста, у Claude Code — ответом вспомогательной ИИ-модели.&#34; width=1500 height=850 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/740/cd2/f9f/740cd2f9fb7637af6487fc6f15cb8b98.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/740/cd2/f9f/740cd2f9fb7637af6487fc6f15cb8b98.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;У Claude в API путь заканчивается цитатой с куском исходного текста, у Claude Code — ответом вспомогательной ИИ-модели.&lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Claude Code ищет редко: в той же выборке Profound веб-поиск сработал в 13% ответов. Поиск здесь один инструмент, WebSearch, и по &lt;a href=https://code.claude.com/docs/en/tools-reference rel=&#34;noopener nofollow&#34;&gt;справочнику инструментов Claude Code&lt;/a&gt; его разрешают или запрещают целиком, без правил для отдельных доменов.&lt;p&gt;Страницу Claude Code открывает инструментом WebFetch, и до основной ИИ-модели она, как правило, доходит уже переработанной. По справочнику, WebFetch получает адрес и промпт с описанием, что нужно извлечь, страницу по этому промпту читает вспомогательная ИИ-модель поменьше, и в большинстве случаев основная получает её ответ, а не исходную страницу. Длинные страницы перед обработкой обрезаются до фиксированного числа символов, а сама документация называет инструмент lossy by design: детали он теряет намеренно.&lt;p&gt;Исключение описано не у Anthropic, а в стороннем &lt;a href=https://giuseppegurgone.com/claude-webfetch rel=&#34;noopener nofollow&#34;&gt;разборе кода Claude Code&lt;/a&gt; от января 2026 года. HTML там переводится в Markdown библиотекой Turndown, а пересказ пропускается при двух условиях сразу: домен входит в зашитый список примерно из 80 сайтов документации (Python Docs, MDN, React, Kubernetes, AWS), и страница отдаётся как &lt;code&gt;text/markdown&lt;/code&gt; короче примерно 100 тысяч символов. Обычный сайт в этот список не входит, поэтому его страницу Claude Code получает пересказом, в каком бы формате она ни была. Anthropic списка не публикует, и он может смениться с любой версией.&lt;p&gt;Остальное в справочнике касается в основном отладки агента: кэш ответа на 15 минут, пятиминутный срок загрузки, переменные окружения. Для сайта важны две детали. Редирект на другой домен WebFetch сам не проходит: он возвращает исходный и новый адрес, и агент должен повторить запрос. А правилом &lt;code&gt;WebFetch(domain:&lt;/code&gt;&lt;a href=http://example.com rel=&#34;noopener nofollow&#34;&gt;&lt;code&gt;example.com&lt;/code&gt;&lt;/a&gt;&lt;code&gt;)&lt;/code&gt; в списке &lt;code&gt;permissions.deny&lt;/code&gt; файла settings.json разработчик может запретить агенту конкретный сайт, даже если robots.txt этого сайта открыт.&lt;h3&gt;Разницу между Claude и Claude Code видно и по ответам&lt;/h3&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/13a/9c1/c5a/13a9c1c5a33917e09eb43d950863f92d.png alt=&#34;Выборка Profound, июль 2026: одни и те же 1 724 запроса, два разных набора чисел на выходе.&#34; title=&#34;Выборка Profound, июль 2026: одни и те же 1 724 запроса, два разных набора чисел на выходе.&#34; width=1500 height=850 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/13a/9c1/c5a/13a9c1c5a33917e09eb43d950863f92d.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/13a/9c1/c5a/13a9c1c5a33917e09eb43d950863f92d.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Выборка Profound, июль 2026: одни и те же 1 724 запроса, два разных набора чисел на выходе.&lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;&lt;a href=https://www.tryprofound.com/blog/claude-and-claude-code-are-distinct-answer-engines rel=&#34;noopener nofollow&#34;&gt;Разбор Profound&lt;/a&gt; показывает ту же разницу со стороны ответов. На одних и тех же 1 724 запросах Claude Code в среднем называет 6,6 бренда за ответ, Claude — 5,2, и совпадает у них лишь один бренд из пяти. Длина ответа тоже разная: в среднем 322 слова у Claude Code и 459 у Claude.&lt;p&gt;Сильнее всего различается форма ответа. По данным Profound, Claude Code вставляет списки в 94% ответов и таблицы в 54%. У Claude списки есть в 56% ответов, таблицы — в 11%. Причин такой разницы Profound не называет, в разборе приведены только доли.&lt;p&gt;Разные и страницы, на которые ходят агенты. В топ-1000 страниц, посещённых с 18 июля по 18 августа, почти три четверти визитов Claude Code пришлись на документацию. У Claude 60% визитов — это robots.txt, карта сайта (sitemap) и главные страницы.&lt;p&gt;Одна цифра в открытых источниках не сходится. Profound на июльской выборке получил поиск больше чем в 93% ответов Claude. Другое исследование, представленное на сессии Profound Zero Click и &lt;a href=https://searchengineland.com/claude-visibility-brave-search-rankings-480053 rel=&#34;noopener nofollow&#34;&gt;пересказанное Search Engine Land&lt;/a&gt; в июне 2026 года, даёт для Claude 36,6% промптов с поиском при примерно 90% у ChatGPT. Там же частота поиска разложена по типам запросов: 81% у вопросов о свежих событиях, 67% у рейтингов, 55% у запросов с привязкой к месту и 51% у сравнений. Profound считает долю ответов, второе исследование — долю промптов, выборки и периоды у них разные, поэтому к одному числу эти данные не сводятся. Сравнивать 13% у Claude Code корректно только с цифрой Profound: обе сняты на одной выборке.&lt;p&gt;Сам Profound выводит из этих данных, что под два инструмента нужно оптимизировать раздельно. Для Claude Code компания советует машиночитаемую документацию и точные технические характеристики, для Claude — брендовый контент широкого охвата.&lt;h3&gt;На сайт заходят три разных бота Anthropic&lt;/h3&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/56a/768/c6d/56a768c6d12678003381ec13db33dc6c.png alt=&#34;Правило в robots.txt действует только на того бота, чьё имя в нём стоит.&#34; title=&#34;Правило в robots.txt действует только на того бота, чьё имя в нём стоит.&#34; width=1500 height=850 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/56a/768/c6d/56a768c6d12678003381ec13db33dc6c.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/56a/768/c6d/56a768c6d12678003381ec13db33dc6c.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;&lt;em&gt;Правило в robots.txt действует только на того бота, чьё имя в нём стоит.&lt;/em&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;У Anthropic три краулера с разными именами и задачами, и правило в robots.txt для одного из них не действует на двух других. По &lt;a href=https://support.claude.com/en/articles/8896518-does-anthropic-crawl-data-from-the-web-and-how-can-site-owners-block-the-crawler rel=&#34;noopener nofollow&#34;&gt;документации поддержки Anthropic&lt;/a&gt;, обновлённой 7 апреля 2026 года, ClaudeBot собирает контент для обучения ИИ-моделей, и его блокировка исключает сайт из будущих обучающих наборов. Claude-User забирает страницу, когда о ней спрашивает конкретный пользователь Claude, и его блокировка снижает видимость сайта в ответах на такие вопросы. Claude-SearchBot индексирует контент для качества поиска, и без него, по той же документации, может упасть точность цитирования.&lt;p&gt;Все три бота соблюдают robots.txt, включая Claude-User. По &lt;a href=https://www.searchenginejournal.com/anthropics-claude-bots-make-robots-txt-decisions-more-granular/568253/ rel=&#34;noopener nofollow&#34;&gt;разбору Search Engine Journal&lt;/a&gt; это отличает Anthropic от OpenAI, у которой ChatGPT-User по её же документации может файл не соблюдать. Правила пишутся на каждого бота и на каждый поддомен отдельно. Кто хочет остаться в ответах Claude, но не отдавать тексты на обучение, закрывает только ClaudeBot:&lt;pre&gt;&lt;code&gt;User-agent: ClaudeBot&#xA;Disallow: /&#xA;&#xA;User-agent: Claude-User&#xA;Allow: /&#xA;&#xA;User-agent: Claude-SearchBot&#xA;Allow: /&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Группы с &lt;code&gt;Allow: /&lt;/code&gt; для Claude-User и Claude-SearchBot нужны, если в файле есть общее правило &lt;code&gt;User-agent:&lt;/code&gt; с запретами: по &lt;a href=https://www.rfc-editor.org/rfc/rfc9309 rel=&#34;noopener nofollow&#34;&gt;стандарту robots.txt (RFC 9309)&lt;/a&gt; бот, для которого есть группа со своим именем, общую группу не читает. Если общего запрета нет, эти две группы можно не писать. Блокировать ботов по IP-адресу Anthropic не советует: они работают на мощностях публичных облачных провайдеров. Список адресов в &lt;a href=https://claude.com/crawling/bots.json rel=&#34;noopener nofollow&#34;&gt;bots.json&lt;/a&gt; компания публикует, но надёжнее различать ботов по user-agent — подписи, которой бот представляется серверу.&lt;p&gt;С user-agent инструмента WebFetch в Claude Code как раз ничего не ясно: какой подписью он ходит на внешние сайты, Anthropic не публикует. Это может быть тот же Claude-User или отдельная строка, а независимые каталоги ботов расходятся даже в строке Claude-User. Ответ для своего сайта даёт только собственный лог.&lt;h3&gt;Боты Anthropic llms.txt почти не читают&lt;/h3&gt;&lt;p&gt;Файл llms.txt лежит по адресу /llms.txt и коротко описывает сайт для ИИ-моделей: ссылки на главные страницы в формате, удобном для машинного чтения. У &lt;a href=http://platform.claude.com rel=&#34;noopener nofollow&#34;&gt;platform.claude.com&lt;/a&gt; и &lt;a href=http://code.claude.com rel=&#34;noopener nofollow&#34;&gt;code.claude.com&lt;/a&gt; такой файл есть, со ссылкой на полную версию llms-full.txt, так что для собственной документации Anthropic этот формат держит.&lt;p&gt;На чужих сайтах его почти никто не запрашивает, в том числе боты Anthropic. &lt;a href=https://ahrefs.com/blog/llmstxt-study/ rel=&#34;noopener nofollow&#34;&gt;Ahrefs&lt;/a&gt; проверил 137 210 доменов в мае 2026 года и нашёл валидный llms.txt примерно у 38 000: у 97% из них за месяц к файлу не было ни одного обращения. Краулеры обучения всех компаний вместе дали 5,3% запросов к llms.txt, а ClaudeBot — 0,8% запросов внутри этой категории. Claude-User набрал 2,5% всех запросов к файлу. &lt;a href=https://saaslinks.net/blog/llms-txt-server-log-study rel=&#34;noopener nofollow&#34;&gt;Saaslinks&lt;/a&gt; разобрал логи одного сайта за две недели: ClaudeBot, Claude-SearchBot и Claude-User сделали 319, 96 и 8 запросов к /robots.txt и ни одного к /llms.txt.&lt;p&gt;Claude Code в исследовании Ahrefs назван среди примеров категории ИИ-агентов, и вся эта категория дала 10,5% запросов к llms.txt. По какой подписи Ahrefs опознавал агентов, в исследовании не раскрыто: сказано только, что все user-agent разложены на двенадцать категорий. Боты, от которых зависит ответ пользователю Claude в чате, файл почти не трогают. Если у проекта есть документация для разработчиков, llms.txt там уместен, Anthropic держит такой файл и у своей. Как способ попасть в ответ Claude на вопрос о бренде он по этим логам не работает.&lt;h3&gt;Как посмотреть в своём логе, кто из ботов Anthropic к вам заходит&lt;/h3&gt;&lt;p&gt;Какие боты Anthropic заходят на конкретный сайт, показывает только его собственный access-лог. У nginx стандартный формат журнала (combined log) содержит поле user-agent, и скрипт на Python считает по нему хиты трёх ботов и запрошенные пути. Официальной подписи WebFetch в Claude Code нет, поэтому все прочие user-agent со словом claude в любом регистре скрипт печатает отдельным списком: по нему видно, какие подписи с этим словом приходят на сайт.&lt;pre&gt;&lt;code&gt;from __future__ import annotations&#xA;&#xA;import re&#xA;import sys&#xA;from collections import Counter&#xA;&#xA;LOG_PATTERN = re.compile(&#xA;    r&amp;#39;(?P&amp;lt;ip&amp;gt;\S+) \S+ \S+ \[(?P&amp;lt;time&amp;gt;[^\]]+)\] &amp;#39;&#xA;    r&amp;#39;&amp;#34;(?P&amp;lt;method&amp;gt;\S+) (?P&amp;lt;path&amp;gt;\S+) \S+&amp;#34; &amp;#39;&#xA;    r&amp;#39;(?P&amp;lt;status&amp;gt;\d+) (?P&amp;lt;size&amp;gt;\S+) &amp;#39;&#xA;    r&amp;#39;&amp;#34;(?P&amp;lt;referer&amp;gt;[^&amp;#34;]*)&amp;#34; &amp;#34;(?P&amp;lt;agent&amp;gt;[^&amp;#34;]*)&amp;#34;&amp;#39;&#xA;)&#xA;&#xA;BOTS = (&amp;#34;ClaudeBot&amp;#34;, &amp;#34;Claude-User&amp;#34;, &amp;#34;Claude-SearchBot&amp;#34;)&#xA;&#xA;def classify(user_agent: str) -&amp;gt; str | None:&#xA;    for name in BOTS:&#xA;        if name in user_agent:&#xA;            return name&#xA;    return None&#xA;&#xA;def analyze(lines):&#xA;    hits: Counter[str] = Counter()&#xA;    paths_by_bot: dict[str, Counter[str]] = {}&#xA;    other_claude_agents: Counter[str] = Counter()&#xA;    for line in lines:&#xA;        match = LOG_PATTERN.match(line)&#xA;        if not match:&#xA;            continue&#xA;        agent = match.group(&amp;#34;agent&amp;#34;)&#xA;        bot = classify(agent)&#xA;        if bot:&#xA;            hits[bot] += 1&#xA;            paths_by_bot.setdefault(bot, Counter())[match.group(&amp;#34;path&amp;#34;)] += 1&#xA;        elif &amp;#34;claude&amp;#34; in agent.lower():&#xA;            other_claude_agents[agent] += 1&#xA;    return hits, paths_by_bot, other_claude_agents&#xA;&#xA;def main() -&amp;gt; None:&#xA;    if len(sys.argv) != 2:&#xA;        sys.exit(&amp;#34;Использование: python3 claude_log_stats.py access.log&amp;#34;)&#xA;    with open(sys.argv[1], encoding=&amp;#34;utf-8&amp;#34;, errors=&amp;#34;replace&amp;#34;) as f:&#xA;        hits, paths_by_bot, other_claude_agents = analyze(f)&#xA;    print(&amp;#34;Хиты по ботам Anthropic:&amp;#34;)&#xA;    for bot, count in hits.most_common():&#xA;        print(f&amp;#34;  {bot}: {count}&amp;#34;)&#xA;    print(&amp;#34;\nПути по ботам:&amp;#34;)&#xA;    for bot, paths in paths_by_bot.items():&#xA;        print(f&amp;#34;  {bot}: {dict(paths.most_common(5))}&amp;#34;)&#xA;    if other_claude_agents:&#xA;        print(&amp;#34;\nДругие user-agent с подстрокой claude:&amp;#34;)&#xA;        for agent, count in other_claude_agents.most_common():&#xA;            print(f&amp;#34;  {agent}: {count}&amp;#34;)&#xA;&#xA;if __name__ == &amp;#34;__main__&amp;#34;:&#xA;    main()&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Файл читается построчно за один проход и не загружается в память целиком, так что большой лог скрипт тоже переварит. Функция &lt;code&gt;classify&lt;/code&gt; ищет в user-agent имя одного из трёх ботов, &lt;code&gt;analyze&lt;/code&gt; раскладывает строки на хиты, пути и прочие подписи с claude, а &lt;code&gt;main&lt;/code&gt; без аргумента печатает подсказку и выходит с кодом 1. Строки, которые не разобрались по формату combined, пропускаются молча: если в логе nginx настроен свой формат, регулярное выражение придётся поправить под него.&lt;p&gt;Я проверил скрипт на шести синтетических строках, которые составил сам, это не трафик реального сайта. В них два захода ClaudeBot, по одному Claude-User и Claude-SearchBot, придуманный агент &lt;code&gt;example-agent/0.1 (claude-test)&lt;/code&gt; и обычный браузер. Вывод прогона:&lt;pre&gt;&lt;code&gt;Хиты по ботам Anthropic:&#xA;  ClaudeBot: 2&#xA;  Claude-User: 1&#xA;  Claude-SearchBot: 1&#xA;&#xA;Пути по ботам:&#xA;  ClaudeBot: {&amp;#39;/robots.txt&amp;#39;: 1, &amp;#39;/blog/article-1&amp;#39;: 1}&#xA;  Claude-User: {&amp;#39;/docs/pricing&amp;#39;: 1}&#xA;  Claude-SearchBot: {&amp;#39;/pricing&amp;#39;: 1}&#xA;&#xA;Другие user-agent с подстрокой claude:&#xA;  example-agent/0.1 (claude-test): 1&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Браузер в счёт не попал, придуманный агент ушёл в последний список. На синтетике это проверка того, что разбор работает, и больше ничего. На настоящем логе в последнем списке окажется подпись, которой к сайту приходит всё остальное с именем Claude, в том числе, возможно, WebFetch из Claude Code.&lt;h3&gt;Что по итогу разбора&lt;/h3&gt;&lt;p&gt;Какой из двух инструментов важнее, решает аудитория сайта. Если вас выбирают покупатели, спрашивая чат, это Claude и его боты Claude-User и Claude-SearchBot. Если вашей документацией пользуются разработчики, добавляется Claude Code. Из документации и открытых исследований для владельца сайта следуют пять действий, первые два делаются за десять минут без разработчика.&lt;ol&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Прописать robots.txt отдельно для ClaudeBot, Claude-User и Claude-SearchBot.&lt;/strong&gt; Правило для одного бота на других не действует. Чтобы остаться в ответах Claude, но не отдавать тексты на обучение, достаточно закрыть только ClaudeBot.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Проверить, что важные страницы отдаются без JavaScript.&lt;/strong&gt; Web fetch tool в Messages API не выполняет JavaScript, а WebFetch в Claude Code, по стороннему разбору кода, переводит в Markdown полученный HTML без выполнения скриптов. Если текст страницы собирает скрипт в браузере, оба инструмента его не увидят. Проверка простая: открыть страницу с отключённым JavaScript (в DevTools браузера это отдельная настройка). Что видно там, то видит и инструмент чтения.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Смотреть в собственный access-лог по user-agent.&lt;/strong&gt; У трёх ботов Anthropic фиксированные подписи, их считает любой разбор лога по этому полю, а неизвестные подписи с claude стоит выводить отдельным списком. Скрипт на 60 строк здесь хватит, но запускать его на сервере обычно будет разработчик или администратор.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Не считать llms.txt способом попасть в ответы Claude.&lt;/strong&gt; Боты, от которых зависят эти ответы, файл почти не запрашивают. У Saaslinks на логах одного сайта за две недели — ноль обращений от каждого из трёх, у Ahrefs 97% файлов на 38 000 доменов за месяц не получили ни одного обращения. Для документации, которую разработчики читают через Claude Code, llms.txt уместен.&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ставить главное в начало длинной страницы.&lt;/strong&gt; WebFetch обрезает длинные страницы до обработки, а затем пересказывает их вспомогательной ИИ-моделью. Markdown-версия обычному сайту пересказа не отменит: по стороннему разбору кода быстрый путь без пересказа есть только у примерно 80 зашитых сайтов документации.&lt;/ol&gt;&lt;p&gt;У разбора три ограничения. Цифры Profound сняты на доменах, которые компания отслеживает сама, вероятно в основном SaaS и tech, и для других ниш доли могут быть другими. Частота поиска у Claude расходится между источниками: больше 93% ответов у Profound и 36,6% промптов во втором исследовании, при разном счёте и разных выборках. User-agent, которым WebFetch в Claude Code ходит на внешние сайты, официально не описан, и узнать его можно только по своему логу.&lt;p&gt;Путь запроса в обоих инструментах описан в открытой документации почти целиком. В Messages API он заканчивается цитатой с адресом и куском исходного текста, у Claude Code — пересказом вспомогательной ИИ-модели, и формата цитат для него в документации Claude Code нет.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <author>sburyi</author>
      <guid>https://habr.com/ru/articles/1082118/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082118</guid>
      <pubDate>Mon, 14 Sep 2026 13:56:17 +0000</pubDate>
    </item>
    <item>
      <title>От поисковой строки к ИИ-агенту: зачем мы открыли Туту через MCP и что увидели на хакатоне</title>
      <link>https://habr.com/ru/companies/tuturu/articles/1081246/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1081246</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;p&gt;Представим обычную задачу: хочется куда-нибудь уехать на выходные. Есть бюджет, свободная суббота и воскресенье, желание оказаться у воды и условие вернуться домой до полуночи.&lt;p&gt;В привычном интерфейсе сначала приходится решить, куда ехать. Потом отдельно проверить билеты, посмотреть отели, сравнить время в пути и цены, вернуться к первому варианту, потому что второй оказался слишком дорогим, и заново собрать поездку.&lt;p&gt;Привет! Меня зовут Арут Агабабян, я CTO Туту и один из организаторов нашего ИИ-хакатона. В Туту я отвечаю за технологическое развитие компании и много работаю с тем, как новые технологии можно превращать в реальные пользовательские сценарии. В этой статье расскажу, зачем мы открыли сервисы Туту для ИИ-агентов через MCP, что получилось у участников хакатона и какие выводы мы сделали про будущее агентных решений в travel-tech.&lt;p&gt;Для ИИ-агента естественнее другой сценарий: человек описывает ограничения обычным языком, а система сама раскладывает задачу на несколько действий и обращается к нужным сервисам.&lt;p&gt;Путешествие редко состоит из одной операции: нужно совместить транспорт, даты, жилье, бюджет и иногда несколько пассажиров. Поэтому в начале лета мы открыли сервисы Туту для ИИ-агентов через Model Context Protocol (MCP), а в августе дали эту инфраструктуру участникам всероссийского хакатона.&lt;p&gt;Рассказываем, зачем вообще тревел-сервису MCP, что на нем начали собирать разработчики и какие ограничения у агентного подхода уже видны.&lt;h3&gt;ИИ постепенно становится еще одним интерфейсом к travel-tech&lt;/h3&gt;&lt;p&gt;До недавнего времени генеративный ИИ в путешествиях в основном использовали как консультанта. У модели можно было спросить, куда поехать на три дня, что посмотреть в Казани или в каком районе Стамбула искать отель.&lt;p&gt;Проблема начиналась на следующем шаге. Модель знает, что из Москвы в Петербург ходят поезда, но ее собственных знаний недостаточно, чтобы надежно ответить, какие места доступны вечером 18 сентября и сколько они стоят прямо сейчас. Для этого нужен доступ к реальным данным сервиса.&lt;p&gt;Параллельно меняется и сам путь пользователя к покупке. В июне Forbes Technology Council описывал эту трансформацию через две технологии: GEO — Generative Engine Optimization, оптимизацию присутствия в ответах генеративных систем, — и MCP — Model Context Protocol, протокол подключения моделей к внешним данным и инструментам. В приведенном Forbes исследовании 64% маркетологов назвали снижение использования традиционного поиска одним из главных ожидаемых последствий распространения AI-поиска.&lt;p&gt;Для travel-tech эти два механизма решают разные задачи.&lt;p&gt;GEO влияет на этап, когда пользователь спрашивает: «Куда мне поехать и что выбрать?» Здесь важны контент, отзывы, рейтинги, упоминания и другие данные, на основании которых модель формирует ответ.&lt;p&gt;MCP нужен дальше, когда от общего совета требуется перейти к конкретному действию: проверить расписание, найти доступный номер, сравнить варианты и подготовить пользователя к оформлению.&lt;p&gt;То есть между запросом «хочу на выходные к морю» и покупкой постепенно появляется новый программный слой.&lt;h3&gt;Что MCP меняет на практике&lt;/h3&gt;&lt;p&gt;Model Context Protocol — открытый протокол для взаимодействия языковых моделей с внешними инструментами и источниками данных.&lt;p&gt;Если сильно упростить архитектуру, получается такая цепочка:&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/5fb/459/325/5fb45932594c1e49c5ae3245f4aece4b.png alt=&#34;LLM в этой схеме отвечает за понимание намерения пользователя и выбор действий. Источником актуальных данных остаются продуктовые сервисы&#34; title=&#34;LLM в этой схеме отвечает за понимание намерения пользователя и выбор действий. Источником актуальных данных остаются продуктовые сервисы&#34; width=1122 height=1402 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/5fb/459/325/5fb45932594c1e49c5ae3245f4aece4b.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/5fb/459/325/5fb45932594c1e49c5ae3245f4aece4b.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;LLM в этой схеме отвечает за понимание намерения пользователя и выбор действий. Источником актуальных данных остаются продуктовые сервисы&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Например, пользователь может попросить найти поезд Москва — Петербург. Агент понимает направление и дату, вызывает доступный через MCP инструмент поиска и получает актуальные варианты. После этого модель уже может работать с результатом: сравнить отправления, объяснить различия и помочь выбрать подходящий.&lt;p&gt;Сейчас через Tutu MCP агентам доступны пять основных направлений: самолеты, поезда, автобусы, электрички и отели. Для размещения можно использовать в том числе оценки и отзывы.&lt;p&gt;При этом мы сознательно оставили границу перед чувствительными действиями. Агент может провести пользователя через поиск и подготовить переход к заказу, но данные пассажира, окончательная проверка и оплата остаются на сайте Туту.&lt;p&gt;Для текущего этапа развития агентных интерфейсов human-in-the-loop — участие человека в критической точке сценария — выглядит вполне практичным компромиссом. Цена ошибки при рекомендации отеля и цена ошибки при оплате билета различаются.&lt;h3&gt;Зачем после запуска MCP понадобился хакатон&lt;/h3&gt;&lt;p&gt;Сделать API или MCP-сервер — только половина задачи. Вторая половина начинается с вопроса: что сторонние разработчики вообще будут строить, если дать им доступ к тревел-инструментам?&lt;p&gt;Внутри продукта мы неизбежно смотрим на путешествие через существующие сценарии. Есть выдача билетов, карточка отеля, заказ, личный кабинет. Внешняя команда гораздо меньше ограничена историей этих интерфейсов.&lt;p&gt;Поэтому с 19 по 21 августа мы провели всероссийский ИИ-хакатон. Задача была довольно широкой: разработать проект на основе Tutu MCP, который решает реальную проблему путешественника.&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/86a/f0b/c4f/86af0bc4fb219c2da9c30a089a65de2c.png width=2314 height=834 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/86a/f0b/c4f/86af0bc4fb219c2da9c30a089a65de2c.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/86a/f0b/c4f/86af0bc4fb219c2da9c30a089a65de2c.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;На участие пришла 301 заявка. На хакатон прошли 150 человек — 71 команда. До сдачи проектов дошли 42 команды, девять решений вышли в финал.&lt;p&gt;Мы специально оценивали не только факт использования ИИ. В критериях были понимание задачи, глубина функционала, UX/UI, документация, архитектура, качество кода, технологическая новизна и стабильность работы. Добавить LLM к форме поиска сравнительно легко. Нам было интереснее посмотреть, появляется ли за этим новый пользовательский сценарий.&lt;h3&gt;Поисковая форма оказалась далеко не единственной точкой входа&lt;/h3&gt;&lt;p&gt;Самое интересное в результатах хакатона — даже не отдельные проекты, а повторяющиеся модели поведения.&lt;p&gt;Часть команд начинала планирование поездки с повода. Человек знает, что ему нужно попасть на мероприятие или собрать людей из разных городов к определенной дате. Город, транспорт и жилье становятся уже результатом расчета.&lt;p&gt;Другие команды экспериментировали с discovery-сценариями, где человек вообще не начинает с формы «откуда — куда». Начальной информацией могут быть впечатление, изображение или очень свободное описание желаемой поездки. Дальше агент постепенно переводит это намерение в параметры, с которыми уже могут работать реальные тревел-сервисы.&lt;p&gt;Еще один класс задач связан с групповым планированием. Обычная выдача хорошо устроена для одного покупателя. Если несколько людей едут из разных городов, нужно учитывать несколько расписаний, общий дедлайн, бюджет и компромиссы между участниками.&lt;p&gt;Наконец, оказалось полезно строить поиск вокруг жесткого ограничения. Например: «У меня 20 тысяч рублей, три дня и нужно вернуться в Москву к понедельнику утром». Здесь задача начинается не с выбора конкретного направления, а с вычисления множества реально достижимых вариантов.&lt;p&gt;Все четыре подхода объединяет одна вещь: пользовательское намерение оказывается сложнее набора полей в традиционной форме поиска. И именно здесь языковая модель полезна как слой, который переводит свободный человеческий запрос в последовательность формализованных действий.&lt;h3&gt;В агентной модели важен источник правды&lt;/h3&gt;&lt;p&gt;Генеративная модель может хорошо понять фразу «хочу недорого уехать куда-нибудь, где тепло». Но цена билета, свободный номер и расписание поезда — это факты, которые нельзя получать из вероятностного ответа модели. Поэтому важно разделять две функции системы.&lt;p&gt;LLM хорошо работает с намерением, контекстом и выбором следующего действия. Продуктовый backend отвечает за фактические данные и выполнение операции.&lt;p&gt;Для пользователя эти слои могут выглядеть как один разговор. Внутри системы между ними должна оставаться четкая граница.&lt;p&gt;Это особенно заметно на travel-tech-продукте, где данные быстро меняются. Даже качественный ответ, построенный на вчерашней цене или уже недоступном месте, бесполезен.&lt;p&gt;MCP здесь интересен не потому, что делает модель «умнее». Он дает ей стандартизированный способ обращаться к инструментам, способным ответить на вопрос сейчас.&lt;h3&gt;Чем больше агент умеет делать, тем важнее безопасность&lt;/h3&gt;&lt;p&gt;У агентной архитектуры есть обратная сторона. Как только модель получает инструменты, последствия ошибки становятся серьезнее.&lt;p&gt;Обычная языковая модель может сгенерировать неправильную фразу. Агент с доступом к инструментам потенциально способен вызвать неправильную функцию, передать лишние параметры или отреагировать на вредоносную инструкцию в данных, которые получил извне.&lt;p&gt;На хакатоне мы дважды зафиксировали попытки атак через инъекции. Сам факт хорошо показывает, почему безопасность MCP-сервера нельзя рассматривать как приложение к основной функциональности.&lt;p&gt;Prompt injection — инъекция инструкций в контекст модели — остается одной из проблем агентных систем. Например, вредоносная инструкция может находиться в тексте, который модель получила из внешнего источника, и попытаться заставить агента изменить исходную задачу или вызвать другой инструмент.&lt;p&gt;Поэтому по мере расширения возможностей агента приходится думать уже не только о качестве промпта. Нужны контроль доступных действий, проверка параметров, разделение read- и write-операций, ограничения прав, журналирование вызовов и подтверждение человеком критичных действий.&lt;p&gt;Это одна из причин, почему в текущем сценарии Туту финальная покупка остается за пользователем.&lt;h3&gt;MCP пока не означает, что пользователи завтра перестанут открывать сайты&lt;/h3&gt;&lt;p&gt;Здесь легко перескочить от эксперимента к выводу, что привычный поиск скоро исчезнет. Мы бы с этим не спешили.&lt;p&gt;Сам факт появления MCP-сервера не создает пользовательский трафик: клиент должен поддерживать протокол, а сервер нужно подключить. Полностью автономных тревел-агентов, которые без участия человека проходят весь путь от идеи до покупки, рынок пока тоже не получил.&lt;p&gt;Но инфраструктурная часть развивается быстро. Сейчас Tutu MCP уже можно подключать к нескольким совместимым клиентам и использовать как инструмент внутри агентного сценария.&lt;p&gt;Поэтому для нас MCP пока скорее новый канал взаимодействия с продуктом, чем замена сайту или приложению.&lt;h3&gt;Что мы вынесли из эксперимента&lt;/h3&gt;&lt;p&gt;После запуска MCP и хакатона наше представление об агентном travel-tech стало немного конкретнее.&lt;p&gt;Первое наблюдение связано с интерфейсом. Когда человек может сформулировать конечную цель обычным языком, точкой входа в продукт необязательно остается поисковая форма. Для некоторых задач удобнее сначала понять намерение и ограничения, а форму параметров собрать уже внутри системы.&lt;p&gt;Второе — агент особенно полезен в задачах, где нужно связать несколько действий. Отдельный поиск авиабилета решается существующими интерфейсами достаточно хорошо. Задача «найди, куда мы втроем можем улететь из разных городов, чтобы встретиться на выходных и уложиться в общий бюджет» уже заметно сложнее.&lt;p&gt;Третье — качественная агентная система требует надежного инструментального слоя. Без актуальных данных языковая модель остается консультантом. Доступ к продуктовым инструментам превращает ее в интерфейс к сервису.&lt;p&gt;Четвертое — полезность агента растет вместе с риском. Чем ближе система подходит к реальным действиям и деньгам, тем важнее контроль инструментов, безопасность и понятные точки подтверждения пользователем.&lt;p&gt;И наконец, MCP сам по себе не решает продуктовую задачу. На хакатоне лучше всего выглядели идеи, где сначала была понятная проблема путешественника, а уже затем появлялись ИИ и инструменты для ее решения.&lt;h3&gt;А что получили участники&lt;/h3&gt;&lt;p&gt;Мы хотели, чтобы хакатон был полезен не только Туту.&lt;p&gt;Для участников это была возможность за несколько дней проверить собственную идею на реальном travel-tech-контексте, поработать с Tutu MCP и получить обратную связь от людей, которые каждый день делают продукты для путешествий.&lt;p&gt;Можно было прийти с гипотезой, собрать ее в работающий прототип и сразу увидеть, где идея действительно решает пользовательскую задачу, а где пока существует только на уровне красивой концепции.&lt;p&gt;Авторы лучших проектов получили призы, технику, мерч и сертификаты Туту на путешествия. Партнером хакатона стала Huawei, которая предоставила технику для призового фонда.&lt;p&gt;Но, пожалуй, самая интересная часть для нас начинается после финального питчинга. Девять проектов вошли в число лучших, три заняли призовые места, а некоторые идеи мы сейчас изучаем вместе с продуктовыми командами. Подробности пока не раскрываем: сначала хотим разобраться, какие из решений можно развивать дальше и в каком формате это делать вместе с авторами.&lt;p&gt;Поэтому участие в наших хакатонах — это не только три дня разработки и возможность забрать приз.&lt;p&gt;Это способ:&lt;p&gt;— получить доступ к реальной технологической инфраструктуре travel-tech-компании;&lt;p&gt;— проверить свою гипотезу на прикладной задаче;&lt;p&gt;— показать решение инженерам и продуктовым командам Туту;&lt;p&gt;— получить обратную связь от экспертов;&lt;p&gt;— и, если идея действительно сильная, продолжить разговор уже после окончания соревнования.&lt;p&gt;Ну и путешествия, конечно. Мы все-таки Туту :)&lt;p&gt;Планируем продолжать такие форматы. Поэтому если вам интересны ИИ, агентные системы и travel-tech, следите за нашими следующими хакатонами.&lt;h3&gt;Что дальше&lt;/h3&gt;&lt;p&gt;Мы пока не знаем, станет ли диалог с ИИ таким же привычным способом покупать путешествия, каким когда-то стал сайт или мобильное приложение.&lt;p&gt;Но уже видно, что агентная модель создает еще один способ взаимодействия с travel-tech. Пользователь формулирует намерение, модель помогает его разобрать, а специализированные сервисы предоставляют актуальные данные и выполняют конкретные операции.&lt;p&gt;Для компаний это означает появление нового технического вопроса: насколько продукт готов к ситуации, когда его интерфейсом становится чужой ИИ-агент?&lt;p&gt;Мы начали отвечать на него с MCP. Хакатон показал, что поверх такого слоя можно строить сценарии, которые сложно было бы придумать, если исходить только из привычной поисковой выдачи.&lt;p&gt;Теперь интереснее всего проверить, какие из них выдержат столкновение с реальными пользователями.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <guid>https://habr.com/ru/companies/tuturu/articles/1081246/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1081246</guid>
      <pubDate>Mon, 14 Sep 2026 13:46:43 +0000</pubDate>
    </item>
    <item>
      <title>Как я учил планировщик класть широкие команды для Эльбруса, и почему LLM проиграла жадному алгоритму за 3 миллисекунды</title>
      <link>https://habr.com/ru/articles/1082094/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082094</link>
      <description>&lt;div&gt;&lt;div class=&#34;article-formatted-body article-formatted-body article-formatted-body_version-2&#34;&gt;&lt;div xmlns=http://www.w3.org/1999/xhtml&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/94b/b82/750/94bb827507b1dbdc13970e98c59f6610.png alt width=1875 height=1038 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/94b/b82/750/94bb827507b1dbdc13970e98c59f6610.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/94b/b82/750/94bb827507b1dbdc13970e98c59f6610.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Все началось с того что я решил, что смогу оптимизировать компилятор Эльбрус с помощью ИИ&lt;br&gt;Вдохновлялся Эльбрусом 2: машина, которая в то время считала ПРО и космические задачи, 10 процессоров, десятки мегафлопс, когда это было реально много. Симметричный мультипроцессор с общей памятью, тегированная архитектура, аппаратная поддержка языков высокого уровня. В общем к проекту это вдохновение никак не повлияло.&lt;p&gt;Потом был Эльбрус-3 — уже с явным параллелизмом инструкций, прообраз VLIW. Потом тяжелые 90-е, потом Эльбрус 2000 / E2K. Ну про историю эльбруса вы уже слышали не раз.&lt;br&gt;Поэтому вернемся к моему проекту.&lt;h3&gt;1. Что это за проект&lt;/h3&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/f2d/c86/b6d/f2dc86b6dd8900ef4afdac119b70467f.png width=1276 height=246 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/f2d/c86/b6d/f2dc86b6dd8900ef4afdac119b70467f.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/f2d/c86/b6d/f2dc86b6dd8900ef4afdac119b70467f.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Прототип к исследовательскому проекту «Планировщик инструкций для VLIW‑архитектур». Главный тезис:&lt;blockquote&gt;&lt;p&gt;Планировщик широких команд принимает жадные необратимые решения, и на реалистичной модели машины у стандартной эвристики есть измеримый, доказуемый разрыв до оптимума — вот бюджет, за который имеет смысл бороться обученной моделью.&lt;/blockquote&gt;&lt;p&gt;Проще говоря, NEX CLI x Elbrus берет граф и модель e2k‑v6 и показывает: сколько насчитал жадный, сколько мог бы идеальный, и на какой операции и каком канале разница.&lt;h3&gt;2. Зачем вообще трогать планирование под Эльбрус&lt;/h3&gt;&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/95f/27e/22e/95f27e22eafffb5a95726d0a40b73b27.png width=426 height=314 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/95f/27e/22e/95f27e22eafffb5a95726d0a40b73b27.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/95f/27e/22e/95f27e22eafffb5a95726d0a40b73b27.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Эльбрус — это VLIW. Там нет того, что прячет ваши косяки на x86: нет развитого OoО.&lt;ul&gt;&lt;li&gt;&lt;p&gt;Что куда можно:&lt;ul&gt;&lt;li&gt;&lt;p&gt;Умножать можно только в каналах 0, 1, 3 и 4. Результат готов через 4 такта.&lt;li&gt;&lt;p&gt;Делить можно только в канале 5. Результат готов через 11 тактов, и следующее деление можно начать только через 2 такта.&lt;li&gt;&lt;p&gt;Загружать данные из памяти можно в каналах 0, 2, 3 и 5. Результат готов через 5 тактов.&lt;li&gt;&lt;p&gt;Записывать в память можно только в каналах 2 и 5.&lt;/ul&gt;&lt;/ul&gt;&lt;p&gt;Ошибся на один такт с делителем — встал весь пакет. Положил запись не туда — заклинил &lt;code&gt;,5&lt;/code&gt;, который нужен и делителю, и загрузкам. Компилятор &lt;code&gt;lcc&lt;/code&gt; — единственный, кто может это разрулить статически.&lt;p&gt;Отсюда идея проекта: не написать «новый компилятор», а сделать честный стенд, где видно разницу между тем, что делает жадная эвристика, и тем, что вообще возможно. И на этом стенде уже можно спорить, мерить и учить модели.&lt;pre&gt;&lt;code class=python&gt;python3 -m vliw run slotclash&#xA;# жадный: 23 такта, оптимум: 22 такта. -1 такт = -4%.&#xA;# Вопрос: где жадный зевнул монопольный ,5?&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:87px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Сценарии так и называются — ловушки: &lt;code&gt;slotclash&lt;/code&gt;, &lt;code&gt;divpressure&lt;/code&gt;, &lt;code&gt;memstream&lt;/code&gt;, &lt;code&gt;unroll4&lt;/code&gt;, &lt;code&gt;wide_ilp&lt;/code&gt;. У каждого — &lt;code&gt;lesson&lt;/code&gt; в одну строчку. Например, ⁣&lt;code&gt;memstream&lt;/code&gt;: 8 троек &lt;code&gt;LOAD-ADD-STORE&lt;/code&gt;, узкое место — запись, потому что записей 8, а каналов под них 2.&lt;h3&gt;3. Цифры не из воздуха. Но я им сам не до конца верю&lt;/h3&gt;&lt;p&gt;Числа в &lt;code&gt;e2k-v6-measured&lt;/code&gt; сняты с настоящего &lt;code&gt;lcc-1.29.16&lt;/code&gt; кросс‑сборкой под &lt;code&gt;e2k-v6&lt;/code&gt; тремя независимыми приемами. Это написано в шапке репа &lt;code&gt;vliw/core/&lt;/code&gt;&lt;a href=http://model.py rel=&#34;noopener nofollow&#34;&gt;&lt;code&gt;model.py&lt;/code&gt;&lt;/a&gt;:&lt;p&gt;&lt;strong&gt;1. Матрица портов — спрошена у ассемблера, а не угадана по листингу.&lt;/strong&gt;&lt;br&gt;Для каждой пары &lt;code&gt;(операция, канал)&lt;/code&gt; собирается крошечный &lt;code&gt;.s&lt;/code&gt;. Если так нельзя аппаратно — ассемблер отвечает прямым отказом: &lt;code&gt;&amp;#39;muls&amp;#39; cannot be encoded in ALC2&lt;/code&gt;. Это ответ про кодировку широкой команды, про само железо, а не про то, что решил планировщик &lt;code&gt;lcc&lt;/code&gt;. Автомат этого — &lt;code&gt;tools/probe_&lt;/code&gt;&lt;a href=http://matrix.py rel=&#34;noopener nofollow&#34;&gt;&lt;code&gt;matrix.py&lt;/code&gt;&lt;/a&gt;.&lt;p&gt;&lt;strong&gt;2. Латентность — цепочкой зависимых.&lt;/strong&gt; &lt;code&gt;x = x * b&lt;/code&gt; пять раз подряд. Следующая обязана ждать предыдущую, компилятор вынужден развести их ровно на latency. Разница номеров bundle читается напрямую. Так намеряны &lt;code&gt;MUL 4, DIV 11, LOAD 5&lt;/code&gt;.&lt;p&gt;&lt;strong&gt;3. Занятие порта — потоком независимых.&lt;/strong&gt; 8–16 штук с готовыми операндами. Шаг между выдачами = темп устройства. Так намеряны &lt;code&gt;DIV раз в 2 такта, MUL раз в такт&lt;/code&gt;.&lt;p&gt;А теперь детектив.&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/886/6d4/055/8866d40555eb6b46e602445d1b143fe1.png alt=&#34;Обожаю несмешно шутить&#34; title=&#34;Обожаю несмешно шутить&#34; width=429 height=413 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/886/6d4/055/8866d40555eb6b46e602445d1b143fe1.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/886/6d4/055/8866d40555eb6b46e602445d1b143fe1.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Обожаю несмешно шутить&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Первая версия модели была неверна. Был &lt;code&gt;probe.c&lt;/code&gt; — 4 деления + 4 умножения. Все &lt;code&gt;muls&lt;/code&gt; легли в &lt;code&gt;,0&lt;/code&gt; подряд. Я прочитал это как «умножитель один, держит порт 8 тактов». А потом проверка ассемблером показала: умножение исполнимо на четырех каналах, а поток из 16 независимых умножений идет по одному в такт. Четырех операций просто не хватило, чтобы у жадного планировщика &lt;code&gt;lcc&lt;/code&gt; появился повод их раскидать.&lt;p&gt;Мораль, которая теперь вбита в README: по выводу компилятора видно то, что компилятор захотел сделать, а не то, что железо может.&lt;p&gt;Все это прогонялось потом десятками ИИ‑агентов для перепроверки пар, каналов, сочетаний. Нашли ровно два запрета сочетаний в одной широкой команде: &lt;code&gt;STORE,2 + LOAD,3&lt;/code&gt; и &lt;code&gt;STORE,5 + LOAD,0&lt;/code&gt; — швы между кластерами &lt;code&gt;,0,1,2&lt;/code&gt; и &lt;code&gt;,3,4,5&lt;/code&gt;. Сверялись с официальным «Руководством по эффективному программированию на платформе Эльбрус» 1.2 — но числа оттуда в модель НЕ подставляли: там Е4С/Е8С, а тулчейн целится в v6. Это сверка, а не источник.&lt;p&gt;И все равно — я не считаю цифры паспортом:&lt;ul&gt;&lt;li&gt;&lt;p&gt;живого железа у меня не было, SSH Эльбруса — мне лень с ним возится.&lt;li&gt;&lt;p&gt;из 19 классов latency измерена у 9. Остальное — &lt;code&gt;ASSUMED=1&lt;/code&gt;, список открыт в &lt;a href=http://ISA.md rel=&#34;noopener nofollow&#34;&gt;&lt;code&gt;ISA.md&lt;/code&gt;&lt;/a&gt;;&lt;li&gt;&lt;p&gt;&lt;code&gt;LOAD 5&lt;/code&gt; против &lt;code&gt;3 (L1 hit, L2 11, L3 40, память ~100)&lt;/code&gt; из доки МЦСТ — расхождение поколений(Я перепроверял, и цифры сходились с документацией 24 года);&lt;li&gt;&lt;p&gt;&lt;code&gt;occupancy&lt;/code&gt; склеивает занятость устройства и слота. &lt;code&gt;div8.c&lt;/code&gt; меряет устройство (раз в 2 такта), а в &lt;code&gt;fma_kernel.s&lt;/code&gt; &lt;code&gt;fdivd,5@t19 + aaurwd,5@t20&lt;/code&gt; — слот свободен раньше. Модель пессимистична сознательно.&lt;/ul&gt;&lt;p&gt;Поэтому давайте честно: это лучшая доступная аппроксимация, а не даташит.&lt;p&gt;И здесь — оффер, ради которого статья и пишется. Это не вам нужен мой проект. Это мне нужны вы.&lt;p&gt;Если у вас есть живой &lt;code&gt;e2k-v6&lt;/code&gt; — прогоните цепочки из &lt;code&gt;examples/probes/&lt;/code&gt; (&lt;code&gt;mul_latency.c, div_latency.c, load_latency.c, store_burst.c...&lt;/code&gt;). Пришлите листинги &lt;code&gt;lcc -O3 -fverbose-asm&lt;/code&gt;. Закроем 13 допущений, сверим &lt;code&gt;LOAD 5 vs 3&lt;/code&gt;, померяем межкластер. Без вас эта таблица так и останется «замерами по поведению компилятора».&lt;h3&gt;4. ИИ‑арка: как Qwen на 2 ГБ пытался стать оптимизатором&lt;/h3&gt;&lt;figure&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/2d0/bf8/517/2d0bf85176836ec188361f87137420b2.png width=447 height=447 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/2d0/bf8/517/2d0bf85176836ec188361f87137420b2.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/2d0/bf8/517/2d0bf85176836ec188361f87137420b2.png 781w&#34; loading=lazy decode=async&gt;&lt;/figure&gt;&lt;p&gt;Изначальный план был наивный и красивый: научить LLM класть расписания напрямую. Граф на вход, строки &lt;code&gt;id: такт=.. канал=..&lt;/code&gt; на выход. Чем не планировщик?&lt;p&gt;База — &lt;code&gt;Qwen2.5-3B-Instruct&lt;/code&gt;, QLoRA &lt;code&gt;r16/alpha32&lt;/code&gt;, &amp;lt;2% обучаемых параметров. Адаптеры по ~120 МБ, три прогона, лежат открыто &lt;code&gt;Shmonika/nex-vliw-lora&lt;/code&gt; — в git не влезли из‑за лимита 100 МБ. База в GGUF &lt;code&gt;Q4_K_M — 2.1 ГБ&lt;/code&gt;, запускается на ноутбуке без GPU через &lt;code&gt;llama.cpp&lt;/code&gt;.&lt;p&gt;Промпт — один и тот же код на обучение и на запуск (&lt;code&gt;training/encode.py&lt;/code&gt;), иначе разъедется молча — уже проходили с EOS‑багом, когда модель без маркера конца не останавливалась и лила 24 лишних id:&lt;pre&gt;&lt;code class=python&gt;vliw/core/     # чистый домен. Никаких print, никакого UI&#xA;vliw/cli.py    # Session, кэш, команды&#xA;vliw/ui/       # plain-рендер для терминала&#xA;vliw/tui/      # полноэкранный Textual&#xA;vliw/agent/    # разговорный агент поверх уже посчитанного&#xA;vliw/learned/  # LoRA-трек, опциональный&#xA;training/ tools/ examples/probes/ docs/&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Датасет — 40 тысяч обученных данных: &lt;code&gt;train_merged.jsonl — 39700&lt;/code&gt; = &lt;code&gt;dataset 20000 + extra 20000 - отсев&lt;/code&gt;. Пары &lt;code&gt;граф -&amp;gt; оптимум&lt;/code&gt; через &lt;code&gt;Oracle(budget=1.5с)&lt;/code&gt;, только доказанные, размеры 6–14. Эвалы: &lt;code&gt;eval 300, eval_wide 300 (4-24), eval_hard 300 (16-24, средний 20.3)&lt;/code&gt;. Хотите — напишите мне, скину детали прогона, скрипты и промпты все открыты.&lt;p&gt;Что вышло по цифрам:&lt;ul&gt;&lt;li&gt;&lt;p&gt;Скорость: baseline — миллисекунды, oracle на n=24 — 0.61с, 60 графов — 1.8с. Модель — &lt;strong&gt;27–30с на граф, ~11 ток/с&lt;/strong&gt;, на реале 178–395с, &lt;code&gt;best-of-8 — 4 минуты на граф(после первого запроса скорость быстрее)&lt;/code&gt;. Сервер держит 3.5 ГБ, без &lt;code&gt;-c&lt;/code&gt; ограничения OOM‑киллер его убивал прямо посреди &lt;code&gt;/learned&lt;/code&gt;.&lt;li&gt;&lt;p&gt;Легкие выборки — пустые. Жадный уже &lt;code&gt;298/300&lt;/code&gt; и &lt;code&gt;300/300&lt;/code&gt; точно в оптимуме CP‑SAT. Выигрывать нечего. 477 секунд модели за тот же ответ, что эвристика дает мгновенно.&lt;li&gt;&lt;p&gt;Прогон 0 без EOS — 26.7% валидных, прогон 1 с EOS — 97.7% на узком, 32.7% на широком. Все 11 провалов из 15 — &lt;code&gt;STORE на чужом канале&lt;/code&gt;. В &lt;code&gt;dataset&lt;/code&gt; не было ни одного STORE. Модель его никогда не видела.&lt;li&gt;&lt;p&gt;Реал — &lt;code&gt;112-386 узлов&lt;/code&gt; против обучения на &lt;code&gt;4-24&lt;/code&gt;, словарь &lt;code&gt;37% UNKNOWN&lt;/code&gt; (предикаты, SIMD, AAU), &lt;code&gt;0/13&lt;/code&gt; против случайного перебора за то же время.&lt;/ul&gt;&lt;p&gt;То есть цифры сказали прямо: как бухгалтер модель медленная и хрупкая. Держать ее в ядре — значит платить 30 секунд за то, что алгоритм делает за 3 миллисекунды, и получать 17% законных на трудных графах.&lt;p&gt;Пришлось перейти на CAB (лучшее, что я случайно придумал).&lt;h3&gt;5. CAB: забираем у модели бухгалтерию, оставляем чутье&lt;/h3&gt;&lt;figure class=full-width&gt;&lt;img src=https://habrastorage.org/r/w1560/getpro/habr/upload_files/ac0/7e0/6a8/ac07e06a884bf6e797bdbaddfb722117.png alt=&#34;Ладно, это очень смешно&#34; title=&#34;Ладно, это очень смешно&#34; width=735 height=547 sizes=&#34;(max-width: 780px) 100vw, 50vw&#34; srcset=&#34;https://habrastorage.org/r/w780/getpro/habr/upload_files/ac0/7e0/6a8/ac07e06a884bf6e797bdbaddfb722117.png 780w,&#xA;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/ac0/7e0/6a8/ac07e06a884bf6e797bdbaddfb722117.png 781w&#34; loading=lazy decode=async&gt;&lt;div&gt;&lt;figcaption&gt;Ладно, это очень смешно&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;CAB — “портфель законных расписаний из одного ответа модели”. Второй этап SEED стоит на нем, но суть — в первом.&lt;p&gt;Идея из &lt;code&gt;docs/DIAG3.md&lt;/code&gt;: модель отлично чувствует порядок, но тонет в бухгалтерии — 315 незаконных каналов, 208 конфликтов, 26 неготовых операндов. При этом из 98 законных ответов 97 были точным оптимумом. Значит — забираем бухгалтерию целиком.&lt;p&gt;Что берем у модели:&lt;div&gt;&lt;div class=table&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th data-colwidth=118 width=118&gt;&lt;p align=left&gt;берем&lt;th data-colwidth=268 width=268&gt;&lt;p align=left&gt;как&lt;th&gt;&lt;p align=left&gt;что выбрасываем&lt;tr&gt;&lt;td data-colwidth=118 width=118&gt;&lt;p align=left&gt;порядок&lt;td data-colwidth=268 width=268&gt;&lt;p align=left&gt;сортировка по ее &lt;code&gt;(такт,канал)&lt;/code&gt; → &lt;code&gt;priority&lt;/code&gt;&lt;td&gt;&lt;p align=left&gt;—&lt;tr&gt;&lt;td data-colwidth=118 width=118&gt;&lt;p align=left&gt;такты&lt;td data-colwidth=268 width=268&gt;&lt;p align=left&gt;как &lt;code&gt;defer_until&lt;/code&gt; «не раньше»&lt;td&gt;&lt;p align=left&gt;—&lt;tr&gt;&lt;td data-colwidth=118 width=118&gt;&lt;p align=left&gt;каналы&lt;td data-colwidth=268 width=268&gt;&lt;p align=left&gt;&lt;strong&gt;не берем вообще&lt;/strong&gt;&lt;td&gt;&lt;p align=left&gt;самая частая ошибка&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Бухгалтерию ведет &lt;code&gt;list_schedule&lt;/code&gt; из &lt;code&gt;core/baseline.py&lt;/code&gt; — он по построению не выдаст раньше предков и не посадит двоих на канал. Расписание законно механизмом, а не удачей.&lt;p&gt;Дорогая здесь только генерация. Поэтому из одного ответа строим 5 кандидатов без лишних генераций: &lt;code&gt;модель, модель+каналы (починка Куном), порядок, порядок+такты, жадный&lt;/code&gt;. Плюс те же 5 со второй генерации по грамматике GBNF. Побеждает min &lt;code&gt;(makespan)&lt;/code&gt;, при равенстве — ответ самой модели, чтобы не приписывать алгоритму чужое. Подпись в UI честная: &lt;code&gt;CAB: &amp;lt;кандидат&amp;gt;&lt;/code&gt;.&lt;p&gt;Предсказание, записанное ДО замера, на &lt;code&gt;slotclash&lt;/code&gt; с убитыми в &lt;code&gt;,0&lt;/code&gt; каналами:&lt;ul&gt;&lt;li&gt;&lt;p&gt;сырой — 22, но незаконно&lt;li&gt;&lt;p&gt;жадный — 23&lt;li&gt;&lt;p&gt;порядок — 23&lt;li&gt;&lt;p&gt;&lt;strong&gt;порядок+такты — 22 = оптимум&lt;/strong&gt;&lt;/ul&gt;&lt;p&gt;Порядка мало, нужно еще «придержать» операцию у монопольного &lt;code&gt;,5&lt;/code&gt;. &lt;code&gt;defer_until&lt;/code&gt; это возвращает.&lt;p&gt;Замер на трудном эвале, где у жадного наконец есть зазор (0/300 в оптимуме, разрыв 1.03):&lt;ul&gt;&lt;li&gt;&lt;p&gt;жадный — 1.03, 0/300&lt;li&gt;&lt;p&gt;модель + CAB — &lt;strong&gt;0.82, 59/300 точно в оптимуме (62/300 после замера STORE=2)&lt;/strong&gt;&lt;li&gt;&lt;p&gt;вклад: порядок 25, модель 16, порядок+такты 13, модель+каналы 7. &lt;strong&gt;Итого 61/300 строго лучше жадного — 20% трудных графов.&lt;/strong&gt;&lt;/ul&gt;&lt;p&gt;Без CAB на тех же данных — 17% законных. С CAB — 100% по построению. Рост законных ничего не доказывает, доказывают 5 тактов на 30 и 60+ графов, отыгранные у доказанного оптимума.&lt;p&gt;SEED-1 (подсказка верхней границей в оракул) при этом мертв замером, а не мнением: даже идеальный оптимум экономит 5% узлов, потому что поиск идет снизу вверх и тратит все на провальные пробы. Платить 30с генерации за 5% от 30мс — бессмысленно. Оставили в коде, пригодится если поиск пойдет сверху вниз.&lt;h3&gt;6. Где ИИ сейчас: не бухгалтер, а мини‑агент&lt;/h3&gt;&lt;p&gt;И тут важный дисклеймер.&lt;p&gt;ИИ неэффективен только в моем сетапе. У меня нет мощностей гонять модели на сотни гигабайт — тысячегиговые монстры явно умнее моего Qwen на 2.5 ГБ. Суть не в том, что «ИИ бесполезен», а в том, что я откинул его на задний план.&lt;p&gt;Он в проекте есть. Он просто занимается фоновым:&lt;ul&gt;&lt;li&gt;&lt;p&gt;поставляет порядок/такты для CAB,&lt;li&gt;&lt;p&gt;помогает алгоритму как эвристика,&lt;li&gt;&lt;p&gt;пересказывает код и панели человеческим языком поверх уже посчитанных ядром чисел,&lt;li&gt;&lt;p&gt;перебирает детали в &lt;code&gt;/doctor&lt;/code&gt;, &lt;code&gt;/explain&lt;/code&gt;, &lt;code&gt;/agent&lt;/code&gt;.&lt;/ul&gt;&lt;p&gt;По сути я собрал агентного мини‑ИИ для Эльбруса на той же Qwen: один &lt;code&gt;llama-server&lt;/code&gt; на unix‑socket, база — для чата, LoRA — для плана, офлайн‑фолбэк с детерминированным ответом, если весов нет. Ядро считает, модель формулирует… Живая заливка решетки токенами, &lt;code&gt;trace&lt;/code&gt; для разбора, &lt;code&gt;--raw/--pure&lt;/code&gt; для аудита.&lt;p&gt;И он ждет большое железо.&lt;p&gt;Если у вас или у меня появится ПК/сервер помощнее, больше ресурсов — туда можно встроить модель поумнее, которая, возможно, справится лучше всяких алгоритмов. Будет ли это прорывом — я не знаю.&lt;br&gt;Но проект это позволяет: реализуете &lt;code&gt;Scheduler(kind=&amp;#34;learned&amp;#34;)&lt;/code&gt; через те же &lt;code&gt;encode_prompt/decode_completion&lt;/code&gt;, кладете адаптер в &lt;code&gt;training/checkpoints/&lt;/code&gt;, GGUF в &lt;code&gt;vendor/models/&lt;/code&gt; — и &lt;code&gt;compare/explain/bench/CAB&lt;/code&gt; подхватят без правок ядра. Сделаете модель бухгалтером — отлично. Не выйдет — откатитесь к помощнику одной строкой флага &lt;code&gt;/learned --cab&lt;/code&gt;.&lt;p&gt;Пропорция смешивания трудных графов (DIV 20% против единиц процентов в реальном &lt;code&gt;lcc&lt;/code&gt;), словарь реала, обрезка контекста &lt;code&gt;4096&lt;/code&gt; — все это отдельные решения, которые еще предстоит принять. Лучше принимать их вместе.&lt;h3&gt;Финал:&lt;/h3&gt;&lt;h4&gt;[Проект оупенсорс] модульный монолит, а не комбайн&lt;/h4&gt;&lt;p&gt;Я специально не делал микросервисы и плагины ради плагинов. Внутри — модульный монолит:&lt;pre&gt;&lt;code&gt;vliw/core/     # чистый домен. Никаких print, никакого UI&#xA;vliw/cli.py    # Session, кэш, команды&#xA;vliw/ui/       # plain-рендер для терминала&#xA;vliw/tui/      # полноэкранный Textual&#xA;vliw/agent/    # разговорный агент поверх уже посчитанного&#xA;vliw/learned/  # LoRA-трек, опциональный&#xA;training/ tools/ examples/probes/ docs/&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Ядро не знает про интерфейс. Оба рендера читают один &lt;code&gt;Session.results()&lt;/code&gt;:&lt;pre&gt;&lt;code class=python&gt;# vliw/cli.py:197&#xA;def results(self):&#xA;    # (scenario, profile, width) -&amp;gt; (baseline, oracle, metrics)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Хочешь свой планировщик — реализуешь один протокол:&lt;pre&gt;&lt;code class=python&gt;# vliw/core/api.py — ЭТО ТОЧКА ПОДСТАНОВКИ МОДЕЛИ&#xA;class Scheduler(Protocol):&#xA;    name: str; kind: str; short: str&#xA;    def schedule(self, dag: DAG, model: MachineModel) -&amp;gt; SchedulingResult: ...&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;kind = &amp;#34;heuristic&amp;#34;&lt;/code&gt; — жадный, &lt;code&gt;&amp;#34;exact&amp;#34;&lt;/code&gt; — оракул, &lt;code&gt;&amp;#34;learned&amp;#34;&lt;/code&gt; — модель. Весь остальной код — валидация, метрики, &lt;code&gt;/compare&lt;/code&gt;, &lt;code&gt;/explain&lt;/code&gt;, визуализация — не меняется.&lt;p&gt;Хочешь свою машину — не правишь планировщики:&lt;pre&gt;&lt;code class=python&gt;MachineModel(name=&amp;#34;my-e2k&amp;#34;, ops={...}, ports=[...])&#xA;PROFILES[&amp;#34;my&amp;#34;] = my  # или get_profile(&amp;#34;e2k-v6-measured&amp;#34;).with_width(3)&#xA;&lt;/code&gt;&lt;div class=code-explainer&gt;&lt;a href=https://sourcecraft.dev/ class=&#34;tm-button code-explainer__link&#34; style=&#34;visibility: hidden;&#34;&gt;&lt;img style=width:14px;height:14px;object-fit:cover;object-position:left;&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;pyproject.toml&lt;/code&gt; честный: &lt;code&gt;dependencies=[]&lt;/code&gt;. Ядро работает на голом Python 3.10. &lt;code&gt;tui=[rich,textual]&lt;/code&gt;, &lt;code&gt;learned=[torch,transformers,peft]&lt;/code&gt; — только extras. Команды стандартные: &lt;code&gt;run, load, compare, doctor, bounds, path, analyze, all, scenarios, sweep, selfcheck&lt;/code&gt;.&lt;p&gt;Это важно для всего, что ниже: проект позволяет пересобрать себя под вашу модель. Слот под «бухгалтера» готов.&lt;p&gt;Исходный код прототипа: &lt;a href=https://github.com/smworklair/NEX-CLI-x-Elbrus%EF%BF%BC rel=&#34;noopener nofollow&#34;&gt;https://github.com/smworklair/NEX‑CLI‑x-Elbrus&lt;br&gt;&lt;/a&gt;Веса LoRA: &lt;a href=https://huggingface.co/Shmonika/nex-vliw-lora rel=&#34;noopener nofollow&#34;&gt;https://huggingface.co/Shmonika/nex‑vliw‑lora&lt;/a&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
      <author>Shmon</author>
      <guid>https://habr.com/ru/articles/1082094/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1082094</guid>
      <pubDate>Mon, 14 Sep 2026 13:43:17 +0000</pubDate>
    </item>
  </channel>
</rss>