В комментариях к моей предыдущей статье многие просили рассказать, как я использую ИИ для написания кода. 80-90% моих строк на последних проектах написаны через LLM, при этом мне удается с первой генерации по сравнительно небольшому промпту получать вплоть до 500-1000 строк комплексной бизнес логики, к тому же крайне высококачественной и полностью соответствующей стилю кода проекта. Мне кажется, ИИ вообще пишет код лучше, чем я на первом проходе: он допускает меньше багов, и его код сразу хорошо отрефакторен.
Использование ИИ позволяет мне получить около 3х к производительности, при этом повысить качество итогового кода и даже сделать разработку более увлекательной, но почему-то у многих моих коллег пока не получается даже близко воспроизвести такой результат. Поэтому надеюсь, мой опыт действительно мог бы быть кому-нибудь полезен.
Прежде всего скажу, что я не ML инженер, и мое понимание работы LLM основано не на многолетнем опыте их разработки, а на более скромных академических и практических познаниях. В то же время, когда я слушаю и читаю экспертов, их тезисы отлично соответствуют моей картине мира, а именно эта картина позволяет мне легко и эффективно использовать LLM.
Я изучал и создавал простые нейросети еще до того, как это стало мейнстримом, в сумме посвятив этому около 200 часов, в процессе активно используя уже имевшиеся знания высшей математики. В результате у меня сформировалось хорошее понимание основ, но если я в чем-то неправ, буду рад услышать более экспертные мнения. Если вы хотите сами получше разобраться в теме, рекомендую следующие курсы по порядку освоения:
Дальнейшее повествование основано на следующих убеждениях, к которым я пришел в ходе упомянутого изучения нейросетей.
Есть популярное заблуждение, что LLM это тупые железяки, которые просто "оперируют вероятностями". Люди, которые так говорят, часто не могут даже внятно объяснить, что эти самые вероятности из себя представляют, и откуда они берутся.
Чтобы надежно генерировать все многообразие текстов, заложенных в тестовых и обучающих выборках, LLM просто обязаны были прийти к концептуальному пониманию слов и их взаимосвязей, в этом и состоит цель создания нейросетей. Еще давным давно создатели языковых моделей добивались того, чтобы представления в векторном пространстве (эмбеддинги) для слов "мама" и "папа" относились как вектора друг к другу примерно как для "брат" и "сестра", то есть разность векторов означала бы переход от мужского пола к женскому. Чтобы "страдание" в одном отношении было противоположно "экстазу" (приятность переживания), а в другом сонаправлено с ним (сила переживания). Информация, полученная современными LLM из обучающей выборки на одном языке, может быть успешно использована для восприятия и генерации текста на совсем другом языке. Возможно, эти доводы смогут посеять в некоторых скептиках зерно сомнения.
Конечно, можно придраться к слову "понимают", так как разные люди в него вкладывают разный смысл. Это вопрос терминологии, я поделюсь своим видением. В моем понимании, "понимание" - это наличие, во-первых, набора сформированных абстрактных концепций, обобщающих информацию о реальном мире, в которых, например, "друг" - это примерно то же самое, что "friend", а во-вторых, связей между этими концепциями, хорошо соответствующими реальному миру. Степень понимания - это мера, в которой эти концепции и связи соответствуют реальному миру. Ради интереса даже посмотрел википедию, на английской странице Understanding приведено похожее определение в разделе "As a model", что понимание - это вид сжатия информации. Человеческое понимание я определяю точно так же.
Более того, по моему опыту, современные нейросети понимают смысл того, что читают и пишут часто даже лучше, чем люди. Например, сейчас в компании мы делаем внутреннюю базу знаний с интерфейсом LLM (чтобы повысить качество, без RAG). И во время начального тестирования на голой LLM еще до прототипа базы, среди множества прочей информации из отдела HR в контексте был такой фрагмент:
непосредственный руководитель заполняет заявку по шаблону (пример BDM [ссылка], там же матрица компетенций) и с этим документом идет к HRD или HR для дальнейших действий. Совместно вносятся корректировки.
Составитель документа имел в виду, что пример BDM - это и есть шаблон, регламентирующий структуру заявки, просто с заполненными данными для BDM, и был недоволен, что LLM не привела структуру этого шаблона из связанного документа на запрос "что должно быть в заявке". В его понимании, "заполнить по шаблону" и "заполнить по примеру" - это практически одно и то же. Но ведь на самом деле, если подумать получше, шаблон - это структура, которую надо заполнить, а пример заполнения заявки - это заполненная заявка для конкретной должности. Неочевидно, что в примере содержатся все разделы шаблона (возможно, некоторые из них опциональны и не были включены в пример). И наоборот, некоторые разделы примера могут быть лишь структурными элементами текста, написанного в свободной форме при заполнении раздела шаблона, а не его обязательными элементами. Поэтому ИИ и не рискнул приводить структуру примера в качестве регламента структуры заявки (он даже так и объяснил свое решение при последующих вопросах), и я бы на его месте поступил бы так же, чтобы не допустить ошибки.
И стоило лишь сделать формулировку правильнее: "...по шаблону (пример BDM [ссылка], там же матрица компетенций, структура примера определяет шаблон) ..." как ответ ИИ кардинально поменялся, он уже не просто упоминал о существовании примера для BDM, а пользовался этим примером, чтобы описать структуру шаблона, по которому заявка должна быть заполнена, как это и подразумевал автор документа.
Это уже чисто мои мысли, которые следуют из понимания устройства LLM.
Первое из отличий фундаментальное - далеко не любой опыт взаимодействия человека с реальным миром можно без потери информации описать текстом или другой формой медиа. Как например описать запахи, тактильный опыт, чувства в их естественном виде? Когда мы общаемся с другими людьми словами, которыми мы обозначаем это все, мы встречаем у них понимание, потому что они сами люди, и для них это все тоже знакомо из опыта взаимодействия с миром (или в результате эволюции, так как мы просто созданы чтобы хорошо понимать реальный мир), соответственно, на слова реагируют те нейронные связи, которые сформировались не просто через обучение языку.
LLM же, как бы ни хотела, не может сформировать таких нейронных связей, поскольку обучается только на цифровом контенте. В то же время, для многих чувственных понятий у нее все-таки сложилось достаточно хорошее понимание, я представляю как если бы разумный агент, очень быстрый и обучаемый, жил бы в изоляции и постигал бы мир на основе книг. Он бы не слишком хорошо понимал то, что не испытывал сам, но мог бы говорить с людьми на одном языке и даже что-то умное. Тем не менее, на текущем этапе развития LLM, помимо прочего, отсутствие целого пласта важнейших данных для обучения не позволяет им постигать и понимать мир полноценно. Но вот какие-то абстрактные понятия, как например "пример" и "шаблон", которые слабо связаны с тем, что не передать словами, как видим, LLM уже научились понимать очень даже хорошо.
Кстати, не так давно вышла статья "The Era of Experience", авторы которой предполагают, что следующим этапом на пути к AGI и ASI должна стать интеграция самообучающихся агентов в реальный мир, чтобы как раз-таки получать кучу чистых реальных данных, не искаженных человеческим восприятием и объемом написанных текстов. Я, например, сомневаюсь, что в каком-то виде фиксируется хотя бы 1/1000 мыслей, которые меня посещают каждый день, и думаю, практически у всех так же.
В идеале надо бы выпустить ИИ агента в реальный мир, чтобы он сам потыкался, повзаимодействовал с людьми и их системами, чтобы прийти, возможно, даже к более правильному пониманию реальности, чем у людей. Как шахматные движки - сначала учились на человеческих играх и человеческом анализе позиций, но затем главной парадигмой стало то, что они стали обучаться сами, непосредственно на чистой игре, и человеческое участие вообще перестало требоваться, так как оно только ограничивало потенциал моделей. Современные шахматные модели умеют не только перебирать миллионы позиций, но и оценивать каждую отдельную позицию с более правильным, комплексным и глубоким пониманием, чем человек, и подобный подход надо перенести на универсальных агентов, если мы хотим супер ИИ. Я эти инициативы полностью поддерживаю. Конечно, нужно в процессе случайно не создать Скайнет, но это уже тема для отдельного обсуждения в области alignment.
Второе отличие функциональное и вполне очевидное - у LLM намного больше оперативной памяти на контекст, чем у человека, и выше скорость обработки этого контекста. Человек не может полностью держать в голове том на 500 страниц. Когда он его анализирует, он неизбежно сжимает длинные абзацы и предложения в их суть, оставляя главное, чтобы разительно уменьшить объем хранимой информации, не потеряв при этом самого важного. Хотя конечно агенты на основе LLM вроде Claude Code тоже используют такие подходы (функция compact) для того, чтобы не терять контекст, но мы сейчас говорим именно про LLM и их контексты на сотни тысяч токенов. Вот LLM реально может оперировать буквальными формулировками и быстро находить самые нетривиальные для человека связи между словами, разделенными десятками глав.
В то же время, длинный контекст все же ухудшает восприятие его элементов, так как внимание LLM тоже не безгранично. Здесь можно поэмпатизировать модели: если бы вам дали таблицу в виде html с атрибутами или в виде, собственно, таблицы, где бы вам было проще анализировать информацию? Из длинного контекста сложнее выделить важные детали, это же относится и к модели, она не парсер или лексический анализатор, чтобы спокойно обрабатывать мегабайты текста, ее процесс обработки гораздо ближе к человеческому. Хотя, конечно, с развитием LLM анализ длинного контекста становится все лучше и у передовых моделей выходит вполне качественным. Актуальный прогресс можно оценить, например, на странице бенчмарка Fiction.liveBench
Еще одно популярное недопонимание звучит как "LLM просто копирует то, что было в ее обучающих данных и не может изобрести что-то новое, в отличие от человека".
У меня всегда была склонность и страсть к изобретательству, и некоторые мои изобретения зарегистрированы в Роспатенте, что подтверждает, что они не только новы и полезны, но и обладают изобретательским уровнем - неочевидностью из уровня техники. Как человек, знакомый с процессом создания нового не понаслышке, я ответственно заявляю, что даже нетривиальные изобретения, которыми я горжусь, выводятся интеллектуальным трудом из того, что уже существует. И так можно сказать про любое изобретение. То, для чего нет научной основы, считается скорее шарлатанством, поскольку основано чисто на умозрительных гипотезах без привязки к реальности.
В качестве примера, смартфон (несомненно гениальное изобретение) можно наверное вывести из того, что пальцы человека подвижны, что человеку привычнее оперировать пальцами, чем объектом-посредником (стилусом), а также из последних технологических разработок, таких как емкостные сенсоры.
LLM обладает нейронными связями между абстрактными концепциями и при нужном промптинге легко создаст то, чего не было в обучающей выборке. Сколько еще всего полезного и прорывного можно изобрести, если только оптимально скомбинировать уже существующие идеи. Конечно, из доводов предыдущего раздела понятно, что поскольку LLM не обладают всей полнотой необходимых нейронных связей о мире, то полностью заменить изобретателей они пока, к сожалению, не в состоянии. Но в тех областях, которые LLM понимают хорошо, они уже могут легко комбинировать существующие понятия нетривиальным образом, получая иногда очень интересные новые идеи. Неоднократно было установлено, что LLM способны создавать новые и даже более оптимальные и оригинальные решения, которые до этого нигде не были описаны.
Из этого всего я лично прихожу к выводу, что процесс интеллектуальной работы современных LLM похож на человеческий (или любого другого абстрактного разумного агента), и его можно хорошо объяснить, если попытаться поставить себя на их место и перенести свой мыслительный процесс на них.
Конечно, для этого нужно обладать определенными навыками рефлексии, чтобы, для начала, понимать свое собственное мышление и уметь рационально объяснить свое (пусть даже иногда иррациональное) поведение. Кроме того, при экстраполяции своего мышления на LLM нужно также понимать, откуда и как она взяла свое понимание мира, чтобы понять, о чем она может хорошо судить, а о чем не очень, что ей известно, а что нет. В этом деле мне конечно максимально помогают мои фундаментальные знания об устройстве нейросетей, без них не уверен, что я бы мог хоть сколько-нибудь уверенно рассуждать о том, почему нейросети ведут себя так, а не иначе.
Например, есть наблюдение, что LLM иногда качественнее думают над вопросом, заданном на английском. Очевиднее всего я с этим столкнулся при тестировании о1 - первой рассуждающей модели, которая не допускала банальных косяков, свойственных моделям прошлого поколения. Когда я дал ей пару задач по спортивному программированию, сформулированных на русском, я получил достаточно скромный прогресс в решении. В то же время, стоило перевести задачи на английский, и модель смогла догадаться до ключевой идеи каждой из задач. В современных моделях я практически не сталкиваюсь с тем, что на русском результаты получаются хуже, но я все равно общаюсь с ними практически исключительно на английском (благо уровень С1-С2 позволяет выражать свои мысли легко и точно).
Это наблюдение я объясняю тем, что благодаря на порядок большему объему англоязычных текстов в обучающей выборке, слова на английском внутри модели имеют в среднем более развитые нейронные связи между собой, то есть то же слово на английском значит для модели чуть больше, чем на русском, и имеет для нее больше смысла. Иногда это приводит к тому, что задача, сформулированная на английском, задействует больше нейронных связей и порождает более объемлющий и глубокий мыслительный процесс, чем если бы она была сформулирована на русском, что приводит к большей осмысленности и полезности ответов.
Или что модели плохо могут знать российское законодательство или софт - в обучающей выборке если и были данные по ним, то не так много, чтобы они сильно повлияли на функцию ошибки при обучении или позволили модели сформировать точные концептуальные понятия в этой области и установить качественные нейронные связи между ними. Все же LLM учится не как человек - человеку достаточно выбрать суть из новой информации и встроить ее в уже имеющуюся и хорошо развитую нейросеть, поэтому ему достаточно один раз увидеть что-то и хорошо усвоить. С LLM же, как правило, это не так, требуется значительный объем данных, чтобы она построила нейронные связи между ними, так как LLM - это не отточенный эволюцией обучающийся механизм. Можно представить, что она учится с нуля, вообще из ничего, в отличие от человека с его развитым с рождения мозгом.
Если осознанно учитывать ограничения наподобие описанных выше, то в большинстве случаев удается хорошо поставить себя на место LLM, как ответственный учитель ставит себя на место ученика, чтобы донести до него информацию оптимальным образом, учитывая его текущий уровень знаний и познавательных способностей. За счет этого у меня практически всегда получается заранее предсказать, как LLM отреагирует на ту или иную формулировку, таким образом нужный результат я получаю с первого раза или заранее понимаю, что я не смогу сформулировать задачу как следует и не пишу вообще ничего.
Зачем я вообще писал все это? Как раз затем, чтобы заложить общую основу, из которой теперь будут очень естественным образом следовать некоторые частные правила. Я лично при изучении новой темы стараюсь сначала очень глубоко погрузиться в теорию, даже не приступая к практике, а после того, как стройная картина в голове сформируется, я начинаю очень эффективно, гибко и творчески решать реальные задачи, чаще всего даже лучше, чем те, кто все это время учился на практике. Я считаю, что фундаментальное понимание теории позволяет (для интеллектуальных дисциплин конечно, а не моторных) свободно владеть практикой во всех ее проявлениях, поэтому при написании статьи я попытался использовать именно этот подход, чтобы вы могли стать как можно более результативными практиками.
Правила промпт-инжиниринга ниже - это лишь ориентировочный набор, который я часто использую на практике, в то же время при грамотном владении теорией, вы сможете придумывать свои подходы, которые будут работать, потому что вы понимаете, как.
В первую очередь, очевидно, задача, которую вы даете LLM, должна быть понятно сформулирована. Просто написать по принципу "я так чувствую" не годится. LLM читает мысли не лучше человека, поэтому представьте, что вы видите вашу формулировку впервые и обладаете только общим уровнем знаний. Насколько однозначно формулировка определяет то, что вы в нее закладывали? Насколько слова, которые вы подобрали, соответствуют задаче и не имеют иных возможных толкований в данном контексте, что могло бы ввести вас в заблуждение относительно смысла других слов в вашей формулировке? В целом, для ИИ не требуется как-то по-особому нянчиться: если человеку со стороны (с должным уровнем экспертизы) ваша формулировка хорошо понятна, она почти наверняка будет понятна и для ИИ.
В качестве примера приведу видоизмененный фрагмент алгоритма, который для меня описал начинающий аналитик на одном из проектов. Не хочу никого обидеть, но так уж вышло, что работы этого аналитика я постоянно привожу в качестве примеров, как не надо делать.
Создать ClassA
Для каждого элемента массива ClassBArray:
Создать ClassB
Указать в поле ClassAId Id созданного ClassA
Указать в поле ClassBId значение ClassBArrayElement.Id
Если массив ClassBArray пустой, или любой ClassBArrayElement.Id is null, не создавать ClassB для каждого элемента массива
Для каждого элемента массива ClassCArray:
Создать ClassC
Указать в поле ClassAId Id созданного ClassA
Назначить поля ClassC1Id = ClassCArrayElement.ClassC1Id , ClassC2Id = ClassCArrayElement.ClassC2Id
Если указаны оба, ClassC1Id сделать null
Если массив ClassCArray пустой, или любой элемент массива содержит оба атрибута is null, не создавать ClassC для каждого элемента массива
Проверить в массиве ClassCArray каждый ClassC2Id (из тех объектов, где ClassC1Id is null) на принадлежность к каждому ClassC1Id (из тех объектов, где ClassC2Id is null). Если принадлежность найдена, то значения таких ClassC2Id не сохранять. (Если в объекте массива, оба атрибута is not null, считать в таком объекте ClassC1Id is null)
Вот я сейчас читаю это, и отдельные моменты вообще не понимаю. Когда я докопался до сути у тимлида, я удивился, как так можно было формулировать. При этом аналитик говорил условно: "я написал, мне понятно, тебе может непонятно", хотя одна из главных задач аналитика (и пользователя LLM) как раз в том, чтобы формулировка была понятна универсально. И, на мой взгляд, на самом деле, если мысль не получается сформулировать четко, это признак того, что и в голове она оформлена нечетко.
Я переписал этот алгоритм так:
Создать ClassA
Для массивов ClassBArray и ClassCArray: удалить дубликаты; если есть элемент, у которого все поля is null, очистить соответствующий массив.
Из элементов массива ClassCArray, где ClassC2Id is null, сформировать список ClassC1Id. Удалить элементы, где ClassC2Id != null и ClassC2.ClassC1Id находится в сформированном списке ClassC1Id. У оставшихся элементов с ClassC2Id != null обеспечить ClassC1Id = null
Для каждого элемента ClassBArray cоздать ClassB, у которого ClassBId = ClassBArrayElement.Id, а ClassAId = Id созданного ClassA.
Для каждого элемента ClassCArray cоздать ClassC, у которого ClassC2Id = ClassCArrayElement.ClassC2Id, ClassC1Id = ClassCArrayElement.ClassC1Id, а ClassAId = Id созданного ClassA.
Получившаяся формулировка намного точнее описывает алгоритм, да еще и компактнее и проще читается. При этом, по моему опыту, формулировки от опытных аналитиков не обязательно читаются просто, но они непременно однозначны и непротиворечивы. То есть когда вникнешь, картинка становится очень четкой, и такие формулировки без изменений подходят для ИИ.
Мне, на самом деле, иногда даже больше нравится общаться с ИИ, чем с человеком, поскольку я частенько выражаю свои мысли достаточно комплексными языковыми структурами вроде длинных сложноподчиненных предложений. Такие предложения лично мне привычно читать, а вот некоторым людям - нет, поэтому они иногда могут не очень хорошо понимать их. В то же время, ИИ запутанность формулировки не пугает: если текст сложный структурно, но при этом точно и однозначно выражает конкретную мысль, то ИИ разберется в формулировках и отлично меня поймет.
Какие бы четкие формулировки у вас ни были, они должны в любом случае использовать те понятия, которые хорошо известны читателю. ИИ хорошо осведомлен об общем уровне техники, но если вы будете описывать задачу, которая связана с какими-то особенностями вашего проекта (например, стилем кода, используемыми паттернами, конвенциями или архитектурой), то поскольку ИИ вообще не в курсе о том, как устроен ваш проект, ваши формулировки будут для него значить очень мало. Он максимум сможет предоставить результат на основе одного из многочисленных вариантов интерпретации задания, и рассчитывать, что он угадает все правильно, не стоит.
Представьте себя на месте ИИ: вы не можете задавать уточняющие вопросы, и вам дают задание по написанию кода, который в разных проектах может быть устроен вообще по-разному. При этом вы не можете отказаться выполнять задание, и вам требуется выполнить его максимально хорошо. Вам ничего не остается, кроме как предположить какую-то архитектуру верной и придерживаться выбранной тактики.
Исполнителю (ИИ) следует знать вообще все, что неочевидно из уровня техники, и что необходимо, чтобы прийти к правильному результату. Не существует какого-то джентльменского набора файлов или окружения, который будет гарантировать достаточность информации - вы должны сами определять, чем дополнить контекст в каждой конкретной задаче.
Благо, это не сложно, если представить себя на месте непосвященного читателя вашего задания и подумать, как он должен прийти именно к тому результату, который вы от него ожидаете, а не к какому-то еще. Почему ваша формулировка не оставляет свободы действий и иных разумных вариантов интерпретации.
Например, вы можете приложить интерфейсы, с которыми должен взаимодействовать создаваемый код, json, структуру которого нужно знать для правильной обработки, код классов, с которыми придется работать, DDL таблиц в бд (очень кстати удобно, если требуется писать на SQL), паттерны и хэлперы, которые обычно используются при написании аналогичного кода.
Учитывайте также, что LLM обладает, в первую очередь, пониманием предметной области, а не строго структурированными знаниями, поэтому например, просто упоминания номера версии фреймворка часто недостаточно, чтобы одним этим номером заставить ИИ писать код только для этой версии. На моем стеке такая ситуация встречается крайне редко, поэтому гипотезы ниже являются умозрительными и не проверены мной лично, но, тем не менее, я достаточно в них уверен, можете проверить сами и отписаться о результатах.
Можно, как вариант, включить в контекст ключевые фрагменты release notes или документации, либо примеры кода, чтобы задействовать у LLM больше нейронных связей с паттернами интересующей версии фреймворка, а не только сравнительно слабые и точечные связи между паттернами и номером версии.
Если у LLM будут качественные доп материалы в контексте, она будет видеть, что "ага, конструкцию AA упоминают в промпте, а я знаю, что с ней еще используются конструкции AB и AG, еще мне приводят другую конструкцию AC, которая тоже связана c AB, но еще и с AD, при этом конструкции BA, BB, BD, BG несовместимы с ними...", то есть требуемое множество паттернов AA-AZ для A версии фреймворка оформится намного четче, и у модели не будет желания использовать конструкции BA-BZ из версии B, если они несовместимы с версией A. При этом если бы мы просто указали номер версии (например, 1.0.3), то связь между 1.0.3 и множеством AA-AZ была бы намного слабее, чем внутри самого множества часто используемых вместе конструкций AA-AZ.
Когда разработчик понимает важность достаточного контекста, у него может возникнуть желание загрузить в LLM весь исходный код проекта. Но здесь кроется очень большой подвох - длинный контекст ухудшает производительность LLM. Если пытаться использовать контекстное окно на максимум, то в среднем LLM будет понимать и выполнять задачу хуже, чем если бы в контексте было только то, что реально необходимо. LLM будет давать больше багов, больше странного кода, результат придется дорабатывать дополнительными указаниями или генерировать повторно в надежде на удачу.
Представьте, что вам бы приходилось изучать весь исходный код проекта с нуля каждый раз, когда нужно внести доработку - насколько проще вам было бы, если бы вам кратко но понятно и точно объяснили, каких правил придерживаются при написании аналогичного кода, какие паттерны и хэлперы используют и дали содержательный пример вместо того, чтобы вы это все выясняли сами. Внимание модели не безгранично, как и у человека, и если оно было потрачено на изучение паттернов, на качественное решение задачи его останется меньше, не говоря уже о том, что нахождение закономерностей внутри длинного контекста дается LLM хуже, чем внутри короткого.
Попытки загрузить весь проект в контекст, ведущие к разочарованию в способностях LLM - это то, что часто останавливает неопытных промпт-инженеров (которые при этом могут быть очень даже опытными разработчиками). LLM намного ближе к человеку, чем к классическим точным алгоритмам, и относиться к ней нужно по-человечески. Вы же сами, когда работаете на проекте, не храните весь исходный код в голове, у вас есть скорее что-то вроде индекса проекта - понимание, как что устроено и где что найти. И если вы хотите, чтобы LLM вам хорошо писала код, эту экспертизу нужно передать ей. Кстати, в этом вам может помочь Claude Code с его командой /init. Она запускает автоматическое изучение репозитория и формирование текстовой сводки по архитектуре и используемым паттернам. Результат работы этой команды очень похож на то, что я писал руками до появления Claude Code.
Цель таких текстовых описаний - избавить разработчика (в том числе ИИ) от необходимости обнаруживать неочевидные закономерности из кода с нуля. Например, на одном из проектов у нас был костыль для обхода бага в библиотеке: к фильтрующему по Id условию нужно было добавлять && true. Чтобы выявить такой паттерн чисто из кода, пришлось бы посмотреть непонятно сколько примеров, а явное текстовое описание отметает необходимость в таком анализе. Или более обыденный случай - у нас была функция с не слишком говорящим названием. Когда она только появилась, разработчик в команде спросил, что она делает - и это максимально правильный подход. Если бы он пошел разбираться сам, ему бы пришлось перелопатить изрядное количество кода, чтобы выдвинуть достаточно вероятную версию. Да и то, меня бы лично в такой ситуации постоянно бы грызли и отвлекали сомнения, а точно ли я все правильно понял. Достаточно было просто словами задокументировать функцию, и объем требуемой интеллектуальной работы для ее пользователей значительно уменьшился.
Как человек будет пыхтеть и страдать, если ему потребуется выявлять паттерны в проекте с нуля, что скорее всего еще и приведет к худшему качеству, чем если бы та же энергия была направлена на решение основной задачи, так и LLM, хоть и несравнимо более быстрая, чем человек, лучше справится, если ей не придется предварительно производить лишнюю интеллектуальную работу.
Когда я начал писать серьезные многоразовые промпты на проекте, я думал, вот бы мне при погружении дали почитать то, что я сейчас объясняю LLM, я бы въехал намного быстрее. Создание емкого и качественного контекста на самом деле полезно не только для ИИ, но и для новых разработчиков в команде.
Несколько способов, как можно сократить контекст, чтобы добиться лучшего результата от LLM:
Вместо полного текста функции, которую нужно использовать, приведите только ее сигнатуру, если этого достаточно для понимания, что она делает. Как развитие идеи - вместо полного класса приведите его интерфейс.
Полный массив похожих друг на друга объектов json обрежьте до пары элементов, демонстрирующих все важные аспекты структуры или создайте сами такой максимально содержательный элемент (можно отдельным промптом) - этого будет достаточно, чтобы ИИ понял, как устроен массив.
Позаботьтесь о минификации структурированной информации. Передавать сырые таблицы на HTML, например, я считаю, вообще негуманно - переведите их лучше в csv.
Вместо простыней кода, который использует принятые у вас паттерны и хэлперы, явно опишите сценарии использования паттернов и хэлперов.
И еще один пункт, который многим может дать самое значительное сокращение контекста распишу отдельно: не ведите общение с LLM в формате диалога, если это необязательно! При работе в режиме диалога в контекст попадает вся ваша прошлая переписка. LLM начинает отвечать медленнее, ее ответы становятся дороже, а результат хуже. Например, если вы решили вдруг повайбкодить и итеративно дорабатываете кусок кода, то зачем LLM знать про все предыдущие версии этого кода? Перетирайте свое первое сообщение, добавляйте актуальную версию и пожелания по правкам.
Режим диалога полезен разве что когда все предыдущие сообщения целиком важны для нового ответа, например, чтобы ИИ знал, о чем он уже рассказал вам, и не повторялся. Но это и не генерация кода, а скорее просто общение с LLM. При серьезной методичной кодогенерации практически без исключений требуемый результат можно и нужно получать с одного промпта. Если результат не устраивает, правильнее будет поменять исходный промпт, а не просить доработать получившийся код.
Для понимания, каким может быть баланс достаточности и минимальности контекста - на моем опыте, оптимизированные многоразовые промпты примерно в 2 раза длиннее выхода, который от них ожидается, а те, что я пишу с нуля - примерно в 3. То есть для генерации, условно, 100 строк кода (около 1000 токенов), требуется 2-3 тысячи токенов на входе.
Главное отличие многоразовых и одноразовых промптов в том, что многоразовые написаны в основном руками и оптимизированы так, что дают результат (на примере крудов, простых или комплексных), который в 70% случаев вообще не приходится править, так как он идеален во всех отношениях, а в остальных 30% случаев правки точечные и быстрые (на пару минут). Всякую инфраструктуру без логики LLM может генерить идеально в 100% случаев. В одноразовых же промптах контекст практически полностью состоит из грамотно скопированного из проекта кода и ТЗ, поэтому создавать их получается быстро, но результат приходится править руками практически всегда, что, впрочем, не отменяет того, что ИИ все равно делает за меня около 80% полезной интеллектуальной работы.
Как ни важно уметь емко описывать ключевые паттерны, некоторые из них (простые, но очень многочисленные) лучше всего продемонстрировать на примере. Например, из вот такого, казалось бы, простого фрагмента на C#
/// <summary>
/// Добавление книги
/// </summary>
public sealed class BookCreateCommand : IRequest<Result>
{
#region Input Property
/// <summary>
/// Идентификатор автора
/// </summary>
public int AuthorId { get; set; }
/// <summary>
/// ISBN
/// </summary>
[MaxLength(64)]
public string? ISBN { get; set; }
/// <summary>
/// Идентификатор жанра
/// </summary>
public int GenreId { get; set; }
#endregion
/// <inheritdoc />
public sealed class CommandHandler : IRequestHandler<BookCreateCommand, Result>
{
...
public async Task<Result> Handle(BookCreateCommand command, CancellationToken cancellationToken)
{
можно с большой вероятностью предположить, что, в частности:
Параметры CQRS команды задаются в ее классе, а не в отдельном DTO
Входные параметры заключаются в #region, при этом после #region и до #endregion, а также между входными параметрами ставится пустая строка
Для документирования CommandHandler используется <inheritdoc />, а для входных параметров <summary>, при этом после данной разметки пустая строка не ставится
У класса команды ставится модификатор sealed
Это очень примитивные, но все же паттерны, и если вы хотите, чтобы сгенерированный код идеально соответствовал стилю проекта, вы должны посвятить LLM и в них. Но если пытаться сформулировать все мельчайшие нюансы словами (список выше можно дополнить и расписать намного подробнее), то такое объяснение только все усложнит. Коллеге же вы не будете объяснять все на словах, а скажете: "смотри, вот пример, делай по аналогии". ИИ обладает далеко не нулевым IQ и без труда обнаружит многие закономерности.
Вообще, примеры - это очень насыщенные и полезные источники информации. Если в контексте у вас будет пример не только результата, но и входных данных, из которых результат получился, то LLM сможет выявить не только микропаттерны оформления, но и не прописанные явно пути преобразования входа в выход. Например:
Input: 3, 1, 4 → Output: 11
Input: 2, 7, 5 → Output: 3
Input: 8, 2, 1 → Output: 6
Input: "apple" → Output: "eppal"
Input: "orange" → Output: "enarog"
Input: "banana" → Output: "aanabn"
Input:
POST /api/paramPamPam - Параметр Pam Pam
Название;Признак;Тип;Обязательность;Уникальность;Описание;БД
Id;PK;int;+;+;Идентификатор;ParamPamPam.id
Something;NF;object;+;;Кое-что, Навигационное поле к Something;ParamPamPam.somethingId
PuPuPuId;FK;int;+;;Надо подумать, FK к PuPuPu;ParamPamPam.puPuPuId
ParamType;;enum(32);;;Тип праметра;ParamPamPam.paramType
Output:
private static Entity ParamPamPamModel() =>
new("paramPamPam", "Параметр Pam Pam")
{
Flags = new[] { EntityFlag.Export, EntityFlag.Extendable, EntityFlag.Extended },
Fields = new List<Field>
{
new Field("Id", "Идентификатор.").AsInt().NotNull().PrimaryKey().Unique(),
new Field("Something", "Кое-что", "Навигационное поле к Something.").AsObject().NotNull().Navigation("something"),
new Field("PuPuPuId", "Надо подумать, FK к PuPuPu", "Навигационное поле к PuPuPu.").AsInt().NotNull().ForeignKey("puPuPu"),
new Field("ParamType", "Тип праметра.").AsEnum(Enum.GetNames(typeof(ParamType)))
}
};
В первом примере закономерность преобразования такая: (левое * правое - среднее)
Во втором предпоследнюю букву ставим в конец, остальные переворачиваем
В третьем примере csv таблица преобразуется в код согласно паттернам проекта
Кроме того, в конце статьи приведен реальный промпт, в котором также есть специально созданный вручную пример для генерации CQRS команд согласно стилю проекта
Добавление в контекст даже одного примера в формате "вход - идеальный выход для входа" позволяет модели значительно лучше понять в том числе те интеллектуальные преобразования, которые вы описали явно, и, кроме того, установить свое внутреннее понимание множества других не описанных вами преобразований, что значительно повышает правильность производимой работы и качество результата.
Посудите сами, в каком случае вам будет проще разобраться в задаче и быть уверенным, что вы решили ее в соответствии с ожиданиями: когда вам дают просто абстрактное ТЗ без примеров, когда вам дают гору кода для самостоятельного изучения и ТЗ или когда вам дают ТЗ, качественное и понятное описание паттернов проекта, притом только тех, которые нужны для данной задачи, да еще и сопровождают примером, на котором видно, как похожая задача решается с применением описанных паттернов.
Качественные примеры в формате вход-выход - это настолько мощный способ усиления промпта, что Anthropic сделали даже отдельную форму для их добавления при работе с Claude. Если вы знакомы с популярным понятием "системный промпт", который рекомендуют использовать чуть ли не в каждом гайде по промпт-инжинирингу, то примеры, на моем опыте, намного важнее. Системным промптом я не пользуюсь вообще, а вот примеры вход-выход вставляю чуть ли не в каждый промпт для серьезной генерации кода.
В целом, это уже было сказано в разделах про достаточный и минимальный контекст, но еще раз акцентирую внимание на том, что задача промпт-инженера - сделать работу модели как можно проще. Зачем заставлять ее проводить сложные размышления над неподготовленными данными, если можно потратить чуть больше времени и включить в контекст результаты уже проделанной интеллектуальной работы.
Например, для того, чтобы выполнять задачи на проекте с плохо задокументированным кодом, для начала требуется произвести интеллектуальную работу по изучению репозитория. Результатом этой работы является понимание устройства проекта, это понимание можно сформулировать в виде текстового описания архитектуры и паттернов и использовать как отправную точку для модели, не вынуждая ее изучать репозиторий каждый раз с нуля. Даже упомянутый ранее Claude Code при автоматическом изучении репозитория формирует файлик claude.md, в который пишет ни что иное, как обнаруженные паттерны и архитектуру, чтобы дальнейшую свою работу основывать на уже проделанной.
Или если вы из опыта знаете, как сформулировать задачу, чтобы ИИ пришлось как можно меньше догадываться до чего-то, то включение такой полезной информации в промпт, пусть и потребует дополнительного времени, но в конечном счете сэкономит вам его за счет лучшего результата, который нужно меньше исправлять. Я даже замерял несколько раз, сколько времени на что у меня уходит при разработке через ИИ. При решении большой задачи на составление промпта я потратил 40 минут, зато потом запустил его и с первого раза получил отличный код, в итоге потратив на его проверку и доработку 2 часа вместо 10, которые требовались бы для написания аналогичного кода с нуля. При этом я уверен, что самописный код был бы в итоге худшего качества, так как при использовании ИИ я трачу силы в основном на рефакторинг и украшательства, а не на реализацию логики.
В целом, нужно стремиться к промпту, из которого желаемый результат получается как минимальным объемом всех логических переходов, так и с минимальной длиной самого длинного из них. Это конечно уже менее важная оптимизация, но не стоит ограничиваться тем, что из промпта однозначно выводится интересующий результат, стоит предоставить в промпте срез дерева вывода по возможности ближе к листьям, а не к корню. Это как если бы вам нужно было вывести 100 число Фибоначчи, а в промпте вы бы задали только рекуррентное соотношение и первые два числа. Если вам вдруг известны, например, 97 и 98 число, количество шагов вывода до результата из промпта значительно бы уменьшилось, и вероятность получения правильного ответа бы значительно возросла.
Этот пункт также уже был затронут ранее, но он крайне важен. Требуется, чтобы по промпту как можно более однозначно определялся требуемый результат, не допуская иных достаточно вероятных толкований. Для самопроверки нужно хорошо понимать, путем каких именно логических переходов из известного уровня техники и вашего промпта ИИ догадается до каждой буквы, скобочки и кавычки ожидаемого результата и не посчитает более вероятной иную интерпретацию задания.
В конце статьи приведен типичный представитель моих многоразовых промптов для генерации крудов различной сложности. В качестве иллюстрации продемонстрирую некоторые пути вывода фрагментов ожидаемого результата из этого промпта и уровня техники.
await _unitOfWorkFactory.GetExecutionStrategy(async () =>
{
using var scope = _unitOfWorkFactory.Create();
try
{
выводится чисто из промпта и примера в промпте, содержащего буквально эти же строчки
Inside CommandHandler Handle method introduce global try-catch with UnitOfWorkFactory scope around it like in the example, but only if database is modified.
var codeExists = await _enterpriseRepository.GetQuery()
.AnyAsync(e => e.Code == command.Code, cancellationToken);
выводится из уровня техники (стандартный синтаксис C# и EF Core), а также фрагментов промпта и примера в нем:
<...>
**Валидация:**
**0080: "Некорректный префикс предприятия"**. Code должен быть уникальным.
<...>
Код;Тип;Ограничения;Обязательность;Описание;БД
<...>
Code;string(8);/^[a-zA-Z0-9]$/;+;Префикс предприятия;enterprise.code
<...>
Enterprise DB model:
/// <summary>
/// Предприятие
/// </summary>
public class Enterprise : BaseHistory<int>, IExtensible, IHasId
{
<...>
/// <summary>
/// Код
/// </summary>
public string Code { get; set; } = null!;
<...>
}
<...>
Necessary entities and their properties' names can be obtained from the "БД" column by using UpperCamelCase on them. Work with entities using async EF Core LINQ instructions on injected in the constructor IBaseRepository<TEntity, int> after calling GetQuery() on its instance.
<...>
Name the flag bool results according to their purpose (like sameNameEntityExists)
<...>
Name lambda expression parameters meaningfully by full class names or meaningful abbreviations or the first letter of the class name
<...>
private readonly IBaseRepository<EquipmentParam, int> _equipmentParamRepository;
<...>
var duplicateEquipmentParamInputExists = await _equipmentInputRepository.GetQuery()
.AnyAsync(e =>
<...>
Рассмотрим второй пример выводимости подробнее (как это должно происходить в голове при самопроверке)
var codeExists = выводится из общих знаний синтаксиса С# (var, так как нам нужна переменная, =, так как мы присваиваем значение этой переменной), а codeExists выводится из фрагментов промпта: Name the flag bool results according to their purpose (like sameNameEntityExists) - здесь явно говорится, что булевые переменные должны называться в соответствии с их назначением, а также неявно приведен пример такого говорящего названия в lowerCamelCase, что, пусть и не инструктирует LLM достаточно четко относительно наилучшего стиля наименования и оставляет пространство для двоякой интерпретации, но на практике приемлемо (так как названия фиксить быстро). Если бы нужно было дальше улучшать промпт, эту инструкцию можно было бы расписать так: Name the bool variables according to their purpose so that the name represents a complete question implying an answer yes or no that allows to track the exact semantic meaning of the variable and why it's valuable throughout the scope (like sameNameEntityExists). После такой правки LLM начинает генерировать еще более говорящие названия, например в данном случае codeExistsInAnotherEnterprise.
await _enterpriseRepository.GetQuery().AnyAsync. _enterpriseRepository выводится из стиля наименования, типичного для приватных полей в C# и неоднократно продемонстрированного в примере из промпта:
private readonly IBaseRepository _equipmentParamRepository.
await, GetQuery и AnyAsync(<...>, cancellationToken) выводится из инструкций в промпте: Work with entities using async EF Core LINQ instructions on injected in the constructor IBaseRepository after calling GetQuery() on its instance.
При этом IBaseRepository и GetQuery() - это наши хэлперы, а await и AnyAsync - стандартные синтаксические конструкции C# и EF Core.
e => e.Code == command.Code, cancellationToken выводится из промпта:
Name lambda expression parameters by full class names or meaningful abbreviations or the first letter of the class names,
из таблицы с описаниями входных параметров функции:
Код;Тип;Ограничения;Обязательность;Описание;БД
<...>
Code;string(8);/^[a-zA-Z0-9]$/;+;Префикс предприятия;enterprise.code
из еще одной соответствующей части промпта:
Necessary entities and their properties' names can be obtained from the "БД" column by using UpperCamelCase on them
и примера, содержащегося в промпте:
<...> .AnyAsync(e => <...>
Аналогичным образом нужно уметь объяснять выводимость из промпта и общего уровня техники любого участка ожидаемого результата чтобы проверять промпт на наличие потенциальных неоднозначностей или даже (при невнимательном составлении) противоречий.
В качестве финального принципа я повторю то, с чего начал. Все правила, описанные выше, очень естественны при работе с разумным ассистентом, будь то ИИ или человек. Когда я даю задание человеку, я руководствуюсь практически теми же самыми принципами. Да и сам я предпочел бы получать задания, сформулированные подобным образом, так как, рефлексируя, понимаю, что грамотная постановка задачи способна сильно упростить мне жизнь и повысить соответствие моего результата ожиданиям заказчика.
Кому-то может показаться, что составление промпта по всем канонам - это неоправданно трудозатратная операция, но по моему опыту, при понимании ключевых принципов, можно заранее оценить трудоемкость составления промпта и понять, не целесообразнее ли написать код самому. На самом деле, при наличии некоторой сноровки, ситуации, когда быстрее написать код, чем промпт с последующим рефакторингом, возникают сравнительно редко, так как хорошие промпты с нуля пишутся за 10-20 минут.
Более того, скорее всего, на проекте есть несколько классов задач, которые приходится решать регулярно, и код для которых структурно достаточно похож (например, круды). Для таких задач можно заморочиться и написать крутой оптимизированный многоразовый промпт, на вход которому будет подаваться алгоритм и доп контекст для конкретного круда, а на выходе будет получаться код, который чаще всего вообще не придется править, так как он будет идеальным, либо же, когда правки требуются, они будут занимать минимум времени. Написание такого мощного промпта занимает часов 5-7 (и улучшения по ходу использования еще пару часов), но если такие задачи частые, это очень оправданное вложение времени, способное сэкономить сотни часов.
Далее привожу пример такого многоразового промпта для крудов и других эндпоинтов, который очень здорово помог на одном из проектов, а после него входные данные для конкретного круда к этому промпту.
Generate a CQRS command according to the description:
Вставьте сюда аналитику и доп контекст
Below are additional instructions and examples
Add summary for the command and its properties copying the Russian command and properties names. Sometimes there are arrays of objects in command's input properties. In this case declare a Dto class with a name comprised of the array's name and command type suffixed with Dto and give it array item's properties (prefixed in the description by semicolon) and declare the array as ICollection of the declared Dto like in the example. Sometimes there are output properties of the command (their description has a similar format to the input properties). In this case after the command declare a result Dto naming it after the command + ResultDto and use Result<TDto> instead of plain Result as the returning type in the command. For dates use DateTimeOffset type. Don't use warning suppression for Dto fields like = null! or similar. In our project nullability of the input parameters is checked automatically and absence of "?" in the end of the type ensures that the property is required. Vice versa the optional properties must have "?" at the end of their types.
Inside CommandHandler Handle method introduce global try-catch with UnitOfWorkFactory scope around it like in the example, but only if database is modified.
Necessary entities and their properties' names can be obtained from the "БД" column by using UpperCamelCase on them. Work with entities using async EF Core LINQ instructions on injected in the constructor IBaseRepository<TEntity, int> after calling GetQuery() on its instance. Name the obtained enitites by their class names in lowerCamelCase and suffix with FromDb. Name the flag bool results according to their purpose (like sameNameEntityExists). When creating entities you should name the variables like their className. Name lambda expression parameters meaningfully by full class names or meaningful abbreviations or the first letter of the class name. You may use x as lambda expression parameter name for some simple calculations. There's also available an IQueryable extension method GetByIdAsync that accepts id value, cancellation token and an optional enterpriseId if you need to check the enterprise of the requested entity and throws if the needed entity cannot be obtained. Don't include parent entity if you just need its key, use child's foreign key instead. Try to minimize database round-trips where it doesn't impair readability significantly.
For each foreign and primary key in the command validate existence of the corresponding entity by using GetByIdAsync. Also perform all the validations inside "Валидации" section. If any validation fails, throw the appropriate error via CustomValidationExceptionFactory.CreateApiError like in the example (first argument is ApiError.ErrorKeyXXXX, next arguments take optional error message arguments to substitute into {} in the error message). Surround all validations (as opposite to the core logic) in #region Validations padded by empty lines. You can perform simple validations (like length or regex match) using attributes for command's properties.
Use AddAsync, UpdateAsync (accepting TEntity) and DeleteAsync (accepting deleted entity key) of IBaseRepository to perform CRUD. There are also AddRangeAsync and DeleteRangeAsync methods accepting IEnumerable<TEntity> for bulk operations. Also in complex scenarios for conciseness you can use SaveChangesAsync.
Example description:
## POST /api/v1/function/equipmentInput-add - Создание настройки входа для конфигурации оборудования
**Валидации:**
67 - Параметр функции не может быть пустым для указанной функции
(FilterFunctionParamI1 - обязательный параметр, в случае, если указан FilterFunction = medianFilter)
65 - Переданные исходные значения тарировки не должны повторяться
Предприятие записи Input соответствует предприятию EquipmentParam.Equipment
106 (В сущности {Entity} значения {fieldName1,fieldName2, ... } должны быть уникальными) - отсутствует запись с такими же EquipmentParamId и InputId
**Формат запроса**
Название;Тип;Ограничения;Обязательность;Описание;В БД
EquipmentParamId;int;equipmentParam;+;Идентификатор настройки параметра оборудования;EquipmentInput.equipmentParamId
FilterFunction;enum(32);;;Функция фильтра;EquipmentInput.filterFunction
FilterFunctionParamI1;int;;;Первый целый параметр функции фильтра;EquipmentInput.filterFunctionParamI1
EquipmentCalibrationData;object[];len > 1;;Данные тарировки;
;EquipmentCalibrationData.Raw;float;;+;Исходное значение;EquipmentCalibrationData.raw
**Алгоритм**
Добавить переданные данные тарировки в качестве дочерних сущностей к добавляемому EquipmentInput
Вернуть идентфикатор созданной настройки входа для конфигурации оборудования
**Формат ответа (content)**
Код;Тип;Описание;БД
Id;int;Идентификатор настройки входа для конфигурации оборудования;equipment_input.id
Ideal output for the description:
/// <summary>
/// Создание настройки входа для конфигурации оборудования
/// </summary>
public sealed class EquipmentInputAddCommand : IRequest<Result<EquipmentInputAddResultDto>>
{
#region Input Property
/// <summary>
/// Идентификатор настройки параметра оборудования
/// </summary>
public int EquipmentParamId { get; set; }
/// <summary>
/// Идентификатор входа
/// </summary>
public int InputId { get; set; }
/// <summary>
/// Функция фильтра
/// </summary>
public FilterFunctionEnum? FilterFunction { get; set; }
/// <summary>
/// Первый целый параметр функции фильтра
/// </summary>
public int? FilterFunctionParamI1 { get; set; }
/// <summary>
/// Данные тарировки
/// </summary>
[MinLength(2)]
public ICollection<EquipmentCalibrationDataAddDto>? EquipmentCalibrationData { get; set; }
public sealed class EquipmentCalibrationDataAddDto
{
/// <summary>
/// Исходное значение
/// </summary>
public float Raw { get; set; }
}
#endregion
/// <inheritdoc />
public sealed class CommandHandler : IRequestHandler<EquipmentInputAddCommand, Result<EquipmentInputAddResultDto>>
{
private readonly IUnitOfWorkFactory _unitOfWorkFactory;
private readonly IBaseRepository<EquipmentParam, int> _equipmentParamRepository;
private readonly IBaseRepository<EquipmentInput, int> _equipmentInputRepository;
private readonly IBaseRepository<Input, int> _inputRepository;
public CommandHandler(IUnitOfWorkFactory unitOfWorkFactory,
IBaseRepository<EquipmentParam, int> equipmentParamRepository,
IBaseRepository<EquipmentInput, int> equipmentInputRepository,
IBaseRepository<Input, int> inputRepository)
{
_unitOfWorkFactory = unitOfWorkFactory;
_equipmentParamRepository = equipmentParamRepository;
_equipmentInputRepository = equipmentInputRepository;
_inputRepository = inputRepository;
}
public async Task<Result<EquipmentInputAddResultDto>> Handle(EquipmentInputAddCommand command, CancellationToken cancellationToken)
{
var result = new Result<EquipmentInputAddResultDto>
{
Content = new EquipmentInputAddResultDto()
};
await _unitOfWorkFactory.GetExecutionStrategy(async () =>
{
using var scope = _unitOfWorkFactory.Create();
try
{
#region Validations
var equipmentParamFromDb = await _equipmentParamRepository.GetQuery()
.Include(e => e.Equipment)
.GetByIdAsync(command.EquipmentParamId, cancellationToken);
if (command.FilterFunction == FilterFunctionEnum.MedianFilter && command.FilterFunctionParamI1 == null)
{
throw CustomValidationExceptionFactory.CreateApiError(ApiError.ErrorKey0067);
}
command.EquipmentCalibrationData ??= new List<EquipmentCalibrationDataAddDto>();
if (command.EquipmentCalibrationData.GroupBy(calData => calData.Raw).Count()
< command.EquipmentCalibrationData.Count)
{
throw CustomValidationExceptionFactory.CreateApiError(ApiError.ErrorKey0065);
}
await _inputRepository.GetQuery()
.GetByIdAsync(command.InputId, cancellationToken, equipmentParamFromDb.Equipment.EnterpriseId);
var duplicateEquipmentParamInputExists = await _equipmentInputRepository.GetQuery()
.AnyAsync(e =>
e.EquipmentParamId == command.EquipmentParamId
&& eqInput.InputId == command.InputId, cancellationToken);
if (duplicateEquipmentParamInputExists)
{
throw CustomValidationExceptionFactory.CreateApiError(
ApiError.ErrorKey0106, nameof(EquipmentInput),
$"{nameof(EquipmentInput.EquipmentParamId)}, {nameof(EquipmentInput.InputId)}");
}
#endregion
var equipmentInput = new EquipmentInput
{
EquipmentParamId = command.EquipmentParamId,
FilterFunction = command.FilterFunction,
FilterFunctionParamI1 = command.FilterFunctionParamI1,
EquipmentCalibrationData = new List<EquipmentCalibrationData>()
};
equipmentInput.EquipmentCalibrationData.AddRange(
command.EquipmentCalibrationData
.Select(calData =>
new EquipmentCalibrationData
{
Raw = calData.Raw,
}));
await _equipmentInputRepository.AddAsync(equipmentInput, cancellationToken);
result.Content.Id = equipmentInput.Id;
await scope.CommitAsync();
}
catch
{
await scope.RollbackAsync();
throw;
}
});
return result;
}
}
}
И ниже аналитическая постановка и доп контекст для подстановки в этот промпт в специально выделенном месте. Круд сложнее среднего (300 строк), поэтому контекст потребовался больший и результат пришлось править чуть сильнее обычного, но мне кажется, этот пример демонстрирует некоторые важные правила формирования контекста, которые обсуждались выше. Его составление заняло минут 10, так как в основном представляло из себя грамотную копипасту.
# Функции
## POST /api/v1/function/enterprise-add - Добавление предприятия
Функция создает новое предприятие, добавляет расширенные атрибуты, добавляет записи в специализации и в справочник типов простоев
**Валидация:**
**0080: "Некорректный префикс предприятия"**. Code должен быть уникальным.
**Алгоритм:**
1. Функция создает новое предприятие
2. Функция добавляет значение в поле Extension
1. Если ParentId not null, то
1. Найти запись в enterprise, где Id = ParentId созданного предприятия
2. Скопировать данные из поля Extension найденной записи в поле Extension записи созданного предприятия
2. Если ParentId is null
1. Скопировать данные из шаблона "Extension", в поле Extension записи созданного предприятия
3. Функция добавляет записи в таблицу stoppageType
1. Если ParentId not null, то
1. Найти записи в stoppageType, где enterpriseId = ParentId созданного предприятия
2. Скопировать найденные записи (кроме Id - автоинкремент) в stoppageType с заменой значения поля enterpriseId на Id созданного предприятия
2. Если ParentId is null
1. Согласно шаблонам из шаблонного массива "StoppageTypes" создать объекты StoppageType и записать в таблицу stoppageType, подставляя enterpriseId = Id созданного предприятия
4. Функция добавляет записи в specialty
1. Если ParentId not null, то
1. Найти системные записи в specialty, где enterpriseId = ParentId созданного предприятия
2. Скопировать найденные записи (кроме Id - автоинкремент) в specialty с заменой значения поля enterpriseId на Id созданного предприятия
2. Если ParentId is null
1. Согласно шаблонам из шаблонного массива "Specialties" создать объекты Specialty и записать в таблицу specialty, подставляя enterpriseId = Id созданного предприятия
5. После создания предприятия, передать в успешном ответе Id вновь созданного предприятия
**Формат запроса**
Код;Тип;Ограничения;Обязательность;Описание;БД
ParentId;int;;;Идентификатор родителя;enterprise.parent_id
Name;string(200);;+;Название;enterprise.name
TimeZone;string(255);;+;Часовой Пояс;enterprise.time_zone
Address;string(200);;;Юридический адрес;enterprise.address
Lat;numeric(8,6);[-90..90];;Широта;enterprise.lat
Lon;numeric(9,6);[-180..180];;Долгота;enterprise.lon
Zoom;int;(0..);;Уровень зум;enterprise.zoom
Code;string(8);/^[a-zA-Z0-9]$/;+;Префикс предприятия;enterprise.code
**Формат ответа**
Название;Тип;Ограничения;Обязательность;Описание;БД
EnterpriseId;int;;+;Идентификатор созданного предприятия;enterprise.id
Below is additional context
Enterprise DB model:
/// <summary>
/// Предприятие
/// </summary>
public class Enterprise : BaseHistory<int>, IExtensible, IHasId
{
/// <summary>
/// Идентификатор родителя
/// </summary>
public int? ParentId { get; set; }
/// <summary>
/// Родительское предприятие
/// </summary>
public Enterprise? Parent { get; set; } = null!;
/// <summary>
/// Название
/// </summary>
public string Name { get; set; } = null!;
/// <summary>
/// Код
/// </summary>
public string Code { get; set; } = null!;
/// <summary>
/// Часовой пояс
/// </summary>
public string? TimeZone { get; set; }
/// <summary>
/// Юридический адрес
/// </summary>
public string? Address { get; set; }
/// <summary>
/// Координаты базы. Широта
/// </summary>
public decimal? Lat { get; set; }
/// <summary>
/// Координаты базы. Долгота
/// </summary>
public decimal? Lon { get; set; }
/// <summary>
/// Координаты базы. Зум
/// </summary>
public int? Zoom { get; set; }
/// <inheritdoc />
public JsonDocument? Extension { get; set; }
public List<EnterpriseUser> EnterpriseUsers { get; set; } = null!;
}
Specialty DB model:
public class Specialty : ISystem<int?>, IHasId
{
/// <summary>
/// Id
/// </summary>
public int Id { get; set; }
/// <summary>
/// Код
/// </summary>
public int? Code { get; set; }
/// <summary>
/// Наименование
/// </summary>
public string Name { get; set; } = null!;
/// <summary>
/// Специальности cотрудников
/// </summary>
public List<EmployeeSpecialty> EmployeeSpecialties { get; set; } = null!;
/// <summary>
/// Идентификатор предприятия
/// </summary>
public int EnterpriseId { get; set; }
/// <summary>
/// Предприятие
/// </summary>
public Enterprise Enterprise { get; set; } = null!;
}
StoppageType DB model:
public class StoppageType : BaseHistory<int>, ISystem<StoppageTypeCode?>, IHasId
{
/// <summary>
/// Идентификатор предприятия
/// </summary>
public int EnterpriseId { get; set; }
/// <summary>
/// Предприятие
/// </summary>
public Enterprise Enterprise { get; set; } = null!;
/// <summary>
/// Код
/// </summary>
public StoppageTypeCode? Code { get; set; }
/// <summary>
/// Наименование
/// </summary>
public string Name { get; set; } = null!;
/// <summary>
/// Категория вида простоя
/// </summary>
public int? CategoryId { get; set; }
/// <summary>
/// Категория вида простоя
/// </summary>
public StoppageTypeCategory? Category { get; set; }
/// <summary>
/// Экспортный код
/// </summary>
public string? ExportCode { get; set; }
/// <summary>
/// Коэффициент технической готовности (КТГ)
/// </summary>
public bool TechnicalReadinessCoefficient { get; set; }
/// <summary>
/// Коэффициент использования оборудования (КИО)
/// </summary>
public bool EquipmentUsageCoefficient { get; set; }
}
The templates for the case when ParentId is not specified are located in EnterpriseTemplates.json with the following content.
{
"Extension": {
"StatusParameters": {
"LowFuelLevel": 45,
"SpeedThreshold": 1,
"EnoughFuelLevel": 55,
"OfflineThreshold": 120
},
"RefuelingParameters": {
"Alf": 0.2,
"FillRate": 2,
"EndInterval": 30,
"StartInterval": 30
},
"WithRespectTechnicalReadiness": false,
"BaseMapLayers": ["osm_layer", "ya_layer", "ya_sputnik_layer", "topo_layer", "rosreestr_layer", "g_sat_layer", "bing_layer"]
},
"Specialties": [
{
"Code": 1,
"Name": "Машинист экскаватора"
},
{
"Code": 2,
"Name": "Помощник машиниста экскаватора"
},
...
],
"StoppageTypes": [
{
"Code": 1,
"Name": "Заправка",
"EquipmentCsageCoefficient": true,
"TechnicalReadinessCoefficient": true
},
{
"Code": 2,
"Name": "Погрузка",
"EquipmentCsageCoefficient": true,
"TechnicalReadinessCoefficient": true
},
....
]
}
You should read this file and parse its content.
You should paste Extension object as is into the Extension field of the Enterprise object if it's needed due to a not specified ParentId. For the Specialties and StoppageTypes arrays you should create separate object in the same situation as mentioned in the algorithm.
Наверняка у некоторых читателей появятся вопросы, какой LLM я пользуюсь, какими агентами и т.д. поэтому расскажу об этом сразу. Соблюдение правил промпт-инжиниринга позволяет получать предсказуемый результат даже от сравнительно старых моделей вроде GPT-3.5 Turbo и GPT-4 Turbo. Еще на них я генерировал очень даже приличный код, к слову, я проверял, что на многоразовом промпте для комплексного круда из примера выше даже GPT-3.5 Turbo отрабатывает отлично, поэтому выбор LLM здесь имеет мало значения. Мне лично нравится Claude 3.7 просто потому что он пишет в среднем чуть более красивый код и чуть лучше угадывает, что именно ты хотел сказать, так как сравнительно неплохо понимает реальность, поэтому с ним проще работать. Для выявления сложных багов я пользуюсь о3 или Gemini 2.5 Pro, так как сейчас это самые сильные рассуждающие модели.
При генерации нового кода я не использую агенты, которые сами пишут код в IDE, так как работа с ассистентом в браузере, по моим впечатлениям, дает намного больше контроля, потому что промпт пишется с чистого листа и не забит системными инструкциями, как у некоторых агентов. Агенты бывают удобны для проведения какого-то масштабного, но примитивного рефакторинга, либо, такие как Claude Code, для изучения проекта, но опять же, в 95% случаев я пользуюсь просто браузерными ассистентами через интерфейсы для пользователей API: console.anthropic.com/workbench, platform.openai.com/playground, aistudio.google.com/prompts/new_chat
На самом деле, я думаю, что мои текущие сценарии использования LLM - это далеко не предел того, что можно делать уже сейчас. Я хочу попробовать выстроить на каком-нибудь проекте процесс разработки так, чтобы ИИ писал не только код, но и аналитические постановки и промпты для себя на основе высокоуровневого описания программного продукта и обратной связи от контролирующих процесс людей. Чтобы зависимость от человека была необходима только в минимуме заранее предусмотренных точек синхронизации с реальностью. Возможно, это позволило бы достичь кратного роста производительности при разработке продукта.
В данном гайде я постарался описать самое ценное, что знаю на данный момент. Надеюсь, он будет полезен и вдохновит кого-нибудь на более масштабное использование ИИ в крупных проектах.
Спасибо, что дочитали до конца, будет интересно узнать ваше мнение в комментариях)