
Полная версия
Право на суждение. Агентность как принцип проектирования ИИ-систем
Два ассистента, с которых всё началось, теперь различимы до конца. Снаружи они по-прежнему одинаковы — тот же интерфейс, та же база, тот же класс модели. Но их различие больше не приходится списывать на неуловимое «один умнее». У них разные владельцы одних и тех же суждений: там, где первый заморозил решение в приложении, второй оставил его модели. Всё, что чувствовалось в открывающем контрасте и не имело имени, оказалось именно этим — картой владельцев, разложенной по-разному.
И остаётся вопрос, обращённый уже не к двум вымышленным ботам, а к системам читателя. Сколько суждений в них заморожено — и заметил ли он момент, когда это произошло? Большинство изъятий не обставлялись решением: они пришли формой по умолчанию и с тех пор не пересматривались. Значит, где-то в знакомой архитектуре стоят замороженные суждения, о которых никто не помнит, что когда-то выбрал их заморозить. Найти их — уже работа; но начинается она не с инструмента, а с одного вопроса, приложенного к каждому решению по очереди.
разбирай систему не на компоненты, а на суждения — у каждого решения в системе есть владелец, и это выбор, а не данность.
Глава 2. Флагманский случай: инъекционный RAG
Команда полгода улучшает извлечение. Меняет модель эмбеддингов на более точную, дробит документы на чанки поумнее, добавляет реранкер, который переупорядочивает найденное перед тем, как отдать его модели. На каждой итерации метрики сходства растут, а качество ответов бота упирается в стену, которую не видно на графиках. Стена не там, где её ищут. Она не в качестве компонентов — все они делают ровно то, чего от них ждали. Она в том, что модель, которая пишет ответ, ни разу не была спрошена, что именно ей искать.
Кто в системе владеет поиском — приложение или модель? Пока этот вопрос не задан, его ответ выглядит настолько само собой разумеющимся, что кажется не решением, а свойством мира: конечно, поиском занимается пайплайн, для того он и построен. Но это решение. Оно вынесено один раз, на этапе проектирования, и с тех пор не менялось. И у него есть цена — не в точности выборки, а в том, какие суждения оно отняло у модели и что стало невозможным вместе с ними.
Модель в такой системе отвечает по выборке, которой не заказывала, — так, как если бы выборка была исчерпывающей. Возразить против принесённого ей нечем: она не знает, что было доступно и чего не хватает. Это не глупость и не дефект обучения. Это слепота к собственному незнанию, встроенная в архитектуру: модель не видит границы того, что ей дали, потому что саму границу проводили без неё.
2.1. Инъекция против агентного извлечения
Возьмём привычную схему, по которой работает большинство ассистентов на базе знаний. Пользователь задаёт вопрос. Приложение превращает его в вектор, идёт в векторную базу — хранилище, где документы лежат как числовые представления смысла, — находит несколько наиболее близких фрагментов, при желании переупорядочивает их реранкером и вклеивает получившийся текст в промпт перед вопросом пользователя. Только теперь вызывается модель. Она видит вопрос и рядом — блок «вот релевантные документы», как будто он всегда там был. На этом блоке она и строит ответ.
Назовём эту механику по имени. Инъекция (инъекционный RAG) — схема, в которой приложение находит и вставляет контекст до вызова модели, а модель получает его как данность. Контекст именно впрыскивается — снаружи, готовым, без участия того, кто будет им пользоваться. RAG (retrieval-augmented generation) — генерация, дополненная извлечением; инъекционный RAG — та его разновидность, где извлечение целиком вынесено из модели в код вокруг неё.
Эта схема — не чья-то ошибка и не признак небрежности. Она стала стандартом по хорошей причине: когда модель нельзя было надёжно попросить самой сходить за нужным документом — сформулировать разумный запрос, не заблудиться, не выдумать источник, — единственным способом дать ей факты было положить факты рядом заранее. Приложение брало поиск на себя, потому что модель его не потянула бы. Инъекция — рациональный ответ на реальную задачу; узнать в ней свой продакшен не стыдно. Стыдно другое — не заметить, что решение, принятое под те условия, продолжает действовать, когда условия изменились. Но об этом уместно говорить, только предъявив сперва, что именно это решение замораживает.
У этой механики есть точный глагол. Инъекция обходит суждение модели о поиске: решение о том, что и где искать, принимается в обход того, кто будет отвечать. Не против модели — мимо неё. Приложение не спорит с моделью о выборке и не показывает ей альтернатив; оно просто ставит модель перед фактом. К моменту, когда модель включается в работу, поиск уже состоялся, его результат заморожен, и переиграть его нельзя — не потому что запрещено, а потому что механика не предусматривает такого хода. Модель здесь — последнее звено конвейера, а не его распорядитель.
Чтобы увидеть это не как схему, а как повседневность продакшена, проследим за одним вопросом к боту поддержки. Пользователь пишет: «После обновления перестал приходить вебхук на платёж — куда копать?» Приложение переводит эту фразу в вектор и уходит в базу знаний. В базе есть точная статья про вебхуки платежей, но её заголовок и текст построены вокруг слов «уведомление о транзакции», а пользователь написал «вебхук на платёж». Семантически это близко, но не настолько, чтобы нужный документ обошёл три статьи про вебхуки вообще — про их настройку, про повторную доставку, про подпись запроса, — которые по формулировке пользователя выглядят роднее. Приложение вклеивает эти три, статью про уведомления оставляет за порогом top-k — числа ближайших фрагментов, которые вообще попадают в выборку, — и вызывает модель. Модель получает три документа про вебхуки, ни один из которых не отвечает на вопрос про платежи после обновления, и добросовестно строит из них правдоподобный, уверенный и неверный ответ. Она не знает, что нужная статья существует и лежала рядом. Она не знает даже, что вопрос был про платежи, а не про вебхуки вообще, — потому что видела не вопрос, а его последствие: выборку. Это и есть обход в действии: суждение о том, где лежит ответ, вынесли за модель, а расплачиваться за него пришлось ей.
Вторая механика — пока только контраст, не рецепт. В агентном извлечении поиск зовёт сама модель. Он оформлен как инструмент, который модель может вызвать, когда сочтёт нужным: сформулировать запрос своими словами, посмотреть на результат, вызвать поиск ещё раз с другой формулировкой, обратиться к другому источнику. Приложение по-прежнему предоставляет механизм поиска — векторную базу, индекс, доступ к документам, — но не решает за модель, когда его применить и с каким запросом. Решение о поиске остаётся внутри работы над задачей, а не выносится перед ней.
Вернёмся к тому же вопросу про вебхук на платёж, но в этой второй механике. Модель, получив вопрос напрямую, видит именно вопрос, а не готовую выборку. Она может заметить, что «вебхук на платёж» — это, скорее всего, про уведомления о транзакции, и построить запрос под тот словарь, которым говорит база. Может, получив три статьи про вебхуки вообще, увидеть, что ни одна не про платежи после обновления, и сходить ещё раз. Может понять, что вопрос распадается на два — про платёж и про обновление — и поискать по каждому. Ничего из этого не гарантировано: модель тоже способна ошибиться формулировкой или остановиться рано. Но сама возможность так поступить — есть. В первой механике её нет: там ни один из этих ходов недоступен не потому, что модель не додумалась, а потому, что к моменту её включения поиск уже закончился. Разница не в том, что вторая механика умнее. Разница в том, что в ней суждение о поиске вообще существует как живое решение, а не как застывший в коде результат.
Различие это — не в качестве. Обе схемы могут стоять на одной и той же векторной базе, с одной и той же моделью эмбеддингов, с одинаково хорошим реранкером. Снаружи, в демонстрации на подготовленных вопросах, они могут быть неотличимы: обе находят релевантные документы, обе отвечают по делу. Разница обнажается там, где вопрос не предусмотрен, где первая выборка мимо, где ответ требует нескольких заходов, — и её источник не в компонентах, а в том, кто владеет поиском.
Это первый полномасштабный ответ на вопрос, который проходит через всю книгу: чьё это суждение? Суждение «что и где искать» — не техническая мелочь, а именно суждение: оно требует понять, что на самом деле спросил пользователь, какими словами это найдётся в базе, в каком из источников это вообще может лежать, и достаточно ли одного поиска. В инъекции всё это суждение целиком принадлежит приложению: оно сформулировано в коде — как строить запрос, куда идти, сколько фрагментов брать — и вынесено один раз, до и вместо модели. В агентном извлечении то же суждение принадлежит модели: она выносит его каждый раз заново, под конкретный вопрос, глядя на то, что уже нашла.
Разобрать систему по решениям и спросить о каждом «чьё это суждение?» — это и есть рабочий метод. В приложении к поиску он даёт точный диагноз, а не общее ощущение. Мы не спрашиваем, хорош ли пайплайн; мы берём одно решение — «с каким запросом и куда идти за ответом» — и смотрим, где оно вынесено и кем. И тут важно слово «заморожено». В инъекции суждение о поиске принимается не в тот момент, когда приходит вопрос пользователя, а гораздо раньше — когда инженер писал код извлечения. Там, на этапе проектирования, было решено: строить запрос вот так, ходить вот сюда, брать вот столько фрагментов. Это решение застыло в коде и с тех пор применяется к каждому вопросу одинаково, каким бы вопрос ни был. Оно не пересматривается под конкретный запрос не потому, что кто-то запретил его пересматривать, а потому, что пересматривать его в этой механике некому: единственный участник, который видит конкретный вопрос целиком, — модель — включается уже после того, как решение применено.
Стоит задержаться на слове «владеет». Речь не о том, кто физически выполняет поиск, — в обеих схемах по индексу ходит один и тот же код. Речь о том, кто выносит решение: с каким запросом пойти, стоит ли идти ещё раз, откуда брать. В инъекции это решение вынесено в код и застыло там; модель его не касается. В агентном извлечении механизм поиска остаётся кодом, но решение о его применении возвращено модели. Владелец поиска — это владелец суждения о поиске, а не исполнитель операции. И именно владелец, а не качество компонентов, определяет, что система сможет, когда вопрос выйдет за пределы предусмотренного.
Различие владельца не привязано к docs-ботам — оно живёт в любой системе, где есть поиск. Ассистент юриста, который по запросу «прецеденты по расторжению из-за скрытого дефекта» либо сам решает, что стоит развести «скрытый дефект» и «существенное нарушение» на два поиска, либо получает единый список, собранный кодом по исходной формулировке. Помощник аналитика, который либо сам выбирает, идти ли в базу отчётов или в базу транзакций, либо всегда ходит туда, куда его направил маршрут в приложении. В каждом случае две системы можно поставить рядом, дать им одинаковый индекс и одинаковую модель — и снаружи, на подготовленной демонстрации, не различить. Различие проступит на первом же вопросе, который не лёг в предусмотренную колею: там, где одна система переспросит саму себя, а другая ответит по единственной выборке, какой бы та ни была.
Два ассистента на одной базе знаний, с одной моделью, за одинаковым интерфейсом могут оказаться разными системами — не потому что у одного лучше эмбеддинги, а потому что в одном суждением о поиске владеет приложение, а в другом — модель. Владелец назван. Осталось предъявить счёт: что именно теряет модель вместе с поиском, когда суждение о нём заморожено в коде. Потеря эта не абстрактна — она раскладывается на несколько совершенно конкретных способностей, каждую из которых легко узнать по тому, как система ведёт себя, когда первая выборка оказывается не той.
2.2. Четыре изъятые способности
Когда поиск вынесен из модели, отнимается не «немного качества». Отнимаются вполне конкретные способности — те самые, которыми живой специалист поддержки пользуется, не задумываясь, когда ищет ответ в документации. Их четыре: переформулировать запрос, сходить за ответом ещё раз, разбить сложный вопрос на части и выбрать, где искать. Все четыре исчезают не по отдельности и не из четырёх разных дефектов — их снимает один архитектурный жест: решение искать до вызова модели. Стоит сделать этот жест — и все четыре способности выпадают разом, потому что каждая из них требует, чтобы у модели было то, чего инъекция ей не оставляет, — право распоряжаться поиском.
У живого специалиста эти четыре хода настолько привычны, что не осознаются как отдельные умения: он переводит жалобу на язык базы, пробует ещё раз, если не нашёл, раскладывает запутанный вопрос и знает, в какой папке лежит нужное, — и всё это за секунды, не называя про себя ни одного из этих действий. Именно потому, что в человеке они невидимы, их отсутствие в системе так легко проглядеть: трудно хватиться способности, которую никогда не замечал. А между тем это и есть та самая абстрактная формула из предыдущего разбора — «поиском владеет приложение», — разменянная на осязаемое. Владеть поиском значит владеть вот этими четырьмя суждениями; отдать поиск приложению значит отнять у модели ровно их. Вопрос о владельце потому и не академический: за ним стоит конкретный список того, что система теперь не умеет.
Первая способность — переформулировать запрос. Пользователь почти никогда не спрашивает на языке базы знаний. Он пишет так, как чувствует проблему: «он тупит, когда жму сохранить». В базе про это есть точная статья, но называется она «Конфликт ручного и автоматического сохранения», и индексирована под словами, которых в жалобе пользователя нет вовсе. Специалист, прочитав жалобу, мысленно переведёт её: «тупит при сохранении» — это, скорее всего, про автосохранение, надо искать по нему. Модель сделала бы тот же перевод — если бы увидела жалобу. Но она её не видит: приложение превращает в вектор исходную фразу как есть и приносит модели статьи, ближайшие к словам «тупит» и «сохранить» — что-то про производительность интерфейса, мимо сути. Изъято здесь суждение о том, как назвать проблему на языке базы: право превратить кривой вопрос пользователя в точный поисковый запрос вынесено за модель — а точнее, не вынесено никуда, потому что в этой механике его просто некому вынести. Что при этом получит пользователь, предсказуемо: модель, послушно опираясь на принесённые ей статьи о производительности интерфейса, объяснит, как ускорить отрисовку, — исчерпывающий ответ на вопрос, которого никто не задавал.
Вторая способность — сходить за ответом ещё раз. Первая выборка не всегда попадает; это нормально, так и у человека. Разница в том, что человек, увидев мимо, ищет иначе, а инъекция даёт ровно один заход. Пользователь спрашивает: «как отменить подписку». Поиск по близости слов приносит «Как оформить подписку» и «Сравнение тарифов» — семантически рядом, по намерению мимо. Специалист бросил бы один взгляд на эти статьи, понял бы, что все они про заведение и выбор подписки, а не про её прекращение, и переспросил бы поиск словом «расторжение» или «отмена». Модель, получи она эту выборку с возможностью сходить снова, поступила бы так же. В инъекции возможности нет: что нашлось с первого раза, из того и придётся отвечать. Изъято суждение о том, годится ли выборка и стоит ли искать заново, — решение «повторить ли поиск» заморожено на «нет». Человек, спросивший, как отменить подписку, получит вежливую инструкцию, как её оформить, — и это худший сорт ошибки, тот, что выглядит как ответ.
Третья способность — разбить сложный вопрос на части. Некоторые вопросы составные: ответ на них собирается из нескольких мест, и один поиск их не обслуживает. Аналитик спрашивает ассистента: «сравни выручку по региону за прошлый квартал с планом и объясни, где расхождение больше нормы». Это не один запрос, а по меньшей мере три: факт по выручке, план, порог нормы — и лежат они, возможно, в разных таблицах и разных документах. Единый поиск по всей фразе принесёт что-то усреднённо-близкое ко всему сразу и точно ни к чему. Человек, взявшись за такой вопрос, разложил бы его на шаги и сделал бы несколько прицельных поисков. Модель способна на ту же декомпозицию — но только если поиск в её руках. В инъекции составной вопрос обслуживается одним заходом, потому что заход всегда один и планирует его не модель. Изъято суждение о том, что вопрос надо разложить и поискать по каждой части, — право спланировать извлечение под структуру задачи. Аналитик получит связный абзац про выручку и план и ни слова про порог нормы — не потому, что модель не справилась с вопросом, а потому, что о пороге ей ничего не принесли, и узнать, что этой части не хватает, ей неоткуда.
Четвёртая способность — выбрать, где искать. У ассистента редко один источник. Есть основная документация, есть журнал изменений с описанием того, что поменялось в последних версиях, есть внутренние регламенты, есть, может быть, форум сообщества. Разные вопросы живут в разных источниках. Пользователь спрашивает: «почему после обновления изменилось поведение выгрузки». Ответ — в журнале изменений, где прямо записано, что в этой версии выгрузку переделали. Но приложение всегда ходит в основную документацию, потому что так настроен маршрут; журнал изменений в этот маршрут не входит. Модель, будь выбор источника за ней, сообразила бы, что вопрос про «после обновления» — это вопрос к журналу изменений, а не к общей документации. Она этого не сделает: куда идти, решено до неё и помимо неё. Изъято суждение о выборе источника — о том, из какого хранилища вообще может прийти ответ. Пользователь про «после обновления» получит уверенное описание старого поведения из основной документации, которую никто не удосужился обновить, — правильный ответ из неправильного источника.
Эти четыре потери к тому же не складываются, а перемножаются. Кривой запрос без права переформулировать — полбеды, если можно сходить ещё раз; но итерация тоже изъята, и первый неудачный заход оказывается единственным. Составной вопрос без декомпозиции ещё как-то вытянул бы верный источник — но и выбор источника заморожен, так что промах по формулировке и промах по хранилищу накладываются друг на друга. Отняв поиск, инъекция отняла не четыре независимые мелочи, а связку: каждая уцелевшая способность могла бы прикрыть отказ соседней, но уцелевших нет. Модель остаётся с одной-единственной выборкой, собранной чужим суждением по исходной формулировке, — и на этой выборке обязана построить ответ, каким бы промахом та ни оказалась.
Эти четыре — не список неудобств, а список конкретных суждений, которые в инъекции принадлежат приложению, а не модели: как назвать вопрос для поиска, стоит ли искать снова, надо ли разбить вопрос на части, где искать. Каждое из них модель могла бы вынести сама — и вынесла бы не хуже кода, а на нестандартном вопросе почти наверняка лучше, потому что видит конкретный вопрос целиком, а код видит только заранее заданную схему. Все четыре сняты одним и тем же жестом и по одной и той же причине: к моменту, когда модель начинает работать, поиск уже позади. Именно этот перечень позже придётся вернуть модели по одному — он и есть точная опись того, что заморожено.
Здесь стоит понять, почему эти потери так долго остаются незамеченными. На предусмотренных вопросах их не видно вовсе. Пользователь, который спрашивает ровно то и теми словами, под какие настраивали пайплайн, получает точную выборку с первого захода — переформулировать нечего, повторять незачем, вопрос простой, источник единственный. Ни одна из четырёх способностей на таком вопросе не нужна, а значит, их отсутствие ничем себя не выдаёт. Демонстрация на подготовленных примерах проходит блестяще; метрики сходства высоки; команда уверена, что система работает. Способности всплывают только там, куда демонстрация не заглядывает: на вопросе не теми словами, на выборке мимо, на составном запросе, на ответе из соседнего источника. Изъятие невидимо ровно до той секунды, пока оно не начинает стоить денег, — а к этой секунде оно уже вшито в архитектуру. И чем лучше система отвечает на предусмотренное, тем убедительнее выглядит, будто с ней всё в порядке, — тем дальше отодвигается та секунда и тем неожиданнее будет расплата.
И вот тут срабатывает инженерный рефлекс, знакомый каждому, кто такие системы строил: раз выборка бывает мимо — улучшим выборку. Возьмём модель эмбеддингов посильнее, нарежем документы аккуратнее, добавим реранкер получше. Рефлекс верный по инстинкту и бесполезный по адресу: он лечит не ту болезнь.
2.3. Почему тюнинг пайплайна этого не лечит
Рефлекс «улучшим выборку» заслуживает того, чтобы отнестись к нему всерьёз, а не отмахнуться. За ним стоит настоящая инженерная работа и настоящий результат. Более сильная модель эмбеддингов действительно располагает документы в смысловом пространстве точнее, так что близкое по смыслу оказывается близким и по вектору. Аккуратная нарезка на чанки действительно спасает от того, что ответ разорван между двумя фрагментами и ни один не попадает в выборку целиком. Хороший реранкер действительно поднимает наверх то, что релевантнее, отодвигая правдоподобный мусор. Всё это не самообман и не карго-культ: качество одноходового поиска этими средствами растёт, и растёт измеримо. Инженер, который этим занимается, не заблуждается — он честно решает реальную задачу.
Но задача эта — не та, что стоит на пути. Четыре потери из предыдущего разбора — не следствие плохой выборки; они следствие того, что выборка одна и собрана до модели. Это свойство механики, а не качества компонентов. Пассивность модели не берётся из слабых эмбеддингов и не лечится сильными: она берётся из места, которое модель занимает в конвейере, — ниже по течению от поиска. Можно довести единственный заход до идеала — он останется единственным заходом. Можно сделать выборку безупречно релевантной исходной формулировке — она останется выборкой под исходную формулировку, а не под то, что пользователь имел в виду. Компонент отвечает за то, насколько хороша выборка; он ничего не может сказать о том, кто решает, какой выборке быть, сколько их будет и откуда. Это разные вопросы, и живут они на разных осях.
Проще всего увидеть это через мысленный предел. Представим, что качество выборки доведено до совершенства: на любом предусмотренном вопросе пайплайн приносит идеально релевантный фрагмент с первого раза. Пассивность модели от этого никуда не делась — она просто перестала быть заметной, пока вопросы предусмотрены. Стоит прийти вопросу, которого схема не ждала, и совершенное качество выборки не поможет ничем: модель по-прежнему не может переформулировать, переспросить, разложить или сменить источник. Дефект, который тут вылезет, — не «выборка плоха», а «модель не властна над выборкой», и второе не выводится из первого никакой настройкой. Пассивность — это не низкое качество того, что принесли; это отсутствие у модели самого права распорядиться тем, что и как приносят. Оно вписано в позицию, а позицию тюнинг не трогает.
Разведём эти две оси прямо, потому что их постоянно путают. Первая ось — качество выборки: насколько хорошо найденное отвечает заданному запросу. По этой оси тюнинг двигает систему вперёд, и это ценно. Вторая ось — владение поиском: кто формулирует запрос, кто решает, что выборка не годится и надо искать снова, кто разбивает вопрос и выбирает источник. По этой оси тюнинг не двигает ничего. Реранкер, каким бы точным он ни был, переупорядочивает то, что уже найдено, — он не может поднять наверх документ, который не был извлечён, потому что запрос под него никто не переформулировал. Лучшая модель эмбеддингов сделает единственный заход точнее — но не превратит его в два захода, если первый промахнулся. Улучшение компонентов оптимизирует выстрел, не отдавая модели права выбрать, куда стрелять, стрелять ли ещё раз и из какого ствола. Оно поднимает потолок одноходового поиска и даже слегка расширяет коридор предусмотренных вопросов — но не делает поиск управляемым. Управляемость лежит на второй оси, а тюнинг всё это время работает на первой.
У этой второй оси есть имя. Владение поиском — это и есть вопрос «чьё это суждение?», приложенный к извлечению. Первая ось на него не отвечает и даже не задаёт его: она молча принимает, что суждение о поиске принадлежит коду, и оптимизирует то, что код принёс. Можно всю жизнь совершенствовать ответ на вопрос, не заметив, что сам вопрос вынесен мимо модели. Тюнинг потому и не сдвигает вторую ось, что не касается её предмета: он делает выборку лучше, ни разу не спросив, кому она принадлежит.









