Как вы делаете long context для code generation (модель должна видеть весь репозиторий)?

Краткий тезис

Подача всего репозитория целиком в LLM|контекст LLM — неэффективный и дорогой подход. Вместо этого строится граф зависимостей кода (через AST или динамический анализ), и по запросу пользователя (например, «напиши функцию для парсинга CSV») извлекается релевантный подграф: связанные функции, классы, импорты. Этот подграф, а не весь репозиторий, загружается в контекст. Используются retrieval-методы (BM25, эмбеддинги кода) и инструменты вроде RepoCoder, CodeGraph, Cursor.

1. Проблема: «Весь репозиторий» — это неразумно

Long context (длинный контекст) — способность LLM обрабатывать большой объём текста на входе. Для code generation это означает, что модель должна «видеть» код, от которого зависит новая функция: импорты, типы, API других модулей, конфигурацию.

Однако загружать весь репозиторий (сотни файлов, миллионы токенов) в контекст — плохая идея:

  • Стоимость: токены контекста дороги (пропорционально длине).
  • Шум: модель отвлекается на нерелевантный код, падает качество генерации (эффект lost in the middle).
  • Латентность: время первого токена растёт линейно с длиной контекста.
  • Ограничения: даже модели с 128k–200k контекста не вмещают большие репозитории.

Решение: не весь репозиторий, а релевантный подграф зависимостей.

2. Репозиторий как граф: AST и граф вызовов

Код — это не плоский текст, а структура. Чтобы понять, какие части кода нужны для генерации, строят:

  • AST (Abstract Syntax Tree) — дерево разбора исходного кода. Из него извлекают определения функций, классов, переменных, импорты.
  • Граф вызовов (call graph) — направленный граф, где узлы — функции/методы, а рёбра — вызовы. Например, функция parse_csv() вызывает split(), validate_row().
  • Граф зависимостей (dependency graph) — расширение графа вызовов, включающее импорты, наследование, использование типов.

Пример построения графа вызовов (упрощённо на Python с ast):

import ast

class CallGraphVisitor(ast.NodeVisitor):
    def __init__(self):
        self.graph = {}  # функция -> список вызываемых функций
        self.current_func = None

    def visit_FunctionDef(self, node):
        self.current_func = node.name
        self.graph[self.current_func] = []
        self.generic_visit(node)

    def visit_Call(self, node):
        if isinstance(node.func, ast.Name):
            self.graph[self.current_func].append(node.func.id)
        self.generic_visit(node)

# Пример использования
code = """
def parse_csv(line):
    parts = split(line)
    return validate(parts)

def split(s):
    return s.split(',')

def validate(parts):
    return [p.strip() for p in parts]
"""
tree = ast.parse(code)
visitor = CallGraphVisitor()
visitor.visit(tree)
print(visitor.graph)
# {'parse_csv': ['split', 'validate'], 'split': [], 'validate': []}

Такой граф позволяет понять: для генерации parse_csv нужны split и validate.

3. Retrieval для кода: как найти релевантные узлы графа

Когда приходит запрос (например, «добавь функцию для чтения CSV из файла»), нужно найти в графе релевантные узлы. Используются два подхода:

3.1 Лексический поиск (BM25)

  • Индексируются все функции/классы по их сигнатурам и документации.
  • Запрос сопоставляется с индексами через BM25 (TF-IDF с насыщением частоты).
  • Быстро, не требует эмбеддингов, хорошо работает для точных совпадений имён.

3.2 Семантический поиск (эмбеддинги кода)

  • Каждый узел графа (функция, класс) превращается в вектор через CodeBERT, GraphCodeBERT или UniXcoder.
  • Запрос тоже эмбеддится, ищется top-k ближайших соседей в векторной БД (FAISS, Annoy).
  • Лучше понимает семантику: «прочитать CSV» найдёт read_csv, load_data, parse_csv.

Гибридный подход: BM25 + эмбеддинги с реранжировкой (например, Cohere Rerank).

4. Стратегии загрузки контекста: от узла к подграфу

После того как найден релевантный узел (например, функция parse_csv), нужно загрузить в контекст не только её, но и всё, от чего она зависит. Стратегии:

4.1 Полный файл

  • Загружается весь файл, где определена релевантная функция.
  • Плюс: простота, контекст включает соседние функции.
  • Минус: файл может быть большим (1000+ строк), много шума.

4.2 Зависимые функции (графовый подход)

  • Из графа вызовов берётся транзитивное замыкание зависимостей: функция A → B → C.
  • В контекст попадают только нужные функции, даже если они в разных файлах.
  • Пример: для parse_csv загружаются split и validate (из других файлов), но не save_to_db.

4.3 Иерархический контекст

  • Сначала загружается сигнатура и документация зависимых функций.
  • Если модель запрашивает больше (через tool calling), подгружается полное тело.
  • Экономит токены, но требует нескольких раундов.

Сравнение стратегий

СтратегияТокенов на запросРиск упустить зависимостьСложность реализации
Полный файлВысокийНизкийНизкая
Зависимые функцииСреднийСредний (если граф неполон)Средняя
ИерархическийНизкийВысокий (если модель не запросит)Высокая

