
Полная версия
Право на суждение. Агентность как принцип проектирования ИИ-систем
Это видно на любой попытке пройти полный цикл улучшений. Команда меняет модель эмбеддингов, перенарезает документы, ставит новый реранкер, прогоняет тестовый набор — и на простых, предусмотренных вопросах метрики растут, выборка стала заметно чище. А потом приходит вопрос вроде «поддерживает ли тариф Pro единый вход и ведётся ли на нём журнал действий» — и система снова отвечает половину. Про единый вход — да, поддерживает, точно и уверенно; про журнал действий — молчание, потому что документ про журналы лежит отдельно, а поиск был один и вытянул то, что ближе к первой половине фразы. Никакой реранкер этого не исправит: нельзя переупорядочить документ, который не был извлечён, потому что второго запроса не было. Команда может тюнинговать этот пайплайн ещё полгода — конкретно этот класс вопросов останется там же, где был, потому что промах здесь не по качеству выборки, а по числу и адресности заходов.
Отсюда — точная граница, которую важно провести, чтобы не свалиться в ложную крайность. Тюнинг пайплайна не бесполезен и не «устарел»; он делает ровно то, для чего нужен, — повышает качество выборки, — и на предусмотренных вопросах этого хватает. Отказываться от него ради красивого лозунга было бы такой же ошибкой, как надеяться, что он вернёт модели поиск. Стоит быть к тюнингу справедливым и на конкретном: бывают провалы, которые лечатся именно им. Если ответ разорван между двумя чанками так, что ни один не попадает в выборку целиком, — это дефект нарезки, и аккуратное перечанкивание его чинит. Если синонимичные формулировки не сходятся в векторном пространстве и близкое по смыслу оказывается далёким по вектору — это дефект эмбеддингов, и модель посильнее его снимает. Это настоящие болезни выборки, и тюнинг — их настоящее лекарство. Разница в том, что все они лежат на первой оси: качество того, что приносит единственный заход. Ни одна из них не про то, кто решает, каким заходам быть. Возврат поиска — это движение по другой оси, другой архитектурный ход. Здесь достаточно понять, что улучшать выборку и возвращать суждение — не одно и то же и не соседние точки на общей шкале: это два перпендикулярных направления, и сколько ни двигайся по первому, к началу второго не приблизишься ни на шаг.
И вот теперь диагноз можно поставить целиком.
Инъекция не делает систему глупее — она делает её слепой к собственному незнанию. Модель получает выборку, которую не запрашивала, отвечает на вопрос, который не формулировала, и не может сказать: «мне принесли не то». Пайплайн можно тюнинговать бесконечно — суждение это не вернёт. Замороженное суждение не ошибается. Оно просто не пересматривается.
В этих двух последних фразах — вся разница между дефектом и конструкцией. Дефект можно исправить, доведя компонент до качества; конструкцию исправить нельзя, её можно только переустроить. Замороженное суждение не совершает ошибок в том смысле, в каком их совершает плохой реранкер: реранкер иногда ставит не тот документ первым, и это чинится настройкой. Замороженное суждение вообще не участвует в происходящем — оно вынесено один раз и с тех пор просто действует. У него нет режима, в котором оно могло бы посмотреть на конкретную выборку и сказать «этого мало» или «это не оттуда». Ошибку реранкера видно — она ловится глазом и метрикой. «Ошибку» замороженного суждения увидеть нельзя, потому что оно не выносит вердикта, в котором можно ошибиться: оно молчит и применяется. Слепота к собственному незнанию — это не то, что можно вылечить более качественной выборкой; более качественная выборка просто аккуратнее укладывается в ту же слепую зону. Сколько ни улучшай то, что модель видит, ты не даёшь ей увидеть то, чего она не видит, — а именно там, в невидимом, и лежит цена.
Так объясняется стена. Команда, месяцами двигавшая метрики сходства, упиралась не в предел компонентов — компоненты ещё можно было улучшать. Она упиралась в то, что всё это улучшение шло по одной оси, а стена стояла на другой. Это и делает её стеной, а не склоном: по склону качества можно карабкаться сколь угодно долго, но он не выводит на ось владения — они не пересекаются. Диагноз этот, впрочем, поставлен пока снаружи: мы сказали, чего нельзя вылечить и почему. Осталась внутренняя сторона того же диагноза — что представляет собой модель, помещённая внутрь этой механики, и как называется положение, в котором она оказывается.
2.4. Модель как пассивный получатель контекста
Соберём главу в одну точку — в положение, которое модель занимает внутри инъекции. Оно описывается одним словом: получатель. Модель здесь не автор запроса. Вопрос, на который она отвечает, — не тот, что задал пользователь, и не тот, что сформулировала бы она сама, а тот, что молча составил код, превратив пользовательскую фразу в поисковый запрос и выборку. Модель получает результат этого чужого запроса как исходные данные и не имеет двух вещей, без которых нельзя владеть поиском: она не может переспросить — уточнить, переформулировать, сходить ещё раз, — и не может отклонить принесённое — сказать «эта выборка не отвечает на вопрос, дайте другую». Переспросить и отклонить — это и есть минимум субъектности в поиске; ни того, ни другого у получателя нет.
Видно это лучше всего там, где выборка приходит совсем мимо. Пользователь спрашивает о способности, которой в продукте попросту нет, — а значит, и в базе знаний про неё ничего. Поиск, обязанный что-то вернуть, возвращает ближайшее по словам: три статьи о смежных, похоже называющихся вещах. Живой специалист на этом месте остановился бы и сказал: «в документации этого нет, дайте уточню» или «покажите, где вы это видели». Модель-получатель так не делает — не потому, что не догадалась бы, а потому, что «остановиться и переспросить» не входит в её ход. Ей передали выборку как факт, и единственное действие, которое механика от неё ждёт, — ответить по этой выборке. И она отвечает: аккуратно сшивает три смежные статьи в складный ответ о способности, которой не существует. То, что модель не отвергла плохую выборку и не запросила другую, — не слабость, которую можно списать на модель; это ход, которого в позиции получателя просто нет. Получатель не отклоняет принесённое, потому что не ему решать, что приносить.
Четыре потери из разбора способностей — не четыре независимых увечья, а четыре грани этого одного положения. Нельзя переформулировать, потому что запрос составлен до тебя. Нельзя повторить, потому что заход не твой. Нельзя разложить вопрос, потому что план поиска не твой. Нельзя выбрать источник, потому что маршрут не твой. Всё это — один и тот же факт, увиденный с четырёх сторон: поиск принадлежит не модели. Отними у модели любую из четырёх способностей поодиночке — получишь частную неполноту; отними их разом, вынеся поиск целиком, — получишь пассивного получателя, и остальное следует само.
Стоит вглядеться в саму фигуру получателя, потому что она обманчива. Получателю можно приносить превосходные вещи. Идеально настроенный пайплайн — это очень хороший поставщик: он приносит получателю чистую, релевантную выборку. Но получатель и с превосходной выборкой остаётся получателем: он не выбирал, что ему принесут, не знает, что осталось непринесённым, и не может послать за другим. Качество подношения не меняет положения того, кому подносят. Именно поэтому пассивность нельзя перепутать с низким качеством: пассивен не тот, кому принесли мало, а тот, кому приносят, не спрашивая.
Слово «пассивный» здесь — не упрёк модели и не про её способности. Одна и та же модель, поставленная в другое положение, повела бы себя иначе; пассивность — свойство места, а не того, кто на нём стоит. Назвать модель пассивным получателем — значит описать не её характер, а её должность в этой конкретной механике. И должность эту назначила не она сама и не чей-то злой умысел: её назначил разумный в своё время выбор — искать до модели. Пассивность модели — оборотная сторона активности приложения: ровно в той мере, в какой поиском распоряжается код, модель от поиска отстранена.
У того, что мы разглядывали, есть имя. Когда суждение, которое модель могла бы вынести сама, принимается и замораживается за неё внешней логикой, происходит изъятие агентности: у модели забирают контроль над суждением внутри решения задачи. Инъекционный RAG — первый и самый наглядный случай этого паттерна: суждение о поиске изъято у модели и заморожено в коде приложения. Название это шире самой механики: изъять можно не только поиск.
Название это опирается прямо на то, чем агентность была определена раньше, — контроль над суждением внутри решения задачи. Если агентность есть мера, в которой модель сама выносит суждения, то изъятие агентности — обратная операция: у модели забирают этот контроль и передают его коду. Пассивный получатель контекста — это модель, у которой изъяли агентность в поиске: не потому что она её не заслужила и не потому что не справилась бы, а потому что решение о поиске вынесли за неё. Паттерн, таким образом, — не новая сущность, а имя для того, что происходит с агентностью, когда суждение замораживают снаружи. Оно понадобится всякий раз, когда мы будем показывать это «снаружи» в новом месте.
Почему «изъятие агентности», а не просто «изъятое суждение»? Потому что здесь работают два уровня, и их полезно различать с самого начала. Изъятие суждения — операционный уровень: разбор одного конкретного решения, та самая единица анализа, к которой мы прикладываем вопрос «чьё это суждение?». «С каким запросом искать» — одно изъятое суждение; «повторить ли поиск» — другое; «где искать» — третье. Каждое можно взять по отдельности, указать пальцем, где оно вынесено и кем. Изъятие агентности — уровень системы: то, что складывается из этих отдельных изъятий, когда их достаточно, чтобы модель перестала быть распорядителем целой области решений. Агентность в поиске изъята не каким-то одним из четырёх изъятий, а всеми вместе: по одному отняли переформулировку, повтор, декомпозицию и выбор источника — и в сумме отняли у модели поиск как таковой.
Связь между уровнями простая и несущая: агентность изымается через изъятие конкретных суждений. Нет отдельного акта «изъятия агентности» помимо изъятия суждений — есть суждения, вынесенные наружу по одному, и есть их сумма, которую мы называем изъятой агентностью. Это различение — рабочий инструмент, а не терминологическая тонкость. Операционный уровень говорит, где именно искать заморозку: в конкретных решениях, каждое из которых можно назвать и проверить. Системный уровень говорит, что складывается в итоге: область, которой модель больше не распоряжается. Держа оба уровня в руках, читатель вооружён точнее, чем если бы у него было только общее ощущение «модели тут мало»: он может показать, из каких именно замороженных суждений собрана эта нехватка.
Приложить этот инструмент к инъекции можно почти построчно. Место в коде, где вопрос пользователя без изменений превращается в поисковый запрос, — вот здесь заморожено суждение «как назвать вопрос для поиска». Отсутствие в схеме второго обращения к поиску — здесь заморожено «повторить ли». Единственный вызов вместо цикла по частям вопроса — заморожено «разбивать ли». Жёстко заданный адрес одного индекса — заморожено «где искать». Четыре места, четыре изъятых суждения; их сумма и есть изъятая у модели агентность в извлечении. Инструмент не заставляет спорить, «хорошая» это архитектура или «плохая», — он лишь делает видимым, где вынесено каждое суждение и кем. Пока достаточно уметь показывать пальцем.
Именно эта пара уровней — то, что читатель уносит из главы. Названный на одном случае, паттерн на нём не заканчивается. Если изъятие агентности — это сумма замороженных суждений, то везде, где суждение заморожено снаружи, работает тот же паттерн, каким бы ни было само суждение. Поиск оказался удобной первой иллюстрацией — наглядной, знакомой, с чётко считываемыми четырьмя изъятиями. Но ничто в определении не привязывает его к поиску.
Стоит на минуту приложить этот инструмент не к учебному примеру, а к системе, которую читатель знает лучше всех, — к своей. Где в ней вопрос пользователя уходит в поиск ровно так, как пришёл? Сколько в ней заходов — один или столько, сколько понадобится? Кто выбрал источник — модель на этом запросе или инженер полгода назад на всех сразу? Вопросы неудобные, потому что ответы чаще всего известны заранее: один заход, исходная формулировка, единственный индекс, выбранный однажды. И тогда всплывает то, что труднее всего заметить в собственной системе, — не то, что она делает плохо, а то, чего она не делает вовсе, потому что права на это у модели нет. Сколько таких замороженных суждений в системе читателя — и помнит ли он момент, когда каждое из них замерзало? Ответа здесь нет — но вопрос уже нельзя развидеть.
Слепота к собственному незнанию теперь читается иначе. Она никогда не была дефектом зрения модели — тем, что можно было бы поправить, обучив модель лучше или дав ей выборку почище. Она конструкция. Модель ослепили не в бою, а на чертеже: суждение о поиске вынесли из неё и заморозили в коде на этапе проектирования, и вместе с ним вынесли саму возможность увидеть, чего в выборке недостаёт. Пассивный получатель слеп к собственному незнанию не потому, что глуп, а потому, что так устроено место, на которое его поставили. Агентность здесь изъята — и изъята не абстрактно, а совершенно конкретно: через четыре замороженных суждения, каждое из которых мы назвали.
И если так устроен всего лишь поиск — почему тот же почерк узнаётся повсюду? Решение о том, что делать с запросом, вынесенное в роутер до модели. Решение, повторять ли попытку, зашитое в одноходовый конвейер. Решение, как ответить, закреплённое жёстким шаблоном. Решение, что запомнить, отданное внешнему управлению памятью. Всякий раз — суждение, которое модель могла бы вынести сама, вынесенное за неё и застывшее снаружи. Инъекция оказалась не единственной комнатой, а первой дверью в длинный коридор, и вопрос, который она задаёт, больше её самой: если суждение можно отнять у поиска, какое ещё суждение уже отнято — и замечено ли это.
Отсюда — формула. Не выборка была плоха и не модель туга: изъяли не качество ответа, а право его искать.
инъекция отнимает не точность, а суждение.
Глава 3. Паттерн целиком: таксономия изъятия
Почерк, однажды разобранный, проступает повсюду. В инъекционном RAG решение о том, что искать, вынесено за модель и застыло в пайплайне — но стоит перевести взгляд на остальную систему, и тот же жест обнаруживается там, где его не ждёшь. Роутер намерения решает за модель, чем ей заняться. Конвейер без петли запрещает ей переиграть неудачный ход. Шаблон отвечает вместо неё — ещё до того, как она сформулировала, что́ отвечать. Харнесс наводит порядок в её памяти, оставляя одно и вычёркивая другое. Пять разных подсистем, пять бригад инженеров, пять обоснований, написанных в пять разных кварталов, — и один и тот же жест в пяти одеждах: суждение, которое могла бы вынести модель, вынесено снаружи и заморожено.
Разложить этот жест по типам — значит получить не список технологий, а классификацию по одному основанию: какое именно суждение отнято. Что делать. Повторить ли. Как ответить. Что помнить. Действовать ли. У этого жеста есть глаголы, точные до буквальности: роутер решение перехватывает, инъекция его обходит, суммаризация подменяет. Разные глаголы описывают разную механику, но общий знаменатель один — чужое замороженное суждение в том месте, где могло бы жить живое.
Такая таксономия работает как проявитель. Приложенная к собственной системе, она превращает то, что на схеме называлось «архитектурой» и «обвязкой», в перечень изъятых суждений — по одному на каждый заботливо выстроенный контур контроля. И тем же движением она ставит вопрос, который до времени останется открытым: если изъятие обнаруживается всюду, то было ли оно всюду ошибкой — или где-то это трезвый выбор, а где-то просто привычка, пережившая свою причину.
3.1. «Что делать»: оркестраторы и роутеры намерения
Первое решение, которое система выносит о запросе, часто принимает не модель. Пользователь пишет ассистенту поддержки: «после обновления приложение не открывается на втором устройстве». Прежде чем эти слова дойдут до модели, их рассматривает роутер намерения — классификатор, относящий входящий запрос к одной из заранее заданных категорий и направляющий его по соответствующей ветке. «Техническая проблема, подкатегория „синхронизация“» — значит, ветка синхронизации: поднять свой набор документов, задать заготовленную пару уточняющих вопросов, при неуспехе завести тикет нужного типа. Модель включится позже, уже внутри ветки. Но развилка пройдена без неё. Чьё это суждение — какую способность применить к запросу?
Ответ вшит в саму конструкцию: суждение вынес роутер. Он посмотрел на запрос, решил, к какому классу задач тот относится, и тем самым определил, чем система займётся дальше, — какие документы поднимать, какие вопросы задавать, какой инструмент считать уместным. Всё это суждения о том, «что делать». И все они приняты до модели и за модель.
Роутер редко приходит один. Обычно он — часть оркестратора: управляющего слоя, задающего заранее прописанную последовательность шагов над запросом. Классическая обвязка выглядит так: сначала классифицировать намерение, затем по намерению выбрать источник, затем извлечь, затем сгенерировать ответ по шаблону ветки, затем прогнать его через фильтр. Каждая стрелка на этой схеме — заранее вынесенное решение. Последовательность неизменна, выбор ветки жёсток, переход между шагами не обсуждается. Инженер, рисовавший эту схему, отвечал на честный вопрос: как сделать поведение системы предсказуемым и покрыть известные случаи? Жёсткая цепочка шагов и классификатор на входе — прямой и разумный ответ на него.
Полезно задержаться на самой цепочке шагов, потому что её жёсткость обманчиво выглядит нейтральной. «Классифицировать → выбрать источник → извлечь → ответить по шаблону → отфильтровать» читается как техническое описание потока данных, а не как последовательность решений. Но каждая стрелка — замороженное суждение, и их легко перечислить поимённо. Стрелка от классификации к выбору источника решает, что для этой категории релевантна вот эта база и никакая другая. Стрелка к шаблону решает, что ответ такого типа выглядит именно так. Порядок стрелок решает, что извлечение всегда предшествует рассуждению, а не наоборот, — хотя встречаются запросы, где сначала стоило бы подумать, а потом искать. Ни одно из этих решений не является технической неизбежностью; каждое — выбор, сделанный однажды и вшитый в граф. То, что решения выстроены в ряд и подписаны стрелками, маскирует их природу: это не поток данных, это очередь вынесенных за модель суждений, каждое из которых могло бы приниматься заново под конкретный запрос.
Тот же контур обнаруживается далеко за пределами поддержки. В разборе входящей почты классификатор сортирует письма по темам и раздаёт их предопределённым обработчикам. В системе, читающей юридические документы, диспетчер по типу договора решает, какой из узкоспециализированных конвейеров запустить. В голосовом помощнике распознаватель намерения сопоставляет фразу с одним из поддерживаемых сценариев и отбрасывает всё, что не легло в список. Механика везде одна: перед моделью стоит слой, который смотрит на задачу и назначает ей маршрут. Модель получает уже не задачу «разберись, что здесь нужно», а узкую подзадачу «выполни шаг ветки N», выбранную за неё.
Внутри ветки модель работает добросовестно — и именно поэтому изъятие незаметно. Ей выдали подзадачу «задай эти три вопроса и, если ответы не совпали с ожидаемыми, заведи тикет», и она выполнит её безупречно. Она не откажется, не усомнится, не спросит, почему её вообще позвали сюда: у неё нет доступа к развилке, на которой решили, что это задача про синхронизацию. Возьмём тот же запрос про второе устройство. Настоящая причина — просроченный токен авторизации, и способная модель распознала бы это по одному уточнению. Но ветка синхронизации про токены не спрашивает: её сценарий писали под другую гипотезу. Модель прилежно ведёт пользователя по вопросам о сети и версии приложения, пользователь честно отвечает, тикет заводится, круг замыкается — и ни в одной точке этого круга не нашлось места, где чей-то интеллект мог бы сказать: развилка выбрана неверно. Интеллект в системе есть, но он заперт ниже той развилки, где его суждение и было нужно.
Редко когда перехват случается однажды. Зрелые системы наслаивают роутеры: верхний классификатор делит запросы на крупные домены, внутри домена другой выбирает подсценарий, внутри подсценария селектор инструментов назначает конкретное действие. Каждый слой — ещё один перехват, ещё одно суждение «что делать», вынесенное до модели. К тому моменту, когда модель наконец получает управление, большинство решений о задаче уже приняты за неё каскадом классификаторов, а её роль сжата до исполнения листа в чужом дереве. Дерево это росло из лучших побуждений — каждая новая ветка добавлялась в ответ на реальный непокрытый случай, — но сумма побуждений даёт архитектуру, в которой суждение о задаче раздроблено между десятком замороженных развилок и нигде не принадлежит тому, кто способен увидеть запрос целиком.
У этого типа изъятия есть точное имя. Роутер не запрещает модели рассуждать и не портит её ответ — он перехватывает решение. Запрос летит к модели, которая сама способна сообразить, что с ним делать, но на подлёте его встречает классификатор и разводит по веткам раньше, чем модель успевает взглянуть. Перехват — это изъятие суждения «что делать», выполненное на входе: решение о задаче принимается до того, как задача дошла до того, кто мог бы вынести его лучше. Перехватывают не результат работы модели, а само право эту работу выбрать.
Образ подлёта буквален. Суждение «что делать» существует ровно один короткий миг — между тем, как запрос сформулирован, и тем, как за него взялись. В этот миг решение ещё открыто: его можно вынести под конкретные слова пользователя. Роутер вклинивается именно сюда, в зазор перед моделью, и закрывает решение прежде, чем оно дошло до того, кто прочитал бы запрос целиком. Оттого перехват так трудно заметить постфактум: к тому времени, когда система выдала ответ, развилки уже нет на схеме исполнения — она отработала на входе и растворилась в маршруте. Чтобы её увидеть, приходится смотреть не на то, что́ система ответила, а на то, кто решил, каким будет вопрос.
Разница между «модель выбрала способ» и «способ выбран за модель» кажется тонкой, пока система движется по размеченным рельсам. Она становится решающей на первом же запросе, который не лёг ни в одну категорию. Пользователь спрашивает разом про списание денег и про синхронизацию; или формулирует так, что классификатор уверенно относит запрос не к той ветке; или описывает случай, которого при проектировании веток просто не предвидели. Классификатор не сигнализирует о промахе — у него нет представления о том, что распознавание могло не удаться. Он выносит суждение с той же уверенностью, что и на идеальном примере из обучающей выборки, и передаёт модель в ветку, где та честно и качественно решает не ту задачу. Решение, застывшее в классификаторе, не пересматривается — потому что в этой архитектуре пересматривать его некому.
Особенно ясно перехват виден на том, что системы делают с запросом-сиротой — тем, что не опознан ни одной категорией. Здесь у архитектуры два обычных выхода, и оба подтверждают диагноз. Либо запрос уходит в ветку «прочее» — универсальный тупик, где пользователю уходит общий шаблон вежливого отказа или совет обратиться к человеку; либо порог классификатора настраивают так низко, что он всё равно приписывает сироту к ближайшей категории, лишь бы не сознаваться в незнании. В первом случае способную модель, которая могла бы разобраться в незнакомом запросе, до него попросту не допускают. Во втором — её запускают в заведомо неверную ветку. Ни то ни другое — не дефект реализации: и тупик «прочее», и заниженный порог — добросовестные ответы на проблему неизвестного входа. Просто оба отвечают на неё изъятием — тем, что суждение о незнакомом запросе выносит правило о пороге, а не интеллект, способный этот запрос прочитать.
У перехвата есть своя убедительность, и её стоит признать прямо. Классификатор намерения измерим: его точность можно посчитать, ошибки — увидеть на размеченной выборке, распределение намерений — вывести на панель и показать на ревью. Он даёт то, чего инженер справедливо хочет от продакшена: наблюдаемость, воспроизводимость, возможность сказать заказчику, что система делает ровно то и только то, что описано в спецификации веток. Роутер — не небрежность и не леность мысли; это инструмент, который делает поведение системы читаемым. Хорош он или плох — вердикт подождёт. Важно другое: что именно он замораживает. И цену стоит назвать без обиняков — читаемость покупается тем, что суждение о задаче переходит от того, кто видит конкретный запрос, к тому, кто заранее расчертил категории, ещё не зная будущих запросов.









