Почти каждый ИИ-чат-бот поддержки показывает под ответом строку источников. С ней ответ выглядит обоснованным: вот откуда я это взял.
В большинстве реализаций эта строка вам врёт. Не со зла: просто она отвечает не на тот вопрос, который задаёт пользователь.
Найдено — не значит использовано
Чат-бот с поиском по базе знаний (RAG) отвечает примерно так: ищет в базе знаний, кладёт лучшие результаты в контекст модели и даёт ей написать ответ. Очевидный способ собрать строку источников — показать всё, что вернул этот поиск.
Так и делает большинство продуктов, и ошибка здесь вполне конкретная. Семантический поиск возвращает десять кандидатов, потому что десять — разумное число вариантов для языковой модели. Модель читает их и использует один. Если показать все десять как источники, пользователь решит, что ответ опирается на десять статей, хотя девять отброшены.
Эта ошибка тихая. Никто не заводит баг «в строке источников слишком много пунктов». Пользователи просто постепенно понимают, что строка для красоты, и перестают её читать. Вы заплатили полную цену за разработку ссылок на источники и не получили того доверия, ради которого они делались.
Когда ответ — список всех статей
Ошибка становится заметнее, как только пользователь спрашивает о структуре.
«О чём у вас есть статьи?»
Хороший ассистент отвечает списком ваших статей. И теперь в строке источников оказываются все статьи центра помощи: те же ссылки, что уже есть в ответе, прямо под ним. Пользователь видит одни и те же тридцать заголовков дважды.
Здесь абстракция ломается у всех на глазах, и к этому стоит отнестись серьёзно, потому что так видна настоящая ошибка: происхождение ответа нельзя вывести из поиска. Его нужно выводить из того, что сделала модель.
Три набора, а не один
Исправление — перестать считать это одним списком. За каждым ответом стоят три разных набора документов:
Найденные — всё, что вернул любой инструмент за ход. Этот набор и показывает большинство продуктов. Но его настоящая задача другая, и она важна: это белый список. Если модель ссылается на документ, которого не было среди найденных, ссылку она выдумала, и её нужно отбросить. Эта проверка не теоретическая. Когда я тестировал свой чат, модель переписывала тридцать ID документов и ошиблась в одном на один символ. Проверка это поймала и не превратила испорченный ID в источник.
Процитированные — документы, на которые ответ действительно ссылается. Благодаря им ссылки в тексте ответа кликабельны, и это честная основа для оплаты по использованию.
Прочитанные — документы, текст которых модель прочла. Только им место в строке источников.
Третий набор никто не вычисляет, хотя это несложно. Вы уже знаете, какой инструмент вернул каждый документ. Одни инструменты дают модели настоящее содержимое: поиск возвращает фрагменты, вызов «получить документ» — весь текст. Другие дают оглавление: список категорий, список статей, просмотр раздела. Они возвращают заголовки.
Отсюда правило:
Модель открыла документ целиком → это источник, даже если ответ на него не ссылается. Само открытие и есть происхождение. Если модель пересказала статью и забыла поставить markdown-ссылку, это промах в оформлении, а не отсутствие основания.
Документ вернул поиск, и ответ на него сослался → это источник. Ссылка показывает, какой из десяти кандидатов пошёл в дело.
Модель видела документ только как заголовок в списке → это не источник, даже если ответ на него ссылается.
По этому правилу на вопрос «какие у вас есть статьи?» приходит чистый список рабочих ссылок без строки источников, потому что модель ничего не читала. А на вопрос «как настроить SSO?» — абзац и под ним одна-две плашки: статьи, которые модель действительно прочла.
Деталь, на которой легко ошибиться
В реализации есть ловушка, и её стоит назвать: ошибку здесь легко сделать так, что всё выглядит нормально, пока пользователь на что-нибудь не нажмёт.
Массив источников обычно работает за двоих. Это данные для плашек и таблица поиска, благодаря которой ссылки в тексте кликабельны: обработчик клика находит ID документа из ссылки в этом массиве и открывает его.
Поэтому просто урезать массив до прочитанных документов нельзя. Иначе каждая ссылка в ответе-списке перестанет работать: она выглядит как ссылка, оформлена как ссылка и ничего не делает при клике.
Оставьте в ответе API весь набор процитированных документов, пометьте каждый флагом и фильтруйте только при показе. Один boolean на источник, фильтр в рендерере. Ссылки работают, плашки говорят правду.
Сделайте флаг необязательным
Если вы выпускаете это как изменение API, сделайте поле необязательным и считайте отсутствующее значение ответом «да, показывать».
Благодаря одному этому решению старый клиент с новым сервером продолжает работать, а новый клиент со старым сервером откатывается к прежнему поведению, а не прячет все источники. Обе стороны в одну секунду вы не задеплоите, а закешированные копии виджета живут у пользователей дольше, чем хотелось бы.
Зачем это нужно
Строка источников — это утверждение: этот ответ взят из этих документов. Если собирать её из результатов поиска, утверждение верно приблизительно, и пользователи рано или поздно это замечают. Если из того, что модель действительно прочла, оно верно точно и становится для поддержки самым быстрым способом разобрать ответ, который пошёл не так.
Есть и вторая польза. Когда «нет источников» надёжно означает «модель ничего не прочла», это становится настоящим сигналом. Ответ без источников на содержательный вопрос о продукте — это пробел в документации. Такие ответы можно отфильтровать и превратить в очередь статей на написание.
Но это работает, только если строка что-то значит.
ИИ-ассистент Kookee работает по этому правилу: в строке «Источники» — статьи, на которых построен ответ, а все статьи, на которые ответ ссылается, остаются кликабельными. Подробности — в статье «Как ассистент выбирает источники», а общая картина — в посте «Как сделать центр помощи для SaaS».