5. Инструменты и фреймворки

  • RepoCoder (Microsoft, 2023): строит граф репозитория, при генерации нового кода извлекает релевантные фрагменты через BM25 + эмбеддинги, добавляет их в промпт. Показал улучшение CodeBLEU на 10–15%.
  • CodeGraph (Sourcegraph): анализирует код, строит граф зависимостей, интегрируется с IDE и LLM.
  • Cursor: IDE с AI-ассистентом, использует @Codebase — автоматически находит релевантные файлы через эмбеддинги и граф.
  • GitHub Copilot: использует контекст открытого файла + соседние табы, но не весь репозиторий. Для полного контекста — Copilot Chat с ручным указанием файлов.
  • Aider: инструмент для code generation с поддержкой map of repo — сжатое представление репозитория (сигнатуры функций, классы) для LLM.

6. Обработка длинного контекста: техники для LLM

Даже после отбора релевантного подграфа контекст может быть большим (10–30 файлов, 50k–100k токенов). Как с этим работают LLM:

  • RoPE (Rotary Position Embedding) — позволяет моделям (Llama 3, Qwen 2.5) обрабатывать до 128k токенов без потери качества.
  • Sliding window attentionMistral, Gemma используют окно внимания (4k–32k токенов), но с rolling cache для длинных контекстов.
  • Context distillation — сжатие контекста: извлечение ключевых строк (сигнатуры, комментарии) вместо полного кода.

Пример сжатия контекста для функции:

# Исходная функция (50 строк)
def parse_csv(filepath, delimiter=','):
    with open(filepath, 'r') as f:
        lines = f.readlines()
    header = lines[0].strip().split(delimiter)
    data = []
    for line in lines[1:]:
        row = line.strip().split(delimiter)
        data.append(dict(zip(header, row)))
    return data

# Сжатый контекст (2 строки)
# parse_csv(filepath, delimiter=',') -> List[Dict]
# Зависимости: open, readlines, split, strip

7. Оценка качества long context для code generation

Метрики для оценки, насколько хорошо модель использует контекст репозитория:

  • CodeBLEUBLEU для кода с учётом синтаксиса и семантики.
  • Compilation success rate — доля сгенерированного кода, который компилируется без ошибок.
  • Functional correctness — прохождение юнит-тестов (HumanEval, MBPP).
  • Context utilization — метрика, показывающая, сколько релевантных зависимостей из графа было использовано в генерации.

Пример оценки

ПодходCodeBLEUCompilation rateFunctional correctness
Без контекста45.262%38%
Полный файл52.171%49%
Граф зависимостей56.878%58%
Граф + иерархия58.380%61%

8. Компромиссы и практические советы

  • Скорость vs полнота: графовый retrieval быстрее (меньше токенов), но может пропустить неявные зависимости (глобальные переменные, monkey-patching).
  • Стоимость токенов: для больших репозиториев используйте иерархический контекст или context caching (API Gemini, Claude поддерживают кэширование контекста).
  • Динамические языки: Python, JavaScript — граф вызовов строится неточно из-за динамической типизации. Дополняйте статический анализ runtime tracing (запуск тестов).
  • Монорепозитории: если репозиторий огромен, используйте multi-stage retrieval: сначала найти модуль, потом функцию внутри модуля.

Пет-проект для закрепления

Задача: Создать инструмент, который по текстовому описанию генерирует функцию на Python, автоматически подгружая зависимости из репозитория.

Инструменты:

  • Python + ast для построения графа вызовов.
  • sentence-transformers (all-MiniLM-L6-v2) для эмбеддингов кода.
  • FAISS для векторного поиска.
  • ollama или API OpenAI для генерации.

Шаги:

  1. Напишите скрипт, который обходит все .py файлы в репозитории, строит AST и извлекает функции/классы с их сигнатурами и документацией.
  2. Постройте граф вызовов: для каждой функции запишите, какие функции она вызывает.
  3. Для каждой функции создайте эмбеддинг её сигнатуры + docstring.
  4. По запросу пользователя (например, «напиши функцию для чтения CSV и подсчёта строк») найдите top-3 релевантные функции через FAISS.
  5. Из графа вызовов возьмите транзитивное замыкание зависимостей для этих функций.
  6. Сформируйте промпт: «Вот релевантный код из репозитория: [зависимые функции]. Напиши новую функцию, которая [запрос]».
  7. Отправьте в LLM, получите код, проверьте, что он использует зависимости.

Ожидаемый результат: Рабочий CLI-инструмент, который для запроса «функция для валидации email» находит в репозитории validate_email, regex_match и генерирует новую функцию, вызывающую их.

Связь с другими вопросами

ВопросТема
5Как вы оцениваете качество retrieval'а в RAG-системе?
7Как вы уменьшаете latency RAG-системы?
9Как вы обновляете документы в существующей RAG-системе?
10Что такое Self-RAG и когда его использовать?
15Как вы обрабатываете запросы, на которые нет ответа в документах?
20Как бы вы спроектировали RAG-систему для 10 000 документов с разной структурой?

Навигация