Как стать автором
Поиск
Написать публикацию
Обновить
AvitoTech
У нас живут ваши объявления

LLM против хаоса: как я автоматизировал ревизию прав доступа в админке Авито

Время на прочтение7 мин
Количество просмотров477

Привет! Я Андрей и сегодня расскажу, как сделал мультиагентную систему, которая автоматизировала ревизию доступов в бэкофисе Авито, копившихся годами. Вы узнаете, как собрать LLM-систему с четырьмя агентами и супервизором, которая не только сгенерировала описания прав доступа, но и с точностью 77% нашла их владельцев без передачи кода и документации внешним моделям. Вперед к прочтению!

Содержание:

Контекст: что не так с доступами?

Права доступа в Авито мы называем ACL Access Control List, список контроля доступа, определяющий, кто и что может делать в админке. В Авито она всегда развивалась параллельно основному продукту, но по остаточному принципу: админка всё-таки для внутренних пользователей, а они не такие притязательные, поэтому внимания ей уделяли меньше. И спустя годы в ней накопилось 1171 ACL-ок. Про 754 мы вообще не знали, кому они принадлежат и какие функции закрывают, — нужно было что-то делать. 

Почему ACL стали нам нужны? 

Сложно запустить ролевую модель

Недавно в компании мы начали перестраивать ролевую модель, и поэтому нужно было понять, какой юнит и кто конкретно в этом юните отвечает за ACL. Правильная ролевая модель помогает вместо точечного назначения доступов выдавать все нужные в привязке к роли сотрудника с первого дня работы (или почти все, с минимумом дополнительных запросов). И никто не бегает в попытках узнать, почему эта функция у него не работает.

Тут еще больше контента

Страдает безопасность 

Без описаний ACL сложно сделать ревью доступов, когда владельцы на регулярной основе решают, кому всё ещё нужен доступ, а у кого лучше отозвать, чтобы снизить риски утечки. 

Примеры ACL

Name

ACL

Расшифровка (была не для всех)

Content / promo / main

content-promo

System / Groups / Edit

system-groups-edit

Редактирование группы в админке прав

Comment / Save

comment-save

Оставить комментарий

Получается, что на входе у нас текстовые данные об особенностях ACL, на выходе — описание и имя владельца, тоже текст. Похоже на задачку для LLM? 

Закинув просто названия ACL в LLM,  мы получим плохой результат — по сути просто попробуем отгадать. А если добавим дополнительный контекст: связанный с ACL код и документацию, то LLM уже сможет все это проанализировать и выдать более релевантный результат.

Архитектура решения

Кроме LLM, нам нужно несколько агентов. Агент помогает генеративной модели получить информацию или совершить действие через сторонние ресурсы. Самый популярный и коммерчески-доступный — Operator от OpenAI. Эта штука может с помощью одного промпта забронировать вам отель для путешествия, найти подходящий товар на маркетплейсе или сделать кучу других полезных дел. 

Я использовал 3 агента: Code Analyzer, Wiki Analyzer и Org Structure Validator. 

  1. Code Analyzer

Делал запросы в Sourcegraph, отправляя туда название ACL и в ответ получал 10 репозиториев. Он анализировал именно код, потому что, как известно, код wins argument: документация может быть некорректной, а разработчики, у которых что-то можно прояснить, могли уволиться. 

Кроме этого, Code Analyzer был интегрирован с реестром микросервисов (мы зовём его Atlas), потому что знания о владельце сервиса — самая надёжная связь между кодовой базой и оргструктурой компании. Если же агент не мог найти ответ в коде, то супервизор отдавал задачу следующему. 

  1. Wiki Analyzer.

Дальше в работу включался Wiki Analyzer и искал всю информацию по ACL во внутренней документации. 

  1. Org Structure Validator.

На финальном этапе супервизор передавал работу валидатору. Он был интегрирован с Avito People — корпоративным порталом, который упрощает HR-процессы. Валидатор проверял, существует ли такой сотрудник внутри или нет. Я добавил проверку, чтобы не выдавались галлюцинации по типу «Петр Петров». А если найденный разработчик, писавший код, уже уволился, и от него осталось только понимание юнита — валидатор искал текущего руководителя юнита. 

Почему многоагентность — лучше? 

Моя архитектура с супервизором — не единственный выбор в этой ситуации. Работу мог выполнить и один агент с четырьмя тулами (Sourcegraph, Confluence, Atlas и сервис Avito People). Но в таком случае он начал бы в них путаться: дробление промпта на подзадачи приводило к лучшему результату. 

К тому же разделение на супервизора и агентов позволяет использовать разные модели, что лучше для безопасности. Для работы первого я брал reasoning-модель о4-mini от OpenAI, а для агентов — наш внутренний LLM Gateway и опенсорсную модель Qwen 2.5 на 32 млрд параметров. Агент выкачивал данные из Sourcegraph и Confluence, там же анализировал, а супервизору отдавал только агрегированную информацию с 5 полями: 

  • justification (не просто так идёт первым — заставляет модель «порассуждать», имитируя технику промптинга Chain-of-Thought), 

  • владелец, 

  • unit владельца, 

  • developer, 

  • confidence (уверенность в правильности определения). 

OpenAI не видел ни кодовую базу, ни статьи в Confluence. 

Жми сюда!

Протокол для LLM, который всё решает

Не обошлось и без модного MCP (Model Context Protocol) — протокола для взаимодействия LLM с тулами. Его основная идея — сделать их взаимозаменяемыми: один раз пишешь MCP-сервер, например, для Sourcegraph или Confluence, — и потом переиспользуешь в других задачах почти без изменений.

MCP поддерживает два режима работы:

  1. HTTP-сервис — поднимается как отдельное приложение, принимает и обрабатывает запросы.

  2. STDIO-режим — запускается как CLI-интерпретатор (в нашем случае — на Python), получает команды через stdin, возвращает ответы через stdout.

Для разметки я написал 4 MCP-сервера: для Sourcegraph, Confluence, Atlas и Avito People. 

Важный момент — отладка MCP. Поскольку MCP работает как отдельный сервис, поднимаемый каждый раз интерпретатором, увидеть ошибку в нём сложно: на  промежуточных шагах эксепшны в другом процессе вы не увидите. 

Поэтому для наблюдаемости  лучше сразу заложить обработку всех возможных сбоев через защитное программирование. Даже если такую ошибку вы видели только в документации — всё равно лучше, если MCP выдаст текстовую строку.

А LLM, как показала практика, достаточно умна, чтобы повторно дёргать инструмент, если, скажем, случился тайм-аут или аргументы были невалидны. У меня такие кейсы случались, и обработка оказалась удобной.

Почему стоит делать сниппетинг? 

Не стоит класть в LLM кодобазу сервиса или даже отдельного файла целиком: это будет элементарно дорого — стоимость запроса зависит от числа входных токенов. И к тому же не очень эффективно — в большом промпте она начнёт больше внимания отдавать начальной и конечной части запроса, упуская из вида середину. 

Поэтому есть смысл сделать сниппетинг. Он помогает вытащить только минимально нужный контекст из всей базы. Например, релевантные запросу методы или классы. А  чтобы не пилить свою реализацию сниппетинга под каждый язык программирования и разметки, я воспользовался tree-sitter.

Эта библиотека строит абстрактное синтаксическое дерево (AST) для кода и помогает удобно вытаскивать нужные фрагменты — функции, методы, классы. Причём под большинство языков уже есть готовые библиотеки. То есть на выходе я получаю структурированное дерево, из которого легко достать нужный сниппет. 

У tree-sitter есть особенность: под каждый язык нужна отдельная грамматика, и деревья получаются разные. Если хочется универсальности, нужно писать кастомный обработчик под каждый язык. Но в Авито их не так много, поэтому парсинг был несложный. 

В тех языках, которые я взял, проверял, что выводятся одинаковые имена, например, function, class. Сложности были только с YAML: ноды там назывались по-другому, и его пришлось обрабатывать отдельно.

В языках без готовых грамматик или в случаях, когда парсер падал, — я реализовал fallback: Sourcegraph возвращает строку, в которой найдена сущность, а дальше мы просто берём контекст ±5 строк вокруг неё. 

Вот весь код, который понадобился, чтобы вытащить информацию: 

def get_context(filename: str, source: str, line: int, context_size: int = 5) -> str:
    ext = filename.split('.')[-1]
    lang_map = {
        'go': tsgo.language(),
        'js': tsjavascript.language(),
        'php': tsphp.language.php(),
        'py': tspython.language(),
        'ts': tstypescript.language_typescript(),
        'yaml': tsyaml.language(),
    }
    if ext not in lang_map:
        return get_region(source, line, context_size)
    
    lang = Language(lang_map[ext])
    parser = Parser(lang)
    
    tree = parser.parse(source.encode('utf8'))
    query = lang.query('(_) @node')
    matches = [
        m['node'][0]
        for _, m in query.matches(tree.root_node)
        if m['node'][0].start_point[0] == line
    ]
    if not matches:
        return get_region(source, line, context_size)
    
    # Find the parent class, method, function, variable declaration or export statement
    parent = matches[0].parent
    while (
        parent is not None
        and parent.type not in [
            tree.root_node.type,
            'class_declaration',
            'method_declaration',
            'function_declaration',
            'var_declaration',
            'export_statement',
        ]
    ):
        parent = parent.parent
    
    if parent is not None and parent.text is not None:
        context = parent.text.decode('utf8')
        if len(context) > 100 and len(context.split('\n')) < context_size * 2:
            return context
    
    return get_region(source, line, context_size)

Напоследок — про трейсинг. Не забывайте про инструменты наблюдаемости, особенно если вы строите систему с вызовами внешних тулов. Я использовал опенсорсный MLflow — он помогает отслеживать, какие запросы LLM отправляет агентам, какие ответы получает и где происходят ошибки. Это сильно упростило отладку. Ну а теперь — к результатам. 

Что получилось?

Качество генерации комментариев было на уровне ~65% (LangChain labeled_score_string). С учётом скорости создания — результат хороший. Работу инструмента я проверял, посмотрев на ответы LLM по тем 78 ACL, которые были максимально подробно размечены вручную. 

Но самое интересное — это точность разметки владельцев:

  • 17% — точное попадание LLM во владельца ACL в юните;

  • 42% — правильный юнит;

  • 18% — правильный кластер;

Итог: в 77% случаев разметка попадает в нужный кластер. По сути только в четверти случаев останется доразметить данные вручную. 

Кликни здесь и узнаешь

Какие планы на будущее? 

Есть гипотеза, что таким же методом при помощи кодовой базы можно различать в контрактах сервисов персональные данные. Её только предстоит проверить. Но другую задачу уже получилось решить. 

В Авито есть 1500 правил iptables между офисом и дата-центром — некоторые из них небезопасные. При помощи LLM-ки и одного тула удалось найти самые рисковые, и теперь мы можем их скорректировать.

Что думаете о таком использовании LLM? Делитесь в комментариях!

А если хотите вместе с нами помогать людям и бизнесу через технологии — присоединяйтесь к командам. Свежие вакансии есть на нашем карьерном сайте.

Теги:
Хабы:
+4
Комментарии1

Публикации

Информация

Сайт
avito.tech
Дата регистрации
Дата основания
2007
Численность
5 001–10 000 человек
Местоположение
Россия
Представитель
vvroschin